Merge package:test_process into the test monorepo
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f189cdd --- /dev/null +++ b/.gitattributes
@@ -0,0 +1,2 @@ +.github/workflows/dart.yml linguist-generated=true +tool/ci.sh linguist-generated=true
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..a53d62f --- /dev/null +++ b/.github/CODEOWNERS
@@ -0,0 +1 @@ +/pkgs/ @dart-lang/core-package-admins
diff --git a/.github/ISSUE_TEMPLATE/01_test_runner_bug.md b/.github/ISSUE_TEMPLATE/01_test_runner_bug.md new file mode 100644 index 0000000..e49d277 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_test_runner_bug.md
@@ -0,0 +1,14 @@ +--- +name: I found a bug in the test runner +about: >- + Report a bug in running tests, integration with a specific platform, or + any behavior of 'package:test'. +title: '' +labels: bug +--- +Describe the bug in detail. +Include the specific command you ran and any relevant details about the +environment, including operating system, Dart SDK version, and the version of +`package:test` you are using. +Whenever possible, include a full reproduction example that we can run to +observe the problem.
diff --git a/.github/ISSUE_TEMPLATE/02_test_runner_feature.md b/.github/ISSUE_TEMPLATE/02_test_runner_feature.md new file mode 100644 index 0000000..56170ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_test_runner_feature.md
@@ -0,0 +1,10 @@ +--- +name: I'd like a new feature in the test runner +about: >- + Propose a feature for 'package:test' that would make testing easier or more + powerful. +title: '' +labels: enhancement +--- +Describe the feature and include specific description of the use case, or use +cases, you have in mind.
diff --git a/.github/ISSUE_TEMPLATE/03_checks_feedback.md b/.github/ISSUE_TEMPLATE/03_checks_feedback.md new file mode 100644 index 0000000..5eb0039 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03_checks_feedback.md
@@ -0,0 +1,8 @@ +--- +name: I have feedback for the package:checks preview +about: Suggest a change, new feature, or bug, for 'package:checks'. +title: '' +labels: package:checks +--- +Add any feedback about `package:checks`. +Include a description of a specific use case for any feature requests.
diff --git a/.github/ISSUE_TEMPLATE/fake_async.md b/.github/ISSUE_TEMPLATE/fake_async.md new file mode 100644 index 0000000..c26edd2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/fake_async.md
@@ -0,0 +1,5 @@ +--- +name: "package:fake_async" +about: "Create a bug or file a feature request against package:fake_async." +labels: "package:fake_async" +--- \ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/matcher.md b/.github/ISSUE_TEMPLATE/matcher.md new file mode 100644 index 0000000..f41464d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/matcher.md
@@ -0,0 +1,5 @@ +--- +name: "package:matcher" +about: "Create a bug or file a feature request against package:matcher." +labels: "package:matcher" +--- \ No newline at end of file
diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..8aa9b97 --- /dev/null +++ b/.github/dependabot.yml
@@ -0,0 +1,33 @@ +# Dependabot configuration file. +# See https://docs.github.com/en/code-security/dependabot/dependabot-version-updates + +version: 2 + +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + labels: + - autosubmit + groups: + github-actions: + patterns: + - "*" + + # Check daily for major version dependency updates for packages which depend + # on package:analyzer. + - package-ecosystem: "pub" + directory: "pkgs/test" + schedule: + interval: "daily" + versioning-strategy: increase-if-necessary + reviewers: + - "natebosch" + - package-ecosystem: "pub" + directory: "pkgs/test_core" + schedule: + interval: "daily" + versioning-strategy: increase-if-necessary + reviewers: + - "natebosch"
diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml new file mode 100644 index 0000000..9a3cd79 --- /dev/null +++ b/.github/workflows/dart.yml
@@ -0,0 +1,1903 @@ +# Created with package:mono_repo v6.6.2 +name: Dart CI +on: + push: + branches: + - main + - master + pull_request: + schedule: + - cron: "0 0 * * 0" +defaults: + run: + shell: bash +env: + PUB_ENVIRONMENT: bot.github +permissions: read-all + +jobs: + job_001: + name: mono_repo self validate + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: stable + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - name: mono_repo self validate + run: dart pub global activate mono_repo 6.6.2 + - name: mono_repo self validate + run: dart pub global run mono_repo generate --validate + job_002: + name: "analyze_and_format; linux; Dart 3.3.0; PKG: pkgs/fake_async; `dart analyze`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/fake_async;commands:analyze_1" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/fake_async + os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.3.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_fake_async_pub_upgrade + name: pkgs/fake_async; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/fake_async + - name: pkgs/fake_async; dart analyze + run: dart analyze + if: "always() && steps.pkgs_fake_async_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/fake_async + job_003: + name: "analyze_and_format; linux; Dart 3.4.0; PKG: pkgs/matcher; `dart analyze`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0;packages:pkgs/matcher;commands:analyze_1" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0;packages:pkgs/matcher + os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.4.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_matcher_pub_upgrade + name: pkgs/matcher; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/matcher + - name: pkgs/matcher; dart analyze + run: dart analyze + if: "always() && steps.pkgs_matcher_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/matcher + job_004: + name: "analyze_and_format; linux; Dart 3.5.0; PKGS: integration_tests/regression, integration_tests/wasm; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + 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" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/regression-integration_tests/wasm + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: integration_tests_regression_pub_upgrade + name: integration_tests/regression; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/regression + - name: "integration_tests/regression; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." + if: "always() && steps.integration_tests_regression_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/regression + - name: "integration_tests/regression; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.integration_tests_regression_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/regression + - id: integration_tests_wasm_pub_upgrade + name: integration_tests/wasm; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/wasm + - name: "integration_tests/wasm; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." + if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/wasm + - name: "integration_tests/wasm; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/wasm + job_005: + name: "analyze_and_format; linux; Dart 3.5.0; PKGS: pkgs/checks, pkgs/test_core; `dart analyze`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/checks-pkgs/test_core;commands:analyze_1" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/checks-pkgs/test_core + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_checks_pub_upgrade + name: pkgs/checks; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/checks + - name: pkgs/checks; dart analyze + run: dart analyze + if: "always() && steps.pkgs_checks_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/checks + - id: pkgs_test_core_pub_upgrade + name: pkgs/test_core; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test_core + - name: pkgs/test_core; dart analyze + run: dart analyze + if: "always() && steps.pkgs_test_core_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test_core + job_006: + name: "analyze_and_format; linux; Dart dev; PKGS: 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; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + 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" + restore-keys: | + 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 + os:ubuntu-latest;pub-cache-hosted;sdk:dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: integration_tests_regression_pub_upgrade + name: integration_tests/regression; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/regression + - name: "integration_tests/regression; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." + if: "always() && steps.integration_tests_regression_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/regression + - name: "integration_tests/regression; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.integration_tests_regression_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/regression + - id: integration_tests_spawn_hybrid_pub_upgrade + name: integration_tests/spawn_hybrid; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/spawn_hybrid + - name: "integration_tests/spawn_hybrid; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." + if: "always() && steps.integration_tests_spawn_hybrid_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/spawn_hybrid + - name: "integration_tests/spawn_hybrid; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.integration_tests_spawn_hybrid_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/spawn_hybrid + - id: integration_tests_wasm_pub_upgrade + name: integration_tests/wasm; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/wasm + - name: "integration_tests/wasm; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." + if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/wasm + - name: "integration_tests/wasm; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/wasm + - id: pkgs_checks_pub_upgrade + name: pkgs/checks; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/checks + - name: "pkgs/checks; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." + if: "always() && steps.pkgs_checks_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/checks + - name: "pkgs/checks; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.pkgs_checks_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/checks + - id: pkgs_fake_async_pub_upgrade + name: pkgs/fake_async; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/fake_async + - name: "pkgs/fake_async; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." + if: "always() && steps.pkgs_fake_async_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/fake_async + - name: "pkgs/fake_async; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.pkgs_fake_async_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/fake_async + - id: pkgs_matcher_pub_upgrade + name: pkgs/matcher; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/matcher + - name: "pkgs/matcher; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." + if: "always() && steps.pkgs_matcher_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/matcher + - name: "pkgs/matcher; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.pkgs_matcher_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/matcher + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + - id: pkgs_test_api_pub_upgrade + name: pkgs/test_api; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test_api + - name: "pkgs/test_api; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." + if: "always() && steps.pkgs_test_api_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test_api + - name: "pkgs/test_api; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.pkgs_test_api_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test_api + - id: pkgs_test_core_pub_upgrade + name: pkgs/test_core; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test_core + - name: "pkgs/test_core; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." + if: "always() && steps.pkgs_test_core_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test_core + - name: "pkgs/test_core; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.pkgs_test_core_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test_core + job_007: + name: "analyze_and_format; windows; Dart 3.5.0; PKG: integration_tests/wasm; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos`" + runs-on: windows-latest + steps: + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: integration_tests_wasm_pub_upgrade + name: integration_tests/wasm; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/wasm + - name: "integration_tests/wasm; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." + if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/wasm + - name: "integration_tests/wasm; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/wasm + job_008: + name: "analyze_and_format; windows; Dart dev; PKG: integration_tests/wasm; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos`" + runs-on: windows-latest + steps: + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: integration_tests_wasm_pub_upgrade + name: integration_tests/wasm; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/wasm + - name: "integration_tests/wasm; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." + if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/wasm + - name: "integration_tests/wasm; dart analyze --fatal-infos" + run: dart analyze --fatal-infos + if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/wasm + job_009: + name: "unit_test; linux; Dart 3.3.0; PKG: pkgs/fake_async; `dart test`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/fake_async;commands:command_00" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0;packages:pkgs/fake_async + os:ubuntu-latest;pub-cache-hosted;sdk:3.3.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.3.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_fake_async_pub_upgrade + name: pkgs/fake_async; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/fake_async + - name: pkgs/fake_async; dart test + run: dart test + if: "always() && steps.pkgs_fake_async_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/fake_async + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_010: + name: "unit_test; linux; Dart 3.4.0; PKG: pkgs/matcher; `dart test`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0;packages:pkgs/matcher;commands:command_00" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0;packages:pkgs/matcher + os:ubuntu-latest;pub-cache-hosted;sdk:3.4.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.4.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_matcher_pub_upgrade + name: pkgs/matcher; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/matcher + - name: pkgs/matcher; dart test + run: dart test + if: "always() && steps.pkgs_matcher_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/matcher + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_011: + name: "unit_test; linux; Dart 3.5.0; PKG: integration_tests/regression; `dart test`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/regression;commands:command_00" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/regression + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: integration_tests_regression_pub_upgrade + name: integration_tests/regression; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/regression + - name: integration_tests/regression; dart test + run: dart test + if: "always() && steps.integration_tests_regression_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/regression + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_012: + name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/checks; `dart test`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/checks;commands:command_00" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/checks + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_checks_pub_upgrade + name: pkgs/checks; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/checks + - name: pkgs/checks; dart test + run: dart test + if: "always() && steps.pkgs_checks_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/checks + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_013: + name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test_core; `dart test`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test_core;commands:command_00" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test_core + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_core_pub_upgrade + name: pkgs/test_core; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test_core + - name: pkgs/test_core; dart test + run: dart test + if: "always() && steps.pkgs_test_core_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test_core + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_014: + name: "unit_test; linux; Dart 3.5.0; PKG: integration_tests/spawn_hybrid; `dart test -p chrome,vm,node`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/spawn_hybrid;commands:test_1" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/spawn_hybrid + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: integration_tests_spawn_hybrid_pub_upgrade + name: integration_tests/spawn_hybrid; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/spawn_hybrid + - name: "integration_tests/spawn_hybrid; dart test -p chrome,vm,node" + run: "dart test -p chrome,vm,node" + if: "always() && steps.integration_tests_spawn_hybrid_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/spawn_hybrid + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_015: + name: "unit_test; linux; Dart 3.5.0; PKG: integration_tests/wasm; `dart test --timeout=60s`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/wasm;commands:test_2" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:integration_tests/wasm + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: integration_tests_wasm_pub_upgrade + name: integration_tests/wasm; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/wasm + - name: "integration_tests/wasm; dart test --timeout=60s" + run: "dart test --timeout=60s" + if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/wasm + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_016: + name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 0`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_01" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 0" + run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 0" + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_017: + name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 1`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_02" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 1" + run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 1" + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_018: + name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 2`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_03" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 2" + run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 2" + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_019: + name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 3`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_04" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 3" + run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 3" + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_020: + name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 4`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_05" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 4" + run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 4" + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_021: + name: "unit_test; linux; Dart 3.5.0; PKG: pkgs/test_api; `dart test --preset travis -x browser`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test_api;commands:command_11" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test_api + os:ubuntu-latest;pub-cache-hosted;sdk:3.5.0 + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_api_pub_upgrade + name: pkgs/test_api; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test_api + - name: "pkgs/test_api; dart test --preset travis -x browser" + run: dart test --preset travis -x browser + if: "always() && steps.pkgs_test_api_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test_api + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_022: + name: "unit_test; linux; Dart dev; PKG: integration_tests/regression; `dart test`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/regression;commands:command_00" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/regression + os:ubuntu-latest;pub-cache-hosted;sdk:dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: integration_tests_regression_pub_upgrade + name: integration_tests/regression; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/regression + - name: integration_tests/regression; dart test + run: dart test + if: "always() && steps.integration_tests_regression_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/regression + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_023: + name: "unit_test; linux; Dart dev; PKG: pkgs/checks; `dart test`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/checks;commands:command_00" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/checks + os:ubuntu-latest;pub-cache-hosted;sdk:dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_checks_pub_upgrade + name: pkgs/checks; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/checks + - name: pkgs/checks; dart test + run: dart test + if: "always() && steps.pkgs_checks_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/checks + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_024: + name: "unit_test; linux; Dart dev; PKG: pkgs/fake_async; `dart test`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/fake_async;commands:command_00" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/fake_async + os:ubuntu-latest;pub-cache-hosted;sdk:dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_fake_async_pub_upgrade + name: pkgs/fake_async; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/fake_async + - name: pkgs/fake_async; dart test + run: dart test + if: "always() && steps.pkgs_fake_async_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/fake_async + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_025: + name: "unit_test; linux; Dart dev; PKG: pkgs/matcher; `dart test`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/matcher;commands:command_00" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/matcher + os:ubuntu-latest;pub-cache-hosted;sdk:dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_matcher_pub_upgrade + name: pkgs/matcher; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/matcher + - name: pkgs/matcher; dart test + run: dart test + if: "always() && steps.pkgs_matcher_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/matcher + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_026: + name: "unit_test; linux; Dart dev; PKG: pkgs/test_core; `dart test`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test_core;commands:command_00" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test_core + os:ubuntu-latest;pub-cache-hosted;sdk:dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_core_pub_upgrade + name: pkgs/test_core; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test_core + - name: pkgs/test_core; dart test + run: dart test + if: "always() && steps.pkgs_test_core_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test_core + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_027: + name: "unit_test; linux; Dart dev; PKG: integration_tests/spawn_hybrid; `dart test -p chrome,vm,node`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/spawn_hybrid;commands:test_1" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/spawn_hybrid + os:ubuntu-latest;pub-cache-hosted;sdk:dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: integration_tests_spawn_hybrid_pub_upgrade + name: integration_tests/spawn_hybrid; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/spawn_hybrid + - name: "integration_tests/spawn_hybrid; dart test -p chrome,vm,node" + run: "dart test -p chrome,vm,node" + if: "always() && steps.integration_tests_spawn_hybrid_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/spawn_hybrid + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_028: + name: "unit_test; linux; Dart dev; PKG: integration_tests/wasm; `dart test --timeout=60s`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/wasm;commands:test_2" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:integration_tests/wasm + os:ubuntu-latest;pub-cache-hosted;sdk:dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: integration_tests_wasm_pub_upgrade + name: integration_tests/wasm; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/wasm + - name: "integration_tests/wasm; dart test --timeout=60s" + run: "dart test --timeout=60s" + if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/wasm + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_029: + name: "unit_test; linux; Dart dev; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 0`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test;commands:command_01" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test + os:ubuntu-latest;pub-cache-hosted;sdk:dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 0" + run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 0" + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_030: + name: "unit_test; linux; Dart dev; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 1`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test;commands:command_02" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test + os:ubuntu-latest;pub-cache-hosted;sdk:dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 1" + run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 1" + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_031: + name: "unit_test; linux; Dart dev; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 2`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test;commands:command_03" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test + os:ubuntu-latest;pub-cache-hosted;sdk:dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 2" + run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 2" + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_032: + name: "unit_test; linux; Dart dev; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 3`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test;commands:command_04" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test + os:ubuntu-latest;pub-cache-hosted;sdk:dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 3" + run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 3" + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_033: + name: "unit_test; linux; Dart dev; PKG: pkgs/test; `xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 4`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test;commands:command_05" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test + os:ubuntu-latest;pub-cache-hosted;sdk:dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 4" + run: "xvfb-run -s \"-screen 0 1024x768x24\" dart test --preset travis --total-shards 5 --shard-index 4" + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_034: + name: "unit_test; linux; Dart dev; PKG: pkgs/test_api; `dart test --preset travis -x browser`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test_api;commands:command_11" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:dev;packages:pkgs/test_api + os:ubuntu-latest;pub-cache-hosted;sdk:dev + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_api_pub_upgrade + name: pkgs/test_api; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test_api + - name: "pkgs/test_api; dart test --preset travis -x browser" + run: dart test --preset travis -x browser + if: "always() && steps.pkgs_test_api_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test_api + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_035: + name: "unit_test; osx; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 0`" + runs-on: macos-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_06" + restore-keys: | + os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test + os:macos-latest;pub-cache-hosted;sdk:3.5.0 + os:macos-latest;pub-cache-hosted + os:macos-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 0" + run: dart test --preset travis --total-shards 5 --shard-index 0 + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_036: + name: "unit_test; osx; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 1`" + runs-on: macos-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_07" + restore-keys: | + os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test + os:macos-latest;pub-cache-hosted;sdk:3.5.0 + os:macos-latest;pub-cache-hosted + os:macos-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 1" + run: dart test --preset travis --total-shards 5 --shard-index 1 + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_037: + name: "unit_test; osx; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 2`" + runs-on: macos-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_08" + restore-keys: | + os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test + os:macos-latest;pub-cache-hosted;sdk:3.5.0 + os:macos-latest;pub-cache-hosted + os:macos-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 2" + run: dart test --preset travis --total-shards 5 --shard-index 2 + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_038: + name: "unit_test; osx; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 3`" + runs-on: macos-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_09" + restore-keys: | + os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test + os:macos-latest;pub-cache-hosted;sdk:3.5.0 + os:macos-latest;pub-cache-hosted + os:macos-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 3" + run: dart test --preset travis --total-shards 5 --shard-index 3 + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_039: + name: "unit_test; osx; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 4`" + runs-on: macos-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@6849a6489940f00c2f30c0fb92c6274307ccb58a + with: + path: "~/.pub-cache/hosted" + key: "os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test;commands:command_10" + restore-keys: | + os:macos-latest;pub-cache-hosted;sdk:3.5.0;packages:pkgs/test + os:macos-latest;pub-cache-hosted;sdk:3.5.0 + os:macos-latest;pub-cache-hosted + os:macos-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 4" + run: dart test --preset travis --total-shards 5 --shard-index 4 + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_040: + name: "unit_test; windows; Dart 3.5.0; PKG: integration_tests/spawn_hybrid; `dart test -p chrome,vm,node`" + runs-on: windows-latest + steps: + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: integration_tests_spawn_hybrid_pub_upgrade + name: integration_tests/spawn_hybrid; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/spawn_hybrid + - name: "integration_tests/spawn_hybrid; dart test -p chrome,vm,node" + run: "dart test -p chrome,vm,node" + if: "always() && steps.integration_tests_spawn_hybrid_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/spawn_hybrid + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_041: + name: "unit_test; windows; Dart 3.5.0; PKG: integration_tests/wasm; `dart test --timeout=60s`" + runs-on: windows-latest + steps: + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: integration_tests_wasm_pub_upgrade + name: integration_tests/wasm; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/wasm + - name: "integration_tests/wasm; dart test --timeout=60s" + run: "dart test --timeout=60s" + if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/wasm + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_042: + name: "unit_test; windows; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 0`" + runs-on: windows-latest + steps: + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 0" + run: dart test --preset travis --total-shards 5 --shard-index 0 + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_043: + name: "unit_test; windows; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 1`" + runs-on: windows-latest + steps: + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 1" + run: dart test --preset travis --total-shards 5 --shard-index 1 + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_044: + name: "unit_test; windows; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 2`" + runs-on: windows-latest + steps: + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 2" + run: dart test --preset travis --total-shards 5 --shard-index 2 + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_045: + name: "unit_test; windows; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 3`" + runs-on: windows-latest + steps: + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 3" + run: dart test --preset travis --total-shards 5 --shard-index 3 + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_046: + name: "unit_test; windows; Dart 3.5.0; PKG: pkgs/test; `dart test --preset travis --total-shards 5 --shard-index 4`" + runs-on: windows-latest + steps: + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: "3.5.0" + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: pkgs_test_pub_upgrade + name: pkgs/test; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: pkgs/test + - name: "pkgs/test; dart test --preset travis --total-shards 5 --shard-index 4" + run: dart test --preset travis --total-shards 5 --shard-index 4 + if: "always() && steps.pkgs_test_pub_upgrade.conclusion == 'success'" + working-directory: pkgs/test + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_047: + name: "unit_test; windows; Dart dev; PKG: integration_tests/spawn_hybrid; `dart test -p chrome,vm,node`" + runs-on: windows-latest + steps: + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: integration_tests_spawn_hybrid_pub_upgrade + name: integration_tests/spawn_hybrid; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/spawn_hybrid + - name: "integration_tests/spawn_hybrid; dart test -p chrome,vm,node" + run: "dart test -p chrome,vm,node" + if: "always() && steps.integration_tests_spawn_hybrid_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/spawn_hybrid + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_048: + name: "unit_test; windows; Dart dev; PKG: integration_tests/wasm; `dart test --timeout=60s`" + runs-on: windows-latest + steps: + - name: Setup Dart SDK + uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: dev + - id: checkout + name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - id: integration_tests_wasm_pub_upgrade + name: integration_tests/wasm; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: integration_tests/wasm + - name: "integration_tests/wasm; dart test --timeout=60s" + run: "dart test --timeout=60s" + if: "always() && steps.integration_tests_wasm_pub_upgrade.conclusion == 'success'" + working-directory: integration_tests/wasm + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + job_049: + name: Notify failure + runs-on: ubuntu-latest + if: "(github.event_name == 'push' || github.event_name == 'schedule') && failure()" + steps: + - run: | + curl -H "Content-Type: application/json" -X POST -d \ + "{'text':'Build failed! ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}'}" \ + "${CHAT_WEBHOOK_URL}" + env: + CHAT_WEBHOOK_URL: "${{ secrets.BUILD_AND_TEST_TEAM_CHAT_WEBHOOK_URL }}" + needs: + - job_001 + - job_002 + - job_003 + - job_004 + - job_005 + - job_006 + - job_007 + - job_008 + - job_009 + - job_010 + - job_011 + - job_012 + - job_013 + - job_014 + - job_015 + - job_016 + - job_017 + - job_018 + - job_019 + - job_020 + - job_021 + - job_022 + - job_023 + - job_024 + - job_025 + - job_026 + - job_027 + - job_028 + - job_029 + - job_030 + - job_031 + - job_032 + - job_033 + - job_034 + - job_035 + - job_036 + - job_037 + - job_038 + - job_039 + - job_040 + - job_041 + - job_042 + - job_043 + - job_044 + - job_045 + - job_046 + - job_047 + - job_048
diff --git a/.github/workflows/health.yaml b/.github/workflows/health.yaml new file mode 100644 index 0000000..1656c16 --- /dev/null +++ b/.github/workflows/health.yaml
@@ -0,0 +1,12 @@ +name: Health +on: + pull_request: + branches: [ main, master ] + types: [opened, synchronize, reopened, labeled, unlabeled] +jobs: + health: + uses: dart-lang/ecosystem/.github/workflows/health.yaml@main + with: + checks: "version,changelog,do-not-submit" + permissions: + pull-requests: write
diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml new file mode 100644 index 0000000..1a6f2ec --- /dev/null +++ b/.github/workflows/no-response.yml
@@ -0,0 +1,35 @@ +# A workflow to close issues where the author hasn't responded to a request for +# more information; see https://github.com/actions/stale. + +name: No Response + +# Run as a daily cron. +on: + schedule: + # Every day at 8am + - cron: '0 8 * * *' + +# All permissions not specified are set to 'none'. +permissions: + issues: write + pull-requests: write + +jobs: + no-response: + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'dart-lang' }} + steps: + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e + with: + days-before-stale: -1 + days-before-close: 14 + stale-issue-label: "needs-info" + close-issue-message: > + Without additional information we're not able to resolve this issue. + Feel free to add more info or respond to any questions above and we + can reopen the case. Thanks for your contribution! + stale-pr-label: "needs-info" + close-pr-message: > + Without additional information we're not able to resolve this PR. + Feel free to add more info or respond to any questions above. + Thanks for your contribution!
diff --git a/.github/workflows/post_summaries.yaml b/.github/workflows/post_summaries.yaml new file mode 100644 index 0000000..9be7d5d --- /dev/null +++ b/.github/workflows/post_summaries.yaml
@@ -0,0 +1,18 @@ +name: Comment on the pull request + +on: + # Trigger this workflow after the Health workflow completes. This workflow + # will have permissions to do things like create comments on the PR, even if + # the original workflow couldn't. + workflow_run: + workflows: + - Health + - Publish + types: + - completed + +jobs: + upload: + uses: dart-lang/ecosystem/.github/workflows/post_summaries.yaml@main + permissions: + pull-requests: write
diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..7d6442c --- /dev/null +++ b/.github/workflows/publish.yaml
@@ -0,0 +1,17 @@ +# A CI configuration to auto-publish pub packages. + +name: Publish + +on: + pull_request: + branches: [ master ] + types: [opened, synchronize, reopened, labeled, unlabeled] + push: + tags: [ '[A-z]+-v[0-9]+.[0-9]+.[0-9]+*' ] + +jobs: + publish: + if: ${{ github.repository_owner == 'dart-lang' }} + uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main + with: + write-comments: false
diff --git a/.github/workflows/scorecards-analysis.yml b/.github/workflows/scorecards-analysis.yml new file mode 100644 index 0000000..3c5c10a --- /dev/null +++ b/.github/workflows/scorecards-analysis.yml
@@ -0,0 +1,55 @@ +name: Scorecards supply-chain security +on: + # Only the default branch is supported. + branch_protection_rule: + push: + branches: [ master ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecards analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + actions: read + contents: read + # Needed to access OIDC token. + id-token: write + + steps: + - name: "Checkout code" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 + with: + results_file: results.sarif + results_format: sarif + # Read-only PAT token. To create it, + # follow the steps in https://github.com/ossf/scorecard-action#pat-token-creation. + repo_token: ${{ secrets.SCORECARD_READ_TOKEN }} + # Publish the results to enable scorecard badges. For more details, see + # https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories, `publish_results` will automatically be set to `false`, + # regardless of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). + - name: "Upload artifact" + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@f09c1c0a94de965c15400f5634aa42fac8fb8f88 # v3.27.5 + with: + sarif_file: results.sarif
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..56c3800 --- /dev/null +++ b/.gitignore
@@ -0,0 +1,13 @@ +# Don’t commit the following directories created by pub. +.buildlog +.dart_tool/ +.pub/ +build/ +packages + +# Include when developing application packages. +pubspec.lock +.packages + +# Ignore common coverage directory +coverage/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..90003c0 --- /dev/null +++ b/CONTRIBUTING.md
@@ -0,0 +1,74 @@ +Want to contribute? Great! First, read this page (including the small print at +the end). + +### Before you contribute + +Before we can use your code, you must sign the +[Google Individual Contributor License Agreement][CLA] (CLA), which you can do +online. The CLA is necessary mainly because you own the copyright to your +changes, even after your contribution becomes part of our codebase, so we need +your permission to use and distribute your code. We also need to be sure of +various other things—for instance that you'll tell us if you know that your code +infringes on other people's patents. You don't have to sign the CLA until after +you've submitted your code for review and a member has approved it, but you must +do it before we can put your code into our codebase. + +Before you start working on a larger contribution, you should get in touch with +us first through the issue tracker with your idea so that we can help out and +possibly guide you. Coordinating up front makes it much easier to avoid +frustration later on. + +[CLA]: https://cla.developers.google.com/about/google-individual + +### Code reviews + +All submissions, including submissions by project members, require review. We +recommend [forking the repository][fork], making changes in your fork, and +[sending us a pull request][pr] so we can review the changes and merge them into +this repository. + +[fork]: https://help.github.com/articles/about-forks/ +[pr]: https://help.github.com/articles/creating-a-pull-request/ + +Functional changes will require tests to be added or changed. The tests live in +the `test/` directory for each package, and are run with `dart test`. If you +need to create new tests, use the existing tests as a guideline for what they +should look like. + +You can run all additional presubmit checks locally if you wish, by using the +`mono_repo` tool. From the root of the repository, run +`pub global run mono_repo presubmit`. + +### Versioning + +You will also need to potentially update the pubspec.yaml and/or CHANGELOG.md of +any package you are updating. If the current version is not a `-dev` version +then you should update it and add a new header to the changelog. If you have no +publicly facing change to list, it is OK for there to be no changes listed. + +We follow pretty strict semantic versioning, feel free to ask on the PR if you +are unsure about what version number you should choose (or do your best, and a +code reviewers will bring it up if it is incorrect). + +### File headers + +All files in the project must start with the following header. + + // Copyright (c) 2023, 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. + +### Publishing + +Publishing is done by package owners creating a tag of the form +`<package>-v<version>`, either manually or through the github release ui +(preferred). The pubspec and changelog must already be updated to the desired +release version in the master branch for this process to work. + +### The small print + +Contributions made by corporations are covered by a different agreement than the +one above, the +[Software Grant and Corporate Contributor License Agreement][CCLA]. + +[CCLA]: https://developers.google.com/open-source/cla/corporate
diff --git a/README.md b/README.md new file mode 100644 index 0000000..25b50aa --- /dev/null +++ b/README.md
@@ -0,0 +1,24 @@ +[](https://github.com/dart-lang/test/actions/workflows/dart.yml) +[](https://deps.dev/project/github/dart-lang%2Ftest) + +## What's here? + +Welcome! [package:test](pkgs/test/) is the standard testing library for Dart and +Flutter. If you have questions about Dart testing, please see the docs for +[package:test](pkgs/test/). `package:test_api` and `package:test_core` +are implementation details and generally not user-facing. + +[package:checks](pkgs/checks/) is a relatively new library for expressing test +expectations. It's a more modern version of `package:matcher` and features a +literate API. + +## Packages + +| Package | Description | Version | +|---|---|---| +| [checks](pkgs/checks/) | A framework for checking values against expectations and building custom expectations. | [](https://pub.dev/packages/checks) | +| [fake_async](pkgs/fake_async/) | Fake asynchronous events such as timers and microtasks for deterministic testing. | [](https://pub.dev/packages/fake_async) | +| [matcher](pkgs/matcher/) | Support for specifying test expectations via an extensible Matcher class. | [](https://pub.dev/packages/matcher) | +| [test](pkgs/test/) | A full featured library for writing and running Dart tests across platforms. | [](https://pub.dev/packages/test) | +| [test_api](pkgs/test_api/) | | [](https://pub.dev/packages/test_api) | +| [test_core](pkgs/test_core/) | | [](https://pub.dev/packages/test_core) |
diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..f6b2dc1 --- /dev/null +++ b/analysis_options.yaml
@@ -0,0 +1,22 @@ +include: package:dart_flutter_team_lints/analysis_options.yaml + + +analyzer: + language: + strict-casts: true + errors: + # There are a number of deprecated members used through this package + deprecated_member_use_from_same_package: ignore + + # Ignoring a number of lints from dart_flutter_team_lints – for now + avoid_catching_errors: ignore + avoid_dynamic_calls: ignore + comment_references: ignore + lines_longer_than_80_chars: ignore + only_throw_errors: ignore + unawaited_futures: ignore + unsafe_html: ignore + +linter: + rules: + - avoid_private_typedef_functions
diff --git a/integration_tests/regression/dart_test.yaml b/integration_tests/regression/dart_test.yaml new file mode 100644 index 0000000..07fd2f5 --- /dev/null +++ b/integration_tests/regression/dart_test.yaml
@@ -0,0 +1,13 @@ +paths: +- lib/issue_2142/test.dart + +platforms: +- chrome +- vm + +compilers: +- dart2js +- dart2wasm +- exe +- kernel +- source
diff --git a/integration_tests/regression/lib/issue_2142/import.dart b/integration_tests/regression/lib/issue_2142/import.dart new file mode 100644 index 0000000..987fbb1 --- /dev/null +++ b/integration_tests/regression/lib/issue_2142/import.dart
@@ -0,0 +1,8 @@ +// Copyright (c) 2023, 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. + +// ignore: prefer_relative_imports +import 'package:regression_tests/issue_2142/test.dart'; + +Thing newThing() => Thing();
diff --git a/integration_tests/regression/lib/issue_2142/test.dart b/integration_tests/regression/lib/issue_2142/test.dart new file mode 100644 index 0000000..06afc5a --- /dev/null +++ b/integration_tests/regression/lib/issue_2142/test.dart
@@ -0,0 +1,14 @@ +// Copyright (c) 2023, 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'; +import 'import.dart'; + +void main() { + test('aThing is a Thing', () { + expect(newThing(), isA<Thing>()); + }); +} + +class Thing {}
diff --git a/integration_tests/regression/mono_pkg.yaml b/integration_tests/regression/mono_pkg.yaml new file mode 100644 index 0000000..4a60bc1 --- /dev/null +++ b/integration_tests/regression/mono_pkg.yaml
@@ -0,0 +1,16 @@ +# See https://pub.dev/packages/mono_repo + +sdk: +- dev +- pubspec + +os: +- linux + +stages: +- analyze_and_format: + - group: + - format + - analyze: --fatal-infos +- unit_test: + - test
diff --git a/integration_tests/regression/pubspec.yaml b/integration_tests/regression/pubspec.yaml new file mode 100644 index 0000000..6987181 --- /dev/null +++ b/integration_tests/regression/pubspec.yaml
@@ -0,0 +1,7 @@ +name: regression_tests +publish_to: none +environment: + sdk: ^3.5.0 +resolution: workspace +dependencies: + test: any
diff --git a/integration_tests/spawn_hybrid/lib/emits_numbers.dart b/integration_tests/spawn_hybrid/lib/emits_numbers.dart new file mode 100644 index 0000000..6429a85 --- /dev/null +++ b/integration_tests/spawn_hybrid/lib/emits_numbers.dart
@@ -0,0 +1,13 @@ +// 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 'package:stream_channel/stream_channel.dart'; + +void hybridMain(StreamChannel channel) { + channel.sink + ..add(1) + ..add(2) + ..add(3) + ..close(); +}
diff --git a/integration_tests/spawn_hybrid/mono_pkg.yaml b/integration_tests/spawn_hybrid/mono_pkg.yaml new file mode 100644 index 0000000..7d75b61 --- /dev/null +++ b/integration_tests/spawn_hybrid/mono_pkg.yaml
@@ -0,0 +1,18 @@ +# See https://pub.dev/packages/mono_repo + +sdk: +- dev +- pubspec + +stages: +- analyze_and_format: + - group: + - format + - analyze: --fatal-infos + sdk: + - dev +- unit_test: + - test: -p chrome,vm,node + os: + - linux + - windows
diff --git a/integration_tests/spawn_hybrid/other_package/lib/emits_numbers.dart b/integration_tests/spawn_hybrid/other_package/lib/emits_numbers.dart new file mode 100644 index 0000000..6429a85 --- /dev/null +++ b/integration_tests/spawn_hybrid/other_package/lib/emits_numbers.dart
@@ -0,0 +1,13 @@ +// 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 'package:stream_channel/stream_channel.dart'; + +void hybridMain(StreamChannel channel) { + channel.sink + ..add(1) + ..add(2) + ..add(3) + ..close(); +}
diff --git a/integration_tests/spawn_hybrid/other_package/pubspec.yaml b/integration_tests/spawn_hybrid/other_package/pubspec.yaml new file mode 100644 index 0000000..b7d026f --- /dev/null +++ b/integration_tests/spawn_hybrid/other_package/pubspec.yaml
@@ -0,0 +1,6 @@ +name: other_package +publish_to: none +environment: + sdk: '>=2.12.0 <3.0.0' +dependencies: + stream_channel: ^2.1.0
diff --git a/integration_tests/spawn_hybrid/pubspec.yaml b/integration_tests/spawn_hybrid/pubspec.yaml new file mode 100644 index 0000000..de9e5e9 --- /dev/null +++ b/integration_tests/spawn_hybrid/pubspec.yaml
@@ -0,0 +1,13 @@ +name: spawn_hybrid +publish_to: none +environment: + sdk: ^3.5.0 +resolution: workspace +dependencies: + async: ^2.9.0 + path: ^1.8.2 + stream_channel: ^2.1.0 +dev_dependencies: + other_package: + path: other_package/ + test: any
diff --git a/integration_tests/spawn_hybrid/test/hybrid_test.dart b/integration_tests/spawn_hybrid/test/hybrid_test.dart new file mode 100644 index 0000000..1493e23 --- /dev/null +++ b/integration_tests/spawn_hybrid/test/hybrid_test.dart
@@ -0,0 +1,357 @@ +// 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:async'; + +import 'package:async/async.dart'; +import 'package:path/path.dart' as p; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/test.dart'; + +void main() { + group('spawnHybridUri():', () { + test('loads a file in a separate isolate connected via StreamChannel', + () async { + expect(spawnHybridUri('util/emits_numbers.dart').stream.toList(), + completion(equals([1, 2, 3]))); + }); + + test('resolves root-relative URIs relative to the package root', () async { + expect(spawnHybridUri('/test/util/emits_numbers.dart').stream.toList(), + completion(equals([1, 2, 3]))); + }); + + test('supports Uri objects', () async { + expect( + spawnHybridUri(Uri.parse('util/emits_numbers.dart')).stream.toList(), + completion(equals([1, 2, 3]))); + }); + + test('supports package: uris referencing the root package', () async { + expect( + spawnHybridUri(Uri.parse('package:spawn_hybrid/emits_numbers.dart')) + .stream + .toList(), + completion(equals([1, 2, 3]))); + }); + + test('supports package: uris referencing dependency packages', () async { + expect( + spawnHybridUri(Uri.parse('package:other_package/emits_numbers.dart')) + .stream + .toList(), + completion(equals([1, 2, 3]))); + }); + + test('rejects non-String, non-Uri objects', () { + expect(() => spawnHybridUri(123), throwsArgumentError); + }); + + test('passes a message to the hybrid isolate', () async { + expect( + spawnHybridUri('util/echos_message.dart', message: 123).stream.first, + completion(equals(123))); + expect( + spawnHybridUri('util/echos_message.dart', message: 'wow') + .stream + .first, + completion(equals('wow'))); + }); + + test('emits an error from the stream channel if the isolate fails to load', + () { + expect(spawnHybridUri('non existent file').stream.first, + throwsA(isA<Exception>())); + }); + }); + + group('spawnHybridCode()', () { + test('loads the code in a separate isolate connected via StreamChannel', + () { + expect(spawnHybridCode(''' + import "package:stream_channel/stream_channel.dart"; + + void hybridMain(StreamChannel channel) { + channel.sink..add(1)..add(2)..add(3)..close(); + } + ''').stream.toList(), completion(equals([1, 2, 3]))); + }); + + test('allows a first parameter with type StreamChannel<Object?>', () { + expect(spawnHybridCode(''' + import "package:stream_channel/stream_channel.dart"; + + void hybridMain(StreamChannel<Object?> channel) { + channel.sink..add(1)..add(2)..add(null)..close(); + } + ''').stream.toList(), completion(equals([1, 2, null]))); + }); + + test('gives a good error when the StreamChannel type is not supported', () { + expect( + spawnHybridCode(''' + import "package:stream_channel/stream_channel.dart"; + + void hybridMain(StreamChannel<Object> channel) { + channel.sink..add(1)..add(2)..add(3)..close(); + } + ''').stream, + emitsError(isA<Exception>().having( + (e) => e.toString(), + 'toString', + contains( + 'The first parameter to the top-level hybridMain() must be a ' + 'StreamChannel<dynamic> or StreamChannel<Object?>. More specific ' + 'types such as StreamChannel<Object> are not supported.')))); + }); + + test('can use dart:io even when run from a browser', () async { + var path = p.join('test', 'hybrid_test.dart'); + expect(spawnHybridCode(""" + import 'dart:io'; + + import 'package:stream_channel/stream_channel.dart'; + + void hybridMain(StreamChannel channel) { + channel.sink + ..add(File(r"$path").readAsStringSync()) + ..close(); + } + """).stream.first, completion(contains('hybrid emits numbers'))); + }, testOn: 'browser'); + + test('forwards data from the test to the hybrid isolate', () async { + var channel = spawnHybridCode(''' + import "package:stream_channel/stream_channel.dart"; + + void hybridMain(StreamChannel channel) { + channel.stream.listen((num) { + channel.sink.add(num + 1); + }); + } + '''); + channel.sink + ..add(1) + ..add(2) + ..add(3); + expect(channel.stream.take(3).toList(), completion(equals([2, 3, 4]))); + }); + + test('passes an initial message to the hybrid isolate', () { + var code = ''' + import "package:stream_channel/stream_channel.dart"; + + void hybridMain(StreamChannel channel, Object message) { + channel.sink..add(message)..close(); + } + '''; + + expect(spawnHybridCode(code, message: [1, 2, 3]).stream.first, + completion(equals([1, 2, 3]))); + expect(spawnHybridCode(code, message: {'a': 'b'}).stream.first, + completion(equals({'a': 'b'}))); + }); + + test('allows the hybrid isolate to send errors across the stream channel', + () { + var channel = spawnHybridCode(''' + import "package:stack_trace/stack_trace.dart"; + import "package:stream_channel/stream_channel.dart"; + + void hybridMain(StreamChannel channel) { + channel.sink.addError("oh no!", Trace.current()); + } + '''); + + channel.stream.listen(null, onError: expectAsync2((error, stackTrace) { + expect(error.toString(), equals('oh no!')); + expect(stackTrace.toString(), contains('hybridMain')); + })); + }); + + test('sends an unhandled synchronous error across the stream channel', () { + var channel = spawnHybridCode(''' + import "package:stream_channel/stream_channel.dart"; + + void hybridMain(StreamChannel channel) { + throw "oh no!"; + } + '''); + + channel.stream.listen(null, onError: expectAsync2((error, stackTrace) { + expect(error.toString(), equals('oh no!')); + expect(stackTrace.toString(), contains('hybridMain')); + })); + }); + + test('sends an unhandled asynchronous error across the stream channel', () { + var channel = spawnHybridCode(''' + import 'dart:async'; + + import "package:stream_channel/stream_channel.dart"; + + void hybridMain(StreamChannel channel) { + scheduleMicrotask(() { + throw "oh no!"; + }); + } + '''); + + channel.stream.listen(null, onError: expectAsync2((error, stackTrace) { + expect(error.toString(), equals('oh no!')); + expect(stackTrace.toString(), contains('hybridMain')); + })); + }); + + test('deserializes TestFailures as TestFailures', () { + var channel = spawnHybridCode(''' + import "package:stream_channel/stream_channel.dart"; + + import "package:test/test.dart"; + + void hybridMain(StreamChannel channel) { + throw TestFailure("oh no!"); + } + '''); + + expect(channel.stream.first, throwsA(isA<TestFailure>())); + }); + + test('gracefully handles an unserializable message in the VM', () { + var channel = spawnHybridCode(''' + import "package:stream_channel/stream_channel.dart"; + + void hybridMain(StreamChannel channel) {} + '''); + + expect(() => channel.sink.add(<Object>[].iterator), throwsArgumentError); + }); + + test('gracefully handles an unserializable message in the browser', + () async { + var channel = spawnHybridCode(''' + import 'package:stream_channel/stream_channel.dart'; + + void hybridMain(StreamChannel channel) {} + '''); + + expect(() => channel.sink.add(<Object>[].iterator), throwsArgumentError); + }, testOn: 'browser'); + + test('gracefully handles an unserializable message in the hybrid isolate', + () { + var channel = spawnHybridCode(''' + import "package:stream_channel/stream_channel.dart"; + + void hybridMain(StreamChannel channel) { + channel.sink.add([].iterator); + } + '''); + + channel.stream.listen(null, onError: expectAsync1((error) { + expect(error.toString(), contains("can't be JSON-encoded.")); + })); + }); + + test('forwards prints from the hybrid isolate', () { + expect(() async { + var channel = spawnHybridCode(''' + import "package:stream_channel/stream_channel.dart"; + + void hybridMain(StreamChannel channel) { + print("hi!"); + channel.sink.add(null); + } + '''); + await channel.stream.first; + }, prints('hi!\n')); + }); + + // This takes special handling, since the code is packed into a data: URI + // that's imported, URIs don't escape $ by default, and $ isn't allowed in + // imports. + test('supports a dollar character in the hybrid code', () { + expect(spawnHybridCode(r''' + import "package:stream_channel/stream_channel.dart"; + + void hybridMain(StreamChannel channel) { + var value = "bar"; + channel.sink.add("foo${value}baz"); + } + ''').stream.first, completion('foobarbaz')); + }); + + test('closes the channel when the hybrid isolate exits', () { + var channel = spawnHybridCode(''' + import "dart:isolate"; + + hybridMain(_) { + Isolate.current.kill(); + } + '''); + + expect(channel.stream.toList(), completion(isEmpty)); + }); + + group('closes the channel when the test finishes by default', () { + late StreamChannel channel; + + test('test 1', () { + channel = spawnHybridCode(''' + import 'package:stream_channel/stream_channel.dart'; + + void hybridMain(StreamChannel channel) {} + '''); + }); + + test('test 2', () async { + var isDone = false; + channel.stream.listen(null, onDone: () => isDone = true); + await pumpEventQueue(); + expect(isDone, isTrue); + }); + }); + + group('persists across multiple tests with stayAlive: true', () { + late StreamQueue queue; + late StreamSink sink; + setUpAll(() { + var channel = spawnHybridCode(''' + import "package:stream_channel/stream_channel.dart"; + + void hybridMain(StreamChannel channel) { + channel.stream.listen((message) { + channel.sink.add(message); + }); + } + ''', stayAlive: true); + queue = StreamQueue(channel.stream); + sink = channel.sink; + }); + + test('echoes a number', () { + expect(queue.next, completion(equals(123))); + sink.add(123); + }); + + test('echoes a string', () { + expect(queue.next, completion(equals('wow'))); + sink.add('wow'); + }); + }); + + test('opts in to null safety by default', () async { + expect(spawnHybridCode(''' + import "package:stream_channel/stream_channel.dart"; + + // Use some null safety syntax + int? x; + + void hybridMain(StreamChannel channel) { + channel.sink..add(1)..add(2)..add(3)..close(); + } + ''').stream.toList(), completion(equals([1, 2, 3]))); + }); + }); +}
diff --git a/integration_tests/spawn_hybrid/test/hybrid_test_io.dart b/integration_tests/spawn_hybrid/test/hybrid_test_io.dart new file mode 100644 index 0000000..0e454db --- /dev/null +++ b/integration_tests/spawn_hybrid/test/hybrid_test_io.dart
@@ -0,0 +1,47 @@ +// 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. + +@TestOn('vm') +library; + +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + test('kills the isolate when the test closes the channel', () async { + var channel = spawnHybridCode(''' + import "dart:async"; + import "dart:io"; + + import "package:shelf/shelf.dart" as shelf; + import "package:shelf/shelf_io.dart" as io; + import "package:stream_channel/stream_channel.dart"; + + hybridMain(StreamChannel channel) async { + var server = await ServerSocket.bind("localhost", 0); + server.listen(null); + channel.sink.add(server.port); + } + '''); + + // Expect that the socket disconnects at some point (presumably when the + // isolate closes). + var port = await channel.stream.first as int; + var socket = await Socket.connect('localhost', port); + expect(socket.listen(null).asFuture<void>(), completes); + + await channel.sink.close(); + }); + + test('spawnHybridUri(): supports absolute file: URIs', () async { + expect( + spawnHybridUri(p.toUri(p.absolute( + p.relative(p.join('test', 'util', 'emits_numbers.dart'))))) + .stream + .toList(), + completion(equals([1, 2, 3]))); + }); +}
diff --git a/integration_tests/spawn_hybrid/test/subdir/hybrid_test.dart b/integration_tests/spawn_hybrid/test/subdir/hybrid_test.dart new file mode 100644 index 0000000..088375c --- /dev/null +++ b/integration_tests/spawn_hybrid/test/subdir/hybrid_test.dart
@@ -0,0 +1,20 @@ +// 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. + +@TestOn('vm') +library; + +import 'package:test/test.dart'; + +void main() { + group('spawnHybridUri():', () { + test('loads uris relative to the test file', () async { + expect( + spawnHybridUri(Uri.parse('../util/emits_numbers.dart')) + .stream + .toList(), + completion(equals([1, 2, 3]))); + }); + }); +}
diff --git a/integration_tests/spawn_hybrid/test/util/echos_message.dart b/integration_tests/spawn_hybrid/test/util/echos_message.dart new file mode 100644 index 0000000..271b44f --- /dev/null +++ b/integration_tests/spawn_hybrid/test/util/echos_message.dart
@@ -0,0 +1,11 @@ +// 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 'package:stream_channel/stream_channel.dart'; + +void hybridMain(StreamChannel channel, Object message) { + channel.sink + ..add(message) + ..close(); +}
diff --git a/integration_tests/spawn_hybrid/test/util/emits_numbers.dart b/integration_tests/spawn_hybrid/test/util/emits_numbers.dart new file mode 100644 index 0000000..6429a85 --- /dev/null +++ b/integration_tests/spawn_hybrid/test/util/emits_numbers.dart
@@ -0,0 +1,13 @@ +// 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 'package:stream_channel/stream_channel.dart'; + +void hybridMain(StreamChannel channel) { + channel.sink + ..add(1) + ..add(2) + ..add(3) + ..close(); +}
diff --git a/integration_tests/wasm/dart_test.yaml b/integration_tests/wasm/dart_test.yaml new file mode 100644 index 0000000..14308c4 --- /dev/null +++ b/integration_tests/wasm/dart_test.yaml
@@ -0,0 +1,5 @@ +platforms: [chrome, firefox] +# Node doesn't work because the version available in the current Ubuntu GitHub runners is too +# old to support WASM+GC, which would be required to run Dart tests. +#platforms: [chrome, firefox, node] +compilers: [dart2wasm]
diff --git a/integration_tests/wasm/mono_pkg.yaml b/integration_tests/wasm/mono_pkg.yaml new file mode 100644 index 0000000..a8f3410 --- /dev/null +++ b/integration_tests/wasm/mono_pkg.yaml
@@ -0,0 +1,18 @@ +# See https://pub.dev/packages/mono_repo + +sdk: +- pubspec +- dev + +os: +- linux +- windows + +stages: +- analyze_and_format: + - group: + - format + - analyze: --fatal-infos +- unit_test: + # The config here is a regression test for https://github.com/dart-lang/test/issues/2006 + - test: --timeout=60s
diff --git a/integration_tests/wasm/pubspec.yaml b/integration_tests/wasm/pubspec.yaml new file mode 100644 index 0000000..370d844 --- /dev/null +++ b/integration_tests/wasm/pubspec.yaml
@@ -0,0 +1,7 @@ +name: wasm_tests +publish_to: none +environment: + sdk: ^3.5.0 +resolution: workspace +dev_dependencies: + test: any
diff --git a/integration_tests/wasm/test/hello_world_test.dart b/integration_tests/wasm/test/hello_world_test.dart new file mode 100644 index 0000000..9ddd511 --- /dev/null +++ b/integration_tests/wasm/test/hello_world_test.dart
@@ -0,0 +1,24 @@ +// 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. + +@TestOn('wasm') +// This retry is a regression test for https://github.com/dart-lang/test/issues/2006 +@Retry(2) +library; + +import 'package:test/test.dart'; + +void main() { + test('1 == 1', () { + expect(1, equals(1)); + }); + + test('asserts are enabled', () { + expect(shouldFail, throwsA(isA<AssertionError>())); + }); +} + +void shouldFail() { + assert(1 == 2); +}
diff --git a/mono_repo.yaml b/mono_repo.yaml new file mode 100644 index 0000000..1131355 --- /dev/null +++ b/mono_repo.yaml
@@ -0,0 +1,21 @@ +# See with https://github.com/dart-lang/mono_repo for details on this file +self_validate: analyze_and_format + +github: + # Setting just `cron` keeps the defaults for `push` and `pull_request` + cron: '0 0 * * 0' # “At 00:00 (UTC) on Sunday.” + on_completion: + - name: "Notify failure" + runs-on: ubuntu-latest + # Run only if other jobs have failed and this is a push or scheduled build. + if: (github.event_name == 'push' || github.event_name == 'schedule') && failure() + steps: + - run: > + curl -H "Content-Type: application/json" -X POST -d \ + "{'text':'Build failed! ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}'}" \ + "${CHAT_WEBHOOK_URL}" + env: + CHAT_WEBHOOK_URL: ${{ secrets.BUILD_AND_TEST_TEAM_CHAT_WEBHOOK_URL }} + +merge_stages: +- analyze_and_format
diff --git a/pkgs/checks/CHANGELOG.md b/pkgs/checks/CHANGELOG.md new file mode 100644 index 0000000..6588352 --- /dev/null +++ b/pkgs/checks/CHANGELOG.md
@@ -0,0 +1,54 @@ +## 0.3.1-wip + +- Directly compare keys across actual and expected `Map` instances when + checking deep collection equality and all the keys can be directly compared + for equality. This maintains the path into a nested collection for typical + cases of checking for equality against a purely value collection. +- Always wrap Condition descriptions in angle brackets. +- Add `containsMatchingInOrder` and `containsEqualInOrder` to replace the + combined functionality in `containsInOrder`. +- Replace `pairwiseComparesTo` with `pairwiseMatches`. +- Increase SDK constraint to ^3.5.0. + +## 0.3.0 + +- **Breaking Changes** + - Remove the `Condition` class and the `it()` utility. Replace calls to + `(it()..someExpectation())` with `((it) => it.someExpectation())`. +- Add class modifiers to restrict extension of implementation classes. + +## 0.2.2 + +- Return the first failure from `softCheck` and `softCheckAsync` as + documented, instead of the last failure when there are multiple failures. +- Add example `because` usage and mention the "reason" name in the migration + guide. +- Add `ComparableChecks` with comparison expectations for subject types that + implement `Comparable`. + +## 0.2.1 + +- Add a link to file issues with feedback in the README. + +## 0.2.0 + +- **Breaking Changes** + - `checkThat` renamed to `check`. + - `nest` and `nestAsync` take `Iterable<String> Function()` arguments for + `label` instead of `String`. + - Async expectation extensions `completes`, `throws`, `emits`, and + `emitsError` no longer return a `Future<Subject>`. Instead they take an + optional `Condition` argument which can check expectations that would + have been checked on the returned subject. + - `nestAsync` no longer returns a `Subject`, callers must pass the + followup `Condition` to the nullable argument. + - Remove the `which` extension on `Future<Subject>`. + - `matches` renamed to `matchesPattern` and now accepts a `Pattern` + argument, instead of limiting to `RegExp`. +- Added an example. +- Include a stack trace in the failure description for unexpected errors from + Futures or Streams. + +## 0.1.0 + +- Initial release.
diff --git a/pkgs/checks/LICENSE b/pkgs/checks/LICENSE new file mode 100644 index 0000000..000cd7b --- /dev/null +++ b/pkgs/checks/LICENSE
@@ -0,0 +1,27 @@ +Copyright 2014, 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/checks/README.md b/pkgs/checks/README.md new file mode 100644 index 0000000..5415f11 --- /dev/null +++ b/pkgs/checks/README.md
@@ -0,0 +1,220 @@ +[](https://pub.dev/packages/checks) +[](https://pub.dev/packages/checks/publisher) + +`package:checks` is a library for expressing test expectations and it features +a literate API. + +## package:checks preview + +`package:checks` is in preview; to provide feedback on the API, please file +[an issue][] with questions, suggestions, feature requests, or general +feedback. + +For documentation about migrating from `package:matcher` to `checks`, see the +[migration guide][]. + +[an issue]:https://github.com/dart-lang/test/issues/new?labels=package%3Achecks&template=03_checks_feedback.md +[migration guide]:https://github.com/dart-lang/test/blob/master/pkgs/checks/doc/migrating_from_matcher.md + +## Quickstart + +1. Add a `dev_dependency` on `checks: ^0.2.0`. + +1. Add an import for `package:checks/checks.dart`. + +1. Use `checks` in your test code: + +```dart +void main() { + test('sample test', () { + // test code here + ... + + check(actual).equals(expected); + check(someList).isNotEmpty(); + check(someObject).isA<Map>(); + check(someString)..startsWith('a')..endsWith('z')..contains('lmno'); + }); +} +``` + +## Checking expectations with `checks` + +Expectations start with `check`. This utility returns a `Subject`, and +expectations can be checked against the subject. Expectations are defined as +extension methods, and different expectations will be available for subjects +with different value types. + +```dart +check(someValue).equals(expectedValue); +check(someList).deepEquals(expectedList); +check(someString).contains('expected pattern'); +``` + +If a failure may not have enough context about the actual or expected values +from the expectation calls alone, add a "Reason" in the failure message by +passing a `because:` argument to `check()`. + +```dart +check( + because: 'log lines must start with the severity', + logLines, +).every((l) => l + ..anyOf([ + (l) => l.startsWith('ERROR'), + (l) => l.startsWith('WARNING'), + (l) => l.startsWith('INFO'), + ])); +``` + + +### Composing expectations + + +Multiple expectations can be checked against the same value using cascade +syntax. When multiple expectations are checked against a single value, a failure +will included descriptions of the expectations that already passed. + +```dart +check(someString) + ..startsWith('a') + ..endsWith('z') + ..contains('lmno'); +``` + +Some nested checks may be not be possible to write with cascade syntax. +There is a `which` utility for this use case which takes a `Condition`. + +```dart +check(someString) + ..startsWith('a') + // A cascade would not be possible on `length` + ..length.which((l) => l + ..isGreatherThan(10) + ..isLessThan(100)); +``` + + +Some expectations return a `Subject` for another value derived from the original +value, such as the `length` extension. + +```dart +check(someString).length.equals(expectedLength); +``` + +Fields or derived values can be extracted from objects for checking further +properties with the `has` utility. + +```dart +check(someValue) + .has((value) => value.property, 'property') + .equals(expectedPropertyValue); +``` + +### Passing a set of expectations as an argument + +Some expectations take arguments which are themselves expectations to apply to +other values. These expectations take `Condition` arguments which have the +signature void Function(Subject)`. The conditions check expectations when they +are called with a `Subject` argument. + +```dart +check(someList).any((e) => e.isGreaterThan(0)); +``` + +### Checking asynchronous expectations + +Expectation extension methods checking asynchronous behavior return a `Future`. +The future should typically be awaited within the test body, however +asynchronous expectations will also ensure that the test is not considered +complete before the expectation is complete. +Expectations with no concrete end conditions, such as an expectation that a +future never completes, cannot be awaited and may cause a failure after the test +has already appeared to complete. + +Asynchronous expectations do not return a `Subject`. When an expectation +extracts a derived value further expectations can be checked by passing a +`Condition`. + +```dart +await check(someFuture).completes((r) => r.isGreaterThan(0)); +``` + +Subjects for `Stream` instances must first be wrapped into a `StreamQueue` to +allow multiple expectations to test against the stream from the same state. +The `withQueue` extension can be used when a given stream instance only needs to +be checked once, or if it is a broadcast stream, but if single subscription +stream needs to have multiple expectations checked separately it should be +wrapped with a `StreamQueue`. + +```dart +await check(someStream).withQueue.inOrder([ + (s) => s.emits((e) => e.equals(1)), + (s) => s.emits((e) => e.equals(2)), + (s) => s.emits((e) => e.equals(3)), + (s) => s.isDone(), +]); + +var someQueue = StreamQueue(someOtherStream); +await check(someQueue).emits((e) => e.equals(1)); +// do something +await check(someQueue).emits((e) => e.equals(2)); +// do something +``` + + +## Writing custom expectations + +Expectations are written as extensions on `Subject` with specific generics. The +library `package:checks/context.dart` gives access to a `context` getter on +`Subject` which offers capabilities for defining expectations on the subject's +value. + +The `Context` allows checking an expectation with `expect`, `expectAsync` and +`expectUnawaited`, or extracting a derived value for performing other checks +with `nest` and `nestAsync`. Failures are reported by returning a `Rejection`, +or an `Extracted.rejection`, extensions should avoid throwing exceptions. + +Descriptions of the clause checked by an expectations are passed through a +separate callback from the predicate which checks the value. Nesting calls are +made with a label directly. When there are no failures the clause callbacks are +not called. When a condition callback is described, the clause callbacks are +called, but the predicate callbacks are not called. Conditions can be checked +against values without throwing an exception using `softCheck` or +`softCheckAsync`. + +```dart +extension CustomChecks on Subject<CustomType> { + void someExpectation() { + context.expect(() => ['meets this expectation'], (actual) { + if (_expectationIsMet(actual)) return null; + return Rejection(which: ['does not meet this expectation']); + }); + } + + Subject<Foo> get someDerivedValue => + context.nest(() => ['has someDerivedValue'], (actual) { + if (_cannotReadDerivedValue(actual)) { + return Extracted.rejection(which: ['cannot read someDerivedValue']); + } + return Extracted.value(_readDerivedValue(actual)); + }); + + // for field reads that will not get rejected, use `has` + Subject<Bar> get someField => has((a) => a.someField, 'someField'); +} +``` + +Extensions may also compose existing expectations under a single name. When +such expectations fail, the test output will refer to the individual +expectations that were called. + +```dart +extension ComposedChecks on Subject<Iterable> { + void hasLengthInRange(int min, int max) { + length + ..isGreaterThan(min) + ..isLessThan(max); + } +} +```
diff --git a/pkgs/checks/doc/migrating_from_matcher.md b/pkgs/checks/doc/migrating_from_matcher.md new file mode 100644 index 0000000..b358e63 --- /dev/null +++ b/pkgs/checks/doc/migrating_from_matcher.md
@@ -0,0 +1,203 @@ +## Migrating from package:matcher + +`package:checks` is currently in preview. Once this package reaches a stable +version, it will be the recommended package by the Dart team to use for most +tests. + +[`package:matcher`][matcher] is the legacy package with an API exported from +`package:test/test.dart` and `package:test/expect.dart`. + +**Do I have to migrate all at once?** No. `package:matcher` will be compatible +with `package:checks`, and old tests can continue to use matchers. Test cases +within the same file can use a mix of `expect` and `check`. + +**_Should_ I migrate all at once?** Probably not, it depends on your tolerance +for having tests use a mix of APIs. As you add new tests, or need to make +updates to existing tests, using `checks` will make testing easier. Tests which +are stable and passing will not get significant benefits from a migration. + +**Do I need to migrate at all?** No. When `package:test`stops exporting +these members it will be possible to add a dependency on `package:matcher` and +continue to use them. `package:matcher` will continue to be available. + +**Why is the Dart team adding a second framework?** The `matcher` package has a +design which is fundamentally incompatible with using static types to validate +correct use. With an entirely new design, the static types in `checks` give +confidence that the expectation is appropriate for the value, and can narrow +autocomplete choices in the IDE for a better editing experience. The clean break +from the legacy implementation and API also gives an opportunity to make small +behavior and signature changes to align with modern Dart idioms. + +**Should I start using checks right away?** There is still a +high potential for minor or major breaking changes during the preview window. +Once this package is stable, yes! The experience of using `checks` improves on +`matcher`. See some of the [improvements to look forward to in checks +below](#improvements-you-can-expect). + +[matcher]: https://pub.dev/packages/matcher + +## Trying Checks as a Preview + +1. Add a `dev_dependency` on `checks: ^0.2.0`. + +1. Replace the existing `package:test/test.dart` import with + `package:test/scaffolding.dart`. + +1. Add an import to `package:checks/checks.dart`. + +1. For an incremental migration within the test, add an import to + `package:test/expect.dart`. Remove it to surface errors in tests that still + need to be migrated, or keep it in so the tests work without being fully + migrated. + +1. Migrate the test cases. + +## Migrating from Matchers + +Replace calls to `expect` or `expectLater` with a call to `check` passing the +first argument. +When a direct replacement is available, change the second argument from calling +a function returning a Matcher, to calling the relevant extension method on the +`Subject`. + +Whenever you see a bare non-matcher value argument for `expected`, assume it +should use the `equals` expectation, although take care when the subject is a +collection. +See below, `.equals` may not always be the correct replacement in +`package:checks`. + +```dart +expect(actual, expected); +check(actual).equals(expected); +// or maybe +check(actualCollection).deepEquals(expected); + +await expectLater(actual, completes()); +await check(actual).completes(); +``` + +If you use the `reason` argument to `expect`, rename it to `because`. + +```dart +expect(actual, expectation(), reason: 'some explanation'); +check(because: 'some explanation', actual).expectation(); +``` + +### Differences in behavior from matcher + +- The `equals` Matcher performed a deep equality check on collections. + `.equals()` expectation will only correspond to [operator ==] so some tests + may need to replace `.equals()` with `.deepEquals()`. +- Streams must be explicitly wrapped into a `StreamQueue` before they can be + tested for behavior. Use `check(actualStream).withQueue`. +- `emitsAnyOf` is `Subject<StreamQueue>.anyOf`. `emitsInOrder` is `inOrder`. + The arguments are `FutureOr<void> Function(Subject<StreamQueue>)` and match + a behavior of the entire stream. In `matcher` the elements to expect could + have been a bare value to check for equality, a matcher for the emitted + value, or a matcher for the entire queue which would match multiple values. + Use `(s) => s.emits((e) => e.interestingCheck())` to check the emitted + elements. +- In `package:matcher` the [`matches` Matcher][matches] converted a `String` + argument into a `Regex`, so `matches(r'\d')` would match the value `'1'`. + This was potentially confusing, because even though `String` is a subtype of + `Pattern`, it wasn't used as a pattern directly. + With `matchesPattern` a `String` argument is used as a `Pattern` and + comparison uses [`String.allMatches`][allMatches]. + For backwards compatibility change `matches(regexString)` to + `matchesPattern(RegExp(regexString))`. +- The `TypeMatcher.having` API is replace by the more general`.has`. While + `.having` could only be called on a `TypeMatcher` using `.isA`, `.has` works + on any `Subject`. `CoreChecks.has` takes 1 fewer arguments - instead of + taking the last argument, a `matcher` to apply to the field, it returns a + `Subject` for the field. + +[matches]:https://pub.dev/documentation/matcher/latest/matcher/Matcher/matches.html +[allMatches]:https://api.dart.dev/stable/2.19.1/dart-core/Pattern/allMatches.html + +### Matchers with replacements under a different name + +- `anyElement` -> `Subject<Iterable>.any` +- `everyElement` -> `Subject<Iterable>.every` +- `completion(Matcher)` -> `completes(conditionCallback)` +- `containsPair(key, value)` -> Use `Subject<Map>[key].equals(value)` +- `hasLength(expected)` -> `length.equals(expected)` +- `isNot(Matcher)` -> `not(conditionCallback)` +- `pairwiseCompare` -> `pairwiseMatches` +- `same` -> `identicalTo` +- `stringContainsInOrder` -> `Subject<String>.containsInOrder` +- `containsAllInOrder(iterable)` -> + `Subject<Iterable>.containsMatchingInOrder(iterable)` to compare with + conditions other than equals, + `Subject<Iterable>.containsEqualInOrder(iterable)` to compare each index + with the equality operator (`==`). + +### Members from `package:test/expect.dart` without a direct replacement + +- `checks` does not ship with any type checking matchers for specific types. + Instead of, for example, `isArgumentError` use `isA<ArgumentError>`, and + similary `throws<ArgumentError>` over `throwsArgumentError`. +- `anything`. When a condition callback is needed that should accept any + value, pass `(_) {}`. +- Specific numeric comparison - `isNegative`, `isPositive`, `isZero` and their + inverses. Use `isLessThan`, `isGreaterThan`, `isLessOrEqual`, and + `isGreaterOrEqual` with appropriate numeric arguments. +- Numeric range comparison, `inClosedOpenRange`, `inExclusiveRange`, + `inInclusiveRange`, `inOpenClosedRange`. Use cascades to chain a check for + both ends of the range onto the same subject. +- `containsOnce`: TODO add missing expectation +- `emitsInAnyOrder`: TODO add missing expectation +- `expectAsync` and `expectAsyncUntil`. Continue to import + `package:test/expect.dart` for these APIs. +- `isIn`: TODO add missing expectation +- `orderedEquals`: Use `deepEquals`. If the equality needs to specifically + *not* be deep equality (this is unusual, nested collections are unlikely to + have a meaningful equality), force using `operator ==` at the first level + with `.deepEquals(expected.map((e) => (Subject<Object?> s) => s.equals(e)))`; +- `prints`: TODO add missing expectation? Is this one worth replacing? +- `predicate`: TODO add missing expectation + +## Improvements you can expect + +Expectations are statically restricted to those which are appropriate for the +type. So while the following is statically allowed with `matcher` but always +fails at runtime, the expectation cannot be written at all with `checks`. + +```dart +expect(1, contains(1)); // No static error, always fails +check(1).contains(1); // Static error. The method 'contains' isn't defined +``` + +These static restrictions also improve the relevance of IDE autocomplete +suggestions. While editing with the cursor at `_`, the suggestions provided +in the `matcher` example can include _any_ top level element including matchers +appropriate for other types of value, type names, and top level definitions from +other packages. With the cursor following a `.` in the `checks` example the +suggestions will only be expectations or utilities appropriate for the value +type. + +```dart +expect(actual, _ // many unrelated suggestions +check(actual)._ // specific suggestions +``` + +Asynchronous matchers in `matcher` are a subtype of synchronous matchers, but do +not satisfy the same behavior contract. Some APIs which use a matcher could not +validate whether it would satisfy the behavior it needs, and it could result in +a false success, false failure, or misleading errors. APIs which correctly use +asynchronous matchers need to do a type check and change their interaction based +on the runtime type. Asynchronous expectations in `checks` are refused at +runtime when a synchronous answer is required. The error will help solve the +specific misuse, instead of resulting in a confusing error, or worse a missed +failure. The reason for the poor compatibility in `matcher` is due to some +history of implementation - asynchronous matchers were written in `test` +alongside `expect`, and synchronous matchers have no dependency on the +asynchronous implementation. + +Asynchronous expectations always return a `Future`, and with the +[`unawaited_futures` lint][unawaited lint] should more safely ensure that +asynchronous expectation work is completed within the test body. With `matcher` +it was up to the author to correctly use `await expecLater` for asynchronous +cases, and `expect` for synchronous cases, and if `expect` was used with an +asynchronous matcher the expectation could fail at any point. + +[unawaited lint]: https://dart.dev/lints/unawaited_futures
diff --git a/pkgs/checks/example/example.dart b/pkgs/checks/example/example.dart new file mode 100644 index 0000000..88bec0b --- /dev/null +++ b/pkgs/checks/example/example.dart
@@ -0,0 +1,26 @@ +// Copyright (c) 2023, 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:checks/checks.dart'; +import 'package:test/scaffolding.dart'; + +void main() { + test('sample test', () { + final someValue = 5; + check(someValue).equals(5); + + final someList = [1, 2, 3, 4, 5]; + check(someList).deepEquals([1, 2, 3, 4, 5]); + + final someString = 'abcdefghijklmnopqrstuvwxyz'; + + check( + because: 'it should contain the beginning, middle and end', + someString, + ) + ..startsWith('a') + ..endsWith('z') + ..contains('lmno'); + }); +}
diff --git a/pkgs/checks/lib/checks.dart b/pkgs/checks/lib/checks.dart new file mode 100644 index 0000000..db8b18e --- /dev/null +++ b/pkgs/checks/lib/checks.dart
@@ -0,0 +1,15 @@ +// 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. + +export 'src/checks.dart' + show AsyncCondition, Condition, SkipExtension, Subject, check; +export 'src/extensions/async.dart' + show FutureChecks, StreamChecks, WithQueueExtension; +export 'src/extensions/core.dart' + show BoolChecks, ComparableChecks, CoreChecks, NullableChecks; +export 'src/extensions/function.dart' show FunctionChecks; +export 'src/extensions/iterable.dart' show IterableChecks; +export 'src/extensions/map.dart' show MapChecks; +export 'src/extensions/math.dart' show NumChecks; +export 'src/extensions/string.dart' show StringChecks;
diff --git a/pkgs/checks/lib/context.dart b/pkgs/checks/lib/context.dart new file mode 100644 index 0000000..cebba71 --- /dev/null +++ b/pkgs/checks/lib/context.dart
@@ -0,0 +1,21 @@ +// 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. + +export 'src/checks.dart' + show + AsyncCondition, + CheckFailure, + Condition, + Context, + ContextExtension, + Extracted, + FailureDetail, + Rejection, + Subject, + describe, + describeAsync, + softCheck, + softCheckAsync; +export 'src/describe.dart' + show escape, indent, literal, postfixLast, prefixFirst;
diff --git a/pkgs/checks/lib/src/checks.dart b/pkgs/checks/lib/src/checks.dart new file mode 100644 index 0000000..e1e4bcb --- /dev/null +++ b/pkgs/checks/lib/src/checks.dart
@@ -0,0 +1,1009 @@ +// 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:async'; + +import 'package:meta/meta.dart' as meta; +import 'package:test_api/hooks.dart'; + +import 'describe.dart'; +import 'extensions/async.dart'; +import 'extensions/core.dart'; +import 'extensions/iterable.dart'; + +/// A target for checking expectations against a value in a test. +/// +/// A subject my have a real value, in which case the expectations can be +/// validated or rejected; or it may be a placeholder, in which case +/// expectations describe what would be checked but cannot be rejected. +/// +/// Expectation methods are defined in extensions `on Subject`, specialized on +/// the generic [T]. +/// Expectation extension methods can use the [ContextExtension] to interact +/// with the [Context] for this subject. +/// +/// Create a subject that throws an exception for missed expectations with the +/// [check] function. +final class Subject<T> { + final Context<T> _context; + Subject._(this._context); +} + +/// A callback that synchronously checks expectations against a subject. +/// +/// Asynchronous expectations should not be used within a `Condition` callback. +typedef Condition<T> = void Function(Subject<T>); + +/// A callback that asynchronously checks expectations against a subject. +/// +/// Any expectations may be used within an `AsyncCondition` callback. +typedef AsyncCondition<T> = FutureOr<void> Function(Subject<T>); + +extension SkipExtension<T> on Subject<T> { + /// Mark the currently running test as skipped and return a [Subject] that + /// will ignore all expectations. + /// + /// Any expectations against the return value will not be checked and will not + /// be included in the "Expected" or "Actual" string representations of a + /// failure. + /// + /// ```dart + /// check(actual) + /// ..stillChecked() + /// ..skip('reason the expectation is temporarily not met').notChecked(); + /// ``` + /// + /// If `skip` is used in a callback passed to `softCheck` or `describe` it + /// will still mark the test as skipped, even though failing the expectation + /// would not have otherwise caused the test to fail. + Subject<T> skip(String message) { + TestHandle.current.markSkipped(message); + return Subject._(_SkippedContext()); + } +} + +/// Creates a [Subject] that can be used to validate expectations against +/// [value], with an exception upon a failed expectation. +/// +/// Expectations that are not satisfied throw a [TestFailure] to interrupt the +/// currently running test and mark it as failed. +/// +/// If [because] is passed it will be included as a "Reason:" line in failure +/// messages. +/// +/// ```dart +/// check(actual).equals(expected); +/// ``` +@meta.useResult +Subject<T> check<T>(T value, {String? because}) => Subject._(_TestContext._root( + value: _Present(value), + // TODO - switch between "a" and "an" + label: 'a $T', + fail: (f) { + final which = f.rejection.which; + throw TestFailure([ + ...prefixFirst('Expected: ', f.detail.expected), + ...prefixFirst('Actual: ', f.detail.actual), + ...indent( + prefixFirst('Actual: ', f.rejection.actual), f.detail.depth), + if (which != null && which.isNotEmpty) + ...indent(prefixFirst('Which: ', which), f.detail.depth), + if (because != null) 'Reason: $because', + ].join('\n')); + }, + allowAsync: true, + allowUnawaited: true, + )); + +/// Checks whether [value] satisfies all expectations invoked in [condition], +/// without throwing an exception. +/// +/// Returns `null` if all expectations are satisfied, otherwise returns the +/// [CheckFailure] for the first expectation that fails. +/// +/// Asynchronous expectations are not allowed in [condition] and will cause a +/// runtime error if they are used. +CheckFailure? softCheck<T>(T value, Condition<T> condition) { + CheckFailure? failure; + final subject = Subject<T>._(_TestContext._root( + value: _Present(value), + fail: (f) { + failure ??= f; + }, + allowAsync: false, + allowUnawaited: false, + )); + condition(subject); + return failure; +} + +/// Checks whether [value] satisfies all expectations invoked in [condition], +/// without throwing an exception. +/// +/// The future will complete to `null` if all expectations are satisfied, +/// otherwise it will complete to the [CheckFailure] for the first expectation +/// that fails. +/// +/// In contrast to [softCheck], asynchronous expectations are allowed in +/// [condition]. +Future<CheckFailure?> softCheckAsync<T>( + T value, AsyncCondition<T> condition) async { + CheckFailure? failure; + final subject = Subject<T>._(_TestContext._root( + value: _Present(value), + fail: (f) { + failure ??= f; + }, + allowAsync: true, + allowUnawaited: false, + )); + await condition(subject); + return failure; +} + +/// Creates a description of the expectations checked by [condition]. +/// +/// The strings are individual lines of a description. +/// The description of an expectation may be one or more adjacent lines. +/// +/// Matches the "Expected: " lines in the output of a failure message if a value +/// did not meet the last expectation in [condition], without the first labeled +/// line. +/// +/// Asynchronous expectations are not allowed in [condition], for async +/// conditions use [describeAsync]. +Iterable<String> describe<T>(Condition<T> condition) { + final context = _TestContext<T>._root( + value: _Absent(), + fail: (_) { + throw UnimplementedError(); + }, + allowAsync: false, + allowUnawaited: true, + ); + condition(Subject._(context)); + return context.detail(context).expected.skip(1); +} + +/// Creates a description of the expectations checked by [condition]. +/// +/// The strings are individual lines of a description. +/// The description of an expectation may be one or more adjacent lines. +/// +/// Matches the "Expected: " lines in the output of a failure message if a value +/// did not meet the last expectation in [condition], without the first labeled +/// line. +/// +/// In contrast to [describe], asynchronous expectations are allowed in +/// [condition]. +Future<Iterable<String>> describeAsync<T>(AsyncCondition<T> condition) async { + final context = _TestContext<T>._root( + value: _Absent(), + fail: (_) { + throw UnimplementedError(); + }, + allowAsync: true, + allowUnawaited: true, + ); + await condition(Subject._(context)); + return context.detail(context).expected.skip(1); +} + +extension ContextExtension<T> on Subject<T> { + /// The expectations and nesting context for this subject. + Context<T> get context => _context; +} + +/// The context for a [Subject] that allows asserting expectations and creating +/// nested subjects. +/// +/// A [Subject] is the target for checking expectations in a test. +/// Every subject has a [Context] which holds the "actual" value, tracks how the +/// value was obtained, and can check expectations about the value. +/// +/// The user focused APIs called within tests are expectation extension methods +/// written in an extension `on Subject`, typically specialized to a specific +/// generic. +/// +/// Expectation extension methods will make a call to one of the APIs on the +/// subject's [Context], and can perform one of two types of operations: +/// +/// - Expect something of the current value (such as [CoreChecks.equals] or +/// [IterableChecks.contains]) by calling [expect], [expectAsync], or +/// [expectUnawaited]. +/// - Expect that a new subject can be extracted from the current value (such +/// as [CoreChecks.has] or [FutureChecks.completes]) by calling [nest] or +/// [nestAsync]. +/// +/// +/// Whichever type of operation, an expectation extension method provides two +/// callbacks. +/// The first callback is an `Iterable<String> Function()` returning a +/// description of the expectation. +/// The second callback always takes the actual value as an argument, and the +/// specific signature varies by operation. +/// +/// +/// In expectation extension methods calling [expect], [expectAsync], or +/// [expectUnawaited], the `predicate` callback can report a [Rejection] if the +/// value fails to satisfy the expectation. +/// The description will be passed in a "clause" callback. +/// {@template clause_description} +/// The clause callback returns a description of what is checked which stands +/// on its own. +/// For instance the `is equal to <1>` in: +/// +/// ``` +/// Expected: a int that: +/// is equal to <1> +/// ``` +/// {@endtemplate} +/// +/// +/// In expectation extension methods calling [nest] or [nestAsync], the +/// `extract` callback can return a [Extracted.rejection] if the value fails to +/// satisfy an expectation which disallows extracting the value, or an +/// [Extracted.value] to become the value in a nested subject. +/// The description will be passed in a "label" callback. +/// {@template label_description} +/// The label callback returns a description of the extracted subject as it +/// relates to the original subject. +/// For instance the `completes to a value` in: +/// +/// ``` +/// Expected a Future<int> that: +/// completes to a value that: +/// is equal to <1> +/// ``` +/// +/// A label should also be sensible when it is read as a clause. +/// If no further expectations are checked on the extracted subject, or if the +/// extraction is rejected, the "that:" is omitted in the output. +/// +/// ``` +/// Expected a Future<int> that: +/// completes to a value +/// ``` +/// {@endtemplate} +/// +/// +/// A rejection carries two descriptions, one description of the "actual" value +/// that was tested, and an optional "which" with further details about how the +/// result different from the expectation. +/// If the "actual" argument is omitted it will be filled with a representation +/// of the value passed to the expectation callback formatted with [literal]. +/// If an expectation extension method is written on a type of subject without a +/// useful `toString()`, the rejection can provide a string representation to +/// use instead. +/// The "which" argument may be omitted if the reason is very obvious based on +/// the clause and "actual" description, but most expectations should include a +/// "which". +/// +/// The behavior of a context following a rejection depends on the source of the +/// [Subject]. +/// +/// When an expectation is rejected for a [check] subject, an exception is +/// thrown to interrupt the test, so no further checks should happen. The +/// failure message will include: +/// - An "Expected" section with descriptions of all the expectations that +/// were checked, including the ones that passed, and the last one that +/// failed. +/// - An "Actual" section, which may be the description directly from the +/// [Rejection] if the failure was on the root subject, or may start with a +/// partial version of the "Expected" description up to the label for the +/// nesting subject that saw a failure, then the "actual" from the rejection. +/// - A "Which" description from the rejection, if it was included. +/// +/// For example, if a failure happens on the root subject, the "actual" is taken +/// directly from the rejection. +/// +/// ``` +/// Expected: a Future<int> that: +/// completes to a value +/// Actual: a future that completes as an error +/// Which: threw <UnimplementedError> at: +/// <stack trace> +/// ``` +/// +/// But if the failure happens on a nested subject, the actual starts with a +/// description of the nesting or non-nesting expectations that succeeded, up +/// to nesting point of the failure, then the "actual" and "which" from the +/// rejection are indented to that level of nesting. +/// +/// ``` +/// Expected: a Future<int> that: +/// completes to a value that: +/// equals <1> +/// Actual: a Future<int> that: +/// completes to a value that: +/// Actual: <0> +/// Which: are not equal +/// ``` +/// +/// ```dart +/// extension CustomChecks on Subject<CustomType> { +/// void someExpectation() { +/// context.expect(() => ['meets this expectation'], (actual) { +/// if (_expectationIsMet(actual)) return null; +/// return Rejection(which: ['does not meet this expectation']); +/// }); +/// } +/// +/// Subject<Foo> get someDerivedValue => +/// context.nest('has someDerivedValue', (actual) { +/// if (_cannotReadDerivedValue(actual)) { +/// return Extracted.rejection(which: ['cannot read someDerivedValue']); +/// } +/// return Extracted.value(_readDerivedValue(actual)); +/// }); +/// +/// // for field reads that will not get rejected, use `has` +/// Subject<Bar> get someField => has((a) => a.someField, 'someField'); +/// } +/// ``` +/// +/// When an expectation is rejected for a subject within a call to [softCheck] +/// or [softCheckAsync] a [CheckFailure] will be returned with the rejection, as +/// well as a [FailureDetail] which could be used to format the same failure +/// message thrown by the [check] subject. +/// +/// {@template callbacks_may_be_unused} +/// The description of an expectation may never be shown to the user, so the +/// callback may never be invoked. +/// If all the conditions on a subject succeed, or if the failure detail for a +/// failed [softCheck] is never read, the descriptions will be unused. +/// String formatting for the descriptions should be performed in the callback, +/// not ahead of time. +/// +/// +/// The context for a subject may hold a real "actual" value to test against, or +/// it may have a placeholder within a call to [describe]. +/// A context with a placeholder value will not invoke the callback to check +/// expectations. +/// +/// If both callbacks are invoked, the description callback will always be +/// called strictly after the expectation callback is called. +/// +/// Callbacks passed to a context should not throw. +/// {@endtemplate} +/// +/// +/// Some contexts disallow certain interactions. +/// {@template async_limitations} +/// Calls to [expectAsync] or [nestAsync] must not be performed by a condition +/// callback passed to [softCheck] or [describe]. +/// Use [softCheckAsync] or [describeAsync] for any condition which checks async +/// expectations. +/// {@endtemplate} +/// {@template unawaited_limitations} +/// Calls to [expectUnawaited] may not be performed by a condition callback +/// passed to [softCheck] or [softCheckAsync]. +/// {@endtemplate} +/// +/// Expectation extension methods can access the context for the subject with +/// the [ContextExtension]. +/// +/// {@template description_lines} +/// Description callbacks return an `Iterable<String>` where each element is a +/// line in the output. Individual elements should not contain newlines. +/// Utilities such as [prefixFirst], [postfixLast], and [literal] may be useful +/// to format values which are potentially multiline. +/// {@endtemplate} +abstract final class Context<T> { + /// Expect that [predicate] will not return a [Rejection] for the checked + /// value. + /// + /// {@macro clause_description} + /// + /// {@macro description_lines} + /// + /// {@macro callbacks_may_be_unused} + /// + /// ```dart + /// void someExpectation() { + /// context.expect(() => ['meets this expectation'], (actual) { + /// if (_expectationIsMet(actual)) return null; + /// return Rejection(which: ['does not meet this expectation']); + /// }); + /// } + /// ``` + void expect( + Iterable<String> Function() clause, Rejection? Function(T) predicate); + + /// Expect that [predicate] will not result in a [Rejection] for the checked + /// value. + /// + /// {@macro clause_description} + /// + /// {@macro description_lines} + /// + /// {@macro callbacks_may_be_unused} + /// + /// {@macro async_limitations} + /// + /// ```dart + /// extension CustomChecks on Subject<CustomType> { + /// Future<void> someAsyncExpectation() async { + /// await context.expectAsync(() => ['meets this async expectation'], + /// (actual) async { + /// if (await _expectationIsMet(actual)) return null; + /// return Rejection(which: ['does not meet this async expectation']); + /// }); + /// } + /// } + /// ``` + Future<void> expectAsync(Iterable<String> Function() clause, + FutureOr<Rejection?> Function(T) predicate); + + /// Expect that [predicate] will not invoke the passed callback with a + /// [Rejection] at any point. + /// + /// In contrast to [expectAsync], a rejection is reported through a + /// callback instead of through a returned Future. The callback may be invoked + /// at any point that the failure surfaces. + /// + /// This may be useful for a condition checking that some event _never_ + /// happens. If there is no specific point where it is know to be safe to stop + /// listening for the event, there is no way to complete a returned future and + /// consider the check "complete". + /// + /// {@macro clause_description} + /// + /// {@macro description_lines} + /// + /// {@macro callbacks_may_be_unused} + /// + /// {@macro unawaited_limitations} + /// The only useful effect of a late rejection is to throw a [TestFailure] + /// when used with a [check] subject. Most conditions should prefer to use + /// [expect] or [expectAsync]. + /// + /// ```dart + /// void someUnawaitableExpectation() async { + /// await context.expectUnawaited( + /// () => ['meets this unawaitable expectation'], (actual, reject) { + /// final failureSignal = _completeIfFailed(actual); + /// unawaited(failureSignal.then((_) { + /// reject(Reject( + /// which: ['unexpectedly failed this unawaited expectation'])); + /// })); + /// }); + /// } + /// ``` + void expectUnawaited(Iterable<String> Function() clause, + void Function(T, void Function(Rejection)) predicate); + + /// Extract a property from the value for further checking. + /// + /// If the property cannot be extracted, [extract] should return an + /// [Extracted.rejection] describing the problem. Otherwise it should return + /// an [Extracted.value]. + /// + /// Subsequent expectations can be checked for the extracted value on the + /// returned [Subject]. + /// + /// {@macro label_description} + /// + /// If [atSameLevel] is true then the returned [Extracted.value] should hold + /// the same instance as the passed value, or an object which is is equivalent + /// but has a type that is more convenient to test. + /// In this case expectations applied to the returned [Subject] will behave as + /// if they were applied to the subject for this context. + /// The [label] will be used as if it were a "clause" argument passed to + /// [expect]. + /// If the label returns an empty iterable, the clause will be omitted. + /// The label should only be left empty if the value extraction cannot be + /// rejected. + /// + /// {@macro description_lines} + /// + /// {@macro callbacks_may_be_unused} + /// + /// ```dart + /// Subject<Foo> get someDerivedValue => + /// context.nest(() => ['has someDerivedValue'], (actual) { + /// if (_cannotReadDerivedValue(actual)) { + /// return Extracted.rejection( + /// which: ['cannot read someDerivedValue']); + /// } + /// return Extracted.value(_readDerivedValue(actual)); + /// }); + /// ``` + Subject<R> nest<R>( + Iterable<String> Function() label, Extracted<R> Function(T) extract, + {bool atSameLevel = false}); + + /// Extract an asynchronous property from the value for further checking. + /// + /// If the property cannot be extracted, [extract] should return an + /// [Extracted.rejection] describing the problem. Otherwise it should return + /// an [Extracted.value]. + /// + /// In contrast to [nest], subsequent expectations need to be passed in + /// [nestedCondition] which will be applied to the subject for the extracted + /// value. + /// + /// {@macro label_description} + /// + /// {@macro description_lines} + /// + /// {@macro callbacks_may_be_unused} + /// + /// {@macro async_limitations} + /// + /// ```dart + /// Future<void> someAsyncResult( + /// [AsyncCondition<Result> resultCondition]) async { + /// await context.nestAsync(() => ['has someAsyncResult'], (actual) async { + /// if (await _asyncOperationFailed(actual)) { + /// return Extracted.rejection(which: ['cannot read someAsyncResult']); + /// } + /// return Extracted.value(await _readAsyncResult(actual)); + /// }, resultCondition); + /// } + /// ``` + Future<void> nestAsync<R>( + Iterable<String> Function() label, + FutureOr<Extracted<R>> Function(T) extract, + AsyncCondition<R>? nestedCondition); +} + +/// A property extracted from a value being checked, or a rejection. +final class Extracted<T> { + final Rejection? _rejection; + final T? _value; + + /// Creates a rejected extraction to indicate a failure trying to read the + /// value. + /// + /// When a nesting is rejected with an omitted or empty [actual] argument, it + /// will be filled in with the [literal] representation of the value. + Extracted.rejection( + {Iterable<String> actual = const [], Iterable<String>? which}) + : _rejection = Rejection(actual: actual, which: which), + _value = null; + Extracted.value(T this._value) : _rejection = null; + + Extracted._(Rejection this._rejection) : _value = null; + + Extracted<R> _map<R>(R Function(T) transform) { + final rejection = _rejection; + if (rejection != null) return Extracted._(rejection); + return Extracted.value(transform(_value as T)); + } + + Extracted<T> _fillActual(Object? actual) => _rejection == null || + _rejection.actual.isNotEmpty + ? this + : Extracted.rejection(actual: literal(actual), which: _rejection.which); +} + +abstract interface class _Optional<T> { + R? apply<R extends FutureOr<Rejection?>>(R Function(T) callback); + Future<Extracted<_Optional<R>>> mapAsync<R>( + FutureOr<Extracted<R>> Function(T) transform); + Extracted<_Optional<R>> map<R>(Extracted<R> Function(T) transform); +} + +class _Present<T> implements _Optional<T> { + final T value; + _Present(this.value); + + @override + R? apply<R extends FutureOr<Rejection?>>(R Function(T) c) => c(value); + + @override + Future<Extracted<_Present<R>>> mapAsync<R>( + FutureOr<Extracted<R>> Function(T) transform) async { + final transformed = await transform(value); + return transformed._map(_Present.new); + } + + @override + Extracted<_Present<R>> map<R>(Extracted<R> Function(T) transform) => + transform(value)._map(_Present.new); +} + +class _Absent<T> implements _Optional<T> { + @override + R? apply<R extends FutureOr<Rejection?>>(R Function(T) c) => null; + + @override + Future<Extracted<_Absent<R>>> mapAsync<R>( + FutureOr<Extracted<R>> Function(T) transform) async => + Extracted.value(_Absent<R>()); + + @override + Extracted<_Absent<R>> map<R>(FutureOr<Extracted<R>> Function(T) transform) => + Extracted.value(_Absent<R>()); +} + +final class _TestContext<T> implements Context<T>, _ClauseDescription { + final _Optional<T> _value; + + /// A reference to find the root context which this context is nested under. + /// + /// null only for the root context. + final _TestContext<dynamic>? _parent; + + final List<_ClauseDescription> _clauses; + final List<_TestContext> _aliases; + + final void Function(CheckFailure) _fail; + + final bool _allowAsync; + final bool _allowUnawaited; + + /// A callback that returns a label for this context. + /// + /// If this context is the root the label should return a phrase like + /// "a List" in + /// + /// ``` + /// Expected: a List that: + /// ``` + /// + /// If this context is nested under another context the lable should return a + /// phrase like "completes to a value" in + /// + /// + /// ``` + /// Expected: a Future<int> that: + /// completes to a value that: + /// ``` + /// + /// In cases where a nested context does not have any expectations checked on + /// it, the "that:" will be will be omitted. + final Iterable<String> Function() _label; + + static Iterable<String> _emptyLabel() => const []; + + /// Create a context appropriate for a subject which is not nested under any + /// other subject. + _TestContext._root({ + required _Optional<T> value, + required void Function(CheckFailure) fail, + required bool allowAsync, + required bool allowUnawaited, + String? label, + }) : _value = value, + _label = (() => [label ?? '']), + _fail = fail, + _allowAsync = allowAsync, + _allowUnawaited = allowUnawaited, + _parent = null, + _clauses = [], + _aliases = []; + + _TestContext._alias(_TestContext original, this._value) + : _parent = original, + _clauses = original._clauses, + _aliases = original._aliases, + _fail = original._fail, + _allowAsync = original._allowAsync, + _allowUnawaited = original._allowUnawaited, + // Never read from an aliased context because they are never present in + // `_clauses`. + _label = _emptyLabel; + + /// Create a context nested under [parent]. + /// + /// The [_label] callback should not return an empty iterable. + _TestContext._child(this._value, this._label, _TestContext<dynamic> parent) + : _parent = parent, + _fail = parent._fail, + _allowAsync = parent._allowAsync, + _allowUnawaited = parent._allowUnawaited, + _clauses = [], + _aliases = []; + + @override + void expect( + Iterable<String> Function() clause, Rejection? Function(T) predicate) { + _clauses.add(_ExpectationClause(clause)); + final rejection = + _value.apply((actual) => predicate(actual)?._fillActual(actual)); + if (rejection != null) { + _fail(_failure(rejection)); + } + } + + @override + Future<void> expectAsync(Iterable<String> Function() clause, + FutureOr<Rejection?> Function(T) predicate) async { + if (!_allowAsync) { + throw StateError( + 'Async expectations cannot be used on a synchronous subject'); + } + _clauses.add(_ExpectationClause(clause)); + final outstandingWork = TestHandle.current.markPending(); + try { + final rejection = await _value.apply( + (actual) async => (await predicate(actual))?._fillActual(actual)); + if (rejection == null) return; + _fail(_failure(rejection)); + } finally { + outstandingWork.complete(); + } + } + + @override + void expectUnawaited(Iterable<String> Function() clause, + void Function(T actual, void Function(Rejection) reject) predicate) { + if (!_allowUnawaited) { + throw StateError('Late expectations cannot be used for soft checks'); + } + _clauses.add(_ExpectationClause(clause)); + _value.apply((actual) { + predicate(actual, (r) => _fail(_failure(r._fillActual(actual)))); + }); + } + + @override + Subject<R> nest<R>( + Iterable<String> Function() label, Extracted<R> Function(T) extract, + {bool atSameLevel = false}) { + final result = _value.map((actual) => extract(actual)._fillActual(actual)); + final rejection = result._rejection; + if (rejection != null) { + _clauses.add(_ExpectationClause(label)); + _fail(_failure(rejection)); + } + final value = result._value ?? _Absent<R>(); + final _TestContext<R> context; + if (atSameLevel) { + context = _TestContext._alias(this, value); + _aliases.add(context); + _clauses.add(_ExpectationClause(label)); + } else { + context = _TestContext._child(value, label, this); + _clauses.add(context); + } + return Subject._(context); + } + + @override + Future<void> nestAsync<R>( + Iterable<String> Function() label, + FutureOr<Extracted<R>> Function(T) extract, + AsyncCondition<R>? nestedCondition) async { + if (!_allowAsync) { + throw StateError( + 'Async expectations cannot be used on a synchronous subject'); + } + final outstandingWork = TestHandle.current.markPending(); + try { + final result = await _value.mapAsync( + (actual) async => (await extract(actual))._fillActual(actual)); + final rejection = result._rejection; + if (rejection != null) { + _clauses.add(_ExpectationClause(label)); + _fail(_failure(rejection)); + } + final value = result._value ?? _Absent<R>(); + final context = _TestContext<R>._child(value, label, this); + _clauses.add(context); + await nestedCondition?.call(Subject<R>._(context)); + } finally { + outstandingWork.complete(); + } + } + + CheckFailure _failure(Rejection rejection) => + CheckFailure(rejection, () => _root.detail(this)); + + _TestContext get _root { + _TestContext<dynamic> current = this; + while (current._parent != null) { + current = current._parent; + } + return current; + } + + @override + FailureDetail detail(_TestContext failingContext) { + final thisContextFailed = + identical(failingContext, this) || _aliases.contains(failingContext); + var foundDepth = thisContextFailed ? 0 : -1; + var foundOverlap = thisContextFailed ? 0 : -1; + var successfulOverlap = 0; + final expected = <String>[]; + if (_clauses.isEmpty) { + expected.addAll(_label()); + } else { + expected.addAll(postfixLast(' that:', _label())); + for (var clause in _clauses) { + final details = clause.detail(failingContext); + expected.addAll(indent(details.expected)); + if (details.depth >= 0) { + assert(foundDepth == -1); + assert(foundOverlap == -1); + foundDepth = details.depth + 1; + foundOverlap = details._actualOverlap + successfulOverlap + 1; + } else { + if (foundDepth == -1) { + successfulOverlap += details.expected.length; + } + } + } + } + return FailureDetail(expected, foundOverlap, foundDepth); + } +} + +/// A context which never runs expectations and can never fail. +final class _SkippedContext<T> implements Context<T> { + @override + void expect( + Iterable<String> Function() clause, Rejection? Function(T) predicate) { + // no-op + } + + @override + Future<void> expectAsync(Iterable<String> Function() clause, + FutureOr<Rejection?> Function(T) predicate) async { + // no-op + } + + @override + void expectUnawaited(Iterable<String> Function() clause, + void Function(T actual, void Function(Rejection) reject) predicate) { + // no-op + } + + @override + Subject<R> nest<R>( + Iterable<String> Function() label, Extracted<R> Function(T p1) extract, + {bool atSameLevel = false}) { + return Subject._(_SkippedContext()); + } + + @override + Future<void> nestAsync<R>( + Iterable<String> Function() label, + FutureOr<Extracted<R>> Function(T p1) extract, + AsyncCondition<R>? nestedCondition) async { + // no-op + } +} + +abstract interface class _ClauseDescription { + FailureDetail detail(_TestContext failingContext); +} + +class _ExpectationClause implements _ClauseDescription { + final Iterable<String> Function() _expected; + _ExpectationClause(this._expected); + @override + FailureDetail detail(_TestContext failingContext) => + FailureDetail(_expected(), -1, -1); +} + +/// The result an expectation that failed for a subject.. +final class CheckFailure { + /// The specific rejected value within the overall subject that caused the + /// failure. + /// + /// The [Rejection.actual] may be a property derived from the value at the + /// root of the subject, for instance a field or an element in a collection. + final Rejection rejection; + + /// The context within the overall subject where an expectation resulted in + /// the [rejection]. + late final FailureDetail detail = _readDetail(); + + final FailureDetail Function() _readDetail; + + CheckFailure(this.rejection, this._readDetail); +} + +/// The context for a failed expectation. +/// +/// A subject may have some number of succeeding expectations, and the failure may +/// be for an expectation against a property derived from the value at the root +/// of the subject. For example, in `check([]).length.equals(1)` the +/// specific value that gets rejected is `0` from the length of the list, and +/// the subject that sees the rejection is nested with the label "has length". +final class FailureDetail { + /// A description of all the conditions the subject was expected to satisfy. + /// + /// Each subject has a label. At the root the label is typically "a + /// <Type>" and nested subjects get a label based on the condition + /// which extracted a property for further checks. Each level of nesting is + /// described as "<label> that:" followed by an indented list of the + /// expectations for that property. + /// + /// For example: + /// + /// a List that: + /// has length that: + /// equals <3> + final Iterable<String> expected; + + /// A description of the conditions the checked value satisfied. + /// + /// Matches the format of [expected], except it will be cut off after the + /// label for the subject that had a failing expectation. For example, if the + /// equality check for the length of a list fails: + /// + /// a List that: + /// has length that: + /// + /// If the subject with a failing expectation is the root, returns an empty + /// list. Instead the "Actual: " value from the rejection can be used without + /// indentation. + Iterable<String> get actual => + _actualOverlap > 0 ? expected.take(_actualOverlap + 1) : const []; + + /// The number of lines from [expected] which describe conditions that were + /// successful. + /// + /// A failed expectation on a derived property may have some number of + /// expectations that were checked and satisfied starting from the root + /// subject. This field indicates how many lines of expectations were + /// successful. + final int _actualOverlap; + + /// The number of times the failing subject was nested from the root subject. + /// + /// Indicates how far the "Actual: " and "Which: " lines from the [Rejection] + /// should be indented so that they are at the same level of indentation as + /// the label for the subject where the expectation failed. + /// + /// For example, if a `List` is expected to and have a certain length + /// [expected] may be: + /// + /// a List that: + /// has length that: + /// equals <3> + /// + /// If the actual value had an incorrect length, the [depth] will be `1` to + /// indicate that the failure occurred checking one of the expectations + /// against the `has length` label. + final int depth; + + FailureDetail(this.expected, this._actualOverlap, this.depth); +} + +/// A description of a value that failed an expectation. +final class Rejection { + /// A description of the actual value as it relates to the expectation. + /// + /// This may use [literal] to show a String representation of the value, or it + /// may be a description of a specific aspect of the value. For instance an + /// expectation that a Future completes to a value may describe the actual as + /// "A Future that completes to an error". + /// + /// When a value is rejected with no [actual] argument, it will be filled in + /// with the [literal] representation of the value. + /// + /// Lines should be split to separate elements, and individual strings should + /// not contain newlines. + /// + /// This is printed following an "Actual: " label in the output of a failure + /// message. All lines in the message will be indented to the level of the + /// expectation in the description, and printed following the descriptions of + /// any expectations that have already passed. + final Iterable<String> actual; + + /// A description of the way that [actual] failed to meet the expectation. + /// + /// An expectation can provide extra detail, or focus attention on a specific + /// part of the value. For instance when comparing multiple elements in a + /// collection, the rejection may describe that the value "has an unequal + /// value at index 3". + /// + /// Lines should be separate values in the iterable, if any element contains a + /// newline it may cause problems with indentation in the output. + /// + /// When provided, this is printed following a "Which: " label at the end of + /// the output for the failure message. + final Iterable<String>? which; + + Rejection _fillActual(Object? value) => actual.isNotEmpty + ? this + : Rejection(actual: literal(value), which: which); + + Rejection({this.actual = const [], this.which}); +}
diff --git a/pkgs/checks/lib/src/collection_equality.dart b/pkgs/checks/lib/src/collection_equality.dart new file mode 100644 index 0000000..da6064a --- /dev/null +++ b/pkgs/checks/lib/src/collection_equality.dart
@@ -0,0 +1,431 @@ +// Copyright (c) 2023, 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:collection'; + +import '../context.dart'; + +/// Returns a descriptive `which` for a rejection if the elements of [actual] +/// are unequal to the elements of [expected]. +/// +/// {@template deep_collection_equals} +/// Elements, keys, or values within [expected] which are a collections are +/// deeply compared for equality with a collection in the same position within +/// [actual]. Elements which are collection types are not compared with the +/// native identity based equality or custom equality operator overrides. +/// +/// Elements, keys, or values within [expected] which are [Condition] callbacks +/// are run against the value in the same position within [actual]. +/// Condition callbacks must take a `Subject<Object?>` or `Subject<dynamic>` and +/// may not use a more specific generic. +/// Use `(Subject<Object?> s) => s.isA<Type>()` to check expectations for +/// specific element types. +/// Note also that the argument type `Subject<Object?>` cannot be inferred and +/// must be explicit in the function definition. +/// +/// Elements and values within [expected] which are any other type are compared +/// using `operator ==` equality. +/// +/// Deep equality checks for [Map] instances depend on the structure of the +/// expected map. +/// If all keys of the expectation are non-collection and non-condition values +/// the keys are compared through the map instances with [Map.containsKey]. +/// +/// If any key in the expectation is a `Condition` or a collection type the map +/// will be compared as a [Set] of entries where both the key and value must +/// match. +/// +/// Comparing sets or maps with complex keys has a runtime which is polynomial +/// on the the size of those collections. +/// These comparisons do not use [Set.contains] or [Map.containsKey], +/// and there will not be runtime benefits from hashing. +/// Custom collection behavior of `contains` is ignored. +/// For example, it is not possible to distinguish between a `Set` and a +/// `Set.identity`. +/// +/// Collections may be nested to a maximum depth of 1000. Recursive collections +/// are not allowed. +/// {@endtemplate} +Iterable<String>? deepCollectionEquals(Object actual, Object expected) { + try { + return _deepCollectionEquals(actual, expected, 0); + } on _ExceededDepthError { + return ['exceeds the depth limit of $_maxDepth']; + } +} + +const _maxDepth = 1000; + +class _ExceededDepthError extends Error {} + +Iterable<String>? _deepCollectionEquals( + Object actual, Object expected, int depth) { + assert(actual is Iterable || actual is Map); + assert(expected is Iterable || expected is Map); + + final queue = Queue.of([_Search(_Path.root(), actual, expected, depth)]); + while (queue.isNotEmpty) { + final toCheck = queue.removeFirst(); + final currentActual = toCheck.actual; + final currentExpected = toCheck.expected; + final path = toCheck.path; + final currentDepth = toCheck.depth; + Iterable<String>? rejectionWhich; + if (currentExpected is Set) { + rejectionWhich = _findSetDifference( + currentActual, currentExpected, path, currentDepth); + } else if (currentExpected is Iterable) { + rejectionWhich = _findIterableDifference( + currentActual, currentExpected, path, queue, currentDepth); + } else { + currentExpected as Map; + rejectionWhich = _findMapDifference( + currentActual, currentExpected, path, queue, currentDepth); + } + if (rejectionWhich != null) return rejectionWhich; + } + return null; +} + +List<String>? _findIterableDifference(Object? actual, + Iterable<Object?> expected, _Path path, Queue<_Search> queue, int depth) { + if (actual is! Iterable) { + return ['${path}is not an Iterable']; + } + var actualIterator = actual.iterator; + var expectedIterator = expected.iterator; + for (var index = 0;; index++) { + var actualNext = actualIterator.moveNext(); + var expectedNext = expectedIterator.moveNext(); + if (!expectedNext && !actualNext) break; + if (!expectedNext) { + return [ + '${path}has more elements than expected', + 'expected an iterable with $index element(s)' + ]; + } + if (!actualNext) { + return [ + '${path}has too few elements', + 'expected an iterable with at least ${index + 1} element(s)' + ]; + } + final difference = _compareValue(actualIterator.current, + expectedIterator.current, path, index, queue, depth); + if (difference != null) return difference; + } + return null; +} + +List<String>? _compareValue(Object? actualValue, Object? expectedValue, + _Path path, Object? pathAppend, Queue<_Search> queue, int depth) { + if (expectedValue is Iterable || expectedValue is Map) { + if (depth + 1 > _maxDepth) throw _ExceededDepthError(); + queue.addLast(_Search( + path.append(pathAppend), actualValue, expectedValue, depth + 1)); + } else if (expectedValue is Condition) { + final failure = softCheck(actualValue, expectedValue); + if (failure != null) { + final which = failure.rejection.which; + return [ + 'has an element ${path.append(pathAppend)}that:', + ...indent(failure.detail.actual.skip(1)), + ...indent(prefixFirst('Actual: ', failure.rejection.actual), + failure.detail.depth + 1), + if (which != null) + ...indent(prefixFirst('which ', which), failure.detail.depth + 1) + ]; + } + } else { + if (actualValue != expectedValue) { + return [ + ...prefixFirst('${path.append(pathAppend)}is ', literal(actualValue)), + ...prefixFirst('which does not equal ', literal(expectedValue)) + ]; + } + } + return null; +} + +bool _elementMatches(Object? actual, Object? expected, int depth) { + if (expected == null) return actual == null; + if (expected is Iterable || expected is Map) { + if (++depth > _maxDepth) throw _ExceededDepthError(); + return actual != null && + _deepCollectionEquals(actual, expected, depth) == null; + } + if (expected is Condition) { + return softCheck(actual, expected) == null; + } + return expected == actual; +} + +Iterable<String>? _findSetDifference( + Object? actual, Set<Object?> expected, _Path path, int depth) { + if (actual is! Set) { + return ['${path}is not a Set']; + } + return unorderedCompare( + actual, + expected, + (actual, expected) => _elementMatches(actual, expected, depth), + (expected, _, count) => [ + ...prefixFirst('${path}has no element to match ', literal(expected)), + if (count > 1) 'or ${count - 1} other elements', + ], + (actual, _, count) => [ + ...prefixFirst('${path}has an unexpected element ', literal(actual)), + if (count > 1) 'and ${count - 1} other unexpected elements', + ], + ); +} + +Iterable<String>? _findMapDifference( + Object? actual, + Map<Object?, Object?> expected, + _Path path, + Queue<_Search> queue, + int depth) { + if (actual is! Map) { + return ['${path}is not a Map']; + } + if (expected.keys + .any((key) => key is Condition || key is Iterable || key is Map)) { + return _findAmbiguousMapDifference(actual, expected, path, depth); + } else { + return _findUnambiguousMapDifference(actual, expected, path, queue, depth); + } +} + +Iterable<String> _describeEntry(MapEntry<Object?, Object?> entry) { + final key = literal(entry.key); + final value = literal(entry.value); + return [ + ...key.take(key.length - 1), + '${key.last}: ${value.first}', + ...value.skip(1) + ]; +} + +/// Returns a description of a difference found between [actual] and [expected] +/// when [expected] has only direct key values and there is a 1:1 mapping +/// between an expected value and a checked value in the map. +Iterable<String>? _findUnambiguousMapDifference( + Map<Object?, Object?> actual, + Map<Object?, Object?> expected, + _Path path, + Queue<_Search> queue, + int depth) { + for (final entry in expected.entries) { + assert(entry.key is! Condition); + assert(entry.key is! Iterable); + assert(entry.key is! Map); + if (!actual.containsKey(entry.key)) { + return prefixFirst( + '${path}has no key matching expected entry ', _describeEntry(entry)); + } + final difference = _compareValue( + actual[entry.key], entry.value, path, entry.key, queue, depth); + if (difference != null) return difference; + } + for (final entry in actual.entries) { + if (!expected.containsKey(entry.key)) { + return prefixFirst( + '${path}has an unexpected key for entry ', _describeEntry(entry)); + } + } + return null; +} + +Iterable<String>? _findAmbiguousMapDifference(Map<Object?, Object?> actual, + Map<Object?, Object?> expected, _Path path, int depth) => + unorderedCompare( + actual.entries, + expected.entries, + (actual, expected) => + _elementMatches(actual.key, expected.key, depth) && + _elementMatches(actual.value, expected.value, depth), + (expectedEntry, _, count) => [ + ...prefixFirst( + '${path}has no entry to match ', _describeEntry(expectedEntry)), + if (count > 1) 'or ${count - 1} other entries', + ], + (actualEntry, _, count) => [ + ...prefixFirst( + '${path}has unexpected entry ', _describeEntry(actualEntry)), + if (count > 1) 'and ${count - 1} other unexpected entries', + ], + ); + +class _Path { + final _Path? parent; + final Object? index; + _Path._(this.parent, this.index); + _Path.root() + : parent = null, + index = ''; + _Path append(Object? index) => _Path._(this, index); + + @override + String toString() { + if (parent == null && index == '') return ''; + final stack = Queue.of([this]); + var current = parent; + while (current?.parent != null) { + stack.addLast(current!); + current = current.parent; + } + final result = StringBuffer('at '); + while (stack.isNotEmpty) { + result.write('['); + result.write(literal(stack.removeLast().index).join(r'\n')); + result.write(']'); + } + result.write(' '); + return result.toString(); + } +} + +class _Search { + final _Path path; + final Object? actual; + final Object? expected; + final int depth; + _Search(this.path, this.actual, this.expected, this.depth); +} + +/// Returns the `which` for a Rejection if there is no pairing between the +/// elements of [actual] and [expected] using [elementsEqual]. +/// +/// If there are unmatched expected elements - either actual was too short, or +/// has mismatched elements - returns a rejection reason from calling +/// [unmatchedExpected] with an expected value that could not be paired, it's +/// index, and the count of unmatched elements. +/// +/// Otherwise, if there are unmatched actual elements - actual was too long - +/// returns a rejection reason from calling [unmatchedActual] with an actual +/// value that could not be paired, it's index, and the count of unmatched +/// elements. +/// +/// Runtime is at least `O(|actual||expected|)`, and for collections with many +/// elements which compare as equal the runtime can reach +/// `O((|actual| + |expected|)^2.5)`. +Iterable<String>? unorderedCompare<T, E>( + Iterable<T> actual, + Iterable<E> expected, + bool Function(T, E) elementsEqual, + Iterable<String> Function(E, int index, int count) unmatchedExpected, + Iterable<String> Function(T, int index, int count) unmatchedActual) { + final indexedExpected = expected.toList(); + final indexedActual = actual.toList(); + final adjacency = <List<int>>[]; + for (var i = 0; i < indexedExpected.length; i++) { + final expectedElement = indexedExpected[i]; + final pairs = [ + for (var j = 0; j < indexedActual.length; j++) + if (elementsEqual(indexedActual[j], expectedElement)) j + ]; + adjacency.add(pairs); + } + final unpaired = _findUnpaired(adjacency, indexedActual.length); + if (unpaired.first.isNotEmpty) { + final firstUnmatched = indexedExpected[unpaired.first.first]; + return unmatchedExpected( + firstUnmatched, unpaired.first.first, unpaired.first.length); + } + if (unpaired.last.isNotEmpty) { + final firstUnmatched = indexedActual[unpaired.last.first]; + return unmatchedActual( + firstUnmatched, unpaired.last.first, unpaired.last.length); + } + return null; +} + +/// Returns the indices which are unmatched in an optimal pairing in the +/// bipartite graph represented by [adjacency]. +/// +/// Vertices are represented as integers. The two sets of vertices (`U` and `V`) +/// in the biparte graph are represented as: +/// - `U` - the indices of [adjacency]. +/// - `V` - values smaller than [rightVertexCount]. +/// +/// An edge from `U[n]` to `V[m]` is represented by the value `m` being present +/// in the list at index `n`. +/// The largest value within any list in [adjacency] must be smaller than +/// [rightVertexCount]. +/// +/// Returns a List with two values, the unpaired values of `U` and `V` in the +/// maximum-caridnality matching betweeen them. +/// +/// If there is a perfect pairing, the returned lists will both be empty. +/// +/// Uses the Hopcroft–Karp algorithm based on pseudocode from +/// https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm +List<List<int>> _findUnpaired(List<List<int>> adjacency, int rightVertexCount) { + final leftLength = adjacency.length; + final rightLength = rightVertexCount; + // The last index represents a "dummy vertex" + final distances = List<num>.filled(leftLength + 1, double.infinity); + // Initially everything is paired with the "dummy vertex" of the opposite set + final leftPairs = List.filled(leftLength, rightLength); + final rightPairs = List.filled(rightLength, leftLength); + + bool bfs() { + final queue = Queue<int>(); + for (var leftIndex = 0; leftIndex < leftLength; leftIndex++) { + if (leftPairs[leftIndex] == rightLength) { + distances[leftIndex] = 0; + queue.add(leftIndex); + } else { + distances[leftIndex] = double.infinity; + } + } + distances.last = double.infinity; + while (queue.isNotEmpty) { + final current = queue.removeFirst(); + if (distances[current] < distances[leftLength]) { + for (final rightIndex in adjacency[current]) { + if (distances[rightPairs[rightIndex]].isInfinite) { + distances[rightPairs[rightIndex]] = distances[current] + 1; + queue.addLast(rightPairs[rightIndex]); + } + } + } + } + return !distances.last.isInfinite; + } + + bool dfs(int leftIndex) { + if (leftIndex == leftLength) return true; + for (final rightIndex in adjacency[leftIndex]) { + if (distances[rightPairs[rightIndex]] == distances[leftIndex] + 1) { + if (dfs(rightPairs[rightIndex])) { + leftPairs[leftIndex] = rightIndex; + rightPairs[rightIndex] = leftIndex; + return true; + } + } + } + distances[leftIndex] = double.infinity; + return false; + } + + while (bfs()) { + for (var leftIndex = 0; leftIndex < leftLength; leftIndex++) { + if (leftPairs[leftIndex] == rightLength) { + dfs(leftIndex); + } + } + } + return [ + [ + for (int i = 0; i < leftLength; i++) + if (leftPairs[i] == rightLength) i + ], + [ + for (int i = 0; i < rightLength; i++) + if (rightPairs[i] == leftLength) i + ] + ]; +}
diff --git a/pkgs/checks/lib/src/describe.dart b/pkgs/checks/lib/src/describe.dart new file mode 100644 index 0000000..f05a164 --- /dev/null +++ b/pkgs/checks/lib/src/describe.dart
@@ -0,0 +1,163 @@ +// 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:convert'; + +import 'checks.dart' show Condition, describe; + +/// Returns a pretty-printed representation of [object]. +/// +/// When possible, lines will be kept under [_maxLineLength]. This isn't +/// guaranteed, since individual objects may have string representations that +/// are too long, but most lines will be less than [_maxLineLength] long. +/// +/// [Iterable]s and [Map]s will only print their first [_maxItems] elements or +/// key/value pairs, respectively. +Iterable<String> literal(Object? object) => _prettyPrint(object, 0, {}, true); + +const _maxLineLength = 80; +const _maxItems = 25; + +Iterable<String> _prettyPrint( + Object? object, int indentSize, Set<Object?> seen, bool isTopLevel) { + if (seen.contains(object)) return ['(recursive)']; + seen = seen.union({object}); + Iterable<String> prettyPrintNested(Object? child) => + _prettyPrint(child, indentSize + 2, seen, false); + + if (object is Iterable) { + String open, close; + if (object is List) { + open = '['; + close = ']'; + } else if (object is Set) { + open = '{'; + close = '}'; + } else { + open = '('; + close = ')'; + } + final elements = object.map(prettyPrintNested).toList(); + return _prettyPrintCollection( + open, close, elements, _maxLineLength - indentSize); + } else if (object is Map) { + final entries = object.entries.map((entry) { + final key = prettyPrintNested(entry.key); + final value = prettyPrintNested(entry.value); + return [ + ...key.take(key.length - 1), + '${key.last}: ${value.first}', + ...value.skip(1) + ]; + }).toList(); + return _prettyPrintCollection( + '{', '}', entries, _maxLineLength - indentSize); + } else if (object is String) { + if (object.isEmpty) return ["''"]; + final escaped = const LineSplitter() + .convert(object) + .map(escape) + .map((line) => line.replaceAll("'", r"\'")) + .toList(); + return prefixFirst("'", postfixLast("'", escaped)); + } else if (object is Condition<Never>) { + return ['<A value that:', ...postfixLast('>', describe(object))]; + } else { + final value = const LineSplitter().convert(object.toString()); + return isTopLevel ? prefixFirst('<', postfixLast('>', value)) : value; + } +} + +Iterable<String> _prettyPrintCollection( + String open, String close, List<Iterable<String>> elements, int maxLength) { + if (elements.length > _maxItems) { + elements.replaceRange(_maxItems - 1, elements.length, [ + ['...'] + ]); + } + if (elements.every((e) => e.length == 1)) { + final singleLine = '$open${elements.map((e) => e.single).join(', ')}$close'; + if (singleLine.length <= maxLength) { + return [singleLine]; + } + } + if (elements.length == 1) { + return prefixFirst(open, postfixLast(close, elements.single)); + } + return [ + ...prefixFirst(open, postfixLast(',', elements.first)), + for (var element in elements.skip(1).take(elements.length - 2)) + ...postfixLast(',', element), + ...postfixLast(close, elements.last), + ]; +} + +Iterable<String> indent(Iterable<String> lines, [int depth = 1]) { + final indent = ' ' * depth; + return lines.map((line) => '$indent$line'); +} + +/// Prepends [prefix] to the first line of [lines]. +/// +/// If [lines] is empty, the result will be as well. The prefix will not be +/// returned for an empty input. +Iterable<String> prefixFirst(String prefix, Iterable<String> lines) sync* { + var isFirst = true; + for (var line in lines) { + if (isFirst) { + yield '$prefix$line'; + isFirst = false; + } else { + yield line; + } + } +} + +/// Append [postfix] to the last line of [lines]. +/// +/// If [lines] is empty, the result will be as well. The postfix will not be +/// returned for an empty input. +Iterable<String> postfixLast(String postfix, Iterable<String> lines) sync* { + var iterator = lines.iterator; + var hasNext = iterator.moveNext(); + while (hasNext) { + final line = iterator.current; + hasNext = iterator.moveNext(); + yield hasNext ? line : '$line$postfix'; + } +} + +/// Returns [output] with all whitespace characters represented as their escape +/// sequences. +/// +/// Backslash characters are escaped as `\\` +String escape(String output) { + output = output.replaceAll('\\', r'\\'); + return output.replaceAllMapped(_escapeRegExp, (match) { + var mapped = _escapeMap[match[0]]; + if (mapped != null) return mapped; + return _hexLiteral(match[0]!); + }); +} + +/// A [RegExp] that matches whitespace characters that should be escaped. +final _escapeRegExp = RegExp( + '[\\x00-\\x07\\x0E-\\x1F${_escapeMap.keys.map(_hexLiteral).join()}]'); + +/// A [Map] between whitespace characters and their escape sequences. +const _escapeMap = { + '\n': r'\n', + '\r': r'\r', + '\f': r'\f', + '\b': r'\b', + '\t': r'\t', + '\v': r'\v', + '\x7F': r'\x7F', // delete +}; + +/// Given single-character string, return the hex-escaped equivalent. +String _hexLiteral(String input) { + var rune = input.runes.single; + return r'\x' + rune.toRadixString(16).toUpperCase().padLeft(2, '0'); +}
diff --git a/pkgs/checks/lib/src/extensions/async.dart b/pkgs/checks/lib/src/extensions/async.dart new file mode 100644 index 0000000..9d9e85a --- /dev/null +++ b/pkgs/checks/lib/src/extensions/async.dart
@@ -0,0 +1,476 @@ +// 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:async'; +import 'dart:convert'; + +import 'package:async/async.dart'; + +import '../../context.dart'; + +extension FutureChecks<T> on Subject<Future<T>> { + /// Expects that the `Future` completes to a value without throwing. + /// + /// Fails if the future completes as an error. + /// + /// Pass [completionCondition] to check expectations on the completion result. + /// + /// The returned future will complete when the subject future has completed, + /// and [completionCondition] has optionally been checked. + Future<void> completes([AsyncCondition<T>? completionCondition]) async { + await context.nestAsync<T>(() => ['completes to a value'], (actual) async { + try { + return Extracted.value(await actual); + } catch (e, st) { + return Extracted.rejection(actual: [ + 'a future that completes as an error' + ], which: [ + ...prefixFirst('threw ', postfixLast(' at:', literal(e))), + ...const LineSplitter().convert(st.toString()) + ]); + } + }, completionCondition); + } + + /// Expects that the `Future` never completes as a value or an error. + /// + /// Immediately returns and does not cause the test to remain running if it + /// ends. + /// If the future completes at any time, raises a test failure. This may + /// happen after the test has already appeared to succeed. + /// + /// Not compatible with [softCheck] or [softCheckAsync] since there is no + /// concrete end point where this condition has definitely succeeded. + void doesNotComplete() { + context.expectUnawaited(() => ['does not complete'], (actual, reject) { + unawaited(actual.then((r) { + reject(Rejection( + actual: prefixFirst('a future that completed to ', literal(r)))); + }, onError: (Object e, StackTrace st) { + reject(Rejection(actual: [ + 'a future that completed as an error:' + ], which: [ + ...prefixFirst('threw ', literal(e)), + ...const LineSplitter().convert(st.toString()) + ])); + })); + }); + } + + /// Expects that the `Future` completes as an error. + /// + /// Fails if the future completes to a value. + /// + /// Pass [errorCondition] to check expectations on the error thrown by the + /// future. + /// + /// The returned future will complete when the subject future has completed, + /// and [errorCondition] has optionally been checked. + Future<void> throws<E extends Object>( + [AsyncCondition<E>? errorCondition]) async { + await context.nestAsync<E>( + () => ['completes to an error${E == Object ? '' : ' of type $E'}'], + (actual) async { + try { + return Extracted.rejection( + actual: prefixFirst('completed to ', literal(await actual)), + which: ['did not throw']); + } on E catch (e) { + return Extracted.value(e); + } catch (e, st) { + return Extracted.rejection( + actual: prefixFirst('completed to error ', literal(e)), + which: [ + 'threw an exception that is not a $E at:', + ...const LineSplitter().convert(st.toString()) + ]); + } + }, errorCondition); + } +} + +/// Expectations on a [StreamQueue]. +/// +/// Streams should be wrapped in user test code so that any reuse of the same +/// Stream, and the full stream lifecycle, is explicit. +extension StreamChecks<T> on Subject<StreamQueue<T>> { + /// Calls [Context.expectAsync] and wraps [predicate] with a transaction. + /// + /// The transaction is committed if the check passes, or rejected if it fails. + Future<void> _expectAsync(Iterable<String> Function() clause, + FutureOr<Rejection?> Function(StreamQueue<T>) predicate) => + context.expectAsync(clause, (actual) async { + final transaction = actual.startTransaction(); + final copy = transaction.newQueue(); + final result = await predicate(copy); + if (result == null) { + transaction.commit(copy); + } else { + transaction.reject(); + } + return result; + }); + + /// Expect that the `Stream` emits a value without first emitting an error. + /// + /// Fails if the stream emits an error instead of a value, or closes without + /// emitting a value. + /// + /// If an error is emitted the queue will be left in its original state, the + /// error will not be consumed. + /// If an event is emitted, it will be consumed from the queue. + /// + /// Pass [emittedCondition] to check expectations on the value emitted by the + /// stream. + /// + /// The returned future will complete when the stream has emitted, errored, or + /// ended, and the [emittedCondition] has optionally been checked. + Future<void> emits([AsyncCondition<T>? emittedCondition]) async { + await context.nestAsync<T>(() => ['emits a value'], (actual) async { + if (!await actual.hasNext) { + return Extracted.rejection( + actual: ['a stream'], + which: ['closed without emitting enough values']); + } + try { + await actual.peek; + return Extracted.value(await actual.next); + } catch (e, st) { + return Extracted.rejection( + actual: prefixFirst('a stream with error ', literal(e)), + which: [ + 'emitted an error instead of a value at:', + ...const LineSplitter().convert(st.toString()) + ]); + } + }, emittedCondition); + } + + /// Expects that the stream emits an error of type [E]. + /// + /// Fails if the stream emits any value. + /// Fails if the stream emits an error with an incorrect type. + /// Fails if the stream closes without emitting an error. + /// + /// If an event is emitted the queue will be left in its original state, the + /// event will not be consumed. + /// If an error is emitted, it will be consumed from the queue. + /// + /// Pass [errorCondition] to check expectations on the error emitted by the + /// stream. + /// + /// The returned future will complete when the stream has emitted, errored, or + /// ended, and the [errorCondition] has optionally been checked. + Future<void> emitsError<E extends Object>( + [AsyncCondition<E>? errorCondition]) async { + await context.nestAsync<E>( + () => ['emits an error${E == Object ? '' : ' of type $E'}'], + (actual) async { + if (!await actual.hasNext) { + return Extracted.rejection( + actual: ['a stream'], + which: ['closed without emitting an expected error']); + } + try { + final value = await actual.peek; + return Extracted.rejection( + actual: prefixFirst('a stream emitting value ', literal(value)), + which: ['closed without emitting an error']); + } on E catch (e) { + await actual.next.then<void>((_) {}, onError: (_) {}); + return Extracted.value(e); + } catch (e, st) { + return Extracted.rejection( + actual: prefixFirst('a stream with error ', literal(e)), + which: [ + 'emitted an error which is not $E at:', + ...const LineSplitter().convert(st.toString()) + ]); + } + }, errorCondition); + } + + /// Expects that the `Stream` emits any number of events before emitting an + /// event that satisfies [condition]. + /// + /// Returns a `Future` that completes after the stream has emitted an event + /// that satisfies [condition]. + /// + /// Fails if the stream emits an error or closes before emitting a matching + /// event. + /// + /// If this expectation fails, the source queue will be left in its original + /// state. + /// If this expectation succeeds, consumes the matching event and all prior + /// events. + Future<void> emitsThrough(AsyncCondition<T> condition) async { + await _expectAsync( + () => [ + 'emits any values then emits a value that:', + ...describe(condition) + ], (actual) async { + var count = 0; + while (await actual.hasNext) { + if (softCheck(await actual.next, condition) == null) { + return null; + } + count++; + } + return Rejection( + actual: ['a stream'], + which: ['ended after emitting $count elements with none matching']); + }); + } + + /// Expects that the stream satisfies each condition in [conditions] serially. + /// + /// Waits for each condition to be satisfied or rejected before checking the + /// next. Subsequent conditions will not see any events consumed by earlier + /// conditions. + /// + /// ```dart + /// await check(someStream).withQueue.inOrder([ + /// (s) => s.emits((e) => e.equals(0)), + /// (s) => s.emits((e) => e.equals(1)), + /// ]); + /// ``` + /// + /// If this expectation fails, the source queue will be left in its original + /// state. + /// If this expectation succeeds, consumes as many events from the source + /// stream as are consumed by all the conditions. + Future<void> inOrder( + Iterable<AsyncCondition<StreamQueue<T>>> conditions) async { + conditions = conditions.toList(); + final descriptions = <String>[]; + await _expectAsync( + () => descriptions.isEmpty + ? ['satisfies ${conditions.length} conditions in order'] + : descriptions, (actual) async { + var satisfiedCount = 0; + for (var condition in conditions) { + descriptions.addAll(await describeAsync(condition)); + final failure = await softCheckAsync(actual, condition); + if (failure != null) { + final which = failure.rejection.which; + return Rejection(actual: [ + 'a stream' + ], which: [ + if (satisfiedCount > 0) 'satisfied $satisfiedCount conditions then', + 'failed to satisfy the condition at index $satisfiedCount', + if (failure.detail.depth > 0) ...[ + 'because it:', + ...indent( + failure.detail.actual.skip(1), failure.detail.depth - 1), + ...indent(prefixFirst('Actual: ', failure.rejection.actual), + failure.detail.depth), + if (which != null) + ...indent(prefixFirst('Which: ', which), failure.detail.depth), + ] else ...[ + if (which != null) ...prefixFirst('because it ', which), + ], + ]); + } + satisfiedCount++; + } + return null; + }); + } + + /// Expects that the stream statisfies at least one condition from + /// [conditions]. + /// + /// If this expectation fails, the source queue will be left in its original + /// state. + /// If this expectation succeeds, consumes the same events from the source + /// queue as the satisfied condition. If multiple conditions are satisfied, + /// chooses the condition which consumed the most events. + Future<void> anyOf( + Iterable<AsyncCondition<StreamQueue<T>>> conditions) async { + conditions = conditions.toList(); + if (conditions.isEmpty) { + throw ArgumentError('conditions may not be empty'); + } + final descriptions = <Iterable<String>>[]; + await context.expectAsync( + () => descriptions.isEmpty + ? ['satisfies any of ${conditions.length} conditions'] + : [ + 'satisfies one of:', + for (var i = 0; i < descriptions.length; i++) ...[ + ...descriptions[i], + if (i < descriptions.length - 1) 'or,' + ] + ], (actual) async { + final transaction = actual.startTransaction(); + StreamQueue<T>? longestAccepted; + final descriptionFuture = Future.wait(conditions.map(describeAsync)); + final failures = await Future.wait(conditions.map((condition) async { + final copy = transaction.newQueue(); + final failure = await softCheckAsync(copy, condition); + if (failure == null && + (longestAccepted == null || + copy.eventsDispatched > longestAccepted!.eventsDispatched)) { + longestAccepted = copy; + } + return failure; + })); + descriptions.addAll(await descriptionFuture); + if (longestAccepted != null) { + transaction.commit(longestAccepted!); + return null; + } + transaction.reject(); + Iterable<String> failureDetails(int index, CheckFailure? failure) { + final actual = failure!.rejection.actual; + final which = failure.rejection.which; + final detail = failure.detail; + final failed = 'failed the condition at index $index'; + if (detail.depth > 0) { + return [ + '$failed because it:', + ...indent(detail.actual.skip(1), detail.depth - 1), + ...indent(prefixFirst('Actual: ', actual), detail.depth), + if (which != null) + ...indent(prefixFirst('Which: ', which), detail.depth), + ]; + } else { + return [ + if (which == null) + failed + else ...[ + '$failed because it:', + ...indent(which), + ], + ]; + } + } + + return Rejection(actual: [ + 'a stream' + ], which: [ + 'failed to satisfy any condition', + for (var i = 0; i < failures.length; i++) + ...failureDetails(i, failures[i]), + ]); + }); + } + + /// Expects that the stream closes without emitting any event that satisfies + /// [condition]. + /// + /// Returns a `Future` that completes after the stream has closed. + /// + /// Fails if the stream emits any even that satisfies [condition]. + /// + /// If this expectation fails, the source queue will be left in its original + /// state. + /// If this expectation succeeds, consumes all the events that did not satisfy + /// [condition] until the end of the stream. + Future<void> neverEmits(AsyncCondition<T> condition) async { + await _expectAsync( + () => ['never emits a value that:', ...describe(condition)], + (actual) async { + var count = 0; + await for (var emitted in actual.rest) { + if (softCheck(emitted, condition) == null) { + return Rejection(actual: [ + 'a stream' + ], which: [ + ...prefixFirst('emitted ', literal(emitted)), + if (count > 0) 'following $count other items' + ]); + } + count++; + } + return null; + }); + } + + /// Optionally consumes an event that matches [condition] from the stream. + /// + /// This expectation never fails. + /// + /// If a non-matching event is emitted, no events are consumed. + /// If a matching event is emitted, that event is consumed. + Future<void> mayEmit(AsyncCondition<T> condition) async { + await context + .expectAsync(() => ['may emit a value that:', ...describe(condition)], + (actual) async { + if (!await actual.hasNext) return null; + try { + final value = await actual.peek; + if (softCheck(value, condition) == null) { + await actual.next; + } + } catch (_) { + // Ignore an emitted error - it does not match he event. + } + return null; + }); + } + + /// Optionally consumes events that match [condition] from the stream. + /// + /// This expectation never fails. + /// + /// Consumes matching events until one of the following happens: + /// - A non-matching event is emitted. + /// - An error is emitted. + /// - The stream closes. + Future<void> mayEmitMultiple(AsyncCondition<T> condition) async { + await context + .expectAsync(() => ['may emit a value that:', ...describe(condition)], + (actual) async { + while (await actual.hasNext) { + try { + final value = await actual.peek; + if (softCheck(value, condition) == null) { + await actual.next; + } else { + return null; + } + } catch (_) { + return null; + } + } + return null; + }); + } + + /// Expects that the stream closes without emitting any events or errors. + /// + /// If this expectation fails, the source queue will be left in its original + /// state, the event or error that caused it to fail will not be consumed. + Future<void> isDone() async { + await _expectAsync(() => ['is done'], (actual) async { + if (!await actual.hasNext) return null; + try { + return Rejection( + actual: ['a stream'], + which: prefixFirst( + 'emitted an unexpected value: ', literal(await actual.next))); + } catch (e, st) { + return Rejection(actual: [ + 'a stream' + ], which: [ + ...prefixFirst('emitted an unexpected error: ', literal(e)), + ...const LineSplitter().convert(st.toString()) + ]); + } + }); + } +} + +extension WithQueueExtension<T> on Subject<Stream<T>> { + /// Wrap the stream in a [StreamQueue] to allow using checks from + /// [StreamChecks]. + /// + /// Stream expectations operate on a queue, instead of directly on the stream, + /// so that they can support conditional expectations and check multiple + /// possibilities from the same point in the stream. + Subject<StreamQueue<T>> get withQueue => + context.nest(() => [], (actual) => Extracted.value(StreamQueue(actual)), + atSameLevel: true); +}
diff --git a/pkgs/checks/lib/src/extensions/core.dart b/pkgs/checks/lib/src/extensions/core.dart new file mode 100644 index 0000000..957e175 --- /dev/null +++ b/pkgs/checks/lib/src/extensions/core.dart
@@ -0,0 +1,184 @@ +// 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:convert'; + +import 'package:meta/meta.dart' as meta; + +import '../../context.dart'; + +extension CoreChecks<T> on Subject<T> { + /// Extracts a property of the value for further expectations. + /// + /// Sets up a clause that the value "has [name] that:" followed by any + /// expectations applied to the returned [Subject]. + @meta.useResult + Subject<R> has<R>(R Function(T) extract, String name) { + return context.nest(() => ['has $name'], (value) { + try { + return Extracted.value(extract(value)); + } catch (e, st) { + return Extracted.rejection(which: [ + ...prefixFirst('threw while trying to read $name: ', literal(e)), + ...const LineSplitter().convert(st.toString()) + ]); + } + }); + } + + /// Applies the expectations invoked in [condition] to this subject. + /// + /// Use this method when it would otherwise not be possible to check multiple + /// expectations for this subject due to cascade notation already being used + /// in a way that would conflict. + /// + /// ``` + /// check(something) + /// ..has((s) => s.foo, 'foo').equals(expectedFoo) + /// ..has((s) => s.bar, 'bar').which((b) => b + /// ..isLessThan(10) + /// ..isGreaterThan(0)); + /// ``` + void which(Condition<T> condition) => condition(this); + + /// Check that the expectations invoked in [condition] are not satisfied by + /// this value. + /// + /// Asynchronous expectations are not allowed in [condition]. + void not(Condition<T> condition) { + context.expect( + () => ['is not a value that:', ...indent(describe(condition))], + (actual) { + if (softCheck(actual, condition) != null) return null; + return Rejection( + which: ['is a value that: ', ...indent(describe(condition))], + ); + }, + ); + } + + /// Expects that the value satisfies the expectations invoked in at least one + /// condition from [conditions]. + /// + /// Asynchronous expectations are not allowed in [conditions]. + void anyOf(Iterable<Condition<T>> conditions) { + context.expect( + () => prefixFirst('matches any condition in ', literal(conditions)), + (actual) { + for (final condition in conditions) { + if (softCheck(actual, condition) == null) return null; + } + return Rejection(which: ['did not match any condition']); + }); + } + + /// Expects that the value is assignable to type [T]. + /// + /// If the value is a [T], returns a [Subject] for further expectations. + Subject<R> isA<R>() { + return context.nest<R>(() => ['is a $R'], (actual) { + if (actual is! R) { + return Extracted.rejection(which: ['Is a ${actual.runtimeType}']); + } + return Extracted.value(actual); + }, atSameLevel: true); + } + + /// Expects that the value is equal to [other] according to [operator ==]. + void equals(T other) { + context.expect(() => prefixFirst('equals ', literal(other)), (actual) { + if (actual == other) return null; + return Rejection(which: ['are not equal']); + }); + } + + /// Expects that the value is [identical] to [other]. + void identicalTo(T other) { + context.expect(() => prefixFirst('is identical to ', literal(other)), + (actual) { + if (identical(actual, other)) return null; + return Rejection(which: ['is not identical']); + }); + } +} + +extension BoolChecks on Subject<bool> { + void isTrue() { + context.expect( + () => ['is true'], + (actual) => actual + ? null // force coverage + : Rejection(), + ); + } + + void isFalse() { + context.expect( + () => ['is false'], + (actual) => !actual + ? null // force coverage + : Rejection(), + ); + } +} + +extension NullableChecks<T> on Subject<T?> { + Subject<T> isNotNull() { + return context.nest<T>(() => ['is not null'], (actual) { + if (actual == null) return Extracted.rejection(); + return Extracted.value(actual); + }, atSameLevel: true); + } + + void isNull() { + context.expect(() => const ['is null'], (actual) { + if (actual != null) return Rejection(); + return null; + }); + } +} + +extension ComparableChecks<T> on Subject<Comparable<T>> { + /// Expects that this value is greater than [other]. + void isGreaterThan(T other) { + context.expect(() => prefixFirst('is greater than ', literal(other)), + (actual) { + if (actual.compareTo(other) > 0) return null; + return Rejection( + which: prefixFirst('is not greater than ', literal(other))); + }); + } + + /// Expects that this value is greater than or equal to [other]. + void isGreaterOrEqual(T other) { + context.expect( + () => prefixFirst('is greater than or equal to ', literal(other)), + (actual) { + if (actual.compareTo(other) >= 0) return null; + return Rejection( + which: + prefixFirst('is not greater than or equal to ', literal(other))); + }); + } + + /// Expects that this value is less than [other]. + void isLessThan(T other) { + context.expect(() => prefixFirst('is less than ', literal(other)), + (actual) { + if (actual.compareTo(other) < 0) return null; + return Rejection(which: prefixFirst('is not less than ', literal(other))); + }); + } + + /// Expects that this value is less than or equal to [other]. + void isLessOrEqual(T other) { + context + .expect(() => prefixFirst('is less than or equal to ', literal(other)), + (actual) { + if (actual.compareTo(other) <= 0) return null; + return Rejection( + which: prefixFirst('is not less than or equal to ', literal(other))); + }); + } +}
diff --git a/pkgs/checks/lib/src/extensions/function.dart b/pkgs/checks/lib/src/extensions/function.dart new file mode 100644 index 0000000..300941c --- /dev/null +++ b/pkgs/checks/lib/src/extensions/function.dart
@@ -0,0 +1,56 @@ +// 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 '../../context.dart'; + +extension FunctionChecks<T> on Subject<T Function()> { + /// Expects that a function throws synchronously when it is called. + /// + /// If the function synchronously throws a value of type [E], return a + /// [Subject] to check further expectations on the error. + /// + /// If the function does not throw synchronously, or if it throws an error + /// that is not of type [E], this expectation will fail. + /// + /// If this function is async and returns a [Future], this expectation will + /// fail. Instead invoke the function and check the expectation on the + /// returned [Future]. + Subject<E> throws<E>() { + return context.nest<E>(() => ['throws an error of type $E'], (actual) { + try { + final result = actual(); + return Extracted.rejection( + actual: prefixFirst('a function that returned ', literal(result)), + which: ['did not throw'], + ); + } catch (e) { + if (e is E) return Extracted.value(e as E); + return Extracted.rejection( + actual: prefixFirst('a function that threw error ', literal(e)), + which: ['did not throw an $E']); + } + }); + } + + /// Expects that the function returns without throwing. + /// + /// If the function runs without exception, return a [Subject] to check + /// further expecations on the returned value. + /// + /// If the function throws synchronously, this expectation will fail. + Subject<T> returnsNormally() { + return context.nest<T>(() => ['returns a value'], (actual) { + try { + return Extracted.value(actual()); + } catch (e, st) { + return Extracted.rejection(actual: [ + 'a function that throws' + ], which: [ + ...prefixFirst('threw ', literal(e)), + ...st.toString().split('\n') + ]); + } + }); + } +}
diff --git a/pkgs/checks/lib/src/extensions/iterable.dart b/pkgs/checks/lib/src/extensions/iterable.dart new file mode 100644 index 0000000..ecfa9c0 --- /dev/null +++ b/pkgs/checks/lib/src/extensions/iterable.dart
@@ -0,0 +1,372 @@ +// 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 '../../context.dart'; + +import '../collection_equality.dart'; +import 'core.dart'; + +extension IterableChecks<T> on Subject<Iterable<T>> { + Subject<int> get length => has((l) => l.length, 'length'); + + Subject<T> get first => context.nest(() => ['has first element'], (actual) { + final iterator = actual.iterator; + if (!iterator.moveNext()) { + return Extracted.rejection(which: ['has no elements']); + } + return Extracted.value(iterator.current); + }); + + Subject<T> get last => context.nest(() => ['has last element'], (actual) { + final iterator = actual.iterator; + if (!iterator.moveNext()) { + return Extracted.rejection(which: ['has no elements']); + } + var current = iterator.current; + while (iterator.moveNext()) { + current = iterator.current; + } + return Extracted.value(current); + }); + + Subject<T> get single => context.nest(() => ['has single element'], (actual) { + final iterator = actual.iterator; + if (!iterator.moveNext()) { + return Extracted.rejection(which: ['has no elements']); + } + final value = iterator.current; + if (iterator.moveNext()) { + return Extracted.rejection(which: ['has more than one element']); + } + return Extracted.value(value); + }); + + void isEmpty() { + context.expect(() => const ['is empty'], (actual) { + if (actual.isEmpty) return null; + return Rejection(which: ['is not empty']); + }); + } + + void isNotEmpty() { + context.expect(() => const ['is not empty'], (actual) { + if (actual.isNotEmpty) return null; + return Rejection(which: ['is empty']); + }); + } + + /// Expects that the iterable contains [element] according to + /// [Iterable.contains]. + void contains(T element) { + context.expect(() { + return prefixFirst('contains ', literal(element)); + }, (actual) { + if (actual.isEmpty) return Rejection(actual: ['an empty iterable']); + if (actual.contains(element)) return null; + return Rejection( + which: prefixFirst('does not contain ', literal(element))); + }); + } + + /// Expects that the iterable contains a value matching each expected value + /// from [elements] in the given order, with any extra elements between + /// them. + /// + /// For example, the following will succeed: + /// + /// ```dart + /// check([1, 0, 2, 0, 3]).containsInOrder([1, 2, 3]); + /// ``` + /// + /// Values in [elements] may be a `T`, a `Condition<T>`, or a + /// `Condition<Object?>`. If an expectation is a condition callback it will be + /// checked against the actual values, and any other expectations, including + /// those that are not a `T` or a condition callback, will be compared with + /// the equality operator. + /// + /// ```dart + /// check([1, 0, 2, 0, 3]) + /// .containsInOrder([1, (Subject<int> v) => v.isGreaterThan(1), 3]); + /// ``` + @Deprecated('Use `containsEqualInOrder` for expectations with values compared' + ' with `==` or `containsMatchingInOrder` for other expectations') + void containsInOrder(Iterable<Object?> elements) { + context.expect(() => prefixFirst('contains, in order: ', literal(elements)), + (actual) { + final expected = elements.toList(); + if (expected.isEmpty) { + throw ArgumentError('expected may not be empty'); + } + var expectedIndex = 0; + for (final element in actual) { + final currentExpected = expected[expectedIndex]; + final matches = currentExpected is Condition<T> + ? softCheck(element, currentExpected) == null + : currentExpected is Condition<dynamic> + ? softCheck(element, currentExpected) == null + : currentExpected == element; + if (matches && ++expectedIndex >= expected.length) return null; + } + return Rejection(which: [ + ...prefixFirst( + 'did not have an element matching the expectation at index ' + '$expectedIndex ', + literal(expected[expectedIndex])), + ]); + }); + } + + /// Expects that the iterable contains a value matching each condition in + /// [conditions] in the given order, with any extra elements between them. + /// + /// For example, the following will succeed: + /// + /// ```dart + /// check([1, 10, 2, 10, 3]).containsMatchingInOrder([ + /// (it) => it.isLessThan(2), + /// (it) => it.isLessThan(3), + /// (it) => it.isLessThan(4), + /// ]); + /// ``` + void containsMatchingInOrder(Iterable<Condition<T>> conditions) { + context + .expect(() => prefixFirst('contains, in order: ', literal(conditions)), + (actual) { + final expected = conditions.toList(); + if (expected.isEmpty) { + throw ArgumentError('expected may not be empty'); + } + var expectedIndex = 0; + for (final element in actual) { + final currentExpected = expected[expectedIndex]; + final matches = softCheck(element, currentExpected) == null; + if (matches && ++expectedIndex >= expected.length) return null; + } + return Rejection(which: [ + ...prefixFirst( + 'did not have an element matching the expectation at index ' + '$expectedIndex ', + literal(expected[expectedIndex])), + ]); + }); + } + + /// Expects that the iterable contains a value equals to each expected value + /// from [elements] in the given order, with any extra elements between + /// them. + /// + /// For example, the following will succeed: + /// + /// ```dart + /// check([1, 0, 2, 0, 3]).containsInOrder([1, 2, 3]); + /// ``` + /// + /// Values, will be compared with the equality operator. + void containsEqualInOrder(Iterable<T> elements) { + context.expect(() => prefixFirst('contains, in order: ', literal(elements)), + (actual) { + final expected = elements.toList(); + if (expected.isEmpty) { + throw ArgumentError('expected may not be empty'); + } + var expectedIndex = 0; + for (final element in actual) { + final currentExpected = expected[expectedIndex]; + final matches = currentExpected == element; + if (matches && ++expectedIndex >= expected.length) return null; + } + return Rejection(which: [ + ...prefixFirst( + 'did not have an element equal to the expectation at index ' + '$expectedIndex ', + literal(expected[expectedIndex])), + ]); + }); + } + + /// Expects that the iterable contains at least on element such that + /// [elementCondition] is satisfied. + void any(Condition<T> elementCondition) { + context.expect(() { + final conditionDescription = describe(elementCondition); + assert(conditionDescription.isNotEmpty); + return [ + 'contains a value that:', + ...conditionDescription, + ]; + }, (actual) { + if (actual.isEmpty) return Rejection(actual: ['an empty iterable']); + for (var e in actual) { + if (softCheck(e, elementCondition) == null) return null; + } + return Rejection(which: ['Contains no matching element']); + }); + } + + /// Expects there are no elements in the iterable which fail to satisfy + /// [elementCondition]. + /// + /// Empty iterables will pass always pass this expectation. + void every(Condition<T> elementCondition) { + context.expect(() { + final conditionDescription = describe(elementCondition); + assert(conditionDescription.isNotEmpty); + return [ + 'only has values that:', + ...conditionDescription, + ]; + }, (actual) { + final iterator = actual.iterator; + for (var i = 0; iterator.moveNext(); i++) { + final element = iterator.current; + final failure = softCheck(element, elementCondition); + if (failure == null) continue; + final which = failure.rejection.which; + return Rejection(which: [ + 'has an element at index $i that:', + ...indent(failure.detail.actual.skip(1)), + ...indent(prefixFirst('Actual: ', failure.rejection.actual), + failure.detail.depth + 1), + if (which != null && which.isNotEmpty) + ...indent(prefixFirst('Which: ', which), failure.detail.depth + 1), + ]); + } + return null; + }); + } + + /// Expects that the iterable contains elements that are deeply equal to the + /// elements of [expected]. + /// + /// {@macro deep_collection_equals} + void deepEquals(Iterable<Object?> expected) => context + .expect(() => prefixFirst('is deeply equal to ', literal(expected)), + (actual) { + final which = deepCollectionEquals(actual, expected); + if (which == null) return null; + return Rejection(which: which); + }); + + /// Expects that the iterable contains elements which equal those of + /// [expected] in any order. + /// + /// Should not be used for very large collections, runtime is O(n^2.5) in the + /// worst case where the iterables contain many equal elements, and O(n^2) in + /// more typical cases. + void unorderedEquals(Iterable<T> expected) { + context.expect(() => prefixFirst('unordered equals ', literal(expected)), + (actual) { + final which = unorderedCompare( + actual, + expected, + (actual, expected) => expected == actual, + (expected, index, count) => [ + ...prefixFirst( + 'has no element equal to the expected element at index ' + '$index: ', + literal(expected)), + if (count > 1) 'or ${count - 1} other elements', + ], + (actual, index, count) => [ + ...prefixFirst( + 'has an unexpected element at index $index: ', literal(actual)), + if (count > 1) 'and ${count - 1} other unexpected elements', + ], + ); + if (which == null) return null; + return Rejection(which: which); + }); + } + + /// Expects that the iterable contains elements which match all conditions of + /// [expected] in any order. + /// + /// Should not be used for very large collections, runtime is O(n^2.5) in the + /// worst case where conditions match many elements, and O(n^2) in more + /// typical cases. + void unorderedMatches(Iterable<Condition<T>> expected) { + context.expect(() => prefixFirst('unordered matches ', literal(expected)), + (actual) { + final which = unorderedCompare( + actual, + expected, + (actual, expected) => softCheck(actual, expected) == null, + (expected, index, count) => [ + 'has no element matching the condition at index $index:', + ...describe(expected), + if (count > 1) 'or ${count - 1} other conditions', + ], + (actual, index, count) => [ + ...prefixFirst( + 'has an unmatched element at index $index: ', literal(actual)), + if (count > 1) 'and ${count - 1} other unmatched elements', + ], + ); + if (which == null) return null; + return Rejection(which: which); + }); + } + + /// Expects that the iterable contains elements that correspond by the + /// [elementCondition] exactly to each element in [expected]. + /// + /// Fails if the iterable has a different length than [expected]. + /// + /// For each element in the iterable, calls [elementCondition] with the + /// corresponding element from [expected] to get the specific condition for + /// that index. + /// + /// [description] is used in the Expected clause. It should be a predicate + /// without the object, for example with the description 'is less than' the + /// full expectation will be: "pairwise is less than $expected" + @Deprecated('Use `pairwiseMatches`') + void pairwiseComparesTo<S>(List<S> expected, + Condition<T> Function(S) elementCondition, String description) => + pairwiseMatches(expected, elementCondition, description); + + /// Expects that the iterable contains elements that correspond by the + /// [elementCondition] exactly to each element in [expected]. + /// + /// Fails if the iterable has a different length than [expected]. + /// + /// For each element in the iterable, calls [elementCondition] with the + /// corresponding element from [expected] to get the specific condition for + /// that index. + /// + /// [description] is used in the Expected clause. It should be a predicate + /// without the object, for example with the description 'is less than' the + /// full expectation will be: "pairwise is less than $expected" + void pairwiseMatches<S>(List<S> expected, + Condition<T> Function(S) elementCondition, String description) { + context.expect(() { + return prefixFirst('pairwise $description ', literal(expected)); + }, (actual) { + final iterator = actual.iterator; + for (var i = 0; i < expected.length; i++) { + final expectedValue = expected[i]; + if (!iterator.moveNext()) { + return Rejection(which: [ + 'has too few elements, there is no element to match at index $i' + ]); + } + final actualValue = iterator.current; + final failure = softCheck(actualValue, elementCondition(expectedValue)); + if (failure == null) continue; + final innerDescription = describe<T>(elementCondition(expectedValue)); + final which = failure.rejection.which; + return Rejection(which: [ + 'does not have an element at index $i that:', + ...innerDescription, + ...prefixFirst( + 'Actual element at index $i: ', failure.rejection.actual), + if (which != null) ...prefixFirst('Which: ', which), + ]); + } + if (!iterator.moveNext()) return null; + return Rejection(which: [ + 'has too many elements, expected exactly ${expected.length}' + ]); + }); + } +}
diff --git a/pkgs/checks/lib/src/extensions/map.dart b/pkgs/checks/lib/src/extensions/map.dart new file mode 100644 index 0000000..4fc0fdf --- /dev/null +++ b/pkgs/checks/lib/src/extensions/map.dart
@@ -0,0 +1,109 @@ +// 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 '../../context.dart'; + +import '../collection_equality.dart'; +import 'core.dart'; + +extension MapChecks<K, V> on Subject<Map<K, V>> { + Subject<Iterable<MapEntry<K, V>>> get entries => + has((m) => m.entries, 'entries'); + Subject<Iterable<K>> get keys => has((m) => m.keys, 'keys'); + Subject<Iterable<V>> get values => has((m) => m.values, 'values'); + Subject<int> get length => has((m) => m.length, 'length'); + Subject<V> operator [](K key) { + return context.nest( + () => prefixFirst('contains a value for ', literal(key)), (actual) { + if (!actual.containsKey(key)) { + return Extracted.rejection( + which: prefixFirst('does not contain the key ', literal(key))); + } + return Extracted.value(actual[key] as V); + }); + } + + void isEmpty() { + context.expect(() => const ['is empty'], (actual) { + if (actual.isEmpty) return null; + return Rejection(which: ['is not empty']); + }); + } + + void isNotEmpty() { + context.expect(() => const ['is not empty'], (actual) { + if (actual.isNotEmpty) return null; + return Rejection(which: ['is not empty']); + }); + } + + /// Expects that the map contains [key] according to [Map.containsKey]. + void containsKey(K key) { + context.expect(() => prefixFirst('contains key ', literal(key)), (actual) { + if (actual.containsKey(key)) return null; + return Rejection( + which: prefixFirst('does not contain key ', literal(key))); + }); + } + + /// Expects that the map contains some key such that [keyCondition] is + /// satisfied. + void containsKeyThat(Condition<K> keyCondition) { + context.expect(() { + final conditionDescription = describe(keyCondition); + assert(conditionDescription.isNotEmpty); + return [ + 'contains a key that:', + ...conditionDescription, + ]; + }, (actual) { + if (actual.isEmpty) return Rejection(actual: ['an empty map']); + for (var k in actual.keys) { + if (softCheck(k, keyCondition) == null) return null; + } + return Rejection(which: ['Contains no matching key']); + }); + } + + /// Expects that the map contains [value] according to [Map.containsValue]. + void containsValue(V value) { + context.expect(() => prefixFirst('contains value ', literal(value)), + (actual) { + if (actual.containsValue(value)) return null; + return Rejection( + which: prefixFirst('does not contain value ', literal(value))); + }); + } + + /// Expects that the map contains some value such that [valueCondition] is + /// satisfied. + void containsValueThat(Condition<V> valueCondition) { + context.expect(() { + final conditionDescription = describe(valueCondition); + assert(conditionDescription.isNotEmpty); + return [ + 'contains a value that:', + ...conditionDescription, + ]; + }, (actual) { + if (actual.isEmpty) return Rejection(actual: ['an empty map']); + for (var v in actual.values) { + if (softCheck(v, valueCondition) == null) return null; + } + return Rejection(which: ['Contains no matching value']); + }); + } + + /// Expects that the map contains entries that are deeply equal to the entries + /// of [expected]. + /// + /// {@macro deep_collection_equals} + void deepEquals(Map<Object?, Object?> expected) => context + .expect(() => prefixFirst('is deeply equal to ', literal(expected)), + (actual) { + final which = deepCollectionEquals(actual, expected); + if (which == null) return null; + return Rejection(which: which); + }); +}
diff --git a/pkgs/checks/lib/src/extensions/math.dart b/pkgs/checks/lib/src/extensions/math.dart new file mode 100644 index 0000000..302f1db --- /dev/null +++ b/pkgs/checks/lib/src/extensions/math.dart
@@ -0,0 +1,88 @@ +// 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 '../../context.dart'; + +extension NumChecks on Subject<num> { + /// Expects that [num.isNaN] is true. + void isNaN() { + context.expect(() => ['is not a number (NaN)'], (actual) { + if (actual.isNaN) return null; + return Rejection(which: ['is a number']); + }); + } + + /// Expects that [num.isNaN] is false. + void isNotNaN() { + context.expect(() => ['is a number (not NaN)'], (actual) { + if (!actual.isNaN) return null; + return Rejection(which: ['is not a number (NaN)']); + }); + } + + /// Expects that [num.isNegative] is true. + void isNegative() { + context.expect(() => ['is negative'], (actual) { + if (actual.isNegative) return null; + return Rejection(which: ['is not negative']); + }); + } + + /// Expects that [num.isNegative] is false. + void isNotNegative() { + context.expect(() => ['is not negative'], (actual) { + if (!actual.isNegative) return null; + return Rejection(which: ['is negative']); + }); + } + + /// Expects that [num.isFinite] is true. + void isFinite() { + context.expect(() => ['is finite'], (actual) { + if (actual.isFinite) return null; + return Rejection(which: ['is not finite']); + }); + } + + /// Expects that [num.isFinite] is false. + /// + /// Satisfied by [double.nan], [double.infinity] and + /// [double.negativeInfinity]. + void isNotFinite() { + context.expect(() => ['is not finite'], (actual) { + if (!actual.isFinite) return null; + return Rejection(which: ['is finite']); + }); + } + + /// Expects that [num.isInfinite] is true. + /// + /// Satisfied by [double.infinity] and [double.negativeInfinity]. + void isInfinite() { + context.expect(() => ['is infinite'], (actual) { + if (actual.isInfinite) return null; + return Rejection(which: ['is not infinite']); + }); + } + + /// Expects that [num.isInfinite] is false. + /// + /// Satisfied by [double.nan] and finite numbers. + void isNotInfinite() { + context.expect(() => ['is not infinite'], (actual) { + if (!actual.isInfinite) return null; + return Rejection(which: ['is infinite']); + }); + } + + /// Expects that the difference between this number and [other] is less than + /// or equal to [delta]. + void isCloseTo(num other, num delta) { + context.expect(() => ['is within <$delta> of <$other>'], (actual) { + final difference = (other - actual).abs(); + if (difference <= delta) return null; + return Rejection(which: ['differs by <$difference>']); + }); + } +}
diff --git a/pkgs/checks/lib/src/extensions/string.dart b/pkgs/checks/lib/src/extensions/string.dart new file mode 100644 index 0000000..84b20f6 --- /dev/null +++ b/pkgs/checks/lib/src/extensions/string.dart
@@ -0,0 +1,229 @@ +// 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:math' as math; + +import '../../context.dart'; + +import 'core.dart'; + +extension StringChecks on Subject<String> { + /// Expects that the value contains [pattern] according to [String.contains]; + void contains(Pattern pattern) { + context.expect(() => prefixFirst('contains ', literal(pattern)), (actual) { + if (actual.contains(pattern)) return null; + return Rejection( + which: prefixFirst('Does not contain ', literal(pattern)), + ); + }); + } + + Subject<int> get length => has((m) => m.length, 'length'); + + void isEmpty() { + context.expect(() => const ['is empty'], (actual) { + if (actual.isEmpty) return null; + return Rejection(which: ['is not empty']); + }); + } + + void isNotEmpty() { + context.expect(() => const ['is not empty'], (actual) { + if (actual.isNotEmpty) return null; + return Rejection(which: ['is empty']); + }); + } + + void startsWith(Pattern other) { + context.expect( + () => prefixFirst('starts with ', literal(other)), + (actual) { + if (actual.startsWith(other)) return null; + return Rejection( + which: prefixFirst('does not start with ', literal(other)), + ); + }, + ); + } + + void endsWith(String other) { + context.expect( + () => prefixFirst('ends with ', literal(other)), + (actual) { + if (actual.endsWith(other)) return null; + return Rejection( + which: prefixFirst('does not end with ', literal(other)), + ); + }, + ); + } + + /// Expects that the string matches the pattern [expected]. + /// + /// Fails if [expected] returns an empty result from calling `allMatches` with + /// the value. + /// + /// ``` + /// check(actual).matchesPattern('abc'); + /// check(actual).matchesPattern(RegExp(r'\d')); + /// ``` + void matchesPattern(Pattern expected) { + context.expect(() => prefixFirst('matches ', literal(expected)), (actual) { + if (expected.allMatches(actual).isNotEmpty) return null; + return Rejection( + which: prefixFirst('does not match ', literal(expected))); + }); + } + + /// Expects that the `String` contains each of the sub strings in expected + /// in the given order, with any content between them. + /// + /// For example, the following will succeed: + /// + /// check('abcdefg').containsInOrder(['a','e']); + void containsInOrder(Iterable<String> expected) { + context.expect(() => prefixFirst('contains, in order: ', literal(expected)), + (actual) { + var fromIndex = 0; + for (var s in expected) { + var index = actual.indexOf(s, fromIndex); + if (index < 0) { + return Rejection(which: [ + ...prefixFirst( + 'does not have a match for the substring ', literal(s)), + if (fromIndex != 0) + 'following the other matches up to character $fromIndex' + ]); + } + fromIndex = index + s.length; + } + return null; + }); + } + + /// Expects that the `String` contains exactly the same code units as + /// [expected]. + void equals(String expected) { + context.expect(() => prefixFirst('equals ', literal(expected)), + (actual) => _findDifference(actual, expected)); + } + + /// Expects that the `String` contains the same characters as [expected] if + /// both were lower case. + void equalsIgnoringCase(String expected) { + context.expect( + () => prefixFirst('equals ignoring case ', literal(expected)), + (actual) => _findDifference( + actual.toLowerCase(), expected.toLowerCase(), actual, expected)); + } + + /// Expects that the `String` contains the same content as [expected], + /// ignoring differences in whitsepace. + /// + /// All runs of whitespace characters are collapsed to a single space, and + /// leading and traiilng whitespace are removed before comparison. + /// + /// For example the following will succeed: + /// + /// check(' hello world ').equalsIgnoringWhitespace('hello world'); + /// + /// While the following will fail: + /// + /// check('helloworld').equalsIgnoringWhitespace('hello world'); + /// check('he llo world').equalsIgnoringWhitespace('hello world'); + void equalsIgnoringWhitespace(String expected) { + context.expect( + () => prefixFirst('equals ignoring whitespace ', literal(expected)), + (actual) { + final collapsedActual = _collapseWhitespace(actual); + final collapsedExpected = _collapseWhitespace(expected); + return _findDifference(collapsedActual, collapsedExpected, + collapsedActual, collapsedExpected); + }); + } +} + +Rejection? _findDifference(String actual, String expected, + [String? actualDisplay, String? expectedDisplay]) { + if (actual == expected) return null; + final escapedActual = escape(actual); + final escapedExpected = escape(expected); + final escapedActualDisplay = + actualDisplay != null ? escape(actualDisplay) : escapedActual; + final escapedExpectedDisplay = + expectedDisplay != null ? escape(expectedDisplay) : escapedExpected; + final minLength = math.min(escapedActual.length, escapedExpected.length); + var i = 0; + for (; i < minLength; i++) { + if (escapedActual.codeUnitAt(i) != escapedExpected.codeUnitAt(i)) { + break; + } + } + if (i == minLength) { + if (escapedExpected.length < escapedActual.length) { + if (expected.isEmpty) { + return Rejection(which: ['is not the empty string']); + } + return Rejection(which: [ + 'is too long with unexpected trailing characters:', + _trailing(escapedActualDisplay, i) + ]); + } else { + if (actual.isEmpty) { + return Rejection(actual: [ + 'an empty string' + ], which: [ + 'is missing all expected characters:', + _trailing(escapedExpectedDisplay, 0) + ]); + } + return Rejection(which: [ + 'is too short with missing trailing characters:', + _trailing(escapedExpectedDisplay, i) + ]); + } + } else { + final indentation = ' ' * (i > 10 ? 14 : i); + return Rejection(which: [ + 'differs at offset $i:', + '${_leading(escapedExpectedDisplay, i)}' + '${_trailing(escapedExpectedDisplay, i)}', + '${_leading(escapedActualDisplay, i)}' + '${_trailing(escapedActualDisplay, i)}', + '$indentation^' + ]); + } +} + +/// The truncated beginning of [s] up to the [end] character. +String _leading(String s, int end) => + (end > 10) ? '... ${s.substring(end - 10, end)}' : s.substring(0, end); + +/// The truncated remainder of [s] starting at the [start] character. +String _trailing(String s, int start) => (start + 10 > s.length) + ? s.substring(start) + : '${s.substring(start, start + 10)} ...'; + +/// Utility function to collapse whitespace runs to single spaces +/// and strip leading/trailing whitespace. +String _collapseWhitespace(String string) { + var result = StringBuffer(); + var skipSpace = true; + for (var i = 0; i < string.length; i++) { + var character = string[i]; + if (_isWhitespace(character)) { + if (!skipSpace) { + result.write(' '); + skipSpace = true; + } + } else { + result.write(character); + skipSpace = false; + } + } + return result.toString().trim(); +} + +bool _isWhitespace(String ch) => + ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t';
diff --git a/pkgs/checks/mono_pkg.yaml b/pkgs/checks/mono_pkg.yaml new file mode 100644 index 0000000..b447edb --- /dev/null +++ b/pkgs/checks/mono_pkg.yaml
@@ -0,0 +1,15 @@ +# See https://pub.dev/packages/mono_repo + +stages: +- analyze_and_format: + - group: + - format + - analyze: --fatal-infos + sdk: dev + - group: + - analyze + sdk: pubspec +- unit_test: + - group: + - command: dart test + sdk: [dev, pubspec]
diff --git a/pkgs/checks/pubspec.yaml b/pkgs/checks/pubspec.yaml new file mode 100644 index 0000000..8c59ba4 --- /dev/null +++ b/pkgs/checks/pubspec.yaml
@@ -0,0 +1,18 @@ +name: checks +version: 0.3.1-wip +description: >- + A framework for checking values against expectations and building custom + expectations. +repository: https://github.com/dart-lang/test/tree/master/pkgs/checks +resolution: workspace + +environment: + sdk: ^3.5.0 + +dependencies: + async: ^2.8.0 + meta: ^1.9.0 + test_api: ">=0.5.0 <0.8.0" + +dev_dependencies: + test: ^1.21.3
diff --git a/pkgs/checks/test/context_test.dart b/pkgs/checks/test/context_test.dart new file mode 100644 index 0000000..820ff88 --- /dev/null +++ b/pkgs/checks/test/context_test.dart
@@ -0,0 +1,172 @@ +// Copyright (c) 2023, 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:convert'; + +import 'package:async/async.dart' hide Result; +import 'package:checks/checks.dart'; +import 'package:checks/context.dart'; +import 'package:test/scaffolding.dart'; +import 'package:test_api/hooks.dart'; +import 'package:test_api/hooks_testing.dart'; + +void main() { + group('Context', () { + test('expectAsync holds test open', () async { + late void Function() callback; + final monitor = TestCaseMonitor.start(() { + check(null).context.expectAsync(() => [''], (actual) async { + final completer = Completer<void>(); + callback = completer.complete; + await completer.future; + return null; + }); + }); + await pumpEventQueue(); + check(monitor).state.equals(State.running); + callback(); + await monitor.onDone; + check(monitor).didPass(); + }); + + test('expectAsync does not hold test open past exception', () async { + late void Function() callback; + final monitor = TestCaseMonitor.start(() { + check(null).context.expectAsync(() => [''], (actual) async { + final completer = Completer<void>(); + callback = completer.complete; + await completer.future; + throw 'oh no!'; + }); + }); + await pumpEventQueue(); + check(monitor).state.equals(State.running); + callback(); + await monitor.onDone; + check(monitor) + ..state.equals(State.failed) + ..errors.single.has((e) => e.error, 'error').equals('oh no!'); + }); + + test('nestAsync holds test open', () async { + late void Function() callback; + final monitor = TestCaseMonitor.start(() { + check(null).context.nestAsync(() => [''], (actual) async { + final completer = Completer<void>(); + callback = completer.complete; + await completer.future; + return Extracted.value(null); + }, null); + }); + await pumpEventQueue(); + check(monitor).state.equals(State.running); + callback(); + await monitor.onDone; + check(monitor).didPass(); + }); + + test('nestAsync holds test open past async condition', () async { + late void Function() callback; + final monitor = TestCaseMonitor.start(() { + check(null).context.nestAsync(() => [''], (actual) async { + return Extracted.value(null); + }, (it) async { + final completer = Completer<void>(); + callback = completer.complete; + await completer.future; + }); + }); + await pumpEventQueue(); + check(monitor).state.equals(State.running); + callback(); + await monitor.onDone; + check(monitor).didPass(); + }); + + test('nestAsync does not hold test open past exception', () async { + late void Function() callback; + final monitor = TestCaseMonitor.start(() { + check(null).context.nestAsync<Object?>(() => [''], (actual) async { + final completer = Completer<void>(); + callback = completer.complete; + await completer.future; + throw 'oh no!'; + }, null); + }); + await pumpEventQueue(); + check(monitor).state.equals(State.running); + callback(); + await monitor.onDone; + check(monitor) + ..state.equals(State.failed) + ..errors.single.has((e) => e.error, 'error').equals('oh no!'); + }); + + test('expectUnawaited can fail the test after it completes', () async { + late void Function() callback; + final monitor = await TestCaseMonitor.run(() { + check(null).context.expectUnawaited(() => [''], (actual, reject) { + final completer = Completer<void>() + ..future.then((_) { + reject(Rejection(which: ['foo'])); + }); + callback = completer.complete; + }); + }); + check(monitor).state.equals(State.passed); + callback(); + await pumpEventQueue(); + check(monitor) + ..state.equals(State.failed) + ..errors.unorderedMatches([ + (it) => it + .has((e) => e.error, 'error') + .isA<TestFailure>() + .has((f) => f.message, 'message') + .isNotNull() + .endsWith('Which: foo'), + (it) => it + .has((e) => e.error, 'error') + .isA<String>() + .startsWith('This test failed after it had already completed.') + ]); + }); + }); + + group('SkipExtension', () { + test('marks the test as skipped', () async { + final monitor = await TestCaseMonitor.run(() { + check(null).skip('skip').isNotNull(); + }); + check(monitor).state.equals(State.skipped); + }); + }); +} + +extension _MonitorChecks on Subject<TestCaseMonitor> { + Subject<State> get state => has((m) => m.state, 'state'); + Subject<Iterable<AsyncError>> get errors => has((m) => m.errors, 'errors'); + Subject<StreamQueue<AsyncError>> get onError => + has((m) => m.onError, 'onError').withQueue; + + /// Expects that the monitored test is completed as success with no errors. + /// + /// Sets up an unawaited expectation that the test does not emit errors in the + /// future in addition to checking there have been no errors yet. + void didPass() { + errors.isEmpty(); + state.equals(State.passed); + onError.context.expectUnawaited(() => ['emits no further errors'], + (actual, reject) async { + await for (var error in actual.rest) { + reject(Rejection(which: [ + ...prefixFirst('threw late error', literal(error.error)), + ...const LineSplitter().convert( + TestHandle.current.formatStackTrace(error.stackTrace).toString()) + ])); + } + }); + } +}
diff --git a/pkgs/checks/test/describe_test.dart b/pkgs/checks/test/describe_test.dart new file mode 100644 index 0000000..d117a7b --- /dev/null +++ b/pkgs/checks/test/describe_test.dart
@@ -0,0 +1,24 @@ +// Copyright (c) 2023, 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:checks/checks.dart'; +import 'package:checks/context.dart'; +import 'package:test/scaffolding.dart'; + +void main() { + group('describe', () { + test('succeeds for empty conditions', () { + check(describe((_) {})).isEmpty(); + }); + test('includes condition clauses', () { + check(describe((it) => it.equals(1))).deepEquals([' equals <1>']); + }); + test('includes nested clauses', () { + check(describe<String>((it) => it.length.equals(1))).deepEquals([ + ' has length that:', + ' equals <1>', + ]); + }); + }); +}
diff --git a/pkgs/checks/test/extensions/async_test.dart b/pkgs/checks/test/extensions/async_test.dart new file mode 100644 index 0000000..5bfe550 --- /dev/null +++ b/pkgs/checks/test/extensions/async_test.dart
@@ -0,0 +1,531 @@ +// 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:async'; + +import 'package:async/async.dart'; +import 'package:checks/checks.dart'; +import 'package:checks/context.dart'; +import 'package:test/scaffolding.dart'; +import 'package:test_api/hooks.dart'; + +import '../test_shared.dart'; + +void main() { + group('FutureChecks', () { + group('completes', () { + test('succeeds for a future that completes to a value', () async { + await check(_futureSuccess()).completes((it) => it.equals(42)); + }); + test('rejects futures which complete as errors', () async { + await check(_futureFail()).isRejectedByAsync( + (it) => it.completes((it) => it.equals(1)), + actual: ['a future that completes as an error'], + which: ['threw <UnimplementedError> at:', 'fake trace'], + ); + }); + test('can be described', () async { + await check((Subject<Future> it) => it.completes()) + .hasAsyncDescriptionWhich( + (it) => it.deepEquals([' completes to a value'])); + await check((Subject<Future> it) => it.completes((it) => it.equals(42))) + .hasAsyncDescriptionWhich((it) => it.deepEquals([ + ' completes to a value that:', + ' equals <42>', + ])); + }); + }); + + group('throws', () { + test( + 'succeeds for a future that compeletes to an error of the expected type', + () async { + await check(_futureFail()).throws<UnimplementedError>( + (it) => it.has((p0) => p0.message, 'message').isNull()); + }); + test('fails for futures that complete to a value', () async { + await check(_futureSuccess()).isRejectedByAsync( + (it) => it.throws(), + actual: ['completed to <42>'], + which: ['did not throw'], + ); + }); + test('failes for futures that complete to an error of the wrong type', + () async { + await check(_futureFail()).isRejectedByAsync( + (it) => it.throws<StateError>(), + actual: ['completed to error <UnimplementedError>'], + which: [ + 'threw an exception that is not a StateError at:', + 'fake trace' + ], + ); + }); + test('can be described', () async { + await check((Subject<Future<void>> it) => it.throws()) + .hasAsyncDescriptionWhich( + (it) => it.deepEquals([' completes to an error'])); + await check((Subject<Future<void>> it) => it.throws<StateError>()) + .hasAsyncDescriptionWhich((it) => + it.deepEquals([' completes to an error of type StateError'])); + }); + }); + + group('doesNotComplete', () { + test('succeeds for a Future that never completes', () async { + check(Completer<void>().future).doesNotComplete(); + }); + test('fails for a Future that completes as a value', () async { + Object? testFailure; + runZonedGuarded(() { + final completer = Completer<String>(); + check(completer.future).doesNotComplete(); + completer.complete('value'); + }, (e, st) { + testFailure = e; + }); + await pumpEventQueue(); + check(testFailure) + .isA<TestFailure>() + .has((f) => f.message, 'message') + .isNotNull() + .equals(''' +Expected: a Future<String> that: + does not complete +Actual: a future that completed to 'value\''''); + }); + test('fails for a Future that completes as an error', () async { + Object? testFailure; + runZonedGuarded(() { + final completer = Completer<String>(); + check(completer.future).doesNotComplete(); + completer.completeError('error', StackTrace.fromString('fake trace')); + }, (e, st) { + testFailure = e; + }); + await pumpEventQueue(); + check(testFailure) + .isA<TestFailure>() + .has((f) => f.message, 'message') + .isNotNull() + .equals(''' +Expected: a Future<String> that: + does not complete +Actual: a future that completed as an error: +Which: threw 'error' +fake trace'''); + }); + test('can be described', () async { + await check((Subject<Future<void>> it) => it.doesNotComplete()) + .hasAsyncDescriptionWhich( + (it) => it.deepEquals([' does not complete'])); + }); + }); + }); + + group('StreamChecks', () { + group('emits', () { + test('succeeds for a stream that emits a value', () async { + await check(_countingStream(5)).emits((it) => it.equals(0)); + }); + test('fails for a stream that closes without emitting', () async { + await check(_countingStream(0)).isRejectedByAsync( + (it) => it.emits(), + actual: ['a stream'], + which: ['closed without emitting enough values'], + ); + }); + test('fails for a stream that emits an error', () async { + await check(_countingStream(1, errorAt: 0)).isRejectedByAsync( + (it) => it.emits(), + actual: ['a stream with error <UnimplementedError: Error at 1>'], + which: ['emitted an error instead of a value at:', 'fake trace'], + ); + }); + test('can be described', () async { + await check((Subject<StreamQueue<void>> it) => it.emits()) + .hasAsyncDescriptionWhich( + (it) => it.deepEquals([' emits a value'])); + await check((Subject<StreamQueue<int>> it) => + it.emits((it) => it.equals(42))) + .hasAsyncDescriptionWhich((it) => it.deepEquals([ + ' emits a value that:', + ' equals <42>', + ])); + }); + test('does not consume error', () async { + final queue = _countingStream(1, errorAt: 0); + await softCheckAsync<StreamQueue<int>>(queue, (it) => it.emits()); + await check(queue).emitsError(); + }); + }); + + group('emitsError', () { + test('succeeds for a stream that emits an error', () async { + await check(_countingStream(1, errorAt: 0)) + .emitsError<UnimplementedError>(); + }); + test('fails for a stream that closes without emitting an error', + () async { + await check(_countingStream(0)).isRejectedByAsync( + (it) => it.emitsError(), + actual: ['a stream'], + which: ['closed without emitting an expected error'], + ); + }); + test('fails for a stream that emits value', () async { + await check(_countingStream(1)).isRejectedByAsync( + (it) => it.emitsError(), + actual: ['a stream emitting value <0>'], + which: ['closed without emitting an error'], + ); + }); + test('fails for a stream that emits an error of the incorrect type', + () async { + await check(_countingStream(1, errorAt: 0)).isRejectedByAsync( + (it) => it.emitsError<StateError>(), + actual: ['a stream with error <UnimplementedError: Error at 1>'], + which: ['emitted an error which is not StateError at:', 'fake trace'], + ); + }); + test('can be described', () async { + await check((Subject<StreamQueue<void>> it) => it.emitsError()) + .hasAsyncDescriptionWhich( + (it) => it.deepEquals([' emits an error'])); + await check( + (Subject<StreamQueue<void>> it) => it.emitsError<StateError>()) + .hasAsyncDescriptionWhich( + (it) => it.deepEquals([' emits an error of type StateError'])); + await check((Subject<StreamQueue<void>> it) => it + ..emitsError<StateError>( + (it) => it.has((e) => e.message, 'message').equals('foo'))) + .hasAsyncDescriptionWhich((it) => it.deepEquals([ + ' emits an error of type StateError that:', + ' has message that:', + ' equals \'foo\'' + ])); + }); + test('uses a transaction', () async { + final queue = _countingStream(1); + await softCheckAsync<StreamQueue<int>>(queue, (it) => it.emitsError()); + await check(queue).emits((it) => it.equals(0)); + }); + }); + + group('emitsThrough', () { + test('succeeds for a stream that eventuall emits a matching value', + () async { + await check(_countingStream(5)).emitsThrough((it) => it.equals(4)); + }); + test('fails for a stream that closes without emitting a matching value', + () async { + await check(_countingStream(4)).isRejectedByAsync( + (it) => it.emitsThrough((it) => it.equals(5)), + actual: ['a stream'], + which: ['ended after emitting 4 elements with none matching'], + ); + }); + test('can be described', () async { + await check((Subject<StreamQueue<int>> it) => + it.emitsThrough((it) => it.equals(42))) + .hasAsyncDescriptionWhich((it) => it.deepEquals([ + ' emits any values then emits a value that:', + ' equals <42>' + ])); + }); + test('uses a transaction', () async { + final queue = _countingStream(1); + await softCheckAsync( + queue, + (Subject<StreamQueue<int>> it) => + it.emitsThrough((it) => it.equals(42))); + await check(queue).emits((it) => it.equals(0)); + }); + test('consumes events', () async { + final queue = _countingStream(3); + await check(queue).emitsThrough((it) => it.equals(1)); + await check(queue).emits((it) => it.equals(2)); + }); + }); + + group('emitsInOrder', () { + test('succeeds for happy case', () async { + await check(_countingStream(2)).inOrder([ + (it) => it.emits((it) => it.equals(0)), + (it) => it.emits((it) => it.equals(1)), + (it) => it.isDone(), + ]); + }); + test('reports which condition failed', () async { + await check(_countingStream(1)).isRejectedByAsync( + (it) => it.inOrder([(it) => it.emits(), (it) => it.emits()]), + actual: ['a stream'], + which: [ + 'satisfied 1 conditions then', + 'failed to satisfy the condition at index 1', + 'because it closed without emitting enough values' + ], + ); + }); + test('nestes the report for deep failures', () async { + await check(_countingStream(2)).isRejectedByAsync( + (it) => it.inOrder( + [(it) => it.emits(), (it) => it.emits((it) => it.equals(2))]), + actual: ['a stream'], + which: [ + 'satisfied 1 conditions then', + 'failed to satisfy the condition at index 1', + 'because it:', + ' emits a value that:', + ' Actual: <1>', + ' Which: are not equal', + ], + ); + }); + test('gets described with the number of conditions', () async { + await check( + (Subject<StreamQueue<int>> it) => it.inOrder([(_) {}, (_) {}])) + .hasAsyncDescriptionWhich( + (it) => it.deepEquals([' satisfies 2 conditions in order'])); + }); + test('uses a transaction', () async { + final queue = _countingStream(3); + await softCheckAsync<StreamQueue<int>>( + queue, + (it) => it.inOrder([ + (it) => it.emits((it) => it.equals(0)), + (it) => it.emits((it) => it.equals(1)), + (it) => it.emits((it) => it.equals(42)), + ])); + await check(queue).inOrder([ + (it) => it.emits((it) => it.equals(0)), + (it) => it.emits((it) => it.equals(1)), + (it) => it.emits((it) => it.equals(2)), + (it) => it.isDone(), + ]); + }); + test('consumes events', () async { + final queue = _countingStream(3); + await check(queue).inOrder([(it) => it.emits(), (it) => it.emits()]); + await check(queue).emits((it) => it.equals(2)); + }); + }); + + group('neverEmits', () { + test( + 'succeeds for a stream that closes without emitting a matching value', + () async { + await check(_countingStream(5)).neverEmits((it) => it.equals(5)); + }); + test('fails for a stream that emits a matching value', () async { + await check(_countingStream(6)).isRejectedByAsync( + (it) => it.neverEmits((it) => it.equals(5)), + actual: ['a stream'], + which: ['emitted <5>', 'following 5 other items'], + ); + }); + test('can be described', () async { + await check((Subject<StreamQueue<int>> it) => + it.neverEmits((it) => it.equals(42))) + .hasAsyncDescriptionWhich((it) => it.deepEquals([ + ' never emits a value that:', + ' equals <42>', + ])); + }); + test('uses a transaction', () async { + final queue = _countingStream(2); + await softCheckAsync<StreamQueue<int>>( + queue, (it) => it.neverEmits((it) => it.equals(1))); + await check(queue).inOrder([ + (it) => it.emits((it) => it.equals(0)), + (it) => it.emits((it) => it.equals(1)), + (it) => it.isDone(), + ]); + }); + }); + + group('mayEmit', () { + test('succeeds for a stream that emits a matching value', () async { + await check(_countingStream(1)).mayEmit((it) => it.equals(0)); + }); + test('succeeds for a stream that emits an error', () async { + await check(_countingStream(1, errorAt: 0)) + .mayEmit((it) => it.equals(0)); + }); + test('succeeds for a stream that closes', () async { + await check(_countingStream(0)).mayEmit((it) => it.equals(42)); + }); + test('consumes a matching event', () async { + final queue = _countingStream(2); + await softCheckAsync<StreamQueue<int>>( + queue, (it) => it.mayEmit((it) => it.equals(0))); + await check(queue).emits((it) => it.equals(1)); + }); + test('does not consume a non-matching event', () async { + final queue = _countingStream(2); + await softCheckAsync<StreamQueue<int>>( + queue, (it) => it.mayEmit((it) => it.equals(1))); + await check(queue).emits((it) => it.equals(0)); + }); + test('does not consume an error', () async { + final queue = _countingStream(1, errorAt: 0); + await softCheckAsync<StreamQueue<int>>( + queue, (it) => it.mayEmit((it) => it.equals(0))); + await check(queue).emitsError<UnimplementedError>( + (it) => it.has((e) => e.message, 'message').equals('Error at 1')); + }); + }); + + group('mayEmitMultiple', () { + test('succeeds for a stream that emits a matching value', () async { + await check(_countingStream(1)).mayEmitMultiple((it) => it.equals(0)); + }); + test('succeeds for a stream that emits an error', () async { + await check(_countingStream(1, errorAt: 0)) + .mayEmitMultiple((it) => it.equals(0)); + }); + test('succeeds for a stream that closes', () async { + await check(_countingStream(0)).mayEmitMultiple((it) => it.equals(42)); + }); + test('consumes matching events', () async { + final queue = _countingStream(3); + await softCheckAsync<StreamQueue<int>>( + queue, (it) => it.mayEmitMultiple((it) => it.isLessThan(2))); + await check(queue).emits((it) => it.equals(2)); + }); + test('consumes no events if no events match', () async { + final queue = _countingStream(2); + await softCheckAsync<StreamQueue<int>>( + queue, (it) => it.mayEmitMultiple((it) => it.isLessThan(0))); + await check(queue).emits((it) => it.equals(0)); + }); + test('does not consume an error', () async { + final queue = _countingStream(1, errorAt: 0); + await softCheckAsync<StreamQueue<int>>( + queue, (it) => it.mayEmitMultiple((it) => it.equals(0))); + await check(queue).emitsError<UnimplementedError>( + (it) => it.has((e) => e.message, 'message').equals('Error at 1')); + }); + }); + + group('isDone', () { + test('succeeds for an empty stream', () async { + await check(_countingStream(0)).isDone(); + }); + test('fails for a stream that emits a value', () async { + await check(_countingStream(1)).isRejectedByAsync((it) => it.isDone(), + actual: ['a stream'], which: ['emitted an unexpected value: <0>']); + }); + test('fails for a stream that emits an error', () async { + final controller = StreamController<void>(); + controller.addError('sad', StackTrace.fromString('fake trace')); + await check(StreamQueue(controller.stream)).isRejectedByAsync( + (it) => it.isDone(), + actual: ['a stream'], + which: ['emitted an unexpected error: \'sad\'', 'fake trace']); + }); + test('uses a transaction', () async { + final queue = _countingStream(1); + await softCheckAsync<StreamQueue<int>>(queue, (it) => it.isDone()); + await check(queue).emits((it) => it.equals(0)); + }); + test('can be described', () async { + await check((Subject<StreamQueue<int>> it) => it.isDone()) + .hasAsyncDescriptionWhich((it) => it.deepEquals([' is done'])); + }); + }); + + group('emitsAnyOf', () { + test('succeeds for a stream that matches one condition', () async { + await check(_countingStream(1)).anyOf([ + (it) => it.emits((it) => it.equals(42)), + (it) => it.emits((it) => it.equals(0)) + ]); + }); + test('fails for a stream that matches no conditions', () async { + await check(_countingStream(0)).isRejectedByAsync( + (it) => it.anyOf([ + (it) => it.emits(), + (it) => it.emitsThrough((it) => it.equals(1)), + ]), + actual: [ + 'a stream' + ], + which: [ + 'failed to satisfy any condition', + 'failed the condition at index 0 because it:', + ' closed without emitting enough values', + 'failed the condition at index 1 because it:', + ' ended after emitting 0 elements with none matching', + ]); + }); + test('includes nested details for nested failures', () async { + await check(_countingStream(1)).isRejectedByAsync( + (it) => it.anyOf([ + (it) => it.emits((it) => it.equals(42)), + (it) => it.emitsThrough((it) => it.equals(10)), + ]), + actual: [ + 'a stream' + ], + which: [ + 'failed to satisfy any condition', + 'failed the condition at index 0 because it:', + ' emits a value that:', + ' Actual: <0>', + ' Which: are not equal', + 'failed the condition at index 1 because it:', + ' ended after emitting 1 elements with none matching', + ]); + }); + test('gets described with the number of conditions', () async { + await check((Subject<StreamQueue<int>> it) => + it..anyOf([(it) => it.emits(), (it) => it.emits()])) + .hasAsyncDescriptionWhich( + (it) => it.deepEquals([' satisfies any of 2 conditions'])); + }); + test('uses a transaction', () async { + final queue = _countingStream(1); + await softCheckAsync<StreamQueue<int>>( + queue, + (it) => it.anyOf([ + (it) => it.emits((it) => it.equals(10)), + (it) => it.emitsThrough((it) => it.equals(42)), + ])); + await check(queue).emits((it) => it.equals(0)); + }); + test('consumes events', () async { + final queue = _countingStream(3); + await check(queue).anyOf([ + (it) => it.emits((it) => it.equals(1)), + (it) => it.emitsThrough((it) => it.equals(1)) + ]); + await check(queue).emits((it) => it.equals(2)); + }); + }); + }); + + group('StreamQueueWrap', () { + test('can wrap streams in a queue', () async { + await check(Stream.value(1)).withQueue.emits(); + }); + }); +} + +Future<int> _futureSuccess() => Future.microtask(() => 42); + +Future<int> _futureFail() => + Future.error(UnimplementedError(), StackTrace.fromString('fake trace')); + +StreamQueue<int> _countingStream(int count, {int? errorAt}) => StreamQueue( + Stream.fromIterable( + Iterable<int>.generate(count, (index) { + if (index == errorAt) { + Error.throwWithStackTrace(UnimplementedError('Error at $count'), + StackTrace.fromString('fake trace')); + } + return index; + }), + ), + );
diff --git a/pkgs/checks/test/extensions/collection_equality_test.dart b/pkgs/checks/test/extensions/collection_equality_test.dart new file mode 100644 index 0000000..28a10dd --- /dev/null +++ b/pkgs/checks/test/extensions/collection_equality_test.dart
@@ -0,0 +1,197 @@ +// Copyright (c) 2023, 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:checks/checks.dart'; +import 'package:checks/src/collection_equality.dart'; +import 'package:test/scaffolding.dart'; + +void main() { + group('deepCollectionEquals', () { + test('allows nested collections with equal elements', () { + check(deepCollectionEquals([ + 'a', + {'b': 1}, + {'c', 'd'}, + [ + ['e'] + ], + ], [ + 'a', + {'b': 1}, + {'c', 'd'}, + [ + ['e'] + ], + ])).isNull(); + }); + + test('allows collections inside sets', () { + check(deepCollectionEquals({ + {'a': 1} + }, { + {'a': 1} + })).isNull(); + }); + + test('allows collections as Map keys', () { + check(deepCollectionEquals([ + { + {'a': 1}: {'b': 2} + } + ], [ + { + {'a': 1}: {'b': 2} + } + ])).isNull(); + }); + + test('allows conditions in place of elements in lists', () { + check(deepCollectionEquals([ + 'a', + 'b' + ], [ + (Subject<dynamic> it) => it.isA<String>().which((it) => it + ..startsWith('a') + ..length.isLessThan(2)), + (Subject<dynamic> it) => it.isA<String>().startsWith('b') + ])).isNull(); + }); + + test('allows conditions in place of values in maps', () { + check(deepCollectionEquals([ + {'a': 'b'} + ], [ + {'a': (Subject<dynamic> it) => it.isA<String>().startsWith('b')} + ])).isNull(); + }); + + test('allows conditions in place of elements in sets', () { + check(deepCollectionEquals({ + 'b', + 'a' + }, { + 'a', + (Subject<dynamic> it) => it.isA<String>().startsWith('b') + })).isNull(); + }); + + test('allows conditions in place of keys in maps', () { + check(deepCollectionEquals({ + 'a': 'b' + }, { + (Subject<dynamic> it) => it.isA<String>().startsWith('a'): 'b' + })).isNull(); + }); + + test('reports non-Set elements', () { + check(deepCollectionEquals([ + ['a'] + ], [ + {'a'} + ])).isNotNull().deepEquals(['at [<0>] is not a Set']); + }); + + test('reports long iterables', () { + check(deepCollectionEquals([0], [])).isNotNull().deepEquals([ + 'has more elements than expected', + 'expected an iterable with 0 element(s)' + ]); + }); + + test('reports short iterables', () { + check(deepCollectionEquals([], [0])).isNotNull().deepEquals([ + 'has too few elements', + 'expected an iterable with at least 1 element(s)' + ]); + }); + + test('reports unequal elements in iterables', () { + check(deepCollectionEquals([0], [1])) + .isNotNull() + .deepEquals(['at [<0>] is <0>', 'which does not equal <1>']); + }); + + test('reports unmet conditions in iterables', () { + check(deepCollectionEquals( + [0], [(Subject<dynamic> it) => it.isA<int>().isGreaterThan(0)])) + .isNotNull() + .deepEquals([ + 'has an element at [<0>] that:', + ' Actual: <0>', + ' which is not greater than <0>' + ]); + }); + + test('reports unmet conditions in map values', () { + check(deepCollectionEquals({ + 'a': 'b' + }, { + 'a': (Subject<dynamic> it) => it.isA<String>().startsWith('a') + })).isNotNull().deepEquals([ + "has an element at ['a'] that:", + " Actual: 'b'", + " which does not start with 'a'", + ]); + }); + + test('reports unmet conditions in map keys', () { + check(deepCollectionEquals({ + 'b': 'a' + }, { + (Subject<dynamic> it) => it.isA<String>().startsWith('a'): 'a' + })).isNotNull().deepEquals([ + 'has no entry to match <A value that:', + ' is a String', + " starts with 'a'>: 'a'", + ]); + }); + + test('maintains paths through maps when the keys are all values', () { + check(deepCollectionEquals({ + 'a': [ + {'b': 'c'} + ] + }, { + 'a': [ + {'b': 'd'} + ] + })).isNotNull().deepEquals([ + "at ['a'][<0>]['b'] is 'c'", + "which does not equal 'd'", + ]); + }); + + test('reports recursive lists', () { + var l = <Object>[]; + l.add(l); + check(deepCollectionEquals(l, l)) + .isNotNull() + .deepEquals(['exceeds the depth limit of 1000']); + }); + + test('reports recursive sets', () { + var s = <Object>{}; + s.add(s); + check(deepCollectionEquals(s, s)) + .isNotNull() + .deepEquals(['exceeds the depth limit of 1000']); + }); + + test('reports maps with recursive keys', () { + var m = <Object, Object>{}; + m[m] = 0; + check(deepCollectionEquals(m, m)) + .isNotNull() + .deepEquals(['exceeds the depth limit of 1000']); + }); + + test('reports maps with recursive values', () { + var m = <Object, Object>{}; + m[0] = m; + check(deepCollectionEquals(m, m)) + .isNotNull() + .deepEquals(['exceeds the depth limit of 1000']); + }); + }); +}
diff --git a/pkgs/checks/test/extensions/core_test.dart b/pkgs/checks/test/extensions/core_test.dart new file mode 100644 index 0000000..2133c36 --- /dev/null +++ b/pkgs/checks/test/extensions/core_test.dart
@@ -0,0 +1,163 @@ +// 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 'package:checks/checks.dart'; +import 'package:test/scaffolding.dart'; + +import '../test_shared.dart'; + +void main() { + group('TypeChecks', () { + test('isA', () { + check(1).isA<int>(); + + check(1).isRejectedBy((it) => it.isA<String>(), which: ['Is a int']); + }); + }); + group('HasField', () { + test('has', () { + check(1).has((v) => v.isOdd, 'isOdd').isTrue(); + + check(null).isRejectedBy( + (it) => it.has((v) { + Error.throwWithStackTrace( + UnimplementedError(), StackTrace.fromString('fake trace')); + }, 'foo').isNotNull(), + which: [ + 'threw while trying to read foo: <UnimplementedError>', + 'fake trace' + ]); + }); + + test('which', () { + check(true).which((it) => it.isTrue()); + }); + + test('not', () { + check(false).not((it) => it.isTrue()); + check(true).isRejectedBy((it) => it.not((it) => it.isTrue()), which: [ + 'is a value that: ', + ' is true', + ]); + }); + + group('anyOf', () { + test('succeeds for happy case', () { + check(-10) + .anyOf([(it) => it.isGreaterThan(1), (it) => it.isLessThan(-1)]); + }); + test('rejects values that do not satisfy any condition', () { + check(0).isRejectedBy( + (it) => it.anyOf( + [(it) => it.isGreaterThan(1), (it) => it.isLessThan(-1)]), + which: ['did not match any condition']); + }); + }); + }); + + group('BoolChecks', () { + test('isTrue', () { + check(true).isTrue(); + + check(false).isRejectedBy((it) => it.isTrue()); + }); + + test('isFalse', () { + check(false).isFalse(); + + check(true).isRejectedBy((it) => it.isFalse()); + }); + }); + + group('EqualityChecks', () { + test('equals', () { + check(1).equals(1); + + check(1).isRejectedBy((it) => it.equals(2), which: ['are not equal']); + }); + test('identical', () { + check(1).identicalTo(1); + + check(1) + .isRejectedBy((it) => it.identicalTo(2), which: ['is not identical']); + }); + }); + group('NullabilityChecks', () { + test('isNotNull', () { + check(1).isNotNull(); + + check(null).isRejectedBy((it) => it.isNotNull()); + }); + test('isNull', () { + check(null).isNull(); + + check(1).isRejectedBy((it) => it.isNull()); + }); + }); + + group('ComparableChecks on Duration', () { + group('isGreaterThan', () { + test('succeeds for greater', () { + check(const Duration(seconds: 10)) + .isGreaterThan(const Duration(seconds: 1)); + }); + test('fails for equal', () { + check(const Duration(seconds: 10)).isRejectedBy( + (it) => it.isGreaterThan(const Duration(seconds: 10)), + which: ['is not greater than <0:00:10.000000>']); + }); + test('fails for less', () { + check(const Duration(seconds: 10)).isRejectedBy( + (it) => it.isGreaterThan(const Duration(seconds: 50)), + which: ['is not greater than <0:00:50.000000>']); + }); + }); + group('isGreaterOrEqual', () { + test('succeeds for greater', () { + check(const Duration(seconds: 10)) + .isGreaterOrEqual(const Duration(seconds: 1)); + }); + test('succeeds for equal', () { + check(const Duration(seconds: 10)) + .isGreaterOrEqual(const Duration(seconds: 10)); + }); + test('fails for less', () { + check(const Duration(seconds: 10)).isRejectedBy( + (it) => it.isGreaterOrEqual(const Duration(seconds: 50)), + which: ['is not greater than or equal to <0:00:50.000000>']); + }); + }); + group('isLessThan', () { + test('succeeds for less', () { + check(const Duration(seconds: 1)) + .isLessThan(const Duration(seconds: 10)); + }); + test('fails for equal', () { + check(const Duration(seconds: 10)).isRejectedBy( + (it) => it.isLessThan(const Duration(seconds: 10)), + which: ['is not less than <0:00:10.000000>']); + }); + test('fails for greater', () { + check(const Duration(seconds: 50)).isRejectedBy( + (it) => it.isLessThan(const Duration(seconds: 10)), + which: ['is not less than <0:00:10.000000>']); + }); + }); + group('isLessOrEqual', () { + test('succeeds for less', () { + check(const Duration(seconds: 10)) + .isLessOrEqual(const Duration(seconds: 50)); + }); + test('succeeds for equal', () { + check(const Duration(seconds: 10)) + .isLessOrEqual(const Duration(seconds: 10)); + }); + test('fails for greater', () { + check(const Duration(seconds: 10)).isRejectedBy( + (it) => it.isLessOrEqual(const Duration(seconds: 1)), + which: ['is not less than or equal to <0:00:01.000000>']); + }); + }); + }); +}
diff --git a/pkgs/checks/test/extensions/function_test.dart b/pkgs/checks/test/extensions/function_test.dart new file mode 100644 index 0000000..034d8fe --- /dev/null +++ b/pkgs/checks/test/extensions/function_test.dart
@@ -0,0 +1,44 @@ +// 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 'package:checks/checks.dart'; +import 'package:test/scaffolding.dart'; + +import '../test_shared.dart'; + +void main() { + group('ThrowsChecks', () { + group('throws', () { + test('succeeds for happy case', () { + check(() => throw StateError('oops!')).throws<StateError>(); + }); + test('fails for functions that return normally', () { + check(() {}).isRejectedBy((it) => it.throws<StateError>(), + actual: ['a function that returned <null>'], + which: ['did not throw']); + }); + test('fails for functions that throw the wrong type', () { + check(() => throw StateError('oops!')).isRejectedBy( + (it) => it.throws<ArgumentError>(), + actual: ['a function that threw error <Bad state: oops!>'], + which: ['did not throw an ArgumentError'], + ); + }); + }); + + group('returnsNormally', () { + test('succeeds for happy case', () { + check(() => 1).returnsNormally().equals(1); + }); + test('fails for functions that throw', () { + check(() { + Error.throwWithStackTrace( + StateError('oops!'), StackTrace.fromString('fake trace')); + }).isRejectedBy((it) => it.returnsNormally(), + actual: ['a function that throws'], + which: ['threw <Bad state: oops!>', 'fake trace']); + }); + }); + }); +}
diff --git a/pkgs/checks/test/extensions/iterable_test.dart b/pkgs/checks/test/extensions/iterable_test.dart new file mode 100644 index 0000000..dafd84d --- /dev/null +++ b/pkgs/checks/test/extensions/iterable_test.dart
@@ -0,0 +1,275 @@ +// 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 'package:checks/checks.dart'; +import 'package:test/scaffolding.dart'; + +import '../test_shared.dart'; + +Iterable<int> get _testIterable => Iterable.generate(2, (i) => i); + +void main() { + test('length', () { + check(_testIterable).length.equals(2); + }); + + group('first', () { + test('succeeds for happy case', () { + check(_testIterable).first.equals(0); + }); + test('rejects empty iterable', () { + check(<Object>[]) + .isRejectedBy((it) => it.first.equals(0), which: ['has no elements']); + }); + }); + + group('last', () { + test('succeeds for happy case', () { + check(_testIterable).last.equals(1); + }); + test('rejects empty iterable', () { + check(<Object>[]) + .isRejectedBy((it) => it.last.equals(0), which: ['has no elements']); + }); + }); + + group('single', () { + test('succeeds for happy case', () { + check([42]).single.equals(42); + }); + test('rejects empty iterable', () { + check(<Object>[]).isRejectedBy((it) => it.single.equals(0), + which: ['has no elements']); + }); + test('rejects iterable with too many elements', () { + check(_testIterable).isRejectedBy((it) => it.single.equals(0), + which: ['has more than one element']); + }); + }); + + test('isEmpty', () { + check(<Object>[]).isEmpty(); + check(_testIterable) + .isRejectedBy((it) => it.isEmpty(), which: ['is not empty']); + }); + + test('isNotEmpty', () { + check(_testIterable).isNotEmpty(); + check(const Iterable<int>.empty()) + .isRejectedBy((it) => it.isNotEmpty(), which: ['is empty']); + }); + + test('contains', () { + check(_testIterable).contains(0); + check(_testIterable) + .isRejectedBy((it) => it.contains(2), which: ['does not contain <2>']); + }); + test('any', () { + check(_testIterable).any((it) => it.equals(1)); + check(_testIterable).isRejectedBy((it) => it.any((it) => it.equals(2)), + which: ['Contains no matching element']); + }); + + group('containsInOrder', () { + test('succeeds for happy case', () { + check([0, 1, 0, 2, 0, 3]).containsInOrder([1, 2, 3]); + }); + test('can use Condition<dynamic>', () { + check([0, 1]).containsInOrder( + [(Subject<dynamic> it) => it.isA<int>().isGreaterThan(0)]); + }); + test('can use Condition<T>', () { + check([0, 1]).containsInOrder([(Subject<int> it) => it.isGreaterThan(0)]); + }); + test('fails for not found elements by equality', () async { + check([0]).isRejectedBy((it) => it.containsInOrder([1]), which: [ + 'did not have an element matching the expectation at index 0 <1>' + ]); + }); + test('fails for not found elements by condition', () async { + check([0]).isRejectedBy( + (it) => it.containsInOrder( + [(Subject<dynamic> it) => it.isA<int>().isGreaterThan(0)]), + which: [ + 'did not have an element matching the expectation at index 0 ' + '<A value that:', + ' is a int', + ' is greater than <0>>' + ]); + }); + test('can be described', () { + check((Subject<Iterable> it) => it.containsInOrder([1, 2, 3])) + .description + .deepEquals([' contains, in order: [1, 2, 3]']); + check((Subject<Iterable> it) => + it.containsInOrder([1, (Subject<dynamic> it) => it.equals(2)])) + .description + .deepEquals([ + ' contains, in order: [1,', + ' <A value that:', + ' equals <2>>]' + ]); + }); + }); + + group('containsMatchingInOrder', () { + test('succeeds for happy case', () { + check([0, 1, 0, 2, 0, 3]).containsMatchingInOrder([ + (it) => it.isLessThan(2), + (it) => it.isLessThan(3), + (it) => it.isLessThan(4), + ]); + }); + test('fails for not found elements', () async { + check([0]).isRejectedBy( + (it) => it.containsMatchingInOrder([(it) => it.isGreaterThan(0)]), + which: [ + 'did not have an element matching the expectation at index 0 ' + '<A value that:', + ' is greater than <0>>' + ]); + }); + test('can be described', () { + check((Subject<Iterable<int>> it) => it.containsMatchingInOrder([ + (it) => it.isLessThan(2), + (it) => it.isLessThan(3), + (it) => it.isLessThan(4), + ])).description.deepEquals([ + ' contains, in order: [<A value that:', + ' is less than <2>>,', + ' <A value that:', + ' is less than <3>>,', + ' <A value that:', + ' is less than <4>>]', + ]); + check((Subject<Iterable<int>> it) => it.containsMatchingInOrder( + [(it) => it.equals(1), (it) => it.equals(2)])) + .description + .deepEquals([ + ' contains, in order: [<A value that:', + ' equals <1>>,', + ' <A value that:', + ' equals <2>>]' + ]); + }); + }); + + group('containsEqualInOrder', () { + test('succeeds for happy case', () { + check([0, 1, 0, 2, 0, 3]).containsEqualInOrder([1, 2, 3]); + }); + test('fails for not found elements', () async { + check([0]).isRejectedBy((it) => it.containsEqualInOrder([1]), which: [ + 'did not have an element equal to the expectation at index 0 <1>' + ]); + }); + test('can be described', () { + check((Subject<Iterable<int>> it) => it.containsEqualInOrder([1, 2, 3])) + .description + .deepEquals([' contains, in order: [1, 2, 3]']); + check((Subject<Iterable<int>> it) => it.containsEqualInOrder([1, 2])) + .description + .deepEquals([ + ' contains, in order: [1, 2]', + ]); + }); + }); + group('every', () { + test('succeeds for the happy path', () { + check(_testIterable).every((it) => it.isGreaterOrEqual(-1)); + }); + + test('includes details of first failing element', () async { + check(_testIterable) + .isRejectedBy((it) => it.every((it) => it.isLessThan(0)), which: [ + 'has an element at index 0 that:', + ' Actual: <0>', + ' Which: is not less than <0>', + ]); + }); + }); + + group('unorderedEquals', () { + test('success for happy case', () { + check(_testIterable).unorderedEquals(_testIterable.toList().reversed); + }); + + test('reports unmatched elements', () { + check(_testIterable).isRejectedBy( + (it) => it.unorderedEquals(_testIterable.followedBy([42, 100])), + which: [ + 'has no element equal to the expected element at index 2: <42>', + 'or 1 other elements' + ]); + }); + + test('reports unexpected elements', () { + check(_testIterable.followedBy([42, 100])) + .isRejectedBy((it) => it.unorderedEquals(_testIterable), which: [ + 'has an unexpected element at index 2: <42>', + 'and 1 other unexpected elements' + ]); + }); + }); + + group('unorderedMatches', () { + test('success for happy case', () { + check(_testIterable).unorderedMatches( + _testIterable.toList().reversed.map((i) => (it) => it.equals(i))); + }); + + test('reports unmatched elements', () { + check(_testIterable).isRejectedBy( + (it) => it.unorderedMatches(_testIterable + .followedBy([42, 100]).map((i) => (it) => it.equals(i))), + which: [ + 'has no element matching the condition at index 2:', + ' equals <42>', + 'or 1 other conditions' + ]); + }); + + test('reports unexpected elements', () { + check(_testIterable.followedBy([42, 100])).isRejectedBy( + (it) => it + .unorderedMatches(_testIterable.map((i) => (it) => it.equals(i))), + which: [ + 'has an unmatched element at index 2: <42>', + 'and 1 other unmatched elements' + ]); + }); + }); + + group('pairwiseMatches', () { + test('succeeds for the happy path', () { + check(_testIterable).pairwiseMatches([1, 2], + (expected) => (it) => it.isLessThan(expected), 'is less than'); + }); + test('fails for mismatched element', () async { + check(_testIterable).isRejectedBy( + (it) => it.pairwiseMatches([1, 1], + (expected) => (it) => it.isLessThan(expected), 'is less than'), + which: [ + 'does not have an element at index 1 that:', + ' is less than <1>', + 'Actual element at index 1: <1>', + 'Which: is not less than <1>' + ]); + }); + test('fails for too few elements', () { + check(_testIterable).isRejectedBy( + (it) => it.pairwiseMatches([1, 2, 3], + (expected) => (it) => it.isLessThan(expected), 'is less than'), + which: [ + 'has too few elements, there is no element to match at index 2' + ]); + }); + test('fails for too many elements', () { + check(_testIterable).isRejectedBy( + (it) => it.pairwiseMatches([1], + (expected) => (it) => it.isLessThan(expected), 'is less than'), + which: ['has too many elements, expected exactly 1']); + }); + }); +}
diff --git a/pkgs/checks/test/extensions/map_test.dart b/pkgs/checks/test/extensions/map_test.dart new file mode 100644 index 0000000..cb3e3e4 --- /dev/null +++ b/pkgs/checks/test/extensions/map_test.dart
@@ -0,0 +1,119 @@ +// 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 'package:checks/checks.dart'; +import 'package:test/scaffolding.dart'; + +import '../test_shared.dart'; + +const _testMap = { + 'a': 1, + 'b': 2, +}; + +void main() { + test('length', () { + check(_testMap).length.equals(2); + }); + test('entries', () { + check(_testMap).entries.any( + (it) => it + ..has((p0) => p0.key, 'key').equals('a') + ..has((p0) => p0.value, 'value').equals(1), + ); + }); + test('keys', () { + check(_testMap).keys.contains('a'); + }); + test('values', () { + check(_testMap).values.contains(1); + }); + + group('operator []', () { + test('succeeds for a key that exists', () { + check(_testMap)['a'].equals(1); + }); + test('fails for a missing key', () { + check(_testMap).isRejectedBy((it) => it['z'], + which: ["does not contain the key 'z'"]); + }); + test('can be described', () { + check((Subject<Map<String, Object>> it) => it['some\nlong\nkey']) + .description + .deepEquals([ + " contains a value for 'some", + ' long', + " key'", + ]); + check((Subject<Map<String, Object>> it) => + it['some\nlong\nkey'].equals(1)).description.deepEquals([ + " contains a value for 'some", + ' long', + " key' that:", + ' equals <1>', + ]); + }); + }); + test('isEmpty', () { + check(<String, int>{}).isEmpty(); + check(_testMap).isRejectedBy((it) => it.isEmpty(), which: ['is not empty']); + }); + test('isNotEmpty', () { + check(_testMap).isNotEmpty(); + check(<Object, Object>{}) + .isRejectedBy((it) => it.isNotEmpty(), which: ['is not empty']); + }); + group('containsKey', () { + test('succeeds for a key that exists', () { + check(_testMap).containsKey('a'); + }); + test('fails for a missing key', () { + check(_testMap).isRejectedBy( + (it) => it.containsKey('c'), + which: ["does not contain key 'c'"], + ); + }); + test('can be described', () { + check((Subject<Map<String, Object>> it) => + it.containsKey('some\nlong\nkey')).description.deepEquals([ + " contains key 'some", + ' long', + " key'", + ]); + }); + }); + test('containsKeyThat', () { + check(_testMap).containsKeyThat((it) => it.equals('a')); + check(_testMap).isRejectedBy( + (it) => it.containsKeyThat((it) => it.equals('c')), + which: ['Contains no matching key'], + ); + }); + group('containsValue', () { + test('succeeds for happy case', () { + check(_testMap).containsValue(1); + }); + test('fails for missing value', () { + check(_testMap).isRejectedBy( + (it) => it.containsValue(3), + which: ['does not contain value <3>'], + ); + }); + test('can be described', () { + check((Subject<Map<String, String>> it) => + it.containsValue('some\nlong\nkey')).description.deepEquals([ + " contains value 'some", + ' long', + " key'", + ]); + }); + }); + test('containsValueThat', () { + check(_testMap).containsValueThat((it) => it.equals(1)); + check(_testMap).isRejectedBy( + (it) => it.containsValueThat((it) => it.equals(3)), + which: ['Contains no matching value'], + ); + }); +}
diff --git a/pkgs/checks/test/extensions/math_test.dart b/pkgs/checks/test/extensions/math_test.dart new file mode 100644 index 0000000..0f346d3 --- /dev/null +++ b/pkgs/checks/test/extensions/math_test.dart
@@ -0,0 +1,149 @@ +// 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 'package:checks/checks.dart'; +import 'package:test/scaffolding.dart'; + +import '../test_shared.dart'; + +void main() { + group('num checks', () { + group('isNaN', () { + test('succeeds for happy case', () { + check(double.nan).isNaN(); + }); + test('fails for ints', () { + check(42).isRejectedBy((it) => it.isNaN(), which: ['is a number']); + }); + test('fails for numeric doubles', () { + check(42.1).isRejectedBy((it) => it.isNaN(), which: ['is a number']); + }); + }); + + group('isNotNan', () { + test('succeeds for ints', () { + check(42).isNotNaN(); + }); + test('succeeds numeric doubles', () { + check(42.1).isNotNaN(); + }); + test('fails for NaN', () { + check(double.nan).isRejectedBy((it) => it.isNotNaN(), + which: ['is not a number (NaN)']); + }); + }); + group('isNegative', () { + test('succeeds for negative ints', () { + check(-1).isNegative(); + }); + test('succeeds for -0.0', () { + check(-0.0).isNegative(); + }); + test('fails for zero', () { + check(0) + .isRejectedBy((it) => it.isNegative(), which: ['is not negative']); + }); + }); + group('isNotNegative', () { + test('succeeds for positive ints', () { + check(1).isNotNegative(); + }); + test('succeeds for 0', () { + check(0).isNotNegative(); + }); + test('fails for -0.0', () { + check(-0.0) + .isRejectedBy((it) => it.isNotNegative(), which: ['is negative']); + }); + test('fails for negative numbers', () { + check(-1) + .isRejectedBy((it) => it.isNotNegative(), which: ['is negative']); + }); + }); + + group('isFinite', () { + test('succeeds for finite numbers', () { + check(1).isFinite(); + }); + test('fails for NaN', () { + check(double.nan) + .isRejectedBy((it) => it.isFinite(), which: ['is not finite']); + }); + test('fails for infinity', () { + check(double.infinity) + .isRejectedBy((it) => it.isFinite(), which: ['is not finite']); + }); + test('fails for negative infinity', () { + check(double.negativeInfinity) + .isRejectedBy((it) => it.isFinite(), which: ['is not finite']); + }); + }); + group('isNotFinite', () { + test('succeeds for infinity', () { + check(double.infinity).isNotFinite(); + }); + test('succeeds for negative infinity', () { + check(double.negativeInfinity).isNotFinite(); + }); + test('succeeds for NaN', () { + check(double.nan).isNotFinite(); + }); + test('fails for finite numbers', () { + check(1).isRejectedBy((it) => it.isNotFinite(), which: ['is finite']); + }); + }); + group('isInfinite', () { + test('succeeds for infinity', () { + check(double.infinity).isInfinite(); + }); + test('succeeds for negative infinity', () { + check(double.negativeInfinity).isInfinite(); + }); + test('fails for NaN', () { + check(double.nan) + .isRejectedBy((it) => it.isInfinite(), which: ['is not infinite']); + }); + test('fails for finite numbers', () { + check(1) + .isRejectedBy((it) => it.isInfinite(), which: ['is not infinite']); + }); + }); + + group('isNotInfinite', () { + test('succeeds for finite numbers', () { + check(1).isNotInfinite(); + }); + test('succeeds for NaN', () { + check(double.nan).isNotInfinite(); + }); + test('fails for infinity', () { + check(double.infinity) + .isRejectedBy((it) => it.isNotInfinite(), which: ['is infinite']); + }); + test('fails for negative infinity', () { + check(double.negativeInfinity) + .isRejectedBy((it) => it.isNotInfinite(), which: ['is infinite']); + }); + }); + group('closeTo', () { + test('succeeds for equal numbers', () { + check(1).isCloseTo(1, 1); + }); + test('succeeds at less than delta away', () { + check(1).isCloseTo(2, 2); + }); + test('succeeds at exactly delta away', () { + check(1).isCloseTo(2, 1); + }); + test('fails for low values', () { + check(1).isRejectedBy((it) => it.isCloseTo(3, 1), + which: ['differs by <2>']); + }); + test('fails for high values', () { + check(5).isRejectedBy((it) => it.isCloseTo(3, 1), + which: ['differs by <2>']); + }); + }); + }); +}
diff --git a/pkgs/checks/test/extensions/string_test.dart b/pkgs/checks/test/extensions/string_test.dart new file mode 100644 index 0000000..2b651f1 --- /dev/null +++ b/pkgs/checks/test/extensions/string_test.dart
@@ -0,0 +1,200 @@ +// 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 'package:checks/checks.dart'; +import 'package:test/scaffolding.dart'; + +import '../test_shared.dart'; + +void main() { + group('StringChecks', () { + test('contains', () { + check('bob').contains('bo'); + check('bob').isRejectedBy((it) => it.contains('kayleb'), + which: ["Does not contain 'kayleb'"]); + }); + test('length', () { + check('bob').length.equals(3); + }); + test('isEmpty', () { + check('').isEmpty(); + check('bob').isRejectedBy((it) => it.isEmpty(), which: ['is not empty']); + }); + test('isNotEmpty', () { + check('bob').isNotEmpty(); + check('').isRejectedBy((it) => it.isNotEmpty(), which: ['is empty']); + }); + test('startsWith', () { + check('bob').startsWith('bo'); + check('bob').isRejectedBy((it) => it.startsWith('kayleb'), + which: ["does not start with 'kayleb'"]); + }); + test('endsWith', () { + check('bob').endsWith('ob'); + check('bob').isRejectedBy((it) => it.endsWith('kayleb'), + which: ["does not end with 'kayleb'"]); + }); + + group('matches', () { + test('succeeds for strings that match a regex', () { + check('123').matchesPattern(RegExp(r'\d\d\d')); + }); + test('succeeds for strings that match a string pattern', () { + check(r'\d').matchesPattern(r'\d'); + }); + test('fails for non-matching regex', () { + check('abc').isRejectedBy((it) => it.matchesPattern(RegExp(r'\d\d\d')), + which: [r'does not match <RegExp: pattern=\d\d\d flags=>']); + }); + test('fails for non-matching string pattern', () { + // A string is _not_ converted to a regex, string patterns must match + // directly. + check('123').isRejectedBy((it) => it.matchesPattern(r'\d\d\d'), + which: [r"does not match '\\d\\d\\d'"]); + }); + test('can be described', () { + check((Subject<String> it) => it.matchesPattern(RegExp(r'\d\d\d'))) + .description + .deepEquals([r' matches <RegExp: pattern=\d\d\d flags=>']); + check((Subject<String> it) => it.matchesPattern('abc')) + .description + .deepEquals([r" matches 'abc'"]); + }); + }); + + group('containsInOrder', () { + test('happy case', () { + check('foo bar baz').containsInOrder(['foo', 'baz']); + }); + test('reports when first substring is missing', () { + check('baz').isRejectedBy((it) => it.containsInOrder(['foo', 'baz']), + which: ['does not have a match for the substring \'foo\'']); + }); + test('reports when substring is missing following a match', () { + check('foo bar') + .isRejectedBy((it) => it.containsInOrder(['foo', 'baz']), which: [ + 'does not have a match for the substring \'baz\'', + 'following the other matches up to character 3' + ]); + }); + }); + + group('equals', () { + test('succeeeds for happy case', () { + check('foo').equals('foo'); + }); + test('succeeeds for equal empty strings', () { + check('').equals(''); + }); + test('reports extra characters for long string', () { + check('foobar').isRejectedBy((it) => it.equals('foo'), + which: ['is too long with unexpected trailing characters:', 'bar']); + }); + test('reports extra characters for long string against empty', () { + check('foo').isRejectedBy((it) => it.equals(''), + which: ['is not the empty string']); + }); + test('reports truncated extra characters for very long string', () { + check('foobar baz more stuff').isRejectedBy((it) => it.equals('foo'), + which: [ + 'is too long with unexpected trailing characters:', + 'bar baz mo ...' + ]); + }); + test('reports missing characters for short string', () { + check('foo').isRejectedBy((it) => it.equals('foobar'), + which: ['is too short with missing trailing characters:', 'bar']); + }); + test('reports missing characters for empty string', () { + check('').isRejectedBy((it) => it.equals('foo bar baz'), + actual: ['an empty string'], + which: ['is missing all expected characters:', 'foo bar ba ...']); + }); + test('reports truncated missing characters for very short string', () { + check('foo').isRejectedBy((it) => it.equals('foobar baz more stuff'), + which: [ + 'is too short with missing trailing characters:', + 'bar baz mo ...' + ]); + }); + test('reports index of different character', () { + check('hit').isRejectedBy((it) => it.equals('hat'), which: [ + 'differs at offset 1:', + 'hat', + 'hit', + ' ^', + ]); + }); + test('reports truncated index of different character in large string', + () { + check('blah blah blah hit blah blah blah').isRejectedBy( + (it) => it.equals('blah blah blah hat blah blah blah'), + which: [ + 'differs at offset 16:', + '... lah blah hat blah bl ...', + '... lah blah hit blah bl ...', + ' ^', + ]); + }); + }); + + group('equalsIgnoringCase', () { + test('succeeeds for happy case', () { + check('FOO').equalsIgnoringCase('foo'); + check('foo').equalsIgnoringCase('FOO'); + }); + test('reports original extra characters for long string', () { + check('FOOBAR').isRejectedBy((it) => it.equalsIgnoringCase('foo'), + which: ['is too long with unexpected trailing characters:', 'BAR']); + }); + test('reports original missing characters for short string', () { + check('FOO').isRejectedBy((it) => it.equalsIgnoringCase('fooBAR'), + which: ['is too short with missing trailing characters:', 'BAR']); + }); + test('reports index of different character with original characters', () { + check('HiT').isRejectedBy((it) => it.equalsIgnoringCase('hAt'), which: [ + 'differs at offset 1:', + 'hAt', + 'HiT', + ' ^', + ]); + }); + }); + + group('equalsIgnoringWhitespace', () { + test('allows differing internal whitespace', () { + check('foo \t\n bar').equalsIgnoringWhitespace('foo bar'); + }); + test('allows extra leading/trailing whitespace', () { + check(' foo ').equalsIgnoringWhitespace('foo'); + }); + test('allows missing leading/trailing whitespace', () { + check('foo').equalsIgnoringWhitespace(' foo '); + }); + test('reports original extra characters for long string', () { + check('foo \t bar \n baz').isRejectedBy( + (it) => it.equalsIgnoringWhitespace('foo bar'), + which: [ + 'is too long with unexpected trailing characters:', + ' baz' + ]); + }); + test('reports original missing characters for short string', () { + check('foo bar').isRejectedBy( + (it) => it.equalsIgnoringWhitespace('foo bar baz'), + which: ['is too short with missing trailing characters:', ' baz']); + }); + test('reports index of different character with original characters', () { + check('x hit x').isRejectedBy( + (it) => it.equalsIgnoringWhitespace('x hat x'), + which: [ + 'differs at offset 3:', + 'x hat x', + 'x hit x', + ' ^', + ]); + }); + }); + }); +}
diff --git a/pkgs/checks/test/failure_message_test.dart b/pkgs/checks/test/failure_message_test.dart new file mode 100644 index 0000000..ee5401c --- /dev/null +++ b/pkgs/checks/test/failure_message_test.dart
@@ -0,0 +1,53 @@ +import 'package:checks/checks.dart'; +import 'package:test/scaffolding.dart'; +import 'package:test_api/hooks.dart' show TestFailure; + +void main() { + group('failures', () { + test('includes expected, actual, and which', () { + check(() { + check(1).isGreaterThan(2); + }).throwsFailure().equals(''' +Expected: a int that: + is greater than <2> +Actual: <1> +Which: is not greater than <2>'''); + }); + + test('includes matching portions of actual', () { + check(() { + check(<dynamic>[]).length.equals(1); + }).throwsFailure().equals(''' +Expected: a List<dynamic> that: + has length that: + equals <1> +Actual: a List<dynamic> that: + has length that: + Actual: <0> + Which: are not equal'''); + }); + + test('include a reason when provided', () { + check(() { + check(because: 'Some reason', 1).isGreaterThan(2); + }).throwsFailure().endsWith('Reason: Some reason'); + }); + + test('retain type label following isNotNull', () { + check(() { + check<int?>(1).isNotNull().isGreaterThan(2); + }).throwsFailure().startsWith('Expected: a int? that:\n'); + }); + + test('retain reason following isNotNull', () { + check(() { + check<int?>(because: 'Some reason', 1).isNotNull().isGreaterThan(2); + }).throwsFailure().endsWith('Reason: Some reason'); + }); + }); +} + +extension on Subject<void Function()> { + Subject<String> throwsFailure() => + throws<TestFailure>().has((f) => f.message, 'message').isNotNull(); +}
diff --git a/pkgs/checks/test/soft_check_test.dart b/pkgs/checks/test/soft_check_test.dart new file mode 100644 index 0000000..d919b0d --- /dev/null +++ b/pkgs/checks/test/soft_check_test.dart
@@ -0,0 +1,30 @@ +// Copyright (c) 2023, 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:checks/checks.dart'; +import 'package:test/scaffolding.dart'; + +import 'test_shared.dart'; + +void main() { + group('softCheck', () { + test('returns the first failure', () { + check(0).isRejectedBy( + (it) => it + ..isGreaterThan(1) + ..isGreaterThan(2), + which: ['is not greater than <1>']); + }); + }); + group('softCheckAsync', () { + test('returns the first failure', () async { + await check(Future.value(0)).isRejectedByAsync( + (it) => it + ..completes((it) => it.isGreaterThan(1)) + ..completes((it) => it.isGreaterThan(2)), + actual: ['<0>'], + which: ['is not greater than <1>']); + }); + }); +}
diff --git a/pkgs/checks/test/test_shared.dart b/pkgs/checks/test/test_shared.dart new file mode 100644 index 0000000..87401ca --- /dev/null +++ b/pkgs/checks/test/test_shared.dart
@@ -0,0 +1,92 @@ +// 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:async'; + +import 'package:checks/checks.dart'; +import 'package:checks/context.dart'; + +extension RejectionChecks<T> on Subject<T> { + void isRejectedBy(Condition<T> condition, + {Iterable<String>? actual, Iterable<String>? which}) { + late T actualValue; + var didRunCallback = false; + final rejection = context.nest<Rejection>( + () => ['does not meet a condition with a Rejection'], (value) { + actualValue = value; + didRunCallback = true; + final failure = softCheck(value, condition); + if (failure == null) { + return Extracted.rejection(which: [ + 'was accepted by the condition checking:', + ...describe(condition) + ]); + } + return Extracted.value(failure.rejection); + }); + if (didRunCallback) { + rejection + .has((r) => r.actual, 'actual') + .deepEquals(actual ?? literal(actualValue)); + } else { + rejection + .has((r) => r.actual, 'actual') + .context + .expect(() => ['is left default'], (_) => null); + } + if (which == null) { + rejection.has((r) => r.which, 'which').isNull(); + } else { + rejection.has((r) => r.which, 'which').isNotNull().deepEquals(which); + } + } + + Future<void> isRejectedByAsync(Condition<T> condition, + {Iterable<String>? actual, Iterable<String>? which}) async { + late T actualValue; + var didRunCallback = false; + await context.nestAsync<Rejection>( + () => ['does not meet an async condition with a Rejection'], + (value) async { + actualValue = value; + didRunCallback = true; + final failure = await softCheckAsync(value, condition); + if (failure == null) { + return Extracted.rejection(which: [ + 'was accepted by the condition checking:', + ...await describeAsync(condition) + ]); + } + return Extracted.value(failure.rejection); + }, (rejection) { + if (didRunCallback) { + rejection + .has((r) => r.actual, 'actual') + .deepEquals(actual ?? literal(actualValue)); + } else { + rejection + .has((r) => r.actual, 'actual') + .context + .expect(() => ['is left default'], (_) => null); + } + if (which == null) { + rejection.has((r) => r.which, 'which').isNull(); + } else { + rejection.has((r) => r.which, 'which').isNotNull().deepEquals(which); + } + }); + } +} + +extension ConditionChecks<T> on Subject<Condition<T>> { + Subject<Iterable<String>> get description => + has((c) => describe<T>(c), 'description'); + Future<void> hasAsyncDescriptionWhich( + Condition<Iterable<String>> descriptionCondition) => + context.nestAsync( + () => ['has description'], + (condition) async => + Extracted.value(await describeAsync<T>(condition)), + descriptionCondition); +}
diff --git a/pkgs/fake_async/.gitignore b/pkgs/fake_async/.gitignore new file mode 100644 index 0000000..63fe85d --- /dev/null +++ b/pkgs/fake_async/.gitignore
@@ -0,0 +1,5 @@ +.packages +.pub/ +.dart_tool/ +build/ +pubspec.lock
diff --git a/pkgs/fake_async/AUTHORS b/pkgs/fake_async/AUTHORS new file mode 100644 index 0000000..e8063a8 --- /dev/null +++ b/pkgs/fake_async/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 <email address> + +Google Inc.
diff --git a/pkgs/fake_async/CHANGELOG.md b/pkgs/fake_async/CHANGELOG.md new file mode 100644 index 0000000..0f964ee --- /dev/null +++ b/pkgs/fake_async/CHANGELOG.md
@@ -0,0 +1,102 @@ +## 1.3.2 + +* Require Dart 3.3 +* Fix bug where a `flushTimers` or `elapse` call from within + the callback of a periodic timer would immediately invoke + the same timer. +* Move to `dart-lang/test` monorepo. +* Require Dart 3.5. + +## 1.3.1 + +* Populate the pubspec `repository` field. + +## 1.3.0 + +* `FakeTimer.tick` will return a value instead of throwing. +* `FakeAsync.includeTimerStackTrace` allows controlling whether timers created + with a FakeAsync will include a creation Stack Trace. + +## 1.2.0 + +* Stable release for null safety. + +## 1.2.0-nullsafety.3 + +* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release + guidelines. + +## 1.2.0-nullsafety.2 + +* Allow prerelease versions of the 2.12 sdk. + +## 1.2.0-nullsafety.1 + +* Allow 2.10 stable and 2.11.0 dev SDK versions. + +## 1.2.0-nullsafety + +Pre-release for the null safety migration of this package. + +Note that `1.2.0` may not be the final stable null safety release version, +we reserve the right to release it as a `2.0.0` breaking change. + +This release will be pinned to only allow pre-release sdk versions starting +from `2.10.0-0`. + +## 1.1.0 + +* Exposed the `FakeTimer` class as a public class. +* Added `FakeAsync.pendingTimers` which gives access to all pending timers at + the time of the call. + +## 1.0.2 + +* Update min SDK to 2.2.0 + +## 1.0.1 + +* Update to lowercase Dart core library constants. +* Fix use of deprecated `isInstanceOf` matcher. + +## 1.0.0 + +This release contains the `FakeAsync` class that was defined in [`quiver`][]. +It's backwards-compatible with both the `quiver` version *and* the old version +of the `fake_async` package. + +[`quiver`]: https://pub.dev/packages/quiver + +### New Features + +* A top-level `fakeAsync()` function was added that encapsulates + `new FakeAsync().run(...)`. + +### New Features Relative to `quiver` + +* `FakeAsync.elapsed` returns the total amount of fake time elapsed since the + `FakeAsync` instance was created. + +* `new FakeAsync()` now takes an `initialTime` argument that sets the default + time for clocks created with `FakeAsync.getClock()`, and for the `clock` + package's top-level `clock` variable. + +### New Features Relative to `fake_async` 0.1 + +* `FakeAsync.periodicTimerCount`, `FakeAsync.nonPeriodicTimerCount`, and + `FakeAsync.microtaskCount` provide visibility into the events scheduled within + `FakeAsync.run()`. + +* `FakeAsync.getClock()` provides access to fully-featured `Clock` objects based + on `FakeAsync`'s elapsed time. + +* `FakeAsync.flushMicrotasks()` empties the microtask queue without elapsing any + time or running any timers. + +* `FakeAsync.flushTimers()` runs all microtasks and timers until there are no + more scheduled. + +## 0.1.2 + +* Integrate with the clock package. +
diff --git a/pkgs/fake_async/LICENSE b/pkgs/fake_async/LICENSE new file mode 100644 index 0000000..7a4a3ea --- /dev/null +++ b/pkgs/fake_async/LICENSE
@@ -0,0 +1,202 @@ + + 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 [yyyy] [name of copyright owner] + + 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. \ No newline at end of file
diff --git a/pkgs/fake_async/README.md b/pkgs/fake_async/README.md new file mode 100644 index 0000000..c7bb40b --- /dev/null +++ b/pkgs/fake_async/README.md
@@ -0,0 +1,51 @@ +[](https://pub.dev/packages/fake_async) +[](https://pub.dev/packages/fake_async/publisher) + +This package provides a [`FakeAsync`][] class, which makes it easy to +deterministically test code that uses asynchronous features like `Future`s, +`Stream`s, `Timer`s, and microtasks. It creates an environment in which the user +can explicitly control Dart's notion of the "current time". When the time is +advanced, `FakeAsync` fires all asynchronous events that are scheduled for that +time period without actually needing the test to wait for real time to elapse. + +[`FakeAsync`]: https://www.dartdocs.org/documentation/fake_async/latest/fake_async/FakeAsync-class.html + +For example: + +```dart +import 'dart:async'; + +import 'package:fake_async/fake_async.dart'; +import 'package:test/test.dart'; + +void main() { + test("Future.timeout() throws an error once the timeout is up", () { + // Any code run within [fakeAsync] is run within the context of the + // [FakeAsync] object passed to the callback. + fakeAsync((async) { + // All asynchronous features that rely on timing are automatically + // controlled by [fakeAsync]. + expect(Completer().future.timeout(Duration(seconds: 5)), + throwsA(isA<TimeoutException>())); + + // This will cause the timeout above to fire immediately, without waiting + // 5 seconds of real time. + async.elapse(Duration(seconds: 5)); + }); + }); +} +``` + +## Integration With `clock` + +`FakeAsync` can't control the time reported by [`DateTime.now()`][] or by +the [`Stopwatch`][] class, since they're not part of `dart:async`. However, if +you create them using the [`clock`][] package's [`clock.now()`][] or +[`clock.stopwatch()`][] functions, `FakeAsync` will automatically override +them to use the same notion of time as `dart:async` classes. + +[`DateTime.now()`]: https://api.dart.dev/stable/dart-core/DateTime/DateTime.now.html +[`Stopwatch`]: https://api.dart.dev/stable/dart-core/Stopwatch-class.html +[`clock`]: https://pub.dev/packages/clock +[`clock.now()`]: https://pub.dev/documentation/clock/latest/clock/Clock/now.html +[`clock.stopwatch()`]: https://pub.dev/documentation/clock/latest/clock/Clock/stopwatch.html
diff --git a/pkgs/fake_async/analysis_options.yaml b/pkgs/fake_async/analysis_options.yaml new file mode 100644 index 0000000..4e1b8e8 --- /dev/null +++ b/pkgs/fake_async/analysis_options.yaml
@@ -0,0 +1,21 @@ +include: package:dart_flutter_team_lints/analysis_options.yaml +analyzer: + language: + strict-casts: true + +linter: + rules: + - avoid_bool_literals_in_conditional_expressions + - avoid_classes_with_only_static_members + - avoid_returning_this + - avoid_unused_constructor_parameters + - cancel_subscriptions + - cascade_invocations + - comment_references + - join_return_with_assignment + - literal_only_boolean_expressions + - no_adjacent_strings_in_list + - prefer_const_constructors + - prefer_final_locals + - test_types_in_equals + - unnecessary_await_in_return
diff --git a/pkgs/fake_async/lib/fake_async.dart b/pkgs/fake_async/lib/fake_async.dart new file mode 100644 index 0000000..b4eea85 --- /dev/null +++ b/pkgs/fake_async/lib/fake_async.dart
@@ -0,0 +1,332 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// 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. + +import 'dart:async'; +import 'dart:collection'; + +import 'package:clock/clock.dart'; +import 'package:collection/collection.dart'; + +/// The type of a microtask callback. +typedef _Microtask = void Function(); + +/// Runs [callback] in a [Zone] where all asynchrony is controlled by an +/// instance of [FakeAsync]. +/// +/// All [Future]s, [Stream]s, [Timer]s, microtasks, and other time-based +/// asynchronous features used within [callback] are controlled by calls to +/// [FakeAsync.elapse] rather than the passing of real time. +/// +/// The [`clock`][] property will be set to a clock that reports the fake +/// elapsed time. By default, it starts at the time [fakeAsync] was created +/// (according to [`clock.now()`][]), but this can be controlled by passing +/// [initialTime]. +/// +/// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html +/// [`clock.now()`]: https://www.dartdocs.org/documentation/clock/latest/clock/Clock/now.html +/// +/// Returns the result of [callback]. +T fakeAsync<T>(T Function(FakeAsync async) callback, {DateTime? initialTime}) => + FakeAsync(initialTime: initialTime).run(callback); + +/// A class that mocks out the passage of time within a [Zone]. +/// +/// Test code can be passed as a callback to [run], which causes it to be run in +/// a [Zone] which fakes timer and microtask creation, such that they are run +/// during calls to [elapse] which simulates the asynchronous passage of time. +/// +/// The synchronous passage of time (as from blocking or expensive calls) can +/// also be simulated using [elapseBlocking]. +class FakeAsync { + /// The value of [clock] within [run]. + late final Clock _clock; + + /// The amount of fake time that's elapsed since this [FakeAsync] was + /// created. + Duration get elapsed => _elapsed; + var _elapsed = Duration.zero; + + /// Whether Timers created by this FakeAsync will include a creation stack + /// trace in [FakeAsync.pendingTimersDebugString]. + final bool includeTimerStackTrace; + + /// The fake time at which the current call to [elapse] will finish running. + /// + /// This is `null` if there's no current call to [elapse]. + Duration? _elapsingTo; + + /// Tasks that are scheduled to run when fake time progresses. + final _microtasks = Queue<_Microtask>(); + + /// All timers created within [run]. + final _timers = <FakeTimer>{}; + + /// All the current pending timers. + List<FakeTimer> get pendingTimers => _timers.toList(growable: false); + + /// The debug strings for all the current pending timers. + List<String> get pendingTimersDebugString => + pendingTimers.map((timer) => timer.debugString).toList(growable: false); + + /// The number of active periodic timers created within a call to [run] or + /// [fakeAsync]. + int get periodicTimerCount => + _timers.where((timer) => timer.isPeriodic).length; + + /// The number of active non-periodic timers created within a call to [run] or + /// [fakeAsync]. + int get nonPeriodicTimerCount => + _timers.where((timer) => !timer.isPeriodic).length; + + /// The number of pending microtasks scheduled within a call to [run] or + /// [fakeAsync]. + int get microtaskCount => _microtasks.length; + + /// Creates a [FakeAsync]. + /// + /// Within [run], the [`clock`][] property will start at [initialTime] and + /// move forward as fake time elapses. + /// + /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html + /// + /// Note: it's usually more convenient to use [fakeAsync] rather than creating + /// a [FakeAsync] object and calling [run] manually. + FakeAsync({DateTime? initialTime, this.includeTimerStackTrace = true}) { + final nonNullInitialTime = initialTime ?? clock.now(); + _clock = Clock(() => nonNullInitialTime.add(elapsed)); + } + + /// Returns a fake [Clock] whose time can is elapsed by calls to [elapse] and + /// [elapseBlocking]. + /// + /// The returned clock starts at [initialTime] plus the fake time that's + /// already been elapsed. Further calls to [elapse] and [elapseBlocking] will + /// advance the clock as well. + /// + /// Note that it's usually easier to use the top-level [`clock`][] property. + /// Only call this function if you want a different [initialTime] than the + /// default. + /// + /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html + Clock getClock(DateTime initialTime) => + Clock(() => initialTime.add(_elapsed)); + + /// Simulates the asynchronous passage of time. + /// + /// Throws an [ArgumentError] if [duration] is negative. Throws a [StateError] + /// if a previous call to [elapse] has not yet completed. + /// + /// Any timers created within [run] or [fakeAsync] will fire if their time is + /// within [duration]. The microtask queue is processed before and after each + /// timer fires. + void elapse(Duration duration) { + if (duration.inMicroseconds < 0) { + throw ArgumentError.value(duration, 'duration', 'may not be negative'); + } else if (_elapsingTo != null) { + throw StateError('Cannot elapse until previous elapse is complete.'); + } + + _elapsingTo = _elapsed + duration; + _fireTimersWhile((next) => next._nextCall <= _elapsingTo!); + _elapseTo(_elapsingTo!); + _elapsingTo = null; + } + + /// Simulates the synchronous passage of time, resulting from blocking or + /// expensive calls. + /// + /// Neither timers nor microtasks are run during this call, but if this is + /// called within [elapse] they may fire afterwards. + /// + /// Throws an [ArgumentError] if [duration] is negative. + void elapseBlocking(Duration duration) { + if (duration.inMicroseconds < 0) { + throw ArgumentError('Cannot call elapse with negative duration'); + } + + _elapsed += duration; + final elapsingTo = _elapsingTo; + if (elapsingTo != null && _elapsed > elapsingTo) _elapsingTo = _elapsed; + } + + /// Runs [callback] in a [Zone] where all asynchrony is controlled by `this`. + /// + /// All [Future]s, [Stream]s, [Timer]s, microtasks, and other time-based + /// asynchronous features used within [callback] are controlled by calls to + /// [elapse] rather than the passing of real time. + /// + /// The [`clock`][] property will be set to a clock that reports the fake + /// elapsed time. By default, it starts at the time the [FakeAsync] was + /// created (according to [`clock.now()`][]), but this can be controlled by + /// passing `initialTime` to [FakeAsync.new]. + /// + /// [`clock`]: https://www.dartdocs.org/documentation/clock/latest/clock/clock.html + /// [`clock.now()`]: https://www.dartdocs.org/documentation/clock/latest/clock/Clock/now.html + /// + /// Calls [callback] with `this` as argument and returns its result. + /// + /// Note: it's usually more convenient to use [fakeAsync] rather than creating + /// a [FakeAsync] object and calling [run] manually. + T run<T>(T Function(FakeAsync self) callback) => + runZoned(() => withClock(_clock, () => callback(this)), + zoneSpecification: ZoneSpecification( + createTimer: (_, __, ___, duration, callback) => + _createTimer(duration, callback, false), + createPeriodicTimer: (_, __, ___, duration, callback) => + _createTimer(duration, callback, true), + scheduleMicrotask: (_, __, ___, microtask) => + _microtasks.add(microtask))); + + /// Runs all pending microtasks scheduled within a call to [run] or + /// [fakeAsync] until there are no more microtasks scheduled. + /// + /// Does not run timers. + void flushMicrotasks() { + while (_microtasks.isNotEmpty) { + _microtasks.removeFirst()(); + } + } + + /// Elapses time until there are no more active timers. + /// + /// If `flushPeriodicTimers` is `true` (the default), this will repeatedly run + /// periodic timers until they're explicitly canceled. Otherwise, this will + /// stop when the only active timers are periodic. + /// + /// The [timeout] controls how much fake time may elapse before a [StateError] + /// is thrown. This ensures that a periodic timer doesn't cause this method to + /// deadlock. It defaults to one hour. + void flushTimers( + {Duration timeout = const Duration(hours: 1), + bool flushPeriodicTimers = true}) { + final absoluteTimeout = _elapsed + timeout; + _fireTimersWhile((timer) { + if (timer._nextCall > absoluteTimeout) { + // TODO(nweiz): Make this a [TimeoutException]. + throw StateError('Exceeded timeout $timeout while flushing timers'); + } + + if (flushPeriodicTimers) return _timers.isNotEmpty; + + // Continue firing timers until the only ones left are periodic *and* + // every periodic timer has had a change to run against the final + // value of [_elapsed]. + return _timers + .any((timer) => !timer.isPeriodic || timer._nextCall <= _elapsed); + }); + } + + /// Invoke the callback for each timer until [predicate] returns `false` for + /// the next timer that would be fired. + /// + /// Microtasks are flushed before and after each timer is fired. Before each + /// timer fires, [_elapsed] is updated to the appropriate duration. + void _fireTimersWhile(bool Function(FakeTimer timer) predicate) { + flushMicrotasks(); + for (;;) { + if (_timers.isEmpty) break; + + final timer = minBy(_timers, (FakeTimer timer) => timer._nextCall)!; + if (!predicate(timer)) break; + + _elapseTo(timer._nextCall); + timer._fire(); + flushMicrotasks(); + } + } + + /// Creates a new timer controlled by `this` that fires [callback] after + /// [duration] (or every [duration] if [periodic] is `true`). + Timer _createTimer(Duration duration, Function callback, bool periodic) { + final timer = FakeTimer._(duration, callback, periodic, this, + includeStackTrace: includeTimerStackTrace); + _timers.add(timer); + return timer; + } + + /// Sets [_elapsed] to [to] if [to] is longer than [_elapsed]. + void _elapseTo(Duration to) { + if (to > _elapsed) _elapsed = to; + } +} + +/// An implementation of [Timer] that's controlled by a [FakeAsync]. +class FakeTimer implements Timer { + /// If this is periodic, the time that should elapse between firings of this + /// timer. + /// + /// This is not used by non-periodic timers. + final Duration duration; + + /// The callback to invoke when the timer fires. + /// + /// For periodic timers, this is a `void Function(Timer)`. For non-periodic + /// timers, it's a `void Function()`. + final Function _callback; + + /// Whether this is a periodic timer. + final bool isPeriodic; + + /// The [FakeAsync] instance that controls this timer. + final FakeAsync _async; + + /// The value of [FakeAsync._elapsed] at (or after) which this timer should be + /// fired. + late Duration _nextCall; + + /// The current stack trace when this timer was created. + /// + /// If [FakeAsync.includeTimerStackTrace] is set to false then accessing + /// this field will throw a [TypeError]. + StackTrace get creationStackTrace => _creationStackTrace!; + final StackTrace? _creationStackTrace; + + var _tick = 0; + + @override + int get tick => _tick; + + /// Returns debugging information to try to identify the source of the + /// [Timer]. + String get debugString => 'Timer (duration: $duration, periodic: $isPeriodic)' + '${_creationStackTrace != null ? ', created:\n$creationStackTrace' : ''}'; + + FakeTimer._(Duration duration, this._callback, this.isPeriodic, this._async, + {bool includeStackTrace = true}) + : duration = duration < Duration.zero ? Duration.zero : duration, + _creationStackTrace = includeStackTrace ? StackTrace.current : null { + _nextCall = _async._elapsed + this.duration; + } + + @override + bool get isActive => _async._timers.contains(this); + + @override + void cancel() => _async._timers.remove(this); + + /// Fires this timer's callback and updates its state as necessary. + void _fire() { + assert(isActive); + _tick++; + if (isPeriodic) { + _nextCall += duration; + // ignore: avoid_dynamic_calls + _callback(this); + } else { + cancel(); + // ignore: avoid_dynamic_calls + _callback(); + } + } +}
diff --git a/pkgs/fake_async/mono_pkg.yaml b/pkgs/fake_async/mono_pkg.yaml new file mode 100644 index 0000000..b447edb --- /dev/null +++ b/pkgs/fake_async/mono_pkg.yaml
@@ -0,0 +1,15 @@ +# See https://pub.dev/packages/mono_repo + +stages: +- analyze_and_format: + - group: + - format + - analyze: --fatal-infos + sdk: dev + - group: + - analyze + sdk: pubspec +- unit_test: + - group: + - command: dart test + sdk: [dev, pubspec]
diff --git a/pkgs/fake_async/pubspec.yaml b/pkgs/fake_async/pubspec.yaml new file mode 100644 index 0000000..f345860 --- /dev/null +++ b/pkgs/fake_async/pubspec.yaml
@@ -0,0 +1,17 @@ +name: fake_async +version: 1.3.2 +description: >- + Fake asynchronous events such as timers and microtasks for deterministic + testing. +repository: https://github.com/dart-lang/test/tree/master/pkgs/fake_async +environment: + sdk: ^3.3.0 + +dependencies: + clock: ^1.1.0 + collection: ^1.15.0 + +dev_dependencies: + async: ^2.5.0 + dart_flutter_team_lints: ^2.0.0 + test: ^1.16.0
diff --git a/pkgs/fake_async/test/fake_async_test.dart b/pkgs/fake_async/test/fake_async_test.dart new file mode 100644 index 0000000..463eecd --- /dev/null +++ b/pkgs/fake_async/test/fake_async_test.dart
@@ -0,0 +1,663 @@ +// Copyright 2014 Google Inc. All Rights Reserved. +// +// 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. + +import 'dart:async'; + +import 'package:clock/clock.dart'; +import 'package:fake_async/fake_async.dart'; +import 'package:test/test.dart'; + +void main() { + final initialTime = DateTime(2000); + final elapseBy = const Duration(days: 1); + + test('should set initial time', () { + expect(FakeAsync().getClock(initialTime).now(), initialTime); + }); + + group('elapseBlocking', () { + test('should elapse time without calling timers', () { + Timer(elapseBy ~/ 2, neverCalled); + FakeAsync().elapseBlocking(elapseBy); + }); + + test('should elapse time by the specified amount', () { + final async = FakeAsync()..elapseBlocking(elapseBy); + expect(async.elapsed, elapseBy); + }); + + test('should throw when called with a negative duration', () { + expect(() => FakeAsync().elapseBlocking(const Duration(days: -1)), + throwsArgumentError); + }); + }); + + group('elapse', () { + test('should elapse time by the specified amount', () { + FakeAsync().run((async) { + async.elapse(elapseBy); + expect(async.elapsed, elapseBy); + }); + }); + + test('should throw ArgumentError when called with a negative duration', () { + expect(() => FakeAsync().elapse(const Duration(days: -1)), + throwsArgumentError); + }); + + test('should throw when called before previous call is complete', () { + FakeAsync().run((async) { + Timer(elapseBy ~/ 2, expectAsync0(() { + expect(() => async.elapse(elapseBy), throwsStateError); + })); + async.elapse(elapseBy); + }); + }); + + group('when creating timers', () { + test('should call timers expiring before or at end time', () { + FakeAsync().run((async) { + Timer(elapseBy ~/ 2, expectAsync0(() {})); + Timer(elapseBy, expectAsync0(() {})); + async.elapse(elapseBy); + }); + }); + + test('should call timers expiring due to elapseBlocking', () { + FakeAsync().run((async) { + Timer(elapseBy, () => async.elapseBlocking(elapseBy)); + Timer(elapseBy * 2, expectAsync0(() {})); + async.elapse(elapseBy); + expect(async.elapsed, elapseBy * 2); + }); + }); + + test('should call timers at their scheduled time', () { + FakeAsync().run((async) { + Timer(elapseBy ~/ 2, expectAsync0(() { + expect(async.elapsed, elapseBy ~/ 2); + })); + + final periodicCalledAt = <Duration>[]; + Timer.periodic( + elapseBy ~/ 2, (_) => periodicCalledAt.add(async.elapsed)); + + async.elapse(elapseBy); + expect(periodicCalledAt, [elapseBy ~/ 2, elapseBy]); + }); + }); + + test('should not call timers expiring after end time', () { + FakeAsync().run((async) { + Timer(elapseBy * 2, neverCalled); + async.elapse(elapseBy); + }); + }); + + test('should not call canceled timers', () { + FakeAsync().run((async) { + Timer(elapseBy ~/ 2, neverCalled).cancel(); + async.elapse(elapseBy); + }); + }); + + test('should call periodic timers each time the duration elapses', () { + FakeAsync().run((async) { + Timer.periodic(elapseBy ~/ 10, expectAsync1((_) {}, count: 10)); + async.elapse(elapseBy); + }); + }); + + test('should call timers occurring at the same time in FIFO order', () { + FakeAsync().run((async) { + final log = <String>[]; + Timer(elapseBy ~/ 2, () => log.add('1')); + Timer(elapseBy ~/ 2, () => log.add('2')); + async.elapse(elapseBy); + expect(log, ['1', '2']); + }); + }); + + test('should maintain FIFO order even with periodic timers', () { + FakeAsync().run((async) { + final log = <String>[]; + Timer.periodic(elapseBy ~/ 2, (_) => log.add('periodic 1')); + Timer(elapseBy ~/ 2, () => log.add('delayed 1')); + Timer(elapseBy, () => log.add('delayed 2')); + Timer.periodic(elapseBy, (_) => log.add('periodic 2')); + + async.elapse(elapseBy); + expect(log, [ + 'periodic 1', + 'delayed 1', + 'periodic 1', + 'delayed 2', + 'periodic 2' + ]); + }); + }); + + test('should process microtasks surrounding each timer', () { + FakeAsync().run((async) { + var microtaskCalls = 0; + var timerCalls = 0; + void scheduleMicrotasks() { + for (var i = 0; i < 5; i++) { + scheduleMicrotask(() => microtaskCalls++); + } + } + + scheduleMicrotasks(); + Timer.periodic(elapseBy ~/ 5, (_) { + timerCalls++; + expect(microtaskCalls, 5 * timerCalls); + scheduleMicrotasks(); + }); + async.elapse(elapseBy); + expect(timerCalls, 5); + expect(microtaskCalls, 5 * (timerCalls + 1)); + }); + }); + + test('should pass the periodic timer itself to callbacks', () { + FakeAsync().run((async) { + late Timer constructed; + constructed = Timer.periodic(elapseBy, expectAsync1((passed) { + expect(passed, same(constructed)); + })); + async.elapse(elapseBy); + }); + }); + + test('should call microtasks before advancing time', () { + FakeAsync().run((async) { + scheduleMicrotask(expectAsync0(() { + expect(async.elapsed, Duration.zero); + })); + async.elapse(const Duration(minutes: 1)); + }); + }); + + test('should add event before advancing time', () { + FakeAsync().run((async) { + final controller = StreamController<void>(); + expect(controller.stream.first.then((_) { + expect(async.elapsed, Duration.zero); + }), completes); + controller.add(null); + async.elapse(const Duration(minutes: 1)); + }); + }); + + test('should increase negative duration timers to zero duration', () { + FakeAsync().run((async) { + final negativeDuration = const Duration(days: -1); + Timer(negativeDuration, expectAsync0(() { + expect(async.elapsed, Duration.zero); + })); + async.elapse(const Duration(minutes: 1)); + }); + }); + + test('should not be additive with elapseBlocking', () { + FakeAsync().run((async) { + Timer(Duration.zero, () => async.elapseBlocking(elapseBy * 5)); + async.elapse(elapseBy); + expect(async.elapsed, elapseBy * 5); + }); + }); + + group('isActive', () { + test('should be false after timer is run', () { + FakeAsync().run((async) { + final timer = Timer(elapseBy ~/ 2, () {}); + async.elapse(elapseBy); + expect(timer.isActive, isFalse); + }); + }); + + test('should be true after periodic timer is run', () { + FakeAsync().run((async) { + final timer = Timer.periodic(elapseBy ~/ 2, (_) {}); + async.elapse(elapseBy); + expect(timer.isActive, isTrue); + }); + }); + + test('should be false after timer is canceled', () { + FakeAsync().run((async) { + final timer = Timer(elapseBy ~/ 2, () {})..cancel(); + expect(timer.isActive, isFalse); + }); + }); + }); + + test('should work with new Future()', () { + FakeAsync().run((async) { + Future(expectAsync0(() {})); + async.elapse(Duration.zero); + }); + }); + + test('should work with Future.delayed', () { + FakeAsync().run((async) { + Future.delayed(elapseBy, expectAsync0(() {})); + async.elapse(elapseBy); + }); + }); + + test('should work with Future.timeout', () { + FakeAsync().run((async) { + final completer = Completer<void>(); + expect(completer.future.timeout(elapseBy ~/ 2), + throwsA(const TypeMatcher<TimeoutException>())); + async.elapse(elapseBy); + completer.complete(); + }); + }); + + // TODO: Pausing and resuming the timeout Stream doesn't work since + // it uses `new Stopwatch()`. + // + // See https://code.google.com/p/dart/issues/detail?id=18149 + test('should work with Stream.periodic', () { + FakeAsync().run((async) { + expect(Stream.periodic(const Duration(minutes: 1), (i) => i), + emitsInOrder([0, 1, 2])); + async.elapse(const Duration(minutes: 3)); + }); + }); + + test('should work with Stream.timeout', () { + FakeAsync().run((async) { + final controller = StreamController<int>(); + final timed = controller.stream.timeout(const Duration(minutes: 2)); + + final events = <int>[]; + final errors = <Object>[]; + timed.listen(events.add, onError: errors.add); + + controller.add(0); + async.elapse(const Duration(minutes: 1)); + expect(events, [0]); + + async.elapse(const Duration(minutes: 1)); + expect(errors, hasLength(1)); + expect(errors.first, const TypeMatcher<TimeoutException>()); + }); + }); + }); + }); + + group('flushMicrotasks', () { + test('should flush a microtask', () { + FakeAsync().run((async) { + Future.microtask(expectAsync0(() {})); + async.flushMicrotasks(); + }); + }); + + test('should flush microtasks scheduled by microtasks in order', () { + FakeAsync().run((async) { + final log = <int>[]; + scheduleMicrotask(() { + log.add(1); + scheduleMicrotask(() => log.add(3)); + }); + scheduleMicrotask(() => log.add(2)); + + async.flushMicrotasks(); + expect(log, [1, 2, 3]); + }); + }); + + test('should not run timers', () { + FakeAsync().run((async) { + final log = <int>[]; + scheduleMicrotask(() => log.add(1)); + Timer.run(() => log.add(2)); + Timer.periodic(const Duration(seconds: 1), (_) => log.add(2)); + + async.flushMicrotasks(); + expect(log, [1]); + }); + }); + }); + + group('flushTimers', () { + test('should flush timers in FIFO order', () { + FakeAsync().run((async) { + final log = <int>[]; + Timer.run(() { + log.add(1); + Timer(elapseBy, () => log.add(3)); + }); + Timer.run(() => log.add(2)); + + async.flushTimers(timeout: elapseBy * 2); + expect(log, [1, 2, 3]); + expect(async.elapsed, elapseBy); + }); + }); + + test( + 'should run collateral periodic timers with non-periodic first if ' + 'scheduled first', () { + FakeAsync().run((async) { + final log = <String>[]; + Timer(const Duration(seconds: 2), () => log.add('delayed')); + Timer.periodic(const Duration(seconds: 1), (_) => log.add('periodic')); + + async.flushTimers(flushPeriodicTimers: false); + expect(log, ['periodic', 'delayed', 'periodic']); + }); + }); + + test( + 'should run collateral periodic timers with periodic first ' + 'if scheduled first', () { + FakeAsync().run((async) { + final log = <String>[]; + Timer.periodic(const Duration(seconds: 1), (_) => log.add('periodic')); + Timer(const Duration(seconds: 2), () => log.add('delayed')); + + async.flushTimers(flushPeriodicTimers: false); + expect(log, ['periodic', 'periodic', 'delayed']); + }); + }); + + test('should time out', () { + FakeAsync().run((async) { + // Schedule 3 timers. All but the last one should fire. + for (var delay in [30, 60, 90]) { + Timer(Duration(minutes: delay), + expectAsync0(() {}, count: delay == 90 ? 0 : 1)); + } + + expect(() => async.flushTimers(), throwsStateError); + }); + }); + + test('should time out a chain of timers', () { + FakeAsync().run((async) { + var count = 0; + void createTimer() { + Timer(const Duration(minutes: 30), () { + count++; + createTimer(); + }); + } + + createTimer(); + expect(() => async.flushTimers(timeout: const Duration(hours: 2)), + throwsStateError); + expect(count, 4); + }); + }); + + test('should time out periodic timers', () { + FakeAsync().run((async) { + Timer.periodic( + const Duration(minutes: 30), expectAsync1((_) {}, count: 2)); + expect(() => async.flushTimers(timeout: const Duration(hours: 1)), + throwsStateError); + }); + }); + + test('should flush periodic timers', () { + FakeAsync().run((async) { + var count = 0; + Timer.periodic(const Duration(minutes: 30), (timer) { + if (count == 3) timer.cancel(); + count++; + }); + async.flushTimers(timeout: const Duration(hours: 20)); + expect(count, 4); + }); + }); + + test('should compute absolute timeout as elapsed + timeout', () { + FakeAsync().run((async) { + var count = 0; + void createTimer() { + Timer(const Duration(minutes: 30), () { + count++; + if (count < 4) createTimer(); + }); + } + + createTimer(); + async + ..elapse(const Duration(hours: 1)) + ..flushTimers(timeout: const Duration(hours: 1)); + expect(count, 4); + }); + }); + }); + + group('stats', () { + test('should report the number of pending microtasks', () { + FakeAsync().run((async) { + expect(async.microtaskCount, 0); + scheduleMicrotask(() {}); + expect(async.microtaskCount, 1); + scheduleMicrotask(() {}); + expect(async.microtaskCount, 2); + async.flushMicrotasks(); + expect(async.microtaskCount, 0); + }); + }); + + test('it should report the number of pending periodic timers', () { + FakeAsync().run((async) { + expect(async.periodicTimerCount, 0); + final timer = Timer.periodic(const Duration(minutes: 30), (_) {}); + expect(async.periodicTimerCount, 1); + Timer.periodic(const Duration(minutes: 20), (_) {}); + expect(async.periodicTimerCount, 2); + async.elapse(const Duration(minutes: 20)); + expect(async.periodicTimerCount, 2); + timer.cancel(); + expect(async.periodicTimerCount, 1); + }); + }); + + test('it should report the number of pending non periodic timers', () { + FakeAsync().run((async) { + expect(async.nonPeriodicTimerCount, 0); + final timer = Timer(const Duration(minutes: 30), () {}); + expect(async.nonPeriodicTimerCount, 1); + Timer(const Duration(minutes: 20), () {}); + expect(async.nonPeriodicTimerCount, 2); + async.elapse(const Duration(minutes: 25)); + expect(async.nonPeriodicTimerCount, 1); + timer.cancel(); + expect(async.nonPeriodicTimerCount, 0); + }); + }); + + test('should report debugging information of pending timers', () { + FakeAsync().run((fakeAsync) { + expect(fakeAsync.pendingTimers, isEmpty); + final nonPeriodic = + Timer(const Duration(seconds: 1), () {}) as FakeTimer; + final periodic = + Timer.periodic(const Duration(seconds: 2), (Timer timer) {}) + as FakeTimer; + final debugInfo = fakeAsync.pendingTimers; + expect(debugInfo.length, 2); + expect( + debugInfo, + containsAll([ + nonPeriodic, + periodic, + ]), + ); + + const thisFileName = 'fake_async_test.dart'; + expect(nonPeriodic.debugString, contains(':01.0')); + expect(nonPeriodic.debugString, contains('periodic: false')); + expect(nonPeriodic.debugString, contains(thisFileName)); + expect(periodic.debugString, contains(':02.0')); + expect(periodic.debugString, contains('periodic: true')); + expect(periodic.debugString, contains(thisFileName)); + }); + }); + + test( + 'should report debugging information of pending timers excluding ' + 'stack traces', () { + FakeAsync(includeTimerStackTrace: false).run((fakeAsync) { + expect(fakeAsync.pendingTimers, isEmpty); + final nonPeriodic = + Timer(const Duration(seconds: 1), () {}) as FakeTimer; + final periodic = + Timer.periodic(const Duration(seconds: 2), (Timer timer) {}) + as FakeTimer; + final debugInfo = fakeAsync.pendingTimers; + expect(debugInfo.length, 2); + expect( + debugInfo, + containsAll([ + nonPeriodic, + periodic, + ]), + ); + + const thisFileName = 'fake_async_test.dart'; + expect(nonPeriodic.debugString, contains(':01.0')); + expect(nonPeriodic.debugString, contains('periodic: false')); + expect(nonPeriodic.debugString, isNot(contains(thisFileName))); + expect(periodic.debugString, contains(':02.0')); + expect(periodic.debugString, contains('periodic: true')); + expect(periodic.debugString, isNot(contains(thisFileName))); + }); + }); + }); + + group('timers', () { + test("should become inactive as soon as they're invoked", () { + return FakeAsync().run((async) { + late Timer timer; + timer = Timer(elapseBy, expectAsync0(() { + expect(timer.isActive, isFalse); + })); + + expect(timer.isActive, isTrue); + async.elapse(elapseBy); + expect(timer.isActive, isFalse); + }); + }); + + test('should increment tick in a non-periodic timer', () { + return FakeAsync().run((async) { + late Timer timer; + timer = Timer(elapseBy, expectAsync0(() { + expect(timer.tick, 1); + })); + + expect(timer.tick, 0); + async.elapse(elapseBy); + }); + }); + + test('should increment tick in a periodic timer', () { + return FakeAsync().run((async) { + final ticks = <int>[]; + Timer.periodic( + elapseBy, + expectAsync1((timer) { + ticks.add(timer.tick); + }, count: 2)); + async + ..elapse(elapseBy) + ..elapse(elapseBy); + expect(ticks, [1, 2]); + }); + }); + + test('should update periodic timer state before invoking callback', () { + // Regression test for: https://github.com/dart-lang/fake_async/issues/88 + FakeAsync().run((async) { + final log = <String>[]; + Timer.periodic(const Duration(seconds: 2), (timer) { + log.add('periodic ${timer.tick}'); + async.elapse(Duration.zero); + }); + Timer(const Duration(seconds: 3), () { + log.add('single'); + }); + + async.flushTimers(flushPeriodicTimers: false); + expect(log, ['periodic 1', 'single']); + }); + }); + }); + + group('clock', () { + test('updates following elapse()', () { + FakeAsync().run((async) { + final before = clock.now(); + async.elapse(elapseBy); + expect(clock.now(), before.add(elapseBy)); + }); + }); + + test('updates following elapseBlocking()', () { + FakeAsync().run((async) { + final before = clock.now(); + async.elapseBlocking(elapseBy); + expect(clock.now(), before.add(elapseBy)); + }); + }); + + group('starts at', () { + test('the time at which the FakeAsync was created', () { + final start = DateTime.now(); + FakeAsync().run((async) { + expect(clock.now(), _closeToTime(start)); + async.elapse(elapseBy); + expect(clock.now(), _closeToTime(start.add(elapseBy))); + }); + }); + + test('the value of clock.now()', () { + final start = DateTime(1990, 8, 11); + withClock(Clock.fixed(start), () { + FakeAsync().run((async) { + expect(clock.now(), start); + async.elapse(elapseBy); + expect(clock.now(), start.add(elapseBy)); + }); + }); + }); + + test('an explicit value', () { + final start = DateTime(1990, 8, 11); + FakeAsync(initialTime: start).run((async) { + expect(clock.now(), start); + async.elapse(elapseBy); + expect(clock.now(), start.add(elapseBy)); + }); + }); + }); + }); +} + +/// Returns a matcher that asserts that a [DateTime] is within 100ms of +/// [expected]. +Matcher _closeToTime(DateTime expected) => predicate( + (actual) => + expected.difference(actual as DateTime).inMilliseconds.abs() < 100, + 'is close to $expected');
diff --git a/pkgs/matcher/.gitignore b/pkgs/matcher/.gitignore new file mode 100644 index 0000000..ab3cb76 --- /dev/null +++ b/pkgs/matcher/.gitignore
@@ -0,0 +1,16 @@ +# Don’t commit the following directories created by pub. +.buildlog +.dart_tool/ +.pub/ +build/ +packages +.packages + +# Or the files created by dart2js. +*.dart.js +*.js_ +*.js.deps +*.js.map + +# Include when developing application packages. +pubspec.lock
diff --git a/pkgs/matcher/CHANGELOG.md b/pkgs/matcher/CHANGELOG.md new file mode 100644 index 0000000..0522c4c --- /dev/null +++ b/pkgs/matcher/CHANGELOG.md
@@ -0,0 +1,289 @@ +## 0.12.18-wip + +* Remove some dynamic invocations. + +## 0.12.17 + +* Require Dart 3.4 +* Move to `dart-lang/test` monorepo. + +## 0.12.16+1 + +* Require Dart 3.0 +* Support latest version of `package:test_api`. + +## 0.12.16 + +* Expand bounds on `test_api` dependency to allow the next breaking release + which will remove the cyclic dependency on this package. + +## 0.12.15 + +* Add `package:matcher/expect.dart` library. Copies the implementation of + `expect` and the asynchronous matchers from `package:test`. + +## 0.12.14 + +* Add `containsOnce` matcher. +* Deprecate `isCyclicInitializationError` and `NullThrownError`. These errors + will be removed from the SDK. Update them to catch more general errors. + +## 0.12.13 + +* Require Dart 2.17 or greater. +* Make `isCastError` no longer depend on the deprecated `CastError` type. +* Annotate `TypeMatcher.having` with `useResult`. + +## 0.12.12 + +* Add a best practices section to readme. +* Populate the pubspec `repository` field. + +## 0.12.11 + +* Change many argument types from `dynamic` to `Object?`. +* Fix `stringContainsInOrder` to account for repetitions and empty strings. + * **Note**: This may break some existing tests, as the behavior does change. + +## 0.12.10 + +* Stable release for null safety. + +## 0.12.10-nullsafety.3 + +* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release + guidelines. + +## 0.12.10-nullsafety.2 + +- Allow prerelease versions of the 2.12 sdk. + +## 0.12.10-nullsafety.1 + +- Allow 2.10 stable and 2.11.0 dev SDK versions. + +## 0.12.10-nullsafety + +- Migrate to NNBD. + - Apis have been updated to express intent of the existing code and how it + handled nulls. + +## 0.12.9 + +- Improve mismatch descriptions for deep matches. Previously, if the user tried + to do a deep match where the expectation included a complex matcher (such as a + "having" matcher), the failure message would just say "failed to match ..."; + it wouldn't call on the expectation's matcher to explain why the match failed. + +## 0.12.8 + +- Add a mismatch description to `TypeMatcher`. + +## 0.12.7 + +- Deprecate the `mirror_matchers.dart` library. + +## 0.12.6 + +- Update minimum Dart SDK to `2.2.0`. +- Consistently point to `isA` as a replacement for `instanceOf`. +- Pretty print with private type names. + +## 0.12.5 + +- Add `isA()` to create `TypeMatcher` instances in a more fluent way. +- **Potentially breaking bug fix**. Ordering matchers no longer treat objects + with a partial ordering (such as NaN for double values) as if they had a + complete ordering. For instance `greaterThan` now compares with the `>` + operator rather not `<` and not `=`. This could cause tests which relied on + this bug to start failing. + +## 0.12.4 + +- Add isCastError. + +## 0.12.3+1 + +- Set max SDK version to <3.0.0, and adjusted other dependencies. + +## 0.12.3 + +- Many improvements to `TypeMatcher` + - Can now be used directly as `const TypeMatcher<MyType>()`. + - Added a type parameter to specify the target `Type`. + - Made the `name` constructor parameter optional and marked it deprecated. + It's redundant to the type parameter. + - Migrated all `isType` matchers to `TypeMatcher`. + - Added a `having` function that allows chained validations of specific + features of the target type. + + ```dart + /// Validates that the object is a [RangeError] with a message containing + /// the string 'details' and `start` and `end` properties that are `null`. + final _rangeMatcher = isRangeError + .having((e) => e.message, 'message', contains('details')) + .having((e) => e.start, 'start', isNull) + .having((e) => e.end, 'end', isNull); + ``` + +- Deprecated the `isInstanceOf` class. Use `TypeMatcher` instead. + +- Improved the output of `Matcher` instances that fail due to type errors. + +## 0.12.2+1 + +- Updated SDK version to 2.0.0-dev.17.0 + +## 0.12.2 + +* Fixed `unorderedMatches` in cases where the matchers may match more than one + element and order of the elements doesn't line up with the order of the + matchers. + +* Add containsAll matcher for Iterables. This Matcher checks that all + values/matchers in an expected iterable are satisfied by an element in the + value without allowing the same value to satisfy multiple matchers. + +## 0.12.1+4 + +* Fixed SDK constraint to allow edge builds. + +## 0.12.1+3 + +* Make `predicate` and `pairwiseCompare` generic methods to allow typed + functions to be passed to them as arguments. + +* Make internal implementations take better advantage of type promotion to avoid + dynamic call overhead. + +## 0.12.1+2 + +* Fixed small documentation issues. + +* Fixed small issue in `StringEqualsMatcher`. + +* Update to support future Dart language changes. + +## 0.12.1+1 + +* Produce a better error message when a `CustomMatcher`'s feature throws. + +## 0.12.1 + +* Add containsAllInOrder matcher for Iterables + +## 0.12.0+2 + +* Fix all strong-mode warnings. + +## 0.12.0+1 + +* Fix test files to use `test` instead of `unittest` pkg. + +## 0.12.0 + +* Moved a number of members to the + [`unittest`](https://pub.dev/packages/unittest) package. + * `TestFailure`, `ErrorFormatter`, `expect`, `fail`, and 'wrapAsync'. + * `completes`, `completion`, `throws`, and `throwsA` Matchers. + * The `Throws` class. + * All of the `throws...Error` Matchers. + +* Removed `FailureHandler`, `DefaultFailureHandler`, + `configureExpectFailureHandler`, and `getOrCreateExpectFailureHandler`. + Now that `expect` is in the `unittest` package, these are no longer needed. + +* Removed the `name` parameter for `isInstanceOf`. This was previously + deprecated, and is no longer necessary since all language implementations now + support converting the type parameter to a string directly. + +## 0.11.4+6 + +* Fix a bug introduced in 0.11.4+5 in which operator matchers broke when taking + lists of matchers. + +## 0.11.4+5 + +* Fix all strong-mode warnings. + +## 0.11.4+4 + +* Deprecate the name parameter to `isInstanceOf`. All language implementations + now support converting the type parameter to a string directly. + +## 0.11.4+3 + +* Fix the examples for `equalsIgnoringWhitespace`. + +## 0.11.4+2 + +* Improve the formatting of strings that contain unprintable ASCII characters. + +## 0.11.4+1 + +* Correctly match and print `String`s containing characters that must be + represented as escape sequences. + +## 0.11.4 + +* Remove the type checks in the `isEmpty` and `isNotEmpty` matchers and simply + access the `isEmpty` respectively `isNotEmpty` fields. This allows them to + work with custom collections. See [Issue + 21792](https://code.google.com/p/dart/issues/detail?id=21792) and [Issue + 21562](https://code.google.com/p/dart/issues/detail?id=21562) + +## 0.11.3+1 + +* Fix the `prints` matcher test on dart2js. + +## 0.11.3 + +* Add a `prints` matcher that matches output a callback emits via `print`. + +## 0.11.2 + +* Add an `isNotEmpty` matcher. + +## 0.11.1+1 + +* Refactored libraries and tests. + +* Fixed spelling mistake. + +## 0.11.1 + +* Added `isNaN` and `isNotNaN` matchers. + +## 0.11.0 + +* Removed deprecated matchers. + +## 0.10.1+1 + +* Get the tests passing when run on dart2js in minified mode. + +## 0.10.1 + +* Compare sets order-independently when using `equals()`. + +## 0.10.0+3 + +* Removed `@deprecated` annotation on matchers due to +[Issue 19173](https://code.google.com/p/dart/issues/detail?id=19173) + +## 0.10.0+2 + +* Added types to a number of constants. + +## 0.10.0+1 + +* Matchers related to bad language use have been removed. These represent code +structure that should rarely or never be validated in tests. + * `isAbstractClassInstantiationError` + * `throwsAbstractClassInstantiationError` + * `isFallThroughError` + * `throwsFallThroughError` + +* Added types to a number of method arguments. + +* The structure of the library and test code has been updated.
diff --git a/pkgs/matcher/LICENSE b/pkgs/matcher/LICENSE new file mode 100644 index 0000000..000cd7b --- /dev/null +++ b/pkgs/matcher/LICENSE
@@ -0,0 +1,27 @@ +Copyright 2014, 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/matcher/README.md b/pkgs/matcher/README.md new file mode 100644 index 0000000..efcae13 --- /dev/null +++ b/pkgs/matcher/README.md
@@ -0,0 +1,265 @@ +[](https://pub.dev/packages/matcher) +[](https://pub.dev/packages/matcher/publisher) + +Support for specifying test expectations, such as for unit tests. + +The matcher library provides a third-generation assertion mechanism, drawing +inspiration from [Hamcrest](https://code.google.com/p/hamcrest/). + +For more information on testing, see +[Unit Testing with Dart](https://github.com/dart-lang/test/blob/master/pkgs/test/README.md#writing-tests). + +## Using matcher + +Expectations start with a call to [`expect()`] or [`expectAsync()`]. + +[`expect()`]: https://pub.dev/documentation/matcher/latest/expect/expect.html +[`expectAsync()`]: https://pub.dev/documentation/matcher/latest/expect/expectAsync.html + +Any matchers package can be used with `expect()` to do +complex validations: + +[`matcher`]: https://pub.dev/documentation/matcher/latest/matcher/matcher-library.html + +```dart +import 'package:test/test.dart'; + +void main() { + test('.split() splits the string on the delimiter', () { + expect('foo,bar,baz', allOf([ + contains('foo'), + isNot(startsWith('bar')), + endsWith('baz') + ])); + }); +} +``` + +If a non-matcher value is passed, it will be wrapped with [`equals()`]. + +[`equals()`]: https://pub.dev/documentation/matcher/latest/expect/equals.html + +## Exception matchers + +You can also test exceptions with the [`throwsA()`] function or a matcher such +as [`throwsFormatException`]: + +[`throwsA()`]: https://pub.dev/documentation/matcher/latest/expect/throwsA.html +[`throwsFormatException`]: https://pub.dev/documentation/matcher/latest/expect/throwsFormatException-constant.html + +```dart +import 'package:test/test.dart'; + +void main() { + test('.parse() fails on invalid input', () { + expect(() => int.parse('X'), throwsFormatException); + }); +} +``` + +### Future Matchers + +There are a number of useful functions and matchers for more advanced +asynchrony. The [`completion()`] matcher can be used to test `Futures`; it +ensures that the test doesn't finish until the `Future` completes, and runs a +matcher against that `Future`'s value. + +[`completion()`]: https://pub.dev/documentation/matcher/latest/expect/completion.html + +```dart +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test('Future.value() returns the value', () { + expect(Future.value(10), completion(equals(10))); + }); +} +``` + +The [`throwsA()`] matcher and the various [`throwsExceptionType`] matchers work +with both synchronous callbacks and asynchronous `Future`s. They ensure that a +particular type of exception is thrown: + +[`throwsExceptionType`]: https://pub.dev/documentation/matcher/latest/expect/throwsException-constant.html + +```dart +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test('Future.error() throws the error', () { + expect(Future.error('oh no'), throwsA(equals('oh no'))); + expect(Future.error(StateError('bad state')), throwsStateError); + }); +} +``` + +The [`expectAsync()`] function wraps another function and has two jobs. First, +it asserts that the wrapped function is called a certain number of times, and +will cause the test to fail if it's called too often; second, it keeps the test +from finishing until the function is called the requisite number of times. + +```dart +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test('Stream.fromIterable() emits the values in the iterable', () { + var stream = Stream.fromIterable([1, 2, 3]); + + stream.listen(expectAsync1((number) { + expect(number, inInclusiveRange(1, 3)); + }, count: 3)); + }); +} +``` + +[`expectAsync()`]: https://pub.dev/documentation/matcher/latest/expect/expectAsync.html + +### Stream Matchers + +The `test` package provides a suite of powerful matchers for dealing with +[asynchronous streams][Stream]. They're expressive and composable, and make it +easy to write complex expectations about the values emitted by a stream. For +example: + +[Stream]: https://api.dart.dev/stable/dart-async/Stream-class.html + +```dart +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test('process emits status messages', () { + // Dummy data to mimic something that might be emitted by a process. + var stdoutLines = Stream.fromIterable([ + 'Ready.', + 'Loading took 150ms.', + 'Succeeded!' + ]); + + expect(stdoutLines, emitsInOrder([ + // Values match individual events. + 'Ready.', + + // Matchers also run against individual events. + startsWith('Loading took'), + + // Stream matchers can be nested. This asserts that one of two events are + // emitted after the "Loading took" line. + emitsAnyOf(['Succeeded!', 'Failed!']), + + // By default, more events are allowed after the matcher finishes + // matching. This asserts instead that the stream emits a done event and + // nothing else. + emitsDone + ])); + }); +} +``` + +A stream matcher can also match the [`async`] package's [`StreamQueue`] class, +which allows events to be requested from a stream rather than pushed to the +consumer. The matcher will consume the matched events, but leave the rest of the +queue alone so that it can still be used by the test, unlike a normal `Stream` +which can only have one subscriber. For example: + +[`async`]: https://pub.dev/packages/async +[`StreamQueue`]: https://pub.dev/documentation/async/latest/async/StreamQueue-class.html + +```dart +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:test/test.dart'; + +void main() { + test('process emits a WebSocket URL', () async { + // Wrap the Stream in a StreamQueue so that we can request events. + var stdout = StreamQueue(Stream.fromIterable([ + 'WebSocket URL:', + 'ws://localhost:1234/', + 'Waiting for connection...' + ])); + + // Ignore lines from the process until it's about to emit the URL. + await expectLater(stdout, emitsThrough('WebSocket URL:')); + + // Parse the next line as a URL. + var url = Uri.parse(await stdout.next); + expect(url.host, equals('localhost')); + + // You can match against the same StreamQueue multiple times. + await expectLater(stdout, emits('Waiting for connection...')); + }); +} +``` + +The following built-in stream matchers are available: + +* [`emits()`] matches a single data event. +* [`emitsError()`] matches a single error event. +* [`emitsDone`] matches a single done event. +* [`mayEmit()`] consumes events if they match an inner matcher, without + requiring them to match. +* [`mayEmitMultiple()`] works like `mayEmit()`, but it matches events against + the matcher as many times as possible. +* [`emitsAnyOf()`] consumes events matching one (or more) of several possible + matchers. +* [`emitsInOrder()`] consumes events matching multiple matchers in a row. +* [`emitsInAnyOrder()`] works like `emitsInOrder()`, but it allows the + matchers to match in any order. +* [`neverEmits()`] matches a stream that finishes *without* matching an inner + matcher. + +You can also define your own custom stream matchers with [`StreamMatcher()`]. + +[`emits()`]: https://pub.dev/documentation/matcher/latest/expect/emits.html +[`emitsError()`]: https://pub.dev/documentation/matcher/latest/expect/emitsError.html +[`emitsDone`]: https://pub.dev/documentation/matcher/latest/expect/emitsDone.html +[`mayEmit()`]: https://pub.dev/documentation/matcher/latest/expect/mayEmit.html +[`mayEmitMultiple()`]: https://pub.dev/documentation/matcher/latest/expect/mayEmitMultiple.html +[`emitsAnyOf()`]: https://pub.dev/documentation/matcher/latest/expect/emitsAnyOf.html +[`emitsInOrder()`]: https://pub.dev/documentation/matcher/latest/expect/emitsInOrder.html +[`emitsInAnyOrder()`]: https://pub.dev/documentation/matcher/latest/expect/emitsInAnyOrder.html +[`neverEmits()`]: https://pub.dev/documentation/matcher/latest/expect/neverEmits.html +[`StreamMatcher()`]: https://pub.dev/documentation/matcher/latest/expect/StreamMatcher-class.html + +## Best Practices + +### Prefer semantically meaningful matchers to comparing derived values + +Matchers which have knowledge of the semantics that are tested are able to emit +more meaningful messages which don't require reading test source to understand +why the test failed. For instance compare the failures between +`expect(someList.length, 1)`, and `expect(someList, hasLength(1))`: + +``` +// expect(someList.length, 1); + Expected: <1> + Actual: <2> +``` + +``` +// expect(someList, hasLength(1)); + Expected: an object with length of <1> + Actual: ['expected value', 'unexpected value'] + Which: has length of <2> + +``` + +### Prefer TypeMatcher to predicate if the match can fail in multiple ways + +The `predicate` utility is a convenient shortcut for testing an arbitrary +(synchronous) property of a value, but it discards context and failures are +opaque. Different failure modes cannot be distinguished in the output which is +determined by a single "description" argument. Using `isA<SomeType>()` and the +`TypeMatcher.having` API to extract and test derived properties in a structured +way brings the context of that structure through to failure messages, so +failures for different reasons will have distinguishable and actionable failure +messages.
diff --git a/pkgs/matcher/analysis_options.yaml b/pkgs/matcher/analysis_options.yaml new file mode 100644 index 0000000..d183f7b --- /dev/null +++ b/pkgs/matcher/analysis_options.yaml
@@ -0,0 +1,30 @@ +include: package:lints/recommended.yaml + +linter: + rules: + - always_declare_return_types + - avoid_dynamic_calls + - avoid_private_typedef_functions + - avoid_unused_constructor_parameters + - cancel_subscriptions + - comment_references + - directives_ordering + - lines_longer_than_80_chars + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_runtimeType_toString + - omit_local_variable_types + - only_throw_errors + - prefer_const_constructors + - prefer_relative_imports + - prefer_single_quotes + - test_types_in_equals + - throw_in_finally + - type_annotate_public_apis + - unawaited_futures + - unnecessary_await_in_return + - unnecessary_lambdas + - unnecessary_parenthesis + - unnecessary_statements + - use_super_parameters
diff --git a/pkgs/matcher/lib/expect.dart b/pkgs/matcher/lib/expect.dart new file mode 100644 index 0000000..c842d30 --- /dev/null +++ b/pkgs/matcher/lib/expect.dart
@@ -0,0 +1,64 @@ +// Copyright (c) 2021, 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +export 'matcher.dart'; + +export 'src/expect/expect.dart' show ErrorFormatter, expect, expectLater, fail; +export 'src/expect/expect_async.dart' + show + Func0, + Func1, + Func2, + Func3, + Func4, + Func5, + Func6, + expectAsync, + expectAsync0, + expectAsync1, + expectAsync2, + expectAsync3, + expectAsync4, + expectAsync5, + expectAsync6, + expectAsyncUntil0, + expectAsyncUntil1, + expectAsyncUntil2, + expectAsyncUntil3, + expectAsyncUntil4, + expectAsyncUntil5, + expectAsyncUntil6; +export 'src/expect/future_matchers.dart' + show completes, completion, doesNotComplete; +export 'src/expect/never_called.dart' show neverCalled; +export 'src/expect/prints_matcher.dart' show prints; +export 'src/expect/stream_matcher.dart' show StreamMatcher; +export 'src/expect/stream_matchers.dart' + show + emitsDone, + emits, + emitsError, + mayEmit, + emitsAnyOf, + emitsInOrder, + emitsInAnyOrder, + emitsThrough, + mayEmitMultiple, + neverEmits; +export 'src/expect/throws_matcher.dart' show Throws, throws, throwsA; +export 'src/expect/throws_matchers.dart' + show + throwsArgumentError, + throwsConcurrentModificationError, + throwsCyclicInitializationError, + throwsException, + throwsFormatException, + throwsNoSuchMethodError, + throwsNullThrownError, + throwsRangeError, + throwsStateError, + throwsUnimplementedError, + throwsUnsupportedError;
diff --git a/pkgs/matcher/lib/matcher.dart b/pkgs/matcher/lib/matcher.dart new file mode 100644 index 0000000..236d6f4 --- /dev/null +++ b/pkgs/matcher/lib/matcher.dart
@@ -0,0 +1,21 @@ +// Copyright (c) 2014, 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. + +/// Support for specifying test expectations, such as for unit tests. +library; + +export 'src/core_matchers.dart'; +export 'src/custom_matcher.dart'; +export 'src/description.dart'; +export 'src/equals_matcher.dart'; +export 'src/error_matchers.dart'; +export 'src/interfaces.dart'; +export 'src/iterable_matchers.dart'; +export 'src/map_matchers.dart'; +export 'src/numeric_matchers.dart'; +export 'src/operator_matchers.dart'; +export 'src/order_matchers.dart'; +export 'src/string_matchers.dart'; +export 'src/type_matcher.dart'; +export 'src/util.dart';
diff --git a/pkgs/matcher/lib/mirror_matchers.dart b/pkgs/matcher/lib/mirror_matchers.dart new file mode 100644 index 0000000..5b2f4b6 --- /dev/null +++ b/pkgs/matcher/lib/mirror_matchers.dart
@@ -0,0 +1,85 @@ +// Copyright (c) 2014, 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. + +@Deprecated('Check properties on known types') +library; + +/// The mirror matchers library provides some additional matchers that +/// make use of `dart:mirrors`. +import 'dart:mirrors'; + +import 'matcher.dart'; + +/// Returns a matcher that checks if a class instance has a property +/// with name [name], and optionally, if that property in turn satisfies +/// a [matcher]. +Matcher hasProperty(String name, [Object? matcher]) => + _HasProperty(name, matcher == null ? null : wrapMatcher(matcher)); + +class _HasProperty extends Matcher { + final String _name; + final Matcher? _matcher; + + const _HasProperty(this._name, [this._matcher]); + + @override + bool matches(Object? item, Map matchState) { + var mirror = reflect(item); + var classMirror = mirror.type; + var symbol = Symbol(_name); + var candidate = classMirror.declarations[symbol]; + if (candidate == null) { + addStateInfo(matchState, {'reason': 'has no property named "$_name"'}); + return false; + } + var isInstanceField = candidate is VariableMirror && !candidate.isStatic; + var isInstanceGetter = + candidate is MethodMirror && candidate.isGetter && !candidate.isStatic; + if (!(isInstanceField || isInstanceGetter)) { + addStateInfo(matchState, { + 'reason': + 'has a member named "$_name", but it is not an instance property' + }); + return false; + } + var matcher = _matcher; + if (matcher == null) return true; + var result = mirror.getField(symbol); + var resultMatches = matcher.matches(result.reflectee, matchState); + if (!resultMatches) { + addStateInfo(matchState, {'value': result.reflectee}); + } + return resultMatches; + } + + @override + Description describe(Description description) { + description.add('has property "$_name"'); + if (_matcher != null) { + description.add(' which matches ').addDescriptionOf(_matcher); + } + return description; + } + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + var reason = matchState['reason']; + if (reason != null) { + mismatchDescription.add(reason as String); + } else { + mismatchDescription + .add('has property "$_name" with value ') + .addDescriptionOf(matchState['value']); + var innerDescription = StringDescription(); + matchState['state'] ??= {}; + _matcher?.describeMismatch(matchState['value'], innerDescription, + matchState['state'] as Map, verbose); + if (innerDescription.length > 0) { + mismatchDescription.add(' which ').add(innerDescription.toString()); + } + } + return mismatchDescription; + } +}
diff --git a/pkgs/matcher/lib/src/core_matchers.dart b/pkgs/matcher/lib/src/core_matchers.dart new file mode 100644 index 0000000..afb835b --- /dev/null +++ b/pkgs/matcher/lib/src/core_matchers.dart
@@ -0,0 +1,327 @@ +// Copyright (c) 2012, 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 'feature_matcher.dart'; +import 'interfaces.dart'; +import 'type_matcher.dart'; +import 'util.dart'; + +/// Returns a matcher that matches the isEmpty property. +const Matcher isEmpty = _Empty(); + +class _Empty extends Matcher { + const _Empty(); + + @override + bool matches(Object? item, Map matchState) => (item as dynamic).isEmpty; + + @override + Description describe(Description description) => description.add('empty'); +} + +/// Returns a matcher that matches the isNotEmpty property. +const Matcher isNotEmpty = _NotEmpty(); + +class _NotEmpty extends Matcher { + const _NotEmpty(); + + @override + bool matches(Object? item, Map matchState) => (item as dynamic).isNotEmpty; + + @override + Description describe(Description description) => description.add('non-empty'); +} + +/// A matcher that matches any null value. +const Matcher isNull = _IsNull(); + +/// A matcher that matches any non-null value. +const Matcher isNotNull = _IsNotNull(); + +class _IsNull extends Matcher { + const _IsNull(); + @override + bool matches(Object? item, Map matchState) => item == null; + @override + Description describe(Description description) => description.add('null'); +} + +class _IsNotNull extends Matcher { + const _IsNotNull(); + @override + bool matches(Object? item, Map matchState) => item != null; + @override + Description describe(Description description) => description.add('not null'); +} + +/// A matcher that matches the Boolean value true. +const Matcher isTrue = _IsTrue(); + +/// A matcher that matches anything except the Boolean value true. +const Matcher isFalse = _IsFalse(); + +class _IsTrue extends Matcher { + const _IsTrue(); + @override + bool matches(Object? item, Map matchState) => item == true; + @override + Description describe(Description description) => description.add('true'); +} + +class _IsFalse extends Matcher { + const _IsFalse(); + @override + bool matches(Object? item, Map matchState) => item == false; + @override + Description describe(Description description) => description.add('false'); +} + +/// A matcher that matches the numeric value NaN. +const Matcher isNaN = _IsNaN(); + +/// A matcher that matches any non-NaN value. +const Matcher isNotNaN = _IsNotNaN(); + +class _IsNaN extends FeatureMatcher<num> { + const _IsNaN(); + @override + bool typedMatches(num item, Map matchState) => + double.nan.compareTo(item) == 0; + @override + Description describe(Description description) => description.add('NaN'); +} + +class _IsNotNaN extends FeatureMatcher<num> { + const _IsNotNaN(); + @override + bool typedMatches(num item, Map matchState) => + double.nan.compareTo(item) != 0; + @override + Description describe(Description description) => description.add('not NaN'); +} + +/// Returns a matches that matches if the value is the same instance +/// as [expected], using [identical]. +Matcher same(Object? expected) => _IsSameAs(expected); + +class _IsSameAs extends Matcher { + final Object? _expected; + const _IsSameAs(this._expected); + @override + bool matches(Object? item, Map matchState) => identical(item, _expected); + // If all types were hashable we could show a hash here. + @override + Description describe(Description description) => + description.add('same instance as ').addDescriptionOf(_expected); +} + +/// A matcher that matches any value. +const Matcher anything = _IsAnything(); + +class _IsAnything extends Matcher { + const _IsAnything(); + @override + bool matches(Object? item, Map matchState) => true; + @override + Description describe(Description description) => description.add('anything'); +} + +/// **DEPRECATED** Use [isA] instead. +/// +/// A matcher that matches if an object is an instance of [T] (or a subtype). +@Deprecated('Use `isA<MyType>()` instead.') +// ignore: camel_case_types +class isInstanceOf<T> extends TypeMatcher<T> { + const isInstanceOf(); +} + +/// A matcher that matches a function call against no exception. +/// +/// The function will be called once. Any exceptions will be silently swallowed. +/// The value passed to expect() should be a reference to the function. +/// Note that the function cannot take arguments; to handle this +/// a wrapper will have to be created. +const Matcher returnsNormally = _ReturnsNormally(); + +class _ReturnsNormally extends FeatureMatcher<Function> { + const _ReturnsNormally(); + + @override + bool typedMatches(Function f, Map matchState) { + try { + // ignore: unnecessary_cast + (f as Function)(); + return true; + } catch (e, s) { + addStateInfo(matchState, {'exception': e, 'stack': s}); + return false; + } + } + + @override + Description describe(Description description) => + description.add('return normally'); + + @override + Description describeTypedMismatch(Function item, + Description mismatchDescription, Map matchState, bool verbose) { + mismatchDescription.add('threw ').addDescriptionOf(matchState['exception']); + if (verbose) { + mismatchDescription.add(' at ').add(matchState['stack'].toString()); + } + return mismatchDescription; + } +} + +/// A matcher for [Map]. +const isMap = TypeMatcher<Map>(); + +/// A matcher for [List]. +const isList = TypeMatcher<List>(); + +/// Returns a matcher that matches if an object has a length property +/// that matches [matcher]. +Matcher hasLength(Object? matcher) => _HasLength(wrapMatcher(matcher)); + +class _HasLength extends Matcher { + final Matcher _matcher; + const _HasLength(this._matcher); + + @override + bool matches(Object? item, Map matchState) { + try { + final length = (item as dynamic).length; + return _matcher.matches(length, matchState); + } catch (e) { + return false; + } + } + + @override + Description describe(Description description) => + description.add('an object with length of ').addDescriptionOf(_matcher); + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + try { + final length = (item as dynamic).length; + return mismatchDescription.add('has length of ').addDescriptionOf(length); + } catch (e) { + return mismatchDescription.add('has no length property'); + } + } +} + +/// Returns a matcher that matches if the match argument contains the expected +/// value. +/// +/// For [String]s this means substring matching; +/// for [Map]s it means the map has the key, and for [Iterable]s +/// it means the iterable has a matching element. In the case of iterables, +/// [expected] can itself be a matcher. +Matcher contains(Object? expected) => _Contains(expected); + +class _Contains extends Matcher { + final Object? _expected; + + const _Contains(this._expected); + + @override + bool matches(Object? item, Map matchState) { + var expected = _expected; + if (item is String) { + return expected is Pattern && item.contains(expected); + } else if (item is Iterable) { + if (expected is Matcher) { + return item.any((e) => expected.matches(e, matchState)); + } else { + return item.contains(_expected); + } + } else if (item is Map) { + return item.containsKey(_expected); + } + return false; + } + + @override + Description describe(Description description) => + description.add('contains ').addDescriptionOf(_expected); + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + if (item is String || item is Iterable || item is Map) { + super.describeMismatch(item, mismatchDescription, matchState, verbose); + mismatchDescription.add('does not contain ').addDescriptionOf(_expected); + return mismatchDescription; + } else { + return mismatchDescription.add('is not a string, map or iterable'); + } + } +} + +/// Returns a matcher that matches if the match argument is in +/// the expected value. This is the converse of [contains]. +Matcher isIn(Object? expected) { + if (expected is Iterable) { + return _In(expected, expected.contains); + } else if (expected is String) { + return _In<Pattern>(expected, expected.contains); + } else if (expected is Map) { + return _In(expected, expected.containsKey); + } + + throw ArgumentError.value( + expected, 'expected', 'Only Iterable, Map, and String are supported.'); +} + +class _In<T> extends FeatureMatcher<T> { + final Object _source; + final bool Function(T) _containsFunction; + + const _In(this._source, this._containsFunction); + + @override + bool typedMatches(T item, Map matchState) => _containsFunction(item); + + @override + Description describe(Description description) => + description.add('is in ').addDescriptionOf(_source); +} + +/// Returns a matcher that uses an arbitrary function that returns whether the +/// value is considered a match. +/// +/// For example: +/// +/// expect(actual, predicate<num>((v) => (v % 2) == 0, 'is even')); +/// +/// Use this method when a value is checked for one conceptual property +/// described by [description]. +/// +/// If the value can be rejected for more than one reason prefer using [isA] and +/// the [TypeMatcher.having] API to build up a matcher with output that can +/// distinquish between them. +/// +/// Using an explicit generict argument allows a passed function literal to have +/// an inferred argument type of [T], and values of the wrong type will be +/// rejected with an informative message. +Matcher predicate<T>(bool Function(T) f, + [String description = 'satisfies function']) => + _Predicate(f, description); + +class _Predicate<T> extends FeatureMatcher<T> { + final bool Function(T) _matcher; + final String _description; + + _Predicate(this._matcher, this._description); + + @override + bool typedMatches(T item, Map matchState) => _matcher(item); + + @override + Description describe(Description description) => + description.add(_description); +}
diff --git a/pkgs/matcher/lib/src/custom_matcher.dart b/pkgs/matcher/lib/src/custom_matcher.dart new file mode 100644 index 0000000..b0f2d6b --- /dev/null +++ b/pkgs/matcher/lib/src/custom_matcher.dart
@@ -0,0 +1,100 @@ +// Copyright (c) 2012, 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:stack_trace/stack_trace.dart'; + +import 'description.dart'; +import 'interfaces.dart'; +import 'util.dart'; + +/// A base class for [Matcher] instances that match based on some feature of the +/// value under test. +/// +/// Derived classes should call the base constructor with a feature name and +/// description, and an instance matcher, and should implement the +/// [featureValueOf] abstract method. +/// +/// The feature description will typically describe the item and the feature, +/// while the feature name will just name the feature. For example, we may +/// have a Widget class where each Widget has a price; we could make a +/// [CustomMatcher] that can make assertions about prices with: +/// +/// ```dart +/// class HasPrice extends CustomMatcher { +/// HasPrice(matcher) : super("Widget with price that is", "price", matcher); +/// featureValueOf(actual) => (actual as Widget).price; +/// } +/// ``` +/// +/// and then use this for example like: +/// +/// ```dart +/// expect(inventoryItem, HasPrice(greaterThan(0))); +/// ``` +class CustomMatcher extends Matcher { + final String _featureDescription; + final String _featureName; + final Matcher _matcher; + + CustomMatcher( + this._featureDescription, this._featureName, Object? valueOrMatcher) + : _matcher = wrapMatcher(valueOrMatcher); + + /// Override this to extract the interesting feature. + Object? featureValueOf(dynamic actual) => actual; + + @override + bool matches(Object? item, Map matchState) { + try { + var f = featureValueOf(item); + if (_matcher.matches(f, matchState)) return true; + addStateInfo(matchState, {'custom.feature': f}); + } catch (exception, stack) { + addStateInfo(matchState, { + 'custom.exception': exception.toString(), + 'custom.stack': Chain.forTrace(stack) + .foldFrames( + (frame) => + frame.package == 'test' || + frame.package == 'stream_channel' || + frame.package == 'matcher', + terse: true) + .toString() + }); + } + return false; + } + + @override + Description describe(Description description) => + description.add(_featureDescription).add(' ').addDescriptionOf(_matcher); + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + if (matchState['custom.exception'] != null) { + mismatchDescription + .add('threw ') + .addDescriptionOf(matchState['custom.exception']) + .add('\n') + .add(matchState['custom.stack'].toString()); + return mismatchDescription; + } + + mismatchDescription + .add('has ') + .add(_featureName) + .add(' with value ') + .addDescriptionOf(matchState['custom.feature']); + var innerDescription = StringDescription(); + + _matcher.describeMismatch(matchState['custom.feature'], innerDescription, + matchState['state'] as Map, verbose); + + if (innerDescription.length > 0) { + mismatchDescription.add(' which ').add(innerDescription.toString()); + } + return mismatchDescription; + } +}
diff --git a/pkgs/matcher/lib/src/description.dart b/pkgs/matcher/lib/src/description.dart new file mode 100644 index 0000000..090aada --- /dev/null +++ b/pkgs/matcher/lib/src/description.dart
@@ -0,0 +1,72 @@ +// Copyright (c) 2012, 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 'interfaces.dart'; +import 'pretty_print.dart'; + +/// The default implementation of [Description]. This should rarely need +/// substitution, although conceivably it is a place where other languages +/// could be supported. +class StringDescription implements Description { + final StringBuffer _out = StringBuffer(); + + /// Initialize the description with initial contents [init]. + StringDescription([String init = '']) { + _out.write(init); + } + + @override + int get length => _out.length; + + /// Get the description as a string. + @override + String toString() => _out.toString(); + + /// Append [text] to the description. + @override + Description add(String text) { + _out.write(text); + return this; + } + + /// Change the value of the description. + @override + Description replace(String text) { + _out.clear(); + return add(text); + } + + /// Appends a description of [value]. If it is an IMatcher use its + /// describe method; if it is a string use its literal value after + /// escaping any embedded control characters; otherwise use its + /// toString() value and wrap it in angular "quotes". + @override + Description addDescriptionOf(Object? value) { + if (value is Matcher) { + value.describe(this); + } else { + add(prettyPrint(value, maxLineLength: 80, maxItems: 25)); + } + return this; + } + + /// Append an [Iterable] [list] of objects to the description, using the + /// specified [separator] and framing the list with [start] + /// and [end]. + @override + Description addAll( + String start, String separator, String end, Iterable list) { + var separate = false; + add(start); + for (var item in list) { + if (separate) { + add(separator); + } + addDescriptionOf(item); + separate = true; + } + add(end); + return this; + } +}
diff --git a/pkgs/matcher/lib/src/equals_matcher.dart b/pkgs/matcher/lib/src/equals_matcher.dart new file mode 100644 index 0000000..5c4f4c5 --- /dev/null +++ b/pkgs/matcher/lib/src/equals_matcher.dart
@@ -0,0 +1,327 @@ +// 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. + +import 'feature_matcher.dart'; +import 'interfaces.dart'; +import 'util.dart'; + +/// Returns a matcher that matches if the value is structurally equal to +/// [expected]. +/// +/// If [expected] is a [Matcher], then it matches using that. Otherwise it tests +/// for equality using `==` on the expected value. +/// +/// For [Iterable]s and [Map]s, this will recursively match the elements. To +/// handle cyclic structures a recursion depth [limit] can be provided. The +/// default limit is 100. [Set]s will be compared order-independently. +Matcher equals(Object? expected, [int limit = 100]) => expected is String + ? _StringEqualsMatcher(expected) + : _DeepMatcher(expected, limit); + +typedef _RecursiveMatcher = _Mismatch? Function(Object?, Object?, String, int); + +/// A special equality matcher for strings. +class _StringEqualsMatcher extends FeatureMatcher<String> { + final String _value; + + _StringEqualsMatcher(this._value); + + @override + bool typedMatches(String item, Map matchState) => _value == item; + + @override + Description describe(Description description) => + description.addDescriptionOf(_value); + + @override + Description describeTypedMismatch(String item, + Description mismatchDescription, Map matchState, bool verbose) { + var buff = StringBuffer(); + buff.write('is different.'); + var escapedItem = escape(item); + var escapedValue = escape(_value); + var minLength = escapedItem.length < escapedValue.length + ? escapedItem.length + : escapedValue.length; + var start = 0; + for (; start < minLength; start++) { + if (escapedValue.codeUnitAt(start) != escapedItem.codeUnitAt(start)) { + break; + } + } + if (start == minLength) { + if (escapedValue.length < escapedItem.length) { + buff.write(' Both strings start the same, but the actual value also' + ' has the following trailing characters: '); + _writeTrailing(buff, escapedItem, escapedValue.length); + } else { + buff.write(' Both strings start the same, but the actual value is' + ' missing the following trailing characters: '); + _writeTrailing(buff, escapedValue, escapedItem.length); + } + } else { + buff.write('\nExpected: '); + _writeLeading(buff, escapedValue, start); + _writeTrailing(buff, escapedValue, start); + buff.write('\n Actual: '); + _writeLeading(buff, escapedItem, start); + _writeTrailing(buff, escapedItem, start); + buff.write('\n '); + for (var i = start > 10 ? 14 : start; i > 0; i--) { + buff.write(' '); + } + buff.write('^\n Differ at offset $start'); + } + + return mismatchDescription.add(buff.toString()); + } + + static void _writeLeading(StringBuffer buff, String s, int start) { + if (start > 10) { + buff.write('... '); + buff.write(s.substring(start - 10, start)); + } else { + buff.write(s.substring(0, start)); + } + } + + static void _writeTrailing(StringBuffer buff, String s, int start) { + if (start + 10 > s.length) { + buff.write(s.substring(start)); + } else { + buff.write(s.substring(start, start + 10)); + buff.write(' ...'); + } + } +} + +class _DeepMatcher extends Matcher { + final Object? _expected; + final int _limit; + + _DeepMatcher(this._expected, [int limit = 1000]) : _limit = limit; + + _Mismatch? _compareIterables(Iterable expected, Object? actual, + _RecursiveMatcher matcher, int depth, String location) { + if (actual is Iterable) { + var expectedIterator = expected.iterator; + var actualIterator = actual.iterator; + for (var index = 0;; index++) { + // Advance in lockstep. + var expectedNext = expectedIterator.moveNext(); + var actualNext = actualIterator.moveNext(); + + // If we reached the end of both, we succeeded. + if (!expectedNext && !actualNext) return null; + + // Fail if their lengths are different. + var newLocation = '$location[$index]'; + if (!expectedNext) { + return _Mismatch.simple(newLocation, actual, 'longer than expected'); + } + if (!actualNext) { + return _Mismatch.simple(newLocation, actual, 'shorter than expected'); + } + + // Match the elements. + var rp = matcher(expectedIterator.current, actualIterator.current, + newLocation, depth); + if (rp != null) return rp; + } + } else { + return _Mismatch.simple(location, actual, 'is not Iterable'); + } + } + + _Mismatch? _compareSets(Set expected, Object? actual, + _RecursiveMatcher matcher, int depth, String location) { + if (actual is Iterable) { + var other = actual.toSet(); + + for (var expectedElement in expected) { + if (other.every((actualElement) => + matcher(expectedElement, actualElement, location, depth) != null)) { + return _Mismatch( + location, + actual, + (description, verbose) => description + .add('does not contain ') + .addDescriptionOf(expectedElement)); + } + } + + if (other.length > expected.length) { + return _Mismatch.simple(location, actual, 'larger than expected'); + } else if (other.length < expected.length) { + return _Mismatch.simple(location, actual, 'smaller than expected'); + } else { + return null; + } + } else { + return _Mismatch.simple(location, actual, 'is not Iterable'); + } + } + + _Mismatch? _recursiveMatch( + Object? expected, Object? actual, String location, int depth) { + // If the expected value is a matcher, try to match it. + if (expected is Matcher) { + var matchState = {}; + if (expected.matches(actual, matchState)) return null; + return _Mismatch(location, actual, (description, verbose) { + var oldLength = description.length; + expected.describeMismatch(actual, description, matchState, verbose); + if (depth > 0 && description.length == oldLength) { + description.add('does not match '); + expected.describe(description); + } + }); + } else { + // Otherwise, test for equality. + try { + if (expected == actual) return null; + } catch (e) { + // TODO(gram): Add a test for this case. + return _Mismatch( + location, + actual, + (description, verbose) => + description.add('== threw ').addDescriptionOf(e)); + } + } + + if (depth > _limit) { + return _Mismatch.simple( + location, actual, 'recursion depth limit exceeded'); + } + + // If _limit is 1 we can only recurse one level into object. + if (depth == 0 || _limit > 1) { + if (expected is Set) { + return _compareSets( + expected, actual, _recursiveMatch, depth + 1, location); + } else if (expected is Iterable) { + return _compareIterables( + expected, actual, _recursiveMatch, depth + 1, location); + } else if (expected is Map) { + if (actual is! Map) { + return _Mismatch.simple(location, actual, 'expected a map'); + } + var err = (expected.length == actual.length) + ? '' + : 'has different length and '; + for (var key in expected.keys) { + if (!actual.containsKey(key)) { + return _Mismatch( + location, + actual, + (description, verbose) => description + .add('${err}is missing map key ') + .addDescriptionOf(key)); + } + } + + for (var key in actual.keys) { + if (!expected.containsKey(key)) { + return _Mismatch( + location, + actual, + (description, verbose) => description + .add('${err}has extra map key ') + .addDescriptionOf(key)); + } + } + + for (var key in expected.keys) { + var rp = _recursiveMatch( + expected[key], actual[key], "$location['$key']", depth + 1); + if (rp != null) return rp; + } + + return null; + } + } + + // If we have recursed, show the expected value too; if not, expect() will + // show it for us. + if (depth > 0) { + return _Mismatch(location, actual, + (description, verbose) => description.addDescriptionOf(expected), + instead: true); + } else { + return _Mismatch(location, actual, null); + } + } + + @override + bool matches(Object? actual, Map matchState) { + var mismatch = _recursiveMatch(_expected, actual, '', 0); + if (mismatch == null) return true; + addStateInfo(matchState, {'mismatch': mismatch}); + return false; + } + + @override + Description describe(Description description) => + description.addDescriptionOf(_expected); + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + var mismatch = matchState['mismatch'] as _Mismatch; + var describeProblem = mismatch.describeProblem; + if (mismatch.location.isNotEmpty) { + mismatchDescription + .add('at location ') + .add(mismatch.location) + .add(' is ') + .addDescriptionOf(mismatch.actual); + if (describeProblem != null) { + mismatchDescription + .add(' ${mismatch.instead ? 'instead of' : 'which'} '); + describeProblem(mismatchDescription, verbose); + } + } else { + // If we didn't get a good reason, that would normally be a + // simple 'is <value>' message. We only add that if the mismatch + // description is non empty (so we are supplementing the mismatch + // description). + if (describeProblem == null) { + if (mismatchDescription.length > 0) { + mismatchDescription.add('is ').addDescriptionOf(item); + } + } else { + describeProblem(mismatchDescription, verbose); + } + } + return mismatchDescription; + } +} + +class _Mismatch { + /// A human-readable description of the location within the collection where + /// the mismatch occurred. + final String location; + + /// The actual value found at [location]. + final Object? actual; + + /// Callback that can create a detailed description of the problem. + final void Function(Description, bool verbose)? describeProblem; + + /// If `true`, [describeProblem] describes the expected value, so when the + /// final mismatch description is pieced together, it will be preceded by + /// `instead of` (e.g. `at location [2] is <3> instead of <4>`). If `false`, + /// [describeProblem] is a problem description from a sub-matcher, so when the + /// final mismatch description is pieced together, it will be preceded by + /// `which` (e.g. `at location [2] is <foo> which has length of 3`). + final bool instead; + + _Mismatch(this.location, this.actual, this.describeProblem, + {this.instead = false}); + + _Mismatch.simple(this.location, this.actual, String problem) + : describeProblem = ((description, verbose) => description.add(problem)), + instead = false; +}
diff --git a/pkgs/matcher/lib/src/error_matchers.dart b/pkgs/matcher/lib/src/error_matchers.dart new file mode 100644 index 0000000..5b6238d --- /dev/null +++ b/pkgs/matcher/lib/src/error_matchers.dart
@@ -0,0 +1,48 @@ +// Copyright (c) 2012, 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 'type_matcher.dart'; + +/// A matcher for [ArgumentError]. +const isArgumentError = TypeMatcher<ArgumentError>(); + +/// A matcher for [TypeError]. +@Deprecated('CastError has been deprecated in favor of TypeError. ') +const isCastError = TypeMatcher<TypeError>(); + +/// A matcher for [ConcurrentModificationError]. +const isConcurrentModificationError = + TypeMatcher<ConcurrentModificationError>(); + +/// A matcher for [Error]. +@Deprecated( + 'CyclicInitializationError is deprecated and will be removed in Dart 3. ' + 'Use `isA<Error>()` instead.') +const isCyclicInitializationError = TypeMatcher<Error>(); + +/// A matcher for [Exception]. +const isException = TypeMatcher<Exception>(); + +/// A matcher for [FormatException]. +const isFormatException = TypeMatcher<FormatException>(); + +/// A matcher for [NoSuchMethodError]. +const isNoSuchMethodError = TypeMatcher<NoSuchMethodError>(); + +/// A matcher for [TypeError]. +@Deprecated('NullThrownError is deprecated and will be removed in Dart 3. ' + 'Use `isA<TypeError>()` instead.') +const isNullThrownError = TypeMatcher<TypeError>(); + +/// A matcher for [RangeError]. +const isRangeError = TypeMatcher<RangeError>(); + +/// A matcher for [StateError]. +const isStateError = TypeMatcher<StateError>(); + +/// A matcher for [UnimplementedError]. +const isUnimplementedError = TypeMatcher<UnimplementedError>(); + +/// A matcher for [UnsupportedError]. +const isUnsupportedError = TypeMatcher<UnsupportedError>();
diff --git a/pkgs/matcher/lib/src/expect/async_matcher.dart b/pkgs/matcher/lib/src/expect/async_matcher.dart new file mode 100644 index 0000000..854151d --- /dev/null +++ b/pkgs/matcher/lib/src/expect/async_matcher.dart
@@ -0,0 +1,68 @@ +// 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:test_api/hooks.dart'; + +import '../description.dart'; +import '../equals_matcher.dart'; +import '../interfaces.dart'; +import '../operator_matchers.dart'; +import '../type_matcher.dart'; +import 'expect.dart'; + +/// A matcher that does asynchronous computation. +/// +/// Rather than implementing [matches], subclasses implement [matchAsync]. +/// [AsyncMatcher.matches] ensures that the test doesn't complete until the +/// returned future completes, and [expect] returns a future that completes when +/// the returned future completes so that tests can wait for it. +abstract class AsyncMatcher extends Matcher { + const AsyncMatcher(); + + /// Returns `null` if this matches [item], or a [String] description of the + /// failure if it doesn't match. + /// + /// This can return a [Future] or a synchronous value. If it returns a + /// [Future], neither [expect] nor the test will complete until that [Future] + /// completes. + /// + /// If this returns a [String] synchronously, [expect] will synchronously + /// throw a [TestFailure] and [matches] will synchronously return `false`. + dynamic /*FutureOr<String>*/ matchAsync(dynamic item); + + @override + bool matches(dynamic item, Map matchState) { + final result = matchAsync(item); + expect( + result, + anyOf([ + equals(null), + const TypeMatcher<Future>(), + const TypeMatcher<String>() + ]), + reason: 'matchAsync() may only return a String, a Future, or null.'); + + if (result is Future) { + final outstandingWork = TestHandle.current.markPending(); + result.then((realResult) { + if (realResult != null) { + fail(formatFailure(this, item, realResult as String)); + } + outstandingWork.complete(); + }); + } else if (result is String) { + matchState[this] = result; + return false; + } + + return true; + } + + @override + Description describeMismatch(dynamic item, Description mismatchDescription, + Map matchState, bool verbose) => + StringDescription(matchState[this] as String); +}
diff --git a/pkgs/matcher/lib/src/expect/expect.dart b/pkgs/matcher/lib/src/expect/expect.dart new file mode 100644 index 0000000..8dd8cae --- /dev/null +++ b/pkgs/matcher/lib/src/expect/expect.dart
@@ -0,0 +1,161 @@ +// Copyright (c) 2015, 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:test_api/hooks.dart'; + +import '../description.dart'; +import '../equals_matcher.dart'; +import '../interfaces.dart'; +import '../operator_matchers.dart'; +import '../type_matcher.dart'; +import '../util.dart'; +import 'async_matcher.dart'; +import 'future_matchers.dart'; +import 'prints_matcher.dart'; +import 'throws_matcher.dart'; +import 'util/pretty_print.dart'; + +/// The type used for functions that can be used to build up error reports +/// upon failures in [expect]. +@Deprecated('Will be removed in 0.13.0.') +typedef ErrorFormatter = String Function(Object? actual, Matcher matcher, + String? reason, Map matchState, bool verbose); + +/// Assert that [actual] matches [matcher]. +/// +/// This is the main assertion function. [reason] is optional and is typically +/// not supplied, as a reason is generated from [matcher]; if [reason] +/// is included it is appended to the reason generated by the matcher. +/// +/// [matcher] can be a value in which case it will be wrapped in an +/// [equals] matcher. +/// +/// If the assertion fails a [TestFailure] is thrown. +/// +/// If [skip] is a String or `true`, the assertion is skipped. The arguments are +/// still evaluated, but [actual] is not verified to match [matcher]. If +/// [actual] is a [Future], the test won't complete until the future emits a +/// value. +/// +/// If [skip] is a string, it should explain why the assertion is skipped; this +/// reason will be printed when running the test. +/// +/// Certain matchers, like [completion] and [throwsA], either match or fail +/// asynchronously. When you use [expect] with these matchers, it ensures that +/// the test doesn't complete until the matcher has either matched or failed. If +/// you want to wait for the matcher to complete before continuing the test, you +/// can call [expectLater] instead and `await` the result. +void expect(dynamic actual, dynamic matcher, + {String? reason, + Object? /* String|bool */ skip, + @Deprecated('Will be removed in 0.13.0.') bool verbose = false, + @Deprecated('Will be removed in 0.13.0.') ErrorFormatter? formatter}) { + _expect(actual, matcher, + reason: reason, skip: skip, verbose: verbose, formatter: formatter); +} + +/// Just like [expect], but returns a [Future] that completes when the matcher +/// has finished matching. +/// +/// For the [completes] and [completion] matchers, as well as [throwsA] and +/// related matchers when they're matched against a [Future], the returned +/// future completes when the matched future completes. For the [prints] +/// matcher, it completes when the future returned by the callback completes. +/// Otherwise, it completes immediately. +/// +/// If the matcher fails asynchronously, that failure is piped to the returned +/// future where it can be handled by user code. +Future expectLater(dynamic actual, dynamic matcher, + {String? reason, Object? /* String|bool */ skip}) => + _expect(actual, matcher, reason: reason, skip: skip); + +/// The implementation of [expect] and [expectLater]. +Future _expect(Object? actual, Object? matcher, + {String? reason, skip, bool verbose = false, ErrorFormatter? formatter}) { + final test = TestHandle.current; + formatter ??= (actual, matcher, reason, matchState, verbose) { + var mismatchDescription = StringDescription(); + matcher.describeMismatch(actual, mismatchDescription, matchState, verbose); + + return formatFailure(matcher, actual, mismatchDescription.toString(), + reason: reason); + }; + + if (skip != null && skip is! bool && skip is! String) { + throw ArgumentError.value(skip, 'skip', 'must be a bool or a String'); + } + + matcher = wrapMatcher(matcher); + if (skip != null && skip != false) { + String message; + if (skip is String) { + message = 'Skip expect: $skip'; + } else if (reason != null) { + message = 'Skip expect ($reason).'; + } else { + var description = StringDescription().addDescriptionOf(matcher); + message = 'Skip expect ($description).'; + } + + test.markSkipped(message); + return Future.sync(() {}); + } + + if (matcher is AsyncMatcher) { + // Avoid async/await so that expect() throws synchronously when possible. + var result = matcher.matchAsync(actual); + expect( + result, + anyOf([ + equals(null), + const TypeMatcher<Future>(), + const TypeMatcher<String>() + ]), + reason: 'matchAsync() may only return a String, a Future, or null.'); + + if (result is String) { + fail(formatFailure(matcher, actual, result, reason: reason)); + } else if (result is Future) { + final outstandingWork = test.markPending(); + return result.then((realResult) { + if (realResult == null) return; + fail(formatFailure(matcher as Matcher, actual, realResult as String, + reason: reason)); + }).whenComplete( + // Always remove this, in case the failure is caught and handled + // gracefully. + outstandingWork.complete); + } + + return Future.sync(() {}); + } + + var matchState = {}; + try { + if ((matcher as Matcher).matches(actual, matchState)) { + return Future.sync(() {}); + } + } catch (e, trace) { + reason ??= '$e at $trace'; + } + fail(formatter(actual, matcher as Matcher, reason, matchState, verbose)); +} + +/// Convenience method for throwing a new [TestFailure] with the provided +/// [message]. +Never fail(String message) => throw TestFailure(message); + +// The default error formatter. +@Deprecated('Will be removed in 0.13.0.') +String formatFailure(Matcher expected, Object? actual, String which, + {String? reason}) { + var buffer = StringBuffer(); + buffer.writeln(indent(prettyPrint(expected), first: 'Expected: ')); + buffer.writeln(indent(prettyPrint(actual), first: ' Actual: ')); + if (which.isNotEmpty) buffer.writeln(indent(which, first: ' Which: ')); + if (reason != null) buffer.writeln(reason); + return buffer.toString(); +}
diff --git a/pkgs/matcher/lib/src/expect/expect_async.dart b/pkgs/matcher/lib/src/expect/expect_async.dart new file mode 100644 index 0000000..88cf6f2 --- /dev/null +++ b/pkgs/matcher/lib/src/expect/expect_async.dart
@@ -0,0 +1,586 @@ +// Copyright (c) 2015, 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_api/hooks.dart'; + +import 'util/placeholder.dart'; + +// Function types returned by expectAsync# methods. + +typedef Func0<T> = T Function(); +typedef Func1<T, A> = T Function([A a]); +typedef Func2<T, A, B> = T Function([A a, B b]); +typedef Func3<T, A, B, C> = T Function([A a, B b, C c]); +typedef Func4<T, A, B, C, D> = T Function([A a, B b, C c, D d]); +typedef Func5<T, A, B, C, D, E> = T Function([A a, B b, C c, D d, E e]); +typedef Func6<T, A, B, C, D, E, F> = T Function([A a, B b, C c, D d, E e, F f]); + +/// A wrapper for a function that ensures that it's called the appropriate +/// number of times. +/// +/// The containing test won't be considered to have completed successfully until +/// this function has been called the appropriate number of times. +/// +/// The wrapper function is accessible via [func]. It supports up to six +/// optional and/or required positional arguments, but no named arguments. +class _ExpectedFunction<T> { + /// The wrapped callback. + final Function _callback; + + /// The minimum number of calls that are expected to be made to the function. + /// + /// If fewer calls than this are made, the test will fail. + final int _minExpectedCalls; + + /// The maximum number of calls that are expected to be made to the function. + /// + /// If more calls than this are made, the test will fail. + final int _maxExpectedCalls; + + /// A callback that should return whether the function is not expected to have + /// any more calls. + /// + /// This will be called after every time the function is run. The test case + /// won't be allowed to terminate until it returns `true`. + /// + /// This may be `null`. If so, the function is considered to be done after + /// it's been run once. + final bool Function()? _isDone; + + /// A descriptive name for the function. + final String _id; + + /// An optional description of why the function is expected to be called. + /// + /// If not passed, this will be an empty string. + final String _reason; + + /// The number of times the function has been called. + int _actualCalls = 0; + + /// The test in which this function was wrapped. + late final TestHandle _test; + + /// Whether this function has been called the requisite number of times. + late bool _complete; + + OutstandingWork? _outstandingWork; + + /// Wraps [callback] in a function that asserts that it's called at least + /// [minExpected] times and no more than [maxExpected] times. + /// + /// If passed, [id] is used as a descriptive name fo the function and [reason] + /// as a reason it's expected to be called. If [isDone] is passed, the test + /// won't be allowed to complete until it returns `true`. + _ExpectedFunction(Function callback, int minExpected, int maxExpected, + {String? id, String? reason, bool Function()? isDone}) + : _callback = callback, + _minExpectedCalls = minExpected, + _maxExpectedCalls = + (maxExpected == 0 && minExpected > 0) ? minExpected : maxExpected, + _isDone = isDone, + _reason = reason == null ? '' : '\n$reason', + _id = _makeCallbackId(id, callback) { + try { + _test = TestHandle.current; + } on OutsideTestException { + throw StateError('`expectAsync` must be called within a test.'); + } + + if (maxExpected > 0 && minExpected > maxExpected) { + throw ArgumentError('max ($maxExpected) may not be less than count ' + '($minExpected).'); + } + + if (isDone != null || minExpected > 0) { + _outstandingWork = _test.markPending(); + _complete = false; + } else { + _complete = true; + } + } + + /// Tries to find a reasonable name for [callback]. + /// + /// If [id] is passed, uses that. Otherwise, tries to determine a name from + /// calling `toString`. If no name can be found, returns the empty string. + static String _makeCallbackId(String? id, Function callback) { + if (id != null) return '$id '; + + // If the callback is not an anonymous closure, try to get the + // name. + var toString = callback.toString(); + var prefix = "Function '"; + var start = toString.indexOf(prefix); + if (start == -1) return ''; + + start += prefix.length; + var end = toString.indexOf("'", start); + if (end == -1) return ''; + return '${toString.substring(start, end)} '; + } + + /// Returns a function that has the same number of positional arguments as the + /// wrapped function (up to a total of 6). + Function get func { + if (_callback is Function(Never, Never, Never, Never, Never, Never)) { + return max6; + } + if (_callback is Function(Never, Never, Never, Never, Never)) return max5; + if (_callback is Function(Never, Never, Never, Never)) return max4; + if (_callback is Function(Never, Never, Never)) return max3; + if (_callback is Function(Never, Never)) return max2; + if (_callback is Function(Never)) return max1; + if (_callback is Function()) return max0; + + _outstandingWork?.complete(); + throw ArgumentError( + 'The wrapped function has more than 6 required arguments'); + } + + // This indirection is critical. It ensures the returned function has an + // argument count of zero. + T max0() => max6(); + + T max1([Object? a0 = placeholder]) => max6(a0); + + T max2([Object? a0 = placeholder, Object? a1 = placeholder]) => max6(a0, a1); + + T max3( + [Object? a0 = placeholder, + Object? a1 = placeholder, + Object? a2 = placeholder]) => + max6(a0, a1, a2); + + T max4( + [Object? a0 = placeholder, + Object? a1 = placeholder, + Object? a2 = placeholder, + Object? a3 = placeholder]) => + max6(a0, a1, a2, a3); + + T max5( + [Object? a0 = placeholder, + Object? a1 = placeholder, + Object? a2 = placeholder, + Object? a3 = placeholder, + Object? a4 = placeholder]) => + max6(a0, a1, a2, a3, a4); + + T max6( + [Object? a0 = placeholder, + Object? a1 = placeholder, + Object? a2 = placeholder, + Object? a3 = placeholder, + Object? a4 = placeholder, + Object? a5 = placeholder]) => + _run([a0, a1, a2, a3, a4, a5].where((a) => a != placeholder)); + + /// Runs the wrapped function with [args] and returns its return value. + T _run(Iterable args) { + // Note that in the old test, this returned `null` if it encountered an + // error, where now it just re-throws that error because Zone machinery will + // pass it to the invoker anyway. + try { + _actualCalls++; + if (_test.shouldBeDone) { + throw TestFailure( + 'Callback ${_id}called ($_actualCalls) after test case ' + '${_test.name} had already completed.$_reason'); + } else if (_maxExpectedCalls >= 0 && _actualCalls > _maxExpectedCalls) { + throw TestFailure('Callback ${_id}called more times than expected ' + '($_maxExpectedCalls).$_reason'); + } + + return Function.apply(_callback, args.toList()) as T; + } finally { + _afterRun(); + } + } + + /// After each time the function is run, check to see if it's complete. + void _afterRun() { + if (_complete) return; + if (_minExpectedCalls > 0 && _actualCalls < _minExpectedCalls) return; + if (_isDone != null && !_isDone()) return; + + // Mark this callback as complete and remove it from the test case's + // outstanding callback count; if that hits zero the test is done. + _complete = true; + _outstandingWork?.complete(); + } +} + +/// This function is deprecated because it doesn't work well with strong mode. +/// Use [expectAsync0], [expectAsync1], +/// [expectAsync2], [expectAsync3], [expectAsync4], [expectAsync5], or +/// [expectAsync6] instead. +@Deprecated('Will be removed in 0.13.0') +Function expectAsync(Function callback, + {int count = 1, int max = 0, String? id, String? reason}) => + _ExpectedFunction(callback, count, max, id: id, reason: reason).func; + +/// Informs the framework that the given [callback] of arity 0 is expected to be +/// called [count] number of times (by default 1). +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// The test framework will wait for the callback to run the [count] times +/// before it considers the current test to be complete. +/// +/// [max] can be used to specify an upper bound on the number of calls; if this +/// is exceeded the test will fail. If [max] is `0` (the default), the callback +/// is expected to be called exactly [count] times. If [max] is `-1`, the +/// callback is allowed to be called any number of times greater than [count]. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with zero arguments. See also +/// [expectAsync1], [expectAsync2], [expectAsync3], [expectAsync4], +/// [expectAsync5], and [expectAsync6] for callbacks with different arity. +Func0<T> expectAsync0<T>(T Function() callback, + {int count = 1, int max = 0, String? id, String? reason}) => + _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max0; + +/// Informs the framework that the given [callback] of arity 1 is expected to be +/// called [count] number of times (by default 1). +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// The test framework will wait for the callback to run the [count] times +/// before it considers the current test to be complete. +/// +/// [max] can be used to specify an upper bound on the number of calls; if this +/// is exceeded the test will fail. If [max] is `0` (the default), the callback +/// is expected to be called exactly [count] times. If [max] is `-1`, the +/// callback is allowed to be called any number of times greater than [count]. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with one argument. See also +/// [expectAsync0], [expectAsync2], [expectAsync3], [expectAsync4], +/// [expectAsync5], and [expectAsync6] for callbacks with different arity. +Func1<T, A> expectAsync1<T, A>(T Function(A) callback, + {int count = 1, int max = 0, String? id, String? reason}) => + _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max1; + +/// Informs the framework that the given [callback] of arity 2 is expected to be +/// called [count] number of times (by default 1). +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// The test framework will wait for the callback to run the [count] times +/// before it considers the current test to be complete. +/// +/// [max] can be used to specify an upper bound on the number of calls; if this +/// is exceeded the test will fail. If [max] is `0` (the default), the callback +/// is expected to be called exactly [count] times. If [max] is `-1`, the +/// callback is allowed to be called any number of times greater than [count]. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with two arguments. See also +/// [expectAsync0], [expectAsync1], [expectAsync3], [expectAsync4], +/// [expectAsync5], and [expectAsync6] for callbacks with different arity. +Func2<T, A, B> expectAsync2<T, A, B>(T Function(A, B) callback, + {int count = 1, int max = 0, String? id, String? reason}) => + _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max2; + +/// Informs the framework that the given [callback] of arity 3 is expected to be +/// called [count] number of times (by default 1). +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// The test framework will wait for the callback to run the [count] times +/// before it considers the current test to be complete. +/// +/// [max] can be used to specify an upper bound on the number of calls; if this +/// is exceeded the test will fail. If [max] is `0` (the default), the callback +/// is expected to be called exactly [count] times. If [max] is `-1`, the +/// callback is allowed to be called any number of times greater than [count]. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with three arguments. See also +/// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync4], +/// [expectAsync5], and [expectAsync6] for callbacks with different arity. +Func3<T, A, B, C> expectAsync3<T, A, B, C>(T Function(A, B, C) callback, + {int count = 1, int max = 0, String? id, String? reason}) => + _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max3; + +/// Informs the framework that the given [callback] of arity 4 is expected to be +/// called [count] number of times (by default 1). +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// The test framework will wait for the callback to run the [count] times +/// before it considers the current test to be complete. +/// +/// [max] can be used to specify an upper bound on the number of calls; if this +/// is exceeded the test will fail. If [max] is `0` (the default), the callback +/// is expected to be called exactly [count] times. If [max] is `-1`, the +/// callback is allowed to be called any number of times greater than [count]. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with four arguments. See also +/// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync3], +/// [expectAsync5], and [expectAsync6] for callbacks with different arity. +Func4<T, A, B, C, D> expectAsync4<T, A, B, C, D>( + T Function(A, B, C, D) callback, + {int count = 1, + int max = 0, + String? id, + String? reason}) => + _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max4; + +/// Informs the framework that the given [callback] of arity 5 is expected to be +/// called [count] number of times (by default 1). +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// The test framework will wait for the callback to run the [count] times +/// before it considers the current test to be complete. +/// +/// [max] can be used to specify an upper bound on the number of calls; if this +/// is exceeded the test will fail. If [max] is `0` (the default), the callback +/// is expected to be called exactly [count] times. If [max] is `-1`, the +/// callback is allowed to be called any number of times greater than [count]. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with five arguments. See also +/// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync3], +/// [expectAsync4], and [expectAsync6] for callbacks with different arity. +Func5<T, A, B, C, D, E> expectAsync5<T, A, B, C, D, E>( + T Function(A, B, C, D, E) callback, + {int count = 1, + int max = 0, + String? id, + String? reason}) => + _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max5; + +/// Informs the framework that the given [callback] of arity 6 is expected to be +/// called [count] number of times (by default 1). +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// The test framework will wait for the callback to run the [count] times +/// before it considers the current test to be complete. +/// +/// [max] can be used to specify an upper bound on the number of calls; if this +/// is exceeded the test will fail. If [max] is `0` (the default), the callback +/// is expected to be called exactly [count] times. If [max] is `-1`, the +/// callback is allowed to be called any number of times greater than [count]. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with six arguments. See also +/// [expectAsync0], [expectAsync1], [expectAsync2], [expectAsync3], +/// [expectAsync4], and [expectAsync5] for callbacks with different arity. +Func6<T, A, B, C, D, E, F> expectAsync6<T, A, B, C, D, E, F>( + T Function(A, B, C, D, E, F) callback, + {int count = 1, + int max = 0, + String? id, + String? reason}) => + _ExpectedFunction<T>(callback, count, max, id: id, reason: reason).max6; + +/// This function is deprecated because it doesn't work well with strong mode. +/// Use [expectAsyncUntil0], [expectAsyncUntil1], +/// [expectAsyncUntil2], [expectAsyncUntil3], [expectAsyncUntil4], +/// [expectAsyncUntil5], or [expectAsyncUntil6] instead. +@Deprecated('Will be removed in 0.13.0') +Function expectAsyncUntil(Function callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction(callback, 0, -1, id: id, reason: reason, isDone: isDone) + .func; + +/// Informs the framework that the given [callback] of arity 0 is expected to be +/// called until [isDone] returns true. +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// [isDone] is called after each time the function is run. Only when it returns +/// true will the callback be considered complete. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with zero arguments. See also +/// [expectAsyncUntil1], [expectAsyncUntil2], [expectAsyncUntil3], +/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for +/// callbacks with different arity. +Func0<T> expectAsyncUntil0<T>(T Function() callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction<T>(callback, 0, -1, + id: id, reason: reason, isDone: isDone) + .max0; + +/// Informs the framework that the given [callback] of arity 1 is expected to be +/// called until [isDone] returns true. +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// [isDone] is called after each time the function is run. Only when it returns +/// true will the callback be considered complete. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with one argument. See also +/// [expectAsyncUntil0], [expectAsyncUntil2], [expectAsyncUntil3], +/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for +/// callbacks with different arity. +Func1<T, A> expectAsyncUntil1<T, A>( + T Function(A) callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction<T>(callback, 0, -1, + id: id, reason: reason, isDone: isDone) + .max1; + +/// Informs the framework that the given [callback] of arity 2 is expected to be +/// called until [isDone] returns true. +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// [isDone] is called after each time the function is run. Only when it returns +/// true will the callback be considered complete. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with two arguments. See also +/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil3], +/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for +/// callbacks with different arity. +Func2<T, A, B> expectAsyncUntil2<T, A, B>( + T Function(A, B) callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction<T>(callback, 0, -1, + id: id, reason: reason, isDone: isDone) + .max2; + +/// Informs the framework that the given [callback] of arity 3 is expected to be +/// called until [isDone] returns true. +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// [isDone] is called after each time the function is run. Only when it returns +/// true will the callback be considered complete. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with three arguments. See also +/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2], +/// [expectAsyncUntil4], [expectAsyncUntil5], and [expectAsyncUntil6] for +/// callbacks with different arity. +Func3<T, A, B, C> expectAsyncUntil3<T, A, B, C>( + T Function(A, B, C) callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction<T>(callback, 0, -1, + id: id, reason: reason, isDone: isDone) + .max3; + +/// Informs the framework that the given [callback] of arity 4 is expected to be +/// called until [isDone] returns true. +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// [isDone] is called after each time the function is run. Only when it returns +/// true will the callback be considered complete. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with four arguments. See also +/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2], +/// [expectAsyncUntil3], [expectAsyncUntil5], and [expectAsyncUntil6] for +/// callbacks with different arity. +Func4<T, A, B, C, D> expectAsyncUntil4<T, A, B, C, D>( + T Function(A, B, C, D) callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction<T>(callback, 0, -1, + id: id, reason: reason, isDone: isDone) + .max4; + +/// Informs the framework that the given [callback] of arity 5 is expected to be +/// called until [isDone] returns true. +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// [isDone] is called after each time the function is run. Only when it returns +/// true will the callback be considered complete. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with five arguments. See also +/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2], +/// [expectAsyncUntil3], [expectAsyncUntil4], and [expectAsyncUntil6] for +/// callbacks with different arity. +Func5<T, A, B, C, D, E> expectAsyncUntil5<T, A, B, C, D, E>( + T Function(A, B, C, D, E) callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction<T>(callback, 0, -1, + id: id, reason: reason, isDone: isDone) + .max5; + +/// Informs the framework that the given [callback] of arity 6 is expected to be +/// called until [isDone] returns true. +/// +/// Returns a wrapped function that should be used as a replacement of the +/// original callback. +/// +/// [isDone] is called after each time the function is run. Only when it returns +/// true will the callback be considered complete. +/// +/// Both [id] and [reason] are optional and provide extra information about the +/// callback when debugging. [id] should be the name of the callback, while +/// [reason] should be the reason the callback is expected to be called. +/// +/// This method takes callbacks with six arguments. See also +/// [expectAsyncUntil0], [expectAsyncUntil1], [expectAsyncUntil2], +/// [expectAsyncUntil3], [expectAsyncUntil4], and [expectAsyncUntil5] for +/// callbacks with different arity. +Func6<T, A, B, C, D, E, F> expectAsyncUntil6<T, A, B, C, D, E, F>( + T Function(A, B, C, D, E, F) callback, bool Function() isDone, + {String? id, String? reason}) => + _ExpectedFunction<T>(callback, 0, -1, + id: id, reason: reason, isDone: isDone) + .max6;
diff --git a/pkgs/matcher/lib/src/expect/future_matchers.dart b/pkgs/matcher/lib/src/expect/future_matchers.dart new file mode 100644 index 0000000..407b9b8 --- /dev/null +++ b/pkgs/matcher/lib/src/expect/future_matchers.dart
@@ -0,0 +1,119 @@ +// Copyright (c) 2012, 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:test_api/hooks.dart' show pumpEventQueue; + +import '../description.dart'; +import '../interfaces.dart'; +import '../util.dart'; +import 'async_matcher.dart'; +import 'expect.dart'; +import 'throws_matcher.dart'; +import 'util/pretty_print.dart'; + +/// Matches a [Future] that completes successfully with any value. +/// +/// This creates an asynchronous expectation. The call to [expect] will return +/// immediately and execution will continue. To wait for the future to +/// complete and the expectation to run use [expectLater] and wait on the +/// returned future. +/// +/// To test that a Future completes with an exception, you can use [throws] and +/// [throwsA]. +final Matcher completes = const _Completes(null); + +/// Matches a [Future] that completes successfully with a value that matches +/// [matcher]. +/// +/// This creates an asynchronous expectation. The call to [expect] will return +/// immediately and execution will continue. Later, when the future completes, +/// the expectation against [matcher] will run. To wait for the future to +/// complete and the expectation to run use [expectLater] and wait on the +/// returned future. +/// +/// To test that a Future completes with an exception, you can use [throws] and +/// [throwsA]. +Matcher completion(Object? matcher, + [@Deprecated('this parameter is ignored') String? description]) => + _Completes(wrapMatcher(matcher)); + +class _Completes extends AsyncMatcher { + final Matcher? _matcher; + + const _Completes(this._matcher); + + // Avoid async/await so we synchronously start listening to [item]. + @override + dynamic /*FutureOr<String>*/ matchAsync(Object? item) { + if (item is! Future) return 'was not a Future'; + + return item.then((value) async { + if (_matcher == null) return null; + + String? result; + if (_matcher is AsyncMatcher) { + result = await _matcher.matchAsync(value) as String?; + if (result == null) return null; + } else { + var matchState = {}; + if (_matcher.matches(value, matchState)) return null; + result = _matcher + .describeMismatch(value, StringDescription(), matchState, false) + .toString(); + } + + var buffer = StringBuffer(); + buffer.writeln(indent(prettyPrint(value), first: 'emitted ')); + if (result.isNotEmpty) buffer.writeln(indent(result, first: ' which ')); + return buffer.toString().trimRight(); + }); + } + + @override + Description describe(Description description) { + if (_matcher == null) { + description.add('completes successfully'); + } else { + description.add('completes to a value that ').addDescriptionOf(_matcher); + } + return description; + } +} + +/// Matches a [Future] that does not complete. +/// +/// Note that this creates an asynchronous expectation. The call to +/// `expect()` that includes this will return immediately and execution will +/// continue. +final Matcher doesNotComplete = const _DoesNotComplete(); + +class _DoesNotComplete extends Matcher { + const _DoesNotComplete(); + + @override + Description describe(Description description) { + description.add('does not complete'); + return description; + } + + @override + bool matches(Object? item, Map matchState) { + if (item is! Future) return false; + item.then((value) { + fail('Future was not expected to complete but completed with a value of ' + '$value'); + }); + expect(pumpEventQueue(), completes); + return true; + } + + @override + Description describeMismatch( + Object? item, Description description, Map matchState, bool verbose) { + if (item is! Future) return description.add('$item is not a Future'); + return description; + } +}
diff --git a/pkgs/matcher/lib/src/expect/never_called.dart b/pkgs/matcher/lib/src/expect/never_called.dart new file mode 100644 index 0000000..20b5299 --- /dev/null +++ b/pkgs/matcher/lib/src/expect/never_called.dart
@@ -0,0 +1,68 @@ +// 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 'package:stack_trace/stack_trace.dart'; +import 'package:test_api/hooks.dart'; + +import 'expect.dart'; +import 'future_matchers.dart'; +import 'util/placeholder.dart'; +import 'util/pretty_print.dart'; + +/// Returns a function that causes the test to fail if it's called. +/// +/// This can safely be passed in place of any callback that takes ten or fewer +/// positional parameters. For example: +/// +/// ```dart +/// // Asserts that the stream never emits an event. +/// stream.listen(neverCalled); +/// ``` +/// +/// This also ensures that the test doesn't complete until a call to +/// [pumpEventQueue] finishes, so that the callback has a chance to be called. +Null Function( + [Object?, + Object?, + Object?, + Object?, + Object?, + Object?, + Object?, + Object?, + Object?, + Object?]) get neverCalled { + // Make sure the test stays alive long enough to call the function if it's + // going to. + expect(pumpEventQueue(), completes); + + var zone = Zone.current; + return ( + [a1 = placeholder, + a2 = placeholder, + a3 = placeholder, + a4 = placeholder, + a5 = placeholder, + a6 = placeholder, + a7 = placeholder, + a8 = placeholder, + a9 = placeholder, + a10 = placeholder]) { + var arguments = [a1, a2, a3, a4, a5, a6, a7, a8, a9, a10] + .where((argument) => argument != placeholder) + .toList(); + + var argsText = arguments.isEmpty + ? ' no arguments.' + : ':\n${bullet(arguments.map(prettyPrint))}'; + zone.handleUncaughtError( + TestFailure( + 'Callback should never have been called, but it was called with' + '$argsText'), + zone.run(Chain.current)); + return null; + }; +}
diff --git a/pkgs/matcher/lib/src/expect/prints_matcher.dart b/pkgs/matcher/lib/src/expect/prints_matcher.dart new file mode 100644 index 0000000..57ae95e --- /dev/null +++ b/pkgs/matcher/lib/src/expect/prints_matcher.dart
@@ -0,0 +1,72 @@ +// Copyright (c) 2014, 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 '../description.dart'; +import '../interfaces.dart'; +import '../util.dart'; +import 'async_matcher.dart'; +import 'expect.dart'; +import 'util/pretty_print.dart'; + +/// Matches a [Function] that prints text that matches [matcher]. +/// +/// [matcher] may be a String or a [Matcher]. +/// +/// If the function this runs against returns a [Future], all text printed by +/// the function (using [Zone] scoping) until that Future completes is matched. +/// +/// This only tracks text printed using the [print] function. +/// +/// This returns an [AsyncMatcher], so [expect] won't complete until the matched +/// function does. +Matcher prints(Object? matcher) => _Prints(wrapMatcher(matcher)); + +class _Prints extends AsyncMatcher { + final Matcher _matcher; + + _Prints(this._matcher); + + // Avoid async/await so we synchronously fail if the function is + // synchronous. + @override + dynamic /*FutureOr<String>*/ matchAsync(Object? item) { + if (item is! Function()) return 'was not a unary Function'; + + var buffer = StringBuffer(); + var result = runZoned(item, + zoneSpecification: ZoneSpecification(print: (_, __, ____, line) { + buffer.writeln(line); + })); + + return result is Future + ? result.then((_) => _check(buffer.toString())) + : _check(buffer.toString()); + } + + @override + Description describe(Description description) => + description.add('prints ').addDescriptionOf(_matcher); + + /// Verifies that [actual] matches [_matcher] and returns a [String] + /// description of the failure if it doesn't. + String? _check(String actual) { + var matchState = {}; + if (_matcher.matches(actual, matchState)) return null; + + var result = _matcher + .describeMismatch(actual, StringDescription(), matchState, false) + .toString(); + + var buffer = StringBuffer(); + if (actual.isEmpty) { + buffer.writeln('printed nothing'); + } else { + buffer.writeln(indent(prettyPrint(actual), first: 'printed ')); + } + if (result.isNotEmpty) buffer.writeln(indent(result, first: ' which ')); + return buffer.toString().trimRight(); + } +}
diff --git a/pkgs/matcher/lib/src/expect/stream_matcher.dart b/pkgs/matcher/lib/src/expect/stream_matcher.dart new file mode 100644 index 0000000..0c1d852 --- /dev/null +++ b/pkgs/matcher/lib/src/expect/stream_matcher.dart
@@ -0,0 +1,196 @@ +// 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:async/async.dart'; +import 'package:test_api/hooks.dart'; + +import '../interfaces.dart'; +import 'async_matcher.dart'; +import 'expect.dart'; +import 'util/pretty_print.dart'; + +/// A matcher that matches events from [Stream]s or [StreamQueue]s. +/// +/// Stream matchers are designed to make it straightforward to create complex +/// expectations for streams, and to interleave expectations with the rest of a +/// test. They can be used on a [Stream] to match all events it emits: +/// +/// ```dart +/// expect(stream, emitsInOrder([ +/// // Values match individual events. +/// "Ready.", +/// +/// // Matchers also run against individual events. +/// startsWith("Loading took"), +/// +/// // Stream matchers can be nested. This asserts that one of two events are +/// // emitted after the "Loading took" line. +/// emitsAnyOf(["Succeeded!", "Failed!"]), +/// +/// // By default, more events are allowed after the matcher finishes +/// // matching. This asserts instead that the stream emits a done event and +/// // nothing else. +/// emitsDone +/// ])); +/// ``` +/// +/// It can also match a [StreamQueue], in which case it consumes the matched +/// events. The call to [expect] returns a [Future] that completes when the +/// matcher is done matching. You can `await` this to consume different events +/// at different times: +/// +/// ```dart +/// var stdout = StreamQueue(stdoutLineStream); +/// +/// // Ignore lines from the process until it's about to emit the URL. +/// await expectLater(stdout, emitsThrough('WebSocket URL:')); +/// +/// // Parse the next line as a URL. +/// var url = Uri.parse(await stdout.next); +/// expect(url.host, equals('localhost')); +/// +/// // You can match against the same StreamQueue multiple times. +/// await expectLater(stdout, emits('Waiting for connection...')); +/// ``` +/// +/// Users can call [StreamMatcher] to create custom matchers. +abstract class StreamMatcher extends Matcher { + /// The description of this matcher. + /// + /// This is in the subjunctive mood, which means it can be used after the word + /// "should". For example, it might be "emit the right events". + String get description; + + /// Creates a new [StreamMatcher] described by [description] that matches + /// events with [matchQueue]. + /// + /// The [matchQueue] callback is used to implement [StreamMatcher.matchQueue], + /// and should follow all the guarantees of that method. In particular: + /// + /// * If it matches successfully, it should return `null` and possibly consume + /// events. + /// * If it fails to match, consume no events and return a description of the + /// failure. + /// * The description should be in past tense. + /// * The description should be grammatically valid when used after "the + /// stream"—"emitted the wrong events", for example. + /// + /// The [matchQueue] callback may return the empty string to indicate a + /// failure if it has no information to add beyond the description of the + /// failure and the events actually emitted by the stream. + /// + /// The [description] should be in the subjunctive mood. This means that it + /// should be grammatically valid when used after the word "should". For + /// example, it might be "emit the right events". + factory StreamMatcher(Future<String?> Function(StreamQueue) matchQueue, + String description) = _StreamMatcher; + + /// Tries to match events emitted by [queue]. + /// + /// If this matches successfully, it consumes the matching events from [queue] + /// and returns `null`. + /// + /// If this fails to match, it doesn't consume any events and returns a + /// description of the failure. This description is in the past tense, and + /// could grammatically be used after "the stream". For example, it might + /// return "emitted the wrong events". + /// + /// The description string may also be empty, which indicates that the + /// matcher's description and the events actually emitted by the stream are + /// enough to understand the failure. + /// + /// If the queue emits an error, that error is re-thrown unless otherwise + /// indicated by the matcher. + Future<String?> matchQueue(StreamQueue queue); +} + +/// A concrete implementation of [StreamMatcher]. +/// +/// This is separate from the original type to hide the private [AsyncMatcher] +/// interface. +class _StreamMatcher extends AsyncMatcher implements StreamMatcher { + @override + final String description; + + /// The callback used to implement [matchQueue]. + final Future<String?> Function(StreamQueue) _matchQueue; + + _StreamMatcher(this._matchQueue, this.description); + + @override + Future<String?> matchQueue(StreamQueue queue) => _matchQueue(queue); + + @override + dynamic /*FutureOr<String>*/ matchAsync(Object? item) { + StreamQueue queue; + var shouldCancelQueue = false; + if (item is StreamQueue) { + queue = item; + } else if (item is Stream) { + queue = StreamQueue(item); + shouldCancelQueue = true; + } else { + return 'was not a Stream or a StreamQueue'; + } + + // Avoid async/await in the outer method so that we synchronously error out + // for an invalid argument type. + var transaction = queue.startTransaction(); + var copy = transaction.newQueue(); + return matchQueue(copy).then((result) async { + // Accept the transaction if the result is null, indicating that the match + // succeeded. + if (result == null) { + transaction.commit(copy); + return null; + } + + // Get a list of events emitted by the stream so we can emit them as part + // of the error message. + var replay = transaction.newQueue(); + var events = <Result?>[]; + var subscription = Result.captureStreamTransformer + .bind(replay.rest.cast()) + .listen(events.add, onDone: () => events.add(null)); + + // Wait on a timer tick so all buffered events are emitted. + await Future.delayed(Duration.zero); + _unawaited(subscription.cancel()); + + var eventsString = events.map((event) { + if (event == null) { + return 'x Stream closed.'; + } else if (event.isValue) { + return addBullet(event.asValue!.value.toString()); + } else { + var error = event.asError!; + var chain = TestHandle.current.formatStackTrace(error.stackTrace); + var text = '${error.error}\n$chain'; + return indent(text, first: '! '); + } + }).join('\n'); + if (eventsString.isEmpty) eventsString = 'no events'; + + transaction.reject(); + + var buffer = StringBuffer(); + buffer.writeln(indent(eventsString, first: 'emitted ')); + if (result.isNotEmpty) buffer.writeln(indent(result, first: ' which ')); + return buffer.toString().trimRight(); + }, onError: (Object error) { + transaction.reject(); + // ignore: only_throw_errors + throw error; + }).then((result) { + if (shouldCancelQueue) queue.cancel(); + return result; + }); + } + + @override + Description describe(Description description) => + description.add('should ').add(this.description); +} + +void _unawaited(Future<void> f) {}
diff --git a/pkgs/matcher/lib/src/expect/stream_matchers.dart b/pkgs/matcher/lib/src/expect/stream_matchers.dart new file mode 100644 index 0000000..02efff3 --- /dev/null +++ b/pkgs/matcher/lib/src/expect/stream_matchers.dart
@@ -0,0 +1,377 @@ +// 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:async/async.dart'; + +import '../description.dart'; +import '../interfaces.dart'; +import '../util.dart'; +import 'async_matcher.dart'; +import 'stream_matcher.dart'; +import 'throws_matcher.dart'; +import 'util/pretty_print.dart'; + +/// Returns a [StreamMatcher] that asserts that the stream emits a "done" event. +final emitsDone = StreamMatcher( + (queue) async => (await queue.hasNext) ? '' : null, 'be done'); + +/// Returns a [StreamMatcher] for [matcher]. +/// +/// If [matcher] is already a [StreamMatcher], it's returned as-is. If it's any +/// other [Matcher], this matches a single event that matches that matcher. If +/// it's any other Object, this matches a single event that's equal to that +/// object. +/// +/// This functions like [wrapMatcher] for [StreamMatcher]s: it can convert any +/// matcher-like value into a proper [StreamMatcher]. +StreamMatcher emits(Object? matcher) { + if (matcher is StreamMatcher) return matcher; + var wrapped = wrapMatcher(matcher); + + var matcherDescription = wrapped.describe(StringDescription()); + + return StreamMatcher((queue) async { + if (!await queue.hasNext) return ''; + + var matchState = {}; + var actual = await queue.next; + if (wrapped.matches(actual, matchState)) return null; + + var mismatchDescription = StringDescription(); + wrapped.describeMismatch(actual, mismatchDescription, matchState, false); + + if (mismatchDescription.length == 0) return ''; + return 'emitted an event that $mismatchDescription'; + }, + // TODO(nweiz): add "should" once matcher#42 is fixed. + 'emit an event that $matcherDescription'); +} + +/// Returns a [StreamMatcher] that matches a single error event that matches +/// [matcher]. +StreamMatcher emitsError(Object? matcher) { + var wrapped = wrapMatcher(matcher); + var matcherDescription = wrapped.describe(StringDescription()); + var throwsMatcher = throwsA(wrapped) as AsyncMatcher; + + return StreamMatcher( + (queue) => throwsMatcher.matchAsync(queue.next) as Future<String?>, + // TODO(nweiz): add "should" once matcher#42 is fixed. + 'emit an error that $matcherDescription'); +} + +/// Returns a [StreamMatcher] that allows (but doesn't require) [matcher] to +/// match the stream. +/// +/// This matcher always succeeds; if [matcher] doesn't match, this just consumes +/// no events. +StreamMatcher mayEmit(Object? matcher) { + var streamMatcher = emits(matcher); + return StreamMatcher((queue) async { + await queue.withTransaction( + (copy) async => (await streamMatcher.matchQueue(copy)) == null); + return null; + }, 'maybe ${streamMatcher.description}'); +} + +/// Returns a [StreamMatcher] that matches the stream if at least one of +/// [matchers] matches. +/// +/// If multiple matchers match the stream, this chooses the matcher that +/// consumes as many events as possible. +/// +/// If any matchers match the stream, no errors from other matchers are thrown. +/// If no matchers match and multiple matchers threw errors, the first error is +/// re-thrown. +StreamMatcher emitsAnyOf(Iterable matchers) { + var streamMatchers = matchers.map(emits).toList(); + if (streamMatchers.isEmpty) { + throw ArgumentError('matcher may not be empty'); + } + + if (streamMatchers.length == 1) return streamMatchers.first; + var description = 'do one of the following:\n' + '${bullet(streamMatchers.map((matcher) => matcher.description))}'; + + return StreamMatcher((queue) async { + var transaction = queue.startTransaction(); + + // Allocate the failures list ahead of time so that its order matches the + // order of [matchers], and thus the order the matchers will be listed in + // the description. + var failures = List<String?>.filled(matchers.length, null); + + // The first error thrown. If no matchers match and this exists, we rethrow + // it. + Object? firstError; + StackTrace? firstStackTrace; + + var futures = <Future>[]; + StreamQueue? consumedMost; + for (var i = 0; i < matchers.length; i++) { + futures.add(() async { + var copy = transaction.newQueue(); + + String? result; + try { + result = await streamMatchers[i].matchQueue(copy); + } catch (error, stackTrace) { + if (firstError == null) { + firstError = error; + firstStackTrace = stackTrace; + } + return; + } + + if (result != null) { + failures[i] = result; + } else if (consumedMost == null || + consumedMost!.eventsDispatched < copy.eventsDispatched) { + consumedMost = copy; + } + }()); + } + + await Future.wait(futures); + + if (consumedMost == null) { + transaction.reject(); + if (firstError != null) { + await Future.error(firstError!, firstStackTrace); + } + + var failureMessages = <String>[]; + for (var i = 0; i < matchers.length; i++) { + var message = 'failed to ${streamMatchers[i].description}'; + if (failures[i]!.isNotEmpty) { + message += message.contains('\n') ? '\n' : ' '; + message += 'because it ${failures[i]}'; + } + + failureMessages.add(message); + } + + return 'failed all options:\n${bullet(failureMessages)}'; + } else { + transaction.commit(consumedMost!); + return null; + } + }, description); +} + +/// Returns a [StreamMatcher] that matches the stream if each matcher in +/// [matchers] matches, one after another. +/// +/// If any matcher fails to match, this fails and consumes no events. +StreamMatcher emitsInOrder(Iterable matchers) { + var streamMatchers = matchers.map(emits).toList(); + if (streamMatchers.length == 1) return streamMatchers.first; + + var description = 'do the following in order:\n' + '${bullet(streamMatchers.map((matcher) => matcher.description))}'; + + return StreamMatcher((queue) async { + for (var i = 0; i < streamMatchers.length; i++) { + var matcher = streamMatchers[i]; + var result = await matcher.matchQueue(queue); + if (result == null) continue; + + var newResult = "didn't ${matcher.description}"; + if (result.isNotEmpty) { + newResult += newResult.contains('\n') ? '\n' : ' '; + newResult += 'because it $result'; + } + return newResult; + } + return null; + }, description); +} + +/// Returns a [StreamMatcher] that matches any number of events followed by +/// events that match [matcher]. +/// +/// This consumes all events matched by [matcher], as well as all events before. +/// If the stream emits a done event without matching [matcher], this fails and +/// consumes no events. +StreamMatcher emitsThrough(Object? matcher) { + var streamMatcher = emits(matcher); + return StreamMatcher((queue) async { + var failures = <String>[]; + + Future<bool> tryHere() => queue.withTransaction((copy) async { + var result = await streamMatcher.matchQueue(copy); + if (result == null) return true; + failures.add(result); + return false; + }); + + while (await queue.hasNext) { + if (await tryHere()) return null; + await queue.next; + } + + // Try after the queue is done in case the matcher can match an empty + // stream. + if (await tryHere()) return null; + + var result = 'never did ${streamMatcher.description}'; + + var failureMessages = + bullet(failures.where((failure) => failure.isNotEmpty)); + if (failureMessages.isNotEmpty) { + result += result.contains('\n') ? '\n' : ' '; + result += 'because it:\n$failureMessages'; + } + + return result; + }, 'eventually ${streamMatcher.description}'); +} + +/// Returns a [StreamMatcher] that matches any number of events that match +/// [matcher]. +/// +/// This consumes events until [matcher] no longer matches. It always succeeds; +/// if [matcher] doesn't match, this just consumes no events. It never rethrows +/// errors. +StreamMatcher mayEmitMultiple(Object? matcher) { + var streamMatcher = emits(matcher); + + var description = streamMatcher.description; + description += description.contains('\n') ? '\n' : ' '; + description += 'zero or more times'; + + return StreamMatcher((queue) async { + while (await _tryMatch(queue, streamMatcher)) { + // Do nothing; the matcher presumably already consumed events. + } + return null; + }, description); +} + +/// Returns a [StreamMatcher] that matches a stream that never matches +/// [matcher]. +/// +/// This doesn't complete until the stream emits a done event. It never consumes +/// any events. It never re-throws errors. +StreamMatcher neverEmits(Object? matcher) { + var streamMatcher = emits(matcher); + return StreamMatcher((queue) async { + var events = 0; + var matched = false; + await queue.withTransaction((copy) async { + while (await copy.hasNext) { + matched = await _tryMatch(copy, streamMatcher); + if (matched) return false; + + events++; + + try { + await copy.next; + } catch (_) { + // Ignore errors events. + } + } + + matched = await _tryMatch(copy, streamMatcher); + return false; + }); + + if (!matched) return null; + return "after $events ${pluralize('event', events)} did " + '${streamMatcher.description}'; + }, 'never ${streamMatcher.description}'); +} + +/// Returns whether [matcher] matches [queue] at its current position. +/// +/// This treats errors as failures to match. +Future<bool> _tryMatch(StreamQueue queue, StreamMatcher matcher) { + return queue.withTransaction((copy) async { + try { + return (await matcher.matchQueue(copy)) == null; + } catch (_) { + return false; + } + }); +} + +/// Returns a [StreamMatcher] that matches the stream if each matcher in +/// [matchers] matches, in any order. +/// +/// If any matcher fails to match, this fails and consumes no events. If the +/// matchers match in multiple different possible orders, this chooses the order +/// that consumes as many events as possible. +/// +/// If any sequence of matchers matches the stream, no errors from other +/// sequences are thrown. If no sequences match and multiple sequences throw +/// errors, the first error is re-thrown. +/// +/// Note that checking every ordering of [matchers] is O(n!) in the worst case, +/// so this should only be called when there are very few [matchers]. +StreamMatcher emitsInAnyOrder(Iterable matchers) { + var streamMatchers = matchers.map(emits).toSet(); + if (streamMatchers.length == 1) return streamMatchers.first; + var description = 'do the following in any order:\n' + '${bullet(streamMatchers.map((matcher) => matcher.description))}'; + + return StreamMatcher( + (queue) async => await _tryInAnyOrder(queue, streamMatchers) ? null : '', + description); +} + +/// Returns whether [queue] matches [matchers] in any order. +Future<bool> _tryInAnyOrder( + StreamQueue queue, Set<StreamMatcher> matchers) async { + if (matchers.length == 1) { + return await matchers.first.matchQueue(queue) == null; + } + + var transaction = queue.startTransaction(); + StreamQueue? consumedMost; + + // The first error thrown. If no matchers match and this exists, we rethrow + // it. + Object? firstError; + StackTrace? firstStackTrace; + + await Future.wait(matchers.map((matcher) async { + var copy = transaction.newQueue(); + try { + if (await matcher.matchQueue(copy) != null) return; + } catch (error, stackTrace) { + if (firstError == null) { + firstError = error; + firstStackTrace = stackTrace; + } + return; + } + + var rest = Set<StreamMatcher>.from(matchers); + rest.remove(matcher); + + try { + if (!await _tryInAnyOrder(copy, rest)) return; + } catch (error, stackTrace) { + if (firstError == null) { + firstError = error; + firstStackTrace = stackTrace; + } + return; + } + + if (consumedMost == null || + consumedMost!.eventsDispatched < copy.eventsDispatched) { + consumedMost = copy; + } + })); + + if (consumedMost == null) { + transaction.reject(); + if (firstError != null) await Future.error(firstError!, firstStackTrace); + return false; + } else { + transaction.commit(consumedMost!); + return true; + } +}
diff --git a/pkgs/matcher/lib/src/expect/throws_matcher.dart b/pkgs/matcher/lib/src/expect/throws_matcher.dart new file mode 100644 index 0000000..37676ef --- /dev/null +++ b/pkgs/matcher/lib/src/expect/throws_matcher.dart
@@ -0,0 +1,139 @@ +// Copyright (c) 2014, 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:test_api/hooks.dart'; + +import '../description.dart'; +import '../interfaces.dart'; +import '../util.dart'; +import 'async_matcher.dart'; +import 'util/pretty_print.dart'; + +/// This function is deprecated. +/// +/// Use [throwsA] instead. We strongly recommend that you add assertions about +/// at least the type of the error, but you can write `throwsA(anything)` to +/// mimic the behavior of this matcher. +@Deprecated('Will be removed in 0.13.0') +const Matcher throws = Throws(); + +/// This can be used to match three kinds of objects: +/// +/// * A [Function] that throws an exception when called. The function cannot +/// take any arguments. If you want to test that a function expecting +/// arguments throws, wrap it in another zero-argument function that calls +/// the one you want to test. +/// +/// * A [Future] that completes with an exception. Note that this creates an +/// asynchronous expectation. The call to `expect()` that includes this will +/// return immediately and execution will continue. Later, when the future +/// completes, the actual expectation will run. +/// +/// * A [Function] that returns a [Future] that completes with an exception. +/// +/// In all three cases, when an exception is thrown, this will test that the +/// exception object matches [matcher]. If [matcher] is not an instance of +/// [Matcher], it will implicitly be treated as `equals(matcher)`. +/// +/// Examples: +/// ```dart +/// void functionThatThrows() => throw SomeException(); +/// +/// void functionWithArgument(bool shouldThrow) { +/// if (shouldThrow) { +/// throw SomeException(); +/// } +/// } +/// +/// Future<void> asyncFunctionThatThrows() async => throw SomeException(); +/// +/// expect(functionThatThrows, throwsA(isA<SomeException>())); +/// +/// expect(() => functionWithArgument(true), throwsA(isA<SomeException>())); +/// +/// var future = asyncFunctionThatThrows(); +/// await expectLater(future, throwsA(isA<SomeException>())); +/// +/// await expectLater( +/// asyncFunctionThatThrows, throwsA(isA<SomeException>())); +/// ``` +Matcher throwsA(Object? matcher) => Throws(wrapMatcher(matcher)); + +/// Use the [throwsA] function instead. +@Deprecated('Will be removed in 0.13.0') +class Throws extends AsyncMatcher { + final Matcher? _matcher; + + const Throws([Matcher? matcher]) : _matcher = matcher; + + // Avoid async/await so we synchronously fail if we match a synchronous + // function. + @override + dynamic /*FutureOr<String>*/ matchAsync(Object? item) { + if (item is! Function && item is! Future) { + return 'was not a Function or Future'; + } + + if (item is Future) { + return _matchFuture(item, 'emitted '); + } + + try { + var value = (item as Function)(); + if (value is Future) { + return _matchFuture(value, 'returned a Future that emitted '); + } + + return indent(prettyPrint(value), first: 'returned '); + } catch (error, trace) { + return _check(error, trace); + } + } + + /// Matches [future], using try/catch since `onError` doesn't seem to work + /// properly in nnbd. + Future<String?> _matchFuture( + Future<dynamic> future, String messagePrefix) async { + try { + var value = await future; + return indent(prettyPrint(value), first: messagePrefix); + } catch (error, trace) { + return _check(error, trace); + } + } + + @override + Description describe(Description description) { + if (_matcher == null) { + return description.add('throws'); + } else { + return description.add('throws ').addDescriptionOf(_matcher); + } + } + + /// Verifies that [error] matches [_matcher] and returns a [String] + /// description of the failure if it doesn't. + String? _check(error, StackTrace? trace) { + if (_matcher == null) return null; + + var matchState = {}; + if (_matcher.matches(error, matchState)) return null; + + var result = _matcher + .describeMismatch(error, StringDescription(), matchState, false) + .toString(); + + var buffer = StringBuffer(); + buffer.writeln(indent(prettyPrint(error), first: 'threw ')); + if (trace != null) { + buffer.writeln(indent( + TestHandle.current.formatStackTrace(trace).toString(), + first: 'stack ')); + } + if (result.isNotEmpty) buffer.writeln(indent(result, first: 'which ')); + return buffer.toString().trimRight(); + } +}
diff --git a/pkgs/matcher/lib/src/expect/throws_matchers.dart b/pkgs/matcher/lib/src/expect/throws_matchers.dart new file mode 100644 index 0000000..67d35b7 --- /dev/null +++ b/pkgs/matcher/lib/src/expect/throws_matchers.dart
@@ -0,0 +1,72 @@ +// Copyright (c) 2014, 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +import '../error_matchers.dart'; +import '../interfaces.dart'; +import '../type_matcher.dart'; +import 'throws_matcher.dart'; + +/// A matcher for functions that throw ArgumentError. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsArgumentError = Throws(isArgumentError); + +/// A matcher for functions that throw ConcurrentModificationError. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsConcurrentModificationError = + Throws(isConcurrentModificationError); + +/// A matcher for functions that throw CyclicInitializationError. +/// +/// See [throwsA] for objects that this can be matched against. +@Deprecated('throwsCyclicInitializationError has been deprecated, because ' + 'the type will longer exists in Dart 3.0. It will now catch any kind of ' + 'error, not only CyclicInitializationError.') +const Matcher throwsCyclicInitializationError = Throws(TypeMatcher<Error>()); + +/// A matcher for functions that throw Exception. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsException = Throws(isException); + +/// A matcher for functions that throw FormatException. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsFormatException = Throws(isFormatException); + +/// A matcher for functions that throw NoSuchMethodError. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsNoSuchMethodError = Throws(isNoSuchMethodError); + +/// A matcher for functions that throw NullThrownError. +/// +/// See [throwsA] for objects that this can be matched against. +@Deprecated('throwsNullThrownError has been deprecated, because ' + 'NullThrownError has been replaced with TypeError. ' + 'Use `throwsA(isA<TypeError>())` instead.') +const Matcher throwsNullThrownError = Throws(TypeMatcher<TypeError>()); + +/// A matcher for functions that throw RangeError. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsRangeError = Throws(isRangeError); + +/// A matcher for functions that throw StateError. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsStateError = Throws(isStateError); + +/// A matcher for functions that throw Exception. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsUnimplementedError = Throws(isUnimplementedError); + +/// A matcher for functions that throw UnsupportedError. +/// +/// See [throwsA] for objects that this can be matched against. +const Matcher throwsUnsupportedError = Throws(isUnsupportedError);
diff --git a/pkgs/matcher/lib/src/expect/util/placeholder.dart b/pkgs/matcher/lib/src/expect/util/placeholder.dart new file mode 100644 index 0000000..ee2dc70 --- /dev/null +++ b/pkgs/matcher/lib/src/expect/util/placeholder.dart
@@ -0,0 +1,16 @@ +// 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. + +/// A class that's used as a default argument to detect whether an argument was +/// passed. +/// +/// We use a custom class for this rather than just `const Object()` so that +/// callers can't accidentally pass the placeholder value. +class _Placeholder { + const _Placeholder(); +} + +/// A placeholder to use as a default argument value to detect whether an +/// argument was passed. +const placeholder = _Placeholder();
diff --git a/pkgs/matcher/lib/src/expect/util/pretty_print.dart b/pkgs/matcher/lib/src/expect/util/pretty_print.dart new file mode 100644 index 0000000..de635ba --- /dev/null +++ b/pkgs/matcher/lib/src/expect/util/pretty_print.dart
@@ -0,0 +1,48 @@ +// Copyright (c) 2021, 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:term_glyph/term_glyph.dart' as glyph; + +import '../../description.dart'; + +/// Indent each line in [text] by [first] spaces. +/// +/// [first] is used in place of the first line's indentation. +String indent(String text, {required String first}) { + final prefix = ' ' * first.length; + var lines = text.split('\n'); + if (lines.length == 1) return '$first$text'; + + var buffer = StringBuffer('$first${lines.first}\n'); + + // Write out all but the first and last lines with [prefix]. + for (var line in lines.skip(1).take(lines.length - 2)) { + buffer.writeln('$prefix$line'); + } + buffer.write('$prefix${lines.last}'); + return buffer.toString(); +} + +/// Returns a pretty-printed representation of [value]. +/// +/// The matcher package doesn't expose its pretty-print function directly, but +/// we can use it through StringDescription. +String prettyPrint(Object? value) => + StringDescription().addDescriptionOf(value).toString(); + +/// Indents [text], and adds a bullet at the beginning. +String addBullet(String text) => indent(text, first: '${glyph.bullet} '); + +/// Converts [strings] to a bulleted list. +String bullet(Iterable<String> strings) => strings.map(addBullet).join('\n'); + +/// Returns [name] if [number] is 1, or the plural of [name] otherwise. +/// +/// By default, this just adds "s" to the end of [name] to get the plural. If +/// [plural] is passed, that's used instead. +String pluralize(String name, int number, {String? plural}) { + if (number == 1) return name; + if (plural != null) return plural; + return '${name}s'; +}
diff --git a/pkgs/matcher/lib/src/feature_matcher.dart b/pkgs/matcher/lib/src/feature_matcher.dart new file mode 100644 index 0000000..1dbe56c --- /dev/null +++ b/pkgs/matcher/lib/src/feature_matcher.dart
@@ -0,0 +1,34 @@ +// 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. + +import 'interfaces.dart'; +import 'type_matcher.dart'; + +/// A package-private [TypeMatcher] implementation that makes it easy for +/// subclasses to validate aspects of specific types while providing consistent +/// type checking. +abstract class FeatureMatcher<T> extends TypeMatcher<T> { + const FeatureMatcher(); + + @override + bool matches(dynamic item, Map matchState) => + super.matches(item, matchState) && typedMatches(item as T, matchState); + + bool typedMatches(T item, Map matchState); + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + if (item is T) { + return describeTypedMismatch( + item, mismatchDescription, matchState, verbose); + } + + return super.describe(mismatchDescription.add('not an ')); + } + + Description describeTypedMismatch(T item, Description mismatchDescription, + Map matchState, bool verbose) => + mismatchDescription; +}
diff --git a/pkgs/matcher/lib/src/having_matcher.dart b/pkgs/matcher/lib/src/having_matcher.dart new file mode 100644 index 0000000..2d2dc3d --- /dev/null +++ b/pkgs/matcher/lib/src/having_matcher.dart
@@ -0,0 +1,75 @@ +// 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. + +import 'custom_matcher.dart'; +import 'interfaces.dart'; +import 'type_matcher.dart'; +import 'util.dart'; + +/// A package-private [TypeMatcher] implementation that handles is returned +/// by calls to [TypeMatcher.having]. +class HavingMatcher<T> implements TypeMatcher<T> { + final TypeMatcher<T> _parent; + final List<_FunctionMatcher<T>> _functionMatchers; + + HavingMatcher(this._parent, String description, Object? Function(T) feature, + dynamic matcher) + : _functionMatchers = [ + _FunctionMatcher<T>(description, feature, matcher) + ]; + + HavingMatcher._fromExisting( + this._parent, + String description, + Object? Function(T) feature, + dynamic matcher, + Iterable<_FunctionMatcher<T>>? existing) + : _functionMatchers = [ + ...?existing, + _FunctionMatcher<T>(description, feature, matcher) + ]; + + @override + TypeMatcher<T> having( + Object? Function(T) feature, String description, dynamic matcher) => + HavingMatcher._fromExisting( + _parent, description, feature, matcher, _functionMatchers); + + @override + bool matches(dynamic item, Map matchState) { + for (var matcher in <Matcher>[_parent].followedBy(_functionMatchers)) { + if (!matcher.matches(item, matchState)) { + addStateInfo(matchState, {'matcher': matcher}); + return false; + } + } + return true; + } + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + var matcher = matchState['matcher'] as Matcher; + matcher.describeMismatch( + item, mismatchDescription, matchState['state'] as Map, verbose); + return mismatchDescription; + } + + @override + Description describe(Description description) => description + .add('') + .addDescriptionOf(_parent) + .add(' with ') + .addAll('', ' and ', '', _functionMatchers); +} + +class _FunctionMatcher<T> extends CustomMatcher { + final Object? Function(T value) _feature; + + _FunctionMatcher(String name, this._feature, Object? matcher) + : super('`$name`:', '`$name`', matcher); + + @override + Object? featureValueOf(covariant T actual) => _feature(actual); +}
diff --git a/pkgs/matcher/lib/src/interfaces.dart b/pkgs/matcher/lib/src/interfaces.dart new file mode 100644 index 0000000..24527f7 --- /dev/null +++ b/pkgs/matcher/lib/src/interfaces.dart
@@ -0,0 +1,60 @@ +// Copyright (c) 2012, 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. + +/// Matchers build up their error messages by appending to Description objects. +/// +/// This interface is implemented by StringDescription. +/// +/// This interface is unlikely to need other implementations, but could be +/// useful to replace in some cases - e.g. language conversion. +abstract class Description { + int get length; + + /// Change the value of the description. + Description replace(String text); + + /// This is used to add arbitrary text to the description. + Description add(String text); + + /// This is used to add a meaningful description of a value. + Description addDescriptionOf(Object? value); + + /// This is used to add a description of an [Iterable] [list], + /// with appropriate [start] and [end] markers and inter-element [separator]. + Description addAll(String start, String separator, String end, Iterable list); +} + +/// The base class for all matchers. +/// +/// [matches] and [describe] must be implemented by subclasses. +/// +/// Subclasses can override [describeMismatch] if a more specific description is +/// required when the matcher fails. +abstract class Matcher { + const Matcher(); + + /// Does the matching of the actual vs expected values. + /// + /// [item] is the actual value. [matchState] can be supplied + /// and may be used to add details about the mismatch that are too + /// costly to determine in [describeMismatch]. + bool matches(dynamic item, Map matchState); + + /// Builds a textual description of the matcher. + Description describe(Description description); + + /// Builds a textual description of a specific mismatch. + /// + /// [item] is the value that was tested by [matches]; [matchState] is + /// the [Map] that was passed to and supplemented by [matches] + /// with additional information about the mismatch, and [mismatchDescription] + /// is the [Description] that is being built to describe the mismatch. + /// + /// A few matchers make use of the [verbose] flag to provide detailed + /// information that is not typically included but can be of help in + /// diagnosing failures, such as stack traces. + Description describeMismatch(dynamic item, Description mismatchDescription, + Map matchState, bool verbose) => + mismatchDescription; +}
diff --git a/pkgs/matcher/lib/src/iterable_matchers.dart b/pkgs/matcher/lib/src/iterable_matchers.dart new file mode 100644 index 0000000..abc9688 --- /dev/null +++ b/pkgs/matcher/lib/src/iterable_matchers.dart
@@ -0,0 +1,416 @@ +// Copyright (c) 2012, 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 'description.dart'; +import 'equals_matcher.dart'; +import 'feature_matcher.dart'; +import 'interfaces.dart'; +import 'util.dart'; + +/// Returns a matcher which matches [Iterable]s in which all elements +/// match the given [valueOrMatcher]. +Matcher everyElement(Object? valueOrMatcher) => + _EveryElement(wrapMatcher(valueOrMatcher)); + +class _EveryElement extends _IterableMatcher { + final Matcher _matcher; + + _EveryElement(this._matcher); + + @override + bool typedMatches(Iterable item, Map matchState) { + var i = 0; + for (var element in item) { + if (!_matcher.matches(element, matchState)) { + addStateInfo(matchState, {'index': i, 'element': element}); + return false; + } + ++i; + } + return true; + } + + @override + Description describe(Description description) => + description.add('every element(').addDescriptionOf(_matcher).add(')'); + + @override + Description describeTypedMismatch(dynamic item, + Description mismatchDescription, Map matchState, bool verbose) { + if (matchState['index'] != null) { + var index = matchState['index']; + var element = matchState['element']; + mismatchDescription + .add('has value ') + .addDescriptionOf(element) + .add(' which '); + var subDescription = StringDescription(); + _matcher.describeMismatch( + element, subDescription, matchState['state'] as Map, verbose); + if (subDescription.length > 0) { + mismatchDescription.add(subDescription.toString()); + } else { + mismatchDescription.add("doesn't match "); + _matcher.describe(mismatchDescription); + } + mismatchDescription.add(' at index $index'); + return mismatchDescription; + } + return super + .describeMismatch(item, mismatchDescription, matchState, verbose); + } +} + +/// Returns a matcher which matches [Iterable]s in which at least one +/// element matches the given [valueOrMatcher]. +Matcher anyElement(Object? valueOrMatcher) => + _AnyElement(wrapMatcher(valueOrMatcher)); + +class _AnyElement extends _IterableMatcher { + final Matcher _matcher; + + _AnyElement(this._matcher); + + @override + bool typedMatches(Iterable item, Map matchState) => + item.any((e) => _matcher.matches(e, matchState)); + + @override + Description describe(Description description) => + description.add('some element ').addDescriptionOf(_matcher); +} + +/// Returns a matcher which matches [Iterable]s that have the same +/// length and the same elements as [expected], in the same order. +/// +/// This is equivalent to [equals] but does not recurse. +Matcher orderedEquals(Iterable expected) => _OrderedEquals(expected); + +class _OrderedEquals extends _IterableMatcher { + final Iterable _expected; + final Matcher _matcher; + + _OrderedEquals(this._expected) : _matcher = equals(_expected, 1); + + @override + bool typedMatches(Iterable item, Map matchState) => + _matcher.matches(item, matchState); + + @override + Description describe(Description description) => + description.add('equals ').addDescriptionOf(_expected).add(' ordered'); + + @override + Description describeTypedMismatch(Iterable item, + Description mismatchDescription, Map matchState, bool verbose) { + return _matcher.describeMismatch( + item, mismatchDescription, matchState, verbose); + } +} + +/// Returns a matcher which matches [Iterable]s that have the same length and +/// the same elements as [expected], but not necessarily in the same order. +/// +/// Note that this is worst case O(n^2) runtime and memory usage so it should +/// only be used on small iterables. +Matcher unorderedEquals(Iterable expected) => _UnorderedEquals(expected); + +class _UnorderedEquals extends _UnorderedMatches { + final List _expectedValues; + + _UnorderedEquals(Iterable expected) + : _expectedValues = expected.toList(), + super(expected.map(equals)); + + @override + Description describe(Description description) => description + .add('equals ') + .addDescriptionOf(_expectedValues) + .add(' unordered'); +} + +/// Iterable matchers match against [Iterable]s. We add this intermediate +/// class to give better mismatch error messages than the base Matcher class. +abstract class _IterableMatcher extends FeatureMatcher<Iterable> { + const _IterableMatcher(); +} + +/// Returns a matcher which matches [Iterable]s whose elements match the +/// matchers in [expected], but not necessarily in the same order. +/// +/// Note that this is worst case O(n^2) runtime and memory usage so it should +/// only be used on small iterables. +Matcher unorderedMatches(Iterable expected) => _UnorderedMatches(expected); + +class _UnorderedMatches extends _IterableMatcher { + final List<Matcher> _expected; + final bool _allowUnmatchedValues; + + _UnorderedMatches(Iterable expected, {bool allowUnmatchedValues = false}) + : _expected = expected.map(wrapMatcher).toList(), + _allowUnmatchedValues = allowUnmatchedValues; + + String? _test(List values) { + // Check the lengths are the same. + if (_expected.length > values.length) { + return 'has too few elements (${values.length} < ${_expected.length})'; + } else if (!_allowUnmatchedValues && _expected.length < values.length) { + return 'has too many elements (${values.length} > ${_expected.length})'; + } + + var edges = List.generate(values.length, (_) => <int>[], growable: false); + for (var v = 0; v < values.length; v++) { + for (var m = 0; m < _expected.length; m++) { + if (_expected[m].matches(values[v], {})) { + edges[v].add(m); + } + } + } + // The index into `values` matched with each matcher or `null` if no value + // has been matched yet. + var matched = List<int?>.filled(_expected.length, null); + for (var valueIndex = 0; valueIndex < values.length; valueIndex++) { + _findPairing(edges, valueIndex, matched); + } + for (var matcherIndex = 0; + matcherIndex < _expected.length; + matcherIndex++) { + if (matched[matcherIndex] == null) { + final description = StringDescription() + .add('has no match for ') + .addDescriptionOf(_expected[matcherIndex]) + .add(' at index $matcherIndex'); + final remainingUnmatched = + matched.sublist(matcherIndex + 1).where((m) => m == null).length; + return remainingUnmatched == 0 + ? description.toString() + : description + .add(' along with $remainingUnmatched other unmatched') + .toString(); + } + } + return null; + } + + @override + bool typedMatches(Iterable item, Map mismatchState) => + _test(item.toList()) == null; + + @override + Description describe(Description description) => description + .add('matches ') + .addAll('[', ', ', ']', _expected) + .add(' unordered'); + + @override + Description describeTypedMismatch(Iterable item, + Description mismatchDescription, Map matchState, bool verbose) => + mismatchDescription.add(_test(item.toList())!); + + /// Returns `true` if the value at [valueIndex] can be paired with some + /// unmatched matcher and updates the state of [matched]. + /// + /// If there is a conflict where multiple values may match the same matcher + /// recursively looks for a new place to match the old value. + bool _findPairing( + List<List<int>> edges, int valueIndex, List<int?> matched) => + _findPairingInner(edges, valueIndex, matched, <int>{}); + + /// Implementation of [_findPairing], tracks [reserved] which are the + /// matchers that have been used _during_ this search. + bool _findPairingInner(List<List<int>> edges, int valueIndex, + List<int?> matched, Set<int> reserved) { + final possiblePairings = + edges[valueIndex].where((m) => !reserved.contains(m)); + for (final matcherIndex in possiblePairings) { + reserved.add(matcherIndex); + final previouslyMatched = matched[matcherIndex]; + if (previouslyMatched == null || + // If the matcher isn't already free, check whether the existing value + // occupying the matcher can be bumped to another one. + _findPairingInner(edges, matched[matcherIndex]!, matched, reserved)) { + matched[matcherIndex] = valueIndex; + return true; + } + } + return false; + } +} + +/// A pairwise matcher for [Iterable]s. +/// +/// The [comparator] function, taking an expected and an actual argument, and +/// returning whether they match, will be applied to each pair in order. +/// [description] should be a meaningful name for the comparator. +Matcher pairwiseCompare<S, T>(Iterable<S> expected, + bool Function(S, T) comparator, String description) => + _PairwiseCompare(expected, comparator, description); + +typedef _Comparator<S, T> = bool Function(S a, T b); + +class _PairwiseCompare<S, T> extends _IterableMatcher { + final Iterable<S> _expected; + final _Comparator<S, T> _comparator; + final String _description; + + _PairwiseCompare(this._expected, this._comparator, this._description); + + @override + bool typedMatches(Iterable item, Map matchState) { + if (item.length != _expected.length) return false; + var iterator = item.iterator; + var i = 0; + for (var e in _expected) { + iterator.moveNext(); + if (!_comparator(e, iterator.current as T)) { + addStateInfo(matchState, + {'index': i, 'expected': e, 'actual': iterator.current}); + return false; + } + i++; + } + return true; + } + + @override + Description describe(Description description) => + description.add('pairwise $_description ').addDescriptionOf(_expected); + + @override + Description describeTypedMismatch(Iterable item, + Description mismatchDescription, Map matchState, bool verbose) { + if (item.length != _expected.length) { + return mismatchDescription + .add('has length ${item.length} instead of ${_expected.length}'); + } else { + return mismatchDescription + .add('has ') + .addDescriptionOf(matchState['actual']) + .add(' which is not $_description ') + .addDescriptionOf(matchState['expected']) + .add(' at index ${matchState["index"]}'); + } + } +} + +/// Matches [Iterable]s which contain an element matching every value in +/// [expected] in any order, and may contain additional values. +/// +/// For example: `[0, 1, 0, 2, 0]` matches `containsAll([1, 2])` and +/// `containsAll([2, 1])` but not `containsAll([1, 2, 3])`. +/// +/// Will only match values which implement [Iterable]. +/// +/// Each element in the value will only be considered a match for a single +/// matcher in [expected] even if it could satisfy more than one. For instance +/// `containsAll([greaterThan(1), greaterThan(2)])` will not be satisfied by +/// `[3]`. To check that all matchers are satisfied within an iterable and allow +/// the same element to satisfy multiple matchers use +/// `allOf(matchers.map(contains))`. +/// +/// Note that this is worst case O(n^2) runtime and memory usage so it should +/// only be used on small iterables. +Matcher containsAll(Iterable expected) => _ContainsAll(expected); + +class _ContainsAll extends _UnorderedMatches { + final Iterable _unwrappedExpected; + + _ContainsAll(Iterable expected) + : _unwrappedExpected = expected, + super(expected.map(wrapMatcher), allowUnmatchedValues: true); + @override + Description describe(Description description) => + description.add('contains all of ').addDescriptionOf(_unwrappedExpected); +} + +/// Matches [Iterable]s which contain an element matching every value in +/// [expected] in the same order, but may contain additional values interleaved +/// throughout. +/// +/// For example: `[0, 1, 0, 2, 0]` matches `containsAllInOrder([1, 2])` but not +/// `containsAllInOrder([2, 1])` or `containsAllInOrder([1, 2, 3])`. +/// +/// Will only match values which implement [Iterable]. +Matcher containsAllInOrder(Iterable expected) => _ContainsAllInOrder(expected); + +class _ContainsAllInOrder extends _IterableMatcher { + final Iterable _expected; + + _ContainsAllInOrder(this._expected); + + String? _test(Iterable item, Map matchState) { + var matchers = _expected.map(wrapMatcher).toList(); + var matcherIndex = 0; + for (var value in item) { + if (matchers[matcherIndex].matches(value, matchState)) matcherIndex++; + if (matcherIndex == matchers.length) return null; + } + return StringDescription() + .add('did not find a value matching ') + .addDescriptionOf(matchers[matcherIndex]) + .add(' following expected prior values') + .toString(); + } + + @override + bool typedMatches(Iterable item, Map matchState) => + _test(item, matchState) == null; + + @override + Description describe(Description description) => description + .add('contains in order(') + .addDescriptionOf(_expected) + .add(')'); + + @override + Description describeTypedMismatch(Iterable item, + Description mismatchDescription, Map matchState, bool verbose) => + mismatchDescription.add(_test(item, matchState)!); +} + +/// Matches [Iterable]s where exactly one element matches the expected +/// value, and all other elements don't match. +Matcher containsOnce(Object? expected) => _ContainsOnce(expected); + +class _ContainsOnce extends _IterableMatcher { + final Object? _expected; + + _ContainsOnce(this._expected); + + String? _test(Iterable item, Map matchState) { + var matcher = wrapMatcher(_expected); + var matches = [ + for (var value in item) + if (matcher.matches(value, matchState)) value, + ]; + if (matches.length == 1) { + return null; + } + if (matches.isEmpty) { + return StringDescription() + .add('did not find a value matching ') + .addDescriptionOf(matcher) + .toString(); + } + return StringDescription() + .add('expected only one value matching ') + .addDescriptionOf(matcher) + .add(' but found multiple: ') + .addAll('', ', ', '', matches) + .toString(); + } + + @override + bool typedMatches(Iterable item, Map matchState) => + _test(item, matchState) == null; + + @override + Description describe(Description description) => + description.add('contains once(').addDescriptionOf(_expected).add(')'); + + @override + Description describeTypedMismatch(Iterable item, + Description mismatchDescription, Map matchState, bool verbose) => + mismatchDescription.add(_test(item, matchState)!); +}
diff --git a/pkgs/matcher/lib/src/map_matchers.dart b/pkgs/matcher/lib/src/map_matchers.dart new file mode 100644 index 0000000..4476d06 --- /dev/null +++ b/pkgs/matcher/lib/src/map_matchers.dart
@@ -0,0 +1,69 @@ +// Copyright (c) 2012, 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 'interfaces.dart'; +import 'util.dart'; + +/// Returns a matcher which matches maps containing the given [value]. +Matcher containsValue(Object? value) => _ContainsValue(value); + +class _ContainsValue extends Matcher { + final Object? _value; + + const _ContainsValue(this._value); + + @override + bool matches(Object? item, Map matchState) => + // ignore: avoid_dynamic_calls + (item as dynamic).containsValue(_value); + @override + Description describe(Description description) => + description.add('contains value ').addDescriptionOf(_value); +} + +/// Returns a matcher which matches maps containing the key-value pair +/// with [key] => [valueOrMatcher]. +Matcher containsPair(Object? key, Object? valueOrMatcher) => + _ContainsMapping(key, wrapMatcher(valueOrMatcher)); + +class _ContainsMapping extends Matcher { + final Object? _key; + final Matcher _valueMatcher; + + const _ContainsMapping(this._key, this._valueMatcher); + + @override + bool matches(Object? item, Map matchState) => + // ignore: avoid_dynamic_calls + (item as dynamic).containsKey(_key) && + _valueMatcher.matches((item as dynamic)[_key], matchState); + + @override + Description describe(Description description) { + return description + .add('contains pair ') + .addDescriptionOf(_key) + .add(' => ') + .addDescriptionOf(_valueMatcher); + } + + @override + Description describeMismatch(Object? item, Description mismatchDescription, + Map matchState, bool verbose) { + // ignore: avoid_dynamic_calls + if (!((item as dynamic).containsKey(_key) as bool)) { + return mismatchDescription + .add(" doesn't contain key ") + .addDescriptionOf(_key); + } else { + mismatchDescription + .add(' contains key ') + .addDescriptionOf(_key) + .add(' but with value '); + _valueMatcher.describeMismatch( + (item as dynamic)[_key], mismatchDescription, matchState, verbose); + return mismatchDescription; + } + } +}
diff --git a/pkgs/matcher/lib/src/numeric_matchers.dart b/pkgs/matcher/lib/src/numeric_matchers.dart new file mode 100644 index 0000000..a798160 --- /dev/null +++ b/pkgs/matcher/lib/src/numeric_matchers.dart
@@ -0,0 +1,89 @@ +// Copyright (c) 2012, 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 'feature_matcher.dart'; +import 'interfaces.dart'; + +/// Returns a matcher which matches if the match argument is within [delta] +/// of some [value]. +/// +/// In other words, this matches if the match argument is greater than +/// than or equal [value]-[delta] and less than or equal to [value]+[delta]. +Matcher closeTo(num value, num delta) => _IsCloseTo(value, delta); + +class _IsCloseTo extends FeatureMatcher<num> { + final num _value, _delta; + + const _IsCloseTo(this._value, this._delta); + + @override + bool typedMatches(num item, Map matchState) { + var diff = item - _value; + if (diff < 0) diff = -diff; + return diff <= _delta; + } + + @override + Description describe(Description description) => description + .add('a numeric value within ') + .addDescriptionOf(_delta) + .add(' of ') + .addDescriptionOf(_value); + + @override + Description describeTypedMismatch( + num item, Description mismatchDescription, Map matchState, bool verbose) { + var diff = item - _value; + if (diff < 0) diff = -diff; + return mismatchDescription.add(' differs by ').addDescriptionOf(diff); + } +} + +/// Returns a matcher which matches if the match argument is greater +/// than or equal to [low] and less than or equal to [high]. +Matcher inInclusiveRange(num low, num high) => _InRange(low, high, true, true); + +/// Returns a matcher which matches if the match argument is greater +/// than [low] and less than [high]. +Matcher inExclusiveRange(num low, num high) => + _InRange(low, high, false, false); + +/// Returns a matcher which matches if the match argument is greater +/// than [low] and less than or equal to [high]. +Matcher inOpenClosedRange(num low, num high) => + _InRange(low, high, false, true); + +/// Returns a matcher which matches if the match argument is greater +/// than or equal to a [low] and less than [high]. +Matcher inClosedOpenRange(num low, num high) => + _InRange(low, high, true, false); + +class _InRange extends FeatureMatcher<num> { + final num _low, _high; + final bool _lowMatchValue, _highMatchValue; + + const _InRange( + this._low, this._high, this._lowMatchValue, this._highMatchValue); + + @override + bool typedMatches(num value, Map matchState) { + if (value < _low || value > _high) { + return false; + } + if (value == _low) { + return _lowMatchValue; + } + if (value == _high) { + return _highMatchValue; + } + // Value may still be outside if range if it can't be compared. + return value > _low && value < _high; + } + + @override + Description describe(Description description) => + description.add('be in range from ' + "$_low (${_lowMatchValue ? 'inclusive' : 'exclusive'}) to " + "$_high (${_highMatchValue ? 'inclusive' : 'exclusive'})"); +}
diff --git a/pkgs/matcher/lib/src/operator_matchers.dart b/pkgs/matcher/lib/src/operator_matchers.dart new file mode 100644 index 0000000..15e50ff --- /dev/null +++ b/pkgs/matcher/lib/src/operator_matchers.dart
@@ -0,0 +1,131 @@ +// Copyright (c) 2012, 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 'interfaces.dart'; +import 'util.dart'; + +/// Returns a matcher that inverts [valueOrMatcher] to its logical negation. +Matcher isNot(Object? valueOrMatcher) => _IsNot(wrapMatcher(valueOrMatcher)); + +class _IsNot extends Matcher { + final Matcher _matcher; + + const _IsNot(this._matcher); + + @override + bool matches(dynamic item, Map matchState) => + !_matcher.matches(item, matchState); + + @override + Description describe(Description description) => + description.add('not ').addDescriptionOf(_matcher); +} + +/// This returns a matcher that matches if all of the matchers passed as +/// arguments (up to 7) match. +/// +/// Instead of passing the matchers separately they can be passed as a single +/// List argument. Any argument that is not a matcher is implicitly wrapped in a +/// Matcher to check for equality. +Matcher allOf(Object? arg0, + [Object? arg1, + Object? arg2, + Object? arg3, + Object? arg4, + Object? arg5, + Object? arg6]) { + return _AllOf(_wrapArgs(arg0, arg1, arg2, arg3, arg4, arg5, arg6)); +} + +class _AllOf extends Matcher { + final List<Matcher> _matchers; + + const _AllOf(this._matchers); + + @override + bool matches(dynamic item, Map matchState) { + for (var matcher in _matchers) { + if (!matcher.matches(item, matchState)) { + addStateInfo(matchState, {'matcher': matcher}); + return false; + } + } + return true; + } + + @override + Description describeMismatch(dynamic item, Description mismatchDescription, + Map matchState, bool verbose) { + var matcher = matchState['matcher'] as Matcher; + matcher.describeMismatch( + item, mismatchDescription, matchState['state'], verbose); + return mismatchDescription; + } + + @override + Description describe(Description description) => + description.addAll('(', ' and ', ')', _matchers); +} + +/// Matches if any of the given matchers evaluate to true. +/// +/// The arguments can be a set of matchers as separate parameters +/// (up to 7), or a List of matchers. +/// +/// The matchers are evaluated from left to right using short-circuit +/// evaluation, so evaluation stops as soon as a matcher returns true. +/// +/// Any argument that is not a matcher is implicitly wrapped in a +/// Matcher to check for equality. +Matcher anyOf(Object? arg0, + [Object? arg1, + Object? arg2, + Object? arg3, + Object? arg4, + Object? arg5, + Object? arg6]) { + return _AnyOf(_wrapArgs(arg0, arg1, arg2, arg3, arg4, arg5, arg6)); +} + +class _AnyOf extends Matcher { + final List<Matcher> _matchers; + + const _AnyOf(this._matchers); + + @override + bool matches(dynamic item, Map matchState) { + for (var matcher in _matchers) { + if (matcher.matches(item, matchState)) { + return true; + } + } + return false; + } + + @override + Description describe(Description description) => + description.addAll('(', ' or ', ')', _matchers); +} + +List<Matcher> _wrapArgs(Object? arg0, Object? arg1, Object? arg2, Object? arg3, + Object? arg4, Object? arg5, Object? arg6) { + Iterable args; + if (arg0 is List) { + if (arg1 != null || + arg2 != null || + arg3 != null || + arg4 != null || + arg5 != null || + arg6 != null) { + throw ArgumentError('If arg0 is a List, all other arguments must be' + ' null.'); + } + + args = arg0; + } else { + args = [arg0, arg1, arg2, arg3, arg4, arg5, arg6].where((e) => e != null); + } + + return args.map(wrapMatcher).toList(); +}
diff --git a/pkgs/matcher/lib/src/order_matchers.dart b/pkgs/matcher/lib/src/order_matchers.dart new file mode 100644 index 0000000..6fe7c76 --- /dev/null +++ b/pkgs/matcher/lib/src/order_matchers.dart
@@ -0,0 +1,108 @@ +// Copyright (c) 2016, 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 'interfaces.dart'; + +/// Returns a matcher which matches if the match argument is greater +/// than the given [value]. +Matcher greaterThan(Object value) => + _OrderingMatcher(value, false, false, true, 'a value greater than'); + +/// Returns a matcher which matches if the match argument is greater +/// than or equal to the given [value]. +Matcher greaterThanOrEqualTo(Object value) => _OrderingMatcher( + value, true, false, true, 'a value greater than or equal to'); + +/// Returns a matcher which matches if the match argument is less +/// than the given [value]. +Matcher lessThan(Object value) => + _OrderingMatcher(value, false, true, false, 'a value less than'); + +/// Returns a matcher which matches if the match argument is less +/// than or equal to the given [value]. +Matcher lessThanOrEqualTo(Object value) => + _OrderingMatcher(value, true, true, false, 'a value less than or equal to'); + +/// A matcher which matches if the match argument is zero. +const Matcher isZero = + _OrderingMatcher(0, true, false, false, 'a value equal to'); + +/// A matcher which matches if the match argument is non-zero. +const Matcher isNonZero = + _OrderingMatcher(0, false, true, true, 'a value not equal to'); + +/// A matcher which matches if the match argument is positive. +const Matcher isPositive = + _OrderingMatcher(0, false, false, true, 'a positive value', false); + +/// A matcher which matches if the match argument is zero or negative. +const Matcher isNonPositive = + _OrderingMatcher(0, true, true, false, 'a non-positive value', false); + +/// A matcher which matches if the match argument is negative. +const Matcher isNegative = + _OrderingMatcher(0, false, true, false, 'a negative value', false); + +/// A matcher which matches if the match argument is zero or positive. +const Matcher isNonNegative = + _OrderingMatcher(0, true, false, true, 'a non-negative value', false); + +// TODO(kevmoo) Note that matchers that use _OrderingComparison only use +// `==` and `<` operators to evaluate the match. Or change the matcher. +class _OrderingMatcher extends Matcher { + /// Expected value. + final Object _value; + + /// What to return if actual == expected + final bool _equalValue; + + /// What to return if actual < expected + final bool _lessThanValue; + + /// What to return if actual > expected + final bool _greaterThanValue; + + /// Textual name of the inequality + final String _comparisonDescription; + + /// Whether to include the expected value in the description + final bool _valueInDescription; + + const _OrderingMatcher(this._value, this._equalValue, this._lessThanValue, + this._greaterThanValue, this._comparisonDescription, + [bool valueInDescription = true]) + : _valueInDescription = valueInDescription; + + @override + bool matches(Object? item, Map matchState) { + if (item == _value) { + return _equalValue; + } else if ((item as dynamic) < _value) { + return _lessThanValue; + } else if ((item as dynamic) > _value) { + return _greaterThanValue; + } else { + return false; + } + } + + @override + Description describe(Description description) { + if (_valueInDescription) { + return description + .add(_comparisonDescription) + .add(' ') + .addDescriptionOf(_value); + } else { + return description.add(_comparisonDescription); + } + } + + @override + Description describeMismatch(dynamic item, Description mismatchDescription, + Map matchState, bool verbose) { + mismatchDescription.add('is not '); + return describe(mismatchDescription); + } +}
diff --git a/pkgs/matcher/lib/src/pretty_print.dart b/pkgs/matcher/lib/src/pretty_print.dart new file mode 100644 index 0000000..d9eaaec --- /dev/null +++ b/pkgs/matcher/lib/src/pretty_print.dart
@@ -0,0 +1,133 @@ +// 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 'description.dart'; +import 'interfaces.dart'; +import 'util.dart'; + +/// Returns a pretty-printed representation of [object]. +/// +/// If [maxLineLength] is passed, this will attempt to ensure that each line is +/// no longer than [maxLineLength] characters long. This isn't guaranteed, since +/// individual objects may have string representations that are too long, but +/// most lines will be less than [maxLineLength] long. +/// +/// If [maxItems] is passed, [Iterable]s and [Map]s will only print their first +/// [maxItems] members or key/value pairs, respectively. +String prettyPrint(Object? object, {int? maxLineLength, int? maxItems}) { + String prettyPrintImpl( + Object? object, int indent, Set<Object?> seen, bool top) { + // If the object is a matcher, use its description. + if (object is Matcher) { + var description = StringDescription(); + object.describe(description); + return '<$description>'; + } + + // Avoid looping infinitely on recursively-nested data structures. + if (seen.contains(object)) return '(recursive)'; + seen = seen.union({object}); + String pp(Object? child) => prettyPrintImpl(child, indent + 2, seen, false); + + if (object is Iterable) { + // Print the type name for non-List iterables. + var type = object is List ? '' : '${_typeName(object)}:'; + + // Truncate the list of strings if it's longer than [maxItems]. + var strings = object.map(pp).toList(); + if (maxItems != null && strings.length > maxItems) { + strings.replaceRange(maxItems - 1, strings.length, ['...']); + } + + // If the printed string is short and doesn't contain a newline, print it + // as a single line. + var singleLine = "$type[${strings.join(', ')}]"; + if ((maxLineLength == null || + singleLine.length + indent <= maxLineLength) && + !singleLine.contains('\n')) { + return singleLine; + } + + // Otherwise, print each member on its own line. + return '$type[\n${strings.map((string) { + return _indent(indent + 2) + string; + }).join(',\n')}\n${_indent(indent)}]'; + } else if (object is Map) { + // Convert the contents of the map to string representations. + var strings = object.keys.map((key) { + return '${pp(key)}: ${pp(object[key])}'; + }).toList(); + + // Truncate the list of strings if it's longer than [maxItems]. + if (maxItems != null && strings.length > maxItems) { + strings.replaceRange(maxItems - 1, strings.length, ['...']); + } + + // If the printed string is short and doesn't contain a newline, print it + // as a single line. + var singleLine = '{${strings.join(", ")}}'; + if ((maxLineLength == null || + singleLine.length + indent <= maxLineLength) && + !singleLine.contains('\n')) { + return singleLine; + } + + // Otherwise, print each key/value pair on its own line. + return '{\n${strings.map((string) { + return _indent(indent + 2) + string; + }).join(',\n')}\n${_indent(indent)}}'; + } else if (object is String) { + // Escape strings and print each line on its own line. + var value = object + .split('\n') + .map(_escapeString) + .join("\\n'\n${_indent(indent + 2)}'"); + return "'$value'"; + } else { + var value = object.toString().replaceAll('\n', '${_indent(indent)}\n'); + var defaultToString = value.startsWith('Instance of '); + + // If this is the top-level call to [prettyPrint], wrap the value on angle + // brackets to set it apart visually. + if (top) value = '<$value>'; + + // Print the type of objects with custom [toString] methods. Primitive + // objects and objects that don't implement a custom [toString] don't need + // to have their types printed. + if (object is num || + object is bool || + object is Function || + object is RegExp || + object is MapEntry || + object is Expando || + object == null || + defaultToString) { + return value; + } else { + return '${_typeName(object)}:$value'; + } + } + } + + return prettyPrintImpl(object, 0, <Object?>{}, true); +} + +String _indent(int length) => List.filled(length, ' ').join(''); + +/// Returns the name of the type of [x] with fallbacks for core types with +/// private implementations. +String _typeName(Object x) { + if (x is Type) return 'Type'; + if (x is Uri) return 'Uri'; + if (x is Set) return 'Set'; + if (x is BigInt) return 'BigInt'; + return '${x.runtimeType}'; +} + +/// Returns [source] with any control characters replaced by their escape +/// sequences. +/// +/// This doesn't add quotes to the string, but it does escape single quote +/// characters so that single quotes can be applied externally. +String _escapeString(String source) => escape(source).replaceAll("'", r"\'");
diff --git a/pkgs/matcher/lib/src/string_matchers.dart b/pkgs/matcher/lib/src/string_matchers.dart new file mode 100644 index 0000000..b819fa5 --- /dev/null +++ b/pkgs/matcher/lib/src/string_matchers.dart
@@ -0,0 +1,183 @@ +// Copyright (c) 2012, 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 'feature_matcher.dart'; +import 'interfaces.dart'; + +/// Returns a matcher which matches if the match argument is a string and +/// is equal to [value] when compared case-insensitively. +Matcher equalsIgnoringCase(String value) => _IsEqualIgnoringCase(value); + +class _IsEqualIgnoringCase extends FeatureMatcher<String> { + final String _value; + final String _matchValue; + + _IsEqualIgnoringCase(String value) + : _value = value, + _matchValue = value.toLowerCase(); + + @override + bool typedMatches(String item, Map matchState) => + _matchValue == item.toLowerCase(); + + @override + Description describe(Description description) => + description.addDescriptionOf(_value).add(' ignoring case'); +} + +/// Returns a matcher which matches if the match argument is a string and +/// is equal to [value], ignoring whitespace. +/// +/// In this matcher, "ignoring whitespace" means comparing with all runs of +/// whitespace collapsed to single space characters and leading and trailing +/// whitespace removed. +/// +/// For example, the following will all match successfully: +/// +/// expect("hello world", equalsIgnoringWhitespace("hello world")); +/// expect(" hello world", equalsIgnoringWhitespace("hello world")); +/// expect("hello world ", equalsIgnoringWhitespace("hello world")); +/// +/// The following will not match: +/// +/// expect("helloworld", equalsIgnoringWhitespace("hello world")); +/// expect("he llo world", equalsIgnoringWhitespace("hello world")); +Matcher equalsIgnoringWhitespace(String value) => + _IsEqualIgnoringWhitespace(value); + +class _IsEqualIgnoringWhitespace extends FeatureMatcher<String> { + final String _matchValue; + + _IsEqualIgnoringWhitespace(String value) + : _matchValue = collapseWhitespace(value); + + @override + bool typedMatches(String item, Map matchState) => + _matchValue == collapseWhitespace(item); + + @override + Description describe(Description description) => + description.addDescriptionOf(_matchValue).add(' ignoring whitespace'); + + @override + Description describeTypedMismatch(dynamic item, + Description mismatchDescription, Map matchState, bool verbose) { + return mismatchDescription + .add('is ') + .addDescriptionOf(collapseWhitespace(item)) + .add(' with whitespace compressed'); + } +} + +/// Returns a matcher that matches if the match argument is a string and +/// starts with [prefixString]. +Matcher startsWith(String prefixString) => _StringStartsWith(prefixString); + +class _StringStartsWith extends FeatureMatcher<String> { + final String _prefix; + + const _StringStartsWith(this._prefix); + + @override + bool typedMatches(String item, Map matchState) => item.startsWith(_prefix); + + @override + Description describe(Description description) => + description.add('a string starting with ').addDescriptionOf(_prefix); +} + +/// Returns a matcher that matches if the match argument is a string and +/// ends with [suffixString]. +Matcher endsWith(String suffixString) => _StringEndsWith(suffixString); + +class _StringEndsWith extends FeatureMatcher<String> { + final String _suffix; + + const _StringEndsWith(this._suffix); + + @override + bool typedMatches(String item, Map matchState) => item.endsWith(_suffix); + + @override + Description describe(Description description) => + description.add('a string ending with ').addDescriptionOf(_suffix); +} + +/// Returns a matcher that matches if the match argument is a string and +/// contains a given list of [substrings] in relative order. +/// +/// For example, `stringContainsInOrder(["a", "e", "i", "o", "u"])` will match +/// "abcdefghijklmnopqrstuvwxyz". + +Matcher stringContainsInOrder(List<String> substrings) => + _StringContainsInOrder(substrings); + +class _StringContainsInOrder extends FeatureMatcher<String> { + final List<String> _substrings; + + const _StringContainsInOrder(this._substrings); + + @override + bool typedMatches(String item, Map matchState) { + var fromIndex = 0; + for (var s in _substrings) { + var index = item.indexOf(s, fromIndex); + if (index < 0) return false; + fromIndex = index + s.length; + } + return true; + } + + @override + Description describe(Description description) => description.addAll( + 'a string containing ', ', ', ' in order', _substrings); +} + +/// Returns a matcher that matches if the match argument is a string and +/// matches the regular expression given by [re]. +/// +/// [re] can be a [RegExp] instance or a [String]; in the latter case it will be +/// used to create a RegExp instance. +Matcher matches(Pattern re) => _MatchesRegExp(re); + +class _MatchesRegExp extends FeatureMatcher<String> { + final RegExp _regexp; + + _MatchesRegExp(Pattern re) + : _regexp = (re is String) + ? RegExp(re) + : (re is RegExp) + ? re + : throw ArgumentError('matches requires a regexp or string'); + + @override + bool typedMatches(dynamic item, Map matchState) => _regexp.hasMatch(item); + + @override + Description describe(Description description) => + description.add("match '${_regexp.pattern}'"); +} + +/// Utility function to collapse whitespace runs to single spaces +/// and strip leading/trailing whitespace. +String collapseWhitespace(String string) { + var result = StringBuffer(); + var skipSpace = true; + for (var i = 0; i < string.length; i++) { + var character = string[i]; + if (_isWhitespace(character)) { + if (!skipSpace) { + result.write(' '); + skipSpace = true; + } + } else { + result.write(character); + skipSpace = false; + } + } + return result.toString().trim(); +} + +bool _isWhitespace(String ch) => + ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t';
diff --git a/pkgs/matcher/lib/src/type_matcher.dart b/pkgs/matcher/lib/src/type_matcher.dart new file mode 100644 index 0000000..9d32b9f --- /dev/null +++ b/pkgs/matcher/lib/src/type_matcher.dart
@@ -0,0 +1,115 @@ +// 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. + +import 'package:meta/meta.dart'; + +import 'having_matcher.dart'; +import 'interfaces.dart'; + +/// Returns a matcher that matches objects with type [T]. +/// +/// ```dart +/// expect(shouldBeDuration, isA<Duration>()); +/// ``` +/// +/// Expectations can be chained on top of the type using the +/// [TypeMatcher.having] method to add additional constraints. +TypeMatcher<T> isA<T>() => TypeMatcher<T>(); + +/// A [Matcher] subclass that supports validating the [Type] of the target +/// object. +/// +/// ```dart +/// expect(shouldBeDuration, TypeMatcher<Duration>()); +/// ``` +/// +/// If you want to further validate attributes of the specified [Type], use the +/// [having] function. +/// +/// ```dart +/// void shouldThrowRangeError(int value) { +/// throw RangeError.range(value, 10, 20); +/// } +/// +/// expect( +/// () => shouldThrowRangeError(5), +/// throwsA(const TypeMatcher<RangeError>() +/// .having((e) => e.start, 'start', greaterThanOrEqualTo(10)) +/// .having((e) => e.end, 'end', lessThanOrEqualTo(20)))); +/// ``` +/// +/// Notice that you can chain multiple calls to [having] to verify multiple +/// aspects of an object. +/// +/// Note: All of the top-level `isType` matchers exposed by this package are +/// instances of [TypeMatcher], so you can use the [having] function without +/// creating your own instance. +/// +/// ```dart +/// expect( +/// () => shouldThrowRangeError(5), +/// throwsA(isRangeError +/// .having((e) => e.start, 'start', greaterThanOrEqualTo(10)) +/// .having((e) => e.end, 'end', lessThanOrEqualTo(20)))); +/// ``` +class TypeMatcher<T> extends Matcher { + final String? _name; + + /// Create a matcher matches instances of type [T]. + /// + /// For a fluent API to create TypeMatchers see [isA]. + const TypeMatcher( + [@Deprecated('Provide a type argument to TypeMatcher and omit the name. ' + 'This argument will be removed in the next release.') + String? name]) + : _name = + // ignore: deprecated_member_use_from_same_package + name; + + /// Returns a new [TypeMatcher] that validates the existing type as well as + /// a specific [feature] of the object with the provided [matcher]. + /// + /// Provides a human-readable [description] of the [feature] to make debugging + /// failures easier. + /// + /// ```dart + /// /// Validates that the object is a [RangeError] with a message containing + /// /// the string 'details' and `start` and `end` properties that are `null`. + /// final _rangeMatcher = isRangeError + /// .having((e) => e.message, 'message', contains('details')) + /// .having((e) => e.start, 'start', isNull) + /// .having((e) => e.end, 'end', isNull); + /// ``` + @useResult + TypeMatcher<T> having( + Object? Function(T) feature, String description, dynamic matcher) => + HavingMatcher(this, description, feature, matcher); + + @override + Description describe(Description description) { + var name = _name ?? _stripDynamic(T); + return description.add("<Instance of '$name'>"); + } + + @override + bool matches(Object? item, Map matchState) => item is T; + + @override + Description describeMismatch(dynamic item, Description mismatchDescription, + Map matchState, bool verbose) { + var name = _name ?? _stripDynamic(T); + return mismatchDescription.add("is not an instance of '$name'"); + } +} + +final _dart2DynamicArgs = RegExp('<dynamic(, dynamic)*>'); + +/// With this expression `{}.runtimeType.toString`, +/// Dart 1: "`<Instance of Map>`" +/// Dart 2: "`<Instance of Map<dynamic, dynamic>>`" +/// +/// This functions returns the Dart 1 output, when Dart 2 runtime semantics +/// are enabled. +String _stripDynamic(Type type) => + type.toString().replaceAll(_dart2DynamicArgs, '');
diff --git a/pkgs/matcher/lib/src/util.dart b/pkgs/matcher/lib/src/util.dart new file mode 100644 index 0000000..af0ba2c --- /dev/null +++ b/pkgs/matcher/lib/src/util.dart
@@ -0,0 +1,70 @@ +// Copyright (c) 2014, 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 'core_matchers.dart'; +import 'equals_matcher.dart'; +import 'interfaces.dart'; + +/// A [Map] between whitespace characters and their escape sequences. +const _escapeMap = { + '\n': r'\n', + '\r': r'\r', + '\f': r'\f', + '\b': r'\b', + '\t': r'\t', + '\v': r'\v', + '\x7F': r'\x7F', // delete +}; + +/// A [RegExp] that matches whitespace characters that should be escaped. +final _escapeRegExp = RegExp( + '[\\x00-\\x07\\x0E-\\x1F${_escapeMap.keys.map(_getHexLiteral).join()}]'); + +/// Useful utility for nesting match states. +void addStateInfo(Map matchState, Map values) { + var innerState = Map.of(matchState); + matchState.clear(); + matchState['state'] = innerState; + matchState.addAll(values); +} + +/// Takes an argument and returns an equivalent [Matcher]. +/// +/// If the argument is already a matcher this does nothing, +/// else if the argument is a function, it generates a predicate +/// function matcher, else it generates an equals matcher. +Matcher wrapMatcher(Object? valueOrMatcher) { + if (valueOrMatcher is Matcher) { + return valueOrMatcher; + } else if (valueOrMatcher is bool Function(Object?)) { + // already a predicate that can handle anything + return predicate(valueOrMatcher); + } else if (valueOrMatcher is bool Function(Never)) { + // unary predicate, but expects a specific type + // so wrap it. + // ignore: unnecessary_lambdas + return predicate((a) => (valueOrMatcher as dynamic)(a)); + } else { + return equals(valueOrMatcher); + } +} + +/// Returns [str] with all whitespace characters represented as their escape +/// sequences. +/// +/// Backslash characters are escaped as `\\` +String escape(String str) { + str = str.replaceAll('\\', r'\\'); + return str.replaceAllMapped(_escapeRegExp, (match) { + var mapped = _escapeMap[match[0]]; + if (mapped != null) return mapped; + return _getHexLiteral(match[0]!); + }); +} + +/// Given single-character string, return the hex-escaped equivalent. +String _getHexLiteral(String input) { + var rune = input.runes.single; + return r'\x' + rune.toRadixString(16).toUpperCase().padLeft(2, '0'); +}
diff --git a/pkgs/matcher/mono_pkg.yaml b/pkgs/matcher/mono_pkg.yaml new file mode 100644 index 0000000..9ef7927 --- /dev/null +++ b/pkgs/matcher/mono_pkg.yaml
@@ -0,0 +1,17 @@ +# See https://pub.dev/packages/mono_repo + +sdk: +- dev + +stages: +- analyze_and_format: + - group: + - format + - analyze: --fatal-infos + - group: + - analyze + sdk: pubspec +- unit_test: + - group: + - command: dart test + sdk: [dev, pubspec]
diff --git a/pkgs/matcher/pubspec.yaml b/pkgs/matcher/pubspec.yaml new file mode 100644 index 0000000..237e559 --- /dev/null +++ b/pkgs/matcher/pubspec.yaml
@@ -0,0 +1,25 @@ +name: matcher +version: 0.12.18-wip +description: >- + Support for specifying test expectations via an extensible Matcher class. + Also includes a number of built-in Matcher implementations for common cases. +repository: https://github.com/dart-lang/test/tree/master/pkgs/matcher + +environment: + sdk: ^3.4.0 + +dependencies: + async: ^2.10.0 + meta: ^1.8.0 + stack_trace: ^1.10.0 + term_glyph: ^1.2.0 + test_api: ">=0.5.0 <0.8.0" + +dev_dependencies: + fake_async: ^1.3.0 + lints: ^3.0.0 + test: ^1.23.0 + +dependency_overrides: + test: 1.25.0 + test_api: 0.7.3
diff --git a/pkgs/matcher/test/core_matchers_test.dart b/pkgs/matcher/test/core_matchers_test.dart new file mode 100644 index 0000000..04fc8b3 --- /dev/null +++ b/pkgs/matcher/test/core_matchers_test.dart
@@ -0,0 +1,247 @@ +// Copyright (c) 2012, 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:matcher/matcher.dart'; +import 'package:test/test.dart' show test, group; + +import 'test_utils.dart'; + +void main() { + test('isTrue', () { + shouldPass(true, isTrue); + shouldFail(false, isTrue, 'Expected: true Actual: <false>'); + }); + + test('isFalse', () { + shouldPass(false, isFalse); + shouldFail(10, isFalse, 'Expected: false Actual: <10>'); + shouldFail(true, isFalse, 'Expected: false Actual: <true>'); + }); + + test('isNull', () { + shouldPass(null, isNull); + shouldFail(false, isNull, 'Expected: null Actual: <false>'); + }); + + test('isNotNull', () { + shouldPass(false, isNotNull); + shouldFail(null, isNotNull, 'Expected: not null Actual: <null>'); + }); + + test('isNaN', () { + shouldPass(double.nan, isNaN); + shouldFail(3.1, isNaN, 'Expected: NaN Actual: <3.1>'); + shouldFail('not a num', isNaN, endsWith('not an <Instance of \'num\'>')); + }); + + test('isNotNaN', () { + shouldPass(3.1, isNotNaN); + shouldFail(double.nan, isNotNaN, 'Expected: not NaN Actual: <NaN>'); + shouldFail('not a num', isNotNaN, endsWith('not an <Instance of \'num\'>')); + }); + + test('same', () { + var a = {}; + var b = {}; + shouldPass(a, same(a)); + shouldFail(b, same(a), 'Expected: same instance as {} Actual: {}'); + }); + + test('equals', () { + var a = {}; + var b = {}; + shouldPass(a, equals(a)); + shouldPass(a, equals(b)); + }); + + test('equals with null', () { + Object? a; // null + var b = {}; + shouldPass(a, equals(a)); + shouldFail( + a, equals(b), 'Expected: {} Actual: <null> Which: expected a map'); + shouldFail(b, equals(a), 'Expected: <null> Actual: {}'); + }); + + test('equals with a set', () { + var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + var set1 = numbers.toSet(); + numbers.shuffle(); + var set2 = numbers.toSet(); + + shouldPass(set2, equals(set1)); + shouldPass(numbers, equals(set1)); + shouldFail( + [1, 2, 3, 4, 5, 6, 7, 8, 9], + equals(set1), + matches(r'Expected: .*:\[1, 2, 3, 4, 5, 6, 7, 8, 9, 10\]' + r' Actual: \[1, 2, 3, 4, 5, 6, 7, 8, 9\]' + r' Which: does not contain <10>')); + shouldFail( + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + equals(set1), + matches(r'Expected: .*:\[1, 2, 3, 4, 5, 6, 7, 8, 9, 10\]' + r' Actual: \[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11\]' + r' Which: larger than expected')); + }); + + test('anything', () { + var a = {}; + shouldPass(0, anything); + shouldPass(null, anything); + shouldPass(a, anything); + shouldFail(a, isNot(anything), 'Expected: not anything Actual: {}'); + }); + + test('returnsNormally', () { + shouldPass(doesNotThrow, returnsNormally); + shouldFail( + doesThrow, + returnsNormally, + matches(r'Expected: return normally' + r' Actual: <Closure.*>' + r' Which: threw StateError:<Bad state: X>')); + shouldFail('not a function', returnsNormally, + contains('not an <Instance of \'Function\'>')); + }); + + test('hasLength', () { + var a = {}; + var b = []; + shouldPass(a, hasLength(0)); + shouldPass(b, hasLength(0)); + shouldPass('a', hasLength(1)); + shouldFail( + 0, + hasLength(0), + 'Expected: an object with length of <0> ' + 'Actual: <0> ' + 'Which: has no length property'); + + b.add(0); + shouldPass(b, hasLength(1)); + shouldFail( + b, + hasLength(2), + 'Expected: an object with length of <2> ' + 'Actual: [0] ' + 'Which: has length of <1>'); + + b.add(0); + shouldFail( + b, + hasLength(1), + 'Expected: an object with length of <1> ' + 'Actual: [0, 0] ' + 'Which: has length of <2>'); + shouldPass(b, hasLength(2)); + }); + + test('scalar type mismatch', () { + shouldFail( + 'error', + equals(5.1), + 'Expected: <5.1> ' + "Actual: 'error'"); + }); + + test('nested type mismatch', () { + shouldFail( + ['error'], + equals([5.1]), + 'Expected: [5.1] ' + "Actual: ['error'] " + "Which: at location [0] is 'error' instead of <5.1>"); + }); + + test('doubly-nested type mismatch', () { + shouldFail( + [ + ['error'] + ], + equals([ + [5.1] + ]), + 'Expected: [[5.1]] ' + "Actual: [['error']] " + "Which: at location [0][0] is 'error' instead of <5.1>"); + }); + + test('doubly nested inequality', () { + var actual1 = [ + ['foo', 'bar'], + ['foo'], + 3, + [] + ]; + var expected1 = [ + ['foo', 'bar'], + ['foo'], + 4, + [] + ]; + var reason1 = "Expected: [['foo', 'bar'], ['foo'], 4, []] " + "Actual: [['foo', 'bar'], ['foo'], 3, []] " + 'Which: at location [2] is <3> instead of <4>'; + + var actual2 = [ + ['foo', 'barry'], + ['foo'], + 4, + [] + ]; + var expected2 = [ + ['foo', 'bar'], + ['foo'], + 4, + [] + ]; + var reason2 = "Expected: [['foo', 'bar'], ['foo'], 4, []] " + "Actual: [['foo', 'barry'], ['foo'], 4, []] " + "Which: at location [0][1] is 'barry' instead of 'bar'"; + + var actual3 = [ + ['foo', 'bar'], + ['foo'], + 4, + {'foo': 'bar'} + ]; + var expected3 = [ + ['foo', 'bar'], + ['foo'], + 4, + {'foo': 'barry'} + ]; + var reason3 = "Expected: [['foo', 'bar'], ['foo'], 4, {'foo': 'barry'}] " + "Actual: [['foo', 'bar'], ['foo'], 4, {'foo': 'bar'}] " + "Which: at location [3]['foo'] is 'bar' instead of 'barry'"; + + shouldFail(actual1, equals(expected1), reason1); + shouldFail(actual2, equals(expected2), reason2); + shouldFail(actual3, equals(expected3), reason3); + }); + + group('Predicate Matchers', () { + test('isInstanceOf', () { + shouldFail(0, predicate((x) => x is String, 'an instance of String'), + 'Expected: an instance of String Actual: <0>'); + shouldPass('cow', predicate((x) => x is String, 'an instance of String')); + + shouldFail(0, predicate((bool x) => x, 'bool value is true'), + endsWith("not an <Instance of 'bool'>")); + }); + }); + + group('wrapMatcher', () { + test('wraps a predicate which allows a nullable argument', () { + final matcher = wrapMatcher((_) => true); + shouldPass(null, matcher); + }); + + test('wraps a predicate which has a typed argument check', () { + final matcher = wrapMatcher((int _) => true); + shouldPass(1, matcher); + }); + }); +}
diff --git a/pkgs/matcher/test/custom_matcher_test.dart b/pkgs/matcher/test/custom_matcher_test.dart new file mode 100644 index 0000000..d0a17c9 --- /dev/null +++ b/pkgs/matcher/test/custom_matcher_test.dart
@@ -0,0 +1,48 @@ +// Copyright (c) 2012, 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:matcher/matcher.dart'; +import 'package:test/test.dart' show test; + +import 'test_utils.dart'; + +class _BadCustomMatcher extends CustomMatcher { + _BadCustomMatcher() : super('feature', 'description', {1: 'a'}); + @override + Object? featureValueOf(dynamic actual) => throw Exception('bang'); +} + +class _HasPrice extends CustomMatcher { + _HasPrice(Object? matcher) + : super('Widget with a price that is', 'price', matcher); + @override + Object? featureValueOf(Object? actual) => (actual as Widget).price; +} + +void main() { + test('Feature Matcher', () { + var w = Widget(); + w.price = 10; + shouldPass(w, _HasPrice(10)); + shouldPass(w, _HasPrice(greaterThan(0))); + shouldFail( + w, + _HasPrice(greaterThan(10)), + 'Expected: Widget with a price that is a value greater than <10> ' + "Actual: <Instance of 'Widget'> " + 'Which: has price with value <10> which is not ' + 'a value greater than <10>'); + }); + + test('Custom Matcher Exception', () { + shouldFail( + 'a', + _BadCustomMatcher(), + allOf([ + contains("Expected: feature {1: 'a'} "), + contains("Actual: 'a' "), + contains("Which: threw 'Exception: bang' "), + ])); + }); +}
diff --git a/pkgs/matcher/test/escape_test.dart b/pkgs/matcher/test/escape_test.dart new file mode 100644 index 0000000..c898054 --- /dev/null +++ b/pkgs/matcher/test/escape_test.dart
@@ -0,0 +1,63 @@ +// Copyright (c) 2015, 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. + +// ignore_for_file: missing_whitespace_between_adjacent_strings + +import 'package:test/test.dart'; + +void main() { + group('escaping should work with', () { + _testEscaping('no escaped chars', 'Hello, world!', 'Hello, world!'); + _testEscaping('newline', '\n', r'\n'); + _testEscaping('carriage return', '\r', r'\r'); + _testEscaping('form feed', '\f', r'\f'); + _testEscaping('backspace', '\b', r'\b'); + _testEscaping('tab', '\t', r'\t'); + _testEscaping('vertical tab', '\v', r'\v'); + _testEscaping('null byte', '\x00', r'\x00'); + _testEscaping('ASCII control character', '\x11', r'\x11'); + _testEscaping('delete', '\x7F', r'\x7F'); + _testEscaping('escape combos', r'\n', r'\\n'); + _testEscaping( + 'All characters', + 'A new line\nA charriage return\rA form feed\fA backspace\b' + 'A tab\tA vertical tab\vA slash\\A null byte\x00A control char\x1D' + 'A delete\x7F', + r'A new line\nA charriage return\rA form feed\fA backspace\b' + r'A tab\tA vertical tab\vA slash\\A null byte\x00A control char\x1D' + r'A delete\x7F'); + }); + + group('unequal strings remain unequal when escaped', () { + _testUnequalStrings('with a newline', '\n', r'\n'); + _testUnequalStrings('with slash literals', '\\', r'\\'); + }); +} + +/// Creates a [test] with name [name] that verifies [source] escapes to value +/// [target]. +void _testEscaping(String name, String source, String target) { + test(name, () { + var escaped = escape(source); + expect(escaped == target, isTrue, + reason: 'Expected escaped value: $target\n' + ' Actual escaped value: $escaped'); + }); +} + +/// Creates a [test] with name [name] that ensures two different [String] values +/// [s1] and [s2] remain unequal when escaped. +void _testUnequalStrings(String name, String s1, String s2) { + test(name, () { + // Explicitly not using the equals matcher + expect(s1 != s2, isTrue, reason: 'The source values should be unequal'); + + var escapedS1 = escape(s1); + var escapedS2 = escape(s2); + + // Explicitly not using the equals matcher + expect(escapedS1 != escapedS2, isTrue, + reason: 'Unequal strings, when escaped, should remain unequal.'); + }); +}
diff --git a/pkgs/matcher/test/expect_async_test.dart b/pkgs/matcher/test/expect_async_test.dart new file mode 100644 index 0000000..7619893 --- /dev/null +++ b/pkgs/matcher/test/expect_async_test.dart
@@ -0,0 +1,393 @@ +// Copyright (c) 2015, 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. + +// ignore_for_file: only_throw_errors + +import 'dart:async'; + +import 'package:fake_async/fake_async.dart'; +import 'package:test/test.dart'; +import 'package:test_api/hooks_testing.dart'; + +import 'utils_new.dart'; + +void main() { + group('supports a function with this many arguments:', () { + test('0', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync0(() { + callbackRun = true; + })(); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('1', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync1((int arg) { + expect(arg, equals(1)); + callbackRun = true; + })(1); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('2', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync2((arg1, arg2) { + expect(arg1, equals(1)); + expect(arg2, equals(2)); + callbackRun = true; + })(1, 2); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('3', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync3((arg1, arg2, arg3) { + expect(arg1, equals(1)); + expect(arg2, equals(2)); + expect(arg3, equals(3)); + callbackRun = true; + })(1, 2, 3); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('4', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync4((arg1, arg2, arg3, arg4) { + expect(arg1, equals(1)); + expect(arg2, equals(2)); + expect(arg3, equals(3)); + expect(arg4, equals(4)); + callbackRun = true; + })(1, 2, 3, 4); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('5', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync5((arg1, arg2, arg3, arg4, arg5) { + expect(arg1, equals(1)); + expect(arg2, equals(2)); + expect(arg3, equals(3)); + expect(arg4, equals(4)); + expect(arg5, equals(5)); + callbackRun = true; + })(1, 2, 3, 4, 5); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('6', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync6((arg1, arg2, arg3, arg4, arg5, arg6) { + expect(arg1, equals(1)); + expect(arg2, equals(2)); + expect(arg3, equals(3)); + expect(arg4, equals(4)); + expect(arg5, equals(5)); + expect(arg6, equals(6)); + callbackRun = true; + })(1, 2, 3, 4, 5, 6); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + }); + + group('with optional arguments', () { + test('allows them to be passed', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync1(([arg = 1]) { + expect(arg, equals(2)); + callbackRun = true; + })(2); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('allows them not to be passed', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + expectAsync1(([arg = 1]) { + expect(arg, equals(1)); + callbackRun = true; + })(); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + }); + + group('by default', () { + test("won't allow the test to complete until it's called", () async { + late void Function() callback; + final monitor = TestCaseMonitor.start(() { + callback = expectAsync0(() {}); + }); + + await pumpEventQueue(); + expect(monitor.state, equals(State.running)); + callback(); + await monitor.onDone; + + expectTestPassed(monitor); + }); + + test('may only be called once', () async { + var monitor = await TestCaseMonitor.run(() { + var callback = expectAsync0(() {}); + callback(); + callback(); + }); + + expectTestFailed( + monitor, 'Callback called more times than expected (1).'); + }); + }); + + group('with count', () { + test( + "won't allow the test to complete until it's called at least that " + 'many times', () async { + late void Function() callback; + final monitor = TestCaseMonitor.start(() { + callback = expectAsync0(() {}, count: 3); + }); + + await pumpEventQueue(); + expect(monitor.state, equals(State.running)); + callback(); + + await pumpEventQueue(); + expect(monitor.state, equals(State.running)); + callback(); + + await pumpEventQueue(); + expect(monitor.state, equals(State.running)); + callback(); + + await monitor.onDone; + + expectTestPassed(monitor); + }); + + test("will throw an error if it's called more than that many times", + () async { + var monitor = await TestCaseMonitor.run(() { + var callback = expectAsync0(() {}, count: 3); + callback(); + callback(); + callback(); + callback(); + }); + + expectTestFailed( + monitor, 'Callback called more times than expected (3).'); + }); + + group('0,', () { + test("won't block the test's completion", () { + expectAsync0(() {}, count: 0); + }); + + test("will throw an error if it's ever called", () async { + var monitor = await TestCaseMonitor.run(() { + expectAsync0(() {}, count: 0)(); + }); + + expectTestFailed( + monitor, 'Callback called more times than expected (0).'); + }); + }); + }); + + group('with max', () { + test('will allow the callback to be called that many times', () { + var callback = expectAsync0(() {}, max: 3); + callback(); + callback(); + callback(); + }); + + test('will allow the callback to be called fewer than that many times', () { + var callback = expectAsync0(() {}, max: 3); + callback(); + }); + + test("will throw an error if it's called more than that many times", + () async { + var monitor = await TestCaseMonitor.run(() { + var callback = expectAsync0(() {}, max: 3); + callback(); + callback(); + callback(); + callback(); + }); + + expectTestFailed( + monitor, 'Callback called more times than expected (3).'); + }); + + test('-1, will allow the callback to be called any number of times', () { + var callback = expectAsync0(() {}, max: -1); + for (var i = 0; i < 20; i++) { + callback(); + } + }); + }); + + test('will throw an error if max is less than count', () { + expect(() => expectAsync0(() {}, max: 1, count: 2), throwsArgumentError); + }); + + group('expectAsyncUntil()', () { + test("won't allow the test to complete until isDone returns true", + () async { + late TestCaseMonitor monitor; + late Future future; + monitor = TestCaseMonitor.start(() { + var done = false; + var callback = expectAsyncUntil0(() {}, () => done); + + future = () async { + await pumpEventQueue(); + expect(monitor.state, equals(State.running)); + callback(); + await pumpEventQueue(); + expect(monitor.state, equals(State.running)); + done = true; + callback(); + }(); + }); + await monitor.onDone; + + expectTestPassed(monitor); + // Ensure that the outer test doesn't complete until the inner future + // completes. + await future; + }); + + test("doesn't call isDone until after the callback is called", () { + var callbackRun = false; + expectAsyncUntil0(() => callbackRun = true, () { + expect(callbackRun, isTrue); + return true; + })(); + }); + }); + + test('allows errors', () async { + var monitor = await TestCaseMonitor.run(() { + expect(expectAsync0(() => throw 'oh no'), throwsA('oh no')); + }); + + expectTestPassed(monitor); + }); + + test('may be called in a non-test zone', () async { + var monitor = await TestCaseMonitor.run(() { + var callback = expectAsync0(() {}); + Zone.root.run(callback); + }); + expectTestPassed(monitor); + }); + + test('may be called in a FakeAsync zone that does not run further', () async { + var monitor = await TestCaseMonitor.run(() { + FakeAsync().run((_) { + var callback = expectAsync0(() {}); + callback(); + }); + }); + expectTestPassed(monitor); + }); + + group('old-style expectAsync()', () { + test('works with no arguments', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package, avoid_dynamic_calls + expectAsync(() { + callbackRun = true; + })(); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('works with dynamic arguments', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package, avoid_dynamic_calls + expectAsync((arg1, arg2) { + callbackRun = true; + })(1, 2); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('works with non-nullable arguments', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package, avoid_dynamic_calls + expectAsync((int arg1, int arg2) { + callbackRun = true; + })(1, 2); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test('works with 6 arguments', () async { + var callbackRun = false; + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package, avoid_dynamic_calls + expectAsync((arg1, arg2, arg3, arg4, arg5, arg6) { + callbackRun = true; + })(1, 2, 3, 4, 5, 6); + }); + + expectTestPassed(monitor); + expect(callbackRun, isTrue); + }); + + test("doesn't support a function with 7 arguments", () { + // ignore: deprecated_member_use_from_same_package + expect(() => expectAsync((a, b, c, d, e, f, g) {}), throwsArgumentError); + }); + }); +}
diff --git a/pkgs/matcher/test/expect_test.dart b/pkgs/matcher/test/expect_test.dart new file mode 100644 index 0000000..e2ef497 --- /dev/null +++ b/pkgs/matcher/test/expect_test.dart
@@ -0,0 +1,36 @@ +// 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:test/test.dart'; + +import 'utils_new.dart'; + +void main() { + group('returned Future from expectLater()', () { + test('completes immediately for a sync matcher', () { + expect(expectLater(true, isTrue), completes); + }); + + test('contains the expect failure', () { + expect(expectLater(Future.value(true), completion(isFalse)), + throwsA(isTestFailure(anything))); + }); + + test('contains an async error', () { + expect(expectLater(Future.error('oh no'), completion(isFalse)), + throwsA('oh no')); + }); + }); + + group('an async matcher that fails synchronously', () { + test('throws synchronously', () { + expect(() => expect(() {}, throwsA(anything)), + throwsA(isTestFailure(anything))); + }); + + test('can be used with synchronous operators', () { + expect(() {}, isNot(throwsA(anything))); + }); + }); +}
diff --git a/pkgs/matcher/test/having_test.dart b/pkgs/matcher/test/having_test.dart new file mode 100644 index 0000000..ddada77 --- /dev/null +++ b/pkgs/matcher/test/having_test.dart
@@ -0,0 +1,129 @@ +// 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. + +// ignore_for_file: lines_longer_than_80_chars + +import 'package:matcher/matcher.dart'; +import 'package:test/test.dart' show test, expect, throwsA, group; + +import 'test_utils.dart'; + +void main() { + test('success', () { + shouldPass(RangeError('details'), _rangeMatcher); + }); + + test('failure', () { + shouldFail( + CustomRangeError.range(-1, 1, 10), + _rangeMatcher, + "Expected: <Instance of 'RangeError'> with " + "`message`: contains 'details' and `start`: null and `end`: null " + 'Actual: CustomRangeError:<RangeError: Invalid value: details> ' + "Which: has `message` with value 'Invalid value' " + "which does not contain 'details'", + ); + }); + + // This code is used in the [TypeMatcher] doc comments. + test('integration and example', () { + void shouldThrowRangeError(int value) { + throw RangeError.range(value, 10, 20); + } + + expect( + () => shouldThrowRangeError(5), + throwsA(const TypeMatcher<RangeError>() + .having((e) => e.start, 'start', greaterThanOrEqualTo(10)) + .having((e) => e.end, 'end', lessThanOrEqualTo(20)))); + + expect( + () => shouldThrowRangeError(5), + throwsA(isRangeError + .having((e) => e.start, 'start', greaterThanOrEqualTo(10)) + .having((e) => e.end, 'end', lessThanOrEqualTo(20)))); + }); + + test('having inside deep matcher', () { + shouldFail( + [RangeError.range(-1, 1, 10)], + equals([_rangeMatcher]), + anyOf([ + equalsIgnoringWhitespace( + "Expected: [ <<Instance of 'RangeError'> with " + "`message`: contains 'details' and `start`: null and `end`: null> ] " + 'Actual: [RangeError:RangeError: ' + 'Invalid value: Not in inclusive range 1..10: -1] ' + 'Which: at location [0] is RangeError:<RangeError: ' + 'Invalid value: Not in inclusive range 1..10: -1> ' + "which has `message` with value 'Invalid value' " + "which does not contain 'details'"), + equalsIgnoringWhitespace(// Older SDKs + "Expected: [ <<Instance of 'RangeError'> with " + "`message`: contains 'details' and `start`: null and `end`: null> ] " + 'Actual: [RangeError:RangeError: ' + 'Invalid value: Not in range 1..10, inclusive: -1] ' + 'Which: at location [0] is RangeError:<RangeError: ' + 'Invalid value: Not in range 1..10, inclusive: -1> ' + "which has `message` with value 'Invalid value' " + "which does not contain 'details'") + ])); + }); + + group('CustomMatcher copy', () { + test('Feature Matcher', () { + var w = Widget(); + w.price = 10; + shouldPass(w, _hasPrice(10)); + shouldPass(w, _hasPrice(greaterThan(0))); + shouldFail( + w, + _hasPrice(greaterThan(10)), + "Expected: <Instance of 'Widget'> with `price`: a value greater than <10> " + "Actual: <Instance of 'Widget'> " + 'Which: has `price` with value <10> which is not ' + 'a value greater than <10>'); + }); + + test('Custom Matcher Exception', () { + shouldFail( + 'a', + _badCustomMatcher(), + allOf([ + contains( + "Expected: <Instance of 'Widget'> with `feature`: {1: 'a'} "), + contains("Actual: 'a'"), + ])); + shouldFail( + Widget(), + _badCustomMatcher(), + allOf([ + contains( + "Expected: <Instance of 'Widget'> with `feature`: {1: 'a'} "), + contains("Actual: <Instance of 'Widget'> "), + contains("Which: threw 'Exception: bang' "), + ])); + }); + }); +} + +final _rangeMatcher = isRangeError + .having((e) => e.message, 'message', contains('details')) + .having((e) => e.start, 'start', isNull) + .having((e) => e.end, 'end', isNull); + +Matcher _hasPrice(Object matcher) => + const TypeMatcher<Widget>().having((e) => e.price, 'price', matcher); + +Matcher _badCustomMatcher() => const TypeMatcher<Widget>() + .having((e) => throw Exception('bang'), 'feature', {1: 'a'}); + +class CustomRangeError extends RangeError { + CustomRangeError.range( + super.invalidValue, int super.minValue, int super.maxValue) + : super.range(); + + @override + String toString() => 'RangeError: Invalid value: details'; +}
diff --git a/pkgs/matcher/test/iterable_matchers_test.dart b/pkgs/matcher/test/iterable_matchers_test.dart new file mode 100644 index 0000000..7607d18 --- /dev/null +++ b/pkgs/matcher/test/iterable_matchers_test.dart
@@ -0,0 +1,395 @@ +// Copyright (c) 2012, 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'; + +import 'test_utils.dart'; + +void main() { + test('isEmpty', () { + shouldPass([], isEmpty); + shouldFail([1], isEmpty, 'Expected: empty Actual: [1]'); + }); + + test('isNotEmpty', () { + shouldFail([], isNotEmpty, 'Expected: non-empty Actual: []'); + shouldPass([1], isNotEmpty); + }); + + test('contains', () { + var d = [1, 2]; + shouldPass(d, contains(1)); + shouldFail( + d, + contains(0), + 'Expected: contains <0> ' + 'Actual: [1, 2] ' + 'Which: does not contain <0>'); + + shouldFail( + 'String', + contains(42), + "Expected: contains <42> Actual: 'String' " + 'Which: does not contain <42>'); + }); + + test('equals with matcher element', () { + var d = ['foo', 'bar']; + shouldPass(d, equals(['foo', startsWith('ba')])); + shouldFail( + d, + equals(['foo', endsWith('ba')]), + "Expected: ['foo', <a string ending with 'ba'>] " + "Actual: ['foo', 'bar'] " + "Which: at location [1] is 'bar' which " + "does not match a string ending with 'ba'"); + }); + + test('isIn', () { + // Iterable + shouldPass(1, isIn([1, 2])); + shouldFail(0, isIn([1, 2]), 'Expected: is in [1, 2] Actual: <0>'); + + // Map + shouldPass(1, isIn({1: null})); + shouldFail(0, isIn({1: null}), 'Expected: is in {1: null} Actual: <0>'); + + // String + shouldPass('42', isIn('1421')); + shouldFail('42', isIn('41'), "Expected: is in '41' Actual: '42'"); + shouldFail( + 0, isIn('a string'), endsWith('not an <Instance of \'Pattern\'>')); + + // Invalid arg + expect(() => isIn(42), throwsArgumentError); + }); + + test('everyElement', () { + var d = [1, 2]; + var e = [1, 1, 1]; + shouldFail( + d, + everyElement(1), + 'Expected: every element(<1>) ' + 'Actual: [1, 2] ' + "Which: has value <2> which doesn't match <1> at index 1"); + shouldPass(e, everyElement(1)); + shouldFail('not iterable', everyElement(1), + endsWith('not an <Instance of \'Iterable\'>')); + }); + + test('nested everyElement', () { + var d = [ + ['foo', 'bar'], + ['foo'], + [] + ]; + var e = [ + ['foo', 'bar'], + ['foo'], + 3, + [] + ]; + shouldPass(d, everyElement(anyOf(isEmpty, contains('foo')))); + shouldFail( + d, + everyElement(everyElement(equals('foo'))), + "Expected: every element(every element('foo')) " + "Actual: [['foo', 'bar'], ['foo'], []] " + "Which: has value ['foo', 'bar'] which has value 'bar' " + 'which is different. Expected: foo Actual: bar ^ ' + 'Differ at offset 0 at index 1 at index 0'); + shouldFail( + d, + everyElement(allOf(hasLength(greaterThan(0)), contains('foo'))), + 'Expected: every element((an object with length of a value ' + "greater than <0> and contains 'foo')) " + "Actual: [['foo', 'bar'], ['foo'], []] " + 'Which: has value [] which has length of <0> at index 2'); + shouldFail( + d, + everyElement(allOf(contains('foo'), hasLength(greaterThan(0)))), + "Expected: every element((contains 'foo' and " + 'an object with length of a value greater than <0>)) ' + "Actual: [['foo', 'bar'], ['foo'], []] " + "Which: has value [] which does not contain 'foo' at index 2"); + shouldFail( + e, + everyElement(allOf(contains('foo'), hasLength(greaterThan(0)))), + "Expected: every element((contains 'foo' and an object with " + 'length of a value greater than <0>)) ' + "Actual: [['foo', 'bar'], ['foo'], 3, []] " + 'Which: has value <3> which is not a string, map or iterable ' + 'at index 2'); + }); + + test('anyElement', () { + var d = [1, 2]; + var e = [1, 1, 1]; + shouldPass(d, anyElement(2)); + shouldFail( + e, anyElement(2), 'Expected: some element <2> Actual: [1, 1, 1]'); + shouldFail('not an iterable', anyElement(2), + endsWith('not an <Instance of \'Iterable\'>')); + }); + + test('orderedEquals', () { + shouldPass([null], orderedEquals([null])); + var d = [1, 2]; + shouldPass(d, orderedEquals([1, 2])); + shouldFail( + d, + orderedEquals([2, 1]), + 'Expected: equals [2, 1] ordered ' + 'Actual: [1, 2] ' + 'Which: at location [0] is <1> instead of <2>'); + shouldFail('not an iterable', orderedEquals([1]), + endsWith('not an <Instance of \'Iterable\'>')); + }); + + test('unorderedEquals', () { + var d = [1, 2]; + shouldPass(d, unorderedEquals([2, 1])); + shouldFail( + d, + unorderedEquals([1]), + 'Expected: equals [1] unordered ' + 'Actual: [1, 2] ' + 'Which: has too many elements (2 > 1)'); + shouldFail( + d, + unorderedEquals([3, 2, 1]), + 'Expected: equals [3, 2, 1] unordered ' + 'Actual: [1, 2] ' + 'Which: has too few elements (2 < 3)'); + shouldFail( + d, + unorderedEquals([3, 1]), + 'Expected: equals [3, 1] unordered ' + 'Actual: [1, 2] ' + 'Which: has no match for <3> at index 0'); + shouldFail( + d, + unorderedEquals([3, 4]), + 'Expected: equals [3, 4] unordered ' + 'Actual: [1, 2] ' + 'Which: has no match for <3> at index 0' + ' along with 1 other unmatched'); + shouldFail('not an iterable', unorderedEquals([1]), + endsWith('not an <Instance of \'Iterable\'>')); + }); + + test('unorderedMatches', () { + var d = [1, 2]; + shouldPass(d, unorderedMatches([2, 1])); + shouldPass(d, unorderedMatches([greaterThan(1), greaterThan(0)])); + shouldPass(d, unorderedMatches([greaterThan(0), greaterThan(1)])); + shouldPass([2, 1], unorderedMatches([greaterThan(1), greaterThan(0)])); + + shouldPass([2, 1], unorderedMatches([greaterThan(0), greaterThan(1)])); + // Excersize the case where pairings should get "bumped" multiple times + shouldPass( + [0, 1, 2, 3, 5, 6], + unorderedMatches([ + greaterThan(1), // 6 + equals(2), // 2 + allOf([lessThan(3), isNot(0)]), // 1 + equals(0), // 0 + predicate((int v) => v % 2 == 1), // 3 + equals(5), // 5 + ])); + shouldFail( + d, + unorderedMatches([greaterThan(0)]), + 'Expected: matches [a value greater than <0>] unordered ' + 'Actual: [1, 2] ' + 'Which: has too many elements (2 > 1)'); + shouldFail( + d, + unorderedMatches([3, 2, 1]), + 'Expected: matches [<3>, <2>, <1>] unordered ' + 'Actual: [1, 2] ' + 'Which: has too few elements (2 < 3)'); + shouldFail( + d, + unorderedMatches([3, 1]), + 'Expected: matches [<3>, <1>] unordered ' + 'Actual: [1, 2] ' + 'Which: has no match for <3> at index 0'); + shouldFail( + d, + unorderedMatches([greaterThan(3), greaterThan(0)]), + 'Expected: matches [a value greater than <3>, a value greater than ' + '<0>] unordered ' + 'Actual: [1, 2] ' + 'Which: has no match for a value greater than <3> at index 0'); + shouldFail('not an iterable', unorderedMatches([greaterThan(1)]), + endsWith('not an <Instance of \'Iterable\'>')); + }); + + test('containsAll', () { + var d = [0, 1, 2]; + shouldPass(d, containsAll([1, 2])); + shouldPass(d, containsAll([2, 1])); + shouldPass(d, containsAll([greaterThan(0), greaterThan(1)])); + shouldPass([2, 1], containsAll([greaterThan(0), greaterThan(1)])); + shouldFail( + d, + containsAll([1, 2, 3]), + 'Expected: contains all of [1, 2, 3] ' + 'Actual: [0, 1, 2] ' + 'Which: has no match for <3> at index 2'); + shouldFail( + 1, + containsAll([1]), + 'Expected: contains all of [1] ' + 'Actual: <1> ' + "Which: not an <Instance of 'Iterable'>"); + shouldFail( + [-1, 2], + containsAll([greaterThan(0), greaterThan(1)]), + 'Expected: contains all of [<a value greater than <0>>, ' + '<a value greater than <1>>] ' + 'Actual: [-1, 2] ' + 'Which: has no match for a value greater than <1> at index 1'); + shouldFail('not an iterable', containsAll([1, 2, 3]), + endsWith('not an <Instance of \'Iterable\'>')); + }); + + test('containsAllInOrder', () { + var d = [0, 1, 0, 2]; + shouldPass(d, containsAllInOrder([1, 2])); + shouldPass(d, containsAllInOrder([greaterThan(0), greaterThan(1)])); + shouldFail( + d, + containsAllInOrder([2, 1]), + 'Expected: contains in order([2, 1]) ' + 'Actual: [0, 1, 0, 2] ' + 'Which: did not find a value matching <1> following expected prior ' + 'values'); + shouldFail( + d, + containsAllInOrder([greaterThan(1), greaterThan(0)]), + 'Expected: contains in order([<a value greater than <1>>, ' + '<a value greater than <0>>]) ' + 'Actual: [0, 1, 0, 2] ' + 'Which: did not find a value matching a value greater than <0> ' + 'following expected prior values'); + shouldFail( + d, + containsAllInOrder([1, 2, 3]), + 'Expected: contains in order([1, 2, 3]) ' + 'Actual: [0, 1, 0, 2] ' + 'Which: did not find a value matching <3> following expected prior ' + 'values'); + shouldFail( + 1, + containsAllInOrder([1]), + 'Expected: contains in order([1]) ' + 'Actual: <1> ' + "Which: not an <Instance of 'Iterable'>"); + }); + + test('containsOnce', () { + shouldPass([1, 2, 3, 4], containsOnce(2)); + shouldPass([1, 2, 11, 3], containsOnce(greaterThan(10))); + shouldFail( + [1, 2, 3, 4], + containsOnce(10), + 'Expected: contains once(<10>) ' + 'Actual: [1, 2, 3, 4] ' + 'Which: did not find a value matching <10>'); + shouldFail( + [1, 2, 3, 4], + containsOnce(greaterThan(10)), + 'Expected: contains once(a value greater than <10>) ' + 'Actual: [1, 2, 3, 4] ' + 'Which: did not find a value matching a value greater than <10>'); + shouldFail( + [1, 2, 1, 2], + containsOnce(2), + 'Expected: contains once(<2>) ' + 'Actual: [1, 2, 1, 2] ' + 'Which: expected only one value matching <2> ' + 'but found multiple: <2>, <2>'); + shouldFail( + [1, 2, 10, 20], + containsOnce(greaterThan(5)), + 'Expected: contains once(a value greater than <5>) ' + 'Actual: [1, 2, 10, 20] ' + 'Which: expected only one value matching a value greater than <5> ' + 'but found multiple: <10>, <20>'); + }); + + test('pairwise compare', () { + var c = [1, 2]; + var d = [1, 2, 3]; + var e = [1, 4, 9]; + shouldFail( + 'x', + pairwiseCompare(e, (int e, int a) => a <= e, 'less than or equal'), + 'Expected: pairwise less than or equal [1, 4, 9] ' + "Actual: 'x' " + "Which: not an <Instance of 'Iterable'>"); + shouldFail( + c, + pairwiseCompare(e, (int e, int a) => a <= e, 'less than or equal'), + 'Expected: pairwise less than or equal [1, 4, 9] ' + 'Actual: [1, 2] ' + 'Which: has length 2 instead of 3'); + shouldPass( + d, pairwiseCompare(e, (int e, int a) => a <= e, 'less than or equal')); + shouldFail( + d, + pairwiseCompare(e, (int e, int a) => a < e, 'less than'), + 'Expected: pairwise less than [1, 4, 9] ' + 'Actual: [1, 2, 3] ' + 'Which: has <1> which is not less than <1> at index 0'); + shouldPass( + d, pairwiseCompare(e, (int e, int a) => a * a == e, 'square root of')); + shouldFail( + d, + pairwiseCompare(e, (int e, int a) => a + a == e, 'double'), + 'Expected: pairwise double [1, 4, 9] ' + 'Actual: [1, 2, 3] ' + 'Which: has <1> which is not double <1> at index 0'); + shouldFail( + 'not an iterable', + pairwiseCompare(e, (int e, int a) => a + a == e, 'double'), + endsWith('not an <Instance of \'Iterable\'>')); + }); + + test('isEmpty', () { + var d = SimpleIterable(0); + var e = SimpleIterable(1); + shouldPass(d, isEmpty); + shouldFail( + e, + isEmpty, + 'Expected: empty ' + 'Actual: SimpleIterable:[1]'); + }); + + test('isNotEmpty', () { + var d = SimpleIterable(0); + var e = SimpleIterable(1); + shouldPass(e, isNotEmpty); + shouldFail( + d, + isNotEmpty, + 'Expected: non-empty ' + 'Actual: SimpleIterable:[]'); + }); + + test('contains', () { + var d = SimpleIterable(3); + shouldPass(d, contains(2)); + shouldFail( + d, + contains(5), + 'Expected: contains <5> ' + 'Actual: SimpleIterable:[3, 2, 1] ' + 'Which: does not contain <5>'); + }); +}
diff --git a/pkgs/matcher/test/map_matchers_test.dart b/pkgs/matcher/test/map_matchers_test.dart new file mode 100644 index 0000000..4c699ab --- /dev/null +++ b/pkgs/matcher/test/map_matchers_test.dart
@@ -0,0 +1,100 @@ +import 'package:matcher/matcher.dart' + show contains, containsValue, containsPair; +import 'package:test/test.dart' show test; + +import 'test_utils.dart'; + +void main() { + test('contains', () { + shouldPass({'a': 1}, contains('a')); + shouldPass({null: 1}, contains(null)); + shouldFail( + {'a': 1}, + contains(2), + 'Expected: contains <2> ' + 'Actual: {\'a\': 1} ' + 'Which: does not contain <2>', + ); + shouldFail( + {'a': 1}, + contains(null), + 'Expected: contains <null> ' + 'Actual: {\'a\': 1} ' + 'Which: does not contain <null>', + ); + }); + + test('containsValue', () { + shouldPass({'a': 1, 'null': null}, containsValue(1)); + shouldPass({'a': 1, 'null': null}, containsValue(null)); + shouldFail( + {'a': 1, 'null': null}, + containsValue(2), + 'Expected: contains value <2> ' + "Actual: {'a': 1, 'null': null}", + ); + }); + + test('containsPair', () { + shouldPass({'a': 1, 'null': null}, containsPair('a', 1)); + shouldPass({'a': 1, 'null': null}, containsPair('null', null)); + shouldPass({null: null}, containsPair(null, null)); + shouldFail( + {'a': 1, 'null': null}, + containsPair('a', 2), + "Expected: contains pair 'a' => <2> " + "Actual: {'a': 1, 'null': null} " + "Which: contains key 'a' but with value is <1>", + ); + shouldFail( + {'a': 1, 'null': null}, + containsPair('b', 1), + "Expected: contains pair 'b' => <1> " + "Actual: {'a': 1, 'null': null} " + "Which: doesn't contain key 'b'", + ); + shouldFail( + {'a': 1, 'null': null}, + containsPair('null', 2), + "Expected: contains pair 'null' => <2> " + "Actual: {'a': 1, 'null': null} " + "Which: contains key 'null' but with value is <null>", + ); + shouldFail( + {'a': 1, 'null': null}, + containsPair('2', null), + "Expected: contains pair '2' => <null> " + "Actual: {'a': 1, 'null': null} " + "Which: doesn't contain key '2'", + ); + shouldFail( + {'a': 1, 'null': null}, + containsPair('2', 'b'), + "Expected: contains pair '2' => 'b' " + "Actual: {'a': 1, 'null': null} " + "Which: doesn't contain key '2'", + ); + shouldFail( + {null: null}, + containsPair('not null', null), + "Expected: contains pair 'not null' => <null> " + 'Actual: {null: null} ' + "Which: doesn't contain key 'not null'", + ); + shouldFail( + {null: null}, + containsPair(null, 'not null'), + 'Expected: contains pair <null> => \'not null\' ' + 'Actual: {null: null} ' + 'Which: contains key <null> but with value not an ' + '<Instance of \'String\'>', + ); + shouldFail( + {null: null}, + containsPair('not null', 'not null'), + 'Expected: contains pair \'not null\' => \'not null\' ' + 'Actual: {null: null} ' + 'Which: doesn\'t contain key \'not null\' ', + ); + }); +}
diff --git a/pkgs/matcher/test/matcher/completion_test.dart b/pkgs/matcher/test/matcher/completion_test.dart new file mode 100644 index 0000000..9259cd1 --- /dev/null +++ b/pkgs/matcher/test/matcher/completion_test.dart
@@ -0,0 +1,192 @@ +// Copyright (c) 2015, 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 'package:test/test.dart'; +import 'package:test_api/hooks_testing.dart'; + +import '../utils_new.dart'; + +void main() { + group('[doesNotComplete]', () { + test('fails when provided a non future', () async { + var monitor = await TestCaseMonitor.run(() { + expect(10, doesNotComplete); + }); + + expectTestFailed(monitor, contains('10 is not a Future')); + }); + + test('succeeds when a future does not complete', () { + var completer = Completer(); + expect(completer.future, doesNotComplete); + }); + + test('fails when a future does complete', () async { + var monitor = await TestCaseMonitor.run(() { + var completer = Completer(); + completer.complete(null); + expect(completer.future, doesNotComplete); + }); + + expectTestFailed( + monitor, + 'Future was not expected to complete but completed with a value of' + ' null'); + }); + + test('fails when a future completes after the expect', () async { + var monitor = await TestCaseMonitor.run(() { + var completer = Completer(); + expect(completer.future, doesNotComplete); + completer.complete(null); + }); + + expectTestFailed( + monitor, + 'Future was not expected to complete but completed with a value of' + ' null'); + }); + + test('fails when a future eventually completes', () async { + var monitor = await TestCaseMonitor.run(() { + var completer = Completer(); + expect(completer.future, doesNotComplete); + Future(() async { + await pumpEventQueue(times: 10); + }).then(completer.complete); + }); + + expectTestFailed( + monitor, + 'Future was not expected to complete but completed with a value of' + ' null'); + }); + }); + group('[completes]', () { + test('blocks the test until the Future completes', () async { + final completer = Completer<void>(); + final monitor = TestCaseMonitor.start(() { + expect(completer.future, completes); + }); + await pumpEventQueue(); + expect(monitor.state, State.running); + completer.complete(); + await monitor.onDone; + expectTestPassed(monitor); + }); + + test('with an error', () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.error('X'), completes); + }); + + expect(monitor.state, equals(State.failed)); + expect(monitor.errors, [isAsyncError(equals('X'))]); + }); + + test('with a failure', () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.error(TestFailure('oh no')), completes); + }); + + expectTestFailed(monitor, 'oh no'); + }); + + test('with a non-future', () async { + var monitor = await TestCaseMonitor.run(() { + expect(10, completes); + }); + + expectTestFailed( + monitor, + 'Expected: completes successfully\n' + ' Actual: <10>\n' + ' Which: was not a Future\n'); + }); + + test('with a successful future', () { + expect(Future.value('1'), completes); + }); + }); + + group('[completion]', () { + test('blocks the test until the Future completes', () async { + final completer = Completer<Object?>(); + final monitor = TestCaseMonitor.start(() { + expect(completer.future, completion(isNull)); + }); + await pumpEventQueue(); + expect(monitor.state, State.running); + completer.complete(null); + await monitor.onDone; + expectTestPassed(monitor); + }); + + test('with an error', () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.error('X'), completion(isNull)); + }); + + expect(monitor.state, equals(State.failed)); + expect(monitor.errors, [isAsyncError(equals('X'))]); + }); + + test('with a failure', () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.error(TestFailure('oh no')), completion(isNull)); + }); + + expectTestFailed(monitor, 'oh no'); + }); + + test('with a non-future', () async { + var monitor = await TestCaseMonitor.run(() { + expect(10, completion(equals(10))); + }); + + expectTestFailed( + monitor, + 'Expected: completes to a value that <10>\n' + ' Actual: <10>\n' + ' Which: was not a Future\n'); + }); + + test('with an incorrect value', () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.value('a'), completion(equals('b'))); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: completes to a value that 'b'\n" + ' Actual: <'), + endsWith('>\n' + " Which: emitted 'a'\n" + ' which is different.\n' + ' Expected: b\n' + ' Actual: a\n' + ' ^\n' + ' Differ at offset 0\n') + ])); + }); + + test("blocks expectLater's Future", () async { + var completer = Completer(); + var fired = false; + unawaited(expectLater(completer.future, completion(equals(1))).then((_) { + fired = true; + })); + + await pumpEventQueue(); + expect(fired, isFalse); + + completer.complete(1); + await pumpEventQueue(); + expect(fired, isTrue); + }); + }); +}
diff --git a/pkgs/matcher/test/matcher/prints_test.dart b/pkgs/matcher/test/matcher/prints_test.dart new file mode 100644 index 0000000..cbdb12a --- /dev/null +++ b/pkgs/matcher/test/matcher/prints_test.dart
@@ -0,0 +1,208 @@ +// Copyright (c) 2014, 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 'package:test/test.dart'; +import 'package:test_api/hooks_testing.dart'; + +import '../utils_new.dart'; + +void main() { + group('synchronous', () { + test('passes with an expected print', () { + expect(() => print('Hello, world!'), prints('Hello, world!\n')); + }); + + test('combines multiple prints', () { + expect(() { + print('Hello'); + print('World!'); + }, prints('Hello\nWorld!\n')); + }); + + test('works with a Matcher', () { + expect(() => print('Hello, world!'), prints(contains('Hello'))); + }); + + test('describes a failure nicely', () async { + void local() => print('Hello, world!'); + var monitor = await TestCaseMonitor.run(() { + expect(local, prints('Goodbye, world!\n')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: prints 'Goodbye, world!\\n'\n" + " ''\n" + ' Actual: <'), + endsWith('>\n' + " Which: printed 'Hello, world!\\n'\n" + " ''\n" + ' which is different.\n' + ' Expected: Goodbye, w ...\n' + ' Actual: Hello, wor ...\n' + ' ^\n' + ' Differ at offset 0\n') + ])); + }); + + test('describes a failure with a non-descriptive Matcher nicely', () async { + void local() => print('Hello, world!'); + var monitor = await TestCaseMonitor.run(() { + expect(local, prints(contains('Goodbye'))); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: prints contains 'Goodbye'\n" + ' Actual: <'), + endsWith('>\n' + " Which: printed 'Hello, world!\\n'\n" + " ''\n" + ' which does not contain \'Goodbye\'\n') + ])); + }); + + test('describes a failure with no text nicely', () async { + void local() {} + var monitor = await TestCaseMonitor.run(() { + expect(local, prints(contains('Goodbye'))); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: prints contains 'Goodbye'\n" + ' Actual: <'), + endsWith('>\n' + ' Which: printed nothing\n' + ' which does not contain \'Goodbye\'\n') + ])); + }); + + test('with a non-function', () async { + var monitor = await TestCaseMonitor.run(() { + expect(10, prints(contains('Goodbye'))); + }); + + expectTestFailed( + monitor, + "Expected: prints contains 'Goodbye'\n" + ' Actual: <10>\n' + ' Which: was not a unary Function\n'); + }); + }); + + group('asynchronous', () { + test('passes with an expected print', () { + expect(() => Future(() => print('Hello, world!')), + prints('Hello, world!\n')); + }); + + test('combines multiple prints', () { + expect( + () => Future(() { + print('Hello'); + print('World!'); + }), + prints('Hello\nWorld!\n')); + }); + + test('works with a Matcher', () { + expect(() => Future(() => print('Hello, world!')), + prints(contains('Hello'))); + }); + + test('describes a failure nicely', () async { + void local() => Future(() => print('Hello, world!')); + var monitor = await TestCaseMonitor.run(() { + expect(local, prints('Goodbye, world!\n')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: prints 'Goodbye, world!\\n'\n" + " ''\n" + ' Actual: <'), + contains('>\n' + " Which: printed 'Hello, world!\\n'\n" + " ''\n" + ' which is different.\n' + ' Expected: Goodbye, w ...\n' + ' Actual: Hello, wor ...\n' + ' ^\n' + ' Differ at offset 0') + ])); + }); + + test('describes a failure with a non-descriptive Matcher nicely', () async { + void local() => Future(() => print('Hello, world!')); + var monitor = await TestCaseMonitor.run(() { + expect(local, prints(contains('Goodbye'))); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: prints contains 'Goodbye'\n" + ' Actual: <'), + contains('>\n' + " Which: printed 'Hello, world!\\n'\n" + " ''") + ])); + }); + + test('describes a failure with no text nicely', () async { + void local() => Future.value(); + var monitor = await TestCaseMonitor.run(() { + expect(local, prints(contains('Goodbye'))); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: prints contains 'Goodbye'\n" + ' Actual: <'), + contains('>\n' + ' Which: printed nothing') + ])); + }); + + test("won't let the test end until the Future completes", () async { + final completer = Completer<void>(); + final monitor = TestCaseMonitor.start(() { + expect(() => completer.future, prints(isEmpty)); + }); + await pumpEventQueue(); + expect(monitor.state, State.running); + completer.complete(); + await monitor.onDone; + expectTestPassed(monitor); + }); + + test("blocks expectLater's Future", () async { + var completer = Completer(); + var fired = false; + + unawaited(expectLater(() { + scheduleMicrotask(() => print('hello!')); + return completer.future; + }, prints('hello!\n')) + .then((_) { + fired = true; + })); + + await pumpEventQueue(); + expect(fired, isFalse); + + completer.complete(); + await pumpEventQueue(); + expect(fired, isTrue); + }); + }); +}
diff --git a/pkgs/matcher/test/matcher/throws_test.dart b/pkgs/matcher/test/matcher/throws_test.dart new file mode 100644 index 0000000..25b93a9 --- /dev/null +++ b/pkgs/matcher/test/matcher/throws_test.dart
@@ -0,0 +1,282 @@ +// Copyright (c) 2015, 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. + +// ignore_for_file: only_throw_errors + +import 'dart:async'; + +import 'package:test/test.dart'; +import 'package:test_api/hooks_testing.dart'; + +import '../utils_new.dart'; + +void main() { + group('synchronous', () { + group('[throws]', () { + test('with a function that throws an error', () { + // ignore: deprecated_member_use_from_same_package + expect(() => throw 'oh no', throws); + }); + + test("with a function that doesn't throw", () async { + void local() {} + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package + expect(local, throws); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith('Expected: throws\n' + ' Actual: <'), + endsWith('>\n' + ' Which: returned <null>\n') + ])); + }); + + test('with a non-function', () async { + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package + expect(10, throws); + }); + + expectTestFailed( + monitor, + 'Expected: throws\n' + ' Actual: <10>\n' + ' Which: was not a Function or Future\n'); + }); + }); + + group('[throwsA]', () { + test('with a function that throws an identical error', () { + expect(() => throw 'oh no', throwsA('oh no')); + }); + + test('with a function that throws a matching error', () { + expect(() => throw const FormatException('bad'), + throwsA(isFormatException)); + }); + + test("with a function that doesn't throw", () async { + void local() {} + var monitor = await TestCaseMonitor.run(() { + expect(local, throwsA('oh no')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: throws 'oh no'\n" + ' Actual: <'), + endsWith('>\n' + ' Which: returned <null>\n') + ])); + }); + + test('with a non-function', () async { + var monitor = await TestCaseMonitor.run(() { + expect(10, throwsA('oh no')); + }); + + expectTestFailed( + monitor, + "Expected: throws 'oh no'\n" + ' Actual: <10>\n' + ' Which: was not a Function or Future\n'); + }); + + test('with a function that throws the wrong error', () async { + var monitor = await TestCaseMonitor.run(() { + expect(() => throw 'aw dang', throwsA('oh no')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: throws 'oh no'\n" + ' Actual: <'), + contains('>\n' + " Which: threw 'aw dang'\n" + ' stack'), + endsWith(' which is different.\n' + ' Expected: oh no\n' + ' Actual: aw dang\n' + ' ^\n' + ' Differ at offset 0\n') + ])); + }); + }); + }); + + group('asynchronous', () { + group('[throws]', () { + test('with a Future that throws an error', () { + // ignore: deprecated_member_use_from_same_package + expect(Future.error('oh no'), throws); + }); + + test("with a Future that doesn't throw", () async { + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package + expect(Future.value(), throws); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith('Expected: throws\n' + ' Actual: <'), + endsWith('>\n' + ' Which: emitted <null>\n') + ])); + }); + + test('with a closure that returns a Future that throws an error', () { + // ignore: deprecated_member_use_from_same_package + expect(() => Future.error('oh no'), throws); + }); + + test("with a closure that returns a Future that doesn't throw", () async { + var monitor = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package + expect(Future.value, throws); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith('Expected: throws\n' + ' Actual: <'), + endsWith('>\n' + ' Which: returned a Future that emitted <null>\n') + ])); + }); + + test("won't let the test end until the Future completes", () async { + late void Function() callback; + final monitor = TestCaseMonitor.start(() { + final completer = Completer<void>(); + // ignore: deprecated_member_use_from_same_package + expect(completer.future, throws); + callback = () => completer.completeError('oh no'); + }); + await pumpEventQueue(); + expect(monitor.state, State.running); + callback(); + await monitor.onDone; + expectTestPassed(monitor); + }); + }); + + group('[throwsA]', () { + test('with a Future that throws an identical error', () { + expect(Future.error('oh no'), throwsA('oh no')); + }); + + test('with a Future that throws a matching error', () { + expect(Future.error(const FormatException('bad')), + throwsA(isFormatException)); + }); + + test("with a Future that doesn't throw", () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.value(), throwsA('oh no')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: throws 'oh no'\n" + ' Actual: <'), + endsWith('>\n' + ' Which: emitted <null>\n') + ])); + }); + + test('with a Future that throws the wrong error', () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.error('aw dang'), throwsA('oh no')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: throws 'oh no'\n" + ' Actual: <'), + contains('>\n' + " Which: threw 'aw dang'\n") + ])); + }); + + test('with a closure that returns a Future that throws a matching error', + () { + expect(() => Future.error(const FormatException('bad')), + throwsA(isFormatException)); + }); + + test("with a closure that returns a Future that doesn't throw", () async { + var monitor = await TestCaseMonitor.run(() { + expect(Future.value, throwsA('oh no')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: throws 'oh no'\n" + ' Actual: <'), + endsWith('>\n' + ' Which: returned a Future that emitted <null>\n') + ])); + }); + + test('with closure that returns a Future that throws the wrong error', + () async { + var monitor = await TestCaseMonitor.run(() { + expect(() => Future.error('aw dang'), throwsA('oh no')); + }); + + expectTestFailed( + monitor, + allOf([ + startsWith("Expected: throws 'oh no'\n" + ' Actual: <'), + contains('>\n' + " Which: threw 'aw dang'\n") + ])); + }); + + test("won't let the test end until the Future completes", () async { + late void Function() callback; + final monitor = TestCaseMonitor.start(() { + final completer = Completer<void>(); + expect(completer.future, throwsA('oh no')); + callback = () => completer.completeError('oh no'); + }); + await pumpEventQueue(); + expect(monitor.state, State.running); + callback(); + await monitor.onDone; + + expectTestPassed(monitor); + }); + + test("blocks expectLater's Future", () async { + var completer = Completer(); + var fired = false; + unawaited(expectLater(completer.future, throwsArgumentError).then((_) { + fired = true; + })); + + await pumpEventQueue(); + expect(fired, isFalse); + + completer.completeError(ArgumentError('oh no')); + await pumpEventQueue(); + expect(fired, isTrue); + }); + }); + }); +}
diff --git a/pkgs/matcher/test/matcher/throws_type_test.dart b/pkgs/matcher/test/matcher/throws_type_test.dart new file mode 100644 index 0000000..557a73a --- /dev/null +++ b/pkgs/matcher/test/matcher/throws_type_test.dart
@@ -0,0 +1,177 @@ +// Copyright (c) 2015, 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. + +// ignore_for_file: only_throw_errors + +import 'package:test/test.dart'; +import 'package:test_api/hooks_testing.dart'; + +import '../utils_new.dart'; + +void main() { + group('[throwsArgumentError]', () { + test('passes when a ArgumentError is thrown', () { + expect(() => throw ArgumentError(''), throwsArgumentError); + }); + + test('fails when a non-ArgumentError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsArgumentError); + }); + + expectTestFailed(liveTest, + startsWith("Expected: throws <Instance of 'ArgumentError'>")); + }); + }); + + group('[throwsConcurrentModificationError]', () { + test('passes when a ConcurrentModificationError is thrown', () { + expect(() => throw ConcurrentModificationError(''), + throwsConcurrentModificationError); + }); + + test('fails when a non-ConcurrentModificationError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsConcurrentModificationError); + }); + + expectTestFailed( + liveTest, + startsWith( + "Expected: throws <Instance of 'ConcurrentModificationError'>")); + }); + }); + + group('[throwsCyclicInitializationError]', () { + test('passes when a CyclicInitializationError is thrown', () { + expect( + () => _CyclicInitializationFailure().x, + // ignore: deprecated_member_use_from_same_package + throwsCyclicInitializationError); + }); + + test('fails when a non-CyclicInitializationError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + // ignore: deprecated_member_use_from_same_package + expect(() => throw Exception(), throwsCyclicInitializationError); + }); + + expectTestFailed( + liveTest, startsWith("Expected: throws <Instance of 'Error'>")); + }); + }); + + group('[throwsException]', () { + test('passes when a Exception is thrown', () { + expect(() => throw Exception(''), throwsException); + }); + + test('fails when a non-Exception is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw 'oh no', throwsException); + }); + + expectTestFailed( + liveTest, startsWith("Expected: throws <Instance of 'Exception'>")); + }); + }); + + group('[throwsFormatException]', () { + test('passes when a FormatException is thrown', () { + expect(() => throw const FormatException(''), throwsFormatException); + }); + + test('fails when a non-FormatException is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsFormatException); + }); + + expectTestFailed(liveTest, + startsWith("Expected: throws <Instance of 'FormatException'>")); + }); + }); + + group('[throwsNoSuchMethodError]', () { + test('passes when a NoSuchMethodError is thrown', () { + expect(() { + // ignore: avoid_dynamic_calls + (1 as dynamic).notAMethodOnInt(); + }, throwsNoSuchMethodError); + }); + + test('fails when a non-NoSuchMethodError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsNoSuchMethodError); + }); + + expectTestFailed(liveTest, + startsWith("Expected: throws <Instance of 'NoSuchMethodError'>")); + }); + }); + + group('[throwsRangeError]', () { + test('passes when a RangeError is thrown', () { + expect(() => throw RangeError(''), throwsRangeError); + }); + + test('fails when a non-RangeError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsRangeError); + }); + + expectTestFailed( + liveTest, startsWith("Expected: throws <Instance of 'RangeError'>")); + }); + }); + + group('[throwsStateError]', () { + test('passes when a StateError is thrown', () { + expect(() => throw StateError(''), throwsStateError); + }); + + test('fails when a non-StateError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsStateError); + }); + + expectTestFailed( + liveTest, startsWith("Expected: throws <Instance of 'StateError'>")); + }); + }); + + group('[throwsUnimplementedError]', () { + test('passes when a UnimplementedError is thrown', () { + expect(() => throw UnimplementedError(''), throwsUnimplementedError); + }); + + test('fails when a non-UnimplementedError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsUnimplementedError); + }); + + expectTestFailed(liveTest, + startsWith("Expected: throws <Instance of 'UnimplementedError'>")); + }); + }); + + group('[throwsUnsupportedError]', () { + test('passes when a UnsupportedError is thrown', () { + expect(() => throw UnsupportedError(''), throwsUnsupportedError); + }); + + test('fails when a non-UnsupportedError is thrown', () async { + var liveTest = await TestCaseMonitor.run(() { + expect(() => throw Exception(), throwsUnsupportedError); + }); + + expectTestFailed(liveTest, + startsWith("Expected: throws <Instance of 'UnsupportedError'>")); + }); + }); +} + +class _CyclicInitializationFailure { + late int x = y; + late int y = x; +}
diff --git a/pkgs/matcher/test/mirror_matchers_test.dart b/pkgs/matcher/test/mirror_matchers_test.dart new file mode 100644 index 0000000..06f0df4 --- /dev/null +++ b/pkgs/matcher/test/mirror_matchers_test.dart
@@ -0,0 +1,56 @@ +// Copyright (c) 2012, 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. + +// ignore_for_file: deprecated_member_use_from_same_package + +@TestOn('vm') +library; + +import 'package:matcher/mirror_matchers.dart'; +import 'package:test/test.dart'; + +import 'test_utils.dart'; + +class C { + int instanceField = 1; + int get instanceGetter => 2; + static int staticField = 3; + static int get staticGetter => 4; +} + +void main() { + test('hasProperty', () { + var foo = [3]; + shouldPass(foo, hasProperty('length', 1)); + shouldFail( + foo, + hasProperty('foo'), + 'Expected: has property "foo" ' + 'Actual: [3] ' + 'Which: has no property named "foo"'); + shouldFail( + foo, + hasProperty('length', 2), + 'Expected: has property "length" which matches <2> ' + 'Actual: [3] ' + 'Which: has property "length" with value <1>'); + var c = C(); + shouldPass(c, hasProperty('instanceField', 1)); + shouldPass(c, hasProperty('instanceGetter', 2)); + shouldFail( + c, + hasProperty('staticField'), + 'Expected: has property "staticField" ' + 'Actual: <Instance of \'C\'> ' + 'Which: has a member named "staticField",' + ' but it is not an instance property'); + shouldFail( + c, + hasProperty('staticGetter'), + 'Expected: has property "staticGetter" ' + 'Actual: <Instance of \'C\'> ' + 'Which: has a member named "staticGetter",' + ' but it is not an instance property'); + }); +}
diff --git a/pkgs/matcher/test/never_called_test.dart b/pkgs/matcher/test/never_called_test.dart new file mode 100644 index 0000000..4c83e39 --- /dev/null +++ b/pkgs/matcher/test/never_called_test.dart
@@ -0,0 +1,75 @@ +// 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:term_glyph/term_glyph.dart' as glyph; +import 'package:test/test.dart'; +import 'package:test_api/hooks_testing.dart'; + +import 'utils_new.dart'; + +void main() { + setUpAll(() { + glyph.ascii = true; + }); + + test("doesn't throw if it isn't called", () async { + var monitor = await TestCaseMonitor.run(() { + const Stream.empty().listen(neverCalled); + }); + + expectTestPassed(monitor); + }); + + group("if it's called", () { + test('throws', () async { + var monitor = await TestCaseMonitor.run(() { + neverCalled(); + }); + + expectTestFailed( + monitor, + 'Callback should never have been called, but it was called with no ' + 'arguments.'); + }); + + test('pretty-prints arguments', () async { + var monitor = await TestCaseMonitor.run(() { + neverCalled(1, 'foo\nbar'); + }); + + expectTestFailed( + monitor, + 'Callback should never have been called, but it was called with:\n' + '* <1>\n' + "* 'foo\\n'\n" + " 'bar'"); + }); + + test('keeps the test alive', () async { + var monitor = await TestCaseMonitor.run(() { + pumpEventQueue(times: 10).then(neverCalled); + }); + + expectTestFailed( + monitor, + 'Callback should never have been called, but it was called with:\n' + '* <null>'); + }); + + test("can't be caught", () async { + var monitor = await TestCaseMonitor.run(() { + try { + neverCalled(); + } catch (_) { + // Do nothing. + } + }); + + expectTestFailed( + monitor, + 'Callback should never have been called, but it was called with ' + 'no arguments.'); + }); + }); +}
diff --git a/pkgs/matcher/test/numeric_matchers_test.dart b/pkgs/matcher/test/numeric_matchers_test.dart new file mode 100644 index 0000000..3919588 --- /dev/null +++ b/pkgs/matcher/test/numeric_matchers_test.dart
@@ -0,0 +1,98 @@ +// Copyright (c) 2012, 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'; + +import 'test_utils.dart'; + +void main() { + test('closeTo', () { + shouldPass(0, closeTo(0, 1)); + shouldPass(-1, closeTo(0, 1)); + shouldPass(1, closeTo(0, 1)); + shouldFail( + 1.001, + closeTo(0, 1), + 'Expected: a numeric value within <1> of <0> ' + 'Actual: <1.001> ' + 'Which: differs by <1.001>'); + shouldFail( + -1.001, + closeTo(0, 1), + 'Expected: a numeric value within <1> of <0> ' + 'Actual: <-1.001> ' + 'Which: differs by <1.001>'); + shouldFail( + 'not a num', closeTo(0, 1), endsWith('not an <Instance of \'num\'>')); + }); + + test('inInclusiveRange', () { + shouldFail( + -1, + inInclusiveRange(0, 2), + 'Expected: be in range from 0 (inclusive) to 2 (inclusive) ' + 'Actual: <-1>'); + shouldPass(0, inInclusiveRange(0, 2)); + shouldPass(1, inInclusiveRange(0, 2)); + shouldPass(2, inInclusiveRange(0, 2)); + shouldFail( + 3, + inInclusiveRange(0, 2), + 'Expected: be in range from 0 (inclusive) to 2 (inclusive) ' + 'Actual: <3>'); + shouldFail('not a num', inInclusiveRange(0, 1), + endsWith('not an <Instance of \'num\'>')); + }); + + test('inExclusiveRange', () { + shouldFail( + 0, + inExclusiveRange(0, 2), + 'Expected: be in range from 0 (exclusive) to 2 (exclusive) ' + 'Actual: <0>'); + shouldPass(1, inExclusiveRange(0, 2)); + shouldFail( + 2, + inExclusiveRange(0, 2), + 'Expected: be in range from 0 (exclusive) to 2 (exclusive) ' + 'Actual: <2>'); + shouldFail('not a num', inExclusiveRange(0, 1), + endsWith('not an <Instance of \'num\'>')); + }); + + test('inOpenClosedRange', () { + shouldFail( + 0, + inOpenClosedRange(0, 2), + 'Expected: be in range from 0 (exclusive) to 2 (inclusive) ' + 'Actual: <0>'); + shouldPass(1, inOpenClosedRange(0, 2)); + shouldPass(2, inOpenClosedRange(0, 2)); + shouldFail('not a num', inOpenClosedRange(0, 1), + endsWith('not an <Instance of \'num\'>')); + }); + + test('inClosedOpenRange', () { + shouldPass(0, inClosedOpenRange(0, 2)); + shouldPass(1, inClosedOpenRange(0, 2)); + shouldFail( + 2, + inClosedOpenRange(0, 2), + 'Expected: be in range from 0 (inclusive) to 2 (exclusive) ' + 'Actual: <2>'); + shouldFail('not a num', inClosedOpenRange(0, 1), + endsWith('not an <Instance of \'num\'>')); + }); + + group('NaN', () { + test('inInclusiveRange', () { + shouldFail( + double.nan, + inExclusiveRange(double.negativeInfinity, double.infinity), + 'Expected: be in range from ' + '-Infinity (exclusive) to Infinity (exclusive) ' + 'Actual: <NaN>'); + }); + }); +}
diff --git a/pkgs/matcher/test/operator_matchers_test.dart b/pkgs/matcher/test/operator_matchers_test.dart new file mode 100644 index 0000000..f4b6d3a --- /dev/null +++ b/pkgs/matcher/test/operator_matchers_test.dart
@@ -0,0 +1,63 @@ +// Copyright (c) 2012, 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:matcher/matcher.dart'; +import 'package:test/test.dart' show test, expect, throwsArgumentError; + +import 'test_utils.dart'; + +void main() { + test('anyOf', () { + // with a list + shouldFail( + 0, anyOf([equals(1), equals(2)]), 'Expected: (<1> or <2>) Actual: <0>'); + shouldPass(1, anyOf([equals(1), equals(2)])); + + // with individual items + shouldFail( + 0, anyOf(equals(1), equals(2)), 'Expected: (<1> or <2>) Actual: <0>'); + shouldPass(1, anyOf(equals(1), equals(2))); + }); + + test('allOf', () { + // with a list + shouldPass(1, allOf([lessThan(10), greaterThan(0)])); + shouldFail( + -1, + allOf([lessThan(10), greaterThan(0)]), + 'Expected: (a value less than <10> and a value greater than <0>) ' + 'Actual: <-1> ' + 'Which: is not a value greater than <0>'); + + // with individual items + shouldPass(1, allOf(lessThan(10), greaterThan(0))); + shouldFail( + -1, + allOf(lessThan(10), greaterThan(0)), + 'Expected: (a value less than <10> and a value greater than <0>) ' + 'Actual: <-1> ' + 'Which: is not a value greater than <0>'); + + // with maximum items + shouldPass( + 1, + allOf(lessThan(10), lessThan(9), lessThan(8), lessThan(7), lessThan(6), + lessThan(5), lessThan(4))); + shouldFail( + 4, + allOf(lessThan(10), lessThan(9), lessThan(8), lessThan(7), lessThan(6), + lessThan(5), lessThan(4)), + 'Expected: (a value less than <10> and a value less than <9> and a ' + 'value less than <8> and a value less than <7> and a value less than ' + '<6> and a value less than <5> and a value less than <4>) ' + 'Actual: <4> ' + 'Which: is not a value less than <4>'); + }); + + test('If the first argument is a List, the rest must be null', () { + expect(() => allOf([], 5), throwsArgumentError); + expect( + () => anyOf([], null, null, null, null, null, 42), throwsArgumentError); + }); +}
diff --git a/pkgs/matcher/test/order_matchers_test.dart b/pkgs/matcher/test/order_matchers_test.dart new file mode 100644 index 0000000..8a7c3df --- /dev/null +++ b/pkgs/matcher/test/order_matchers_test.dart
@@ -0,0 +1,149 @@ +// Copyright (c) 2012, 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'; + +import 'test_utils.dart'; + +void main() { + test('greaterThan', () { + shouldPass(10, greaterThan(9)); + shouldFail( + 9, + greaterThan(10), + 'Expected: a value greater than <10> ' + 'Actual: <9> ' + 'Which: is not a value greater than <10>'); + }); + + test('greaterThanOrEqualTo', () { + shouldPass(10, greaterThanOrEqualTo(10)); + shouldFail( + 9, + greaterThanOrEqualTo(10), + 'Expected: a value greater than or equal to <10> ' + 'Actual: <9> ' + 'Which: is not a value greater than or equal to <10>'); + }); + + test('lessThan', () { + shouldFail( + 10, + lessThan(9), + 'Expected: a value less than <9> ' + 'Actual: <10> ' + 'Which: is not a value less than <9>'); + shouldPass(9, lessThan(10)); + }); + + test('lessThanOrEqualTo', () { + shouldPass(10, lessThanOrEqualTo(10)); + shouldFail( + 11, + lessThanOrEqualTo(10), + 'Expected: a value less than or equal to <10> ' + 'Actual: <11> ' + 'Which: is not a value less than or equal to <10>'); + }); + + test('isZero', () { + shouldPass(0, isZero); + shouldFail( + 1, + isZero, + 'Expected: a value equal to <0> ' + 'Actual: <1> ' + 'Which: is not a value equal to <0>'); + }); + + test('isNonZero', () { + shouldFail( + 0, + isNonZero, + 'Expected: a value not equal to <0> ' + 'Actual: <0> ' + 'Which: is not a value not equal to <0>'); + shouldPass(1, isNonZero); + }); + + test('isPositive', () { + shouldFail( + -1, + isPositive, + 'Expected: a positive value ' + 'Actual: <-1> ' + 'Which: is not a positive value'); + shouldFail( + 0, + isPositive, + 'Expected: a positive value ' + 'Actual: <0> ' + 'Which: is not a positive value'); + shouldPass(1, isPositive); + }); + + test('isNegative', () { + shouldPass(-1, isNegative); + shouldFail( + 0, + isNegative, + 'Expected: a negative value ' + 'Actual: <0> ' + 'Which: is not a negative value'); + }); + + test('isNonPositive', () { + shouldPass(-1, isNonPositive); + shouldPass(0, isNonPositive); + shouldFail( + 1, + isNonPositive, + 'Expected: a non-positive value ' + 'Actual: <1> ' + 'Which: is not a non-positive value'); + }); + + test('isNonNegative', () { + shouldPass(1, isNonNegative); + shouldPass(0, isNonNegative); + shouldFail( + -1, + isNonNegative, + 'Expected: a non-negative value ' + 'Actual: <-1> ' + 'Which: is not a non-negative value'); + }); + + group('NaN', () { + test('greaterThan', () { + shouldFail( + double.nan, + greaterThan(10), + 'Expected: a value greater than <10> ' + 'Actual: <NaN> ' + 'Which: is not a value greater than <10>'); + shouldFail( + 10, + greaterThan(double.nan), + 'Expected: a value greater than <NaN> ' + 'Actual: <10> ' + 'Which: is not a value greater than <NaN>'); + }); + + test('lessThanOrEqualTo', () { + shouldFail( + double.nan, + lessThanOrEqualTo(10), + 'Expected: a value less than or equal to <10> ' + 'Actual: <NaN> ' + 'Which: is not a value less than or equal to <10>'); + shouldFail( + 10, + lessThanOrEqualTo(double.nan), + 'Expected: a value less than or equal to <NaN> ' + 'Actual: <10> ' + 'Which: is not a value less than or equal to <NaN>'); + }); + }); +}
diff --git a/pkgs/matcher/test/pretty_print_test.dart b/pkgs/matcher/test/pretty_print_test.dart new file mode 100644 index 0000000..184704b --- /dev/null +++ b/pkgs/matcher/test/pretty_print_test.dart
@@ -0,0 +1,263 @@ +// 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 'dart:collection'; + +import 'package:matcher/matcher.dart'; +import 'package:matcher/src/pretty_print.dart'; +import 'package:test/test.dart' show group, test, expect; + +class DefaultToString {} + +class CustomToString { + @override + String toString() => 'string representation'; +} + +class _PrivateName { + @override + String toString() => 'string representation'; +} + +class _PrivateNameIterable extends IterableMixin { + @override + Iterator get iterator => [1, 2, 3].iterator; +} + +void main() { + test('with primitive objects', () { + expect(prettyPrint(12), equals('<12>')); + expect(prettyPrint(12.13), equals('<12.13>')); + expect(prettyPrint(true), equals('<true>')); + expect(prettyPrint(null), equals('<null>')); + expect(prettyPrint(() => 12), matches(r'<Closure.*>')); + }); + + group('with a string', () { + test('containing simple characters', () { + expect(prettyPrint('foo'), equals("'foo'")); + }); + + test('containing newlines', () { + expect( + prettyPrint('foo\nbar\nbaz'), + equals("'foo\\n'\n" + " 'bar\\n'\n" + " 'baz'")); + }); + + test('containing escapable characters', () { + expect(prettyPrint("foo\rbar\tbaz'qux\v"), + equals(r"'foo\rbar\tbaz\'qux\v'")); + }); + }); + + group('with an iterable', () { + test('containing primitive objects', () { + expect(prettyPrint([1, true, 'foo']), equals("[1, true, 'foo']")); + }); + + test('containing a multiline string', () { + expect( + prettyPrint(['foo', 'bar\nbaz\nbip', 'qux']), + equals('[\n' + " 'foo',\n" + " 'bar\\n'\n" + " 'baz\\n'\n" + " 'bip',\n" + " 'qux'\n" + ']')); + }); + + test('containing a matcher', () { + expect(prettyPrint(['foo', endsWith('qux')]), + equals("['foo', <a string ending with 'qux'>]")); + }); + + test("that's under maxLineLength", () { + expect(prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxLineLength: 30), + equals('[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]')); + }); + + test("that's over maxLineLength", () { + expect( + prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxLineLength: 29), + equals('[\n' + ' 0,\n' + ' 1,\n' + ' 2,\n' + ' 3,\n' + ' 4,\n' + ' 5,\n' + ' 6,\n' + ' 7,\n' + ' 8,\n' + ' 9\n' + ']')); + }); + + test('factors indentation into maxLineLength', () { + expect( + prettyPrint([ + 'foo\nbar', + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + ], maxLineLength: 30), + equals('[\n' + " 'foo\\n'\n" + " 'bar',\n" + ' [\n' + ' 0,\n' + ' 1,\n' + ' 2,\n' + ' 3,\n' + ' 4,\n' + ' 5,\n' + ' 6,\n' + ' 7,\n' + ' 8,\n' + ' 9\n' + ' ]\n' + ']')); + }); + + test("that's under maxItems", () { + expect(prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxItems: 10), + equals('[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]')); + }); + + test("that's over maxItems", () { + expect(prettyPrint([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], maxItems: 9), + equals('[0, 1, 2, 3, 4, 5, 6, 7, ...]')); + }); + + test("that's recursive", () { + var list = <dynamic>[1, 2, 3]; + list.add(list); + expect(prettyPrint(list), equals('[1, 2, 3, (recursive)]')); + }); + }); + + group('with a map', () { + test('containing primitive objects', () { + expect(prettyPrint({'foo': 1, 'bar': true}), + equals("{'foo': 1, 'bar': true}")); + }); + + test('containing a multiline string key', () { + expect( + prettyPrint({'foo\nbar': 1, 'bar': true}), + equals('{\n' + " 'foo\\n'\n" + " 'bar': 1,\n" + " 'bar': true\n" + '}')); + }); + + test('containing a multiline string value', () { + expect( + prettyPrint({'foo': 'bar\nbaz', 'qux': true}), + equals('{\n' + " 'foo': 'bar\\n'\n" + " 'baz',\n" + " 'qux': true\n" + '}')); + }); + + test('containing a multiline string key/value pair', () { + expect( + prettyPrint({'foo\nbar': 'baz\nqux'}), + equals('{\n' + " 'foo\\n'\n" + " 'bar': 'baz\\n'\n" + " 'qux'\n" + '}')); + }); + + test('containing a matcher key', () { + expect(prettyPrint({endsWith('bar'): 'qux'}), + equals("{<a string ending with 'bar'>: 'qux'}")); + }); + + test('containing a matcher value', () { + expect(prettyPrint({'foo': endsWith('qux')}), + equals("{'foo': <a string ending with 'qux'>}")); + }); + + test("that's under maxLineLength", () { + expect(prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxLineLength: 32), + equals("{'0': 1, '2': 3, '4': 5, '6': 7}")); + }); + + test("that's over maxLineLength", () { + expect( + prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxLineLength: 31), + equals('{\n' + " '0': 1,\n" + " '2': 3,\n" + " '4': 5,\n" + " '6': 7\n" + '}')); + }); + + test('factors indentation into maxLineLength', () { + expect( + prettyPrint([ + 'foo\nbar', + {'0': 1, '2': 3, '4': 5, '6': 7} + ], maxLineLength: 32), + equals('[\n' + " 'foo\\n'\n" + " 'bar',\n" + ' {\n' + " '0': 1,\n" + " '2': 3,\n" + " '4': 5,\n" + " '6': 7\n" + ' }\n' + ']')); + }); + + test("that's under maxItems", () { + expect(prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxItems: 4), + equals("{'0': 1, '2': 3, '4': 5, '6': 7}")); + }); + + test("that's over maxItems", () { + expect(prettyPrint({'0': 1, '2': 3, '4': 5, '6': 7}, maxItems: 3), + equals("{'0': 1, '2': 3, ...}")); + }); + }); + group('with an object', () { + test('with a default [toString]', () { + expect(prettyPrint(DefaultToString()), + equals("<Instance of 'DefaultToString'>")); + }); + + test('with a custom [toString]', () { + expect(prettyPrint(CustomToString()), + equals('CustomToString:<string representation>')); + }); + + test('with a custom [toString] and a private name', () { + expect(prettyPrint(_PrivateName()), + equals('_PrivateName:<string representation>')); + }); + }); + + group('with an iterable', () { + test("that's not a list", () { + expect(prettyPrint([1, 2, 3, 4].map((n) => n * 2)), + equals('MappedListIterable<int, int>:[2, 4, 6, 8]')); + }); + + test("that's not a list and has a private name", () { + expect(prettyPrint(_PrivateNameIterable()), + equals('_PrivateNameIterable:[1, 2, 3]')); + }); + }); + + test('Type', () { + expect(prettyPrint(''.runtimeType), 'Type:<String>'); + }); +}
diff --git a/pkgs/matcher/test/stream_matcher_test.dart b/pkgs/matcher/test/stream_matcher_test.dart new file mode 100644 index 0000000..c4af666 --- /dev/null +++ b/pkgs/matcher/test/stream_matcher_test.dart
@@ -0,0 +1,358 @@ +// 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 'package:async/async.dart'; +import 'package:term_glyph/term_glyph.dart' as glyph; +import 'package:test/test.dart'; + +import 'utils_new.dart'; + +void main() { + setUpAll(() { + glyph.ascii = true; + }); + + late Stream stream; + late StreamQueue queue; + late Stream errorStream; + late StreamQueue errorQueue; + setUp(() { + stream = Stream.fromIterable([1, 2, 3, 4, 5]); + queue = StreamQueue(Stream.fromIterable([1, 2, 3, 4, 5])); + errorStream = Stream.fromFuture(Future.error('oh no!', StackTrace.current)); + errorQueue = StreamQueue( + Stream.fromFuture(Future.error('oh no!', StackTrace.current))); + }); + + group('emits()', () { + test('matches the first event of a Stream', () { + expect(stream, emits(1)); + }); + + test('rejects the first event of a Stream', () { + expect( + expectLater(stream, emits(2)), + throwsTestFailure(allOf([ + startsWith('Expected: should emit an event that <2>\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n') + ]))); + }); + + test('matches and consumes the next event of a StreamQueue', () { + expect(queue, emits(1)); + expect(queue.next, completion(equals(2))); + expect(queue, emits(3)); + expect(queue.next, completion(equals(4))); + }); + + test('rejects and does not consume the first event of a StreamQueue', () { + expect( + expectLater(queue, emits(2)), + throwsTestFailure(allOf([ + startsWith('Expected: should emit an event that <2>\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n') + ]))); + + expect(queue, emits(1)); + }); + + test('rejects an empty stream', () { + expect( + expectLater(const Stream.empty(), emits(1)), + throwsTestFailure(allOf([ + startsWith('Expected: should emit an event that <1>\n'), + endsWith(' Which: emitted x Stream closed.\n') + ]))); + }); + + test('forwards a stream error', () { + expect(expectLater(errorStream, emits(1)), throwsA('oh no!')); + }); + + test('wraps a normal matcher', () { + expect(queue, emits(lessThan(5))); + expect(expectLater(queue, emits(greaterThan(5))), + throwsTestFailure(anything)); + }); + + test('returns a StreamMatcher as-is', () { + expect(queue, emits(emitsThrough(4))); + expect(queue, emits(5)); + }); + }); + + group('emitsDone', () { + test('succeeds for an empty stream', () { + expect(const Stream.empty(), emitsDone); + }); + + test('fails for a stream with events', () { + expect( + expectLater(stream, emitsDone), + throwsTestFailure(allOf([ + startsWith('Expected: should be done\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n') + ]))); + }); + }); + + group('emitsError()', () { + test('consumes a matching error', () { + expect(errorQueue, emitsError('oh no!')); + expect(errorQueue.hasNext, completion(isFalse)); + }); + + test('fails for a non-matching error', () { + expect( + expectLater(errorStream, emitsError('oh heck')), + throwsTestFailure(allOf([ + startsWith("Expected: should emit an error that 'oh heck'\n"), + contains(' Which: emitted ! oh no!\n'), + contains(' x Stream closed.\n' + " which threw 'oh no!'\n" + ' stack '), + endsWith(' which is different.\n' + ' Expected: oh heck\n' + ' Actual: oh no!\n' + ' ^\n' + ' Differ at offset 3\n') + ]))); + }); + + test('fails for a stream with events', () { + expect( + expectLater(stream, emitsDone), + throwsTestFailure(allOf([ + startsWith('Expected: should be done\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n') + ]))); + }); + }); + + group('mayEmit()', () { + test('consumes a matching event', () { + expect(queue, mayEmit(1)); + expect(queue, emits(2)); + }); + + test('allows a non-matching event', () { + expect(queue, mayEmit('fish')); + expect(queue, emits(1)); + }); + }); + + group('emitsAnyOf()', () { + test('consumes an event that matches a matcher', () { + expect(queue, emitsAnyOf([2, 1, 3])); + expect(queue, emits(2)); + }); + + test('consumes as many events as possible', () { + expect( + queue, + emitsAnyOf([ + 1, + emitsInOrder([1, 2]), + emitsInOrder([1, 2, 3]) + ])); + + expect(queue, emits(4)); + }); + + test('fails if no matchers match', () { + expect( + expectLater(stream, emitsAnyOf([2, 3, 4])), + throwsTestFailure(allOf([ + startsWith('Expected: should do one of the following:\n' + ' * emit an event that <2>\n' + ' * emit an event that <3>\n' + ' * emit an event that <4>\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n' + ' which failed all options:\n' + ' * failed to emit an event that <2>\n' + ' * failed to emit an event that <3>\n' + ' * failed to emit an event that <4>\n') + ]))); + }); + + test('allows an error if any matcher matches', () { + expect(errorStream, emitsAnyOf([1, 2, emitsError('oh no!')])); + }); + + test('rethrows an error if no matcher matches', () { + expect( + expectLater(errorStream, emitsAnyOf([1, 2, 3])), throwsA('oh no!')); + }); + }); + + group('emitsInOrder()', () { + test('consumes matching events', () { + expect(queue, emitsInOrder([1, 2, emitsThrough(4)])); + expect(queue, emits(5)); + }); + + test("fails if the matchers don't match in order", () { + expect( + expectLater(queue, emitsInOrder([1, 3, 2])), + throwsTestFailure(allOf([ + startsWith('Expected: should do the following in order:\n' + ' * emit an event that <1>\n' + ' * emit an event that <3>\n' + ' * emit an event that <2>\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n' + " which didn't emit an event that <3>\n") + ]))); + }); + }); + + group('emitsThrough()', () { + test('consumes events including those matching the matcher', () { + expect(queue, emitsThrough(emitsInOrder([3, 4]))); + expect(queue, emits(5)); + }); + + test('consumes the entire queue with emitsDone', () { + expect(queue, emitsThrough(emitsDone)); + expect(queue.hasNext, completion(isFalse)); + }); + + test('fails if the queue never matches the matcher', () { + expect( + expectLater(queue, emitsThrough(6)), + throwsTestFailure(allOf([ + startsWith('Expected: should eventually emit an event that <6>\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n' + ' which never did emit an event that <6>\n') + ]))); + }); + }); + + group('mayEmitMultiple()', () { + test('consumes multiple instances of the given matcher', () { + expect(queue, mayEmitMultiple(lessThan(3))); + expect(queue, emits(3)); + }); + + test('consumes zero instances of the given matcher', () { + expect(queue, mayEmitMultiple(6)); + expect(queue, emits(1)); + }); + + test("doesn't rethrow errors", () { + expect(errorQueue, mayEmitMultiple(1)); + expect(errorQueue, emitsError('oh no!')); + }); + }); + + group('neverEmits()', () { + test('succeeds if the event never matches', () { + expect(queue, neverEmits(6)); + expect(queue, emits(1)); + }); + + test('fails if the event matches', () { + expect( + expectLater(stream, neverEmits(4)), + throwsTestFailure(allOf([ + startsWith('Expected: should never emit an event that <4>\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n' + ' which after 3 events did emit an event that <4>\n') + ]))); + }); + + test('fails if emitsDone matches', () { + expect(expectLater(stream, neverEmits(emitsDone)), + throwsTestFailure(anything)); + }); + + test("doesn't rethrow errors", () { + expect(errorQueue, neverEmits(6)); + expect(errorQueue, emitsError('oh no!')); + }); + }); + + group('emitsInAnyOrder()', () { + test('consumes events that match in any order', () { + expect(queue, emitsInAnyOrder([3, 1, 2])); + expect(queue, emits(4)); + }); + + test("fails if the events don't match in any order", () { + expect( + expectLater(stream, emitsInAnyOrder([4, 1, 2])), + throwsTestFailure(allOf([ + startsWith('Expected: should do the following in any order:\n' + ' * emit an event that <4>\n' + ' * emit an event that <1>\n' + ' * emit an event that <2>\n'), + endsWith(' Which: emitted * 1\n' + ' * 2\n' + ' * 3\n' + ' * 4\n' + ' * 5\n' + ' x Stream closed.\n') + ]))); + }); + + test("doesn't rethrow if some ordering matches", () { + expect(errorQueue, emitsInAnyOrder([emitsDone, emitsError('oh no!')])); + }); + + test('rethrows if no ordering matches', () { + expect( + expectLater(errorQueue, emitsInAnyOrder([1, emitsError('oh no!')])), + throwsA('oh no!')); + }); + }); + + test('A custom StreamController doesn\'t hang on close', () async { + var controller = StreamController<void>(); + var done = expectLater(controller.stream, emits(null)); + controller.add(null); + await done; + await controller.close(); + }); +}
diff --git a/pkgs/matcher/test/string_matchers_test.dart b/pkgs/matcher/test/string_matchers_test.dart new file mode 100644 index 0000000..be9e768 --- /dev/null +++ b/pkgs/matcher/test/string_matchers_test.dart
@@ -0,0 +1,151 @@ +// Copyright (c) 2012, 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:matcher/matcher.dart'; +import 'package:test/test.dart' show test, expect; + +import 'test_utils.dart'; + +void main() { + test('Reports mismatches in whitespace and escape sequences', () { + shouldFail('before\nafter', equals('before\\nafter'), + contains('Differ at offset 7')); + }); + + test('Retains outer matcher mismatch text', () { + shouldFail( + {'word': 'thing'}, + containsPair('word', equals('notthing')), + allOf([ + contains("contains key 'word' but with value is different"), + contains('Differ at offset 0') + ])); + }); + + test('collapseWhitespace', () { + var source = '\t\r\n hello\t\r\n world\r\t \n'; + expect(collapseWhitespace(source), 'hello world'); + }); + + test('isEmpty', () { + shouldPass('', isEmpty); + shouldFail(null, isEmpty, startsWith('Expected: empty Actual: <null>')); + shouldFail(0, isEmpty, startsWith('Expected: empty Actual: <0>')); + shouldFail('a', isEmpty, startsWith("Expected: empty Actual: 'a'")); + }); + + // Regression test for: https://code.google.com/p/dart/issues/detail?id=21562 + test('isNot(isEmpty)', () { + shouldPass('a', isNot(isEmpty)); + shouldFail('', isNot(isEmpty), 'Expected: not empty Actual: \'\''); + shouldFail(null, isNot(isEmpty), + startsWith('Expected: not empty Actual: <null>')); + }); + + test('isNotEmpty', () { + shouldFail('', isNotEmpty, startsWith("Expected: non-empty Actual: ''")); + shouldFail( + null, isNotEmpty, startsWith('Expected: non-empty Actual: <null>')); + shouldFail(0, isNotEmpty, startsWith('Expected: non-empty Actual: <0>')); + shouldPass('a', isNotEmpty); + }); + + test('equalsIgnoringCase', () { + shouldPass('hello', equalsIgnoringCase('HELLO')); + shouldFail('hi', equalsIgnoringCase('HELLO'), + "Expected: 'HELLO' ignoring case Actual: 'hi'"); + shouldFail(42, equalsIgnoringCase('HELLO'), + endsWith('not an <Instance of \'String\'>')); + }); + + test('equalsIgnoringWhitespace', () { + shouldPass(' hello world ', equalsIgnoringWhitespace('hello world')); + shouldFail( + ' helloworld ', + equalsIgnoringWhitespace('hello world'), + "Expected: 'hello world' ignoring whitespace " + "Actual: ' helloworld ' " + "Which: is 'helloworld' with whitespace compressed"); + shouldFail(42, equalsIgnoringWhitespace('HELLO'), + endsWith('not an <Instance of \'String\'>')); + }); + + test('startsWith', () { + shouldPass('hello', startsWith('')); + shouldPass('hello', startsWith('hell')); + shouldPass('hello', startsWith('hello')); + shouldFail( + 'hello', + startsWith('hello '), + "Expected: a string starting with 'hello ' " + "Actual: 'hello'"); + shouldFail( + 42, startsWith('hello '), endsWith('not an <Instance of \'String\'>')); + }); + + test('endsWith', () { + shouldPass('hello', endsWith('')); + shouldPass('hello', endsWith('lo')); + shouldPass('hello', endsWith('hello')); + shouldFail( + 'hello', + endsWith(' hello'), + "Expected: a string ending with ' hello' " + "Actual: 'hello'"); + shouldFail( + 42, startsWith('hello '), endsWith('not an <Instance of \'String\'>')); + }); + + test('contains', () { + shouldPass('hello', contains('')); + shouldPass('hello', contains('h')); + shouldPass('hello', contains('o')); + shouldPass('hello', contains('hell')); + shouldPass('hello', contains('hello')); + shouldFail('hello', contains(' '), + "Expected: contains ' ' Actual: 'hello' Which: does not contain ' '"); + }); + + test('stringContainsInOrder', () { + shouldPass('goodbye cruel world', stringContainsInOrder([''])); + shouldPass('goodbye cruel world', stringContainsInOrder(['goodbye'])); + shouldPass('goodbye cruel world', stringContainsInOrder(['cruel'])); + shouldPass('goodbye cruel world', stringContainsInOrder(['world'])); + shouldPass( + 'goodbye cruel world', stringContainsInOrder(['good', 'bye', 'world'])); + shouldPass( + 'goodbye cruel world', stringContainsInOrder(['goodbye', 'cruel'])); + shouldPass( + 'goodbye cruel world', stringContainsInOrder(['cruel', 'world'])); + shouldPass('goodbye cruel world', + stringContainsInOrder(['goodbye', 'cruel', 'world'])); + shouldPass( + 'foo', stringContainsInOrder(['f', '', '', '', 'o', '', '', 'o'])); + + shouldFail( + 'abc', + stringContainsInOrder(['ab', 'bc']), + "Expected: a string containing 'ab', 'bc' in order " + "Actual: 'abc'"); + shouldFail( + 'hello', + stringContainsInOrder(['hello', 'hello']), + "Expected: a string containing 'hello', 'hello' in order " + "Actual: 'hello'"); + shouldFail( + 'goodbye cruel world', + stringContainsInOrder(['goo', 'cruel', 'bye']), + "Expected: a string containing 'goo', 'cruel', 'bye' in order " + "Actual: 'goodbye cruel world'"); + }); + + test('matches', () { + shouldPass('c0d', matches('[a-z][0-9][a-z]')); + shouldPass('c0d', matches(RegExp('[a-z][0-9][a-z]'))); + shouldFail('cOd', matches('[a-z][0-9][a-z]'), + "Expected: match '[a-z][0-9][a-z]' Actual: 'cOd'"); + shouldFail(42, matches('[a-z][0-9][a-z]'), + endsWith('not an <Instance of \'String\'>')); + }); +}
diff --git a/pkgs/matcher/test/test_utils.dart b/pkgs/matcher/test/test_utils.dart new file mode 100644 index 0000000..67b61a1 --- /dev/null +++ b/pkgs/matcher/test/test_utils.dart
@@ -0,0 +1,67 @@ +// Copyright (c) 2012, 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'; + +void shouldFail(Object? value, Matcher matcher, Object? expected) { + var failed = false; + try { + expect(value, matcher); + } on TestFailure catch (err) { + failed = true; + + var errorString = err.message; + + if (expected is String) { + expect(errorString, equalsIgnoringWhitespace(expected)); + } else { + expect(errorString?.replaceAll('\n', ''), expected); + } + } + + expect(failed, isTrue, reason: 'Expected to fail.'); +} + +void shouldPass(Object? value, Matcher matcher) { + expect(value, matcher); +} + +void doesNotThrow() {} +void doesThrow() { + throw StateError('X'); +} + +class Widget { + int? price; +} + +class SimpleIterable extends Iterable<int> { + final int count; + + SimpleIterable(this.count); + + @override + Iterator<int> get iterator => _SimpleIterator(count); +} + +class _SimpleIterator implements Iterator<int> { + int _count; + int _current; + + _SimpleIterator(this._count) : _current = -1; + + @override + bool moveNext() { + if (_count > 0) { + _current = _count; + _count--; + return true; + } + _current = -1; + return false; + } + + @override + int get current => _current; +}
diff --git a/pkgs/matcher/test/type_matcher_test.dart b/pkgs/matcher/test/type_matcher_test.dart new file mode 100644 index 0000000..99d4459 --- /dev/null +++ b/pkgs/matcher/test/type_matcher_test.dart
@@ -0,0 +1,66 @@ +// Copyright (c) 2012, 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. + +// ignore_for_file: deprecated_member_use_from_same_package +import 'package:matcher/matcher.dart'; +import 'package:test/test.dart' show test, group; + +import 'test_utils.dart'; + +void main() { + _test(isMap, {}, name: 'Map'); + _test(isList, [], name: 'List'); + _test(isArgumentError, ArgumentError()); + _test(isCastError, TypeError()); + _test<Exception>(isException, const FormatException()); + _test(isFormatException, const FormatException()); + _test(isStateError, StateError('oops')); + _test(isRangeError, RangeError('oops')); + _test(isUnimplementedError, UnimplementedError('oops')); + _test(isUnsupportedError, UnsupportedError('oops')); + _test(isConcurrentModificationError, ConcurrentModificationError()); + _test(isCyclicInitializationError, Error()); + _test<NoSuchMethodError?>(isNoSuchMethodError, null, + name: 'NoSuchMethodError'); + _test(isNullThrownError, TypeError()); + + group('custom `TypeMatcher`', () { + _test(const isInstanceOf<String>(), 'hello'); + _test(const _StringMatcher(), 'hello'); + _test(const TypeMatcher<String>(), 'hello'); + _test(isA<String>(), 'hello'); + }); +} + +void _test<T>(Matcher typeMatcher, T matchingInstance, {String? name}) { + name ??= T.toString(); + group('for `$name`', () { + if (matchingInstance != null) { + test('succeeds', () { + shouldPass(matchingInstance, typeMatcher); + }); + } + + test('fails', () { + shouldFail( + const _TestType(), + typeMatcher, + "Expected: <Instance of '$name'> Actual: <Instance of '_TestType'>" + " Which: is not an instance of '$name'", + ); + }); + }); +} + +// Validate that existing implementations continue to work. +class _StringMatcher extends TypeMatcher { + const _StringMatcher() : super('String'); + + @override + bool matches(dynamic item, Map matchState) => item is String; +} + +class _TestType { + const _TestType(); +}
diff --git a/pkgs/matcher/test/utils_new.dart b/pkgs/matcher/test/utils_new.dart new file mode 100644 index 0000000..7d85a02 --- /dev/null +++ b/pkgs/matcher/test/utils_new.dart
@@ -0,0 +1,49 @@ +// Copyright (c) 2023, 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 'package:matcher/expect.dart'; +import 'package:test_api/hooks_testing.dart'; + +/// Asserts that [monitor] has completed and passed. +/// +/// If the test had any errors, they're surfaced nicely into the outer test. +void expectTestPassed(TestCaseMonitor monitor) { + // Since the test is expected to pass, we forward any current or future errors + // to the running test, because they're definitely unexpected and it is most + // useful for the error to point directly to the throw point. + for (var error in monitor.errors) { + Zone.current.handleUncaughtError(error.error, error.stackTrace); + } + monitor.onError.listen((error) { + Zone.current.handleUncaughtError(error.error, error.stackTrace); + }); + + expect(monitor.state, State.passed); +} + +/// Asserts that [monitor] failed with a single [TestFailure] whose message +/// matches [message]. +void expectTestFailed(TestCaseMonitor monitor, Object? message) { + expect(monitor.state, State.failed); + expect(monitor.errors, [isAsyncError(isTestFailure(message))]); +} + +/// Returns a matcher that matches a [AsyncError] with an `error` field matching +/// [errorMatcher]. +Matcher isAsyncError(Matcher errorMatcher) => + isA<AsyncError>().having((e) => e.error, 'error', errorMatcher); + +/// Returns a matcher that matches a [TestFailure] with the given [message]. +/// +/// [message] can be a string or a [Matcher]. +Matcher isTestFailure(Object? message) => const TypeMatcher<TestFailure>() + .having((e) => e.message, 'message', message); + +/// Returns a matcher that matches a callback or Future that throws a +/// [TestFailure] with the given [message]. +/// +/// [message] can be a string or a [Matcher]. +Matcher throwsTestFailure(Object? message) => throwsA(isTestFailure(message));
diff --git a/pkgs/test/.test_config b/pkgs/test/.test_config new file mode 100644 index 0000000..412fc5c --- /dev/null +++ b/pkgs/test/.test_config
@@ -0,0 +1,3 @@ +{ + "test_package": true +} \ No newline at end of file
diff --git a/pkgs/test/CHANGELOG.md b/pkgs/test/CHANGELOG.md new file mode 100644 index 0000000..afe8dec --- /dev/null +++ b/pkgs/test/CHANGELOG.md
@@ -0,0 +1,1836 @@ +## 1.25.12 + +* Fix hang when running multiple precompiled browser tests. + +## 1.25.11 + +* Update to be forward compatible with `package:shelf_web_socket` version `3.x`. + +## 1.25.10 + +* Update the `package:vm_service` constraint to allow version `15.x`. + +## 1.25.9 + +* Allow `analyzer: '>=6.0.0 <8.0.0'` +* Fix dart2wasm tests on windows. +* Increase SDK constraint to ^3.5.0. +* Support running Node.js tests compiled with dart2wasm. +* Allow `firefox` or `firefox-bin` executable name on macOS. + +## 1.25.8 + +* Increase SDK constraint to ^3.4.0. + +## 1.25.7 + +* Enable asserts for `dart2wasm` tests. + +## 1.25.6 + +* Point API doc links to `package:test` canonical libraries. +* Fix testing with `dart2wasm` - use `dart compile wasm` instead of depending on + SDK internals +* Update min SDK constraint to 3.2.0. + +## 1.25.5 + +* Update the `package:web_socket_channel` version constraint to allow `3.x`. +* Update the `package:shelf_web_socket` version constraint to allow `2.x`. + +## 1.25.4 + +* Add `@doNotSubmit` to more declarations of the `solo` parameter. + +## 1.25.3 + +* Remove outdated StreamMatcher link from README table of contents. +* Document the silent reporter in CLI help output. +* Support enabling experiments with the dart2wasm compiler. +* Added [`@doNotSubmit`](https://pub.dev/documentation/meta/latest/meta/doNotSubmit-constant.html) to `test(solo: ...)` and `group(solo: ...)`. In + practice, this means that code that was relying on ignoring deprecation + warnings and using `solo` or `group` with a `skip` parameter will now fail if + `dart analyze --fatal-infos` (or similar) is enabled. + +## 1.25.2 + +* Fix a bug running browser tests with paths containing windows directory + separator follow by a character which is an invalid Dart string escape + sequence. + +## 1.25.1 + +* Fix a bug where in precompiled mode, html files for tests were no longer + created. +* Support the latest version of `package:js`. +* Document the silent reporter in CLI help output. + +## 1.25.0 + +* Handle paths with leading `/` when spawning test isolates. +* Add support for the `dart2wasm` compiler in chrome and firefox. +* **BREAKING**: Remove the `experimental-chrome-wasm` platform, you can now use + `-p chrome -c dart2wasm` instead. + * Note that this has always been advertised as a change that would happen in a + future non-breaking release. +* **BREAKING**:Dropped support for `--pub-serve` which has long not been tested + or supported. + * We do not anticipate much if any actual breakage or existing usage of this + feature, which is why we are making this change in a non-breaking release. + * If you do require this feature, file an issue and we can look at adding it + back. +* **BREAKING**: Fully remove support for Internet Explorer. +* Fix running of tests defined under `lib/` with relative imports to other + libraries in the package. + +## 1.24.9 + +* Update the vm_service constraint to allow version `13.x`. + +## 1.24.8 + +* Remove spurious deprecation during autocomplete for `setUp` and `tearDown`. + +## 1.24.7 + +* Simplify the initialization of the per-suite message channel within browser + tests. See https://github.com/dart-lang/test/issues/2065 +* Add a timeout to browser test suite loads. +* Fix running of browser tests that use deferred loaded libraries. + +## 1.24.6 + +* Fix communication failures between minified test apps and the non-minified + host app. +* Add support for discontinuing after the first failing test with `--fail-fast`. + +## 1.24.5 + +* Change `compiling <path>` to `loading <path>` message in all cases. Surface + the "loading" messages in the situations where previously only the + "compiling" message would be used. +* Support browser tests where the frame creates the message channel. + +## 1.24.4 + +* Drop support for null unsafe Dart, bump SDK constraint to `3.0.0`. +* Make some annotation classes `final`: `OnPlatform`, `Retry`, `Skip`, `Tags`, + `TestOn`, `Timeout`. +* Fix the `root_` fields in the JSON reporter when running a test on Windows + with an absolute path. +* Add support for `SAFARI_EXECUTABLE`, `FIREFOX_EXECUTABLE` and + `MS_EDGE_EXECUTABLE` for custom browser installations. +* Allow the latest analyzer (6.x.x). +* Add `MOZ_AUTOMATION=1` environmental variable to Firefox runner, to make + launcher process on Windows wait for browser exit. + +## 1.24.3 + +* Fix compatibility with wasm number semantics. + +## 1.24.2 + +* Copy an existing nonce from a script on the test HTML page to the script + created by the test runner host javascript. This only impacts environments + testing with custom HTML that includes a nonce. +* Support the Microsoft Edge browser (use the `edge` platform in your test + configuration file or `-p edge` on the command line). + +## 1.24.1 + +* Handle a missing `'compiler'` value when running a test compiled against a + newer `test_api` than the runner back end is using. The expectation was that + the json protocol is only used across packages compatible with the same major + version of the `test_api` package, but `flutter test` does not check the + version of packages in the pub solve for user test code. + +## 1.24.0 + +* Support the `--compiler` flag, which can be used to configure which compiler + to use. + * To specify a compiler by platform, the argument supports platform selectors + through this syntax `[<platform>:]<compiler>`. For example the command line + argument `--compiler vm:source` would run all vm tests from source instead + of compiling to kernel first. + * If no given compiler is compatible for a platform, it will use its default + compiler instead. +* Add support for running tests as native executables (vm platform only). + * You can run tests this way with `--compiler exe`. +* Support compiler identifiers in platform selectors. +* List the supported compilers for each platform in the usage text. +* Update all reporters to print the compiler along with the platform name + when configured to print the platform. Extend the logic for printing platofrm + information to do so if any compilers are explicitly configured. +* Deprecate `--use-data-isolate-strategy`. It is now an alias for `-c vm:source` + which is roughly equivalent. If this is breaking for you please file an issue. + +## 1.23.1 + +* Fix running paths by absolute path (with drive letter) on windows. + +## 1.23.0 + +* Avoid empty expandable groups for tests without extra output in Github + reporter. +* Add support for CHROME_EXECUTABLE environment variable. This overrides any + config file settings. +* Support running tests by absolute file uri. + +## 1.22.2 + +* Don't run `tearDown` until the test body and outstanding work is complete, + even if the test has already failed. +* Change URL secrets for browser tests to always be alphanumeric characters. + +## 1.22.1 + +* Add documentation for the `--ignore-timeouts` argument. +* Merge command lines args repeating the same test path to run the suite one + time with all the test cases across the different arguments. +* Fix VM tests which run after some test has changed the working directory. + There are still issues with browser tests after changing directory. +* Deprecate `throwsNullThrownError`, use `throwsA(isA<TypeError>())` instead. The + implementation has been changed to ease migrations. +* Deprecate `throwsCyclicInitializationError` and replace the implementation + with `Throws(TypeMatcher<Error>())`. The specific exception no longer exists + and there is no guarantee about what type of error will be thrown. + +## 1.22.0 + +* Fix an issue with the github reporter where tests that fail asynchronously + after they've completed would show up as succeeded tests. +* Add the `experimental-chrome-wasm` platform. This is very unstable and will + eventually be deleted, to be replaced by a `--compiler` flag. See + https://github.com/dart-lang/test/issues/1776 for more information on future + plans. + +## 1.21.7 + +* Support `package:matcher` version `0.12.13`. + +## 1.21.6 + +* Require Dart >= 2.18.0 +* Fix the coverage usage example in the README.md +* Support the latest `package:test_api` and `package:test_core`. + +## 1.21.5 + +* Fix `printOnFailure` output to be associated with the correct test. +* Migrate all dom interactions to static interop. + +## 1.21.4 + +* Make the labels for test loading more readable in the compact and expanded + reporters, use gray instead of black. +* Print a command to re-run the failed test after each failure in the compact + reporter. +* Fix the package config path used when running pre-compiled vm tests. + +## 1.21.3 + +* Support the latest `package:test_api` and `package:test_core`. + +## 1.21.2 + +* Add `Target` to restrict `TestOn` annotation to library level. +* Update the github reporter to output the platform in the test names when + multiple platforms are used. +* Fix `spawnHybridUri` support for `package:` uris. + +## 1.21.1 + +* Fix a bug loading JS sources with non-utf8 content while parsing coverage + information from chrome. + +## 1.21.0 + +* Allow analyzer version `4.x`. +* Add a `github` reporter option for use with GitHub Actions. +* Make the `github` test reporter the default when we detect we're running on + GitHub Actions. + +## 1.20.2 + +* Drop `dart2js-path` command line argument. +* Allow loading tests under a path with the directory named `packages`. +* Add retry for launching browsers. Reduce timeout back to 30 seconds. + +## 1.20.1 + +* Allow the latest `vm_service` package. + +## 1.20.0 + +* Update `analyzer` constraint to `>=2.0.0 <4.0.0`. +* Add an `--ignore-timeouts` command line flag, which disables all timeouts + for all tests. This can be useful when debugging, so tests don't time out + during debug sessions. +* Create a trusted types policy when available for assigning the script URL for + web tests. + +## 1.19.5 + +* Try to get more logging from `chrome` on windows to diagnose intermittent + failures. + +## 1.19.4 + +* Wait for paused VM platform isolates before shutdown. +* `TestFailure` implements `Exception` for compatibility with + `only_throw_exceptions`. + +## 1.19.3 + +* Remove duplicate logging of suggestion to enable the `chain-stack-traces` + flag, a single log will now appear at the end. + +## 1.19.2 + +* Republish with missing JS file for browser tests. + +## 1.19.1 + +* Fix parsing of file paths into a URI on windows. + +## 1.19.0 + +* Support query parameters `name`, `full-name`, `line`, and `col` on test paths, + which will apply the filters to only those test suites. + * All specified filters must match for a test to run. + * Global filters (ie: `--name`) are also still respected and must match. + * The `line` and `col` will match if any frame from the test trace matches + (the test trace is the current stack trace where `test` is invoked). +* Give a better exception when using `markTestSkipped` outside of a test. + +## 1.18.2 + +* Publish with the `host.dart.js` file. + +## 1.18.1 + +* Add defaulting for older test backends that don't pass a configuration for + the `allow_duplicate_test_names` parameter to the remote listener. + +## 1.18.0 + +* Add configuration to disallow duplicate test and group names. See the + [docs][allow_duplicate_test_names] for more information. +* Remove dependency on pedantic. + +[allow_duplicate_test_names]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#allow_duplicate_test_names + +## 1.17.12 + +* Support the latest `test_core`. +* Re-use the cached dill file from previous runs on subsequent runs. + +## 1.17.11 + +* Use the latest `package:matcher`. + * Change many argument types from `dynamic` to `Object?`. + * Fix `stringContainsInOrder` to account for repetitions and empty strings. + * **Note**: This may break some existing tests, as the behavior does change. + +## 1.17.10 + +* Report incomplete tests as errors in the JSON reporter when the run is + canceled early. +* Update `analyzer` constraint to `>=1.0.0 <3.0.0`. + +## 1.17.9 + +* Fix a bug where a tag level configuration would cause test suites with that + tag to ignore the `--test-randomize-ordering-seed` argument. + +## 1.17.8 + +* Update json reporter docs with updated nullability annotations and + descriptions. +* Add `time` field to the json reporters `allSuites` event type so that all + event types can be unified. + +## 1.17.7 + +* Support the latest `test_core`. + +## 1.17.6 + +* Give a better error when `printOnFailure` is called from outside a test + zone. + +## 1.17.5 + +* Support the latest vm_service release (`7.0.0`). + +## 1.17.4 + +* Fix race condition between compilation of vm tests and the running of + isolates. + +## 1.17.3 + +* Forward experiment args from the runner executable to the compiler with the + new vm test loading strategy. + +## 1.17.2 + +* Fix a windows issue with the new loading strategy. + +## 1.17.1 + +* Fix an issue where you couldn't have tests compiled in both sound and + unsound null safety modes. + +## 1.17.0 + +* Change the default way VM tests are launched and ran to greatly speed up + loading performance. + * You can force the old strategy with `--use-data-isolate-strategy` flag if + you run into issues, but please also file a bug. +* Disable stack trace chaining by default. It can be re-enabled by explicitly + passing the `--chain-stack-traces` flag. +* Remove `phantomjs` support completely, it was previously broken. +* Fix `expectAsync` function type checks. +* Add libraries `scaffolding.dart`, and `expect.dart` to allow importing a + subset of the normal surface area. + +## 1.16.8 + +* Fix an issue where coverage collection could hang on Chrome. +* ~~Disable stack trace chaining by default. It can be re-enabled by explicitly + passing the `--chain-stack-traces` flag.~~ + +## 1.16.7 + +* Update `spawnHybridCode` to default to the current packages language version. +* Update `test_core` and `test_api` deps. + +## 1.16.6 + +* Complete the migration to null safety. + +## 1.16.5 + +* Expand several deps to allow the latest versions. + +## 1.16.4 + +* Update `test_core` dependency to `0.3.14`. + +## 1.16.3 + +* Update `web_socket_channel` dependency to support latest. + +## 1.16.2 + +* Update `test_core` dependency to `0.3.13`. + +## 1.16.1 + +* Allow the latest analyzer `1.0.0`. + +## 1.16.0 + +* Stable null safety release. + +## 1.16.0-nullsafety.19 + +* Use the `test_api` for stable null safety. + +## 1.16.0-nullsafety.18 + +* Expand upper bound constraints for some null safe migrated packages. + +## 1.16.0-nullsafety.17 + +* Support the latest shelf release (`1.x.x`). + +## 1.16.0-nullsafety.16 + +* Support the latest vm_service release (`6.x.x`). + +## 1.16.0-nullsafety.15 + +* Support the latest coverage release (`0.15.x`). + +## 1.16.0-nullsafety.14 + +* Allow the latest args release (`2.x`). + +## 1.16.0-nullsafety.13 + +* Allow the latest glob release (`2.x`). + +## 1.16.0-nullsafety.12 + +* Fix `spawnHybridUri` on windows. +* Fix failures running tests on the `node` platform. +* Allow `package:yaml` version `3.x.x`. + +## 1.16.0-nullsafety.11 + +* Set up a stack trace mapper in precompiled mode if source maps exist. If + the stack traces are already mapped then this has no effect, otherwise it + will try to map any JS lines it sees. + +## 1.16.0-nullsafety.10 + +* Allow injecting a test channel for browser tests. +* Allow `package:analyzer` version `0.41.x`. + +## 1.16.0-nullsafety.9 + +* Fix `spawnHybridUri` to respect language versioning of the spawned uri. + +## 1.16.0-nullsafety.8 + +* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release + guidelines. + +## 1.16.0-nullsafety.7 + +* Allow prerelease versions of the 2.12 sdk. + +## 1.16.0-nullsafety.6 + +* Add `markTestSkipped` API. + +## 1.16.0-nullsafety.5 + +* Allow `2.10` stable and `2.11.0-dev` SDKs. +* Annotate the classes used as annotations to restrict their usage to library + level. +* Stop required a `SILENT_OBSERVATORY` environment variable to run with + debugging and the JSON reporter. + +## 1.16.0-nullsafety.4 + +* Depend on the latest test_core. + +## 1.16.0-nullsafety.3 + +* Clean up `--help` output. + +## 1.16.0-nullsafety.2 + +* Allow version `0.40.x` of `analyzer`. + +## 1.16.0-nullsafety.1 + +* Depend on the latest test_core. + +## 1.16.0-nullsafety + +* Support running tests with null safety. + * Note that the test runner itself is not fully migrated yet. +* Add the `Fake` class, available through `package:test_api/fake.dart`. This + was previously part of the Mockito package, but with null safety it is useful + enough that we decided to make it available through `package:test`. In a + future release it will be made available directly through + `package:test_api/test_api.dart` (and hence through + `package:test_core/test_core.dart` and `package:test/test.dart`). + +## 1.15.7 (Backport) + +* Fix `spawnHybridUri` on windows. + +## 1.15.6 (Backport) + +* Support `package:analyzer` version `0.41.x`. + +## 1.15.5 (Backport) + +* Fix `spawnHybridUri` to respect language versioning of the spawned uri. + +## 1.15.4 + +* Allow analyzer 0.40.x. + +## 1.15.3 + +* Update to `matcher` version `0.12.9` which improves the mismatch description + for deep collection equality matchers and TypeMatcher. + +## 1.15.2 + +* Use the latest `test_core` which resolves an issue with the latest + `package:meta`. + +## 1.15.1 + +* Avoid a confusing stack trace when there is a problem loading a platform when + using the JSON reporter and enabling debugging. +* Restore behavior of listening for both `IPv6` and `IPv4` sockets for the node + platform. + +## 1.15.0 + +* Update bootstrapping logic to ensure the bootstrap library has + the same language version as the test. +* The Node platform will now communicate over only IPv6 if it is available. + +## 1.14.7 + +* Support the latest `package:coverage`. + + +## 1.14.6 + +* Update `test_core` to `0.3.6`. + +## 1.14.5 + +* Add additional information to an exception when we end up with a null + `RunnerSuite`. + +## 1.14.4 + +* Use non-headless Chrome when provided the flag `--pause-after-load`. + +## 1.14.3 + +* Fix an issue where coverage tests could not run in Chrome headless. +* Fix an issue where coverage collection would not work with source + maps that contained absolute file URIs. +* Fix error messages for incorrect string literals in test annotations. +* Update `test_core` to `0.3.4`. + +## 1.14.2 + +* Update `test_core` to `0.3.3`. + +## 1.14.1 + +* Allow the latest shelf_packages_handler. + +## 1.14.0 + +* Drop the `package_resolver` dependency for the `package_config` dependency + which is lower level. + +## 1.13.0 + +* Enable asserts in code running through `spawnHybrid` APIs. +* Exit with a non-zero code if no tests were ran, whether due to skips or having + no tests defined. +* Fix the stack trace labels in SDK code for `dart2js` compiled tests. +* Cancel any StreamQueue that is created as a part of a stream matcher once it + is done matching. + * This fixes a bug where using a matcher on a custom stream controller and + then awaiting the `close()` method on that controller would hang. +* Avoid causing the test runner to hang if there is a timeout during a + `tearDown` callback following a failing test case. + +## 1.12.0 + +* Bump minimum SDK to `2.4.0` for safer usage of for-loop elements. +* Deprecate `PhantomJS` and provide warning when used. Support for `PhantomJS` + will be removed in version `2.0.0`. +* Support coverage collection for the Chrome platform. See `README.md` for usage + details. + +## 1.11.1 + +* Allow `test_api` `0.2.13` to work around a bug in the SDK version `2.3.0`. + +## 1.11.0 + +* Add `file_reporters` configuration option and `--file-reporter` CLI option to + allow specifying a separate reporter that writes to a file instead of stdout. + +## 1.10.0 + +* Add `customHtmlTemplateFile` configuration option to allow sharing an + html template between tests +* Depend on the latest `package:test_core`. +* Depend on the latest `package:test_api`. + +## 1.9.4 + +* Extend the timeout for synthetic tests, e.g. `tearDownAll`. +* Depend on the latest `package:test_core`. +* Depend on the latest `package:test_api`. + +## 1.9.3 + +* Depend on the latest `package:test_core`. +* Support the latest `package:analyzer`. +* Update to latest `package:matcher`. Improves output for instances of private + classes. + +## 1.9.2 + +* Depend on the latest `package:test_api` and `package:test_core`. +* While using `solo` tests that are not run will now be reported as skipped. + +## 1.9.1 + +* Depend on latest `test_core`. + +## 1.9.0 + +* Implement code coverage collection for VM based tests + +## 1.8.0 + +* Expose the previously hidden sharding arguments + * `--total-shards` specifies how many shards the suite should + be split into + * `--shard-index` specifies which shard should be run + +## 1.7.0 + +* Add a `--debug` flag for running the VM/Chrome in debug mode. + +## 1.6.11 + +* Depend on the latest `test_core` and `test_api`. + +## 1.6.10 + +* Depend on the latest `test_core`. + +## 1.6.9 + +* Add `--disable-dev-shm-usage` to the default Chrome flags. + +## 1.6.8 + +* Depend on the latest `test_core` and `test_api`. + +## 1.6.7 + +* Allow `analyzer` version `0.38.x`. + +## 1.6.6 + +* Pass `--server-mode` to dart2js instead of `--categories=Server` to fix a + warning about the flag deprecation. +* Drop dependency on `pub_semver`. +* Fix issue with the latest `Utf8Decoder` and the `node` platform. + +## 1.6.5 + +* Depend on the latest `test_core`. +* Depend on the latest `package:analyzer`. + +## 1.6.4 + +* Don't swallow exceptions from callbacks in `expectAsync*`. +* Internal cleanup - fix lints. + +## 1.6.3 + +* Depend on latest `package:test_core`. + * This fixes an issue where non-completed tests were considered passing. + +## 1.6.2 + +* Avoid `dart:isolate` imports on code loaded in tests. + +## 1.6.1 + +* Allow `stream_channel` version `2.0.0`. + +## 1.6.0 + +* Allow `analyzer` version `0.36.x`. +* Matcher changes: + * Add `isA()` to create `TypeMatcher` instances in a more fluent way. + * Add `isCastError`. + * **Potentially breaking bug fix**. Ordering matchers no longer treat objects + with a partial ordering (such as NaN for double values) as if they had a + complete ordering. For instance `greaterThan` now compares with the `>` + operator rather not `<` and not `=`. This could cause tests which relied on + this bug to start failing. + +## 1.5.3 + +* Allow `analyzer` version `0.35.x`. + +## 1.5.2 + +* Require Dart SDK `>=2.1.0`. +* Depend on latest `test_core` and `test_api`. + +## 1.5.1 + +* Depend on latest `test_core` and `test_api`. + +## 1.5.0 + +* Depend on `package:test_core` for core functionality. + +## 1.4.0 + +* Depend on `package:test_api` for core functionality. + +## 1.3.4 + +* Allow remote_listener to be closed and sent an event on close. + +## 1.3.3 + +* Add conditional imports so that `dart:io` is not imported from the main + `test.dart` entrypoint unless it is available. +* Fix an issue with dartdevc in precompiled mode and the json reporter. +* Fix an issue parsing test metadata annotations without explicit `const`. + +## 1.3.2 + +* Widen the constraints on the analyzer package. + +## 1.3.1 + +* Handle parsing annotations which omit `const` on collection literals. +* Fix an issue where `root_line`, `root_column`, and `root_url` in the + JSON reported may not be populated correctly on Windows. +* Removed requirement for the test/pub_serve transformer in --pub-serve mode. + +## 1.3.0 + +* When using `--precompiled`, the test runner now allows symlinks to reach + outside the precompiled directory. This allows more efficient creation of + precompiled directories (using symlinks instead of copies). +* Updated max sdk range to `<3.0.0`. + +## 1.2.0 + +* Added support for using precompiled kernel files when running vm tests. + * When using the `--precompiled` flag we will now first check for a + `<original-test-path>.vm_test.vm.app.dill` file, and if present load that + directly in the isolate. Otherwise the `<original-test-path>.vm_test.dart` + file will be used. + +## 1.1.0 + +* Added a new `pid` field to the StartEvent in the json runner containing the + pid of the VM process running the tests. + +## 1.0.0 + +* No change from `0.12.42`. We are simply signalling to users that this is a + well supported package and is the preferred way to write Dart tests. + +## 0.12.42 + +* Add support for `solo` test and group. When the argument is `true` only tests + and groups marked as solo will be run. It is still recommended that users + instead filter their tests by using the runner argument `-n`. + +* Updated exported `package:matcher` to `0.12.3` which includes these updates: + + - Many improvements to `TypeMatcher` + - Can now be used directly as `const TypeMatcher<MyType>()`. + - Added a type parameter to specify the target `Type`. + - Made the `name` constructor parameter optional and marked it deprecated. + It's redundant to the type parameter. + - Migrated all `isType` matchers to `TypeMatcher`. + - Added a `having` function that allows chained validations of specific + features of the target type. + + ```dart + /// Validates that the object is a [RangeError] with a message containing + /// the string 'details' and `start` and `end` properties that are `null`. + final _rangeMatcher = isRangeError + .having((e) => e.message, 'message', contains('details')) + .having((e) => e.start, 'start', isNull) + .having((e) => e.end, 'end', isNull); + ``` + + - Deprecated the `isInstanceOf` class. Use `TypeMatcher` instead. + + - Improved the output of `Matcher` instances that fail due to type errors. + +## 0.12.41 + +* Add support for debugging VM tests. +* Tweak default reporter and color logic again so that they are always enabled + on all non-windows platforms. + +## 0.12.40 + +* Added some new optional fields to the json reporter, `root_line`, + `root_column`, and `root_url`. These will be present if `url` is not the same + as the suite url, and will represent the location in the original test suite + from which the call to `test` originated. + +## 0.12.39 + +* Change the default reporter and color defaults to be based on + `stdout.supportsAnsiEscapes` instead of based on platform (previously both + were disabled on windows). + +## 0.12.38+3 + +* Fix Dart 2 runtime errors around communicating with browsers. + + +## 0.12.38+2 + +* Fix more Dart 2 runtime type errors. + +## 0.12.38+1 + +* Fix several Dart 2 runtime type errors. + +## 0.12.38 + +* Give `neverCalled` a type that works in Dart 2 semantics. +* Support `package:analyzer` `0.32.0`. + +## 0.12.37 + +* Removed the transformer, and the `pub_serve.dart` entrypoint. This is not + being treated as a breaking change because the minimum sdk constraint now + points to an sdk which does not support pub serve or barback any more anyways. +* Drop the dependency on `barback`. + +## 0.12.36 + +* Expose the test bootstrapping methods, so that build systems can precompile + tests without relying on internal apis. + +## 0.12.35 + +* Dropped support for Dart 1. Going forward only Dart 2 will be supported. + * If you experience blocking issues and are still on the Dart 1 sdk, we will + consider bug fixes on a per-case basis based on severity and impact. + * Drop support for `dartium` and `content-shell` platforms since those are + removed from the Dart 2 SDK. +* Fixed an issue `--precompiled` node tests in subdirectories. +* Fixed some dart2 issues with node test bootstrapping code so that dartdevc + tests can run. +* Fixed default custom html handler so it correctly includes the + packages/test/dart.js file. This allows you to get proper errors instead of + timeouts if there are load exceptions in the browser. +* Upgrade to package:matcher 0.12.2 + +## 0.12.34 + +* Requires at least Dart 1.24.0. +* The `--precompiled` flag is now supported for the vm platform and the node + platform. +* On browser platforms the `--precompiled` flag now serves all sources directly + from the precompiled directory, and will never attempt to do its own + compilation. + +## 0.12.33 + +* Pass `--categories=Server` to `dart2js` when compiling tests for Node.js. This + tells it that `dart:html` is unavailable. + +* Don't crash when attempting to format stack traces when running via + `dart path/to/test.dart`. + +## 0.12.32+2 + +* Work around an SDK bug that caused timeouts in asynchronous code. + +## 0.12.32+1 + +* Fix a bug that broke content shell on Dart 1.24. + +## 0.12.32 + +* Add an `include` configuration field which specifies the path to another + configuration file whose configuration should be used. + +* Add a `google` platform selector variable that's only true on Google's + internal infrastructure. + +## 0.12.31 + +* Add a `headless` configuration option for Chrome. + +* Re-enable headless mode for Chrome by default. + +* Don't hang when a Node.js test fails to compile. + +## 0.12.30+4 + +* Stop running Chrome in headless mode temporarily to work around a browser bug. + +## 0.12.30+3 + +* Fix a memory leak when loading browser tests. + +## 0.12.30+2 + +* Avoid loading test suites whose tags are excluded by `--excluded-tags`. + +## 0.12.30+1 + +* Internal changes. + +## 0.12.30 + +* Platform selectors for operating systems now work for Node.js tests + ([#742][]). + +* `fail()` is now typed to return `Null`, so it can be used in the same places + as a raw `throw`. + +* Run Chrome in headless mode unless debugging is enabled. + +[#742]: https://github.com/dart-lang/test/issues/742 + +## 0.12.29+1 + +* Fix strong mode runtime cast failures. + +## 0.12.29 + +* Node.js tests can now import modules from a top-level `node_modules` + directory, if one exists. + +* Raw `console.log()` calls no longer crash Node.js tests. + +* When a browser crashes, include its standard output in the error message. + +## 0.12.28+1 + +* Add a `pumpEventQueue()` function to make it easy to wait until all + asynchronous tasks are complete. + +* Add a `neverCalled` getter that returns a function that causes the test to + fail if it's ever called. + +## 0.12.27+1 + +* Increase the timeout for loading tests to 12 minutes. + +## 0.12.27 + +* When `addTearDown()` is called within a call to `setUpAll()`, it runs its + callback after *all* tests instead of running it after the `setUpAll()` + callback. + +* When running in an interactive terminal, the test runner now prints status + lines as wide as the terminal and no wider. + +## 0.12.26+1 + +* Fix lower bound on package `stack_trace`. Now 1.6.0. +* Manually close browser process streams to prevent test hangs. + +## 0.12.26 + +* The `spawnHybridUri()` function now allows root-relative URLs, which are + interpreted as relative to the root of the package. + +## 0.12.25 + +* Add a `override_platforms` configuration field which allows test platforms' + settings (such as browsers' executables) to be overridden by the user. + +* Add a `define_platforms` configuration field which makes it possible to define + new platforms that use the same logic as existing ones but have different + settings. + +## 0.12.24+8 + +* `spawnHybridUri()` now interprets relative URIs correctly in browser tests. + +## 0.12.24+7 + +* Declare support for `async` 2.0.0. + +## 0.12.24+6 + +* Small refactoring to make the package compatible with strong-mode compliant Zone API. + No user-visible change. + +## 0.12.24+5 + +* Expose a way for tests to forward a `loadException` to the server. + +## 0.12.24+4 + +* Drain browser process `stdout` and `stdin`. This resolves test flakiness, especially in Travis + with the `Precise` image. + +## 0.12.24+3 + +* Extend `deserializeTimeout`. + +## 0.12.24+2 + +* Only force exit if `FORCE_TEST_EXIT` is set in the environment. + +## 0.12.24+1 + +* Widen version constraint on `analyzer`. + +## 0.12.24 + +* Add a `node` platform for compiling tests to JavaScript and running them on + Node.js. + +## 0.12.23+1 + +* Remove unused imports. + +## 0.12.23 + +* Add a `fold_stack_frames` field for `dart_test.yaml`. This will + allow users to customize which packages' frames are folded. + +## 0.12.22+2 + +* Properly allocate ports when debugging Chrome and Dartium in an IPv6-only + environment. + +## 0.12.22+1 + +* Support `args` 1.0.0. + +* Run tear-down callbacks in the same error zone as the test function. This + makes it possible to safely share `Future`s and `Stream`s between tests and + their tear-downs. + +## 0.12.22 + +* Add a `retry` option to `test()` and `group()` functions, as well + as `@Retry()` annotation for test files and a `retry` + configuration field for `dart_test.yaml`. A test with reties + enabled will be re-run if it fails for a reason other than a + `TestFailure`. + +* Add a `--no-retry` runner flag that disables retries of failing tests. + +* Fix a "concurrent modification during iteration" error when calling + `addTearDown()` from within a tear down. + +## 0.12.21 + +* Add a `doesNotComplete` matcher that asserts that a Future never completes. + +* `throwsA()` and all related matchers will now match functions that return + `Future`s that emit exceptions. + +* Respect `onPlatform` for groups. + +* Only print browser load errors once per browser. + +* Gracefully time out when attempting to deserialize a test suite. + +## 0.12.20+13 + +* Upgrade to package:matcher 0.12.1 + +## 0.12.20+12 + +* Now support `v0.30.0` of `pkg/analyzer` + +* The test executable now does a "hard exit" when complete to ensure lingering + isolates or async code don't block completion. This may affect users trying + to use the Dart service protocol or observatory. + +## 0.12.20+11 + +* Refactor bootstrapping to simplify the test/pub_serve transformer. + +## 0.12.20+10 + +* Refactor for internal tools. + +## 0.12.20+9 + +* Introduce new flag `--chain-stack-traces` to conditionally chain stack traces. + +## 0.12.20+8 + +* Fixed more blockers for compiling with `dev_compiler`. +* Dartfmt the entire repo. + +* **Note:** 0.12.20+5-0.12.20+7 were tagged but not officially published. + +## 0.12.20+4 + +* Fixed strong-mode errors and other blockers for compiling with `dev_compiler`. + +## 0.12.20+3 + +* `--pause-after-load` no longer deadlocks with recent versions of Chrome. + +* Fix Dartified stack traces for JS-compiled tests run through `pub serve`. + +## 0.12.20+2 + +* Print "[E]" after test failures to make them easier to identify visually and + via automated search. + +## 0.12.20+1 + +* Tighten the dependency on `stream_channel` to reflect the APIs being used. + +* Use a 1024 x 768 iframe for browser tests. + +## 0.12.20 + +* **Breaking change:** The `expect()` method no longer returns a `Future`, since + this broke backwards-compatibility in cases where a void function was + returning an `expect()` (such as `void foo() => expect(...)`). Instead, a new + `expectLater()` function has been added that return a `Future` that completes + when the matcher has finished running. + +* The `verbose` parameter to `expect()` and the `formatFailure()` function are + deprecated. + +## 0.12.19+1 + +* Make sure asynchronous matchers that can fail synchronously, such as + `throws*()` and `prints()`, can be used with synchronous matcher operators + like `isNot()`. + +## 0.12.19 + +* Added the `StreamMatcher` class, as well as several built-in stream matchers: + `emits()`, `emitsError()`, `emitsDone, mayEmit()`, `mayEmitMultiple()`, + `emitsAnyOf()`, `emitsInOrder()`, `emitsInAnyOrder()`, and `neverEmits()`. + +* `expect()` now returns a Future for the asynchronous matchers `completes`, + `completion()`, `throws*()`, and `prints()`. + +* Add a `printOnFailure()` method for providing debugging information that's + only printed when a test fails. + +* Automatically configure the [`term_glyph`][term_glyph] package to use ASCII + glyphs when the test runner is running on Windows. + +[term_glyph]: https://pub.dev/packages/term_glyph + +* Deprecate the `throws` matcher in favor of `throwsA()`. + +* Deprecate the `Throws` class. These matchers should only be constructed via + `throwsA()`. + +## 0.12.18+1 + +* Fix the deprecated `expectAsync()` function. The deprecation caused it to + fail to support functions that take arguments. + +## 0.12.18 + +* Add an `addTearDown()` function, which allows tests to register additional + tear-down callbacks as they're running. + +* Add the `spawnHybridUri()` and `spawnHybridCode()` functions, which allow + browser tests to run code on the VM. + +* Fix the new `expectAsync` functions so that they don't produce analysis errors + when passed callbacks with optional arguments. + +## 0.12.17+3 + +* Internal changes only. + +## 0.12.17+2 + +* Fix Dartium debugging on Windows. + +## 0.12.17+1 + +* Fix a bug where tags couldn't be marked as skipped. + +## 0.12.17 + +* Deprecate `expectAsync` and `expectAsyncUntil`, since they currently can't be + made to work cleanly in strong mode. They are replaced with separate methods + for each number of callback arguments: + * `expectAsync0`, `expectAsync1`, ... `expectAsync6`, and + * `expectAsyncUntil0`, `expectAsyncUntil1`, ... `expectAsyncUntil6`. + +## 0.12.16 + +* Allow tools to interact with browser debuggers using the JSON reporter. + +## 0.12.15+12 + +* Fix a race condition that could cause the runner to stall for up to three + seconds after completing. + +## 0.12.15+11 + +* Make test iframes visible when debugging. + +## 0.12.15+10 + +* Throw a better error if a group body is asynchronous. + +## 0.12.15+9 + +* Widen version constraint on `analyzer`. + +## 0.12.15+8 + +* Make test suites with thousands of tests load much faster on the VM (and + possibly other platforms). + +## 0.12.15+7 + +* Fix a bug where tags would be dropped when `on_platform` was defined in a + config file. + +## 0.12.15+6 + +* Fix a broken link in the `--help` documentation. + +## 0.12.15+5 + +* Internal-only change. + +## 0.12.15+4 + +* Widen version constraint on `analyzer`. + +## 0.12.15+3 + +* Move `nestingMiddleware` to `lib/src/util/path_handler.dart` to enable a + cleaner separation between test-runner files and test writing files. + +## 0.12.15+2 + +* Support running without a `packages/` directory. + +## 0.12.15+1 + +* Declare support for version 1.19 of the Dart SDK. + +## 0.12.15 + +* Add a `skip` parameter to `expect()`. Marking a single expect as skipped will + cause the test itself to be marked as skipped. + +* Add a `--run-skipped` parameter and `run_skipped` configuration field that + cause tests to be run even if they're marked as skipped. + +## 0.12.14+1 + +* Narrow the constraint on `yaml`. + +## 0.12.14 + +* Add test and group location information to the JSON reporter. + +## 0.12.13+5 + +* Declare support for version 1.18 of the Dart SDK. + +* Use the latest `collection` package. + +## 0.12.13+4 + +* Compatibility with an upcoming release of the `collection` package. + +## 0.12.13+3 + +* Internal changes only. + +## 0.12.13+2 + +* Fix all strong-mode errors and warnings. + +## 0.12.13+1 + +* Declare support for version 1.17 of the Dart SDK. + +## 0.12.13 + +* Add support for a global configuration file. On Windows, this file defaults to + `%LOCALAPPDATA%\DartTest.yaml`. On Unix, it defaults to `~/.dart_test.yaml`. + It can also be explicitly set using the `DART_TEST_CONFIG` environment + variable. See [the configuration documentation][global config] for details. + +* The `--name` and `--plain-name` arguments may be passed more than once, and + may be passed together. A test must match all name constraints in order to be + run. + +* Add `names` and `plain_names` fields to the package configuration file. These + allow presets to control which tests are run based on their names. + +* Add `include_tags` and `exclude_tags` fields to the package configuration + file. These allow presets to control which tests are run based on their tags. + +* Add a `pause_after_load` field to the package configuration file. This allows + presets to enable debugging mode. + +[global config]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#global-configuration + +## 0.12.12 + +* Add support for [test presets][]. These are defined using the `presets` field + in the package configuration file. They can be selected by passing `--preset` + or `-P`, or by using the `add_presets` field in the package configuration + file. + +* Add an `on_os` field to the package configuration file that allows users to + select different configuration for different operating systems. + +* Add an `on_platform` field to the package configuration file that allows users + to configure all tests differently depending on which platform they run on. + +* Add an `ios` platform selector variable. This variable will only be true when + the `test` executable itself is running on iOS, not when it's running browser + tests on an iOS browser. + +[test presets]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/package_config.md#configuration-presets + +## 0.12.11+2 + +* Update to `shelf_web_socket` 0.2.0. + +## 0.12.11+1 + +* Purely internal change. + +## 0.12.11 + +* Add a `tags` field to the package configuration file that allows users to + provide configuration for specific tags. + +* The `--tags` and `--exclude-tags` command-line flags now allow + [boolean selector syntax][]. For example, you can now pass `--tags "(chrome || + firefox) && !slow"` to select quick Chrome or Firefox tests. + +[boolean selector syntax]: https://github.com/dart-lang/boolean_selector/blob/master/README.md + +## 0.12.10+2 + +* Re-add help output separators. + +* Tighten the constraint on `args`. + +## 0.12.10+1 + +* Temporarily remove separators from the help output. Version 0.12.8 was + erroneously released without an appropriate `args` constraint for the features + it used; this version will help ensure that users who can't use `args` 0.13.1 + will get a working version of `test`. + +## 0.12.10 + +* Add support for a package-level configuration file called `dart_test.yaml`. + +## 0.12.9 + +* Add `SuiteEvent` to the JSON reporter, which reports data about the suites in + which tests are run. + +* Add `AllSuitesEvent` to the JSON reporter, which reports the total number of + suites that will be run. + +* Add `Group.testCount` to the JSON reporter, which reports the total number of + tests in each group. + +## 0.12.8 + +* Organize the `--help` output into sections. + +* Add a `--timeout` flag. + +## 0.12.7 + +* Add the ability to re-run tests while debugging. When the browser is paused at + a breakpoint, the test runner will open an interactive console on the command + line that can be used to restart the test. + +* Add support for passing any object as a description to `test()` and `group()`. + These objects will be converted to strings. + +* Add the ability to tag tests. Tests with specific tags may be run by passing + the `--tags` command-line argument, or excluded by passing the + `--exclude-tags` parameter. + + This feature is not yet complete. For now, tags are only intended to be added + temporarily to enable use-cases like [focusing][] on a specific test or group. + Further development can be followed on [the issue tracker][issue 16]. + +* Wait for a test's tear-down logic to run, even if it times out. + +[focusing]: https://jasmine.github.io/2.1/focused_specs.html +[issue 16]: https://github.com/dart-lang/test/issues/16 + +## 0.12.6+2 + +* Declare compatibility with `http_parser` 2.0.0. + +## 0.12.6+1 + +* Declare compatibility with `http_multi_server` 2.0.0. + +## 0.12.6 + +* Add a machine-readable JSON reporter. For details, see + [the protocol documentation][json-protocol]. + +* Skipped groups now properly print skip messages. + +[json-protocol]: https://github.com/dart-lang/test/blob/master/pkgs/test/json_reporter.md + +## 0.12.5+2 + +* Declare compatibility with Dart 1.14 and 1.15. + +## 0.12.5+1 + +* Fixed a deadlock bug when using `setUpAll()` and `tearDownAll()`. + +## 0.12.5 + +* Add `setUpAll()` and `tearDownAll()` methods that run callbacks before and + after all tests in a group or suite. **Note that these methods are for special + cases and should be avoided**—they make it very easy to accidentally introduce + dependencies between tests. Use `setUp()` and `tearDown()` instead if + possible. + +* Allow `setUp()` and `tearDown()` to be called multiple times within the same + group. + +* When a `tearDown()` callback runs after a signal has been caught, it can now + schedule out-of-band asynchronous callbacks normally rather than having them + throw exceptions. + +* Don't show package warnings when compiling tests with dart2js. This was + accidentally enabled in 0.12.2, but was never intended. + +## 0.12.4+9 + +* If a `tearDown()` callback throws an error, outer `tearDown()` callbacks are + still executed. + +## 0.12.4+8 + +* Don't compile tests to JavaScript when running via `pub serve` on Dartium or + content shell. + +## 0.12.4+7 + +* Support `http_parser` 1.0.0. + +## 0.12.4+6 + +* Fix a broken link in the README. + +## 0.12.4+5 + +* Internal changes only. + +## 0.12.4+4 + +* Widen the Dart SDK constraint to include `1.13.0`. + +## 0.12.4+3 + +* Make source maps work properly in the browser when not using `--pub-serve`. + +## 0.12.4+2 + +* Fix a memory leak when running many browser tests where old test suites failed + to be unloaded when they were supposed to. + +## 0.12.4+1 + +* Require Dart SDK >= `1.11.0` and `shelf` >= `0.6.0`, allowing `test` to remove + various hacks and workarounds. + +## 0.12.4 + +* Add a `--pause-after-load` flag that pauses the test runner after each suite + is loaded so that breakpoints and other debugging annotations can be added. + Currently this is only supported on browsers. + +* Add a `Timeout.none` value indicating that a test should never time out. + +* The `dart-vm` platform selector variable is now `true` for Dartium and content + shell. + +* The compact reporter no longer prints status lines that only update the clock + if they would get in the way of messages or errors from a test. + +* The expanded reporter no longer double-prints the descriptions of skipped + tests. + +## 0.12.3+9 + +* Widen the constraint on `analyzer` to include `0.26.0`. + +## 0.12.3+8 + +* Fix an uncaught error that could crop up when killing the test runner process + at the wrong time. + +## 0.12.3+7 + +* Add a missing dependency on the `collection` package. + +## 0.12.3+6 + +**This version was unpublished due to [issue 287][].** + +* Properly report load errors caused by failing to start browsers. + +* Substantially increase browser timeouts. These timeouts are the cause of a lot + of flakiness, and now that they don't block test running there's less harm in + making them longer. + +## 0.12.3+5 + +**This version was unpublished due to [issue 287][].** + +* Fix a crash when skipping tests because their platforms don't match. + +## 0.12.3+4 + +**This version was unpublished due to [issue 287][].** + +* The compact reporter will update the timer every second, rather than only + updating it occasionally. + +* The compact reporter will now print the full, untruncated test name before any + errors or prints emitted by a test. + +* The expanded reporter will now *always* print the full, untruncated test name. + +## 0.12.3+3 + +**This version was unpublished due to [issue 287][].** + +* Limit the number of test suites loaded at once. This helps ensure that the + test runner won't run out of memory when running many test suites that each + load a large amount of code. + +## 0.12.3+2 + +**This version was unpublished due to [issue 287][].** + +[issue 287]: https://github.com/dart-lang/test/issues/287 + +* Improve the display of syntax errors in VM tests. + +* Work around a [Firefox bug][]. Computed styles now work in tests on Firefox. + +[Firefox bug]: https://bugzilla.mozilla.org/show_bug.cgi?id=548397 + +* Fix a bug where VM tests would be loaded from the wrong URLs on Windows (or in + special circumstances on other operating systems). + +## 0.12.3+1 + +* Fix a bug that caused the test runner to crash on Windows because symlink + resolution failed. + +## 0.12.3 + +* If a future matched against the `completes` or `completion()` matcher throws + an error, that error is printed directly rather than being wrapped in a + string. This allows such errors to be captured using the Zone API and improves + formatting. + +* Improve support for Polymer tests. This fixes a flaky time-out error and adds + support for Dartifying JavaScript stack traces when running Polymer tests via + `pub serve`. + +* In order to be more extensible, all exception handling within tests now uses + the Zone API. + +* Add a heartbeat to reset a test's timeout whenever the test interacts with the + test infrastructure. + +* `expect()`, `expectAsync()`, and `expectAsyncUntil()` throw more useful errors + if called outside a test body. + +## 0.12.2 + +* Convert JavaScript stack traces into Dart stack traces using source maps. This + can be disabled with the new `--js-trace` flag. + +* Improve the browser test suite timeout logic to avoid timeouts when running + many browser suites at once. + +## 0.12.1 + +* Add a `--verbose-trace` flag to include core library frames in stack traces. + +## 0.12.0 + +### Test Runner + +`0.12.0` adds support for a test runner, which can be run via +`pub run test:test` (or `pub run test` in Dart 1.10). By default it runs all +files recursively in the `test/` directory that end in `_test.dart` and aren't +in a `packages/` directory. + +The test runner supports running tests on the Dart VM and many different +browsers. Test files can use the `@TestOn` annotation to declare which platforms +they support. For more information on this and many more new features, see [the +README](README). + +[README]: https://github.com/dart-lang/test/blob/master/README.md + +### Removed and Changed APIs + +As part of moving to a runner-based model, most test configuration is moving out +of the test file and into the runner. As such, many ancillary APIs have been +removed. These APIs include `skip_` and `solo_` functions, `Configuration` and +all its subclasses, `TestCase`, `TestFunction`, `testConfiguration`, +`formatStacks`, `filterStacks`, `groupSep`, `logMessage`, `testCases`, +`BREATH_INTERVAL`, `currentTestCase`, `PASS`, `FAIL`, `ERROR`, `filterTests`, +`runTests`, `ensureInitialized`, `setSoloTest`, `enableTest`, `disableTest`, and +`withTestEnvironment`. + +`FailureHandler`, `DefaultFailureHandler`, `configureExpectFailureHandler`, and +`getOrCreateExpectFailureHandler` which used to be exported from the `matcher` +package have also been removed. They existed to enable integration between +`test` and `matcher` that has been streamlined. + +A number of APIs from `matcher` have been into `test`, including: `completes`, +`completion`, `ErrorFormatter`, `expect`,`fail`, `prints`, `TestFailure`, +`Throws`, and all of the `throws` methods. Some of these have changed slightly: + +* `expect` no longer has a named `failureHandler` argument. + +* `expect` added an optional `formatter` argument. + +* `completion` argument `id` renamed to `description`. + +## 0.11.6+4 + +* Fix some strong mode warnings we missed in the `vm_config.dart` and + `html_config.dart` libraries. + +## 0.11.6+3 + +* Fix a bug introduced in 0.11.6+2 in which operator matchers broke when taking + lists of matchers. + +## 0.11.6+2 + +* Fix all strong mode warnings. + +## 0.11.6+1 + +* Give tests more time to start running. + +## 0.11.6 + +* Merge in the last `0.11.x` release of `matcher` to allow projects to use both + `test` and `unittest` without conflicts. + +* Fix running individual tests with `HtmlIndividualConfiguration` when the test + name contains URI-escaped values and is provided with the `group` query + parameter. + +## 0.11.5+1 + +* Internal code cleanups and documentation improvements. + +## 0.11.5 + +* Bumped the version constraint for `matcher`. + +## 0.11.4 + +* Bump the version constraint for `matcher`. + +## 0.11.3 + +* Narrow the constraint on matcher to ensure that new features are reflected in + unittest's version. + +## 0.11.2 + +* Prints a warning instead of throwing an error when setting the test + configuration after it has already been set. The first configuration is always + used. + +## 0.11.1+1 + +* Fix bug in withTestEnvironment where test cases were not reinitialized if + called multiple times. + +## 0.11.1 + +* Add `reason` named argument to `expectAsync` and `expectAsyncUntil`, which has + the same definition as `expect`'s `reason` argument. +* Added support for private test environments. + +## 0.11.0+6 + +* Refactored package tests. + +## 0.11.0+5 + +* Release test functions after each test is run. + +## 0.11.0+4 + +* Fix for [20153](https://code.google.com/p/dart/issues/detail?id=20153) + +## 0.11.0+3 + +* Updated maximum `matcher` version. + +## 0.11.0+2 + +* Removed unused files from tests and standardized remaining test file names. + +## 0.11.0+1 + +* Widen the version constraint for `stack_trace`. + +## 0.11.0 + +* Deprecated methods have been removed: + * `expectAsync0`, `expectAsync1`, and `expectAsync2` - use `expectAsync` + instead + * `expectAsyncUntil0`, `expectAsyncUntil1`, and `expectAsyncUntil2` - use + `expectAsyncUntil` instead + * `guardAsync` - no longer needed + * `protectAsync0`, `protectAsync1`, and `protectAsync2` - no longer needed +* `matcher.dart` and `mirror_matchers.dart` have been removed. They are now in + the `matcher` package. +* `mock.dart` has been removed. It is now in the `mock` package. + +## 0.10.1+2 + +* Fixed deprecation message for `mock`. + +## 0.10.1+1 + +* Fixed CHANGELOG +* Moved to triple-slash for all doc comments. + +## 0.10.1 + +* **DEPRECATED** + * `matcher.dart` and `mirror_matchers.dart` are now in the `matcher` + package. + * `mock.dart` is now in the `mock` package. +* `equals` now allows a nested matcher as an expected list element or map value + when doing deep matching. +* `expectAsync` and `expectAsyncUntil` now support up to 6 positional arguments + and correctly handle functions with optional positional arguments with default + values. + +## 0.10.0 + +* Each test is run in a separate `Zone`. This ensures that any exceptions that + occur is async operations are reported back to the source test case. +* **DEPRECATED** `guardAsync`, `protectAsync0`, `protectAsync1`, + and `protectAsync2` + * Running each test in a `Zone` addresses the need for these methods. +* **NEW!** `expectAsync` replaces the now deprecated `expectAsync0`, + `expectAsync1` and `expectAsync2` +* **NEW!** `expectAsyncUntil` replaces the now deprecated `expectAsyncUntil0`, + `expectAsyncUntil1` and `expectAsyncUntil2` +* `TestCase`: + * Removed properties: `setUp`, `tearDown`, `testFunction` + * `enabled` is now get-only + * Removed methods: `pass`, `fail`, `error` +* `interactive_html_config.dart` has been removed. +* `runTests`, `tearDown`, `setUp`, `test`, `group`, `solo_test`, and + `solo_group` now throw a `StateError` if called while tests are running. +* `rerunTests` has been removed.
diff --git a/pkgs/test/LICENSE b/pkgs/test/LICENSE new file mode 100644 index 0000000..000cd7b --- /dev/null +++ b/pkgs/test/LICENSE
@@ -0,0 +1,27 @@ +Copyright 2014, 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/README.md b/pkgs/test/README.md new file mode 100644 index 0000000..36103d9 --- /dev/null +++ b/pkgs/test/README.md
@@ -0,0 +1,874 @@ +[](https://pub.dev/packages/test) +[](https://pub.dev/packages/test/publisher) + +`test` provides a standard way of writing and running tests in Dart. + +## Using package:test + +* [Writing Tests](#writing-tests) +* [Running Tests](#running-tests) + * [Sharding Tests](#sharding-tests) + * [Test Concurrency](#test-concurrency) + * [Shuffling Tests](#shuffling-tests) + * [Selecting a Test Reporter](#selecting-a-test-reporter) + * [Collecting Code Coverage](#collecting-code-coverage) + * [Restricting Tests to Certain Platforms](#restricting-tests-to-certain-platforms) + * [Platform Selectors](#platform-selectors) + * [Compiler Selectors](#compiler-selectors) + * [Running Tests on Node.js](#running-tests-on-nodejs) +* [Asynchronous Tests](#asynchronous-tests) +* [Running Tests With Custom HTML](#running-tests-with-custom-html) + * [Providing a custom HTML template](#providing-a-custom-html-template) +* [Configuring Tests](#configuring-tests) + * [Skipping Tests](#skipping-tests) + * [Timeouts](#timeouts) + * [Platform-Specific Configuration](#platform-specific-configuration) + * [Whole-Package Configuration](#whole-package-configuration) +* [Tagging Tests](#tagging-tests) +* [Debugging](#debugging) +* [Browser/VM Hybrid Tests](#browservm-hybrid-tests) +* [Support for Other Packages](#support-for-other-packages) + * [`build_runner`](#build_runner) + * [`term_glyph`](#term_glyph) +* [Further Reading](#further-reading) + +## Writing Tests + +Tests are specified using the top-level [`test()`] function. +Test asserts can be made using [`expect` from `package:matcher`][expect] + +[`test()`]: https://pub.dev/documentation/test/latest/test/test.html + +[expect]: https://pub.dev/documentation/matcher/latest/expect/expect.html + +```dart +import 'package:test/test.dart'; + +void main() { + test('String.split() splits the string on the delimiter', () { + var string = 'foo,bar,baz'; + expect(string.split(','), equals(['foo', 'bar', 'baz'])); + }); + + test('String.trim() removes surrounding whitespace', () { + var string = ' foo '; + expect(string.trim(), equals('foo')); + }); +} +``` + +Tests can be grouped together using the [`group()`] function. Each group's +description is added to the beginning of its test's descriptions. + +[`group()`]: https://pub.dev/documentation/test/latest/test/group.html + +```dart +import 'package:test/test.dart'; + +void main() { + group('String', () { + test('.split() splits the string on the delimiter', () { + var string = 'foo,bar,baz'; + expect(string.split(','), equals(['foo', 'bar', 'baz'])); + }); + + test('.trim() removes surrounding whitespace', () { + var string = ' foo '; + expect(string.trim(), equals('foo')); + }); + }); + + group('int', () { + test('.remainder() returns the remainder of division', () { + expect(11.remainder(3), equals(2)); + }); + + test('.toRadixString() returns a hex string', () { + expect(11.toRadixString(16), equals('b')); + }); + }); +} +``` + +You can use the [`setUp()`] and [`tearDown()`] functions to share code between +tests. The `setUp()` callback will run before every test in a group or test +suite, and `tearDown()` will run after. `tearDown()` will run even if a test +fails, to ensure that it has a chance to clean up after itself. + +```dart +import 'package:test/test.dart'; + +void main() { + late HttpServer server; + late Uri url; + setUp(() async { + server = await HttpServer.bind('localhost', 0); + url = Uri.parse('http://${server.address.host}:${server.port}'); + }); + + tearDown(() async { + await server.close(force: true); + server = null; + url = null; + }); + + // ... +} +``` + +[`setUp()`]: https://pub.dev/documentation/test/latest/test/setUp.html + +[`tearDown()`]: https://pub.dev/documentation/test/latest/test/tearDown.html + +## Running Tests + +A single test file can be run just using `dart test path/to/test.dart` (as of +Dart 2.10 - prior sdk versions must use `pub run test` instead of `dart test`). + + + +Many tests can be run at a time using `dart test path/to/dir`. + + + +It's also possible to run a test on the Dart VM only by invoking it using `dart +path/to/test.dart`, but this doesn't load the full test runner and will be +missing some features. + +The test runner accepts one or more path arguments. If there are no path +arguments the runner defaults to running tests under the `test/` directory. When +running for `test/` or any other directory, the runner will recursively search +the directory for files that match the test name pattern `*_test.dart`. The +pattern can be overridden in `dart_test.yaml`. When a path argument is a file +instead of a directory it will be run as a test, regardless of the file name. +Arguments which use shell globbing should avoid including non-test files in the +path argument. Tests which are run by other test runners may use a different +default path, such as `integration_test/`. Directories other than `test/` are +ignored by `dart test` unless passed explicitly as a path to run. + +You can select specific tests cases to run by name using `dart test -n "test +name"`. The string is interpreted as a regular expression, and only tests whose +description (including any group descriptions) match that regular expression +will be run. You can also use the `-N` flag to run tests whose names contain a +plain-text string. + +By default, tests are run in the Dart VM, but you can run them in the browser as +well by passing `dart test -p chrome path/to/test.dart`. `test` will take +care of starting the browser and loading the tests, and all the results will be +reported on the command line just like for VM tests. In fact, you can even run +tests on both platforms with a single command: `dart test -p "chrome,vm" +path/to/test.dart`. + +By default each platform has a default compiler, but some of them support +more than one compiler. You can choose which compiler to use by passing +`dart test -c source`, which would run all VM tests from source instead of +compiling them to kernel. This also supports targeting a specific platform +using normal platform selectors, like this `dart test -c vm:source`. + +### Test Path Queries + +Some query parameters are supported on test paths, which allow you to filter the +tests that will run within just those paths. These filters are merged with any +global options that are passed, and all filters must match for a test to be ran. + +- **name**: Works the same as `--name` (simple contains check). + - This is the only option that supports more than one entry. +- **full-name**: Requires an exact match for the name of the test. +- **line**: Matches any test that originates from this line in the test suite. +- **col**: Matches any test that originates from this column in the test suite. + +**Example Usage**: `dart test "path/to/test.dart?line=10&col=2"` + +#### Line/Col Matching Semantics + +The `line` and `col` filters match against the current stack trace taken from +the invocation to the `test` function, and are considered a match if +**any frame** in the trace meets **all** of the following criteria: + +* The URI of the frame matches the root test suite uri. + * This means it will not match lines from imported libraries. +* If both `line` and `col` are passed, both must match **the same frame**. +* The specific `line` and `col` to be matched are defined by the tools creating + the stack trace. This generally means they are 1 based and not 0 based, but + this package is not in control of the exact semantics and they may vary based + on platform implementations. + +### Sharding Tests + +Tests can also be sharded with the `--total-shards` and `--shard-index` arguments, +allowing you to split up your test suites and run them separately. For example, +if you wanted to run 3 shards of your test suite, you could run them as follows: + +```bash +dart test --total-shards 3 --shard-index 0 path/to/test.dart +dart test --total-shards 3 --shard-index 1 path/to/test.dart +dart test --total-shards 3 --shard-index 2 path/to/test.dart +``` +Sharding: This refers to the process of splitting up a large test suite into +smaller subsets (called shards) that can be run independently. Sharding is +particularly useful for distributed testing, where multiple machines are used +to run tests simultaneously. By dividing the test suite into smaller subsets, +you can run tests in parallel across multiple machines, which can significantly +reduce the overall testing time. + +### Test concurrency + +Test suites run concurrently by default, using half of the host's CPU cores. Use +`--concurrency` to control the number of test suites that runs concurrently, +meaning that multiple tests in independent suites or platforms can run at the +same time. For example, if you wanted to run the tests on 4 threads, you could +run the tests as follows: + +```bash +dart test --concurrency=4 +``` +This can speed up the overall testing process, especially if you have a large +number of test suites. + +### Shuffling Tests + +Test order can be shuffled with the `--test-randomize-ordering-seed` argument. +This allows you to shuffle your tests with a specific seed (deterministic) or +a random seed for each run. For example, consider the following test runs: + +```bash +dart test --test-randomize-ordering-seed=12345 +dart test --test-randomize-ordering-seed=random +``` + +Setting `--test-randomize-ordering-seed=0` will have the same effect as not +specifying it at all, meaning the test order will remain as-is. + +### Selecting a Test Reporter + +You can adjust the output format of test results using the `--reporter=<option>` +command line flag. The default format is the `compact` output format - a single +line, continuously updated as tests are run. When running on the GitHub Actions CI +however (detected via checking the `GITHUB_ACTIONS` environment variable for `true`), +the default changes to the `github` output format - a reporter customized +for that CI/CD system. + +The available options for the `--reporter` flag are: + +- `compact`: a single, continuously updated line +- `expanded`: a separate line for each update +- `github`: a custom reporter for GitHub Actions +- `json`: a machine-readable format; see https://dart.dev/go/test-docs/json_reporter.md + +### Collecting Code Coverage + +To collect code coverage, you can run tests with the `--coverage <directory>` +argument. The directory specified can be an absolute or relative path. +If a directory does not exist at the path specified, a directory will be +created. If a directory does exist, files may be overwritten with the latest +coverage data, if they conflict. + +This option will enable code coverage collection on a suite-by-suite basis, +and the resulting coverage files will be outputted in the directory specified. +The files can then be formatted using the `package:coverage` +`format_coverage` executable. + +Coverage gathering is currently only implemented for tests run on the Dart VM or +Chrome. + +Here's an example of how to run tests and format the collected coverage to LCOV: + +```shell +## Run Dart tests and output them at directory `./coverage`: +dart run test --coverage=./coverage + +## Activate package `coverage` (if needed): +dart pub global activate coverage + +## Format collected coverage to LCOV (only for directory "lib") +dart pub global run coverage:format_coverage --packages=.dart_tool/package_config.json --report-on=lib --lcov -o ./coverage/lcov.info -i ./coverage + +## Generate LCOV report: +genhtml -o ./coverage/report ./coverage/lcov.info + +## Open the HTML coverage report: +open ./coverage/report/index.html +``` + +* *LCOV is a GNU tool which provides information about what parts of a program are + actually executed (i.e. "covered") while running a particular test case.* +* The binary `genhtml` is one of the LCOV tools. +* See the LCOV project for more: https://github.com/linux-test-project/lcov +* See the Homebrew LCOV formula: https://formulae.brew.sh/formula/lcov + +### Restricting Tests to Certain Platforms + +Some test files only make sense to run on particular platforms. They may use +`dart:html` or `dart:io`, they might test Windows' particular filesystem +behavior, or they might use a feature that's only available in Chrome. The +[`@TestOn`] annotation makes it easy to declare exactly which platforms a test +file should run on. Just put it at the top of your file, before any `library` or +`import` declarations: + +```dart +@TestOn('vm') + +import 'dart:io'; + +import 'package:test/test.dart'; + +void main() { + // ... +} +``` + +[`@TestOn`]: https://pub.dev/documentation/test/latest/test/TestOn-class.html + +The string you pass to `@TestOn` is what's called a "platform selector", and it +specifies exactly which platforms a test can run on. It can be as simple as the +name of a platform, or a more complex Dart-like boolean expression involving +these platform names. + +You can also declare that your entire package only works on certain platforms by +adding a [`test_on` field] to your package config file. + +[`test_on` field]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#test_on + +### Platform Selectors + +Platform selectors use the [boolean selector syntax] defined in the +[`boolean_selector`] package, which is a subset of Dart's expression syntax that +only supports boolean operations. The following identifiers are defined: + +[boolean selector syntax]: https://github.com/dart-lang/boolean_selector/blob/master/README.md + +[`boolean_selector`]: https://pub.dev/packages/boolean_selector + +* `vm`: Whether the test is running on the command-line Dart VM. + +* `chrome`: Whether the test is running on Google Chrome. + +* `firefox`: Whether the test is running on Mozilla Firefox. + +* `safari`: Whether the test is running on Apple Safari. + +* `edge`: Whether the test is running on Microsoft Edge browser. + +* `node`: Whether the test is running on Node.js. + +* `dart-vm`: Whether the test is running on the Dart VM in any context. It's + identical to `!js`. + +* `browser`: Whether the test is running in any browser. + +* `js`: Whether the test has been compiled to JS. This is identical to + `!dart-vm`. + +* `blink`: Whether the test is running in a browser that uses the Blink + rendering engine. + +* `windows`: Whether the test is running on Windows. This can only be `true` if + either `vm` or `node` is true. + +* `mac-os`: Whether the test is running on MacOS. This can only be `true` if + either `vm` or `node` is true. + +* `linux`: Whether the test is running on Linux. This can only be `true` if + either `vm` or `node` is true. + +* `android`: Whether the test is running on Android. If `vm` is false, this will + be `false` as well, which means that this *won't* be true if the test is + running on an Android browser. + +* `ios`: Whether the test is running on iOS. If `vm` is false, this will be + `false` as well, which means that this *won't* be true if the test is running + on an iOS browser. + +* `posix`: Whether the test is running on a POSIX operating system. This is + equivalent to `!windows`. + +* `dart2js`: Whether the test has been compiled with Dart2Js. + +* `dart2wasm`: Whether the test has been compiled with Dart2Wasm. + +* `kernel`: Whether the test has been compiled to kernel. + +* `source`: Whether the test has been run with no compiler (from source). + +For example, if you wanted to run a test on every browser but Chrome, you would +write `@TestOn('browser && !chrome')`. + +### Running Tests on Node.js + +The test runner also supports compiling tests to JavaScript and running them on +[Node.js] by passing `--platform node`. Note that Node has access to *neither* +`dart:html` nor `dart:io`, so any platform-specific APIs will have to be invoked +using the [`js`] package. However, it may be useful when testing APIs that are +meant to be used by JavaScript code. + +[Node.js]: https://nodejs.org/en/ + +[`js`]: https://pub.dev/packages/js + +The test runner looks for an executable named `node` (on Mac OS or Linux) or +`node.exe` (on Windows) on your system path. When compiling Node.js tests, it +passes `-Dnode=true`, so tests can determine whether they're running on Node +using [`const bool.fromEnvironment('node')`][bool.fromEnvironment]. It also sets +`--server-mode`, which will tell the compiler that `dart:html` is not available. + +[bool.fromEnvironment]: https://api.dart.dev/stable/dart-core/bool/bool.fromEnvironment.html + +If a top-level `node_modules` directory exists, tests running on Node.js can +import modules from it. + +## Asynchronous Tests + +Tests written with `async`/`await` will work automatically. The test runner +won't consider the test finished until the returned `Future` completes. + +```dart +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test('Future.value() returns the value', () async { + var value = await Future.value(10); + expect(value, equals(10)); + }); +} +``` + +### Uncaught Async Errors + +Any uncaught asynchronous error throws within the zone that a test is running in +will cause the test to be considered a failure. This can cause a test which was +previously considered complete and passing to change into a failure if the +uncaught async error is raised late. If all test cases within the suite have +completed this may cause some errors to be missed, or to surface in only some +runs. + +Avoid uncaught async errors by ensuring that all futures have an error handler +[before they complete as an error][early-handler]. + +[early-handler]:https://dart.dev/guides/libraries/futures-error-handling#potential-problem-failing-to-register-error-handlers-early + +## Running Tests With Custom HTML + +By default, the test runner will generate its own empty HTML file for browser +tests. However, tests that need custom HTML can create their own files. These +files have three requirements: + +* They must have the same name as the test, with `.dart` replaced by `.html`. You can also + provide a configuration path to an HTML file if you want it to be reused across all tests. + See [Providing a custom HTML template](#providing-a-custom-html-template) below. + +* They must contain a `link` tag with `rel="x-dart-test"` and an `href` + attribute pointing to the test script. + +* They must contain `<script src="packages/test/dart.js"></script>`. + +For example, if you had a test called `custom_html_test.dart`, you might write +the following HTML file: + +```html +<!doctype html> +<!-- custom_html_test.html --> +<html> + <head> + <title>Custom HTML Test</title> + <link rel="x-dart-test" href="custom_html_test.dart"> + <script src="packages/test/dart.js"></script> + </head> + <body> + // ... + </body> +</html> +``` + +### Providing a custom HTML template + +If you want to share the same HTML file across all tests, you can provide a +`custom_html_template_path` configuration option to your configuration file. +This file should follow the rules above, except that instead of the link tag +add exactly one `{{testScript}}` in the place where you want the template processor to insert it. + +You can also optionally use any number of `{{testName}}` placeholders which will be replaced by the test filename. + +The template can't be named like any test file, as that would clash with using the +custom HTML mechanics. In such a case, an error will be thrown. + +For example: + +```yaml +custom_html_template_path: html_template.html.tpl +``` + +```html +<!doctype html> +<!-- html_template.html.tpl --> +<html> + <head> + <title>{{testName}} Test</title> + {{testScript}} + <script src="packages/test/dart.js"></script> + </head> + <body> + // ... + </body> +</html> +``` + +## Configuring Tests + +### Skipping Tests + +If a test, group, or entire suite isn't working yet, and you just want it to stop +complaining, you can mark it as "skipped". The test or tests won't be run, and, +if you supply a reason why, that reason will be printed. In general, skipping +tests indicates that they should run but is temporarily not working. If they're +fundamentally incompatible with a platform, [`@TestOn`/`testOn`][TestOn] +should be used instead. + +[TestOn]: #restricting-tests-to-certain-platforms + +To skip a test suite, put a `@Skip` annotation at the top of the file: + +```dart +@Skip('currently failing (see issue 1234)') + +import 'package:test/test.dart'; + +void main() { + // ... +} +``` + +The string you pass should describe why the test is skipped. You don't have to +include it, but it's a good idea to document why the test isn't running. + +Groups and individual tests can be skipped by passing the `skip` parameter. This +can be either `true` or a String describing why the test is skipped. For +example: + +```dart +import 'package:test/test.dart'; + +void main() { + group('complicated algorithm tests', () { + // ... + }, skip: "the algorithm isn't quite right"); + + test('error-checking test', () { + // ... + }, skip: 'TODO: add error-checking.'); +} +``` + +### Timeouts + +By default, tests will time out after 30 seconds of inactivity. The timeout +applies to deadlocks or cases where the test stops making progress, it does not +ensure that an overall test case or test suite completes within any set time. + +Timeouts can be configured on a per-test, -group, or -suite basis. To change the +timeout for a test suite, put a `@Timeout` annotation at the top of the file: + +```dart +@Timeout(Duration(seconds: 45)) + +import 'package:test/test.dart'; + +void main() { + // ... +} +``` + +In addition to setting an absolute timeout, you can set the timeout relative to +the default using `@Timeout.factor`. For example, `@Timeout.factor(1.5)` will +set the timeout to one and a half times as long as the default—45 seconds. + +Timeouts can be set for tests and groups using the `timeout` parameter. This +parameter takes a `Timeout` object just like the annotation. For example: + +```dart +import 'package:test/test.dart'; + +void main() { + group('slow tests', () { + // ... + + test('even slower test', () { + // ... + }, timeout: Timeout.factor(2)); + }, timeout: Timeout(Duration(minutes: 1))); +} +``` + +Nested timeouts apply in order from outermost to innermost. That means that +"even slower test" will take two minutes to time out, since it multiplies the +group's timeout by 2. + +### Platform-Specific Configuration + +Sometimes a test may need to be configured differently for different platforms. +Windows might run your code slower than other platforms, or your DOM +manipulation might not work right on Safari yet. For these cases, you can use +the `@OnPlatform` annotation and the `onPlatform` named parameter to `test()` +and `group()`. For example: + +```dart +@OnPlatform({ + // Give Windows some extra wiggle-room before timing out. + 'windows': Timeout.factor(2) +}) + +import 'package:test/test.dart'; + +void main() { + test('do a thing', () { + // ... + }, onPlatform: { + 'safari': Skip('Safari is currently broken (see #1234)') + }); +} +``` + +Both the annotation and the parameter take a map. The map's keys are [platform +selectors](#platform-selectors) which describe the platforms for which the +specialized configuration applies. Its values are instances of some of the same +annotation classes that can be used for a suite: `Skip` and `Timeout`. A value +can also be a list of these values. + +If multiple platforms match, the configuration is applied in order from first to +last, just as they would in nested groups. This means that for configuration +like duration-based timeouts, the last matching value wins. + +You can also set up global platform-specific configuration using the +[package configuration file][configuring platforms]. + +[configuring platforms]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-platforms + +### Tagging Tests + +Tags are short strings that you can associate with tests, groups, and suites. +They don't have any built-in meaning, but they're very useful nonetheless: you +can associate your own custom configuration with them, or you can use them to +easily filter tests so you only run the ones you need to. + +Tags are defined using the `@Tags` annotation for suites and the `tags` named +parameter to `test()` and `group()`. For example: + +```dart +@Tags(['browser']) + +import 'package:test/test.dart'; + +void main() { + test('successfully launches Chrome', () { + // ... + }, tags: 'chrome'); + + test('launches two browsers at once', () { + // ... + }, tags: ['chrome', 'firefox']); +} +``` + +If the test runner encounters a tag that wasn't declared in the +[package configuration file][configuring tags], it'll print a warning, so be +sure to include all your tags there. You can also use the file to provide +default configuration for tags, like giving all `browser` tests twice as much +time before they time out. + +[configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags + +Tests can be filtered based on their tags by passing command line flags. The +`--tags` or `-t` flag will cause the test runner to only run tests with the +given tags, and the `--exclude-tags` or `-x` flag will cause it to only run +tests *without* the given tags. These flags also support +[boolean selector syntax]. For example, you can pass `--tags "(chrome || +firefox) && !slow"` to select quick Chrome or Firefox tests. + +Note that tags must be valid Dart identifiers, although they may also contain +hyphens. + +### Whole-Package Configuration + +For configuration that applies across multiple files, or even the entire +package, `test` supports a configuration file called `dart_test.yaml`. At its +simplest, this file can contain the same sort of configuration that can be +passed as command-line arguments: + +```yaml +# This package's tests are very slow. Double the default timeout. +timeout: 2x + +# This is a browser-only package, so test on chrome by default. +platforms: [chrome] +``` + +The configuration file sets new defaults. These defaults can still be overridden +by command-line arguments, just like the built-in defaults. In the example +above, you could pass `--platform firefox` to run on Firefox. + +A configuration file can do much more than just set global defaults. See +[the full documentation][package config] for more details. + +[package config]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md + +### Compiler flags + +The test runner does not support general purpose flags to control compilation +such as `-D` defines or flags like `--no-sound-null-safety`. In most cases it is +preferable to avoid writing tests that depend on the fine-grained compiler +configuration. For instance to choose between sound and unsound null safety, +prefer to choose a language version for each test which has the desired behavior +by default - choose a language version below `2.12` to disable sound null +safety, and a language version above `2.12` to enable sound null safety. When +fine-grained configuration is unavoidable, the approach varies by platform. + +Compilation for browser and node tests can be configured by passing arguments to +`dart compile js` with `--dart2js-args` options. + +Fine-grained compilation configuration is not supported for the VM. Any +configuration which impacts runtime behavior for the entire VM, such as `-D` +defines (when used for non-const values) and runtime behavior experiments, will +influence both the test runner and the isolates spawned to run test suites. +Experiments which are breaking may cause incompatibilities with the test runner. +These may be specified with a `DART_VM_OPTIONS` environment variable when +running with `pub run test`, or by passing them to the `dart` command before the +`test` subcommand when using `dart test`. + +## Debugging + +Tests can be debugged interactively using platforms' built-in development tools. +Tests running on browsers can use those browsers' development consoles to inspect +the document, set breakpoints, and step through code. Those running on the Dart +VM use [Dart DevTools][devtools]. + +[devtools]: https://dart.dev/tools/dart-devtools + +The first step when debugging is to pass the `--pause-after-load` flag to the +test runner. This pauses the browser after each test suite has loaded, so that +you have time to open the development tools and set breakpoints. For the Dart VM +it will print the remote debugger URL. + +Once you've set breakpoints, either click the big arrow in the middle of the web +page or press Enter in your terminal to start the tests running. When you hit a +breakpoint, the runner will open its own debugging console in the terminal that +controls how tests are run. You can type "restart" there to re-run your test as +many times as you need to figure out what's going on. + +Normally, browser tests are run in hidden iframes. However, when debugging, the +iframe for the current test suite is expanded to fill the browser window so you +can see and interact with any HTML it renders. Note that the Dart animation may +still be visible behind the iframe; to hide it, just add a `background-color` to +the page's HTML. + +## Browser/VM Hybrid Tests + +Code that's written for the browser often needs to talk to some kind of server. +Maybe you're testing the HTML served by your app, or maybe you're writing a +library that communicates over WebSockets. We call tests that run code on both +the browser and the VM **hybrid tests**. + +Hybrid tests use one of two functions: [`spawnHybridCode()`] and +[`spawnHybridUri()`]. Both of these spawn Dart VM +[isolates][dart:isolate] that can import `dart:io` and other VM-only libraries. +The only difference is where the code from the isolate comes from: +`spawnHybridCode()` takes a chunk of actual Dart code, whereas +`spawnHybridUri()` takes a URL. They both return a [`StreamChannel`] that +communicates with the hybrid isolate. For example: + +[`spawnHybridCode()`]: https://pub.dev/documentation/test/latest/test/spawnHybridCode.html + +[`spawnHybridUri()`]: https://pub.dev/documentation/test/latest/test/spawnHybridUri.html + +[dart:isolate]: https://api.dart.dev/stable/dart-isolate/dart-isolate-library.html + +[`StreamChannel`]: https://pub.dev/documentation/stream_channel/latest/stream_channel/StreamChannel-class.html + +```dart +// ## test/web_socket_server.dart + +// The library loaded by spawnHybridUri() can import any packages that your +// package depends on, including those that only work on the VM. +import 'package:shelf/shelf_io.dart' as io; +import 'package:shelf_web_socket/shelf_web_socket.dart'; +import 'package:stream_channel/stream_channel.dart'; + +// Once the hybrid isolate starts, it will call the special function +// hybridMain() with a StreamChannel that's connected to the channel +// returned spawnHybridCode(). +hybridMain(StreamChannel channel) async { + // Start a WebSocket server that just sends "hello!" to its clients. + var server = await io.serve(webSocketHandler((webSocket, _) { + webSocket.sink.add('hello!'); + }), 'localhost', 0); + + // Send the port number of the WebSocket server to the browser test, so + // it knows what to connect to. + channel.sink.add(server.port); +} + + +// ## test/web_socket_test.dart + +@TestOn('browser') + +import 'dart:html'; + +import 'package:test/test.dart'; + +void main() { + test('connects to a server-side WebSocket', () async { + // Each spawnHybrid function returns a StreamChannel that communicates with + // the hybrid isolate. You can close this channel to kill the isolate. + var channel = spawnHybridUri('web_socket_server.dart'); + + // Get the port for the WebSocket server from the hybrid isolate. + var port = await channel.stream.first; + + var socket = WebSocket('ws://localhost:$port'); + var message = await socket.onMessage.first; + expect(message.data, equals('hello!')); + }); +} +``` + + + +**Note**: If you write hybrid tests, be sure to add a dependency on the +`stream_channel` package, since you're using its API! + +## Support for Other Packages + +### `build_runner` + +If you are using `package:build_runner` to build your package, then you will +need a dependency on `build_test` in your `dev_dependencies`, and then you can +use the `pub run build_runner test` command to run tests. + +To supply arguments to `package:test`, you need to separate them from your build +args with a `--` argument. For example, running all web tests in release mode +would look like this `pub run build_runner test --release -- -p vm`. + +### `term_glyph` + +The [`term_glyph`] package provides getters for Unicode glyphs with +ASCII alternatives. `test` ensures that it's configured to produce ASCII when +the user is running on Windows, where Unicode isn't supported. This ensures that +testing libraries can use Unicode on POSIX operating systems without breaking +Windows users. + +[`term_glyph`]: https://pub.dev/packages/term_glyph + +## Further Reading + +Check out the [API docs] for detailed information about all the functions +available to tests. + +[API docs]: https://pub.dev/documentation/test/latest/ + +The test runner also supports a machine-readable JSON-based reporter. This +reporter allows the test runner to be wrapped and its progress presented in +custom ways (for example, in an IDE). See [the protocol documentation][json] for +more details. + +[json]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/json_reporter.md
diff --git a/pkgs/test/bin/test.dart b/pkgs/test/bin/test.dart new file mode 100644 index 0000000..0b40282 --- /dev/null +++ b/pkgs/test/bin/test.dart
@@ -0,0 +1,5 @@ +// Copyright (c) 2015, 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. + +export 'package:test/src/executable.dart';
diff --git a/pkgs/test/dart_test.yaml b/pkgs/test/dart_test.yaml new file mode 100644 index 0000000..49af036 --- /dev/null +++ b/pkgs/test/dart_test.yaml
@@ -0,0 +1,55 @@ +# Disallow duplicate test names in this package +allow_duplicate_test_names: false + +# Fold frames from helper packages we use in our tests, but not from test +# itself. +fold_stack_frames: + except: + - shelf_test_handler + - stream_channel + - test_descriptor + - test_process + +presets: + # "-P terse-trace" folds frames from test's implementation to make the output + # less verbose when + terse-trace: + fold_stack_frames: + except: [test] + +tags: + browser: + timeout: 2x + + # Browsers can sometimes randomly time out while starting, especially on + # Travis which is pretty slow. Don't retry locally because it makes + # debugging more annoying. + presets: {travis: {retry: 3}} + + dart2js: + add_tags: [browser] + timeout: 2x + + firefox: + add_tags: [dart2js] + test_on: linux + chrome: {add_tags: [dart2js]} + + safari: + add_tags: [dart2js] + test_on: mac-os + + edge: + add_tags: [dart2js] + test_on: windows + + # Tests that run pub. These tests may need to be excluded when there are local + # dependency_overrides. + pub: + timeout: 2x + + # Tests that use Node.js. These tests may need to be excluded on systems that + # don't have Node installed. + node: + timeout: 2x + test_on: linux
diff --git a/pkgs/test/doc/architecture.md b/pkgs/test/doc/architecture.md new file mode 100644 index 0000000..c378e0c --- /dev/null +++ b/pkgs/test/doc/architecture.md
@@ -0,0 +1,236 @@ +# Test Package Architecture + +* [Code Organization](#code-organization) + * [Frontend](#frontend) + * [Backend](#backend) + * [Runner](#runner) +* [Lifecycle of a Test Run](#lifecycle-of-a-test-run) + * [Loading a Suite on the VM](#loading-a-suite-on-the-vm) + * [Loading a Suite in the Browser](#loading-a-suite-in-the-browser) + +## Code Organization + +From a user's perspective, the test package provides two main pieces of +functionality: an API for defining tests, and a command-line tool to run those +tests. The structure of the package reflects this division. The code is divided +into three main sections: the frontend, the backend, and the runner. + +### Frontend + +The [`lib/src/frontend`][frontend] directory contains APIs that are exposed to +the user when they import `package:test/test.dart`. This includes core functions +such as `expect()` and `expectAsync()`, test-specific matchers such as +`throwsA()` and `prints()`, and annotation classes such as `TestOn` and +`Timeout`. The functions that define the top-level structure of the test, such +as `test()` and `group()`, are defined in `lib/test.dart`, but they can be +thought of as frontend functions as well. + +[frontend]: https://github.com/dart-lang/test/tree/master/lib/src/frontend + +The frontend communicates with the backend using zone-scoped getters. +[`Invoker.current`][Invoker] provides access to the current test case to +matchers like [`completion()`][completion], for example to control when +it completes. Structural functions use [`Declarer.current`][Declarer] to +gradually build up an in-memory representation of a test suite. The runner is in +charge of setting up these variables, but the frontend never communicates with +the runner directly. + +[Invoker]: https://github.com/dart-lang/test/blob/master/lib/src/backend/invoker.dart +[completion]: https://pub.dev/documentation/matcher/latest/expect/completion.html +[Declarer]: https://github.com/dart-lang/test/blob/master/lib/src/backend/declarer.dart + +### Backend + +The [`lib/src/backend`][backend] directory contains classes that represent the +in-memory structure of a test suite. A [`Suite`][Suite] represents a single test +file, and class contains a tree of [`Group`][Group]s, each of which contains +many [`Test`][Test]s. These classes are built using a [`Declarer`][Declarer]. + +[backend]: https://github.com/dart-lang/test/tree/master/lib/src/backend +[Suite]: https://github.com/dart-lang/test/blob/master/lib/src/backend/suite.dart +[Group]: https://github.com/dart-lang/test/blob/master/lib/src/backend/group.dart +[Test]: https://github.com/dart-lang/test/blob/master/lib/src/backend/test.dart + +The backend also contains the [`Invoker`][Invoker], which is responsible for +actually running an individual test case—including tracking how many outstanding +asynchronous callbacks are pending, handling exceptions, and timing out the test +if it takes too long. The `Invoker` provides information about the status of a +running test as streams and futures on a [`LiveTest`][LiveTest] object. + +[LiveTest]: https://github.com/dart-lang/test/blob/master/lib/src/backend/live_test.dart + +The backend provides a bridge between the frontend and the runner. The runner +sets up the `Declarer` and starts the `Invoker`, which the frontend functions +then communicate with directly. + +### Runner + +The [`lib/src/runner`][runner] directory contains the code that's executed when +`dart test` is invoked. It's in charge of locating test files, loading them, +executing them, and communicating their results to the user. It's also by far +the biggest section. For more information on the runner architecture, see +[Lifecycle of a Test Run](#lifecycle-of-a-test-suite) below. + +[runner]: https://github.com/dart-lang/test/tree/master/lib/src/runner + +## Lifecycle of a Test Run + +To understand generally how the test runner works, let's look at an example run. +When the user first invokes `dart test`, the command-line arguments and +[configuration files][] are combined into a single +[`Configuration`][Configuration] object which is passed into the +[`Runner`][Runner] class. The `Runner` is mostly just glue: it starts up the +various components necessary for a test run, and connects them to one another. +It's also in charge of handling certain `Configuration` flags. + +[configuration files]: https://github.com/dart-lang/test/blob/master/doc/configuration.md +[Configuration]: https://github.com/dart-lang/test/tree/master/lib/src/runner/configuration.dart +[Runner]: https://github.com/dart-lang/test/tree/master/lib/src/runner.dart + +The first thing the runner starts is the [`Engine`][Engine]. The engine iterates +through a test suite's tests and invokes them in order. It knows how to handle +set-up and tear-down functions, and how to combine the output of multiple test +suites running concurrently. It exposes its progress through a collection of +getters and streams that provide access to individual [`LiveTest`][LiveTest]s. + +[Engine]: https://github.com/dart-lang/test/tree/master/lib/src/runner/engine.dart + +The runner then passes the `Engine` to a [`Reporter`][Reporter], which listens +to the `Engine`'s streams and exposes the information there to the user, usually +by printing human-readable text. [`CompactReporter`][CompactReporter] is the +default on Posix platforms, but others may be selected based on the +`Configuration`. Nearly everything the user sees comes through the reporter. + +[Reporter]: https://github.com/dart-lang/test/tree/master/lib/src/runner/reporter.dart +[CompactReporter]: https://github.com/dart-lang/test/tree/master/lib/src/runner/reporter/compact.dart + +The `Engine` and `Reporter` can't do much of anything, though, without any test +suites to run. The next step is to load those suites. The [`Loader`][Loader] is +in charge of this part. It takes in file or directory paths and finds all the +test files they contain—by default any files matching `*_test.dart`. It then +proceeds to load each file on all the platforms specified in the `Configuration` +that's also supported by the test suite. + +[Loader]: https://github.com/dart-lang/test/tree/master/lib/src/runner/loader.dart + +The specifics of loading suites differs based on whether the platform is a +browser or the Dart VM. I'll cover each platform below, but for now let's stick +to what they have in common. Every platform will emit a +[`LoadSuite`][LoadSuite], which is a synthetic [`Suite`][Suite] containing a +single test that, when invoked, produces the actual `Suite` defined in the test +file. + +[LoadSuite]: https://github.com/dart-lang/test/tree/master/lib/src/runner/load_suite.dart + +Wrapping the loading process in a synthetic `Suite` gives us the very useful +invariant that *all test errors occur within a `Suite`*. Loading can fail in all +sorts of ways—the code might not compile, the `main()` method might throw, the +browser might not be installed, and so on. Locating those errors within a +`Suite` means that the `Engine` and `Reporter`, which already know how to deal +with test errors, can deal with load errors in exactly the same way. It makes +the load process a little more complex, but it makes everything else a lot +cleaner. + +Once a `Suite` has been loaded, the runner does a little post-processing to make +sure the `Configuration` is handled properly. It filters out tests whose tags +don't match the `--tags` flag, or whose names don't match the `--name` flag. +Then it passes the resulting `Suite`s on to the `Engine` and they begin to run. + +### Loading a Suite on the VM + +Let's start with looking at how suites are loaded on the Dart VM, since the +process is substantially simpler than loading them on a browser. This loading is +handled by the [`VMPlatform`][VMPlatform], which extends the +[`PlatformPlugin`][PlatformPlugin] class. [Eventually][issue 49], we plan to +support a user-accessible platform plugin API, so we model platforms as plugins +to prepare for that. + +[VMPlatform]: https://github.com/dart-lang/test/tree/master/lib/src/runner/vm/platform.dart +[PlatformPlugin]: https://github.com/dart-lang/test/tree/master/lib/src/runner/plugin/platform.dart +[issue 49]: https://github.com/dart-lang/test/issues/49 + +In its simplest form, a `PlatformPlugin`'s responsibility is just to create a +[`StreamChannel`][StreamChannel] that connects the test runner to a remote +isolate—everything else is handled by helper functions. The `VMPlatform` uses +[`Isolate`][Isolate]s to dynamically load its test suites, and then communicates +with them using an [`IsolateChannel`][IsolateChannel]. It passes in a `data:` +URI containing Dart code that imports the user's code, and runs that code in the +context of the [`serializeSuite()`][remote platform helpers] helper, and the +`PlatformPlugin` superclass deserializes it on the other side using +[`deserializeSuite()`][platform helpers]. + +[StreamChannel]: https://pub.dev/packages/stream_channel +[Isolate]: https://api.dart.dev/stable/dart-isolate/Isolate-class.html +[IsolateChannel]: https://pub.dev/documentation/stream_channel/latest/stream_channel/IsolateChannel-class.html +[remote platform helpers]: https://github.com/dart-lang/test/tree/master/lib/src/runner/plugin/remote_platform_helpers.dart +[platform helpers]: https://github.com/dart-lang/test/tree/master/lib/src/runner/plugin/platform_helpers.dart + +When a test suite is serialized and deserialized, it's not just converted to and +from some static representation like JSON. The [`Engine`][Engine] needs +fine-grained control over the remote suite, and the [`Reporter`][Reporter] needs +fine-grained access to the [`LiveTest`][LiveTest]s it emits. To make this work, +the helper functions use the [`MultiChannel`][MultiChannel] class to tunnel +streams for each test through the main `IsolateChannel`. Each test has its own +virtual channel that gets a message when the test runner calls +[`Test.load()`][Test], and that sends messages back to indicate the progress of +the test. + +Information about these virtual channels, as well as test names and metadata, +are bundled up into a JSON object and sent over the `IsolateChannel` to be +deserialized. The deserialization process then converts them into +[`RunnerTest`][RunnerTest]s within a [`RunnerSuite`][RunnerSuite], which the +`Engine` can then run just like normal `Test`s in a normal [`Suite`][Suite]. + +[MultiChannel]: https://pub.dev/documentation/stream_channel/latest/stream_channel/MultiChannel-class.html +[RunnerTest]: https://github.com/dart-lang/test/tree/master/lib/src/runner/runner_test.dart +[RunnerSuite]: https://github.com/dart-lang/test/tree/master/lib/src/runner/runner_suite.dart + +### Loading a Suite in the Browser + +The [`BrowserPlatform`][BrowserPlatform] class also extends +[`PlatformPlugin`][PlatformPlugin], but rather than just emitting a +[`StreamChannel`][StreamChannel] and letting the plugin helpers do the rest, it +takes more control over the loading process. It emits its own +[`RunnerSuite`][RunnerSuite], which allows it to expose its own +[`Environment`][Environment] to enable debugging. + +[BrowserPlatform]: https://github.com/dart-lang/test/tree/master/lib/src/runner/browser/platform.dart +[Environment]: https://github.com/dart-lang/test/tree/master/lib/src/runner/environment.dart + +Whereas the [`VMPlatform`][VMPlatform] loads each separate suite in isolation, +the `BrowserPlatform` shares a substantial amount of resources between suites. +All suites load their code from a single HTTP server, which is managed by the +platform. This server provides access to compiled JavaScript for other browsers, +and to HTML files that bootstrap the tests. + +In addition to sharing a server, when multiple suites are loaded for the same +browser, they all share a tab within that browser. Each separate browser is +controlled by its own [`BrowserManager`][BrowserManager], which uses +`WebSocket`s to communicate with Dart code running in the main frame—also known +as [the host][host]. + +[BrowserManager]: https://github.com/dart-lang/test/tree/master/lib/src/runner/browser/browser_manager.dart +[host]: https://github.com/dart-lang/test/tree/master/lib/src/runner/browser/static/host.dart + +Each browser is spawned with a tab pointing to +`packages/test/src/runner/browser/static/index.html`, the host page. The host's +code then opens a `WebSocket` connection to a dynamically-generated URL. This +URL tells the `BrowserPlatform` which `BrowserManager` to send the `WebSocket` +to. + +To load a suite for this browser, the `BrowserPlatform` passes the URL for that +suite's HTML file to the `BrowserManager`, which in turn sends it down to the +host page. The host opens this HTML in an iframe, opens a +[`StreamChannel`][StreamChannel] with this iframe using +[`Window.postMessage()`][Window.postMessage]. It then tunnels this channel +through the `WebSocket` connection, again using [`MultiChannel`][MultiChannel], +so that the `BrowserManager` has a direct line to the iframe where the tests are +defined. + +[Window.postMessage]: https://api.dart.dev/stable/dart-html/Window/postMessage.html + +From this point forward the process is similar to `VMPlatform`. The iframe +serializes its test suite using [`serializeSuite()`][remote platform helpers], +and the `BrowserManager` deserializes it using +[`deserializeSuite()`][platform helpers]. It's then forwarded to the `Loader` +via the `BrowserPlatform`.
diff --git a/pkgs/test/doc/configuration.md b/pkgs/test/doc/configuration.md new file mode 100644 index 0000000..6541b97 --- /dev/null +++ b/pkgs/test/doc/configuration.md
@@ -0,0 +1,890 @@ +Each package may include a configuration file that applies to the package as a +whole. This file can be used to provide custom defaults for various options, to +define configuration for multiple files, and more. + +The file is named `dart_test.yaml` and lives at the root of the package, next to +the package's pubspec. Like the pubspec, it's a [YAML][] file. Here's an +example: + +[YAML]: http://yaml.org/ + +```yaml +# This package's tests are very slow. Double the default timeout. +timeout: 2x + +# This is a browser-only package, so test on chrome by default. +platforms: [chrome] + +tags: + # Integration tests are even slower, so increase the timeout again. + integration: {timeout: 2x} + + # Sanity tests are quick and verify that nothing is obviously wrong. Declaring + # the tag allows us to tag tests with it without getting warnings. + sanity: +``` + +* [Test Configuration](#test-configuration) + * [`timeout`](#timeout) + * [`ignore-timeouts`](#ignore-timeouts) + * [`verbose_trace`](#verbose_trace) + * [`chain_stack_traces`](#chain_stack_traces) + * [`js_trace`](#js_trace) + * [`skip`](#skip) + * [`retry`](#retry) + * [`test_on`](#test_on) + * [`allow_test_randomization`](#allow_test_randomization) + * [`allow_duplicate_test_names`](#allow_duplicate_test_names) +* [Runner Configuration](#runner-configuration) + * [`include`](#include) + * [`paths`](#paths) + * [`filename`](#filename) + * [`names`](#names) + * [`plain_names`](#plain_names) + * [`include_tags`](#include_tags) + * [`exclude_tags`](#exclude_tags) + * [`platforms`](#platforms) + * [`concurrency`](#concurrency) + * [`pause_after_load`](#pause_after_load) + * [`run_skipped`](#run_skipped) + * [`reporter`](#reporter) + * [`file_reporters`](#file_reporters) + * [`fold_stack_frames`](#fold_stack_frames) + * [`custom_html_template_path`](#custom_html_template_path) +* [Configuring Tags](#configuring-tags) + * [`tags`](#tags) + * [`add_tags`](#add_tags) +* [Configuring Platforms](#configuring-platforms) + * [`on_os`](#on_os) + * [`on_platform`](#on_platform) + * [`override_platforms`](#override_platforms) + * [`define_platforms`](#define_platforms) + * [Browser and Node.js Settings](#browser-and-nodejs-settings) + * [`arguments`](#arguments) + * [`executable`](#executable) + * [`headless`](#headless) +* [Configuration Presets](#configuration-presets) + * [`presets`](#presets) + * [`add_presets`](#add_presets) +* [Global Configuration](#global-configuration) + +## Test Configuration + +There are two major categories of configuration field: "test" and "runner". Test +configuration controls how individual tests run, while +[runner configuration](#runner-configuration) controls the test runner as a +whole. Both types of fields may be used at the top level of a configuration +file. However, because different tests can have different test configuration, +only test configuration fields may be used to [configure tags](#tags) or +[platforms](#on_platform). + +### `timeout` + +This field indicates how much time the test runner should allow a test to remain +inactive before it considers that test to have failed. It has three possible +formats: + +* The string "none" indicates that tests should never time out. + +* A number followed by a unit abbreviation indicates an exact time. For example, + "1m" means a timeout of one minute, and "30s" means a timeout of thirty + seconds. Multiple numbers can be combined, as in "1m 30s". + +* A number followed by "x" indicates a multiple. This is applied to the default + value of 30s. + +```yaml +timeout: 1m +``` + +### `ignore-timeouts` + +This field disables all timeouts for all tests. This can be useful when debugging, so tests don't time out during debug sessions. It defaults to `false`. + +```yaml +ignore-timeouts: true +``` + +### `verbose_trace` + +This boolean field controls whether or not stack traces caused by errors are +trimmed to remove internal stack frames. This includes frames from the Dart core +libraries, the [`stack_trace`][stack_trace] package, and the `test` package +itself. It defaults to `false`. + +[stack_trace]: https://pub.dev/packages/stack_trace + +```yaml +verbose_trace: true +``` + +### `chain_stack_traces` + +This boolean field controls whether or not stack traces are chained. +Disabling [`stack trace chaining`][stack trace chaining] will improve +performance for heavily asynchronous code at the cost of debuggability. + +[stack trace chaining]: https://github.com/dart-lang/stack_trace/blob/master/README.md#stack-chains + +```yaml +chain_stack_traces: false +``` + +### `js_trace` + +This boolean field controls whether or not stack traces caused by errors that +occur while running Dart compiled to JS are converted back to Dart style. This +conversion uses the source map generated by `dart2js` to approximate the +original Dart line, column, and in some cases member name for each stack frame. +It defaults to `false`. + +```yaml +js_trace: true +``` + +### `skip` + +This field controls whether or not tests are skipped. It's usually applied to +[specific tags](#configuring-tags) rather than used at the top level. Like the +`skip` parameter for [`test()`][test], it can either be a boolean indicating +whether the tests are skipped or a string indicating the reason they're skipped. + +[test]: https://pub.dev/documentation/test/latest/test/test.html + +```yaml +tags: + chrome: + skip: "Our Chrome launcher is busted. See issue 1234." +``` + +This field is not supported in the +[global configuration file](#global-configuration). + +### `retry` + +This int field controls how many times a test is retried upon failure. + +```yaml +tags: + chrome: + retry: 3 # Retry chrome failures 3 times. +``` + +This field is not supported in the +[global configuration file](#global-configuration). + +### `test_on` + +This field declares which platforms a test supports. It takes a +[platform selector][platform selectors] and only allows tests to run on +platforms that match the selector. It's often used with +[specific tags](#configuring-tags) to ensure that certain features will only be +tested on supported platforms. + +[platform selectors]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors + +```yaml +tags: + # Test on browsers other than firefox + some_feature: {test_on: "browser && !firefox"} +``` + +The field can also be used at the top level of the configuration file to +indicate that the entire package only supports a particular platform. If someone +tries to run the tests on an unsupported platform, the runner will print a +warning and skip that platform. + +```yaml +# This package uses dart:io. +test_on: vm +``` + +This field is not supported in the +[global configuration file](#global-configuration). + +### `allow_test_randomization` + +This can be used to disable test randomization for certain tests, regardless +of the `--test-randomize-ordering-seed` configuration. + +This is typically useful when a subset of your tests are order dependent, but +you want to run the other ones with randomized ordering. + +```yaml +tags: + doNotRandomize: + allow_test_randomization: false +``` + +### `allow_duplicate_test_names` + +This can be used to allow multiple tests in the same suite to have the same +name. This is disabled by default because it is usually an indication of a +mistake, and it causes problems with IDE integrations which run tests by name. + +It can be disabled for all tests: + +```yaml +allow_duplicate_test_names: true +``` + +Or for tagged tests only (useful if migrating to enable this): + +```yaml +tags: + allowDuplicates: + allow_duplicate_test_names: true +``` + +It cannot be globally enabled or configured on the command line - this would +make it so that tests might pass on one users machine but not anothers which +should be avoided. + +## Runner Configuration + +Unlike [test configuration](#test-configuration), runner configuration affects +the test runner as a whole rather than individual tests. It can only be used at +the top level of the configuration file. + +### `include` + +This field loads another configuration file. It's useful for repositories that +contain multiple packages and want to share configuration among them. It takes a +(usually relative) `file:` URL. + +If you have a repository with the following structure: + +``` +repo/ + dart_test_base.yaml + package/ + test/ + dart_test.yaml + pubspec.yaml +``` + +```yaml +# repo/dart_test_base.yaml +filename: "test_*.dart" +``` + +```yaml +# repo/package/dart_test.yaml +include: ../dart_test_base.yaml +``` + +...tests in the `package` directory will use configuration from both +`dart_test_base.yaml` and `dart_test.yaml`. + +The local configuration file's fields take precedence over those from an +included file, so it's possible to override a base configuration. + +### `paths` + +This field indicates the default paths that the test runner should run. These +paths are usually directories, although single filenames may be used as well. +Paths must be relative, and they must be in URL format so that they're +compatible across operating systems. This defaults to `[test]`. + +```yaml +paths: [dart/test] + +paths: +- test/instantaneous +- test/fast +- test/middling +``` + +This field is not supported in the +[global configuration file](#global-configuration). + +### `filename` + +This field indicates the filename pattern that the test runner uses to find test +files in directories. All files in directories passed on the command line (or in +directories in [`paths`](#paths), if none are passed) whose basenames match this +pattern will be loaded and run as tests. + +This supports the full [glob syntax][]. However, since it's only compared +against a path's basename, path separators aren't especially useful. It defaults +to `"*_test.dart"`. + +```yaml +filename: "test_*.dart" +``` + +[glob syntax]: https://github.com/dart-lang/glob#syntax + +This field is not supported in the +[global configuration file](#global-configuration). + +### `names` + +This field causes the runner to only run tests whose names match the given +regular expressions. A test's name must match *all* regular expressions in +`names`, as well as containing all strings in [`plain_names`](#plain_names), in +order to be run. + +This is usually used in a [preset](#configuration-presets) to make it possible +to quickly select a given set of tests. + +```yaml +presets: + # Pass "-P chrome" to run only Chrome tests. + chrome: + names: + - "^browser:" + - "[Cc]hrome" +``` + +This field is not supported in the +[global configuration file](#global-configuration). + +### `plain_names` + +This field causes the runner to only run tests whose names contain the given +strings. A test's name must contain *all* strings in `plain_names`, as well as +matching all regular expressions in [`names`](#names), in order to be run. + +This is usually used in a [preset](#configuration-presets) to make it possible +to quickly select a given set of tests. + +```yaml +presets: + # Pass "-P feature" to run only tests with "feature name" in the name. + feature: + plain_names: + - "feature name" +``` + +This field is not supported in the +[global configuration file](#global-configuration). + +### `include_tags` + +This field causes the runner to only run tests whose tags match the given +[boolean selector][]. If both `include_tags` and [`exclude_tags`](#exclude_tags) +are used, the exclusions take precedence. + +[boolean selector]: https://github.com/dart-lang/boolean_selector/blob/master/README.md + +This is usually used in a [preset](#configuration-preset) to make it possible to +quickly select a set of tests. + +```yaml +presets: + # Pass "-P windowless" to run tests that don't open browser windows. + windowless: + include_tags: !browser || content-shell +``` + +This field is not supported in the +[global configuration file](#global-configuration). + +### `exclude_tags` + +This field causes the runner not to run tests whose tags match the given +[boolean selector][]. If both [`include_tags`](#include_tags) and `exclude_tags` +are used, the exclusions take precedence. + +This is usually used in a [preset](#configuration-preset) to make it possible to +quickly select a set of tests. + +```yaml +presets: + # Pass "-P windowless" to run tests that don't open browser windows. + windowless: + exclude_tags: browser && !content-shell +``` + +This field is not supported in the +[global configuration file](#global-configuration). + +### `platforms` + +This field indicates which platforms tests should run on by default. It allows +the same platform identifiers that can be passed to `--platform`. If multiple +platforms are included, the test runner will default to running tests on all of +them. This defaults to `[vm]`. + +```yaml +platforms: [chrome] + +platforms: +- chrome +- firefox +``` + +### `compilers` + +This field indicates which compilers tests should be compiled with by default. +It allows the same compiler selectors that can be passed to `--compiler`. If +a given platform has no supported compiler configured, it will use its default. + +```yaml +compilers: [source] + +compilers: +- source +``` + +### `concurrency` + +This field indicates the default number of test suites to run in parallel. More +parallelism can improve the overall speed of running tests up to a point, but +eventually it just adds more memory overhead without any performance gain. This +defaults to approximately half the number of processors on the current machine. +If it's set to 1, only one test suite will run at a time. + +```yaml +concurrency: 3 +``` + +### `pause_after_load` + +This field indicates that the test runner should pause for debugging after each +test suite is loaded but before its tests are executed. If it's set, +[`concurrency`](#concurrency) is automatically set to 1 and +[`timeout`](#timeout) is automatically set to `none`. + +This is usually used in a [preset](#configuration-presets). + +```yaml +presets: + # Pass "-P debug" to enable debugging configuration + debug: + pause_after_load: true + exclude_tags: undebuggable + reporter: expanded +``` + +### `run_skipped` + +This field indicates that the test runner should run tests even if they're +marked as skipped. + +This is usually used in a [preset](#configuration-presets). + +```yaml +presets: + # Pass "-P all" to run all tests + debug: + run_skipped: true + paths: ["test/", "extra_test/"] +``` + +### `reporter` + +This field indicates the default reporter to use. It may be set to "compact", +"expanded", or "json" (although why anyone would want to default to JSON is +beyond me). It defaults to "expanded" on Windows and "compact" everywhere else. + +```yaml +reporter: expanded +``` + +This field is not supported in the +[global configuration file](#global-configuration). + +### `file_reporters` + +This field specifies additional reporters to use that will write their output to +a file rather than stdout. It should be a map of reporter names to filepaths. + +```yaml +file_reporters: + json: reports/tests.json +``` + +This field is not supported in the +[global configuration file](#global-configuration). + +### `fold_stack_frames` + +This field controls which packages' stack frames will be folded away +when displaying stack traces. Packages contained in the `except` +option will be folded. If `only` is provided, all packages not +contained in this list will be folded. By default, +frames from the `test` package and the `stream_channel` +package are folded. + +```yaml +fold_stack_frames: + except: + - test + - stream_channel +``` + +Sample stack trace, note the absence of `package:test` +and `package:stream_channel`: +``` +test/sample_test.dart 7:5 main.<fn> +===== asynchronous gap =========================== +dart:async _Completer.completeError +test/sample_test.dart 8:3 main.<fn> +===== asynchronous gap =========================== +dart:async _asyncThenWrapperHelper +test/sample_test.dart 5:27 main.<fn> +``` + +### `custom_html_template_path` + +This field specifies the path of the HTML template file to be used for tests run in an HTML environment. +Any HTML file that is named the same as the test and in the same directory will take precedence over the template. +For more information about the usage of this option see [Providing a custom HTML template](https://github.com/dart-lang/test/blob/master/README.md#providing-a-custom-html-template) + +## Configuring Tags + +### `tags` + +The `tag` field can be used to apply [test configuration](#test-configuration) +to all tests [with a given tag][tagging tests] or set of tags. It takes a map +from tag selectors to configuration maps. These configuration maps are just like +the top level of the configuration file, except that they may not contain +[runner configuration](#runner-configuration). + +[tagging tests]: https://github.com/dart-lang/test/blob/master/README.md#tagging-tests + +```yaml +tags: + # Integration tests need more time to run. + integration: + timeout: 1m +``` + +Tags may also have no configuration associated with them. The test runner prints +a warning whenever it encounters a tag it doesn't recognize, and this the best +way to tell it that a tag exists. + +```yaml +# We occasionally want to use --tags or --exclude-tags on these tags. +tags: + # A test that spawns a browser. + browser: + + # A test that needs Ruby installed. + ruby: +``` + +You can also use [boolean selector syntax][] to define configuration that +involves multiple tags. For example: + +[boolean selector syntax]: https://github.com/dart-lang/boolean_selector/blob/master/README.md + +```yaml +tags: + # Tests that invoke sub-processes tend to be a little slower. + ruby || python: + timeout: 1.5x +``` + +Tag configuration is applied at whatever level the tag appears—so if a group is +tagged as `integration`, its timeout will take precedence over the suite's +timeout but not any tests'. If the group itself had a timeout declared, the +group's explicit timeout would take precedence over the tag. + +If multiple tags appear at the same level, and they have conflicting +configurations, the test runner *does not guarantee* what order they'll be +resolved in. In practice, conflicting configuration is pretty unlikely and it's +easy to just explicitly specify what you want on the test itself. + +This field counts as [test configuration](#test-configuration). It is not +supported in the [global configuration file](#global-configuration). + +### `add_tags` + +This field adds additional tags. It's technically +[test configuration](#test-configuration), but it's usually used in more +specific contexts. For example, when included in a tag's configuration, it can +be used to enable tag inheritance, where adding one tag implicitly adds another +as well. It takes a list of tag name strings. + +```yaml +tags: + # Any test that spawns a browser. + browser: + timeout: 2x + + # Tests that spawn specific browsers. These automatically get the browser tag + # as well. + chrome: {add_tags: [browser]} + firefox: {add_tags: [browser]} + safari: {add_tags: [browser]} + edge: {add_tags: [browser]} +``` + +This field is not supported in the +[global configuration file](#global-configuration). + +## Configuring Platforms + +There are two different kinds of platform configuration. +[Operating system configuration](#on_os) cares about the operating system on +which test runner is running. It sets global configuration for the runner on +particular OSes. [Test platform configuration](#on_platform), on the other hand, +cares about the platform the *test* is running on (like the +[`@OnPlatform` annotation][@OnPlatform]). It sets configuration for particular +tests that are running on matching platforms. + +[@OnPlatform]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-specific-configuration + +### `on_os` + +This field applies configuration when specific operating systems are being used. +It takes a map from operating system identifiers (the same ones that are used in +[platform selectors][]) to configuration maps that are applied on those +operating systems. These configuration maps are just like the top level of the +configuration file, and allow any fields that may be used in the context where +`on_os` was used. + +```yaml +on_os: + windows: + # Both of these are the defaults anyway, but let's be explicit about it. + color: false + runner: expanded + + # My Windows machine is real slow. + timeout: 2x + + # My Linux machine has SO MUCH RAM. + linux: + concurrency: 500 +``` + +This field counts as [test configuration](#test-configuration). If it's used in +a context that only allows test configuration, it may only contain test +configuration. + +### `on_platform` + +This field applies configuration to tests that are run on specific platforms. It +takes a map from [platform selectors][] to configuration maps that are applied +to tests run on those platforms. These configuration maps are just like the top +level of the configuration file, except that they may not contain +[runner configuration](#runner-configuration). + +```yaml +# Our code is kind of slow on Blink and WebKit. +on_platform: + chrome || safari: {timeout: 2x} +``` + +**Note**: operating system names that appear in `on_platform` refer to tests +that are run on the Dart VM under that operating system. To configure all tests +when running on a particular operating system, use [`on_os`](#on_os) instead. + +This field counts as [test configuration](#test-configuration). + +### `override_platforms` + +This field allows you to customize the settings for built-in test platforms. It +takes a map from platform identifiers to settings for those platforms. For example: + +```yaml +override_platforms: + chrome: + # The settings to override for this platform. + settings: + executable: chromium +``` + +This tells the test runner to use the `chromium` executable for Chrome tests. It +calls that executable with the same logic and flags it normally uses for Chrome. + +Each platform can define exactly which settings it supports. All browsers and +Node.js support [the same settings](#browser-and-node-js-settings), but the VM +doesn't support any settings and so can't be overridden. + +### `define_platforms` + +You can define new platforms in terms of old ones using the `define_platforms` +field. This lets you define variants of existing platforms without overriding +the old ones. This field takes a map from the new platform identifiers to +definitions for those platforms. For example: + +```yaml +define_platforms: + # This identifier is used to select the platform with the --platform flag. + chromium: + # A human-friendly name for the platform. + name: Chromium + + # The identifier for the platform that this is based on. + extends: chrome + + # Settings for the new child platform. + settings: + executable: chromium +``` + +Once this is defined, you can run `dart test -p chromium` and it will run +those tests in the Chromium browser, using the same logic it normally uses for +Chrome. You can even use `chromium` in platform selectors; for example, you +might pass `testOn: "chromium"` to declare that a test is Chromium-specific. +User-defined platforms also count as their parents, so Chromium will run tests +that say `testOn: "chrome"` as well. + +Each platform can define exactly which settings it supports. All browsers and +Node.js support [the same settings](#browser-and-node-js-settings), but the VM +doesn't support any settings and so can't be extended. + +This field is not supported in the +[global configuration file](#global-configuration). + +### Browser and Node.js Settings + +All built-in browser platforms, as well as the built-in Node.js platform, +provide the same settings that can be set using +[`define_platforms`](#define_platforms), which control how their executables are +invoked. + +#### `arguments` + +The `arguments` field provides extra arguments to the executable. It takes a +string, and parses it in the same way as the POSIX shell: + +```yaml +override_platforms: + firefox: + settings: + arguments: -headless +``` + +#### `executable` + +The `executable` field tells the test runner where to look for the executable to +use to start the subprocess. It has three sub-keys, one for each supported +operating system, which each take a path or an executable name: + +```yaml +define_platforms: + chromium: + name: Chromium + extends: chrome + + settings: + executable: + linux: chromium + mac_os: /Applications/Chromium.app/Contents/MacOS/Chromium + windows: Chromium\Application\chrome.exe +``` + +Executables can be defined in three ways: + +* As a plain basename, with no path separators. These executables are passed + directly to the OS, which looks them up using the `PATH` environment variable. + +* As an absolute path, which is used as-is. + +* **Only on Windows**, as a relative path. The test runner will look up this + path relative to the `LOCALAPPATA`, `PROGRAMFILES`, and `PROGRAMFILES(X86)` + environment variables, in that order. + +If a platform is omitted, it defaults to using the built-in executable location. + +As a shorthand, you can also define the same executable for all operating +systems: + +```yaml +define_platforms: + chromium: + name: Chromium + extends: chrome + + settings: + executable: chromium +``` + +#### `headless` + +The `headless` field says whether or not to run the browser in headless mode. +It defaults to `true`. It's currently only supported for Chrome: + +```yaml +override_platforms: + chrome: + settings: + headless: false +``` + +Note that headless mode is always disabled when debugging. + +## Configuration Presets + +*Presets* are collections of configuration that can be explicitly selected on +the command-line. They're useful for quickly selecting options that are +frequently used together, for providing special configuration for continuous +integration systems, and for defining more complex logic than can be expressed +directly using command-line arguments. + +Presets can be selected on the command line using the `--preset` or `-P` flag. +Any number of presets can be selected this way; if they conflict, the last one +selected wins. Only presets that are defined in the configuration file may be +selected. + +### `presets` + +This field defines which presets are available to select. It takes a map from +preset names to configuration maps that are applied when those presets are +selected. These configuration maps are just like the top level of the +configuration file, and allow any fields that may be used in the context where +`presets` was used. + +```yaml +presets: + # Use this when you need completely un-munged stack traces. + debug: + verbose_trace: false + js_trace: true + + # Shortcut for running only browser tests. + browser: + paths: + - test/runner/browser +``` + +The `presets` field counts as [test configuration](#test-configuration). It can +be useful to use it in combination with other fields for advanced preset +behavior. + +```yaml +tags: + chrome: + skip: "Our Chrome launcher is busted. See issue 1234." + + # Pass -P force to verify that the launcher is still busted. + presets: {force: {skip: false}} +``` + +### `add_presets` + +This field selects additional presets. It's technically +[runner configuration](#runner-configuration), but it's usually used in more +specific contexts. For example, when included in a preset's configuration, it +can be used to enable preset inheritance, where selecting one preset implicitly +selects another as well. It takes a list of preset name strings. + +```yaml +presets: + # Shortcut for running only browser tests. + browser: + paths: [test/runner/browser] + + # Shortcut for running only Chrome tests. + chrome: + filename: "chrome_*_test.dart" + add_presets: [browser] +``` + +## Global Configuration + +The test runner also supports a global configuration file. On Windows, this +file's local defaults to `%LOCALAPPDATA%\DartTest.yaml`. On Unix, it defaults to +`~/.dart_test.yaml`. It can also be explicitly set using the `DART_TEST_CONFIG` +environment variable. + +The global configuration file supports a subset of the fields supported by the +package-specific configuration file. In general, it doesn't support fields that +are closely tied to the structure of an individual package. Fields that are not +supported in the global configuration file say so in their documentation.
diff --git a/pkgs/test/doc/json_reporter.md b/pkgs/test/doc/json_reporter.md new file mode 100644 index 0000000..be18ddb --- /dev/null +++ b/pkgs/test/doc/json_reporter.md
@@ -0,0 +1,504 @@ +JSON Reporter Protocol +====================== + +The test runner supports a JSON reporter which provides a machine-readable +representation of the test runner's progress. This reporter is intended for use +by IDEs and other tools to present a custom view of the test runner's operation +without needing to parse output intended for humans. + +Note that the test runner is highly asynchronous, and users of this protocol +shouldn't make assumptions about the ordering of events beyond what's explicitly +specified in this document. It's possible for events from multiple tests to be +intertwined, for a single test to emit an error after it completed successfully, +and so on. + +## Usage + +Pass the `--reporter json` command-line flag to the test runner to activate the +JSON reporter. + + dart test --reporter json <path-to-test-file> + +You may also use the `--file-reporter` option to enable the JSON reporter output +to a file, in addition to another reporter writing to stdout. + + dart test --file-reporter json:reports/tests.json <path-to-test-file> + +The JSON stream will be emitted via standard output. It will be a stream of JSON +objects, separated by newlines. + +See `json_reporter.schema.json` for a formal description of the protocol schema. +See `test/runner/json_reporter_test.dart` for some sample output. + +## Compatibility + +The protocol emitted by the JSON reporter is considered part of the public API +of the `test` package, and is subject to its [semantic versioning][semver] +restrictions. In particular: + +[semver]: https://dart.dev/tools/pub/versioning#semantic-versions + +* No new feature will be added to the protocol without increasing the test + package's minor version number. + +* No breaking change will be made to the protocol without increasing the test + package's major version number. + +The following changes are not considered breaking. This is not necessarily a +comprehensive list. + +* Adding a new attribute to an existing object. + +* Adding a new type of any object with a `type` parameter. + +* Adding new test state values. + +## Reading this Document + +Each major type of JSON object used by the protocol is described by a class. +Classes have names which are referred to in this document, but are not used as +part of the protocol. Classes have typed attributes, which refer to the types +and names of attributes in the JSON objects. If an attribute's type is another +class, that refers to a nested object. The special type `List<...>` indicates a +JSON list of the given type. + +Classes can "extend" one another, meaning that the subclass has all the +attributes of the superclass. Concrete subclasses can be distinguished by the +specific value of their `type` attribute. Classes may be abstract, indicating +that only their subclasses will ever be used. + +## Events + +### Event + +``` +abstract class Event { + // The type of the event. + // + // This is always one of the subclass types listed below. + String type; + + // The time (in milliseconds) that has elapsed since the test runner started. + int time; +} +``` + +This is the root class of the protocol. All root-level objects emitted by the +JSON reporter will be subclasses of `Event`. + +### StartEvent + +``` +class StartEvent extends Event { + String type = "start"; + + // The version of the JSON reporter protocol being used. + // + // This is a semantic version, but it reflects only the version of the + // protocol—it's not identical to the version of the test runner itself. + String protocolVersion; + + // The version of the test runner being used. + // + // This is null if for some reason the version couldn't be loaded. + String? runnerVersion; + + // The pid of the VM process running the tests. + int pid; +} +``` + +A single start event is emitted before any other events. It indicates that the +test runner has started running. + +### AllSuitesEvent + +``` +class AllSuitesEvent extends Event { + String type = "allSuites"; + + /// The total number of suites that will be loaded. + int count; +} +``` + +A single suite count event is emitted once the test runner knows the total +number of suites that will be loaded over the course of the test run. Because +this is determined asynchronously, its position relative to other events (except +`StartEvent`) is not guaranteed. + +### SuiteEvent + +``` +class SuiteEvent extends Event { + String type = "suite"; + + /// Metadata about the suite. + Suite suite; +} +``` + +A suite event is emitted before any `GroupEvent`s for groups in a given test +suite. This is the only event that contains the full metadata about a suite; +future events will refer to the suite by its opaque ID. + +### DebugEvent + +``` +class DebugEvent extends Event { + String type = "debug"; + + /// The suite for which debug information is reported. + int suiteID; + + /// The HTTP URL for the Dart Observatory, or `null` if the Observatory isn't + /// available for this suite. + String? observatory; + + /// The HTTP URL for the remote debugger for this suite's host page, or `null` + /// if no remote debugger is available for this suite. + String? remoteDebugger; +} +``` + +A debug event is emitted after (although not necessarily directly after) a +`SuiteEvent`, and includes information about how to debug that suite. It's only +emitted if the `--debug` flag is passed to the test runner. + +Note that the `remoteDebugger` URL refers to a remote debugger whose protocol +may differ based on the browser the suite is running on. You can tell which +protocol is in use by the `Suite.platform` field for the suite with the given +ID. Since the same browser instance is used for multiple suites, different +suites may have the same `host` URL, although only one suite at a time will be +active when `--pause-after-load` is passed. + +### GroupEvent + +``` +class GroupEvent extends Event { + String type = "group"; + + /// Metadata about the group. + Group group; +} +``` + +A group event is emitted before any `TestStartEvent`s for tests in a given +group. This is the only event that contains the full metadata about a group; +future events will refer to the group by its opaque ID. + +This includes the implicit group at the root of each suite, which has a `null` +name. However, it does *not* include implicit groups for the virtual suites +generated to represent loading test files. + +If the group is skipped, a single `TestStartEvent` will be emitted for a test +within the group, followed by a `TestDoneEvent` marked as skipped. The +`group.metadata` field should *not* be used for determining whether a group is +skipped. + +### TestStartEvent + +``` +class TestStartEvent extends Event { + String type = "testStart"; + + // Metadata about the test that started. + Test test; +} +``` + +An event emitted when a test begins running. This is the only event that +contains the full metadata about a test; future events will refer to the test by +its opaque ID. + +If the test is skipped, its `TestDoneEvent` will have `skipped` set to `true`. +The `test.metadata` should *not* be used for determining whether a test is +skipped. + +### MessageEvent + +``` +class MessageEvent extends Event { + String type = "print"; + + // The ID of the test that printed a message. + int testID; + + // The type of message being printed. + String messageType; + + // The message that was printed. + String message; +} +``` + +A `MessageEvent` indicates that a test emitted a message that should be +displayed to the user. The `messageType` field indicates the precise type of +this message. Different message types should be visually distinguishable. + +A message of type "print" comes from a user explicitly calling `print()`. + +A message of type "skip" comes from a test, or a section of a test, being +skipped. A skip message shouldn't be considered the authoritative source that a +test was skipped; the `TestDoneEvent.skipped` field should be used instead. + +### ErrorEvent + +``` +class ErrorEvent extends Event { + String type = "error"; + + // The ID of the test that experienced the error. + int testID; + + // The result of calling toString() on the error object. + String error; + + // The error's stack trace, in the stack_trace package format. + String stackTrace; + + // Whether the error was a TestFailure. + bool isFailure; +} +``` + +A `ErrorEvent` indicates that a test encountered an uncaught error. Note +that this may happen even after the test has completed, in which case it should +be considered to have failed. + +If a test is asynchronous, it may encounter multiple errors, which will result +in multiple `ErrorEvent`s. + +### TestDoneEvent + +``` +class TestDoneEvent extends Event { + String type = "testDone"; + + // The ID of the test that completed. + int testID; + + // The result of the test. + String result; + + // Whether the test's result should be hidden. + bool hidden; + + // Whether the test (or some part of it) was skipped. + bool skipped; +} +``` + +An event emitted when a test completes. The `result` attribute indicates the +result of the test: + +* `"success"` if the test had no errors. + +* `"failure"` if the test had a `TestFailure` but no other errors. + +* `"error"` if the test had an error other than a `TestFailure`. + +If the test encountered an error, the `TestDoneEvent` will be emitted after the +corresponding `ErrorEvent`. + +The `hidden` attribute indicates that the test's result should be hidden and not +counted towards the total number of tests run for the suite. This is true for +virtual tests created for loading test suites, `setUpAll()`, and +`tearDownAll()`. Only successful tests will be hidden. + +Note that it's possible for a test to encounter an error after completing. In +that case, it should be considered to have failed, but no additional +`TestDoneEvent` will be emitted. If a previously-hidden test encounters an +error after completing, it should be made visible. + +### DoneEvent + +``` +class DoneEvent extends Event { + String type = "done"; + + // Whether all tests succeeded (or were skipped). + // + // Will be `null` if the test runner was close before all tests completed + // running. + bool? success; +} +``` + +An event indicating the result of the entire test run. This will be the final +event emitted by the reporter. + +## Other Classes + +### Test + +``` +class Test { + // An opaque ID for the test. + int id; + + // The name of the test, including prefixes from any containing groups. + String name; + + // The ID of the suite containing this test. + int suiteID; + + // The IDs of groups containing this test, in order from outermost to + // innermost. + List<int> groupIDs; + + // The (1-based) line on which the test was defined, or `null`. + int? line; + + // The (1-based) column on which the test was defined, or `null`. + int? column; + + // The URL for the file in which the test was defined, or `null`. + String? url; + + // The (1-based) line in the original test suite from which the test + // originated. + // + // Will only be present if `root_url` is different from `url`. + int? root_line; + + // The (1-based) line on in the original test suite from which the test + // originated. + // + // Will only be present if `root_url` is different from `url`. + int? root_column; + + // The URL for the original test suite in which the test was defined. + // + // Will only be present if different from `url`. + String? root_url; + + // This field is deprecated and should not be used. + Metadata metadata; +} +``` + +A single test case. The test's ID is unique in the context of this test run. +It's used elsewhere in the protocol to refer to this test without including its +full representation. + +Most tests will have at least one group ID, representing the implicit root +group. However, some may not; these should be treated as having no group +metadata. + +The `line`, `column`, and `url` fields indicate the location the `test()` +function was called to create this test. They're treated as a unit: they'll +either all be `null` or they'll all be non-`null`. The URL is always absolute, +and may be a `package:` URL. + +### Suite + +``` +class Suite { + // An opaque ID for the group. + int id; + + // The platform on which the suite is running. + String platform; + + // The path to the suite's file, or `null` if that path is unknown. + String? path; +} +``` + +A test suite corresponding to a loaded test file. The suite's ID is unique in +the context of this test run. It's used elsewhere in the protocol to refer to +this suite without including its full representation. + +A suite's platform is one of the platforms that can be passed to the +`--platform` option, or `null` if there is no platform (for example if the file +doesn't exist at all). Its path is either absolute or relative to the root of +the current package. + +### Group + +``` +class Group { + // An opaque ID for the group. + int id; + + // The name of the group, including prefixes from any containing groups. + String name; + + // The ID of the suite containing this group. + int suiteID; + + // The ID of the group's parent group, unless it's the root group. + int? parentID; + + // The number of tests (recursively) within this group. + int testCount; + + // The (1-based) line on which the group was defined, or `null`. + int? line; + + // The (1-based) column on which the group was defined, or `null`. + int? column; + + // The URL for the file in which the group was defined, or `null`. + String? url; + + // This field is deprecated and should not be used. + Metadata metadata; +} +``` + +A group containing test cases. The group's ID is unique in the context of this +test run. It's used elsewhere in the protocol to refer to this group without +including its full representation. + +The implicit group at the root of each test suite has `null` `name` and +`parentID` attributes. + +The `line`, `column`, and `url` fields indicate the location the `group()` +function was called to create this group. They're treated as a unit: they'll +either all be `null` or they'll all be non-`null`. The URL is always absolute, +and may be a `package:` URL. + +### Metadata + +``` +class Metadata { + bool skip; + + // The reason the tests was skipped, or `null` if it wasn't skipped. + String? skipReason; +} +``` + +The metadata class is deprecated and should not be used. + +## Remote Debugger APIs + +When running browser tests with `--pause-after-load`, the test package embeds a +few APIs in the JavaScript context of the host page. These allow tools to +control the debugging process in the same way a user might do from the command +line. They can be accessed by connecting to the remote debugger using the +[`DebugEvent.remoteDebugger`](#DebugEvent) URL. + +All APIs are defined as methods on the top-level `dartTest` object. The +following methods are available: + +### `resume()` + +Calling `resume()` when the test runner is paused causes it to resume running +tests. If the test runner is not paused, it won't do anything. When +`--pause-after-load` is passed, the test runner will pause after loading each +suite but before any tests are run. + +This gives external tools a chance to use the remote debugger protocol to set +breakpoints before tests have begun executing. They can start the test runner +with `--pause-after-load`, connect to the remote debugger using the +[`DebugEvent.remoteDebugger`](#DebugEvent) URL, set breakpoints, then call +`dartTest.resume()` in the host frame when they're finished. + +### `restartCurrent()` + +Calling `restartCurrent()` when the test runner is running a test causes it to +re-run that test once it completes its current run. It's intended to be called +when the browser is paused, as at a breakpoint.
diff --git a/pkgs/test/doc/json_reporter.schema.json b/pkgs/test/doc/json_reporter.schema.json new file mode 100644 index 0000000..25417e2 --- /dev/null +++ b/pkgs/test/doc/json_reporter.schema.json
@@ -0,0 +1,220 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "An event emitted by the test package's JSON reporter", + + "definitions": { + "GroupEntry": { + "required": [ + "id", + "suiteID", + "metadata", + "line", + "column", + "url" + ], + "properties": { + "id": {"type": "integer", "minimum": 0}, + "suiteID": {"type": "integer", "minimum": 0}, + "metadata": {"$ref": "#/definitions/Metadata"} + }, + "oneOf": [ + { + "properties": { + "line": {"type": "integer", "minimum": 1}, + "column": {"type": "integer", "minimum": 1}, + "url": {"type": "string", "format": "uri"} + } + }, + { + "properties": { + "line": {"type": "null"}, + "column": {"type": "null"}, + "url": {"type": "null"} + } + } + ] + }, + + "Test": { + "allOf": [{"$ref": "#/definitions/GroupEntry"}], + "required": [ + "name", + "groupIDs" + ], + "properties": { + "name": {"type": "string"}, + "groupIDs": { + "type": "array", + "items": {"type": "integer", "minimum": 0} + } + } + }, + + "Suite": { + "required": ["id", "platform", "path"], + "properties": { + "id": {"type": "integer", "minimum": 0}, + "platform": { + "oneOf": [{"type": "string"}, {"type": "null"}] + }, + "path": {"type": "string"} + } + }, + + "Group": { + "allOf": [{"$ref": "#/definitions/GroupEntry"}], + "required": ["name"], + "properties": { + "parentID": { + "oneOf": [{"type": "integer", "minimum": 0}, {"type": "null"}] + }, + "name": { + "oneOf": [{"type": "string"}, {"type": "null"}] + }, + "testCount": {"type": "integer", "minimum": 0} + } + }, + + "Metadata": { + "required": ["skip", "skipReason"], + "properties": { + "skip": {"type": "boolean"}, + "skipReason": { + "oneOf": [{"type": "string"}, {"type": "null"}] + } + } + } + }, + + "required": ["type", "time"], + "properties": { + "time": {"type": "integer", "minimum": 0}, + "type": {"type": "string"} + }, + + "oneOf": [ + { + "title": "StartEvent", + "required": ["protocolVersion", "runnerVersion"], + "properties": { + "type": {"enum": ["start"]}, + "pid": {"type": "integer", "minimum": 0}, + "protocolVersion": {"type": "string", "pattern": "^0\\.1\\."}, + "runnerVersion": { + "oneOf": [{"type": "string"}, {"type": "null"}] + } + } + }, + + { + "title": "TestStartEvent", + "required": ["test"], + "properties": { + "type": {"enum": ["testStart"]}, + "test": {"$ref": "#/definitions/Test"} + } + }, + + { + "title": "AllSuitesEvent", + "required": ["count"], + "properties": { + "type": {"enum": ["allSuites"]}, + "count": {"type": "integer", "minimum": 0} + } + }, + + { + "title": "SuiteEvent", + "required": ["suite"], + "properties": { + "type": {"enum": ["suite"]}, + "suite": {"$ref": "#/definitions/Suite"} + } + }, + + { + "title": "DebugEvent", + "required": ["suiteID"], + "properties": { + "type": {"enum": ["debug"]}, + "suiteID": {"type": "integer", "minimum": 0}, + "observatory": { + "oneOf": [{"type": "string", "format": "uri"}, {"type": "null"}] + }, + "remoteDebugger": { + "oneOf": [{"type": "string", "format": "uri"}, {"type": "null"}] + } + } + }, + + { + "title": "GroupEvent", + "required": ["group"], + "properties": { + "type": {"enum": ["group"]}, + "group": {"$ref": "#/definitions/Group"} + } + }, + + { + "title": "MessageEvent", + "required": ["testID", "message", "type"], + "properties": { + "type": {"enum": ["print"]}, + "testID": {"type": "integer", "minimum": 0}, + "message": {"type": "string"}, + "messageType": {"type": "string", "enum": ["print", "skip"]} + } + }, + + { + "title": "ErrorEvent", + "required": ["testID", "error", "stackTrace", "isFailure"], + "properties": { + "type": {"enum": ["error"]}, + "testID": {"type": "integer", "minimum": 0}, + "error": {"type": "string"}, + "stackTrace": {"type": "string"}, + "isFailure": {"type": "boolean"} + } + }, + + { + "title": "TestDoneEvent", + "required": ["testID", "result", "hidden", "skipped"], + "properties": { + "type": {"enum": ["testDone"]}, + "testID": {"type": "integer", "minimum": 0}, + "result": {"type": "string", "enum": ["success", "failure", "error"]}, + "hidden": {"type": "boolean"}, + "skipped": {"type": "boolean"} + } + }, + + { + "title": "DoneEvent", + "required": ["success"], + "properties": { + "type": {"enum": ["done"]}, + "success": {"type": "boolean"} + } + }, + + { + "title": "FutureEvent", + "description": + "A placeholder event to allow new events to be added in the future.", + "properties": { + "type": { + "not": { + "enum": [ + "start", "testStart", "allSuites", "suite", "group", "print", + "error", "testDone", "done", "debug" + ] + } + } + } + } + ] +}
diff --git a/pkgs/test/doc/package_config.md b/pkgs/test/doc/package_config.md new file mode 120000 index 0000000..422034a --- /dev/null +++ b/pkgs/test/doc/package_config.md
@@ -0,0 +1 @@ +configuration.md \ No newline at end of file
diff --git a/pkgs/test/image/hybrid.png b/pkgs/test/image/hybrid.png new file mode 100644 index 0000000..554a28c --- /dev/null +++ b/pkgs/test/image/hybrid.png Binary files differ
diff --git a/pkgs/test/image/test1.gif b/pkgs/test/image/test1.gif new file mode 100644 index 0000000..289e3ee --- /dev/null +++ b/pkgs/test/image/test1.gif Binary files differ
diff --git a/pkgs/test/image/test2.gif b/pkgs/test/image/test2.gif new file mode 100644 index 0000000..7bae0fc --- /dev/null +++ b/pkgs/test/image/test2.gif Binary files differ
diff --git a/pkgs/test/lib/bootstrap/browser.dart b/pkgs/test/lib/bootstrap/browser.dart new file mode 100644 index 0000000..b70cf11 --- /dev/null +++ b/pkgs/test/lib/bootstrap/browser.dart
@@ -0,0 +1,5 @@ +// 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. + +export '../src/bootstrap/browser.dart';
diff --git a/pkgs/test/lib/bootstrap/node.dart b/pkgs/test/lib/bootstrap/node.dart new file mode 100644 index 0000000..0214d6b --- /dev/null +++ b/pkgs/test/lib/bootstrap/node.dart
@@ -0,0 +1,5 @@ +// 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. + +export '../src/bootstrap/node.dart';
diff --git a/pkgs/test/lib/bootstrap/vm.dart b/pkgs/test/lib/bootstrap/vm.dart new file mode 100644 index 0000000..8115736 --- /dev/null +++ b/pkgs/test/lib/bootstrap/vm.dart
@@ -0,0 +1,5 @@ +// 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. + +export 'package:test_core/src/bootstrap/vm.dart';
diff --git a/pkgs/test/lib/dart.js b/pkgs/test/lib/dart.js new file mode 100644 index 0000000..3840ba7 --- /dev/null +++ b/pkgs/test/lib/dart.js
@@ -0,0 +1,80 @@ +// Copyright (c) 2015, 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. + +// This script runs in HTML files and loads the corresponding test scripts for +// a JS browser. It's used by "pub serve" and user-authored HTML files; +window.onload = function() { + +// Sends an error message to the server indicating that the script failed to +// load. +// +// This mimics a MultiChannel-formatted message. +var sendLoadException = function(message) { + window.parent.postMessage({ + "data": [0, {"type": "loadException", "message": message}], + "exception": true, + }, window.location.origin); +} + +// Listen for dartLoadException events and forward to the server. +window.addEventListener('dartLoadException', function(e) { + sendLoadException(e.detail); +}); + +// The basename of the current page. +var name = window.location.href.replace(/.*\//, '').replace(/#.*/, ''); + +// Find <link rel="x-dart-test">. +var links = document.getElementsByTagName("link"); +var testLinks = []; +var length = links.length; +for (var i = 0; i < length; ++i) { + if (links[i].rel == "x-dart-test") testLinks.push(links[i]); +} + +if (testLinks.length != 1) { + sendLoadException( + 'Expected exactly 1 <link rel="x-dart-test"> in ' + name + ', found ' + + testLinks.length + '.'); + return; +} + +var link = testLinks[0]; + +if (link.href == '') { + sendLoadException( + 'Expected <link rel="x-dart-test"> in ' + name + ' to have an "href" ' + + 'attribute.'); + return; +} + +var script = document.createElement('script'); + +if (typeof trustedTypes !== 'undefined') { + const sanitizer = trustedTypes.createPolicy('dart#test', { + createScriptURL: (input) => input + '.browser_test.dart.js' + }); + script.src = sanitizer.createScriptURL(link.href); +} else { + script.src = link.href + '.browser_test.dart.js'; +} + +script.onerror = function(event) { + var message = "Failed to load script at " + script.src + + (event.message ? ": " + event.message : "."); + sendLoadException(message); +}; + +Array.from(document.querySelectorAll('script')).some(currentScript => { + if (currentScript.nonce) { + script.nonce = currentScript.nonce; + return true; + } +}); + +var parent = link.parentNode; +document.currentScript = script; +parent.replaceChild(script, link); + +};
diff --git a/pkgs/test/lib/expect.dart b/pkgs/test/lib/expect.dart new file mode 100644 index 0000000..5e62622 --- /dev/null +++ b/pkgs/test/lib/expect.dart
@@ -0,0 +1,5 @@ +// Copyright (c) 2021, 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. + +export 'package:matcher/expect.dart';
diff --git a/pkgs/test/lib/fake.dart b/pkgs/test/lib/fake.dart new file mode 100644 index 0000000..fc28bef --- /dev/null +++ b/pkgs/test/lib/fake.dart
@@ -0,0 +1,10 @@ +// Copyright (c) 2020, 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. + +// Note: eventually we would like to fold this into test.dart, but we can't do +// so until Mockito stops implementing its own version of `Fake`, because there +// is code in the wild that imports both test_api.dart and Mockito. + +// ignore: deprecated_member_use +export 'package:test_api/fake.dart';
diff --git a/pkgs/test/lib/scaffolding.dart b/pkgs/test/lib/scaffolding.dart new file mode 100644 index 0000000..06e9029 --- /dev/null +++ b/pkgs/test/lib/scaffolding.dart
@@ -0,0 +1,5 @@ +// Copyright (c) 2021, 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. + +export 'package:test_core/src/scaffolding.dart';
diff --git a/pkgs/test/lib/src/bootstrap/browser.dart b/pkgs/test/lib/src/bootstrap/browser.dart new file mode 100644 index 0000000..1a66e2b --- /dev/null +++ b/pkgs/test/lib/src/bootstrap/browser.dart
@@ -0,0 +1,21 @@ +// 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:stream_channel/stream_channel.dart'; +import 'package:test_core/src/runner/plugin/remote_platform_helpers.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports + +import '../runner/browser/post_message_channel.dart'; + +/// Bootstraps a browser test to communicate with the test runner. +void internalBootstrapBrowserTest(Function Function() getMain, + {StreamChannel<Object?>? testChannel}) { + var channel = serializeSuite(getMain, hidePrints: false, + beforeLoad: (suiteChannel) async { + var serialized = await suiteChannel('test.browser.mapper').stream.first; + if (serialized is! Map) return; + setStackTraceMapper(JSStackTraceMapper.deserialize(serialized)!); + }); + (testChannel ?? postMessageChannel()).pipe(channel); +}
diff --git a/pkgs/test/lib/src/bootstrap/node.dart b/pkgs/test/lib/src/bootstrap/node.dart new file mode 100644 index 0000000..21089a4 --- /dev/null +++ b/pkgs/test/lib/src/bootstrap/node.dart
@@ -0,0 +1,18 @@ +// 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:test_core/src/runner/plugin/remote_platform_helpers.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports + +import '../runner/node/socket_channel.dart'; + +/// Bootstraps a browser test to communicate with the test runner. +void internalBootstrapNodeTest(Function Function() getMain) { + var channel = serializeSuite(getMain, beforeLoad: (suiteChannel) async { + var serialized = await suiteChannel('test.node.mapper').stream.first; + if (serialized is! Map) return; + setStackTraceMapper(JSStackTraceMapper.deserialize(serialized)!); + }); + socketChannel().then((socket) => socket.pipe(channel)); +}
diff --git a/pkgs/test/lib/src/executable.dart b/pkgs/test/lib/src/executable.dart new file mode 100644 index 0000000..5780694 --- /dev/null +++ b/pkgs/test/lib/src/executable.dart
@@ -0,0 +1,23 @@ +// Copyright (c) 2015, 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_api/src/backend/runtime.dart'; // ignore: implementation_imports +// ignore: implementation_imports +import 'package:test_core/src/executable.dart' as executable; +import 'package:test_core/src/runner/hack_register_platform.dart'; // ignore: implementation_imports + +import 'runner/browser/platform.dart'; +import 'runner/node/platform.dart'; + +Future<void> main(List<String> args) async { + registerPlatformPlugin([Runtime.nodeJS], NodePlatform.new); + registerPlatformPlugin([ + Runtime.chrome, + Runtime.edge, + Runtime.firefox, + Runtime.safari, + ], BrowserPlatform.start); + + await executable.main(args); +}
diff --git a/pkgs/test/lib/src/runner/browser/browser.dart b/pkgs/test/lib/src/runner/browser/browser.dart new file mode 100644 index 0000000..b025ea2 --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/browser.dart
@@ -0,0 +1,138 @@ +// Copyright (c) 2015, 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:convert'; +import 'dart:io'; + +import 'package:test_core/src/runner/application_exception.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/errors.dart'; // ignore: implementation_imports + +/// An interface for running browser instances. +/// +/// This is intentionally coarse-grained: browsers are controlled primary from +/// inside a single tab. Thus this interface only provides support for closing +/// the browser and seeing if it closes itself. +/// +/// Any errors starting or running the browser process are reported through +/// [onExit]. +abstract class Browser { + String get name; + + /// The remote debugger URL for this browser. + /// + /// This will complete to `null` for browsers that don't support remote + /// debugging, or if the remote debugging URL can't be found. + Future<Uri?> get remoteDebuggerUrl async => null; + + /// The underlying process. + /// + /// This will fire once the process has started successfully. + Future<Process> get _process => _processCompleter.future; + final _processCompleter = Completer<Process>(); + + /// Whether [close] has been called. + var _closed = false; + + /// A future that completes when the browser exits. + /// + /// If there's a problem starting or running the browser, this will complete + /// with an error. + Future<void> get onExit => _onExitCompleter.future; + final _onExitCompleter = Completer<void>(); + + /// Standard IO streams for the underlying browser process. + final _ioSubscriptions = <StreamSubscription<String>>[]; + + final output = <String>[]; + + /// Creates a new browser. + /// + /// This is intended to be called by subclasses. They pass in [startBrowser], + /// which asynchronously returns the browser process. Any errors in + /// [startBrowser] (even those raised asynchronously after it returns) are + /// piped to [onExit] and will cause the browser to be killed. + Browser(Future<Process> Function() startBrowser) { + // Don't return a Future here because there's no need for the caller to wait + // for the process to actually start. They should just wait for the HTTP + // request instead. + runZonedGuarded(() async { + var process = await startBrowser(); + _processCompleter.complete(process); + + void drainOutput(Stream<List<int>> stream) { + try { + _ioSubscriptions.add(stream + .transform(utf8.decoder) + .transform(const LineSplitter()) + .listen(output.add, cancelOnError: true)); + } on StateError catch (_) {} + } + + // If we don't drain the stdout and stderr the process can hang. + drainOutput(process.stdout); + drainOutput(process.stderr); + + var exitCode = await process.exitCode; + + // This hack dodges an otherwise intractable race condition. When the user + // presses Control-C, the signal is sent to the browser and the test + // runner at the same time. It's possible for the browser to exit before + // the [Browser.close] is called, which would trigger the error below. + // + // A negative exit code signals that the process exited due to a signal. + // However, it's possible that this signal didn't come from the user's + // Control-C, in which case we do want to throw the error. The only way to + // resolve the ambiguity is to wait a brief amount of time and see if this + // browser is actually closed. + if (!_closed && exitCode < 0) { + await Future<void>.delayed(const Duration(milliseconds: 200)); + } + + if (!_closed && exitCode != 0) { + var outputString = output.join('\n'); + var message = '$name failed with exit code $exitCode.'; + if (outputString.isNotEmpty) { + message += '\nStandard output:\n$outputString'; + } + + throw ApplicationException(message); + } + + _onExitCompleter.complete(); + }, (error, stackTrace) { + // Ignore any errors after the browser has been closed. + if (_closed) return; + + // Make sure the process dies even if the error wasn't fatal. + _process.then((process) => process.kill()); + + if (_onExitCompleter.isCompleted) return; + _onExitCompleter.completeError( + ApplicationException( + 'Failed to run $name: ${getErrorMessage(error)}.'), + stackTrace); + }); + } + + /// Kills the browser process. + /// + /// Returns the same [Future] as [onExit], except that it won't emit + /// exceptions. + Future<void> close() async { + _closed = true; + + // If we don't manually close the stream the test runner can hang. + // For example this happens with Chrome Headless. + // See SDK issue: https://github.com/dart-lang/sdk/issues/31264 + for (var stream in _ioSubscriptions) { + unawaited(stream.cancel()); + } + + (await _process).kill(); + + // Swallow exceptions. The user should explicitly use [onExit] for these. + return onExit.onError((_, __) {}); + } +}
diff --git a/pkgs/test/lib/src/runner/browser/browser_manager.dart b/pkgs/test/lib/src/runner/browser/browser_manager.dart new file mode 100644 index 0000000..409fbb4 --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/browser_manager.dart
@@ -0,0 +1,359 @@ +// Copyright (c) 2015, 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:convert'; + +import 'package:async/async.dart'; +import 'package:pool/pool.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test_api/backend.dart' show Compiler, Runtime, StackTraceMapper; +import 'package:test_core/src/runner/application_exception.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/environment.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/load_exception.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/plugin/platform_helpers.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports +import 'package:web_socket_channel/web_socket_channel.dart'; + +import '../executable_settings.dart'; +import 'browser.dart'; +import 'chrome.dart'; +import 'firefox.dart'; +import 'microsoft_edge.dart'; +import 'safari.dart'; + +/// A class that manages the connection to a single running browser. +/// +/// This is in charge of telling the browser which test suites to load and +/// converting its responses into [Suite] objects. +class BrowserManager { + /// The browser instance that this is connected to via [_channel]. + final Browser _browser; + + // TODO(nweiz): Consider removing the duplication between this and + // [_browser.name]. + /// The [Runtime] for [_browser]. + final Runtime _runtime; + + /// The channel used to communicate with the browser. + /// + /// This is connected to a page running `static/host.dart`. + late final MultiChannel<Object> _channel; + + /// A pool that ensures that limits the number of initial connections the + /// manager will wait for at once. + /// + /// This isn't the *total* number of connections; any number of iframes may be + /// loaded in the same browser. However, the browser can only load so many at + /// once, and we want a timeout in case they fail so we only wait for so many + /// at once. + final _pool = Pool(8); + + /// The ID of the next suite to be loaded. + /// + /// This is used to ensure that the suites can be referred to consistently + /// across the client and server. + int _suiteID = 0; + + /// Whether the channel to the browser has closed. + bool _closed = false; + + /// The completer for [_BrowserEnvironment.displayPause]. + /// + /// This will be `null` as long as the browser isn't displaying a pause + /// screen. + CancelableCompleter<void>? _pauseCompleter; + + /// The controller for [_BrowserEnvironment.onRestart]. + final _onRestartController = StreamController<void>.broadcast(); + + /// The environment to attach to each suite. + late final Future<_BrowserEnvironment> _environment; + + /// Controllers for every suite in this browser. + /// + /// These are used to mark suites as debugging or not based on the browser's + /// pings. + final _controllers = <RunnerSuiteController>{}; + + // A timer that's reset whenever we receive a message from the browser. + // + // Because the browser stops running code when the user is actively debugging, + // this lets us detect whether they're debugging reasonably accurately. + late final RestartableTimer _timer; + + /// Starts the browser identified by [runtime] and has it connect to [url]. + /// + /// [url] should serve a page that establishes a WebSocket connection with + /// this process. That connection, once established, should be emitted via + /// [future]. If [debug] is true, starts the browser in debug mode, with its + /// debugger interfaces on and detected. + /// + /// The [settings] indicate how to invoke this browser's executable. + /// + /// Returns the browser manager, or throws an [ApplicationException] if a + /// connection fails to be established. + static Future<BrowserManager> start( + Runtime runtime, + Uri url, + Future<WebSocketChannel> future, + ExecutableSettings settings, + Configuration configuration) => + _start(runtime, url, future, settings, configuration, 1); + + static const _maxRetries = 3; + static Future<BrowserManager> _start( + Runtime runtime, + Uri url, + Future<WebSocketChannel> future, + ExecutableSettings settings, + Configuration configuration, + int attempt) { + var browser = _newBrowser(url, runtime, settings, configuration); + + var completer = Completer<BrowserManager>(); + + // TODO(nweiz): Gracefully handle the browser being killed before the + // tests complete. + browser.onExit + .then<void>((_) => throw ApplicationException( + '${runtime.name} exited before connecting.')) + .onError<Object>((error, stackTrace) { + if (!completer.isCompleted) { + completer.completeError(error, stackTrace); + } + }); + + future.then((webSocket) { + if (completer.isCompleted) return; + completer.complete(BrowserManager._(browser, runtime, webSocket)); + }).onError((Object error, StackTrace stackTrace) { + browser.close(); + if (completer.isCompleted) return; + completer.completeError(error, stackTrace); + }); + + return completer.future.timeout(const Duration(seconds: 30), onTimeout: () { + browser.close(); + if (attempt >= _maxRetries) { + throw ApplicationException( + 'Timed out waiting for ${runtime.name} to connect.\n' + 'Browser output: ${browser.output.join('\n')}'); + } + return _start(runtime, url, future, settings, configuration, ++attempt); + }); + } + + /// Starts the browser identified by [browser] using [settings] and has it load [url]. + /// + /// If [debug] is true, starts the browser in debug mode. + static Browser _newBrowser(Uri url, Runtime browser, + ExecutableSettings settings, Configuration configuration) => + switch (browser.root) { + Runtime.chrome => Chrome(url, configuration, settings: settings), + Runtime.firefox => Firefox(url, settings: settings), + Runtime.safari => Safari(url, settings: settings), + Runtime.edge => MicrosoftEdge(url, configuration, settings: settings), + _ => throw ArgumentError('$browser is not a browser.'), + }; + + /// Creates a new BrowserManager that communicates with [browser] over + /// [webSocket]. + BrowserManager._(this._browser, this._runtime, WebSocketChannel webSocket) { + // The duration should be short enough that the debugging console is open as + // soon as the user is done setting breakpoints, but long enough that a test + // doing a lot of synchronous work doesn't trigger a false positive. + // + // Start this canceled because we don't want it to start ticking until we + // get some response from the iframe. + _timer = RestartableTimer(const Duration(seconds: 3), () { + for (var controller in _controllers) { + controller.setDebugging(true); + } + }) + ..cancel(); + + // Whenever we get a message, no matter which child channel it's for, we the + // know browser is still running code which means the user isn't debugging. + _channel = MultiChannel( + webSocket.cast<String>().transform(jsonDocument).changeStream((stream) { + return stream.map((message) { + if (!_closed) _timer.reset(); + for (var controller in _controllers) { + controller.setDebugging(false); + } + + return message; + }); + })); + + _environment = _loadBrowserEnvironment(); + _channel.stream.listen( + (message) => _onMessage(message as Map<Object, Object?>), + onDone: close); + } + + /// Loads [_BrowserEnvironment]. + Future<_BrowserEnvironment> _loadBrowserEnvironment() async => + _BrowserEnvironment( + this, await _browser.remoteDebuggerUrl, _onRestartController.stream); + + /// Tells the browser the load a test suite from the URL [url]. + /// + /// [url] should be an HTML page with a reference to the JS-compiled test + /// suite. [path] is the path of the original test suite file, which is used + /// for reporting. [suiteConfig] is the configuration for the test suite. + /// + /// If [mapper] is passed, it's used to map stack traces for errors coming + /// from this test suite. + Future<RunnerSuite> load(String path, Uri url, SuiteConfiguration suiteConfig, + Map<String, Object?> message, Compiler compiler, + {StackTraceMapper? mapper, Duration? timeout}) async { + url = url.replace( + fragment: Uri.encodeFull(jsonEncode({ + 'metadata': suiteConfig.metadata.serialize(), + 'browser': _runtime.identifier, + 'compiler': compiler.serialize(), + }))); + + var suiteID = _suiteID++; + RunnerSuiteController? controller; + void closeIframe() { + if (_closed) return; + if (controller != null) _controllers.remove(controller); + _channel.sink.add({'command': 'closeSuite', 'id': suiteID}); + } + + // The virtual channel will be closed when the suite is closed, in which + // case we should unload the iframe. + var virtualChannel = _channel.virtualChannel(); + var suiteChannelID = virtualChannel.id; + var suiteChannel = virtualChannel + .transformStream(StreamTransformer.fromHandlers(handleDone: (sink) { + closeIframe(); + sink.close(); + })); + + var suite = _pool.withResource<RunnerSuite>(() async { + _channel.sink.add({ + 'command': 'loadSuite', + 'url': url.toString(), + 'id': suiteID, + 'channel': suiteChannelID + }); + + try { + controller = deserializeSuite( + path, + currentPlatform(_runtime, compiler), + suiteConfig, + await _environment, + suiteChannel.cast(), + message, gatherCoverage: () async { + var browser = _browser; + if (browser is Chrome) return browser.gatherCoverage(); + return {}; + }); + + controller! + .channel('test.browser.mapper') + .sink + .add(mapper?.serialize()); + + _controllers.add(controller!); + return await controller!.suite; + } catch (_) { + closeIframe(); + rethrow; + } + }); + if (timeout != null) { + suite = suite.timeout(timeout, onTimeout: () { + throw LoadException( + path, + 'Timed out waiting for browser to load test suite. ' + 'Browser output: ${_browser.output.join('\n')}'); + }); + } + return suite; + } + + /// An implementation of [Environment.displayPause]. + CancelableOperation<void> _displayPause() { + if (_pauseCompleter != null) return _pauseCompleter!.operation; + + final pauseCompleter = _pauseCompleter = CancelableCompleter(onCancel: () { + _channel.sink.add({'command': 'resume'}); + _pauseCompleter = null; + }); + + pauseCompleter.operation.value.whenComplete(() { + _pauseCompleter = null; + }); + + _channel.sink.add({'command': 'displayPause'}); + + return pauseCompleter.operation; + } + + /// The callback for handling messages received from the host page. + void _onMessage(Map<Object, Object?> message) { + switch (message['command'] as String) { + case 'ping': + break; + + case 'restart': + _onRestartController.add(null); + break; + + case 'resume': + _pauseCompleter?.complete(); + break; + + default: + // Unreachable. + assert(false); + break; + } + } + + /// Closes the manager and releases any resources it owns, including closing + /// the browser. + Future<void> close() => _closeMemoizer.runOnce(() { + _closed = true; + _timer.cancel(); + _pauseCompleter?.complete(); + _pauseCompleter = null; + _controllers.clear(); + return _browser.close(); + }); + final _closeMemoizer = AsyncMemoizer<void>(); +} + +/// An implementation of [Environment] for the browser. +/// +/// All methods forward directly to [BrowserManager]. +class _BrowserEnvironment implements Environment { + final BrowserManager _manager; + + @override + final supportsDebugging = true; + + @override + Null get observatoryUrl => null; + + @override + final Uri? remoteDebuggerUrl; + + @override + final Stream<void> onRestart; + + _BrowserEnvironment(this._manager, this.remoteDebuggerUrl, this.onRestart); + + @override + CancelableOperation<void> displayPause() => _manager._displayPause(); +}
diff --git a/pkgs/test/lib/src/runner/browser/chrome.dart b/pkgs/test/lib/src/runner/browser/chrome.dart new file mode 100644 index 0000000..e7dff75 --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/chrome.dart
@@ -0,0 +1,194 @@ +// Copyright (c) 2015, 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:convert'; +import 'dart:io'; + +import 'package:collection/collection.dart'; +import 'package:coverage/coverage.dart'; +import 'package:path/path.dart' as p; +import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports +import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'; + +import '../executable_settings.dart'; +import 'browser.dart'; +import 'chromium.dart'; +import 'default_settings.dart'; + +/// A class for running an instance of Chrome. +/// +/// Most of the communication with the browser is expected to happen via HTTP, +/// so this exposes a bare-bones API. The browser starts as soon as the class is +/// constructed, and is killed when [close] is called. +/// +/// Any errors starting or running the process are reported through [onExit]. +class Chrome extends Browser { + @override + final name = 'Chrome'; + + @override + final Future<Uri?> remoteDebuggerUrl; + + final Future<WipConnection> _tabConnection; + final Map<String, String> _idToUrl; + + /// Starts a new instance of Chrome open to the given [url], which may be a + /// [Uri] or a [String]. + factory Chrome(Uri url, Configuration configuration, + {ExecutableSettings? settings}) { + settings ??= defaultSettings[Runtime.chrome]!; + var remoteDebuggerCompleter = Completer<Uri?>.sync(); + var connectionCompleter = Completer<WipConnection>(); + var idToUrl = <String, String>{}; + return Chrome._(() async { + Future<Process> tryPort([int? port]) async { + var process = await ChromiumBasedBrowser.chrome.spawn( + url, + configuration, + settings: settings, + additionalArgs: [ + if (port != null) + // Chrome doesn't provide any way of ensuring that this port was + // successfully bound. It produces an error if the binding fails, + // but without a reliable and fast way to tell if it succeeded + // that doesn't provide us much. It's very unlikely that this port + // will fail, though. + '--remote-debugging-port=$port', + ], + ); + + if (port != null) { + remoteDebuggerCompleter.complete( + getRemoteDebuggerUrl(Uri.parse('http://localhost:$port'))); + + connectionCompleter.complete(_connect(process, port, idToUrl, url)); + } else { + remoteDebuggerCompleter.complete(null); + } + + return process; + } + + if (!configuration.debug) return tryPort(); + return getUnusedPort<Process>(tryPort); + }, remoteDebuggerCompleter.future, connectionCompleter.future, idToUrl); + } + + /// Returns a Dart based hit-map containing coverage report, suitable for use + /// with `package:coverage`. + Future<Map<String, dynamic>> gatherCoverage() async { + var tabConnection = await _tabConnection; + var response = await tabConnection.debugger.connection + .sendCommand('Profiler.takePreciseCoverage', {}); + var result = + (response.result!['result'] as List).cast<Map<String, dynamic>>(); + var httpClient = HttpClient(); + var coverage = await parseChromeCoverage( + result, + (scriptId) => _sourceProvider(scriptId, httpClient), + (scriptId) => _sourceMapProvider(scriptId, httpClient), + _sourceUriProvider, + ); + httpClient.close(force: true); + return coverage; + } + + Chrome._(super.startBrowser, this.remoteDebuggerUrl, this._tabConnection, + this._idToUrl); + + Future<Uri?> _sourceUriProvider(String sourceUrl, String scriptId) async { + var script = _idToUrl[scriptId]; + if (script == null) return null; + var sourceUri = Uri.parse(sourceUrl); + if (sourceUri.scheme == 'file') return sourceUri; + // If the provided sourceUrl is relative, determine the package path. + var uri = Uri.parse(script); + var path = p.join( + p.joinAll(uri.pathSegments.sublist(1, uri.pathSegments.length - 1)), + sourceUrl); + return path.contains('/packages/') + ? Uri(scheme: 'package', path: path.split('/packages/').last) + : null; + } + + Future<String?> _sourceMapProvider( + String scriptId, HttpClient httpClient) async { + var script = _idToUrl[scriptId]; + if (script == null) return null; + return await httpClient.getString('$script.map'); + } + + Future<String?> _sourceProvider( + String scriptId, HttpClient httpClient) async { + var script = _idToUrl[scriptId]; + if (script == null) return null; + return await httpClient.getString(script); + } +} + +Future<WipConnection> _connect( + Process process, int port, Map<String, String> idToUrl, Uri url) async { + // Wait for Chrome to be in a ready state. + await process.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .firstWhere((line) => line.startsWith('DevTools listening')); + + var chromeConnection = ChromeConnection('localhost', port); + ChromeTab? tab; + var attempt = 0; + while (tab == null) { + attempt++; + var tabs = await chromeConnection.getTabs(); + tab = tabs.firstWhereOrNull((tab) => tab.url == url.toString()); + if (tab == null) { + await Future<void>.delayed(const Duration(milliseconds: 100)); + if (attempt > 5) { + throw StateError('Could not connect to test tab with url: $url'); + } + } + } + var tabConnection = await tab.connect(); + + // Enable debugging. + await tabConnection.debugger.enable(); + + // Coverage reports are in terms of scriptIds so keep note of URLs. + tabConnection.debugger.onScriptParsed.listen((data) { + var script = data.script; + if (script.url.isNotEmpty) idToUrl[script.scriptId] = script.url; + }); + + // Enable coverage collection. + await tabConnection.debugger.connection.sendCommand('Profiler.enable', {}); + await tabConnection.debugger.connection.sendCommand( + 'Profiler.startPreciseCoverage', {'detailed': true, 'callCount': false}); + + return tabConnection; +} + +extension on HttpClient { + Encoding determineEncoding(HttpHeaders headers) { + final contentType = headers.contentType?.charset; + + /// Using the `charset` property of the `contentType` if available. + /// If it's unavailable or if the encoding name is unknown, [latin1] is used by default, + /// as per [RFC 2616][]. + /// + /// [RFC 2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html + return Encoding.getByName(contentType) ?? latin1; + } + + Future<String?> getString(String url) async { + final request = await getUrl(Uri.parse(url)); + final response = await request.close(); + if (response.statusCode != HttpStatus.ok) return null; + var bytes = [await for (var chunk in response) ...chunk]; + final encoding = determineEncoding(response.headers); + return encoding.decode(bytes); + } +}
diff --git a/pkgs/test/lib/src/runner/browser/chromium.dart b/pkgs/test/lib/src/runner/browser/chromium.dart new file mode 100644 index 0000000..b0243ba --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/chromium.dart
@@ -0,0 +1,63 @@ +// Copyright (c) 2023, 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:test_api/src/backend/runtime.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports + +import '../executable_settings.dart'; +import 'default_settings.dart'; + +enum ChromiumBasedBrowser { + chrome(Runtime.chrome), + microsoftEdge(Runtime.edge); + + final Runtime runtime; + + const ChromiumBasedBrowser(this.runtime); + + Future<Process> spawn( + Uri url, + Configuration configuration, { + ExecutableSettings? settings, + List<String> additionalArgs = const [], + }) async { + settings ??= defaultSettings[runtime]; + + var dir = createTempDir(); + var args = [ + '--user-data-dir=$dir', + url.toString(), + '--enable-logging=stderr', + '--v=0', + '--disable-extensions', + '--disable-popup-blocking', + '--bwsi', + '--no-first-run', + '--no-default-browser-check', + '--disable-default-apps', + '--disable-translate', + '--disable-dev-shm-usage', + if (settings!.headless && !configuration.pauseAfterLoad) ...[ + '--headless', + '--disable-gpu', + ], + if (!configuration.debug) + // We don't actually connect to the remote debugger, but Chrome will + // close as soon as the page is loaded if we don't turn it on. + '--remote-debugging-port=0', + ...settings.arguments, + ...additionalArgs, + ]; + + var process = await Process.start(settings.executable, args); + + unawaited(process.exitCode.then((_) => Directory(dir).deleteWithRetry())); + + return process; + } +}
diff --git a/pkgs/test/lib/src/runner/browser/compilers/compiler_support.dart b/pkgs/test/lib/src/runner/browser/compilers/compiler_support.dart new file mode 100644 index 0000000..4fa1b88 --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/compilers/compiler_support.dart
@@ -0,0 +1,105 @@ +// Copyright (c) 2023, 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:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:shelf/shelf.dart' as shelf; +import 'package:test_api/backend.dart' show StackTraceMapper, SuitePlatform; +import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports +import 'package:web_socket_channel/web_socket_channel.dart'; // ignore: implementation_imports + +/// The shared interface for all compiler support libraries. +abstract class CompilerSupport { + /// The global test runner configuration. + final Configuration config; + + /// The default template path. + final String defaultTemplatePath; + + CompilerSupport(this.config, this.defaultTemplatePath); + + /// The URL at which this compiler serves its tests. + /// + /// Each compiler serves its tests under a different directory. + Uri get serverUrl; + + /// Compiles [dartPath] using [suiteConfig] for [platform]. + /// + /// [dartPath] is the path to the original `.dart` test suite, relative to the + /// package root. + Future<void> compileSuite( + String dartPath, SuiteConfiguration suiteConfig, SuitePlatform platform); + + /// Retrieves a stack trace mapper for [dartPath] if available. + /// + /// [dartPath] is the path to the original `.dart` test suite, relative to the + /// package root. + StackTraceMapper? stackTraceMapperForPath(String dartPath); + + /// Returns the eventual URI for the web socket, as well as the channel itself + /// once the connection is established. + (Uri uri, Future<WebSocketChannel> socket) get webSocket; + + /// Closes down anything necessary for this implementation. + Future<void> close(); + + /// A handler that serves html wrapper files used to bootstrap tests. + shelf.Response htmlWrapperHandler(shelf.Request request); +} + +mixin JsHtmlWrapper on CompilerSupport { + @override + shelf.Response htmlWrapperHandler(shelf.Request request) { + var path = p.fromUri(request.url); + + if (path.endsWith('.html')) { + var test = p.setExtension(path, '.dart'); + var scriptBase = htmlEscape.convert(p.basename(test)); + var link = '<link rel="x-dart-test" href="$scriptBase">'; + var testName = htmlEscape.convert(test); + var template = config.customHtmlTemplatePath ?? defaultTemplatePath; + var contents = File(template).readAsStringSync(); + var processedContents = contents + // Checked during loading phase that there is only one {{testScript}} placeholder. + .replaceFirst('{{testScript}}', link) + .replaceAll('{{testName}}', testName); + return shelf.Response.ok(processedContents, + headers: {'Content-Type': 'text/html'}); + } + + return shelf.Response.notFound('Not found.'); + } +} + +mixin WasmHtmlWrapper on CompilerSupport { + @override + shelf.Response htmlWrapperHandler(shelf.Request request) { + var path = p.fromUri(request.url); + + if (path.endsWith('.html')) { + var test = '${p.withoutExtension(path)}.dart'; + var scriptBase = htmlEscape.convert(p.basename(test)); + var link = '<link rel="x-dart-test" href="$scriptBase">'; + var testName = htmlEscape.convert(test); + var template = config.customHtmlTemplatePath ?? defaultTemplatePath; + var contents = File(template).readAsStringSync(); + var jsRuntime = p.basename('$test.browser_test.dart.mjs'); + var wasmData = '<data id="WasmBootstrapInfo" ' + 'data-wasmurl="${p.basename('$test.browser_test.dart.wasm')}" ' + 'data-jsruntimeurl="$jsRuntime"></data>'; + var processedContents = contents + // Checked during loading phase that there is only one {{testScript}} placeholder. + .replaceFirst('{{testScript}}', '$link\n$wasmData') + .replaceAll('{{testName}}', testName); + return shelf.Response.ok(processedContents, + headers: {'Content-Type': 'text/html'}); + } + + return shelf.Response.notFound('Not found.'); + } +}
diff --git a/pkgs/test/lib/src/runner/browser/compilers/dart2js.dart b/pkgs/test/lib/src/runner/browser/compilers/dart2js.dart new file mode 100644 index 0000000..3cd6745 --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/compilers/dart2js.dart
@@ -0,0 +1,186 @@ +// Copyright (c) 2023, 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:http_multi_server/http_multi_server.dart'; +import 'package:path/path.dart' as p; +import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf/shelf_io.dart' as shelf_io; +import 'package:shelf_packages_handler/shelf_packages_handler.dart'; +import 'package:shelf_static/shelf_static.dart'; +import 'package:shelf_web_socket/shelf_web_socket.dart'; +import 'package:test_api/backend.dart' show StackTraceMapper, SuitePlatform; +import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/dart2js_compiler_pool.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/package_version.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports +import 'package:web_socket_channel/web_socket_channel.dart'; + +import '../../../util/math.dart'; +import '../../../util/one_off_handler.dart'; +import '../../../util/package_map.dart'; +import '../../../util/path_handler.dart'; +import 'compiler_support.dart'; + +/// Support for Dart2Js compiled tests. +class Dart2JsSupport extends CompilerSupport with JsHtmlWrapper { + /// Whether [close] has been called. + bool _closed = false; + + /// The temporary directory in which compiled JS is emitted. + final _compiledDir = createTempDir(); + + /// A map from test suite paths to Futures that will complete once those + /// suites are finished compiling. + /// + /// This is used to make sure that a given test suite is only compiled once + /// per run, rather than once per browser per run. + final _compileFutures = <String, Future<void>>{}; + + /// The [Dart2JsCompilerPool] managing active instances of `dart2js`. + final _compilerPool = Dart2JsCompilerPool(); + + /// Mappers for Dartifying stack traces, indexed by test path. + final _mappers = <String, StackTraceMapper>{}; + + /// A [PathHandler] used to serve test specific artifacts. + final _pathHandler = PathHandler(); + + /// The root directory served statically by this server. + final String _root; + + /// Each compiler serves its tests under a different randomly-generated + /// secret URI to ensure that other users on the same system can't snoop + /// on data being served through this server, as well as distinguish tests + /// from different compilers from each other. + final String _secret = randomUrlSecret(); + + /// The underlying server. + final shelf.Server _server; + + /// A [OneOffHandler] for servicing WebSocket connections for + /// [BrowserManager]s. + /// + /// This is one-off because each [BrowserManager] can only connect to a single + /// WebSocket. + final _webSocketHandler = OneOffHandler(); + + @override + Uri get serverUrl => _server.url.resolve('$_secret/'); + + Dart2JsSupport._(super.config, super.defaultTemplatePath, this._server, + this._root, String faviconPath) { + var cascade = shelf.Cascade() + .add(_webSocketHandler.handler) + .add(packagesDirHandler()) + .add(_pathHandler.handler) + .add(createStaticHandler(_root)) + .add(htmlWrapperHandler); + + var pipeline = const shelf.Pipeline() + .addMiddleware(PathHandler.nestedIn(_secret)) + .addHandler(cascade.handler); + + _server.mount(shelf.Cascade() + .add(createFileHandler(faviconPath)) + .add(pipeline) + .handler); + } + + static Future<Dart2JsSupport> start({ + required Configuration config, + required String defaultTemplatePath, + required String root, + required String faviconPath, + }) async { + var server = shelf_io.IOServer(await HttpMultiServer.loopback(0)); + return Dart2JsSupport._( + config, defaultTemplatePath, server, root, faviconPath); + } + + @override + Future<void> compileSuite( + String dartPath, SuiteConfiguration suiteConfig, SuitePlatform platform) { + return _compileFutures.putIfAbsent(dartPath, () async { + var dir = Directory(_compiledDir).createTempSync('test_').path; + var jsPath = p.join(dir, '${p.basename(dartPath)}.browser_test.dart.js'); + var bootstrapContent = ''' + ${suiteConfig.metadata.languageVersionComment ?? await rootPackageLanguageVersionComment} + import 'package:test/src/bootstrap/browser.dart'; + import 'package:test/src/runner/browser/dom.dart' as dom; + + import '${await absoluteUri(dartPath)}' as test; + + void main() { + dom.window.console.log(r'Startup for test path $dartPath'); + internalBootstrapBrowserTest(() => test.main); + } + '''; + + await _compilerPool.compile(bootstrapContent, jsPath, suiteConfig); + if (_closed) return; + + var bootstrapUrl = '${p.toUri(p.relative(dartPath, from: _root)).path}' + '.browser_test.dart'; + _pathHandler.add(bootstrapUrl, (request) { + return shelf.Response.ok(bootstrapContent, + headers: {'Content-Type': 'application/dart'}); + }); + + var jsUrl = '${p.toUri(p.relative(dartPath, from: _root)).path}' + '.browser_test.dart.js'; + _pathHandler.add(jsUrl, (request) { + return shelf.Response.ok(File(jsPath).readAsStringSync(), + headers: {'Content-Type': 'application/javascript'}); + }); + + var mapUrl = '${p.toUri(p.relative(dartPath, from: _root)).path}' + '.browser_test.dart.js.map'; + _pathHandler.add(mapUrl, (request) { + return shelf.Response.ok(File('$jsPath.map').readAsStringSync(), + headers: {'Content-Type': 'application/json'}); + }); + + if (suiteConfig.jsTrace) return; + var mapPath = '$jsPath.map'; + _mappers[dartPath] = JSStackTraceMapper(File(mapPath).readAsStringSync(), + mapUrl: p.toUri(mapPath), + sdkRoot: Uri.parse('org-dartlang-sdk:///sdk'), + packageMap: (await currentPackageConfig).toPackageMap()); + }); + } + + @override + Future<void> close() async { + if (_closed) return; + _closed = true; + await Future.wait([ + Directory(_compiledDir).deleteWithRetry(), + _compilerPool.close(), + _server.close(), + ]); + } + + @override + StackTraceMapper? stackTraceMapperForPath(String dartPath) => + _mappers[dartPath]; + + @override + (Uri, Future<WebSocketChannel>) get webSocket { + var completer = Completer<WebSocketChannel>.sync(); + // Note: the WebSocketChannel type below is needed for compatibility with + // package:shelf_web_socket v2. + var path = + _webSocketHandler.create(webSocketHandler((WebSocketChannel ws, _) { + completer.complete(ws); + })); + var webSocketUrl = serverUrl.replace(scheme: 'ws').resolve(path); + return (webSocketUrl, completer.future); + } +}
diff --git a/pkgs/test/lib/src/runner/browser/compilers/dart2wasm.dart b/pkgs/test/lib/src/runner/browser/compilers/dart2wasm.dart new file mode 100644 index 0000000..31ad661 --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/compilers/dart2wasm.dart
@@ -0,0 +1,192 @@ +// Copyright (c) 2023, 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:http_multi_server/http_multi_server.dart'; +import 'package:path/path.dart' as p; +import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf/shelf_io.dart' as shelf_io; +import 'package:shelf_packages_handler/shelf_packages_handler.dart'; +import 'package:shelf_static/shelf_static.dart'; +import 'package:shelf_web_socket/shelf_web_socket.dart'; +import 'package:test_api/backend.dart' show StackTraceMapper, SuitePlatform; +import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/package_version.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/wasm_compiler_pool.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports +import 'package:web_socket_channel/web_socket_channel.dart'; + +import '../../../util/math.dart'; +import '../../../util/one_off_handler.dart'; +import '../../../util/path_handler.dart'; +import '../browser_manager.dart'; +import 'compiler_support.dart'; + +/// Support for Dart2Wasm compiled tests. +class Dart2WasmSupport extends CompilerSupport with WasmHtmlWrapper { + /// Whether [close] has been called. + bool _closed = false; + + /// The temporary directory in which compiled JS is emitted. + final _compiledDir = createTempDir(); + + /// A map from test suite paths to Futures that will complete once those + /// suites are finished compiling. + /// + /// This is used to make sure that a given test suite is only compiled once + /// per run, rather than once per browser per run. + final _compileFutures = <String, Future<void>>{}; + + /// The [WasmCompilerPool] managing active instances of `dart2wasm`. + final _compilerPool = WasmCompilerPool(); + + /// The `package:test` side wrapper for the Dart2Wasm runtime. + final String _jsRuntimeWrapper; + + /// Mappers for Dartifying stack traces, indexed by test path. + final _mappers = <String, StackTraceMapper>{}; + + /// A [PathHandler] used to serve test specific artifacts. + final _pathHandler = PathHandler(); + + /// The root directory served statically by this server. + final String _root; + + /// Each compiler serves its tests under a different randomly-generated + /// secret URI to ensure that other users on the same system can't snoop + /// on data being served through this server, as well as distinguish tests + /// from different compilers from each other. + final String _secret = randomUrlSecret(); + + /// The underlying server. + final shelf.Server _server; + + /// A [OneOffHandler] for servicing WebSocket connections for + /// [BrowserManager]s. + /// + /// This is one-off because each [BrowserManager] can only connect to a single + /// WebSocket. + final _webSocketHandler = OneOffHandler(); + + @override + Uri get serverUrl => _server.url.resolve('$_secret/'); + + Dart2WasmSupport._(super.config, super.defaultTemplatePath, + this._jsRuntimeWrapper, this._server, this._root, String faviconPath) { + var cascade = shelf.Cascade() + .add(_webSocketHandler.handler) + .add(packagesDirHandler()) + .add(_pathHandler.handler) + .add(createStaticHandler(_root)) + .add(htmlWrapperHandler); + + var pipeline = const shelf.Pipeline() + .addMiddleware(PathHandler.nestedIn(_secret)) + .addHandler(cascade.handler); + + _server.mount(shelf.Cascade() + .add(createFileHandler(faviconPath)) + .add(pipeline) + .handler); + } + + static Future<Dart2WasmSupport> start({ + required Configuration config, + required String defaultTemplatePath, + required String jsRuntimeWrapper, + required String root, + required String faviconPath, + }) async { + var server = shelf_io.IOServer(await HttpMultiServer.loopback(0)); + return Dart2WasmSupport._(config, defaultTemplatePath, jsRuntimeWrapper, + server, root, faviconPath); + } + + @override + Future<void> compileSuite( + String dartPath, SuiteConfiguration suiteConfig, SuitePlatform platform) { + return _compileFutures.putIfAbsent(dartPath, () async { + var dir = Directory(_compiledDir).createTempSync('test_').path; + + var baseCompiledPath = + p.join(dir, '${p.basename(dartPath)}.browser_test.dart'); + var baseUrl = + '${p.toUri(p.relative(dartPath, from: _root)).path}.browser_test.dart'; + var wasmUrl = '$baseUrl.wasm'; + var jsRuntimeWrapperUrl = '$baseUrl.js'; + var jsRuntimeUrl = '$baseUrl.mjs'; + var htmlUrl = '$baseUrl.html'; + + var bootstrapContent = ''' + ${suiteConfig.metadata.languageVersionComment ?? await rootPackageLanguageVersionComment} + import 'package:test/src/bootstrap/browser.dart'; + + import '${await absoluteUri(dartPath)}' as test; + + void main() { + internalBootstrapBrowserTest(() => test.main); + } + '''; + + await _compilerPool.compile( + bootstrapContent, baseCompiledPath, suiteConfig); + if (_closed) return; + + var wasmPath = '$baseCompiledPath.wasm'; + _pathHandler.add(wasmUrl, (request) { + return shelf.Response.ok(File(wasmPath).readAsBytesSync(), + headers: {'Content-Type': 'application/wasm'}); + }); + + _pathHandler.add(jsRuntimeWrapperUrl, (request) { + return shelf.Response.ok(File(_jsRuntimeWrapper).readAsBytesSync(), + headers: {'Content-Type': 'application/javascript'}); + }); + + var jsRuntimePath = '$baseCompiledPath.mjs'; + _pathHandler.add(jsRuntimeUrl, (request) { + return shelf.Response.ok(File(jsRuntimePath).readAsBytesSync(), + headers: {'Content-Type': 'application/javascript'}); + }); + + var htmlPath = '$baseCompiledPath.html'; + _pathHandler.add(htmlUrl, (request) { + return shelf.Response.ok(File(htmlPath).readAsBytesSync(), + headers: {'Content-Type': 'text/html'}); + }); + }); + } + + @override + Future<void> close() async { + if (_closed) return; + _closed = true; + await Future.wait([ + Directory(_compiledDir).deleteWithRetry(), + _compilerPool.close(), + _server.close(), + ]); + } + + @override + StackTraceMapper? stackTraceMapperForPath(String dartPath) => + _mappers[dartPath]; + + @override + (Uri, Future<WebSocketChannel>) get webSocket { + var completer = Completer<WebSocketChannel>.sync(); + // Note: the WebSocketChannel type below is needed for compatibility with + // package:shelf_web_socket v2. + var path = + _webSocketHandler.create(webSocketHandler((WebSocketChannel ws, _) { + completer.complete(ws); + })); + var webSocketUrl = serverUrl.replace(scheme: 'ws').resolve(path); + return (webSocketUrl, completer.future); + } +}
diff --git a/pkgs/test/lib/src/runner/browser/compilers/precompiled.dart b/pkgs/test/lib/src/runner/browser/compilers/precompiled.dart new file mode 100644 index 0000000..9cf44aa --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/compilers/precompiled.dart
@@ -0,0 +1,149 @@ +// Copyright (c) 2023, 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:http_multi_server/http_multi_server.dart'; +import 'package:path/path.dart' as p; +import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf/shelf_io.dart' as shelf_io; +import 'package:shelf_packages_handler/shelf_packages_handler.dart'; +import 'package:shelf_static/shelf_static.dart'; +import 'package:shelf_web_socket/shelf_web_socket.dart'; +import 'package:test_api/backend.dart' + show Compiler, StackTraceMapper, SuitePlatform; +import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports +import 'package:web_socket_channel/web_socket_channel.dart'; + +import '../../../util/math.dart'; +import '../../../util/one_off_handler.dart'; +import '../../../util/package_map.dart'; +import '../../../util/path_handler.dart'; +import 'compiler_support.dart'; + +class JsPrecompiledSupport = PrecompiledSupport with JsHtmlWrapper; +class WasmPrecompiledSupport = PrecompiledSupport with WasmHtmlWrapper; + +/// Support for precompiled test files. +abstract class PrecompiledSupport extends CompilerSupport { + /// Whether [close] has been called. + bool _closed = false; + + /// Mappers for Dartifying stack traces, indexed by test path. + final _mappers = <String, StackTraceMapper>{}; + + /// The root directory served statically by the server. + final String _root; + + /// Each compiler serves its tests under a different randomly-generated + /// secret URI to ensure that other users on the same system can't snoop + /// on data being served through this server, as well as distinguish tests + /// from different compilers from each other. + final String _secret = randomUrlSecret(); + + /// The underlying server. + final shelf.Server _server; + + /// A [OneOffHandler] for servicing WebSocket connections for + /// [BrowserManager]s. + /// + /// This is one-off because each [BrowserManager] can only connect to a single + /// WebSocket. + final _webSocketHandler = OneOffHandler(); + + /// The URL at which this compiler serves its tests. + /// + /// Each compiler serves its tests under a different directory. + @override + Uri get serverUrl => _server.url.resolve('$_secret/'); + + PrecompiledSupport._(super.config, super.defaultTemplatePath, this._server, + this._root, String faviconPath) { + var cascade = shelf.Cascade() + .add(_webSocketHandler.handler) + .add(createStaticHandler(_root, serveFilesOutsidePath: true)) + // TODO: This packages dir handler should not be necessary? + .add(packagesDirHandler()) + // Even for precompiled tests, we will auto-create a bootstrap html file + // if none was present. + .add(htmlWrapperHandler); + + var pipeline = const shelf.Pipeline() + .addMiddleware(PathHandler.nestedIn(_secret)) + .addHandler(cascade.handler); + + _server.mount(shelf.Cascade() + .add(createFileHandler(faviconPath)) + .add(pipeline) + .handler); + } + + static Future<PrecompiledSupport> start({ + required Compiler compiler, + required Configuration config, + required String defaultTemplatePath, + required String root, + required String faviconPath, + }) async { + var server = shelf_io.IOServer(await HttpMultiServer.loopback(0)); + + return switch (compiler) { + Compiler.dart2js => JsPrecompiledSupport._( + config, defaultTemplatePath, server, root, faviconPath), + Compiler.dart2wasm => WasmPrecompiledSupport._( + config, defaultTemplatePath, server, root, faviconPath), + Compiler.exe || + Compiler.kernel || + Compiler.source => + throw UnsupportedError( + 'The browser platform does not support $compiler'), + }; + } + + /// Compiles [dartPath] using [suiteConfig] for [platform]. + @override + Future<void> compileSuite(String dartPath, SuiteConfiguration suiteConfig, + SuitePlatform platform) async { + if (suiteConfig.jsTrace) return; + var mapPath = p.join( + suiteConfig.precompiledPath!, '$dartPath.browser_test.dart.js.map'); + var mapFile = File(mapPath); + if (mapFile.existsSync()) { + _mappers[dartPath] = JSStackTraceMapper(mapFile.readAsStringSync(), + mapUrl: p.toUri(mapPath), + sdkRoot: Uri.parse(r'/packages/$sdk'), + packageMap: (await currentPackageConfig).toPackageMap()); + } + } + + /// Retrieves a stack trace mapper for [path] if available. + @override + StackTraceMapper? stackTraceMapperForPath(String dartPath) => + _mappers[dartPath]; + + /// Closes down anything necessary for this implementation. + @override + Future<void> close() async { + if (_closed) return; + _closed = true; + await _server.close(); + } + + @override + (Uri, Future<WebSocketChannel>) get webSocket { + var completer = Completer<WebSocketChannel>.sync(); + // Note: the WebSocketChannel type below is needed for compatibility with + // package:shelf_web_socket v2. + var path = + _webSocketHandler.create(webSocketHandler((WebSocketChannel ws, _) { + completer.complete(ws); + })); + var webSocketUrl = serverUrl.replace(scheme: 'ws').resolve(path); + return (webSocketUrl, completer.future); + } +}
diff --git a/pkgs/test/lib/src/runner/browser/default_settings.dart b/pkgs/test/lib/src/runner/browser/default_settings.dart new file mode 100644 index 0000000..312fc11 --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/default_settings.dart
@@ -0,0 +1,37 @@ +// 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:collection'; + +import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports +import '../executable_settings.dart'; + +/// Default settings for starting browser executables. +final defaultSettings = UnmodifiableMapView({ + Runtime.chrome: ExecutableSettings( + linuxExecutable: 'google-chrome', + macOSExecutable: + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + windowsExecutable: r'Google\Chrome\Application\chrome.exe', + environmentOverride: 'CHROME_EXECUTABLE'), + Runtime.edge: ExecutableSettings( + linuxExecutable: 'microsoft-edge-stable', + windowsExecutable: r'Microsoft\Edge\Application\msedge.exe', + macOSExecutable: + '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge', + environmentOverride: 'MS_EDGE_EXECUTABLE', + ), + Runtime.firefox: ExecutableSettings( + linuxExecutable: 'firefox', + macOSExecutables: [ + '/Applications/Firefox.app/Contents/MacOS/firefox-bin', + '/Applications/Firefox.app/Contents/MacOS/firefox', + 'firefox', + ], + windowsExecutable: r'Mozilla Firefox\firefox.exe', + environmentOverride: 'FIREFOX_EXECUTABLE'), + Runtime.safari: ExecutableSettings( + macOSExecutable: '/Applications/Safari.app/Contents/MacOS/Safari', + environmentOverride: 'SAFARI_EXECUTABLE'), +});
diff --git a/pkgs/test/lib/src/runner/browser/dom.dart b/pkgs/test/lib/src/runner/browser/dom.dart new file mode 100644 index 0000000..d0fba48 --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/dom.dart
@@ -0,0 +1,280 @@ +// 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:js_util' as js_util; + +import 'package:js/js.dart'; + +@JS() +@staticInterop +class Window extends EventTarget {} + +extension WindowExtension on Window { + @pragma('dart2js:as:trust') + Window get parent => js_util.getProperty<dynamic>(this, 'parent') as Window; + external Location get location; + Console get console => js_util.getProperty(this, 'console') as Console; + CSSStyleDeclaration? getComputedStyle(Element elt, [String? pseudoElt]) => + js_util.callMethod(this, 'getComputedStyle', <Object>[ + elt, + if (pseudoElt != null) pseudoElt + ]) as CSSStyleDeclaration?; + external Navigator get navigator; + void postMessage(Object message, String targetOrigin, + [List<MessagePort>? messagePorts]) => + js_util.callMethod(this, 'postMessage', <Object?>[ + js_util.jsify(message), + targetOrigin, + if (messagePorts != null) js_util.jsify(messagePorts) + ]); +} + +@JS('window') +external Window get window; + +@JS() +@staticInterop +class Console {} + +extension ConsoleExtension on Console { + external void log(Object? object); + external void warn(Object? object); +} + +@JS() +@staticInterop +class Document extends Node {} + +extension DocumentExtension on Document { + external Element? querySelector(String selectors); + Element createElement(String name, [Object? options]) => js_util.callMethod( + this, 'createElement', <Object>[name, if (options != null) options]) + as Element; +} + +@JS() +@staticInterop +class HTMLDocument extends Document {} + +extension HTMLDocumentExtension on HTMLDocument { + external HTMLBodyElement? get body; + external String? get title; +} + +@JS('document') +external HTMLDocument get document; + +@JS() +@staticInterop +class Navigator {} + +extension NavigatorExtension on Navigator { + external String get userAgent; +} + +@JS() +@staticInterop +class Element extends Node {} + +extension DomElementExtension on Element { + external DomTokenList get classList; +} + +@JS() +@staticInterop +class HTMLElement extends Element {} + +@JS() +@staticInterop +class HTMLBodyElement extends HTMLElement {} + +@JS() +@staticInterop +class Node extends EventTarget {} + +extension NodeExtension on Node { + external Node appendChild(Node node); + void remove() { + if (parentNode != null) { + final parent = parentNode!; + parent.removeChild(this); + } + } + + external Node removeChild(Node child); + external Node? get parentNode; +} + +@JS() +@staticInterop +class EventTarget {} + +extension EventTargetExtension on EventTarget { + void addEventListener(String type, EventListener? listener, + [bool? useCapture]) { + if (listener != null) { + js_util.callMethod<void>(this, 'addEventListener', + <Object>[type, listener, if (useCapture != null) useCapture]); + } + } + + void removeEventListener(String type, EventListener? listener, + [bool? useCapture]) { + if (listener != null) { + js_util.callMethod<void>(this, 'removeEventListener', + <Object>[type, listener, if (useCapture != null) useCapture]); + } + } +} + +typedef EventListener = void Function(Event event); + +@JS() +@staticInterop +class Event {} + +extension EventExtension on Event { + external void stopPropagation(); +} + +@JS() +@staticInterop +class MessageEvent extends Event {} + +extension MessageEventExtension on MessageEvent { + dynamic get data => js_util.dartify(js_util.getProperty(this, 'data')); + external String get origin; + List<MessagePort> get ports => + js_util.getProperty<List>(this, 'ports').cast<MessagePort>(); + + /// The source may be a `WindowProxy`, a `MessagePort`, or a `ServiceWorker`. + /// + /// When a message is sent from an iframe through `window.parent.postMessage` + /// the source will be a `WindowProxy` which has the same methods as [Window]. + @pragma('dart2js:as:trust') + MessageEventSource get source => + js_util.getProperty<dynamic>(this, 'source') as MessageEventSource; +} + +@JS() +@staticInterop +class MessageEventSource {} + +extension MessageEventSourceExtension on MessageEventSource { + @pragma('dart2js:as:trust') + MessageEventSourceLocation? get location => + js_util.getProperty<dynamic>(this, 'location') + as MessageEventSourceLocation; +} + +@JS() +@staticInterop +class MessageEventSourceLocation {} + +extension MessageEventSourceLocationExtension on MessageEventSourceLocation { + external String? get href; +} + +@JS() +@staticInterop +class Location {} + +extension LocationExtension on Location { + external String get href; + external String get origin; +} + +@JS() +@staticInterop +class MessagePort extends EventTarget {} + +extension MessagePortExtension on MessagePort { + void postMessage(Object? message) => js_util.callMethod(this, 'postMessage', + <Object>[if (message != null) js_util.jsify(message) as Object]); + external void start(); +} + +@JS() +@staticInterop +class CSSStyleDeclaration {} + +@JS() +@staticInterop +class HTMLScriptElement extends HTMLElement {} + +extension HTMLScriptElementExtension on HTMLScriptElement { + external set src(String value); +} + +HTMLScriptElement createHTMLScriptElement() => + document.createElement('script') as HTMLScriptElement; + +@JS() +@staticInterop +class DomTokenList {} + +extension DomTokenListExtension on DomTokenList { + external void add(String value); + external void remove(String value); + external bool contains(String token); +} + +@JS() +@staticInterop +class HTMLIFrameElement extends HTMLElement {} + +extension HTMLIFrameElementExtension on HTMLIFrameElement { + external String? get src; + external set src(String? value); + external Window get contentWindow; +} + +HTMLIFrameElement createHTMLIFrameElement() => + document.createElement('iframe') as HTMLIFrameElement; + +@JS() +@staticInterop +class WebSocket extends EventTarget {} + +extension WebSocketExtension on WebSocket { + external void send(Object? data); +} + +WebSocket createWebSocket(String url) => + _callConstructor('WebSocket', <Object>[url])! as WebSocket; + +@JS() +@staticInterop +class MessageChannel {} + +extension MessageChannelExtension on MessageChannel { + external MessagePort get port1; + external MessagePort get port2; +} + +MessageChannel createMessageChannel() => + _callConstructor('MessageChannel', <Object>[])! as MessageChannel; + +Object? _findConstructor(String constructorName) => + js_util.getProperty(window, constructorName); + +Object? _callConstructor(String constructorName, List<Object?> args) { + final constructor = _findConstructor(constructorName); + if (constructor == null) { + return null; + } + return js_util.callConstructor(constructor, args); +} + +class Subscription { + final String type; + final EventTarget target; + final EventListener listener; + + Subscription(this.target, this.type, this.listener) { + target.addEventListener(type, listener); + } + + void cancel() => target.removeEventListener(type, listener); +}
diff --git a/pkgs/test/lib/src/runner/browser/firefox.dart b/pkgs/test/lib/src/runner/browser/firefox.dart new file mode 100644 index 0000000..503f103 --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/firefox.dart
@@ -0,0 +1,59 @@ +// Copyright (c) 2015, 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:path/path.dart' as p; +import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports + +import '../executable_settings.dart'; +import 'browser.dart'; +import 'default_settings.dart'; + +final _preferences = ''' +user_pref("browser.shell.checkDefaultBrowser", false); +user_pref("dom.disable_open_during_load", false); +user_pref("dom.max_script_run_time", 0); +'''; + +/// A class for running an instance of Firefox. +/// +/// Most of the communication with the browser is expected to happen via HTTP, +/// so this exposes a bare-bones API. The browser starts as soon as the class is +/// constructed, and is killed when [close] is called. +/// +/// Any errors starting or running the process are reported through [onExit]. +class Firefox extends Browser { + @override + final name = 'Firefox'; + + Firefox(Uri url, {ExecutableSettings? settings}) + : super(() => + _startBrowser(url, settings ?? defaultSettings[Runtime.firefox]!)); + + /// Starts a new instance of Firefox open to the given [url], which may be a + /// [Uri] or a [String]. + static Future<Process> _startBrowser( + Uri url, ExecutableSettings settings) async { + var dir = createTempDir(); + File(p.join(dir, 'prefs.js')).writeAsStringSync(_preferences); + + var process = await Process.start(settings.executable, [ + '--profile', + dir, + url.toString(), + '--no-remote', + ...settings.arguments, + ], environment: { + 'MOZ_CRASHREPORTER_DISABLE': '1', + 'MOZ_AUTOMATION': '1', + }); + + unawaited(process.exitCode.then((_) => Directory(dir).deleteWithRetry())); + + return process; + } +}
diff --git a/pkgs/test/lib/src/runner/browser/microsoft_edge.dart b/pkgs/test/lib/src/runner/browser/microsoft_edge.dart new file mode 100644 index 0000000..08fa8e2 --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/microsoft_edge.dart
@@ -0,0 +1,23 @@ +// Copyright (c) 2023, 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_core/src/runner/configuration.dart'; // ignore: implementation_imports + +import '../executable_settings.dart'; +import 'browser.dart'; +import 'chromium.dart'; + +/// A class for running an instance of Microsoft Edge, a Chromium-based browser. +class MicrosoftEdge extends Browser { + @override + String get name => 'Edge'; + + MicrosoftEdge(Uri url, Configuration configuration, + {ExecutableSettings? settings}) + : super(() => ChromiumBasedBrowser.microsoftEdge.spawn( + url, + configuration, + settings: settings, + )); +}
diff --git a/pkgs/test/lib/src/runner/browser/platform.dart b/pkgs/test/lib/src/runner/browser/platform.dart new file mode 100644 index 0000000..da03a3e --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/platform.dart
@@ -0,0 +1,269 @@ +// Copyright (c) 2016, 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_api/backend.dart' show Compiler, Runtime, SuitePlatform; +import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/load_exception.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/platform.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/plugin/customizable_platform.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports +import 'package:yaml/yaml.dart'; + +import '../executable_settings.dart'; +import 'browser_manager.dart'; +import 'compilers/compiler_support.dart'; +import 'compilers/dart2js.dart'; +import 'compilers/dart2wasm.dart'; +import 'compilers/precompiled.dart'; +import 'default_settings.dart'; + +class BrowserPlatform extends PlatformPlugin + implements CustomizablePlatform<ExecutableSettings> { + /// Starts the server. + /// + /// [root] is the root directory that the server should serve. It defaults to + /// the working directory. + static Future<BrowserPlatform> start({String? root}) async { + var packageConfig = await currentPackageConfig; + return BrowserPlatform._( + Configuration.current, + p.fromUri(packageConfig.resolve( + Uri.parse('package:test/src/runner/browser/static/favicon.ico'))), + p.fromUri(packageConfig.resolve(Uri.parse( + 'package:test/src/runner/browser/static/default.html.tpl'))), + p.fromUri(packageConfig.resolve(Uri.parse( + 'package:test/src/runner/browser/static/run_wasm_chrome.js'))), + root: root); + } + + /// The test runner configuration. + final Configuration _config; + + /// The cached [CompilerSupport] for each compiler. + final _compilerSupport = <Compiler, Future<CompilerSupport>>{}; + + /// The `package:test` side wrapper for the Dart2Wasm runtime. + final String _jsRuntimeWrapper; + + /// The URL for this server and [compiler] combination. + /// + /// Each compiler serves its tests under a different randomly-generated + /// secret URI to ensure that other users on the same system can't snoop + /// on data being served through this server, as well as distinguish tests + /// from different compilers from each other. + Future<CompilerSupport> compilerSupport(Compiler compiler) => + _compilerSupport.putIfAbsent(compiler, () { + if (_config.suiteDefaults.precompiledPath != null) { + return PrecompiledSupport.start( + compiler: compiler, + config: _config, + defaultTemplatePath: _defaultTemplatePath, + root: _config.suiteDefaults.precompiledPath!, + faviconPath: _faviconPath); + } + return switch (compiler) { + Compiler.dart2js => Dart2JsSupport.start( + config: _config, + defaultTemplatePath: _defaultTemplatePath, + root: _root, + faviconPath: _faviconPath), + Compiler.dart2wasm => Dart2WasmSupport.start( + config: _config, + defaultTemplatePath: _defaultTemplatePath, + jsRuntimeWrapper: _jsRuntimeWrapper, + root: _root, + faviconPath: _faviconPath), + _ => throw StateError('Unexpected compiler $compiler'), + }; + }); + + /// The root directory served statically by this server. + final String _root; + + /// Whether [close] has been called. + bool get _closed => _closeMemo.hasRun; + + /// A map from browser identifiers to futures that will complete to the + /// [BrowserManager]s for those browsers, or `null` if they failed to load. + /// + /// This should only be accessed through [_browserManagerFor]. + final _browserManagers = <(Runtime, Compiler), Future<BrowserManager?>>{}; + + /// Settings for invoking each browser. + /// + /// This starts out with the default settings, which may be overridden by user settings. + final _browserSettings = + Map<Runtime, ExecutableSettings>.from(defaultSettings); + + /// The default template for html tests. + final String _defaultTemplatePath; + + final String _faviconPath; + + BrowserPlatform._(Configuration config, this._faviconPath, + this._defaultTemplatePath, this._jsRuntimeWrapper, + {String? root}) + : _config = config, + _root = root ?? p.current; + + @override + ExecutableSettings parsePlatformSettings(YamlMap settings) => + ExecutableSettings.parse(settings); + + @override + ExecutableSettings mergePlatformSettings( + ExecutableSettings settings1, ExecutableSettings settings2) => + settings1.merge(settings2); + + @override + void customizePlatform(Runtime runtime, ExecutableSettings settings) { + var oldSettings = + _browserSettings[runtime] ?? _browserSettings[runtime.root]; + if (oldSettings != null) settings = oldSettings.merge(settings); + _browserSettings[runtime] = settings; + } + + /// Loads the test suite at [path] on the platform [platform]. + /// + /// This will start a browser to load the suite if one isn't already running. + /// Throws an [ArgumentError] if `platform.platform` isn't a browser. + @override + Future<RunnerSuite?> load(String path, SuitePlatform platform, + SuiteConfiguration suiteConfig, Map<String, Object?> message) async { + var browser = platform.runtime; + assert(suiteConfig.runtimes.contains(browser.identifier)); + + if (!browser.isBrowser) { + throw ArgumentError('$browser is not a browser.'); + } + + var compiler = platform.compiler; + var support = await compilerSupport(compiler); + + var htmlPathFromTestPath = '${p.withoutExtension(path)}.html'; + if (File(htmlPathFromTestPath).existsSync()) { + if (_config.customHtmlTemplatePath != null && + p.basename(htmlPathFromTestPath) == + p.basename(_config.customHtmlTemplatePath!)) { + throw LoadException( + path, + 'template file "${p.basename(_config.customHtmlTemplatePath!)}" cannot be named ' + 'like the test file.'); + } + _checkHtmlCorrectness(htmlPathFromTestPath, path); + } else if (_config.customHtmlTemplatePath != null) { + var htmlTemplatePath = _config.customHtmlTemplatePath!; + if (!File(htmlTemplatePath).existsSync()) { + throw LoadException( + path, '"$htmlTemplatePath" does not exist or is not readable'); + } + + final templateFileContents = File(htmlTemplatePath).readAsStringSync(); + if ('{{testScript}}'.allMatches(templateFileContents).length != 1) { + throw LoadException(path, + '"$htmlTemplatePath" must contain exactly one {{testScript}} placeholder'); + } + _checkHtmlCorrectness(htmlTemplatePath, path); + } + + if (_closed) return null; + await support.compileSuite(path, suiteConfig, platform); + + var suiteUrl = support.serverUrl.resolveUri( + p.toUri('${p.withoutExtension(p.relative(path, from: _root))}.html')); + + if (_closed) return null; + + var browserManager = await _browserManagerFor(browser, compiler); + if (_closed || browserManager == null) return null; + + var timeout = const Duration(seconds: 30); + if (suiteConfig.metadata.timeout.apply(timeout) case final suiteTimeout? + when suiteTimeout > timeout) { + timeout = suiteTimeout; + } + var suite = await browserManager.load( + path, suiteUrl, suiteConfig, message, platform.compiler, + mapper: (await compilerSupport(compiler)).stackTraceMapperForPath(path), + timeout: timeout); + if (_closed) return null; + return suite; + } + + void _checkHtmlCorrectness(String htmlPath, String path) { + if (!File(htmlPath).readAsStringSync().contains('packages/test/dart.js')) { + throw LoadException( + path, + '"$htmlPath" must contain <script src="packages/test/dart.js">' + '</script>.'); + } + } + + /// Returns the [BrowserManager] for [runtime], which should be a browser. + /// + /// If no browser manager is running yet, starts one. + Future<BrowserManager?> _browserManagerFor( + Runtime browser, Compiler compiler) { + var managerFuture = _browserManagers[(browser, compiler)]; + if (managerFuture != null) return managerFuture; + + var future = _createBrowserManager(browser, compiler); + // Store null values for browsers that error out so we know not to load them + // again. + _browserManagers[(browser, compiler)] = + future.then<BrowserManager?>((value) => value).onError((_, __) => null); + + return future; + } + + Future<BrowserManager> _createBrowserManager( + Runtime browser, Compiler compiler) async { + var support = await compilerSupport(compiler); + var (webSocketUrl, socketFuture) = support.webSocket; + var hostUrl = support.serverUrl + .resolve('packages/test/src/runner/browser/static/index.html') + .replace(queryParameters: { + 'managerUrl': webSocketUrl.toString(), + 'debug': _config.debug.toString() + }); + + return BrowserManager.start( + browser, hostUrl, socketFuture, _browserSettings[browser]!, _config); + } + + /// Close all the browsers that the server currently has open. + /// + /// Note that this doesn't close the server itself. Browser tests can still be + /// loaded, they'll just spawn new browsers. + @override + Future<List<void>> closeEphemeral() { + var managers = _browserManagers.values.toList(); + _browserManagers.clear(); + return Future.wait(managers.map((manager) async { + var result = await manager; + if (result == null) return; + await result.close(); + })); + } + + /// Closes the server and releases all its resources. + /// + /// Returns a [Future] that completes once the server is closed and its + /// resources have been fully released. + @override + Future<void> close() async => _closeMemo.runOnce(() => Future.wait([ + for (var browser in _browserManagers.values) + browser.then((b) => b?.close()), + for (var support in _compilerSupport.values) + support.then((s) => s.close()), + ])); + final _closeMemo = AsyncMemoizer<void>(); +}
diff --git a/pkgs/test/lib/src/runner/browser/post_message_channel.dart b/pkgs/test/lib/src/runner/browser/post_message_channel.dart new file mode 100644 index 0000000..60a5a6b --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/post_message_channel.dart
@@ -0,0 +1,31 @@ +// Copyright (c) 2015, 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:js_util'; + +import 'package:stream_channel/stream_channel.dart'; + +import 'dom.dart' as dom; + +/// Constructs a [StreamChannel] wrapping a new [MessageChannel] communicating +/// with the host page. +/// +/// Sends a [MessagePort] to the host page for the channel. +StreamChannel<Object?> postMessageChannel() { + dom.window.console.log('Suite starting, sending channel to host'); + var controller = StreamChannelController<Object?>(sync: true); + var channel = dom.createMessageChannel(); + dom.window.parent + .postMessage('port', dom.window.location.origin, [channel.port2]); + var portSubscription = dom.Subscription(channel.port1, 'message', + allowInterop((dom.Event event) { + controller.local.sink.add((event as dom.MessageEvent).data); + })); + channel.port1.start(); + + controller.local.stream + .listen(channel.port1.postMessage, onDone: portSubscription.cancel); + + return controller.foreign; +}
diff --git a/pkgs/test/lib/src/runner/browser/safari.dart b/pkgs/test/lib/src/runner/browser/safari.dart new file mode 100644 index 0000000..4e97205 --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/safari.dart
@@ -0,0 +1,48 @@ +// Copyright (c) 2015, 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:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports + +import '../executable_settings.dart'; +import 'browser.dart'; +import 'default_settings.dart'; + +/// A class for running an instance of Safari. +/// +/// Any errors starting or running the process are reported through [onExit]. +class Safari extends Browser { + @override + final name = 'Safari'; + + Safari(Uri url, {ExecutableSettings? settings}) + : super(() => + _startBrowser(url, settings ?? defaultSettings[Runtime.safari]!)); + + /// Starts a new instance of Safari open to the given [url], which may be a + /// [Uri] or a [String]. + static Future<Process> _startBrowser( + Uri url, ExecutableSettings settings) async { + var dir = createTempDir(); + + // Safari will only open files (not general URLs) via the command-line + // API, so we create a dummy file to redirect it to the page we actually + // want it to load. + var redirect = p.join(dir, 'redirect.html'); + File(redirect).writeAsStringSync( + '<script>location = ${jsonEncode(url.toString())}</script>'); + + var process = await Process.start( + settings.executable, settings.arguments.toList()..add(redirect)); + + unawaited(process.exitCode.then((_) => Directory(dir).deleteWithRetry())); + + return process; + } +}
diff --git a/pkgs/test/lib/src/runner/browser/static/default.html.tpl b/pkgs/test/lib/src/runner/browser/static/default.html.tpl new file mode 100644 index 0000000..a92f529 --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/static/default.html.tpl
@@ -0,0 +1,8 @@ +<!DOCTYPE html> +<html> + <head> + <title>{{testName}} Test</title> + {{testScript}} + <script src="packages/test/dart.js"></script> + </head> +</html>
diff --git a/pkgs/test/lib/src/runner/browser/static/favicon.ico b/pkgs/test/lib/src/runner/browser/static/favicon.ico new file mode 100644 index 0000000..7ba349b --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/static/favicon.ico Binary files differ
diff --git a/pkgs/test/lib/src/runner/browser/static/host.css b/pkgs/test/lib/src/runner/browser/static/host.css new file mode 100644 index 0000000..b8e0352 --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/static/host.css
@@ -0,0 +1,316 @@ +/* Copyright (c) 2015, 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. */ + +iframe { + /* We would use display: none here, but then Firefox fails to properly compute + * styles. See #274 */ + visibility: hidden; + width: 1024px; + height: 768px; +} + +#play { + display: none; + cursor: pointer; +} + +#dark { + display: none; +} + +.paused #play { + display: block; + z-index: 1; +} + +.paused #dark { + display: block; + position: absolute; + width: 100%; + height: 100%; + top: 0; + right: 0; + background-color: rgba(0, 0, 0, 0.5); +} + +.paused #right-flank, .paused #right-ear, .paused #right-paw, +.paused #left-flank, .paused #left-ear, .paused #left-paw { + -webkit-animation-play-state: paused; + animation-play-state: paused; +} + +.debug body { + margin: 0; + padding: 0; +} + +.debug iframe { + visibility: visible; + border: none; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +/* Compiled output from + * http://codepen.io/mknadler/pen/11b75cb014a3c382f54abf527655af21. */ + +svg { + position: absolute; + margin: auto; + left: 0; + right: 0; + bottom: 0; + top: 0; +} + +#right-flank { + fill: #0074C1; + stroke-color: #0074C1; + -webkit-animation: right-flank 8s ease infinite alternate; + animation: right-flank 8s ease infinite alternate; +} + +#right-ear { + fill: #00B5AB; + stroke-color: #00B5AB; + -webkit-animation: right-ear 8s ease-in infinite alternate; + animation: right-ear 8s ease-in infinite alternate; +} + +#right-paw { + fill: #00A6E4; + stroke-color: #00A6E4; + -webkit-animation: right-paw 8s ease-out infinite alternate; + animation: right-paw 8s ease-out infinite alternate; +} + +#left-flank { + fill: #00B5AB; + stroke-color: #00B5AB; + -webkit-animation: left-flank 8s ease-in-out infinite alternate; + animation: left-flank 8s ease-in-out infinite alternate; +} + +#left-ear { + fill: #0074C1; + stroke-color: #0074C1; + -webkit-animation: left-ear 8s linear infinite alternate; + animation: left-ear 8s linear infinite alternate; +} + +#left-paw { + fill: #41C1BC; + stroke-color: #41C1BC; + -webkit-animation: left-paw 8s ease infinite alternate; + animation: left-paw 8s ease infinite alternate; +} + +@-webkit-keyframes left-ear { + 20% { + -webkit-transform: translate(250px, 150px) rotateY(180deg) scale(0.6); + transform: translate(250px, 150px) rotateY(180deg) scale(0.6); + fill: #00A6E4; + } + 50% { + -webkit-transform: translate(100px, 75px) rotateY(80deg) scale(1.1); + transform: translate(100px, 75px) rotateY(80deg) scale(1.1); + fill: #41C1BC; + } + 80% { + -webkit-transform: translate(0px, 0px) scale(1); + transform: translate(0px, 0px) scale(1); + fill: #0074C1; + } +} + +@keyframes left-ear { + 20% { + -webkit-transform: translate(250px, 150px) rotateY(180deg) scale(0.6); + transform: translate(250px, 150px) rotateY(180deg) scale(0.6); + fill: #00A6E4; + } + 50% { + -webkit-transform: translate(100px, 75px) rotateY(80deg) scale(1.1); + transform: translate(100px, 75px) rotateY(80deg) scale(1.1); + fill: #41C1BC; + } + 80% { + -webkit-transform: translate(0px, 0px) scale(1); + transform: translate(0px, 0px) scale(1); + fill: #0074C1; + } +} +@-webkit-keyframes right-ear { + 20% { + -webkit-transform: translate(200px, 250px) rotateX(180deg) scale(0.6); + transform: translate(200px, 250px) rotateX(180deg) scale(0.6); + fill: #41C1BC; + } + 50% { + -webkit-transform: translate(75px, 100px) rotateX(80deg) scale(1.1); + transform: translate(75px, 100px) rotateX(80deg) scale(1.1); + fill: #00A6E4; + } + 80% { + -webkit-transform: translate(0px, 0px) scale(1); + transform: translate(0px, 0px) scale(1); + fill: #00B5AB; + } +} +@keyframes right-ear { + 20% { + -webkit-transform: translate(200px, 250px) rotateX(180deg) scale(0.6); + transform: translate(200px, 250px) rotateX(180deg) scale(0.6); + fill: #41C1BC; + } + 50% { + -webkit-transform: translate(75px, 100px) rotateX(80deg) scale(1.1); + transform: translate(75px, 100px) rotateX(80deg) scale(1.1); + fill: #00A6E4; + } + 80% { + -webkit-transform: translate(0px, 0px) scale(1); + transform: translate(0px, 0px) scale(1); + fill: #00B5AB; + } +} +@-webkit-keyframes left-paw { + 20% { + -webkit-transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, 60deg) scale(0.6); + transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, 60deg) scale(0.6); + fill: #00B5AB; + } + 50% { + -webkit-transform: translate(150px, 250px) rotate3d(-1, 0, 0.5, 90deg) scale(0.6); + transform: translate(150px, 250px) rotate3d(-1, 0, 0.5, 90deg) scale(0.6); + fill: #00B5AB; + } + 80% { + -webkit-transform: translate(0px, 0px) scale(1); + transform: translate(0px, 0px) scale(1); + fill: #41C1BC; + } +} +@keyframes left-paw { + 20% { + -webkit-transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, 60deg) scale(0.6); + transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, 60deg) scale(0.6); + fill: #00B5AB; + } + 50% { + -webkit-transform: translate(150px, 250px) rotate3d(-1, 0, 0.5, 90deg) scale(0.6); + transform: translate(150px, 250px) rotate3d(-1, 0, 0.5, 90deg) scale(0.6); + fill: #00B5AB; + } + 80% { + -webkit-transform: translate(0px, 0px) scale(1); + transform: translate(0px, 0px) scale(1); + fill: #41C1BC; + } +} +@-webkit-keyframes right-paw { + 20% { + -webkit-transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, -60deg) scale(0.6); + transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, -60deg) scale(0.6); + fill: #41C1BC; + } + 50% { + -webkit-transform: translate(100px, 250px) rotate3d(-1, 0, 0.5, -90deg) scale(0.6); + transform: translate(100px, 250px) rotate3d(-1, 0, 0.5, -90deg) scale(0.6); + fill: #41C1BC; + } + 80% { + -webkit-transform: translate(0px, 0px) scale(1); + transform: translate(0px, 0px) scale(1); + fill: #00A6E4; + } +} +@keyframes right-paw { + 20% { + -webkit-transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, -60deg) scale(0.6); + transform: translate(200px, 200px) rotate3d(-1, 0, 0.5, -60deg) scale(0.6); + fill: #41C1BC; + } + 50% { + -webkit-transform: translate(100px, 250px) rotate3d(-1, 0, 0.5, -90deg) scale(0.6); + transform: translate(100px, 250px) rotate3d(-1, 0, 0.5, -90deg) scale(0.6); + fill: #41C1BC; + } + 80% { + -webkit-transform: translate(0px, 0px) scale(1); + transform: translate(0px, 0px) scale(1); + fill: #00A6E4; + } +} +@-webkit-keyframes left-flank { + 20% { + -webkit-transform: translate(0px, 100px) scale(0.6); + transform: translate(0px, 100px) scale(0.6); + fill: #00A6E4; + } + 50% { + -webkit-transform: translate(0px, 100px) scale(0.4); + transform: translate(0px, 100px) scale(0.4); + fill: #00A6E4; + } + 80% { + -webkit-transform: translate(0px, 0px) scale(1); + transform: translate(0px, 0px) scale(1); + fill: #00B5AB; + } +} +@keyframes left-flank { + 20% { + -webkit-transform: translate(0px, 100px) scale(0.6); + transform: translate(0px, 100px) scale(0.6); + fill: #00A6E4; + } + 50% { + -webkit-transform: translate(0px, 100px) scale(0.4); + transform: translate(0px, 100px) scale(0.4); + fill: #00A6E4; + } + 80% { + -webkit-transform: translate(0px, 0px) scale(1); + transform: translate(0px, 0px) scale(1); + fill: #00B5AB; + } +} +@-webkit-keyframes right-flank { + 20% { + -webkit-transform: translate(100px, -25px) scale(0.6); + transform: translate(100px, -25px) scale(0.6); + fill: #41C1BC; + } + 50% { + -webkit-transform: translate(110px, 0px) scale(0.4); + transform: translate(110px, 0px) scale(0.4); + fill: #00A6E4; + } + 80% { + -webkit-transform: translate(0px, 0px) scale(1); + transform: translate(0px, 0px) scale(1); + fill: #0074C1; + } +} +@keyframes right-flank { + 20% { + -webkit-transform: translate(100px, -25px) scale(0.6); + transform: translate(100px, -25px) scale(0.6); + fill: #41C1BC; + } + 50% { + -webkit-transform: translate(110px, 0px) scale(0.4); + transform: translate(110px, 0px) scale(0.4); + fill: #00A6E4; + } + 80% { + -webkit-transform: translate(0px, 0px) scale(1); + transform: translate(0px, 0px) scale(1); + fill: #0074C1; + } +}
diff --git a/pkgs/test/lib/src/runner/browser/static/host.dart.js b/pkgs/test/lib/src/runner/browser/static/host.dart.js new file mode 100644 index 0000000..fd3ef38 --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/static/host.dart.js
@@ -0,0 +1,18333 @@ +// Generated by dart2js (NullSafetyMode.sound, csp, intern-composite-values), the Dart to JavaScript compiler version: 3.2.0-228.0.dev. +// The code supports the following hooks: +// dartPrint(message): +// if this function is defined it is called instead of the Dart [print] +// method. +// +// dartMainRunner(main, args): +// if this function is defined, the Dart [main] method will not be invoked +// directly. Instead, a closure that will invoke [main], and its arguments +// [args] is passed to [dartMainRunner]. +// +// dartDeferredLibraryLoader(uri, successCallback, errorCallback, loadId, loadPriority): +// if this function is defined, it will be called when a deferred library +// is loaded. It should load and eval the javascript of `uri`, and call +// successCallback. If it fails to do so, it should call errorCallback with +// an error. The loadId argument is the deferred import that resulted in +// this uri being loaded. The loadPriority argument is the priority the +// library should be loaded with as specified in the code via the +// load-priority annotation (0: normal, 1: high). +// +// dartCallInstrumentation(id, qualifiedName): +// if this function is defined, it will be called at each entry of a +// method or constructor. Used only when compiling programs with +// --experiment-call-instrumentation. +(function dartProgram() { + function copyProperties(from, to) { + var keys = Object.keys(from); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + to[key] = from[key]; + } + } + function mixinPropertiesHard(from, to) { + var keys = Object.keys(from); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (!to.hasOwnProperty(key)) + to[key] = from[key]; + } + } + function mixinPropertiesEasy(from, to) { + Object.assign(to, from); + } + var supportsDirectProtoAccess = function() { + var cls = function() { + }; + cls.prototype = {p: {}}; + var object = new cls(); + if (!(Object.getPrototypeOf(object) && Object.getPrototypeOf(object).p === cls.prototype.p)) + return false; + try { + if (typeof navigator != "undefined" && typeof navigator.userAgent == "string" && navigator.userAgent.indexOf("Chrome/") >= 0) + return true; + if (typeof version == "function" && version.length == 0) { + var v = version(); + if (/^\d+\.\d+\.\d+\.\d+$/.test(v)) + return true; + } + } catch (_) { + } + return false; + }(); + function inherit(cls, sup) { + cls.prototype.constructor = cls; + cls.prototype["$is" + cls.name] = cls; + if (sup != null) { + if (supportsDirectProtoAccess) { + Object.setPrototypeOf(cls.prototype, sup.prototype); + return; + } + var clsPrototype = Object.create(sup.prototype); + copyProperties(cls.prototype, clsPrototype); + cls.prototype = clsPrototype; + } + } + function inheritMany(sup, classes) { + for (var i = 0; i < classes.length; i++) + inherit(classes[i], sup); + } + function mixinEasy(cls, mixin) { + mixinPropertiesEasy(mixin.prototype, cls.prototype); + cls.prototype.constructor = cls; + } + function mixinHard(cls, mixin) { + mixinPropertiesHard(mixin.prototype, cls.prototype); + cls.prototype.constructor = cls; + } + function lazyOld(holder, name, getterName, initializer) { + var uninitializedSentinel = holder; + holder[name] = uninitializedSentinel; + holder[getterName] = function() { + holder[getterName] = function() { + A.throwCyclicInit(name); + }; + var result; + var sentinelInProgress = initializer; + try { + if (holder[name] === uninitializedSentinel) { + result = holder[name] = sentinelInProgress; + result = holder[name] = initializer(); + } else + result = holder[name]; + } finally { + if (result === sentinelInProgress) + holder[name] = null; + holder[getterName] = function() { + return this[name]; + }; + } + return result; + }; + } + function lazy(holder, name, getterName, initializer) { + var uninitializedSentinel = holder; + holder[name] = uninitializedSentinel; + holder[getterName] = function() { + if (holder[name] === uninitializedSentinel) + holder[name] = initializer(); + holder[getterName] = function() { + return this[name]; + }; + return holder[name]; + }; + } + function lazyFinal(holder, name, getterName, initializer) { + var uninitializedSentinel = holder; + holder[name] = uninitializedSentinel; + holder[getterName] = function() { + if (holder[name] === uninitializedSentinel) { + var value = initializer(); + if (holder[name] !== uninitializedSentinel) + A.throwLateFieldADI(name); + holder[name] = value; + } + var finalValue = holder[name]; + holder[getterName] = function() { + return finalValue; + }; + return finalValue; + }; + } + function makeConstList(list) { + list.immutable$list = Array; + list.fixed$length = Array; + return list; + } + function convertToFastObject(properties) { + function t() { + } + t.prototype = properties; + new t(); + return properties; + } + function convertAllToFastObject(arrayOfObjects) { + for (var i = 0; i < arrayOfObjects.length; ++i) + convertToFastObject(arrayOfObjects[i]); + } + var functionCounter = 0; + function instanceTearOffGetter(isIntercepted, parameters) { + var cache = null; + return isIntercepted ? function(receiver) { + if (cache === null) + cache = A.closureFromTearOff(parameters); + return new cache(receiver, this); + } : function() { + if (cache === null) + cache = A.closureFromTearOff(parameters); + return new cache(this, null); + }; + } + function staticTearOffGetter(parameters) { + var cache = null; + return function() { + if (cache === null) + cache = A.closureFromTearOff(parameters).prototype; + return cache; + }; + } + var typesOffset = 0; + function tearOffParameters(container, isStatic, isIntercepted, requiredParameterCount, optionalParameterDefaultValues, callNames, funsOrNames, funType, applyIndex, needsDirectAccess) { + if (typeof funType == "number") + funType += typesOffset; + return {co: container, iS: isStatic, iI: isIntercepted, rC: requiredParameterCount, dV: optionalParameterDefaultValues, cs: callNames, fs: funsOrNames, fT: funType, aI: applyIndex || 0, nDA: needsDirectAccess}; + } + function installStaticTearOff(holder, getterName, requiredParameterCount, optionalParameterDefaultValues, callNames, funsOrNames, funType, applyIndex) { + var parameters = tearOffParameters(holder, true, false, requiredParameterCount, optionalParameterDefaultValues, callNames, funsOrNames, funType, applyIndex, false); + var getterFunction = staticTearOffGetter(parameters); + holder[getterName] = getterFunction; + } + function installInstanceTearOff(prototype, getterName, isIntercepted, requiredParameterCount, optionalParameterDefaultValues, callNames, funsOrNames, funType, applyIndex, needsDirectAccess) { + isIntercepted = !!isIntercepted; + var parameters = tearOffParameters(prototype, false, isIntercepted, requiredParameterCount, optionalParameterDefaultValues, callNames, funsOrNames, funType, applyIndex, !!needsDirectAccess); + var getterFunction = instanceTearOffGetter(isIntercepted, parameters); + prototype[getterName] = getterFunction; + } + function setOrUpdateInterceptorsByTag(newTags) { + var tags = init.interceptorsByTag; + if (!tags) { + init.interceptorsByTag = newTags; + return; + } + copyProperties(newTags, tags); + } + function setOrUpdateLeafTags(newTags) { + var tags = init.leafTags; + if (!tags) { + init.leafTags = newTags; + return; + } + copyProperties(newTags, tags); + } + function updateTypes(newTypes) { + var types = init.types; + var length = types.length; + types.push.apply(types, newTypes); + return length; + } + function updateHolder(holder, newHolder) { + copyProperties(newHolder, holder); + return holder; + } + var hunkHelpers = function() { + var mkInstance = function(isIntercepted, requiredParameterCount, optionalParameterDefaultValues, callNames, applyIndex) { + return function(container, getterName, name, funType) { + return installInstanceTearOff(container, getterName, isIntercepted, requiredParameterCount, optionalParameterDefaultValues, callNames, [name], funType, applyIndex, false); + }; + }, + mkStatic = function(requiredParameterCount, optionalParameterDefaultValues, callNames, applyIndex) { + return function(container, getterName, name, funType) { + return installStaticTearOff(container, getterName, requiredParameterCount, optionalParameterDefaultValues, callNames, [name], funType, applyIndex); + }; + }; + return {inherit: inherit, inheritMany: inheritMany, mixin: mixinEasy, mixinHard: mixinHard, installStaticTearOff: installStaticTearOff, installInstanceTearOff: installInstanceTearOff, _instance_0u: mkInstance(0, 0, null, ["call$0"], 0), _instance_1u: mkInstance(0, 1, null, ["call$1"], 0), _instance_2u: mkInstance(0, 2, null, ["call$2"], 0), _instance_0i: mkInstance(1, 0, null, ["call$0"], 0), _instance_1i: mkInstance(1, 1, null, ["call$1"], 0), _instance_2i: mkInstance(1, 2, null, ["call$2"], 0), _static_0: mkStatic(0, null, ["call$0"], 0), _static_1: mkStatic(1, null, ["call$1"], 0), _static_2: mkStatic(2, null, ["call$2"], 0), makeConstList: makeConstList, lazy: lazy, lazyFinal: lazyFinal, lazyOld: lazyOld, updateHolder: updateHolder, convertToFastObject: convertToFastObject, updateTypes: updateTypes, setOrUpdateInterceptorsByTag: setOrUpdateInterceptorsByTag, setOrUpdateLeafTags: setOrUpdateLeafTags}; + }(); + function initializeDeferredHunk(hunk) { + typesOffset = init.types.length; + hunk(hunkHelpers, init, holders, $); + } + var J = { + makeDispatchRecord(interceptor, proto, extension, indexability) { + return {i: interceptor, p: proto, e: extension, x: indexability}; + }, + getNativeInterceptor(object) { + var proto, objectProto, $constructor, interceptor, t1, + record = object[init.dispatchPropertyName]; + if (record == null) + if ($.initNativeDispatchFlag == null) { + A.initNativeDispatch(); + record = object[init.dispatchPropertyName]; + } + if (record != null) { + proto = record.p; + if (false === proto) + return record.i; + if (true === proto) + return object; + objectProto = Object.getPrototypeOf(object); + if (proto === objectProto) + return record.i; + if (record.e === objectProto) + throw A.wrapException(A.UnimplementedError$("Return interceptor for " + A.S(proto(object, record)))); + } + $constructor = object.constructor; + if ($constructor == null) + interceptor = null; + else { + t1 = $._JS_INTEROP_INTERCEPTOR_TAG; + if (t1 == null) + t1 = $._JS_INTEROP_INTERCEPTOR_TAG = init.getIsolateTag("_$dart_js"); + interceptor = $constructor[t1]; + } + if (interceptor != null) + return interceptor; + interceptor = A.lookupAndCacheInterceptor(object); + if (interceptor != null) + return interceptor; + if (typeof object == "function") + return B.JavaScriptFunction_methods; + proto = Object.getPrototypeOf(object); + if (proto == null) + return B.PlainJavaScriptObject_methods; + if (proto === Object.prototype) + return B.PlainJavaScriptObject_methods; + if (typeof $constructor == "function") { + t1 = $._JS_INTEROP_INTERCEPTOR_TAG; + if (t1 == null) + t1 = $._JS_INTEROP_INTERCEPTOR_TAG = init.getIsolateTag("_$dart_js"); + Object.defineProperty($constructor, t1, {value: B.UnknownJavaScriptObject_methods, enumerable: false, writable: true, configurable: true}); + return B.UnknownJavaScriptObject_methods; + } + return B.UnknownJavaScriptObject_methods; + }, + JSArray_JSArray$fixed($length, $E) { + if ($length < 0 || $length > 4294967295) + throw A.wrapException(A.RangeError$range($length, 0, 4294967295, "length", null)); + return J.JSArray_JSArray$markFixed(new Array($length), $E); + }, + JSArray_JSArray$growable($length, $E) { + if ($length < 0) + throw A.wrapException(A.ArgumentError$("Length must be a non-negative integer: " + $length, null)); + return A._setArrayType(new Array($length), $E._eval$1("JSArray<0>")); + }, + JSArray_JSArray$markFixed(allocation, $E) { + return J.JSArray_markFixedList(A._setArrayType(allocation, $E._eval$1("JSArray<0>")), $E); + }, + JSArray_markFixedList(list, $T) { + list.fixed$length = Array; + return list; + }, + JSArray_markUnmodifiableList(list) { + list.fixed$length = Array; + list.immutable$list = Array; + return list; + }, + JSString__isWhitespace(codeUnit) { + if (codeUnit < 256) + switch (codeUnit) { + case 9: + case 10: + case 11: + case 12: + case 13: + case 32: + case 133: + case 160: + return true; + default: + return false; + } + switch (codeUnit) { + case 5760: + case 8192: + case 8193: + case 8194: + case 8195: + case 8196: + case 8197: + case 8198: + case 8199: + case 8200: + case 8201: + case 8202: + case 8232: + case 8233: + case 8239: + case 8287: + case 12288: + case 65279: + return true; + default: + return false; + } + }, + JSString__skipLeadingWhitespace(string, index) { + var t1, codeUnit; + for (t1 = string.length; index < t1;) { + codeUnit = string.charCodeAt(index); + if (codeUnit !== 32 && codeUnit !== 13 && !J.JSString__isWhitespace(codeUnit)) + break; + ++index; + } + return index; + }, + JSString__skipTrailingWhitespace(string, index) { + var t1, index0, codeUnit; + for (t1 = string.length; index > 0; index = index0) { + index0 = index - 1; + if (!(index0 < t1)) + return A.ioore(string, index0); + codeUnit = string.charCodeAt(index0); + if (codeUnit !== 32 && codeUnit !== 13 && !J.JSString__isWhitespace(codeUnit)) + break; + } + return index; + }, + getInterceptor$(receiver) { + if (typeof receiver == "number") { + if (Math.floor(receiver) == receiver) + return J.JSInt.prototype; + return J.JSNumNotInt.prototype; + } + if (typeof receiver == "string") + return J.JSString.prototype; + if (receiver == null) + return J.JSNull.prototype; + if (typeof receiver == "boolean") + return J.JSBool.prototype; + if (Array.isArray(receiver)) + return J.JSArray.prototype; + if (typeof receiver != "object") { + if (typeof receiver == "function") + return J.JavaScriptFunction.prototype; + if (typeof receiver == "symbol") + return J.JavaScriptSymbol.prototype; + if (typeof receiver == "bigint") + return J.JavaScriptBigInt.prototype; + return receiver; + } + if (receiver instanceof A.Object) + return receiver; + return J.getNativeInterceptor(receiver); + }, + getInterceptor$asx(receiver) { + if (typeof receiver == "string") + return J.JSString.prototype; + if (receiver == null) + return receiver; + if (Array.isArray(receiver)) + return J.JSArray.prototype; + if (typeof receiver != "object") { + if (typeof receiver == "function") + return J.JavaScriptFunction.prototype; + if (typeof receiver == "symbol") + return J.JavaScriptSymbol.prototype; + if (typeof receiver == "bigint") + return J.JavaScriptBigInt.prototype; + return receiver; + } + if (receiver instanceof A.Object) + return receiver; + return J.getNativeInterceptor(receiver); + }, + getInterceptor$ax(receiver) { + if (receiver == null) + return receiver; + if (Array.isArray(receiver)) + return J.JSArray.prototype; + if (typeof receiver != "object") { + if (typeof receiver == "function") + return J.JavaScriptFunction.prototype; + if (typeof receiver == "symbol") + return J.JavaScriptSymbol.prototype; + if (typeof receiver == "bigint") + return J.JavaScriptBigInt.prototype; + return receiver; + } + if (receiver instanceof A.Object) + return receiver; + return J.getNativeInterceptor(receiver); + }, + getInterceptor$n(receiver) { + if (typeof receiver == "number") + return J.JSNumber.prototype; + if (receiver == null) + return receiver; + if (!(receiver instanceof A.Object)) + return J.UnknownJavaScriptObject.prototype; + return receiver; + }, + getInterceptor$s(receiver) { + if (typeof receiver == "string") + return J.JSString.prototype; + if (receiver == null) + return receiver; + if (!(receiver instanceof A.Object)) + return J.UnknownJavaScriptObject.prototype; + return receiver; + }, + getInterceptor$x(receiver) { + if (receiver == null) + return receiver; + if (typeof receiver != "object") { + if (typeof receiver == "function") + return J.JavaScriptFunction.prototype; + if (typeof receiver == "symbol") + return J.JavaScriptSymbol.prototype; + if (typeof receiver == "bigint") + return J.JavaScriptBigInt.prototype; + return receiver; + } + if (receiver instanceof A.Object) + return receiver; + return J.getNativeInterceptor(receiver); + }, + getInterceptor$z(receiver) { + if (receiver == null) + return receiver; + if (!(receiver instanceof A.Object)) + return J.UnknownJavaScriptObject.prototype; + return receiver; + }, + get$hashCode$(receiver) { + return J.getInterceptor$(receiver).get$hashCode(receiver); + }, + get$isEmpty$asx(receiver) { + return J.getInterceptor$asx(receiver).get$isEmpty(receiver); + }, + get$isNotEmpty$asx(receiver) { + return J.getInterceptor$asx(receiver).get$isNotEmpty(receiver); + }, + get$iterator$ax(receiver) { + return J.getInterceptor$ax(receiver).get$iterator(receiver); + }, + get$keys$x(receiver) { + return J.getInterceptor$x(receiver).get$keys(receiver); + }, + get$length$asx(receiver) { + return J.getInterceptor$asx(receiver).get$length(receiver); + }, + get$parent$z(receiver) { + return J.getInterceptor$z(receiver).get$parent(receiver); + }, + get$runtimeType$(receiver) { + return J.getInterceptor$(receiver).get$runtimeType(receiver); + }, + $eq$(receiver, a0) { + if (receiver == null) + return a0 == null; + if (typeof receiver != "object") + return a0 != null && receiver === a0; + return J.getInterceptor$(receiver).$eq(receiver, a0); + }, + $index$asx(receiver, a0) { + if (typeof a0 === "number") + if (Array.isArray(receiver) || typeof receiver == "string" || A.isJsIndexable(receiver, receiver[init.dispatchPropertyName])) + if (a0 >>> 0 === a0 && a0 < receiver.length) + return receiver[a0]; + return J.getInterceptor$asx(receiver).$index(receiver, a0); + }, + $indexSet$ax(receiver, a0, a1) { + return J.getInterceptor$ax(receiver).$indexSet(receiver, a0, a1); + }, + allMatches$1$s(receiver, a0) { + return J.getInterceptor$s(receiver).allMatches$1(receiver, a0); + }, + allMatches$2$s(receiver, a0, a1) { + return J.getInterceptor$s(receiver).allMatches$2(receiver, a0, a1); + }, + cancel$0$z(receiver) { + return J.getInterceptor$z(receiver).cancel$0(receiver); + }, + cast$1$0$ax(receiver, $T1) { + return J.getInterceptor$ax(receiver).cast$1$0(receiver, $T1); + }, + codeUnitAt$1$s(receiver, a0) { + return J.getInterceptor$s(receiver).codeUnitAt$1(receiver, a0); + }, + contains$1$asx(receiver, a0) { + return J.getInterceptor$asx(receiver).contains$1(receiver, a0); + }, + containsKey$1$x(receiver, a0) { + return J.getInterceptor$x(receiver).containsKey$1(receiver, a0); + }, + elementAt$1$ax(receiver, a0) { + return J.getInterceptor$ax(receiver).elementAt$1(receiver, a0); + }, + endsWith$1$s(receiver, a0) { + return J.getInterceptor$s(receiver).endsWith$1(receiver, a0); + }, + forEach$1$x(receiver, a0) { + return J.getInterceptor$x(receiver).forEach$1(receiver, a0); + }, + map$1$1$ax(receiver, a0, $T1) { + return J.getInterceptor$ax(receiver).map$1$1(receiver, a0, $T1); + }, + matchAsPrefix$2$s(receiver, a0, a1) { + return J.getInterceptor$s(receiver).matchAsPrefix$2(receiver, a0, a1); + }, + noSuchMethod$1$(receiver, a0) { + return J.getInterceptor$(receiver).noSuchMethod$1(receiver, a0); + }, + skip$1$ax(receiver, a0) { + return J.getInterceptor$ax(receiver).skip$1(receiver, a0); + }, + toInt$0$n(receiver) { + return J.getInterceptor$n(receiver).toInt$0(receiver); + }, + toList$0$ax(receiver) { + return J.getInterceptor$ax(receiver).toList$0(receiver); + }, + toString$0$(receiver) { + return J.getInterceptor$(receiver).toString$0(receiver); + }, + Interceptor: function Interceptor() { + }, + JSBool: function JSBool() { + }, + JSNull: function JSNull() { + }, + JavaScriptObject: function JavaScriptObject() { + }, + LegacyJavaScriptObject: function LegacyJavaScriptObject() { + }, + PlainJavaScriptObject: function PlainJavaScriptObject() { + }, + UnknownJavaScriptObject: function UnknownJavaScriptObject() { + }, + JavaScriptFunction: function JavaScriptFunction() { + }, + JavaScriptBigInt: function JavaScriptBigInt() { + }, + JavaScriptSymbol: function JavaScriptSymbol() { + }, + JSArray: function JSArray(t0) { + this.$ti = t0; + }, + JSUnmodifiableArray: function JSUnmodifiableArray(t0) { + this.$ti = t0; + }, + ArrayIterator: function ArrayIterator(t0, t1, t2) { + var _ = this; + _._iterable = t0; + _.__interceptors$_length = t1; + _._index = 0; + _._current = null; + _.$ti = t2; + }, + JSNumber: function JSNumber() { + }, + JSInt: function JSInt() { + }, + JSNumNotInt: function JSNumNotInt() { + }, + JSString: function JSString() { + } + }, + A = {JS_CONST: function JS_CONST() { + }, + CastIterable_CastIterable(source, $S, $T) { + if ($S._eval$1("EfficientLengthIterable<0>")._is(source)) + return new A._EfficientLengthCastIterable(source, $S._eval$1("@<0>")._bind$1($T)._eval$1("_EfficientLengthCastIterable<1,2>")); + return new A.CastIterable(source, $S._eval$1("@<0>")._bind$1($T)._eval$1("CastIterable<1,2>")); + }, + hexDigitValue(char) { + var letter, + digit = char ^ 48; + if (digit <= 9) + return digit; + letter = char | 32; + if (97 <= letter && letter <= 102) + return letter - 87; + return -1; + }, + SystemHash_combine(hash, value) { + hash = hash + value & 536870911; + hash = hash + ((hash & 524287) << 10) & 536870911; + return hash ^ hash >>> 6; + }, + SystemHash_finish(hash) { + hash = hash + ((hash & 67108863) << 3) & 536870911; + hash ^= hash >>> 11; + return hash + ((hash & 16383) << 15) & 536870911; + }, + checkNotNullable(value, $name, $T) { + return value; + }, + isToStringVisiting(object) { + var t1, i; + for (t1 = $.toStringVisiting.length, i = 0; i < t1; ++i) + if (object === $.toStringVisiting[i]) + return true; + return false; + }, + SubListIterable$(_iterable, _start, _endOrLength, $E) { + A.RangeError_checkNotNegative(_start, "start"); + if (_endOrLength != null) { + A.RangeError_checkNotNegative(_endOrLength, "end"); + if (_start > _endOrLength) + A.throwExpression(A.RangeError$range(_start, 0, _endOrLength, "start", null)); + } + return new A.SubListIterable(_iterable, _start, _endOrLength, $E._eval$1("SubListIterable<0>")); + }, + MappedIterable_MappedIterable(iterable, $function, $S, $T) { + if (type$.EfficientLengthIterable_dynamic._is(iterable)) + return new A.EfficientLengthMappedIterable(iterable, $function, $S._eval$1("@<0>")._bind$1($T)._eval$1("EfficientLengthMappedIterable<1,2>")); + return new A.MappedIterable(iterable, $function, $S._eval$1("@<0>")._bind$1($T)._eval$1("MappedIterable<1,2>")); + }, + TakeIterable_TakeIterable(iterable, takeCount, $E) { + var _s9_ = "takeCount"; + A.ArgumentError_checkNotNull(takeCount, _s9_, type$.int); + A.RangeError_checkNotNegative(takeCount, _s9_); + if (type$.EfficientLengthIterable_dynamic._is(iterable)) + return new A.EfficientLengthTakeIterable(iterable, takeCount, $E._eval$1("EfficientLengthTakeIterable<0>")); + return new A.TakeIterable(iterable, takeCount, $E._eval$1("TakeIterable<0>")); + }, + SkipIterable_SkipIterable(iterable, count, $E) { + var _s5_ = "count"; + if (type$.EfficientLengthIterable_dynamic._is(iterable)) { + A.ArgumentError_checkNotNull(count, _s5_, type$.int); + A.RangeError_checkNotNegative(count, _s5_); + return new A.EfficientLengthSkipIterable(iterable, count, $E._eval$1("EfficientLengthSkipIterable<0>")); + } + A.ArgumentError_checkNotNull(count, _s5_, type$.int); + A.RangeError_checkNotNegative(count, _s5_); + return new A.SkipIterable(iterable, count, $E._eval$1("SkipIterable<0>")); + }, + IterableElementError_noElement() { + return new A.StateError("No element"); + }, + IterableElementError_tooFew() { + return new A.StateError("Too few elements"); + }, + CastStream: function CastStream(t0, t1) { + this._source = t0; + this.$ti = t1; + }, + CastStreamSubscription: function CastStreamSubscription(t0, t1, t2) { + var _ = this; + _._source = t0; + _.__internal$_zone = t1; + _._handleError = _._handleData = null; + _.$ti = t2; + }, + _CastIterableBase: function _CastIterableBase() { + }, + CastIterator: function CastIterator(t0, t1) { + this._source = t0; + this.$ti = t1; + }, + CastIterable: function CastIterable(t0, t1) { + this._source = t0; + this.$ti = t1; + }, + _EfficientLengthCastIterable: function _EfficientLengthCastIterable(t0, t1) { + this._source = t0; + this.$ti = t1; + }, + _CastListBase: function _CastListBase() { + }, + CastList: function CastList(t0, t1) { + this._source = t0; + this.$ti = t1; + }, + LateError: function LateError(t0) { + this._message = t0; + }, + CodeUnits: function CodeUnits(t0) { + this._string = t0; + }, + nullFuture_closure: function nullFuture_closure() { + }, + SentinelValue: function SentinelValue() { + }, + EfficientLengthIterable: function EfficientLengthIterable() { + }, + ListIterable: function ListIterable() { + }, + SubListIterable: function SubListIterable(t0, t1, t2, t3) { + var _ = this; + _.__internal$_iterable = t0; + _._start = t1; + _._endOrLength = t2; + _.$ti = t3; + }, + ListIterator: function ListIterator(t0, t1, t2) { + var _ = this; + _.__internal$_iterable = t0; + _.__internal$_length = t1; + _.__internal$_index = 0; + _.__internal$_current = null; + _.$ti = t2; + }, + MappedIterable: function MappedIterable(t0, t1, t2) { + this.__internal$_iterable = t0; + this._f = t1; + this.$ti = t2; + }, + EfficientLengthMappedIterable: function EfficientLengthMappedIterable(t0, t1, t2) { + this.__internal$_iterable = t0; + this._f = t1; + this.$ti = t2; + }, + MappedIterator: function MappedIterator(t0, t1, t2) { + var _ = this; + _.__internal$_current = null; + _._iterator = t0; + _._f = t1; + _.$ti = t2; + }, + MappedListIterable: function MappedListIterable(t0, t1, t2) { + this._source = t0; + this._f = t1; + this.$ti = t2; + }, + WhereIterable: function WhereIterable(t0, t1, t2) { + this.__internal$_iterable = t0; + this._f = t1; + this.$ti = t2; + }, + WhereIterator: function WhereIterator(t0, t1, t2) { + this._iterator = t0; + this._f = t1; + this.$ti = t2; + }, + ExpandIterable: function ExpandIterable(t0, t1, t2) { + this.__internal$_iterable = t0; + this._f = t1; + this.$ti = t2; + }, + ExpandIterator: function ExpandIterator(t0, t1, t2, t3) { + var _ = this; + _._iterator = t0; + _._f = t1; + _._currentExpansion = t2; + _.__internal$_current = null; + _.$ti = t3; + }, + TakeIterable: function TakeIterable(t0, t1, t2) { + this.__internal$_iterable = t0; + this._takeCount = t1; + this.$ti = t2; + }, + EfficientLengthTakeIterable: function EfficientLengthTakeIterable(t0, t1, t2) { + this.__internal$_iterable = t0; + this._takeCount = t1; + this.$ti = t2; + }, + TakeIterator: function TakeIterator(t0, t1, t2) { + this._iterator = t0; + this._remaining = t1; + this.$ti = t2; + }, + SkipIterable: function SkipIterable(t0, t1, t2) { + this.__internal$_iterable = t0; + this._skipCount = t1; + this.$ti = t2; + }, + EfficientLengthSkipIterable: function EfficientLengthSkipIterable(t0, t1, t2) { + this.__internal$_iterable = t0; + this._skipCount = t1; + this.$ti = t2; + }, + SkipIterator: function SkipIterator(t0, t1, t2) { + this._iterator = t0; + this._skipCount = t1; + this.$ti = t2; + }, + SkipWhileIterable: function SkipWhileIterable(t0, t1, t2) { + this.__internal$_iterable = t0; + this._f = t1; + this.$ti = t2; + }, + SkipWhileIterator: function SkipWhileIterator(t0, t1, t2) { + var _ = this; + _._iterator = t0; + _._f = t1; + _._hasSkipped = false; + _.$ti = t2; + }, + EmptyIterable: function EmptyIterable(t0) { + this.$ti = t0; + }, + EmptyIterator: function EmptyIterator(t0) { + this.$ti = t0; + }, + WhereTypeIterable: function WhereTypeIterable(t0, t1) { + this._source = t0; + this.$ti = t1; + }, + WhereTypeIterator: function WhereTypeIterator(t0, t1) { + this._source = t0; + this.$ti = t1; + }, + FixedLengthListMixin: function FixedLengthListMixin() { + }, + UnmodifiableListMixin: function UnmodifiableListMixin() { + }, + UnmodifiableListBase: function UnmodifiableListBase() { + }, + ReversedListIterable: function ReversedListIterable(t0, t1) { + this._source = t0; + this.$ti = t1; + }, + Symbol: function Symbol(t0) { + this._name = t0; + }, + __CastListBase__CastIterableBase_ListMixin: function __CastListBase__CastIterableBase_ListMixin() { + }, + ConstantMap__throwUnmodifiable() { + throw A.wrapException(A.UnsupportedError$("Cannot modify unmodifiable Map")); + }, + unminifyOrTag(rawClassName) { + var preserved = init.mangledGlobalNames[rawClassName]; + if (preserved != null) + return preserved; + return rawClassName; + }, + isJsIndexable(object, record) { + var result; + if (record != null) { + result = record.x; + if (result != null) + return result; + } + return type$.JavaScriptIndexingBehavior_dynamic._is(object); + }, + S(value) { + var result; + if (typeof value == "string") + return value; + if (typeof value == "number") { + if (value !== 0) + return "" + value; + } else if (true === value) + return "true"; + else if (false === value) + return "false"; + else if (value == null) + return "null"; + result = J.toString$0$(value); + return result; + }, + Primitives_objectHashCode(object) { + var hash, + property = $.Primitives__identityHashCodeProperty; + if (property == null) + property = $.Primitives__identityHashCodeProperty = Symbol("identityHashCode"); + hash = object[property]; + if (hash == null) { + hash = Math.random() * 0x3fffffff | 0; + object[property] = hash; + } + return hash; + }, + Primitives_parseInt(source, radix) { + var decimalMatch, maxCharCode, digitsPart, t1, i, _null = null, + match = /^\s*[+-]?((0x[a-f0-9]+)|(\d+)|([a-z0-9]+))\s*$/i.exec(source); + if (match == null) + return _null; + if (3 >= match.length) + return A.ioore(match, 3); + decimalMatch = match[3]; + if (radix == null) { + if (decimalMatch != null) + return parseInt(source, 10); + if (match[2] != null) + return parseInt(source, 16); + return _null; + } + if (radix < 2 || radix > 36) + throw A.wrapException(A.RangeError$range(radix, 2, 36, "radix", _null)); + if (radix === 10 && decimalMatch != null) + return parseInt(source, 10); + if (radix < 10 || decimalMatch == null) { + maxCharCode = radix <= 10 ? 47 + radix : 86 + radix; + digitsPart = match[1]; + for (t1 = digitsPart.length, i = 0; i < t1; ++i) + if ((digitsPart.charCodeAt(i) | 32) > maxCharCode) + return _null; + } + return parseInt(source, radix); + }, + Primitives_objectTypeName(object) { + return A.Primitives__objectTypeNameNewRti(object); + }, + Primitives__objectTypeNameNewRti(object) { + var interceptor, dispatchName, $constructor, constructorName; + if (object instanceof A.Object) + return A._rtiToString(A.instanceType(object), null); + interceptor = J.getInterceptor$(object); + if (interceptor === B.Interceptor_methods || interceptor === B.JavaScriptObject_methods || type$.UnknownJavaScriptObject._is(object)) { + dispatchName = B.C_JS_CONST(object); + if (dispatchName !== "Object" && dispatchName !== "") + return dispatchName; + $constructor = object.constructor; + if (typeof $constructor == "function") { + constructorName = $constructor.name; + if (typeof constructorName == "string" && constructorName !== "Object" && constructorName !== "") + return constructorName; + } + } + return A._rtiToString(A.instanceType(object), null); + }, + Primitives_safeToString(object) { + if (typeof object == "number" || A._isBool(object)) + return J.toString$0$(object); + if (typeof object == "string") + return JSON.stringify(object); + if (object instanceof A.Closure) + return object.toString$0(0); + return "Instance of '" + A.Primitives_objectTypeName(object) + "'"; + }, + Primitives_currentUri() { + if (!!self.location) + return self.location.href; + return null; + }, + Primitives__fromCharCodeApply(array) { + var result, i, i0, chunkEnd, + end = array.length; + if (end <= 500) + return String.fromCharCode.apply(null, array); + for (result = "", i = 0; i < end; i = i0) { + i0 = i + 500; + chunkEnd = i0 < end ? i0 : end; + result += String.fromCharCode.apply(null, array.slice(i, chunkEnd)); + } + return result; + }, + Primitives_stringFromCodePoints(codePoints) { + var t1, _i, i, + a = A._setArrayType([], type$.JSArray_int); + for (t1 = codePoints.length, _i = 0; _i < codePoints.length; codePoints.length === t1 || (0, A.throwConcurrentModificationError)(codePoints), ++_i) { + i = codePoints[_i]; + if (!A._isInt(i)) + throw A.wrapException(A.argumentErrorValue(i)); + if (i <= 65535) + B.JSArray_methods.add$1(a, i); + else if (i <= 1114111) { + B.JSArray_methods.add$1(a, 55296 + (B.JSInt_methods._shrOtherPositive$1(i - 65536, 10) & 1023)); + B.JSArray_methods.add$1(a, 56320 + (i & 1023)); + } else + throw A.wrapException(A.argumentErrorValue(i)); + } + return A.Primitives__fromCharCodeApply(a); + }, + Primitives_stringFromCharCodes(charCodes) { + var t1, _i, i; + for (t1 = charCodes.length, _i = 0; _i < t1; ++_i) { + i = charCodes[_i]; + if (!A._isInt(i)) + throw A.wrapException(A.argumentErrorValue(i)); + if (i < 0) + throw A.wrapException(A.argumentErrorValue(i)); + if (i > 65535) + return A.Primitives_stringFromCodePoints(charCodes); + } + return A.Primitives__fromCharCodeApply(charCodes); + }, + Primitives_stringFromNativeUint8List(charCodes, start, end) { + var i, result, i0, chunkEnd; + if (end <= 500 && start === 0 && end === charCodes.length) + return String.fromCharCode.apply(null, charCodes); + for (i = start, result = ""; i < end; i = i0) { + i0 = i + 500; + chunkEnd = i0 < end ? i0 : end; + result += String.fromCharCode.apply(null, charCodes.subarray(i, chunkEnd)); + } + return result; + }, + Primitives_stringFromCharCode(charCode) { + var bits; + if (0 <= charCode) { + if (charCode <= 65535) + return String.fromCharCode(charCode); + if (charCode <= 1114111) { + bits = charCode - 65536; + return String.fromCharCode((B.JSInt_methods._shrOtherPositive$1(bits, 10) | 55296) >>> 0, bits & 1023 | 56320); + } + } + throw A.wrapException(A.RangeError$range(charCode, 0, 1114111, null, null)); + }, + Primitives_lazyAsJsDate(receiver) { + if (receiver.date === void 0) + receiver.date = new Date(receiver._core$_value); + return receiver.date; + }, + Primitives_getYear(receiver) { + var t1 = A.Primitives_lazyAsJsDate(receiver).getUTCFullYear() + 0; + return t1; + }, + Primitives_getMonth(receiver) { + var t1 = A.Primitives_lazyAsJsDate(receiver).getUTCMonth() + 1; + return t1; + }, + Primitives_getDay(receiver) { + var t1 = A.Primitives_lazyAsJsDate(receiver).getUTCDate() + 0; + return t1; + }, + Primitives_getHours(receiver) { + var t1 = A.Primitives_lazyAsJsDate(receiver).getUTCHours() + 0; + return t1; + }, + Primitives_getMinutes(receiver) { + var t1 = A.Primitives_lazyAsJsDate(receiver).getUTCMinutes() + 0; + return t1; + }, + Primitives_getSeconds(receiver) { + var t1 = A.Primitives_lazyAsJsDate(receiver).getUTCSeconds() + 0; + return t1; + }, + Primitives_getMilliseconds(receiver) { + var t1 = A.Primitives_lazyAsJsDate(receiver).getUTCMilliseconds() + 0; + return t1; + }, + Primitives_functionNoSuchMethod($function, positionalArguments, namedArguments) { + var $arguments, namedArgumentList, t1 = {}; + t1.argumentCount = 0; + $arguments = []; + namedArgumentList = []; + t1.argumentCount = positionalArguments.length; + B.JSArray_methods.addAll$1($arguments, positionalArguments); + t1.names = ""; + if (namedArguments != null && namedArguments._length !== 0) + namedArguments.forEach$1(0, new A.Primitives_functionNoSuchMethod_closure(t1, namedArgumentList, $arguments)); + return J.noSuchMethod$1$($function, new A.JSInvocationMirror(B.Symbol_call, 0, $arguments, namedArgumentList, 0)); + }, + Primitives_applyFunction($function, positionalArguments, namedArguments) { + var t1, argumentCount, jsStub; + if (Array.isArray(positionalArguments)) + t1 = namedArguments == null || namedArguments._length === 0; + else + t1 = false; + if (t1) { + argumentCount = positionalArguments.length; + if (argumentCount === 0) { + if (!!$function.call$0) + return $function.call$0(); + } else if (argumentCount === 1) { + if (!!$function.call$1) + return $function.call$1(positionalArguments[0]); + } else if (argumentCount === 2) { + if (!!$function.call$2) + return $function.call$2(positionalArguments[0], positionalArguments[1]); + } else if (argumentCount === 3) { + if (!!$function.call$3) + return $function.call$3(positionalArguments[0], positionalArguments[1], positionalArguments[2]); + } else if (argumentCount === 4) { + if (!!$function.call$4) + return $function.call$4(positionalArguments[0], positionalArguments[1], positionalArguments[2], positionalArguments[3]); + } else if (argumentCount === 5) + if (!!$function.call$5) + return $function.call$5(positionalArguments[0], positionalArguments[1], positionalArguments[2], positionalArguments[3], positionalArguments[4]); + jsStub = $function["call" + "$" + argumentCount]; + if (jsStub != null) + return jsStub.apply($function, positionalArguments); + } + return A.Primitives__generalApplyFunction($function, positionalArguments, namedArguments); + }, + Primitives__generalApplyFunction($function, positionalArguments, namedArguments) { + var defaultValuesClosure, t1, defaultValues, interceptor, jsFunction, maxArguments, missingDefaults, keys, _i, defaultValue, used, key, + $arguments = Array.isArray(positionalArguments) ? positionalArguments : A.List_List$of(positionalArguments, true, type$.dynamic), + argumentCount = $arguments.length, + requiredParameterCount = $function.$requiredArgCount; + if (argumentCount < requiredParameterCount) + return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments); + defaultValuesClosure = $function.$defaultValues; + t1 = defaultValuesClosure == null; + defaultValues = !t1 ? defaultValuesClosure() : null; + interceptor = J.getInterceptor$($function); + jsFunction = interceptor["call*"]; + if (typeof jsFunction == "string") + jsFunction = interceptor[jsFunction]; + if (t1) { + if (namedArguments != null && namedArguments._length !== 0) + return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments); + if (argumentCount === requiredParameterCount) + return jsFunction.apply($function, $arguments); + return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments); + } + if (Array.isArray(defaultValues)) { + if (namedArguments != null && namedArguments._length !== 0) + return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments); + maxArguments = requiredParameterCount + defaultValues.length; + if (argumentCount > maxArguments) + return A.Primitives_functionNoSuchMethod($function, $arguments, null); + if (argumentCount < maxArguments) { + missingDefaults = defaultValues.slice(argumentCount - requiredParameterCount); + if ($arguments === positionalArguments) + $arguments = A.List_List$of($arguments, true, type$.dynamic); + B.JSArray_methods.addAll$1($arguments, missingDefaults); + } + return jsFunction.apply($function, $arguments); + } else { + if (argumentCount > requiredParameterCount) + return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments); + if ($arguments === positionalArguments) + $arguments = A.List_List$of($arguments, true, type$.dynamic); + keys = Object.keys(defaultValues); + if (namedArguments == null) + for (t1 = keys.length, _i = 0; _i < keys.length; keys.length === t1 || (0, A.throwConcurrentModificationError)(keys), ++_i) { + defaultValue = defaultValues[A._asString(keys[_i])]; + if (B.C__Required === defaultValue) + return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments); + B.JSArray_methods.add$1($arguments, defaultValue); + } + else { + for (t1 = keys.length, used = 0, _i = 0; _i < keys.length; keys.length === t1 || (0, A.throwConcurrentModificationError)(keys), ++_i) { + key = A._asString(keys[_i]); + if (namedArguments.containsKey$1(0, key)) { + ++used; + B.JSArray_methods.add$1($arguments, namedArguments.$index(0, key)); + } else { + defaultValue = defaultValues[key]; + if (B.C__Required === defaultValue) + return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments); + B.JSArray_methods.add$1($arguments, defaultValue); + } + } + if (used !== namedArguments._length) + return A.Primitives_functionNoSuchMethod($function, $arguments, namedArguments); + } + return jsFunction.apply($function, $arguments); + } + }, + iae(argument) { + throw A.wrapException(A.argumentErrorValue(argument)); + }, + ioore(receiver, index) { + if (receiver == null) + J.get$length$asx(receiver); + throw A.wrapException(A.diagnoseIndexError(receiver, index)); + }, + diagnoseIndexError(indexable, index) { + var $length, _s5_ = "index"; + if (!A._isInt(index)) + return new A.ArgumentError(true, index, _s5_, null); + $length = A._asInt(J.get$length$asx(indexable)); + if (index < 0 || index >= $length) + return A.IndexError$withLength(index, $length, indexable, _s5_); + return A.RangeError$value(index, _s5_); + }, + diagnoseRangeError(start, end, $length) { + if (start > $length) + return A.RangeError$range(start, 0, $length, "start", null); + if (end != null) + if (end < start || end > $length) + return A.RangeError$range(end, start, $length, "end", null); + return new A.ArgumentError(true, end, "end", null); + }, + argumentErrorValue(object) { + return new A.ArgumentError(true, object, null, null); + }, + wrapException(ex) { + return A.initializeExceptionWrapper(new Error(), ex); + }, + initializeExceptionWrapper(wrapper, ex) { + var t1; + if (ex == null) + ex = new A.TypeError(); + wrapper.dartException = ex; + t1 = A.toStringWrapper; + if ("defineProperty" in Object) { + Object.defineProperty(wrapper, "message", {get: t1}); + wrapper.name = ""; + } else + wrapper.toString = t1; + return wrapper; + }, + toStringWrapper() { + return J.toString$0$(this.dartException); + }, + throwExpression(ex) { + throw A.wrapException(ex); + }, + throwExpressionWithWrapper(ex, wrapper) { + throw A.initializeExceptionWrapper(wrapper, ex); + }, + throwConcurrentModificationError(collection) { + throw A.wrapException(A.ConcurrentModificationError$(collection)); + }, + TypeErrorDecoder_extractPattern(message) { + var match, $arguments, argumentsExpr, expr, method, receiver; + message = A.quoteStringForRegExp(message.replace(String({}), "$receiver$")); + match = message.match(/\\\$[a-zA-Z]+\\\$/g); + if (match == null) + match = A._setArrayType([], type$.JSArray_String); + $arguments = match.indexOf("\\$arguments\\$"); + argumentsExpr = match.indexOf("\\$argumentsExpr\\$"); + expr = match.indexOf("\\$expr\\$"); + method = match.indexOf("\\$method\\$"); + receiver = match.indexOf("\\$receiver\\$"); + return new A.TypeErrorDecoder(message.replace(new RegExp("\\\\\\$arguments\\\\\\$", "g"), "((?:x|[^x])*)").replace(new RegExp("\\\\\\$argumentsExpr\\\\\\$", "g"), "((?:x|[^x])*)").replace(new RegExp("\\\\\\$expr\\\\\\$", "g"), "((?:x|[^x])*)").replace(new RegExp("\\\\\\$method\\\\\\$", "g"), "((?:x|[^x])*)").replace(new RegExp("\\\\\\$receiver\\\\\\$", "g"), "((?:x|[^x])*)"), $arguments, argumentsExpr, expr, method, receiver); + }, + TypeErrorDecoder_provokeCallErrorOn(expression) { + return function($expr$) { + var $argumentsExpr$ = "$arguments$"; + try { + $expr$.$method$($argumentsExpr$); + } catch (e) { + return e.message; + } + }(expression); + }, + TypeErrorDecoder_provokePropertyErrorOn(expression) { + return function($expr$) { + try { + $expr$.$method$; + } catch (e) { + return e.message; + } + }(expression); + }, + JsNoSuchMethodError$(_message, match) { + var t1 = match == null, + t2 = t1 ? null : match.method; + return new A.JsNoSuchMethodError(_message, t2, t1 ? null : match.receiver); + }, + unwrapException(ex) { + if (ex == null) + return new A.NullThrownFromJavaScriptException(ex); + if (typeof ex !== "object") + return ex; + if ("dartException" in ex) + return A.saveStackTrace(ex, ex.dartException); + return A._unwrapNonDartException(ex); + }, + saveStackTrace(ex, error) { + if (type$.Error._is(error)) + if (error.$thrownJsError == null) + error.$thrownJsError = ex; + return error; + }, + _unwrapNonDartException(ex) { + var message, number, ieErrorCode, nsme, notClosure, nullCall, nullLiteralCall, undefCall, undefLiteralCall, nullProperty, undefProperty, undefLiteralProperty, match; + if (!("message" in ex)) + return ex; + message = ex.message; + if ("number" in ex && typeof ex.number == "number") { + number = ex.number; + ieErrorCode = number & 65535; + if ((B.JSInt_methods._shrOtherPositive$1(number, 16) & 8191) === 10) + switch (ieErrorCode) { + case 438: + return A.saveStackTrace(ex, A.JsNoSuchMethodError$(A.S(message) + " (Error " + ieErrorCode + ")", null)); + case 445: + case 5007: + A.S(message); + return A.saveStackTrace(ex, new A.NullError()); + } + } + if (ex instanceof TypeError) { + nsme = $.$get$TypeErrorDecoder_noSuchMethodPattern(); + notClosure = $.$get$TypeErrorDecoder_notClosurePattern(); + nullCall = $.$get$TypeErrorDecoder_nullCallPattern(); + nullLiteralCall = $.$get$TypeErrorDecoder_nullLiteralCallPattern(); + undefCall = $.$get$TypeErrorDecoder_undefinedCallPattern(); + undefLiteralCall = $.$get$TypeErrorDecoder_undefinedLiteralCallPattern(); + nullProperty = $.$get$TypeErrorDecoder_nullPropertyPattern(); + $.$get$TypeErrorDecoder_nullLiteralPropertyPattern(); + undefProperty = $.$get$TypeErrorDecoder_undefinedPropertyPattern(); + undefLiteralProperty = $.$get$TypeErrorDecoder_undefinedLiteralPropertyPattern(); + match = nsme.matchTypeError$1(message); + if (match != null) + return A.saveStackTrace(ex, A.JsNoSuchMethodError$(A._asString(message), match)); + else { + match = notClosure.matchTypeError$1(message); + if (match != null) { + match.method = "call"; + return A.saveStackTrace(ex, A.JsNoSuchMethodError$(A._asString(message), match)); + } else if (nullCall.matchTypeError$1(message) != null || nullLiteralCall.matchTypeError$1(message) != null || undefCall.matchTypeError$1(message) != null || undefLiteralCall.matchTypeError$1(message) != null || nullProperty.matchTypeError$1(message) != null || nullLiteralCall.matchTypeError$1(message) != null || undefProperty.matchTypeError$1(message) != null || undefLiteralProperty.matchTypeError$1(message) != null) { + A._asString(message); + return A.saveStackTrace(ex, new A.NullError()); + } + } + return A.saveStackTrace(ex, new A.UnknownJsTypeError(typeof message == "string" ? message : "")); + } + if (ex instanceof RangeError) { + if (typeof message == "string" && message.indexOf("call stack") !== -1) + return new A.StackOverflowError(); + message = function(ex) { + try { + return String(ex); + } catch (e) { + } + return null; + }(ex); + return A.saveStackTrace(ex, new A.ArgumentError(false, null, null, typeof message == "string" ? message.replace(/^RangeError:\s*/, "") : message)); + } + if (typeof InternalError == "function" && ex instanceof InternalError) + if (typeof message == "string" && message === "too much recursion") + return new A.StackOverflowError(); + return ex; + }, + getTraceFromException(exception) { + var trace; + if (exception == null) + return new A._StackTrace(exception); + trace = exception.$cachedTrace; + if (trace != null) + return trace; + trace = new A._StackTrace(exception); + if (typeof exception === "object") + exception.$cachedTrace = trace; + return trace; + }, + objectHashCode(object) { + if (object == null) + return J.get$hashCode$(object); + if (typeof object == "object") + return A.Primitives_objectHashCode(object); + return J.get$hashCode$(object); + }, + fillLiteralMap(keyValuePairs, result) { + var index, index0, index1, + $length = keyValuePairs.length; + for (index = 0; index < $length; index = index1) { + index0 = index + 1; + index1 = index0 + 1; + result.$indexSet(0, keyValuePairs[index], keyValuePairs[index0]); + } + return result; + }, + _invokeClosure(closure, numberOfArguments, arg1, arg2, arg3, arg4) { + type$.Function._as(closure); + switch (A._asInt(numberOfArguments)) { + case 0: + return closure.call$0(); + case 1: + return closure.call$1(arg1); + case 2: + return closure.call$2(arg1, arg2); + case 3: + return closure.call$3(arg1, arg2, arg3); + case 4: + return closure.call$4(arg1, arg2, arg3, arg4); + } + throw A.wrapException(new A._Exception("Unsupported number of arguments for wrapped closure")); + }, + convertDartClosureToJS(closure, arity) { + var $function = closure.$identity; + if (!!$function) + return $function; + $function = A.convertDartClosureToJSUncached(closure, arity); + closure.$identity = $function; + return $function; + }, + convertDartClosureToJSUncached(closure, arity) { + var entry; + switch (arity) { + case 0: + entry = closure.call$0; + break; + case 1: + entry = closure.call$1; + break; + case 2: + entry = closure.call$2; + break; + case 3: + entry = closure.call$3; + break; + case 4: + entry = closure.call$4; + break; + default: + entry = null; + } + if (entry != null) + return entry.bind(closure); + return function(closure, arity, invoke) { + return function(a1, a2, a3, a4) { + return invoke(closure, arity, a1, a2, a3, a4); + }; + }(closure, arity, A._invokeClosure); + }, + Closure_fromTearOff(parameters) { + var $prototype, $constructor, t2, trampoline, applyTrampoline, i, stub, stub0, stubName, stubCallName, + container = parameters.co, + isStatic = parameters.iS, + isIntercepted = parameters.iI, + needsDirectAccess = parameters.nDA, + applyTrampolineIndex = parameters.aI, + funsOrNames = parameters.fs, + callNames = parameters.cs, + $name = funsOrNames[0], + callName = callNames[0], + $function = container[$name], + t1 = parameters.fT; + t1.toString; + $prototype = isStatic ? Object.create(new A.StaticClosure().constructor.prototype) : Object.create(new A.BoundClosure(null, null).constructor.prototype); + $prototype.$initialize = $prototype.constructor; + if (isStatic) + $constructor = function static_tear_off() { + this.$initialize(); + }; + else + $constructor = function tear_off(a, b) { + this.$initialize(a, b); + }; + $prototype.constructor = $constructor; + $constructor.prototype = $prototype; + $prototype.$_name = $name; + $prototype.$_target = $function; + t2 = !isStatic; + if (t2) + trampoline = A.Closure_forwardCallTo($name, $function, isIntercepted, needsDirectAccess); + else { + $prototype.$static_name = $name; + trampoline = $function; + } + $prototype.$signature = A.Closure__computeSignatureFunctionNewRti(t1, isStatic, isIntercepted); + $prototype[callName] = trampoline; + for (applyTrampoline = trampoline, i = 1; i < funsOrNames.length; ++i) { + stub = funsOrNames[i]; + if (typeof stub == "string") { + stub0 = container[stub]; + stubName = stub; + stub = stub0; + } else + stubName = ""; + stubCallName = callNames[i]; + if (stubCallName != null) { + if (t2) + stub = A.Closure_forwardCallTo(stubName, stub, isIntercepted, needsDirectAccess); + $prototype[stubCallName] = stub; + } + if (i === applyTrampolineIndex) + applyTrampoline = stub; + } + $prototype["call*"] = applyTrampoline; + $prototype.$requiredArgCount = parameters.rC; + $prototype.$defaultValues = parameters.dV; + return $constructor; + }, + Closure__computeSignatureFunctionNewRti(functionType, isStatic, isIntercepted) { + if (typeof functionType == "number") + return functionType; + if (typeof functionType == "string") { + if (isStatic) + throw A.wrapException("Cannot compute signature for static tearoff."); + return function(recipe, evalOnReceiver) { + return function() { + return evalOnReceiver(this, recipe); + }; + }(functionType, A.BoundClosure_evalRecipe); + } + throw A.wrapException("Error in functionType of tearoff"); + }, + Closure_cspForwardCall(arity, needsDirectAccess, stubName, $function) { + var getReceiver = A.BoundClosure_receiverOf; + switch (needsDirectAccess ? -1 : arity) { + case 0: + return function(entry, receiverOf) { + return function() { + return receiverOf(this)[entry](); + }; + }(stubName, getReceiver); + case 1: + return function(entry, receiverOf) { + return function(a) { + return receiverOf(this)[entry](a); + }; + }(stubName, getReceiver); + case 2: + return function(entry, receiverOf) { + return function(a, b) { + return receiverOf(this)[entry](a, b); + }; + }(stubName, getReceiver); + case 3: + return function(entry, receiverOf) { + return function(a, b, c) { + return receiverOf(this)[entry](a, b, c); + }; + }(stubName, getReceiver); + case 4: + return function(entry, receiverOf) { + return function(a, b, c, d) { + return receiverOf(this)[entry](a, b, c, d); + }; + }(stubName, getReceiver); + case 5: + return function(entry, receiverOf) { + return function(a, b, c, d, e) { + return receiverOf(this)[entry](a, b, c, d, e); + }; + }(stubName, getReceiver); + default: + return function(f, receiverOf) { + return function() { + return f.apply(receiverOf(this), arguments); + }; + }($function, getReceiver); + } + }, + Closure_forwardCallTo(stubName, $function, isIntercepted, needsDirectAccess) { + var arity, t1; + if (isIntercepted) + return A.Closure_forwardInterceptedCallTo(stubName, $function, needsDirectAccess); + arity = $function.length; + t1 = A.Closure_cspForwardCall(arity, needsDirectAccess, stubName, $function); + return t1; + }, + Closure_cspForwardInterceptedCall(arity, needsDirectAccess, stubName, $function) { + var getReceiver = A.BoundClosure_receiverOf, + getInterceptor = A.BoundClosure_interceptorOf; + switch (needsDirectAccess ? -1 : arity) { + case 0: + throw A.wrapException(new A.RuntimeError("Intercepted function with no arguments.")); + case 1: + return function(entry, interceptorOf, receiverOf) { + return function() { + return interceptorOf(this)[entry](receiverOf(this)); + }; + }(stubName, getInterceptor, getReceiver); + case 2: + return function(entry, interceptorOf, receiverOf) { + return function(a) { + return interceptorOf(this)[entry](receiverOf(this), a); + }; + }(stubName, getInterceptor, getReceiver); + case 3: + return function(entry, interceptorOf, receiverOf) { + return function(a, b) { + return interceptorOf(this)[entry](receiverOf(this), a, b); + }; + }(stubName, getInterceptor, getReceiver); + case 4: + return function(entry, interceptorOf, receiverOf) { + return function(a, b, c) { + return interceptorOf(this)[entry](receiverOf(this), a, b, c); + }; + }(stubName, getInterceptor, getReceiver); + case 5: + return function(entry, interceptorOf, receiverOf) { + return function(a, b, c, d) { + return interceptorOf(this)[entry](receiverOf(this), a, b, c, d); + }; + }(stubName, getInterceptor, getReceiver); + case 6: + return function(entry, interceptorOf, receiverOf) { + return function(a, b, c, d, e) { + return interceptorOf(this)[entry](receiverOf(this), a, b, c, d, e); + }; + }(stubName, getInterceptor, getReceiver); + default: + return function(f, interceptorOf, receiverOf) { + return function() { + var a = [receiverOf(this)]; + Array.prototype.push.apply(a, arguments); + return f.apply(interceptorOf(this), a); + }; + }($function, getInterceptor, getReceiver); + } + }, + Closure_forwardInterceptedCallTo(stubName, $function, needsDirectAccess) { + var arity, t1; + if ($.BoundClosure__interceptorFieldNameCache == null) + $.BoundClosure__interceptorFieldNameCache = A.BoundClosure__computeFieldNamed("interceptor"); + if ($.BoundClosure__receiverFieldNameCache == null) + $.BoundClosure__receiverFieldNameCache = A.BoundClosure__computeFieldNamed("receiver"); + arity = $function.length; + t1 = A.Closure_cspForwardInterceptedCall(arity, needsDirectAccess, stubName, $function); + return t1; + }, + closureFromTearOff(parameters) { + return A.Closure_fromTearOff(parameters); + }, + BoundClosure_evalRecipe(closure, recipe) { + return A._Universe_evalInEnvironment(init.typeUniverse, A.instanceType(closure._receiver), recipe); + }, + BoundClosure_receiverOf(closure) { + return closure._receiver; + }, + BoundClosure_interceptorOf(closure) { + return closure._interceptor; + }, + BoundClosure__computeFieldNamed(fieldName) { + var t1, i, $name, + template = new A.BoundClosure("receiver", "interceptor"), + names = J.JSArray_markFixedList(Object.getOwnPropertyNames(template), type$.nullable_Object); + for (t1 = names.length, i = 0; i < t1; ++i) { + $name = names[i]; + if (template[$name] === fieldName) + return $name; + } + throw A.wrapException(A.ArgumentError$("Field name " + fieldName + " not found.", null)); + }, + boolConversionCheck(value) { + if (value == null) + A.assertThrow("boolean expression must not be null"); + return value; + }, + assertThrow(message) { + throw A.wrapException(new A._AssertionError(message)); + }, + throwCyclicInit(staticName) { + throw A.wrapException(new A._CyclicInitializationError(staticName)); + }, + getIsolateAffinityTag($name) { + return init.getIsolateTag($name); + }, + defineProperty(obj, property, value) { + Object.defineProperty(obj, property, {value: value, enumerable: false, writable: true, configurable: true}); + }, + lookupAndCacheInterceptor(obj) { + var interceptor, interceptorClass, altTag, mark, t1, + tag = A._asString($.getTagFunction.call$1(obj)), + record = $.dispatchRecordsForInstanceTags[tag]; + if (record != null) { + Object.defineProperty(obj, init.dispatchPropertyName, {value: record, enumerable: false, writable: true, configurable: true}); + return record.i; + } + interceptor = $.interceptorsForUncacheableTags[tag]; + if (interceptor != null) + return interceptor; + interceptorClass = init.interceptorsByTag[tag]; + if (interceptorClass == null) { + altTag = A._asStringQ($.alternateTagFunction.call$2(obj, tag)); + if (altTag != null) { + record = $.dispatchRecordsForInstanceTags[altTag]; + if (record != null) { + Object.defineProperty(obj, init.dispatchPropertyName, {value: record, enumerable: false, writable: true, configurable: true}); + return record.i; + } + interceptor = $.interceptorsForUncacheableTags[altTag]; + if (interceptor != null) + return interceptor; + interceptorClass = init.interceptorsByTag[altTag]; + tag = altTag; + } + } + if (interceptorClass == null) + return null; + interceptor = interceptorClass.prototype; + mark = tag[0]; + if (mark === "!") { + record = A.makeLeafDispatchRecord(interceptor); + $.dispatchRecordsForInstanceTags[tag] = record; + Object.defineProperty(obj, init.dispatchPropertyName, {value: record, enumerable: false, writable: true, configurable: true}); + return record.i; + } + if (mark === "~") { + $.interceptorsForUncacheableTags[tag] = interceptor; + return interceptor; + } + if (mark === "-") { + t1 = A.makeLeafDispatchRecord(interceptor); + Object.defineProperty(Object.getPrototypeOf(obj), init.dispatchPropertyName, {value: t1, enumerable: false, writable: true, configurable: true}); + return t1.i; + } + if (mark === "+") + return A.patchInteriorProto(obj, interceptor); + if (mark === "*") + throw A.wrapException(A.UnimplementedError$(tag)); + if (init.leafTags[tag] === true) { + t1 = A.makeLeafDispatchRecord(interceptor); + Object.defineProperty(Object.getPrototypeOf(obj), init.dispatchPropertyName, {value: t1, enumerable: false, writable: true, configurable: true}); + return t1.i; + } else + return A.patchInteriorProto(obj, interceptor); + }, + patchInteriorProto(obj, interceptor) { + var proto = Object.getPrototypeOf(obj); + Object.defineProperty(proto, init.dispatchPropertyName, {value: J.makeDispatchRecord(interceptor, proto, null, null), enumerable: false, writable: true, configurable: true}); + return interceptor; + }, + makeLeafDispatchRecord(interceptor) { + return J.makeDispatchRecord(interceptor, false, null, !!interceptor.$isJavaScriptIndexingBehavior); + }, + makeDefaultDispatchRecord(tag, interceptorClass, proto) { + var interceptor = interceptorClass.prototype; + if (init.leafTags[tag] === true) + return A.makeLeafDispatchRecord(interceptor); + else + return J.makeDispatchRecord(interceptor, proto, null, null); + }, + initNativeDispatch() { + if (true === $.initNativeDispatchFlag) + return; + $.initNativeDispatchFlag = true; + A.initNativeDispatchContinue(); + }, + initNativeDispatchContinue() { + var map, tags, fun, i, tag, proto, record, interceptorClass; + $.dispatchRecordsForInstanceTags = Object.create(null); + $.interceptorsForUncacheableTags = Object.create(null); + A.initHooks(); + map = init.interceptorsByTag; + tags = Object.getOwnPropertyNames(map); + if (typeof window != "undefined") { + window; + fun = function() { + }; + for (i = 0; i < tags.length; ++i) { + tag = tags[i]; + proto = $.prototypeForTagFunction.call$1(tag); + if (proto != null) { + record = A.makeDefaultDispatchRecord(tag, map[tag], proto); + if (record != null) { + Object.defineProperty(proto, init.dispatchPropertyName, {value: record, enumerable: false, writable: true, configurable: true}); + fun.prototype = proto; + } + } + } + } + for (i = 0; i < tags.length; ++i) { + tag = tags[i]; + if (/^[A-Za-z_]/.test(tag)) { + interceptorClass = map[tag]; + map["!" + tag] = interceptorClass; + map["~" + tag] = interceptorClass; + map["-" + tag] = interceptorClass; + map["+" + tag] = interceptorClass; + map["*" + tag] = interceptorClass; + } + } + }, + initHooks() { + var transformers, i, transformer, getTag, getUnknownTag, prototypeForTag, + hooks = B.C_JS_CONST0(); + hooks = A.applyHooksTransformer(B.C_JS_CONST1, A.applyHooksTransformer(B.C_JS_CONST2, A.applyHooksTransformer(B.C_JS_CONST3, A.applyHooksTransformer(B.C_JS_CONST3, A.applyHooksTransformer(B.C_JS_CONST4, A.applyHooksTransformer(B.C_JS_CONST5, A.applyHooksTransformer(B.C_JS_CONST6(B.C_JS_CONST), hooks))))))); + if (typeof dartNativeDispatchHooksTransformer != "undefined") { + transformers = dartNativeDispatchHooksTransformer; + if (typeof transformers == "function") + transformers = [transformers]; + if (Array.isArray(transformers)) + for (i = 0; i < transformers.length; ++i) { + transformer = transformers[i]; + if (typeof transformer == "function") + hooks = transformer(hooks) || hooks; + } + } + getTag = hooks.getTag; + getUnknownTag = hooks.getUnknownTag; + prototypeForTag = hooks.prototypeForTag; + $.getTagFunction = new A.initHooks_closure(getTag); + $.alternateTagFunction = new A.initHooks_closure0(getUnknownTag); + $.prototypeForTagFunction = new A.initHooks_closure1(prototypeForTag); + }, + applyHooksTransformer(transformer, hooks) { + return transformer(hooks) || hooks; + }, + createRecordTypePredicate(shape, fieldRtis) { + var $length = fieldRtis.length, + $function = init.rttc["" + $length + ";" + shape]; + if ($function == null) + return null; + if ($length === 0) + return $function; + if ($length === $function.length) + return $function.apply(null, fieldRtis); + return $function(fieldRtis); + }, + JSSyntaxRegExp_makeNative(source, multiLine, caseSensitive, unicode, dotAll, global) { + var m = multiLine ? "m" : "", + i = caseSensitive ? "" : "i", + u = unicode ? "u" : "", + s = dotAll ? "s" : "", + g = global ? "g" : "", + regexp = function(source, modifiers) { + try { + return new RegExp(source, modifiers); + } catch (e) { + return e; + } + }(source, m + i + u + s + g); + if (regexp instanceof RegExp) + return regexp; + throw A.wrapException(A.FormatException$("Illegal RegExp pattern (" + String(regexp) + ")", source, null)); + }, + stringContainsUnchecked(receiver, other, startIndex) { + var t1; + if (typeof other == "string") + return receiver.indexOf(other, startIndex) >= 0; + else if (other instanceof A.JSSyntaxRegExp) { + t1 = B.JSString_methods.substring$1(receiver, startIndex); + return other._nativeRegExp.test(t1); + } else { + t1 = J.allMatches$1$s(other, B.JSString_methods.substring$1(receiver, startIndex)); + return !t1.get$isEmpty(t1); + } + }, + escapeReplacement(replacement) { + if (replacement.indexOf("$", 0) >= 0) + return replacement.replace(/\$/g, "$$$$"); + return replacement; + }, + stringReplaceFirstRE(receiver, regexp, replacement, startIndex) { + var match = regexp._execGlobal$2(receiver, startIndex); + if (match == null) + return receiver; + return A.stringReplaceRangeUnchecked(receiver, match._match.index, match.get$end(match), replacement); + }, + quoteStringForRegExp(string) { + if (/[[\]{}()*+?.\\^$|]/.test(string)) + return string.replace(/[[\]{}()*+?.\\^$|]/g, "\\$&"); + return string; + }, + stringReplaceAllUnchecked(receiver, pattern, replacement) { + var nativeRegexp; + if (typeof pattern == "string") + return A.stringReplaceAllUncheckedString(receiver, pattern, replacement); + if (pattern instanceof A.JSSyntaxRegExp) { + nativeRegexp = pattern.get$_nativeGlobalVersion(); + nativeRegexp.lastIndex = 0; + return receiver.replace(nativeRegexp, A.escapeReplacement(replacement)); + } + return A.stringReplaceAllGeneral(receiver, pattern, replacement); + }, + stringReplaceAllGeneral(receiver, pattern, replacement) { + var t1, startIndex, t2, match; + for (t1 = J.allMatches$1$s(pattern, receiver), t1 = t1.get$iterator(t1), startIndex = 0, t2 = ""; t1.moveNext$0();) { + match = t1.get$current(t1); + t2 = t2 + receiver.substring(startIndex, match.get$start(match)) + replacement; + startIndex = match.get$end(match); + } + t1 = t2 + receiver.substring(startIndex); + return t1.charCodeAt(0) == 0 ? t1 : t1; + }, + stringReplaceAllUncheckedString(receiver, pattern, replacement) { + var $length, t1, i; + if (pattern === "") { + if (receiver === "") + return replacement; + $length = receiver.length; + t1 = "" + replacement; + for (i = 0; i < $length; ++i) + t1 = t1 + receiver[i] + replacement; + return t1.charCodeAt(0) == 0 ? t1 : t1; + } + if (receiver.indexOf(pattern, 0) < 0) + return receiver; + if (receiver.length < 500 || replacement.indexOf("$", 0) >= 0) + return receiver.split(pattern).join(replacement); + return receiver.replace(new RegExp(A.quoteStringForRegExp(pattern), "g"), A.escapeReplacement(replacement)); + }, + stringReplaceFirstUnchecked(receiver, pattern, replacement, startIndex) { + var index, t1, matches, match; + if (typeof pattern == "string") { + index = receiver.indexOf(pattern, startIndex); + if (index < 0) + return receiver; + return A.stringReplaceRangeUnchecked(receiver, index, index + pattern.length, replacement); + } + if (pattern instanceof A.JSSyntaxRegExp) + return startIndex === 0 ? receiver.replace(pattern._nativeRegExp, A.escapeReplacement(replacement)) : A.stringReplaceFirstRE(receiver, pattern, replacement, startIndex); + t1 = J.allMatches$2$s(pattern, receiver, startIndex); + matches = t1.get$iterator(t1); + if (!matches.moveNext$0()) + return receiver; + match = matches.get$current(matches); + return B.JSString_methods.replaceRange$3(receiver, match.get$start(match), match.get$end(match), replacement); + }, + stringReplaceRangeUnchecked(receiver, start, end, replacement) { + return receiver.substring(0, start) + replacement + receiver.substring(end); + }, + ConstantMapView: function ConstantMapView(t0, t1) { + this._collection$_map = t0; + this.$ti = t1; + }, + ConstantMap: function ConstantMap() { + }, + ConstantStringMap: function ConstantStringMap(t0, t1, t2) { + this._jsIndex = t0; + this._values = t1; + this.$ti = t2; + }, + _KeysOrValues: function _KeysOrValues(t0, t1) { + this._elements = t0; + this.$ti = t1; + }, + _KeysOrValuesOrElementsIterator: function _KeysOrValuesOrElementsIterator(t0, t1, t2) { + var _ = this; + _._elements = t0; + _._length = t1; + _.__js_helper$_index = 0; + _.__js_helper$_current = null; + _.$ti = t2; + }, + Instantiation: function Instantiation() { + }, + Instantiation1: function Instantiation1(t0, t1) { + this._genericClosure = t0; + this.$ti = t1; + }, + JSInvocationMirror: function JSInvocationMirror(t0, t1, t2, t3, t4) { + var _ = this; + _._memberName = t0; + _.__js_helper$_kind = t1; + _._arguments = t2; + _._namedArgumentNames = t3; + _._typeArgumentCount = t4; + }, + Primitives_functionNoSuchMethod_closure: function Primitives_functionNoSuchMethod_closure(t0, t1, t2) { + this._box_0 = t0; + this.namedArgumentList = t1; + this.$arguments = t2; + }, + TypeErrorDecoder: function TypeErrorDecoder(t0, t1, t2, t3, t4, t5) { + var _ = this; + _._pattern = t0; + _._arguments = t1; + _._argumentsExpr = t2; + _._expr = t3; + _._method = t4; + _._receiver = t5; + }, + NullError: function NullError() { + }, + JsNoSuchMethodError: function JsNoSuchMethodError(t0, t1, t2) { + this.__js_helper$_message = t0; + this._method = t1; + this._receiver = t2; + }, + UnknownJsTypeError: function UnknownJsTypeError(t0) { + this.__js_helper$_message = t0; + }, + NullThrownFromJavaScriptException: function NullThrownFromJavaScriptException(t0) { + this._irritant = t0; + }, + _StackTrace: function _StackTrace(t0) { + this._exception = t0; + this._trace = null; + }, + Closure: function Closure() { + }, + Closure0Args: function Closure0Args() { + }, + Closure2Args: function Closure2Args() { + }, + TearOffClosure: function TearOffClosure() { + }, + StaticClosure: function StaticClosure() { + }, + BoundClosure: function BoundClosure(t0, t1) { + this._receiver = t0; + this._interceptor = t1; + }, + _CyclicInitializationError: function _CyclicInitializationError(t0) { + this.variableName = t0; + }, + RuntimeError: function RuntimeError(t0) { + this.message = t0; + }, + _AssertionError: function _AssertionError(t0) { + this.message = t0; + }, + _Required: function _Required() { + }, + JsLinkedHashMap: function JsLinkedHashMap(t0) { + var _ = this; + _._length = 0; + _._last = _._first = _.__js_helper$_rest = _._nums = _._strings = null; + _._modifications = 0; + _.$ti = t0; + }, + JsLinkedHashMap_values_closure: function JsLinkedHashMap_values_closure(t0) { + this.$this = t0; + }, + LinkedHashMapCell: function LinkedHashMapCell(t0, t1) { + var _ = this; + _.hashMapCellKey = t0; + _.hashMapCellValue = t1; + _._previous = _._next = null; + }, + LinkedHashMapKeyIterable: function LinkedHashMapKeyIterable(t0, t1) { + this._map = t0; + this.$ti = t1; + }, + LinkedHashMapKeyIterator: function LinkedHashMapKeyIterator(t0, t1, t2) { + var _ = this; + _._map = t0; + _._modifications = t1; + _.__js_helper$_current = _._cell = null; + _.$ti = t2; + }, + initHooks_closure: function initHooks_closure(t0) { + this.getTag = t0; + }, + initHooks_closure0: function initHooks_closure0(t0) { + this.getUnknownTag = t0; + }, + initHooks_closure1: function initHooks_closure1(t0) { + this.prototypeForTag = t0; + }, + JSSyntaxRegExp: function JSSyntaxRegExp(t0, t1) { + var _ = this; + _.pattern = t0; + _._nativeRegExp = t1; + _._nativeAnchoredRegExp = _._nativeGlobalRegExp = null; + }, + _MatchImplementation: function _MatchImplementation(t0) { + this._match = t0; + }, + _AllMatchesIterable: function _AllMatchesIterable(t0, t1, t2) { + this._re = t0; + this.__js_helper$_string = t1; + this.__js_helper$_start = t2; + }, + _AllMatchesIterator: function _AllMatchesIterator(t0, t1, t2) { + var _ = this; + _._regExp = t0; + _.__js_helper$_string = t1; + _._nextIndex = t2; + _.__js_helper$_current = null; + }, + StringMatch: function StringMatch(t0, t1) { + this.start = t0; + this.pattern = t1; + }, + _StringAllMatchesIterable: function _StringAllMatchesIterable(t0, t1, t2) { + this._input = t0; + this._pattern = t1; + this.__js_helper$_index = t2; + }, + _StringAllMatchesIterator: function _StringAllMatchesIterator(t0, t1, t2) { + var _ = this; + _._input = t0; + _._pattern = t1; + _.__js_helper$_index = t2; + _.__js_helper$_current = null; + }, + throwLateFieldNI(fieldName) { + A.throwExpressionWithWrapper(new A.LateError("Field '" + fieldName + "' has not been initialized."), new Error()); + }, + throwLateFieldAI(fieldName) { + A.throwExpressionWithWrapper(new A.LateError("Field '" + fieldName + "' has already been initialized."), new Error()); + }, + throwLateFieldADI(fieldName) { + A.throwExpressionWithWrapper(new A.LateError("Field '" + fieldName + string$.x27_has_), new Error()); + }, + _Cell$named(_name) { + var t1 = new A._Cell(_name); + return t1._value = t1; + }, + _InitializedCell$named(_name, _initializer) { + var t1 = new A._InitializedCell(_name, _initializer); + return t1._value = t1; + }, + _Cell: function _Cell(t0) { + this.__late_helper$_name = t0; + this._value = null; + }, + _InitializedCell: function _InitializedCell(t0, t1) { + this.__late_helper$_name = t0; + this._value = null; + this._initializer = t1; + }, + _ensureNativeList(list) { + return list; + }, + NativeInt8List__create1(arg) { + return new Int8Array(arg); + }, + _checkValidIndex(index, list, $length) { + if (index >>> 0 !== index || index >= $length) + throw A.wrapException(A.diagnoseIndexError(list, index)); + }, + _checkValidRange(start, end, $length) { + var t1; + if (!(start >>> 0 !== start)) + t1 = end >>> 0 !== end || start > end || end > $length; + else + t1 = true; + if (t1) + throw A.wrapException(A.diagnoseRangeError(start, end, $length)); + return end; + }, + NativeByteBuffer: function NativeByteBuffer() { + }, + NativeTypedData: function NativeTypedData() { + }, + NativeByteData: function NativeByteData() { + }, + NativeTypedArray: function NativeTypedArray() { + }, + NativeTypedArrayOfDouble: function NativeTypedArrayOfDouble() { + }, + NativeTypedArrayOfInt: function NativeTypedArrayOfInt() { + }, + NativeFloat32List: function NativeFloat32List() { + }, + NativeFloat64List: function NativeFloat64List() { + }, + NativeInt16List: function NativeInt16List() { + }, + NativeInt32List: function NativeInt32List() { + }, + NativeInt8List: function NativeInt8List() { + }, + NativeUint16List: function NativeUint16List() { + }, + NativeUint32List: function NativeUint32List() { + }, + NativeUint8ClampedList: function NativeUint8ClampedList() { + }, + NativeUint8List: function NativeUint8List() { + }, + _NativeTypedArrayOfDouble_NativeTypedArray_ListMixin: function _NativeTypedArrayOfDouble_NativeTypedArray_ListMixin() { + }, + _NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin: function _NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin() { + }, + _NativeTypedArrayOfInt_NativeTypedArray_ListMixin: function _NativeTypedArrayOfInt_NativeTypedArray_ListMixin() { + }, + _NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin: function _NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin() { + }, + Rti__getQuestionFromStar(universe, rti) { + var question = rti._precomputed1; + return question == null ? rti._precomputed1 = A._Universe__lookupQuestionRti(universe, rti._primary, true) : question; + }, + Rti__getFutureFromFutureOr(universe, rti) { + var future = rti._precomputed1; + return future == null ? rti._precomputed1 = A._Universe__lookupInterfaceRti(universe, "Future", [rti._primary]) : future; + }, + Rti__getIsSubtypeCache(rti) { + var probe = rti._isSubtypeCache; + if (probe != null) + return probe; + return rti._isSubtypeCache = new Map(); + }, + Rti__isUnionOfFunctionType(rti) { + var kind = rti._kind; + if (kind === 6 || kind === 7 || kind === 8) + return A.Rti__isUnionOfFunctionType(rti._primary); + return kind === 12 || kind === 13; + }, + Rti__getCanonicalRecipe(rti) { + return rti._canonicalRecipe; + }, + findType(recipe) { + return A._Universe_eval(init.typeUniverse, recipe, false); + }, + instantiatedGenericFunctionType(genericFunctionRti, instantiationRti) { + var t1, cache, key, probe, rti; + if (genericFunctionRti == null) + return null; + t1 = instantiationRti._rest; + cache = genericFunctionRti._bindCache; + if (cache == null) + cache = genericFunctionRti._bindCache = new Map(); + key = instantiationRti._canonicalRecipe; + probe = cache.get(key); + if (probe != null) + return probe; + rti = A._substitute(init.typeUniverse, genericFunctionRti._primary, t1, 0); + cache.set(key, rti); + return rti; + }, + _substitute(universe, rti, typeArguments, depth) { + var baseType, substitutedBaseType, interfaceTypeArguments, substitutedInterfaceTypeArguments, base, substitutedBase, $arguments, substitutedArguments, returnType, substitutedReturnType, functionParameters, substitutedFunctionParameters, bounds, substitutedBounds, index, argument, + kind = rti._kind; + switch (kind) { + case 5: + case 1: + case 2: + case 3: + case 4: + return rti; + case 6: + baseType = rti._primary; + substitutedBaseType = A._substitute(universe, baseType, typeArguments, depth); + if (substitutedBaseType === baseType) + return rti; + return A._Universe__lookupStarRti(universe, substitutedBaseType, true); + case 7: + baseType = rti._primary; + substitutedBaseType = A._substitute(universe, baseType, typeArguments, depth); + if (substitutedBaseType === baseType) + return rti; + return A._Universe__lookupQuestionRti(universe, substitutedBaseType, true); + case 8: + baseType = rti._primary; + substitutedBaseType = A._substitute(universe, baseType, typeArguments, depth); + if (substitutedBaseType === baseType) + return rti; + return A._Universe__lookupFutureOrRti(universe, substitutedBaseType, true); + case 9: + interfaceTypeArguments = rti._rest; + substitutedInterfaceTypeArguments = A._substituteArray(universe, interfaceTypeArguments, typeArguments, depth); + if (substitutedInterfaceTypeArguments === interfaceTypeArguments) + return rti; + return A._Universe__lookupInterfaceRti(universe, rti._primary, substitutedInterfaceTypeArguments); + case 10: + base = rti._primary; + substitutedBase = A._substitute(universe, base, typeArguments, depth); + $arguments = rti._rest; + substitutedArguments = A._substituteArray(universe, $arguments, typeArguments, depth); + if (substitutedBase === base && substitutedArguments === $arguments) + return rti; + return A._Universe__lookupBindingRti(universe, substitutedBase, substitutedArguments); + case 12: + returnType = rti._primary; + substitutedReturnType = A._substitute(universe, returnType, typeArguments, depth); + functionParameters = rti._rest; + substitutedFunctionParameters = A._substituteFunctionParameters(universe, functionParameters, typeArguments, depth); + if (substitutedReturnType === returnType && substitutedFunctionParameters === functionParameters) + return rti; + return A._Universe__lookupFunctionRti(universe, substitutedReturnType, substitutedFunctionParameters); + case 13: + bounds = rti._rest; + depth += bounds.length; + substitutedBounds = A._substituteArray(universe, bounds, typeArguments, depth); + base = rti._primary; + substitutedBase = A._substitute(universe, base, typeArguments, depth); + if (substitutedBounds === bounds && substitutedBase === base) + return rti; + return A._Universe__lookupGenericFunctionRti(universe, substitutedBase, substitutedBounds, true); + case 14: + index = rti._primary; + if (index < depth) + return rti; + argument = typeArguments[index - depth]; + if (argument == null) + return rti; + return argument; + default: + throw A.wrapException(A.AssertionError$("Attempted to substitute unexpected RTI kind " + kind)); + } + }, + _substituteArray(universe, rtiArray, typeArguments, depth) { + var changed, i, rti, substitutedRti, + $length = rtiArray.length, + result = A._Utils_newArrayOrEmpty($length); + for (changed = false, i = 0; i < $length; ++i) { + rti = rtiArray[i]; + substitutedRti = A._substitute(universe, rti, typeArguments, depth); + if (substitutedRti !== rti) + changed = true; + result[i] = substitutedRti; + } + return changed ? result : rtiArray; + }, + _substituteNamed(universe, namedArray, typeArguments, depth) { + var changed, i, t1, t2, rti, substitutedRti, + $length = namedArray.length, + result = A._Utils_newArrayOrEmpty($length); + for (changed = false, i = 0; i < $length; i += 3) { + t1 = namedArray[i]; + t2 = namedArray[i + 1]; + rti = namedArray[i + 2]; + substitutedRti = A._substitute(universe, rti, typeArguments, depth); + if (substitutedRti !== rti) + changed = true; + result.splice(i, 3, t1, t2, substitutedRti); + } + return changed ? result : namedArray; + }, + _substituteFunctionParameters(universe, functionParameters, typeArguments, depth) { + var result, + requiredPositional = functionParameters._requiredPositional, + substitutedRequiredPositional = A._substituteArray(universe, requiredPositional, typeArguments, depth), + optionalPositional = functionParameters._optionalPositional, + substitutedOptionalPositional = A._substituteArray(universe, optionalPositional, typeArguments, depth), + named = functionParameters._named, + substitutedNamed = A._substituteNamed(universe, named, typeArguments, depth); + if (substitutedRequiredPositional === requiredPositional && substitutedOptionalPositional === optionalPositional && substitutedNamed === named) + return functionParameters; + result = new A._FunctionParameters(); + result._requiredPositional = substitutedRequiredPositional; + result._optionalPositional = substitutedOptionalPositional; + result._named = substitutedNamed; + return result; + }, + _setArrayType(target, rti) { + target[init.arrayRti] = rti; + return target; + }, + closureFunctionType(closure) { + var t1, + signature = closure.$signature; + if (signature != null) { + if (typeof signature == "number") + return A.getTypeFromTypesTable(signature); + t1 = closure.$signature(); + return t1; + } + return null; + }, + instanceOrFunctionType(object, testRti) { + var rti; + if (A.Rti__isUnionOfFunctionType(testRti)) + if (object instanceof A.Closure) { + rti = A.closureFunctionType(object); + if (rti != null) + return rti; + } + return A.instanceType(object); + }, + instanceType(object) { + if (object instanceof A.Object) + return A._instanceType(object); + if (Array.isArray(object)) + return A._arrayInstanceType(object); + return A._instanceTypeFromConstructor(J.getInterceptor$(object)); + }, + _arrayInstanceType(object) { + var rti = object[init.arrayRti], + defaultRti = type$.JSArray_dynamic; + if (rti == null) + return defaultRti; + if (rti.constructor !== defaultRti.constructor) + return defaultRti; + return rti; + }, + _instanceType(object) { + var rti = object.$ti; + return rti != null ? rti : A._instanceTypeFromConstructor(object); + }, + _instanceTypeFromConstructor(instance) { + var $constructor = instance.constructor, + probe = $constructor.$ccache; + if (probe != null) + return probe; + return A._instanceTypeFromConstructorMiss(instance, $constructor); + }, + _instanceTypeFromConstructorMiss(instance, $constructor) { + var effectiveConstructor = instance instanceof A.Closure ? Object.getPrototypeOf(Object.getPrototypeOf(instance)).constructor : $constructor, + rti = A._Universe_findErasedType(init.typeUniverse, effectiveConstructor.name); + $constructor.$ccache = rti; + return rti; + }, + getTypeFromTypesTable(index) { + var rti, + table = init.types, + type = table[index]; + if (typeof type == "string") { + rti = A._Universe_eval(init.typeUniverse, type, false); + table[index] = rti; + return rti; + } + return type; + }, + getRuntimeTypeOfDartObject(object) { + return A.createRuntimeType(A._instanceType(object)); + }, + getRuntimeTypeOfClosure(closure) { + var rti = A.closureFunctionType(closure); + return A.createRuntimeType(rti == null ? A.instanceType(closure) : rti); + }, + _structuralTypeOf(object) { + var functionRti = object instanceof A.Closure ? A.closureFunctionType(object) : null; + if (functionRti != null) + return functionRti; + if (type$.TrustedGetRuntimeType._is(object)) + return J.get$runtimeType$(object)._rti; + if (Array.isArray(object)) + return A._arrayInstanceType(object); + return A.instanceType(object); + }, + createRuntimeType(rti) { + var t1 = rti._cachedRuntimeType; + return t1 == null ? rti._cachedRuntimeType = A._createRuntimeType(rti) : t1; + }, + _createRuntimeType(rti) { + var starErasedRti, t1, + s = rti._canonicalRecipe, + starErasedRecipe = s.replace(/\*/g, ""); + if (starErasedRecipe === s) + return rti._cachedRuntimeType = new A._Type(rti); + starErasedRti = A._Universe_eval(init.typeUniverse, starErasedRecipe, true); + t1 = starErasedRti._cachedRuntimeType; + return t1 == null ? starErasedRti._cachedRuntimeType = A._createRuntimeType(starErasedRti) : t1; + }, + typeLiteral(recipe) { + return A.createRuntimeType(A._Universe_eval(init.typeUniverse, recipe, false)); + }, + _installSpecializedIsTest(object) { + var t1, unstarred, unstarredKind, isFn, $name, predicate, testRti = this; + if (testRti === type$.Object) + return A._finishIsFn(testRti, object, A._isObject); + if (!A.isStrongTopType(testRti)) + if (!(testRti === type$.legacy_Object)) + t1 = false; + else + t1 = true; + else + t1 = true; + if (t1) + return A._finishIsFn(testRti, object, A._isTop); + t1 = testRti._kind; + if (t1 === 7) + return A._finishIsFn(testRti, object, A._generalNullableIsTestImplementation); + if (t1 === 1) + return A._finishIsFn(testRti, object, A._isNever); + unstarred = t1 === 6 ? testRti._primary : testRti; + unstarredKind = unstarred._kind; + if (unstarredKind === 8) + return A._finishIsFn(testRti, object, A._isFutureOr); + if (unstarred === type$.int) + isFn = A._isInt; + else if (unstarred === type$.double || unstarred === type$.num) + isFn = A._isNum; + else if (unstarred === type$.String) + isFn = A._isString; + else + isFn = unstarred === type$.bool ? A._isBool : null; + if (isFn != null) + return A._finishIsFn(testRti, object, isFn); + if (unstarredKind === 9) { + $name = unstarred._primary; + if (unstarred._rest.every(A.isTopType)) { + testRti._specializedTestResource = "$is" + $name; + if ($name === "List") + return A._finishIsFn(testRti, object, A._isListTestViaProperty); + return A._finishIsFn(testRti, object, A._isTestViaProperty); + } + } else if (unstarredKind === 11) { + predicate = A.createRecordTypePredicate(unstarred._primary, unstarred._rest); + return A._finishIsFn(testRti, object, predicate == null ? A._isNever : predicate); + } + return A._finishIsFn(testRti, object, A._generalIsTestImplementation); + }, + _finishIsFn(testRti, object, isFn) { + testRti._is = isFn; + return testRti._is(object); + }, + _installSpecializedAsCheck(object) { + var t1, testRti = this, + asFn = A._generalAsCheckImplementation; + if (!A.isStrongTopType(testRti)) + if (!(testRti === type$.legacy_Object)) + t1 = false; + else + t1 = true; + else + t1 = true; + if (t1) + asFn = A._asTop; + else if (testRti === type$.Object) + asFn = A._asObject; + else { + t1 = A.isNullable(testRti); + if (t1) + asFn = A._generalNullableAsCheckImplementation; + } + testRti._as = asFn; + return testRti._as(object); + }, + _nullIs(testRti) { + var t1, + kind = testRti._kind; + if (!A.isStrongTopType(testRti)) + if (!(testRti === type$.legacy_Object)) + if (!(testRti === type$.legacy_Never)) + if (kind !== 7) + if (!(kind === 6 && A._nullIs(testRti._primary))) + t1 = kind === 8 && A._nullIs(testRti._primary) || testRti === type$.Null || testRti === type$.JSNull; + else + t1 = true; + else + t1 = true; + else + t1 = true; + else + t1 = true; + else + t1 = true; + return t1; + }, + _generalIsTestImplementation(object) { + var testRti = this; + if (object == null) + return A._nullIs(testRti); + return A.isSubtype(init.typeUniverse, A.instanceOrFunctionType(object, testRti), testRti); + }, + _generalNullableIsTestImplementation(object) { + if (object == null) + return true; + return this._primary._is(object); + }, + _isTestViaProperty(object) { + var tag, testRti = this; + if (object == null) + return A._nullIs(testRti); + tag = testRti._specializedTestResource; + if (object instanceof A.Object) + return !!object[tag]; + return !!J.getInterceptor$(object)[tag]; + }, + _isListTestViaProperty(object) { + var tag, testRti = this; + if (object == null) + return A._nullIs(testRti); + if (typeof object != "object") + return false; + if (Array.isArray(object)) + return true; + tag = testRti._specializedTestResource; + if (object instanceof A.Object) + return !!object[tag]; + return !!J.getInterceptor$(object)[tag]; + }, + _generalAsCheckImplementation(object) { + var t1, testRti = this; + if (object == null) { + t1 = A.isNullable(testRti); + if (t1) + return object; + } else if (testRti._is(object)) + return object; + A._failedAsCheck(object, testRti); + }, + _generalNullableAsCheckImplementation(object) { + var testRti = this; + if (object == null) + return object; + else if (testRti._is(object)) + return object; + A._failedAsCheck(object, testRti); + }, + _failedAsCheck(object, testRti) { + throw A.wrapException(A._TypeError$fromMessage(A._Error_compose(object, A._rtiToString(testRti, null)))); + }, + checkTypeBound(type, bound, variable, methodName) { + if (A.isSubtype(init.typeUniverse, type, bound)) + return type; + throw A.wrapException(A._TypeError$fromMessage("The type argument '" + A._rtiToString(type, null) + "' is not a subtype of the type variable bound '" + A._rtiToString(bound, null) + "' of type variable '" + variable + "' in '" + methodName + "'.")); + }, + _Error_compose(object, checkedTypeDescription) { + return A.Error_safeToString(object) + ": type '" + A._rtiToString(A._structuralTypeOf(object), null) + "' is not a subtype of type '" + checkedTypeDescription + "'"; + }, + _TypeError$fromMessage(message) { + return new A._TypeError("TypeError: " + message); + }, + _TypeError__TypeError$forType(object, type) { + return new A._TypeError("TypeError: " + A._Error_compose(object, type)); + }, + _isFutureOr(object) { + var testRti = this, + unstarred = testRti._kind === 6 ? testRti._primary : testRti; + return unstarred._primary._is(object) || A.Rti__getFutureFromFutureOr(init.typeUniverse, unstarred)._is(object); + }, + _isObject(object) { + return object != null; + }, + _asObject(object) { + if (object != null) + return object; + throw A.wrapException(A._TypeError__TypeError$forType(object, "Object")); + }, + _isTop(object) { + return true; + }, + _asTop(object) { + return object; + }, + _isNever(object) { + return false; + }, + _isBool(object) { + return true === object || false === object; + }, + _asBool(object) { + if (true === object) + return true; + if (false === object) + return false; + throw A.wrapException(A._TypeError__TypeError$forType(object, "bool")); + }, + _asBoolS(object) { + if (true === object) + return true; + if (false === object) + return false; + if (object == null) + return object; + throw A.wrapException(A._TypeError__TypeError$forType(object, "bool")); + }, + _asBoolQ(object) { + if (true === object) + return true; + if (false === object) + return false; + if (object == null) + return object; + throw A.wrapException(A._TypeError__TypeError$forType(object, "bool?")); + }, + _asDouble(object) { + if (typeof object == "number") + return object; + throw A.wrapException(A._TypeError__TypeError$forType(object, "double")); + }, + _asDoubleS(object) { + if (typeof object == "number") + return object; + if (object == null) + return object; + throw A.wrapException(A._TypeError__TypeError$forType(object, "double")); + }, + _asDoubleQ(object) { + if (typeof object == "number") + return object; + if (object == null) + return object; + throw A.wrapException(A._TypeError__TypeError$forType(object, "double?")); + }, + _isInt(object) { + return typeof object == "number" && Math.floor(object) === object; + }, + _asInt(object) { + if (typeof object == "number" && Math.floor(object) === object) + return object; + throw A.wrapException(A._TypeError__TypeError$forType(object, "int")); + }, + _asIntS(object) { + if (typeof object == "number" && Math.floor(object) === object) + return object; + if (object == null) + return object; + throw A.wrapException(A._TypeError__TypeError$forType(object, "int")); + }, + _asIntQ(object) { + if (typeof object == "number" && Math.floor(object) === object) + return object; + if (object == null) + return object; + throw A.wrapException(A._TypeError__TypeError$forType(object, "int?")); + }, + _isNum(object) { + return typeof object == "number"; + }, + _asNum(object) { + if (typeof object == "number") + return object; + throw A.wrapException(A._TypeError__TypeError$forType(object, "num")); + }, + _asNumS(object) { + if (typeof object == "number") + return object; + if (object == null) + return object; + throw A.wrapException(A._TypeError__TypeError$forType(object, "num")); + }, + _asNumQ(object) { + if (typeof object == "number") + return object; + if (object == null) + return object; + throw A.wrapException(A._TypeError__TypeError$forType(object, "num?")); + }, + _isString(object) { + return typeof object == "string"; + }, + _asString(object) { + if (typeof object == "string") + return object; + throw A.wrapException(A._TypeError__TypeError$forType(object, "String")); + }, + _asStringS(object) { + if (typeof object == "string") + return object; + if (object == null) + return object; + throw A.wrapException(A._TypeError__TypeError$forType(object, "String")); + }, + _asStringQ(object) { + if (typeof object == "string") + return object; + if (object == null) + return object; + throw A.wrapException(A._TypeError__TypeError$forType(object, "String?")); + }, + _rtiArrayToString(array, genericContext) { + var s, sep, i; + for (s = "", sep = "", i = 0; i < array.length; ++i, sep = ", ") + s += sep + A._rtiToString(array[i], genericContext); + return s; + }, + _recordRtiToString(recordType, genericContext) { + var fieldCount, names, namesIndex, s, comma, i, + partialShape = recordType._primary, + fields = recordType._rest; + if ("" === partialShape) + return "(" + A._rtiArrayToString(fields, genericContext) + ")"; + fieldCount = fields.length; + names = partialShape.split(","); + namesIndex = names.length - fieldCount; + for (s = "(", comma = "", i = 0; i < fieldCount; ++i, comma = ", ") { + s += comma; + if (namesIndex === 0) + s += "{"; + s += A._rtiToString(fields[i], genericContext); + if (namesIndex >= 0) + s += " " + names[namesIndex]; + ++namesIndex; + } + return s + "})"; + }, + _functionRtiToString(functionType, genericContext, bounds) { + var boundsLength, outerContextLength, offset, i, t1, t2, typeParametersText, typeSep, t3, t4, boundRti, kind, parameters, requiredPositional, requiredPositionalLength, optionalPositional, optionalPositionalLength, named, namedLength, returnTypeText, argumentsText, sep, _s2_ = ", "; + if (bounds != null) { + boundsLength = bounds.length; + if (genericContext == null) { + genericContext = A._setArrayType([], type$.JSArray_String); + outerContextLength = null; + } else + outerContextLength = genericContext.length; + offset = genericContext.length; + for (i = boundsLength; i > 0; --i) + B.JSArray_methods.add$1(genericContext, "T" + (offset + i)); + for (t1 = type$.nullable_Object, t2 = type$.legacy_Object, typeParametersText = "<", typeSep = "", i = 0; i < boundsLength; ++i, typeSep = _s2_) { + t3 = genericContext.length; + t4 = t3 - 1 - i; + if (!(t4 >= 0)) + return A.ioore(genericContext, t4); + typeParametersText = B.JSString_methods.$add(typeParametersText + typeSep, genericContext[t4]); + boundRti = bounds[i]; + kind = boundRti._kind; + if (!(kind === 2 || kind === 3 || kind === 4 || kind === 5 || boundRti === t1)) + if (!(boundRti === t2)) + t3 = false; + else + t3 = true; + else + t3 = true; + if (!t3) + typeParametersText += " extends " + A._rtiToString(boundRti, genericContext); + } + typeParametersText += ">"; + } else { + typeParametersText = ""; + outerContextLength = null; + } + t1 = functionType._primary; + parameters = functionType._rest; + requiredPositional = parameters._requiredPositional; + requiredPositionalLength = requiredPositional.length; + optionalPositional = parameters._optionalPositional; + optionalPositionalLength = optionalPositional.length; + named = parameters._named; + namedLength = named.length; + returnTypeText = A._rtiToString(t1, genericContext); + for (argumentsText = "", sep = "", i = 0; i < requiredPositionalLength; ++i, sep = _s2_) + argumentsText += sep + A._rtiToString(requiredPositional[i], genericContext); + if (optionalPositionalLength > 0) { + argumentsText += sep + "["; + for (sep = "", i = 0; i < optionalPositionalLength; ++i, sep = _s2_) + argumentsText += sep + A._rtiToString(optionalPositional[i], genericContext); + argumentsText += "]"; + } + if (namedLength > 0) { + argumentsText += sep + "{"; + for (sep = "", i = 0; i < namedLength; i += 3, sep = _s2_) { + argumentsText += sep; + if (named[i + 1]) + argumentsText += "required "; + argumentsText += A._rtiToString(named[i + 2], genericContext) + " " + named[i]; + } + argumentsText += "}"; + } + if (outerContextLength != null) { + genericContext.toString; + genericContext.length = outerContextLength; + } + return typeParametersText + "(" + argumentsText + ") => " + returnTypeText; + }, + _rtiToString(rti, genericContext) { + var s, questionArgument, argumentKind, $name, $arguments, t1, t2, + kind = rti._kind; + if (kind === 5) + return "erased"; + if (kind === 2) + return "dynamic"; + if (kind === 3) + return "void"; + if (kind === 1) + return "Never"; + if (kind === 4) + return "any"; + if (kind === 6) { + s = A._rtiToString(rti._primary, genericContext); + return s; + } + if (kind === 7) { + questionArgument = rti._primary; + s = A._rtiToString(questionArgument, genericContext); + argumentKind = questionArgument._kind; + return (argumentKind === 12 || argumentKind === 13 ? "(" + s + ")" : s) + "?"; + } + if (kind === 8) + return "FutureOr<" + A._rtiToString(rti._primary, genericContext) + ">"; + if (kind === 9) { + $name = A._unminifyOrTag(rti._primary); + $arguments = rti._rest; + return $arguments.length > 0 ? $name + ("<" + A._rtiArrayToString($arguments, genericContext) + ">") : $name; + } + if (kind === 11) + return A._recordRtiToString(rti, genericContext); + if (kind === 12) + return A._functionRtiToString(rti, genericContext, null); + if (kind === 13) + return A._functionRtiToString(rti._primary, genericContext, rti._rest); + if (kind === 14) { + t1 = rti._primary; + t2 = genericContext.length; + t1 = t2 - 1 - t1; + if (!(t1 >= 0 && t1 < t2)) + return A.ioore(genericContext, t1); + return genericContext[t1]; + } + return "?"; + }, + _unminifyOrTag(rawClassName) { + var preserved = init.mangledGlobalNames[rawClassName]; + if (preserved != null) + return preserved; + return rawClassName; + }, + _Universe_findRule(universe, targetType) { + var rule = universe.tR[targetType]; + for (; typeof rule == "string";) + rule = universe.tR[rule]; + return rule; + }, + _Universe_findErasedType(universe, cls) { + var $length, erased, $arguments, i, $interface, + t1 = universe.eT, + probe = t1[cls]; + if (probe == null) + return A._Universe_eval(universe, cls, false); + else if (typeof probe == "number") { + $length = probe; + erased = A._Universe__lookupTerminalRti(universe, 5, "#"); + $arguments = A._Utils_newArrayOrEmpty($length); + for (i = 0; i < $length; ++i) + $arguments[i] = erased; + $interface = A._Universe__lookupInterfaceRti(universe, cls, $arguments); + t1[cls] = $interface; + return $interface; + } else + return probe; + }, + _Universe_addRules(universe, rules) { + return A._Utils_objectAssign(universe.tR, rules); + }, + _Universe_addErasedTypes(universe, types) { + return A._Utils_objectAssign(universe.eT, types); + }, + _Universe_eval(universe, recipe, normalize) { + var rti, + t1 = universe.eC, + probe = t1.get(recipe); + if (probe != null) + return probe; + rti = A._Parser_parse(A._Parser_create(universe, null, recipe, normalize)); + t1.set(recipe, rti); + return rti; + }, + _Universe_evalInEnvironment(universe, environment, recipe) { + var probe, rti, + cache = environment._evalCache; + if (cache == null) + cache = environment._evalCache = new Map(); + probe = cache.get(recipe); + if (probe != null) + return probe; + rti = A._Parser_parse(A._Parser_create(universe, environment, recipe, true)); + cache.set(recipe, rti); + return rti; + }, + _Universe_bind(universe, environment, argumentsRti) { + var argumentsRecipe, probe, rti, + cache = environment._bindCache; + if (cache == null) + cache = environment._bindCache = new Map(); + argumentsRecipe = argumentsRti._canonicalRecipe; + probe = cache.get(argumentsRecipe); + if (probe != null) + return probe; + rti = A._Universe__lookupBindingRti(universe, environment, argumentsRti._kind === 10 ? argumentsRti._rest : [argumentsRti]); + cache.set(argumentsRecipe, rti); + return rti; + }, + _Universe__installTypeTests(universe, rti) { + rti._as = A._installSpecializedAsCheck; + rti._is = A._installSpecializedIsTest; + return rti; + }, + _Universe__lookupTerminalRti(universe, kind, key) { + var rti, t1, + probe = universe.eC.get(key); + if (probe != null) + return probe; + rti = new A.Rti(null, null); + rti._kind = kind; + rti._canonicalRecipe = key; + t1 = A._Universe__installTypeTests(universe, rti); + universe.eC.set(key, t1); + return t1; + }, + _Universe__lookupStarRti(universe, baseType, normalize) { + var t1, + key = baseType._canonicalRecipe + "*", + probe = universe.eC.get(key); + if (probe != null) + return probe; + t1 = A._Universe__createStarRti(universe, baseType, key, normalize); + universe.eC.set(key, t1); + return t1; + }, + _Universe__createStarRti(universe, baseType, key, normalize) { + var baseKind, t1, rti; + if (normalize) { + baseKind = baseType._kind; + if (!A.isStrongTopType(baseType)) + t1 = baseType === type$.Null || baseType === type$.JSNull || baseKind === 7 || baseKind === 6; + else + t1 = true; + if (t1) + return baseType; + } + rti = new A.Rti(null, null); + rti._kind = 6; + rti._primary = baseType; + rti._canonicalRecipe = key; + return A._Universe__installTypeTests(universe, rti); + }, + _Universe__lookupQuestionRti(universe, baseType, normalize) { + var t1, + key = baseType._canonicalRecipe + "?", + probe = universe.eC.get(key); + if (probe != null) + return probe; + t1 = A._Universe__createQuestionRti(universe, baseType, key, normalize); + universe.eC.set(key, t1); + return t1; + }, + _Universe__createQuestionRti(universe, baseType, key, normalize) { + var baseKind, t1, starArgument, rti; + if (normalize) { + baseKind = baseType._kind; + if (!A.isStrongTopType(baseType)) + if (!(baseType === type$.Null || baseType === type$.JSNull)) + if (baseKind !== 7) + t1 = baseKind === 8 && A.isNullable(baseType._primary); + else + t1 = true; + else + t1 = true; + else + t1 = true; + if (t1) + return baseType; + else if (baseKind === 1 || baseType === type$.legacy_Never) + return type$.Null; + else if (baseKind === 6) { + starArgument = baseType._primary; + if (starArgument._kind === 8 && A.isNullable(starArgument._primary)) + return starArgument; + else + return A.Rti__getQuestionFromStar(universe, baseType); + } + } + rti = new A.Rti(null, null); + rti._kind = 7; + rti._primary = baseType; + rti._canonicalRecipe = key; + return A._Universe__installTypeTests(universe, rti); + }, + _Universe__lookupFutureOrRti(universe, baseType, normalize) { + var t1, + key = baseType._canonicalRecipe + "/", + probe = universe.eC.get(key); + if (probe != null) + return probe; + t1 = A._Universe__createFutureOrRti(universe, baseType, key, normalize); + universe.eC.set(key, t1); + return t1; + }, + _Universe__createFutureOrRti(universe, baseType, key, normalize) { + var t1, t2, rti; + if (normalize) { + t1 = baseType._kind; + if (!A.isStrongTopType(baseType)) + if (!(baseType === type$.legacy_Object)) + t2 = false; + else + t2 = true; + else + t2 = true; + if (t2 || baseType === type$.Object) + return baseType; + else if (t1 === 1) + return A._Universe__lookupInterfaceRti(universe, "Future", [baseType]); + else if (baseType === type$.Null || baseType === type$.JSNull) + return type$.nullable_Future_Null; + } + rti = new A.Rti(null, null); + rti._kind = 8; + rti._primary = baseType; + rti._canonicalRecipe = key; + return A._Universe__installTypeTests(universe, rti); + }, + _Universe__lookupGenericFunctionParameterRti(universe, index) { + var rti, t1, + key = "" + index + "^", + probe = universe.eC.get(key); + if (probe != null) + return probe; + rti = new A.Rti(null, null); + rti._kind = 14; + rti._primary = index; + rti._canonicalRecipe = key; + t1 = A._Universe__installTypeTests(universe, rti); + universe.eC.set(key, t1); + return t1; + }, + _Universe__canonicalRecipeJoin($arguments) { + var s, sep, i, + $length = $arguments.length; + for (s = "", sep = "", i = 0; i < $length; ++i, sep = ",") + s += sep + $arguments[i]._canonicalRecipe; + return s; + }, + _Universe__canonicalRecipeJoinNamed($arguments) { + var s, sep, i, t1, nameSep, + $length = $arguments.length; + for (s = "", sep = "", i = 0; i < $length; i += 3, sep = ",") { + t1 = $arguments[i]; + nameSep = $arguments[i + 1] ? "!" : ":"; + s += sep + t1 + nameSep + $arguments[i + 2]._canonicalRecipe; + } + return s; + }, + _Universe__lookupInterfaceRti(universe, $name, $arguments) { + var probe, rti, t1, + s = $name; + if ($arguments.length > 0) + s += "<" + A._Universe__canonicalRecipeJoin($arguments) + ">"; + probe = universe.eC.get(s); + if (probe != null) + return probe; + rti = new A.Rti(null, null); + rti._kind = 9; + rti._primary = $name; + rti._rest = $arguments; + if ($arguments.length > 0) + rti._precomputed1 = $arguments[0]; + rti._canonicalRecipe = s; + t1 = A._Universe__installTypeTests(universe, rti); + universe.eC.set(s, t1); + return t1; + }, + _Universe__lookupBindingRti(universe, base, $arguments) { + var newBase, newArguments, key, probe, rti, t1; + if (base._kind === 10) { + newBase = base._primary; + newArguments = base._rest.concat($arguments); + } else { + newArguments = $arguments; + newBase = base; + } + key = newBase._canonicalRecipe + (";<" + A._Universe__canonicalRecipeJoin(newArguments) + ">"); + probe = universe.eC.get(key); + if (probe != null) + return probe; + rti = new A.Rti(null, null); + rti._kind = 10; + rti._primary = newBase; + rti._rest = newArguments; + rti._canonicalRecipe = key; + t1 = A._Universe__installTypeTests(universe, rti); + universe.eC.set(key, t1); + return t1; + }, + _Universe__lookupRecordRti(universe, partialShapeTag, fields) { + var rti, t1, + key = "+" + (partialShapeTag + "(" + A._Universe__canonicalRecipeJoin(fields) + ")"), + probe = universe.eC.get(key); + if (probe != null) + return probe; + rti = new A.Rti(null, null); + rti._kind = 11; + rti._primary = partialShapeTag; + rti._rest = fields; + rti._canonicalRecipe = key; + t1 = A._Universe__installTypeTests(universe, rti); + universe.eC.set(key, t1); + return t1; + }, + _Universe__lookupFunctionRti(universe, returnType, parameters) { + var sep, key, probe, rti, t1, + s = returnType._canonicalRecipe, + requiredPositional = parameters._requiredPositional, + requiredPositionalLength = requiredPositional.length, + optionalPositional = parameters._optionalPositional, + optionalPositionalLength = optionalPositional.length, + named = parameters._named, + namedLength = named.length, + recipe = "(" + A._Universe__canonicalRecipeJoin(requiredPositional); + if (optionalPositionalLength > 0) { + sep = requiredPositionalLength > 0 ? "," : ""; + recipe += sep + "[" + A._Universe__canonicalRecipeJoin(optionalPositional) + "]"; + } + if (namedLength > 0) { + sep = requiredPositionalLength > 0 ? "," : ""; + recipe += sep + "{" + A._Universe__canonicalRecipeJoinNamed(named) + "}"; + } + key = s + (recipe + ")"); + probe = universe.eC.get(key); + if (probe != null) + return probe; + rti = new A.Rti(null, null); + rti._kind = 12; + rti._primary = returnType; + rti._rest = parameters; + rti._canonicalRecipe = key; + t1 = A._Universe__installTypeTests(universe, rti); + universe.eC.set(key, t1); + return t1; + }, + _Universe__lookupGenericFunctionRti(universe, baseFunctionType, bounds, normalize) { + var t1, + key = baseFunctionType._canonicalRecipe + ("<" + A._Universe__canonicalRecipeJoin(bounds) + ">"), + probe = universe.eC.get(key); + if (probe != null) + return probe; + t1 = A._Universe__createGenericFunctionRti(universe, baseFunctionType, bounds, key, normalize); + universe.eC.set(key, t1); + return t1; + }, + _Universe__createGenericFunctionRti(universe, baseFunctionType, bounds, key, normalize) { + var $length, typeArguments, count, i, bound, substitutedBase, substitutedBounds, rti; + if (normalize) { + $length = bounds.length; + typeArguments = A._Utils_newArrayOrEmpty($length); + for (count = 0, i = 0; i < $length; ++i) { + bound = bounds[i]; + if (bound._kind === 1) { + typeArguments[i] = bound; + ++count; + } + } + if (count > 0) { + substitutedBase = A._substitute(universe, baseFunctionType, typeArguments, 0); + substitutedBounds = A._substituteArray(universe, bounds, typeArguments, 0); + return A._Universe__lookupGenericFunctionRti(universe, substitutedBase, substitutedBounds, bounds !== substitutedBounds); + } + } + rti = new A.Rti(null, null); + rti._kind = 13; + rti._primary = baseFunctionType; + rti._rest = bounds; + rti._canonicalRecipe = key; + return A._Universe__installTypeTests(universe, rti); + }, + _Parser_create(universe, environment, recipe, normalize) { + return {u: universe, e: environment, r: recipe, s: [], p: 0, n: normalize}; + }, + _Parser_parse(parser) { + var t2, i, ch, t3, array, end, item, + source = parser.r, + t1 = parser.s; + for (t2 = source.length, i = 0; i < t2;) { + ch = source.charCodeAt(i); + if (ch >= 48 && ch <= 57) + i = A._Parser_handleDigit(i + 1, ch, source, t1); + else if ((((ch | 32) >>> 0) - 97 & 65535) < 26 || ch === 95 || ch === 36 || ch === 124) + i = A._Parser_handleIdentifier(parser, i, source, t1, false); + else if (ch === 46) + i = A._Parser_handleIdentifier(parser, i, source, t1, true); + else { + ++i; + switch (ch) { + case 44: + break; + case 58: + t1.push(false); + break; + case 33: + t1.push(true); + break; + case 59: + t1.push(A._Parser_toType(parser.u, parser.e, t1.pop())); + break; + case 94: + t1.push(A._Universe__lookupGenericFunctionParameterRti(parser.u, t1.pop())); + break; + case 35: + t1.push(A._Universe__lookupTerminalRti(parser.u, 5, "#")); + break; + case 64: + t1.push(A._Universe__lookupTerminalRti(parser.u, 2, "@")); + break; + case 126: + t1.push(A._Universe__lookupTerminalRti(parser.u, 3, "~")); + break; + case 60: + t1.push(parser.p); + parser.p = t1.length; + break; + case 62: + A._Parser_handleTypeArguments(parser, t1); + break; + case 38: + A._Parser_handleExtendedOperations(parser, t1); + break; + case 42: + t3 = parser.u; + t1.push(A._Universe__lookupStarRti(t3, A._Parser_toType(t3, parser.e, t1.pop()), parser.n)); + break; + case 63: + t3 = parser.u; + t1.push(A._Universe__lookupQuestionRti(t3, A._Parser_toType(t3, parser.e, t1.pop()), parser.n)); + break; + case 47: + t3 = parser.u; + t1.push(A._Universe__lookupFutureOrRti(t3, A._Parser_toType(t3, parser.e, t1.pop()), parser.n)); + break; + case 40: + t1.push(-3); + t1.push(parser.p); + parser.p = t1.length; + break; + case 41: + A._Parser_handleArguments(parser, t1); + break; + case 91: + t1.push(parser.p); + parser.p = t1.length; + break; + case 93: + array = t1.splice(parser.p); + A._Parser_toTypes(parser.u, parser.e, array); + parser.p = t1.pop(); + t1.push(array); + t1.push(-1); + break; + case 123: + t1.push(parser.p); + parser.p = t1.length; + break; + case 125: + array = t1.splice(parser.p); + A._Parser_toTypesNamed(parser.u, parser.e, array); + parser.p = t1.pop(); + t1.push(array); + t1.push(-2); + break; + case 43: + end = source.indexOf("(", i); + t1.push(source.substring(i, end)); + t1.push(-4); + t1.push(parser.p); + parser.p = t1.length; + i = end + 1; + break; + default: + throw "Bad character " + ch; + } + } + } + item = t1.pop(); + return A._Parser_toType(parser.u, parser.e, item); + }, + _Parser_handleDigit(i, digit, source, stack) { + var t1, ch, + value = digit - 48; + for (t1 = source.length; i < t1; ++i) { + ch = source.charCodeAt(i); + if (!(ch >= 48 && ch <= 57)) + break; + value = value * 10 + (ch - 48); + } + stack.push(value); + return i; + }, + _Parser_handleIdentifier(parser, start, source, stack, hasPeriod) { + var t1, ch, t2, string, environment, recipe, + i = start + 1; + for (t1 = source.length; i < t1; ++i) { + ch = source.charCodeAt(i); + if (ch === 46) { + if (hasPeriod) + break; + hasPeriod = true; + } else { + if (!((((ch | 32) >>> 0) - 97 & 65535) < 26 || ch === 95 || ch === 36 || ch === 124)) + t2 = ch >= 48 && ch <= 57; + else + t2 = true; + if (!t2) + break; + } + } + string = source.substring(start, i); + if (hasPeriod) { + t1 = parser.u; + environment = parser.e; + if (environment._kind === 10) + environment = environment._primary; + recipe = A._Universe_findRule(t1, environment._primary)[string]; + if (recipe == null) + A.throwExpression('No "' + string + '" in "' + A.Rti__getCanonicalRecipe(environment) + '"'); + stack.push(A._Universe_evalInEnvironment(t1, environment, recipe)); + } else + stack.push(string); + return i; + }, + _Parser_handleTypeArguments(parser, stack) { + var base, + t1 = parser.u, + $arguments = A._Parser_collectArray(parser, stack), + head = stack.pop(); + if (typeof head == "string") + stack.push(A._Universe__lookupInterfaceRti(t1, head, $arguments)); + else { + base = A._Parser_toType(t1, parser.e, head); + switch (base._kind) { + case 12: + stack.push(A._Universe__lookupGenericFunctionRti(t1, base, $arguments, parser.n)); + break; + default: + stack.push(A._Universe__lookupBindingRti(t1, base, $arguments)); + break; + } + } + }, + _Parser_handleArguments(parser, stack) { + var optionalPositional, named, requiredPositional, returnType, parameters, _null = null, + t1 = parser.u, + head = stack.pop(); + if (typeof head == "number") + switch (head) { + case -1: + optionalPositional = stack.pop(); + named = _null; + break; + case -2: + named = stack.pop(); + optionalPositional = _null; + break; + default: + stack.push(head); + named = _null; + optionalPositional = named; + break; + } + else { + stack.push(head); + named = _null; + optionalPositional = named; + } + requiredPositional = A._Parser_collectArray(parser, stack); + head = stack.pop(); + switch (head) { + case -3: + head = stack.pop(); + if (optionalPositional == null) + optionalPositional = t1.sEA; + if (named == null) + named = t1.sEA; + returnType = A._Parser_toType(t1, parser.e, head); + parameters = new A._FunctionParameters(); + parameters._requiredPositional = requiredPositional; + parameters._optionalPositional = optionalPositional; + parameters._named = named; + stack.push(A._Universe__lookupFunctionRti(t1, returnType, parameters)); + return; + case -4: + stack.push(A._Universe__lookupRecordRti(t1, stack.pop(), requiredPositional)); + return; + default: + throw A.wrapException(A.AssertionError$("Unexpected state under `()`: " + A.S(head))); + } + }, + _Parser_handleExtendedOperations(parser, stack) { + var $top = stack.pop(); + if (0 === $top) { + stack.push(A._Universe__lookupTerminalRti(parser.u, 1, "0&")); + return; + } + if (1 === $top) { + stack.push(A._Universe__lookupTerminalRti(parser.u, 4, "1&")); + return; + } + throw A.wrapException(A.AssertionError$("Unexpected extended operation " + A.S($top))); + }, + _Parser_collectArray(parser, stack) { + var array = stack.splice(parser.p); + A._Parser_toTypes(parser.u, parser.e, array); + parser.p = stack.pop(); + return array; + }, + _Parser_toType(universe, environment, item) { + if (typeof item == "string") + return A._Universe__lookupInterfaceRti(universe, item, universe.sEA); + else if (typeof item == "number") { + environment.toString; + return A._Parser_indexToType(universe, environment, item); + } else + return item; + }, + _Parser_toTypes(universe, environment, items) { + var i, + $length = items.length; + for (i = 0; i < $length; ++i) + items[i] = A._Parser_toType(universe, environment, items[i]); + }, + _Parser_toTypesNamed(universe, environment, items) { + var i, + $length = items.length; + for (i = 2; i < $length; i += 3) + items[i] = A._Parser_toType(universe, environment, items[i]); + }, + _Parser_indexToType(universe, environment, index) { + var typeArguments, len, + kind = environment._kind; + if (kind === 10) { + if (index === 0) + return environment._primary; + typeArguments = environment._rest; + len = typeArguments.length; + if (index <= len) + return typeArguments[index - 1]; + index -= len; + environment = environment._primary; + kind = environment._kind; + } else if (index === 0) + return environment; + if (kind !== 9) + throw A.wrapException(A.AssertionError$("Indexed base must be an interface type")); + typeArguments = environment._rest; + if (index <= typeArguments.length) + return typeArguments[index - 1]; + throw A.wrapException(A.AssertionError$("Bad index " + index + " for " + environment.toString$0(0))); + }, + isSubtype(universe, s, t) { + var result, + sCache = A.Rti__getIsSubtypeCache(s), + probe = sCache.get(t); + if (probe != null) + return probe; + result = A._isSubtype(universe, s, null, t, null); + sCache.set(t, result); + return result; + }, + _isSubtype(universe, s, sEnv, t, tEnv) { + var t1, sKind, leftTypeVariable, tKind, t2, sBounds, tBounds, sLength, i, sBound, tBound; + if (s === t) + return true; + if (!A.isStrongTopType(t)) + if (!(t === type$.legacy_Object)) + t1 = false; + else + t1 = true; + else + t1 = true; + if (t1) + return true; + sKind = s._kind; + if (sKind === 4) + return true; + if (A.isStrongTopType(s)) + return false; + if (s._kind !== 1) + t1 = false; + else + t1 = true; + if (t1) + return true; + leftTypeVariable = sKind === 14; + if (leftTypeVariable) + if (A._isSubtype(universe, sEnv[s._primary], sEnv, t, tEnv)) + return true; + tKind = t._kind; + t1 = s === type$.Null || s === type$.JSNull; + if (t1) { + if (tKind === 8) + return A._isSubtype(universe, s, sEnv, t._primary, tEnv); + return t === type$.Null || t === type$.JSNull || tKind === 7 || tKind === 6; + } + if (t === type$.Object) { + if (sKind === 8) + return A._isSubtype(universe, s._primary, sEnv, t, tEnv); + if (sKind === 6) + return A._isSubtype(universe, s._primary, sEnv, t, tEnv); + return sKind !== 7; + } + if (sKind === 6) + return A._isSubtype(universe, s._primary, sEnv, t, tEnv); + if (tKind === 6) { + t1 = A.Rti__getQuestionFromStar(universe, t); + return A._isSubtype(universe, s, sEnv, t1, tEnv); + } + if (sKind === 8) { + if (!A._isSubtype(universe, s._primary, sEnv, t, tEnv)) + return false; + return A._isSubtype(universe, A.Rti__getFutureFromFutureOr(universe, s), sEnv, t, tEnv); + } + if (sKind === 7) { + t1 = A._isSubtype(universe, type$.Null, sEnv, t, tEnv); + return t1 && A._isSubtype(universe, s._primary, sEnv, t, tEnv); + } + if (tKind === 8) { + if (A._isSubtype(universe, s, sEnv, t._primary, tEnv)) + return true; + return A._isSubtype(universe, s, sEnv, A.Rti__getFutureFromFutureOr(universe, t), tEnv); + } + if (tKind === 7) { + t1 = A._isSubtype(universe, s, sEnv, type$.Null, tEnv); + return t1 || A._isSubtype(universe, s, sEnv, t._primary, tEnv); + } + if (leftTypeVariable) + return false; + t1 = sKind !== 12; + if ((!t1 || sKind === 13) && t === type$.Function) + return true; + t2 = sKind === 11; + if (t2 && t === type$.Record) + return true; + if (tKind === 13) { + if (s === type$.JavaScriptFunction) + return true; + if (sKind !== 13) + return false; + sBounds = s._rest; + tBounds = t._rest; + sLength = sBounds.length; + if (sLength !== tBounds.length) + return false; + sEnv = sEnv == null ? sBounds : sBounds.concat(sEnv); + tEnv = tEnv == null ? tBounds : tBounds.concat(tEnv); + for (i = 0; i < sLength; ++i) { + sBound = sBounds[i]; + tBound = tBounds[i]; + if (!A._isSubtype(universe, sBound, sEnv, tBound, tEnv) || !A._isSubtype(universe, tBound, tEnv, sBound, sEnv)) + return false; + } + return A._isFunctionSubtype(universe, s._primary, sEnv, t._primary, tEnv); + } + if (tKind === 12) { + if (s === type$.JavaScriptFunction) + return true; + if (t1) + return false; + return A._isFunctionSubtype(universe, s, sEnv, t, tEnv); + } + if (sKind === 9) { + if (tKind !== 9) + return false; + return A._isInterfaceSubtype(universe, s, sEnv, t, tEnv); + } + if (t2 && tKind === 11) + return A._isRecordSubtype(universe, s, sEnv, t, tEnv); + return false; + }, + _isFunctionSubtype(universe, s, sEnv, t, tEnv) { + var sParameters, tParameters, sRequiredPositional, tRequiredPositional, sRequiredPositionalLength, tRequiredPositionalLength, requiredPositionalDelta, sOptionalPositional, tOptionalPositional, sOptionalPositionalLength, tOptionalPositionalLength, i, t1, sNamed, tNamed, sNamedLength, tNamedLength, sIndex, tIndex, tName, sName, sIsRequired; + if (!A._isSubtype(universe, s._primary, sEnv, t._primary, tEnv)) + return false; + sParameters = s._rest; + tParameters = t._rest; + sRequiredPositional = sParameters._requiredPositional; + tRequiredPositional = tParameters._requiredPositional; + sRequiredPositionalLength = sRequiredPositional.length; + tRequiredPositionalLength = tRequiredPositional.length; + if (sRequiredPositionalLength > tRequiredPositionalLength) + return false; + requiredPositionalDelta = tRequiredPositionalLength - sRequiredPositionalLength; + sOptionalPositional = sParameters._optionalPositional; + tOptionalPositional = tParameters._optionalPositional; + sOptionalPositionalLength = sOptionalPositional.length; + tOptionalPositionalLength = tOptionalPositional.length; + if (sRequiredPositionalLength + sOptionalPositionalLength < tRequiredPositionalLength + tOptionalPositionalLength) + return false; + for (i = 0; i < sRequiredPositionalLength; ++i) { + t1 = sRequiredPositional[i]; + if (!A._isSubtype(universe, tRequiredPositional[i], tEnv, t1, sEnv)) + return false; + } + for (i = 0; i < requiredPositionalDelta; ++i) { + t1 = sOptionalPositional[i]; + if (!A._isSubtype(universe, tRequiredPositional[sRequiredPositionalLength + i], tEnv, t1, sEnv)) + return false; + } + for (i = 0; i < tOptionalPositionalLength; ++i) { + t1 = sOptionalPositional[requiredPositionalDelta + i]; + if (!A._isSubtype(universe, tOptionalPositional[i], tEnv, t1, sEnv)) + return false; + } + sNamed = sParameters._named; + tNamed = tParameters._named; + sNamedLength = sNamed.length; + tNamedLength = tNamed.length; + for (sIndex = 0, tIndex = 0; tIndex < tNamedLength; tIndex += 3) { + tName = tNamed[tIndex]; + for (; true;) { + if (sIndex >= sNamedLength) + return false; + sName = sNamed[sIndex]; + sIndex += 3; + if (tName < sName) + return false; + sIsRequired = sNamed[sIndex - 2]; + if (sName < tName) { + if (sIsRequired) + return false; + continue; + } + t1 = tNamed[tIndex + 1]; + if (sIsRequired && !t1) + return false; + t1 = sNamed[sIndex - 1]; + if (!A._isSubtype(universe, tNamed[tIndex + 2], tEnv, t1, sEnv)) + return false; + break; + } + } + for (; sIndex < sNamedLength;) { + if (sNamed[sIndex + 1]) + return false; + sIndex += 3; + } + return true; + }, + _isInterfaceSubtype(universe, s, sEnv, t, tEnv) { + var rule, recipes, $length, supertypeArgs, i, t1, t2, + sName = s._primary, + tName = t._primary; + for (; sName !== tName;) { + rule = universe.tR[sName]; + if (rule == null) + return false; + if (typeof rule == "string") { + sName = rule; + continue; + } + recipes = rule[tName]; + if (recipes == null) + return false; + $length = recipes.length; + supertypeArgs = $length > 0 ? new Array($length) : init.typeUniverse.sEA; + for (i = 0; i < $length; ++i) + supertypeArgs[i] = A._Universe_evalInEnvironment(universe, s, recipes[i]); + return A._areArgumentsSubtypes(universe, supertypeArgs, null, sEnv, t._rest, tEnv); + } + t1 = s._rest; + t2 = t._rest; + return A._areArgumentsSubtypes(universe, t1, null, sEnv, t2, tEnv); + }, + _areArgumentsSubtypes(universe, sArgs, sVariances, sEnv, tArgs, tEnv) { + var i, t1, t2, + $length = sArgs.length; + for (i = 0; i < $length; ++i) { + t1 = sArgs[i]; + t2 = tArgs[i]; + if (!A._isSubtype(universe, t1, sEnv, t2, tEnv)) + return false; + } + return true; + }, + _isRecordSubtype(universe, s, sEnv, t, tEnv) { + var i, + sFields = s._rest, + tFields = t._rest, + sCount = sFields.length; + if (sCount !== tFields.length) + return false; + if (s._primary !== t._primary) + return false; + for (i = 0; i < sCount; ++i) + if (!A._isSubtype(universe, sFields[i], sEnv, tFields[i], tEnv)) + return false; + return true; + }, + isNullable(t) { + var t1, + kind = t._kind; + if (!(t === type$.Null || t === type$.JSNull)) + if (!A.isStrongTopType(t)) + if (kind !== 7) + if (!(kind === 6 && A.isNullable(t._primary))) + t1 = kind === 8 && A.isNullable(t._primary); + else + t1 = true; + else + t1 = true; + else + t1 = true; + else + t1 = true; + return t1; + }, + isTopType(t) { + var t1; + if (!A.isStrongTopType(t)) + if (!(t === type$.legacy_Object)) + t1 = false; + else + t1 = true; + else + t1 = true; + return t1; + }, + isStrongTopType(t) { + var kind = t._kind; + return kind === 2 || kind === 3 || kind === 4 || kind === 5 || t === type$.nullable_Object; + }, + _Utils_objectAssign(o, other) { + var i, key, + keys = Object.keys(other), + $length = keys.length; + for (i = 0; i < $length; ++i) { + key = keys[i]; + o[key] = other[key]; + } + }, + _Utils_newArrayOrEmpty($length) { + return $length > 0 ? new Array($length) : init.typeUniverse.sEA; + }, + Rti: function Rti(t0, t1) { + var _ = this; + _._as = t0; + _._is = t1; + _._cachedRuntimeType = _._specializedTestResource = _._unsoundIsSubtypeCache = _._isSubtypeCache = _._precomputed1 = null; + _._kind = 0; + _._canonicalRecipe = _._bindCache = _._evalCache = _._rest = _._primary = null; + }, + _FunctionParameters: function _FunctionParameters() { + this._named = this._optionalPositional = this._requiredPositional = null; + }, + _Type: function _Type(t0) { + this._rti = t0; + }, + _Error: function _Error() { + }, + _TypeError: function _TypeError(t0) { + this.__rti$_message = t0; + }, + _AsyncRun__initializeScheduleImmediate() { + var div, span, t1 = {}; + if (self.scheduleImmediate != null) + return A.async__AsyncRun__scheduleImmediateJsOverride$closure(); + if (self.MutationObserver != null && self.document != null) { + div = self.document.createElement("div"); + span = self.document.createElement("span"); + t1.storedCallback = null; + new self.MutationObserver(A.convertDartClosureToJS(new A._AsyncRun__initializeScheduleImmediate_internalCallback(t1), 1)).observe(div, {childList: true}); + return new A._AsyncRun__initializeScheduleImmediate_closure(t1, div, span); + } else if (self.setImmediate != null) + return A.async__AsyncRun__scheduleImmediateWithSetImmediate$closure(); + return A.async__AsyncRun__scheduleImmediateWithTimer$closure(); + }, + _AsyncRun__scheduleImmediateJsOverride(callback) { + self.scheduleImmediate(A.convertDartClosureToJS(new A._AsyncRun__scheduleImmediateJsOverride_internalCallback(type$.void_Function._as(callback)), 0)); + }, + _AsyncRun__scheduleImmediateWithSetImmediate(callback) { + self.setImmediate(A.convertDartClosureToJS(new A._AsyncRun__scheduleImmediateWithSetImmediate_internalCallback(type$.void_Function._as(callback)), 0)); + }, + _AsyncRun__scheduleImmediateWithTimer(callback) { + A.Timer__createTimer(B.Duration_0, type$.void_Function._as(callback)); + }, + Timer__createTimer(duration, callback) { + var milliseconds = B.JSInt_methods._tdivFast$1(duration._duration, 1000); + return A._TimerImpl$(milliseconds, callback); + }, + Timer__createPeriodicTimer(duration, callback) { + var milliseconds = B.JSInt_methods._tdivFast$1(duration._duration, 1000); + return A._TimerImpl$periodic(milliseconds, callback); + }, + _TimerImpl$(milliseconds, callback) { + var t1 = new A._TimerImpl(); + t1._TimerImpl$2(milliseconds, callback); + return t1; + }, + _TimerImpl$periodic(milliseconds, callback) { + var t1 = new A._TimerImpl(); + t1._TimerImpl$periodic$2(milliseconds, callback); + return t1; + }, + AsyncError$(error, stackTrace) { + var t1 = A.checkNotNullable(error, "error", type$.Object); + return new A.AsyncError(t1, stackTrace == null ? A.AsyncError_defaultStackTrace(error) : stackTrace); + }, + AsyncError_defaultStackTrace(error) { + var stackTrace; + if (type$.Error._is(error)) { + stackTrace = error.get$stackTrace(); + if (stackTrace != null) + return stackTrace; + } + return B._StringStackTrace_3uE; + }, + Future_Future$value(value, $T) { + var t1; + $T._as(value); + t1 = new A._Future($.Zone__current, $T._eval$1("_Future<0>")); + t1._asyncComplete$1(value); + return t1; + }, + _Future__chainCoreFutureSync(source, target) { + var t1, t2, listeners; + for (t1 = type$._Future_dynamic; t2 = source._async$_state, (t2 & 4) !== 0;) + source = t1._as(source._resultOrListeners); + if ((t2 & 24) !== 0) { + listeners = target._removeListeners$0(); + target._cloneResult$1(source); + A._Future__propagateToListeners(target, listeners); + } else { + listeners = type$.nullable__FutureListener_dynamic_dynamic._as(target._resultOrListeners); + target._setChained$1(source); + source._prependListeners$1(listeners); + } + }, + _Future__chainCoreFutureAsync(source, target) { + var t2, t3, listeners, _box_0 = {}, + t1 = _box_0.source = source; + for (t2 = type$._Future_dynamic; t3 = t1._async$_state, (t3 & 4) !== 0; t1 = source) { + source = t2._as(t1._resultOrListeners); + _box_0.source = source; + } + if ((t3 & 24) === 0) { + listeners = type$.nullable__FutureListener_dynamic_dynamic._as(target._resultOrListeners); + target._setChained$1(t1); + _box_0.source._prependListeners$1(listeners); + return; + } + if ((t3 & 16) === 0 && target._resultOrListeners == null) { + target._cloneResult$1(t1); + return; + } + target._async$_state ^= 2; + target._zone.scheduleMicrotask$1(new A._Future__chainCoreFutureAsync_closure(_box_0, target)); + }, + _Future__propagateToListeners(source, listeners) { + var t2, t3, t4, _box_0, t5, t6, hasError, asyncError, nextListener, nextListener0, sourceResult, t7, zone, oldZone, result, current, _box_1 = {}, + t1 = _box_1.source = source; + for (t2 = type$.AsyncError, t3 = type$.nullable__FutureListener_dynamic_dynamic, t4 = type$.Future_dynamic; true;) { + _box_0 = {}; + t5 = t1._async$_state; + t6 = (t5 & 16) === 0; + hasError = !t6; + if (listeners == null) { + if (hasError && (t5 & 1) === 0) { + asyncError = t2._as(t1._resultOrListeners); + t1._zone.handleUncaughtError$2(asyncError.error, asyncError.stackTrace); + } + return; + } + _box_0.listener = listeners; + nextListener = listeners._nextListener; + for (t1 = listeners; nextListener != null; t1 = nextListener, nextListener = nextListener0) { + t1._nextListener = null; + A._Future__propagateToListeners(_box_1.source, t1); + _box_0.listener = nextListener; + nextListener0 = nextListener._nextListener; + } + t5 = _box_1.source; + sourceResult = t5._resultOrListeners; + _box_0.listenerHasError = hasError; + _box_0.listenerValueOrError = sourceResult; + if (t6) { + t7 = t1.state; + t7 = (t7 & 1) !== 0 || (t7 & 15) === 8; + } else + t7 = true; + if (t7) { + zone = t1.result._zone; + if (hasError) { + t1 = t5._zone; + t1 = !(t1 === zone || t1.get$errorZone() === zone.get$errorZone()); + } else + t1 = false; + if (t1) { + t1 = _box_1.source; + asyncError = t2._as(t1._resultOrListeners); + t1._zone.handleUncaughtError$2(asyncError.error, asyncError.stackTrace); + return; + } + oldZone = $.Zone__current; + if (oldZone !== zone) + $.Zone__current = zone; + else + oldZone = null; + t1 = _box_0.listener.state; + if ((t1 & 15) === 8) + new A._Future__propagateToListeners_handleWhenCompleteCallback(_box_0, _box_1, hasError).call$0(); + else if (t6) { + if ((t1 & 1) !== 0) + new A._Future__propagateToListeners_handleValueCallback(_box_0, sourceResult).call$0(); + } else if ((t1 & 2) !== 0) + new A._Future__propagateToListeners_handleError(_box_1, _box_0).call$0(); + if (oldZone != null) + $.Zone__current = oldZone; + t1 = _box_0.listenerValueOrError; + if (t1 instanceof A._Future) { + t5 = _box_0.listener.$ti; + t5 = t5._eval$1("Future<2>")._is(t1) || !t5._rest[1]._is(t1); + } else + t5 = false; + if (t5) { + t4._as(t1); + result = _box_0.listener.result; + if ((t1._async$_state & 24) !== 0) { + current = t3._as(result._resultOrListeners); + result._resultOrListeners = null; + listeners = result._reverseListeners$1(current); + result._async$_state = t1._async$_state & 30 | result._async$_state & 1; + result._resultOrListeners = t1._resultOrListeners; + _box_1.source = t1; + continue; + } else + A._Future__chainCoreFutureSync(t1, result); + return; + } + } + result = _box_0.listener.result; + current = t3._as(result._resultOrListeners); + result._resultOrListeners = null; + listeners = result._reverseListeners$1(current); + t1 = _box_0.listenerHasError; + t5 = _box_0.listenerValueOrError; + if (!t1) { + result.$ti._precomputed1._as(t5); + result._async$_state = 8; + result._resultOrListeners = t5; + } else { + t2._as(t5); + result._async$_state = result._async$_state & 1 | 16; + result._resultOrListeners = t5; + } + _box_1.source = result; + t1 = result; + } + }, + _registerErrorHandler(errorHandler, zone) { + if (type$.dynamic_Function_Object_StackTrace._is(errorHandler)) + return zone.registerBinaryCallback$3$1(errorHandler, type$.dynamic, type$.Object, type$.StackTrace); + if (type$.dynamic_Function_Object._is(errorHandler)) + return zone.registerUnaryCallback$2$1(errorHandler, type$.dynamic, type$.Object); + throw A.wrapException(A.ArgumentError$value(errorHandler, "onError", string$.Error_)); + }, + _microtaskLoop() { + var entry, next; + for (entry = $._nextCallback; entry != null; entry = $._nextCallback) { + $._lastPriorityCallback = null; + next = entry.next; + $._nextCallback = next; + if (next == null) + $._lastCallback = null; + entry.callback.call$0(); + } + }, + _startMicrotaskLoop() { + $._isInCallbackLoop = true; + try { + A._microtaskLoop(); + } finally { + $._lastPriorityCallback = null; + $._isInCallbackLoop = false; + if ($._nextCallback != null) + $.$get$_AsyncRun__scheduleImmediateClosure().call$1(A.async___startMicrotaskLoop$closure()); + } + }, + _scheduleAsyncCallback(callback) { + var newEntry = new A._AsyncCallbackEntry(callback), + lastCallback = $._lastCallback; + if (lastCallback == null) { + $._nextCallback = $._lastCallback = newEntry; + if (!$._isInCallbackLoop) + $.$get$_AsyncRun__scheduleImmediateClosure().call$1(A.async___startMicrotaskLoop$closure()); + } else + $._lastCallback = lastCallback.next = newEntry; + }, + _schedulePriorityAsyncCallback(callback) { + var entry, lastPriorityCallback, next, + t1 = $._nextCallback; + if (t1 == null) { + A._scheduleAsyncCallback(callback); + $._lastPriorityCallback = $._lastCallback; + return; + } + entry = new A._AsyncCallbackEntry(callback); + lastPriorityCallback = $._lastPriorityCallback; + if (lastPriorityCallback == null) { + entry.next = t1; + $._nextCallback = $._lastPriorityCallback = entry; + } else { + next = lastPriorityCallback.next; + entry.next = next; + $._lastPriorityCallback = lastPriorityCallback.next = entry; + if (next == null) + $._lastCallback = entry; + } + }, + scheduleMicrotask(callback) { + var t1, _null = null, + currentZone = $.Zone__current; + if (B.C__RootZone === currentZone) { + A._rootScheduleMicrotask(_null, _null, B.C__RootZone, callback); + return; + } + if (B.C__RootZone === currentZone.get$_scheduleMicrotask().zone) + t1 = B.C__RootZone.get$errorZone() === currentZone.get$errorZone(); + else + t1 = false; + if (t1) { + A._rootScheduleMicrotask(_null, _null, currentZone, currentZone.registerCallback$1$1(callback, type$.void)); + return; + } + t1 = $.Zone__current; + t1.scheduleMicrotask$1(t1.bindCallbackGuarded$1(callback)); + }, + StreamController_StreamController(onCancel, onListen, sync, $T) { + return new A._SyncStreamController(onListen, null, null, onCancel, $T._eval$1("_SyncStreamController<0>")); + }, + _runGuarded(notificationHandler) { + var e, s, exception; + if (notificationHandler == null) + return; + try { + notificationHandler.call$0(); + } catch (exception) { + e = A.unwrapException(exception); + s = A.getTraceFromException(exception); + $.Zone__current.handleUncaughtError$2(e, s); + } + }, + _BufferingStreamSubscription__registerDataHandler(zone, handleData, $T) { + var t1 = handleData == null ? A.async___nullDataHandler$closure() : handleData; + return zone.registerUnaryCallback$2$1(t1, type$.void, $T); + }, + _BufferingStreamSubscription__registerErrorHandler(zone, handleError) { + if (handleError == null) + handleError = A.async___nullErrorHandler$closure(); + if (type$.void_Function_Object_StackTrace._is(handleError)) + return zone.registerBinaryCallback$3$1(handleError, type$.dynamic, type$.Object, type$.StackTrace); + if (type$.void_Function_Object._is(handleError)) + return zone.registerUnaryCallback$2$1(handleError, type$.dynamic, type$.Object); + throw A.wrapException(A.ArgumentError$(string$.handle, null)); + }, + _nullDataHandler(value) { + }, + _nullErrorHandler(error, stackTrace) { + type$.Object._as(error); + type$.StackTrace._as(stackTrace); + $.Zone__current.handleUncaughtError$2(error, stackTrace); + }, + _nullDoneHandler() { + }, + Timer_Timer$periodic(duration, callback) { + var boundCallback, + t1 = $.Zone__current; + if (t1 === B.C__RootZone) + return t1.createPeriodicTimer$2(duration, callback); + boundCallback = t1.bindUnaryCallbackGuarded$1$1(callback, type$.Timer); + return $.Zone__current.createPeriodicTimer$2(duration, boundCallback); + }, + ZoneSpecification_ZoneSpecification$from(other, handleUncaughtError) { + var t1 = handleUncaughtError == null ? other.handleUncaughtError : handleUncaughtError; + return new A._ZoneSpecification(t1, other.run, other.runUnary, other.runBinary, other.registerCallback, other.registerUnaryCallback, other.registerBinaryCallback, other.errorCallback, other.scheduleMicrotask, other.createTimer, other.createPeriodicTimer, other.print, other.fork); + }, + _rootHandleUncaughtError($self, $parent, zone, error, stackTrace) { + A._rootHandleError(error, type$.StackTrace._as(stackTrace)); + }, + _rootHandleError(error, stackTrace) { + A._schedulePriorityAsyncCallback(new A._rootHandleError_closure(error, stackTrace)); + }, + _rootRun($self, $parent, zone, f, $R) { + var old, t1; + type$.nullable_Zone._as($self); + type$.nullable_ZoneDelegate._as($parent); + type$.Zone._as(zone); + $R._eval$1("0()")._as(f); + t1 = $.Zone__current; + if (t1 === zone) + return f.call$0(); + $.Zone__current = zone; + old = t1; + try { + t1 = f.call$0(); + return t1; + } finally { + $.Zone__current = old; + } + }, + _rootRunUnary($self, $parent, zone, f, arg, $R, $T) { + var old, t1; + type$.nullable_Zone._as($self); + type$.nullable_ZoneDelegate._as($parent); + type$.Zone._as(zone); + $R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(f); + $T._as(arg); + t1 = $.Zone__current; + if (t1 === zone) + return f.call$1(arg); + $.Zone__current = zone; + old = t1; + try { + t1 = f.call$1(arg); + return t1; + } finally { + $.Zone__current = old; + } + }, + _rootRunBinary($self, $parent, zone, f, arg1, arg2, $R, T1, T2) { + var old, t1; + type$.nullable_Zone._as($self); + type$.nullable_ZoneDelegate._as($parent); + type$.Zone._as(zone); + $R._eval$1("@<0>")._bind$1(T1)._bind$1(T2)._eval$1("1(2,3)")._as(f); + T1._as(arg1); + T2._as(arg2); + t1 = $.Zone__current; + if (t1 === zone) + return f.call$2(arg1, arg2); + $.Zone__current = zone; + old = t1; + try { + t1 = f.call$2(arg1, arg2); + return t1; + } finally { + $.Zone__current = old; + } + }, + _rootRegisterCallback($self, $parent, zone, f, $R) { + return $R._eval$1("0()")._as(f); + }, + _rootRegisterUnaryCallback($self, $parent, zone, f, $R, $T) { + return $R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(f); + }, + _rootRegisterBinaryCallback($self, $parent, zone, f, $R, T1, T2) { + return $R._eval$1("@<0>")._bind$1(T1)._bind$1(T2)._eval$1("1(2,3)")._as(f); + }, + _rootErrorCallback($self, $parent, zone, error, stackTrace) { + type$.nullable_StackTrace._as(stackTrace); + return null; + }, + _rootScheduleMicrotask($self, $parent, zone, f) { + var t1, t2; + type$.void_Function._as(f); + if (B.C__RootZone !== zone) { + t1 = B.C__RootZone.get$errorZone(); + t2 = zone.get$errorZone(); + f = t1 !== t2 ? zone.bindCallbackGuarded$1(f) : zone.bindCallback$1$1(f, type$.void); + } + A._scheduleAsyncCallback(f); + }, + _rootCreateTimer($self, $parent, zone, duration, callback) { + type$.Duration._as(duration); + type$.void_Function._as(callback); + return A.Timer__createTimer(duration, B.C__RootZone !== zone ? zone.bindCallback$1$1(callback, type$.void) : callback); + }, + _rootCreatePeriodicTimer($self, $parent, zone, duration, callback) { + type$.Duration._as(duration); + type$.void_Function_Timer._as(callback); + return A.Timer__createPeriodicTimer(duration, B.C__RootZone !== zone ? zone.bindUnaryCallback$2$1(callback, type$.void, type$.Timer) : callback); + }, + _rootPrint($self, $parent, zone, line) { + A.printString(A.S(A._asString(line))); + }, + _rootFork($self, $parent, zone, specification, zoneValues) { + var valueMap, t1, handleUncaughtError; + type$.nullable_ZoneSpecification._as(specification); + type$.nullable_Map_of_nullable_Object_and_nullable_Object._as(zoneValues); + valueMap = zone.get$_async$_map(); + t1 = new A._CustomZone(zone.get$_run(), zone.get$_runUnary(), zone.get$_runBinary(), zone.get$_registerCallback(), zone.get$_registerUnaryCallback(), zone.get$_registerBinaryCallback(), zone.get$_errorCallback(), zone.get$_scheduleMicrotask(), zone.get$_createTimer(), zone.get$_createPeriodicTimer(), zone.get$_print(), zone.get$_fork(), zone.get$_handleUncaughtError(), zone, valueMap); + handleUncaughtError = specification.handleUncaughtError; + if (handleUncaughtError != null) + t1.set$_handleUncaughtError(new A._ZoneFunction(t1, handleUncaughtError, type$._ZoneFunction_of_void_Function_Zone_ZoneDelegate_Zone_Object_StackTrace)); + return t1; + }, + runZonedGuarded(body, onError, $R) { + var error, stackTrace, parentZone, errorHandler, t1, exception, _null = null, zoneSpecification = null, zoneValues = null; + A.checkNotNullable(body, "body", $R._eval$1("0()")); + A.checkNotNullable(onError, "onError", type$.void_Function_Object_StackTrace); + parentZone = $.Zone__current; + errorHandler = new A.runZonedGuarded_closure(parentZone, onError); + if (zoneSpecification == null) + zoneSpecification = new A._ZoneSpecification(errorHandler, _null, _null, _null, _null, _null, _null, _null, _null, _null, _null, _null, _null); + else + zoneSpecification = A.ZoneSpecification_ZoneSpecification$from(zoneSpecification, errorHandler); + try { + t1 = parentZone.fork$2$specification$zoneValues(zoneSpecification, zoneValues).run$1$1(body, $R); + return t1; + } catch (exception) { + error = A.unwrapException(exception); + stackTrace = A.getTraceFromException(exception); + onError.call$2(error, stackTrace); + } + return _null; + }, + _AsyncRun__initializeScheduleImmediate_internalCallback: function _AsyncRun__initializeScheduleImmediate_internalCallback(t0) { + this._box_0 = t0; + }, + _AsyncRun__initializeScheduleImmediate_closure: function _AsyncRun__initializeScheduleImmediate_closure(t0, t1, t2) { + this._box_0 = t0; + this.div = t1; + this.span = t2; + }, + _AsyncRun__scheduleImmediateJsOverride_internalCallback: function _AsyncRun__scheduleImmediateJsOverride_internalCallback(t0) { + this.callback = t0; + }, + _AsyncRun__scheduleImmediateWithSetImmediate_internalCallback: function _AsyncRun__scheduleImmediateWithSetImmediate_internalCallback(t0) { + this.callback = t0; + }, + _TimerImpl: function _TimerImpl() { + this._tick = 0; + }, + _TimerImpl_internalCallback: function _TimerImpl_internalCallback(t0, t1) { + this.$this = t0; + this.callback = t1; + }, + _TimerImpl$periodic_closure: function _TimerImpl$periodic_closure(t0, t1, t2, t3) { + var _ = this; + _.$this = t0; + _.milliseconds = t1; + _.start = t2; + _.callback = t3; + }, + AsyncError: function AsyncError(t0, t1) { + this.error = t0; + this.stackTrace = t1; + }, + _Completer: function _Completer() { + }, + _AsyncCompleter: function _AsyncCompleter(t0, t1) { + this.future = t0; + this.$ti = t1; + }, + _SyncCompleter: function _SyncCompleter(t0, t1) { + this.future = t0; + this.$ti = t1; + }, + _FutureListener: function _FutureListener(t0, t1, t2, t3, t4) { + var _ = this; + _._nextListener = null; + _.result = t0; + _.state = t1; + _.callback = t2; + _.errorCallback = t3; + _.$ti = t4; + }, + _Future: function _Future(t0, t1) { + var _ = this; + _._async$_state = 0; + _._zone = t0; + _._resultOrListeners = null; + _.$ti = t1; + }, + _Future__addListener_closure: function _Future__addListener_closure(t0, t1) { + this.$this = t0; + this.listener = t1; + }, + _Future__prependListeners_closure: function _Future__prependListeners_closure(t0, t1) { + this._box_0 = t0; + this.$this = t1; + }, + _Future__chainForeignFuture_closure: function _Future__chainForeignFuture_closure(t0) { + this.$this = t0; + }, + _Future__chainForeignFuture_closure0: function _Future__chainForeignFuture_closure0(t0) { + this.$this = t0; + }, + _Future__chainForeignFuture_closure1: function _Future__chainForeignFuture_closure1(t0, t1, t2) { + this.$this = t0; + this.e = t1; + this.s = t2; + }, + _Future__chainCoreFutureAsync_closure: function _Future__chainCoreFutureAsync_closure(t0, t1) { + this._box_0 = t0; + this.target = t1; + }, + _Future__asyncCompleteWithValue_closure: function _Future__asyncCompleteWithValue_closure(t0, t1) { + this.$this = t0; + this.value = t1; + }, + _Future__asyncCompleteError_closure: function _Future__asyncCompleteError_closure(t0, t1, t2) { + this.$this = t0; + this.error = t1; + this.stackTrace = t2; + }, + _Future__propagateToListeners_handleWhenCompleteCallback: function _Future__propagateToListeners_handleWhenCompleteCallback(t0, t1, t2) { + this._box_0 = t0; + this._box_1 = t1; + this.hasError = t2; + }, + _Future__propagateToListeners_handleWhenCompleteCallback_closure: function _Future__propagateToListeners_handleWhenCompleteCallback_closure(t0) { + this.originalSource = t0; + }, + _Future__propagateToListeners_handleValueCallback: function _Future__propagateToListeners_handleValueCallback(t0, t1) { + this._box_0 = t0; + this.sourceResult = t1; + }, + _Future__propagateToListeners_handleError: function _Future__propagateToListeners_handleError(t0, t1) { + this._box_1 = t0; + this._box_0 = t1; + }, + _AsyncCallbackEntry: function _AsyncCallbackEntry(t0) { + this.callback = t0; + this.next = null; + }, + Stream: function Stream() { + }, + Stream_pipe_closure: function Stream_pipe_closure(t0) { + this.streamConsumer = t0; + }, + Stream_length_closure: function Stream_length_closure(t0, t1) { + this._box_0 = t0; + this.$this = t1; + }, + Stream_length_closure0: function Stream_length_closure0(t0, t1) { + this._box_0 = t0; + this.future = t1; + }, + _StreamController: function _StreamController() { + }, + _StreamController__subscribe_closure: function _StreamController__subscribe_closure(t0) { + this.$this = t0; + }, + _StreamController__recordCancel_complete: function _StreamController__recordCancel_complete(t0) { + this.$this = t0; + }, + _SyncStreamControllerDispatch: function _SyncStreamControllerDispatch() { + }, + _SyncStreamController: function _SyncStreamController(t0, t1, t2, t3, t4) { + var _ = this; + _._varData = null; + _._async$_state = 0; + _._doneFuture = null; + _.onListen = t0; + _.onPause = t1; + _.onResume = t2; + _.onCancel = t3; + _.$ti = t4; + }, + _ControllerStream: function _ControllerStream(t0, t1) { + this._controller = t0; + this.$ti = t1; + }, + _ControllerSubscription: function _ControllerSubscription(t0, t1, t2, t3, t4, t5, t6) { + var _ = this; + _._controller = t0; + _._onData = t1; + _._onError = t2; + _._onDone = t3; + _._zone = t4; + _._async$_state = t5; + _._pending = _._cancelFuture = null; + _.$ti = t6; + }, + _StreamSinkWrapper: function _StreamSinkWrapper(t0, t1) { + this._target = t0; + this.$ti = t1; + }, + _AddStreamState_cancel_closure: function _AddStreamState_cancel_closure(t0) { + this.$this = t0; + }, + _BufferingStreamSubscription: function _BufferingStreamSubscription() { + }, + _BufferingStreamSubscription__sendError_sendError: function _BufferingStreamSubscription__sendError_sendError(t0, t1, t2) { + this.$this = t0; + this.error = t1; + this.stackTrace = t2; + }, + _BufferingStreamSubscription__sendDone_sendDone: function _BufferingStreamSubscription__sendDone_sendDone(t0) { + this.$this = t0; + }, + _StreamImpl: function _StreamImpl() { + }, + _DelayedEvent: function _DelayedEvent() { + }, + _DelayedData: function _DelayedData(t0, t1) { + this.value = t0; + this.next = null; + this.$ti = t1; + }, + _DelayedError: function _DelayedError(t0, t1) { + this.error = t0; + this.stackTrace = t1; + this.next = null; + }, + _DelayedDone: function _DelayedDone() { + }, + _PendingEvents: function _PendingEvents(t0) { + var _ = this; + _._async$_state = 0; + _.lastPendingEvent = _.firstPendingEvent = null; + _.$ti = t0; + }, + _PendingEvents_schedule_closure: function _PendingEvents_schedule_closure(t0, t1) { + this.$this = t0; + this.dispatch = t1; + }, + _DoneStreamSubscription: function _DoneStreamSubscription(t0, t1) { + var _ = this; + _._async$_state = 1; + _._zone = t0; + _._onDone = null; + _.$ti = t1; + }, + _EmptyStream: function _EmptyStream(t0) { + this.$ti = t0; + }, + _ZoneFunction: function _ZoneFunction(t0, t1, t2) { + this.zone = t0; + this.$function = t1; + this.$ti = t2; + }, + _ZoneSpecification: function _ZoneSpecification(t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12) { + var _ = this; + _.handleUncaughtError = t0; + _.run = t1; + _.runUnary = t2; + _.runBinary = t3; + _.registerCallback = t4; + _.registerUnaryCallback = t5; + _.registerBinaryCallback = t6; + _.errorCallback = t7; + _.scheduleMicrotask = t8; + _.createTimer = t9; + _.createPeriodicTimer = t10; + _.print = t11; + _.fork = t12; + }, + _ZoneDelegate: function _ZoneDelegate(t0) { + this._delegationTarget = t0; + }, + _Zone: function _Zone() { + }, + _CustomZone: function _CustomZone(t0, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14) { + var _ = this; + _._run = t0; + _._runUnary = t1; + _._runBinary = t2; + _._registerCallback = t3; + _._registerUnaryCallback = t4; + _._registerBinaryCallback = t5; + _._errorCallback = t6; + _._scheduleMicrotask = t7; + _._createTimer = t8; + _._createPeriodicTimer = t9; + _._print = t10; + _._fork = t11; + _._handleUncaughtError = t12; + _._delegateCache = null; + _.parent = t13; + _._async$_map = t14; + }, + _CustomZone_bindCallback_closure: function _CustomZone_bindCallback_closure(t0, t1, t2) { + this.$this = t0; + this.registered = t1; + this.R = t2; + }, + _CustomZone_bindUnaryCallback_closure: function _CustomZone_bindUnaryCallback_closure(t0, t1, t2, t3) { + var _ = this; + _.$this = t0; + _.registered = t1; + _.T = t2; + _.R = t3; + }, + _CustomZone_bindCallbackGuarded_closure: function _CustomZone_bindCallbackGuarded_closure(t0, t1) { + this.$this = t0; + this.registered = t1; + }, + _CustomZone_bindUnaryCallbackGuarded_closure: function _CustomZone_bindUnaryCallbackGuarded_closure(t0, t1, t2) { + this.$this = t0; + this.registered = t1; + this.T = t2; + }, + _rootHandleError_closure: function _rootHandleError_closure(t0, t1) { + this.error = t0; + this.stackTrace = t1; + }, + _RootZone: function _RootZone() { + }, + _RootZone_bindCallback_closure: function _RootZone_bindCallback_closure(t0, t1, t2) { + this.$this = t0; + this.f = t1; + this.R = t2; + }, + _RootZone_bindUnaryCallback_closure: function _RootZone_bindUnaryCallback_closure(t0, t1, t2, t3) { + var _ = this; + _.$this = t0; + _.f = t1; + _.T = t2; + _.R = t3; + }, + _RootZone_bindCallbackGuarded_closure: function _RootZone_bindCallbackGuarded_closure(t0, t1) { + this.$this = t0; + this.f = t1; + }, + _RootZone_bindUnaryCallbackGuarded_closure: function _RootZone_bindUnaryCallbackGuarded_closure(t0, t1, t2) { + this.$this = t0; + this.f = t1; + this.T = t2; + }, + runZonedGuarded_closure: function runZonedGuarded_closure(t0, t1) { + this.parentZone = t0; + this.onError = t1; + }, + HashMap_HashMap($K, $V) { + return new A._HashMap($K._eval$1("@<0>")._bind$1($V)._eval$1("_HashMap<1,2>")); + }, + _HashMap__getTableEntry(table, key) { + var entry = table[key]; + return entry === table ? null : entry; + }, + _HashMap__setTableEntry(table, key, value) { + if (value == null) + table[key] = table; + else + table[key] = value; + }, + _HashMap__newHashTable() { + var table = Object.create(null); + A._HashMap__setTableEntry(table, "<non-identifier-key>", table); + delete table["<non-identifier-key>"]; + return table; + }, + LinkedHashMap_LinkedHashMap$_literal(keyValuePairs, $K, $V) { + return $K._eval$1("@<0>")._bind$1($V)._eval$1("LinkedHashMap<1,2>")._as(A.fillLiteralMap(keyValuePairs, new A.JsLinkedHashMap($K._eval$1("@<0>")._bind$1($V)._eval$1("JsLinkedHashMap<1,2>")))); + }, + LinkedHashMap_LinkedHashMap$_empty($K, $V) { + return new A.JsLinkedHashMap($K._eval$1("@<0>")._bind$1($V)._eval$1("JsLinkedHashMap<1,2>")); + }, + LinkedHashSet_LinkedHashSet$_empty($E) { + return new A._LinkedHashSet($E._eval$1("_LinkedHashSet<0>")); + }, + _LinkedHashSet__newHashTable() { + var table = Object.create(null); + table["<non-identifier-key>"] = table; + delete table["<non-identifier-key>"]; + return table; + }, + _LinkedHashSetIterator$(_set, _modifications, $E) { + var t1 = new A._LinkedHashSetIterator(_set, _modifications, $E._eval$1("_LinkedHashSetIterator<0>")); + t1._collection$_cell = _set._collection$_first; + return t1; + }, + MapBase_mapToString(m) { + var result, t1 = {}; + if (A.isToStringVisiting(m)) + return "{...}"; + result = new A.StringBuffer(""); + try { + B.JSArray_methods.add$1($.toStringVisiting, m); + result._contents += "{"; + t1.first = true; + J.forEach$1$x(m, new A.MapBase_mapToString_closure(t1, result)); + result._contents += "}"; + } finally { + if (0 >= $.toStringVisiting.length) + return A.ioore($.toStringVisiting, -1); + $.toStringVisiting.pop(); + } + t1 = result._contents; + return t1.charCodeAt(0) == 0 ? t1 : t1; + }, + _HashMap: function _HashMap(t0) { + var _ = this; + _._collection$_length = 0; + _._keys = _._collection$_rest = _._collection$_nums = _._collection$_strings = null; + _.$ti = t0; + }, + _IdentityHashMap: function _IdentityHashMap(t0) { + var _ = this; + _._collection$_length = 0; + _._keys = _._collection$_rest = _._collection$_nums = _._collection$_strings = null; + _.$ti = t0; + }, + _HashMapKeyIterable: function _HashMapKeyIterable(t0, t1) { + this._collection$_map = t0; + this.$ti = t1; + }, + _HashMapKeyIterator: function _HashMapKeyIterator(t0, t1, t2) { + var _ = this; + _._collection$_map = t0; + _._keys = t1; + _._offset = 0; + _._collection$_current = null; + _.$ti = t2; + }, + _LinkedHashSet: function _LinkedHashSet(t0) { + var _ = this; + _._collection$_length = 0; + _._collection$_last = _._collection$_first = _._collection$_rest = _._collection$_nums = _._collection$_strings = null; + _._collection$_modifications = 0; + _.$ti = t0; + }, + _LinkedHashSetCell: function _LinkedHashSetCell(t0) { + this._element = t0; + this._collection$_previous = this._collection$_next = null; + }, + _LinkedHashSetIterator: function _LinkedHashSetIterator(t0, t1, t2) { + var _ = this; + _._set = t0; + _._collection$_modifications = t1; + _._collection$_current = _._collection$_cell = null; + _.$ti = t2; + }, + ListBase: function ListBase() { + }, + MapBase: function MapBase() { + }, + MapBase_mapToString_closure: function MapBase_mapToString_closure(t0, t1) { + this._box_0 = t0; + this.result = t1; + }, + _UnmodifiableMapMixin: function _UnmodifiableMapMixin() { + }, + MapView: function MapView() { + }, + UnmodifiableMapView: function UnmodifiableMapView(t0, t1) { + this._collection$_map = t0; + this.$ti = t1; + }, + SetBase: function SetBase() { + }, + _SetBase: function _SetBase() { + }, + _UnmodifiableMapView_MapView__UnmodifiableMapMixin: function _UnmodifiableMapView_MapView__UnmodifiableMapMixin() { + }, + _parseJson(source, reviver) { + var e, exception, t1, parsed = null; + try { + parsed = JSON.parse(source); + } catch (exception) { + e = A.unwrapException(exception); + t1 = A.FormatException$(String(e), null, null); + throw A.wrapException(t1); + } + t1 = A._convertJsonToDartLazy(parsed); + return t1; + }, + _convertJsonToDartLazy(object) { + var i; + if (object == null) + return null; + if (typeof object != "object") + return object; + if (Object.getPrototypeOf(object) !== Array.prototype) + return new A._JsonMap(object, Object.create(null)); + for (i = 0; i < object.length; ++i) + object[i] = A._convertJsonToDartLazy(object[i]); + return object; + }, + Utf8Decoder__convertIntercepted(allowMalformed, codeUnits, start, end) { + var casted, result; + if (codeUnits instanceof Uint8Array) { + casted = codeUnits; + end = casted.length; + if (end - start < 15) + return null; + result = A.Utf8Decoder__convertInterceptedUint8List(allowMalformed, casted, start, end); + if (result != null && allowMalformed) + if (result.indexOf("\ufffd") >= 0) + return null; + return result; + } + return null; + }, + Utf8Decoder__convertInterceptedUint8List(allowMalformed, codeUnits, start, end) { + var decoder = allowMalformed ? $.$get$Utf8Decoder__decoderNonfatal() : $.$get$Utf8Decoder__decoder(); + if (decoder == null) + return null; + if (0 === start && end === codeUnits.length) + return A.Utf8Decoder__useTextDecoder(decoder, codeUnits); + return A.Utf8Decoder__useTextDecoder(decoder, codeUnits.subarray(start, A.RangeError_checkValidRange(start, end, codeUnits.length))); + }, + Utf8Decoder__useTextDecoder(decoder, codeUnits) { + var t1, exception; + try { + t1 = decoder.decode(codeUnits); + return t1; + } catch (exception) { + } + return null; + }, + Base64Codec__checkPadding(source, sourceIndex, sourceEnd, firstPadding, paddingCount, $length) { + if (B.JSInt_methods.$mod($length, 4) !== 0) + throw A.wrapException(A.FormatException$("Invalid base64 padding, padded length must be multiple of four, is " + $length, source, sourceEnd)); + if (firstPadding + paddingCount !== $length) + throw A.wrapException(A.FormatException$("Invalid base64 padding, '=' not at the end", source, sourceIndex)); + if (paddingCount > 2) + throw A.wrapException(A.FormatException$("Invalid base64 padding, more than two '=' characters", source, sourceIndex)); + }, + JsonUnsupportedObjectError$(unsupportedObject, cause, partialResult) { + return new A.JsonUnsupportedObjectError(unsupportedObject, cause); + }, + _defaultToEncodable(object) { + return object.toJson$0(); + }, + _JsonStringStringifier$(_sink, _toEncodable) { + return new A._JsonStringStringifier(_sink, [], A.convert___defaultToEncodable$closure()); + }, + _JsonStringStringifier_stringify(object, toEncodable, indent) { + var t1, + output = new A.StringBuffer(""); + A._JsonStringStringifier_printOn(object, output, toEncodable, indent); + t1 = output._contents; + return t1.charCodeAt(0) == 0 ? t1 : t1; + }, + _JsonStringStringifier_printOn(object, output, toEncodable, indent) { + var stringifier = A._JsonStringStringifier$(output, toEncodable); + stringifier.writeObject$1(object); + }, + _Utf8Decoder_errorDescription(state) { + switch (state) { + case 65: + return "Missing extension byte"; + case 67: + return "Unexpected extension byte"; + case 69: + return "Invalid UTF-8 byte"; + case 71: + return "Overlong encoding"; + case 73: + return "Out of unicode range"; + case 75: + return "Encoded surrogate"; + case 77: + return "Unfinished UTF-8 octet sequence"; + default: + return ""; + } + }, + _Utf8Decoder__makeUint8List(codeUnits, start, end) { + var t1, i, b, + $length = end - start, + bytes = new Uint8Array($length); + for (t1 = J.getInterceptor$asx(codeUnits), i = 0; i < $length; ++i) { + b = t1.$index(codeUnits, start + i); + if ((b & 4294967040) >>> 0 !== 0) + b = 255; + if (!(i < $length)) + return A.ioore(bytes, i); + bytes[i] = b; + } + return bytes; + }, + _JsonMap: function _JsonMap(t0, t1) { + this._original = t0; + this._processed = t1; + this._data = null; + }, + _JsonMapKeyIterable: function _JsonMapKeyIterable(t0) { + this._convert$_parent = t0; + }, + Utf8Decoder__decoder_closure: function Utf8Decoder__decoder_closure() { + }, + Utf8Decoder__decoderNonfatal_closure: function Utf8Decoder__decoderNonfatal_closure() { + }, + AsciiCodec: function AsciiCodec() { + }, + _UnicodeSubsetEncoder: function _UnicodeSubsetEncoder() { + }, + AsciiEncoder: function AsciiEncoder(t0) { + this._subsetMask = t0; + }, + Base64Codec: function Base64Codec() { + }, + Base64Encoder: function Base64Encoder() { + }, + Codec: function Codec() { + }, + _FusedCodec: function _FusedCodec(t0, t1, t2) { + this._convert$_first = t0; + this._second = t1; + this.$ti = t2; + }, + Converter: function Converter() { + }, + Encoding: function Encoding() { + }, + JsonUnsupportedObjectError: function JsonUnsupportedObjectError(t0, t1) { + this.unsupportedObject = t0; + this.cause = t1; + }, + JsonCyclicError: function JsonCyclicError(t0, t1) { + this.unsupportedObject = t0; + this.cause = t1; + }, + JsonCodec: function JsonCodec() { + }, + JsonEncoder: function JsonEncoder(t0) { + this._toEncodable = t0; + }, + JsonDecoder: function JsonDecoder(t0) { + this._reviver = t0; + }, + _JsonStringifier: function _JsonStringifier() { + }, + _JsonStringifier_writeMap_closure: function _JsonStringifier_writeMap_closure(t0, t1) { + this._box_0 = t0; + this.keyValueList = t1; + }, + _JsonStringStringifier: function _JsonStringStringifier(t0, t1, t2) { + this._sink = t0; + this._seen = t1; + this._toEncodable = t2; + }, + Utf8Codec: function Utf8Codec() { + }, + Utf8Encoder: function Utf8Encoder() { + }, + _Utf8Encoder: function _Utf8Encoder(t0) { + this._bufferIndex = this._carry = 0; + this._buffer = t0; + }, + Utf8Decoder: function Utf8Decoder(t0) { + this._allowMalformed = t0; + }, + _Utf8Decoder: function _Utf8Decoder(t0) { + this.allowMalformed = t0; + this._state = 16; + this._charOrIndex = 0; + }, + int_parse(source, radix) { + var value = A.Primitives_parseInt(source, radix); + if (value != null) + return value; + throw A.wrapException(A.FormatException$(source, null, null)); + }, + Error__throw(error, stackTrace) { + error = A.wrapException(error); + if (error == null) + error = type$.Object._as(error); + error.stack = stackTrace.toString$0(0); + throw error; + throw A.wrapException("unreachable"); + }, + List_List$filled($length, fill, growable, $E) { + var i, + result = growable ? J.JSArray_JSArray$growable($length, $E) : J.JSArray_JSArray$fixed($length, $E); + if ($length !== 0 && fill != null) + for (i = 0; i < result.length; ++i) + result[i] = fill; + return result; + }, + List_List$from(elements, growable, $E) { + var t1, + list = A._setArrayType([], $E._eval$1("JSArray<0>")); + for (t1 = J.get$iterator$ax(elements); t1.moveNext$0();) + B.JSArray_methods.add$1(list, $E._as(t1.get$current(t1))); + if (growable) + return list; + return J.JSArray_markFixedList(list, $E); + }, + List_List$of(elements, growable, $E) { + var t1; + if (growable) + return A.List_List$_of(elements, $E); + t1 = J.JSArray_markFixedList(A.List_List$_of(elements, $E), $E); + return t1; + }, + List_List$_of(elements, $E) { + var list, t1; + if (Array.isArray(elements)) + return A._setArrayType(elements.slice(0), $E._eval$1("JSArray<0>")); + list = A._setArrayType([], $E._eval$1("JSArray<0>")); + for (t1 = J.get$iterator$ax(elements); t1.moveNext$0();) + B.JSArray_methods.add$1(list, t1.get$current(t1)); + return list; + }, + List_List$unmodifiable(elements, $E) { + return J.JSArray_markUnmodifiableList(A.List_List$from(elements, false, $E)); + }, + String_String$fromCharCodes(charCodes, start, end) { + var array, len; + if (Array.isArray(charCodes)) { + array = charCodes; + len = array.length; + end = A.RangeError_checkValidRange(start, end, len); + return A.Primitives_stringFromCharCodes(start > 0 || end < len ? array.slice(start, end) : array); + } + if (type$.NativeUint8List._is(charCodes)) + return A.Primitives_stringFromNativeUint8List(charCodes, start, A.RangeError_checkValidRange(start, end, charCodes.length)); + return A.String__stringFromIterable(charCodes, start, end); + }, + String_String$fromCharCode(charCode) { + return A.Primitives_stringFromCharCode(charCode); + }, + String__stringFromIterable(charCodes, start, end) { + var t1, it, i, list, _null = null; + if (start < 0) + throw A.wrapException(A.RangeError$range(start, 0, J.get$length$asx(charCodes), _null, _null)); + t1 = end == null; + if (!t1 && end < start) + throw A.wrapException(A.RangeError$range(end, start, J.get$length$asx(charCodes), _null, _null)); + it = J.get$iterator$ax(charCodes); + for (i = 0; i < start; ++i) + if (!it.moveNext$0()) + throw A.wrapException(A.RangeError$range(start, 0, i, _null, _null)); + list = []; + if (t1) + for (; it.moveNext$0();) + list.push(it.get$current(it)); + else + for (i = start; i < end; ++i) { + if (!it.moveNext$0()) + throw A.wrapException(A.RangeError$range(end, start, i, _null, _null)); + list.push(it.get$current(it)); + } + return A.Primitives_stringFromCharCodes(list); + }, + RegExp_RegExp(source, multiLine) { + return new A.JSSyntaxRegExp(source, A.JSSyntaxRegExp_makeNative(source, multiLine, true, false, false, false)); + }, + StringBuffer__writeAll(string, objects, separator) { + var iterator = J.get$iterator$ax(objects); + if (!iterator.moveNext$0()) + return string; + if (separator.length === 0) { + do + string += A.S(iterator.get$current(iterator)); + while (iterator.moveNext$0()); + } else { + string += A.S(iterator.get$current(iterator)); + for (; iterator.moveNext$0();) + string = string + separator + A.S(iterator.get$current(iterator)); + } + return string; + }, + NoSuchMethodError_NoSuchMethodError$withInvocation(receiver, invocation) { + return new A.NoSuchMethodError(receiver, invocation.get$memberName(), invocation.get$positionalArguments(), invocation.get$namedArguments()); + }, + Uri_base() { + var cachedUri, uri, + current = A.Primitives_currentUri(); + if (current == null) + throw A.wrapException(A.UnsupportedError$("'Uri.base' is not supported")); + cachedUri = $.Uri__cachedBaseUri; + if (cachedUri != null && current === $.Uri__cachedBaseString) + return cachedUri; + uri = A.Uri_parse(current); + $.Uri__cachedBaseUri = uri; + $.Uri__cachedBaseString = current; + return uri; + }, + _Uri__uriEncode(canonicalTable, text, encoding, spaceToPlus) { + var t1, bytes, i, t2, byte, t3, + _s16_ = "0123456789ABCDEF"; + if (encoding === B.C_Utf8Codec) { + t1 = $.$get$_Uri__needsNoEncoding(); + t1 = t1._nativeRegExp.test(text); + } else + t1 = false; + if (t1) + return text; + bytes = B.C_Utf8Encoder.convert$1(text); + for (t1 = bytes.length, i = 0, t2 = ""; i < t1; ++i) { + byte = bytes[i]; + if (byte < 128) { + t3 = byte >>> 4; + if (!(t3 < 8)) + return A.ioore(canonicalTable, t3); + t3 = (canonicalTable[t3] & 1 << (byte & 15)) !== 0; + } else + t3 = false; + if (t3) + t2 += A.Primitives_stringFromCharCode(byte); + else + t2 = spaceToPlus && byte === 32 ? t2 + "+" : t2 + "%" + _s16_[byte >>> 4 & 15] + _s16_[byte & 15]; + } + return t2.charCodeAt(0) == 0 ? t2 : t2; + }, + DateTime__fourDigits(n) { + var absN = Math.abs(n), + sign = n < 0 ? "-" : ""; + if (absN >= 1000) + return "" + n; + if (absN >= 100) + return sign + "0" + absN; + if (absN >= 10) + return sign + "00" + absN; + return sign + "000" + absN; + }, + DateTime__threeDigits(n) { + if (n >= 100) + return "" + n; + if (n >= 10) + return "0" + n; + return "00" + n; + }, + DateTime__twoDigits(n) { + if (n >= 10) + return "" + n; + return "0" + n; + }, + Error_safeToString(object) { + if (typeof object == "number" || A._isBool(object) || object == null) + return J.toString$0$(object); + if (typeof object == "string") + return JSON.stringify(object); + return A.Primitives_safeToString(object); + }, + Error_throwWithStackTrace(error, stackTrace) { + A.checkNotNullable(error, "error", type$.Object); + A.checkNotNullable(stackTrace, "stackTrace", type$.StackTrace); + A.Error__throw(error, stackTrace); + }, + AssertionError$(message) { + return new A.AssertionError(message); + }, + ArgumentError$(message, $name) { + return new A.ArgumentError(false, null, $name, message); + }, + ArgumentError$value(value, $name, message) { + return new A.ArgumentError(true, value, $name, message); + }, + ArgumentError_checkNotNull(argument, $name, $T) { + return argument; + }, + RangeError$value(value, $name) { + return new A.RangeError(null, null, true, value, $name, "Value not in range"); + }, + RangeError$range(invalidValue, minValue, maxValue, $name, message) { + return new A.RangeError(minValue, maxValue, true, invalidValue, $name, "Invalid value"); + }, + RangeError_checkValueInInterval(value, minValue, maxValue, $name) { + if (value < minValue || value > maxValue) + throw A.wrapException(A.RangeError$range(value, minValue, maxValue, $name, null)); + return value; + }, + RangeError_checkValidRange(start, end, $length) { + if (0 > start || start > $length) + throw A.wrapException(A.RangeError$range(start, 0, $length, "start", null)); + if (end != null) { + if (start > end || end > $length) + throw A.wrapException(A.RangeError$range(end, start, $length, "end", null)); + return end; + } + return $length; + }, + RangeError_checkNotNegative(value, $name) { + if (value < 0) + throw A.wrapException(A.RangeError$range(value, 0, null, $name, null)); + return value; + }, + IndexError$withLength(invalidValue, $length, indexable, $name) { + return new A.IndexError($length, true, invalidValue, $name, "Index out of range"); + }, + UnsupportedError$(message) { + return new A.UnsupportedError(message); + }, + UnimplementedError$(message) { + return new A.UnimplementedError(message); + }, + StateError$(message) { + return new A.StateError(message); + }, + ConcurrentModificationError$(modifiedObject) { + return new A.ConcurrentModificationError(modifiedObject); + }, + FormatException$(message, source, offset) { + return new A.FormatException(message, source, offset); + }, + Iterable_iterableToShortString(iterable, leftDelimiter, rightDelimiter) { + var parts, t1; + if (A.isToStringVisiting(iterable)) { + if (leftDelimiter === "(" && rightDelimiter === ")") + return "(...)"; + return leftDelimiter + "..." + rightDelimiter; + } + parts = A._setArrayType([], type$.JSArray_String); + B.JSArray_methods.add$1($.toStringVisiting, iterable); + try { + A._iterablePartsToStrings(iterable, parts); + } finally { + if (0 >= $.toStringVisiting.length) + return A.ioore($.toStringVisiting, -1); + $.toStringVisiting.pop(); + } + t1 = A.StringBuffer__writeAll(leftDelimiter, type$.Iterable_dynamic._as(parts), ", ") + rightDelimiter; + return t1.charCodeAt(0) == 0 ? t1 : t1; + }, + Iterable_iterableToFullString(iterable, leftDelimiter, rightDelimiter) { + var buffer, t1; + if (A.isToStringVisiting(iterable)) + return leftDelimiter + "..." + rightDelimiter; + buffer = new A.StringBuffer(leftDelimiter); + B.JSArray_methods.add$1($.toStringVisiting, iterable); + try { + t1 = buffer; + t1._contents = A.StringBuffer__writeAll(t1._contents, iterable, ", "); + } finally { + if (0 >= $.toStringVisiting.length) + return A.ioore($.toStringVisiting, -1); + $.toStringVisiting.pop(); + } + buffer._contents += rightDelimiter; + t1 = buffer._contents; + return t1.charCodeAt(0) == 0 ? t1 : t1; + }, + _iterablePartsToStrings(iterable, parts) { + var next, ultimateString, penultimateString, penultimate, ultimate, ultimate0, elision, + it = iterable.get$iterator(iterable), + $length = 0, count = 0; + while (true) { + if (!($length < 80 || count < 3)) + break; + if (!it.moveNext$0()) + return; + next = A.S(it.get$current(it)); + B.JSArray_methods.add$1(parts, next); + $length += next.length + 2; + ++count; + } + if (!it.moveNext$0()) { + if (count <= 5) + return; + if (0 >= parts.length) + return A.ioore(parts, -1); + ultimateString = parts.pop(); + if (0 >= parts.length) + return A.ioore(parts, -1); + penultimateString = parts.pop(); + } else { + penultimate = it.get$current(it); + ++count; + if (!it.moveNext$0()) { + if (count <= 4) { + B.JSArray_methods.add$1(parts, A.S(penultimate)); + return; + } + ultimateString = A.S(penultimate); + if (0 >= parts.length) + return A.ioore(parts, -1); + penultimateString = parts.pop(); + $length += ultimateString.length + 2; + } else { + ultimate = it.get$current(it); + ++count; + for (; it.moveNext$0(); penultimate = ultimate, ultimate = ultimate0) { + ultimate0 = it.get$current(it); + ++count; + if (count > 100) { + while (true) { + if (!($length > 75 && count > 3)) + break; + if (0 >= parts.length) + return A.ioore(parts, -1); + $length -= parts.pop().length + 2; + --count; + } + B.JSArray_methods.add$1(parts, "..."); + return; + } + } + penultimateString = A.S(penultimate); + ultimateString = A.S(ultimate); + $length += ultimateString.length + penultimateString.length + 4; + } + } + if (count > parts.length + 2) { + $length += 5; + elision = "..."; + } else + elision = null; + while (true) { + if (!($length > 80 && parts.length > 3)) + break; + if (0 >= parts.length) + return A.ioore(parts, -1); + $length -= parts.pop().length + 2; + if (elision == null) { + $length += 5; + elision = "..."; + } + } + if (elision != null) + B.JSArray_methods.add$1(parts, elision); + B.JSArray_methods.add$1(parts, penultimateString); + B.JSArray_methods.add$1(parts, ultimateString); + }, + Object_hash(object1, object2, object3, object4) { + var t1; + if (B.C_SentinelValue === object3) { + t1 = J.get$hashCode$(object1); + object2 = J.get$hashCode$(object2); + return A.SystemHash_finish(A.SystemHash_combine(A.SystemHash_combine($.$get$_hashSeed(), t1), object2)); + } + if (B.C_SentinelValue === object4) { + t1 = J.get$hashCode$(object1); + object2 = J.get$hashCode$(object2); + object3 = J.get$hashCode$(object3); + return A.SystemHash_finish(A.SystemHash_combine(A.SystemHash_combine(A.SystemHash_combine($.$get$_hashSeed(), t1), object2), object3)); + } + t1 = J.get$hashCode$(object1); + object2 = J.get$hashCode$(object2); + object3 = J.get$hashCode$(object3); + object4 = J.get$hashCode$(object4); + object4 = A.SystemHash_finish(A.SystemHash_combine(A.SystemHash_combine(A.SystemHash_combine(A.SystemHash_combine($.$get$_hashSeed(), t1), object2), object3), object4)); + return object4; + }, + Uri_Uri$dataFromString($content) { + var t1, _null = null, + buffer = new A.StringBuffer(""), + indices = A._setArrayType([-1], type$.JSArray_int); + A.UriData__writeUri(_null, _null, _null, buffer, indices); + B.JSArray_methods.add$1(indices, buffer._contents.length); + buffer._contents += ","; + A.UriData__uriEncodeBytes(B.List_oFp, B.C_AsciiCodec.encode$1($content), buffer); + t1 = buffer._contents; + return new A.UriData(t1.charCodeAt(0) == 0 ? t1 : t1, indices, _null).get$uri(); + }, + Uri_parse(uri) { + var delta, indices, schemeEnd, hostStart, portStart, pathStart, queryStart, fragmentStart, isSimple, scheme, t1, t2, schemeAuth, queryStart0, pathStart0, userInfoStart, userInfo, host, portNumber, port, path, query, _null = null, + end = uri.length; + if (end >= 5) { + if (4 >= end) + return A.ioore(uri, 4); + delta = ((uri.charCodeAt(4) ^ 58) * 3 | uri.charCodeAt(0) ^ 100 | uri.charCodeAt(1) ^ 97 | uri.charCodeAt(2) ^ 116 | uri.charCodeAt(3) ^ 97) >>> 0; + if (delta === 0) + return A.UriData__parse(end < end ? B.JSString_methods.substring$2(uri, 0, end) : uri, 5, _null).get$uri(); + else if (delta === 32) + return A.UriData__parse(B.JSString_methods.substring$2(uri, 5, end), 0, _null).get$uri(); + } + indices = A.List_List$filled(8, 0, false, type$.int); + B.JSArray_methods.$indexSet(indices, 0, 0); + B.JSArray_methods.$indexSet(indices, 1, -1); + B.JSArray_methods.$indexSet(indices, 2, -1); + B.JSArray_methods.$indexSet(indices, 7, -1); + B.JSArray_methods.$indexSet(indices, 3, 0); + B.JSArray_methods.$indexSet(indices, 4, 0); + B.JSArray_methods.$indexSet(indices, 5, end); + B.JSArray_methods.$indexSet(indices, 6, end); + if (A._scan(uri, 0, end, 0, indices) >= 14) + B.JSArray_methods.$indexSet(indices, 7, end); + schemeEnd = indices[1]; + if (schemeEnd >= 0) + if (A._scan(uri, 0, schemeEnd, 20, indices) === 20) + indices[7] = schemeEnd; + hostStart = indices[2] + 1; + portStart = indices[3]; + pathStart = indices[4]; + queryStart = indices[5]; + fragmentStart = indices[6]; + if (fragmentStart < queryStart) + queryStart = fragmentStart; + if (pathStart < hostStart) + pathStart = queryStart; + else if (pathStart <= schemeEnd) + pathStart = schemeEnd + 1; + if (portStart < hostStart) + portStart = pathStart; + isSimple = indices[7] < 0; + if (isSimple) + if (hostStart > schemeEnd + 3) { + scheme = _null; + isSimple = false; + } else { + t1 = portStart > 0; + if (t1 && portStart + 1 === pathStart) { + scheme = _null; + isSimple = false; + } else { + if (!B.JSString_methods.startsWith$2(uri, "\\", pathStart)) + if (hostStart > 0) + t2 = B.JSString_methods.startsWith$2(uri, "\\", hostStart - 1) || B.JSString_methods.startsWith$2(uri, "\\", hostStart - 2); + else + t2 = false; + else + t2 = true; + if (t2) { + scheme = _null; + isSimple = false; + } else { + if (!(queryStart < end && queryStart === pathStart + 2 && B.JSString_methods.startsWith$2(uri, "..", pathStart))) + t2 = queryStart > pathStart + 2 && B.JSString_methods.startsWith$2(uri, "/..", queryStart - 3); + else + t2 = true; + if (t2) { + scheme = _null; + isSimple = false; + } else { + if (schemeEnd === 4) + if (B.JSString_methods.startsWith$2(uri, "file", 0)) { + if (hostStart <= 0) { + if (!B.JSString_methods.startsWith$2(uri, "/", pathStart)) { + schemeAuth = "file:///"; + delta = 3; + } else { + schemeAuth = "file://"; + delta = 2; + } + uri = schemeAuth + B.JSString_methods.substring$2(uri, pathStart, end); + schemeEnd -= 0; + t1 = delta - 0; + queryStart += t1; + fragmentStart += t1; + end = uri.length; + hostStart = 7; + portStart = 7; + pathStart = 7; + } else if (pathStart === queryStart) { + ++fragmentStart; + queryStart0 = queryStart + 1; + uri = B.JSString_methods.replaceRange$3(uri, pathStart, queryStart, "/"); + ++end; + queryStart = queryStart0; + } + scheme = "file"; + } else if (B.JSString_methods.startsWith$2(uri, "http", 0)) { + if (t1 && portStart + 3 === pathStart && B.JSString_methods.startsWith$2(uri, "80", portStart + 1)) { + fragmentStart -= 3; + pathStart0 = pathStart - 3; + queryStart -= 3; + uri = B.JSString_methods.replaceRange$3(uri, portStart, pathStart, ""); + end -= 3; + pathStart = pathStart0; + } + scheme = "http"; + } else + scheme = _null; + else if (schemeEnd === 5 && B.JSString_methods.startsWith$2(uri, "https", 0)) { + if (t1 && portStart + 4 === pathStart && B.JSString_methods.startsWith$2(uri, "443", portStart + 1)) { + fragmentStart -= 4; + pathStart0 = pathStart - 4; + queryStart -= 4; + uri = B.JSString_methods.replaceRange$3(uri, portStart, pathStart, ""); + end -= 3; + pathStart = pathStart0; + } + scheme = "https"; + } else + scheme = _null; + isSimple = true; + } + } + } + } + else + scheme = _null; + if (isSimple) { + if (end < uri.length) { + uri = B.JSString_methods.substring$2(uri, 0, end); + schemeEnd -= 0; + hostStart -= 0; + portStart -= 0; + pathStart -= 0; + queryStart -= 0; + fragmentStart -= 0; + } + return new A._SimpleUri(uri, schemeEnd, hostStart, portStart, pathStart, queryStart, fragmentStart, scheme); + } + if (scheme == null) + if (schemeEnd > 0) + scheme = A._Uri__makeScheme(uri, 0, schemeEnd); + else { + if (schemeEnd === 0) + A._Uri__fail(uri, 0, "Invalid empty scheme"); + scheme = ""; + } + if (hostStart > 0) { + userInfoStart = schemeEnd + 3; + userInfo = userInfoStart < hostStart ? A._Uri__makeUserInfo(uri, userInfoStart, hostStart - 1) : ""; + host = A._Uri__makeHost(uri, hostStart, portStart, false); + t1 = portStart + 1; + if (t1 < pathStart) { + portNumber = A.Primitives_parseInt(B.JSString_methods.substring$2(uri, t1, pathStart), _null); + port = A._Uri__makePort(portNumber == null ? A.throwExpression(A.FormatException$("Invalid port", uri, t1)) : portNumber, scheme); + } else + port = _null; + } else { + port = _null; + host = port; + userInfo = ""; + } + path = A._Uri__makePath(uri, pathStart, queryStart, _null, scheme, host != null); + query = queryStart < fragmentStart ? A._Uri__makeQuery(uri, queryStart + 1, fragmentStart, _null) : _null; + return A._Uri$_internal(scheme, userInfo, host, port, path, query, fragmentStart < end ? A._Uri__makeFragment(uri, fragmentStart + 1, end) : _null); + }, + Uri_decodeComponent(encodedComponent) { + A._asString(encodedComponent); + return A._Uri__uriDecode(encodedComponent, 0, encodedComponent.length, B.C_Utf8Codec, false); + }, + Uri_splitQueryString(query) { + var t1 = type$.String; + return B.JSArray_methods.fold$1$2(A._setArrayType(query.split("&"), type$.JSArray_String), A.LinkedHashMap_LinkedHashMap$_empty(t1, t1), new A.Uri_splitQueryString_closure(B.C_Utf8Codec), type$.Map_String_String); + }, + Uri__parseIPv4Address(host, start, end) { + var t1, i, partStart, partIndex, char, part, partIndex0, + _s43_ = "IPv4 address should contain exactly 4 parts", + _s37_ = "each part must be in the range 0..255", + error = new A.Uri__parseIPv4Address_error(host), + result = new Uint8Array(4); + for (t1 = host.length, i = start, partStart = i, partIndex = 0; i < end; ++i) { + if (!(i >= 0 && i < t1)) + return A.ioore(host, i); + char = host.charCodeAt(i); + if (char !== 46) { + if ((char ^ 48) > 9) + error.call$2("invalid character", i); + } else { + if (partIndex === 3) + error.call$2(_s43_, i); + part = A.int_parse(B.JSString_methods.substring$2(host, partStart, i), null); + if (part > 255) + error.call$2(_s37_, partStart); + partIndex0 = partIndex + 1; + if (!(partIndex < 4)) + return A.ioore(result, partIndex); + result[partIndex] = part; + partStart = i + 1; + partIndex = partIndex0; + } + } + if (partIndex !== 3) + error.call$2(_s43_, end); + part = A.int_parse(B.JSString_methods.substring$2(host, partStart, end), null); + if (part > 255) + error.call$2(_s37_, partStart); + if (!(partIndex < 4)) + return A.ioore(result, partIndex); + result[partIndex] = part; + return result; + }, + Uri_parseIPv6Address(host, start, end) { + var parts, i, partStart, wildcardSeen, seenDot, char, atEnd, last, bytes, wildCardLength, index, value, j, t2, _null = null, + error = new A.Uri_parseIPv6Address_error(host), + parseHex = new A.Uri_parseIPv6Address_parseHex(error, host), + t1 = host.length; + if (t1 < 2) + error.call$2("address is too short", _null); + parts = A._setArrayType([], type$.JSArray_int); + for (i = start, partStart = i, wildcardSeen = false, seenDot = false; i < end; ++i) { + if (!(i >= 0 && i < t1)) + return A.ioore(host, i); + char = host.charCodeAt(i); + if (char === 58) { + if (i === start) { + ++i; + if (!(i < t1)) + return A.ioore(host, i); + if (host.charCodeAt(i) !== 58) + error.call$2("invalid start colon.", i); + partStart = i; + } + if (i === partStart) { + if (wildcardSeen) + error.call$2("only one wildcard `::` is allowed", i); + B.JSArray_methods.add$1(parts, -1); + wildcardSeen = true; + } else + B.JSArray_methods.add$1(parts, parseHex.call$2(partStart, i)); + partStart = i + 1; + } else if (char === 46) + seenDot = true; + } + if (parts.length === 0) + error.call$2("too few parts", _null); + atEnd = partStart === end; + t1 = B.JSArray_methods.get$last(parts); + if (atEnd && t1 !== -1) + error.call$2("expected a part after last `:`", end); + if (!atEnd) + if (!seenDot) + B.JSArray_methods.add$1(parts, parseHex.call$2(partStart, end)); + else { + last = A.Uri__parseIPv4Address(host, partStart, end); + B.JSArray_methods.add$1(parts, (last[0] << 8 | last[1]) >>> 0); + B.JSArray_methods.add$1(parts, (last[2] << 8 | last[3]) >>> 0); + } + if (wildcardSeen) { + if (parts.length > 7) + error.call$2("an address with a wildcard must have less than 7 parts", _null); + } else if (parts.length !== 8) + error.call$2("an address without a wildcard must contain exactly 8 parts", _null); + bytes = new Uint8Array(16); + for (t1 = parts.length, wildCardLength = 9 - t1, i = 0, index = 0; i < t1; ++i) { + value = parts[i]; + if (value === -1) + for (j = 0; j < wildCardLength; ++j) { + if (!(index >= 0 && index < 16)) + return A.ioore(bytes, index); + bytes[index] = 0; + t2 = index + 1; + if (!(t2 < 16)) + return A.ioore(bytes, t2); + bytes[t2] = 0; + index += 2; + } + else { + t2 = B.JSInt_methods._shrOtherPositive$1(value, 8); + if (!(index >= 0 && index < 16)) + return A.ioore(bytes, index); + bytes[index] = t2; + t2 = index + 1; + if (!(t2 < 16)) + return A.ioore(bytes, t2); + bytes[t2] = value & 255; + index += 2; + } + } + return bytes; + }, + _Uri$_internal(scheme, _userInfo, _host, _port, path, _query, _fragment) { + return new A._Uri(scheme, _userInfo, _host, _port, path, _query, _fragment); + }, + _Uri__Uri(host, path, pathSegments, scheme) { + var userInfo, query, fragment, port, isFile, t1, hasAuthority, t2, _null = null; + scheme = scheme == null ? "" : A._Uri__makeScheme(scheme, 0, scheme.length); + userInfo = A._Uri__makeUserInfo(_null, 0, 0); + host = A._Uri__makeHost(host, 0, host == null ? 0 : host.length, false); + query = A._Uri__makeQuery(_null, 0, 0, _null); + fragment = A._Uri__makeFragment(_null, 0, 0); + port = A._Uri__makePort(_null, scheme); + isFile = scheme === "file"; + if (host == null) + t1 = userInfo.length !== 0 || port != null || isFile; + else + t1 = false; + if (t1) + host = ""; + t1 = host == null; + hasAuthority = !t1; + path = A._Uri__makePath(path, 0, path == null ? 0 : path.length, pathSegments, scheme, hasAuthority); + t2 = scheme.length === 0; + if (t2 && t1 && !B.JSString_methods.startsWith$1(path, "/")) + path = A._Uri__normalizeRelativePath(path, !t2 || hasAuthority); + else + path = A._Uri__removeDotSegments(path); + return A._Uri$_internal(scheme, userInfo, t1 && B.JSString_methods.startsWith$1(path, "//") ? "" : host, port, path, query, fragment); + }, + _Uri__defaultPort(scheme) { + if (scheme === "http") + return 80; + if (scheme === "https") + return 443; + return 0; + }, + _Uri__fail(uri, index, message) { + throw A.wrapException(A.FormatException$(message, uri, index)); + }, + _Uri__Uri$file(path, windows) { + return windows ? A._Uri__makeWindowsFileUrl(path, false) : A._Uri__makeFileUri(path, false); + }, + _Uri__checkNonWindowsPathReservedCharacters(segments, argumentError) { + var t1, _i, segment; + for (t1 = segments.length, _i = 0; _i < t1; ++_i) { + segment = segments[_i]; + if (J.contains$1$asx(segment, "/")) { + t1 = A.UnsupportedError$("Illegal path character " + A.S(segment)); + throw A.wrapException(t1); + } + } + }, + _Uri__checkWindowsPathReservedCharacters(segments, argumentError, firstSegment) { + var t1, t2, t3; + for (t1 = A.SubListIterable$(segments, firstSegment, null, A._arrayInstanceType(segments)._precomputed1), t2 = t1.$ti, t1 = new A.ListIterator(t1, t1.get$length(t1), t2._eval$1("ListIterator<ListIterable.E>")), t2 = t2._eval$1("ListIterable.E"); t1.moveNext$0();) { + t3 = t1.__internal$_current; + if (t3 == null) + t3 = t2._as(t3); + if (B.JSString_methods.contains$1(t3, A.RegExp_RegExp('["*/:<>?\\\\|]', false))) + if (argumentError) + throw A.wrapException(A.ArgumentError$("Illegal character in path", null)); + else + throw A.wrapException(A.UnsupportedError$("Illegal character in path: " + t3)); + } + }, + _Uri__checkWindowsDriveLetter(charCode, argumentError) { + var t1, + _s21_ = "Illegal drive letter "; + if (!(65 <= charCode && charCode <= 90)) + t1 = 97 <= charCode && charCode <= 122; + else + t1 = true; + if (t1) + return; + if (argumentError) + throw A.wrapException(A.ArgumentError$(_s21_ + A.String_String$fromCharCode(charCode), null)); + else + throw A.wrapException(A.UnsupportedError$(_s21_ + A.String_String$fromCharCode(charCode))); + }, + _Uri__makeFileUri(path, slashTerminated) { + var _null = null, + segments = A._setArrayType(path.split("/"), type$.JSArray_String); + if (B.JSString_methods.startsWith$1(path, "/")) + return A._Uri__Uri(_null, _null, segments, "file"); + else + return A._Uri__Uri(_null, _null, segments, _null); + }, + _Uri__makeWindowsFileUrl(path, slashTerminated) { + var t1, pathSegments, pathStart, hostPart, _s1_ = "\\", _null = null, _s4_ = "file"; + if (B.JSString_methods.startsWith$1(path, "\\\\?\\")) + if (B.JSString_methods.startsWith$2(path, "UNC\\", 4)) + path = B.JSString_methods.replaceRange$3(path, 0, 7, _s1_); + else { + path = B.JSString_methods.substring$1(path, 4); + t1 = path.length; + if (t1 >= 3) { + if (1 >= t1) + return A.ioore(path, 1); + if (path.charCodeAt(1) === 58) { + if (2 >= t1) + return A.ioore(path, 2); + t1 = path.charCodeAt(2) !== 92; + } else + t1 = true; + } else + t1 = true; + if (t1) + throw A.wrapException(A.ArgumentError$value(path, "path", "Windows paths with \\\\?\\ prefix must be absolute")); + } + else + path = A.stringReplaceAllUnchecked(path, "/", _s1_); + t1 = path.length; + if (t1 > 1 && path.charCodeAt(1) === 58) { + if (0 >= t1) + return A.ioore(path, 0); + A._Uri__checkWindowsDriveLetter(path.charCodeAt(0), true); + if (t1 !== 2) { + if (2 >= t1) + return A.ioore(path, 2); + t1 = path.charCodeAt(2) !== 92; + } else + t1 = true; + if (t1) + throw A.wrapException(A.ArgumentError$value(path, "path", "Windows paths with drive letter must be absolute")); + pathSegments = A._setArrayType(path.split(_s1_), type$.JSArray_String); + A._Uri__checkWindowsPathReservedCharacters(pathSegments, true, 1); + return A._Uri__Uri(_null, _null, pathSegments, _s4_); + } + if (B.JSString_methods.startsWith$1(path, _s1_)) + if (B.JSString_methods.startsWith$2(path, _s1_, 1)) { + pathStart = B.JSString_methods.indexOf$2(path, _s1_, 2); + t1 = pathStart < 0; + hostPart = t1 ? B.JSString_methods.substring$1(path, 2) : B.JSString_methods.substring$2(path, 2, pathStart); + pathSegments = A._setArrayType((t1 ? "" : B.JSString_methods.substring$1(path, pathStart + 1)).split(_s1_), type$.JSArray_String); + A._Uri__checkWindowsPathReservedCharacters(pathSegments, true, 0); + return A._Uri__Uri(hostPart, _null, pathSegments, _s4_); + } else { + pathSegments = A._setArrayType(path.split(_s1_), type$.JSArray_String); + A._Uri__checkWindowsPathReservedCharacters(pathSegments, true, 0); + return A._Uri__Uri(_null, _null, pathSegments, _s4_); + } + else { + pathSegments = A._setArrayType(path.split(_s1_), type$.JSArray_String); + A._Uri__checkWindowsPathReservedCharacters(pathSegments, true, 0); + return A._Uri__Uri(_null, _null, pathSegments, _null); + } + }, + _Uri__makePort(port, scheme) { + if (port != null && port === A._Uri__defaultPort(scheme)) + return null; + return port; + }, + _Uri__makeHost(host, start, end, strictIPv6) { + var t1, t2, index, zoneIDstart, zoneID, i; + if (host == null) + return null; + if (start === end) + return ""; + t1 = host.length; + if (!(start >= 0 && start < t1)) + return A.ioore(host, start); + if (host.charCodeAt(start) === 91) { + t2 = end - 1; + if (!(t2 >= 0 && t2 < t1)) + return A.ioore(host, t2); + if (host.charCodeAt(t2) !== 93) + A._Uri__fail(host, start, "Missing end `]` to match `[` in host"); + t1 = start + 1; + index = A._Uri__checkZoneID(host, t1, t2); + if (index < t2) { + zoneIDstart = index + 1; + zoneID = A._Uri__normalizeZoneID(host, B.JSString_methods.startsWith$2(host, "25", zoneIDstart) ? index + 3 : zoneIDstart, t2, "%25"); + } else + zoneID = ""; + A.Uri_parseIPv6Address(host, t1, index); + return B.JSString_methods.substring$2(host, start, index).toLowerCase() + zoneID + "]"; + } + for (i = start; i < end; ++i) { + if (!(i < t1)) + return A.ioore(host, i); + if (host.charCodeAt(i) === 58) { + index = B.JSString_methods.indexOf$2(host, "%", start); + index = index >= start && index < end ? index : end; + if (index < end) { + zoneIDstart = index + 1; + zoneID = A._Uri__normalizeZoneID(host, B.JSString_methods.startsWith$2(host, "25", zoneIDstart) ? index + 3 : zoneIDstart, end, "%25"); + } else + zoneID = ""; + A.Uri_parseIPv6Address(host, start, index); + return "[" + B.JSString_methods.substring$2(host, start, index) + zoneID + "]"; + } + } + return A._Uri__normalizeRegName(host, start, end); + }, + _Uri__checkZoneID(host, start, end) { + var index = B.JSString_methods.indexOf$2(host, "%", start); + return index >= start && index < end ? index : end; + }, + _Uri__normalizeZoneID(host, start, end, prefix) { + var t1, index, sectionStart, isNormalized, char, replacement, t2, t3, tail, sourceLength, slice, + buffer = prefix !== "" ? new A.StringBuffer(prefix) : null; + for (t1 = host.length, index = start, sectionStart = index, isNormalized = true; index < end;) { + if (!(index >= 0 && index < t1)) + return A.ioore(host, index); + char = host.charCodeAt(index); + if (char === 37) { + replacement = A._Uri__normalizeEscape(host, index, true); + t2 = replacement == null; + if (t2 && isNormalized) { + index += 3; + continue; + } + if (buffer == null) + buffer = new A.StringBuffer(""); + t3 = buffer._contents += B.JSString_methods.substring$2(host, sectionStart, index); + if (t2) + replacement = B.JSString_methods.substring$2(host, index, index + 3); + else if (replacement === "%") + A._Uri__fail(host, index, "ZoneID should not contain % anymore"); + buffer._contents = t3 + replacement; + index += 3; + sectionStart = index; + isNormalized = true; + } else { + if (char < 127) { + t2 = char >>> 4; + if (!(t2 < 8)) + return A.ioore(B.List_M1A, t2); + t2 = (B.List_M1A[t2] & 1 << (char & 15)) !== 0; + } else + t2 = false; + if (t2) { + if (isNormalized && 65 <= char && 90 >= char) { + if (buffer == null) + buffer = new A.StringBuffer(""); + if (sectionStart < index) { + buffer._contents += B.JSString_methods.substring$2(host, sectionStart, index); + sectionStart = index; + } + isNormalized = false; + } + ++index; + } else { + if ((char & 64512) === 55296 && index + 1 < end) { + t2 = index + 1; + if (!(t2 < t1)) + return A.ioore(host, t2); + tail = host.charCodeAt(t2); + if ((tail & 64512) === 56320) { + char = (char & 1023) << 10 | tail & 1023 | 65536; + sourceLength = 2; + } else + sourceLength = 1; + } else + sourceLength = 1; + slice = B.JSString_methods.substring$2(host, sectionStart, index); + if (buffer == null) { + buffer = new A.StringBuffer(""); + t2 = buffer; + } else + t2 = buffer; + t2._contents += slice; + t2._contents += A._Uri__escapeChar(char); + index += sourceLength; + sectionStart = index; + } + } + } + if (buffer == null) + return B.JSString_methods.substring$2(host, start, end); + if (sectionStart < end) + buffer._contents += B.JSString_methods.substring$2(host, sectionStart, end); + t1 = buffer._contents; + return t1.charCodeAt(0) == 0 ? t1 : t1; + }, + _Uri__normalizeRegName(host, start, end) { + var t1, index, sectionStart, buffer, isNormalized, char, replacement, t2, slice, t3, sourceLength, tail; + for (t1 = host.length, index = start, sectionStart = index, buffer = null, isNormalized = true; index < end;) { + if (!(index >= 0 && index < t1)) + return A.ioore(host, index); + char = host.charCodeAt(index); + if (char === 37) { + replacement = A._Uri__normalizeEscape(host, index, true); + t2 = replacement == null; + if (t2 && isNormalized) { + index += 3; + continue; + } + if (buffer == null) + buffer = new A.StringBuffer(""); + slice = B.JSString_methods.substring$2(host, sectionStart, index); + t3 = buffer._contents += !isNormalized ? slice.toLowerCase() : slice; + if (t2) { + replacement = B.JSString_methods.substring$2(host, index, index + 3); + sourceLength = 3; + } else if (replacement === "%") { + replacement = "%25"; + sourceLength = 1; + } else + sourceLength = 3; + buffer._contents = t3 + replacement; + index += sourceLength; + sectionStart = index; + isNormalized = true; + } else { + if (char < 127) { + t2 = char >>> 4; + if (!(t2 < 8)) + return A.ioore(B.List_ejq, t2); + t2 = (B.List_ejq[t2] & 1 << (char & 15)) !== 0; + } else + t2 = false; + if (t2) { + if (isNormalized && 65 <= char && 90 >= char) { + if (buffer == null) + buffer = new A.StringBuffer(""); + if (sectionStart < index) { + buffer._contents += B.JSString_methods.substring$2(host, sectionStart, index); + sectionStart = index; + } + isNormalized = false; + } + ++index; + } else { + if (char <= 93) { + t2 = char >>> 4; + if (!(t2 < 8)) + return A.ioore(B.List_YmH, t2); + t2 = (B.List_YmH[t2] & 1 << (char & 15)) !== 0; + } else + t2 = false; + if (t2) + A._Uri__fail(host, index, "Invalid character"); + else { + if ((char & 64512) === 55296 && index + 1 < end) { + t2 = index + 1; + if (!(t2 < t1)) + return A.ioore(host, t2); + tail = host.charCodeAt(t2); + if ((tail & 64512) === 56320) { + char = (char & 1023) << 10 | tail & 1023 | 65536; + sourceLength = 2; + } else + sourceLength = 1; + } else + sourceLength = 1; + slice = B.JSString_methods.substring$2(host, sectionStart, index); + if (!isNormalized) + slice = slice.toLowerCase(); + if (buffer == null) { + buffer = new A.StringBuffer(""); + t2 = buffer; + } else + t2 = buffer; + t2._contents += slice; + t2._contents += A._Uri__escapeChar(char); + index += sourceLength; + sectionStart = index; + } + } + } + } + if (buffer == null) + return B.JSString_methods.substring$2(host, start, end); + if (sectionStart < end) { + slice = B.JSString_methods.substring$2(host, sectionStart, end); + buffer._contents += !isNormalized ? slice.toLowerCase() : slice; + } + t1 = buffer._contents; + return t1.charCodeAt(0) == 0 ? t1 : t1; + }, + _Uri__makeScheme(scheme, start, end) { + var t1, i, containsUpperCase, codeUnit, t2; + if (start === end) + return ""; + t1 = scheme.length; + if (!(start < t1)) + return A.ioore(scheme, start); + if (!A._Uri__isAlphabeticCharacter(scheme.charCodeAt(start))) + A._Uri__fail(scheme, start, "Scheme not starting with alphabetic character"); + for (i = start, containsUpperCase = false; i < end; ++i) { + if (!(i < t1)) + return A.ioore(scheme, i); + codeUnit = scheme.charCodeAt(i); + if (codeUnit < 128) { + t2 = codeUnit >>> 4; + if (!(t2 < 8)) + return A.ioore(B.List_MMm, t2); + t2 = (B.List_MMm[t2] & 1 << (codeUnit & 15)) !== 0; + } else + t2 = false; + if (!t2) + A._Uri__fail(scheme, i, "Illegal scheme character"); + if (65 <= codeUnit && codeUnit <= 90) + containsUpperCase = true; + } + scheme = B.JSString_methods.substring$2(scheme, start, end); + return A._Uri__canonicalizeScheme(containsUpperCase ? scheme.toLowerCase() : scheme); + }, + _Uri__canonicalizeScheme(scheme) { + if (scheme === "http") + return "http"; + if (scheme === "file") + return "file"; + if (scheme === "https") + return "https"; + if (scheme === "package") + return "package"; + return scheme; + }, + _Uri__makeUserInfo(userInfo, start, end) { + if (userInfo == null) + return ""; + return A._Uri__normalizeOrSubstring(userInfo, start, end, B.List_OL3, false, false); + }, + _Uri__makePath(path, start, end, pathSegments, scheme, hasAuthority) { + var t1, result, + isFile = scheme === "file", + ensureLeadingSlash = isFile || hasAuthority; + if (path == null) { + if (pathSegments == null) + return isFile ? "/" : ""; + t1 = A._arrayInstanceType(pathSegments); + result = new A.MappedListIterable(pathSegments, t1._eval$1("String(1)")._as(new A._Uri__makePath_closure()), t1._eval$1("MappedListIterable<1,String>")).join$1(0, "/"); + } else if (pathSegments != null) + throw A.wrapException(A.ArgumentError$("Both path and pathSegments specified", null)); + else + result = A._Uri__normalizeOrSubstring(path, start, end, B.List_XRg, true, true); + if (result.length === 0) { + if (isFile) + return "/"; + } else if (ensureLeadingSlash && !B.JSString_methods.startsWith$1(result, "/")) + result = "/" + result; + return A._Uri__normalizePath(result, scheme, hasAuthority); + }, + _Uri__normalizePath(path, scheme, hasAuthority) { + var t1 = scheme.length === 0; + if (t1 && !hasAuthority && !B.JSString_methods.startsWith$1(path, "/") && !B.JSString_methods.startsWith$1(path, "\\")) + return A._Uri__normalizeRelativePath(path, !t1 || hasAuthority); + return A._Uri__removeDotSegments(path); + }, + _Uri__makeQuery(query, start, end, queryParameters) { + if (query != null) + return A._Uri__normalizeOrSubstring(query, start, end, B.List_oFp, true, false); + return null; + }, + _Uri__makeFragment(fragment, start, end) { + if (fragment == null) + return null; + return A._Uri__normalizeOrSubstring(fragment, start, end, B.List_oFp, true, false); + }, + _Uri__normalizeEscape(source, index, lowerCase) { + var t3, firstDigit, secondDigit, firstDigitValue, secondDigitValue, value, + t1 = index + 2, + t2 = source.length; + if (t1 >= t2) + return "%"; + t3 = index + 1; + if (!(t3 >= 0 && t3 < t2)) + return A.ioore(source, t3); + firstDigit = source.charCodeAt(t3); + if (!(t1 >= 0)) + return A.ioore(source, t1); + secondDigit = source.charCodeAt(t1); + firstDigitValue = A.hexDigitValue(firstDigit); + secondDigitValue = A.hexDigitValue(secondDigit); + if (firstDigitValue < 0 || secondDigitValue < 0) + return "%"; + value = firstDigitValue * 16 + secondDigitValue; + if (value < 127) { + t1 = B.JSInt_methods._shrOtherPositive$1(value, 4); + if (!(t1 < 8)) + return A.ioore(B.List_M1A, t1); + t1 = (B.List_M1A[t1] & 1 << (value & 15)) !== 0; + } else + t1 = false; + if (t1) + return A.Primitives_stringFromCharCode(lowerCase && 65 <= value && 90 >= value ? (value | 32) >>> 0 : value); + if (firstDigit >= 97 || secondDigit >= 97) + return B.JSString_methods.substring$2(source, index, index + 3).toUpperCase(); + return null; + }, + _Uri__escapeChar(char) { + var codeUnits, t1, flag, encodedBytes, index, byte, t2, t3, + _s16_ = "0123456789ABCDEF"; + if (char < 128) { + codeUnits = new Uint8Array(3); + codeUnits[0] = 37; + t1 = char >>> 4; + if (!(t1 < 16)) + return A.ioore(_s16_, t1); + codeUnits[1] = _s16_.charCodeAt(t1); + codeUnits[2] = _s16_.charCodeAt(char & 15); + } else { + if (char > 2047) + if (char > 65535) { + flag = 240; + encodedBytes = 4; + } else { + flag = 224; + encodedBytes = 3; + } + else { + flag = 192; + encodedBytes = 2; + } + t1 = 3 * encodedBytes; + codeUnits = new Uint8Array(t1); + for (index = 0; --encodedBytes, encodedBytes >= 0; flag = 128) { + byte = B.JSInt_methods._shrReceiverPositive$1(char, 6 * encodedBytes) & 63 | flag; + if (!(index < t1)) + return A.ioore(codeUnits, index); + codeUnits[index] = 37; + t2 = index + 1; + t3 = byte >>> 4; + if (!(t3 < 16)) + return A.ioore(_s16_, t3); + if (!(t2 < t1)) + return A.ioore(codeUnits, t2); + codeUnits[t2] = _s16_.charCodeAt(t3); + t3 = index + 2; + if (!(t3 < t1)) + return A.ioore(codeUnits, t3); + codeUnits[t3] = _s16_.charCodeAt(byte & 15); + index += 3; + } + } + return A.String_String$fromCharCodes(codeUnits, 0, null); + }, + _Uri__normalizeOrSubstring(component, start, end, charTable, escapeDelimiters, replaceBackslash) { + var t1 = A._Uri__normalize(component, start, end, charTable, escapeDelimiters, replaceBackslash); + return t1 == null ? B.JSString_methods.substring$2(component, start, end) : t1; + }, + _Uri__normalize(component, start, end, charTable, escapeDelimiters, replaceBackslash) { + var t1, t2, index, sectionStart, buffer, char, t3, replacement, sourceLength, tail, t4, _null = null; + for (t1 = !escapeDelimiters, t2 = component.length, index = start, sectionStart = index, buffer = _null; index < end;) { + if (!(index >= 0 && index < t2)) + return A.ioore(component, index); + char = component.charCodeAt(index); + if (char < 127) { + t3 = char >>> 4; + if (!(t3 < 8)) + return A.ioore(charTable, t3); + t3 = (charTable[t3] & 1 << (char & 15)) !== 0; + } else + t3 = false; + if (t3) + ++index; + else { + if (char === 37) { + replacement = A._Uri__normalizeEscape(component, index, false); + if (replacement == null) { + index += 3; + continue; + } + if ("%" === replacement) { + replacement = "%25"; + sourceLength = 1; + } else + sourceLength = 3; + } else if (char === 92 && replaceBackslash) { + replacement = "/"; + sourceLength = 1; + } else { + if (t1) + if (char <= 93) { + t3 = char >>> 4; + if (!(t3 < 8)) + return A.ioore(B.List_YmH, t3); + t3 = (B.List_YmH[t3] & 1 << (char & 15)) !== 0; + } else + t3 = false; + else + t3 = false; + if (t3) { + A._Uri__fail(component, index, "Invalid character"); + sourceLength = _null; + replacement = sourceLength; + } else { + if ((char & 64512) === 55296) { + t3 = index + 1; + if (t3 < end) { + if (!(t3 < t2)) + return A.ioore(component, t3); + tail = component.charCodeAt(t3); + if ((tail & 64512) === 56320) { + char = (char & 1023) << 10 | tail & 1023 | 65536; + sourceLength = 2; + } else + sourceLength = 1; + } else + sourceLength = 1; + } else + sourceLength = 1; + replacement = A._Uri__escapeChar(char); + } + } + if (buffer == null) { + buffer = new A.StringBuffer(""); + t3 = buffer; + } else + t3 = buffer; + t4 = t3._contents += B.JSString_methods.substring$2(component, sectionStart, index); + t3._contents = t4 + A.S(replacement); + if (typeof sourceLength !== "number") + return A.iae(sourceLength); + index += sourceLength; + sectionStart = index; + } + } + if (buffer == null) + return _null; + if (sectionStart < end) + buffer._contents += B.JSString_methods.substring$2(component, sectionStart, end); + t1 = buffer._contents; + return t1.charCodeAt(0) == 0 ? t1 : t1; + }, + _Uri__mayContainDotSegments(path) { + if (B.JSString_methods.startsWith$1(path, ".")) + return true; + return B.JSString_methods.indexOf$1(path, "/.") !== -1; + }, + _Uri__removeDotSegments(path) { + var output, t1, t2, appendSlash, _i, segment, t3; + if (!A._Uri__mayContainDotSegments(path)) + return path; + output = A._setArrayType([], type$.JSArray_String); + for (t1 = path.split("/"), t2 = t1.length, appendSlash = false, _i = 0; _i < t2; ++_i) { + segment = t1[_i]; + if (J.$eq$(segment, "..")) { + t3 = output.length; + if (t3 !== 0) { + if (0 >= t3) + return A.ioore(output, -1); + output.pop(); + if (output.length === 0) + B.JSArray_methods.add$1(output, ""); + } + appendSlash = true; + } else if ("." === segment) + appendSlash = true; + else { + B.JSArray_methods.add$1(output, segment); + appendSlash = false; + } + } + if (appendSlash) + B.JSArray_methods.add$1(output, ""); + return B.JSArray_methods.join$1(output, "/"); + }, + _Uri__normalizeRelativePath(path, allowScheme) { + var output, t1, t2, appendSlash, _i, segment; + if (!A._Uri__mayContainDotSegments(path)) + return !allowScheme ? A._Uri__escapeScheme(path) : path; + output = A._setArrayType([], type$.JSArray_String); + for (t1 = path.split("/"), t2 = t1.length, appendSlash = false, _i = 0; _i < t2; ++_i) { + segment = t1[_i]; + if (".." === segment) + if (output.length !== 0 && B.JSArray_methods.get$last(output) !== "..") { + if (0 >= output.length) + return A.ioore(output, -1); + output.pop(); + appendSlash = true; + } else { + B.JSArray_methods.add$1(output, ".."); + appendSlash = false; + } + else if ("." === segment) + appendSlash = true; + else { + B.JSArray_methods.add$1(output, segment); + appendSlash = false; + } + } + t1 = output.length; + if (t1 !== 0) + if (t1 === 1) { + if (0 >= t1) + return A.ioore(output, 0); + t1 = output[0].length === 0; + } else + t1 = false; + else + t1 = true; + if (t1) + return "./"; + if (appendSlash || B.JSArray_methods.get$last(output) === "..") + B.JSArray_methods.add$1(output, ""); + if (!allowScheme) { + if (0 >= output.length) + return A.ioore(output, 0); + B.JSArray_methods.$indexSet(output, 0, A._Uri__escapeScheme(output[0])); + } + return B.JSArray_methods.join$1(output, "/"); + }, + _Uri__escapeScheme(path) { + var i, char, t2, + t1 = path.length; + if (t1 >= 2 && A._Uri__isAlphabeticCharacter(path.charCodeAt(0))) + for (i = 1; i < t1; ++i) { + char = path.charCodeAt(i); + if (char === 58) + return B.JSString_methods.substring$2(path, 0, i) + "%3A" + B.JSString_methods.substring$1(path, i + 1); + if (char <= 127) { + t2 = char >>> 4; + if (!(t2 < 8)) + return A.ioore(B.List_MMm, t2); + t2 = (B.List_MMm[t2] & 1 << (char & 15)) === 0; + } else + t2 = true; + if (t2) + break; + } + return path; + }, + _Uri__packageNameEnd(uri, path) { + if (uri.isScheme$1("package") && uri._host == null) + return A._skipPackageNameChars(path, 0, path.length); + return -1; + }, + _Uri__toWindowsFilePath(uri) { + var hasDriveLetter, t2, host, + segments = uri.get$pathSegments(), + t1 = segments.length; + if (t1 > 0 && J.get$length$asx(segments[0]) === 2 && J.codeUnitAt$1$s(segments[0], 1) === 58) { + if (0 >= t1) + return A.ioore(segments, 0); + A._Uri__checkWindowsDriveLetter(J.codeUnitAt$1$s(segments[0], 0), false); + A._Uri__checkWindowsPathReservedCharacters(segments, false, 1); + hasDriveLetter = true; + } else { + A._Uri__checkWindowsPathReservedCharacters(segments, false, 0); + hasDriveLetter = false; + } + t2 = uri.get$hasAbsolutePath() && !hasDriveLetter ? "" + "\\" : ""; + if (uri.get$hasAuthority()) { + host = uri.get$host(uri); + if (host.length !== 0) + t2 = t2 + "\\" + host + "\\"; + } + t2 = A.StringBuffer__writeAll(t2, segments, "\\"); + t1 = hasDriveLetter && t1 === 1 ? t2 + "\\" : t2; + return t1.charCodeAt(0) == 0 ? t1 : t1; + }, + _Uri__hexCharPairToByte(s, pos) { + var t1, byte, i, t2, charCode; + for (t1 = s.length, byte = 0, i = 0; i < 2; ++i) { + t2 = pos + i; + if (!(t2 < t1)) + return A.ioore(s, t2); + charCode = s.charCodeAt(t2); + if (48 <= charCode && charCode <= 57) + byte = byte * 16 + charCode - 48; + else { + charCode |= 32; + if (97 <= charCode && charCode <= 102) + byte = byte * 16 + charCode - 87; + else + throw A.wrapException(A.ArgumentError$("Invalid URL encoding", null)); + } + } + return byte; + }, + _Uri__uriDecode(text, start, end, encoding, plusToSpace) { + var simple, codeUnit, t2, bytes, + t1 = text.length, + i = start; + while (true) { + if (!(i < end)) { + simple = true; + break; + } + if (!(i < t1)) + return A.ioore(text, i); + codeUnit = text.charCodeAt(i); + if (codeUnit <= 127) + if (codeUnit !== 37) + t2 = plusToSpace && codeUnit === 43; + else + t2 = true; + else + t2 = true; + if (t2) { + simple = false; + break; + } + ++i; + } + if (simple) { + if (B.C_Utf8Codec !== encoding) + t1 = false; + else + t1 = true; + if (t1) + return B.JSString_methods.substring$2(text, start, end); + else + bytes = new A.CodeUnits(B.JSString_methods.substring$2(text, start, end)); + } else { + bytes = A._setArrayType([], type$.JSArray_int); + for (i = start; i < end; ++i) { + if (!(i < t1)) + return A.ioore(text, i); + codeUnit = text.charCodeAt(i); + if (codeUnit > 127) + throw A.wrapException(A.ArgumentError$("Illegal percent encoding in URI", null)); + if (codeUnit === 37) { + if (i + 3 > t1) + throw A.wrapException(A.ArgumentError$("Truncated URI", null)); + B.JSArray_methods.add$1(bytes, A._Uri__hexCharPairToByte(text, i + 1)); + i += 2; + } else if (plusToSpace && codeUnit === 43) + B.JSArray_methods.add$1(bytes, 32); + else + B.JSArray_methods.add$1(bytes, codeUnit); + } + } + type$.List_int._as(bytes); + return B.Utf8Decoder_false.convert$1(bytes); + }, + _Uri__isAlphabeticCharacter(codeUnit) { + var lowerCase = codeUnit | 32; + return 97 <= lowerCase && lowerCase <= 122; + }, + UriData__writeUri(mimeType, charsetName, parameters, buffer, indices) { + var slashIndex, t1; + if (true) + buffer._contents = buffer._contents; + else { + slashIndex = A.UriData__validateMimeType(""); + if (slashIndex < 0) + throw A.wrapException(A.ArgumentError$value("", "mimeType", "Invalid MIME type")); + t1 = buffer._contents += A._Uri__uriEncode(B.List_yzX, B.JSString_methods.substring$2("", 0, slashIndex), B.C_Utf8Codec, false); + buffer._contents = t1 + "/"; + buffer._contents += A._Uri__uriEncode(B.List_yzX, B.JSString_methods.substring$1("", slashIndex + 1), B.C_Utf8Codec, false); + } + }, + UriData__validateMimeType(mimeType) { + var t1, slashIndex, i; + for (t1 = mimeType.length, slashIndex = -1, i = 0; i < t1; ++i) { + if (mimeType.charCodeAt(i) !== 47) + continue; + if (slashIndex < 0) { + slashIndex = i; + continue; + } + return -1; + } + return slashIndex; + }, + UriData__parse(text, start, sourceUri) { + var t1, i, slashIndex, char, equalsIndex, lastSeparator, t2, data, + _s17_ = "Invalid MIME type", + indices = A._setArrayType([start - 1], type$.JSArray_int); + for (t1 = text.length, i = start, slashIndex = -1, char = null; i < t1; ++i) { + char = text.charCodeAt(i); + if (char === 44 || char === 59) + break; + if (char === 47) { + if (slashIndex < 0) { + slashIndex = i; + continue; + } + throw A.wrapException(A.FormatException$(_s17_, text, i)); + } + } + if (slashIndex < 0 && i > start) + throw A.wrapException(A.FormatException$(_s17_, text, i)); + for (; char !== 44;) { + B.JSArray_methods.add$1(indices, i); + ++i; + for (equalsIndex = -1; i < t1; ++i) { + if (!(i >= 0)) + return A.ioore(text, i); + char = text.charCodeAt(i); + if (char === 61) { + if (equalsIndex < 0) + equalsIndex = i; + } else if (char === 59 || char === 44) + break; + } + if (equalsIndex >= 0) + B.JSArray_methods.add$1(indices, equalsIndex); + else { + lastSeparator = B.JSArray_methods.get$last(indices); + if (char !== 44 || i !== lastSeparator + 7 || !B.JSString_methods.startsWith$2(text, "base64", lastSeparator + 1)) + throw A.wrapException(A.FormatException$("Expecting '='", text, i)); + break; + } + } + B.JSArray_methods.add$1(indices, i); + t2 = i + 1; + if ((indices.length & 1) === 1) + text = B.C_Base64Codec.normalize$3(0, text, t2, t1); + else { + data = A._Uri__normalize(text, t2, t1, B.List_oFp, true, false); + if (data != null) + text = B.JSString_methods.replaceRange$3(text, t2, t1, data); + } + return new A.UriData(text, indices, sourceUri); + }, + UriData__uriEncodeBytes(canonicalTable, bytes, buffer) { + var t1, byteOr, i, byte, t2, + _s16_ = "0123456789ABCDEF"; + for (t1 = bytes.length, byteOr = 0, i = 0; i < t1; ++i) { + byte = bytes[i]; + byteOr |= byte; + if (byte < 128) { + t2 = byte >>> 4; + if (!(t2 < 8)) + return A.ioore(canonicalTable, t2); + t2 = (canonicalTable[t2] & 1 << (byte & 15)) !== 0; + } else + t2 = false; + if (t2) + buffer._contents += A.Primitives_stringFromCharCode(byte); + else { + buffer._contents += A.Primitives_stringFromCharCode(37); + t2 = byte >>> 4; + if (!(t2 < 16)) + return A.ioore(_s16_, t2); + buffer._contents += A.Primitives_stringFromCharCode(_s16_.charCodeAt(t2)); + buffer._contents += A.Primitives_stringFromCharCode(_s16_.charCodeAt(byte & 15)); + } + } + if ((byteOr & 4294967040) !== 0) + for (i = 0; i < t1; ++i) { + byte = bytes[i]; + if (byte > 255) + throw A.wrapException(A.ArgumentError$value(byte, "non-byte value", null)); + } + }, + _createTables() { + var _i, t1, t2, t3, b, + _s77_ = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~!$&'()*+,;=", + _s1_ = ".", _s1_0 = ":", _s1_1 = "/", _s1_2 = "\\", _s1_3 = "?", _s1_4 = "#", _s2_ = "/\\", + tables = A._setArrayType(new Array(22), type$.JSArray_Uint8List); + for (_i = 0; _i < 22; ++_i) + tables[_i] = new Uint8Array(96); + t1 = new A._createTables_build(tables); + t2 = new A._createTables_setChars(); + t3 = new A._createTables_setRange(); + b = t1.call$2(0, 225); + t2.call$3(b, _s77_, 1); + t2.call$3(b, _s1_, 14); + t2.call$3(b, _s1_0, 34); + t2.call$3(b, _s1_1, 3); + t2.call$3(b, _s1_2, 227); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(14, 225); + t2.call$3(b, _s77_, 1); + t2.call$3(b, _s1_, 15); + t2.call$3(b, _s1_0, 34); + t2.call$3(b, _s2_, 234); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(15, 225); + t2.call$3(b, _s77_, 1); + t2.call$3(b, "%", 225); + t2.call$3(b, _s1_0, 34); + t2.call$3(b, _s1_1, 9); + t2.call$3(b, _s1_2, 233); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(1, 225); + t2.call$3(b, _s77_, 1); + t2.call$3(b, _s1_0, 34); + t2.call$3(b, _s1_1, 10); + t2.call$3(b, _s1_2, 234); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(2, 235); + t2.call$3(b, _s77_, 139); + t2.call$3(b, _s1_1, 131); + t2.call$3(b, _s1_2, 131); + t2.call$3(b, _s1_, 146); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(3, 235); + t2.call$3(b, _s77_, 11); + t2.call$3(b, _s1_1, 68); + t2.call$3(b, _s1_2, 68); + t2.call$3(b, _s1_, 18); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(4, 229); + t2.call$3(b, _s77_, 5); + t3.call$3(b, "AZ", 229); + t2.call$3(b, _s1_0, 102); + t2.call$3(b, "@", 68); + t2.call$3(b, "[", 232); + t2.call$3(b, _s1_1, 138); + t2.call$3(b, _s1_2, 138); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(5, 229); + t2.call$3(b, _s77_, 5); + t3.call$3(b, "AZ", 229); + t2.call$3(b, _s1_0, 102); + t2.call$3(b, "@", 68); + t2.call$3(b, _s1_1, 138); + t2.call$3(b, _s1_2, 138); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(6, 231); + t3.call$3(b, "19", 7); + t2.call$3(b, "@", 68); + t2.call$3(b, _s1_1, 138); + t2.call$3(b, _s1_2, 138); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(7, 231); + t3.call$3(b, "09", 7); + t2.call$3(b, "@", 68); + t2.call$3(b, _s1_1, 138); + t2.call$3(b, _s1_2, 138); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + t2.call$3(t1.call$2(8, 8), "]", 5); + b = t1.call$2(9, 235); + t2.call$3(b, _s77_, 11); + t2.call$3(b, _s1_, 16); + t2.call$3(b, _s2_, 234); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(16, 235); + t2.call$3(b, _s77_, 11); + t2.call$3(b, _s1_, 17); + t2.call$3(b, _s2_, 234); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(17, 235); + t2.call$3(b, _s77_, 11); + t2.call$3(b, _s1_1, 9); + t2.call$3(b, _s1_2, 233); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(10, 235); + t2.call$3(b, _s77_, 11); + t2.call$3(b, _s1_, 18); + t2.call$3(b, _s1_1, 10); + t2.call$3(b, _s1_2, 234); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(18, 235); + t2.call$3(b, _s77_, 11); + t2.call$3(b, _s1_, 19); + t2.call$3(b, _s2_, 234); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(19, 235); + t2.call$3(b, _s77_, 11); + t2.call$3(b, _s2_, 234); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(11, 235); + t2.call$3(b, _s77_, 11); + t2.call$3(b, _s1_1, 10); + t2.call$3(b, _s1_2, 234); + t2.call$3(b, _s1_3, 172); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(12, 236); + t2.call$3(b, _s77_, 12); + t2.call$3(b, _s1_3, 12); + t2.call$3(b, _s1_4, 205); + b = t1.call$2(13, 237); + t2.call$3(b, _s77_, 13); + t2.call$3(b, _s1_3, 13); + t3.call$3(t1.call$2(20, 245), "az", 21); + b = t1.call$2(21, 245); + t3.call$3(b, "az", 21); + t3.call$3(b, "09", 21); + t2.call$3(b, "+-.", 21); + return tables; + }, + _scan(uri, start, end, state, indices) { + var t1, i, table, char, transition, + tables = $.$get$_scannerTables(); + for (t1 = uri.length, i = start; i < end; ++i) { + if (!(state >= 0 && state < tables.length)) + return A.ioore(tables, state); + table = tables[state]; + if (!(i < t1)) + return A.ioore(uri, i); + char = uri.charCodeAt(i) ^ 96; + transition = table[char > 95 ? 31 : char]; + state = transition & 31; + B.JSArray_methods.$indexSet(indices, transition >>> 5, i); + } + return state; + }, + _SimpleUri__packageNameEnd(uri) { + if (uri._schemeEnd === 7 && B.JSString_methods.startsWith$1(uri._uri, "package") && uri._hostStart <= 0) + return A._skipPackageNameChars(uri._uri, uri._pathStart, uri._queryStart); + return -1; + }, + _skipPackageNameChars(source, start, end) { + var t1, i, dots, char; + for (t1 = source.length, i = start, dots = 0; i < end; ++i) { + if (!(i >= 0 && i < t1)) + return A.ioore(source, i); + char = source.charCodeAt(i); + if (char === 47) + return dots !== 0 ? i : -1; + if (char === 37 || char === 58) + return -1; + dots |= char ^ 46; + } + return -1; + }, + _caseInsensitiveCompareStart(prefix, string, start) { + var t1, t2, result, i, t3, stringChar, delta, lowerChar; + for (t1 = prefix.length, t2 = string.length, result = 0, i = 0; i < t1; ++i) { + t3 = start + i; + if (!(t3 < t2)) + return A.ioore(string, t3); + stringChar = string.charCodeAt(t3); + delta = prefix.charCodeAt(i) ^ stringChar; + if (delta !== 0) { + if (delta === 32) { + lowerChar = stringChar | delta; + if (97 <= lowerChar && lowerChar <= 122) { + result = 32; + continue; + } + } + return -1; + } + } + return result; + }, + NoSuchMethodError_toString_closure: function NoSuchMethodError_toString_closure(t0, t1) { + this._box_0 = t0; + this.sb = t1; + }, + DateTime: function DateTime(t0, t1) { + this._core$_value = t0; + this.isUtc = t1; + }, + Duration: function Duration(t0) { + this._duration = t0; + }, + Error: function Error() { + }, + AssertionError: function AssertionError(t0) { + this.message = t0; + }, + TypeError: function TypeError() { + }, + ArgumentError: function ArgumentError(t0, t1, t2, t3) { + var _ = this; + _._hasValue = t0; + _.invalidValue = t1; + _.name = t2; + _.message = t3; + }, + RangeError: function RangeError(t0, t1, t2, t3, t4, t5) { + var _ = this; + _.start = t0; + _.end = t1; + _._hasValue = t2; + _.invalidValue = t3; + _.name = t4; + _.message = t5; + }, + IndexError: function IndexError(t0, t1, t2, t3, t4) { + var _ = this; + _.length = t0; + _._hasValue = t1; + _.invalidValue = t2; + _.name = t3; + _.message = t4; + }, + NoSuchMethodError: function NoSuchMethodError(t0, t1, t2, t3) { + var _ = this; + _._core$_receiver = t0; + _._core$_memberName = t1; + _._core$_arguments = t2; + _._namedArguments = t3; + }, + UnsupportedError: function UnsupportedError(t0) { + this.message = t0; + }, + UnimplementedError: function UnimplementedError(t0) { + this.message = t0; + }, + StateError: function StateError(t0) { + this.message = t0; + }, + ConcurrentModificationError: function ConcurrentModificationError(t0) { + this.modifiedObject = t0; + }, + OutOfMemoryError: function OutOfMemoryError() { + }, + StackOverflowError: function StackOverflowError() { + }, + _Exception: function _Exception(t0) { + this.message = t0; + }, + FormatException: function FormatException(t0, t1, t2) { + this.message = t0; + this.source = t1; + this.offset = t2; + }, + Iterable: function Iterable() { + }, + Null: function Null() { + }, + Object: function Object() { + }, + _StringStackTrace: function _StringStackTrace(t0) { + this._stackTrace = t0; + }, + StringBuffer: function StringBuffer(t0) { + this._contents = t0; + }, + Uri_splitQueryString_closure: function Uri_splitQueryString_closure(t0) { + this.encoding = t0; + }, + Uri__parseIPv4Address_error: function Uri__parseIPv4Address_error(t0) { + this.host = t0; + }, + Uri_parseIPv6Address_error: function Uri_parseIPv6Address_error(t0) { + this.host = t0; + }, + Uri_parseIPv6Address_parseHex: function Uri_parseIPv6Address_parseHex(t0, t1) { + this.error = t0; + this.host = t1; + }, + _Uri: function _Uri(t0, t1, t2, t3, t4, t5, t6) { + var _ = this; + _.scheme = t0; + _._userInfo = t1; + _._host = t2; + _._port = t3; + _.path = t4; + _._query = t5; + _._fragment = t6; + _.___Uri_queryParameters_FI = _.___Uri_hashCode_FI = _.___Uri_pathSegments_FI = _.___Uri__text_FI = $; + }, + _Uri__makePath_closure: function _Uri__makePath_closure() { + }, + UriData: function UriData(t0, t1, t2) { + this._text = t0; + this._separatorIndices = t1; + this._uriCache = t2; + }, + _createTables_build: function _createTables_build(t0) { + this.tables = t0; + }, + _createTables_setChars: function _createTables_setChars() { + }, + _createTables_setRange: function _createTables_setRange() { + }, + _SimpleUri: function _SimpleUri(t0, t1, t2, t3, t4, t5, t6, t7) { + var _ = this; + _._uri = t0; + _._schemeEnd = t1; + _._hostStart = t2; + _._portStart = t3; + _._pathStart = t4; + _._queryStart = t5; + _._fragmentStart = t6; + _._schemeCache = t7; + _._hashCodeCache = null; + }, + _DataUri: function _DataUri(t0, t1, t2, t3, t4, t5, t6) { + var _ = this; + _.scheme = t0; + _._userInfo = t1; + _._host = t2; + _._port = t3; + _.path = t4; + _._query = t5; + _._fragment = t6; + _.___Uri_queryParameters_FI = _.___Uri_hashCode_FI = _.___Uri_pathSegments_FI = _.___Uri__text_FI = $; + }, + HtmlElement: function HtmlElement() { + }, + AccessibleNodeList: function AccessibleNodeList() { + }, + AnchorElement: function AnchorElement() { + }, + AreaElement: function AreaElement() { + }, + Blob: function Blob() { + }, + CharacterData: function CharacterData() { + }, + CssPerspective: function CssPerspective() { + }, + CssRule: function CssRule() { + }, + CssStyleDeclaration: function CssStyleDeclaration() { + }, + CssStyleDeclarationBase: function CssStyleDeclarationBase() { + }, + CssStyleValue: function CssStyleValue() { + }, + CssTransformComponent: function CssTransformComponent() { + }, + CssTransformValue: function CssTransformValue() { + }, + CssUnparsedValue: function CssUnparsedValue() { + }, + DataTransferItemList: function DataTransferItemList() { + }, + DomException: function DomException() { + }, + DomRectList: function DomRectList() { + }, + DomRectReadOnly: function DomRectReadOnly() { + }, + DomStringList: function DomStringList() { + }, + DomTokenList: function DomTokenList() { + }, + Element: function Element() { + }, + EventTarget: function EventTarget() { + }, + File: function File() { + }, + FileList: function FileList() { + }, + FileWriter: function FileWriter() { + }, + FormElement: function FormElement() { + }, + Gamepad: function Gamepad() { + }, + History: function History() { + }, + HtmlCollection: function HtmlCollection() { + }, + Location: function Location() { + }, + MediaList: function MediaList() { + }, + MidiInputMap: function MidiInputMap() { + }, + MidiInputMap_keys_closure: function MidiInputMap_keys_closure(t0) { + this.keys = t0; + }, + MidiOutputMap: function MidiOutputMap() { + }, + MidiOutputMap_keys_closure: function MidiOutputMap_keys_closure(t0) { + this.keys = t0; + }, + MimeType: function MimeType() { + }, + MimeTypeArray: function MimeTypeArray() { + }, + Node: function Node() { + }, + NodeList: function NodeList() { + }, + Plugin: function Plugin() { + }, + PluginArray: function PluginArray() { + }, + RtcStatsReport: function RtcStatsReport() { + }, + RtcStatsReport_keys_closure: function RtcStatsReport_keys_closure(t0) { + this.keys = t0; + }, + SelectElement: function SelectElement() { + }, + SourceBuffer: function SourceBuffer() { + }, + SourceBufferList: function SourceBufferList() { + }, + SpeechGrammar: function SpeechGrammar() { + }, + SpeechGrammarList: function SpeechGrammarList() { + }, + SpeechRecognitionResult: function SpeechRecognitionResult() { + }, + Storage: function Storage() { + }, + Storage_keys_closure: function Storage_keys_closure(t0) { + this.keys = t0; + }, + StyleSheet: function StyleSheet() { + }, + TextTrack: function TextTrack() { + }, + TextTrackCue: function TextTrackCue() { + }, + TextTrackCueList: function TextTrackCueList() { + }, + TextTrackList: function TextTrackList() { + }, + TimeRanges: function TimeRanges() { + }, + Touch: function Touch() { + }, + TouchList: function TouchList() { + }, + TrackDefaultList: function TrackDefaultList() { + }, + Url: function Url() { + }, + VideoTrackList: function VideoTrackList() { + }, + _CssRuleList: function _CssRuleList() { + }, + _DomRect: function _DomRect() { + }, + _GamepadList: function _GamepadList() { + }, + _NamedNodeMap: function _NamedNodeMap() { + }, + _SpeechRecognitionResultList: function _SpeechRecognitionResultList() { + }, + _StyleSheetList: function _StyleSheetList() { + }, + ImmutableListMixin: function ImmutableListMixin() { + }, + FixedSizeListIterator: function FixedSizeListIterator(t0, t1, t2) { + var _ = this; + _._array = t0; + _._html$_length = t1; + _._position = -1; + _._html$_current = null; + _.$ti = t2; + }, + _CssStyleDeclaration_JavaScriptObject_CssStyleDeclarationBase: function _CssStyleDeclaration_JavaScriptObject_CssStyleDeclarationBase() { + }, + _DomRectList_JavaScriptObject_ListMixin: function _DomRectList_JavaScriptObject_ListMixin() { + }, + _DomRectList_JavaScriptObject_ListMixin_ImmutableListMixin: function _DomRectList_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + _DomStringList_JavaScriptObject_ListMixin: function _DomStringList_JavaScriptObject_ListMixin() { + }, + _DomStringList_JavaScriptObject_ListMixin_ImmutableListMixin: function _DomStringList_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + _FileList_JavaScriptObject_ListMixin: function _FileList_JavaScriptObject_ListMixin() { + }, + _FileList_JavaScriptObject_ListMixin_ImmutableListMixin: function _FileList_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + _HtmlCollection_JavaScriptObject_ListMixin: function _HtmlCollection_JavaScriptObject_ListMixin() { + }, + _HtmlCollection_JavaScriptObject_ListMixin_ImmutableListMixin: function _HtmlCollection_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + _MidiInputMap_JavaScriptObject_MapMixin: function _MidiInputMap_JavaScriptObject_MapMixin() { + }, + _MidiOutputMap_JavaScriptObject_MapMixin: function _MidiOutputMap_JavaScriptObject_MapMixin() { + }, + _MimeTypeArray_JavaScriptObject_ListMixin: function _MimeTypeArray_JavaScriptObject_ListMixin() { + }, + _MimeTypeArray_JavaScriptObject_ListMixin_ImmutableListMixin: function _MimeTypeArray_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + _NodeList_JavaScriptObject_ListMixin: function _NodeList_JavaScriptObject_ListMixin() { + }, + _NodeList_JavaScriptObject_ListMixin_ImmutableListMixin: function _NodeList_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + _PluginArray_JavaScriptObject_ListMixin: function _PluginArray_JavaScriptObject_ListMixin() { + }, + _PluginArray_JavaScriptObject_ListMixin_ImmutableListMixin: function _PluginArray_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + _RtcStatsReport_JavaScriptObject_MapMixin: function _RtcStatsReport_JavaScriptObject_MapMixin() { + }, + _SourceBufferList_EventTarget_ListMixin: function _SourceBufferList_EventTarget_ListMixin() { + }, + _SourceBufferList_EventTarget_ListMixin_ImmutableListMixin: function _SourceBufferList_EventTarget_ListMixin_ImmutableListMixin() { + }, + _SpeechGrammarList_JavaScriptObject_ListMixin: function _SpeechGrammarList_JavaScriptObject_ListMixin() { + }, + _SpeechGrammarList_JavaScriptObject_ListMixin_ImmutableListMixin: function _SpeechGrammarList_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + _Storage_JavaScriptObject_MapMixin: function _Storage_JavaScriptObject_MapMixin() { + }, + _TextTrackCueList_JavaScriptObject_ListMixin: function _TextTrackCueList_JavaScriptObject_ListMixin() { + }, + _TextTrackCueList_JavaScriptObject_ListMixin_ImmutableListMixin: function _TextTrackCueList_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + _TextTrackList_EventTarget_ListMixin: function _TextTrackList_EventTarget_ListMixin() { + }, + _TextTrackList_EventTarget_ListMixin_ImmutableListMixin: function _TextTrackList_EventTarget_ListMixin_ImmutableListMixin() { + }, + _TouchList_JavaScriptObject_ListMixin: function _TouchList_JavaScriptObject_ListMixin() { + }, + _TouchList_JavaScriptObject_ListMixin_ImmutableListMixin: function _TouchList_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + __CssRuleList_JavaScriptObject_ListMixin: function __CssRuleList_JavaScriptObject_ListMixin() { + }, + __CssRuleList_JavaScriptObject_ListMixin_ImmutableListMixin: function __CssRuleList_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + __GamepadList_JavaScriptObject_ListMixin: function __GamepadList_JavaScriptObject_ListMixin() { + }, + __GamepadList_JavaScriptObject_ListMixin_ImmutableListMixin: function __GamepadList_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + __NamedNodeMap_JavaScriptObject_ListMixin: function __NamedNodeMap_JavaScriptObject_ListMixin() { + }, + __NamedNodeMap_JavaScriptObject_ListMixin_ImmutableListMixin: function __NamedNodeMap_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + __SpeechRecognitionResultList_JavaScriptObject_ListMixin: function __SpeechRecognitionResultList_JavaScriptObject_ListMixin() { + }, + __SpeechRecognitionResultList_JavaScriptObject_ListMixin_ImmutableListMixin: function __SpeechRecognitionResultList_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + __StyleSheetList_JavaScriptObject_ListMixin: function __StyleSheetList_JavaScriptObject_ListMixin() { + }, + __StyleSheetList_JavaScriptObject_ListMixin_ImmutableListMixin: function __StyleSheetList_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + _convertDartFunctionFast(f) { + var ret, + existing = f.$dart_jsFunction; + if (existing != null) + return existing; + ret = function(_call, f) { + return function() { + return _call(f, Array.prototype.slice.apply(arguments)); + }; + }(A._callDartFunctionFast, f); + ret[$.$get$DART_CLOSURE_PROPERTY_NAME()] = f; + f.$dart_jsFunction = ret; + return ret; + }, + _callDartFunctionFast(callback, $arguments) { + type$.List_dynamic._as($arguments); + type$.Function._as(callback); + return A.Primitives_applyFunction(callback, $arguments, null); + }, + allowInterop(f, $F) { + if (typeof f == "function") + return f; + else + return $F._as(A._convertDartFunctionFast(f)); + }, + _noJsifyRequired(o) { + return o == null || A._isBool(o) || typeof o == "number" || typeof o == "string" || type$.Int8List._is(o) || type$.Uint8List._is(o) || type$.Uint8ClampedList._is(o) || type$.Int16List._is(o) || type$.Uint16List._is(o) || type$.Int32List._is(o) || type$.Uint32List._is(o) || type$.Float32List._is(o) || type$.Float64List._is(o) || type$.ByteBuffer._is(o) || type$.ByteData._is(o); + }, + jsify(object) { + if (A._noJsifyRequired(object)) + return object; + return new A.jsify__convert(new A._IdentityHashMap(type$._IdentityHashMap_of_nullable_Object_and_nullable_Object)).call$1(object); + }, + getProperty(o, $name, $T) { + return $T._as(o[$name]); + }, + callMethod(o, method, args, $T) { + return $T._as(o[method].apply(o, args)); + }, + callConstructor(constr, $arguments, $T) { + var args, factoryFunction; + if ($arguments instanceof Array) + switch ($arguments.length) { + case 0: + return $T._as(new constr()); + case 1: + return $T._as(new constr($arguments[0])); + case 2: + return $T._as(new constr($arguments[0], $arguments[1])); + case 3: + return $T._as(new constr($arguments[0], $arguments[1], $arguments[2])); + case 4: + return $T._as(new constr($arguments[0], $arguments[1], $arguments[2], $arguments[3])); + } + args = [null]; + B.JSArray_methods.addAll$1(args, $arguments); + factoryFunction = constr.bind.apply(constr, args); + String(factoryFunction); + return $T._as(new factoryFunction()); + }, + promiseToFuture(jsPromise, $T) { + var t1 = new A._Future($.Zone__current, $T._eval$1("_Future<0>")), + completer = new A._AsyncCompleter(t1, $T._eval$1("_AsyncCompleter<0>")); + jsPromise.then(A.convertDartClosureToJS(new A.promiseToFuture_closure(completer, $T), 1), A.convertDartClosureToJS(new A.promiseToFuture_closure0(completer), 1)); + return t1; + }, + _noDartifyRequired(o) { + return o == null || typeof o === "boolean" || typeof o === "number" || typeof o === "string" || o instanceof Int8Array || o instanceof Uint8Array || o instanceof Uint8ClampedArray || o instanceof Int16Array || o instanceof Uint16Array || o instanceof Int32Array || o instanceof Uint32Array || o instanceof Float32Array || o instanceof Float64Array || o instanceof ArrayBuffer || o instanceof DataView; + }, + dartify(o) { + if (A._noDartifyRequired(o)) + return o; + return new A.dartify_convert(new A._IdentityHashMap(type$._IdentityHashMap_of_nullable_Object_and_nullable_Object)).call$1(o); + }, + jsify__convert: function jsify__convert(t0) { + this._convertedObjects = t0; + }, + promiseToFuture_closure: function promiseToFuture_closure(t0, t1) { + this.completer = t0; + this.T = t1; + }, + promiseToFuture_closure0: function promiseToFuture_closure0(t0) { + this.completer = t0; + }, + dartify_convert: function dartify_convert(t0) { + this._convertedObjects = t0; + }, + NullRejectionException: function NullRejectionException(t0) { + this.isUndefined = t0; + }, + Length: function Length() { + }, + LengthList: function LengthList() { + }, + Number: function Number() { + }, + NumberList: function NumberList() { + }, + PointList: function PointList() { + }, + StringList: function StringList() { + }, + Transform: function Transform() { + }, + TransformList: function TransformList() { + }, + _LengthList_JavaScriptObject_ListMixin: function _LengthList_JavaScriptObject_ListMixin() { + }, + _LengthList_JavaScriptObject_ListMixin_ImmutableListMixin: function _LengthList_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + _NumberList_JavaScriptObject_ListMixin: function _NumberList_JavaScriptObject_ListMixin() { + }, + _NumberList_JavaScriptObject_ListMixin_ImmutableListMixin: function _NumberList_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + _StringList_JavaScriptObject_ListMixin: function _StringList_JavaScriptObject_ListMixin() { + }, + _StringList_JavaScriptObject_ListMixin_ImmutableListMixin: function _StringList_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + _TransformList_JavaScriptObject_ListMixin: function _TransformList_JavaScriptObject_ListMixin() { + }, + _TransformList_JavaScriptObject_ListMixin_ImmutableListMixin: function _TransformList_JavaScriptObject_ListMixin_ImmutableListMixin() { + }, + AudioBuffer: function AudioBuffer() { + }, + AudioParamMap: function AudioParamMap() { + }, + AudioParamMap_keys_closure: function AudioParamMap_keys_closure(t0) { + this.keys = t0; + }, + AudioTrackList: function AudioTrackList() { + }, + BaseAudioContext: function BaseAudioContext() { + }, + OfflineAudioContext: function OfflineAudioContext() { + }, + _AudioParamMap_JavaScriptObject_MapMixin: function _AudioParamMap_JavaScriptObject_MapMixin() { + }, + NullStreamSink: function NullStreamSink(t0, t1) { + var _ = this; + _.done = t0; + _._addingStream = _._null_stream_sink$_closed = false; + _.$ti = t1; + }, + NullStreamSink_addStream_closure: function NullStreamSink_addStream_closure(t0) { + this.$this = t0; + }, + Context_Context(style) { + return new A.Context(style, "."); + }, + _parseUri(uri) { + return uri; + }, + _validateArgList(method, args) { + var numArgs, i, numArgs0, message, t1, t2, t3, t4; + for (numArgs = args.length, i = 1; i < numArgs; ++i) { + if (args[i] == null || args[i - 1] != null) + continue; + for (; numArgs >= 1; numArgs = numArgs0) { + numArgs0 = numArgs - 1; + if (args[numArgs0] != null) + break; + } + message = new A.StringBuffer(""); + t1 = "" + (method + "("); + message._contents = t1; + t2 = A._arrayInstanceType(args); + t3 = t2._eval$1("SubListIterable<1>"); + t4 = new A.SubListIterable(args, 0, numArgs, t3); + t4.SubListIterable$3(args, 0, numArgs, t2._precomputed1); + t3 = t1 + new A.MappedListIterable(t4, t3._eval$1("String(ListIterable.E)")._as(new A._validateArgList_closure()), t3._eval$1("MappedListIterable<ListIterable.E,String>")).join$1(0, ", "); + message._contents = t3; + message._contents = t3 + ("): part " + (i - 1) + " was null, but part " + i + " was not."); + throw A.wrapException(A.ArgumentError$(message.toString$0(0), null)); + } + }, + Context: function Context(t0, t1) { + this.style = t0; + this._context$_current = t1; + }, + Context_joinAll_closure: function Context_joinAll_closure() { + }, + Context_split_closure: function Context_split_closure() { + }, + _validateArgList_closure: function _validateArgList_closure() { + }, + InternalStyle: function InternalStyle() { + }, + ParsedPath_ParsedPath$parse(path, style) { + var t1, parts, separators, t2, start, i, + root = style.getRoot$1(path); + style.isRootRelative$1(path); + if (root != null) + path = B.JSString_methods.substring$1(path, root.length); + t1 = type$.JSArray_String; + parts = A._setArrayType([], t1); + separators = A._setArrayType([], t1); + t1 = path.length; + if (t1 !== 0) { + if (0 >= t1) + return A.ioore(path, 0); + t2 = style.isSeparator$1(path.charCodeAt(0)); + } else + t2 = false; + if (t2) { + if (0 >= t1) + return A.ioore(path, 0); + B.JSArray_methods.add$1(separators, path[0]); + start = 1; + } else { + B.JSArray_methods.add$1(separators, ""); + start = 0; + } + for (i = start; i < t1; ++i) + if (style.isSeparator$1(path.charCodeAt(i))) { + B.JSArray_methods.add$1(parts, B.JSString_methods.substring$2(path, start, i)); + B.JSArray_methods.add$1(separators, path[i]); + start = i + 1; + } + if (start < t1) { + B.JSArray_methods.add$1(parts, B.JSString_methods.substring$1(path, start)); + B.JSArray_methods.add$1(separators, ""); + } + return new A.ParsedPath(style, root, parts, separators); + }, + ParsedPath: function ParsedPath(t0, t1, t2, t3) { + var _ = this; + _.style = t0; + _.root = t1; + _.parts = t2; + _.separators = t3; + }, + PathException$(message) { + return new A.PathException(message); + }, + PathException: function PathException(t0) { + this.message = t0; + }, + Style__getPlatformStyle() { + if (A.Uri_base().get$scheme() !== "file") + return $.$get$Style_url(); + var t1 = A.Uri_base(); + if (!B.JSString_methods.endsWith$1(t1.get$path(t1), "/")) + return $.$get$Style_url(); + if (A._Uri__Uri(null, "a/b", null, null).toFilePath$0() === "a\\b") + return $.$get$Style_windows(); + return $.$get$Style_posix(); + }, + Style: function Style() { + }, + PosixStyle: function PosixStyle(t0, t1, t2) { + this.separatorPattern = t0; + this.needsSeparatorPattern = t1; + this.rootPattern = t2; + }, + UrlStyle: function UrlStyle(t0, t1, t2, t3) { + var _ = this; + _.separatorPattern = t0; + _.needsSeparatorPattern = t1; + _.rootPattern = t2; + _.relativeRootPattern = t3; + }, + WindowsStyle: function WindowsStyle(t0, t1, t2, t3) { + var _ = this; + _.separatorPattern = t0; + _.needsSeparatorPattern = t1; + _.rootPattern = t2; + _.relativeRootPattern = t3; + }, + WindowsStyle_absolutePathToUri_closure: function WindowsStyle_absolutePathToUri_closure() { + }, + Chain_Chain$parse(chain) { + var t1, t2, + _s51_ = string$.x3d_____; + if (chain.length === 0) + return new A.Chain(A.List_List$unmodifiable(A._setArrayType([], type$.JSArray_Trace), type$.Trace)); + t1 = $.$get$vmChainGap(); + if (B.JSString_methods.contains$1(chain, t1)) { + t1 = B.JSString_methods.split$1(chain, t1); + t2 = A._arrayInstanceType(t1); + return new A.Chain(A.List_List$unmodifiable(new A.MappedIterable(new A.WhereIterable(t1, t2._eval$1("bool(1)")._as(new A.Chain_Chain$parse_closure()), t2._eval$1("WhereIterable<1>")), t2._eval$1("Trace(1)")._as(A.trace_Trace___parseVM_tearOff$closure()), t2._eval$1("MappedIterable<1,Trace>")), type$.Trace)); + } + if (!B.JSString_methods.contains$1(chain, _s51_)) + return new A.Chain(A.List_List$unmodifiable(A._setArrayType([A.Trace_Trace$parse(chain)], type$.JSArray_Trace), type$.Trace)); + return new A.Chain(A.List_List$unmodifiable(new A.MappedListIterable(A._setArrayType(chain.split(_s51_), type$.JSArray_String), type$.Trace_Function_String._as(A.trace_Trace___parseFriendly_tearOff$closure()), type$.MappedListIterable_String_Trace), type$.Trace)); + }, + Chain: function Chain(t0) { + this.traces = t0; + }, + Chain_Chain$parse_closure: function Chain_Chain$parse_closure() { + }, + Chain_toTrace_closure: function Chain_toTrace_closure() { + }, + Chain_toString_closure0: function Chain_toString_closure0() { + }, + Chain_toString__closure0: function Chain_toString__closure0() { + }, + Chain_toString_closure: function Chain_toString_closure(t0) { + this.longest = t0; + }, + Chain_toString__closure: function Chain_toString__closure(t0) { + this.longest = t0; + }, + Frame___parseVM_tearOff(frame) { + return A.Frame_Frame$parseVM(A._asString(frame)); + }, + Frame_Frame$parseVM(frame) { + return A.Frame__catchFormatException(frame, new A.Frame_Frame$parseVM_closure(frame)); + }, + Frame___parseV8_tearOff(frame) { + return A.Frame_Frame$parseV8(A._asString(frame)); + }, + Frame_Frame$parseV8(frame) { + return A.Frame__catchFormatException(frame, new A.Frame_Frame$parseV8_closure(frame)); + }, + Frame_Frame$_parseFirefoxEval(frame) { + return A.Frame__catchFormatException(frame, new A.Frame_Frame$_parseFirefoxEval_closure(frame)); + }, + Frame___parseFirefox_tearOff(frame) { + return A.Frame_Frame$parseFirefox(A._asString(frame)); + }, + Frame_Frame$parseFirefox(frame) { + return A.Frame__catchFormatException(frame, new A.Frame_Frame$parseFirefox_closure(frame)); + }, + Frame___parseFriendly_tearOff(frame) { + return A.Frame_Frame$parseFriendly(A._asString(frame)); + }, + Frame_Frame$parseFriendly(frame) { + return A.Frame__catchFormatException(frame, new A.Frame_Frame$parseFriendly_closure(frame)); + }, + Frame__uriOrPathToUri(uriOrPath) { + if (B.JSString_methods.contains$1(uriOrPath, $.$get$Frame__uriRegExp())) + return A.Uri_parse(uriOrPath); + else if (B.JSString_methods.contains$1(uriOrPath, $.$get$Frame__windowsRegExp())) + return A._Uri__Uri$file(uriOrPath, true); + else if (B.JSString_methods.startsWith$1(uriOrPath, "/")) + return A._Uri__Uri$file(uriOrPath, false); + if (B.JSString_methods.contains$1(uriOrPath, "\\")) + return $.$get$windows().toUri$1(uriOrPath); + return A.Uri_parse(uriOrPath); + }, + Frame__catchFormatException(text, body) { + var t1, exception; + try { + t1 = body.call$0(); + return t1; + } catch (exception) { + if (A.unwrapException(exception) instanceof A.FormatException) + return new A.UnparsedFrame(A._Uri__Uri(null, "unparsed", null, null), text); + else + throw exception; + } + }, + Frame: function Frame(t0, t1, t2, t3) { + var _ = this; + _.uri = t0; + _.line = t1; + _.column = t2; + _.member = t3; + }, + Frame_Frame$parseVM_closure: function Frame_Frame$parseVM_closure(t0) { + this.frame = t0; + }, + Frame_Frame$parseV8_closure: function Frame_Frame$parseV8_closure(t0) { + this.frame = t0; + }, + Frame_Frame$parseV8_closure_parseLocation: function Frame_Frame$parseV8_closure_parseLocation(t0) { + this.frame = t0; + }, + Frame_Frame$_parseFirefoxEval_closure: function Frame_Frame$_parseFirefoxEval_closure(t0) { + this.frame = t0; + }, + Frame_Frame$parseFirefox_closure: function Frame_Frame$parseFirefox_closure(t0) { + this.frame = t0; + }, + Frame_Frame$parseFriendly_closure: function Frame_Frame$parseFriendly_closure(t0) { + this.frame = t0; + }, + LazyTrace: function LazyTrace(t0) { + this._thunk = t0; + this.__LazyTrace__trace_FI = $; + }, + LazyTrace_terse_closure: function LazyTrace_terse_closure(t0) { + this.$this = t0; + }, + Trace_Trace$from(trace) { + if (type$.Trace._is(trace)) + return trace; + if (trace instanceof A.Chain) + return trace.toTrace$0(); + return new A.LazyTrace(new A.Trace_Trace$from_closure(trace)); + }, + Trace_Trace$parse(trace) { + var error, t1, exception; + try { + if (trace.length === 0) { + t1 = A.Trace$(A._setArrayType([], type$.JSArray_Frame), null); + return t1; + } + if (B.JSString_methods.contains$1(trace, $.$get$_v8Trace())) { + t1 = A.Trace$parseV8(trace); + return t1; + } + if (B.JSString_methods.contains$1(trace, "\tat ")) { + t1 = A.Trace$parseJSCore(trace); + return t1; + } + if (B.JSString_methods.contains$1(trace, $.$get$_firefoxSafariTrace()) || B.JSString_methods.contains$1(trace, $.$get$_firefoxEvalTrace())) { + t1 = A.Trace$parseFirefox(trace); + return t1; + } + if (B.JSString_methods.contains$1(trace, string$.x3d_____)) { + t1 = A.Chain_Chain$parse(trace).toTrace$0(); + return t1; + } + if (B.JSString_methods.contains$1(trace, $.$get$_friendlyTrace())) { + t1 = A.Trace$parseFriendly(trace); + return t1; + } + t1 = A.Trace$parseVM(trace); + return t1; + } catch (exception) { + t1 = A.unwrapException(exception); + if (t1 instanceof A.FormatException) { + error = t1; + throw A.wrapException(A.FormatException$(error.message + "\nStack trace:\n" + trace, null, null)); + } else + throw exception; + } + }, + Trace___parseVM_tearOff(trace) { + return A.Trace$parseVM(A._asString(trace)); + }, + Trace$parseVM(trace) { + var t1 = A.List_List$unmodifiable(A.Trace__parseVM(trace), type$.Frame); + return new A.Trace(t1, new A._StringStackTrace(trace)); + }, + Trace__parseVM(trace) { + var $frames, + t1 = B.JSString_methods.trim$0(trace), + t2 = $.$get$vmChainGap(), + t3 = type$.WhereIterable_String, + lines = new A.WhereIterable(A._setArrayType(A.stringReplaceAllUnchecked(t1, t2, "").split("\n"), type$.JSArray_String), type$.bool_Function_String._as(new A.Trace__parseVM_closure()), t3); + if (!lines.get$iterator(lines).moveNext$0()) + return A._setArrayType([], type$.JSArray_Frame); + t1 = A.TakeIterable_TakeIterable(lines, lines.get$length(lines) - 1, t3._eval$1("Iterable.E")); + t2 = A._instanceType(t1); + t2 = A.MappedIterable_MappedIterable(t1, t2._eval$1("Frame(Iterable.E)")._as(A.frame_Frame___parseVM_tearOff$closure()), t2._eval$1("Iterable.E"), type$.Frame); + $frames = A.List_List$of(t2, true, A._instanceType(t2)._eval$1("Iterable.E")); + if (!J.endsWith$1$s(lines.get$last(lines), ".da")) + B.JSArray_methods.add$1($frames, A.Frame_Frame$parseVM(lines.get$last(lines))); + return $frames; + }, + Trace$parseV8(trace) { + var t2, t3, + t1 = A.SubListIterable$(A._setArrayType(trace.split("\n"), type$.JSArray_String), 1, null, type$.String); + t1 = t1.super$Iterable$skipWhile(0, t1.$ti._eval$1("bool(ListIterable.E)")._as(new A.Trace$parseV8_closure())); + t2 = type$.Frame; + t3 = t1.$ti; + t2 = A.List_List$unmodifiable(A.MappedIterable_MappedIterable(t1, t3._eval$1("Frame(Iterable.E)")._as(A.frame_Frame___parseV8_tearOff$closure()), t3._eval$1("Iterable.E"), t2), t2); + return new A.Trace(t2, new A._StringStackTrace(trace)); + }, + Trace$parseJSCore(trace) { + var t1 = A.List_List$unmodifiable(new A.MappedIterable(new A.WhereIterable(A._setArrayType(trace.split("\n"), type$.JSArray_String), type$.bool_Function_String._as(new A.Trace$parseJSCore_closure()), type$.WhereIterable_String), type$.Frame_Function_String._as(A.frame_Frame___parseV8_tearOff$closure()), type$.MappedIterable_String_Frame), type$.Frame); + return new A.Trace(t1, new A._StringStackTrace(trace)); + }, + Trace$parseFirefox(trace) { + var t1 = A.List_List$unmodifiable(new A.MappedIterable(new A.WhereIterable(A._setArrayType(B.JSString_methods.trim$0(trace).split("\n"), type$.JSArray_String), type$.bool_Function_String._as(new A.Trace$parseFirefox_closure()), type$.WhereIterable_String), type$.Frame_Function_String._as(A.frame_Frame___parseFirefox_tearOff$closure()), type$.MappedIterable_String_Frame), type$.Frame); + return new A.Trace(t1, new A._StringStackTrace(trace)); + }, + Trace___parseFriendly_tearOff(trace) { + return A.Trace$parseFriendly(A._asString(trace)); + }, + Trace$parseFriendly(trace) { + var t1 = trace.length === 0 ? A._setArrayType([], type$.JSArray_Frame) : new A.MappedIterable(new A.WhereIterable(A._setArrayType(B.JSString_methods.trim$0(trace).split("\n"), type$.JSArray_String), type$.bool_Function_String._as(new A.Trace$parseFriendly_closure()), type$.WhereIterable_String), type$.Frame_Function_String._as(A.frame_Frame___parseFriendly_tearOff$closure()), type$.MappedIterable_String_Frame); + t1 = A.List_List$unmodifiable(t1, type$.Frame); + return new A.Trace(t1, new A._StringStackTrace(trace)); + }, + Trace$($frames, original) { + var t1 = A.List_List$unmodifiable($frames, type$.Frame); + return new A.Trace(t1, new A._StringStackTrace(original == null ? "" : original)); + }, + Trace: function Trace(t0, t1) { + this.frames = t0; + this.original = t1; + }, + Trace_Trace$from_closure: function Trace_Trace$from_closure(t0) { + this.trace = t0; + }, + Trace__parseVM_closure: function Trace__parseVM_closure() { + }, + Trace$parseV8_closure: function Trace$parseV8_closure() { + }, + Trace$parseJSCore_closure: function Trace$parseJSCore_closure() { + }, + Trace$parseFirefox_closure: function Trace$parseFirefox_closure() { + }, + Trace$parseFriendly_closure: function Trace$parseFriendly_closure() { + }, + Trace_terse_closure: function Trace_terse_closure() { + }, + Trace_foldFrames_closure: function Trace_foldFrames_closure(t0) { + this.oldPredicate = t0; + }, + Trace_foldFrames_closure0: function Trace_foldFrames_closure0(t0) { + this._box_0 = t0; + }, + Trace_toString_closure0: function Trace_toString_closure0() { + }, + Trace_toString_closure: function Trace_toString_closure(t0) { + this.longest = t0; + }, + UnparsedFrame: function UnparsedFrame(t0, t1) { + this.uri = t0; + this.member = t1; + }, + GuaranteeChannel$(innerStream, innerSink, allowSinkErrors, $T) { + var t2, t1 = {}; + t1.innerStream = innerStream; + t2 = new A.GuaranteeChannel($T._eval$1("GuaranteeChannel<0>")); + t2.GuaranteeChannel$3$allowSinkErrors(innerSink, true, t1, $T); + return t2; + }, + GuaranteeChannel: function GuaranteeChannel(t0) { + var _ = this; + _.__GuaranteeChannel__streamController_F = _.__GuaranteeChannel__sink_F = $; + _._subscription = null; + _._disconnected = false; + _.$ti = t0; + }, + GuaranteeChannel_closure: function GuaranteeChannel_closure(t0, t1, t2) { + this._box_0 = t0; + this.$this = t1; + this.T = t2; + }, + GuaranteeChannel__closure: function GuaranteeChannel__closure(t0) { + this.$this = t0; + }, + _GuaranteeSink: function _GuaranteeSink(t0, t1, t2, t3, t4) { + var _ = this; + _._inner = t0; + _._channel = t1; + _._doneCompleter = t2; + _._closed = _._disconnected = false; + _._addStreamCompleter = _._addStreamSubscription = null; + _._allowErrors = t3; + _.$ti = t4; + }, + _GuaranteeSink_addStream_closure: function _GuaranteeSink_addStream_closure(t0) { + this.$this = t0; + }, + _MultiChannel$(inner, $T) { + var t1 = type$.int; + t1 = new A._MultiChannel(inner, A.StreamChannelController$(true, $T), A.LinkedHashMap_LinkedHashMap$_empty(t1, $T._eval$1("StreamChannelController<0>")), A.LinkedHashSet_LinkedHashSet$_empty(t1), A.LinkedHashSet_LinkedHashSet$_empty(t1), $T._eval$1("_MultiChannel<0>")); + t1._MultiChannel$1(inner, $T); + return t1; + }, + _MultiChannel: function _MultiChannel(t0, t1, t2, t3, t4, t5) { + var _ = this; + _._multi_channel$_inner = t0; + _._innerStreamSubscription = null; + _._mainController = t1; + _._controllers = t2; + _._pendingIds = t3; + _._closedIds = t4; + _._nextId = 1; + _.$ti = t5; + }, + _MultiChannel_closure: function _MultiChannel_closure(t0, t1) { + this.$this = t0; + this.T = t1; + }, + _MultiChannel_closure0: function _MultiChannel_closure0(t0) { + this.$this = t0; + }, + _MultiChannel_closure1: function _MultiChannel_closure1(t0, t1) { + this.$this = t0; + this.T = t1; + }, + _MultiChannel__closure: function _MultiChannel__closure(t0, t1, t2) { + this.$this = t0; + this.id = t1; + this.T = t2; + }, + _MultiChannel_virtualChannel_closure: function _MultiChannel_virtualChannel_closure(t0, t1) { + this._box_0 = t0; + this.$this = t1; + }, + _MultiChannel_virtualChannel_closure0: function _MultiChannel_virtualChannel_closure0(t0, t1) { + this._box_0 = t0; + this.$this = t1; + }, + VirtualChannel: function VirtualChannel(t0, t1, t2, t3) { + var _ = this; + _._parent = t0; + _.stream = t1; + _.sink = t2; + _.$ti = t3; + }, + StreamChannelController$(sync, $T) { + var _null = null, + t1 = new A.StreamChannelController($T._eval$1("StreamChannelController<0>")), + localToForeignController = A.StreamController_StreamController(_null, _null, true, $T), + foreignToLocalController = A.StreamController_StreamController(_null, _null, true, $T), + t2 = A._instanceType(foreignToLocalController), + t3 = A._instanceType(localToForeignController), + t4 = $T._eval$1("StreamChannel<0>"); + t1.set$__StreamChannelController__local_F(t4._as(A.GuaranteeChannel$(new A._ControllerStream(foreignToLocalController, t2._eval$1("_ControllerStream<1>")), new A._StreamSinkWrapper(localToForeignController, t3._eval$1("_StreamSinkWrapper<1>")), true, $T))); + t2 = t4._as(A.GuaranteeChannel$(new A._ControllerStream(localToForeignController, t3._eval$1("_ControllerStream<1>")), new A._StreamSinkWrapper(foreignToLocalController, t2._eval$1("_StreamSinkWrapper<1>")), true, $T)); + t1.__StreamChannelController__foreign_F !== $ && A.throwLateFieldAI("_foreign"); + t1.set$__StreamChannelController__foreign_F(t2); + return t1; + }, + StreamChannelController: function StreamChannelController(t0) { + this.__StreamChannelController__foreign_F = this.__StreamChannelController__local_F = $; + this.$ti = t0; + }, + StreamChannelMixin: function StreamChannelMixin() { + }, + EventTargetExtension_addEventListener(_this, type, listener) { + var t1 = A._setArrayType([type, listener], type$.JSArray_Object); + A.callMethod(_this, "addEventListener", t1, type$.dynamic); + }, + EventTargetExtension_removeEventListener(_this, type, listener) { + var t1 = A._setArrayType([type, listener], type$.JSArray_Object); + A.callMethod(_this, "removeEventListener", t1, type$.dynamic); + }, + MessagePortExtension_get_postMessage(_this) { + return new A.MessagePortExtension_get_postMessage_closure(_this); + }, + _callConstructor(constructorName, args) { + var $constructor = self.window[constructorName]; + if ($constructor == null) + return null; + return A.callConstructor($constructor, args, type$.nullable_Object); + }, + Subscription$(target, type, listener) { + A.EventTargetExtension_addEventListener(target, type, listener); + return new A.Subscription(type, target, listener); + }, + MessagePortExtension_get_postMessage_closure: function MessagePortExtension_get_postMessage_closure(t0) { + this._this = t0; + }, + Subscription: function Subscription(t0, t1, t2) { + this.type = t0; + this.target = t1; + this.listener = t2; + }, + main() { + var t1 = type$.JavaScriptObject; + t1._as(self.window.console).log("Dart test runner browser host running"); + if (J.$eq$($.$get$_currentUrl().get$queryParameters().$index(0, "debug"), "true")) + t1._as(type$.nullable_JavaScriptObject._as(self.document.body).classList).add("debug"); + A.runZonedGuarded(new A.main_closure(), new A.main_closure0(), type$.Null); + }, + _connectToServer() { + var t2, controller, t3, + t1 = $.$get$_currentUrl().get$queryParameters().$index(0, "managerUrl"); + t1.toString; + t1 = A._callConstructor("WebSocket", A._setArrayType([t1], type$.JSArray_Object)); + t1.toString; + type$.JavaScriptObject._as(t1); + t2 = type$.dynamic; + controller = A.StreamChannelController$(true, t2); + A.EventTargetExtension_addEventListener(t1, "message", A.allowInterop(new A._connectToServer_closure(controller), type$.void_Function_JavaScriptObject)); + t3 = controller.__StreamChannelController__local_F; + t3 === $ && A.throwLateFieldNI("_local"); + t3 = t3.__GuaranteeChannel__streamController_F; + t3 === $ && A.throwLateFieldNI("_streamController"); + new A._ControllerStream(t3, A._instanceType(t3)._eval$1("_ControllerStream<1>")).listen$1(new A._connectToServer_closure0(t1)); + t1 = controller.__StreamChannelController__foreign_F; + t1 === $ && A.throwLateFieldNI("_foreign"); + return A._MultiChannel$(t1, t2); + }, + _connectToIframe(url, id) { + var t2, t3, t4, iframe, controller, windowSubscription, + suiteUrl = A.Uri_parse(url).removeFragment$0(), + t1 = type$.JavaScriptObject; + t1._as(self.window.console).log("Starting suite " + suiteUrl.toString$0(0)); + t2 = self.document; + t3 = A._setArrayType(["iframe"], type$.JSArray_Object); + t4 = type$.dynamic; + iframe = t1._as(A.callMethod(t2, "createElement", t3, t4)); + $._iframes.$indexSet(0, id, iframe); + controller = A.StreamChannelController$(true, t4); + windowSubscription = A._Cell$named("windowSubscription"); + windowSubscription._value = A.Subscription$(self.window, "message", A.allowInterop(new A._connectToIframe_closure(iframe, windowSubscription, suiteUrl, id, controller), type$.void_Function_JavaScriptObject)); + iframe.src = url; + t1._as(type$.nullable_JavaScriptObject._as(self.document.body).appendChild(iframe)); + t1 = controller.__StreamChannelController__foreign_F; + t1 === $ && A.throwLateFieldNI("_foreign"); + return t1; + }, + main_closure: function main_closure() { + }, + main__closure: function main__closure(t0) { + this.serverChannel = t0; + }, + main___closure: function main___closure(t0) { + this._0_0 = t0; + }, + main___closure0: function main___closure0(t0) { + this._0_0 = t0; + }, + main___closure1: function main___closure1(t0) { + this._0_0 = t0; + }, + main___closure2: function main___closure2(t0) { + this._0_0 = t0; + }, + main___closure3: function main___closure3(t0) { + this._0_0 = t0; + }, + main___closure4: function main___closure4(t0) { + this._0_0 = t0; + }, + main__closure0: function main__closure0(t0) { + this.serverChannel = t0; + }, + main__closure1: function main__closure1(t0) { + this.serverChannel = t0; + }, + main__closure2: function main__closure2(t0) { + this.serverChannel = t0; + }, + main__closure3: function main__closure3(t0) { + this.serverChannel = t0; + }, + main_closure0: function main_closure0() { + }, + _connectToServer_closure: function _connectToServer_closure(t0) { + this.controller = t0; + }, + _connectToServer_closure0: function _connectToServer_closure0(t0) { + this.webSocket = t0; + }, + _connectToIframe_closure: function _connectToIframe_closure(t0, t1, t2, t3, t4) { + var _ = this; + _.iframe = t0; + _.windowSubscription = t1; + _.suiteUrl = t2; + _.id = t3; + _.controller = t4; + }, + _connectToIframe__closure1: function _connectToIframe__closure1(t0) { + this.controller = t0; + }, + _connectToIframe__closure: function _connectToIframe__closure(t0) { + this._0_0 = t0; + }, + _connectToIframe__closure0: function _connectToIframe__closure0(t0) { + this._0_0 = t0; + }, + max(a, b, $T) { + A.checkTypeBound($T, type$.num, "T", "max"); + return Math.max($T._as(a), $T._as(b)); + }, + printString(string) { + if (typeof dartPrint == "function") { + dartPrint(string); + return; + } + if (typeof console == "object" && typeof console.log != "undefined") { + console.log(string); + return; + } + if (typeof print == "function") { + print(string); + return; + } + throw "Unable to print message: " + String(string); + }, + _convertNativeToDart_Value(value) { + var proto, t1, values, i; + if (value == null) + return value; + if (typeof value == "string" || typeof value == "number" || A._isBool(value)) + return value; + proto = Object.getPrototypeOf(value); + t1 = proto === Object.prototype; + t1.toString; + if (!t1) { + t1 = proto === null; + t1.toString; + } else + t1 = true; + if (t1) + return A.convertNativeToDart_Dictionary(value); + t1 = Array.isArray(value); + t1.toString; + if (t1) { + values = []; + i = 0; + while (true) { + t1 = value.length; + t1.toString; + if (!(i < t1)) + break; + values.push(A._convertNativeToDart_Value(value[i])); + ++i; + } + return values; + } + return value; + }, + convertNativeToDart_Dictionary(object) { + var dict, keys, t1, _i, key, t2; + if (object == null) + return null; + dict = A.LinkedHashMap_LinkedHashMap$_empty(type$.String, type$.dynamic); + keys = Object.getOwnPropertyNames(object); + for (t1 = keys.length, _i = 0; _i < keys.length; keys.length === t1 || (0, A.throwConcurrentModificationError)(keys), ++_i) { + key = keys[_i]; + t2 = key; + t2.toString; + dict.$indexSet(0, t2, A._convertNativeToDart_Value(object[key])); + } + return dict; + }, + current() { + var exception, t1, path, lastIndex, uri = null; + try { + uri = A.Uri_base(); + } catch (exception) { + if (type$.Exception._is(A.unwrapException(exception))) { + t1 = $._current; + if (t1 != null) + return t1; + throw exception; + } else + throw exception; + } + if (J.$eq$(uri, $._currentUriBase)) { + t1 = $._current; + t1.toString; + return t1; + } + $._currentUriBase = uri; + if ($.$get$Style_platform() === $.$get$Style_url()) + t1 = $._current = uri.resolve$1(".").toString$0(0); + else { + path = uri.toFilePath$0(); + lastIndex = path.length - 1; + t1 = $._current = lastIndex === 0 ? path : B.JSString_methods.substring$2(path, 0, lastIndex); + } + return t1; + }, + isAlphabetic(char) { + var t1; + if (!(char >= 65 && char <= 90)) + t1 = char >= 97 && char <= 122; + else + t1 = true; + return t1; + }, + isDriveLetter(path, index) { + var t3, + t1 = path.length, + t2 = index + 2; + if (t1 < t2) + return false; + if (!(index >= 0 && index < t1)) + return A.ioore(path, index); + if (!A.isAlphabetic(path.charCodeAt(index))) + return false; + t3 = index + 1; + if (!(t3 < t1)) + return A.ioore(path, t3); + if (path.charCodeAt(t3) !== 58) + return false; + if (t1 === t2) + return true; + if (!(t2 >= 0 && t2 < t1)) + return A.ioore(path, t2); + return path.charCodeAt(t2) === 47; + } + }, + B = {}; + var holders = [A, J, B]; + var $ = {}; + A.JS_CONST.prototype = {}; + J.Interceptor.prototype = { + $eq(receiver, other) { + return receiver === other; + }, + get$hashCode(receiver) { + return A.Primitives_objectHashCode(receiver); + }, + toString$0(receiver) { + return "Instance of '" + A.Primitives_objectTypeName(receiver) + "'"; + }, + noSuchMethod$1(receiver, invocation) { + throw A.wrapException(A.NoSuchMethodError_NoSuchMethodError$withInvocation(receiver, type$.Invocation._as(invocation))); + }, + get$runtimeType(receiver) { + return A.createRuntimeType(A._instanceTypeFromConstructor(this)); + } + }; + J.JSBool.prototype = { + toString$0(receiver) { + return String(receiver); + }, + get$hashCode(receiver) { + return receiver ? 519018 : 218159; + }, + get$runtimeType(receiver) { + return A.createRuntimeType(type$.bool); + }, + $isTrustedGetRuntimeType: 1, + $isbool: 1 + }; + J.JSNull.prototype = { + $eq(receiver, other) { + return null == other; + }, + toString$0(receiver) { + return "null"; + }, + get$hashCode(receiver) { + return 0; + }, + $isTrustedGetRuntimeType: 1, + $isNull: 1 + }; + J.JavaScriptObject.prototype = {}; + J.LegacyJavaScriptObject.prototype = { + get$hashCode(receiver) { + return 0; + }, + toString$0(receiver) { + return String(receiver); + } + }; + J.PlainJavaScriptObject.prototype = {}; + J.UnknownJavaScriptObject.prototype = {}; + J.JavaScriptFunction.prototype = { + toString$0(receiver) { + var dartClosure = receiver[$.$get$DART_CLOSURE_PROPERTY_NAME()]; + if (dartClosure == null) + return this.super$LegacyJavaScriptObject$toString(receiver); + return "JavaScript function for " + J.toString$0$(dartClosure); + }, + $isFunction: 1 + }; + J.JavaScriptBigInt.prototype = { + get$hashCode(receiver) { + return 0; + }, + toString$0(receiver) { + return String(receiver); + } + }; + J.JavaScriptSymbol.prototype = { + get$hashCode(receiver) { + return 0; + }, + toString$0(receiver) { + return String(receiver); + } + }; + J.JSArray.prototype = { + cast$1$0(receiver, $R) { + return new A.CastList(receiver, A._arrayInstanceType(receiver)._eval$1("@<1>")._bind$1($R)._eval$1("CastList<1,2>")); + }, + add$1(receiver, value) { + A._arrayInstanceType(receiver)._precomputed1._as(value); + if (!!receiver.fixed$length) + A.throwExpression(A.UnsupportedError$("add")); + receiver.push(value); + }, + removeAt$1(receiver, index) { + var t1; + if (!!receiver.fixed$length) + A.throwExpression(A.UnsupportedError$("removeAt")); + t1 = receiver.length; + if (index >= t1) + throw A.wrapException(A.RangeError$value(index, null)); + return receiver.splice(index, 1)[0]; + }, + insert$2(receiver, index, value) { + var t1; + A._arrayInstanceType(receiver)._precomputed1._as(value); + if (!!receiver.fixed$length) + A.throwExpression(A.UnsupportedError$("insert")); + t1 = receiver.length; + if (index > t1) + throw A.wrapException(A.RangeError$value(index, null)); + receiver.splice(index, 0, value); + }, + insertAll$2(receiver, index, iterable) { + var insertionLength, end; + A._arrayInstanceType(receiver)._eval$1("Iterable<1>")._as(iterable); + if (!!receiver.fixed$length) + A.throwExpression(A.UnsupportedError$("insertAll")); + A.RangeError_checkValueInInterval(index, 0, receiver.length, "index"); + if (!type$.EfficientLengthIterable_dynamic._is(iterable)) + iterable = J.toList$0$ax(iterable); + insertionLength = J.get$length$asx(iterable); + receiver.length = receiver.length + insertionLength; + end = index + insertionLength; + this.setRange$4(receiver, end, receiver.length, receiver, index); + this.setRange$3(receiver, index, end, iterable); + }, + removeLast$0(receiver) { + if (!!receiver.fixed$length) + A.throwExpression(A.UnsupportedError$("removeLast")); + if (receiver.length === 0) + throw A.wrapException(A.diagnoseIndexError(receiver, -1)); + return receiver.pop(); + }, + addAll$1(receiver, collection) { + var t1; + A._arrayInstanceType(receiver)._eval$1("Iterable<1>")._as(collection); + if (!!receiver.fixed$length) + A.throwExpression(A.UnsupportedError$("addAll")); + if (Array.isArray(collection)) { + this._addAllFromArray$1(receiver, collection); + return; + } + for (t1 = J.get$iterator$ax(collection); t1.moveNext$0();) + receiver.push(t1.get$current(t1)); + }, + _addAllFromArray$1(receiver, array) { + var len, i; + type$.JSArray_dynamic._as(array); + len = array.length; + if (len === 0) + return; + if (receiver === array) + throw A.wrapException(A.ConcurrentModificationError$(receiver)); + for (i = 0; i < len; ++i) + receiver.push(array[i]); + }, + clear$0(receiver) { + if (!!receiver.fixed$length) + A.throwExpression(A.UnsupportedError$("clear")); + receiver.length = 0; + }, + map$1$1(receiver, f, $T) { + var t1 = A._arrayInstanceType(receiver); + return new A.MappedListIterable(receiver, t1._bind$1($T)._eval$1("1(2)")._as(f), t1._eval$1("@<1>")._bind$1($T)._eval$1("MappedListIterable<1,2>")); + }, + join$1(receiver, separator) { + var i, + list = A.List_List$filled(receiver.length, "", false, type$.String); + for (i = 0; i < receiver.length; ++i) + this.$indexSet(list, i, A.S(receiver[i])); + return list.join(separator); + }, + join$0($receiver) { + return this.join$1($receiver, ""); + }, + skip$1(receiver, n) { + return A.SubListIterable$(receiver, n, null, A._arrayInstanceType(receiver)._precomputed1); + }, + fold$1$2(receiver, initialValue, combine, $T) { + var $length, value, i; + $T._as(initialValue); + A._arrayInstanceType(receiver)._bind$1($T)._eval$1("1(1,2)")._as(combine); + $length = receiver.length; + for (value = initialValue, i = 0; i < $length; ++i) { + value = combine.call$2(value, receiver[i]); + if (receiver.length !== $length) + throw A.wrapException(A.ConcurrentModificationError$(receiver)); + } + return value; + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + get$first(receiver) { + if (receiver.length > 0) + return receiver[0]; + throw A.wrapException(A.IterableElementError_noElement()); + }, + get$last(receiver) { + var t1 = receiver.length; + if (t1 > 0) + return receiver[t1 - 1]; + throw A.wrapException(A.IterableElementError_noElement()); + }, + setRange$4(receiver, start, end, iterable, skipCount) { + var $length, otherList, otherStart, t1, i; + A._arrayInstanceType(receiver)._eval$1("Iterable<1>")._as(iterable); + if (!!receiver.immutable$list) + A.throwExpression(A.UnsupportedError$("setRange")); + A.RangeError_checkValidRange(start, end, receiver.length); + $length = end - start; + if ($length === 0) + return; + A.RangeError_checkNotNegative(skipCount, "skipCount"); + if (type$.List_dynamic._is(iterable)) { + otherList = iterable; + otherStart = skipCount; + } else { + otherList = J.skip$1$ax(iterable, skipCount).toList$1$growable(0, false); + otherStart = 0; + } + t1 = J.getInterceptor$asx(otherList); + if (otherStart + $length > t1.get$length(otherList)) + throw A.wrapException(A.IterableElementError_tooFew()); + if (otherStart < start) + for (i = $length - 1; i >= 0; --i) + receiver[start + i] = t1.$index(otherList, otherStart + i); + else + for (i = 0; i < $length; ++i) + receiver[start + i] = t1.$index(otherList, otherStart + i); + }, + setRange$3($receiver, start, end, iterable) { + return this.setRange$4($receiver, start, end, iterable, 0); + }, + contains$1(receiver, other) { + var i; + for (i = 0; i < receiver.length; ++i) + if (J.$eq$(receiver[i], other)) + return true; + return false; + }, + get$isEmpty(receiver) { + return receiver.length === 0; + }, + get$isNotEmpty(receiver) { + return receiver.length !== 0; + }, + toString$0(receiver) { + return A.Iterable_iterableToFullString(receiver, "[", "]"); + }, + toList$1$growable(receiver, growable) { + var t1 = A._setArrayType(receiver.slice(0), A._arrayInstanceType(receiver)); + return t1; + }, + toList$0($receiver) { + return this.toList$1$growable($receiver, true); + }, + get$iterator(receiver) { + return new J.ArrayIterator(receiver, receiver.length, A._arrayInstanceType(receiver)._eval$1("ArrayIterator<1>")); + }, + get$hashCode(receiver) { + return A.Primitives_objectHashCode(receiver); + }, + get$length(receiver) { + return receiver.length; + }, + $index(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + throw A.wrapException(A.diagnoseIndexError(receiver, index)); + return receiver[index]; + }, + $indexSet(receiver, index, value) { + A._arrayInstanceType(receiver)._precomputed1._as(value); + if (!!receiver.immutable$list) + A.throwExpression(A.UnsupportedError$("indexed set")); + if (!(index >= 0 && index < receiver.length)) + throw A.wrapException(A.diagnoseIndexError(receiver, index)); + receiver[index] = value; + }, + $isEfficientLengthIterable: 1, + $isIterable: 1, + $isList: 1 + }; + J.JSUnmodifiableArray.prototype = {}; + J.ArrayIterator.prototype = { + get$current(_) { + var t1 = this._current; + return t1 == null ? this.$ti._precomputed1._as(t1) : t1; + }, + moveNext$0() { + var t2, _this = this, + t1 = _this._iterable, + $length = t1.length; + if (_this.__interceptors$_length !== $length) { + t1 = A.throwConcurrentModificationError(t1); + throw A.wrapException(t1); + } + t2 = _this._index; + if (t2 >= $length) { + _this.set$_current(null); + return false; + } + _this.set$_current(t1[t2]); + ++_this._index; + return true; + }, + set$_current(_current) { + this._current = this.$ti._eval$1("1?")._as(_current); + }, + $isIterator: 1 + }; + J.JSNumber.prototype = { + toInt$0(receiver) { + var t1; + if (receiver >= -2147483648 && receiver <= 2147483647) + return receiver | 0; + if (isFinite(receiver)) { + t1 = receiver < 0 ? Math.ceil(receiver) : Math.floor(receiver); + return t1 + 0; + } + throw A.wrapException(A.UnsupportedError$("" + receiver + ".toInt()")); + }, + toString$0(receiver) { + if (receiver === 0 && 1 / receiver < 0) + return "-0.0"; + else + return "" + receiver; + }, + get$hashCode(receiver) { + var absolute, floorLog2, factor, scaled, + intValue = receiver | 0; + if (receiver === intValue) + return intValue & 536870911; + absolute = Math.abs(receiver); + floorLog2 = Math.log(absolute) / 0.6931471805599453 | 0; + factor = Math.pow(2, floorLog2); + scaled = absolute < 1 ? absolute / factor : factor / absolute; + return ((scaled * 9007199254740992 | 0) + (scaled * 3542243181176521 | 0)) * 599197 + floorLog2 * 1259 & 536870911; + }, + $mod(receiver, other) { + var result = receiver % other; + if (result === 0) + return 0; + if (result > 0) + return result; + return result + other; + }, + $tdiv(receiver, other) { + if ((receiver | 0) === receiver) + if (other >= 1 || false) + return receiver / other | 0; + return this._tdivSlow$1(receiver, other); + }, + _tdivFast$1(receiver, other) { + return (receiver | 0) === receiver ? receiver / other | 0 : this._tdivSlow$1(receiver, other); + }, + _tdivSlow$1(receiver, other) { + var quotient = receiver / other; + if (quotient >= -2147483648 && quotient <= 2147483647) + return quotient | 0; + if (quotient > 0) { + if (quotient !== 1 / 0) + return Math.floor(quotient); + } else if (quotient > -1 / 0) + return Math.ceil(quotient); + throw A.wrapException(A.UnsupportedError$("Result of truncating division is " + A.S(quotient) + ": " + A.S(receiver) + " ~/ " + other)); + }, + _shrOtherPositive$1(receiver, other) { + var t1; + if (receiver > 0) + t1 = this._shrBothPositive$1(receiver, other); + else { + t1 = other > 31 ? 31 : other; + t1 = receiver >> t1 >>> 0; + } + return t1; + }, + _shrReceiverPositive$1(receiver, other) { + if (0 > other) + throw A.wrapException(A.argumentErrorValue(other)); + return this._shrBothPositive$1(receiver, other); + }, + _shrBothPositive$1(receiver, other) { + return other > 31 ? 0 : receiver >>> other; + }, + get$runtimeType(receiver) { + return A.createRuntimeType(type$.num); + }, + $isdouble: 1, + $isnum: 1 + }; + J.JSInt.prototype = { + get$runtimeType(receiver) { + return A.createRuntimeType(type$.int); + }, + $isTrustedGetRuntimeType: 1, + $isint: 1 + }; + J.JSNumNotInt.prototype = { + get$runtimeType(receiver) { + return A.createRuntimeType(type$.double); + }, + $isTrustedGetRuntimeType: 1 + }; + J.JSString.prototype = { + codeUnitAt$1(receiver, index) { + if (index < 0) + throw A.wrapException(A.diagnoseIndexError(receiver, index)); + if (index >= receiver.length) + A.throwExpression(A.diagnoseIndexError(receiver, index)); + return receiver.charCodeAt(index); + }, + allMatches$2(receiver, string, start) { + var t1 = string.length; + if (start > t1) + throw A.wrapException(A.RangeError$range(start, 0, t1, null, null)); + return new A._StringAllMatchesIterable(string, receiver, start); + }, + allMatches$1($receiver, string) { + return this.allMatches$2($receiver, string, 0); + }, + matchAsPrefix$2(receiver, string, start) { + var t1, t2, i, t3, _null = null; + if (start < 0 || start > string.length) + throw A.wrapException(A.RangeError$range(start, 0, string.length, _null, _null)); + t1 = receiver.length; + t2 = string.length; + if (start + t1 > t2) + return _null; + for (i = 0; i < t1; ++i) { + t3 = start + i; + if (!(t3 >= 0 && t3 < t2)) + return A.ioore(string, t3); + if (string.charCodeAt(t3) !== receiver.charCodeAt(i)) + return _null; + } + return new A.StringMatch(start, receiver); + }, + $add(receiver, other) { + return receiver + other; + }, + endsWith$1(receiver, other) { + var otherLength = other.length, + t1 = receiver.length; + if (otherLength > t1) + return false; + return other === this.substring$1(receiver, t1 - otherLength); + }, + replaceFirst$2(receiver, from, to) { + A.RangeError_checkValueInInterval(0, 0, receiver.length, "startIndex"); + return A.stringReplaceFirstUnchecked(receiver, from, to, 0); + }, + split$1(receiver, pattern) { + if (typeof pattern == "string") + return A._setArrayType(receiver.split(pattern), type$.JSArray_String); + else if (pattern instanceof A.JSSyntaxRegExp && pattern.get$_nativeAnchoredVersion().exec("").length - 2 === 0) + return A._setArrayType(receiver.split(pattern._nativeRegExp), type$.JSArray_String); + else + return this._defaultSplit$1(receiver, pattern); + }, + replaceRange$3(receiver, start, end, replacement) { + var e = A.RangeError_checkValidRange(start, end, receiver.length); + return A.stringReplaceRangeUnchecked(receiver, start, e, replacement); + }, + _defaultSplit$1(receiver, pattern) { + var t1, start, $length, match, matchStart, matchEnd, + result = A._setArrayType([], type$.JSArray_String); + for (t1 = J.allMatches$1$s(pattern, receiver), t1 = t1.get$iterator(t1), start = 0, $length = 1; t1.moveNext$0();) { + match = t1.get$current(t1); + matchStart = match.get$start(match); + matchEnd = match.get$end(match); + $length = matchEnd - matchStart; + if ($length === 0 && start === matchStart) + continue; + B.JSArray_methods.add$1(result, this.substring$2(receiver, start, matchStart)); + start = matchEnd; + } + if (start < receiver.length || $length > 0) + B.JSArray_methods.add$1(result, this.substring$1(receiver, start)); + return result; + }, + startsWith$2(receiver, pattern, index) { + var endIndex; + if (index < 0 || index > receiver.length) + throw A.wrapException(A.RangeError$range(index, 0, receiver.length, null, null)); + if (typeof pattern == "string") { + endIndex = index + pattern.length; + if (endIndex > receiver.length) + return false; + return pattern === receiver.substring(index, endIndex); + } + return J.matchAsPrefix$2$s(pattern, receiver, index) != null; + }, + startsWith$1($receiver, pattern) { + return this.startsWith$2($receiver, pattern, 0); + }, + substring$2(receiver, start, end) { + return receiver.substring(start, A.RangeError_checkValidRange(start, end, receiver.length)); + }, + substring$1($receiver, start) { + return this.substring$2($receiver, start, null); + }, + trim$0(receiver) { + var startIndex, t1, endIndex0, + result = receiver.trim(), + endIndex = result.length; + if (endIndex === 0) + return result; + if (0 >= endIndex) + return A.ioore(result, 0); + if (result.charCodeAt(0) === 133) { + startIndex = J.JSString__skipLeadingWhitespace(result, 1); + if (startIndex === endIndex) + return ""; + } else + startIndex = 0; + t1 = endIndex - 1; + if (!(t1 >= 0)) + return A.ioore(result, t1); + endIndex0 = result.charCodeAt(t1) === 133 ? J.JSString__skipTrailingWhitespace(result, t1) : endIndex; + if (startIndex === 0 && endIndex0 === endIndex) + return result; + return result.substring(startIndex, endIndex0); + }, + $mul(receiver, times) { + var s, result; + if (0 >= times) + return ""; + if (times === 1 || receiver.length === 0) + return receiver; + if (times !== times >>> 0) + throw A.wrapException(B.C_OutOfMemoryError); + for (s = receiver, result = ""; true;) { + if ((times & 1) === 1) + result = s + result; + times = times >>> 1; + if (times === 0) + break; + s += s; + } + return result; + }, + padLeft$2(receiver, width, padding) { + var delta = width - receiver.length; + if (delta <= 0) + return receiver; + return this.$mul(padding, delta) + receiver; + }, + padRight$1(receiver, width) { + var delta = width - receiver.length; + if (delta <= 0) + return receiver; + return receiver + this.$mul(" ", delta); + }, + indexOf$2(receiver, pattern, start) { + var t1; + if (start < 0 || start > receiver.length) + throw A.wrapException(A.RangeError$range(start, 0, receiver.length, null, null)); + t1 = receiver.indexOf(pattern, start); + return t1; + }, + indexOf$1($receiver, pattern) { + return this.indexOf$2($receiver, pattern, 0); + }, + lastIndexOf$2(receiver, pattern, start) { + var t1, t2; + if (start == null) + start = receiver.length; + else if (start < 0 || start > receiver.length) + throw A.wrapException(A.RangeError$range(start, 0, receiver.length, null, null)); + t1 = pattern.length; + t2 = receiver.length; + if (start + t1 > t2) + start = t2 - t1; + return receiver.lastIndexOf(pattern, start); + }, + lastIndexOf$1($receiver, pattern) { + return this.lastIndexOf$2($receiver, pattern, null); + }, + contains$1(receiver, other) { + return A.stringContainsUnchecked(receiver, other, 0); + }, + toString$0(receiver) { + return receiver; + }, + get$hashCode(receiver) { + var t1, hash, i; + for (t1 = receiver.length, hash = 0, i = 0; i < t1; ++i) { + hash = hash + receiver.charCodeAt(i) & 536870911; + hash = hash + ((hash & 524287) << 10) & 536870911; + hash ^= hash >> 6; + } + hash = hash + ((hash & 67108863) << 3) & 536870911; + hash ^= hash >> 11; + return hash + ((hash & 16383) << 15) & 536870911; + }, + get$runtimeType(receiver) { + return A.createRuntimeType(type$.String); + }, + get$length(receiver) { + return receiver.length; + }, + $isTrustedGetRuntimeType: 1, + $isPattern: 1, + $isString: 1 + }; + A.CastStream.prototype = { + listen$4$cancelOnError$onDone$onError(onData, cancelOnError, onDone, onError) { + var t2, + t1 = this.$ti; + t1._eval$1("~(2)?")._as(onData); + t2 = this._source.listen$3$cancelOnError$onDone(null, cancelOnError, type$.nullable_void_Function._as(onDone)); + t1 = new A.CastStreamSubscription(t2, $.Zone__current, t1._eval$1("@<1>")._bind$1(t1._rest[1])._eval$1("CastStreamSubscription<1,2>")); + t2.onData$1(t1.get$__internal$_onData()); + t1.onData$1(onData); + t1.onError$1(0, onError); + return t1; + }, + listen$1(onData) { + return this.listen$4$cancelOnError$onDone$onError(onData, null, null, null); + }, + listen$3$onDone$onError(onData, onDone, onError) { + return this.listen$4$cancelOnError$onDone$onError(onData, null, onDone, onError); + }, + listen$3$cancelOnError$onDone(onData, cancelOnError, onDone) { + return this.listen$4$cancelOnError$onDone$onError(onData, cancelOnError, onDone, null); + } + }; + A.CastStreamSubscription.prototype = { + cancel$0(_) { + return this._source.cancel$0(0); + }, + onData$1(handleData) { + var t1 = this.$ti; + t1._eval$1("~(2)?")._as(handleData); + this.set$_handleData(handleData == null ? null : this.__internal$_zone.registerUnaryCallback$2$1(handleData, type$.dynamic, t1._rest[1])); + }, + onError$1(_, handleError) { + var _this = this; + _this._source.onError$1(0, handleError); + if (handleError == null) + _this._handleError = null; + else if (type$.void_Function_Object_StackTrace._is(handleError)) + _this._handleError = _this.__internal$_zone.registerBinaryCallback$3$1(handleError, type$.dynamic, type$.Object, type$.StackTrace); + else if (type$.void_Function_Object._is(handleError)) + _this._handleError = _this.__internal$_zone.registerUnaryCallback$2$1(handleError, type$.dynamic, type$.Object); + else + throw A.wrapException(A.ArgumentError$(string$.handle, null)); + }, + __internal$_onData$1(data) { + var targetData, error, stack, handleError, t2, exception, _this = this, + t1 = _this.$ti; + t1._precomputed1._as(data); + t2 = _this._handleData; + if (t2 == null) + return; + targetData = null; + try { + targetData = t1._rest[1]._as(data); + } catch (exception) { + error = A.unwrapException(exception); + stack = A.getTraceFromException(exception); + handleError = _this._handleError; + if (handleError == null) + _this.__internal$_zone.handleUncaughtError$2(error, stack); + else { + t1 = type$.Object; + t2 = _this.__internal$_zone; + if (type$.void_Function_Object_StackTrace._is(handleError)) + t2.runBinaryGuarded$2$3(handleError, error, stack, t1, type$.StackTrace); + else + t2.runUnaryGuarded$1$2(type$.void_Function_Object._as(handleError), error, t1); + } + return; + } + _this.__internal$_zone.runUnaryGuarded$1$2(t2, targetData, t1._rest[1]); + }, + set$_handleData(_handleData) { + this._handleData = this.$ti._eval$1("~(2)?")._as(_handleData); + }, + $isStreamSubscription: 1 + }; + A._CastIterableBase.prototype = { + get$iterator(_) { + var t1 = A._instanceType(this); + return new A.CastIterator(J.get$iterator$ax(this.get$_source()), t1._eval$1("@<1>")._bind$1(t1._rest[1])._eval$1("CastIterator<1,2>")); + }, + get$length(_) { + return J.get$length$asx(this.get$_source()); + }, + get$isEmpty(_) { + return J.get$isEmpty$asx(this.get$_source()); + }, + get$isNotEmpty(_) { + return J.get$isNotEmpty$asx(this.get$_source()); + }, + skip$1(_, count) { + var t1 = A._instanceType(this); + return A.CastIterable_CastIterable(J.skip$1$ax(this.get$_source(), count), t1._precomputed1, t1._rest[1]); + }, + elementAt$1(_, index) { + return A._instanceType(this)._rest[1]._as(J.elementAt$1$ax(this.get$_source(), index)); + }, + toString$0(_) { + return J.toString$0$(this.get$_source()); + } + }; + A.CastIterator.prototype = { + moveNext$0() { + return this._source.moveNext$0(); + }, + get$current(_) { + var t1 = this._source; + return this.$ti._rest[1]._as(t1.get$current(t1)); + }, + $isIterator: 1 + }; + A.CastIterable.prototype = { + get$_source() { + return this._source; + } + }; + A._EfficientLengthCastIterable.prototype = {$isEfficientLengthIterable: 1}; + A._CastListBase.prototype = { + $index(_, index) { + return this.$ti._rest[1]._as(J.$index$asx(this._source, index)); + }, + $indexSet(_, index, value) { + var t1 = this.$ti; + J.$indexSet$ax(this._source, index, t1._precomputed1._as(t1._rest[1]._as(value))); + }, + $isEfficientLengthIterable: 1, + $isList: 1 + }; + A.CastList.prototype = { + cast$1$0(_, $R) { + return new A.CastList(this._source, this.$ti._eval$1("@<1>")._bind$1($R)._eval$1("CastList<1,2>")); + }, + get$_source() { + return this._source; + } + }; + A.LateError.prototype = { + toString$0(_) { + return "LateInitializationError: " + this._message; + } + }; + A.CodeUnits.prototype = { + get$length(_) { + return this._string.length; + }, + $index(_, i) { + var t1 = this._string; + if (!(i >= 0 && i < t1.length)) + return A.ioore(t1, i); + return t1.charCodeAt(i); + } + }; + A.nullFuture_closure.prototype = { + call$0() { + return A.Future_Future$value(null, type$.Null); + }, + $signature: 60 + }; + A.SentinelValue.prototype = {}; + A.EfficientLengthIterable.prototype = {}; + A.ListIterable.prototype = { + get$iterator(_) { + var _this = this; + return new A.ListIterator(_this, _this.get$length(_this), A._instanceType(_this)._eval$1("ListIterator<ListIterable.E>")); + }, + get$isEmpty(_) { + return this.get$length(this) === 0; + }, + join$1(_, separator) { + var first, t1, i, _this = this, + $length = _this.get$length(_this); + if (separator.length !== 0) { + if ($length === 0) + return ""; + first = A.S(_this.elementAt$1(0, 0)); + if ($length !== _this.get$length(_this)) + throw A.wrapException(A.ConcurrentModificationError$(_this)); + for (t1 = first, i = 1; i < $length; ++i) { + t1 = t1 + separator + A.S(_this.elementAt$1(0, i)); + if ($length !== _this.get$length(_this)) + throw A.wrapException(A.ConcurrentModificationError$(_this)); + } + return t1.charCodeAt(0) == 0 ? t1 : t1; + } else { + for (i = 0, t1 = ""; i < $length; ++i) { + t1 += A.S(_this.elementAt$1(0, i)); + if ($length !== _this.get$length(_this)) + throw A.wrapException(A.ConcurrentModificationError$(_this)); + } + return t1.charCodeAt(0) == 0 ? t1 : t1; + } + }, + join$0($receiver) { + return this.join$1($receiver, ""); + }, + map$1$1(_, toElement, $T) { + var t1 = A._instanceType(this); + return new A.MappedListIterable(this, t1._bind$1($T)._eval$1("1(ListIterable.E)")._as(toElement), t1._eval$1("@<ListIterable.E>")._bind$1($T)._eval$1("MappedListIterable<1,2>")); + }, + fold$1$2(_, initialValue, combine, $T) { + var $length, value, i, _this = this; + $T._as(initialValue); + A._instanceType(_this)._bind$1($T)._eval$1("1(1,ListIterable.E)")._as(combine); + $length = _this.get$length(_this); + for (value = initialValue, i = 0; i < $length; ++i) { + value = combine.call$2(value, _this.elementAt$1(0, i)); + if ($length !== _this.get$length(_this)) + throw A.wrapException(A.ConcurrentModificationError$(_this)); + } + return value; + }, + skip$1(_, count) { + return A.SubListIterable$(this, count, null, A._instanceType(this)._eval$1("ListIterable.E")); + } + }; + A.SubListIterable.prototype = { + SubListIterable$3(_iterable, _start, _endOrLength, $E) { + var endOrLength, + t1 = this._start; + A.RangeError_checkNotNegative(t1, "start"); + endOrLength = this._endOrLength; + if (endOrLength != null) { + A.RangeError_checkNotNegative(endOrLength, "end"); + if (t1 > endOrLength) + throw A.wrapException(A.RangeError$range(t1, 0, endOrLength, "start", null)); + } + }, + get$_endIndex() { + var $length = J.get$length$asx(this.__internal$_iterable), + endOrLength = this._endOrLength; + if (endOrLength == null || endOrLength > $length) + return $length; + return endOrLength; + }, + get$_startIndex() { + var $length = J.get$length$asx(this.__internal$_iterable), + t1 = this._start; + if (t1 > $length) + return $length; + return t1; + }, + get$length(_) { + var endOrLength, + $length = J.get$length$asx(this.__internal$_iterable), + t1 = this._start; + if (t1 >= $length) + return 0; + endOrLength = this._endOrLength; + if (endOrLength == null || endOrLength >= $length) + return $length - t1; + if (typeof endOrLength !== "number") + return endOrLength.$sub(); + return endOrLength - t1; + }, + elementAt$1(_, index) { + var _this = this, + realIndex = _this.get$_startIndex() + index; + if (index < 0 || realIndex >= _this.get$_endIndex()) + throw A.wrapException(A.IndexError$withLength(index, _this.get$length(_this), _this, "index")); + return J.elementAt$1$ax(_this.__internal$_iterable, realIndex); + }, + skip$1(_, count) { + var newStart, endOrLength, _this = this; + A.RangeError_checkNotNegative(count, "count"); + newStart = _this._start + count; + endOrLength = _this._endOrLength; + if (endOrLength != null && newStart >= endOrLength) + return new A.EmptyIterable(_this.$ti._eval$1("EmptyIterable<1>")); + return A.SubListIterable$(_this.__internal$_iterable, newStart, endOrLength, _this.$ti._precomputed1); + }, + toList$1$growable(_, growable) { + var $length, result, i, _this = this, + start = _this._start, + t1 = _this.__internal$_iterable, + t2 = J.getInterceptor$asx(t1), + end = t2.get$length(t1), + endOrLength = _this._endOrLength; + if (endOrLength != null && endOrLength < end) + end = endOrLength; + $length = end - start; + if ($length <= 0) { + t1 = J.JSArray_JSArray$fixed(0, _this.$ti._precomputed1); + return t1; + } + result = A.List_List$filled($length, t2.elementAt$1(t1, start), false, _this.$ti._precomputed1); + for (i = 1; i < $length; ++i) { + B.JSArray_methods.$indexSet(result, i, t2.elementAt$1(t1, start + i)); + if (t2.get$length(t1) < end) + throw A.wrapException(A.ConcurrentModificationError$(_this)); + } + return result; + } + }; + A.ListIterator.prototype = { + get$current(_) { + var t1 = this.__internal$_current; + return t1 == null ? this.$ti._precomputed1._as(t1) : t1; + }, + moveNext$0() { + var t3, _this = this, + t1 = _this.__internal$_iterable, + t2 = J.getInterceptor$asx(t1), + $length = t2.get$length(t1); + if (_this.__internal$_length !== $length) + throw A.wrapException(A.ConcurrentModificationError$(t1)); + t3 = _this.__internal$_index; + if (t3 >= $length) { + _this.set$__internal$_current(null); + return false; + } + _this.set$__internal$_current(t2.elementAt$1(t1, t3)); + ++_this.__internal$_index; + return true; + }, + set$__internal$_current(_current) { + this.__internal$_current = this.$ti._eval$1("1?")._as(_current); + }, + $isIterator: 1 + }; + A.MappedIterable.prototype = { + get$iterator(_) { + var t1 = A._instanceType(this); + return new A.MappedIterator(J.get$iterator$ax(this.__internal$_iterable), this._f, t1._eval$1("@<1>")._bind$1(t1._rest[1])._eval$1("MappedIterator<1,2>")); + }, + get$length(_) { + return J.get$length$asx(this.__internal$_iterable); + }, + get$isEmpty(_) { + return J.get$isEmpty$asx(this.__internal$_iterable); + }, + elementAt$1(_, index) { + return this._f.call$1(J.elementAt$1$ax(this.__internal$_iterable, index)); + } + }; + A.EfficientLengthMappedIterable.prototype = {$isEfficientLengthIterable: 1}; + A.MappedIterator.prototype = { + moveNext$0() { + var _this = this, + t1 = _this._iterator; + if (t1.moveNext$0()) { + _this.set$__internal$_current(_this._f.call$1(t1.get$current(t1))); + return true; + } + _this.set$__internal$_current(null); + return false; + }, + get$current(_) { + var t1 = this.__internal$_current; + return t1 == null ? this.$ti._rest[1]._as(t1) : t1; + }, + set$__internal$_current(_current) { + this.__internal$_current = this.$ti._eval$1("2?")._as(_current); + }, + $isIterator: 1 + }; + A.MappedListIterable.prototype = { + get$length(_) { + return J.get$length$asx(this._source); + }, + elementAt$1(_, index) { + return this._f.call$1(J.elementAt$1$ax(this._source, index)); + } + }; + A.WhereIterable.prototype = { + get$iterator(_) { + return new A.WhereIterator(J.get$iterator$ax(this.__internal$_iterable), this._f, this.$ti._eval$1("WhereIterator<1>")); + }, + map$1$1(_, toElement, $T) { + var t1 = this.$ti; + return new A.MappedIterable(this, t1._bind$1($T)._eval$1("1(2)")._as(toElement), t1._eval$1("@<1>")._bind$1($T)._eval$1("MappedIterable<1,2>")); + } + }; + A.WhereIterator.prototype = { + moveNext$0() { + var t1, t2; + for (t1 = this._iterator, t2 = this._f; t1.moveNext$0();) + if (A.boolConversionCheck(t2.call$1(t1.get$current(t1)))) + return true; + return false; + }, + get$current(_) { + var t1 = this._iterator; + return t1.get$current(t1); + }, + $isIterator: 1 + }; + A.ExpandIterable.prototype = { + get$iterator(_) { + var t1 = this.$ti; + return new A.ExpandIterator(J.get$iterator$ax(this.__internal$_iterable), this._f, B.C_EmptyIterator, t1._eval$1("@<1>")._bind$1(t1._rest[1])._eval$1("ExpandIterator<1,2>")); + } + }; + A.ExpandIterator.prototype = { + get$current(_) { + var t1 = this.__internal$_current; + return t1 == null ? this.$ti._rest[1]._as(t1) : t1; + }, + moveNext$0() { + var t1, t2, _this = this; + if (_this._currentExpansion == null) + return false; + for (t1 = _this._iterator, t2 = _this._f; !_this._currentExpansion.moveNext$0();) { + _this.set$__internal$_current(null); + if (t1.moveNext$0()) { + _this.set$_currentExpansion(null); + _this.set$_currentExpansion(J.get$iterator$ax(t2.call$1(t1.get$current(t1)))); + } else + return false; + } + t1 = _this._currentExpansion; + _this.set$__internal$_current(t1.get$current(t1)); + return true; + }, + set$_currentExpansion(_currentExpansion) { + this._currentExpansion = this.$ti._eval$1("Iterator<2>?")._as(_currentExpansion); + }, + set$__internal$_current(_current) { + this.__internal$_current = this.$ti._eval$1("2?")._as(_current); + }, + $isIterator: 1 + }; + A.TakeIterable.prototype = { + get$iterator(_) { + return new A.TakeIterator(J.get$iterator$ax(this.__internal$_iterable), this._takeCount, A._instanceType(this)._eval$1("TakeIterator<1>")); + } + }; + A.EfficientLengthTakeIterable.prototype = { + get$length(_) { + var iterableLength = J.get$length$asx(this.__internal$_iterable), + t1 = this._takeCount; + if (iterableLength > t1) + return t1; + return iterableLength; + }, + $isEfficientLengthIterable: 1 + }; + A.TakeIterator.prototype = { + moveNext$0() { + if (--this._remaining >= 0) + return this._iterator.moveNext$0(); + this._remaining = -1; + return false; + }, + get$current(_) { + var t1; + if (this._remaining < 0) { + this.$ti._precomputed1._as(null); + return null; + } + t1 = this._iterator; + return t1.get$current(t1); + }, + $isIterator: 1 + }; + A.SkipIterable.prototype = { + skip$1(_, count) { + A.ArgumentError_checkNotNull(count, "count", type$.int); + A.RangeError_checkNotNegative(count, "count"); + return new A.SkipIterable(this.__internal$_iterable, this._skipCount + count, A._instanceType(this)._eval$1("SkipIterable<1>")); + }, + get$iterator(_) { + return new A.SkipIterator(J.get$iterator$ax(this.__internal$_iterable), this._skipCount, A._instanceType(this)._eval$1("SkipIterator<1>")); + } + }; + A.EfficientLengthSkipIterable.prototype = { + get$length(_) { + var $length = J.get$length$asx(this.__internal$_iterable) - this._skipCount; + if ($length >= 0) + return $length; + return 0; + }, + skip$1(_, count) { + A.ArgumentError_checkNotNull(count, "count", type$.int); + A.RangeError_checkNotNegative(count, "count"); + return new A.EfficientLengthSkipIterable(this.__internal$_iterable, this._skipCount + count, this.$ti); + }, + $isEfficientLengthIterable: 1 + }; + A.SkipIterator.prototype = { + moveNext$0() { + var t1, i; + for (t1 = this._iterator, i = 0; i < this._skipCount; ++i) + t1.moveNext$0(); + this._skipCount = 0; + return t1.moveNext$0(); + }, + get$current(_) { + var t1 = this._iterator; + return t1.get$current(t1); + }, + $isIterator: 1 + }; + A.SkipWhileIterable.prototype = { + get$iterator(_) { + return new A.SkipWhileIterator(J.get$iterator$ax(this.__internal$_iterable), this._f, this.$ti._eval$1("SkipWhileIterator<1>")); + } + }; + A.SkipWhileIterator.prototype = { + moveNext$0() { + var t1, t2, _this = this; + if (!_this._hasSkipped) { + _this._hasSkipped = true; + for (t1 = _this._iterator, t2 = _this._f; t1.moveNext$0();) + if (!A.boolConversionCheck(t2.call$1(t1.get$current(t1)))) + return true; + } + return _this._iterator.moveNext$0(); + }, + get$current(_) { + var t1 = this._iterator; + return t1.get$current(t1); + }, + $isIterator: 1 + }; + A.EmptyIterable.prototype = { + get$iterator(_) { + return B.C_EmptyIterator; + }, + get$isEmpty(_) { + return true; + }, + get$length(_) { + return 0; + }, + elementAt$1(_, index) { + throw A.wrapException(A.RangeError$range(index, 0, 0, "index", null)); + }, + map$1$1(_, toElement, $T) { + this.$ti._bind$1($T)._eval$1("1(2)")._as(toElement); + return new A.EmptyIterable($T._eval$1("EmptyIterable<0>")); + }, + skip$1(_, count) { + A.RangeError_checkNotNegative(count, "count"); + return this; + } + }; + A.EmptyIterator.prototype = { + moveNext$0() { + return false; + }, + get$current(_) { + throw A.wrapException(A.IterableElementError_noElement()); + }, + $isIterator: 1 + }; + A.WhereTypeIterable.prototype = { + get$iterator(_) { + return new A.WhereTypeIterator(J.get$iterator$ax(this._source), this.$ti._eval$1("WhereTypeIterator<1>")); + } + }; + A.WhereTypeIterator.prototype = { + moveNext$0() { + var t1, t2; + for (t1 = this._source, t2 = this.$ti._precomputed1; t1.moveNext$0();) + if (t2._is(t1.get$current(t1))) + return true; + return false; + }, + get$current(_) { + var t1 = this._source; + return this.$ti._precomputed1._as(t1.get$current(t1)); + }, + $isIterator: 1 + }; + A.FixedLengthListMixin.prototype = {}; + A.UnmodifiableListMixin.prototype = { + $indexSet(_, index, value) { + A._instanceType(this)._eval$1("UnmodifiableListMixin.E")._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot modify an unmodifiable list")); + } + }; + A.UnmodifiableListBase.prototype = {}; + A.ReversedListIterable.prototype = { + get$length(_) { + return J.get$length$asx(this._source); + }, + elementAt$1(_, index) { + var t1 = this._source, + t2 = J.getInterceptor$asx(t1); + return t2.elementAt$1(t1, t2.get$length(t1) - 1 - index); + } + }; + A.Symbol.prototype = { + get$hashCode(_) { + var hash = this._hashCode; + if (hash != null) + return hash; + hash = 664597 * B.JSString_methods.get$hashCode(this._name) & 536870911; + this._hashCode = hash; + return hash; + }, + toString$0(_) { + return 'Symbol("' + this._name + '")'; + }, + $eq(_, other) { + if (other == null) + return false; + return other instanceof A.Symbol && this._name === other._name; + }, + $isSymbol0: 1 + }; + A.__CastListBase__CastIterableBase_ListMixin.prototype = {}; + A.ConstantMapView.prototype = {}; + A.ConstantMap.prototype = { + get$isEmpty(_) { + return this.get$length(this) === 0; + }, + toString$0(_) { + return A.MapBase_mapToString(this); + }, + $indexSet(_, key, value) { + var t1 = A._instanceType(this); + t1._precomputed1._as(key); + t1._rest[1]._as(value); + A.ConstantMap__throwUnmodifiable(); + }, + $isMap: 1 + }; + A.ConstantStringMap.prototype = { + get$length(_) { + return this._values.length; + }, + get$__js_helper$_keys() { + var keys = this.$keys; + if (keys == null) { + keys = Object.keys(this._jsIndex); + this.$keys = keys; + } + return keys; + }, + containsKey$1(_, key) { + if (typeof key != "string") + return false; + if ("__proto__" === key) + return false; + return this._jsIndex.hasOwnProperty(key); + }, + $index(_, key) { + if (!this.containsKey$1(0, key)) + return null; + return this._values[this._jsIndex[key]]; + }, + forEach$1(_, f) { + var keys, values, t1, i; + this.$ti._eval$1("~(1,2)")._as(f); + keys = this.get$__js_helper$_keys(); + values = this._values; + for (t1 = keys.length, i = 0; i < t1; ++i) + f.call$2(keys[i], values[i]); + }, + get$keys(_) { + return new A._KeysOrValues(this.get$__js_helper$_keys(), this.$ti._eval$1("_KeysOrValues<1>")); + } + }; + A._KeysOrValues.prototype = { + get$length(_) { + return this._elements.length; + }, + get$isEmpty(_) { + return 0 === this._elements.length; + }, + get$isNotEmpty(_) { + return 0 !== this._elements.length; + }, + get$iterator(_) { + var t1 = this._elements; + return new A._KeysOrValuesOrElementsIterator(t1, t1.length, this.$ti._eval$1("_KeysOrValuesOrElementsIterator<1>")); + } + }; + A._KeysOrValuesOrElementsIterator.prototype = { + get$current(_) { + var t1 = this.__js_helper$_current; + return t1 == null ? this.$ti._precomputed1._as(t1) : t1; + }, + moveNext$0() { + var _this = this, + t1 = _this.__js_helper$_index; + if (t1 >= _this._length) { + _this.set$__js_helper$_current(null); + return false; + } + _this.set$__js_helper$_current(_this._elements[t1]); + ++_this.__js_helper$_index; + return true; + }, + set$__js_helper$_current(_current) { + this.__js_helper$_current = this.$ti._eval$1("1?")._as(_current); + }, + $isIterator: 1 + }; + A.Instantiation.prototype = { + $eq(_, other) { + if (other == null) + return false; + return other instanceof A.Instantiation1 && this._genericClosure.$eq(0, other._genericClosure) && A.getRuntimeTypeOfClosure(this) === A.getRuntimeTypeOfClosure(other); + }, + get$hashCode(_) { + return A.Object_hash(this._genericClosure, A.getRuntimeTypeOfClosure(this), B.C_SentinelValue, B.C_SentinelValue); + }, + toString$0(_) { + var t1 = B.JSArray_methods.join$1([A.createRuntimeType(this.$ti._precomputed1)], ", "); + return this._genericClosure.toString$0(0) + " with " + ("<" + t1 + ">"); + } + }; + A.Instantiation1.prototype = { + call$2(a0, a1) { + return this._genericClosure.call$1$2(a0, a1, this.$ti._rest[0]); + }, + call$4(a0, a1, a2, a3) { + return this._genericClosure.call$1$4(a0, a1, a2, a3, this.$ti._rest[0]); + }, + $signature() { + return A.instantiatedGenericFunctionType(A.closureFunctionType(this._genericClosure), this.$ti); + } + }; + A.JSInvocationMirror.prototype = { + get$memberName() { + var t1 = this._memberName; + return t1; + }, + get$positionalArguments() { + var t1, argumentCount, list, index, _this = this; + if (_this.__js_helper$_kind === 1) + return B.List_empty0; + t1 = _this._arguments; + argumentCount = t1.length - _this._namedArgumentNames.length - _this._typeArgumentCount; + if (argumentCount === 0) + return B.List_empty0; + list = []; + for (index = 0; index < argumentCount; ++index) { + if (!(index < t1.length)) + return A.ioore(t1, index); + list.push(t1[index]); + } + return J.JSArray_markUnmodifiableList(list); + }, + get$namedArguments() { + var t1, namedArgumentCount, t2, namedArgumentsStartIndex, map, i, t3, t4, _this = this; + if (_this.__js_helper$_kind !== 0) + return B.Map_empty0; + t1 = _this._namedArgumentNames; + namedArgumentCount = t1.length; + t2 = _this._arguments; + namedArgumentsStartIndex = t2.length - namedArgumentCount - _this._typeArgumentCount; + if (namedArgumentCount === 0) + return B.Map_empty0; + map = new A.JsLinkedHashMap(type$.JsLinkedHashMap_Symbol_dynamic); + for (i = 0; i < namedArgumentCount; ++i) { + if (!(i < t1.length)) + return A.ioore(t1, i); + t3 = t1[i]; + t4 = namedArgumentsStartIndex + i; + if (!(t4 >= 0 && t4 < t2.length)) + return A.ioore(t2, t4); + map.$indexSet(0, new A.Symbol(t3), t2[t4]); + } + return new A.ConstantMapView(map, type$.ConstantMapView_Symbol_dynamic); + }, + $isInvocation: 1 + }; + A.Primitives_functionNoSuchMethod_closure.prototype = { + call$2($name, argument) { + var t1; + A._asString($name); + t1 = this._box_0; + t1.names = t1.names + "$" + $name; + B.JSArray_methods.add$1(this.namedArgumentList, $name); + B.JSArray_methods.add$1(this.$arguments, argument); + ++t1.argumentCount; + }, + $signature: 4 + }; + A.TypeErrorDecoder.prototype = { + matchTypeError$1(message) { + var result, t1, _this = this, + match = new RegExp(_this._pattern).exec(message); + if (match == null) + return null; + result = Object.create(null); + t1 = _this._arguments; + if (t1 !== -1) + result.arguments = match[t1 + 1]; + t1 = _this._argumentsExpr; + if (t1 !== -1) + result.argumentsExpr = match[t1 + 1]; + t1 = _this._expr; + if (t1 !== -1) + result.expr = match[t1 + 1]; + t1 = _this._method; + if (t1 !== -1) + result.method = match[t1 + 1]; + t1 = _this._receiver; + if (t1 !== -1) + result.receiver = match[t1 + 1]; + return result; + } + }; + A.NullError.prototype = { + toString$0(_) { + return "Null check operator used on a null value"; + } + }; + A.JsNoSuchMethodError.prototype = { + toString$0(_) { + var t2, _this = this, + _s38_ = "NoSuchMethodError: method not found: '", + t1 = _this._method; + if (t1 == null) + return "NoSuchMethodError: " + _this.__js_helper$_message; + t2 = _this._receiver; + if (t2 == null) + return _s38_ + t1 + "' (" + _this.__js_helper$_message + ")"; + return _s38_ + t1 + "' on '" + t2 + "' (" + _this.__js_helper$_message + ")"; + } + }; + A.UnknownJsTypeError.prototype = { + toString$0(_) { + var t1 = this.__js_helper$_message; + return t1.length === 0 ? "Error" : "Error: " + t1; + } + }; + A.NullThrownFromJavaScriptException.prototype = { + toString$0(_) { + return "Throw of null ('" + (this._irritant === null ? "null" : "undefined") + "' from JavaScript)"; + }, + $isException: 1 + }; + A._StackTrace.prototype = { + toString$0(_) { + var trace, + t1 = this._trace; + if (t1 != null) + return t1; + t1 = this._exception; + trace = t1 !== null && typeof t1 === "object" ? t1.stack : null; + return this._trace = trace == null ? "" : trace; + }, + $isStackTrace: 1 + }; + A.Closure.prototype = { + toString$0(_) { + var $constructor = this.constructor, + $name = $constructor == null ? null : $constructor.name; + return "Closure '" + A.unminifyOrTag($name == null ? "unknown" : $name) + "'"; + }, + $isFunction: 1, + get$$call() { + return this; + }, + "call*": "call$1", + $requiredArgCount: 1, + $defaultValues: null + }; + A.Closure0Args.prototype = {"call*": "call$0", $requiredArgCount: 0}; + A.Closure2Args.prototype = {"call*": "call$2", $requiredArgCount: 2}; + A.TearOffClosure.prototype = {}; + A.StaticClosure.prototype = { + toString$0(_) { + var $name = this.$static_name; + if ($name == null) + return "Closure of unknown static method"; + return "Closure '" + A.unminifyOrTag($name) + "'"; + } + }; + A.BoundClosure.prototype = { + $eq(_, other) { + if (other == null) + return false; + if (this === other) + return true; + if (!(other instanceof A.BoundClosure)) + return false; + return this.$_target === other.$_target && this._receiver === other._receiver; + }, + get$hashCode(_) { + return (A.objectHashCode(this._receiver) ^ A.Primitives_objectHashCode(this.$_target)) >>> 0; + }, + toString$0(_) { + return "Closure '" + this.$_name + "' of " + ("Instance of '" + A.Primitives_objectTypeName(this._receiver) + "'"); + } + }; + A._CyclicInitializationError.prototype = { + toString$0(_) { + return "Reading static variable '" + this.variableName + "' during its initialization"; + } + }; + A.RuntimeError.prototype = { + toString$0(_) { + return "RuntimeError: " + this.message; + } + }; + A._AssertionError.prototype = { + toString$0(_) { + return "Assertion failed: " + A.Error_safeToString(this.message); + } + }; + A._Required.prototype = {}; + A.JsLinkedHashMap.prototype = { + get$length(_) { + return this._length; + }, + get$isEmpty(_) { + return this._length === 0; + }, + get$keys(_) { + return new A.LinkedHashMapKeyIterable(this, A._instanceType(this)._eval$1("LinkedHashMapKeyIterable<1>")); + }, + get$values(_) { + var t1 = A._instanceType(this); + return A.MappedIterable_MappedIterable(new A.LinkedHashMapKeyIterable(this, t1._eval$1("LinkedHashMapKeyIterable<1>")), new A.JsLinkedHashMap_values_closure(this), t1._precomputed1, t1._rest[1]); + }, + containsKey$1(_, key) { + var strings, nums; + if (typeof key == "string") { + strings = this._strings; + if (strings == null) + return false; + return strings[key] != null; + } else if (typeof key == "number" && (key & 0x3fffffff) === key) { + nums = this._nums; + if (nums == null) + return false; + return nums[key] != null; + } else + return this.internalContainsKey$1(key); + }, + internalContainsKey$1(key) { + var rest = this.__js_helper$_rest; + if (rest == null) + return false; + return this.internalFindBucketIndex$2(rest[this.internalComputeHashCode$1(key)], key) >= 0; + }, + $index(_, key) { + var strings, cell, t1, nums, _null = null; + if (typeof key == "string") { + strings = this._strings; + if (strings == null) + return _null; + cell = strings[key]; + t1 = cell == null ? _null : cell.hashMapCellValue; + return t1; + } else if (typeof key == "number" && (key & 0x3fffffff) === key) { + nums = this._nums; + if (nums == null) + return _null; + cell = nums[key]; + t1 = cell == null ? _null : cell.hashMapCellValue; + return t1; + } else + return this.internalGet$1(key); + }, + internalGet$1(key) { + var bucket, index, + rest = this.__js_helper$_rest; + if (rest == null) + return null; + bucket = rest[this.internalComputeHashCode$1(key)]; + index = this.internalFindBucketIndex$2(bucket, key); + if (index < 0) + return null; + return bucket[index].hashMapCellValue; + }, + $indexSet(_, key, value) { + var strings, nums, rest, hash, bucket, index, _this = this, + t1 = A._instanceType(_this); + t1._precomputed1._as(key); + t1._rest[1]._as(value); + if (typeof key == "string") { + strings = _this._strings; + _this._addHashTableEntry$3(strings == null ? _this._strings = _this._newHashTable$0() : strings, key, value); + } else if (typeof key == "number" && (key & 0x3fffffff) === key) { + nums = _this._nums; + _this._addHashTableEntry$3(nums == null ? _this._nums = _this._newHashTable$0() : nums, key, value); + } else { + rest = _this.__js_helper$_rest; + if (rest == null) + rest = _this.__js_helper$_rest = _this._newHashTable$0(); + hash = _this.internalComputeHashCode$1(key); + bucket = rest[hash]; + if (bucket == null) + rest[hash] = [_this._newLinkedCell$2(key, value)]; + else { + index = _this.internalFindBucketIndex$2(bucket, key); + if (index >= 0) + bucket[index].hashMapCellValue = value; + else + bucket.push(_this._newLinkedCell$2(key, value)); + } + } + }, + putIfAbsent$2(_, key, ifAbsent) { + var t2, value, _this = this, + t1 = A._instanceType(_this); + t1._precomputed1._as(key); + t1._eval$1("2()")._as(ifAbsent); + if (_this.containsKey$1(0, key)) { + t2 = _this.$index(0, key); + return t2 == null ? t1._rest[1]._as(t2) : t2; + } + value = ifAbsent.call$0(); + _this.$indexSet(0, key, value); + return value; + }, + remove$1(_, key) { + var _this = this; + if (typeof key == "string") + return _this._removeHashTableEntry$2(_this._strings, key); + else if (typeof key == "number" && (key & 0x3fffffff) === key) + return _this._removeHashTableEntry$2(_this._nums, key); + else + return _this.internalRemove$1(key); + }, + internalRemove$1(key) { + var hash, bucket, index, cell, _this = this, + rest = _this.__js_helper$_rest; + if (rest == null) + return null; + hash = _this.internalComputeHashCode$1(key); + bucket = rest[hash]; + index = _this.internalFindBucketIndex$2(bucket, key); + if (index < 0) + return null; + cell = bucket.splice(index, 1)[0]; + _this._unlinkCell$1(cell); + if (bucket.length === 0) + delete rest[hash]; + return cell.hashMapCellValue; + }, + clear$0(_) { + var _this = this; + if (_this._length > 0) { + _this._strings = _this._nums = _this.__js_helper$_rest = _this._first = _this._last = null; + _this._length = 0; + _this._modified$0(); + } + }, + forEach$1(_, action) { + var cell, modifications, _this = this; + A._instanceType(_this)._eval$1("~(1,2)")._as(action); + cell = _this._first; + modifications = _this._modifications; + for (; cell != null;) { + action.call$2(cell.hashMapCellKey, cell.hashMapCellValue); + if (modifications !== _this._modifications) + throw A.wrapException(A.ConcurrentModificationError$(_this)); + cell = cell._next; + } + }, + _addHashTableEntry$3(table, key, value) { + var cell, + t1 = A._instanceType(this); + t1._precomputed1._as(key); + t1._rest[1]._as(value); + cell = table[key]; + if (cell == null) + table[key] = this._newLinkedCell$2(key, value); + else + cell.hashMapCellValue = value; + }, + _removeHashTableEntry$2(table, key) { + var cell; + if (table == null) + return null; + cell = table[key]; + if (cell == null) + return null; + this._unlinkCell$1(cell); + delete table[key]; + return cell.hashMapCellValue; + }, + _modified$0() { + this._modifications = this._modifications + 1 & 1073741823; + }, + _newLinkedCell$2(key, value) { + var _this = this, + t1 = A._instanceType(_this), + cell = new A.LinkedHashMapCell(t1._precomputed1._as(key), t1._rest[1]._as(value)); + if (_this._first == null) + _this._first = _this._last = cell; + else { + t1 = _this._last; + t1.toString; + cell._previous = t1; + _this._last = t1._next = cell; + } + ++_this._length; + _this._modified$0(); + return cell; + }, + _unlinkCell$1(cell) { + var _this = this, + previous = cell._previous, + next = cell._next; + if (previous == null) + _this._first = next; + else + previous._next = next; + if (next == null) + _this._last = previous; + else + next._previous = previous; + --_this._length; + _this._modified$0(); + }, + internalComputeHashCode$1(key) { + return J.get$hashCode$(key) & 1073741823; + }, + internalFindBucketIndex$2(bucket, key) { + var $length, i; + if (bucket == null) + return -1; + $length = bucket.length; + for (i = 0; i < $length; ++i) + if (J.$eq$(bucket[i].hashMapCellKey, key)) + return i; + return -1; + }, + toString$0(_) { + return A.MapBase_mapToString(this); + }, + _newHashTable$0() { + var table = Object.create(null); + table["<non-identifier-key>"] = table; + delete table["<non-identifier-key>"]; + return table; + }, + $isLinkedHashMap: 1 + }; + A.JsLinkedHashMap_values_closure.prototype = { + call$1(each) { + var t1 = this.$this, + t2 = A._instanceType(t1); + t1 = t1.$index(0, t2._precomputed1._as(each)); + return t1 == null ? t2._rest[1]._as(t1) : t1; + }, + $signature() { + return A._instanceType(this.$this)._eval$1("2(1)"); + } + }; + A.LinkedHashMapCell.prototype = {}; + A.LinkedHashMapKeyIterable.prototype = { + get$length(_) { + return this._map._length; + }, + get$isEmpty(_) { + return this._map._length === 0; + }, + get$iterator(_) { + var t1 = this._map, + t2 = new A.LinkedHashMapKeyIterator(t1, t1._modifications, this.$ti._eval$1("LinkedHashMapKeyIterator<1>")); + t2._cell = t1._first; + return t2; + }, + contains$1(_, element) { + return this._map.containsKey$1(0, element); + } + }; + A.LinkedHashMapKeyIterator.prototype = { + get$current(_) { + return this.__js_helper$_current; + }, + moveNext$0() { + var cell, _this = this, + t1 = _this._map; + if (_this._modifications !== t1._modifications) + throw A.wrapException(A.ConcurrentModificationError$(t1)); + cell = _this._cell; + if (cell == null) { + _this.set$__js_helper$_current(null); + return false; + } else { + _this.set$__js_helper$_current(cell.hashMapCellKey); + _this._cell = cell._next; + return true; + } + }, + set$__js_helper$_current(_current) { + this.__js_helper$_current = this.$ti._eval$1("1?")._as(_current); + }, + $isIterator: 1 + }; + A.initHooks_closure.prototype = { + call$1(o) { + return this.getTag(o); + }, + $signature: 15 + }; + A.initHooks_closure0.prototype = { + call$2(o, tag) { + return this.getUnknownTag(o, tag); + }, + $signature: 36 + }; + A.initHooks_closure1.prototype = { + call$1(tag) { + return this.prototypeForTag(A._asString(tag)); + }, + $signature: 37 + }; + A.JSSyntaxRegExp.prototype = { + toString$0(_) { + return "RegExp/" + this.pattern + "/" + this._nativeRegExp.flags; + }, + get$_nativeGlobalVersion() { + var _this = this, + t1 = _this._nativeGlobalRegExp; + if (t1 != null) + return t1; + t1 = _this._nativeRegExp; + return _this._nativeGlobalRegExp = A.JSSyntaxRegExp_makeNative(_this.pattern, t1.multiline, !t1.ignoreCase, t1.unicode, t1.dotAll, true); + }, + get$_nativeAnchoredVersion() { + var _this = this, + t1 = _this._nativeAnchoredRegExp; + if (t1 != null) + return t1; + t1 = _this._nativeRegExp; + return _this._nativeAnchoredRegExp = A.JSSyntaxRegExp_makeNative(_this.pattern + "|()", t1.multiline, !t1.ignoreCase, t1.unicode, t1.dotAll, true); + }, + firstMatch$1(string) { + var m = this._nativeRegExp.exec(string); + if (m == null) + return null; + return new A._MatchImplementation(m); + }, + allMatches$2(_, string, start) { + var t1 = string.length; + if (start > t1) + throw A.wrapException(A.RangeError$range(start, 0, t1, null, null)); + return new A._AllMatchesIterable(this, string, start); + }, + allMatches$1($receiver, string) { + return this.allMatches$2($receiver, string, 0); + }, + _execGlobal$2(string, start) { + var match, + regexp = this.get$_nativeGlobalVersion(); + if (regexp == null) + regexp = type$.Object._as(regexp); + regexp.lastIndex = start; + match = regexp.exec(string); + if (match == null) + return null; + return new A._MatchImplementation(match); + }, + _execAnchored$2(string, start) { + var match, + regexp = this.get$_nativeAnchoredVersion(); + if (regexp == null) + regexp = type$.Object._as(regexp); + regexp.lastIndex = start; + match = regexp.exec(string); + if (match == null) + return null; + if (0 >= match.length) + return A.ioore(match, -1); + if (match.pop() != null) + return null; + return new A._MatchImplementation(match); + }, + matchAsPrefix$2(_, string, start) { + if (start < 0 || start > string.length) + throw A.wrapException(A.RangeError$range(start, 0, string.length, null, null)); + return this._execAnchored$2(string, start); + }, + $isPattern: 1, + $isRegExp: 1 + }; + A._MatchImplementation.prototype = { + get$start(_) { + return this._match.index; + }, + get$end(_) { + var t1 = this._match; + return t1.index + t1[0].length; + }, + $isMatch: 1, + $isRegExpMatch: 1 + }; + A._AllMatchesIterable.prototype = { + get$iterator(_) { + return new A._AllMatchesIterator(this._re, this.__js_helper$_string, this.__js_helper$_start); + } + }; + A._AllMatchesIterator.prototype = { + get$current(_) { + var t1 = this.__js_helper$_current; + return t1 == null ? type$.RegExpMatch._as(t1) : t1; + }, + moveNext$0() { + var t1, t2, t3, match, nextIndex, _this = this, + string = _this.__js_helper$_string; + if (string == null) + return false; + t1 = _this._nextIndex; + t2 = string.length; + if (t1 <= t2) { + t3 = _this._regExp; + match = t3._execGlobal$2(string, t1); + if (match != null) { + _this.__js_helper$_current = match; + nextIndex = match.get$end(match); + if (match._match.index === nextIndex) { + if (t3._nativeRegExp.unicode) { + t1 = _this._nextIndex; + t3 = t1 + 1; + if (t3 < t2) { + if (!(t1 >= 0 && t1 < t2)) + return A.ioore(string, t1); + t1 = string.charCodeAt(t1); + if (t1 >= 55296 && t1 <= 56319) { + if (!(t3 >= 0)) + return A.ioore(string, t3); + t1 = string.charCodeAt(t3); + t1 = t1 >= 56320 && t1 <= 57343; + } else + t1 = false; + } else + t1 = false; + } else + t1 = false; + nextIndex = (t1 ? nextIndex + 1 : nextIndex) + 1; + } + _this._nextIndex = nextIndex; + return true; + } + } + _this.__js_helper$_string = _this.__js_helper$_current = null; + return false; + }, + $isIterator: 1 + }; + A.StringMatch.prototype = { + get$end(_) { + return this.start + this.pattern.length; + }, + $isMatch: 1, + get$start(receiver) { + return this.start; + } + }; + A._StringAllMatchesIterable.prototype = { + get$iterator(_) { + return new A._StringAllMatchesIterator(this._input, this._pattern, this.__js_helper$_index); + } + }; + A._StringAllMatchesIterator.prototype = { + moveNext$0() { + var index, end, _this = this, + t1 = _this.__js_helper$_index, + t2 = _this._pattern, + t3 = t2.length, + t4 = _this._input, + t5 = t4.length; + if (t1 + t3 > t5) { + _this.__js_helper$_current = null; + return false; + } + index = t4.indexOf(t2, t1); + if (index < 0) { + _this.__js_helper$_index = t5 + 1; + _this.__js_helper$_current = null; + return false; + } + end = index + t3; + _this.__js_helper$_current = new A.StringMatch(index, t2); + _this.__js_helper$_index = end === _this.__js_helper$_index ? end + 1 : end; + return true; + }, + get$current(_) { + var t1 = this.__js_helper$_current; + t1.toString; + return t1; + }, + $isIterator: 1 + }; + A._Cell.prototype = { + _readLocal$0() { + var t1 = this._value; + if (t1 === this) + throw A.wrapException(new A.LateError("Local '" + this.__late_helper$_name + "' has not been initialized.")); + return t1; + } + }; + A._InitializedCell.prototype = { + _readFinal$0() { + var result, _this = this, + t1 = _this._value; + if (t1 === _this) { + result = _this._initializer.call$0(); + if (_this._value !== _this) + throw A.wrapException(new A.LateError("Local '" + _this.__late_helper$_name + string$.x27_has_)); + _this._value = result; + t1 = result; + } + return t1; + } + }; + A.NativeByteBuffer.prototype = { + get$runtimeType(receiver) { + return B.Type_ByteBuffer_RkP; + }, + $isTrustedGetRuntimeType: 1, + $isByteBuffer: 1 + }; + A.NativeTypedData.prototype = {}; + A.NativeByteData.prototype = { + get$runtimeType(receiver) { + return B.Type_ByteData_zNC; + }, + $isTrustedGetRuntimeType: 1, + $isByteData: 1 + }; + A.NativeTypedArray.prototype = { + get$length(receiver) { + return receiver.length; + }, + $isJavaScriptIndexingBehavior: 1 + }; + A.NativeTypedArrayOfDouble.prototype = { + $index(receiver, index) { + A._checkValidIndex(index, receiver, receiver.length); + return receiver[index]; + }, + $indexSet(receiver, index, value) { + A._asDouble(value); + A._checkValidIndex(index, receiver, receiver.length); + receiver[index] = value; + }, + $isEfficientLengthIterable: 1, + $isIterable: 1, + $isList: 1 + }; + A.NativeTypedArrayOfInt.prototype = { + $indexSet(receiver, index, value) { + A._asInt(value); + A._checkValidIndex(index, receiver, receiver.length); + receiver[index] = value; + }, + $isEfficientLengthIterable: 1, + $isIterable: 1, + $isList: 1 + }; + A.NativeFloat32List.prototype = { + get$runtimeType(receiver) { + return B.Type_Float32List_LB7; + }, + $isTrustedGetRuntimeType: 1, + $isFloat32List: 1 + }; + A.NativeFloat64List.prototype = { + get$runtimeType(receiver) { + return B.Type_Float64List_LB7; + }, + $isTrustedGetRuntimeType: 1, + $isFloat64List: 1 + }; + A.NativeInt16List.prototype = { + get$runtimeType(receiver) { + return B.Type_Int16List_uXf; + }, + $index(receiver, index) { + A._checkValidIndex(index, receiver, receiver.length); + return receiver[index]; + }, + $isTrustedGetRuntimeType: 1, + $isInt16List: 1 + }; + A.NativeInt32List.prototype = { + get$runtimeType(receiver) { + return B.Type_Int32List_O50; + }, + $index(receiver, index) { + A._checkValidIndex(index, receiver, receiver.length); + return receiver[index]; + }, + $isTrustedGetRuntimeType: 1, + $isInt32List: 1 + }; + A.NativeInt8List.prototype = { + get$runtimeType(receiver) { + return B.Type_Int8List_ekJ; + }, + $index(receiver, index) { + A._checkValidIndex(index, receiver, receiver.length); + return receiver[index]; + }, + $isTrustedGetRuntimeType: 1, + $isInt8List: 1 + }; + A.NativeUint16List.prototype = { + get$runtimeType(receiver) { + return B.Type_Uint16List_2bx; + }, + $index(receiver, index) { + A._checkValidIndex(index, receiver, receiver.length); + return receiver[index]; + }, + $isTrustedGetRuntimeType: 1, + $isUint16List: 1 + }; + A.NativeUint32List.prototype = { + get$runtimeType(receiver) { + return B.Type_Uint32List_2bx; + }, + $index(receiver, index) { + A._checkValidIndex(index, receiver, receiver.length); + return receiver[index]; + }, + $isTrustedGetRuntimeType: 1, + $isUint32List: 1 + }; + A.NativeUint8ClampedList.prototype = { + get$runtimeType(receiver) { + return B.Type_Uint8ClampedList_Jik; + }, + get$length(receiver) { + return receiver.length; + }, + $index(receiver, index) { + A._checkValidIndex(index, receiver, receiver.length); + return receiver[index]; + }, + $isTrustedGetRuntimeType: 1, + $isUint8ClampedList: 1 + }; + A.NativeUint8List.prototype = { + get$runtimeType(receiver) { + return B.Type_Uint8List_WLA; + }, + get$length(receiver) { + return receiver.length; + }, + $index(receiver, index) { + A._checkValidIndex(index, receiver, receiver.length); + return receiver[index]; + }, + sublist$2(receiver, start, end) { + return new Uint8Array(receiver.subarray(start, A._checkValidRange(start, end, receiver.length))); + }, + $isTrustedGetRuntimeType: 1, + $isNativeUint8List: 1, + $isUint8List: 1 + }; + A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin.prototype = {}; + A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin.prototype = {}; + A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin.prototype = {}; + A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin.prototype = {}; + A.Rti.prototype = { + _eval$1(recipe) { + return A._Universe_evalInEnvironment(init.typeUniverse, this, recipe); + }, + _bind$1(typeOrTuple) { + return A._Universe_bind(init.typeUniverse, this, typeOrTuple); + } + }; + A._FunctionParameters.prototype = {}; + A._Type.prototype = { + toString$0(_) { + return A._rtiToString(this._rti, null); + } + }; + A._Error.prototype = { + toString$0(_) { + return this.__rti$_message; + } + }; + A._TypeError.prototype = {$isTypeError: 1}; + A._AsyncRun__initializeScheduleImmediate_internalCallback.prototype = { + call$1(_) { + var t1 = this._box_0, + f = t1.storedCallback; + t1.storedCallback = null; + f.call$0(); + }, + $signature: 10 + }; + A._AsyncRun__initializeScheduleImmediate_closure.prototype = { + call$1(callback) { + var t1, t2; + this._box_0.storedCallback = type$.void_Function._as(callback); + t1 = this.div; + t2 = this.span; + t1.firstChild ? t1.removeChild(t2) : t1.appendChild(t2); + }, + $signature: 30 + }; + A._AsyncRun__scheduleImmediateJsOverride_internalCallback.prototype = { + call$0() { + this.callback.call$0(); + }, + $signature: 3 + }; + A._AsyncRun__scheduleImmediateWithSetImmediate_internalCallback.prototype = { + call$0() { + this.callback.call$0(); + }, + $signature: 3 + }; + A._TimerImpl.prototype = { + _TimerImpl$2(milliseconds, callback) { + if (self.setTimeout != null) + self.setTimeout(A.convertDartClosureToJS(new A._TimerImpl_internalCallback(this, callback), 0), milliseconds); + else + throw A.wrapException(A.UnsupportedError$("`setTimeout()` not found.")); + }, + _TimerImpl$periodic$2(milliseconds, callback) { + if (self.setTimeout != null) + self.setInterval(A.convertDartClosureToJS(new A._TimerImpl$periodic_closure(this, milliseconds, Date.now(), callback), 0), milliseconds); + else + throw A.wrapException(A.UnsupportedError$("Periodic timer.")); + }, + $isTimer: 1 + }; + A._TimerImpl_internalCallback.prototype = { + call$0() { + this.$this._tick = 1; + this.callback.call$0(); + }, + $signature: 0 + }; + A._TimerImpl$periodic_closure.prototype = { + call$0() { + var duration, _this = this, + t1 = _this.$this, + tick = t1._tick + 1, + t2 = _this.milliseconds; + if (t2 > 0) { + duration = Date.now() - _this.start; + if (duration > (tick + 1) * t2) + tick = B.JSInt_methods.$tdiv(duration, t2); + } + t1._tick = tick; + _this.callback.call$1(t1); + }, + $signature: 3 + }; + A.AsyncError.prototype = { + toString$0(_) { + return A.S(this.error); + }, + $isError: 1, + get$stackTrace() { + return this.stackTrace; + } + }; + A._Completer.prototype = { + completeError$2(error, stackTrace) { + var replacement; + A.checkNotNullable(error, "error", type$.Object); + if ((this.future._async$_state & 30) !== 0) + throw A.wrapException(A.StateError$("Future already completed")); + replacement = $.Zone__current.errorCallback$2(error, stackTrace); + if (replacement != null) { + error = replacement.error; + stackTrace = replacement.stackTrace; + } else if (stackTrace == null) + stackTrace = A.AsyncError_defaultStackTrace(error); + this._completeError$2(error, stackTrace); + }, + completeError$1(error) { + return this.completeError$2(error, null); + }, + $isCompleter: 1 + }; + A._AsyncCompleter.prototype = { + complete$1(_, value) { + var t2, + t1 = this.$ti; + t1._eval$1("1/?")._as(value); + t2 = this.future; + if ((t2._async$_state & 30) !== 0) + throw A.wrapException(A.StateError$("Future already completed")); + t2._asyncComplete$1(t1._eval$1("1/")._as(value)); + }, + complete$0($receiver) { + return this.complete$1($receiver, null); + }, + _completeError$2(error, stackTrace) { + this.future._asyncCompleteError$2(error, stackTrace); + } + }; + A._SyncCompleter.prototype = { + complete$1(_, value) { + var t2, + t1 = this.$ti; + t1._eval$1("1/?")._as(value); + t2 = this.future; + if ((t2._async$_state & 30) !== 0) + throw A.wrapException(A.StateError$("Future already completed")); + t2._complete$1(t1._eval$1("1/")._as(value)); + }, + complete$0($receiver) { + return this.complete$1($receiver, null); + }, + _completeError$2(error, stackTrace) { + this.future._completeError$2(error, stackTrace); + } + }; + A._FutureListener.prototype = { + matchesErrorTest$1(asyncError) { + if ((this.state & 15) !== 6) + return true; + return this.result._zone.runUnary$2$2(type$.bool_Function_Object._as(this.callback), asyncError.error, type$.bool, type$.Object); + }, + handleError$1(asyncError) { + var exception, _this = this, + errorCallback = _this.errorCallback, + result = null, + t1 = type$.dynamic, + t2 = type$.Object, + t3 = asyncError.error, + t4 = _this.result._zone; + if (type$.dynamic_Function_Object_StackTrace._is(errorCallback)) + result = t4.runBinary$3$3(errorCallback, t3, asyncError.stackTrace, t1, t2, type$.StackTrace); + else + result = t4.runUnary$2$2(type$.dynamic_Function_Object._as(errorCallback), t3, t1, t2); + try { + t1 = _this.$ti._eval$1("2/")._as(result); + return t1; + } catch (exception) { + if (type$.TypeError._is(A.unwrapException(exception))) { + if ((_this.state & 1) !== 0) + throw A.wrapException(A.ArgumentError$("The error handler of Future.then must return a value of the returned future's type", "onError")); + throw A.wrapException(A.ArgumentError$("The error handler of Future.catchError must return a value of the future's type", "onError")); + } else + throw exception; + } + } + }; + A._Future.prototype = { + _setChained$1(source) { + this._async$_state = this._async$_state & 1 | 4; + this._resultOrListeners = source; + }, + then$1$2$onError(f, onError, $R) { + var currentZone, result, t2, + t1 = this.$ti; + t1._bind$1($R)._eval$1("1/(2)")._as(f); + currentZone = $.Zone__current; + if (currentZone === B.C__RootZone) { + if (onError != null && !type$.dynamic_Function_Object_StackTrace._is(onError) && !type$.dynamic_Function_Object._is(onError)) + throw A.wrapException(A.ArgumentError$value(onError, "onError", string$.Error_)); + } else { + f = currentZone.registerUnaryCallback$2$1(f, $R._eval$1("0/"), t1._precomputed1); + if (onError != null) + onError = A._registerErrorHandler(onError, currentZone); + } + result = new A._Future($.Zone__current, $R._eval$1("_Future<0>")); + t2 = onError == null ? 1 : 3; + this._addListener$1(new A._FutureListener(result, t2, f, onError, t1._eval$1("@<1>")._bind$1($R)._eval$1("_FutureListener<1,2>"))); + return result; + }, + then$1$1(f, $R) { + return this.then$1$2$onError(f, null, $R); + }, + whenComplete$1(action) { + var t1, t2, result; + type$.dynamic_Function._as(action); + t1 = this.$ti; + t2 = $.Zone__current; + result = new A._Future(t2, t1); + if (t2 !== B.C__RootZone) + action = t2.registerCallback$1$1(action, type$.dynamic); + this._addListener$1(new A._FutureListener(result, 8, action, null, t1._eval$1("@<1>")._bind$1(t1._precomputed1)._eval$1("_FutureListener<1,2>"))); + return result; + }, + _setErrorObject$1(error) { + this._async$_state = this._async$_state & 1 | 16; + this._resultOrListeners = error; + }, + _cloneResult$1(source) { + this._async$_state = source._async$_state & 30 | this._async$_state & 1; + this._resultOrListeners = source._resultOrListeners; + }, + _addListener$1(listener) { + var source, _this = this, + t1 = _this._async$_state; + if (t1 <= 3) { + listener._nextListener = type$.nullable__FutureListener_dynamic_dynamic._as(_this._resultOrListeners); + _this._resultOrListeners = listener; + } else { + if ((t1 & 4) !== 0) { + source = type$._Future_dynamic._as(_this._resultOrListeners); + if ((source._async$_state & 24) === 0) { + source._addListener$1(listener); + return; + } + _this._cloneResult$1(source); + } + _this._zone.scheduleMicrotask$1(new A._Future__addListener_closure(_this, listener)); + } + }, + _prependListeners$1(listeners) { + var t1, existingListeners, next, cursor, next0, source, _this = this, _box_0 = {}; + _box_0.listeners = listeners; + if (listeners == null) + return; + t1 = _this._async$_state; + if (t1 <= 3) { + existingListeners = type$.nullable__FutureListener_dynamic_dynamic._as(_this._resultOrListeners); + _this._resultOrListeners = listeners; + if (existingListeners != null) { + next = listeners._nextListener; + for (cursor = listeners; next != null; cursor = next, next = next0) + next0 = next._nextListener; + cursor._nextListener = existingListeners; + } + } else { + if ((t1 & 4) !== 0) { + source = type$._Future_dynamic._as(_this._resultOrListeners); + if ((source._async$_state & 24) === 0) { + source._prependListeners$1(listeners); + return; + } + _this._cloneResult$1(source); + } + _box_0.listeners = _this._reverseListeners$1(listeners); + _this._zone.scheduleMicrotask$1(new A._Future__prependListeners_closure(_box_0, _this)); + } + }, + _removeListeners$0() { + var current = type$.nullable__FutureListener_dynamic_dynamic._as(this._resultOrListeners); + this._resultOrListeners = null; + return this._reverseListeners$1(current); + }, + _reverseListeners$1(listeners) { + var current, prev, next; + for (current = listeners, prev = null; current != null; prev = current, current = next) { + next = current._nextListener; + current._nextListener = prev; + } + return prev; + }, + _chainForeignFuture$1(source) { + var e, s, exception, _this = this; + _this._async$_state ^= 2; + try { + source.then$1$2$onError(new A._Future__chainForeignFuture_closure(_this), new A._Future__chainForeignFuture_closure0(_this), type$.Null); + } catch (exception) { + e = A.unwrapException(exception); + s = A.getTraceFromException(exception); + A.scheduleMicrotask(new A._Future__chainForeignFuture_closure1(_this, e, s)); + } + }, + _complete$1(value) { + var listeners, _this = this, + t1 = _this.$ti; + t1._eval$1("1/")._as(value); + if (t1._eval$1("Future<1>")._is(value)) + if (t1._is(value)) + A._Future__chainCoreFutureSync(value, _this); + else + _this._chainForeignFuture$1(value); + else { + listeners = _this._removeListeners$0(); + t1._precomputed1._as(value); + _this._async$_state = 8; + _this._resultOrListeners = value; + A._Future__propagateToListeners(_this, listeners); + } + }, + _completeWithValue$1(value) { + var listeners, _this = this; + _this.$ti._precomputed1._as(value); + listeners = _this._removeListeners$0(); + _this._async$_state = 8; + _this._resultOrListeners = value; + A._Future__propagateToListeners(_this, listeners); + }, + _completeError$2(error, stackTrace) { + var listeners; + type$.Object._as(error); + type$.StackTrace._as(stackTrace); + listeners = this._removeListeners$0(); + this._setErrorObject$1(A.AsyncError$(error, stackTrace)); + A._Future__propagateToListeners(this, listeners); + }, + _asyncComplete$1(value) { + var t1 = this.$ti; + t1._eval$1("1/")._as(value); + if (t1._eval$1("Future<1>")._is(value)) { + this._chainFuture$1(value); + return; + } + this._asyncCompleteWithValue$1(value); + }, + _asyncCompleteWithValue$1(value) { + var _this = this; + _this.$ti._precomputed1._as(value); + _this._async$_state ^= 2; + _this._zone.scheduleMicrotask$1(new A._Future__asyncCompleteWithValue_closure(_this, value)); + }, + _chainFuture$1(value) { + var t1 = this.$ti; + t1._eval$1("Future<1>")._as(value); + if (t1._is(value)) { + A._Future__chainCoreFutureAsync(value, this); + return; + } + this._chainForeignFuture$1(value); + }, + _asyncCompleteError$2(error, stackTrace) { + type$.StackTrace._as(stackTrace); + this._async$_state ^= 2; + this._zone.scheduleMicrotask$1(new A._Future__asyncCompleteError_closure(this, error, stackTrace)); + }, + $isFuture: 1 + }; + A._Future__addListener_closure.prototype = { + call$0() { + A._Future__propagateToListeners(this.$this, this.listener); + }, + $signature: 0 + }; + A._Future__prependListeners_closure.prototype = { + call$0() { + A._Future__propagateToListeners(this.$this, this._box_0.listeners); + }, + $signature: 0 + }; + A._Future__chainForeignFuture_closure.prototype = { + call$1(value) { + var error, stackTrace, exception, + t1 = this.$this; + t1._async$_state ^= 2; + try { + t1._completeWithValue$1(t1.$ti._precomputed1._as(value)); + } catch (exception) { + error = A.unwrapException(exception); + stackTrace = A.getTraceFromException(exception); + t1._completeError$2(error, stackTrace); + } + }, + $signature: 10 + }; + A._Future__chainForeignFuture_closure0.prototype = { + call$2(error, stackTrace) { + this.$this._completeError$2(type$.Object._as(error), type$.StackTrace._as(stackTrace)); + }, + $signature: 26 + }; + A._Future__chainForeignFuture_closure1.prototype = { + call$0() { + this.$this._completeError$2(this.e, this.s); + }, + $signature: 0 + }; + A._Future__chainCoreFutureAsync_closure.prototype = { + call$0() { + A._Future__chainCoreFutureSync(this._box_0.source, this.target); + }, + $signature: 0 + }; + A._Future__asyncCompleteWithValue_closure.prototype = { + call$0() { + this.$this._completeWithValue$1(this.value); + }, + $signature: 0 + }; + A._Future__asyncCompleteError_closure.prototype = { + call$0() { + this.$this._completeError$2(this.error, this.stackTrace); + }, + $signature: 0 + }; + A._Future__propagateToListeners_handleWhenCompleteCallback.prototype = { + call$0() { + var e, s, t1, exception, t2, originalSource, _this = this, completeResult = null; + try { + t1 = _this._box_0.listener; + completeResult = t1.result._zone.run$1$1(type$.dynamic_Function._as(t1.callback), type$.dynamic); + } catch (exception) { + e = A.unwrapException(exception); + s = A.getTraceFromException(exception); + t1 = _this.hasError && type$.AsyncError._as(_this._box_1.source._resultOrListeners).error === e; + t2 = _this._box_0; + if (t1) + t2.listenerValueOrError = type$.AsyncError._as(_this._box_1.source._resultOrListeners); + else + t2.listenerValueOrError = A.AsyncError$(e, s); + t2.listenerHasError = true; + return; + } + if (completeResult instanceof A._Future && (completeResult._async$_state & 24) !== 0) { + if ((completeResult._async$_state & 16) !== 0) { + t1 = _this._box_0; + t1.listenerValueOrError = type$.AsyncError._as(completeResult._resultOrListeners); + t1.listenerHasError = true; + } + return; + } + if (completeResult instanceof A._Future) { + originalSource = _this._box_1.source; + t1 = _this._box_0; + t1.listenerValueOrError = completeResult.then$1$1(new A._Future__propagateToListeners_handleWhenCompleteCallback_closure(originalSource), type$.dynamic); + t1.listenerHasError = false; + } + }, + $signature: 0 + }; + A._Future__propagateToListeners_handleWhenCompleteCallback_closure.prototype = { + call$1(_) { + return this.originalSource; + }, + $signature: 28 + }; + A._Future__propagateToListeners_handleValueCallback.prototype = { + call$0() { + var e, s, t1, t2, t3, t4, t5, exception; + try { + t1 = this._box_0; + t2 = t1.listener; + t3 = t2.$ti; + t4 = t3._precomputed1; + t5 = t4._as(this.sourceResult); + t1.listenerValueOrError = t2.result._zone.runUnary$2$2(t3._eval$1("2/(1)")._as(t2.callback), t5, t3._eval$1("2/"), t4); + } catch (exception) { + e = A.unwrapException(exception); + s = A.getTraceFromException(exception); + t1 = this._box_0; + t1.listenerValueOrError = A.AsyncError$(e, s); + t1.listenerHasError = true; + } + }, + $signature: 0 + }; + A._Future__propagateToListeners_handleError.prototype = { + call$0() { + var asyncError, e, s, t1, exception, t2, _this = this; + try { + asyncError = type$.AsyncError._as(_this._box_1.source._resultOrListeners); + t1 = _this._box_0; + if (t1.listener.matchesErrorTest$1(asyncError) && t1.listener.errorCallback != null) { + t1.listenerValueOrError = t1.listener.handleError$1(asyncError); + t1.listenerHasError = false; + } + } catch (exception) { + e = A.unwrapException(exception); + s = A.getTraceFromException(exception); + t1 = type$.AsyncError._as(_this._box_1.source._resultOrListeners); + t2 = _this._box_0; + if (t1.error === e) + t2.listenerValueOrError = t1; + else + t2.listenerValueOrError = A.AsyncError$(e, s); + t2.listenerHasError = true; + } + }, + $signature: 0 + }; + A._AsyncCallbackEntry.prototype = {}; + A.Stream.prototype = { + pipe$1(streamConsumer) { + A._instanceType(this)._eval$1("StreamConsumer<Stream.T>")._as(streamConsumer); + return streamConsumer.addStream$1(0, this).then$1$1(new A.Stream_pipe_closure(streamConsumer), type$.dynamic); + }, + get$length(_) { + var t1 = {}, + future = new A._Future($.Zone__current, type$._Future_int); + t1.count = 0; + this.listen$4$cancelOnError$onDone$onError(new A.Stream_length_closure(t1, this), true, new A.Stream_length_closure0(t1, future), future.get$_completeError()); + return future; + } + }; + A.Stream_pipe_closure.prototype = { + call$1(_) { + return this.streamConsumer.close$0(0); + }, + $signature: 29 + }; + A.Stream_length_closure.prototype = { + call$1(_) { + A._instanceType(this.$this)._eval$1("Stream.T")._as(_); + ++this._box_0.count; + }, + $signature() { + return A._instanceType(this.$this)._eval$1("~(Stream.T)"); + } + }; + A.Stream_length_closure0.prototype = { + call$0() { + this.future._complete$1(this._box_0.count); + }, + $signature: 0 + }; + A._StreamController.prototype = { + get$_pendingEvents() { + var t1, _this = this; + if ((_this._async$_state & 8) === 0) + return A._instanceType(_this)._eval$1("_PendingEvents<1>?")._as(_this._varData); + t1 = A._instanceType(_this); + return t1._eval$1("_PendingEvents<1>?")._as(t1._eval$1("_StreamControllerAddStreamState<1>")._as(_this._varData).varData); + }, + _ensurePendingEvents$0() { + var events, t1, state, _this = this; + if ((_this._async$_state & 8) === 0) { + events = _this._varData; + if (events == null) + events = _this._varData = new A._PendingEvents(A._instanceType(_this)._eval$1("_PendingEvents<1>")); + return A._instanceType(_this)._eval$1("_PendingEvents<1>")._as(events); + } + t1 = A._instanceType(_this); + state = t1._eval$1("_StreamControllerAddStreamState<1>")._as(_this._varData); + events = state.varData; + if (events == null) + events = state.varData = new A._PendingEvents(t1._eval$1("_PendingEvents<1>")); + return t1._eval$1("_PendingEvents<1>")._as(events); + }, + get$_async$_subscription() { + var varData = this._varData; + if ((this._async$_state & 8) !== 0) + varData = type$._StreamControllerAddStreamState_nullable_Object._as(varData).varData; + return A._instanceType(this)._eval$1("_ControllerSubscription<1>")._as(varData); + }, + _badEventState$0() { + if ((this._async$_state & 4) !== 0) + return new A.StateError("Cannot add event after closing"); + return new A.StateError("Cannot add event while adding a stream"); + }, + _ensureDoneFuture$0() { + var t1 = this._doneFuture; + if (t1 == null) + t1 = this._doneFuture = (this._async$_state & 2) !== 0 ? $.$get$Future__nullFuture() : new A._Future($.Zone__current, type$._Future_void); + return t1; + }, + add$1(_, value) { + var _this = this; + A._instanceType(_this)._precomputed1._as(value); + if (_this._async$_state >= 4) + throw A.wrapException(_this._badEventState$0()); + _this._add$1(0, value); + }, + addError$2(error, stackTrace) { + var replacement, + t1 = type$.Object; + t1._as(error); + type$.nullable_StackTrace._as(stackTrace); + A.checkNotNullable(error, "error", t1); + if (this._async$_state >= 4) + throw A.wrapException(this._badEventState$0()); + replacement = $.Zone__current.errorCallback$2(error, stackTrace); + if (replacement != null) { + error = replacement.error; + stackTrace = replacement.stackTrace; + } else if (stackTrace == null) + stackTrace = A.AsyncError_defaultStackTrace(error); + this._async$_addError$2(error, stackTrace); + }, + addError$1(error) { + return this.addError$2(error, null); + }, + close$0(_) { + var _this = this, + t1 = _this._async$_state; + if ((t1 & 4) !== 0) + return _this._ensureDoneFuture$0(); + if (t1 >= 4) + throw A.wrapException(_this._badEventState$0()); + t1 = _this._async$_state = t1 | 4; + if ((t1 & 1) !== 0) + _this._sendDone$0(); + else if ((t1 & 3) === 0) + _this._ensurePendingEvents$0().add$1(0, B.C__DelayedDone); + return _this._ensureDoneFuture$0(); + }, + _add$1(_, value) { + var t2, _this = this, + t1 = A._instanceType(_this); + t1._precomputed1._as(value); + t2 = _this._async$_state; + if ((t2 & 1) !== 0) + _this._sendData$1(value); + else if ((t2 & 3) === 0) + _this._ensurePendingEvents$0().add$1(0, new A._DelayedData(value, t1._eval$1("_DelayedData<1>"))); + }, + _async$_addError$2(error, stackTrace) { + var t1 = this._async$_state; + if ((t1 & 1) !== 0) + this._sendError$2(error, stackTrace); + else if ((t1 & 3) === 0) + this._ensurePendingEvents$0().add$1(0, new A._DelayedError(error, stackTrace)); + }, + _subscribe$4(onData, onError, onDone, cancelOnError) { + var t2, t3, t4, t5, t6, subscription, pendingEvents, addState, _this = this, + t1 = A._instanceType(_this); + t1._eval$1("~(1)?")._as(onData); + type$.nullable_void_Function._as(onDone); + if ((_this._async$_state & 3) !== 0) + throw A.wrapException(A.StateError$("Stream has already been listened to.")); + t2 = $.Zone__current; + t3 = cancelOnError ? 1 : 0; + t4 = A._BufferingStreamSubscription__registerDataHandler(t2, onData, t1._precomputed1); + t5 = A._BufferingStreamSubscription__registerErrorHandler(t2, onError); + t6 = onDone == null ? A.async___nullDoneHandler$closure() : onDone; + subscription = new A._ControllerSubscription(_this, t4, t5, t2.registerCallback$1$1(t6, type$.void), t2, t3, t1._eval$1("_ControllerSubscription<1>")); + pendingEvents = _this.get$_pendingEvents(); + t3 = _this._async$_state |= 1; + if ((t3 & 8) !== 0) { + addState = t1._eval$1("_StreamControllerAddStreamState<1>")._as(_this._varData); + addState.varData = subscription; + addState.addSubscription.resume$0(0); + } else + _this._varData = subscription; + subscription._setPendingEvents$1(pendingEvents); + subscription._guardCallback$1(new A._StreamController__subscribe_closure(_this)); + return subscription; + }, + _recordCancel$1(subscription) { + var result, onCancel, cancelResult, e, s, exception, result0, _this = this, + t1 = A._instanceType(_this); + t1._eval$1("StreamSubscription<1>")._as(subscription); + result = null; + if ((_this._async$_state & 8) !== 0) + result = t1._eval$1("_StreamControllerAddStreamState<1>")._as(_this._varData).cancel$0(0); + _this._varData = null; + _this._async$_state = _this._async$_state & 4294967286 | 2; + onCancel = _this.onCancel; + if (onCancel != null) + if (result == null) + try { + cancelResult = onCancel.call$0(); + if (cancelResult instanceof A._Future) + result = cancelResult; + } catch (exception) { + e = A.unwrapException(exception); + s = A.getTraceFromException(exception); + result0 = new A._Future($.Zone__current, type$._Future_void); + result0._asyncCompleteError$2(e, s); + result = result0; + } + else + result = result.whenComplete$1(onCancel); + t1 = new A._StreamController__recordCancel_complete(_this); + if (result != null) + result = result.whenComplete$1(t1); + else + t1.call$0(); + return result; + }, + $isStreamConsumer: 1, + $isStreamSink: 1, + $isStreamController: 1, + $is_StreamControllerLifecycle: 1, + $is_EventDispatch: 1 + }; + A._StreamController__subscribe_closure.prototype = { + call$0() { + A._runGuarded(this.$this.onListen); + }, + $signature: 0 + }; + A._StreamController__recordCancel_complete.prototype = { + call$0() { + var doneFuture = this.$this._doneFuture; + if (doneFuture != null && (doneFuture._async$_state & 30) === 0) + doneFuture._asyncComplete$1(null); + }, + $signature: 0 + }; + A._SyncStreamControllerDispatch.prototype = { + _sendData$1(data) { + this.$ti._precomputed1._as(data); + this.get$_async$_subscription()._add$1(0, data); + }, + _sendError$2(error, stackTrace) { + this.get$_async$_subscription()._async$_addError$2(error, stackTrace); + }, + _sendDone$0() { + this.get$_async$_subscription()._close$0(); + } + }; + A._SyncStreamController.prototype = {}; + A._ControllerStream.prototype = { + get$hashCode(_) { + return (A.Primitives_objectHashCode(this._controller) ^ 892482866) >>> 0; + }, + $eq(_, other) { + if (other == null) + return false; + if (this === other) + return true; + return other instanceof A._ControllerStream && other._controller === this._controller; + } + }; + A._ControllerSubscription.prototype = { + _onCancel$0() { + return this._controller._recordCancel$1(this); + }, + _onPause$0() { + var t1 = this._controller, + t2 = A._instanceType(t1); + t2._eval$1("StreamSubscription<1>")._as(this); + if ((t1._async$_state & 8) !== 0) + t2._eval$1("_StreamControllerAddStreamState<1>")._as(t1._varData).addSubscription.pause$0(0); + A._runGuarded(t1.onPause); + }, + _onResume$0() { + var t1 = this._controller, + t2 = A._instanceType(t1); + t2._eval$1("StreamSubscription<1>")._as(this); + if ((t1._async$_state & 8) !== 0) + t2._eval$1("_StreamControllerAddStreamState<1>")._as(t1._varData).addSubscription.resume$0(0); + A._runGuarded(t1.onResume); + } + }; + A._StreamSinkWrapper.prototype = { + add$1(_, data) { + this._target.add$1(0, this.$ti._precomputed1._as(data)); + }, + $isStreamConsumer: 1, + $isStreamSink: 1 + }; + A._AddStreamState_cancel_closure.prototype = { + call$0() { + this.$this.addStreamFuture._asyncComplete$1(null); + }, + $signature: 3 + }; + A._BufferingStreamSubscription.prototype = { + _setPendingEvents$1(pendingEvents) { + var _this = this; + A._instanceType(_this)._eval$1("_PendingEvents<_BufferingStreamSubscription.T>?")._as(pendingEvents); + if (pendingEvents == null) + return; + _this.set$_pending(pendingEvents); + if (pendingEvents.lastPendingEvent != null) { + _this._async$_state = (_this._async$_state | 64) >>> 0; + pendingEvents.schedule$1(_this); + } + }, + onData$1(handleData) { + var t1 = A._instanceType(this); + this.set$_onData(A._BufferingStreamSubscription__registerDataHandler(this._zone, t1._eval$1("~(_BufferingStreamSubscription.T)?")._as(handleData), t1._eval$1("_BufferingStreamSubscription.T"))); + }, + onError$1(_, handleError) { + this._onError = A._BufferingStreamSubscription__registerErrorHandler(this._zone, handleError); + }, + cancel$0(_) { + var _this = this, + t1 = (_this._async$_state & 4294967279) >>> 0; + _this._async$_state = t1; + if ((t1 & 8) === 0) + _this._cancel$0(); + t1 = _this._cancelFuture; + return t1 == null ? $.$get$Future__nullFuture() : t1; + }, + _cancel$0() { + var t2, _this = this, + t1 = _this._async$_state = (_this._async$_state | 8) >>> 0; + if ((t1 & 64) !== 0) { + t2 = _this._pending; + if (t2._async$_state === 1) + t2._async$_state = 3; + } + if ((t1 & 32) === 0) + _this.set$_pending(null); + _this._cancelFuture = _this._onCancel$0(); + }, + _add$1(_, data) { + var t2, _this = this, + t1 = A._instanceType(_this); + t1._eval$1("_BufferingStreamSubscription.T")._as(data); + t2 = _this._async$_state; + if ((t2 & 8) !== 0) + return; + if (t2 < 32) + _this._sendData$1(data); + else + _this._addPending$1(new A._DelayedData(data, t1._eval$1("_DelayedData<_BufferingStreamSubscription.T>"))); + }, + _async$_addError$2(error, stackTrace) { + var t1 = this._async$_state; + if ((t1 & 8) !== 0) + return; + if (t1 < 32) + this._sendError$2(error, stackTrace); + else + this._addPending$1(new A._DelayedError(error, stackTrace)); + }, + _close$0() { + var _this = this, + t1 = _this._async$_state; + if ((t1 & 8) !== 0) + return; + t1 = (t1 | 2) >>> 0; + _this._async$_state = t1; + if (t1 < 32) + _this._sendDone$0(); + else + _this._addPending$1(B.C__DelayedDone); + }, + _onPause$0() { + }, + _onResume$0() { + }, + _onCancel$0() { + return null; + }, + _addPending$1($event) { + var t1, _this = this, + pending = _this._pending; + if (pending == null) { + pending = new A._PendingEvents(A._instanceType(_this)._eval$1("_PendingEvents<_BufferingStreamSubscription.T>")); + _this.set$_pending(pending); + } + pending.add$1(0, $event); + t1 = _this._async$_state; + if ((t1 & 64) === 0) { + t1 = (t1 | 64) >>> 0; + _this._async$_state = t1; + if (t1 < 128) + pending.schedule$1(_this); + } + }, + _sendData$1(data) { + var t2, _this = this, + t1 = A._instanceType(_this)._eval$1("_BufferingStreamSubscription.T"); + t1._as(data); + t2 = _this._async$_state; + _this._async$_state = (t2 | 32) >>> 0; + _this._zone.runUnaryGuarded$1$2(_this._onData, data, t1); + _this._async$_state = (_this._async$_state & 4294967263) >>> 0; + _this._checkState$1((t2 & 4) !== 0); + }, + _sendError$2(error, stackTrace) { + var cancelFuture, _this = this, + t1 = _this._async$_state, + t2 = new A._BufferingStreamSubscription__sendError_sendError(_this, error, stackTrace); + if ((t1 & 1) !== 0) { + _this._async$_state = (t1 | 16) >>> 0; + _this._cancel$0(); + cancelFuture = _this._cancelFuture; + if (cancelFuture != null && cancelFuture !== $.$get$Future__nullFuture()) + cancelFuture.whenComplete$1(t2); + else + t2.call$0(); + } else { + t2.call$0(); + _this._checkState$1((t1 & 4) !== 0); + } + }, + _sendDone$0() { + var cancelFuture, _this = this, + t1 = new A._BufferingStreamSubscription__sendDone_sendDone(_this); + _this._cancel$0(); + _this._async$_state = (_this._async$_state | 16) >>> 0; + cancelFuture = _this._cancelFuture; + if (cancelFuture != null && cancelFuture !== $.$get$Future__nullFuture()) + cancelFuture.whenComplete$1(t1); + else + t1.call$0(); + }, + _guardCallback$1(callback) { + var t1, _this = this; + type$.void_Function._as(callback); + t1 = _this._async$_state; + _this._async$_state = (t1 | 32) >>> 0; + callback.call$0(); + _this._async$_state = (_this._async$_state & 4294967263) >>> 0; + _this._checkState$1((t1 & 4) !== 0); + }, + _checkState$1(wasInputPaused) { + var t2, isInputPaused, _this = this, + t1 = _this._async$_state; + if ((t1 & 64) !== 0 && _this._pending.lastPendingEvent == null) { + t1 = _this._async$_state = (t1 & 4294967231) >>> 0; + if ((t1 & 4) !== 0) + if (t1 < 128) { + t2 = _this._pending; + t2 = t2 == null ? null : t2.lastPendingEvent == null; + t2 = t2 !== false; + } else + t2 = false; + else + t2 = false; + if (t2) { + t1 = (t1 & 4294967291) >>> 0; + _this._async$_state = t1; + } + } + for (; true; wasInputPaused = isInputPaused) { + if ((t1 & 8) !== 0) { + _this.set$_pending(null); + return; + } + isInputPaused = (t1 & 4) !== 0; + if (wasInputPaused === isInputPaused) + break; + _this._async$_state = (t1 ^ 32) >>> 0; + if (isInputPaused) + _this._onPause$0(); + else + _this._onResume$0(); + t1 = (_this._async$_state & 4294967263) >>> 0; + _this._async$_state = t1; + } + if ((t1 & 64) !== 0 && t1 < 128) + _this._pending.schedule$1(_this); + }, + set$_onData(_onData) { + this._onData = A._instanceType(this)._eval$1("~(_BufferingStreamSubscription.T)")._as(_onData); + }, + set$_pending(_pending) { + this._pending = A._instanceType(this)._eval$1("_PendingEvents<_BufferingStreamSubscription.T>?")._as(_pending); + }, + $isStreamSubscription: 1, + $is_EventDispatch: 1 + }; + A._BufferingStreamSubscription__sendError_sendError.prototype = { + call$0() { + var onError, t3, t4, + t1 = this.$this, + t2 = t1._async$_state; + if ((t2 & 8) !== 0 && (t2 & 16) === 0) + return; + t1._async$_state = (t2 | 32) >>> 0; + onError = t1._onError; + t2 = this.error; + t3 = type$.Object; + t4 = t1._zone; + if (type$.void_Function_Object_StackTrace._is(onError)) + t4.runBinaryGuarded$2$3(onError, t2, this.stackTrace, t3, type$.StackTrace); + else + t4.runUnaryGuarded$1$2(type$.void_Function_Object._as(onError), t2, t3); + t1._async$_state = (t1._async$_state & 4294967263) >>> 0; + }, + $signature: 0 + }; + A._BufferingStreamSubscription__sendDone_sendDone.prototype = { + call$0() { + var t1 = this.$this, + t2 = t1._async$_state; + if ((t2 & 16) === 0) + return; + t1._async$_state = (t2 | 42) >>> 0; + t1._zone.runGuarded$1(t1._onDone); + t1._async$_state = (t1._async$_state & 4294967263) >>> 0; + }, + $signature: 0 + }; + A._StreamImpl.prototype = { + listen$4$cancelOnError$onDone$onError(onData, cancelOnError, onDone, onError) { + var t1 = this.$ti; + t1._eval$1("~(1)?")._as(onData); + type$.nullable_void_Function._as(onDone); + return this._controller._subscribe$4(t1._eval$1("~(1)?")._as(onData), onError, onDone, cancelOnError === true); + }, + listen$1(onData) { + return this.listen$4$cancelOnError$onDone$onError(onData, null, null, null); + }, + listen$3$onDone$onError(onData, onDone, onError) { + return this.listen$4$cancelOnError$onDone$onError(onData, null, onDone, onError); + }, + listen$2$onDone(onData, onDone) { + return this.listen$4$cancelOnError$onDone$onError(onData, null, onDone, null); + }, + listen$3$cancelOnError$onDone(onData, cancelOnError, onDone) { + return this.listen$4$cancelOnError$onDone$onError(onData, cancelOnError, onDone, null); + } + }; + A._DelayedEvent.prototype = { + set$next(_, next) { + this.next = type$.nullable__DelayedEvent_dynamic._as(next); + }, + get$next(receiver) { + return this.next; + } + }; + A._DelayedData.prototype = { + perform$1(dispatch) { + this.$ti._eval$1("_EventDispatch<1>")._as(dispatch)._sendData$1(this.value); + } + }; + A._DelayedError.prototype = { + perform$1(dispatch) { + dispatch._sendError$2(this.error, this.stackTrace); + } + }; + A._DelayedDone.prototype = { + perform$1(dispatch) { + dispatch._sendDone$0(); + }, + get$next(_) { + return null; + }, + set$next(_, _0) { + throw A.wrapException(A.StateError$("No events after a done.")); + }, + $is_DelayedEvent: 1 + }; + A._PendingEvents.prototype = { + schedule$1(dispatch) { + var t1, _this = this; + _this.$ti._eval$1("_EventDispatch<1>")._as(dispatch); + t1 = _this._async$_state; + if (t1 === 1) + return; + if (t1 >= 1) { + _this._async$_state = 1; + return; + } + A.scheduleMicrotask(new A._PendingEvents_schedule_closure(_this, dispatch)); + _this._async$_state = 1; + }, + add$1(_, $event) { + var _this = this, + lastEvent = _this.lastPendingEvent; + if (lastEvent == null) + _this.firstPendingEvent = _this.lastPendingEvent = $event; + else { + lastEvent.set$next(0, $event); + _this.lastPendingEvent = $event; + } + } + }; + A._PendingEvents_schedule_closure.prototype = { + call$0() { + var t2, $event, nextEvent, + t1 = this.$this, + oldState = t1._async$_state; + t1._async$_state = 0; + if (oldState === 3) + return; + t2 = t1.$ti._eval$1("_EventDispatch<1>")._as(this.dispatch); + $event = t1.firstPendingEvent; + nextEvent = $event.get$next($event); + t1.firstPendingEvent = nextEvent; + if (nextEvent == null) + t1.lastPendingEvent = null; + $event.perform$1(t2); + }, + $signature: 0 + }; + A._DoneStreamSubscription.prototype = { + onData$1(handleData) { + this.$ti._eval$1("~(1)?")._as(handleData); + }, + onError$1(_, handleError) { + }, + cancel$0(_) { + this._async$_state = -1; + this.set$_onDone(null); + return $.$get$Future__nullFuture(); + }, + _onMicrotask$0() { + var _0_0, doneHandler, t1, _this = this, + unscheduledState = _this._async$_state - 1; + if (unscheduledState === 0) { + _this._async$_state = -1; + _0_0 = _this._onDone; + if (_0_0 != null) { + doneHandler = _0_0; + t1 = true; + } else { + doneHandler = null; + t1 = false; + } + if (t1) { + _this.set$_onDone(null); + _this._zone.runGuarded$1(doneHandler); + } + } else + _this._async$_state = unscheduledState; + }, + set$_onDone(_onDone) { + this._onDone = type$.nullable_void_Function._as(_onDone); + }, + $isStreamSubscription: 1 + }; + A._EmptyStream.prototype = { + listen$4$cancelOnError$onDone$onError(onData, cancelOnError, onDone, onError) { + var t2, + t1 = this.$ti; + t1._eval$1("~(1)?")._as(onData); + type$.nullable_void_Function._as(onDone); + t2 = $.Zone__current; + t1 = new A._DoneStreamSubscription(t2, t1._eval$1("_DoneStreamSubscription<1>")); + A.scheduleMicrotask(t1.get$_onMicrotask()); + if (onDone != null) + t1.set$_onDone(t2.registerCallback$1$1(onDone, type$.void)); + return t1; + }, + listen$1(onData) { + return this.listen$4$cancelOnError$onDone$onError(onData, null, null, null); + }, + listen$3$onDone$onError(onData, onDone, onError) { + return this.listen$4$cancelOnError$onDone$onError(onData, null, onDone, onError); + }, + listen$3$cancelOnError$onDone(onData, cancelOnError, onDone) { + return this.listen$4$cancelOnError$onDone$onError(onData, cancelOnError, onDone, null); + } + }; + A._ZoneFunction.prototype = {}; + A._ZoneSpecification.prototype = {$isZoneSpecification: 1}; + A._ZoneDelegate.prototype = {$isZoneDelegate: 1}; + A._Zone.prototype = { + _processUncaughtError$3(zone, error, stackTrace) { + var implZone, handler, parentDelegate, parentZone, currentZone, e, s, implementation, t1, exception; + type$.StackTrace._as(stackTrace); + implementation = this.get$_handleUncaughtError(); + implZone = implementation.zone; + if (implZone === B.C__RootZone) { + A._rootHandleError(error, stackTrace); + return; + } + handler = implementation.$function; + parentDelegate = implZone.get$_parentDelegate(); + t1 = J.get$parent$z(implZone); + t1.toString; + parentZone = t1; + currentZone = $.Zone__current; + try { + $.Zone__current = parentZone; + handler.call$5(implZone, parentDelegate, zone, error, stackTrace); + $.Zone__current = currentZone; + } catch (exception) { + e = A.unwrapException(exception); + s = A.getTraceFromException(exception); + $.Zone__current = currentZone; + t1 = error === e ? stackTrace : s; + parentZone._processUncaughtError$3(implZone, e, t1); + } + }, + $isZone: 1 + }; + A._CustomZone.prototype = { + get$_delegate() { + var t1 = this._delegateCache; + return t1 == null ? this._delegateCache = new A._ZoneDelegate(this) : t1; + }, + get$_parentDelegate() { + return this.parent.get$_delegate(); + }, + get$errorZone() { + return this._handleUncaughtError.zone; + }, + runGuarded$1(f) { + var e, s, exception; + type$.void_Function._as(f); + try { + this.run$1$1(f, type$.void); + } catch (exception) { + e = A.unwrapException(exception); + s = A.getTraceFromException(exception); + this._processUncaughtError$3(this, type$.Object._as(e), type$.StackTrace._as(s)); + } + }, + runUnaryGuarded$1$2(f, arg, $T) { + var e, s, exception; + $T._eval$1("~(0)")._as(f); + $T._as(arg); + try { + this.runUnary$2$2(f, arg, type$.void, $T); + } catch (exception) { + e = A.unwrapException(exception); + s = A.getTraceFromException(exception); + this._processUncaughtError$3(this, type$.Object._as(e), type$.StackTrace._as(s)); + } + }, + runBinaryGuarded$2$3(f, arg1, arg2, T1, T2) { + var e, s, exception; + T1._eval$1("@<0>")._bind$1(T2)._eval$1("~(1,2)")._as(f); + T1._as(arg1); + T2._as(arg2); + try { + this.runBinary$3$3(f, arg1, arg2, type$.void, T1, T2); + } catch (exception) { + e = A.unwrapException(exception); + s = A.getTraceFromException(exception); + this._processUncaughtError$3(this, type$.Object._as(e), type$.StackTrace._as(s)); + } + }, + bindCallback$1$1(f, $R) { + return new A._CustomZone_bindCallback_closure(this, this.registerCallback$1$1($R._eval$1("0()")._as(f), $R), $R); + }, + bindUnaryCallback$2$1(f, $R, $T) { + return new A._CustomZone_bindUnaryCallback_closure(this, this.registerUnaryCallback$2$1($R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(f), $R, $T), $T, $R); + }, + bindCallbackGuarded$1(f) { + return new A._CustomZone_bindCallbackGuarded_closure(this, this.registerCallback$1$1(type$.void_Function._as(f), type$.void)); + }, + bindUnaryCallbackGuarded$1$1(f, $T) { + return new A._CustomZone_bindUnaryCallbackGuarded_closure(this, this.registerUnaryCallback$2$1($T._eval$1("~(0)")._as(f), type$.void, $T), $T); + }, + handleUncaughtError$2(error, stackTrace) { + this._processUncaughtError$3(this, error, type$.StackTrace._as(stackTrace)); + }, + fork$2$specification$zoneValues(specification, zoneValues) { + var implementation = this._fork, + t1 = implementation.zone; + return implementation.$function.call$5(t1, t1.get$_parentDelegate(), this, specification, zoneValues); + }, + run$1$1(f, $R) { + var implementation, t1; + $R._eval$1("0()")._as(f); + implementation = this._run; + t1 = implementation.zone; + return implementation.$function.call$1$4(t1, t1.get$_parentDelegate(), this, f, $R); + }, + runUnary$2$2(f, arg, $R, $T) { + var implementation, t1; + $R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(f); + $T._as(arg); + implementation = this._runUnary; + t1 = implementation.zone; + return implementation.$function.call$2$5(t1, t1.get$_parentDelegate(), this, f, arg, $R, $T); + }, + runBinary$3$3(f, arg1, arg2, $R, T1, T2) { + var implementation, t1; + $R._eval$1("@<0>")._bind$1(T1)._bind$1(T2)._eval$1("1(2,3)")._as(f); + T1._as(arg1); + T2._as(arg2); + implementation = this._runBinary; + t1 = implementation.zone; + return implementation.$function.call$3$6(t1, t1.get$_parentDelegate(), this, f, arg1, arg2, $R, T1, T2); + }, + registerCallback$1$1(callback, $R) { + var implementation, t1; + $R._eval$1("0()")._as(callback); + implementation = this._registerCallback; + t1 = implementation.zone; + return implementation.$function.call$1$4(t1, t1.get$_parentDelegate(), this, callback, $R); + }, + registerUnaryCallback$2$1(callback, $R, $T) { + var implementation, t1; + $R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(callback); + implementation = this._registerUnaryCallback; + t1 = implementation.zone; + return implementation.$function.call$2$4(t1, t1.get$_parentDelegate(), this, callback, $R, $T); + }, + registerBinaryCallback$3$1(callback, $R, T1, T2) { + var implementation, t1; + $R._eval$1("@<0>")._bind$1(T1)._bind$1(T2)._eval$1("1(2,3)")._as(callback); + implementation = this._registerBinaryCallback; + t1 = implementation.zone; + return implementation.$function.call$3$4(t1, t1.get$_parentDelegate(), this, callback, $R, T1, T2); + }, + errorCallback$2(error, stackTrace) { + var implementation, implementationZone; + A.checkNotNullable(error, "error", type$.Object); + implementation = this._errorCallback; + implementationZone = implementation.zone; + if (implementationZone === B.C__RootZone) + return null; + return implementation.$function.call$5(implementationZone, implementationZone.get$_parentDelegate(), this, error, stackTrace); + }, + scheduleMicrotask$1(f) { + var implementation, t1; + type$.void_Function._as(f); + implementation = this._scheduleMicrotask; + t1 = implementation.zone; + return implementation.$function.call$4(t1, t1.get$_parentDelegate(), this, f); + }, + createPeriodicTimer$2(duration, f) { + var implementation, t1; + type$.void_Function_Timer._as(f); + implementation = this._createPeriodicTimer; + t1 = implementation.zone; + return implementation.$function.call$5(t1, t1.get$_parentDelegate(), this, duration, f); + }, + set$_handleUncaughtError(_handleUncaughtError) { + this._handleUncaughtError = type$._ZoneFunction_of_void_Function_Zone_ZoneDelegate_Zone_Object_StackTrace._as(_handleUncaughtError); + }, + get$_run() { + return this._run; + }, + get$_runUnary() { + return this._runUnary; + }, + get$_runBinary() { + return this._runBinary; + }, + get$_registerCallback() { + return this._registerCallback; + }, + get$_registerUnaryCallback() { + return this._registerUnaryCallback; + }, + get$_registerBinaryCallback() { + return this._registerBinaryCallback; + }, + get$_errorCallback() { + return this._errorCallback; + }, + get$_scheduleMicrotask() { + return this._scheduleMicrotask; + }, + get$_createTimer() { + return this._createTimer; + }, + get$_createPeriodicTimer() { + return this._createPeriodicTimer; + }, + get$_print() { + return this._print; + }, + get$_fork() { + return this._fork; + }, + get$_handleUncaughtError() { + return this._handleUncaughtError; + }, + get$parent(receiver) { + return this.parent; + }, + get$_async$_map() { + return this._async$_map; + } + }; + A._CustomZone_bindCallback_closure.prototype = { + call$0() { + return this.$this.run$1$1(this.registered, this.R); + }, + $signature() { + return this.R._eval$1("0()"); + } + }; + A._CustomZone_bindUnaryCallback_closure.prototype = { + call$1(arg) { + var _this = this, + t1 = _this.T; + return _this.$this.runUnary$2$2(_this.registered, t1._as(arg), _this.R, t1); + }, + $signature() { + return this.R._eval$1("@<0>")._bind$1(this.T)._eval$1("1(2)"); + } + }; + A._CustomZone_bindCallbackGuarded_closure.prototype = { + call$0() { + return this.$this.runGuarded$1(this.registered); + }, + $signature: 0 + }; + A._CustomZone_bindUnaryCallbackGuarded_closure.prototype = { + call$1(arg) { + var t1 = this.T; + return this.$this.runUnaryGuarded$1$2(this.registered, t1._as(arg), t1); + }, + $signature() { + return this.T._eval$1("~(0)"); + } + }; + A._rootHandleError_closure.prototype = { + call$0() { + A.Error_throwWithStackTrace(this.error, this.stackTrace); + }, + $signature: 0 + }; + A._RootZone.prototype = { + get$_run() { + return B._ZoneFunction__RootZone__rootRun; + }, + get$_runUnary() { + return B._ZoneFunction__RootZone__rootRunUnary; + }, + get$_runBinary() { + return B._ZoneFunction__RootZone__rootRunBinary; + }, + get$_registerCallback() { + return B._ZoneFunction__RootZone__rootRegisterCallback; + }, + get$_registerUnaryCallback() { + return B._ZoneFunction_Eeh; + }, + get$_registerBinaryCallback() { + return B._ZoneFunction_7G2; + }, + get$_errorCallback() { + return B._ZoneFunction__RootZone__rootErrorCallback; + }, + get$_scheduleMicrotask() { + return B._ZoneFunction__RootZone__rootScheduleMicrotask; + }, + get$_createTimer() { + return B._ZoneFunction__RootZone__rootCreateTimer; + }, + get$_createPeriodicTimer() { + return B._ZoneFunction_3bB; + }, + get$_print() { + return B._ZoneFunction__RootZone__rootPrint; + }, + get$_fork() { + return B._ZoneFunction__RootZone__rootFork; + }, + get$_handleUncaughtError() { + return B._ZoneFunction_NMc; + }, + get$parent(_) { + return null; + }, + get$_async$_map() { + return $.$get$_RootZone__rootMap(); + }, + get$_delegate() { + var t1 = $._RootZone__rootDelegate; + return t1 == null ? $._RootZone__rootDelegate = new A._ZoneDelegate(this) : t1; + }, + get$_parentDelegate() { + var t1 = $._RootZone__rootDelegate; + return t1 == null ? $._RootZone__rootDelegate = new A._ZoneDelegate(this) : t1; + }, + get$errorZone() { + return this; + }, + runGuarded$1(f) { + var e, s, exception; + type$.void_Function._as(f); + try { + if (B.C__RootZone === $.Zone__current) { + f.call$0(); + return; + } + A._rootRun(null, null, this, f, type$.void); + } catch (exception) { + e = A.unwrapException(exception); + s = A.getTraceFromException(exception); + A._rootHandleError(type$.Object._as(e), type$.StackTrace._as(s)); + } + }, + runUnaryGuarded$1$2(f, arg, $T) { + var e, s, exception; + $T._eval$1("~(0)")._as(f); + $T._as(arg); + try { + if (B.C__RootZone === $.Zone__current) { + f.call$1(arg); + return; + } + A._rootRunUnary(null, null, this, f, arg, type$.void, $T); + } catch (exception) { + e = A.unwrapException(exception); + s = A.getTraceFromException(exception); + A._rootHandleError(type$.Object._as(e), type$.StackTrace._as(s)); + } + }, + runBinaryGuarded$2$3(f, arg1, arg2, T1, T2) { + var e, s, exception; + T1._eval$1("@<0>")._bind$1(T2)._eval$1("~(1,2)")._as(f); + T1._as(arg1); + T2._as(arg2); + try { + if (B.C__RootZone === $.Zone__current) { + f.call$2(arg1, arg2); + return; + } + A._rootRunBinary(null, null, this, f, arg1, arg2, type$.void, T1, T2); + } catch (exception) { + e = A.unwrapException(exception); + s = A.getTraceFromException(exception); + A._rootHandleError(type$.Object._as(e), type$.StackTrace._as(s)); + } + }, + bindCallback$1$1(f, $R) { + return new A._RootZone_bindCallback_closure(this, $R._eval$1("0()")._as(f), $R); + }, + bindUnaryCallback$2$1(f, $R, $T) { + return new A._RootZone_bindUnaryCallback_closure(this, $R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(f), $T, $R); + }, + bindCallbackGuarded$1(f) { + return new A._RootZone_bindCallbackGuarded_closure(this, type$.void_Function._as(f)); + }, + bindUnaryCallbackGuarded$1$1(f, $T) { + return new A._RootZone_bindUnaryCallbackGuarded_closure(this, $T._eval$1("~(0)")._as(f), $T); + }, + handleUncaughtError$2(error, stackTrace) { + A._rootHandleError(error, type$.StackTrace._as(stackTrace)); + }, + fork$2$specification$zoneValues(specification, zoneValues) { + return A._rootFork(null, null, this, specification, zoneValues); + }, + run$1$1(f, $R) { + $R._eval$1("0()")._as(f); + if ($.Zone__current === B.C__RootZone) + return f.call$0(); + return A._rootRun(null, null, this, f, $R); + }, + runUnary$2$2(f, arg, $R, $T) { + $R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(f); + $T._as(arg); + if ($.Zone__current === B.C__RootZone) + return f.call$1(arg); + return A._rootRunUnary(null, null, this, f, arg, $R, $T); + }, + runBinary$3$3(f, arg1, arg2, $R, T1, T2) { + $R._eval$1("@<0>")._bind$1(T1)._bind$1(T2)._eval$1("1(2,3)")._as(f); + T1._as(arg1); + T2._as(arg2); + if ($.Zone__current === B.C__RootZone) + return f.call$2(arg1, arg2); + return A._rootRunBinary(null, null, this, f, arg1, arg2, $R, T1, T2); + }, + registerCallback$1$1(f, $R) { + return $R._eval$1("0()")._as(f); + }, + registerUnaryCallback$2$1(f, $R, $T) { + return $R._eval$1("@<0>")._bind$1($T)._eval$1("1(2)")._as(f); + }, + registerBinaryCallback$3$1(f, $R, T1, T2) { + return $R._eval$1("@<0>")._bind$1(T1)._bind$1(T2)._eval$1("1(2,3)")._as(f); + }, + errorCallback$2(error, stackTrace) { + return null; + }, + scheduleMicrotask$1(f) { + A._rootScheduleMicrotask(null, null, this, type$.void_Function._as(f)); + }, + createPeriodicTimer$2(duration, f) { + return A.Timer__createPeriodicTimer(duration, type$.void_Function_Timer._as(f)); + } + }; + A._RootZone_bindCallback_closure.prototype = { + call$0() { + return this.$this.run$1$1(this.f, this.R); + }, + $signature() { + return this.R._eval$1("0()"); + } + }; + A._RootZone_bindUnaryCallback_closure.prototype = { + call$1(arg) { + var _this = this, + t1 = _this.T; + return _this.$this.runUnary$2$2(_this.f, t1._as(arg), _this.R, t1); + }, + $signature() { + return this.R._eval$1("@<0>")._bind$1(this.T)._eval$1("1(2)"); + } + }; + A._RootZone_bindCallbackGuarded_closure.prototype = { + call$0() { + return this.$this.runGuarded$1(this.f); + }, + $signature: 0 + }; + A._RootZone_bindUnaryCallbackGuarded_closure.prototype = { + call$1(arg) { + var t1 = this.T; + return this.$this.runUnaryGuarded$1$2(this.f, t1._as(arg), t1); + }, + $signature() { + return this.T._eval$1("~(0)"); + } + }; + A.runZonedGuarded_closure.prototype = { + call$5($self, $parent, zone, error, stackTrace) { + var e, s, exception, t2, + t1 = type$.StackTrace; + t1._as(stackTrace); + try { + this.parentZone.runBinary$3$3(this.onError, error, stackTrace, type$.void, type$.Object, t1); + } catch (exception) { + e = A.unwrapException(exception); + s = A.getTraceFromException(exception); + t2 = $parent._delegationTarget; + if (e === error) + t2._processUncaughtError$3(zone, error, stackTrace); + else + t2._processUncaughtError$3(zone, type$.Object._as(e), t1._as(s)); + } + }, + $signature: 34 + }; + A._HashMap.prototype = { + get$length(_) { + return this._collection$_length; + }, + get$isEmpty(_) { + return this._collection$_length === 0; + }, + get$keys(_) { + return new A._HashMapKeyIterable(this, A._instanceType(this)._eval$1("_HashMapKeyIterable<1>")); + }, + containsKey$1(_, key) { + var strings, nums; + if (typeof key == "string" && key !== "__proto__") { + strings = this._collection$_strings; + return strings == null ? false : strings[key] != null; + } else if (typeof key == "number" && (key & 1073741823) === key) { + nums = this._collection$_nums; + return nums == null ? false : nums[key] != null; + } else + return this._containsKey$1(key); + }, + _containsKey$1(key) { + var rest = this._collection$_rest; + if (rest == null) + return false; + return this._findBucketIndex$2(this._getBucket$2(rest, key), key) >= 0; + }, + $index(_, key) { + var strings, t1, nums; + if (typeof key == "string" && key !== "__proto__") { + strings = this._collection$_strings; + t1 = strings == null ? null : A._HashMap__getTableEntry(strings, key); + return t1; + } else if (typeof key == "number" && (key & 1073741823) === key) { + nums = this._collection$_nums; + t1 = nums == null ? null : A._HashMap__getTableEntry(nums, key); + return t1; + } else + return this._get$1(0, key); + }, + _get$1(_, key) { + var bucket, index, + rest = this._collection$_rest; + if (rest == null) + return null; + bucket = this._getBucket$2(rest, key); + index = this._findBucketIndex$2(bucket, key); + return index < 0 ? null : bucket[index + 1]; + }, + $indexSet(_, key, value) { + var strings, nums, _this = this, + t1 = A._instanceType(_this); + t1._precomputed1._as(key); + t1._rest[1]._as(value); + if (typeof key == "string" && key !== "__proto__") { + strings = _this._collection$_strings; + _this._collection$_addHashTableEntry$3(strings == null ? _this._collection$_strings = A._HashMap__newHashTable() : strings, key, value); + } else if (typeof key == "number" && (key & 1073741823) === key) { + nums = _this._collection$_nums; + _this._collection$_addHashTableEntry$3(nums == null ? _this._collection$_nums = A._HashMap__newHashTable() : nums, key, value); + } else + _this._set$2(key, value); + }, + _set$2(key, value) { + var rest, hash, bucket, index, _this = this, + t1 = A._instanceType(_this); + t1._precomputed1._as(key); + t1._rest[1]._as(value); + rest = _this._collection$_rest; + if (rest == null) + rest = _this._collection$_rest = A._HashMap__newHashTable(); + hash = _this._computeHashCode$1(key); + bucket = rest[hash]; + if (bucket == null) { + A._HashMap__setTableEntry(rest, hash, [key, value]); + ++_this._collection$_length; + _this._keys = null; + } else { + index = _this._findBucketIndex$2(bucket, key); + if (index >= 0) + bucket[index + 1] = value; + else { + bucket.push(key, value); + ++_this._collection$_length; + _this._keys = null; + } + } + }, + forEach$1(_, action) { + var keys, $length, t2, i, key, t3, _this = this, + t1 = A._instanceType(_this); + t1._eval$1("~(1,2)")._as(action); + keys = _this._computeKeys$0(); + for ($length = keys.length, t2 = t1._precomputed1, t1 = t1._rest[1], i = 0; i < $length; ++i) { + key = keys[i]; + t2._as(key); + t3 = _this.$index(0, key); + action.call$2(key, t3 == null ? t1._as(t3) : t3); + if (keys !== _this._keys) + throw A.wrapException(A.ConcurrentModificationError$(_this)); + } + }, + _computeKeys$0() { + var strings, names, entries, index, i, nums, rest, bucket, $length, i0, _this = this, + result = _this._keys; + if (result != null) + return result; + result = A.List_List$filled(_this._collection$_length, null, false, type$.dynamic); + strings = _this._collection$_strings; + if (strings != null) { + names = Object.getOwnPropertyNames(strings); + entries = names.length; + for (index = 0, i = 0; i < entries; ++i) { + result[index] = names[i]; + ++index; + } + } else + index = 0; + nums = _this._collection$_nums; + if (nums != null) { + names = Object.getOwnPropertyNames(nums); + entries = names.length; + for (i = 0; i < entries; ++i) { + result[index] = +names[i]; + ++index; + } + } + rest = _this._collection$_rest; + if (rest != null) { + names = Object.getOwnPropertyNames(rest); + entries = names.length; + for (i = 0; i < entries; ++i) { + bucket = rest[names[i]]; + $length = bucket.length; + for (i0 = 0; i0 < $length; i0 += 2) { + result[index] = bucket[i0]; + ++index; + } + } + } + return _this._keys = result; + }, + _collection$_addHashTableEntry$3(table, key, value) { + var t1 = A._instanceType(this); + t1._precomputed1._as(key); + t1._rest[1]._as(value); + if (table[key] == null) { + ++this._collection$_length; + this._keys = null; + } + A._HashMap__setTableEntry(table, key, value); + }, + _computeHashCode$1(key) { + return J.get$hashCode$(key) & 1073741823; + }, + _getBucket$2(table, key) { + return table[this._computeHashCode$1(key)]; + }, + _findBucketIndex$2(bucket, key) { + var $length, i; + if (bucket == null) + return -1; + $length = bucket.length; + for (i = 0; i < $length; i += 2) + if (J.$eq$(bucket[i], key)) + return i; + return -1; + } + }; + A._IdentityHashMap.prototype = { + _computeHashCode$1(key) { + return A.objectHashCode(key) & 1073741823; + }, + _findBucketIndex$2(bucket, key) { + var $length, i, t1; + if (bucket == null) + return -1; + $length = bucket.length; + for (i = 0; i < $length; i += 2) { + t1 = bucket[i]; + if (t1 == null ? key == null : t1 === key) + return i; + } + return -1; + } + }; + A._HashMapKeyIterable.prototype = { + get$length(_) { + return this._collection$_map._collection$_length; + }, + get$isEmpty(_) { + return this._collection$_map._collection$_length === 0; + }, + get$isNotEmpty(_) { + return this._collection$_map._collection$_length !== 0; + }, + get$iterator(_) { + var t1 = this._collection$_map; + return new A._HashMapKeyIterator(t1, t1._computeKeys$0(), this.$ti._eval$1("_HashMapKeyIterator<1>")); + }, + contains$1(_, element) { + return this._collection$_map.containsKey$1(0, element); + } + }; + A._HashMapKeyIterator.prototype = { + get$current(_) { + var t1 = this._collection$_current; + return t1 == null ? this.$ti._precomputed1._as(t1) : t1; + }, + moveNext$0() { + var _this = this, + keys = _this._keys, + offset = _this._offset, + t1 = _this._collection$_map; + if (keys !== t1._keys) + throw A.wrapException(A.ConcurrentModificationError$(t1)); + else if (offset >= keys.length) { + _this.set$_collection$_current(null); + return false; + } else { + _this.set$_collection$_current(keys[offset]); + _this._offset = offset + 1; + return true; + } + }, + set$_collection$_current(_current) { + this._collection$_current = this.$ti._eval$1("1?")._as(_current); + }, + $isIterator: 1 + }; + A._LinkedHashSet.prototype = { + get$iterator(_) { + var _this = this, + t1 = new A._LinkedHashSetIterator(_this, _this._collection$_modifications, _this.$ti._eval$1("_LinkedHashSetIterator<1>")); + t1._collection$_cell = _this._collection$_first; + return t1; + }, + get$length(_) { + return this._collection$_length; + }, + get$isEmpty(_) { + return this._collection$_length === 0; + }, + get$isNotEmpty(_) { + return this._collection$_length !== 0; + }, + contains$1(_, object) { + var nums; + if ((object & 1073741823) === object) { + nums = this._collection$_nums; + if (nums == null) + return false; + return type$.nullable__LinkedHashSetCell._as(nums[object]) != null; + } else + return this._contains$1(object); + }, + _contains$1(object) { + var rest = this._collection$_rest; + if (rest == null) + return false; + return this._findBucketIndex$2(rest[B.JSInt_methods.get$hashCode(object) & 1073741823], object) >= 0; + }, + add$1(_, element) { + var strings, nums, _this = this; + _this.$ti._precomputed1._as(element); + if (typeof element == "string" && element !== "__proto__") { + strings = _this._collection$_strings; + return _this._collection$_addHashTableEntry$2(strings == null ? _this._collection$_strings = A._LinkedHashSet__newHashTable() : strings, element); + } else if (typeof element == "number" && (element & 1073741823) === element) { + nums = _this._collection$_nums; + return _this._collection$_addHashTableEntry$2(nums == null ? _this._collection$_nums = A._LinkedHashSet__newHashTable() : nums, element); + } else + return _this._collection$_add$1(0, element); + }, + _collection$_add$1(_, element) { + var rest, hash, bucket, _this = this; + _this.$ti._precomputed1._as(element); + rest = _this._collection$_rest; + if (rest == null) + rest = _this._collection$_rest = A._LinkedHashSet__newHashTable(); + hash = J.get$hashCode$(element) & 1073741823; + bucket = rest[hash]; + if (bucket == null) + rest[hash] = [_this._collection$_newLinkedCell$1(element)]; + else { + if (_this._findBucketIndex$2(bucket, element) >= 0) + return false; + bucket.push(_this._collection$_newLinkedCell$1(element)); + } + return true; + }, + remove$1(_, object) { + if ((object & 1073741823) === object) + return this._collection$_removeHashTableEntry$2(this._collection$_nums, object); + else + return this._remove$1(0, object); + }, + _remove$1(_, object) { + var hash, bucket, index, cell, + rest = this._collection$_rest; + if (rest == null) + return false; + hash = B.JSInt_methods.get$hashCode(object) & 1073741823; + bucket = rest[hash]; + index = this._findBucketIndex$2(bucket, object); + if (index < 0) + return false; + cell = bucket.splice(index, 1)[0]; + if (0 === bucket.length) + delete rest[hash]; + this._collection$_unlinkCell$1(cell); + return true; + }, + _collection$_addHashTableEntry$2(table, element) { + this.$ti._precomputed1._as(element); + if (type$.nullable__LinkedHashSetCell._as(table[element]) != null) + return false; + table[element] = this._collection$_newLinkedCell$1(element); + return true; + }, + _collection$_removeHashTableEntry$2(table, element) { + var cell; + if (table == null) + return false; + cell = type$.nullable__LinkedHashSetCell._as(table[element]); + if (cell == null) + return false; + this._collection$_unlinkCell$1(cell); + delete table[element]; + return true; + }, + _collection$_modified$0() { + this._collection$_modifications = this._collection$_modifications + 1 & 1073741823; + }, + _collection$_newLinkedCell$1(element) { + var t1, _this = this, + cell = new A._LinkedHashSetCell(_this.$ti._precomputed1._as(element)); + if (_this._collection$_first == null) + _this._collection$_first = _this._collection$_last = cell; + else { + t1 = _this._collection$_last; + t1.toString; + cell._collection$_previous = t1; + _this._collection$_last = t1._collection$_next = cell; + } + ++_this._collection$_length; + _this._collection$_modified$0(); + return cell; + }, + _collection$_unlinkCell$1(cell) { + var _this = this, + previous = cell._collection$_previous, + next = cell._collection$_next; + if (previous == null) + _this._collection$_first = next; + else + previous._collection$_next = next; + if (next == null) + _this._collection$_last = previous; + else + next._collection$_previous = previous; + --_this._collection$_length; + _this._collection$_modified$0(); + }, + _findBucketIndex$2(bucket, element) { + var $length, i; + if (bucket == null) + return -1; + $length = bucket.length; + for (i = 0; i < $length; ++i) + if (J.$eq$(bucket[i]._element, element)) + return i; + return -1; + } + }; + A._LinkedHashSetCell.prototype = {}; + A._LinkedHashSetIterator.prototype = { + get$current(_) { + var t1 = this._collection$_current; + return t1 == null ? this.$ti._precomputed1._as(t1) : t1; + }, + moveNext$0() { + var _this = this, + cell = _this._collection$_cell, + t1 = _this._set; + if (_this._collection$_modifications !== t1._collection$_modifications) + throw A.wrapException(A.ConcurrentModificationError$(t1)); + else if (cell == null) { + _this.set$_collection$_current(null); + return false; + } else { + _this.set$_collection$_current(_this.$ti._eval$1("1?")._as(cell._element)); + _this._collection$_cell = cell._collection$_next; + return true; + } + }, + set$_collection$_current(_current) { + this._collection$_current = this.$ti._eval$1("1?")._as(_current); + }, + $isIterator: 1 + }; + A.ListBase.prototype = { + get$iterator(receiver) { + return new A.ListIterator(receiver, this.get$length(receiver), A.instanceType(receiver)._eval$1("ListIterator<ListBase.E>")); + }, + elementAt$1(receiver, index) { + return this.$index(receiver, index); + }, + get$isEmpty(receiver) { + return this.get$length(receiver) === 0; + }, + get$isNotEmpty(receiver) { + return !this.get$isEmpty(receiver); + }, + get$first(receiver) { + if (this.get$length(receiver) === 0) + throw A.wrapException(A.IterableElementError_noElement()); + return this.$index(receiver, 0); + }, + map$1$1(receiver, f, $T) { + var t1 = A.instanceType(receiver); + return new A.MappedListIterable(receiver, t1._bind$1($T)._eval$1("1(ListBase.E)")._as(f), t1._eval$1("@<ListBase.E>")._bind$1($T)._eval$1("MappedListIterable<1,2>")); + }, + skip$1(receiver, count) { + return A.SubListIterable$(receiver, count, null, A.instanceType(receiver)._eval$1("ListBase.E")); + }, + cast$1$0(receiver, $R) { + return new A.CastList(receiver, A.instanceType(receiver)._eval$1("@<ListBase.E>")._bind$1($R)._eval$1("CastList<1,2>")); + }, + fillRange$3(receiver, start, end, fill) { + var i; + A.instanceType(receiver)._eval$1("ListBase.E?")._as(fill); + A.RangeError_checkValidRange(start, end, this.get$length(receiver)); + for (i = start; i < end; ++i) + this.$indexSet(receiver, i, fill); + }, + toString$0(receiver) { + return A.Iterable_iterableToFullString(receiver, "[", "]"); + }, + $isEfficientLengthIterable: 1, + $isIterable: 1, + $isList: 1 + }; + A.MapBase.prototype = { + forEach$1(receiver, action) { + var t2, key, t3, + t1 = A.instanceType(receiver); + t1._eval$1("~(MapBase.K,MapBase.V)")._as(action); + for (t2 = J.get$iterator$ax(this.get$keys(receiver)), t1 = t1._eval$1("MapBase.V"); t2.moveNext$0();) { + key = t2.get$current(t2); + t3 = this.$index(receiver, key); + action.call$2(key, t3 == null ? t1._as(t3) : t3); + } + }, + containsKey$1(receiver, key) { + return J.contains$1$asx(this.get$keys(receiver), key); + }, + get$length(receiver) { + return J.get$length$asx(this.get$keys(receiver)); + }, + get$isEmpty(receiver) { + return J.get$isEmpty$asx(this.get$keys(receiver)); + }, + toString$0(receiver) { + return A.MapBase_mapToString(receiver); + }, + $isMap: 1 + }; + A.MapBase_mapToString_closure.prototype = { + call$2(k, v) { + var t2, + t1 = this._box_0; + if (!t1.first) + this.result._contents += ", "; + t1.first = false; + t1 = this.result; + t2 = t1._contents += A.S(k); + t1._contents = t2 + ": "; + t1._contents += A.S(v); + }, + $signature: 16 + }; + A._UnmodifiableMapMixin.prototype = { + $indexSet(_, key, value) { + var t1 = A._instanceType(this); + t1._precomputed1._as(key); + t1._rest[1]._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot modify unmodifiable map")); + } + }; + A.MapView.prototype = { + $index(_, key) { + return J.$index$asx(this._collection$_map, key); + }, + $indexSet(_, key, value) { + var t1 = A._instanceType(this); + J.$indexSet$ax(this._collection$_map, t1._precomputed1._as(key), t1._rest[1]._as(value)); + }, + containsKey$1(_, key) { + return J.containsKey$1$x(this._collection$_map, key); + }, + forEach$1(_, action) { + J.forEach$1$x(this._collection$_map, A._instanceType(this)._eval$1("~(1,2)")._as(action)); + }, + get$isEmpty(_) { + return J.get$isEmpty$asx(this._collection$_map); + }, + get$length(_) { + return J.get$length$asx(this._collection$_map); + }, + get$keys(_) { + return J.get$keys$x(this._collection$_map); + }, + toString$0(_) { + return J.toString$0$(this._collection$_map); + }, + $isMap: 1 + }; + A.UnmodifiableMapView.prototype = {}; + A.SetBase.prototype = { + get$isEmpty(_) { + return this._collection$_length === 0; + }, + get$isNotEmpty(_) { + return this._collection$_length !== 0; + }, + map$1$1(_, f, $T) { + var t1 = this.$ti; + return new A.EfficientLengthMappedIterable(this, t1._bind$1($T)._eval$1("1(2)")._as(f), t1._eval$1("@<1>")._bind$1($T)._eval$1("EfficientLengthMappedIterable<1,2>")); + }, + toString$0(_) { + return A.Iterable_iterableToFullString(this, "{", "}"); + }, + skip$1(_, n) { + return A.SkipIterable_SkipIterable(this, n, this.$ti._precomputed1); + }, + elementAt$1(_, index) { + var iterator, skipCount, t1, _this = this; + A.RangeError_checkNotNegative(index, "index"); + iterator = A._LinkedHashSetIterator$(_this, _this._collection$_modifications, _this.$ti._precomputed1); + for (skipCount = index; iterator.moveNext$0();) { + if (skipCount === 0) { + t1 = iterator._collection$_current; + return t1 == null ? iterator.$ti._precomputed1._as(t1) : t1; + } + --skipCount; + } + throw A.wrapException(A.IndexError$withLength(index, index - skipCount, _this, "index")); + }, + $isEfficientLengthIterable: 1, + $isIterable: 1, + $isSet: 1 + }; + A._SetBase.prototype = {}; + A._UnmodifiableMapView_MapView__UnmodifiableMapMixin.prototype = {}; + A._JsonMap.prototype = { + $index(_, key) { + var result, + t1 = this._processed; + if (t1 == null) + return this._data.$index(0, key); + else if (typeof key != "string") + return null; + else { + result = t1[key]; + return typeof result == "undefined" ? this._process$1(key) : result; + } + }, + get$length(_) { + return this._processed == null ? this._data._length : this._convert$_computeKeys$0().length; + }, + get$isEmpty(_) { + return this.get$length(this) === 0; + }, + get$keys(_) { + var t1; + if (this._processed == null) { + t1 = this._data; + return new A.LinkedHashMapKeyIterable(t1, A._instanceType(t1)._eval$1("LinkedHashMapKeyIterable<1>")); + } + return new A._JsonMapKeyIterable(this); + }, + $indexSet(_, key, value) { + var processed, original, _this = this; + if (_this._processed == null) + _this._data.$indexSet(0, key, value); + else if (_this.containsKey$1(0, key)) { + processed = _this._processed; + processed[key] = value; + original = _this._original; + if (original == null ? processed != null : original !== processed) + original[key] = null; + } else + _this._upgrade$0().$indexSet(0, key, value); + }, + containsKey$1(_, key) { + if (this._processed == null) + return this._data.containsKey$1(0, key); + return Object.prototype.hasOwnProperty.call(this._original, key); + }, + forEach$1(_, f) { + var keys, i, key, value, _this = this; + type$.void_Function_String_dynamic._as(f); + if (_this._processed == null) + return _this._data.forEach$1(0, f); + keys = _this._convert$_computeKeys$0(); + for (i = 0; i < keys.length; ++i) { + key = keys[i]; + value = _this._processed[key]; + if (typeof value == "undefined") { + value = A._convertJsonToDartLazy(_this._original[key]); + _this._processed[key] = value; + } + f.call$2(key, value); + if (keys !== _this._data) + throw A.wrapException(A.ConcurrentModificationError$(_this)); + } + }, + _convert$_computeKeys$0() { + var keys = type$.nullable_List_dynamic._as(this._data); + if (keys == null) + keys = this._data = A._setArrayType(Object.keys(this._original), type$.JSArray_String); + return keys; + }, + _upgrade$0() { + var result, keys, i, t1, key, _this = this; + if (_this._processed == null) + return _this._data; + result = A.LinkedHashMap_LinkedHashMap$_empty(type$.String, type$.dynamic); + keys = _this._convert$_computeKeys$0(); + for (i = 0; t1 = keys.length, i < t1; ++i) { + key = keys[i]; + result.$indexSet(0, key, _this.$index(0, key)); + } + if (t1 === 0) + B.JSArray_methods.add$1(keys, ""); + else + B.JSArray_methods.clear$0(keys); + _this._original = _this._processed = null; + return _this._data = result; + }, + _process$1(key) { + var result; + if (!Object.prototype.hasOwnProperty.call(this._original, key)) + return null; + result = A._convertJsonToDartLazy(this._original[key]); + return this._processed[key] = result; + } + }; + A._JsonMapKeyIterable.prototype = { + get$length(_) { + var t1 = this._convert$_parent; + return t1.get$length(t1); + }, + elementAt$1(_, index) { + var t1 = this._convert$_parent; + if (t1._processed == null) + t1 = t1.get$keys(t1).elementAt$1(0, index); + else { + t1 = t1._convert$_computeKeys$0(); + if (!(index >= 0 && index < t1.length)) + return A.ioore(t1, index); + t1 = t1[index]; + } + return t1; + }, + get$iterator(_) { + var t1 = this._convert$_parent; + if (t1._processed == null) { + t1 = t1.get$keys(t1); + t1 = t1.get$iterator(t1); + } else { + t1 = t1._convert$_computeKeys$0(); + t1 = new J.ArrayIterator(t1, t1.length, A._arrayInstanceType(t1)._eval$1("ArrayIterator<1>")); + } + return t1; + }, + contains$1(_, key) { + return this._convert$_parent.containsKey$1(0, key); + } + }; + A.Utf8Decoder__decoder_closure.prototype = { + call$0() { + var t1, exception; + try { + t1 = new TextDecoder("utf-8", {fatal: true}); + return t1; + } catch (exception) { + } + return null; + }, + $signature: 2 + }; + A.Utf8Decoder__decoderNonfatal_closure.prototype = { + call$0() { + var t1, exception; + try { + t1 = new TextDecoder("utf-8", {fatal: false}); + return t1; + } catch (exception) { + } + return null; + }, + $signature: 2 + }; + A.AsciiCodec.prototype = { + encode$1(source) { + return B.AsciiEncoder_127.convert$1(source); + } + }; + A._UnicodeSubsetEncoder.prototype = { + convert$1(string) { + var stringLength, $length, result, t1, i, codeUnit; + A._asString(string); + stringLength = string.length; + $length = A.RangeError_checkValidRange(0, null, stringLength) - 0; + result = new Uint8Array($length); + for (t1 = ~this._subsetMask, i = 0; i < $length; ++i) { + if (!(i < stringLength)) + return A.ioore(string, i); + codeUnit = string.charCodeAt(i); + if ((codeUnit & t1) !== 0) + throw A.wrapException(A.ArgumentError$value(string, "string", "Contains invalid characters.")); + if (!(i < $length)) + return A.ioore(result, i); + result[i] = codeUnit; + } + return result; + } + }; + A.AsciiEncoder.prototype = {}; + A.Base64Codec.prototype = { + normalize$3(_, source, start, end) { + var inverseAlphabet, t2, i, sliceStart, buffer, firstPadding, firstPaddingSourceIndex, paddingCount, i0, char, i1, digit1, t3, digit2, char0, value, endLength, $length, + _s64_ = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", + _s31_ = "Invalid base64 encoding length ", + t1 = source.length; + end = A.RangeError_checkValidRange(start, end, t1); + inverseAlphabet = $.$get$_Base64Decoder__inverseAlphabet(); + for (t2 = inverseAlphabet.length, i = start, sliceStart = i, buffer = null, firstPadding = -1, firstPaddingSourceIndex = -1, paddingCount = 0; i < end; i = i0) { + i0 = i + 1; + if (!(i < t1)) + return A.ioore(source, i); + char = source.charCodeAt(i); + if (char === 37) { + i1 = i0 + 2; + if (i1 <= end) { + if (!(i0 < t1)) + return A.ioore(source, i0); + digit1 = A.hexDigitValue(source.charCodeAt(i0)); + t3 = i0 + 1; + if (!(t3 < t1)) + return A.ioore(source, t3); + digit2 = A.hexDigitValue(source.charCodeAt(t3)); + char0 = digit1 * 16 + digit2 - (digit2 & 256); + if (char0 === 37) + char0 = -1; + i0 = i1; + } else + char0 = -1; + } else + char0 = char; + if (0 <= char0 && char0 <= 127) { + if (!(char0 >= 0 && char0 < t2)) + return A.ioore(inverseAlphabet, char0); + value = inverseAlphabet[char0]; + if (value >= 0) { + if (!(value < 64)) + return A.ioore(_s64_, value); + char0 = _s64_.charCodeAt(value); + if (char0 === char) + continue; + char = char0; + } else { + if (value === -1) { + if (firstPadding < 0) { + t3 = buffer == null ? null : buffer._contents.length; + if (t3 == null) + t3 = 0; + firstPadding = t3 + (i - sliceStart); + firstPaddingSourceIndex = i; + } + ++paddingCount; + if (char === 61) + continue; + } + char = char0; + } + if (value !== -2) { + if (buffer == null) { + buffer = new A.StringBuffer(""); + t3 = buffer; + } else + t3 = buffer; + t3._contents += B.JSString_methods.substring$2(source, sliceStart, i); + t3._contents += A.Primitives_stringFromCharCode(char); + sliceStart = i0; + continue; + } + } + throw A.wrapException(A.FormatException$("Invalid base64 data", source, i)); + } + if (buffer != null) { + t1 = buffer._contents += B.JSString_methods.substring$2(source, sliceStart, end); + t2 = t1.length; + if (firstPadding >= 0) + A.Base64Codec__checkPadding(source, firstPaddingSourceIndex, end, firstPadding, paddingCount, t2); + else { + endLength = B.JSInt_methods.$mod(t2 - 1, 4) + 1; + if (endLength === 1) + throw A.wrapException(A.FormatException$(_s31_, source, end)); + for (; endLength < 4;) { + t1 += "="; + buffer._contents = t1; + ++endLength; + } + } + t1 = buffer._contents; + return B.JSString_methods.replaceRange$3(source, start, end, t1.charCodeAt(0) == 0 ? t1 : t1); + } + $length = end - start; + if (firstPadding >= 0) + A.Base64Codec__checkPadding(source, firstPaddingSourceIndex, end, firstPadding, paddingCount, $length); + else { + endLength = B.JSInt_methods.$mod($length, 4); + if (endLength === 1) + throw A.wrapException(A.FormatException$(_s31_, source, end)); + if (endLength > 1) + source = B.JSString_methods.replaceRange$3(source, end, end, endLength === 2 ? "==" : "="); + } + return source; + } + }; + A.Base64Encoder.prototype = {}; + A.Codec.prototype = {}; + A._FusedCodec.prototype = {}; + A.Converter.prototype = {$isStreamTransformer: 1}; + A.Encoding.prototype = {}; + A.JsonUnsupportedObjectError.prototype = { + toString$0(_) { + var safeString = A.Error_safeToString(this.unsupportedObject); + return (this.cause != null ? "Converting object to an encodable object failed:" : "Converting object did not return an encodable object:") + " " + safeString; + } + }; + A.JsonCyclicError.prototype = { + toString$0(_) { + return "Cyclic error in JSON stringify"; + } + }; + A.JsonCodec.prototype = { + decode$2$reviver(_, source, reviver) { + var t1 = A._parseJson(source, this.get$decoder()._reviver); + return t1; + }, + encode$2$toEncodable(value, toEncodable) { + var t1 = A._JsonStringStringifier_stringify(value, this.get$encoder()._toEncodable, null); + return t1; + }, + get$encoder() { + return B.JsonEncoder_null; + }, + get$decoder() { + return B.JsonDecoder_null; + } + }; + A.JsonEncoder.prototype = {}; + A.JsonDecoder.prototype = {}; + A._JsonStringifier.prototype = { + writeStringContent$1(s) { + var offset, i, charCode, t1, t2, _this = this, + $length = s.length; + for (offset = 0, i = 0; i < $length; ++i) { + charCode = s.charCodeAt(i); + if (charCode > 92) { + if (charCode >= 55296) { + t1 = charCode & 64512; + if (t1 === 55296) { + t2 = i + 1; + t2 = !(t2 < $length && (s.charCodeAt(t2) & 64512) === 56320); + } else + t2 = false; + if (!t2) + if (t1 === 56320) { + t1 = i - 1; + t1 = !(t1 >= 0 && (s.charCodeAt(t1) & 64512) === 55296); + } else + t1 = false; + else + t1 = true; + if (t1) { + if (i > offset) + _this.writeStringSlice$3(s, offset, i); + offset = i + 1; + _this.writeCharCode$1(92); + _this.writeCharCode$1(117); + _this.writeCharCode$1(100); + t1 = charCode >>> 8 & 15; + _this.writeCharCode$1(t1 < 10 ? 48 + t1 : 87 + t1); + t1 = charCode >>> 4 & 15; + _this.writeCharCode$1(t1 < 10 ? 48 + t1 : 87 + t1); + t1 = charCode & 15; + _this.writeCharCode$1(t1 < 10 ? 48 + t1 : 87 + t1); + } + } + continue; + } + if (charCode < 32) { + if (i > offset) + _this.writeStringSlice$3(s, offset, i); + offset = i + 1; + _this.writeCharCode$1(92); + switch (charCode) { + case 8: + _this.writeCharCode$1(98); + break; + case 9: + _this.writeCharCode$1(116); + break; + case 10: + _this.writeCharCode$1(110); + break; + case 12: + _this.writeCharCode$1(102); + break; + case 13: + _this.writeCharCode$1(114); + break; + default: + _this.writeCharCode$1(117); + _this.writeCharCode$1(48); + _this.writeCharCode$1(48); + t1 = charCode >>> 4 & 15; + _this.writeCharCode$1(t1 < 10 ? 48 + t1 : 87 + t1); + t1 = charCode & 15; + _this.writeCharCode$1(t1 < 10 ? 48 + t1 : 87 + t1); + break; + } + } else if (charCode === 34 || charCode === 92) { + if (i > offset) + _this.writeStringSlice$3(s, offset, i); + offset = i + 1; + _this.writeCharCode$1(92); + _this.writeCharCode$1(charCode); + } + } + if (offset === 0) + _this.writeString$1(s); + else if (offset < $length) + _this.writeStringSlice$3(s, offset, $length); + }, + _checkCycle$1(object) { + var t1, t2, i, t3; + for (t1 = this._seen, t2 = t1.length, i = 0; i < t2; ++i) { + t3 = t1[i]; + if (object == null ? t3 == null : object === t3) + throw A.wrapException(new A.JsonCyclicError(object, null)); + } + B.JSArray_methods.add$1(t1, object); + }, + writeObject$1(object) { + var customJson, e, t1, exception, _this = this; + if (_this.writeJsonValue$1(object)) + return; + _this._checkCycle$1(object); + try { + customJson = _this._toEncodable.call$1(object); + if (!_this.writeJsonValue$1(customJson)) { + t1 = A.JsonUnsupportedObjectError$(object, null, _this.get$_partialResult()); + throw A.wrapException(t1); + } + t1 = _this._seen; + if (0 >= t1.length) + return A.ioore(t1, -1); + t1.pop(); + } catch (exception) { + e = A.unwrapException(exception); + t1 = A.JsonUnsupportedObjectError$(object, e, _this.get$_partialResult()); + throw A.wrapException(t1); + } + }, + writeJsonValue$1(object) { + var t1, success, _this = this; + if (typeof object == "number") { + if (!isFinite(object)) + return false; + _this.writeNumber$1(object); + return true; + } else if (object === true) { + _this.writeString$1("true"); + return true; + } else if (object === false) { + _this.writeString$1("false"); + return true; + } else if (object == null) { + _this.writeString$1("null"); + return true; + } else if (typeof object == "string") { + _this.writeString$1('"'); + _this.writeStringContent$1(object); + _this.writeString$1('"'); + return true; + } else if (type$.List_dynamic._is(object)) { + _this._checkCycle$1(object); + _this.writeList$1(object); + t1 = _this._seen; + if (0 >= t1.length) + return A.ioore(t1, -1); + t1.pop(); + return true; + } else if (type$.Map_dynamic_dynamic._is(object)) { + _this._checkCycle$1(object); + success = _this.writeMap$1(object); + t1 = _this._seen; + if (0 >= t1.length) + return A.ioore(t1, -1); + t1.pop(); + return success; + } else + return false; + }, + writeList$1(list) { + var t1, i, _this = this; + _this.writeString$1("["); + t1 = J.getInterceptor$asx(list); + if (t1.get$isNotEmpty(list)) { + _this.writeObject$1(t1.$index(list, 0)); + for (i = 1; i < t1.get$length(list); ++i) { + _this.writeString$1(","); + _this.writeObject$1(t1.$index(list, i)); + } + } + _this.writeString$1("]"); + }, + writeMap$1(map) { + var t2, keyValueList, i, separator, _this = this, _box_0 = {}, + t1 = J.getInterceptor$asx(map); + if (t1.get$isEmpty(map)) { + _this.writeString$1("{}"); + return true; + } + t2 = t1.get$length(map) * 2; + keyValueList = A.List_List$filled(t2, null, false, type$.nullable_Object); + i = _box_0.i = 0; + _box_0.allStringKeys = true; + t1.forEach$1(map, new A._JsonStringifier_writeMap_closure(_box_0, keyValueList)); + if (!_box_0.allStringKeys) + return false; + _this.writeString$1("{"); + for (separator = '"'; i < t2; i += 2, separator = ',"') { + _this.writeString$1(separator); + _this.writeStringContent$1(A._asString(keyValueList[i])); + _this.writeString$1('":'); + t1 = i + 1; + if (!(t1 < t2)) + return A.ioore(keyValueList, t1); + _this.writeObject$1(keyValueList[t1]); + } + _this.writeString$1("}"); + return true; + } + }; + A._JsonStringifier_writeMap_closure.prototype = { + call$2(key, value) { + var t1, t2; + if (typeof key != "string") + this._box_0.allStringKeys = false; + t1 = this.keyValueList; + t2 = this._box_0; + B.JSArray_methods.$indexSet(t1, t2.i++, key); + B.JSArray_methods.$indexSet(t1, t2.i++, value); + }, + $signature: 16 + }; + A._JsonStringStringifier.prototype = { + get$_partialResult() { + var t1 = this._sink; + return t1 instanceof A.StringBuffer ? t1.toString$0(0) : null; + }, + writeNumber$1(number) { + this._sink.write$1(0, B.JSNumber_methods.toString$0(number)); + }, + writeString$1(string) { + this._sink.write$1(0, string); + }, + writeStringSlice$3(string, start, end) { + this._sink.write$1(0, B.JSString_methods.substring$2(string, start, end)); + }, + writeCharCode$1(charCode) { + this._sink.writeCharCode$1(charCode); + } + }; + A.Utf8Codec.prototype = {}; + A.Utf8Encoder.prototype = { + convert$1(string) { + var stringLength, end, $length, t1, encoder, t2; + A._asString(string); + stringLength = string.length; + end = A.RangeError_checkValidRange(0, null, stringLength); + $length = end - 0; + if ($length === 0) + return new Uint8Array(0); + t1 = new Uint8Array($length * 3); + encoder = new A._Utf8Encoder(t1); + if (encoder._fillBuffer$3(string, 0, end) !== end) { + t2 = end - 1; + if (!(t2 >= 0 && t2 < stringLength)) + return A.ioore(string, t2); + encoder._writeReplacementCharacter$0(); + } + return B.NativeUint8List_methods.sublist$2(t1, 0, encoder._bufferIndex); + } + }; + A._Utf8Encoder.prototype = { + _writeReplacementCharacter$0() { + var _this = this, + t1 = _this._buffer, + t2 = _this._bufferIndex, + t3 = _this._bufferIndex = t2 + 1, + t4 = t1.length; + if (!(t2 < t4)) + return A.ioore(t1, t2); + t1[t2] = 239; + t2 = _this._bufferIndex = t3 + 1; + if (!(t3 < t4)) + return A.ioore(t1, t3); + t1[t3] = 191; + _this._bufferIndex = t2 + 1; + if (!(t2 < t4)) + return A.ioore(t1, t2); + t1[t2] = 189; + }, + _writeSurrogate$2(leadingSurrogate, nextCodeUnit) { + var rune, t1, t2, t3, t4, _this = this; + if ((nextCodeUnit & 64512) === 56320) { + rune = 65536 + ((leadingSurrogate & 1023) << 10) | nextCodeUnit & 1023; + t1 = _this._buffer; + t2 = _this._bufferIndex; + t3 = _this._bufferIndex = t2 + 1; + t4 = t1.length; + if (!(t2 < t4)) + return A.ioore(t1, t2); + t1[t2] = rune >>> 18 | 240; + t2 = _this._bufferIndex = t3 + 1; + if (!(t3 < t4)) + return A.ioore(t1, t3); + t1[t3] = rune >>> 12 & 63 | 128; + t3 = _this._bufferIndex = t2 + 1; + if (!(t2 < t4)) + return A.ioore(t1, t2); + t1[t2] = rune >>> 6 & 63 | 128; + _this._bufferIndex = t3 + 1; + if (!(t3 < t4)) + return A.ioore(t1, t3); + t1[t3] = rune & 63 | 128; + return true; + } else { + _this._writeReplacementCharacter$0(); + return false; + } + }, + _fillBuffer$3(str, start, end) { + var t1, t2, t3, stringIndex, codeUnit, t4, t5, _this = this; + if (start !== end) { + t1 = end - 1; + if (!(t1 >= 0 && t1 < str.length)) + return A.ioore(str, t1); + t1 = (str.charCodeAt(t1) & 64512) === 55296; + } else + t1 = false; + if (t1) + --end; + for (t1 = _this._buffer, t2 = t1.length, t3 = str.length, stringIndex = start; stringIndex < end; ++stringIndex) { + if (!(stringIndex < t3)) + return A.ioore(str, stringIndex); + codeUnit = str.charCodeAt(stringIndex); + if (codeUnit <= 127) { + t4 = _this._bufferIndex; + if (t4 >= t2) + break; + _this._bufferIndex = t4 + 1; + t1[t4] = codeUnit; + } else { + t4 = codeUnit & 64512; + if (t4 === 55296) { + if (_this._bufferIndex + 4 > t2) + break; + t4 = stringIndex + 1; + if (!(t4 < t3)) + return A.ioore(str, t4); + if (_this._writeSurrogate$2(codeUnit, str.charCodeAt(t4))) + stringIndex = t4; + } else if (t4 === 56320) { + if (_this._bufferIndex + 3 > t2) + break; + _this._writeReplacementCharacter$0(); + } else if (codeUnit <= 2047) { + t4 = _this._bufferIndex; + t5 = t4 + 1; + if (t5 >= t2) + break; + _this._bufferIndex = t5; + if (!(t4 < t2)) + return A.ioore(t1, t4); + t1[t4] = codeUnit >>> 6 | 192; + _this._bufferIndex = t5 + 1; + t1[t5] = codeUnit & 63 | 128; + } else { + t4 = _this._bufferIndex; + if (t4 + 2 >= t2) + break; + t5 = _this._bufferIndex = t4 + 1; + if (!(t4 < t2)) + return A.ioore(t1, t4); + t1[t4] = codeUnit >>> 12 | 224; + t4 = _this._bufferIndex = t5 + 1; + if (!(t5 < t2)) + return A.ioore(t1, t5); + t1[t5] = codeUnit >>> 6 & 63 | 128; + _this._bufferIndex = t4 + 1; + if (!(t4 < t2)) + return A.ioore(t1, t4); + t1[t4] = codeUnit & 63 | 128; + } + } + } + return stringIndex; + } + }; + A.Utf8Decoder.prototype = { + convert$1(codeUnits) { + var t1, result; + type$.List_int._as(codeUnits); + t1 = this._allowMalformed; + result = A.Utf8Decoder__convertIntercepted(t1, codeUnits, 0, null); + if (result != null) + return result; + return new A._Utf8Decoder(t1).convertGeneral$4(codeUnits, 0, null, true); + } + }; + A._Utf8Decoder.prototype = { + convertGeneral$4(codeUnits, start, maybeEnd, single) { + var end, bytes, errorOffset, result, t1, message, _this = this; + type$.List_int._as(codeUnits); + end = A.RangeError_checkValidRange(start, maybeEnd, J.get$length$asx(codeUnits)); + if (start === end) + return ""; + if (type$.Uint8List._is(codeUnits)) { + bytes = codeUnits; + errorOffset = 0; + } else { + bytes = A._Utf8Decoder__makeUint8List(codeUnits, start, end); + end -= start; + errorOffset = start; + start = 0; + } + result = _this._convertRecursive$4(bytes, start, end, single); + t1 = _this._state; + if ((t1 & 1) !== 0) { + message = A._Utf8Decoder_errorDescription(t1); + _this._state = 0; + throw A.wrapException(A.FormatException$(message, codeUnits, errorOffset + _this._charOrIndex)); + } + return result; + }, + _convertRecursive$4(bytes, start, end, single) { + var mid, s1, _this = this; + if (end - start > 1000) { + mid = B.JSInt_methods._tdivFast$1(start + end, 2); + s1 = _this._convertRecursive$4(bytes, start, mid, false); + if ((_this._state & 1) !== 0) + return s1; + return s1 + _this._convertRecursive$4(bytes, mid, end, single); + } + return _this.decodeGeneral$4(bytes, start, end, single); + }, + decodeGeneral$4(bytes, start, end, single) { + var byte, t2, type, t3, i0, markEnd, i1, m, _this = this, + _s256_ = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFFFFFFFFFFFFFFFFGGGGGGGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHIHHHJEEBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBKCCCCCCCCCCCCDCLONNNMEEEEEEEEEEE", + _s144_ = " \x000:XECCCCCN:lDb \x000:XECCCCCNvlDb \x000:XECCCCCN:lDb AAAAA\x00\x00\x00\x00\x00AAAAA00000AAAAA:::::AAAAAGG000AAAAA00KKKAAAAAG::::AAAAA:IIIIAAAAA000\x800AAAAA\x00\x00\x00\x00 AAAAA", + _65533 = 65533, + state = _this._state, + char = _this._charOrIndex, + buffer = new A.StringBuffer(""), + i = start + 1, + t1 = bytes.length; + if (!(start >= 0 && start < t1)) + return A.ioore(bytes, start); + byte = bytes[start]; + $label0$0: + for (t2 = _this.allowMalformed; true;) { + for (; true; i = i0) { + if (!(byte >= 0 && byte < 256)) + return A.ioore(_s256_, byte); + type = _s256_.charCodeAt(byte) & 31; + char = state <= 32 ? byte & 61694 >>> type : (byte & 63 | char << 6) >>> 0; + t3 = state + type; + if (!(t3 >= 0 && t3 < 144)) + return A.ioore(_s144_, t3); + state = _s144_.charCodeAt(t3); + if (state === 0) { + buffer._contents += A.Primitives_stringFromCharCode(char); + if (i === end) + break $label0$0; + break; + } else if ((state & 1) !== 0) { + if (t2) + switch (state) { + case 69: + case 67: + buffer._contents += A.Primitives_stringFromCharCode(_65533); + break; + case 65: + buffer._contents += A.Primitives_stringFromCharCode(_65533); + --i; + break; + default: + t3 = buffer._contents += A.Primitives_stringFromCharCode(_65533); + buffer._contents = t3 + A.Primitives_stringFromCharCode(_65533); + break; + } + else { + _this._state = state; + _this._charOrIndex = i - 1; + return ""; + } + state = 0; + } + if (i === end) + break $label0$0; + i0 = i + 1; + if (!(i >= 0 && i < t1)) + return A.ioore(bytes, i); + byte = bytes[i]; + } + i0 = i + 1; + if (!(i >= 0 && i < t1)) + return A.ioore(bytes, i); + byte = bytes[i]; + if (byte < 128) { + while (true) { + if (!(i0 < end)) { + markEnd = end; + break; + } + i1 = i0 + 1; + if (!(i0 >= 0 && i0 < t1)) + return A.ioore(bytes, i0); + byte = bytes[i0]; + if (byte >= 128) { + markEnd = i1 - 1; + i0 = i1; + break; + } + i0 = i1; + } + if (markEnd - i < 20) + for (m = i; m < markEnd; ++m) { + if (!(m < t1)) + return A.ioore(bytes, m); + buffer._contents += A.Primitives_stringFromCharCode(bytes[m]); + } + else + buffer._contents += A.String_String$fromCharCodes(bytes, i, markEnd); + if (markEnd === end) + break $label0$0; + i = i0; + } else + i = i0; + } + if (single && state > 32) + if (t2) + buffer._contents += A.Primitives_stringFromCharCode(_65533); + else { + _this._state = 77; + _this._charOrIndex = end; + return ""; + } + _this._state = state; + _this._charOrIndex = char; + t1 = buffer._contents; + return t1.charCodeAt(0) == 0 ? t1 : t1; + } + }; + A.NoSuchMethodError_toString_closure.prototype = { + call$2(key, value) { + var t1, t2, t3; + type$.Symbol._as(key); + t1 = this.sb; + t2 = this._box_0; + t3 = t1._contents += t2.comma; + t3 += key._name; + t1._contents = t3; + t1._contents = t3 + ": "; + t1._contents += A.Error_safeToString(value); + t2.comma = ", "; + }, + $signature: 39 + }; + A.DateTime.prototype = { + $eq(_, other) { + if (other == null) + return false; + return other instanceof A.DateTime && this._core$_value === other._core$_value && true; + }, + get$hashCode(_) { + var t1 = this._core$_value; + return (t1 ^ B.JSInt_methods._shrOtherPositive$1(t1, 30)) & 1073741823; + }, + toString$0(_) { + var _this = this, + y = A.DateTime__fourDigits(A.Primitives_getYear(_this)), + m = A.DateTime__twoDigits(A.Primitives_getMonth(_this)), + d = A.DateTime__twoDigits(A.Primitives_getDay(_this)), + h = A.DateTime__twoDigits(A.Primitives_getHours(_this)), + min = A.DateTime__twoDigits(A.Primitives_getMinutes(_this)), + sec = A.DateTime__twoDigits(A.Primitives_getSeconds(_this)), + ms = A.DateTime__threeDigits(A.Primitives_getMilliseconds(_this)); + return y + "-" + m + "-" + d + " " + h + ":" + min + ":" + sec + "." + ms + "Z"; + } + }; + A.Duration.prototype = { + $eq(_, other) { + if (other == null) + return false; + return other instanceof A.Duration && this._duration === other._duration; + }, + get$hashCode(_) { + return B.JSInt_methods.get$hashCode(this._duration); + }, + toString$0(_) { + var minutes, minutesPadding, seconds, secondsPadding, + microseconds = this._duration, + hours = B.JSInt_methods._tdivFast$1(microseconds, 3600000000); + microseconds %= 3600000000; + minutes = B.JSInt_methods._tdivFast$1(microseconds, 60000000); + microseconds %= 60000000; + minutesPadding = minutes < 10 ? "0" : ""; + seconds = B.JSInt_methods._tdivFast$1(microseconds, 1000000); + secondsPadding = seconds < 10 ? "0" : ""; + return "" + hours + ":" + minutesPadding + minutes + ":" + secondsPadding + seconds + "." + B.JSString_methods.padLeft$2(B.JSInt_methods.toString$0(microseconds % 1000000), 6, "0"); + } + }; + A.Error.prototype = { + get$stackTrace() { + return A.getTraceFromException(this.$thrownJsError); + } + }; + A.AssertionError.prototype = { + toString$0(_) { + var t1 = this.message; + if (t1 != null) + return "Assertion failed: " + A.Error_safeToString(t1); + return "Assertion failed"; + } + }; + A.TypeError.prototype = {}; + A.ArgumentError.prototype = { + get$_errorName() { + return "Invalid argument" + (!this._hasValue ? "(s)" : ""); + }, + get$_errorExplanation() { + return ""; + }, + toString$0(_) { + var _this = this, + $name = _this.name, + nameString = $name == null ? "" : " (" + $name + ")", + message = _this.message, + messageString = message == null ? "" : ": " + A.S(message), + prefix = _this.get$_errorName() + nameString + messageString; + if (!_this._hasValue) + return prefix; + return prefix + _this.get$_errorExplanation() + ": " + A.Error_safeToString(_this.get$invalidValue()); + }, + get$invalidValue() { + return this.invalidValue; + } + }; + A.RangeError.prototype = { + get$invalidValue() { + return A._asNumQ(this.invalidValue); + }, + get$_errorName() { + return "RangeError"; + }, + get$_errorExplanation() { + var explanation, + start = this.start, + end = this.end; + if (start == null) + explanation = end != null ? ": Not less than or equal to " + A.S(end) : ""; + else if (end == null) + explanation = ": Not greater than or equal to " + A.S(start); + else if (end > start) + explanation = ": Not in inclusive range " + A.S(start) + ".." + A.S(end); + else + explanation = end < start ? ": Valid value range is empty" : ": Only valid value is " + A.S(start); + return explanation; + } + }; + A.IndexError.prototype = { + get$invalidValue() { + return A._asInt(this.invalidValue); + }, + get$_errorName() { + return "RangeError"; + }, + get$_errorExplanation() { + if (A._asInt(this.invalidValue) < 0) + return ": index must not be negative"; + var t1 = this.length; + if (t1 === 0) + return ": no indices are valid"; + return ": index should be less than " + t1; + }, + get$length(receiver) { + return this.length; + } + }; + A.NoSuchMethodError.prototype = { + toString$0(_) { + var $arguments, t1, _i, t2, t3, argument, receiverText, actualParameters, _this = this, _box_0 = {}, + sb = new A.StringBuffer(""); + _box_0.comma = ""; + $arguments = _this._core$_arguments; + for (t1 = $arguments.length, _i = 0, t2 = "", t3 = ""; _i < t1; ++_i, t3 = ", ") { + argument = $arguments[_i]; + sb._contents = t2 + t3; + t2 = sb._contents += A.Error_safeToString(argument); + _box_0.comma = ", "; + } + _this._namedArguments.forEach$1(0, new A.NoSuchMethodError_toString_closure(_box_0, sb)); + receiverText = A.Error_safeToString(_this._core$_receiver); + actualParameters = sb.toString$0(0); + return "NoSuchMethodError: method not found: '" + _this._core$_memberName._name + "'\nReceiver: " + receiverText + "\nArguments: [" + actualParameters + "]"; + } + }; + A.UnsupportedError.prototype = { + toString$0(_) { + return "Unsupported operation: " + this.message; + } + }; + A.UnimplementedError.prototype = { + toString$0(_) { + return "UnimplementedError: " + this.message; + } + }; + A.StateError.prototype = { + toString$0(_) { + return "Bad state: " + this.message; + } + }; + A.ConcurrentModificationError.prototype = { + toString$0(_) { + var t1 = this.modifiedObject; + if (t1 == null) + return "Concurrent modification during iteration."; + return "Concurrent modification during iteration: " + A.Error_safeToString(t1) + "."; + } + }; + A.OutOfMemoryError.prototype = { + toString$0(_) { + return "Out of Memory"; + }, + get$stackTrace() { + return null; + }, + $isError: 1 + }; + A.StackOverflowError.prototype = { + toString$0(_) { + return "Stack Overflow"; + }, + get$stackTrace() { + return null; + }, + $isError: 1 + }; + A._Exception.prototype = { + toString$0(_) { + return "Exception: " + this.message; + }, + $isException: 1 + }; + A.FormatException.prototype = { + toString$0(_) { + var t1, lineEnd, lineNum, lineStart, previousCharWasCR, i, char, end, start, prefix, postfix, + message = this.message, + report = "" !== message ? "FormatException: " + message : "FormatException", + offset = this.offset, + source = this.source; + if (typeof source == "string") { + if (offset != null) + t1 = offset < 0 || offset > source.length; + else + t1 = false; + if (t1) + offset = null; + if (offset == null) { + if (source.length > 78) + source = B.JSString_methods.substring$2(source, 0, 75) + "..."; + return report + "\n" + source; + } + for (lineEnd = source.length, lineNum = 1, lineStart = 0, previousCharWasCR = false, i = 0; i < offset; ++i) { + if (!(i < lineEnd)) + return A.ioore(source, i); + char = source.charCodeAt(i); + if (char === 10) { + if (lineStart !== i || !previousCharWasCR) + ++lineNum; + lineStart = i + 1; + previousCharWasCR = false; + } else if (char === 13) { + ++lineNum; + lineStart = i + 1; + previousCharWasCR = true; + } + } + report = lineNum > 1 ? report + (" (at line " + lineNum + ", character " + (offset - lineStart + 1) + ")\n") : report + (" (at character " + (offset + 1) + ")\n"); + for (i = offset; i < lineEnd; ++i) { + if (!(i >= 0)) + return A.ioore(source, i); + char = source.charCodeAt(i); + if (char === 10 || char === 13) { + lineEnd = i; + break; + } + } + if (lineEnd - lineStart > 78) + if (offset - lineStart < 75) { + end = lineStart + 75; + start = lineStart; + prefix = ""; + postfix = "..."; + } else { + if (lineEnd - offset < 75) { + start = lineEnd - 75; + end = lineEnd; + postfix = ""; + } else { + start = offset - 36; + end = offset + 36; + postfix = "..."; + } + prefix = "..."; + } + else { + end = lineEnd; + start = lineStart; + prefix = ""; + postfix = ""; + } + return report + prefix + B.JSString_methods.substring$2(source, start, end) + postfix + "\n" + B.JSString_methods.$mul(" ", offset - start + prefix.length) + "^\n"; + } else + return offset != null ? report + (" (at offset " + A.S(offset) + ")") : report; + }, + $isException: 1 + }; + A.Iterable.prototype = { + cast$1$0(_, $R) { + return A.CastIterable_CastIterable(this, A._instanceType(this)._eval$1("Iterable.E"), $R); + }, + map$1$1(_, toElement, $T) { + var t1 = A._instanceType(this); + return A.MappedIterable_MappedIterable(this, t1._bind$1($T)._eval$1("1(Iterable.E)")._as(toElement), t1._eval$1("Iterable.E"), $T); + }, + toList$1$growable(_, growable) { + return A.List_List$of(this, growable, A._instanceType(this)._eval$1("Iterable.E")); + }, + toList$0($receiver) { + return this.toList$1$growable($receiver, true); + }, + get$length(_) { + var count, + it = this.get$iterator(this); + for (count = 0; it.moveNext$0();) + ++count; + return count; + }, + get$isEmpty(_) { + return !this.get$iterator(this).moveNext$0(); + }, + get$isNotEmpty(_) { + return !this.get$isEmpty(this); + }, + skip$1(_, count) { + return A.SkipIterable_SkipIterable(this, count, A._instanceType(this)._eval$1("Iterable.E")); + }, + skipWhile$1(_, test) { + var t1 = A._instanceType(this); + return new A.SkipWhileIterable(this, t1._eval$1("bool(Iterable.E)")._as(test), t1._eval$1("SkipWhileIterable<Iterable.E>")); + }, + get$first(_) { + var it = this.get$iterator(this); + if (!it.moveNext$0()) + throw A.wrapException(A.IterableElementError_noElement()); + return it.get$current(it); + }, + get$last(_) { + var result, + it = this.get$iterator(this); + if (!it.moveNext$0()) + throw A.wrapException(A.IterableElementError_noElement()); + do + result = it.get$current(it); + while (it.moveNext$0()); + return result; + }, + elementAt$1(_, index) { + var iterator, skipCount; + A.RangeError_checkNotNegative(index, "index"); + iterator = this.get$iterator(this); + for (skipCount = index; iterator.moveNext$0();) { + if (skipCount === 0) + return iterator.get$current(iterator); + --skipCount; + } + throw A.wrapException(A.IndexError$withLength(index, index - skipCount, this, "index")); + }, + toString$0(_) { + return A.Iterable_iterableToShortString(this, "(", ")"); + } + }; + A.Null.prototype = { + get$hashCode(_) { + return A.Object.prototype.get$hashCode.call(this, this); + }, + toString$0(_) { + return "null"; + } + }; + A.Object.prototype = {$isObject: 1, + $eq(_, other) { + return this === other; + }, + get$hashCode(_) { + return A.Primitives_objectHashCode(this); + }, + toString$0(_) { + return "Instance of '" + A.Primitives_objectTypeName(this) + "'"; + }, + noSuchMethod$1(_, invocation) { + throw A.wrapException(A.NoSuchMethodError_NoSuchMethodError$withInvocation(this, type$.Invocation._as(invocation))); + }, + get$runtimeType(_) { + return A.getRuntimeTypeOfDartObject(this); + }, + toString() { + return this.toString$0(this); + } + }; + A._StringStackTrace.prototype = { + toString$0(_) { + return this._stackTrace; + }, + $isStackTrace: 1 + }; + A.StringBuffer.prototype = { + get$length(_) { + return this._contents.length; + }, + write$1(_, obj) { + this._contents += A.S(obj); + }, + writeCharCode$1(charCode) { + this._contents += A.Primitives_stringFromCharCode(charCode); + }, + toString$0(_) { + var t1 = this._contents; + return t1.charCodeAt(0) == 0 ? t1 : t1; + }, + $isStringSink: 1 + }; + A.Uri_splitQueryString_closure.prototype = { + call$2(map, element) { + var index, key, value, t1; + type$.Map_String_String._as(map); + A._asString(element); + index = B.JSString_methods.indexOf$1(element, "="); + if (index === -1) { + if (element !== "") + J.$indexSet$ax(map, A._Uri__uriDecode(element, 0, element.length, this.encoding, true), ""); + } else if (index !== 0) { + key = B.JSString_methods.substring$2(element, 0, index); + value = B.JSString_methods.substring$1(element, index + 1); + t1 = this.encoding; + J.$indexSet$ax(map, A._Uri__uriDecode(key, 0, key.length, t1, true), A._Uri__uriDecode(value, 0, value.length, t1, true)); + } + return map; + }, + $signature: 40 + }; + A.Uri__parseIPv4Address_error.prototype = { + call$2(msg, position) { + throw A.wrapException(A.FormatException$("Illegal IPv4 address, " + msg, this.host, position)); + }, + $signature: 25 + }; + A.Uri_parseIPv6Address_error.prototype = { + call$2(msg, position) { + throw A.wrapException(A.FormatException$("Illegal IPv6 address, " + msg, this.host, position)); + }, + $signature: 45 + }; + A.Uri_parseIPv6Address_parseHex.prototype = { + call$2(start, end) { + var value; + if (end - start > 4) + this.error.call$2("an IPv6 part can only contain a maximum of 4 hex digits", start); + value = A.int_parse(B.JSString_methods.substring$2(this.host, start, end), 16); + if (value < 0 || value > 65535) + this.error.call$2("each part must be in the range of `0x0..0xFFFF`", start); + return value; + }, + $signature: 46 + }; + A._Uri.prototype = { + get$_text() { + var t1, t2, t3, t4, _this = this, + value = _this.___Uri__text_FI; + if (value === $) { + t1 = _this.scheme; + t2 = t1.length !== 0 ? "" + t1 + ":" : ""; + t3 = _this._host; + t4 = t3 == null; + if (!t4 || t1 === "file") { + t1 = t2 + "//"; + t2 = _this._userInfo; + if (t2.length !== 0) + t1 = t1 + t2 + "@"; + if (!t4) + t1 += t3; + t2 = _this._port; + if (t2 != null) + t1 = t1 + ":" + A.S(t2); + } else + t1 = t2; + t1 += _this.path; + t2 = _this._query; + if (t2 != null) + t1 = t1 + "?" + t2; + t2 = _this._fragment; + if (t2 != null) + t1 = t1 + "#" + t2; + value !== $ && A.throwLateFieldADI("_text"); + value = _this.___Uri__text_FI = t1.charCodeAt(0) == 0 ? t1 : t1; + } + return value; + }, + get$pathSegments() { + var pathToSplit, t1, result, _this = this, + value = _this.___Uri_pathSegments_FI; + if (value === $) { + pathToSplit = _this.path; + t1 = pathToSplit.length; + if (t1 !== 0) { + if (0 >= t1) + return A.ioore(pathToSplit, 0); + t1 = pathToSplit.charCodeAt(0) === 47; + } else + t1 = false; + if (t1) + pathToSplit = B.JSString_methods.substring$1(pathToSplit, 1); + result = pathToSplit.length === 0 ? B.List_empty : A.List_List$unmodifiable(new A.MappedListIterable(A._setArrayType(pathToSplit.split("/"), type$.JSArray_String), type$.dynamic_Function_String._as(A.core_Uri_decodeComponent$closure()), type$.MappedListIterable_String_dynamic), type$.String); + _this.___Uri_pathSegments_FI !== $ && A.throwLateFieldADI("pathSegments"); + _this.set$___Uri_pathSegments_FI(result); + value = result; + } + return value; + }, + get$hashCode(_) { + var result, _this = this, + value = _this.___Uri_hashCode_FI; + if (value === $) { + result = B.JSString_methods.get$hashCode(_this.get$_text()); + _this.___Uri_hashCode_FI !== $ && A.throwLateFieldADI("hashCode"); + _this.___Uri_hashCode_FI = result; + value = result; + } + return value; + }, + get$queryParameters() { + var t1, result, _this = this, + value = _this.___Uri_queryParameters_FI; + if (value === $) { + t1 = _this._query; + result = new A.UnmodifiableMapView(A.Uri_splitQueryString(t1 == null ? "" : t1), type$.UnmodifiableMapView_String_String); + _this.___Uri_queryParameters_FI !== $ && A.throwLateFieldADI("queryParameters"); + _this.set$___Uri_queryParameters_FI(result); + value = result; + } + return value; + }, + get$userInfo() { + return this._userInfo; + }, + get$host(_) { + var host = this._host; + if (host == null) + return ""; + if (B.JSString_methods.startsWith$1(host, "[")) + return B.JSString_methods.substring$2(host, 1, host.length - 1); + return host; + }, + get$port(_) { + var t1 = this._port; + return t1 == null ? A._Uri__defaultPort(this.scheme) : t1; + }, + get$query(_) { + var t1 = this._query; + return t1 == null ? "" : t1; + }, + get$fragment() { + var t1 = this._fragment; + return t1 == null ? "" : t1; + }, + isScheme$1(scheme) { + var thisScheme = this.scheme; + if (scheme.length !== thisScheme.length) + return false; + return A._caseInsensitiveCompareStart(scheme, thisScheme, 0) >= 0; + }, + removeFragment$0() { + var _this = this; + if (_this._fragment == null) + return _this; + return A._Uri$_internal(_this.scheme, _this._userInfo, _this._host, _this._port, _this.path, _this._query, null); + }, + _mergePaths$2(base, reference) { + var backCount, refStart, baseEnd, t1, newEnd, delta, t2, t3; + for (backCount = 0, refStart = 0; B.JSString_methods.startsWith$2(reference, "../", refStart);) { + refStart += 3; + ++backCount; + } + baseEnd = B.JSString_methods.lastIndexOf$1(base, "/"); + t1 = base.length; + while (true) { + if (!(baseEnd > 0 && backCount > 0)) + break; + newEnd = B.JSString_methods.lastIndexOf$2(base, "/", baseEnd - 1); + if (newEnd < 0) + break; + delta = baseEnd - newEnd; + t2 = delta !== 2; + if (!t2 || delta === 3) { + t3 = newEnd + 1; + if (!(t3 < t1)) + return A.ioore(base, t3); + if (base.charCodeAt(t3) === 46) + if (t2) { + t2 = newEnd + 2; + if (!(t2 < t1)) + return A.ioore(base, t2); + t2 = base.charCodeAt(t2) === 46; + } else + t2 = true; + else + t2 = false; + } else + t2 = false; + if (t2) + break; + --backCount; + baseEnd = newEnd; + } + return B.JSString_methods.replaceRange$3(base, baseEnd + 1, null, B.JSString_methods.substring$1(reference, refStart - 3 * backCount)); + }, + resolve$1(reference) { + return this.resolveUri$1(A.Uri_parse(reference)); + }, + resolveUri$1(reference) { + var targetScheme, targetUserInfo, targetHost, targetPort, targetPath, targetQuery, packageNameEnd, packageName, mergedPath, t1, _this = this, _null = null; + if (reference.get$scheme().length !== 0) { + targetScheme = reference.get$scheme(); + if (reference.get$hasAuthority()) { + targetUserInfo = reference.get$userInfo(); + targetHost = reference.get$host(reference); + targetPort = reference.get$hasPort() ? reference.get$port(reference) : _null; + } else { + targetPort = _null; + targetHost = targetPort; + targetUserInfo = ""; + } + targetPath = A._Uri__removeDotSegments(reference.get$path(reference)); + targetQuery = reference.get$hasQuery() ? reference.get$query(reference) : _null; + } else { + targetScheme = _this.scheme; + if (reference.get$hasAuthority()) { + targetUserInfo = reference.get$userInfo(); + targetHost = reference.get$host(reference); + targetPort = A._Uri__makePort(reference.get$hasPort() ? reference.get$port(reference) : _null, targetScheme); + targetPath = A._Uri__removeDotSegments(reference.get$path(reference)); + targetQuery = reference.get$hasQuery() ? reference.get$query(reference) : _null; + } else { + targetUserInfo = _this._userInfo; + targetHost = _this._host; + targetPort = _this._port; + targetPath = _this.path; + if (reference.get$path(reference) === "") + targetQuery = reference.get$hasQuery() ? reference.get$query(reference) : _this._query; + else { + packageNameEnd = A._Uri__packageNameEnd(_this, targetPath); + if (packageNameEnd > 0) { + packageName = B.JSString_methods.substring$2(targetPath, 0, packageNameEnd); + targetPath = reference.get$hasAbsolutePath() ? packageName + A._Uri__removeDotSegments(reference.get$path(reference)) : packageName + A._Uri__removeDotSegments(_this._mergePaths$2(B.JSString_methods.substring$1(targetPath, packageName.length), reference.get$path(reference))); + } else if (reference.get$hasAbsolutePath()) + targetPath = A._Uri__removeDotSegments(reference.get$path(reference)); + else if (targetPath.length === 0) + if (targetHost == null) + targetPath = targetScheme.length === 0 ? reference.get$path(reference) : A._Uri__removeDotSegments(reference.get$path(reference)); + else + targetPath = A._Uri__removeDotSegments("/" + reference.get$path(reference)); + else { + mergedPath = _this._mergePaths$2(targetPath, reference.get$path(reference)); + t1 = targetScheme.length === 0; + if (!t1 || targetHost != null || B.JSString_methods.startsWith$1(targetPath, "/")) + targetPath = A._Uri__removeDotSegments(mergedPath); + else + targetPath = A._Uri__normalizeRelativePath(mergedPath, !t1 || targetHost != null); + } + targetQuery = reference.get$hasQuery() ? reference.get$query(reference) : _null; + } + } + } + return A._Uri$_internal(targetScheme, targetUserInfo, targetHost, targetPort, targetPath, targetQuery, reference.get$hasFragment() ? reference.get$fragment() : _null); + }, + get$hasAuthority() { + return this._host != null; + }, + get$hasPort() { + return this._port != null; + }, + get$hasQuery() { + return this._query != null; + }, + get$hasFragment() { + return this._fragment != null; + }, + get$hasAbsolutePath() { + return B.JSString_methods.startsWith$1(this.path, "/"); + }, + toFilePath$0() { + var pathSegments, _this = this, + t1 = _this.scheme; + if (t1 !== "" && t1 !== "file") + throw A.wrapException(A.UnsupportedError$("Cannot extract a file path from a " + t1 + " URI")); + t1 = _this._query; + if ((t1 == null ? "" : t1) !== "") + throw A.wrapException(A.UnsupportedError$(string$.Cannotfq)); + t1 = _this._fragment; + if ((t1 == null ? "" : t1) !== "") + throw A.wrapException(A.UnsupportedError$(string$.Cannotff)); + t1 = $.$get$_Uri__isWindowsCached(); + if (t1) + t1 = A._Uri__toWindowsFilePath(_this); + else { + if (_this._host != null && _this.get$host(_this) !== "") + A.throwExpression(A.UnsupportedError$(string$.Cannotn)); + pathSegments = _this.get$pathSegments(); + A._Uri__checkNonWindowsPathReservedCharacters(pathSegments, false); + t1 = A.StringBuffer__writeAll(B.JSString_methods.startsWith$1(_this.path, "/") ? "" + "/" : "", pathSegments, "/"); + t1 = t1.charCodeAt(0) == 0 ? t1 : t1; + } + return t1; + }, + toString$0(_) { + return this.get$_text(); + }, + $eq(_, other) { + var t1, t2, _this = this; + if (other == null) + return false; + if (_this === other) + return true; + if (type$.Uri._is(other)) + if (_this.scheme === other.get$scheme()) + if (_this._host != null === other.get$hasAuthority()) + if (_this._userInfo === other.get$userInfo()) + if (_this.get$host(_this) === other.get$host(other)) + if (_this.get$port(_this) === other.get$port(other)) + if (_this.path === other.get$path(other)) { + t1 = _this._query; + t2 = t1 == null; + if (!t2 === other.get$hasQuery()) { + if (t2) + t1 = ""; + if (t1 === other.get$query(other)) { + t1 = _this._fragment; + t2 = t1 == null; + if (!t2 === other.get$hasFragment()) { + if (t2) + t1 = ""; + t1 = t1 === other.get$fragment(); + } else + t1 = false; + } else + t1 = false; + } else + t1 = false; + } else + t1 = false; + else + t1 = false; + else + t1 = false; + else + t1 = false; + else + t1 = false; + else + t1 = false; + else + t1 = false; + return t1; + }, + set$___Uri_pathSegments_FI(___Uri_pathSegments_FI) { + this.___Uri_pathSegments_FI = type$.List_String._as(___Uri_pathSegments_FI); + }, + set$___Uri_queryParameters_FI(___Uri_queryParameters_FI) { + this.___Uri_queryParameters_FI = type$.Map_String_String._as(___Uri_queryParameters_FI); + }, + $isUri: 1, + get$scheme() { + return this.scheme; + }, + get$path(receiver) { + return this.path; + } + }; + A._Uri__makePath_closure.prototype = { + call$1(s) { + return A._Uri__uriEncode(B.List_XRg0, A._asString(s), B.C_Utf8Codec, false); + }, + $signature: 17 + }; + A.UriData.prototype = { + get$uri() { + var t2, queryIndex, end, query, _this = this, _null = null, + t1 = _this._uriCache; + if (t1 == null) { + t1 = _this._separatorIndices; + if (0 >= t1.length) + return A.ioore(t1, 0); + t2 = _this._text; + t1 = t1[0] + 1; + queryIndex = B.JSString_methods.indexOf$2(t2, "?", t1); + end = t2.length; + if (queryIndex >= 0) { + query = A._Uri__normalizeOrSubstring(t2, queryIndex + 1, end, B.List_oFp, false, false); + end = queryIndex; + } else + query = _null; + t1 = _this._uriCache = new A._DataUri("data", "", _null, _null, A._Uri__normalizeOrSubstring(t2, t1, end, B.List_XRg, false, false), query, _null); + } + return t1; + }, + toString$0(_) { + var t2, + t1 = this._separatorIndices; + if (0 >= t1.length) + return A.ioore(t1, 0); + t2 = this._text; + return t1[0] === -1 ? "data:" + t2 : t2; + } + }; + A._createTables_build.prototype = { + call$2(state, defaultTransition) { + var t1 = this.tables; + if (!(state < t1.length)) + return A.ioore(t1, state); + t1 = t1[state]; + B.NativeUint8List_methods.fillRange$3(t1, 0, 96, defaultTransition); + return t1; + }, + $signature: 61 + }; + A._createTables_setChars.prototype = { + call$3(target, chars, transition) { + var t1, i, t2; + for (t1 = chars.length, i = 0; i < t1; ++i) { + t2 = chars.charCodeAt(i) ^ 96; + if (!(t2 < 96)) + return A.ioore(target, t2); + target[t2] = transition; + } + }, + $signature: 18 + }; + A._createTables_setRange.prototype = { + call$3(target, range, transition) { + var i, n, + t1 = range.length; + if (0 >= t1) + return A.ioore(range, 0); + i = range.charCodeAt(0); + if (1 >= t1) + return A.ioore(range, 1); + n = range.charCodeAt(1); + for (; i <= n; ++i) { + t1 = (i ^ 96) >>> 0; + if (!(t1 < 96)) + return A.ioore(target, t1); + target[t1] = transition; + } + }, + $signature: 18 + }; + A._SimpleUri.prototype = { + get$hasAuthority() { + return this._hostStart > 0; + }, + get$hasPort() { + return this._hostStart > 0 && this._portStart + 1 < this._pathStart; + }, + get$hasQuery() { + return this._queryStart < this._fragmentStart; + }, + get$hasFragment() { + return this._fragmentStart < this._uri.length; + }, + get$hasAbsolutePath() { + return B.JSString_methods.startsWith$2(this._uri, "/", this._pathStart); + }, + get$scheme() { + var t1 = this._schemeCache; + return t1 == null ? this._schemeCache = this._computeScheme$0() : t1; + }, + _computeScheme$0() { + var t2, _this = this, + t1 = _this._schemeEnd; + if (t1 <= 0) + return ""; + t2 = t1 === 4; + if (t2 && B.JSString_methods.startsWith$1(_this._uri, "http")) + return "http"; + if (t1 === 5 && B.JSString_methods.startsWith$1(_this._uri, "https")) + return "https"; + if (t2 && B.JSString_methods.startsWith$1(_this._uri, "file")) + return "file"; + if (t1 === 7 && B.JSString_methods.startsWith$1(_this._uri, "package")) + return "package"; + return B.JSString_methods.substring$2(_this._uri, 0, t1); + }, + get$userInfo() { + var t1 = this._hostStart, + t2 = this._schemeEnd + 3; + return t1 > t2 ? B.JSString_methods.substring$2(this._uri, t2, t1 - 1) : ""; + }, + get$host(_) { + var t1 = this._hostStart; + return t1 > 0 ? B.JSString_methods.substring$2(this._uri, t1, this._portStart) : ""; + }, + get$port(_) { + var t1, _this = this; + if (_this.get$hasPort()) + return A.int_parse(B.JSString_methods.substring$2(_this._uri, _this._portStart + 1, _this._pathStart), null); + t1 = _this._schemeEnd; + if (t1 === 4 && B.JSString_methods.startsWith$1(_this._uri, "http")) + return 80; + if (t1 === 5 && B.JSString_methods.startsWith$1(_this._uri, "https")) + return 443; + return 0; + }, + get$path(_) { + return B.JSString_methods.substring$2(this._uri, this._pathStart, this._queryStart); + }, + get$query(_) { + var t1 = this._queryStart, + t2 = this._fragmentStart; + return t1 < t2 ? B.JSString_methods.substring$2(this._uri, t1 + 1, t2) : ""; + }, + get$fragment() { + var t1 = this._fragmentStart, + t2 = this._uri; + return t1 < t2.length ? B.JSString_methods.substring$1(t2, t1 + 1) : ""; + }, + get$pathSegments() { + var parts, t2, i, + start = this._pathStart, + end = this._queryStart, + t1 = this._uri; + if (B.JSString_methods.startsWith$2(t1, "/", start)) + ++start; + if (start === end) + return B.List_empty; + parts = A._setArrayType([], type$.JSArray_String); + for (t2 = t1.length, i = start; i < end; ++i) { + if (!(i >= 0 && i < t2)) + return A.ioore(t1, i); + if (t1.charCodeAt(i) === 47) { + B.JSArray_methods.add$1(parts, B.JSString_methods.substring$2(t1, start, i)); + start = i + 1; + } + } + B.JSArray_methods.add$1(parts, B.JSString_methods.substring$2(t1, start, end)); + return A.List_List$unmodifiable(parts, type$.String); + }, + get$queryParameters() { + var _this = this; + if (_this._queryStart >= _this._fragmentStart) + return B.Map_empty; + return new A.UnmodifiableMapView(A.Uri_splitQueryString(_this.get$query(_this)), type$.UnmodifiableMapView_String_String); + }, + _isPort$1(port) { + var portDigitStart = this._portStart + 1; + return portDigitStart + port.length === this._pathStart && B.JSString_methods.startsWith$2(this._uri, port, portDigitStart); + }, + removeFragment$0() { + var _this = this, + t1 = _this._fragmentStart, + t2 = _this._uri; + if (t1 >= t2.length) + return _this; + return new A._SimpleUri(B.JSString_methods.substring$2(t2, 0, t1), _this._schemeEnd, _this._hostStart, _this._portStart, _this._pathStart, _this._queryStart, t1, _this._schemeCache); + }, + resolve$1(reference) { + return this.resolveUri$1(A.Uri_parse(reference)); + }, + resolveUri$1(reference) { + if (reference instanceof A._SimpleUri) + return this._simpleMerge$2(this, reference); + return this._toNonSimple$0().resolveUri$1(reference); + }, + _simpleMerge$2(base, ref) { + var t2, t3, t4, isSimple, delta, refStart, basePathStart, packageNameEnd, basePathStart0, baseStart, baseEnd, baseUri, baseStart0, backCount, refStart0, insert, + t1 = ref._schemeEnd; + if (t1 > 0) + return ref; + t2 = ref._hostStart; + if (t2 > 0) { + t3 = base._schemeEnd; + if (t3 <= 0) + return ref; + t4 = t3 === 4; + if (t4 && B.JSString_methods.startsWith$1(base._uri, "file")) + isSimple = ref._pathStart !== ref._queryStart; + else if (t4 && B.JSString_methods.startsWith$1(base._uri, "http")) + isSimple = !ref._isPort$1("80"); + else + isSimple = !(t3 === 5 && B.JSString_methods.startsWith$1(base._uri, "https")) || !ref._isPort$1("443"); + if (isSimple) { + delta = t3 + 1; + return new A._SimpleUri(B.JSString_methods.substring$2(base._uri, 0, delta) + B.JSString_methods.substring$1(ref._uri, t1 + 1), t3, t2 + delta, ref._portStart + delta, ref._pathStart + delta, ref._queryStart + delta, ref._fragmentStart + delta, base._schemeCache); + } else + return this._toNonSimple$0().resolveUri$1(ref); + } + refStart = ref._pathStart; + t1 = ref._queryStart; + if (refStart === t1) { + t2 = ref._fragmentStart; + if (t1 < t2) { + t3 = base._queryStart; + delta = t3 - t1; + return new A._SimpleUri(B.JSString_methods.substring$2(base._uri, 0, t3) + B.JSString_methods.substring$1(ref._uri, t1), base._schemeEnd, base._hostStart, base._portStart, base._pathStart, t1 + delta, t2 + delta, base._schemeCache); + } + t1 = ref._uri; + if (t2 < t1.length) { + t3 = base._fragmentStart; + return new A._SimpleUri(B.JSString_methods.substring$2(base._uri, 0, t3) + B.JSString_methods.substring$1(t1, t2), base._schemeEnd, base._hostStart, base._portStart, base._pathStart, base._queryStart, t2 + (t3 - t2), base._schemeCache); + } + return base.removeFragment$0(); + } + t2 = ref._uri; + if (B.JSString_methods.startsWith$2(t2, "/", refStart)) { + basePathStart = base._pathStart; + packageNameEnd = A._SimpleUri__packageNameEnd(this); + basePathStart0 = packageNameEnd > 0 ? packageNameEnd : basePathStart; + delta = basePathStart0 - refStart; + return new A._SimpleUri(B.JSString_methods.substring$2(base._uri, 0, basePathStart0) + B.JSString_methods.substring$1(t2, refStart), base._schemeEnd, base._hostStart, base._portStart, basePathStart, t1 + delta, ref._fragmentStart + delta, base._schemeCache); + } + baseStart = base._pathStart; + baseEnd = base._queryStart; + if (baseStart === baseEnd && base._hostStart > 0) { + for (; B.JSString_methods.startsWith$2(t2, "../", refStart);) + refStart += 3; + delta = baseStart - refStart + 1; + return new A._SimpleUri(B.JSString_methods.substring$2(base._uri, 0, baseStart) + "/" + B.JSString_methods.substring$1(t2, refStart), base._schemeEnd, base._hostStart, base._portStart, baseStart, t1 + delta, ref._fragmentStart + delta, base._schemeCache); + } + baseUri = base._uri; + packageNameEnd = A._SimpleUri__packageNameEnd(this); + if (packageNameEnd >= 0) + baseStart0 = packageNameEnd; + else + for (baseStart0 = baseStart; B.JSString_methods.startsWith$2(baseUri, "../", baseStart0);) + baseStart0 += 3; + backCount = 0; + while (true) { + refStart0 = refStart + 3; + if (!(refStart0 <= t1 && B.JSString_methods.startsWith$2(t2, "../", refStart))) + break; + ++backCount; + refStart = refStart0; + } + for (t3 = baseUri.length, insert = ""; baseEnd > baseStart0;) { + --baseEnd; + if (!(baseEnd >= 0 && baseEnd < t3)) + return A.ioore(baseUri, baseEnd); + if (baseUri.charCodeAt(baseEnd) === 47) { + if (backCount === 0) { + insert = "/"; + break; + } + --backCount; + insert = "/"; + } + } + if (baseEnd === baseStart0 && base._schemeEnd <= 0 && !B.JSString_methods.startsWith$2(baseUri, "/", baseStart)) { + refStart -= backCount * 3; + insert = ""; + } + delta = baseEnd - refStart + insert.length; + return new A._SimpleUri(B.JSString_methods.substring$2(baseUri, 0, baseEnd) + insert + B.JSString_methods.substring$1(t2, refStart), base._schemeEnd, base._hostStart, base._portStart, baseStart, t1 + delta, ref._fragmentStart + delta, base._schemeCache); + }, + toFilePath$0() { + var t2, t3, _this = this, + t1 = _this._schemeEnd; + if (t1 >= 0) { + t2 = !(t1 === 4 && B.JSString_methods.startsWith$1(_this._uri, "file")); + t1 = t2; + } else + t1 = false; + if (t1) + throw A.wrapException(A.UnsupportedError$("Cannot extract a file path from a " + _this.get$scheme() + " URI")); + t1 = _this._queryStart; + t2 = _this._uri; + if (t1 < t2.length) { + if (t1 < _this._fragmentStart) + throw A.wrapException(A.UnsupportedError$(string$.Cannotfq)); + throw A.wrapException(A.UnsupportedError$(string$.Cannotff)); + } + t3 = $.$get$_Uri__isWindowsCached(); + if (t3) + t1 = A._Uri__toWindowsFilePath(_this); + else { + if (_this._hostStart < _this._portStart) + A.throwExpression(A.UnsupportedError$(string$.Cannotn)); + t1 = B.JSString_methods.substring$2(t2, _this._pathStart, t1); + } + return t1; + }, + get$hashCode(_) { + var t1 = this._hashCodeCache; + return t1 == null ? this._hashCodeCache = B.JSString_methods.get$hashCode(this._uri) : t1; + }, + $eq(_, other) { + if (other == null) + return false; + if (this === other) + return true; + return type$.Uri._is(other) && this._uri === other.toString$0(0); + }, + _toNonSimple$0() { + var _this = this, _null = null, + t1 = _this.get$scheme(), + t2 = _this.get$userInfo(), + t3 = _this._hostStart > 0 ? _this.get$host(_this) : _null, + t4 = _this.get$hasPort() ? _this.get$port(_this) : _null, + t5 = _this._uri, + t6 = _this._queryStart, + t7 = B.JSString_methods.substring$2(t5, _this._pathStart, t6), + t8 = _this._fragmentStart; + t6 = t6 < t8 ? _this.get$query(_this) : _null; + return A._Uri$_internal(t1, t2, t3, t4, t7, t6, t8 < t5.length ? _this.get$fragment() : _null); + }, + toString$0(_) { + return this._uri; + }, + $isUri: 1 + }; + A._DataUri.prototype = {}; + A.HtmlElement.prototype = {}; + A.AccessibleNodeList.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A.AnchorElement.prototype = { + toString$0(receiver) { + var t1 = String(receiver); + t1.toString; + return t1; + } + }; + A.AreaElement.prototype = { + toString$0(receiver) { + var t1 = String(receiver); + t1.toString; + return t1; + } + }; + A.Blob.prototype = {}; + A.CharacterData.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A.CssPerspective.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A.CssRule.prototype = {$isCssRule: 1}; + A.CssStyleDeclaration.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + } + }; + A.CssStyleDeclarationBase.prototype = {}; + A.CssStyleValue.prototype = {}; + A.CssTransformComponent.prototype = {}; + A.CssTransformValue.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A.CssUnparsedValue.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A.DataTransferItemList.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A.DomException.prototype = { + toString$0(receiver) { + var t1 = String(receiver); + t1.toString; + return t1; + } + }; + A.DomRectList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.Rectangle_num._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A.DomRectReadOnly.prototype = { + toString$0(receiver) { + var t2, + t1 = receiver.left; + t1.toString; + t2 = receiver.top; + t2.toString; + return "Rectangle (" + A.S(t1) + ", " + A.S(t2) + ") " + A.S(this.get$width(receiver)) + " x " + A.S(this.get$height(receiver)); + }, + $eq(receiver, other) { + var t1, t2; + if (other == null) + return false; + if (type$.Rectangle_num._is(other)) { + t1 = receiver.left; + t1.toString; + t2 = other.left; + t2.toString; + if (t1 === t2) { + t1 = receiver.top; + t1.toString; + t2 = other.top; + t2.toString; + if (t1 === t2) { + t1 = J.getInterceptor$x(other); + t1 = this.get$width(receiver) === t1.get$width(other) && this.get$height(receiver) === t1.get$height(other); + } else + t1 = false; + } else + t1 = false; + } else + t1 = false; + return t1; + }, + get$hashCode(receiver) { + var t2, + t1 = receiver.left; + t1.toString; + t2 = receiver.top; + t2.toString; + return A.Object_hash(t1, t2, this.get$width(receiver), this.get$height(receiver)); + }, + get$_height(receiver) { + return receiver.height; + }, + get$height(receiver) { + var t1 = this.get$_height(receiver); + t1.toString; + return t1; + }, + get$_width(receiver) { + return receiver.width; + }, + get$width(receiver) { + var t1 = this.get$_width(receiver); + t1.toString; + return t1; + }, + $isRectangle: 1 + }; + A.DomStringList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + A._asString(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A.DomTokenList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + } + }; + A.Element.prototype = { + toString$0(receiver) { + var t1 = receiver.localName; + t1.toString; + return t1; + } + }; + A.EventTarget.prototype = {}; + A.File.prototype = {$isFile: 1}; + A.FileList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.File._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A.FileWriter.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A.FormElement.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A.Gamepad.prototype = {$isGamepad: 1}; + A.History.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + } + }; + A.HtmlCollection.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.Node._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A.Location.prototype = { + toString$0(receiver) { + var t1 = String(receiver); + t1.toString; + return t1; + } + }; + A.MediaList.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A.MidiInputMap.prototype = { + containsKey$1(receiver, key) { + return A.convertNativeToDart_Dictionary(receiver.get(key)) != null; + }, + $index(receiver, key) { + return A.convertNativeToDart_Dictionary(receiver.get(A._asString(key))); + }, + forEach$1(receiver, f) { + var entries, entry, t1; + type$.void_Function_String_dynamic._as(f); + entries = receiver.entries(); + for (; true;) { + entry = entries.next(); + t1 = entry.done; + t1.toString; + if (t1) + return; + t1 = entry.value[0]; + t1.toString; + f.call$2(t1, A.convertNativeToDart_Dictionary(entry.value[1])); + } + }, + get$keys(receiver) { + var keys = A._setArrayType([], type$.JSArray_String); + this.forEach$1(receiver, new A.MidiInputMap_keys_closure(keys)); + return keys; + }, + get$length(receiver) { + var t1 = receiver.size; + t1.toString; + return t1; + }, + get$isEmpty(receiver) { + var t1 = receiver.size; + t1.toString; + return t1 === 0; + }, + $indexSet(receiver, key, value) { + throw A.wrapException(A.UnsupportedError$("Not supported")); + }, + $isMap: 1 + }; + A.MidiInputMap_keys_closure.prototype = { + call$2(k, v) { + return B.JSArray_methods.add$1(this.keys, k); + }, + $signature: 4 + }; + A.MidiOutputMap.prototype = { + containsKey$1(receiver, key) { + return A.convertNativeToDart_Dictionary(receiver.get(key)) != null; + }, + $index(receiver, key) { + return A.convertNativeToDart_Dictionary(receiver.get(A._asString(key))); + }, + forEach$1(receiver, f) { + var entries, entry, t1; + type$.void_Function_String_dynamic._as(f); + entries = receiver.entries(); + for (; true;) { + entry = entries.next(); + t1 = entry.done; + t1.toString; + if (t1) + return; + t1 = entry.value[0]; + t1.toString; + f.call$2(t1, A.convertNativeToDart_Dictionary(entry.value[1])); + } + }, + get$keys(receiver) { + var keys = A._setArrayType([], type$.JSArray_String); + this.forEach$1(receiver, new A.MidiOutputMap_keys_closure(keys)); + return keys; + }, + get$length(receiver) { + var t1 = receiver.size; + t1.toString; + return t1; + }, + get$isEmpty(receiver) { + var t1 = receiver.size; + t1.toString; + return t1 === 0; + }, + $indexSet(receiver, key, value) { + throw A.wrapException(A.UnsupportedError$("Not supported")); + }, + $isMap: 1 + }; + A.MidiOutputMap_keys_closure.prototype = { + call$2(k, v) { + return B.JSArray_methods.add$1(this.keys, k); + }, + $signature: 4 + }; + A.MimeType.prototype = {$isMimeType: 1}; + A.MimeTypeArray.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.MimeType._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A.Node.prototype = { + toString$0(receiver) { + var value = receiver.nodeValue; + return value == null ? this.super$Interceptor$toString(receiver) : value; + }, + $isNode: 1 + }; + A.NodeList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.Node._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A.Plugin.prototype = { + get$length(receiver) { + return receiver.length; + }, + $isPlugin: 1 + }; + A.PluginArray.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.Plugin._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A.RtcStatsReport.prototype = { + containsKey$1(receiver, key) { + return A.convertNativeToDart_Dictionary(receiver.get(key)) != null; + }, + $index(receiver, key) { + return A.convertNativeToDart_Dictionary(receiver.get(A._asString(key))); + }, + forEach$1(receiver, f) { + var entries, entry, t1; + type$.void_Function_String_dynamic._as(f); + entries = receiver.entries(); + for (; true;) { + entry = entries.next(); + t1 = entry.done; + t1.toString; + if (t1) + return; + t1 = entry.value[0]; + t1.toString; + f.call$2(t1, A.convertNativeToDart_Dictionary(entry.value[1])); + } + }, + get$keys(receiver) { + var keys = A._setArrayType([], type$.JSArray_String); + this.forEach$1(receiver, new A.RtcStatsReport_keys_closure(keys)); + return keys; + }, + get$length(receiver) { + var t1 = receiver.size; + t1.toString; + return t1; + }, + get$isEmpty(receiver) { + var t1 = receiver.size; + t1.toString; + return t1 === 0; + }, + $indexSet(receiver, key, value) { + throw A.wrapException(A.UnsupportedError$("Not supported")); + }, + $isMap: 1 + }; + A.RtcStatsReport_keys_closure.prototype = { + call$2(k, v) { + return B.JSArray_methods.add$1(this.keys, k); + }, + $signature: 4 + }; + A.SelectElement.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A.SourceBuffer.prototype = {$isSourceBuffer: 1}; + A.SourceBufferList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.SourceBuffer._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A.SpeechGrammar.prototype = {$isSpeechGrammar: 1}; + A.SpeechGrammarList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.SpeechGrammar._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A.SpeechRecognitionResult.prototype = { + get$length(receiver) { + return receiver.length; + }, + $isSpeechRecognitionResult: 1 + }; + A.Storage.prototype = { + containsKey$1(receiver, key) { + return receiver.getItem(key) != null; + }, + $index(receiver, key) { + return receiver.getItem(A._asString(key)); + }, + $indexSet(receiver, key, value) { + receiver.setItem(key, value); + }, + forEach$1(receiver, f) { + var i, key, t1; + type$.void_Function_String_String._as(f); + for (i = 0; true; ++i) { + key = receiver.key(i); + if (key == null) + return; + t1 = receiver.getItem(key); + t1.toString; + f.call$2(key, t1); + } + }, + get$keys(receiver) { + var keys = A._setArrayType([], type$.JSArray_String); + this.forEach$1(receiver, new A.Storage_keys_closure(keys)); + return keys; + }, + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + get$isEmpty(receiver) { + return receiver.key(0) == null; + }, + $isMap: 1 + }; + A.Storage_keys_closure.prototype = { + call$2(k, v) { + return B.JSArray_methods.add$1(this.keys, k); + }, + $signature: 27 + }; + A.StyleSheet.prototype = {$isStyleSheet: 1}; + A.TextTrack.prototype = {$isTextTrack: 1}; + A.TextTrackCue.prototype = {$isTextTrackCue: 1}; + A.TextTrackCueList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.TextTrackCue._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A.TextTrackList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.TextTrack._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A.TimeRanges.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + } + }; + A.Touch.prototype = {$isTouch: 1}; + A.TouchList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.Touch._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A.TrackDefaultList.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A.Url.prototype = { + toString$0(receiver) { + var t1 = String(receiver); + t1.toString; + return t1; + } + }; + A.VideoTrackList.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A._CssRuleList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.CssRule._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A._DomRect.prototype = { + toString$0(receiver) { + var t2, t3, t4, + t1 = receiver.left; + t1.toString; + t2 = receiver.top; + t2.toString; + t3 = receiver.width; + t3.toString; + t4 = receiver.height; + t4.toString; + return "Rectangle (" + A.S(t1) + ", " + A.S(t2) + ") " + A.S(t3) + " x " + A.S(t4); + }, + $eq(receiver, other) { + var t1, t2; + if (other == null) + return false; + if (type$.Rectangle_num._is(other)) { + t1 = receiver.left; + t1.toString; + t2 = other.left; + t2.toString; + if (t1 === t2) { + t1 = receiver.top; + t1.toString; + t2 = other.top; + t2.toString; + if (t1 === t2) { + t1 = receiver.width; + t1.toString; + t2 = J.getInterceptor$x(other); + if (t1 === t2.get$width(other)) { + t1 = receiver.height; + t1.toString; + t2 = t1 === t2.get$height(other); + t1 = t2; + } else + t1 = false; + } else + t1 = false; + } else + t1 = false; + } else + t1 = false; + return t1; + }, + get$hashCode(receiver) { + var t2, t3, t4, + t1 = receiver.left; + t1.toString; + t2 = receiver.top; + t2.toString; + t3 = receiver.width; + t3.toString; + t4 = receiver.height; + t4.toString; + return A.Object_hash(t1, t2, t3, t4); + }, + get$_height(receiver) { + return receiver.height; + }, + get$height(receiver) { + var t1 = receiver.height; + t1.toString; + return t1; + }, + get$_width(receiver) { + return receiver.width; + }, + get$width(receiver) { + var t1 = receiver.width; + t1.toString; + return t1; + } + }; + A._GamepadList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + return receiver[index]; + }, + $indexSet(receiver, index, value) { + type$.nullable_Gamepad._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A._NamedNodeMap.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.Node._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A._SpeechRecognitionResultList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.SpeechRecognitionResult._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A._StyleSheetList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length, + t2 = index >>> 0 !== index || index >= t1; + t2.toString; + if (t2) + throw A.wrapException(A.IndexError$withLength(index, t1, receiver, null)); + t1 = receiver[index]; + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.StyleSheet._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + if (!(index >= 0 && index < receiver.length)) + return A.ioore(receiver, index); + return receiver[index]; + }, + $isEfficientLengthIterable: 1, + $isJavaScriptIndexingBehavior: 1, + $isIterable: 1, + $isList: 1 + }; + A.ImmutableListMixin.prototype = { + get$iterator(receiver) { + return new A.FixedSizeListIterator(receiver, this.get$length(receiver), A.instanceType(receiver)._eval$1("FixedSizeListIterator<ImmutableListMixin.E>")); + } + }; + A.FixedSizeListIterator.prototype = { + moveNext$0() { + var _this = this, + nextPosition = _this._position + 1, + t1 = _this._html$_length; + if (nextPosition < t1) { + _this.set$_html$_current(J.$index$asx(_this._array, nextPosition)); + _this._position = nextPosition; + return true; + } + _this.set$_html$_current(null); + _this._position = t1; + return false; + }, + get$current(_) { + var t1 = this._html$_current; + return t1 == null ? this.$ti._precomputed1._as(t1) : t1; + }, + set$_html$_current(_current) { + this._html$_current = this.$ti._eval$1("1?")._as(_current); + }, + $isIterator: 1 + }; + A._CssStyleDeclaration_JavaScriptObject_CssStyleDeclarationBase.prototype = {}; + A._DomRectList_JavaScriptObject_ListMixin.prototype = {}; + A._DomRectList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A._DomStringList_JavaScriptObject_ListMixin.prototype = {}; + A._DomStringList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A._FileList_JavaScriptObject_ListMixin.prototype = {}; + A._FileList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A._HtmlCollection_JavaScriptObject_ListMixin.prototype = {}; + A._HtmlCollection_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A._MidiInputMap_JavaScriptObject_MapMixin.prototype = {}; + A._MidiOutputMap_JavaScriptObject_MapMixin.prototype = {}; + A._MimeTypeArray_JavaScriptObject_ListMixin.prototype = {}; + A._MimeTypeArray_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A._NodeList_JavaScriptObject_ListMixin.prototype = {}; + A._NodeList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A._PluginArray_JavaScriptObject_ListMixin.prototype = {}; + A._PluginArray_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A._RtcStatsReport_JavaScriptObject_MapMixin.prototype = {}; + A._SourceBufferList_EventTarget_ListMixin.prototype = {}; + A._SourceBufferList_EventTarget_ListMixin_ImmutableListMixin.prototype = {}; + A._SpeechGrammarList_JavaScriptObject_ListMixin.prototype = {}; + A._SpeechGrammarList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A._Storage_JavaScriptObject_MapMixin.prototype = {}; + A._TextTrackCueList_JavaScriptObject_ListMixin.prototype = {}; + A._TextTrackCueList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A._TextTrackList_EventTarget_ListMixin.prototype = {}; + A._TextTrackList_EventTarget_ListMixin_ImmutableListMixin.prototype = {}; + A._TouchList_JavaScriptObject_ListMixin.prototype = {}; + A._TouchList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A.__CssRuleList_JavaScriptObject_ListMixin.prototype = {}; + A.__CssRuleList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A.__GamepadList_JavaScriptObject_ListMixin.prototype = {}; + A.__GamepadList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A.__NamedNodeMap_JavaScriptObject_ListMixin.prototype = {}; + A.__NamedNodeMap_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin.prototype = {}; + A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A.__StyleSheetList_JavaScriptObject_ListMixin.prototype = {}; + A.__StyleSheetList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A.jsify__convert.prototype = { + call$1(o) { + var t1, convertedMap, t2, key, convertedList; + if (A._noJsifyRequired(o)) + return o; + t1 = this._convertedObjects; + if (t1.containsKey$1(0, o)) + return t1.$index(0, o); + if (type$.Map_of_nullable_Object_and_nullable_Object._is(o)) { + convertedMap = {}; + t1.$indexSet(0, o, convertedMap); + for (t1 = J.getInterceptor$x(o), t2 = J.get$iterator$ax(t1.get$keys(o)); t2.moveNext$0();) { + key = t2.get$current(t2); + convertedMap[key] = this.call$1(t1.$index(o, key)); + } + return convertedMap; + } else if (type$.Iterable_nullable_Object._is(o)) { + convertedList = []; + t1.$indexSet(0, o, convertedList); + B.JSArray_methods.addAll$1(convertedList, J.map$1$1$ax(o, this, type$.dynamic)); + return convertedList; + } else + return o; + }, + $signature: 19 + }; + A.promiseToFuture_closure.prototype = { + call$1(r) { + return this.completer.complete$1(0, this.T._eval$1("0/?")._as(r)); + }, + $signature: 5 + }; + A.promiseToFuture_closure0.prototype = { + call$1(e) { + if (e == null) + return this.completer.completeError$1(new A.NullRejectionException(e === undefined)); + return this.completer.completeError$1(e); + }, + $signature: 5 + }; + A.dartify_convert.prototype = { + call$1(o) { + var t1, millisSinceEpoch, proto, t2, dartObject, originalKeys, dartKeys, i, jsKey, dartKey, l, $length; + if (A._noDartifyRequired(o)) + return o; + t1 = this._convertedObjects; + o.toString; + if (t1.containsKey$1(0, o)) + return t1.$index(0, o); + if (o instanceof Date) { + millisSinceEpoch = o.getTime(); + if (Math.abs(millisSinceEpoch) <= 864e13) + t1 = false; + else + t1 = true; + if (t1) + A.throwExpression(A.ArgumentError$("DateTime is outside valid range: " + millisSinceEpoch, null)); + A.checkNotNullable(true, "isUtc", type$.bool); + return new A.DateTime(millisSinceEpoch, true); + } + if (o instanceof RegExp) + throw A.wrapException(A.ArgumentError$("structured clone of RegExp", null)); + if (typeof Promise != "undefined" && o instanceof Promise) + return A.promiseToFuture(o, type$.nullable_Object); + proto = Object.getPrototypeOf(o); + if (proto === Object.prototype || proto === null) { + t2 = type$.nullable_Object; + dartObject = A.LinkedHashMap_LinkedHashMap$_empty(t2, t2); + t1.$indexSet(0, o, dartObject); + originalKeys = Object.keys(o); + dartKeys = []; + for (t1 = J.getInterceptor$ax(originalKeys), t2 = t1.get$iterator(originalKeys); t2.moveNext$0();) + dartKeys.push(A.dartify(t2.get$current(t2))); + for (i = 0; i < t1.get$length(originalKeys); ++i) { + jsKey = t1.$index(originalKeys, i); + if (!(i < dartKeys.length)) + return A.ioore(dartKeys, i); + dartKey = dartKeys[i]; + if (jsKey != null) + dartObject.$indexSet(0, dartKey, this.call$1(o[jsKey])); + } + return dartObject; + } + if (o instanceof Array) { + l = o; + dartObject = []; + t1.$indexSet(0, o, dartObject); + $length = A._asInt(o.length); + for (t1 = J.getInterceptor$asx(l), i = 0; i < $length; ++i) + dartObject.push(this.call$1(t1.$index(l, i))); + return dartObject; + } + return o; + }, + $signature: 19 + }; + A.NullRejectionException.prototype = { + toString$0(_) { + return "Promise was rejected with a value of `" + (this.isUndefined ? "undefined" : "null") + "`."; + }, + $isException: 1 + }; + A.Length.prototype = {$isLength: 1}; + A.LengthList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length; + t1.toString; + t1 = index >>> 0 !== index || index >= t1; + t1.toString; + if (t1) + throw A.wrapException(A.IndexError$withLength(index, this.get$length(receiver), receiver, null)); + t1 = receiver.getItem(index); + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.Length._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + return this.$index(receiver, index); + }, + $isEfficientLengthIterable: 1, + $isIterable: 1, + $isList: 1 + }; + A.Number.prototype = {$isNumber: 1}; + A.NumberList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length; + t1.toString; + t1 = index >>> 0 !== index || index >= t1; + t1.toString; + if (t1) + throw A.wrapException(A.IndexError$withLength(index, this.get$length(receiver), receiver, null)); + t1 = receiver.getItem(index); + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.Number._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + return this.$index(receiver, index); + }, + $isEfficientLengthIterable: 1, + $isIterable: 1, + $isList: 1 + }; + A.PointList.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A.StringList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length; + t1.toString; + t1 = index >>> 0 !== index || index >= t1; + t1.toString; + if (t1) + throw A.wrapException(A.IndexError$withLength(index, this.get$length(receiver), receiver, null)); + t1 = receiver.getItem(index); + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + A._asString(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + return this.$index(receiver, index); + }, + $isEfficientLengthIterable: 1, + $isIterable: 1, + $isList: 1 + }; + A.Transform.prototype = {$isTransform: 1}; + A.TransformList.prototype = { + get$length(receiver) { + var t1 = receiver.length; + t1.toString; + return t1; + }, + $index(receiver, index) { + var t1 = receiver.length; + t1.toString; + t1 = index >>> 0 !== index || index >= t1; + t1.toString; + if (t1) + throw A.wrapException(A.IndexError$withLength(index, this.get$length(receiver), receiver, null)); + t1 = receiver.getItem(index); + t1.toString; + return t1; + }, + $indexSet(receiver, index, value) { + type$.Transform._as(value); + throw A.wrapException(A.UnsupportedError$("Cannot assign element of immutable List.")); + }, + elementAt$1(receiver, index) { + return this.$index(receiver, index); + }, + $isEfficientLengthIterable: 1, + $isIterable: 1, + $isList: 1 + }; + A._LengthList_JavaScriptObject_ListMixin.prototype = {}; + A._LengthList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A._NumberList_JavaScriptObject_ListMixin.prototype = {}; + A._NumberList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A._StringList_JavaScriptObject_ListMixin.prototype = {}; + A._StringList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A._TransformList_JavaScriptObject_ListMixin.prototype = {}; + A._TransformList_JavaScriptObject_ListMixin_ImmutableListMixin.prototype = {}; + A.AudioBuffer.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A.AudioParamMap.prototype = { + containsKey$1(receiver, key) { + return A.convertNativeToDart_Dictionary(receiver.get(key)) != null; + }, + $index(receiver, key) { + return A.convertNativeToDart_Dictionary(receiver.get(A._asString(key))); + }, + forEach$1(receiver, f) { + var entries, entry, t1; + type$.void_Function_String_dynamic._as(f); + entries = receiver.entries(); + for (; true;) { + entry = entries.next(); + t1 = entry.done; + t1.toString; + if (t1) + return; + t1 = entry.value[0]; + t1.toString; + f.call$2(t1, A.convertNativeToDart_Dictionary(entry.value[1])); + } + }, + get$keys(receiver) { + var keys = A._setArrayType([], type$.JSArray_String); + this.forEach$1(receiver, new A.AudioParamMap_keys_closure(keys)); + return keys; + }, + get$length(receiver) { + var t1 = receiver.size; + t1.toString; + return t1; + }, + get$isEmpty(receiver) { + var t1 = receiver.size; + t1.toString; + return t1 === 0; + }, + $indexSet(receiver, key, value) { + throw A.wrapException(A.UnsupportedError$("Not supported")); + }, + $isMap: 1 + }; + A.AudioParamMap_keys_closure.prototype = { + call$2(k, v) { + return B.JSArray_methods.add$1(this.keys, k); + }, + $signature: 4 + }; + A.AudioTrackList.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A.BaseAudioContext.prototype = {}; + A.OfflineAudioContext.prototype = { + get$length(receiver) { + return receiver.length; + } + }; + A._AudioParamMap_JavaScriptObject_MapMixin.prototype = {}; + A.NullStreamSink.prototype = { + addStream$1(_, stream) { + var _this = this; + _this.$ti._eval$1("Stream<1>")._as(stream); + _this._checkEventAllowed$0(); + _this._addingStream = true; + return stream.listen$1(null).cancel$0(0).whenComplete$1(new A.NullStreamSink_addStream_closure(_this)); + }, + _checkEventAllowed$0() { + if (this._null_stream_sink$_closed) + throw A.wrapException(A.StateError$("Cannot add to a closed sink.")); + if (this._addingStream) + throw A.wrapException(A.StateError$("Cannot add to a sink while adding a stream.")); + }, + close$0(_) { + this._null_stream_sink$_closed = true; + return this.done; + }, + $isStreamConsumer: 1, + $isStreamSink: 1 + }; + A.NullStreamSink_addStream_closure.prototype = { + call$0() { + this.$this._addingStream = false; + }, + $signature: 3 + }; + A.Context.prototype = { + absolute$15(_, part1, part2, part3, part4, part5, part6, part7, part8, part9, part10, part11, part12, part13, part14, part15) { + var t1; + A._validateArgList("absolute", A._setArrayType([part1, part2, part3, part4, part5, part6, part7, part8, part9, part10, part11, part12, part13, part14, part15], type$.JSArray_nullable_String)); + t1 = this.style; + t1 = t1.rootLength$1(part1) > 0 && !t1.isRootRelative$1(part1); + if (t1) + return part1; + t1 = this._context$_current; + return this.join$16(0, t1 == null ? A.current() : t1, part1, part2, part3, part4, part5, part6, part7, part8, part9, part10, part11, part12, part13, part14, part15); + }, + absolute$1($receiver, part1) { + return this.absolute$15($receiver, part1, null, null, null, null, null, null, null, null, null, null, null, null, null, null); + }, + join$16(_, part1, part2, part3, part4, part5, part6, part7, part8, part9, part10, part11, part12, part13, part14, part15, part16) { + var parts = A._setArrayType([part1, part2, part3, part4, part5, part6, part7, part8, part9, part10, part11, part12, part13, part14, part15, part16], type$.JSArray_nullable_String); + A._validateArgList("join", parts); + return this.joinAll$1(new A.WhereTypeIterable(parts, type$.WhereTypeIterable_String)); + }, + join$2($receiver, part1, part2) { + return this.join$16($receiver, part1, part2, null, null, null, null, null, null, null, null, null, null, null, null, null, null); + }, + joinAll$1(parts) { + var t1, t2, t3, needsSeparator, isAbsoluteAndNotRootRelative, t4, t5, parsed, path, t6; + type$.Iterable_String._as(parts); + for (t1 = parts.$ti, t2 = t1._eval$1("bool(Iterable.E)")._as(new A.Context_joinAll_closure()), t3 = parts.get$iterator(parts), t1 = new A.WhereIterator(t3, t2, t1._eval$1("WhereIterator<Iterable.E>")), t2 = this.style, needsSeparator = false, isAbsoluteAndNotRootRelative = false, t4 = ""; t1.moveNext$0();) { + t5 = t3.get$current(t3); + if (t2.isRootRelative$1(t5) && isAbsoluteAndNotRootRelative) { + parsed = A.ParsedPath_ParsedPath$parse(t5, t2); + path = t4.charCodeAt(0) == 0 ? t4 : t4; + t4 = B.JSString_methods.substring$2(path, 0, t2.rootLength$2$withDrive(path, true)); + parsed.root = t4; + if (t2.needsSeparator$1(t4)) + B.JSArray_methods.$indexSet(parsed.separators, 0, t2.get$separator()); + t4 = "" + parsed.toString$0(0); + } else if (t2.rootLength$1(t5) > 0) { + isAbsoluteAndNotRootRelative = !t2.isRootRelative$1(t5); + t4 = "" + t5; + } else { + t6 = t5.length; + if (t6 !== 0) { + if (0 >= t6) + return A.ioore(t5, 0); + t6 = t2.containsSeparator$1(t5[0]); + } else + t6 = false; + if (!t6) + if (needsSeparator) + t4 += t2.get$separator(); + t4 += t5; + } + needsSeparator = t2.needsSeparator$1(t5); + } + return t4.charCodeAt(0) == 0 ? t4 : t4; + }, + split$1(_, path) { + var parsed = A.ParsedPath_ParsedPath$parse(path, this.style), + t1 = parsed.parts, + t2 = A._arrayInstanceType(t1), + t3 = t2._eval$1("WhereIterable<1>"); + parsed.set$parts(A.List_List$of(new A.WhereIterable(t1, t2._eval$1("bool(1)")._as(new A.Context_split_closure()), t3), true, t3._eval$1("Iterable.E"))); + t1 = parsed.root; + if (t1 != null) + B.JSArray_methods.insert$2(parsed.parts, 0, t1); + return parsed.parts; + }, + normalize$1(_, path) { + var parsed; + if (!this._needsNormalization$1(path)) + return path; + parsed = A.ParsedPath_ParsedPath$parse(path, this.style); + parsed.normalize$0(0); + return parsed.toString$0(0); + }, + _needsNormalization$1(path) { + var t2, i, start, previous, t3, previousPrevious, codeUnit, t4, + t1 = this.style, + root = t1.rootLength$1(path); + if (root !== 0) { + if (t1 === $.$get$Style_windows()) + for (t2 = path.length, i = 0; i < root; ++i) { + if (!(i < t2)) + return A.ioore(path, i); + if (path.charCodeAt(i) === 47) + return true; + } + start = root; + previous = 47; + } else { + start = 0; + previous = null; + } + for (t2 = new A.CodeUnits(path)._string, t3 = t2.length, i = start, previousPrevious = null; i < t3; ++i, previousPrevious = previous, previous = codeUnit) { + if (!(i >= 0)) + return A.ioore(t2, i); + codeUnit = t2.charCodeAt(i); + if (t1.isSeparator$1(codeUnit)) { + if (t1 === $.$get$Style_windows() && codeUnit === 47) + return true; + if (previous != null && t1.isSeparator$1(previous)) + return true; + if (previous === 46) + t4 = previousPrevious == null || previousPrevious === 46 || t1.isSeparator$1(previousPrevious); + else + t4 = false; + if (t4) + return true; + } + } + if (previous == null) + return true; + if (t1.isSeparator$1(previous)) + return true; + if (previous === 46) + t1 = previousPrevious == null || t1.isSeparator$1(previousPrevious) || previousPrevious === 46; + else + t1 = false; + if (t1) + return true; + return false; + }, + relative$1(path) { + var from, fromParsed, pathParsed, t3, t4, t5, _this = this, + _s26_ = 'Unable to find a path to "', + t1 = _this.style, + t2 = t1.rootLength$1(path); + if (t2 <= 0) + return _this.normalize$1(0, path); + t2 = _this._context$_current; + from = t2 == null ? A.current() : t2; + if (t1.rootLength$1(from) <= 0 && t1.rootLength$1(path) > 0) + return _this.normalize$1(0, path); + if (t1.rootLength$1(path) <= 0 || t1.isRootRelative$1(path)) + path = _this.absolute$1(0, path); + if (t1.rootLength$1(path) <= 0 && t1.rootLength$1(from) > 0) + throw A.wrapException(A.PathException$(_s26_ + path + '" from "' + from + '".')); + fromParsed = A.ParsedPath_ParsedPath$parse(from, t1); + fromParsed.normalize$0(0); + pathParsed = A.ParsedPath_ParsedPath$parse(path, t1); + pathParsed.normalize$0(0); + t2 = fromParsed.parts; + t3 = t2.length; + if (t3 !== 0) { + if (0 >= t3) + return A.ioore(t2, 0); + t2 = J.$eq$(t2[0], "."); + } else + t2 = false; + if (t2) + return pathParsed.toString$0(0); + t2 = fromParsed.root; + t3 = pathParsed.root; + if (t2 != t3) + t2 = t2 == null || t3 == null || !t1.pathsEqual$2(t2, t3); + else + t2 = false; + if (t2) + return pathParsed.toString$0(0); + while (true) { + t2 = fromParsed.parts; + t3 = t2.length; + if (t3 !== 0) { + t4 = pathParsed.parts; + t5 = t4.length; + if (t5 !== 0) { + if (0 >= t3) + return A.ioore(t2, 0); + t2 = t2[0]; + if (0 >= t5) + return A.ioore(t4, 0); + t4 = t1.pathsEqual$2(t2, t4[0]); + t2 = t4; + } else + t2 = false; + } else + t2 = false; + if (!t2) + break; + B.JSArray_methods.removeAt$1(fromParsed.parts, 0); + B.JSArray_methods.removeAt$1(fromParsed.separators, 1); + B.JSArray_methods.removeAt$1(pathParsed.parts, 0); + B.JSArray_methods.removeAt$1(pathParsed.separators, 1); + } + t2 = fromParsed.parts; + t3 = t2.length; + if (t3 !== 0) { + if (0 >= t3) + return A.ioore(t2, 0); + t2 = J.$eq$(t2[0], ".."); + } else + t2 = false; + if (t2) + throw A.wrapException(A.PathException$(_s26_ + path + '" from "' + from + '".')); + t2 = type$.String; + B.JSArray_methods.insertAll$2(pathParsed.parts, 0, A.List_List$filled(fromParsed.parts.length, "..", false, t2)); + B.JSArray_methods.$indexSet(pathParsed.separators, 0, ""); + B.JSArray_methods.insertAll$2(pathParsed.separators, 1, A.List_List$filled(fromParsed.parts.length, t1.get$separator(), false, t2)); + t1 = pathParsed.parts; + t2 = t1.length; + if (t2 === 0) + return "."; + if (t2 > 1 && J.$eq$(B.JSArray_methods.get$last(t1), ".")) { + B.JSArray_methods.removeLast$0(pathParsed.parts); + t1 = pathParsed.separators; + if (0 >= t1.length) + return A.ioore(t1, -1); + t1.pop(); + if (0 >= t1.length) + return A.ioore(t1, -1); + t1.pop(); + B.JSArray_methods.add$1(t1, ""); + } + pathParsed.root = ""; + pathParsed.removeTrailingSeparators$0(); + return pathParsed.toString$0(0); + }, + toUri$1(path) { + var t2, + t1 = this.style; + if (t1.rootLength$1(path) <= 0) + return t1.relativePathToUri$1(path); + else { + t2 = this._context$_current; + return t1.absolutePathToUri$1(this.join$2(0, t2 == null ? A.current() : t2, path)); + } + }, + prettyUri$1(uri) { + var path, rel, _this = this, + typedUri = A._parseUri(uri); + if (typedUri.get$scheme() === "file" && _this.style === $.$get$Style_url()) + return typedUri.toString$0(0); + else if (typedUri.get$scheme() !== "file" && typedUri.get$scheme() !== "" && _this.style !== $.$get$Style_url()) + return typedUri.toString$0(0); + path = _this.normalize$1(0, _this.style.pathFromUri$1(A._parseUri(typedUri))); + rel = _this.relative$1(path); + return _this.split$1(0, rel).length > _this.split$1(0, path).length ? path : rel; + } + }; + A.Context_joinAll_closure.prototype = { + call$1(part) { + return A._asString(part) !== ""; + }, + $signature: 1 + }; + A.Context_split_closure.prototype = { + call$1(part) { + return A._asString(part).length !== 0; + }, + $signature: 1 + }; + A._validateArgList_closure.prototype = { + call$1(arg) { + A._asStringQ(arg); + return arg == null ? "null" : '"' + arg + '"'; + }, + $signature: 31 + }; + A.InternalStyle.prototype = { + getRoot$1(path) { + var t1, + $length = this.rootLength$1(path); + if ($length > 0) + return B.JSString_methods.substring$2(path, 0, $length); + if (this.isRootRelative$1(path)) { + if (0 >= path.length) + return A.ioore(path, 0); + t1 = path[0]; + } else + t1 = null; + return t1; + }, + relativePathToUri$1(path) { + var segments, t2, _null = null, + t1 = path.length; + if (t1 === 0) + return A._Uri__Uri(_null, _null, _null, _null); + segments = A.Context_Context(this).split$1(0, path); + t2 = t1 - 1; + if (!(t2 >= 0)) + return A.ioore(path, t2); + if (this.isSeparator$1(path.charCodeAt(t2))) + B.JSArray_methods.add$1(segments, ""); + return A._Uri__Uri(_null, _null, segments, _null); + }, + pathsEqual$2(path1, path2) { + return path1 === path2; + } + }; + A.ParsedPath.prototype = { + get$hasTrailingSeparator() { + var t1 = this.parts; + if (t1.length !== 0) + t1 = J.$eq$(B.JSArray_methods.get$last(t1), "") || !J.$eq$(B.JSArray_methods.get$last(this.separators), ""); + else + t1 = false; + return t1; + }, + removeTrailingSeparators$0() { + var t1, t2, _this = this; + while (true) { + t1 = _this.parts; + if (!(t1.length !== 0 && J.$eq$(B.JSArray_methods.get$last(t1), ""))) + break; + B.JSArray_methods.removeLast$0(_this.parts); + t1 = _this.separators; + if (0 >= t1.length) + return A.ioore(t1, -1); + t1.pop(); + } + t1 = _this.separators; + t2 = t1.length; + if (t2 !== 0) + B.JSArray_methods.$indexSet(t1, t2 - 1, ""); + }, + normalize$0(_) { + var t1, t2, leadingDoubles, _i, part, t3, _this = this, + newParts = A._setArrayType([], type$.JSArray_String); + for (t1 = _this.parts, t2 = t1.length, leadingDoubles = 0, _i = 0; _i < t1.length; t1.length === t2 || (0, A.throwConcurrentModificationError)(t1), ++_i) { + part = t1[_i]; + t3 = J.getInterceptor$(part); + if (!(t3.$eq(part, ".") || t3.$eq(part, ""))) + if (t3.$eq(part, "..")) { + t3 = newParts.length; + if (t3 !== 0) { + if (0 >= t3) + return A.ioore(newParts, -1); + newParts.pop(); + } else + ++leadingDoubles; + } else + B.JSArray_methods.add$1(newParts, part); + } + if (_this.root == null) + B.JSArray_methods.insertAll$2(newParts, 0, A.List_List$filled(leadingDoubles, "..", false, type$.String)); + if (newParts.length === 0 && _this.root == null) + B.JSArray_methods.add$1(newParts, "."); + _this.set$parts(newParts); + t1 = _this.style; + _this.set$separators(A.List_List$filled(newParts.length + 1, t1.get$separator(), true, type$.String)); + t2 = _this.root; + if (t2 == null || newParts.length === 0 || !t1.needsSeparator$1(t2)) + B.JSArray_methods.$indexSet(_this.separators, 0, ""); + t2 = _this.root; + if (t2 != null && t1 === $.$get$Style_windows()) { + t2.toString; + _this.root = A.stringReplaceAllUnchecked(t2, "/", "\\"); + } + _this.removeTrailingSeparators$0(); + }, + toString$0(_) { + var i, t2, t3, _this = this, + t1 = _this.root; + t1 = t1 != null ? "" + t1 : ""; + for (i = 0; i < _this.parts.length; ++i, t1 = t3) { + t2 = _this.separators; + if (!(i < t2.length)) + return A.ioore(t2, i); + t2 = A.S(t2[i]); + t3 = _this.parts; + if (!(i < t3.length)) + return A.ioore(t3, i); + t3 = t1 + t2 + A.S(t3[i]); + } + t1 += A.S(B.JSArray_methods.get$last(_this.separators)); + return t1.charCodeAt(0) == 0 ? t1 : t1; + }, + set$parts(parts) { + this.parts = type$.List_String._as(parts); + }, + set$separators(separators) { + this.separators = type$.List_String._as(separators); + } + }; + A.PathException.prototype = { + toString$0(_) { + return "PathException: " + this.message; + }, + $isException: 1 + }; + A.Style.prototype = { + toString$0(_) { + return this.get$name(this); + } + }; + A.PosixStyle.prototype = { + containsSeparator$1(path) { + return B.JSString_methods.contains$1(path, "/"); + }, + isSeparator$1(codeUnit) { + return codeUnit === 47; + }, + needsSeparator$1(path) { + var t2, + t1 = path.length; + if (t1 !== 0) { + t2 = t1 - 1; + if (!(t2 >= 0)) + return A.ioore(path, t2); + t2 = path.charCodeAt(t2) !== 47; + t1 = t2; + } else + t1 = false; + return t1; + }, + rootLength$2$withDrive(path, withDrive) { + var t1 = path.length; + if (t1 !== 0) { + if (0 >= t1) + return A.ioore(path, 0); + t1 = path.charCodeAt(0) === 47; + } else + t1 = false; + if (t1) + return 1; + return 0; + }, + rootLength$1(path) { + return this.rootLength$2$withDrive(path, false); + }, + isRootRelative$1(path) { + return false; + }, + pathFromUri$1(uri) { + var t1; + if (uri.get$scheme() === "" || uri.get$scheme() === "file") { + t1 = uri.get$path(uri); + return A._Uri__uriDecode(t1, 0, t1.length, B.C_Utf8Codec, false); + } + throw A.wrapException(A.ArgumentError$("Uri " + uri.toString$0(0) + " must have scheme 'file:'.", null)); + }, + absolutePathToUri$1(path) { + var parsed = A.ParsedPath_ParsedPath$parse(path, this), + t1 = parsed.parts; + if (t1.length === 0) + B.JSArray_methods.addAll$1(t1, A._setArrayType(["", ""], type$.JSArray_String)); + else if (parsed.get$hasTrailingSeparator()) + B.JSArray_methods.add$1(parsed.parts, ""); + return A._Uri__Uri(null, null, parsed.parts, "file"); + }, + get$name() { + return "posix"; + }, + get$separator() { + return "/"; + } + }; + A.UrlStyle.prototype = { + containsSeparator$1(path) { + return B.JSString_methods.contains$1(path, "/"); + }, + isSeparator$1(codeUnit) { + return codeUnit === 47; + }, + needsSeparator$1(path) { + var t2, + t1 = path.length; + if (t1 === 0) + return false; + t2 = t1 - 1; + if (!(t2 >= 0)) + return A.ioore(path, t2); + if (path.charCodeAt(t2) !== 47) + return true; + return B.JSString_methods.endsWith$1(path, "://") && this.rootLength$1(path) === t1; + }, + rootLength$2$withDrive(path, withDrive) { + var i, codeUnit, index, t2, + t1 = path.length; + if (t1 === 0) + return 0; + if (0 >= t1) + return A.ioore(path, 0); + if (path.charCodeAt(0) === 47) + return 1; + for (i = 0; i < t1; ++i) { + codeUnit = path.charCodeAt(i); + if (codeUnit === 47) + return 0; + if (codeUnit === 58) { + if (i === 0) + return 0; + index = B.JSString_methods.indexOf$2(path, "/", B.JSString_methods.startsWith$2(path, "//", i + 1) ? i + 3 : i); + if (index <= 0) + return t1; + if (!withDrive || t1 < index + 3) + return index; + if (!B.JSString_methods.startsWith$1(path, "file://")) + return index; + if (!A.isDriveLetter(path, index + 1)) + return index; + t2 = index + 3; + return t1 === t2 ? t2 : index + 4; + } + } + return 0; + }, + rootLength$1(path) { + return this.rootLength$2$withDrive(path, false); + }, + isRootRelative$1(path) { + var t1 = path.length; + if (t1 !== 0) { + if (0 >= t1) + return A.ioore(path, 0); + t1 = path.charCodeAt(0) === 47; + } else + t1 = false; + return t1; + }, + pathFromUri$1(uri) { + return uri.toString$0(0); + }, + relativePathToUri$1(path) { + return A.Uri_parse(path); + }, + absolutePathToUri$1(path) { + return A.Uri_parse(path); + }, + get$name() { + return "url"; + }, + get$separator() { + return "/"; + } + }; + A.WindowsStyle.prototype = { + containsSeparator$1(path) { + return B.JSString_methods.contains$1(path, "/"); + }, + isSeparator$1(codeUnit) { + return codeUnit === 47 || codeUnit === 92; + }, + needsSeparator$1(path) { + var t2, + t1 = path.length; + if (t1 === 0) + return false; + t2 = t1 - 1; + if (!(t2 >= 0)) + return A.ioore(path, t2); + t2 = path.charCodeAt(t2); + return !(t2 === 47 || t2 === 92); + }, + rootLength$2$withDrive(path, withDrive) { + var t2, index, + t1 = path.length; + if (t1 === 0) + return 0; + if (0 >= t1) + return A.ioore(path, 0); + if (path.charCodeAt(0) === 47) + return 1; + if (path.charCodeAt(0) === 92) { + if (t1 >= 2) { + if (1 >= t1) + return A.ioore(path, 1); + t2 = path.charCodeAt(1) !== 92; + } else + t2 = true; + if (t2) + return 1; + index = B.JSString_methods.indexOf$2(path, "\\", 2); + if (index > 0) { + index = B.JSString_methods.indexOf$2(path, "\\", index + 1); + if (index > 0) + return index; + } + return t1; + } + if (t1 < 3) + return 0; + if (!A.isAlphabetic(path.charCodeAt(0))) + return 0; + if (path.charCodeAt(1) !== 58) + return 0; + t1 = path.charCodeAt(2); + if (!(t1 === 47 || t1 === 92)) + return 0; + return 3; + }, + rootLength$1(path) { + return this.rootLength$2$withDrive(path, false); + }, + isRootRelative$1(path) { + return this.rootLength$1(path) === 1; + }, + pathFromUri$1(uri) { + var path, t1; + if (uri.get$scheme() !== "" && uri.get$scheme() !== "file") + throw A.wrapException(A.ArgumentError$("Uri " + uri.toString$0(0) + " must have scheme 'file:'.", null)); + path = uri.get$path(uri); + if (uri.get$host(uri) === "") { + if (path.length >= 3 && B.JSString_methods.startsWith$1(path, "/") && A.isDriveLetter(path, 1)) + path = B.JSString_methods.replaceFirst$2(path, "/", ""); + } else + path = "\\\\" + uri.get$host(uri) + path; + t1 = A.stringReplaceAllUnchecked(path, "/", "\\"); + return A._Uri__uriDecode(t1, 0, t1.length, B.C_Utf8Codec, false); + }, + absolutePathToUri$1(path) { + var rootParts, t2, + parsed = A.ParsedPath_ParsedPath$parse(path, this), + t1 = parsed.root; + t1.toString; + if (B.JSString_methods.startsWith$1(t1, "\\\\")) { + rootParts = new A.WhereIterable(A._setArrayType(t1.split("\\"), type$.JSArray_String), type$.bool_Function_String._as(new A.WindowsStyle_absolutePathToUri_closure()), type$.WhereIterable_String); + B.JSArray_methods.insert$2(parsed.parts, 0, rootParts.get$last(rootParts)); + if (parsed.get$hasTrailingSeparator()) + B.JSArray_methods.add$1(parsed.parts, ""); + return A._Uri__Uri(rootParts.get$first(rootParts), null, parsed.parts, "file"); + } else { + if (parsed.parts.length === 0 || parsed.get$hasTrailingSeparator()) + B.JSArray_methods.add$1(parsed.parts, ""); + t1 = parsed.parts; + t2 = parsed.root; + t2.toString; + t2 = A.stringReplaceAllUnchecked(t2, "/", ""); + B.JSArray_methods.insert$2(t1, 0, A.stringReplaceAllUnchecked(t2, "\\", "")); + return A._Uri__Uri(null, null, parsed.parts, "file"); + } + }, + codeUnitsEqual$2(codeUnit1, codeUnit2) { + var upperCase1; + if (codeUnit1 === codeUnit2) + return true; + if (codeUnit1 === 47) + return codeUnit2 === 92; + if (codeUnit1 === 92) + return codeUnit2 === 47; + if ((codeUnit1 ^ codeUnit2) !== 32) + return false; + upperCase1 = codeUnit1 | 32; + return upperCase1 >= 97 && upperCase1 <= 122; + }, + pathsEqual$2(path1, path2) { + var t1, t2, i; + if (path1 === path2) + return true; + t1 = path1.length; + t2 = path2.length; + if (t1 !== t2) + return false; + for (i = 0; i < t1; ++i) { + if (!(i < t2)) + return A.ioore(path2, i); + if (!this.codeUnitsEqual$2(path1.charCodeAt(i), path2.charCodeAt(i))) + return false; + } + return true; + }, + get$name() { + return "windows"; + }, + get$separator() { + return "\\"; + } + }; + A.WindowsStyle_absolutePathToUri_closure.prototype = { + call$1(part) { + return A._asString(part) !== ""; + }, + $signature: 1 + }; + A.Chain.prototype = { + toTrace$0() { + var t1 = this.traces, + t2 = A._arrayInstanceType(t1); + return A.Trace$(new A.ExpandIterable(t1, t2._eval$1("Iterable<Frame>(1)")._as(new A.Chain_toTrace_closure()), t2._eval$1("ExpandIterable<1,Frame>")), null); + }, + toString$0(_) { + var t1 = this.traces, + t2 = A._arrayInstanceType(t1); + return new A.MappedListIterable(t1, t2._eval$1("String(1)")._as(new A.Chain_toString_closure(new A.MappedListIterable(t1, t2._eval$1("int(1)")._as(new A.Chain_toString_closure0()), t2._eval$1("MappedListIterable<1,int>")).fold$1$2(0, 0, B.CONSTANT, type$.int))), t2._eval$1("MappedListIterable<1,String>")).join$1(0, string$.x3d_____); + }, + $isStackTrace: 1 + }; + A.Chain_Chain$parse_closure.prototype = { + call$1(line) { + return A._asString(line).length !== 0; + }, + $signature: 1 + }; + A.Chain_toTrace_closure.prototype = { + call$1(trace) { + return type$.Trace._as(trace).get$frames(); + }, + $signature: 32 + }; + A.Chain_toString_closure0.prototype = { + call$1(trace) { + var t1 = type$.Trace._as(trace).get$frames(), + t2 = A._arrayInstanceType(t1); + return new A.MappedListIterable(t1, t2._eval$1("int(1)")._as(new A.Chain_toString__closure0()), t2._eval$1("MappedListIterable<1,int>")).fold$1$2(0, 0, B.CONSTANT, type$.int); + }, + $signature: 33 + }; + A.Chain_toString__closure0.prototype = { + call$1(frame) { + type$.Frame._as(frame); + return frame.get$location(frame).length; + }, + $signature: 20 + }; + A.Chain_toString_closure.prototype = { + call$1(trace) { + var t1 = type$.Trace._as(trace).get$frames(), + t2 = A._arrayInstanceType(t1); + return new A.MappedListIterable(t1, t2._eval$1("String(1)")._as(new A.Chain_toString__closure(this.longest)), t2._eval$1("MappedListIterable<1,String>")).join$0(0); + }, + $signature: 35 + }; + A.Chain_toString__closure.prototype = { + call$1(frame) { + type$.Frame._as(frame); + return B.JSString_methods.padRight$1(frame.get$location(frame), this.longest) + " " + A.S(frame.get$member()) + "\n"; + }, + $signature: 21 + }; + A.Frame.prototype = { + get$isCore() { + return this.uri.get$scheme() === "dart"; + }, + get$library() { + var t1 = this.uri; + if (t1.get$scheme() === "data") + return "data:..."; + return $.$get$context().prettyUri$1(t1); + }, + get$$package() { + var t1 = this.uri; + if (t1.get$scheme() !== "package") + return null; + return B.JSArray_methods.get$first(t1.get$path(t1).split("/")); + }, + get$location(_) { + var t2, _this = this, + t1 = _this.line; + if (t1 == null) + return _this.get$library(); + t2 = _this.column; + if (t2 == null) + return _this.get$library() + " " + A.S(t1); + return _this.get$library() + " " + A.S(t1) + ":" + A.S(t2); + }, + toString$0(_) { + return this.get$location(this) + " in " + A.S(this.member); + }, + get$uri() { + return this.uri; + }, + get$line(receiver) { + return this.line; + }, + get$column() { + return this.column; + }, + get$member() { + return this.member; + } + }; + A.Frame_Frame$parseVM_closure.prototype = { + call$0() { + var match, t2, t3, member, uri, lineAndColumn, line, _null = null, + t1 = this.frame; + if (t1 === "...") + return new A.Frame(A._Uri__Uri(_null, _null, _null, _null), _null, _null, "..."); + match = $.$get$_vmFrame().firstMatch$1(t1); + if (match == null) + return new A.UnparsedFrame(A._Uri__Uri(_null, "unparsed", _null, _null), t1); + t1 = match._match; + if (1 >= t1.length) + return A.ioore(t1, 1); + t2 = t1[1]; + t2.toString; + t3 = $.$get$_asyncBody(); + t2 = A.stringReplaceAllUnchecked(t2, t3, "<async>"); + member = A.stringReplaceAllUnchecked(t2, "<anonymous closure>", "<fn>"); + if (2 >= t1.length) + return A.ioore(t1, 2); + t2 = t1[2]; + t3 = t2; + t3.toString; + if (B.JSString_methods.startsWith$1(t3, "<data:")) + uri = A.Uri_Uri$dataFromString(""); + else { + t2 = t2; + t2.toString; + uri = A.Uri_parse(t2); + } + if (3 >= t1.length) + return A.ioore(t1, 3); + lineAndColumn = t1[3].split(":"); + t1 = lineAndColumn.length; + line = t1 > 1 ? A.int_parse(lineAndColumn[1], _null) : _null; + return new A.Frame(uri, line, t1 > 2 ? A.int_parse(lineAndColumn[2], _null) : _null, member); + }, + $signature: 6 + }; + A.Frame_Frame$parseV8_closure.prototype = { + call$0() { + var t2, t3, t4, _s4_ = "<fn>", + t1 = this.frame, + match = $.$get$_v8Frame().firstMatch$1(t1); + if (match == null) + return new A.UnparsedFrame(A._Uri__Uri(null, "unparsed", null, null), t1); + t1 = new A.Frame_Frame$parseV8_closure_parseLocation(t1); + t2 = match._match; + t3 = t2.length; + if (2 >= t3) + return A.ioore(t2, 2); + t4 = t2[2]; + if (t4 != null) { + t3 = t4; + t3.toString; + t2 = t2[1]; + t2.toString; + t2 = A.stringReplaceAllUnchecked(t2, "<anonymous>", _s4_); + t2 = A.stringReplaceAllUnchecked(t2, "Anonymous function", _s4_); + return t1.call$2(t3, A.stringReplaceAllUnchecked(t2, "(anonymous function)", _s4_)); + } else { + if (3 >= t3) + return A.ioore(t2, 3); + t2 = t2[3]; + t2.toString; + return t1.call$2(t2, _s4_); + } + }, + $signature: 6 + }; + A.Frame_Frame$parseV8_closure_parseLocation.prototype = { + call$2($location, member) { + var t2, urlMatch, uri, line, columnMatch, _null = null, + t1 = $.$get$_v8EvalLocation(), + evalMatch = t1.firstMatch$1($location); + for (; evalMatch != null; $location = t2) { + t2 = evalMatch._match; + if (1 >= t2.length) + return A.ioore(t2, 1); + t2 = t2[1]; + t2.toString; + evalMatch = t1.firstMatch$1(t2); + } + if ($location === "native") + return new A.Frame(A.Uri_parse("native"), _null, _null, member); + urlMatch = $.$get$_v8UrlLocation().firstMatch$1($location); + if (urlMatch == null) + return new A.UnparsedFrame(A._Uri__Uri(_null, "unparsed", _null, _null), this.frame); + t1 = urlMatch._match; + if (1 >= t1.length) + return A.ioore(t1, 1); + t2 = t1[1]; + t2.toString; + uri = A.Frame__uriOrPathToUri(t2); + if (2 >= t1.length) + return A.ioore(t1, 2); + t2 = t1[2]; + t2.toString; + line = A.int_parse(t2, _null); + if (3 >= t1.length) + return A.ioore(t1, 3); + columnMatch = t1[3]; + return new A.Frame(uri, line, columnMatch != null ? A.int_parse(columnMatch, _null) : _null, member); + }, + $signature: 38 + }; + A.Frame_Frame$_parseFirefoxEval_closure.prototype = { + call$0() { + var t2, member, uri, line, _null = null, + t1 = this.frame, + match = $.$get$_firefoxEvalLocation().firstMatch$1(t1); + if (match == null) + return new A.UnparsedFrame(A._Uri__Uri(_null, "unparsed", _null, _null), t1); + t1 = match._match; + if (1 >= t1.length) + return A.ioore(t1, 1); + t2 = t1[1]; + t2.toString; + member = A.stringReplaceAllUnchecked(t2, "/<", ""); + if (2 >= t1.length) + return A.ioore(t1, 2); + t2 = t1[2]; + t2.toString; + uri = A.Frame__uriOrPathToUri(t2); + if (3 >= t1.length) + return A.ioore(t1, 3); + t1 = t1[3]; + t1.toString; + line = A.int_parse(t1, _null); + return new A.Frame(uri, line, _null, member.length === 0 || member === "anonymous" ? "<fn>" : member); + }, + $signature: 6 + }; + A.Frame_Frame$parseFirefox_closure.prototype = { + call$0() { + var t2, t3, t4, uri, member, line, column, _null = null, + t1 = this.frame, + match = $.$get$_firefoxSafariFrame().firstMatch$1(t1); + if (match == null) + return new A.UnparsedFrame(A._Uri__Uri(_null, "unparsed", _null, _null), t1); + t2 = match._match; + if (3 >= t2.length) + return A.ioore(t2, 3); + t3 = t2[3]; + t4 = t3; + t4.toString; + if (B.JSString_methods.contains$1(t4, " line ")) + return A.Frame_Frame$_parseFirefoxEval(t1); + t1 = t3; + t1.toString; + uri = A.Frame__uriOrPathToUri(t1); + t1 = t2.length; + if (1 >= t1) + return A.ioore(t2, 1); + member = t2[1]; + if (member != null) { + if (2 >= t1) + return A.ioore(t2, 2); + t1 = t2[2]; + t1.toString; + t1 = B.JSString_methods.allMatches$1("/", t1); + member += B.JSArray_methods.join$0(A.List_List$filled(t1.get$length(t1), ".<fn>", false, type$.String)); + if (member === "") + member = "<fn>"; + member = B.JSString_methods.replaceFirst$2(member, $.$get$_initialDot(), ""); + } else + member = "<fn>"; + if (4 >= t2.length) + return A.ioore(t2, 4); + t1 = t2[4]; + if (t1 === "") + line = _null; + else { + t1 = t1; + t1.toString; + line = A.int_parse(t1, _null); + } + if (5 >= t2.length) + return A.ioore(t2, 5); + t1 = t2[5]; + if (t1 == null || t1 === "") + column = _null; + else { + t1 = t1; + t1.toString; + column = A.int_parse(t1, _null); + } + return new A.Frame(uri, line, column, member); + }, + $signature: 6 + }; + A.Frame_Frame$parseFriendly_closure.prototype = { + call$0() { + var t2, uri, line, column, _null = null, + t1 = this.frame, + match = $.$get$_friendlyFrame().firstMatch$1(t1); + if (match == null) + throw A.wrapException(A.FormatException$("Couldn't parse package:stack_trace stack trace line '" + t1 + "'.", _null, _null)); + t1 = match._match; + if (1 >= t1.length) + return A.ioore(t1, 1); + t2 = t1[1]; + if (t2 === "data:...") + uri = A.Uri_Uri$dataFromString(""); + else { + t2 = t2; + t2.toString; + uri = A.Uri_parse(t2); + } + if (uri.get$scheme() === "") { + t2 = $.$get$context(); + uri = t2.toUri$1(t2.absolute$15(0, t2.style.pathFromUri$1(A._parseUri(uri)), _null, _null, _null, _null, _null, _null, _null, _null, _null, _null, _null, _null, _null, _null)); + } + if (2 >= t1.length) + return A.ioore(t1, 2); + t2 = t1[2]; + if (t2 == null) + line = _null; + else { + t2 = t2; + t2.toString; + line = A.int_parse(t2, _null); + } + if (3 >= t1.length) + return A.ioore(t1, 3); + t2 = t1[3]; + if (t2 == null) + column = _null; + else { + t2 = t2; + t2.toString; + column = A.int_parse(t2, _null); + } + if (4 >= t1.length) + return A.ioore(t1, 4); + return new A.Frame(uri, line, column, t1[4]); + }, + $signature: 6 + }; + A.LazyTrace.prototype = { + get$_lazy_trace$_trace() { + var result, _this = this, + value = _this.__LazyTrace__trace_FI; + if (value === $) { + result = _this._thunk.call$0(); + _this.__LazyTrace__trace_FI !== $ && A.throwLateFieldADI("_trace"); + _this.__LazyTrace__trace_FI = result; + value = result; + } + return value; + }, + get$frames() { + return this.get$_lazy_trace$_trace().get$frames(); + }, + get$terse() { + return new A.LazyTrace(new A.LazyTrace_terse_closure(this)); + }, + toString$0(_) { + return this.get$_lazy_trace$_trace().toString$0(0); + }, + $isStackTrace: 1, + $isTrace: 1 + }; + A.LazyTrace_terse_closure.prototype = { + call$0() { + return this.$this.get$_lazy_trace$_trace().get$terse(); + }, + $signature: 22 + }; + A.Trace.prototype = { + get$terse() { + return this.foldFrames$2$terse(new A.Trace_terse_closure(), true); + }, + foldFrames$2$terse(predicate, terse) { + var newFrames, t1, t2, t3, _box_0 = {}; + _box_0.predicate = predicate; + type$.bool_Function_Frame._as(predicate); + _box_0.predicate = predicate; + _box_0.predicate = new A.Trace_foldFrames_closure(predicate); + newFrames = A._setArrayType([], type$.JSArray_Frame); + for (t1 = this.frames, t2 = A._arrayInstanceType(t1)._eval$1("ReversedListIterable<1>"), t1 = new A.ReversedListIterable(t1, t2), t1 = new A.ListIterator(t1, t1.get$length(t1), t2._eval$1("ListIterator<ListIterable.E>")), t2 = t2._eval$1("ListIterable.E"); t1.moveNext$0();) { + t3 = t1.__internal$_current; + if (t3 == null) + t3 = t2._as(t3); + if (t3 instanceof A.UnparsedFrame || !A.boolConversionCheck(_box_0.predicate.call$1(t3))) + B.JSArray_methods.add$1(newFrames, t3); + else if (newFrames.length === 0 || !A.boolConversionCheck(_box_0.predicate.call$1(B.JSArray_methods.get$last(newFrames)))) + B.JSArray_methods.add$1(newFrames, new A.Frame(t3.get$uri(), t3.get$line(t3), t3.get$column(), t3.get$member())); + } + t1 = type$.MappedListIterable_Frame_Frame; + newFrames = A.List_List$of(new A.MappedListIterable(newFrames, type$.Frame_Function_Frame._as(new A.Trace_foldFrames_closure0(_box_0)), t1), true, t1._eval$1("ListIterable.E")); + if (newFrames.length > 1 && A.boolConversionCheck(_box_0.predicate.call$1(B.JSArray_methods.get$first(newFrames)))) + B.JSArray_methods.removeAt$1(newFrames, 0); + return A.Trace$(new A.ReversedListIterable(newFrames, A._arrayInstanceType(newFrames)._eval$1("ReversedListIterable<1>")), this.original._stackTrace); + }, + toString$0(_) { + var t1 = this.frames, + t2 = A._arrayInstanceType(t1); + return new A.MappedListIterable(t1, t2._eval$1("String(1)")._as(new A.Trace_toString_closure(new A.MappedListIterable(t1, t2._eval$1("int(1)")._as(new A.Trace_toString_closure0()), t2._eval$1("MappedListIterable<1,int>")).fold$1$2(0, 0, B.CONSTANT, type$.int))), t2._eval$1("MappedListIterable<1,String>")).join$0(0); + }, + $isStackTrace: 1, + get$frames() { + return this.frames; + } + }; + A.Trace_Trace$from_closure.prototype = { + call$0() { + return A.Trace_Trace$parse(this.trace.toString$0(0)); + }, + $signature: 22 + }; + A.Trace__parseVM_closure.prototype = { + call$1(line) { + return A._asString(line).length !== 0; + }, + $signature: 1 + }; + A.Trace$parseV8_closure.prototype = { + call$1(line) { + return !B.JSString_methods.startsWith$1(A._asString(line), $.$get$_v8TraceLine()); + }, + $signature: 1 + }; + A.Trace$parseJSCore_closure.prototype = { + call$1(line) { + return A._asString(line) !== "\tat "; + }, + $signature: 1 + }; + A.Trace$parseFirefox_closure.prototype = { + call$1(line) { + A._asString(line); + return line.length !== 0 && line !== "[native code]"; + }, + $signature: 1 + }; + A.Trace$parseFriendly_closure.prototype = { + call$1(line) { + return !B.JSString_methods.startsWith$1(A._asString(line), "====="); + }, + $signature: 1 + }; + A.Trace_terse_closure.prototype = { + call$1(_) { + return false; + }, + $signature: 23 + }; + A.Trace_foldFrames_closure.prototype = { + call$1(frame) { + var t1; + if (A.boolConversionCheck(this.oldPredicate.call$1(frame))) + return true; + if (frame.get$isCore()) + return true; + if (frame.get$$package() === "stack_trace") + return true; + t1 = frame.get$member(); + t1.toString; + if (!B.JSString_methods.contains$1(t1, "<async>")) + return false; + return frame.get$line(frame) == null; + }, + $signature: 23 + }; + A.Trace_foldFrames_closure0.prototype = { + call$1(frame) { + var t1, t2; + type$.Frame._as(frame); + if (frame instanceof A.UnparsedFrame || !A.boolConversionCheck(this._box_0.predicate.call$1(frame))) + return frame; + t1 = frame.get$library(); + t2 = $.$get$_terseRegExp(); + return new A.Frame(A.Uri_parse(A.stringReplaceAllUnchecked(t1, t2, "")), null, null, frame.get$member()); + }, + $signature: 62 + }; + A.Trace_toString_closure0.prototype = { + call$1(frame) { + type$.Frame._as(frame); + return frame.get$location(frame).length; + }, + $signature: 20 + }; + A.Trace_toString_closure.prototype = { + call$1(frame) { + type$.Frame._as(frame); + if (frame instanceof A.UnparsedFrame) + return frame.toString$0(0) + "\n"; + return B.JSString_methods.padRight$1(frame.get$location(frame), this.longest) + " " + A.S(frame.get$member()) + "\n"; + }, + $signature: 21 + }; + A.UnparsedFrame.prototype = { + toString$0(_) { + return this.member; + }, + $isFrame: 1, + get$uri() { + return this.uri; + }, + get$line() { + return null; + }, + get$column() { + return null; + }, + get$isCore() { + return false; + }, + get$library() { + return "unparsed"; + }, + get$$package() { + return null; + }, + get$location() { + return "unparsed"; + }, + get$member() { + return this.member; + } + }; + A.GuaranteeChannel.prototype = { + GuaranteeChannel$3$allowSinkErrors(innerSink, allowSinkErrors, _box_0, $T) { + var _this = this, + t1 = _this.$ti, + t2 = t1._eval$1("_GuaranteeSink<1>")._as(new A._GuaranteeSink(innerSink, _this, new A._AsyncCompleter(new A._Future($.Zone__current, type$._Future_dynamic), type$._AsyncCompleter_dynamic), true, $T._eval$1("_GuaranteeSink<0>"))); + _this.__GuaranteeChannel__sink_F !== $ && A.throwLateFieldAI("_sink"); + _this.set$__GuaranteeChannel__sink_F(t2); + t1 = t1._eval$1("StreamController<1>")._as(A.StreamController_StreamController(null, new A.GuaranteeChannel_closure(_box_0, _this, $T), true, $T)); + _this.__GuaranteeChannel__streamController_F !== $ && A.throwLateFieldAI("_streamController"); + _this.set$__GuaranteeChannel__streamController_F(t1); + }, + _onSinkDisconnected$0() { + var subscription, t1; + this._disconnected = true; + subscription = this._subscription; + if (subscription != null) + subscription.cancel$0(0); + t1 = this.__GuaranteeChannel__streamController_F; + t1 === $ && A.throwLateFieldNI("_streamController"); + t1.close$0(0); + }, + set$__GuaranteeChannel__sink_F(__GuaranteeChannel__sink_F) { + this.__GuaranteeChannel__sink_F = this.$ti._eval$1("_GuaranteeSink<1>")._as(__GuaranteeChannel__sink_F); + }, + set$__GuaranteeChannel__streamController_F(__GuaranteeChannel__streamController_F) { + this.__GuaranteeChannel__streamController_F = this.$ti._eval$1("StreamController<1>")._as(__GuaranteeChannel__streamController_F); + }, + set$_subscription(_subscription) { + this._subscription = this.$ti._eval$1("StreamSubscription<1>?")._as(_subscription); + } + }; + A.GuaranteeChannel_closure.prototype = { + call$0() { + var t2, t3, + t1 = this.$this; + if (t1._disconnected) + return; + t2 = this._box_0.innerStream; + t3 = t1.__GuaranteeChannel__streamController_F; + t3 === $ && A.throwLateFieldNI("_streamController"); + t1.set$_subscription(t2.listen$3$onDone$onError(this.T._eval$1("~(0)")._as(t3.get$add(t3)), new A.GuaranteeChannel__closure(t1), t3.get$addError())); + }, + $signature: 0 + }; + A.GuaranteeChannel__closure.prototype = { + call$0() { + var t1 = this.$this, + t2 = t1.__GuaranteeChannel__sink_F; + t2 === $ && A.throwLateFieldNI("_sink"); + t2._onStreamDisconnected$0(); + t1 = t1.__GuaranteeChannel__streamController_F; + t1 === $ && A.throwLateFieldNI("_streamController"); + t1.close$0(0); + }, + $signature: 0 + }; + A._GuaranteeSink.prototype = { + add$1(_, data) { + var t1, _this = this; + _this.$ti._precomputed1._as(data); + if (_this._closed) + throw A.wrapException(A.StateError$("Cannot add event after closing.")); + if (_this._addStreamSubscription != null) + throw A.wrapException(A.StateError$("Cannot add event while adding stream.")); + if (_this._disconnected) + return; + t1 = _this._inner; + t1._target.add$1(0, t1.$ti._precomputed1._as(data)); + }, + addError$2(error, stackTrace) { + var _this = this; + type$.Object._as(error); + type$.nullable_StackTrace._as(stackTrace); + if (_this._closed) + throw A.wrapException(A.StateError$("Cannot add event after closing.")); + if (_this._addStreamSubscription != null) + throw A.wrapException(A.StateError$("Cannot add event while adding stream.")); + if (_this._disconnected) + return; + _this._addError$2(error, stackTrace); + }, + addError$1(error) { + return this.addError$2(error, null); + }, + _addError$2(error, stackTrace) { + this._inner._target.addError$2(type$.Object._as(error), type$.nullable_StackTrace._as(stackTrace)); + return; + }, + _addError$1(error) { + return this._addError$2(error, null); + }, + addStream$1(_, stream) { + var t2, t3, _this = this, + t1 = _this.$ti; + t1._eval$1("Stream<1>")._as(stream); + if (_this._closed) + throw A.wrapException(A.StateError$("Cannot add stream after closing.")); + if (_this._addStreamSubscription != null) + throw A.wrapException(A.StateError$("Cannot add stream while adding stream.")); + if (_this._disconnected) + return A.Future_Future$value(null, type$.void); + t2 = _this._addStreamCompleter = new A._SyncCompleter(new A._Future($.Zone__current, type$._Future_dynamic), type$._SyncCompleter_dynamic); + t3 = _this._inner; + _this.set$_addStreamSubscription(stream.listen$3$onDone$onError(t1._eval$1("~(1)")._as(t3.get$add(t3)), type$.void_Function_$opt_dynamic._as(t2.get$complete(t2)), _this.get$_addError())); + return _this._addStreamCompleter.future.then$1$1(new A._GuaranteeSink_addStream_closure(_this), type$.void); + }, + close$0(_) { + var _this = this; + if (_this._addStreamSubscription != null) + throw A.wrapException(A.StateError$("Cannot close sink while adding stream.")); + if (_this._closed) + return _this._doneCompleter.future; + _this._closed = true; + if (!_this._disconnected) { + _this._channel._onSinkDisconnected$0(); + _this._doneCompleter.complete$1(0, _this._inner._target.close$0(0)); + } + return _this._doneCompleter.future; + }, + _onStreamDisconnected$0() { + var t1, t2, _this = this; + _this._disconnected = true; + t1 = _this._doneCompleter; + if ((t1.future._async$_state & 30) === 0) + t1.complete$0(0); + t1 = _this._addStreamSubscription; + if (t1 == null) + return; + t2 = _this._addStreamCompleter; + t2.toString; + t2.complete$1(0, t1.cancel$0(0)); + _this._addStreamCompleter = null; + _this.set$_addStreamSubscription(null); + }, + set$_addStreamSubscription(_addStreamSubscription) { + this._addStreamSubscription = this.$ti._eval$1("StreamSubscription<1>?")._as(_addStreamSubscription); + }, + $isStreamConsumer: 1, + $isStreamSink: 1 + }; + A._GuaranteeSink_addStream_closure.prototype = { + call$1(_) { + var t1 = this.$this; + t1._addStreamCompleter = null; + t1.set$_addStreamSubscription(null); + }, + $signature: 10 + }; + A._MultiChannel.prototype = { + _MultiChannel$1(inner, $T) { + var t2, t3, _this = this, + _s17_ = "_streamController", + t1 = _this._mainController; + _this._controllers.$indexSet(0, 0, t1); + t1 = t1.__StreamChannelController__local_F; + t1 === $ && A.throwLateFieldNI("_local"); + t2 = t1.__GuaranteeChannel__streamController_F; + t2 === $ && A.throwLateFieldNI(_s17_); + new A._ControllerStream(t2, A._instanceType(t2)._eval$1("_ControllerStream<1>")).listen$2$onDone(new A._MultiChannel_closure(_this, $T), new A._MultiChannel_closure0(_this)); + t2 = _this._multi_channel$_inner.__GuaranteeChannel__streamController_F; + t2 === $ && A.throwLateFieldNI(_s17_); + t3 = A._instanceType(t2)._eval$1("_ControllerStream<1>"); + t1 = t1.__GuaranteeChannel__sink_F; + t1 === $ && A.throwLateFieldNI("_sink"); + _this._innerStreamSubscription = new A.CastStream(new A._ControllerStream(t2, t3), t3._eval$1("CastStream<Stream.T,List<@>>")).listen$3$onDone$onError(new A._MultiChannel_closure1(_this, $T), _this.get$_closeInnerChannel(), t1.get$addError()); + }, + virtualChannel$1(id) { + var t2, controller, _this = this, + _s17_ = "_streamController", + _s8_ = "_foreign", + t1 = {}; + t1.outputId = t1.inputId = null; + t1.inputId = id; + t1.outputId = id + 1; + if (_this._multi_channel$_inner == null) { + t1 = _this.$ti; + t2 = A.Future_Future$value(null, type$.dynamic); + return new A.VirtualChannel(_this, new A._EmptyStream(t1._eval$1("_EmptyStream<1>")), new A.NullStreamSink(t2, t1._eval$1("NullStreamSink<1>")), t1._eval$1("VirtualChannel<1>")); + } + controller = A._Cell$named("controller"); + if (_this._pendingIds.remove$1(0, id)) { + t2 = _this._controllers.$index(0, id); + t2.toString; + controller._value = t2; + } else { + t2 = _this._controllers; + if (t2.containsKey$1(0, id) || _this._closedIds.contains$1(0, id)) + throw A.wrapException(A.ArgumentError$("A virtual channel with id " + id + " already exists.", null)); + else { + controller._value = A.StreamChannelController$(true, _this.$ti._precomputed1); + t2.$indexSet(0, id, controller._readLocal$0()); + } + } + t2 = controller._readLocal$0().__StreamChannelController__local_F; + t2 === $ && A.throwLateFieldNI("_local"); + t2 = t2.__GuaranteeChannel__streamController_F; + t2 === $ && A.throwLateFieldNI(_s17_); + new A._ControllerStream(t2, A._instanceType(t2)._eval$1("_ControllerStream<1>")).listen$2$onDone(new A._MultiChannel_virtualChannel_closure(t1, _this), new A._MultiChannel_virtualChannel_closure0(t1, _this)); + t1 = controller._readLocal$0().__StreamChannelController__foreign_F; + t1 === $ && A.throwLateFieldNI(_s8_); + t1 = t1.__GuaranteeChannel__streamController_F; + t1 === $ && A.throwLateFieldNI(_s17_); + t2 = controller._readLocal$0().__StreamChannelController__foreign_F; + t2 === $ && A.throwLateFieldNI(_s8_); + t2 = t2.__GuaranteeChannel__sink_F; + t2 === $ && A.throwLateFieldNI("_sink"); + return new A.VirtualChannel(_this, new A._ControllerStream(t1, A._instanceType(t1)._eval$1("_ControllerStream<1>")), t2, _this.$ti._eval$1("VirtualChannel<1>")); + }, + _closeChannel$2(inputId, outputId) { + var t1, t2, _this = this; + _this._closedIds.add$1(0, inputId); + t1 = _this._controllers; + t2 = t1.remove$1(0, inputId); + t2.toString; + t2 = t2.__StreamChannelController__local_F; + t2 === $ && A.throwLateFieldNI("_local"); + t2 = t2.__GuaranteeChannel__sink_F; + t2 === $ && A.throwLateFieldNI("_sink"); + t2.close$0(0); + t2 = _this._multi_channel$_inner; + if (t2 == null) + return; + t2 = t2.__GuaranteeChannel__sink_F; + t2 === $ && A.throwLateFieldNI("_sink"); + t2.add$1(0, A._setArrayType([outputId], type$.JSArray_int)); + if (t1._length === 0) + _this._closeInnerChannel$0(); + }, + _closeInnerChannel$0() { + var t2, t3, _i, t4, _this = this, + t1 = _this._multi_channel$_inner.__GuaranteeChannel__sink_F; + t1 === $ && A.throwLateFieldNI("_sink"); + t1.close$0(0); + _this._innerStreamSubscription._source.cancel$0(0); + _this._multi_channel$_inner = null; + for (t1 = _this._controllers, t2 = t1.get$values(t1), t2 = A.List_List$of(t2, false, A._instanceType(t2)._eval$1("Iterable.E")), t3 = t2.length, _i = 0; _i < t3; ++_i) { + t4 = t2[_i].__StreamChannelController__local_F; + t4 === $ && A.throwLateFieldNI("_local"); + t4 = t4.__GuaranteeChannel__sink_F; + t4 === $ && A.throwLateFieldNI("_sink"); + t4.close$0(0); + } + t1.clear$0(0); + }, + $isMultiChannel: 1 + }; + A._MultiChannel_closure.prototype = { + call$1(message) { + var t1; + this.T._as(message); + t1 = this.$this._multi_channel$_inner.__GuaranteeChannel__sink_F; + t1 === $ && A.throwLateFieldNI("_sink"); + return t1.add$1(0, [0, message]); + }, + $signature() { + return this.T._eval$1("~(0)"); + } + }; + A._MultiChannel_closure0.prototype = { + call$0() { + return this.$this._closeChannel$2(0, 0); + }, + $signature: 0 + }; + A._MultiChannel_closure1.prototype = { + call$1(message) { + var t1, id, t2, t3, controller, t4; + type$.List_dynamic._as(message); + t1 = J.getInterceptor$asx(message); + id = B.JSNumber_methods.toInt$0(A._asNum(t1.$index(message, 0))); + t2 = this.$this; + if (t2._closedIds.contains$1(0, id)) + return; + t3 = this.T; + controller = t2._controllers.putIfAbsent$2(0, id, new A._MultiChannel__closure(t2, id, t3)); + t2 = t1.get$length(message); + t4 = controller.__StreamChannelController__local_F; + if (t2 > 1) { + t4 === $ && A.throwLateFieldNI("_local"); + t2 = t4.__GuaranteeChannel__sink_F; + t2 === $ && A.throwLateFieldNI("_sink"); + t2.add$1(0, t3._as(t1.$index(message, 1))); + } else { + t4 === $ && A.throwLateFieldNI("_local"); + t1 = t4.__GuaranteeChannel__sink_F; + t1 === $ && A.throwLateFieldNI("_sink"); + t1.close$0(0); + } + }, + $signature: 42 + }; + A._MultiChannel__closure.prototype = { + call$0() { + this.$this._pendingIds.add$1(0, this.id); + return A.StreamChannelController$(true, this.T); + }, + $signature() { + return this.T._eval$1("StreamChannelController<0>()"); + } + }; + A._MultiChannel_virtualChannel_closure.prototype = { + call$1(message) { + var t1 = this.$this; + t1.$ti._precomputed1._as(message); + t1 = t1._multi_channel$_inner.__GuaranteeChannel__sink_F; + t1 === $ && A.throwLateFieldNI("_sink"); + return t1.add$1(0, [this._box_0.outputId, message]); + }, + $signature() { + return this.$this.$ti._eval$1("~(1)"); + } + }; + A._MultiChannel_virtualChannel_closure0.prototype = { + call$0() { + var t1 = this._box_0; + return this.$this._closeChannel$2(t1.inputId, t1.outputId); + }, + $signature: 0 + }; + A.VirtualChannel.prototype = {$isMultiChannel: 1}; + A.StreamChannelController.prototype = { + set$__StreamChannelController__local_F(__StreamChannelController__local_F) { + this.__StreamChannelController__local_F = this.$ti._eval$1("StreamChannel<1>")._as(__StreamChannelController__local_F); + }, + set$__StreamChannelController__foreign_F(__StreamChannelController__foreign_F) { + this.__StreamChannelController__foreign_F = this.$ti._eval$1("StreamChannel<1>")._as(__StreamChannelController__foreign_F); + } + }; + A.StreamChannelMixin.prototype = {$isStreamChannel: 1}; + A.MessagePortExtension_get_postMessage_closure.prototype = { + call$1(message) { + var t2, + t1 = A._setArrayType([], type$.JSArray_Object); + if (message != null) { + t2 = A.jsify(message); + t1.push(t2 == null ? type$.Object._as(t2) : t2); + } + return A.callMethod(this._this, "postMessage", t1, type$.void); + }, + $signature: 7 + }; + A.Subscription.prototype = {}; + A.main_closure.prototype = { + call$0() { + var play, + serverChannel = A._connectToServer(), + t1 = serverChannel._mainController.__StreamChannelController__foreign_F; + t1 === $ && A.throwLateFieldNI("_foreign"); + t1 = t1.__GuaranteeChannel__streamController_F; + t1 === $ && A.throwLateFieldNI("_streamController"); + new A._ControllerStream(t1, A._instanceType(t1)._eval$1("_ControllerStream<1>")).listen$1(new A.main__closure(serverChannel)); + A.Timer_Timer$periodic(new A.Duration(1000000), new A.main__closure0(serverChannel)); + play = type$.nullable_JavaScriptObject._as(self.document.querySelector("#play")); + play.toString; + A.EventTargetExtension_addEventListener(play, "click", A.allowInterop(new A.main__closure1(serverChannel), type$.void_Function_JavaScriptObject)); + t1 = type$.void_Function; + self.dartTest = type$.JavaScriptObject._as({resume: A.allowInterop(new A.main__closure2(serverChannel), t1), restartCurrent: A.allowInterop(new A.main__closure3(serverChannel), t1)}); + }, + $signature: 3 + }; + A.main__closure.prototype = { + call$1(message) { + var _0_4, _0_3, _0_9, _0_12, _0_15, _0_14, t1, t2, channel, url, id, suiteChannel, _null = null; + $label0$0: { + _0_4 = A._InitializedCell$named("#0#4", new A.main___closure(message)); + _0_3 = A._InitializedCell$named("#0#3", new A.main___closure0(message)); + _0_9 = A._InitializedCell$named("#0#9", new A.main___closure1(message)); + _0_12 = A._InitializedCell$named("#0#12", new A.main___closure2(message)); + _0_15 = A._InitializedCell$named("#0#15", new A.main___closure3(message)); + _0_14 = A._InitializedCell$named("#0#14", new A.main___closure4(message)); + t1 = type$.Map_dynamic_dynamic._is(message); + if (t1) { + if (_0_4._readFinal$0() == null) + t2 = A.boolConversionCheck(_0_3._readFinal$0()); + else + t2 = true; + if (t2) + if ("loadSuite" === _0_4._readFinal$0()) { + if (_0_9._readFinal$0() == null) + t2 = J.containsKey$1$x(message, "channel"); + else + t2 = true; + if (t2) + if (typeof _0_9._readFinal$0() == "number") { + channel = _0_9._readFinal$0(); + if (_0_12._readFinal$0() == null) + t2 = J.containsKey$1$x(message, "url"); + else + t2 = true; + if (t2) + if (typeof _0_12._readFinal$0() == "string") { + url = _0_12._readFinal$0(); + if (_0_15._readFinal$0() == null) + t2 = A.boolConversionCheck(_0_14._readFinal$0()); + else + t2 = true; + if (t2) + if (typeof _0_15._readFinal$0() == "number") { + id = _0_15._readFinal$0(); + t2 = true; + } else { + id = _null; + t2 = false; + } + else { + id = _null; + t2 = false; + } + } else { + id = _null; + url = id; + t2 = false; + } + else { + id = _null; + url = id; + t2 = false; + } + } else { + id = _null; + url = id; + channel = url; + t2 = false; + } + else { + id = _null; + url = id; + channel = url; + t2 = false; + } + } else { + id = _null; + url = id; + channel = url; + t2 = false; + } + else { + id = _null; + url = id; + channel = url; + t2 = false; + } + } else { + id = _null; + url = id; + channel = url; + t2 = false; + } + if (t2) { + suiteChannel = this.serverChannel.virtualChannel$1(J.toInt$0$n(channel)); + t1 = suiteChannel.$ti._eval$1("StreamChannel<1>")._as(A._connectToIframe(url, J.toInt$0$n(id))); + t2 = t1.__GuaranteeChannel__sink_F; + t2 === $ && A.throwLateFieldNI("_sink"); + suiteChannel.stream.pipe$1(t2); + t1 = t1.__GuaranteeChannel__streamController_F; + t1 === $ && A.throwLateFieldNI("_streamController"); + new A._ControllerStream(t1, A._instanceType(t1)._eval$1("_ControllerStream<1>")).pipe$1(suiteChannel.sink); + break $label0$0; + } + if (t1) { + if (_0_4._readFinal$0() == null) + t2 = A.boolConversionCheck(_0_3._readFinal$0()); + else + t2 = true; + t2 = t2 && "displayPause" === _0_4._readFinal$0(); + } else + t2 = false; + if (t2) { + type$.JavaScriptObject._as(type$.nullable_JavaScriptObject._as(self.document.body).classList).add("paused"); + break $label0$0; + } + if (t1) { + if (_0_4._readFinal$0() == null) + t2 = A.boolConversionCheck(_0_3._readFinal$0()); + else + t2 = true; + t2 = t2 && "resume" === _0_4._readFinal$0(); + } else + t2 = false; + if (t2) { + type$.JavaScriptObject._as(type$.nullable_JavaScriptObject._as(self.document.body).classList).remove("paused"); + break $label0$0; + } + if (t1) { + if (_0_4._readFinal$0() == null) + t1 = A.boolConversionCheck(_0_3._readFinal$0()); + else + t1 = true; + if (t1) + if ("closeSuite" === _0_4._readFinal$0()) { + if (_0_15._readFinal$0() == null) + t1 = A.boolConversionCheck(_0_14._readFinal$0()); + else + t1 = true; + if (t1) { + id = _0_15._readFinal$0(); + t1 = true; + } else { + id = _null; + t1 = false; + } + } else { + id = _null; + t1 = false; + } + else { + id = _null; + t1 = false; + } + } else { + id = _null; + t1 = false; + } + if (t1) { + t1 = $._iframes.remove$1(0, id); + t2 = type$.nullable_JavaScriptObject; + if (t2._as(t1.parentNode) != null) + type$.JavaScriptObject._as(t2._as(t1.parentNode).removeChild(t1)); + t1 = $._subscriptions.remove$1(0, id); + if (t1 != null) + J.cancel$0$z(t1); + t1 = $._domSubscriptions.remove$1(0, id); + if (t1 != null) + A.EventTargetExtension_removeEventListener(t1.target, t1.type, t1.listener); + break $label0$0; + } + type$.JavaScriptObject._as(self.window.console).warn("Unhandled message from test runner: " + A.S(message)); + } + }, + $signature: 5 + }; + A.main___closure.prototype = { + call$0() { + return J.$index$asx(type$.Map_dynamic_dynamic._as(this._0_0), "command"); + }, + $signature: 2 + }; + A.main___closure0.prototype = { + call$0() { + return J.containsKey$1$x(type$.Map_dynamic_dynamic._as(this._0_0), "command"); + }, + $signature: 24 + }; + A.main___closure1.prototype = { + call$0() { + return J.$index$asx(type$.Map_dynamic_dynamic._as(this._0_0), "channel"); + }, + $signature: 2 + }; + A.main___closure2.prototype = { + call$0() { + return J.$index$asx(type$.Map_dynamic_dynamic._as(this._0_0), "url"); + }, + $signature: 2 + }; + A.main___closure3.prototype = { + call$0() { + return J.$index$asx(type$.Map_dynamic_dynamic._as(this._0_0), "id"); + }, + $signature: 2 + }; + A.main___closure4.prototype = { + call$0() { + return J.containsKey$1$x(type$.Map_dynamic_dynamic._as(this._0_0), "id"); + }, + $signature: 24 + }; + A.main__closure0.prototype = { + call$1(_) { + var t1, t2; + type$.Timer._as(_); + t1 = this.serverChannel._mainController.__StreamChannelController__foreign_F; + t1 === $ && A.throwLateFieldNI("_foreign"); + t1 = t1.__GuaranteeChannel__sink_F; + t1 === $ && A.throwLateFieldNI("_sink"); + t2 = type$.String; + return t1.add$1(0, A.LinkedHashMap_LinkedHashMap$_literal(["command", "ping"], t2, t2)); + }, + $signature: 44 + }; + A.main__closure1.prototype = { + call$1(_) { + var t2, + t1 = type$.JavaScriptObject; + t1._as(_); + t2 = type$.nullable_JavaScriptObject; + if (!A._asBool(t1._as(t2._as(self.document.body).classList).contains("paused"))) + return; + t1._as(t2._as(self.document.body).classList).remove("paused"); + t1 = this.serverChannel._mainController.__StreamChannelController__foreign_F; + t1 === $ && A.throwLateFieldNI("_foreign"); + t1 = t1.__GuaranteeChannel__sink_F; + t1 === $ && A.throwLateFieldNI("_sink"); + t2 = type$.String; + t1.add$1(0, A.LinkedHashMap_LinkedHashMap$_literal(["command", "resume"], t2, t2)); + }, + $signature: 8 + }; + A.main__closure2.prototype = { + call$0() { + var t1 = type$.nullable_JavaScriptObject, + t2 = type$.JavaScriptObject; + if (!A._asBool(t2._as(t1._as(self.document.body).classList).contains("paused"))) + return; + t2._as(t1._as(self.document.body).classList).remove("paused"); + t1 = this.serverChannel._mainController.__StreamChannelController__foreign_F; + t1 === $ && A.throwLateFieldNI("_foreign"); + t1 = t1.__GuaranteeChannel__sink_F; + t1 === $ && A.throwLateFieldNI("_sink"); + t2 = type$.String; + t1.add$1(0, A.LinkedHashMap_LinkedHashMap$_literal(["command", "resume"], t2, t2)); + }, + $signature: 0 + }; + A.main__closure3.prototype = { + call$0() { + var t2, + t1 = this.serverChannel._mainController.__StreamChannelController__foreign_F; + t1 === $ && A.throwLateFieldNI("_foreign"); + t1 = t1.__GuaranteeChannel__sink_F; + t1 === $ && A.throwLateFieldNI("_sink"); + t2 = type$.String; + t1.add$1(0, A.LinkedHashMap_LinkedHashMap$_literal(["command", "restart"], t2, t2)); + }, + $signature: 0 + }; + A.main_closure0.prototype = { + call$2(error, stackTrace) { + type$.Object._as(error); + type$.StackTrace._as(stackTrace); + type$.JavaScriptObject._as(self.window.console).warn(A.S(error) + "\n" + A.Trace_Trace$from(stackTrace).get$terse().toString$0(0)); + }, + $signature: 11 + }; + A._connectToServer_closure.prototype = { + call$1(message) { + var t1; + type$.JavaScriptObject._as(message); + t1 = this.controller.__StreamChannelController__local_F; + t1 === $ && A.throwLateFieldNI("_local"); + t1 = t1.__GuaranteeChannel__sink_F; + t1 === $ && A.throwLateFieldNI("_sink"); + t1.add$1(0, B.C_JsonCodec.decode$2$reviver(0, A._asString(A.dartify(message.data)), null)); + }, + $signature: 8 + }; + A._connectToServer_closure0.prototype = { + call$1(message) { + return this.webSocket.send(B.C_JsonCodec.encode$2$toEncodable(message, null)); + }, + $signature: 5 + }; + A._connectToIframe_closure.prototype = { + call$1($event) { + var t2, _0_0, _0_6, _0_11, port, data, _this = this, _null = null, + t1 = type$.JavaScriptObject; + t1._as($event); + if (A._asString($event.origin) !== A._asString(t1._as(self.window.location).origin)) + return; + t2 = $event.source.location; + t2 = t2 == null ? _null : A._asStringQ(t2.href); + if (t2 != A._asStringQ(_this.iframe.src)) + return; + $event.stopPropagation(); + t2 = _this.windowSubscription._readLocal$0(); + A.EventTargetExtension_removeEventListener(t2.target, t2.type, t2.listener); + $label0$0: { + _0_0 = A.dartify($event.data); + _0_6 = A._InitializedCell$named("#0#6", new A._connectToIframe__closure(_0_0)); + _0_11 = A._InitializedCell$named("#0#11", new A._connectToIframe__closure0(_0_0)); + if ("port" === _0_0) { + t1._as(self.window.console).log("Connecting channel for suite " + _this.suiteUrl.toString$0(0)); + t1 = J.cast$1$0$ax(type$.List_dynamic._as($event.ports), t1); + port = t1.get$first(t1); + t1 = _this.id; + t2 = _this.controller; + $._domSubscriptions.$indexSet(0, t1, A.Subscription$(port, "message", A.allowInterop(new A._connectToIframe__closure1(t2), type$.void_Function_JavaScriptObject))); + port.start(); + t2 = t2.__StreamChannelController__local_F; + t2 === $ && A.throwLateFieldNI("_local"); + t2 = t2.__GuaranteeChannel__streamController_F; + t2 === $ && A.throwLateFieldNI("_streamController"); + $._subscriptions.$indexSet(0, t1, new A._ControllerStream(t2, A._instanceType(t2)._eval$1("_ControllerStream<1>")).listen$1(A.MessagePortExtension_get_postMessage(port))); + break $label0$0; + } + if (type$.Map_dynamic_dynamic._is(_0_0)) { + if (_0_6._readFinal$0() == null) + t1 = J.containsKey$1$x(_0_0, "exception"); + else + t1 = true; + if (t1) + if (true === _0_6._readFinal$0()) { + if (_0_11._readFinal$0() == null) + t1 = J.containsKey$1$x(_0_0, "data"); + else + t1 = true; + if (t1) { + data = _0_11._readFinal$0(); + t1 = true; + } else { + data = _null; + t1 = false; + } + } else { + data = _null; + t1 = false; + } + else { + data = _null; + t1 = false; + } + } else { + data = _null; + t1 = false; + } + if (t1) { + t1 = _this.controller.__StreamChannelController__local_F; + t1 === $ && A.throwLateFieldNI("_local"); + t1 = t1.__GuaranteeChannel__sink_F; + t1 === $ && A.throwLateFieldNI("_sink"); + t1.add$1(0, data); + } + } + }, + $signature: 8 + }; + A._connectToIframe__closure1.prototype = { + call$1($event) { + var t1; + type$.JavaScriptObject._as($event); + t1 = this.controller.__StreamChannelController__local_F; + t1 === $ && A.throwLateFieldNI("_local"); + t1 = t1.__GuaranteeChannel__sink_F; + t1 === $ && A.throwLateFieldNI("_sink"); + t1.add$1(0, A.dartify($event.data)); + }, + $signature: 8 + }; + A._connectToIframe__closure.prototype = { + call$0() { + return J.$index$asx(type$.Map_dynamic_dynamic._as(this._0_0), "exception"); + }, + $signature: 2 + }; + A._connectToIframe__closure0.prototype = { + call$0() { + return J.$index$asx(type$.Map_dynamic_dynamic._as(this._0_0), "data"); + }, + $signature: 2 + }; + (function aliases() { + var _ = J.Interceptor.prototype; + _.super$Interceptor$toString = _.toString$0; + _ = J.LegacyJavaScriptObject.prototype; + _.super$LegacyJavaScriptObject$toString = _.toString$0; + _ = A.Iterable.prototype; + _.super$Iterable$skipWhile = _.skipWhile$1; + })(); + (function installTearOffs() { + var _instance_1_u = hunkHelpers._instance_1u, + _static_1 = hunkHelpers._static_1, + _static_0 = hunkHelpers._static_0, + _static_2 = hunkHelpers._static_2, + _static = hunkHelpers.installStaticTearOff, + _instance = hunkHelpers.installInstanceTearOff, + _instance_2_u = hunkHelpers._instance_2u, + _instance_1_i = hunkHelpers._instance_1i, + _instance_0_u = hunkHelpers._instance_0u; + _instance_1_u(A.CastStreamSubscription.prototype, "get$__internal$_onData", "__internal$_onData$1", 7); + _static_1(A, "async__AsyncRun__scheduleImmediateJsOverride$closure", "_AsyncRun__scheduleImmediateJsOverride", 13); + _static_1(A, "async__AsyncRun__scheduleImmediateWithSetImmediate$closure", "_AsyncRun__scheduleImmediateWithSetImmediate", 13); + _static_1(A, "async__AsyncRun__scheduleImmediateWithTimer$closure", "_AsyncRun__scheduleImmediateWithTimer", 13); + _static_0(A, "async___startMicrotaskLoop$closure", "_startMicrotaskLoop", 0); + _static_1(A, "async___nullDataHandler$closure", "_nullDataHandler", 5); + _static_2(A, "async___nullErrorHandler$closure", "_nullErrorHandler", 11); + _static_0(A, "async___nullDoneHandler$closure", "_nullDoneHandler", 0); + _static(A, "async___rootHandleUncaughtError$closure", 5, null, ["call$5"], ["_rootHandleUncaughtError"], 47, 0); + _static(A, "async___rootRun$closure", 4, null, ["call$1$4", "call$4"], ["_rootRun", function($self, $parent, zone, f) { + return A._rootRun($self, $parent, zone, f, type$.dynamic); + }], 48, 1); + _static(A, "async___rootRunUnary$closure", 5, null, ["call$2$5", "call$5"], ["_rootRunUnary", function($self, $parent, zone, f, arg) { + return A._rootRunUnary($self, $parent, zone, f, arg, type$.dynamic, type$.dynamic); + }], 49, 1); + _static(A, "async___rootRunBinary$closure", 6, null, ["call$3$6", "call$6"], ["_rootRunBinary", function($self, $parent, zone, f, arg1, arg2) { + return A._rootRunBinary($self, $parent, zone, f, arg1, arg2, type$.dynamic, type$.dynamic, type$.dynamic); + }], 50, 1); + _static(A, "async___rootRegisterCallback$closure", 4, null, ["call$1$4", "call$4"], ["_rootRegisterCallback", function($self, $parent, zone, f) { + return A._rootRegisterCallback($self, $parent, zone, f, type$.dynamic); + }], 51, 0); + _static(A, "async___rootRegisterUnaryCallback$closure", 4, null, ["call$2$4", "call$4"], ["_rootRegisterUnaryCallback", function($self, $parent, zone, f) { + return A._rootRegisterUnaryCallback($self, $parent, zone, f, type$.dynamic, type$.dynamic); + }], 52, 0); + _static(A, "async___rootRegisterBinaryCallback$closure", 4, null, ["call$3$4", "call$4"], ["_rootRegisterBinaryCallback", function($self, $parent, zone, f) { + return A._rootRegisterBinaryCallback($self, $parent, zone, f, type$.dynamic, type$.dynamic, type$.dynamic); + }], 53, 0); + _static(A, "async___rootErrorCallback$closure", 5, null, ["call$5"], ["_rootErrorCallback"], 54, 0); + _static(A, "async___rootScheduleMicrotask$closure", 4, null, ["call$4"], ["_rootScheduleMicrotask"], 55, 0); + _static(A, "async___rootCreateTimer$closure", 5, null, ["call$5"], ["_rootCreateTimer"], 56, 0); + _static(A, "async___rootCreatePeriodicTimer$closure", 5, null, ["call$5"], ["_rootCreatePeriodicTimer"], 57, 0); + _static(A, "async___rootPrint$closure", 4, null, ["call$4"], ["_rootPrint"], 58, 0); + _static(A, "async___rootFork$closure", 5, null, ["call$5"], ["_rootFork"], 59, 0); + _instance(A._SyncCompleter.prototype, "get$complete", 1, 0, function() { + return [null]; + }, ["call$1", "call$0"], ["complete$1", "complete$0"], 43, 0, 0); + _instance_2_u(A._Future.prototype, "get$_completeError", "_completeError$2", 11); + var _; + _instance_1_i(_ = A._StreamController.prototype, "get$add", "add$1", 7); + _instance(_, "get$addError", 0, 1, function() { + return [null]; + }, ["call$2", "call$1"], ["addError$2", "addError$1"], 12, 0, 0); + _instance_1_i(A._StreamSinkWrapper.prototype, "get$add", "add$1", 7); + _instance_0_u(A._DoneStreamSubscription.prototype, "get$_onMicrotask", "_onMicrotask$0", 0); + _static_1(A, "convert___defaultToEncodable$closure", "_defaultToEncodable", 15); + _static_1(A, "core_Uri_decodeComponent$closure", "Uri_decodeComponent", 17); + _static_1(A, "frame_Frame___parseVM_tearOff$closure", "Frame___parseVM_tearOff", 9); + _static_1(A, "frame_Frame___parseV8_tearOff$closure", "Frame___parseV8_tearOff", 9); + _static_1(A, "frame_Frame___parseFirefox_tearOff$closure", "Frame___parseFirefox_tearOff", 9); + _static_1(A, "frame_Frame___parseFriendly_tearOff$closure", "Frame___parseFriendly_tearOff", 9); + _static_1(A, "trace_Trace___parseVM_tearOff$closure", "Trace___parseVM_tearOff", 14); + _static_1(A, "trace_Trace___parseFriendly_tearOff$closure", "Trace___parseFriendly_tearOff", 14); + _instance(_ = A._GuaranteeSink.prototype, "get$addError", 0, 1, function() { + return [null]; + }, ["call$2", "call$1"], ["addError$2", "addError$1"], 12, 0, 0); + _instance(_, "get$_addError", 0, 1, function() { + return [null]; + }, ["call$2", "call$1"], ["_addError$2", "_addError$1"], 12, 0, 0); + _instance_0_u(A._MultiChannel.prototype, "get$_closeInnerChannel", "_closeInnerChannel$0", 0); + _static(A, "math__max$closure", 2, null, ["call$1$2", "call$2"], ["max", function(a, b) { + return A.max(a, b, type$.num); + }], 41, 0); + })(); + (function inheritance() { + var _mixin = hunkHelpers.mixin, + _inherit = hunkHelpers.inherit, + _inheritMany = hunkHelpers.inheritMany; + _inherit(A.Object, null); + _inheritMany(A.Object, [A.JS_CONST, J.Interceptor, J.ArrayIterator, A.Stream, A.CastStreamSubscription, A.Iterable, A.CastIterator, A.Error, A.ListBase, A.Closure, A.SentinelValue, A.ListIterator, A.MappedIterator, A.WhereIterator, A.ExpandIterator, A.TakeIterator, A.SkipIterator, A.SkipWhileIterator, A.EmptyIterator, A.WhereTypeIterator, A.FixedLengthListMixin, A.UnmodifiableListMixin, A.Symbol, A.MapView, A.ConstantMap, A._KeysOrValuesOrElementsIterator, A.JSInvocationMirror, A.TypeErrorDecoder, A.NullThrownFromJavaScriptException, A._StackTrace, A._Required, A.MapBase, A.LinkedHashMapCell, A.LinkedHashMapKeyIterator, A.JSSyntaxRegExp, A._MatchImplementation, A._AllMatchesIterator, A.StringMatch, A._StringAllMatchesIterator, A._Cell, A._InitializedCell, A.Rti, A._FunctionParameters, A._Type, A._TimerImpl, A.AsyncError, A._Completer, A._FutureListener, A._Future, A._AsyncCallbackEntry, A._StreamController, A._SyncStreamControllerDispatch, A._BufferingStreamSubscription, A._StreamSinkWrapper, A._DelayedEvent, A._DelayedDone, A._PendingEvents, A._DoneStreamSubscription, A._ZoneFunction, A._ZoneSpecification, A._ZoneDelegate, A._Zone, A._HashMapKeyIterator, A.SetBase, A._LinkedHashSetCell, A._LinkedHashSetIterator, A._UnmodifiableMapMixin, A.Codec, A.Converter, A._JsonStringifier, A._Utf8Encoder, A._Utf8Decoder, A.DateTime, A.Duration, A.OutOfMemoryError, A.StackOverflowError, A._Exception, A.FormatException, A.Null, A._StringStackTrace, A.StringBuffer, A._Uri, A.UriData, A._SimpleUri, A.CssStyleDeclarationBase, A.ImmutableListMixin, A.FixedSizeListIterator, A.NullRejectionException, A.NullStreamSink, A.Context, A.Style, A.ParsedPath, A.PathException, A.Chain, A.Frame, A.LazyTrace, A.Trace, A.UnparsedFrame, A.StreamChannelMixin, A._GuaranteeSink, A.StreamChannelController, A.Subscription]); + _inheritMany(J.Interceptor, [J.JSBool, J.JSNull, J.JavaScriptObject, J.JavaScriptBigInt, J.JavaScriptSymbol, J.JSNumber, J.JSString]); + _inheritMany(J.JavaScriptObject, [J.LegacyJavaScriptObject, J.JSArray, A.NativeByteBuffer, A.NativeTypedData, A.EventTarget, A.AccessibleNodeList, A.Blob, A.CssTransformComponent, A.CssRule, A._CssStyleDeclaration_JavaScriptObject_CssStyleDeclarationBase, A.CssStyleValue, A.DataTransferItemList, A.DomException, A._DomRectList_JavaScriptObject_ListMixin, A.DomRectReadOnly, A._DomStringList_JavaScriptObject_ListMixin, A.DomTokenList, A._FileList_JavaScriptObject_ListMixin, A.Gamepad, A.History, A._HtmlCollection_JavaScriptObject_ListMixin, A.Location, A.MediaList, A._MidiInputMap_JavaScriptObject_MapMixin, A._MidiOutputMap_JavaScriptObject_MapMixin, A.MimeType, A._MimeTypeArray_JavaScriptObject_ListMixin, A._NodeList_JavaScriptObject_ListMixin, A.Plugin, A._PluginArray_JavaScriptObject_ListMixin, A._RtcStatsReport_JavaScriptObject_MapMixin, A.SpeechGrammar, A._SpeechGrammarList_JavaScriptObject_ListMixin, A.SpeechRecognitionResult, A._Storage_JavaScriptObject_MapMixin, A.StyleSheet, A._TextTrackCueList_JavaScriptObject_ListMixin, A.TimeRanges, A.Touch, A._TouchList_JavaScriptObject_ListMixin, A.TrackDefaultList, A.Url, A.__CssRuleList_JavaScriptObject_ListMixin, A.__GamepadList_JavaScriptObject_ListMixin, A.__NamedNodeMap_JavaScriptObject_ListMixin, A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin, A.__StyleSheetList_JavaScriptObject_ListMixin, A.Length, A._LengthList_JavaScriptObject_ListMixin, A.Number, A._NumberList_JavaScriptObject_ListMixin, A.PointList, A._StringList_JavaScriptObject_ListMixin, A.Transform, A._TransformList_JavaScriptObject_ListMixin, A.AudioBuffer, A._AudioParamMap_JavaScriptObject_MapMixin]); + _inheritMany(J.LegacyJavaScriptObject, [J.PlainJavaScriptObject, J.UnknownJavaScriptObject, J.JavaScriptFunction]); + _inherit(J.JSUnmodifiableArray, J.JSArray); + _inheritMany(J.JSNumber, [J.JSInt, J.JSNumNotInt]); + _inheritMany(A.Stream, [A.CastStream, A._StreamImpl, A._EmptyStream]); + _inheritMany(A.Iterable, [A._CastIterableBase, A.EfficientLengthIterable, A.MappedIterable, A.WhereIterable, A.ExpandIterable, A.TakeIterable, A.SkipIterable, A.SkipWhileIterable, A.WhereTypeIterable, A._KeysOrValues, A._AllMatchesIterable, A._StringAllMatchesIterable]); + _inheritMany(A._CastIterableBase, [A.CastIterable, A.__CastListBase__CastIterableBase_ListMixin]); + _inherit(A._EfficientLengthCastIterable, A.CastIterable); + _inherit(A._CastListBase, A.__CastListBase__CastIterableBase_ListMixin); + _inherit(A.CastList, A._CastListBase); + _inheritMany(A.Error, [A.LateError, A.TypeError, A.JsNoSuchMethodError, A.UnknownJsTypeError, A._CyclicInitializationError, A.RuntimeError, A.AssertionError, A._Error, A.JsonUnsupportedObjectError, A.ArgumentError, A.NoSuchMethodError, A.UnsupportedError, A.UnimplementedError, A.StateError, A.ConcurrentModificationError]); + _inherit(A.UnmodifiableListBase, A.ListBase); + _inherit(A.CodeUnits, A.UnmodifiableListBase); + _inheritMany(A.Closure, [A.Closure0Args, A.Instantiation, A.Closure2Args, A.TearOffClosure, A.JsLinkedHashMap_values_closure, A.initHooks_closure, A.initHooks_closure1, A._AsyncRun__initializeScheduleImmediate_internalCallback, A._AsyncRun__initializeScheduleImmediate_closure, A._Future__chainForeignFuture_closure, A._Future__propagateToListeners_handleWhenCompleteCallback_closure, A.Stream_pipe_closure, A.Stream_length_closure, A._CustomZone_bindUnaryCallback_closure, A._CustomZone_bindUnaryCallbackGuarded_closure, A._RootZone_bindUnaryCallback_closure, A._RootZone_bindUnaryCallbackGuarded_closure, A.runZonedGuarded_closure, A._Uri__makePath_closure, A._createTables_setChars, A._createTables_setRange, A.jsify__convert, A.promiseToFuture_closure, A.promiseToFuture_closure0, A.dartify_convert, A.Context_joinAll_closure, A.Context_split_closure, A._validateArgList_closure, A.WindowsStyle_absolutePathToUri_closure, A.Chain_Chain$parse_closure, A.Chain_toTrace_closure, A.Chain_toString_closure0, A.Chain_toString__closure0, A.Chain_toString_closure, A.Chain_toString__closure, A.Trace__parseVM_closure, A.Trace$parseV8_closure, A.Trace$parseJSCore_closure, A.Trace$parseFirefox_closure, A.Trace$parseFriendly_closure, A.Trace_terse_closure, A.Trace_foldFrames_closure, A.Trace_foldFrames_closure0, A.Trace_toString_closure0, A.Trace_toString_closure, A._GuaranteeSink_addStream_closure, A._MultiChannel_closure, A._MultiChannel_closure1, A._MultiChannel_virtualChannel_closure, A.MessagePortExtension_get_postMessage_closure, A.main__closure, A.main__closure0, A.main__closure1, A._connectToServer_closure, A._connectToServer_closure0, A._connectToIframe_closure, A._connectToIframe__closure1]); + _inheritMany(A.Closure0Args, [A.nullFuture_closure, A._AsyncRun__scheduleImmediateJsOverride_internalCallback, A._AsyncRun__scheduleImmediateWithSetImmediate_internalCallback, A._TimerImpl_internalCallback, A._TimerImpl$periodic_closure, A._Future__addListener_closure, A._Future__prependListeners_closure, A._Future__chainForeignFuture_closure1, A._Future__chainCoreFutureAsync_closure, A._Future__asyncCompleteWithValue_closure, A._Future__asyncCompleteError_closure, A._Future__propagateToListeners_handleWhenCompleteCallback, A._Future__propagateToListeners_handleValueCallback, A._Future__propagateToListeners_handleError, A.Stream_length_closure0, A._StreamController__subscribe_closure, A._StreamController__recordCancel_complete, A._AddStreamState_cancel_closure, A._BufferingStreamSubscription__sendError_sendError, A._BufferingStreamSubscription__sendDone_sendDone, A._PendingEvents_schedule_closure, A._CustomZone_bindCallback_closure, A._CustomZone_bindCallbackGuarded_closure, A._rootHandleError_closure, A._RootZone_bindCallback_closure, A._RootZone_bindCallbackGuarded_closure, A.Utf8Decoder__decoder_closure, A.Utf8Decoder__decoderNonfatal_closure, A.NullStreamSink_addStream_closure, A.Frame_Frame$parseVM_closure, A.Frame_Frame$parseV8_closure, A.Frame_Frame$_parseFirefoxEval_closure, A.Frame_Frame$parseFirefox_closure, A.Frame_Frame$parseFriendly_closure, A.LazyTrace_terse_closure, A.Trace_Trace$from_closure, A.GuaranteeChannel_closure, A.GuaranteeChannel__closure, A._MultiChannel_closure0, A._MultiChannel__closure, A._MultiChannel_virtualChannel_closure0, A.main_closure, A.main___closure, A.main___closure0, A.main___closure1, A.main___closure2, A.main___closure3, A.main___closure4, A.main__closure2, A.main__closure3, A._connectToIframe__closure, A._connectToIframe__closure0]); + _inheritMany(A.EfficientLengthIterable, [A.ListIterable, A.EmptyIterable, A.LinkedHashMapKeyIterable, A._HashMapKeyIterable]); + _inheritMany(A.ListIterable, [A.SubListIterable, A.MappedListIterable, A.ReversedListIterable, A._JsonMapKeyIterable]); + _inherit(A.EfficientLengthMappedIterable, A.MappedIterable); + _inherit(A.EfficientLengthTakeIterable, A.TakeIterable); + _inherit(A.EfficientLengthSkipIterable, A.SkipIterable); + _inherit(A._UnmodifiableMapView_MapView__UnmodifiableMapMixin, A.MapView); + _inherit(A.UnmodifiableMapView, A._UnmodifiableMapView_MapView__UnmodifiableMapMixin); + _inherit(A.ConstantMapView, A.UnmodifiableMapView); + _inherit(A.ConstantStringMap, A.ConstantMap); + _inherit(A.Instantiation1, A.Instantiation); + _inheritMany(A.Closure2Args, [A.Primitives_functionNoSuchMethod_closure, A.initHooks_closure0, A._Future__chainForeignFuture_closure0, A.MapBase_mapToString_closure, A._JsonStringifier_writeMap_closure, A.NoSuchMethodError_toString_closure, A.Uri_splitQueryString_closure, A.Uri__parseIPv4Address_error, A.Uri_parseIPv6Address_error, A.Uri_parseIPv6Address_parseHex, A._createTables_build, A.MidiInputMap_keys_closure, A.MidiOutputMap_keys_closure, A.RtcStatsReport_keys_closure, A.Storage_keys_closure, A.AudioParamMap_keys_closure, A.Frame_Frame$parseV8_closure_parseLocation, A.main_closure0]); + _inherit(A.NullError, A.TypeError); + _inheritMany(A.TearOffClosure, [A.StaticClosure, A.BoundClosure]); + _inherit(A._AssertionError, A.AssertionError); + _inheritMany(A.MapBase, [A.JsLinkedHashMap, A._HashMap, A._JsonMap]); + _inheritMany(A.NativeTypedData, [A.NativeByteData, A.NativeTypedArray]); + _inheritMany(A.NativeTypedArray, [A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin, A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin]); + _inherit(A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin, A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin); + _inherit(A.NativeTypedArrayOfDouble, A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin); + _inherit(A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin, A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin); + _inherit(A.NativeTypedArrayOfInt, A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin); + _inheritMany(A.NativeTypedArrayOfDouble, [A.NativeFloat32List, A.NativeFloat64List]); + _inheritMany(A.NativeTypedArrayOfInt, [A.NativeInt16List, A.NativeInt32List, A.NativeInt8List, A.NativeUint16List, A.NativeUint32List, A.NativeUint8ClampedList, A.NativeUint8List]); + _inherit(A._TypeError, A._Error); + _inheritMany(A._Completer, [A._AsyncCompleter, A._SyncCompleter]); + _inherit(A._SyncStreamController, A._StreamController); + _inherit(A._ControllerStream, A._StreamImpl); + _inherit(A._ControllerSubscription, A._BufferingStreamSubscription); + _inheritMany(A._DelayedEvent, [A._DelayedData, A._DelayedError]); + _inheritMany(A._Zone, [A._CustomZone, A._RootZone]); + _inherit(A._IdentityHashMap, A._HashMap); + _inherit(A._SetBase, A.SetBase); + _inherit(A._LinkedHashSet, A._SetBase); + _inheritMany(A.Codec, [A.Encoding, A.Base64Codec, A._FusedCodec, A.JsonCodec]); + _inheritMany(A.Encoding, [A.AsciiCodec, A.Utf8Codec]); + _inheritMany(A.Converter, [A._UnicodeSubsetEncoder, A.Base64Encoder, A.JsonEncoder, A.JsonDecoder, A.Utf8Encoder, A.Utf8Decoder]); + _inherit(A.AsciiEncoder, A._UnicodeSubsetEncoder); + _inherit(A.JsonCyclicError, A.JsonUnsupportedObjectError); + _inherit(A._JsonStringStringifier, A._JsonStringifier); + _inheritMany(A.ArgumentError, [A.RangeError, A.IndexError]); + _inherit(A._DataUri, A._Uri); + _inheritMany(A.EventTarget, [A.Node, A.FileWriter, A.SourceBuffer, A._SourceBufferList_EventTarget_ListMixin, A.TextTrack, A.TextTrackCue, A._TextTrackList_EventTarget_ListMixin, A.VideoTrackList, A.AudioTrackList, A.BaseAudioContext]); + _inheritMany(A.Node, [A.Element, A.CharacterData]); + _inherit(A.HtmlElement, A.Element); + _inheritMany(A.HtmlElement, [A.AnchorElement, A.AreaElement, A.FormElement, A.SelectElement]); + _inherit(A.CssPerspective, A.CssTransformComponent); + _inherit(A.CssStyleDeclaration, A._CssStyleDeclaration_JavaScriptObject_CssStyleDeclarationBase); + _inheritMany(A.CssStyleValue, [A.CssTransformValue, A.CssUnparsedValue]); + _inherit(A._DomRectList_JavaScriptObject_ListMixin_ImmutableListMixin, A._DomRectList_JavaScriptObject_ListMixin); + _inherit(A.DomRectList, A._DomRectList_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A._DomStringList_JavaScriptObject_ListMixin_ImmutableListMixin, A._DomStringList_JavaScriptObject_ListMixin); + _inherit(A.DomStringList, A._DomStringList_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A.File, A.Blob); + _inherit(A._FileList_JavaScriptObject_ListMixin_ImmutableListMixin, A._FileList_JavaScriptObject_ListMixin); + _inherit(A.FileList, A._FileList_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A._HtmlCollection_JavaScriptObject_ListMixin_ImmutableListMixin, A._HtmlCollection_JavaScriptObject_ListMixin); + _inherit(A.HtmlCollection, A._HtmlCollection_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A.MidiInputMap, A._MidiInputMap_JavaScriptObject_MapMixin); + _inherit(A.MidiOutputMap, A._MidiOutputMap_JavaScriptObject_MapMixin); + _inherit(A._MimeTypeArray_JavaScriptObject_ListMixin_ImmutableListMixin, A._MimeTypeArray_JavaScriptObject_ListMixin); + _inherit(A.MimeTypeArray, A._MimeTypeArray_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A._NodeList_JavaScriptObject_ListMixin_ImmutableListMixin, A._NodeList_JavaScriptObject_ListMixin); + _inherit(A.NodeList, A._NodeList_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A._PluginArray_JavaScriptObject_ListMixin_ImmutableListMixin, A._PluginArray_JavaScriptObject_ListMixin); + _inherit(A.PluginArray, A._PluginArray_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A.RtcStatsReport, A._RtcStatsReport_JavaScriptObject_MapMixin); + _inherit(A._SourceBufferList_EventTarget_ListMixin_ImmutableListMixin, A._SourceBufferList_EventTarget_ListMixin); + _inherit(A.SourceBufferList, A._SourceBufferList_EventTarget_ListMixin_ImmutableListMixin); + _inherit(A._SpeechGrammarList_JavaScriptObject_ListMixin_ImmutableListMixin, A._SpeechGrammarList_JavaScriptObject_ListMixin); + _inherit(A.SpeechGrammarList, A._SpeechGrammarList_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A.Storage, A._Storage_JavaScriptObject_MapMixin); + _inherit(A._TextTrackCueList_JavaScriptObject_ListMixin_ImmutableListMixin, A._TextTrackCueList_JavaScriptObject_ListMixin); + _inherit(A.TextTrackCueList, A._TextTrackCueList_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A._TextTrackList_EventTarget_ListMixin_ImmutableListMixin, A._TextTrackList_EventTarget_ListMixin); + _inherit(A.TextTrackList, A._TextTrackList_EventTarget_ListMixin_ImmutableListMixin); + _inherit(A._TouchList_JavaScriptObject_ListMixin_ImmutableListMixin, A._TouchList_JavaScriptObject_ListMixin); + _inherit(A.TouchList, A._TouchList_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A.__CssRuleList_JavaScriptObject_ListMixin_ImmutableListMixin, A.__CssRuleList_JavaScriptObject_ListMixin); + _inherit(A._CssRuleList, A.__CssRuleList_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A._DomRect, A.DomRectReadOnly); + _inherit(A.__GamepadList_JavaScriptObject_ListMixin_ImmutableListMixin, A.__GamepadList_JavaScriptObject_ListMixin); + _inherit(A._GamepadList, A.__GamepadList_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A.__NamedNodeMap_JavaScriptObject_ListMixin_ImmutableListMixin, A.__NamedNodeMap_JavaScriptObject_ListMixin); + _inherit(A._NamedNodeMap, A.__NamedNodeMap_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin_ImmutableListMixin, A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin); + _inherit(A._SpeechRecognitionResultList, A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A.__StyleSheetList_JavaScriptObject_ListMixin_ImmutableListMixin, A.__StyleSheetList_JavaScriptObject_ListMixin); + _inherit(A._StyleSheetList, A.__StyleSheetList_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A._LengthList_JavaScriptObject_ListMixin_ImmutableListMixin, A._LengthList_JavaScriptObject_ListMixin); + _inherit(A.LengthList, A._LengthList_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A._NumberList_JavaScriptObject_ListMixin_ImmutableListMixin, A._NumberList_JavaScriptObject_ListMixin); + _inherit(A.NumberList, A._NumberList_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A._StringList_JavaScriptObject_ListMixin_ImmutableListMixin, A._StringList_JavaScriptObject_ListMixin); + _inherit(A.StringList, A._StringList_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A._TransformList_JavaScriptObject_ListMixin_ImmutableListMixin, A._TransformList_JavaScriptObject_ListMixin); + _inherit(A.TransformList, A._TransformList_JavaScriptObject_ListMixin_ImmutableListMixin); + _inherit(A.AudioParamMap, A._AudioParamMap_JavaScriptObject_MapMixin); + _inherit(A.OfflineAudioContext, A.BaseAudioContext); + _inherit(A.InternalStyle, A.Style); + _inheritMany(A.InternalStyle, [A.PosixStyle, A.UrlStyle, A.WindowsStyle]); + _inheritMany(A.StreamChannelMixin, [A.GuaranteeChannel, A._MultiChannel, A.VirtualChannel]); + _mixin(A.UnmodifiableListBase, A.UnmodifiableListMixin); + _mixin(A.__CastListBase__CastIterableBase_ListMixin, A.ListBase); + _mixin(A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin, A.ListBase); + _mixin(A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin, A.FixedLengthListMixin); + _mixin(A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin, A.ListBase); + _mixin(A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin, A.FixedLengthListMixin); + _mixin(A._SyncStreamController, A._SyncStreamControllerDispatch); + _mixin(A._UnmodifiableMapView_MapView__UnmodifiableMapMixin, A._UnmodifiableMapMixin); + _mixin(A._CssStyleDeclaration_JavaScriptObject_CssStyleDeclarationBase, A.CssStyleDeclarationBase); + _mixin(A._DomRectList_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A._DomRectList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._DomStringList_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A._DomStringList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._FileList_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A._FileList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._HtmlCollection_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A._HtmlCollection_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._MidiInputMap_JavaScriptObject_MapMixin, A.MapBase); + _mixin(A._MidiOutputMap_JavaScriptObject_MapMixin, A.MapBase); + _mixin(A._MimeTypeArray_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A._MimeTypeArray_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._NodeList_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A._NodeList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._PluginArray_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A._PluginArray_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._RtcStatsReport_JavaScriptObject_MapMixin, A.MapBase); + _mixin(A._SourceBufferList_EventTarget_ListMixin, A.ListBase); + _mixin(A._SourceBufferList_EventTarget_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._SpeechGrammarList_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A._SpeechGrammarList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._Storage_JavaScriptObject_MapMixin, A.MapBase); + _mixin(A._TextTrackCueList_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A._TextTrackCueList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._TextTrackList_EventTarget_ListMixin, A.ListBase); + _mixin(A._TextTrackList_EventTarget_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._TouchList_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A._TouchList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A.__CssRuleList_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A.__CssRuleList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A.__GamepadList_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A.__GamepadList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A.__NamedNodeMap_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A.__NamedNodeMap_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A.__SpeechRecognitionResultList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A.__StyleSheetList_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A.__StyleSheetList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._LengthList_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A._LengthList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._NumberList_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A._NumberList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._StringList_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A._StringList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._TransformList_JavaScriptObject_ListMixin, A.ListBase); + _mixin(A._TransformList_JavaScriptObject_ListMixin_ImmutableListMixin, A.ImmutableListMixin); + _mixin(A._AudioParamMap_JavaScriptObject_MapMixin, A.MapBase); + })(); + var init = { + typeUniverse: {eC: new Map(), tR: {}, eT: {}, tPV: {}, sEA: []}, + mangledGlobalNames: {int: "int", double: "double", num: "num", String: "String", bool: "bool", Null: "Null", List: "List"}, + mangledNames: {}, + types: ["~()", "bool(String)", "@()", "Null()", "~(String,@)", "~(@)", "Frame()", "~(Object?)", "~(JavaScriptObject)", "Frame(String)", "Null(@)", "~(Object,StackTrace)", "~(Object[StackTrace?])", "~(~())", "Trace(String)", "@(@)", "~(Object?,Object?)", "String(String)", "~(Uint8List,String,int)", "Object?(Object?)", "int(Frame)", "String(Frame)", "Trace()", "bool(Frame)", "bool()", "~(String,int)", "Null(Object,StackTrace)", "~(String,String)", "_Future<@>(@)", "Future<@>(@)", "Null(~())", "String(String?)", "List<Frame>(Trace)", "int(Trace)", "~(Zone,ZoneDelegate,Zone,Object,StackTrace)", "String(Trace)", "@(@,String)", "@(String)", "Frame(String,String)", "~(Symbol0,@)", "Map<String,String>(Map<String,String>,String)", "0^(0^,0^)<num>", "~(List<@>)", "~([Object?])", "~(Timer)", "~(String,int?)", "int(int,int)", "~(Zone?,ZoneDelegate?,Zone,Object,StackTrace)", "0^(Zone?,ZoneDelegate?,Zone,0^())<Object?>", "0^(Zone?,ZoneDelegate?,Zone,0^(1^),1^)<Object?,Object?>", "0^(Zone?,ZoneDelegate?,Zone,0^(1^,2^),1^,2^)<Object?,Object?,Object?>", "0^()(Zone,ZoneDelegate,Zone,0^())<Object?>", "0^(1^)(Zone,ZoneDelegate,Zone,0^(1^))<Object?,Object?>", "0^(1^,2^)(Zone,ZoneDelegate,Zone,0^(1^,2^))<Object?,Object?,Object?>", "AsyncError?(Zone,ZoneDelegate,Zone,Object,StackTrace?)", "~(Zone?,ZoneDelegate?,Zone,~())", "Timer(Zone,ZoneDelegate,Zone,Duration,~())", "Timer(Zone,ZoneDelegate,Zone,Duration,~(Timer))", "~(Zone,ZoneDelegate,Zone,String)", "Zone(Zone?,ZoneDelegate?,Zone,ZoneSpecification?,Map<Object?,Object?>?)", "Future<Null>()", "Uint8List(@,@)", "Frame(Frame)"], + interceptorsByTag: null, + leafTags: null, + arrayRti: Symbol("$ti") + }; + A._Universe_addRules(init.typeUniverse, JSON.parse('{"PlainJavaScriptObject":"LegacyJavaScriptObject","UnknownJavaScriptObject":"LegacyJavaScriptObject","JavaScriptFunction":"LegacyJavaScriptObject","AbortPaymentEvent":"JavaScriptObject","ExtendableEvent":"JavaScriptObject","Event":"JavaScriptObject","AudioContext":"BaseAudioContext","AbsoluteOrientationSensor":"EventTarget","OrientationSensor":"EventTarget","Sensor":"EventTarget","MathMLElement":"Element","AudioElement":"HtmlElement","MediaElement":"HtmlElement","HtmlDocument":"Node","Document":"Node","VttCue":"TextTrackCue","CDataSection":"CharacterData","Text":"CharacterData","HtmlFormControlsCollection":"HtmlCollection","CssCharsetRule":"CssRule","CssMatrixComponent":"CssTransformComponent","CssStyleSheet":"StyleSheet","CssurlImageValue":"CssStyleValue","CssImageValue":"CssStyleValue","CssResourceValue":"CssStyleValue","JSBool":{"bool":[],"TrustedGetRuntimeType":[]},"JSNull":{"Null":[],"TrustedGetRuntimeType":[]},"LegacyJavaScriptObject":{"JavaScriptObject":[]},"JSArray":{"List":["1"],"JavaScriptObject":[],"EfficientLengthIterable":["1"],"Iterable":["1"]},"JSUnmodifiableArray":{"JSArray":["1"],"List":["1"],"JavaScriptObject":[],"EfficientLengthIterable":["1"],"Iterable":["1"]},"ArrayIterator":{"Iterator":["1"]},"JSNumber":{"double":[],"num":[]},"JSInt":{"double":[],"int":[],"num":[],"TrustedGetRuntimeType":[]},"JSNumNotInt":{"double":[],"num":[],"TrustedGetRuntimeType":[]},"JSString":{"String":[],"Pattern":[],"TrustedGetRuntimeType":[]},"CastStream":{"Stream":["2"],"Stream.T":"2"},"CastStreamSubscription":{"StreamSubscription":["2"]},"_CastIterableBase":{"Iterable":["2"]},"CastIterator":{"Iterator":["2"]},"CastIterable":{"_CastIterableBase":["1","2"],"Iterable":["2"],"Iterable.E":"2"},"_EfficientLengthCastIterable":{"CastIterable":["1","2"],"_CastIterableBase":["1","2"],"EfficientLengthIterable":["2"],"Iterable":["2"],"Iterable.E":"2"},"_CastListBase":{"ListBase":["2"],"List":["2"],"_CastIterableBase":["1","2"],"EfficientLengthIterable":["2"],"Iterable":["2"]},"CastList":{"_CastListBase":["1","2"],"ListBase":["2"],"List":["2"],"_CastIterableBase":["1","2"],"EfficientLengthIterable":["2"],"Iterable":["2"],"ListBase.E":"2","Iterable.E":"2"},"LateError":{"Error":[]},"CodeUnits":{"ListBase":["int"],"UnmodifiableListMixin":["int"],"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"],"ListBase.E":"int","UnmodifiableListMixin.E":"int"},"EfficientLengthIterable":{"Iterable":["1"]},"ListIterable":{"EfficientLengthIterable":["1"],"Iterable":["1"]},"SubListIterable":{"ListIterable":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"],"ListIterable.E":"1","Iterable.E":"1"},"ListIterator":{"Iterator":["1"]},"MappedIterable":{"Iterable":["2"],"Iterable.E":"2"},"EfficientLengthMappedIterable":{"MappedIterable":["1","2"],"EfficientLengthIterable":["2"],"Iterable":["2"],"Iterable.E":"2"},"MappedIterator":{"Iterator":["2"]},"MappedListIterable":{"ListIterable":["2"],"EfficientLengthIterable":["2"],"Iterable":["2"],"ListIterable.E":"2","Iterable.E":"2"},"WhereIterable":{"Iterable":["1"],"Iterable.E":"1"},"WhereIterator":{"Iterator":["1"]},"ExpandIterable":{"Iterable":["2"],"Iterable.E":"2"},"ExpandIterator":{"Iterator":["2"]},"TakeIterable":{"Iterable":["1"],"Iterable.E":"1"},"EfficientLengthTakeIterable":{"TakeIterable":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"],"Iterable.E":"1"},"TakeIterator":{"Iterator":["1"]},"SkipIterable":{"Iterable":["1"],"Iterable.E":"1"},"EfficientLengthSkipIterable":{"SkipIterable":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"],"Iterable.E":"1"},"SkipIterator":{"Iterator":["1"]},"SkipWhileIterable":{"Iterable":["1"],"Iterable.E":"1"},"SkipWhileIterator":{"Iterator":["1"]},"EmptyIterable":{"EfficientLengthIterable":["1"],"Iterable":["1"],"Iterable.E":"1"},"EmptyIterator":{"Iterator":["1"]},"WhereTypeIterable":{"Iterable":["1"],"Iterable.E":"1"},"WhereTypeIterator":{"Iterator":["1"]},"UnmodifiableListBase":{"ListBase":["1"],"UnmodifiableListMixin":["1"],"List":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"]},"ReversedListIterable":{"ListIterable":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"],"ListIterable.E":"1","Iterable.E":"1"},"Symbol":{"Symbol0":[]},"ConstantMapView":{"UnmodifiableMapView":["1","2"],"_UnmodifiableMapView_MapView__UnmodifiableMapMixin":["1","2"],"MapView":["1","2"],"_UnmodifiableMapMixin":["1","2"],"Map":["1","2"]},"ConstantMap":{"Map":["1","2"]},"ConstantStringMap":{"ConstantMap":["1","2"],"Map":["1","2"]},"_KeysOrValues":{"Iterable":["1"],"Iterable.E":"1"},"_KeysOrValuesOrElementsIterator":{"Iterator":["1"]},"Instantiation":{"Closure":[],"Function":[]},"Instantiation1":{"Closure":[],"Function":[]},"JSInvocationMirror":{"Invocation":[]},"NullError":{"TypeError":[],"Error":[]},"JsNoSuchMethodError":{"Error":[]},"UnknownJsTypeError":{"Error":[]},"NullThrownFromJavaScriptException":{"Exception":[]},"_StackTrace":{"StackTrace":[]},"Closure":{"Function":[]},"Closure0Args":{"Closure":[],"Function":[]},"Closure2Args":{"Closure":[],"Function":[]},"TearOffClosure":{"Closure":[],"Function":[]},"StaticClosure":{"Closure":[],"Function":[]},"BoundClosure":{"Closure":[],"Function":[]},"_CyclicInitializationError":{"Error":[]},"RuntimeError":{"Error":[]},"_AssertionError":{"Error":[]},"JsLinkedHashMap":{"MapBase":["1","2"],"LinkedHashMap":["1","2"],"Map":["1","2"],"MapBase.K":"1","MapBase.V":"2"},"LinkedHashMapKeyIterable":{"EfficientLengthIterable":["1"],"Iterable":["1"],"Iterable.E":"1"},"LinkedHashMapKeyIterator":{"Iterator":["1"]},"JSSyntaxRegExp":{"RegExp":[],"Pattern":[]},"_MatchImplementation":{"RegExpMatch":[],"Match":[]},"_AllMatchesIterable":{"Iterable":["RegExpMatch"],"Iterable.E":"RegExpMatch"},"_AllMatchesIterator":{"Iterator":["RegExpMatch"]},"StringMatch":{"Match":[]},"_StringAllMatchesIterable":{"Iterable":["Match"],"Iterable.E":"Match"},"_StringAllMatchesIterator":{"Iterator":["Match"]},"NativeByteBuffer":{"JavaScriptObject":[],"ByteBuffer":[],"TrustedGetRuntimeType":[]},"NativeTypedData":{"JavaScriptObject":[]},"NativeByteData":{"JavaScriptObject":[],"ByteData":[],"TrustedGetRuntimeType":[]},"NativeTypedArray":{"JavaScriptIndexingBehavior":["1"],"JavaScriptObject":[]},"NativeTypedArrayOfDouble":{"ListBase":["double"],"List":["double"],"JavaScriptIndexingBehavior":["double"],"JavaScriptObject":[],"EfficientLengthIterable":["double"],"Iterable":["double"],"FixedLengthListMixin":["double"]},"NativeTypedArrayOfInt":{"ListBase":["int"],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"]},"NativeFloat32List":{"ListBase":["double"],"Float32List":[],"List":["double"],"JavaScriptIndexingBehavior":["double"],"JavaScriptObject":[],"EfficientLengthIterable":["double"],"Iterable":["double"],"FixedLengthListMixin":["double"],"TrustedGetRuntimeType":[],"ListBase.E":"double"},"NativeFloat64List":{"ListBase":["double"],"Float64List":[],"List":["double"],"JavaScriptIndexingBehavior":["double"],"JavaScriptObject":[],"EfficientLengthIterable":["double"],"Iterable":["double"],"FixedLengthListMixin":["double"],"TrustedGetRuntimeType":[],"ListBase.E":"double"},"NativeInt16List":{"ListBase":["int"],"Int16List":[],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeInt32List":{"ListBase":["int"],"Int32List":[],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeInt8List":{"ListBase":["int"],"Int8List":[],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeUint16List":{"ListBase":["int"],"Uint16List":[],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeUint32List":{"ListBase":["int"],"Uint32List":[],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeUint8ClampedList":{"ListBase":["int"],"Uint8ClampedList":[],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"NativeUint8List":{"ListBase":["int"],"Uint8List":[],"List":["int"],"JavaScriptIndexingBehavior":["int"],"JavaScriptObject":[],"EfficientLengthIterable":["int"],"Iterable":["int"],"FixedLengthListMixin":["int"],"TrustedGetRuntimeType":[],"ListBase.E":"int"},"_Error":{"Error":[]},"_TypeError":{"TypeError":[],"Error":[]},"AsyncError":{"Error":[]},"_Future":{"Future":["1"]},"_TimerImpl":{"Timer":[]},"_Completer":{"Completer":["1"]},"_AsyncCompleter":{"_Completer":["1"],"Completer":["1"]},"_SyncCompleter":{"_Completer":["1"],"Completer":["1"]},"_StreamController":{"StreamController":["1"],"StreamSink":["1"],"StreamConsumer":["1"],"_StreamControllerLifecycle":["1"],"_EventDispatch":["1"]},"_SyncStreamController":{"_SyncStreamControllerDispatch":["1"],"_StreamController":["1"],"StreamController":["1"],"StreamSink":["1"],"StreamConsumer":["1"],"_StreamControllerLifecycle":["1"],"_EventDispatch":["1"]},"_ControllerStream":{"_StreamImpl":["1"],"Stream":["1"],"Stream.T":"1"},"_ControllerSubscription":{"_BufferingStreamSubscription":["1"],"StreamSubscription":["1"],"_EventDispatch":["1"],"_BufferingStreamSubscription.T":"1"},"_StreamSinkWrapper":{"StreamSink":["1"],"StreamConsumer":["1"]},"_BufferingStreamSubscription":{"StreamSubscription":["1"],"_EventDispatch":["1"],"_BufferingStreamSubscription.T":"1"},"_StreamImpl":{"Stream":["1"]},"_DelayedData":{"_DelayedEvent":["1"]},"_DelayedError":{"_DelayedEvent":["@"]},"_DelayedDone":{"_DelayedEvent":["@"]},"_DoneStreamSubscription":{"StreamSubscription":["1"]},"_EmptyStream":{"Stream":["1"],"Stream.T":"1"},"_ZoneSpecification":{"ZoneSpecification":[]},"_ZoneDelegate":{"ZoneDelegate":[]},"_Zone":{"Zone":[]},"_CustomZone":{"_Zone":[],"Zone":[]},"_RootZone":{"_Zone":[],"Zone":[]},"_HashMap":{"MapBase":["1","2"],"Map":["1","2"],"MapBase.K":"1","MapBase.V":"2"},"_IdentityHashMap":{"_HashMap":["1","2"],"MapBase":["1","2"],"Map":["1","2"],"MapBase.K":"1","MapBase.V":"2"},"_HashMapKeyIterable":{"EfficientLengthIterable":["1"],"Iterable":["1"],"Iterable.E":"1"},"_HashMapKeyIterator":{"Iterator":["1"]},"_LinkedHashSet":{"SetBase":["1"],"Set":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"]},"_LinkedHashSetIterator":{"Iterator":["1"]},"ListBase":{"List":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"]},"MapBase":{"Map":["1","2"]},"MapView":{"Map":["1","2"]},"UnmodifiableMapView":{"_UnmodifiableMapView_MapView__UnmodifiableMapMixin":["1","2"],"MapView":["1","2"],"_UnmodifiableMapMixin":["1","2"],"Map":["1","2"]},"SetBase":{"Set":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"]},"_SetBase":{"SetBase":["1"],"Set":["1"],"EfficientLengthIterable":["1"],"Iterable":["1"]},"_JsonMap":{"MapBase":["String","@"],"Map":["String","@"],"MapBase.K":"String","MapBase.V":"@"},"_JsonMapKeyIterable":{"ListIterable":["String"],"EfficientLengthIterable":["String"],"Iterable":["String"],"ListIterable.E":"String","Iterable.E":"String"},"AsciiCodec":{"Codec":["String","List<int>"]},"_UnicodeSubsetEncoder":{"Converter":["String","List<int>"],"StreamTransformer":["String","List<int>"]},"AsciiEncoder":{"Converter":["String","List<int>"],"StreamTransformer":["String","List<int>"]},"Base64Codec":{"Codec":["List<int>","String"]},"Base64Encoder":{"Converter":["List<int>","String"],"StreamTransformer":["List<int>","String"]},"_FusedCodec":{"Codec":["1","3"]},"Converter":{"StreamTransformer":["1","2"]},"Encoding":{"Codec":["String","List<int>"]},"JsonUnsupportedObjectError":{"Error":[]},"JsonCyclicError":{"Error":[]},"JsonCodec":{"Codec":["Object?","String"]},"JsonEncoder":{"Converter":["Object?","String"],"StreamTransformer":["Object?","String"]},"JsonDecoder":{"Converter":["String","Object?"],"StreamTransformer":["String","Object?"]},"Utf8Codec":{"Codec":["String","List<int>"]},"Utf8Encoder":{"Converter":["String","List<int>"],"StreamTransformer":["String","List<int>"]},"Utf8Decoder":{"Converter":["List<int>","String"],"StreamTransformer":["List<int>","String"]},"double":{"num":[]},"int":{"num":[]},"List":{"EfficientLengthIterable":["1"],"Iterable":["1"]},"RegExpMatch":{"Match":[]},"String":{"Pattern":[]},"AssertionError":{"Error":[]},"TypeError":{"Error":[]},"ArgumentError":{"Error":[]},"RangeError":{"Error":[]},"IndexError":{"Error":[]},"NoSuchMethodError":{"Error":[]},"UnsupportedError":{"Error":[]},"UnimplementedError":{"Error":[]},"StateError":{"Error":[]},"ConcurrentModificationError":{"Error":[]},"OutOfMemoryError":{"Error":[]},"StackOverflowError":{"Error":[]},"_Exception":{"Exception":[]},"FormatException":{"Exception":[]},"_StringStackTrace":{"StackTrace":[]},"StringBuffer":{"StringSink":[]},"_Uri":{"Uri":[]},"_SimpleUri":{"Uri":[]},"_DataUri":{"Uri":[]},"CssRule":{"JavaScriptObject":[]},"File":{"JavaScriptObject":[]},"Gamepad":{"JavaScriptObject":[]},"MimeType":{"JavaScriptObject":[]},"Node":{"JavaScriptObject":[]},"Plugin":{"JavaScriptObject":[]},"SourceBuffer":{"JavaScriptObject":[]},"SpeechGrammar":{"JavaScriptObject":[]},"SpeechRecognitionResult":{"JavaScriptObject":[]},"StyleSheet":{"JavaScriptObject":[]},"TextTrack":{"JavaScriptObject":[]},"TextTrackCue":{"JavaScriptObject":[]},"Touch":{"JavaScriptObject":[]},"HtmlElement":{"Node":[],"JavaScriptObject":[]},"AccessibleNodeList":{"JavaScriptObject":[]},"AnchorElement":{"Node":[],"JavaScriptObject":[]},"AreaElement":{"Node":[],"JavaScriptObject":[]},"Blob":{"JavaScriptObject":[]},"CharacterData":{"Node":[],"JavaScriptObject":[]},"CssPerspective":{"JavaScriptObject":[]},"CssStyleDeclaration":{"JavaScriptObject":[]},"CssStyleValue":{"JavaScriptObject":[]},"CssTransformComponent":{"JavaScriptObject":[]},"CssTransformValue":{"JavaScriptObject":[]},"CssUnparsedValue":{"JavaScriptObject":[]},"DataTransferItemList":{"JavaScriptObject":[]},"DomException":{"JavaScriptObject":[]},"DomRectList":{"ListBase":["Rectangle<num>"],"ImmutableListMixin":["Rectangle<num>"],"List":["Rectangle<num>"],"JavaScriptIndexingBehavior":["Rectangle<num>"],"JavaScriptObject":[],"EfficientLengthIterable":["Rectangle<num>"],"Iterable":["Rectangle<num>"],"ImmutableListMixin.E":"Rectangle<num>","ListBase.E":"Rectangle<num>"},"DomRectReadOnly":{"JavaScriptObject":[],"Rectangle":["num"]},"DomStringList":{"ListBase":["String"],"ImmutableListMixin":["String"],"List":["String"],"JavaScriptIndexingBehavior":["String"],"JavaScriptObject":[],"EfficientLengthIterable":["String"],"Iterable":["String"],"ImmutableListMixin.E":"String","ListBase.E":"String"},"DomTokenList":{"JavaScriptObject":[]},"Element":{"Node":[],"JavaScriptObject":[]},"EventTarget":{"JavaScriptObject":[]},"FileList":{"ListBase":["File"],"ImmutableListMixin":["File"],"List":["File"],"JavaScriptIndexingBehavior":["File"],"JavaScriptObject":[],"EfficientLengthIterable":["File"],"Iterable":["File"],"ImmutableListMixin.E":"File","ListBase.E":"File"},"FileWriter":{"JavaScriptObject":[]},"FormElement":{"Node":[],"JavaScriptObject":[]},"History":{"JavaScriptObject":[]},"HtmlCollection":{"ListBase":["Node"],"ImmutableListMixin":["Node"],"List":["Node"],"JavaScriptIndexingBehavior":["Node"],"JavaScriptObject":[],"EfficientLengthIterable":["Node"],"Iterable":["Node"],"ImmutableListMixin.E":"Node","ListBase.E":"Node"},"Location":{"JavaScriptObject":[]},"MediaList":{"JavaScriptObject":[]},"MidiInputMap":{"JavaScriptObject":[],"MapBase":["String","@"],"Map":["String","@"],"MapBase.K":"String","MapBase.V":"@"},"MidiOutputMap":{"JavaScriptObject":[],"MapBase":["String","@"],"Map":["String","@"],"MapBase.K":"String","MapBase.V":"@"},"MimeTypeArray":{"ListBase":["MimeType"],"ImmutableListMixin":["MimeType"],"List":["MimeType"],"JavaScriptIndexingBehavior":["MimeType"],"JavaScriptObject":[],"EfficientLengthIterable":["MimeType"],"Iterable":["MimeType"],"ImmutableListMixin.E":"MimeType","ListBase.E":"MimeType"},"NodeList":{"ListBase":["Node"],"ImmutableListMixin":["Node"],"List":["Node"],"JavaScriptIndexingBehavior":["Node"],"JavaScriptObject":[],"EfficientLengthIterable":["Node"],"Iterable":["Node"],"ImmutableListMixin.E":"Node","ListBase.E":"Node"},"PluginArray":{"ListBase":["Plugin"],"ImmutableListMixin":["Plugin"],"List":["Plugin"],"JavaScriptIndexingBehavior":["Plugin"],"JavaScriptObject":[],"EfficientLengthIterable":["Plugin"],"Iterable":["Plugin"],"ImmutableListMixin.E":"Plugin","ListBase.E":"Plugin"},"RtcStatsReport":{"JavaScriptObject":[],"MapBase":["String","@"],"Map":["String","@"],"MapBase.K":"String","MapBase.V":"@"},"SelectElement":{"Node":[],"JavaScriptObject":[]},"SourceBufferList":{"ListBase":["SourceBuffer"],"ImmutableListMixin":["SourceBuffer"],"List":["SourceBuffer"],"JavaScriptIndexingBehavior":["SourceBuffer"],"JavaScriptObject":[],"EfficientLengthIterable":["SourceBuffer"],"Iterable":["SourceBuffer"],"ImmutableListMixin.E":"SourceBuffer","ListBase.E":"SourceBuffer"},"SpeechGrammarList":{"ListBase":["SpeechGrammar"],"ImmutableListMixin":["SpeechGrammar"],"List":["SpeechGrammar"],"JavaScriptIndexingBehavior":["SpeechGrammar"],"JavaScriptObject":[],"EfficientLengthIterable":["SpeechGrammar"],"Iterable":["SpeechGrammar"],"ImmutableListMixin.E":"SpeechGrammar","ListBase.E":"SpeechGrammar"},"Storage":{"JavaScriptObject":[],"MapBase":["String","String"],"Map":["String","String"],"MapBase.K":"String","MapBase.V":"String"},"TextTrackCueList":{"ListBase":["TextTrackCue"],"ImmutableListMixin":["TextTrackCue"],"List":["TextTrackCue"],"JavaScriptIndexingBehavior":["TextTrackCue"],"JavaScriptObject":[],"EfficientLengthIterable":["TextTrackCue"],"Iterable":["TextTrackCue"],"ImmutableListMixin.E":"TextTrackCue","ListBase.E":"TextTrackCue"},"TextTrackList":{"ListBase":["TextTrack"],"ImmutableListMixin":["TextTrack"],"List":["TextTrack"],"JavaScriptIndexingBehavior":["TextTrack"],"JavaScriptObject":[],"EfficientLengthIterable":["TextTrack"],"Iterable":["TextTrack"],"ImmutableListMixin.E":"TextTrack","ListBase.E":"TextTrack"},"TimeRanges":{"JavaScriptObject":[]},"TouchList":{"ListBase":["Touch"],"ImmutableListMixin":["Touch"],"List":["Touch"],"JavaScriptIndexingBehavior":["Touch"],"JavaScriptObject":[],"EfficientLengthIterable":["Touch"],"Iterable":["Touch"],"ImmutableListMixin.E":"Touch","ListBase.E":"Touch"},"TrackDefaultList":{"JavaScriptObject":[]},"Url":{"JavaScriptObject":[]},"VideoTrackList":{"JavaScriptObject":[]},"_CssRuleList":{"ListBase":["CssRule"],"ImmutableListMixin":["CssRule"],"List":["CssRule"],"JavaScriptIndexingBehavior":["CssRule"],"JavaScriptObject":[],"EfficientLengthIterable":["CssRule"],"Iterable":["CssRule"],"ImmutableListMixin.E":"CssRule","ListBase.E":"CssRule"},"_DomRect":{"JavaScriptObject":[],"Rectangle":["num"]},"_GamepadList":{"ListBase":["Gamepad?"],"ImmutableListMixin":["Gamepad?"],"List":["Gamepad?"],"JavaScriptIndexingBehavior":["Gamepad?"],"JavaScriptObject":[],"EfficientLengthIterable":["Gamepad?"],"Iterable":["Gamepad?"],"ImmutableListMixin.E":"Gamepad?","ListBase.E":"Gamepad?"},"_NamedNodeMap":{"ListBase":["Node"],"ImmutableListMixin":["Node"],"List":["Node"],"JavaScriptIndexingBehavior":["Node"],"JavaScriptObject":[],"EfficientLengthIterable":["Node"],"Iterable":["Node"],"ImmutableListMixin.E":"Node","ListBase.E":"Node"},"_SpeechRecognitionResultList":{"ListBase":["SpeechRecognitionResult"],"ImmutableListMixin":["SpeechRecognitionResult"],"List":["SpeechRecognitionResult"],"JavaScriptIndexingBehavior":["SpeechRecognitionResult"],"JavaScriptObject":[],"EfficientLengthIterable":["SpeechRecognitionResult"],"Iterable":["SpeechRecognitionResult"],"ImmutableListMixin.E":"SpeechRecognitionResult","ListBase.E":"SpeechRecognitionResult"},"_StyleSheetList":{"ListBase":["StyleSheet"],"ImmutableListMixin":["StyleSheet"],"List":["StyleSheet"],"JavaScriptIndexingBehavior":["StyleSheet"],"JavaScriptObject":[],"EfficientLengthIterable":["StyleSheet"],"Iterable":["StyleSheet"],"ImmutableListMixin.E":"StyleSheet","ListBase.E":"StyleSheet"},"FixedSizeListIterator":{"Iterator":["1"]},"NullRejectionException":{"Exception":[]},"Length":{"JavaScriptObject":[]},"Number":{"JavaScriptObject":[]},"Transform":{"JavaScriptObject":[]},"LengthList":{"ListBase":["Length"],"ImmutableListMixin":["Length"],"List":["Length"],"JavaScriptObject":[],"EfficientLengthIterable":["Length"],"Iterable":["Length"],"ImmutableListMixin.E":"Length","ListBase.E":"Length"},"NumberList":{"ListBase":["Number"],"ImmutableListMixin":["Number"],"List":["Number"],"JavaScriptObject":[],"EfficientLengthIterable":["Number"],"Iterable":["Number"],"ImmutableListMixin.E":"Number","ListBase.E":"Number"},"PointList":{"JavaScriptObject":[]},"StringList":{"ListBase":["String"],"ImmutableListMixin":["String"],"List":["String"],"JavaScriptObject":[],"EfficientLengthIterable":["String"],"Iterable":["String"],"ImmutableListMixin.E":"String","ListBase.E":"String"},"TransformList":{"ListBase":["Transform"],"ImmutableListMixin":["Transform"],"List":["Transform"],"JavaScriptObject":[],"EfficientLengthIterable":["Transform"],"Iterable":["Transform"],"ImmutableListMixin.E":"Transform","ListBase.E":"Transform"},"AudioBuffer":{"JavaScriptObject":[]},"AudioParamMap":{"JavaScriptObject":[],"MapBase":["String","@"],"Map":["String","@"],"MapBase.K":"String","MapBase.V":"@"},"AudioTrackList":{"JavaScriptObject":[]},"BaseAudioContext":{"JavaScriptObject":[]},"OfflineAudioContext":{"JavaScriptObject":[]},"NullStreamSink":{"StreamSink":["1"],"StreamConsumer":["1"]},"PathException":{"Exception":[]},"PosixStyle":{"InternalStyle":[]},"UrlStyle":{"InternalStyle":[]},"WindowsStyle":{"InternalStyle":[]},"Chain":{"StackTrace":[]},"LazyTrace":{"Trace":[],"StackTrace":[]},"Trace":{"StackTrace":[]},"UnparsedFrame":{"Frame":[]},"GuaranteeChannel":{"StreamChannelMixin":["1"],"StreamChannel":["1"]},"_GuaranteeSink":{"StreamSink":["1"],"StreamConsumer":["1"]},"_MultiChannel":{"StreamChannelMixin":["1"],"MultiChannel":["1"],"StreamChannel":["1"]},"VirtualChannel":{"StreamChannelMixin":["1"],"MultiChannel":["1"],"StreamChannel":["1"]},"StreamChannelMixin":{"StreamChannel":["1"]},"Int8List":{"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"]},"Uint8List":{"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"]},"Uint8ClampedList":{"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"]},"Int16List":{"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"]},"Uint16List":{"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"]},"Int32List":{"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"]},"Uint32List":{"List":["int"],"EfficientLengthIterable":["int"],"Iterable":["int"]},"Float32List":{"List":["double"],"EfficientLengthIterable":["double"],"Iterable":["double"]},"Float64List":{"List":["double"],"EfficientLengthIterable":["double"],"Iterable":["double"]}}')); + A._Universe_addErasedTypes(init.typeUniverse, JSON.parse('{"UnmodifiableListBase":1,"__CastListBase__CastIterableBase_ListMixin":2,"NativeTypedArray":1,"_DelayedEvent":1,"_SetBase":1}')); + var string$ = { + x27_has_: "' has been assigned during initialization.", + x3d_____: "===== asynchronous gap ===========================\n", + Cannotff: "Cannot extract a file path from a URI with a fragment component", + Cannotfq: "Cannot extract a file path from a URI with a query component", + Cannotn: "Cannot extract a non-Windows file path from a file URI with an authority", + Error_: "Error handler must accept one Object or one Object and a StackTrace as arguments, and return a value of the returned future's type", + handle: "handleError callback must take either an Object (the error), or both an Object (the error) and a StackTrace." + }; + var type$ = (function rtii() { + var findType = A.findType; + return { + AsyncError: findType("AsyncError"), + ByteBuffer: findType("ByteBuffer"), + ByteData: findType("ByteData"), + ConstantMapView_Symbol_dynamic: findType("ConstantMapView<Symbol0,@>"), + CssRule: findType("CssRule"), + Duration: findType("Duration"), + EfficientLengthIterable_dynamic: findType("EfficientLengthIterable<@>"), + Error: findType("Error"), + Exception: findType("Exception"), + File: findType("File"), + Float32List: findType("Float32List"), + Float64List: findType("Float64List"), + Frame: findType("Frame"), + Frame_Function_Frame: findType("Frame(Frame)"), + Frame_Function_String: findType("Frame(String)"), + Function: findType("Function"), + Future_dynamic: findType("Future<@>"), + Int16List: findType("Int16List"), + Int32List: findType("Int32List"), + Int8List: findType("Int8List"), + Invocation: findType("Invocation"), + Iterable_String: findType("Iterable<String>"), + Iterable_dynamic: findType("Iterable<@>"), + Iterable_nullable_Object: findType("Iterable<Object?>"), + JSArray_Frame: findType("JSArray<Frame>"), + JSArray_Object: findType("JSArray<Object>"), + JSArray_String: findType("JSArray<String>"), + JSArray_Trace: findType("JSArray<Trace>"), + JSArray_Uint8List: findType("JSArray<Uint8List>"), + JSArray_dynamic: findType("JSArray<@>"), + JSArray_int: findType("JSArray<int>"), + JSArray_nullable_String: findType("JSArray<String?>"), + JSNull: findType("JSNull"), + JavaScriptFunction: findType("JavaScriptFunction"), + JavaScriptIndexingBehavior_dynamic: findType("JavaScriptIndexingBehavior<@>"), + JavaScriptObject: findType("JavaScriptObject"), + JsLinkedHashMap_Symbol_dynamic: findType("JsLinkedHashMap<Symbol0,@>"), + Length: findType("Length"), + List_String: findType("List<String>"), + List_dynamic: findType("List<@>"), + List_int: findType("List<int>"), + Map_String_String: findType("Map<String,String>"), + Map_dynamic_dynamic: findType("Map<@,@>"), + Map_of_nullable_Object_and_nullable_Object: findType("Map<Object?,Object?>"), + MappedIterable_String_Frame: findType("MappedIterable<String,Frame>"), + MappedListIterable_Frame_Frame: findType("MappedListIterable<Frame,Frame>"), + MappedListIterable_String_Trace: findType("MappedListIterable<String,Trace>"), + MappedListIterable_String_dynamic: findType("MappedListIterable<String,@>"), + MimeType: findType("MimeType"), + NativeUint8List: findType("NativeUint8List"), + Node: findType("Node"), + Null: findType("Null"), + Number: findType("Number"), + Object: findType("Object"), + Plugin: findType("Plugin"), + Record: findType("Record"), + Rectangle_num: findType("Rectangle<num>"), + RegExpMatch: findType("RegExpMatch"), + SourceBuffer: findType("SourceBuffer"), + SpeechGrammar: findType("SpeechGrammar"), + SpeechRecognitionResult: findType("SpeechRecognitionResult"), + StackTrace: findType("StackTrace"), + String: findType("String"), + StyleSheet: findType("StyleSheet"), + Symbol: findType("Symbol0"), + TextTrack: findType("TextTrack"), + TextTrackCue: findType("TextTrackCue"), + Timer: findType("Timer"), + Touch: findType("Touch"), + Trace: findType("Trace"), + Trace_Function_String: findType("Trace(String)"), + Transform: findType("Transform"), + TrustedGetRuntimeType: findType("TrustedGetRuntimeType"), + TypeError: findType("TypeError"), + Uint16List: findType("Uint16List"), + Uint32List: findType("Uint32List"), + Uint8ClampedList: findType("Uint8ClampedList"), + Uint8List: findType("Uint8List"), + UnknownJavaScriptObject: findType("UnknownJavaScriptObject"), + UnmodifiableMapView_String_String: findType("UnmodifiableMapView<String,String>"), + Uri: findType("Uri"), + WhereIterable_String: findType("WhereIterable<String>"), + WhereTypeIterable_String: findType("WhereTypeIterable<String>"), + Zone: findType("Zone"), + _AsyncCompleter_dynamic: findType("_AsyncCompleter<@>"), + _Future_dynamic: findType("_Future<@>"), + _Future_int: findType("_Future<int>"), + _Future_void: findType("_Future<~>"), + _IdentityHashMap_of_nullable_Object_and_nullable_Object: findType("_IdentityHashMap<Object?,Object?>"), + _StreamControllerAddStreamState_nullable_Object: findType("_StreamControllerAddStreamState<Object?>"), + _SyncCompleter_dynamic: findType("_SyncCompleter<@>"), + _ZoneFunction_of_void_Function_Zone_ZoneDelegate_Zone_Object_StackTrace: findType("_ZoneFunction<~(Zone,ZoneDelegate,Zone,Object,StackTrace)>"), + bool: findType("bool"), + bool_Function_Frame: findType("bool(Frame)"), + bool_Function_Object: findType("bool(Object)"), + bool_Function_String: findType("bool(String)"), + double: findType("double"), + dynamic: findType("@"), + dynamic_Function: findType("@()"), + dynamic_Function_Object: findType("@(Object)"), + dynamic_Function_Object_StackTrace: findType("@(Object,StackTrace)"), + dynamic_Function_String: findType("@(String)"), + int: findType("int"), + legacy_Never: findType("0&*"), + legacy_Object: findType("Object*"), + nullable_Future_Null: findType("Future<Null>?"), + nullable_Gamepad: findType("Gamepad?"), + nullable_JavaScriptObject: findType("JavaScriptObject?"), + nullable_List_dynamic: findType("List<@>?"), + nullable_Map_of_nullable_Object_and_nullable_Object: findType("Map<Object?,Object?>?"), + nullable_Object: findType("Object?"), + nullable_StackTrace: findType("StackTrace?"), + nullable_Zone: findType("Zone?"), + nullable_ZoneDelegate: findType("ZoneDelegate?"), + nullable_ZoneSpecification: findType("ZoneSpecification?"), + nullable__DelayedEvent_dynamic: findType("_DelayedEvent<@>?"), + nullable__FutureListener_dynamic_dynamic: findType("_FutureListener<@,@>?"), + nullable__LinkedHashSetCell: findType("_LinkedHashSetCell?"), + nullable_void_Function: findType("~()?"), + num: findType("num"), + void: findType("~"), + void_Function: findType("~()"), + void_Function_$opt_dynamic: findType("~([@])"), + void_Function_JavaScriptObject: findType("~(JavaScriptObject)"), + void_Function_Object: findType("~(Object)"), + void_Function_Object_StackTrace: findType("~(Object,StackTrace)"), + void_Function_String_String: findType("~(String,String)"), + void_Function_String_dynamic: findType("~(String,@)"), + void_Function_Timer: findType("~(Timer)") + }; + })(); + (function constants() { + var makeConstList = hunkHelpers.makeConstList; + B.Interceptor_methods = J.Interceptor.prototype; + B.JSArray_methods = J.JSArray.prototype; + B.JSInt_methods = J.JSInt.prototype; + B.JSNumber_methods = J.JSNumber.prototype; + B.JSString_methods = J.JSString.prototype; + B.JavaScriptFunction_methods = J.JavaScriptFunction.prototype; + B.JavaScriptObject_methods = J.JavaScriptObject.prototype; + B.NativeUint8List_methods = A.NativeUint8List.prototype; + B.PlainJavaScriptObject_methods = J.PlainJavaScriptObject.prototype; + B.UnknownJavaScriptObject_methods = J.UnknownJavaScriptObject.prototype; + B.AsciiEncoder_127 = new A.AsciiEncoder(127); + B.CONSTANT = new A.Instantiation1(A.math__max$closure(), A.findType("Instantiation1<int>")); + B.C_AsciiCodec = new A.AsciiCodec(); + B.C_Base64Encoder = new A.Base64Encoder(); + B.C_Base64Codec = new A.Base64Codec(); + B.C_EmptyIterator = new A.EmptyIterator(A.findType("EmptyIterator<0&>")); + B.C_JS_CONST = function getTagFallback(o) { + var s = Object.prototype.toString.call(o); + return s.substring(8, s.length - 1); +}; + B.C_JS_CONST0 = function() { + var toStringFunction = Object.prototype.toString; + function getTag(o) { + var s = toStringFunction.call(o); + return s.substring(8, s.length - 1); + } + function getUnknownTag(object, tag) { + if (/^HTML[A-Z].*Element$/.test(tag)) { + var name = toStringFunction.call(object); + if (name == "[object Object]") return null; + return "HTMLElement"; + } + } + function getUnknownTagGenericBrowser(object, tag) { + if (self.HTMLElement && object instanceof HTMLElement) return "HTMLElement"; + return getUnknownTag(object, tag); + } + function prototypeForTag(tag) { + if (typeof window == "undefined") return null; + if (typeof window[tag] == "undefined") return null; + var constructor = window[tag]; + if (typeof constructor != "function") return null; + return constructor.prototype; + } + function discriminator(tag) { return null; } + var isBrowser = typeof navigator == "object"; + return { + getTag: getTag, + getUnknownTag: isBrowser ? getUnknownTagGenericBrowser : getUnknownTag, + prototypeForTag: prototypeForTag, + discriminator: discriminator }; +}; + B.C_JS_CONST6 = function(getTagFallback) { + return function(hooks) { + if (typeof navigator != "object") return hooks; + var ua = navigator.userAgent; + if (ua.indexOf("DumpRenderTree") >= 0) return hooks; + if (ua.indexOf("Chrome") >= 0) { + function confirm(p) { + return typeof window == "object" && window[p] && window[p].name == p; + } + if (confirm("Window") && confirm("HTMLElement")) return hooks; + } + hooks.getTag = getTagFallback; + }; +}; + B.C_JS_CONST1 = function(hooks) { + if (typeof dartExperimentalFixupGetTag != "function") return hooks; + hooks.getTag = dartExperimentalFixupGetTag(hooks.getTag); +}; + B.C_JS_CONST2 = function(hooks) { + var getTag = hooks.getTag; + var prototypeForTag = hooks.prototypeForTag; + function getTagFixed(o) { + var tag = getTag(o); + if (tag == "Document") { + if (!!o.xmlVersion) return "!Document"; + return "!HTMLDocument"; + } + return tag; + } + function prototypeForTagFixed(tag) { + if (tag == "Document") return null; + return prototypeForTag(tag); + } + hooks.getTag = getTagFixed; + hooks.prototypeForTag = prototypeForTagFixed; +}; + B.C_JS_CONST5 = function(hooks) { + var userAgent = typeof navigator == "object" ? navigator.userAgent : ""; + if (userAgent.indexOf("Firefox") == -1) return hooks; + var getTag = hooks.getTag; + var quickMap = { + "BeforeUnloadEvent": "Event", + "DataTransfer": "Clipboard", + "GeoGeolocation": "Geolocation", + "Location": "!Location", + "WorkerMessageEvent": "MessageEvent", + "XMLDocument": "!Document"}; + function getTagFirefox(o) { + var tag = getTag(o); + return quickMap[tag] || tag; + } + hooks.getTag = getTagFirefox; +}; + B.C_JS_CONST4 = function(hooks) { + var userAgent = typeof navigator == "object" ? navigator.userAgent : ""; + if (userAgent.indexOf("Trident/") == -1) return hooks; + var getTag = hooks.getTag; + var quickMap = { + "BeforeUnloadEvent": "Event", + "DataTransfer": "Clipboard", + "HTMLDDElement": "HTMLElement", + "HTMLDTElement": "HTMLElement", + "HTMLPhraseElement": "HTMLElement", + "Position": "Geoposition" + }; + function getTagIE(o) { + var tag = getTag(o); + var newTag = quickMap[tag]; + if (newTag) return newTag; + if (tag == "Object") { + if (window.DataView && (o instanceof window.DataView)) return "DataView"; + } + return tag; + } + function prototypeForTagIE(tag) { + var constructor = window[tag]; + if (constructor == null) return null; + return constructor.prototype; + } + hooks.getTag = getTagIE; + hooks.prototypeForTag = prototypeForTagIE; +}; + B.C_JS_CONST3 = function(hooks) { return hooks; } +; + B.C_JsonCodec = new A.JsonCodec(); + B.C_OutOfMemoryError = new A.OutOfMemoryError(); + B.C_SentinelValue = new A.SentinelValue(); + B.C_Utf8Codec = new A.Utf8Codec(); + B.C_Utf8Encoder = new A.Utf8Encoder(); + B.C__DelayedDone = new A._DelayedDone(); + B.C__Required = new A._Required(); + B.C__RootZone = new A._RootZone(); + B.Duration_0 = new A.Duration(0); + B.JsonDecoder_null = new A.JsonDecoder(null); + B.JsonEncoder_null = new A.JsonEncoder(null); + B.List_M1A = A._setArrayType(makeConstList([0, 0, 24576, 1023, 65534, 34815, 65534, 18431]), type$.JSArray_int); + B.List_MMm = A._setArrayType(makeConstList([0, 0, 26624, 1023, 65534, 2047, 65534, 2047]), type$.JSArray_int); + B.List_OL3 = A._setArrayType(makeConstList([0, 0, 32722, 12287, 65534, 34815, 65534, 18431]), type$.JSArray_int); + B.List_XRg0 = A._setArrayType(makeConstList([0, 0, 32722, 12287, 65535, 34815, 65534, 18431]), type$.JSArray_int); + B.List_XRg = A._setArrayType(makeConstList([0, 0, 65490, 12287, 65535, 34815, 65534, 18431]), type$.JSArray_int); + B.List_YmH = A._setArrayType(makeConstList([0, 0, 32776, 33792, 1, 10240, 0, 0]), type$.JSArray_int); + B.List_ejq = A._setArrayType(makeConstList([0, 0, 32754, 11263, 65534, 34815, 65534, 18431]), type$.JSArray_int); + B.List_empty = A._setArrayType(makeConstList([]), type$.JSArray_String); + B.List_empty0 = A._setArrayType(makeConstList([]), type$.JSArray_dynamic); + B.List_oFp = A._setArrayType(makeConstList([0, 0, 65490, 45055, 65535, 34815, 65534, 18431]), type$.JSArray_int); + B.List_yzX = A._setArrayType(makeConstList([0, 0, 27858, 1023, 65534, 51199, 65535, 32767]), type$.JSArray_int); + B.Object_empty = {}; + B.Map_empty = new A.ConstantStringMap(B.Object_empty, [], A.findType("ConstantStringMap<String,String>")); + B.Map_empty0 = new A.ConstantStringMap(B.Object_empty, [], A.findType("ConstantStringMap<Symbol0,@>")); + B.Symbol_call = new A.Symbol("call"); + B.Type_ByteBuffer_RkP = A.typeLiteral("ByteBuffer"); + B.Type_ByteData_zNC = A.typeLiteral("ByteData"); + B.Type_Float32List_LB7 = A.typeLiteral("Float32List"); + B.Type_Float64List_LB7 = A.typeLiteral("Float64List"); + B.Type_Int16List_uXf = A.typeLiteral("Int16List"); + B.Type_Int32List_O50 = A.typeLiteral("Int32List"); + B.Type_Int8List_ekJ = A.typeLiteral("Int8List"); + B.Type_Object_xQ6 = A.typeLiteral("Object"); + B.Type_Uint16List_2bx = A.typeLiteral("Uint16List"); + B.Type_Uint32List_2bx = A.typeLiteral("Uint32List"); + B.Type_Uint8ClampedList_Jik = A.typeLiteral("Uint8ClampedList"); + B.Type_Uint8List_WLA = A.typeLiteral("Uint8List"); + B.Utf8Decoder_false = new A.Utf8Decoder(false); + B._StringStackTrace_3uE = new A._StringStackTrace(""); + B._ZoneFunction_3bB = new A._ZoneFunction(B.C__RootZone, A.async___rootCreatePeriodicTimer$closure(), A.findType("_ZoneFunction<Timer(Zone,ZoneDelegate,Zone,Duration,~(Timer))>")); + B._ZoneFunction_7G2 = new A._ZoneFunction(B.C__RootZone, A.async___rootRegisterBinaryCallback$closure(), A.findType("_ZoneFunction<0^(1^,2^)(Zone,ZoneDelegate,Zone,0^(1^,2^))<Object?,Object?,Object?>>")); + B._ZoneFunction_Eeh = new A._ZoneFunction(B.C__RootZone, A.async___rootRegisterUnaryCallback$closure(), A.findType("_ZoneFunction<0^(1^)(Zone,ZoneDelegate,Zone,0^(1^))<Object?,Object?>>")); + B._ZoneFunction_NMc = new A._ZoneFunction(B.C__RootZone, A.async___rootHandleUncaughtError$closure(), type$._ZoneFunction_of_void_Function_Zone_ZoneDelegate_Zone_Object_StackTrace); + B._ZoneFunction__RootZone__rootCreateTimer = new A._ZoneFunction(B.C__RootZone, A.async___rootCreateTimer$closure(), A.findType("_ZoneFunction<Timer(Zone,ZoneDelegate,Zone,Duration,~())>")); + B._ZoneFunction__RootZone__rootErrorCallback = new A._ZoneFunction(B.C__RootZone, A.async___rootErrorCallback$closure(), A.findType("_ZoneFunction<AsyncError?(Zone,ZoneDelegate,Zone,Object,StackTrace?)>")); + B._ZoneFunction__RootZone__rootFork = new A._ZoneFunction(B.C__RootZone, A.async___rootFork$closure(), A.findType("_ZoneFunction<Zone(Zone,ZoneDelegate,Zone,ZoneSpecification?,Map<Object?,Object?>?)>")); + B._ZoneFunction__RootZone__rootPrint = new A._ZoneFunction(B.C__RootZone, A.async___rootPrint$closure(), A.findType("_ZoneFunction<~(Zone,ZoneDelegate,Zone,String)>")); + B._ZoneFunction__RootZone__rootRegisterCallback = new A._ZoneFunction(B.C__RootZone, A.async___rootRegisterCallback$closure(), A.findType("_ZoneFunction<0^()(Zone,ZoneDelegate,Zone,0^())<Object?>>")); + B._ZoneFunction__RootZone__rootRun = new A._ZoneFunction(B.C__RootZone, A.async___rootRun$closure(), A.findType("_ZoneFunction<0^(Zone,ZoneDelegate,Zone,0^())<Object?>>")); + B._ZoneFunction__RootZone__rootRunBinary = new A._ZoneFunction(B.C__RootZone, A.async___rootRunBinary$closure(), A.findType("_ZoneFunction<0^(Zone,ZoneDelegate,Zone,0^(1^,2^),1^,2^)<Object?,Object?,Object?>>")); + B._ZoneFunction__RootZone__rootRunUnary = new A._ZoneFunction(B.C__RootZone, A.async___rootRunUnary$closure(), A.findType("_ZoneFunction<0^(Zone,ZoneDelegate,Zone,0^(1^),1^)<Object?,Object?>>")); + B._ZoneFunction__RootZone__rootScheduleMicrotask = new A._ZoneFunction(B.C__RootZone, A.async___rootScheduleMicrotask$closure(), A.findType("_ZoneFunction<~(Zone,ZoneDelegate,Zone,~())>")); + })(); + (function staticFields() { + $._JS_INTEROP_INTERCEPTOR_TAG = null; + $.toStringVisiting = A._setArrayType([], type$.JSArray_Object); + $.Primitives__identityHashCodeProperty = null; + $.BoundClosure__receiverFieldNameCache = null; + $.BoundClosure__interceptorFieldNameCache = null; + $.getTagFunction = null; + $.alternateTagFunction = null; + $.prototypeForTagFunction = null; + $.dispatchRecordsForInstanceTags = null; + $.interceptorsForUncacheableTags = null; + $.initNativeDispatchFlag = null; + $._nextCallback = null; + $._lastCallback = null; + $._lastPriorityCallback = null; + $._isInCallbackLoop = false; + $.Zone__current = B.C__RootZone; + $._RootZone__rootDelegate = null; + $.Uri__cachedBaseString = ""; + $.Uri__cachedBaseUri = null; + $._currentUriBase = null; + $._current = null; + $._iframes = A.LinkedHashMap_LinkedHashMap$_empty(type$.int, type$.JavaScriptObject); + $._subscriptions = A.LinkedHashMap_LinkedHashMap$_empty(type$.int, A.findType("StreamSubscription<~>")); + $._domSubscriptions = A.LinkedHashMap_LinkedHashMap$_empty(type$.int, A.findType("Subscription")); + })(); + (function lazyInitializers() { + var _lazyFinal = hunkHelpers.lazyFinal; + _lazyFinal($, "DART_CLOSURE_PROPERTY_NAME", "$get$DART_CLOSURE_PROPERTY_NAME", () => A.getIsolateAffinityTag("_$dart_dartClosure")); + _lazyFinal($, "nullFuture", "$get$nullFuture", () => B.C__RootZone.run$1$1(new A.nullFuture_closure(), A.findType("Future<Null>"))); + _lazyFinal($, "TypeErrorDecoder_noSuchMethodPattern", "$get$TypeErrorDecoder_noSuchMethodPattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokeCallErrorOn({ + toString: function() { + return "$receiver$"; + } + }))); + _lazyFinal($, "TypeErrorDecoder_notClosurePattern", "$get$TypeErrorDecoder_notClosurePattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokeCallErrorOn({$method$: null, + toString: function() { + return "$receiver$"; + } + }))); + _lazyFinal($, "TypeErrorDecoder_nullCallPattern", "$get$TypeErrorDecoder_nullCallPattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokeCallErrorOn(null))); + _lazyFinal($, "TypeErrorDecoder_nullLiteralCallPattern", "$get$TypeErrorDecoder_nullLiteralCallPattern", () => A.TypeErrorDecoder_extractPattern(function() { + var $argumentsExpr$ = "$arguments$"; + try { + null.$method$($argumentsExpr$); + } catch (e) { + return e.message; + } + }())); + _lazyFinal($, "TypeErrorDecoder_undefinedCallPattern", "$get$TypeErrorDecoder_undefinedCallPattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokeCallErrorOn(void 0))); + _lazyFinal($, "TypeErrorDecoder_undefinedLiteralCallPattern", "$get$TypeErrorDecoder_undefinedLiteralCallPattern", () => A.TypeErrorDecoder_extractPattern(function() { + var $argumentsExpr$ = "$arguments$"; + try { + (void 0).$method$($argumentsExpr$); + } catch (e) { + return e.message; + } + }())); + _lazyFinal($, "TypeErrorDecoder_nullPropertyPattern", "$get$TypeErrorDecoder_nullPropertyPattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokePropertyErrorOn(null))); + _lazyFinal($, "TypeErrorDecoder_nullLiteralPropertyPattern", "$get$TypeErrorDecoder_nullLiteralPropertyPattern", () => A.TypeErrorDecoder_extractPattern(function() { + try { + null.$method$; + } catch (e) { + return e.message; + } + }())); + _lazyFinal($, "TypeErrorDecoder_undefinedPropertyPattern", "$get$TypeErrorDecoder_undefinedPropertyPattern", () => A.TypeErrorDecoder_extractPattern(A.TypeErrorDecoder_provokePropertyErrorOn(void 0))); + _lazyFinal($, "TypeErrorDecoder_undefinedLiteralPropertyPattern", "$get$TypeErrorDecoder_undefinedLiteralPropertyPattern", () => A.TypeErrorDecoder_extractPattern(function() { + try { + (void 0).$method$; + } catch (e) { + return e.message; + } + }())); + _lazyFinal($, "_AsyncRun__scheduleImmediateClosure", "$get$_AsyncRun__scheduleImmediateClosure", () => A._AsyncRun__initializeScheduleImmediate()); + _lazyFinal($, "Future__nullFuture", "$get$Future__nullFuture", () => A.findType("_Future<Null>")._as($.$get$nullFuture())); + _lazyFinal($, "_RootZone__rootMap", "$get$_RootZone__rootMap", () => { + var t1 = type$.dynamic; + return A.HashMap_HashMap(t1, t1); + }); + _lazyFinal($, "Utf8Decoder__decoder", "$get$Utf8Decoder__decoder", () => new A.Utf8Decoder__decoder_closure().call$0()); + _lazyFinal($, "Utf8Decoder__decoderNonfatal", "$get$Utf8Decoder__decoderNonfatal", () => new A.Utf8Decoder__decoderNonfatal_closure().call$0()); + _lazyFinal($, "_Base64Decoder__inverseAlphabet", "$get$_Base64Decoder__inverseAlphabet", () => A.NativeInt8List__create1(A._ensureNativeList(A._setArrayType([-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -1, -2, -2, -2, -2, -2, 62, -2, 62, -2, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -2, -2, -2, -1, -2, -2, -2, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -2, -2, -2, -2, 63, -2, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -2, -2, -2, -2, -2], type$.JSArray_int)))); + _lazyFinal($, "_Uri__isWindowsCached", "$get$_Uri__isWindowsCached", () => typeof process != "undefined" && Object.prototype.toString.call(process) == "[object process]" && process.platform == "win32"); + _lazyFinal($, "_Uri__needsNoEncoding", "$get$_Uri__needsNoEncoding", () => A.RegExp_RegExp("^[\\-\\.0-9A-Z_a-z~]*$", false)); + _lazyFinal($, "_hashSeed", "$get$_hashSeed", () => A.objectHashCode(B.Type_Object_xQ6)); + _lazyFinal($, "_scannerTables", "$get$_scannerTables", () => A._createTables()); + _lazyFinal($, "windows", "$get$windows", () => A.Context_Context($.$get$Style_windows())); + _lazyFinal($, "context", "$get$context", () => new A.Context($.$get$Style_platform(), null)); + _lazyFinal($, "Style_posix", "$get$Style_posix", () => new A.PosixStyle(A.RegExp_RegExp("/", false), A.RegExp_RegExp("[^/]$", false), A.RegExp_RegExp("^/", false))); + _lazyFinal($, "Style_windows", "$get$Style_windows", () => new A.WindowsStyle(A.RegExp_RegExp("[/\\\\]", false), A.RegExp_RegExp("[^/\\\\]$", false), A.RegExp_RegExp("^(\\\\\\\\[^\\\\]+\\\\[^\\\\/]+|[a-zA-Z]:[/\\\\])", false), A.RegExp_RegExp("^[/\\\\](?![/\\\\])", false))); + _lazyFinal($, "Style_url", "$get$Style_url", () => new A.UrlStyle(A.RegExp_RegExp("/", false), A.RegExp_RegExp("(^[a-zA-Z][-+.a-zA-Z\\d]*://|[^/])$", false), A.RegExp_RegExp("[a-zA-Z][-+.a-zA-Z\\d]*://[^/]*", false), A.RegExp_RegExp("^/", false))); + _lazyFinal($, "Style_platform", "$get$Style_platform", () => A.Style__getPlatformStyle()); + _lazyFinal($, "_vmFrame", "$get$_vmFrame", () => A.RegExp_RegExp("^#\\d+\\s+(\\S.*) \\((.+?)((?::\\d+){0,2})\\)$", false)); + _lazyFinal($, "_v8Frame", "$get$_v8Frame", () => A.RegExp_RegExp("^\\s*at (?:(\\S.*?)(?: \\[as [^\\]]+\\])? \\((.*)\\)|(.*))$", false)); + _lazyFinal($, "_v8UrlLocation", "$get$_v8UrlLocation", () => A.RegExp_RegExp("^(.*?):(\\d+)(?::(\\d+))?$|native$", false)); + _lazyFinal($, "_v8EvalLocation", "$get$_v8EvalLocation", () => A.RegExp_RegExp("^eval at (?:\\S.*?) \\((.*)\\)(?:, .*?:\\d+:\\d+)?$", false)); + _lazyFinal($, "_firefoxEvalLocation", "$get$_firefoxEvalLocation", () => A.RegExp_RegExp("(\\S+)@(\\S+) line (\\d+) >.* (Function|eval):\\d+:\\d+", false)); + _lazyFinal($, "_firefoxSafariFrame", "$get$_firefoxSafariFrame", () => A.RegExp_RegExp("^(?:([^@(/]*)(?:\\(.*\\))?((?:/[^/]*)*)(?:\\(.*\\))?@)?(.*?):(\\d*)(?::(\\d*))?$", false)); + _lazyFinal($, "_friendlyFrame", "$get$_friendlyFrame", () => A.RegExp_RegExp("^(\\S+)(?: (\\d+)(?::(\\d+))?)?\\s+([^\\d].*)$", false)); + _lazyFinal($, "_asyncBody", "$get$_asyncBody", () => A.RegExp_RegExp("<(<anonymous closure>|[^>]+)_async_body>", false)); + _lazyFinal($, "_initialDot", "$get$_initialDot", () => A.RegExp_RegExp("^\\.", false)); + _lazyFinal($, "Frame__uriRegExp", "$get$Frame__uriRegExp", () => A.RegExp_RegExp("^[a-zA-Z][-+.a-zA-Z\\d]*://", false)); + _lazyFinal($, "Frame__windowsRegExp", "$get$Frame__windowsRegExp", () => A.RegExp_RegExp("^([a-zA-Z]:[\\\\/]|\\\\\\\\)", false)); + _lazyFinal($, "_terseRegExp", "$get$_terseRegExp", () => A.RegExp_RegExp("(-patch)?([/\\\\].*)?$", false)); + _lazyFinal($, "_v8Trace", "$get$_v8Trace", () => A.RegExp_RegExp("\\n ?at ", false)); + _lazyFinal($, "_v8TraceLine", "$get$_v8TraceLine", () => A.RegExp_RegExp(" ?at ", false)); + _lazyFinal($, "_firefoxEvalTrace", "$get$_firefoxEvalTrace", () => A.RegExp_RegExp("@\\S+ line \\d+ >.* (Function|eval):\\d+:\\d+", false)); + _lazyFinal($, "_firefoxSafariTrace", "$get$_firefoxSafariTrace", () => A.RegExp_RegExp("^(([.0-9A-Za-z_$/<]|\\(.*\\))*@)?[^\\s]*:\\d*$", true)); + _lazyFinal($, "_friendlyTrace", "$get$_friendlyTrace", () => A.RegExp_RegExp("^[^\\s<][^\\s]*( \\d+(:\\d+)?)?[ \\t]+[^\\s]+$", true)); + _lazyFinal($, "vmChainGap", "$get$vmChainGap", () => A.RegExp_RegExp("^<asynchronous suspension>\\n?$", true)); + _lazyFinal($, "_currentUrl", "$get$_currentUrl", () => A.Uri_parse(A.getProperty(A.getProperty(self.window, "location", type$.JavaScriptObject), "href", type$.String))); + })(); + (function nativeSupport() { + !function() { + var intern = function(s) { + var o = {}; + o[s] = 1; + return Object.keys(hunkHelpers.convertToFastObject(o))[0]; + }; + init.getIsolateTag = function(name) { + return intern("___dart_" + name + init.isolateTag); + }; + var tableProperty = "___dart_isolate_tags_"; + var usedProperties = Object[tableProperty] || (Object[tableProperty] = Object.create(null)); + var rootProperty = "_ZxYxX"; + for (var i = 0;; i++) { + var property = intern(rootProperty + "_" + i + "_"); + if (!(property in usedProperties)) { + usedProperties[property] = 1; + init.isolateTag = property; + break; + } + } + init.dispatchPropertyName = init.getIsolateTag("dispatch_record"); + }(); + hunkHelpers.setOrUpdateInterceptorsByTag({WebGL: J.Interceptor, AbortPaymentEvent: J.JavaScriptObject, AnimationEffectReadOnly: J.JavaScriptObject, AnimationEffectTiming: J.JavaScriptObject, AnimationEffectTimingReadOnly: J.JavaScriptObject, AnimationEvent: J.JavaScriptObject, AnimationPlaybackEvent: J.JavaScriptObject, AnimationTimeline: J.JavaScriptObject, AnimationWorkletGlobalScope: J.JavaScriptObject, ApplicationCacheErrorEvent: J.JavaScriptObject, AuthenticatorAssertionResponse: J.JavaScriptObject, AuthenticatorAttestationResponse: J.JavaScriptObject, AuthenticatorResponse: J.JavaScriptObject, BackgroundFetchClickEvent: J.JavaScriptObject, BackgroundFetchEvent: J.JavaScriptObject, BackgroundFetchFailEvent: J.JavaScriptObject, BackgroundFetchFetch: J.JavaScriptObject, BackgroundFetchManager: J.JavaScriptObject, BackgroundFetchSettledFetch: J.JavaScriptObject, BackgroundFetchedEvent: J.JavaScriptObject, BarProp: J.JavaScriptObject, BarcodeDetector: J.JavaScriptObject, BeforeInstallPromptEvent: J.JavaScriptObject, BeforeUnloadEvent: J.JavaScriptObject, BlobEvent: J.JavaScriptObject, BluetoothRemoteGATTDescriptor: J.JavaScriptObject, Body: J.JavaScriptObject, BudgetState: J.JavaScriptObject, CacheStorage: J.JavaScriptObject, CanMakePaymentEvent: J.JavaScriptObject, CanvasGradient: J.JavaScriptObject, CanvasPattern: J.JavaScriptObject, CanvasRenderingContext2D: J.JavaScriptObject, Client: J.JavaScriptObject, Clients: J.JavaScriptObject, ClipboardEvent: J.JavaScriptObject, CloseEvent: J.JavaScriptObject, CompositionEvent: J.JavaScriptObject, CookieStore: J.JavaScriptObject, Coordinates: J.JavaScriptObject, Credential: J.JavaScriptObject, CredentialUserData: J.JavaScriptObject, CredentialsContainer: J.JavaScriptObject, Crypto: J.JavaScriptObject, CryptoKey: J.JavaScriptObject, CSS: J.JavaScriptObject, CSSVariableReferenceValue: J.JavaScriptObject, CustomElementRegistry: J.JavaScriptObject, CustomEvent: J.JavaScriptObject, DataTransfer: J.JavaScriptObject, DataTransferItem: J.JavaScriptObject, DeprecatedStorageInfo: J.JavaScriptObject, DeprecatedStorageQuota: J.JavaScriptObject, DeprecationReport: J.JavaScriptObject, DetectedBarcode: J.JavaScriptObject, DetectedFace: J.JavaScriptObject, DetectedText: J.JavaScriptObject, DeviceAcceleration: J.JavaScriptObject, DeviceMotionEvent: J.JavaScriptObject, DeviceOrientationEvent: J.JavaScriptObject, DeviceRotationRate: J.JavaScriptObject, DirectoryEntry: J.JavaScriptObject, webkitFileSystemDirectoryEntry: J.JavaScriptObject, FileSystemDirectoryEntry: J.JavaScriptObject, DirectoryReader: J.JavaScriptObject, WebKitDirectoryReader: J.JavaScriptObject, webkitFileSystemDirectoryReader: J.JavaScriptObject, FileSystemDirectoryReader: J.JavaScriptObject, DocumentOrShadowRoot: J.JavaScriptObject, DocumentTimeline: J.JavaScriptObject, DOMError: J.JavaScriptObject, DOMImplementation: J.JavaScriptObject, Iterator: J.JavaScriptObject, DOMMatrix: J.JavaScriptObject, DOMMatrixReadOnly: J.JavaScriptObject, DOMParser: J.JavaScriptObject, DOMPoint: J.JavaScriptObject, DOMPointReadOnly: J.JavaScriptObject, DOMQuad: J.JavaScriptObject, DOMStringMap: J.JavaScriptObject, Entry: J.JavaScriptObject, webkitFileSystemEntry: J.JavaScriptObject, FileSystemEntry: J.JavaScriptObject, ErrorEvent: J.JavaScriptObject, Event: J.JavaScriptObject, InputEvent: J.JavaScriptObject, SubmitEvent: J.JavaScriptObject, ExtendableEvent: J.JavaScriptObject, ExtendableMessageEvent: J.JavaScriptObject, External: J.JavaScriptObject, FaceDetector: J.JavaScriptObject, FederatedCredential: J.JavaScriptObject, FetchEvent: J.JavaScriptObject, FileEntry: J.JavaScriptObject, webkitFileSystemFileEntry: J.JavaScriptObject, FileSystemFileEntry: J.JavaScriptObject, DOMFileSystem: J.JavaScriptObject, WebKitFileSystem: J.JavaScriptObject, webkitFileSystem: J.JavaScriptObject, FileSystem: J.JavaScriptObject, FocusEvent: J.JavaScriptObject, FontFace: J.JavaScriptObject, FontFaceSetLoadEvent: J.JavaScriptObject, FontFaceSource: J.JavaScriptObject, ForeignFetchEvent: J.JavaScriptObject, FormData: J.JavaScriptObject, GamepadButton: J.JavaScriptObject, GamepadEvent: J.JavaScriptObject, GamepadPose: J.JavaScriptObject, Geolocation: J.JavaScriptObject, Position: J.JavaScriptObject, GeolocationPosition: J.JavaScriptObject, HashChangeEvent: J.JavaScriptObject, Headers: J.JavaScriptObject, HTMLHyperlinkElementUtils: J.JavaScriptObject, IdleDeadline: J.JavaScriptObject, ImageBitmap: J.JavaScriptObject, ImageBitmapRenderingContext: J.JavaScriptObject, ImageCapture: J.JavaScriptObject, ImageData: J.JavaScriptObject, InputDeviceCapabilities: J.JavaScriptObject, InstallEvent: J.JavaScriptObject, IntersectionObserver: J.JavaScriptObject, IntersectionObserverEntry: J.JavaScriptObject, InterventionReport: J.JavaScriptObject, KeyboardEvent: J.JavaScriptObject, KeyframeEffect: J.JavaScriptObject, KeyframeEffectReadOnly: J.JavaScriptObject, MediaCapabilities: J.JavaScriptObject, MediaCapabilitiesInfo: J.JavaScriptObject, MediaDeviceInfo: J.JavaScriptObject, MediaEncryptedEvent: J.JavaScriptObject, MediaError: J.JavaScriptObject, MediaKeyMessageEvent: J.JavaScriptObject, MediaKeyStatusMap: J.JavaScriptObject, MediaKeySystemAccess: J.JavaScriptObject, MediaKeys: J.JavaScriptObject, MediaKeysPolicy: J.JavaScriptObject, MediaMetadata: J.JavaScriptObject, MediaQueryListEvent: J.JavaScriptObject, MediaSession: J.JavaScriptObject, MediaSettingsRange: J.JavaScriptObject, MediaStreamEvent: J.JavaScriptObject, MediaStreamTrackEvent: J.JavaScriptObject, MemoryInfo: J.JavaScriptObject, MessageChannel: J.JavaScriptObject, MessageEvent: J.JavaScriptObject, Metadata: J.JavaScriptObject, MIDIConnectionEvent: J.JavaScriptObject, MIDIMessageEvent: J.JavaScriptObject, MouseEvent: J.JavaScriptObject, DragEvent: J.JavaScriptObject, MutationEvent: J.JavaScriptObject, MutationObserver: J.JavaScriptObject, WebKitMutationObserver: J.JavaScriptObject, MutationRecord: J.JavaScriptObject, NavigationPreloadManager: J.JavaScriptObject, Navigator: J.JavaScriptObject, NavigatorAutomationInformation: J.JavaScriptObject, NavigatorConcurrentHardware: J.JavaScriptObject, NavigatorCookies: J.JavaScriptObject, NavigatorUserMediaError: J.JavaScriptObject, NodeFilter: J.JavaScriptObject, NodeIterator: J.JavaScriptObject, NonDocumentTypeChildNode: J.JavaScriptObject, NonElementParentNode: J.JavaScriptObject, NoncedElement: J.JavaScriptObject, NotificationEvent: J.JavaScriptObject, OffscreenCanvasRenderingContext2D: J.JavaScriptObject, OverconstrainedError: J.JavaScriptObject, PageTransitionEvent: J.JavaScriptObject, PaintRenderingContext2D: J.JavaScriptObject, PaintSize: J.JavaScriptObject, PaintWorkletGlobalScope: J.JavaScriptObject, PasswordCredential: J.JavaScriptObject, Path2D: J.JavaScriptObject, PaymentAddress: J.JavaScriptObject, PaymentInstruments: J.JavaScriptObject, PaymentManager: J.JavaScriptObject, PaymentRequestEvent: J.JavaScriptObject, PaymentRequestUpdateEvent: J.JavaScriptObject, PaymentResponse: J.JavaScriptObject, PerformanceEntry: J.JavaScriptObject, PerformanceLongTaskTiming: J.JavaScriptObject, PerformanceMark: J.JavaScriptObject, PerformanceMeasure: J.JavaScriptObject, PerformanceNavigation: J.JavaScriptObject, PerformanceNavigationTiming: J.JavaScriptObject, PerformanceObserver: J.JavaScriptObject, PerformanceObserverEntryList: J.JavaScriptObject, PerformancePaintTiming: J.JavaScriptObject, PerformanceResourceTiming: J.JavaScriptObject, PerformanceServerTiming: J.JavaScriptObject, PerformanceTiming: J.JavaScriptObject, Permissions: J.JavaScriptObject, PhotoCapabilities: J.JavaScriptObject, PointerEvent: J.JavaScriptObject, PopStateEvent: J.JavaScriptObject, PositionError: J.JavaScriptObject, GeolocationPositionError: J.JavaScriptObject, Presentation: J.JavaScriptObject, PresentationConnectionAvailableEvent: J.JavaScriptObject, PresentationConnectionCloseEvent: J.JavaScriptObject, PresentationReceiver: J.JavaScriptObject, ProgressEvent: J.JavaScriptObject, PromiseRejectionEvent: J.JavaScriptObject, PublicKeyCredential: J.JavaScriptObject, PushEvent: J.JavaScriptObject, PushManager: J.JavaScriptObject, PushMessageData: J.JavaScriptObject, PushSubscription: J.JavaScriptObject, PushSubscriptionOptions: J.JavaScriptObject, Range: J.JavaScriptObject, RelatedApplication: J.JavaScriptObject, ReportBody: J.JavaScriptObject, ReportingObserver: J.JavaScriptObject, ResizeObserver: J.JavaScriptObject, ResizeObserverEntry: J.JavaScriptObject, RTCCertificate: J.JavaScriptObject, RTCDataChannelEvent: J.JavaScriptObject, RTCDTMFToneChangeEvent: J.JavaScriptObject, RTCIceCandidate: J.JavaScriptObject, mozRTCIceCandidate: J.JavaScriptObject, RTCLegacyStatsReport: J.JavaScriptObject, RTCPeerConnectionIceEvent: J.JavaScriptObject, RTCRtpContributingSource: J.JavaScriptObject, RTCRtpReceiver: J.JavaScriptObject, RTCRtpSender: J.JavaScriptObject, RTCSessionDescription: J.JavaScriptObject, mozRTCSessionDescription: J.JavaScriptObject, RTCStatsResponse: J.JavaScriptObject, RTCTrackEvent: J.JavaScriptObject, Screen: J.JavaScriptObject, ScrollState: J.JavaScriptObject, ScrollTimeline: J.JavaScriptObject, SecurityPolicyViolationEvent: J.JavaScriptObject, Selection: J.JavaScriptObject, SensorErrorEvent: J.JavaScriptObject, SharedArrayBuffer: J.JavaScriptObject, SpeechRecognitionAlternative: J.JavaScriptObject, SpeechRecognitionError: J.JavaScriptObject, SpeechRecognitionEvent: J.JavaScriptObject, SpeechSynthesisEvent: J.JavaScriptObject, SpeechSynthesisVoice: J.JavaScriptObject, StaticRange: J.JavaScriptObject, StorageEvent: J.JavaScriptObject, StorageManager: J.JavaScriptObject, StyleMedia: J.JavaScriptObject, StylePropertyMap: J.JavaScriptObject, StylePropertyMapReadonly: J.JavaScriptObject, SyncEvent: J.JavaScriptObject, SyncManager: J.JavaScriptObject, TaskAttributionTiming: J.JavaScriptObject, TextDetector: J.JavaScriptObject, TextEvent: J.JavaScriptObject, TextMetrics: J.JavaScriptObject, TouchEvent: J.JavaScriptObject, TrackDefault: J.JavaScriptObject, TrackEvent: J.JavaScriptObject, TransitionEvent: J.JavaScriptObject, WebKitTransitionEvent: J.JavaScriptObject, TreeWalker: J.JavaScriptObject, TrustedHTML: J.JavaScriptObject, TrustedScriptURL: J.JavaScriptObject, TrustedURL: J.JavaScriptObject, UIEvent: J.JavaScriptObject, UnderlyingSourceBase: J.JavaScriptObject, URLSearchParams: J.JavaScriptObject, VRCoordinateSystem: J.JavaScriptObject, VRDeviceEvent: J.JavaScriptObject, VRDisplayCapabilities: J.JavaScriptObject, VRDisplayEvent: J.JavaScriptObject, VREyeParameters: J.JavaScriptObject, VRFrameData: J.JavaScriptObject, VRFrameOfReference: J.JavaScriptObject, VRPose: J.JavaScriptObject, VRSessionEvent: J.JavaScriptObject, VRStageBounds: J.JavaScriptObject, VRStageBoundsPoint: J.JavaScriptObject, VRStageParameters: J.JavaScriptObject, ValidityState: J.JavaScriptObject, VideoPlaybackQuality: J.JavaScriptObject, VideoTrack: J.JavaScriptObject, VTTRegion: J.JavaScriptObject, WheelEvent: J.JavaScriptObject, WindowClient: J.JavaScriptObject, WorkletAnimation: J.JavaScriptObject, WorkletGlobalScope: J.JavaScriptObject, XPathEvaluator: J.JavaScriptObject, XPathExpression: J.JavaScriptObject, XPathNSResolver: J.JavaScriptObject, XPathResult: J.JavaScriptObject, XMLSerializer: J.JavaScriptObject, XSLTProcessor: J.JavaScriptObject, Bluetooth: J.JavaScriptObject, BluetoothCharacteristicProperties: J.JavaScriptObject, BluetoothRemoteGATTServer: J.JavaScriptObject, BluetoothRemoteGATTService: J.JavaScriptObject, BluetoothUUID: J.JavaScriptObject, BudgetService: J.JavaScriptObject, Cache: J.JavaScriptObject, DOMFileSystemSync: J.JavaScriptObject, DirectoryEntrySync: J.JavaScriptObject, DirectoryReaderSync: J.JavaScriptObject, EntrySync: J.JavaScriptObject, FileEntrySync: J.JavaScriptObject, FileReaderSync: J.JavaScriptObject, FileWriterSync: J.JavaScriptObject, HTMLAllCollection: J.JavaScriptObject, Mojo: J.JavaScriptObject, MojoHandle: J.JavaScriptObject, MojoInterfaceRequestEvent: J.JavaScriptObject, MojoWatcher: J.JavaScriptObject, NFC: J.JavaScriptObject, PagePopupController: J.JavaScriptObject, Report: J.JavaScriptObject, Request: J.JavaScriptObject, ResourceProgressEvent: J.JavaScriptObject, Response: J.JavaScriptObject, SubtleCrypto: J.JavaScriptObject, USBAlternateInterface: J.JavaScriptObject, USBConfiguration: J.JavaScriptObject, USBConnectionEvent: J.JavaScriptObject, USBDevice: J.JavaScriptObject, USBEndpoint: J.JavaScriptObject, USBInTransferResult: J.JavaScriptObject, USBInterface: J.JavaScriptObject, USBIsochronousInTransferPacket: J.JavaScriptObject, USBIsochronousInTransferResult: J.JavaScriptObject, USBIsochronousOutTransferPacket: J.JavaScriptObject, USBIsochronousOutTransferResult: J.JavaScriptObject, USBOutTransferResult: J.JavaScriptObject, WorkerLocation: J.JavaScriptObject, WorkerNavigator: J.JavaScriptObject, Worklet: J.JavaScriptObject, IDBCursor: J.JavaScriptObject, IDBCursorWithValue: J.JavaScriptObject, IDBFactory: J.JavaScriptObject, IDBIndex: J.JavaScriptObject, IDBKeyRange: J.JavaScriptObject, IDBObjectStore: J.JavaScriptObject, IDBObservation: J.JavaScriptObject, IDBObserver: J.JavaScriptObject, IDBObserverChanges: J.JavaScriptObject, IDBVersionChangeEvent: J.JavaScriptObject, SVGAngle: J.JavaScriptObject, SVGAnimatedAngle: J.JavaScriptObject, SVGAnimatedBoolean: J.JavaScriptObject, SVGAnimatedEnumeration: J.JavaScriptObject, SVGAnimatedInteger: J.JavaScriptObject, SVGAnimatedLength: J.JavaScriptObject, SVGAnimatedLengthList: J.JavaScriptObject, SVGAnimatedNumber: J.JavaScriptObject, SVGAnimatedNumberList: J.JavaScriptObject, SVGAnimatedPreserveAspectRatio: J.JavaScriptObject, SVGAnimatedRect: J.JavaScriptObject, SVGAnimatedString: J.JavaScriptObject, SVGAnimatedTransformList: J.JavaScriptObject, SVGMatrix: J.JavaScriptObject, SVGPoint: J.JavaScriptObject, SVGPreserveAspectRatio: J.JavaScriptObject, SVGRect: J.JavaScriptObject, SVGUnitTypes: J.JavaScriptObject, AudioListener: J.JavaScriptObject, AudioParam: J.JavaScriptObject, AudioProcessingEvent: J.JavaScriptObject, AudioTrack: J.JavaScriptObject, AudioWorkletGlobalScope: J.JavaScriptObject, AudioWorkletProcessor: J.JavaScriptObject, OfflineAudioCompletionEvent: J.JavaScriptObject, PeriodicWave: J.JavaScriptObject, WebGLActiveInfo: J.JavaScriptObject, ANGLEInstancedArrays: J.JavaScriptObject, ANGLE_instanced_arrays: J.JavaScriptObject, WebGLBuffer: J.JavaScriptObject, WebGLCanvas: J.JavaScriptObject, WebGLColorBufferFloat: J.JavaScriptObject, WebGLCompressedTextureASTC: J.JavaScriptObject, WebGLCompressedTextureATC: J.JavaScriptObject, WEBGL_compressed_texture_atc: J.JavaScriptObject, WebGLCompressedTextureETC1: J.JavaScriptObject, WEBGL_compressed_texture_etc1: J.JavaScriptObject, WebGLCompressedTextureETC: J.JavaScriptObject, WebGLCompressedTexturePVRTC: J.JavaScriptObject, WEBGL_compressed_texture_pvrtc: J.JavaScriptObject, WebGLCompressedTextureS3TC: J.JavaScriptObject, WEBGL_compressed_texture_s3tc: J.JavaScriptObject, WebGLCompressedTextureS3TCsRGB: J.JavaScriptObject, WebGLContextEvent: J.JavaScriptObject, WebGLDebugRendererInfo: J.JavaScriptObject, WEBGL_debug_renderer_info: J.JavaScriptObject, WebGLDebugShaders: J.JavaScriptObject, WEBGL_debug_shaders: J.JavaScriptObject, WebGLDepthTexture: J.JavaScriptObject, WEBGL_depth_texture: J.JavaScriptObject, WebGLDrawBuffers: J.JavaScriptObject, WEBGL_draw_buffers: J.JavaScriptObject, EXTsRGB: J.JavaScriptObject, EXT_sRGB: J.JavaScriptObject, EXTBlendMinMax: J.JavaScriptObject, EXT_blend_minmax: J.JavaScriptObject, EXTColorBufferFloat: J.JavaScriptObject, EXTColorBufferHalfFloat: J.JavaScriptObject, EXTDisjointTimerQuery: J.JavaScriptObject, EXTDisjointTimerQueryWebGL2: J.JavaScriptObject, EXTFragDepth: J.JavaScriptObject, EXT_frag_depth: J.JavaScriptObject, EXTShaderTextureLOD: J.JavaScriptObject, EXT_shader_texture_lod: J.JavaScriptObject, EXTTextureFilterAnisotropic: J.JavaScriptObject, EXT_texture_filter_anisotropic: J.JavaScriptObject, WebGLFramebuffer: J.JavaScriptObject, WebGLGetBufferSubDataAsync: J.JavaScriptObject, WebGLLoseContext: J.JavaScriptObject, WebGLExtensionLoseContext: J.JavaScriptObject, WEBGL_lose_context: J.JavaScriptObject, OESElementIndexUint: J.JavaScriptObject, OES_element_index_uint: J.JavaScriptObject, OESStandardDerivatives: J.JavaScriptObject, OES_standard_derivatives: J.JavaScriptObject, OESTextureFloat: J.JavaScriptObject, OES_texture_float: J.JavaScriptObject, OESTextureFloatLinear: J.JavaScriptObject, OES_texture_float_linear: J.JavaScriptObject, OESTextureHalfFloat: J.JavaScriptObject, OES_texture_half_float: J.JavaScriptObject, OESTextureHalfFloatLinear: J.JavaScriptObject, OES_texture_half_float_linear: J.JavaScriptObject, OESVertexArrayObject: J.JavaScriptObject, OES_vertex_array_object: J.JavaScriptObject, WebGLProgram: J.JavaScriptObject, WebGLQuery: J.JavaScriptObject, WebGLRenderbuffer: J.JavaScriptObject, WebGLRenderingContext: J.JavaScriptObject, WebGL2RenderingContext: J.JavaScriptObject, WebGLSampler: J.JavaScriptObject, WebGLShader: J.JavaScriptObject, WebGLShaderPrecisionFormat: J.JavaScriptObject, WebGLSync: J.JavaScriptObject, WebGLTexture: J.JavaScriptObject, WebGLTimerQueryEXT: J.JavaScriptObject, WebGLTransformFeedback: J.JavaScriptObject, WebGLUniformLocation: J.JavaScriptObject, WebGLVertexArrayObject: J.JavaScriptObject, WebGLVertexArrayObjectOES: J.JavaScriptObject, WebGL2RenderingContextBase: J.JavaScriptObject, ArrayBuffer: A.NativeByteBuffer, ArrayBufferView: A.NativeTypedData, DataView: A.NativeByteData, Float32Array: A.NativeFloat32List, Float64Array: A.NativeFloat64List, Int16Array: A.NativeInt16List, Int32Array: A.NativeInt32List, Int8Array: A.NativeInt8List, Uint16Array: A.NativeUint16List, Uint32Array: A.NativeUint32List, Uint8ClampedArray: A.NativeUint8ClampedList, CanvasPixelArray: A.NativeUint8ClampedList, Uint8Array: A.NativeUint8List, HTMLAudioElement: A.HtmlElement, HTMLBRElement: A.HtmlElement, HTMLBaseElement: A.HtmlElement, HTMLBodyElement: A.HtmlElement, HTMLButtonElement: A.HtmlElement, HTMLCanvasElement: A.HtmlElement, HTMLContentElement: A.HtmlElement, HTMLDListElement: A.HtmlElement, HTMLDataElement: A.HtmlElement, HTMLDataListElement: A.HtmlElement, HTMLDetailsElement: A.HtmlElement, HTMLDialogElement: A.HtmlElement, HTMLDivElement: A.HtmlElement, HTMLEmbedElement: A.HtmlElement, HTMLFieldSetElement: A.HtmlElement, HTMLHRElement: A.HtmlElement, HTMLHeadElement: A.HtmlElement, HTMLHeadingElement: A.HtmlElement, HTMLHtmlElement: A.HtmlElement, HTMLIFrameElement: A.HtmlElement, HTMLImageElement: A.HtmlElement, HTMLInputElement: A.HtmlElement, HTMLLIElement: A.HtmlElement, HTMLLabelElement: A.HtmlElement, HTMLLegendElement: A.HtmlElement, HTMLLinkElement: A.HtmlElement, HTMLMapElement: A.HtmlElement, HTMLMediaElement: A.HtmlElement, HTMLMenuElement: A.HtmlElement, HTMLMetaElement: A.HtmlElement, HTMLMeterElement: A.HtmlElement, HTMLModElement: A.HtmlElement, HTMLOListElement: A.HtmlElement, HTMLObjectElement: A.HtmlElement, HTMLOptGroupElement: A.HtmlElement, HTMLOptionElement: A.HtmlElement, HTMLOutputElement: A.HtmlElement, HTMLParagraphElement: A.HtmlElement, HTMLParamElement: A.HtmlElement, HTMLPictureElement: A.HtmlElement, HTMLPreElement: A.HtmlElement, HTMLProgressElement: A.HtmlElement, HTMLQuoteElement: A.HtmlElement, HTMLScriptElement: A.HtmlElement, HTMLShadowElement: A.HtmlElement, HTMLSlotElement: A.HtmlElement, HTMLSourceElement: A.HtmlElement, HTMLSpanElement: A.HtmlElement, HTMLStyleElement: A.HtmlElement, HTMLTableCaptionElement: A.HtmlElement, HTMLTableCellElement: A.HtmlElement, HTMLTableDataCellElement: A.HtmlElement, HTMLTableHeaderCellElement: A.HtmlElement, HTMLTableColElement: A.HtmlElement, HTMLTableElement: A.HtmlElement, HTMLTableRowElement: A.HtmlElement, HTMLTableSectionElement: A.HtmlElement, HTMLTemplateElement: A.HtmlElement, HTMLTextAreaElement: A.HtmlElement, HTMLTimeElement: A.HtmlElement, HTMLTitleElement: A.HtmlElement, HTMLTrackElement: A.HtmlElement, HTMLUListElement: A.HtmlElement, HTMLUnknownElement: A.HtmlElement, HTMLVideoElement: A.HtmlElement, HTMLDirectoryElement: A.HtmlElement, HTMLFontElement: A.HtmlElement, HTMLFrameElement: A.HtmlElement, HTMLFrameSetElement: A.HtmlElement, HTMLMarqueeElement: A.HtmlElement, HTMLElement: A.HtmlElement, AccessibleNodeList: A.AccessibleNodeList, HTMLAnchorElement: A.AnchorElement, HTMLAreaElement: A.AreaElement, Blob: A.Blob, CDATASection: A.CharacterData, CharacterData: A.CharacterData, Comment: A.CharacterData, ProcessingInstruction: A.CharacterData, Text: A.CharacterData, CSSPerspective: A.CssPerspective, CSSCharsetRule: A.CssRule, CSSConditionRule: A.CssRule, CSSFontFaceRule: A.CssRule, CSSGroupingRule: A.CssRule, CSSImportRule: A.CssRule, CSSKeyframeRule: A.CssRule, MozCSSKeyframeRule: A.CssRule, WebKitCSSKeyframeRule: A.CssRule, CSSKeyframesRule: A.CssRule, MozCSSKeyframesRule: A.CssRule, WebKitCSSKeyframesRule: A.CssRule, CSSMediaRule: A.CssRule, CSSNamespaceRule: A.CssRule, CSSPageRule: A.CssRule, CSSRule: A.CssRule, CSSStyleRule: A.CssRule, CSSSupportsRule: A.CssRule, CSSViewportRule: A.CssRule, CSSStyleDeclaration: A.CssStyleDeclaration, MSStyleCSSProperties: A.CssStyleDeclaration, CSS2Properties: A.CssStyleDeclaration, CSSImageValue: A.CssStyleValue, CSSKeywordValue: A.CssStyleValue, CSSNumericValue: A.CssStyleValue, CSSPositionValue: A.CssStyleValue, CSSResourceValue: A.CssStyleValue, CSSUnitValue: A.CssStyleValue, CSSURLImageValue: A.CssStyleValue, CSSStyleValue: A.CssStyleValue, CSSMatrixComponent: A.CssTransformComponent, CSSRotation: A.CssTransformComponent, CSSScale: A.CssTransformComponent, CSSSkew: A.CssTransformComponent, CSSTranslation: A.CssTransformComponent, CSSTransformComponent: A.CssTransformComponent, CSSTransformValue: A.CssTransformValue, CSSUnparsedValue: A.CssUnparsedValue, DataTransferItemList: A.DataTransferItemList, DOMException: A.DomException, ClientRectList: A.DomRectList, DOMRectList: A.DomRectList, DOMRectReadOnly: A.DomRectReadOnly, DOMStringList: A.DomStringList, DOMTokenList: A.DomTokenList, MathMLElement: A.Element, SVGAElement: A.Element, SVGAnimateElement: A.Element, SVGAnimateMotionElement: A.Element, SVGAnimateTransformElement: A.Element, SVGAnimationElement: A.Element, SVGCircleElement: A.Element, SVGClipPathElement: A.Element, SVGDefsElement: A.Element, SVGDescElement: A.Element, SVGDiscardElement: A.Element, SVGEllipseElement: A.Element, SVGFEBlendElement: A.Element, SVGFEColorMatrixElement: A.Element, SVGFEComponentTransferElement: A.Element, SVGFECompositeElement: A.Element, SVGFEConvolveMatrixElement: A.Element, SVGFEDiffuseLightingElement: A.Element, SVGFEDisplacementMapElement: A.Element, SVGFEDistantLightElement: A.Element, SVGFEFloodElement: A.Element, SVGFEFuncAElement: A.Element, SVGFEFuncBElement: A.Element, SVGFEFuncGElement: A.Element, SVGFEFuncRElement: A.Element, SVGFEGaussianBlurElement: A.Element, SVGFEImageElement: A.Element, SVGFEMergeElement: A.Element, SVGFEMergeNodeElement: A.Element, SVGFEMorphologyElement: A.Element, SVGFEOffsetElement: A.Element, SVGFEPointLightElement: A.Element, SVGFESpecularLightingElement: A.Element, SVGFESpotLightElement: A.Element, SVGFETileElement: A.Element, SVGFETurbulenceElement: A.Element, SVGFilterElement: A.Element, SVGForeignObjectElement: A.Element, SVGGElement: A.Element, SVGGeometryElement: A.Element, SVGGraphicsElement: A.Element, SVGImageElement: A.Element, SVGLineElement: A.Element, SVGLinearGradientElement: A.Element, SVGMarkerElement: A.Element, SVGMaskElement: A.Element, SVGMetadataElement: A.Element, SVGPathElement: A.Element, SVGPatternElement: A.Element, SVGPolygonElement: A.Element, SVGPolylineElement: A.Element, SVGRadialGradientElement: A.Element, SVGRectElement: A.Element, SVGScriptElement: A.Element, SVGSetElement: A.Element, SVGStopElement: A.Element, SVGStyleElement: A.Element, SVGElement: A.Element, SVGSVGElement: A.Element, SVGSwitchElement: A.Element, SVGSymbolElement: A.Element, SVGTSpanElement: A.Element, SVGTextContentElement: A.Element, SVGTextElement: A.Element, SVGTextPathElement: A.Element, SVGTextPositioningElement: A.Element, SVGTitleElement: A.Element, SVGUseElement: A.Element, SVGViewElement: A.Element, SVGGradientElement: A.Element, SVGComponentTransferFunctionElement: A.Element, SVGFEDropShadowElement: A.Element, SVGMPathElement: A.Element, Element: A.Element, AbsoluteOrientationSensor: A.EventTarget, Accelerometer: A.EventTarget, AccessibleNode: A.EventTarget, AmbientLightSensor: A.EventTarget, Animation: A.EventTarget, ApplicationCache: A.EventTarget, DOMApplicationCache: A.EventTarget, OfflineResourceList: A.EventTarget, BackgroundFetchRegistration: A.EventTarget, BatteryManager: A.EventTarget, BroadcastChannel: A.EventTarget, CanvasCaptureMediaStreamTrack: A.EventTarget, DedicatedWorkerGlobalScope: A.EventTarget, EventSource: A.EventTarget, FileReader: A.EventTarget, FontFaceSet: A.EventTarget, Gyroscope: A.EventTarget, XMLHttpRequest: A.EventTarget, XMLHttpRequestEventTarget: A.EventTarget, XMLHttpRequestUpload: A.EventTarget, LinearAccelerationSensor: A.EventTarget, Magnetometer: A.EventTarget, MediaDevices: A.EventTarget, MediaKeySession: A.EventTarget, MediaQueryList: A.EventTarget, MediaRecorder: A.EventTarget, MediaSource: A.EventTarget, MediaStream: A.EventTarget, MediaStreamTrack: A.EventTarget, MessagePort: A.EventTarget, MIDIAccess: A.EventTarget, MIDIInput: A.EventTarget, MIDIOutput: A.EventTarget, MIDIPort: A.EventTarget, NetworkInformation: A.EventTarget, Notification: A.EventTarget, OffscreenCanvas: A.EventTarget, OrientationSensor: A.EventTarget, PaymentRequest: A.EventTarget, Performance: A.EventTarget, PermissionStatus: A.EventTarget, PresentationAvailability: A.EventTarget, PresentationConnection: A.EventTarget, PresentationConnectionList: A.EventTarget, PresentationRequest: A.EventTarget, RelativeOrientationSensor: A.EventTarget, RemotePlayback: A.EventTarget, RTCDataChannel: A.EventTarget, DataChannel: A.EventTarget, RTCDTMFSender: A.EventTarget, RTCPeerConnection: A.EventTarget, webkitRTCPeerConnection: A.EventTarget, mozRTCPeerConnection: A.EventTarget, ScreenOrientation: A.EventTarget, Sensor: A.EventTarget, ServiceWorker: A.EventTarget, ServiceWorkerContainer: A.EventTarget, ServiceWorkerGlobalScope: A.EventTarget, ServiceWorkerRegistration: A.EventTarget, SharedWorker: A.EventTarget, SharedWorkerGlobalScope: A.EventTarget, SpeechRecognition: A.EventTarget, webkitSpeechRecognition: A.EventTarget, SpeechSynthesis: A.EventTarget, SpeechSynthesisUtterance: A.EventTarget, VR: A.EventTarget, VRDevice: A.EventTarget, VRDisplay: A.EventTarget, VRSession: A.EventTarget, VisualViewport: A.EventTarget, WebSocket: A.EventTarget, Window: A.EventTarget, DOMWindow: A.EventTarget, Worker: A.EventTarget, WorkerGlobalScope: A.EventTarget, WorkerPerformance: A.EventTarget, BluetoothDevice: A.EventTarget, BluetoothRemoteGATTCharacteristic: A.EventTarget, Clipboard: A.EventTarget, MojoInterfaceInterceptor: A.EventTarget, USB: A.EventTarget, IDBDatabase: A.EventTarget, IDBOpenDBRequest: A.EventTarget, IDBVersionChangeRequest: A.EventTarget, IDBRequest: A.EventTarget, IDBTransaction: A.EventTarget, AnalyserNode: A.EventTarget, RealtimeAnalyserNode: A.EventTarget, AudioBufferSourceNode: A.EventTarget, AudioDestinationNode: A.EventTarget, AudioNode: A.EventTarget, AudioScheduledSourceNode: A.EventTarget, AudioWorkletNode: A.EventTarget, BiquadFilterNode: A.EventTarget, ChannelMergerNode: A.EventTarget, AudioChannelMerger: A.EventTarget, ChannelSplitterNode: A.EventTarget, AudioChannelSplitter: A.EventTarget, ConstantSourceNode: A.EventTarget, ConvolverNode: A.EventTarget, DelayNode: A.EventTarget, DynamicsCompressorNode: A.EventTarget, GainNode: A.EventTarget, AudioGainNode: A.EventTarget, IIRFilterNode: A.EventTarget, MediaElementAudioSourceNode: A.EventTarget, MediaStreamAudioDestinationNode: A.EventTarget, MediaStreamAudioSourceNode: A.EventTarget, OscillatorNode: A.EventTarget, Oscillator: A.EventTarget, PannerNode: A.EventTarget, AudioPannerNode: A.EventTarget, webkitAudioPannerNode: A.EventTarget, ScriptProcessorNode: A.EventTarget, JavaScriptAudioNode: A.EventTarget, StereoPannerNode: A.EventTarget, WaveShaperNode: A.EventTarget, EventTarget: A.EventTarget, File: A.File, FileList: A.FileList, FileWriter: A.FileWriter, HTMLFormElement: A.FormElement, Gamepad: A.Gamepad, History: A.History, HTMLCollection: A.HtmlCollection, HTMLFormControlsCollection: A.HtmlCollection, HTMLOptionsCollection: A.HtmlCollection, Location: A.Location, MediaList: A.MediaList, MIDIInputMap: A.MidiInputMap, MIDIOutputMap: A.MidiOutputMap, MimeType: A.MimeType, MimeTypeArray: A.MimeTypeArray, Document: A.Node, DocumentFragment: A.Node, HTMLDocument: A.Node, ShadowRoot: A.Node, XMLDocument: A.Node, Attr: A.Node, DocumentType: A.Node, Node: A.Node, NodeList: A.NodeList, RadioNodeList: A.NodeList, Plugin: A.Plugin, PluginArray: A.PluginArray, RTCStatsReport: A.RtcStatsReport, HTMLSelectElement: A.SelectElement, SourceBuffer: A.SourceBuffer, SourceBufferList: A.SourceBufferList, SpeechGrammar: A.SpeechGrammar, SpeechGrammarList: A.SpeechGrammarList, SpeechRecognitionResult: A.SpeechRecognitionResult, Storage: A.Storage, CSSStyleSheet: A.StyleSheet, StyleSheet: A.StyleSheet, TextTrack: A.TextTrack, TextTrackCue: A.TextTrackCue, VTTCue: A.TextTrackCue, TextTrackCueList: A.TextTrackCueList, TextTrackList: A.TextTrackList, TimeRanges: A.TimeRanges, Touch: A.Touch, TouchList: A.TouchList, TrackDefaultList: A.TrackDefaultList, URL: A.Url, VideoTrackList: A.VideoTrackList, CSSRuleList: A._CssRuleList, ClientRect: A._DomRect, DOMRect: A._DomRect, GamepadList: A._GamepadList, NamedNodeMap: A._NamedNodeMap, MozNamedAttrMap: A._NamedNodeMap, SpeechRecognitionResultList: A._SpeechRecognitionResultList, StyleSheetList: A._StyleSheetList, SVGLength: A.Length, SVGLengthList: A.LengthList, SVGNumber: A.Number, SVGNumberList: A.NumberList, SVGPointList: A.PointList, SVGStringList: A.StringList, SVGTransform: A.Transform, SVGTransformList: A.TransformList, AudioBuffer: A.AudioBuffer, AudioParamMap: A.AudioParamMap, AudioTrackList: A.AudioTrackList, AudioContext: A.BaseAudioContext, webkitAudioContext: A.BaseAudioContext, BaseAudioContext: A.BaseAudioContext, OfflineAudioContext: A.OfflineAudioContext}); + hunkHelpers.setOrUpdateLeafTags({WebGL: true, AbortPaymentEvent: true, AnimationEffectReadOnly: true, AnimationEffectTiming: true, AnimationEffectTimingReadOnly: true, AnimationEvent: true, AnimationPlaybackEvent: true, AnimationTimeline: true, AnimationWorkletGlobalScope: true, ApplicationCacheErrorEvent: true, AuthenticatorAssertionResponse: true, AuthenticatorAttestationResponse: true, AuthenticatorResponse: true, BackgroundFetchClickEvent: true, BackgroundFetchEvent: true, BackgroundFetchFailEvent: true, BackgroundFetchFetch: true, BackgroundFetchManager: true, BackgroundFetchSettledFetch: true, BackgroundFetchedEvent: true, BarProp: true, BarcodeDetector: true, BeforeInstallPromptEvent: true, BeforeUnloadEvent: true, BlobEvent: true, BluetoothRemoteGATTDescriptor: true, Body: true, BudgetState: true, CacheStorage: true, CanMakePaymentEvent: true, CanvasGradient: true, CanvasPattern: true, CanvasRenderingContext2D: true, Client: true, Clients: true, ClipboardEvent: true, CloseEvent: true, CompositionEvent: true, CookieStore: true, Coordinates: true, Credential: true, CredentialUserData: true, CredentialsContainer: true, Crypto: true, CryptoKey: true, CSS: true, CSSVariableReferenceValue: true, CustomElementRegistry: true, CustomEvent: true, DataTransfer: true, DataTransferItem: true, DeprecatedStorageInfo: true, DeprecatedStorageQuota: true, DeprecationReport: true, DetectedBarcode: true, DetectedFace: true, DetectedText: true, DeviceAcceleration: true, DeviceMotionEvent: true, DeviceOrientationEvent: true, DeviceRotationRate: true, DirectoryEntry: true, webkitFileSystemDirectoryEntry: true, FileSystemDirectoryEntry: true, DirectoryReader: true, WebKitDirectoryReader: true, webkitFileSystemDirectoryReader: true, FileSystemDirectoryReader: true, DocumentOrShadowRoot: true, DocumentTimeline: true, DOMError: true, DOMImplementation: true, Iterator: true, DOMMatrix: true, DOMMatrixReadOnly: true, DOMParser: true, DOMPoint: true, DOMPointReadOnly: true, DOMQuad: true, DOMStringMap: true, Entry: true, webkitFileSystemEntry: true, FileSystemEntry: true, ErrorEvent: true, Event: true, InputEvent: true, SubmitEvent: true, ExtendableEvent: true, ExtendableMessageEvent: true, External: true, FaceDetector: true, FederatedCredential: true, FetchEvent: true, FileEntry: true, webkitFileSystemFileEntry: true, FileSystemFileEntry: true, DOMFileSystem: true, WebKitFileSystem: true, webkitFileSystem: true, FileSystem: true, FocusEvent: true, FontFace: true, FontFaceSetLoadEvent: true, FontFaceSource: true, ForeignFetchEvent: true, FormData: true, GamepadButton: true, GamepadEvent: true, GamepadPose: true, Geolocation: true, Position: true, GeolocationPosition: true, HashChangeEvent: true, Headers: true, HTMLHyperlinkElementUtils: true, IdleDeadline: true, ImageBitmap: true, ImageBitmapRenderingContext: true, ImageCapture: true, ImageData: true, InputDeviceCapabilities: true, InstallEvent: true, IntersectionObserver: true, IntersectionObserverEntry: true, InterventionReport: true, KeyboardEvent: true, KeyframeEffect: true, KeyframeEffectReadOnly: true, MediaCapabilities: true, MediaCapabilitiesInfo: true, MediaDeviceInfo: true, MediaEncryptedEvent: true, MediaError: true, MediaKeyMessageEvent: true, MediaKeyStatusMap: true, MediaKeySystemAccess: true, MediaKeys: true, MediaKeysPolicy: true, MediaMetadata: true, MediaQueryListEvent: true, MediaSession: true, MediaSettingsRange: true, MediaStreamEvent: true, MediaStreamTrackEvent: true, MemoryInfo: true, MessageChannel: true, MessageEvent: true, Metadata: true, MIDIConnectionEvent: true, MIDIMessageEvent: true, MouseEvent: true, DragEvent: true, MutationEvent: true, MutationObserver: true, WebKitMutationObserver: true, MutationRecord: true, NavigationPreloadManager: true, Navigator: true, NavigatorAutomationInformation: true, NavigatorConcurrentHardware: true, NavigatorCookies: true, NavigatorUserMediaError: true, NodeFilter: true, NodeIterator: true, NonDocumentTypeChildNode: true, NonElementParentNode: true, NoncedElement: true, NotificationEvent: true, OffscreenCanvasRenderingContext2D: true, OverconstrainedError: true, PageTransitionEvent: true, PaintRenderingContext2D: true, PaintSize: true, PaintWorkletGlobalScope: true, PasswordCredential: true, Path2D: true, PaymentAddress: true, PaymentInstruments: true, PaymentManager: true, PaymentRequestEvent: true, PaymentRequestUpdateEvent: true, PaymentResponse: true, PerformanceEntry: true, PerformanceLongTaskTiming: true, PerformanceMark: true, PerformanceMeasure: true, PerformanceNavigation: true, PerformanceNavigationTiming: true, PerformanceObserver: true, PerformanceObserverEntryList: true, PerformancePaintTiming: true, PerformanceResourceTiming: true, PerformanceServerTiming: true, PerformanceTiming: true, Permissions: true, PhotoCapabilities: true, PointerEvent: true, PopStateEvent: true, PositionError: true, GeolocationPositionError: true, Presentation: true, PresentationConnectionAvailableEvent: true, PresentationConnectionCloseEvent: true, PresentationReceiver: true, ProgressEvent: true, PromiseRejectionEvent: true, PublicKeyCredential: true, PushEvent: true, PushManager: true, PushMessageData: true, PushSubscription: true, PushSubscriptionOptions: true, Range: true, RelatedApplication: true, ReportBody: true, ReportingObserver: true, ResizeObserver: true, ResizeObserverEntry: true, RTCCertificate: true, RTCDataChannelEvent: true, RTCDTMFToneChangeEvent: true, RTCIceCandidate: true, mozRTCIceCandidate: true, RTCLegacyStatsReport: true, RTCPeerConnectionIceEvent: true, RTCRtpContributingSource: true, RTCRtpReceiver: true, RTCRtpSender: true, RTCSessionDescription: true, mozRTCSessionDescription: true, RTCStatsResponse: true, RTCTrackEvent: true, Screen: true, ScrollState: true, ScrollTimeline: true, SecurityPolicyViolationEvent: true, Selection: true, SensorErrorEvent: true, SharedArrayBuffer: true, SpeechRecognitionAlternative: true, SpeechRecognitionError: true, SpeechRecognitionEvent: true, SpeechSynthesisEvent: true, SpeechSynthesisVoice: true, StaticRange: true, StorageEvent: true, StorageManager: true, StyleMedia: true, StylePropertyMap: true, StylePropertyMapReadonly: true, SyncEvent: true, SyncManager: true, TaskAttributionTiming: true, TextDetector: true, TextEvent: true, TextMetrics: true, TouchEvent: true, TrackDefault: true, TrackEvent: true, TransitionEvent: true, WebKitTransitionEvent: true, TreeWalker: true, TrustedHTML: true, TrustedScriptURL: true, TrustedURL: true, UIEvent: true, UnderlyingSourceBase: true, URLSearchParams: true, VRCoordinateSystem: true, VRDeviceEvent: true, VRDisplayCapabilities: true, VRDisplayEvent: true, VREyeParameters: true, VRFrameData: true, VRFrameOfReference: true, VRPose: true, VRSessionEvent: true, VRStageBounds: true, VRStageBoundsPoint: true, VRStageParameters: true, ValidityState: true, VideoPlaybackQuality: true, VideoTrack: true, VTTRegion: true, WheelEvent: true, WindowClient: true, WorkletAnimation: true, WorkletGlobalScope: true, XPathEvaluator: true, XPathExpression: true, XPathNSResolver: true, XPathResult: true, XMLSerializer: true, XSLTProcessor: true, Bluetooth: true, BluetoothCharacteristicProperties: true, BluetoothRemoteGATTServer: true, BluetoothRemoteGATTService: true, BluetoothUUID: true, BudgetService: true, Cache: true, DOMFileSystemSync: true, DirectoryEntrySync: true, DirectoryReaderSync: true, EntrySync: true, FileEntrySync: true, FileReaderSync: true, FileWriterSync: true, HTMLAllCollection: true, Mojo: true, MojoHandle: true, MojoInterfaceRequestEvent: true, MojoWatcher: true, NFC: true, PagePopupController: true, Report: true, Request: true, ResourceProgressEvent: true, Response: true, SubtleCrypto: true, USBAlternateInterface: true, USBConfiguration: true, USBConnectionEvent: true, USBDevice: true, USBEndpoint: true, USBInTransferResult: true, USBInterface: true, USBIsochronousInTransferPacket: true, USBIsochronousInTransferResult: true, USBIsochronousOutTransferPacket: true, USBIsochronousOutTransferResult: true, USBOutTransferResult: true, WorkerLocation: true, WorkerNavigator: true, Worklet: true, IDBCursor: true, IDBCursorWithValue: true, IDBFactory: true, IDBIndex: true, IDBKeyRange: true, IDBObjectStore: true, IDBObservation: true, IDBObserver: true, IDBObserverChanges: true, IDBVersionChangeEvent: true, SVGAngle: true, SVGAnimatedAngle: true, SVGAnimatedBoolean: true, SVGAnimatedEnumeration: true, SVGAnimatedInteger: true, SVGAnimatedLength: true, SVGAnimatedLengthList: true, SVGAnimatedNumber: true, SVGAnimatedNumberList: true, SVGAnimatedPreserveAspectRatio: true, SVGAnimatedRect: true, SVGAnimatedString: true, SVGAnimatedTransformList: true, SVGMatrix: true, SVGPoint: true, SVGPreserveAspectRatio: true, SVGRect: true, SVGUnitTypes: true, AudioListener: true, AudioParam: true, AudioProcessingEvent: true, AudioTrack: true, AudioWorkletGlobalScope: true, AudioWorkletProcessor: true, OfflineAudioCompletionEvent: true, PeriodicWave: true, WebGLActiveInfo: true, ANGLEInstancedArrays: true, ANGLE_instanced_arrays: true, WebGLBuffer: true, WebGLCanvas: true, WebGLColorBufferFloat: true, WebGLCompressedTextureASTC: true, WebGLCompressedTextureATC: true, WEBGL_compressed_texture_atc: true, WebGLCompressedTextureETC1: true, WEBGL_compressed_texture_etc1: true, WebGLCompressedTextureETC: true, WebGLCompressedTexturePVRTC: true, WEBGL_compressed_texture_pvrtc: true, WebGLCompressedTextureS3TC: true, WEBGL_compressed_texture_s3tc: true, WebGLCompressedTextureS3TCsRGB: true, WebGLContextEvent: true, WebGLDebugRendererInfo: true, WEBGL_debug_renderer_info: true, WebGLDebugShaders: true, WEBGL_debug_shaders: true, WebGLDepthTexture: true, WEBGL_depth_texture: true, WebGLDrawBuffers: true, WEBGL_draw_buffers: true, EXTsRGB: true, EXT_sRGB: true, EXTBlendMinMax: true, EXT_blend_minmax: true, EXTColorBufferFloat: true, EXTColorBufferHalfFloat: true, EXTDisjointTimerQuery: true, EXTDisjointTimerQueryWebGL2: true, EXTFragDepth: true, EXT_frag_depth: true, EXTShaderTextureLOD: true, EXT_shader_texture_lod: true, EXTTextureFilterAnisotropic: true, EXT_texture_filter_anisotropic: true, WebGLFramebuffer: true, WebGLGetBufferSubDataAsync: true, WebGLLoseContext: true, WebGLExtensionLoseContext: true, WEBGL_lose_context: true, OESElementIndexUint: true, OES_element_index_uint: true, OESStandardDerivatives: true, OES_standard_derivatives: true, OESTextureFloat: true, OES_texture_float: true, OESTextureFloatLinear: true, OES_texture_float_linear: true, OESTextureHalfFloat: true, OES_texture_half_float: true, OESTextureHalfFloatLinear: true, OES_texture_half_float_linear: true, OESVertexArrayObject: true, OES_vertex_array_object: true, WebGLProgram: true, WebGLQuery: true, WebGLRenderbuffer: true, WebGLRenderingContext: true, WebGL2RenderingContext: true, WebGLSampler: true, WebGLShader: true, WebGLShaderPrecisionFormat: true, WebGLSync: true, WebGLTexture: true, WebGLTimerQueryEXT: true, WebGLTransformFeedback: true, WebGLUniformLocation: true, WebGLVertexArrayObject: true, WebGLVertexArrayObjectOES: true, WebGL2RenderingContextBase: true, ArrayBuffer: true, ArrayBufferView: false, DataView: true, Float32Array: true, Float64Array: true, Int16Array: true, Int32Array: true, Int8Array: true, Uint16Array: true, Uint32Array: true, Uint8ClampedArray: true, CanvasPixelArray: true, Uint8Array: false, HTMLAudioElement: true, HTMLBRElement: true, HTMLBaseElement: true, HTMLBodyElement: true, HTMLButtonElement: true, HTMLCanvasElement: true, HTMLContentElement: true, HTMLDListElement: true, HTMLDataElement: true, HTMLDataListElement: true, HTMLDetailsElement: true, HTMLDialogElement: true, HTMLDivElement: true, HTMLEmbedElement: true, HTMLFieldSetElement: true, HTMLHRElement: true, HTMLHeadElement: true, HTMLHeadingElement: true, HTMLHtmlElement: true, HTMLIFrameElement: true, HTMLImageElement: true, HTMLInputElement: true, HTMLLIElement: true, HTMLLabelElement: true, HTMLLegendElement: true, HTMLLinkElement: true, HTMLMapElement: true, HTMLMediaElement: true, HTMLMenuElement: true, HTMLMetaElement: true, HTMLMeterElement: true, HTMLModElement: true, HTMLOListElement: true, HTMLObjectElement: true, HTMLOptGroupElement: true, HTMLOptionElement: true, HTMLOutputElement: true, HTMLParagraphElement: true, HTMLParamElement: true, HTMLPictureElement: true, HTMLPreElement: true, HTMLProgressElement: true, HTMLQuoteElement: true, HTMLScriptElement: true, HTMLShadowElement: true, HTMLSlotElement: true, HTMLSourceElement: true, HTMLSpanElement: true, HTMLStyleElement: true, HTMLTableCaptionElement: true, HTMLTableCellElement: true, HTMLTableDataCellElement: true, HTMLTableHeaderCellElement: true, HTMLTableColElement: true, HTMLTableElement: true, HTMLTableRowElement: true, HTMLTableSectionElement: true, HTMLTemplateElement: true, HTMLTextAreaElement: true, HTMLTimeElement: true, HTMLTitleElement: true, HTMLTrackElement: true, HTMLUListElement: true, HTMLUnknownElement: true, HTMLVideoElement: true, HTMLDirectoryElement: true, HTMLFontElement: true, HTMLFrameElement: true, HTMLFrameSetElement: true, HTMLMarqueeElement: true, HTMLElement: false, AccessibleNodeList: true, HTMLAnchorElement: true, HTMLAreaElement: true, Blob: false, CDATASection: true, CharacterData: true, Comment: true, ProcessingInstruction: true, Text: true, CSSPerspective: true, CSSCharsetRule: true, CSSConditionRule: true, CSSFontFaceRule: true, CSSGroupingRule: true, CSSImportRule: true, CSSKeyframeRule: true, MozCSSKeyframeRule: true, WebKitCSSKeyframeRule: true, CSSKeyframesRule: true, MozCSSKeyframesRule: true, WebKitCSSKeyframesRule: true, CSSMediaRule: true, CSSNamespaceRule: true, CSSPageRule: true, CSSRule: true, CSSStyleRule: true, CSSSupportsRule: true, CSSViewportRule: true, CSSStyleDeclaration: true, MSStyleCSSProperties: true, CSS2Properties: true, CSSImageValue: true, CSSKeywordValue: true, CSSNumericValue: true, CSSPositionValue: true, CSSResourceValue: true, CSSUnitValue: true, CSSURLImageValue: true, CSSStyleValue: false, CSSMatrixComponent: true, CSSRotation: true, CSSScale: true, CSSSkew: true, CSSTranslation: true, CSSTransformComponent: false, CSSTransformValue: true, CSSUnparsedValue: true, DataTransferItemList: true, DOMException: true, ClientRectList: true, DOMRectList: true, DOMRectReadOnly: false, DOMStringList: true, DOMTokenList: true, MathMLElement: true, SVGAElement: true, SVGAnimateElement: true, SVGAnimateMotionElement: true, SVGAnimateTransformElement: true, SVGAnimationElement: true, SVGCircleElement: true, SVGClipPathElement: true, SVGDefsElement: true, SVGDescElement: true, SVGDiscardElement: true, SVGEllipseElement: true, SVGFEBlendElement: true, SVGFEColorMatrixElement: true, SVGFEComponentTransferElement: true, SVGFECompositeElement: true, SVGFEConvolveMatrixElement: true, SVGFEDiffuseLightingElement: true, SVGFEDisplacementMapElement: true, SVGFEDistantLightElement: true, SVGFEFloodElement: true, SVGFEFuncAElement: true, SVGFEFuncBElement: true, SVGFEFuncGElement: true, SVGFEFuncRElement: true, SVGFEGaussianBlurElement: true, SVGFEImageElement: true, SVGFEMergeElement: true, SVGFEMergeNodeElement: true, SVGFEMorphologyElement: true, SVGFEOffsetElement: true, SVGFEPointLightElement: true, SVGFESpecularLightingElement: true, SVGFESpotLightElement: true, SVGFETileElement: true, SVGFETurbulenceElement: true, SVGFilterElement: true, SVGForeignObjectElement: true, SVGGElement: true, SVGGeometryElement: true, SVGGraphicsElement: true, SVGImageElement: true, SVGLineElement: true, SVGLinearGradientElement: true, SVGMarkerElement: true, SVGMaskElement: true, SVGMetadataElement: true, SVGPathElement: true, SVGPatternElement: true, SVGPolygonElement: true, SVGPolylineElement: true, SVGRadialGradientElement: true, SVGRectElement: true, SVGScriptElement: true, SVGSetElement: true, SVGStopElement: true, SVGStyleElement: true, SVGElement: true, SVGSVGElement: true, SVGSwitchElement: true, SVGSymbolElement: true, SVGTSpanElement: true, SVGTextContentElement: true, SVGTextElement: true, SVGTextPathElement: true, SVGTextPositioningElement: true, SVGTitleElement: true, SVGUseElement: true, SVGViewElement: true, SVGGradientElement: true, SVGComponentTransferFunctionElement: true, SVGFEDropShadowElement: true, SVGMPathElement: true, Element: false, AbsoluteOrientationSensor: true, Accelerometer: true, AccessibleNode: true, AmbientLightSensor: true, Animation: true, ApplicationCache: true, DOMApplicationCache: true, OfflineResourceList: true, BackgroundFetchRegistration: true, BatteryManager: true, BroadcastChannel: true, CanvasCaptureMediaStreamTrack: true, DedicatedWorkerGlobalScope: true, EventSource: true, FileReader: true, FontFaceSet: true, Gyroscope: true, XMLHttpRequest: true, XMLHttpRequestEventTarget: true, XMLHttpRequestUpload: true, LinearAccelerationSensor: true, Magnetometer: true, MediaDevices: true, MediaKeySession: true, MediaQueryList: true, MediaRecorder: true, MediaSource: true, MediaStream: true, MediaStreamTrack: true, MessagePort: true, MIDIAccess: true, MIDIInput: true, MIDIOutput: true, MIDIPort: true, NetworkInformation: true, Notification: true, OffscreenCanvas: true, OrientationSensor: true, PaymentRequest: true, Performance: true, PermissionStatus: true, PresentationAvailability: true, PresentationConnection: true, PresentationConnectionList: true, PresentationRequest: true, RelativeOrientationSensor: true, RemotePlayback: true, RTCDataChannel: true, DataChannel: true, RTCDTMFSender: true, RTCPeerConnection: true, webkitRTCPeerConnection: true, mozRTCPeerConnection: true, ScreenOrientation: true, Sensor: true, ServiceWorker: true, ServiceWorkerContainer: true, ServiceWorkerGlobalScope: true, ServiceWorkerRegistration: true, SharedWorker: true, SharedWorkerGlobalScope: true, SpeechRecognition: true, webkitSpeechRecognition: true, SpeechSynthesis: true, SpeechSynthesisUtterance: true, VR: true, VRDevice: true, VRDisplay: true, VRSession: true, VisualViewport: true, WebSocket: true, Window: true, DOMWindow: true, Worker: true, WorkerGlobalScope: true, WorkerPerformance: true, BluetoothDevice: true, BluetoothRemoteGATTCharacteristic: true, Clipboard: true, MojoInterfaceInterceptor: true, USB: true, IDBDatabase: true, IDBOpenDBRequest: true, IDBVersionChangeRequest: true, IDBRequest: true, IDBTransaction: true, AnalyserNode: true, RealtimeAnalyserNode: true, AudioBufferSourceNode: true, AudioDestinationNode: true, AudioNode: true, AudioScheduledSourceNode: true, AudioWorkletNode: true, BiquadFilterNode: true, ChannelMergerNode: true, AudioChannelMerger: true, ChannelSplitterNode: true, AudioChannelSplitter: true, ConstantSourceNode: true, ConvolverNode: true, DelayNode: true, DynamicsCompressorNode: true, GainNode: true, AudioGainNode: true, IIRFilterNode: true, MediaElementAudioSourceNode: true, MediaStreamAudioDestinationNode: true, MediaStreamAudioSourceNode: true, OscillatorNode: true, Oscillator: true, PannerNode: true, AudioPannerNode: true, webkitAudioPannerNode: true, ScriptProcessorNode: true, JavaScriptAudioNode: true, StereoPannerNode: true, WaveShaperNode: true, EventTarget: false, File: true, FileList: true, FileWriter: true, HTMLFormElement: true, Gamepad: true, History: true, HTMLCollection: true, HTMLFormControlsCollection: true, HTMLOptionsCollection: true, Location: true, MediaList: true, MIDIInputMap: true, MIDIOutputMap: true, MimeType: true, MimeTypeArray: true, Document: true, DocumentFragment: true, HTMLDocument: true, ShadowRoot: true, XMLDocument: true, Attr: true, DocumentType: true, Node: false, NodeList: true, RadioNodeList: true, Plugin: true, PluginArray: true, RTCStatsReport: true, HTMLSelectElement: true, SourceBuffer: true, SourceBufferList: true, SpeechGrammar: true, SpeechGrammarList: true, SpeechRecognitionResult: true, Storage: true, CSSStyleSheet: true, StyleSheet: true, TextTrack: true, TextTrackCue: true, VTTCue: true, TextTrackCueList: true, TextTrackList: true, TimeRanges: true, Touch: true, TouchList: true, TrackDefaultList: true, URL: true, VideoTrackList: true, CSSRuleList: true, ClientRect: true, DOMRect: true, GamepadList: true, NamedNodeMap: true, MozNamedAttrMap: true, SpeechRecognitionResultList: true, StyleSheetList: true, SVGLength: true, SVGLengthList: true, SVGNumber: true, SVGNumberList: true, SVGPointList: true, SVGStringList: true, SVGTransform: true, SVGTransformList: true, AudioBuffer: true, AudioParamMap: true, AudioTrackList: true, AudioContext: true, webkitAudioContext: true, BaseAudioContext: false, OfflineAudioContext: true}); + A.NativeTypedArray.$nativeSuperclassTag = "ArrayBufferView"; + A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin.$nativeSuperclassTag = "ArrayBufferView"; + A._NativeTypedArrayOfDouble_NativeTypedArray_ListMixin_FixedLengthListMixin.$nativeSuperclassTag = "ArrayBufferView"; + A.NativeTypedArrayOfDouble.$nativeSuperclassTag = "ArrayBufferView"; + A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin.$nativeSuperclassTag = "ArrayBufferView"; + A._NativeTypedArrayOfInt_NativeTypedArray_ListMixin_FixedLengthListMixin.$nativeSuperclassTag = "ArrayBufferView"; + A.NativeTypedArrayOfInt.$nativeSuperclassTag = "ArrayBufferView"; + A._SourceBufferList_EventTarget_ListMixin.$nativeSuperclassTag = "EventTarget"; + A._SourceBufferList_EventTarget_ListMixin_ImmutableListMixin.$nativeSuperclassTag = "EventTarget"; + A._TextTrackList_EventTarget_ListMixin.$nativeSuperclassTag = "EventTarget"; + A._TextTrackList_EventTarget_ListMixin_ImmutableListMixin.$nativeSuperclassTag = "EventTarget"; + })(); + Function.prototype.call$1 = function(a) { + return this(a); + }; + Function.prototype.call$2 = function(a, b) { + return this(a, b); + }; + Function.prototype.call$0 = function() { + return this(); + }; + Function.prototype.call$1$1 = function(a) { + return this(a); + }; + Function.prototype.call$3$3 = function(a, b, c) { + return this(a, b, c); + }; + Function.prototype.call$5 = function(a, b, c, d, e) { + return this(a, b, c, d, e); + }; + Function.prototype.call$3 = function(a, b, c) { + return this(a, b, c); + }; + Function.prototype.call$3$6 = function(a, b, c, d, e, f) { + return this(a, b, c, d, e, f); + }; + Function.prototype.call$1$4 = function(a, b, c, d) { + return this(a, b, c, d); + }; + Function.prototype.call$2$1 = function(a) { + return this(a); + }; + Function.prototype.call$2$5 = function(a, b, c, d, e) { + return this(a, b, c, d, e); + }; + Function.prototype.call$2$4 = function(a, b, c, d) { + return this(a, b, c, d); + }; + Function.prototype.call$4 = function(a, b, c, d) { + return this(a, b, c, d); + }; + Function.prototype.call$2$2 = function(a, b) { + return this(a, b); + }; + Function.prototype.call$3$1 = function(a) { + return this(a); + }; + Function.prototype.call$3$4 = function(a, b, c, d) { + return this(a, b, c, d); + }; + Function.prototype.call$1$2 = function(a, b) { + return this(a, b); + }; + Function.prototype.call$1$0 = function() { + return this(); + }; + Function.prototype.call$2$3 = function(a, b, c) { + return this(a, b, c); + }; + convertAllToFastObject(holders); + convertToFastObject($); + (function(callback) { + if (typeof document === "undefined") { + callback(null); + return; + } + if (typeof document.currentScript != "undefined") { + callback(document.currentScript); + return; + } + var scripts = document.scripts; + function onLoad(event) { + for (var i = 0; i < scripts.length; ++i) + scripts[i].removeEventListener("load", onLoad, false); + callback(event.target); + } + for (var i = 0; i < scripts.length; ++i) + scripts[i].addEventListener("load", onLoad, false); + })(function(currentScript) { + init.currentScript = currentScript; + var callMain = A.main; + if (typeof dartMainRunner === "function") + dartMainRunner(callMain, []); + else + callMain([]); + }); +})(); + +//# sourceMappingURL=host.dart.js.map
diff --git a/pkgs/test/lib/src/runner/browser/static/index.html b/pkgs/test/lib/src/runner/browser/static/index.html new file mode 100644 index 0000000..69617fa --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/static/index.html
@@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html> +<head> + <title>test Browser Host</title> + <link rel="stylesheet" type="text/css" href="host.css" /> +</head> +<body> + <svg id="dart" version="1.1" x="0px" y="0px" width="400px" height="400px" viewBox="0 0 400 400"> + <path id="right-flank" fill="#0083C9" d="M249.379,226.486l-6.676,15.572L166.174,166h58.82c0,0,2.807-0.409,3.645,1.966L249.379,226.486z"/> + <path id="right-ear" fill="#00D2B8" d="M201.84,141.906L166.174,166h58.82c0,0,2.168-0.25,2.645,0.566l-2.694-8.848l-15.024-14.68C207.555,140.329,203.578,140.744,201.84,141.906z"/> + <path id="left-flank" fill="#00D2B8" d="M242.616,241.856l-15.022,6.799l-60.493-21.429c-1.035-0.395-1.101-3.696-1.101-3.696v-57.932L242.616,241.856z"/> + <path id="left-paw" fill="#55DECA" d="M167.003,227.098l60.636,21.558l15.064-6.799L237.224,259h-43.856c0,0-14.077-13.929-18.141-17.993C171.162,236.943,169.162,233.989,167.003,227.098z"/> + <path id="right-paw" fill="#00A4E4" d="M227.676,166.365c0.963,1.401,1.361,2.473,1.361,2.473l20.352,57.648l-6.711,15.37L259,236.463v-44.854c0,0-13.678-13.965-17.741-17.882C237.193,169.811,231.466,166.319,227.676,166.365z"/> + <path id="left-ear" fill="#0083C9" d="M166.769,227.098c0,0-0.769-1.104-0.769-4.355v-57.144l-23.115,34.877c-1.626,1.774-1.567,6.538,1.595,9.755l13.636,13.892L166.769,227.098z"/> + </svg> + <div id="dark"></div> + <svg id="play" version="1.1" x="0px" y="0px" width="80px" height="80px" viewBox="0 0 25 25"> + <defs><filter id="blur"><feGaussianBlur stdDeviation="0.3" id="feGaussianBlur5097" /></filter></defs> + <path d="M 3.777014,1.3715789 A 1.1838119,1.1838119 0 0 0 2.693923,2.5488509 V 22.444746 a 1.1838119,1.1838119 0 0 0 1.765908,1.035999 l 17.235259,-9.95972 a 1.1838119,1.1838119 0 0 0 0,-2.071998 L 4.459831,1.5128519 A 1.1838119,1.1838119 0 0 0 3.777014,1.3715789 z" style="opacity:0.5;stroke:#000000;stroke-width:1;filter:url(#blur)" /> + <path style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.32722104" d="M 3.4770491,1.0714664 A 1.1838119,1.1838119 0 0 0 2.3939589,2.2487382 V 22.144633 a 1.1838119,1.1838119 0 0 0 1.7659079,1.035999 l 17.2352602,-9.95972 a 1.1838119,1.1838119 0 0 0 0,-2.071998 L 4.1598668,1.2127389 A 1.1838119,1.1838119 0 0 0 3.4770491,1.0714664 z" /> + </svg> + <script src="host.dart.js"></script> +</body> +</html>
diff --git a/pkgs/test/lib/src/runner/browser/static/run_wasm_chrome.js b/pkgs/test/lib/src/runner/browser/static/run_wasm_chrome.js new file mode 100644 index 0000000..6e3bda0 --- /dev/null +++ b/pkgs/test/lib/src/runner/browser/static/run_wasm_chrome.js
@@ -0,0 +1,47 @@ +// 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. + +// TODO(joshualitt): Investigate making this a module. Currently, Dart2Wasm is +// broken in D8 with modules because of an issue with async. This may or may not +// affect chrome. +(async () => { + // Fetch and compile Wasm binary. + let data = document.getElementById("WasmBootstrapInfo").dataset; + + // Instantiate the Dart module, importing from the global scope. + let dart2wasmJsRuntime = await import("./" + data.jsruntimeurl); + + // Support three versions of dart2wasm: + // + // (1) Versions before 3.6.0-167.0.dev require the user to compile using the + // browser's `WebAssembly` API, the compiled module needs to be instantiated + // using the JS runtime. + // + // (2) Versions starting with 3.6.0-167.0.dev added helpers for compiling and + // instantiating. + // + // (3) Versions starting with 3.6.0-212.0.dev made compilation functions + // return a new type that comes with instantiation and invoke methods. + + if (dart2wasmJsRuntime.compileStreaming !== undefined) { + // Version (2) or (3). + let compiledModule = await dart2wasmJsRuntime.compileStreaming( + fetch(data.wasmurl), + ); + if (compiledModule.instantiate !== undefined) { + // Version (3). + let instantiatedModule = await compiledModule.instantiate(); + instantiatedModule.invokeMain(); + } else { + // Version (2). + let dartInstance = await dart2wasmJsRuntime.instantiate(compiledModule, {}); + await dart2wasmJsRuntime.invoke(dartInstance); + } + } else { + // Version (1). + let modulePromise = WebAssembly.compileStreaming(fetch(data.wasmurl)); + let dartInstance = await dart2wasmJsRuntime.instantiate(modulePromise, {}); + await dart2wasmJsRuntime.invoke(dartInstance); + } +})();
diff --git a/pkgs/test/lib/src/runner/executable_settings.dart b/pkgs/test/lib/src/runner/executable_settings.dart new file mode 100644 index 0000000..904dc51 --- /dev/null +++ b/pkgs/test/lib/src/runner/executable_settings.dart
@@ -0,0 +1,224 @@ +// 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:io/io.dart'; +import 'package:path/path.dart' as p; +import 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +/// User-provided settings for invoking an executable. +class ExecutableSettings { + /// Additional arguments to pass to the executable. + final List<String> arguments; + + /// The path to the executable on Linux. + /// + /// This may be an absolute path or a basename, in which case it will be + /// looked up on the system path. It may not be relative. + final String? _linuxExecutable; + + /// Potential commands to execute on Mac OS to launch this executable. + /// + /// The values may be an absolute path, or a basename. + /// The chosen command will be the first value from this list which either is + /// a full path that exists, or is a basename. When a basename is included it + /// should always be at the end of the list because it will always terminate + /// the search through the list. Relative paths are not allowed. + final List<String>? _macOSExectuables; + + /// The path to the executable on Windows. + /// + /// This may be an absolute path; a basename, in which case it will be looked + /// up on the system path; or a relative path, in which case it will be looked + /// up relative to the paths in the `LOCALAPPDATA`, `PROGRAMFILES`, and + /// `PROGRAMFILES(X64)` environment variables. + final String? _windowsExecutable; + + /// The environment variable, if any, to use as an override for the default + /// path. + final String? _environmentOverride; + + /// The path to the executable for the current operating system. + String get executable { + if (_environmentOverride != null) { + final envVariable = Platform.environment[_environmentOverride]; + if (envVariable != null) return envVariable; + } + + if (Platform.isMacOS) { + if (_macOSExectuables != null) { + for (final path in _macOSExectuables) { + if (p.basename(path) == path) return path; + if (p.isAbsolute(path)) { + if (File(path).existsSync()) return path; + } else { + throw ArgumentError( + 'Mac OS executable must be a basename or an absolute path.' + ' Got relative path: $path'); + } + } + } + throw ArgumentError('Could not find a command basename or an existing ' + 'path in $_macOSExectuables'); + } + if (!Platform.isWindows) return _linuxExecutable!; + final windowsExecutable = _windowsExecutable!; + if (p.isAbsolute(windowsExecutable)) return windowsExecutable; + if (p.basename(windowsExecutable) == windowsExecutable) { + return windowsExecutable; + } + + var prefixes = [ + Platform.environment['LOCALAPPDATA'], + Platform.environment['PROGRAMFILES'], + Platform.environment['PROGRAMFILES(X86)'] + ]; + + for (var prefix in prefixes) { + if (prefix == null) continue; + + var path = p.join(prefix, windowsExecutable); + if (File(path).existsSync()) return path; + } + + // If we can't find a path that works, return one that doesn't. This will + // cause an "executable not found" error to surface. + return p.join( + prefixes.firstWhere((prefix) => prefix != null, orElse: () => '.')!, + _windowsExecutable); + } + + /// Whether to invoke the browser in headless mode. + /// + /// This is currently only supported by Chrome. + bool get headless => _headless ?? true; + final bool? _headless; + + /// Parses settings from a user-provided YAML mapping. + factory ExecutableSettings.parse(YamlMap settings) { + List<String>? arguments; + var argumentsNode = settings.nodes['arguments']; + if (argumentsNode != null) { + var value = argumentsNode.value; + if (value is String) { + try { + arguments = shellSplit(value); + } on FormatException catch (error) { + throw SourceSpanFormatException(error.message, argumentsNode.span); + } + } else { + throw SourceSpanFormatException( + 'Must be a string.', argumentsNode.span); + } + } + + String? linuxExecutable; + String? macOSExecutable; + String? windowsExecutable; + var executableNode = settings.nodes['executable']; + if (executableNode != null) { + var value = executableNode.value; + if (value is String) { + // Don't check this on Windows because people may want to set relative + // paths in their global config. + if (!Platform.isWindows) { + _assertNotRelative(executableNode as YamlScalar); + } + + linuxExecutable = value; + macOSExecutable = value; + windowsExecutable = value; + } else if (executableNode is YamlMap) { + linuxExecutable = _getExecutable(executableNode.nodes['linux']); + macOSExecutable = _getExecutable(executableNode.nodes['mac_os']); + windowsExecutable = _getExecutable(executableNode.nodes['windows'], + allowRelative: true); + } else { + throw SourceSpanFormatException( + 'Must be a map or a string.', executableNode.span); + } + } + + var headless = true; + var headlessNode = settings.nodes['headless']; + if (headlessNode != null) { + var value = headlessNode.value; + if (value is bool) { + headless = value; + } else { + throw SourceSpanFormatException( + 'Must be a boolean.', headlessNode.span); + } + } + + return ExecutableSettings( + arguments: arguments, + linuxExecutable: linuxExecutable, + macOSExecutable: macOSExecutable, + windowsExecutable: windowsExecutable, + headless: headless); + } + + /// Asserts that [executableNode] is a string or `null` and returns it. + /// + /// If [allowRelative] is `false` (the default), asserts that the value isn't + /// a relative path. + static String? _getExecutable(YamlNode? executableNode, + {bool allowRelative = false}) { + if (executableNode == null || executableNode.value == null) return null; + if (executableNode.value is! String) { + throw SourceSpanFormatException('Must be a string.', executableNode.span); + } + if (!allowRelative) _assertNotRelative(executableNode as YamlScalar); + return executableNode.value as String; + } + + /// Throws a [SourceSpanFormatException] if [executableNode]'s value is a + /// relative POSIX path that's not just a plain basename. + /// + /// We loop up basenames on the PATH and we can resolve absolute paths, but we + /// have no way of interpreting relative paths. + static void _assertNotRelative(YamlScalar executableNode) { + var executable = executableNode.value as String; + if (!p.posix.isRelative(executable)) return; + if (p.posix.basename(executable) == executable) return; + + throw SourceSpanFormatException( + 'Linux and Mac OS executables may not be relative paths.', + executableNode.span); + } + + ExecutableSettings( + {Iterable<String>? arguments, + String? linuxExecutable, + String? macOSExecutable, + List<String>? macOSExecutables, + String? windowsExecutable, + String? environmentOverride, + bool? headless}) + : arguments = arguments == null ? const [] : List.unmodifiable(arguments), + _linuxExecutable = linuxExecutable, + _macOSExectuables = + _normalizeMacExecutable(macOSExecutable, macOSExecutables), + _windowsExecutable = windowsExecutable, + _environmentOverride = environmentOverride, + _headless = headless; + + /// Merges [this] with [other], with [other]'s settings taking priority. + ExecutableSettings merge(ExecutableSettings other) => ExecutableSettings( + arguments: arguments.toList()..addAll(other.arguments), + headless: other._headless ?? _headless, + linuxExecutable: other._linuxExecutable ?? _linuxExecutable, + macOSExecutables: other._macOSExectuables ?? _macOSExectuables, + windowsExecutable: other._windowsExecutable ?? _windowsExecutable); +} + +List<String>? _normalizeMacExecutable( + String? singleArgument, List<String>? listArgument) { + if (listArgument != null) return listArgument; + if (singleArgument != null) return [singleArgument]; + return null; +}
diff --git a/pkgs/test/lib/src/runner/node/platform.dart b/pkgs/test/lib/src/runner/node/platform.dart new file mode 100644 index 0000000..16cd9c6 --- /dev/null +++ b/pkgs/test/lib/src/runner/node/platform.dart
@@ -0,0 +1,380 @@ +// 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:convert'; +import 'dart:io'; + +import 'package:async/async.dart'; +import 'package:node_preamble/preamble.dart' as preamble; +import 'package:package_config/package_config.dart'; +import 'package:path/path.dart' as p; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test_api/backend.dart' + show Compiler, Runtime, StackTraceMapper, SuitePlatform; +import 'package:test_core/src/runner/application_exception.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/configuration.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/dart2js_compiler_pool.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/load_exception.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/package_version.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/platform.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/plugin/customizable_platform.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/plugin/environment.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/plugin/platform_helpers.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/runner_suite.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/suite.dart'; // ignore: implementation_imports +import 'package:test_core/src/runner/wasm_compiler_pool.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/errors.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/io.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/package_config.dart'; // ignore: implementation_imports +import 'package:test_core/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports +import 'package:yaml/yaml.dart'; + +import '../../util/package_map.dart'; +import '../executable_settings.dart'; + +/// A platform that loads tests in Node.js processes. +class NodePlatform extends PlatformPlugin + implements CustomizablePlatform<ExecutableSettings> { + /// The test runner configuration. + final Configuration _config; + + /// The [Dart2JsCompilerPool] managing active instances of `dart2js`. + final _jsCompilers = Dart2JsCompilerPool(['-Dnode=true', '--server-mode']); + final _wasmCompilers = WasmCompilerPool(['-Dnode=true']); + + /// The temporary directory in which compiled JS is emitted. + final _compiledDir = createTempDir(); + + /// Executable settings for [Runtime.nodeJS] and runtimes that extend + /// it. + final _settings = { + Runtime.nodeJS: ExecutableSettings( + linuxExecutable: 'node', + macOSExecutable: 'node', + windowsExecutable: 'node.exe') + }; + + NodePlatform() : _config = Configuration.current; + + @override + ExecutableSettings parsePlatformSettings(YamlMap settings) => + ExecutableSettings.parse(settings); + + @override + ExecutableSettings mergePlatformSettings( + ExecutableSettings settings1, ExecutableSettings settings2) => + settings1.merge(settings2); + + @override + void customizePlatform(Runtime runtime, ExecutableSettings settings) { + var oldSettings = _settings[runtime] ?? _settings[runtime.root]; + if (oldSettings != null) settings = oldSettings.merge(settings); + _settings[runtime] = settings; + } + + @override + Future<RunnerSuite> load(String path, SuitePlatform platform, + SuiteConfiguration suiteConfig, Map<String, Object?> message) async { + if (platform.compiler != Compiler.dart2js && + platform.compiler != Compiler.dart2wasm) { + throw StateError( + 'Unsupported compiler for the Node platform ${platform.compiler}.'); + } + var (channel, stackMapper) = + await _loadChannel(path, platform, suiteConfig); + var controller = deserializeSuite(path, platform, suiteConfig, + const PluginEnvironment(), channel, message); + + controller.channel('test.node.mapper').sink.add(stackMapper?.serialize()); + + return await controller.suite; + } + + /// Loads a [StreamChannel] communicating with the test suite at [path]. + /// + /// Returns that channel along with a [StackTraceMapper] representing the + /// source map for the compiled suite. + Future<(StreamChannel<Object?>, StackTraceMapper?)> _loadChannel(String path, + SuitePlatform platform, SuiteConfiguration suiteConfig) async { + final servers = await _loopback(); + + try { + var (process, stackMapper) = + await _spawnProcess(path, platform, suiteConfig, servers.first.port); + + // Forward Node's standard IO to the print handler so it's associated with + // the load test. + // + // TODO(nweiz): Associate this with the current test being run, if any. + process.stdout.transform(lineSplitter).listen(print); + process.stderr.transform(lineSplitter).listen(print); + + // Wait for the first connection (either over ipv4 or v6). If the proccess + // exits before it connects, throw instead of waiting for a connection + // indefinitely. + var socket = await Future.any([ + StreamGroup.merge(servers).first, + process.exitCode.then((_) => null), + ]); + + if (socket == null) { + throw LoadException( + path, 'Node exited before connecting to the test channel.'); + } + + var channel = StreamChannel(socket.cast<List<int>>(), socket) + .transform(StreamChannelTransformer.fromCodec(utf8)) + .transform(_chunksToLines) + .transform(jsonDocument) + .transformStream(StreamTransformer.fromHandlers(handleDone: (sink) { + process.kill(); + sink.close(); + })); + + return (channel, stackMapper); + } finally { + unawaited(Future.wait<void>(servers.map((s) => + s.close().then<ServerSocket?>((v) => v).onError((_, __) => null)))); + } + } + + /// Spawns a Node.js process that loads the Dart test suite at [path]. + /// + /// Returns that channel along with a [StackTraceMapper] representing the + /// source map for the compiled suite. + Future<(Process, StackTraceMapper?)> _spawnProcess( + String path, + SuitePlatform platform, + SuiteConfiguration suiteConfig, + int socketPort) async { + if (_config.suiteDefaults.precompiledPath != null) { + return _spawnPrecompiledProcess(path, platform.runtime, suiteConfig, + socketPort, _config.suiteDefaults.precompiledPath!); + } else { + return switch (platform.compiler) { + Compiler.dart2js => _spawnNormalJsProcess( + path, platform.runtime, suiteConfig, socketPort), + Compiler.dart2wasm => _spawnNormalWasmProcess( + path, platform.runtime, suiteConfig, socketPort), + _ => throw StateError('Unsupported compiler ${platform.compiler}'), + }; + } + } + + Future<String> _entrypointScriptForTest( + String testPath, SuiteConfiguration suiteConfig) async { + return ''' + ${suiteConfig.metadata.languageVersionComment ?? await rootPackageLanguageVersionComment} + import "package:test/src/bootstrap/node.dart"; + + import "${p.toUri(p.absolute(testPath))}" as test; + + void main() { + internalBootstrapNodeTest(() => test.main); + } + '''; + } + + /// Compiles [testPath] with dart2js, adds the node preamble, and then spawns + /// a Node.js process that loads that Dart test suite. + Future<(Process, StackTraceMapper?)> _spawnNormalJsProcess(String testPath, + Runtime runtime, SuiteConfiguration suiteConfig, int socketPort) async { + var dir = Directory(_compiledDir).createTempSync('test_').path; + var jsPath = p.join(dir, '${p.basename(testPath)}.node_test.dart.js'); + await _jsCompilers.compile( + await _entrypointScriptForTest(testPath, suiteConfig), + jsPath, + suiteConfig, + ); + + // Add the Node.js preamble to ensure that the dart2js output is + // compatible. Use the minified version so the source map remains valid. + var jsFile = File(jsPath); + await jsFile.writeAsString( + preamble.getPreamble(minified: true) + await jsFile.readAsString()); + + StackTraceMapper? mapper; + if (!suiteConfig.jsTrace) { + var mapPath = '$jsPath.map'; + mapper = JSStackTraceMapper(await File(mapPath).readAsString(), + mapUrl: p.toUri(mapPath), + sdkRoot: Uri.parse('org-dartlang-sdk:///sdk'), + packageMap: (await currentPackageConfig).toPackageMap()); + } + + return (await _startProcess(runtime, jsPath, socketPort), mapper); + } + + /// Compiles [testPath] with dart2wasm, adds a JS entrypoint and then spawns + /// a Node.js process loading the compiled test suite. + Future<(Process, StackTraceMapper?)> _spawnNormalWasmProcess(String testPath, + Runtime runtime, SuiteConfiguration suiteConfig, int socketPort) async { + var dir = Directory(_compiledDir).createTempSync('test_').path; + // dart2wasm will emit a .wasm file and a .mjs file responsible for loading + // that file. + var wasmPath = p.join(dir, '${p.basename(testPath)}.node_test.dart.wasm'); + var loader = '${p.basename(testPath)}.node_test.dart.wasm.mjs'; + + // We need to create an additional entrypoint file loading the wasm module. + var jsPath = p.join(dir, '${p.basename(testPath)}.node_test.dart.js'); + + await _wasmCompilers.compile( + await _entrypointScriptForTest(testPath, suiteConfig), + wasmPath, + suiteConfig, + ); + + await File(jsPath).writeAsString(''' +const { createReadStream } = require('fs'); +const { once } = require('events'); +const { PassThrough } = require('stream'); + +const main = async () => { + const { instantiate, invoke } = await import("./$loader"); + + const wasmContents = createReadStream("$wasmPath.wasm"); + const stream = new PassThrough(); + wasmContents.pipe(stream); + + await once(wasmContents, 'open'); + const response = new Response( + stream, + { + headers: { + "Content-Type": "application/wasm" + } + } + ); + const instancePromise = WebAssembly.compileStreaming(response); + const module = await instantiate(instancePromise, {}); + invoke(module); +}; + +main(); +'''); + + return (await _startProcess(runtime, jsPath, socketPort), null); + } + + /// Spawns a Node.js process that loads the Dart test suite at [testPath] + /// under [precompiledPath]. + Future<(Process, StackTraceMapper?)> _spawnPrecompiledProcess( + String testPath, + Runtime runtime, + SuiteConfiguration suiteConfig, + int socketPort, + String precompiledPath) async { + StackTraceMapper? mapper; + var jsPath = p.join(precompiledPath, '$testPath.node_test.dart.js'); + if (!suiteConfig.jsTrace) { + var mapPath = '$jsPath.map'; + mapper = JSStackTraceMapper(await File(mapPath).readAsString(), + mapUrl: p.toUri(mapPath), + sdkRoot: Uri.parse('org-dartlang-sdk:///sdk'), + packageMap: (await findPackageConfig(Directory(precompiledPath)))! + .toPackageMap()); + } + + return (await _startProcess(runtime, jsPath, socketPort), mapper); + } + + /// Starts the Node.js process for [runtime] with [jsPath]. + Future<Process> _startProcess( + Runtime runtime, String jsPath, int socketPort) async { + var settings = _settings[runtime]!; + + var nodeModules = p.absolute('node_modules'); + var nodePath = Platform.environment['NODE_PATH']; + nodePath = nodePath == null ? nodeModules : '$nodePath:$nodeModules'; + + try { + return await Process.start( + settings.executable, + settings.arguments.toList() + ..add(jsPath) + ..add(socketPort.toString()), + environment: {'NODE_PATH': nodePath}); + } catch (error, stackTrace) { + await Future<Never>.error( + ApplicationException( + 'Failed to run ${runtime.name}: ${getErrorMessage(error)}'), + stackTrace); + } + } + + @override + Future<void> close() => _closeMemo.runOnce(() async { + await _jsCompilers.close(); + await _wasmCompilers.close(); + await Directory(_compiledDir).deleteWithRetry(); + }); + final _closeMemo = AsyncMemoizer<void>(); +} + +Future<List<ServerSocket>> _loopback({int remainingRetries = 5}) async { + if (!await _supportsIPv4) { + return [await ServerSocket.bind(InternetAddress.loopbackIPv6, 0)]; + } + + var v4Server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + if (!await _supportsIPv6) return [v4Server]; + + try { + // Reuse the IPv4 server's port so that if [port] is 0, both servers use + // the same ephemeral port. + var v6Server = + await ServerSocket.bind(InternetAddress.loopbackIPv6, v4Server.port); + return [v4Server, v6Server]; + } on SocketException catch (error) { + if (error.osError?.errorCode != _addressInUseErrno) rethrow; + if (remainingRetries == 0) rethrow; + + // A port being available on IPv4 doesn't necessarily mean that the same + // port is available on IPv6. If it's not (which is rare in practice), + // we try again until we find one that's available on both. + unawaited(v4Server.close()); + return await _loopback(remainingRetries: remainingRetries - 1); + } +} + +/// Whether this computer supports binding to IPv6 addresses. +final Future<bool> _supportsIPv6 = () async { + try { + var socket = await ServerSocket.bind(InternetAddress.loopbackIPv6, 0); + unawaited(socket.close()); + return true; + } on SocketException catch (_) { + return false; + } +}(); + +/// Whether this computer supports binding to IPv4 addresses. +final Future<bool> _supportsIPv4 = () async { + try { + var socket = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0); + unawaited(socket.close()); + return true; + } on SocketException catch (_) { + return false; + } +}(); + +/// The error code for an error caused by a port already being in use. +final int _addressInUseErrno = () { + if (Platform.isWindows) return 10048; + if (Platform.isMacOS) return 48; + assert(Platform.isLinux); + return 98; +}(); + +/// A [StreamChannelTransformer] that converts a chunked string channel to a +/// line-by-line channel. +/// +/// Note that this is only safe for channels whose messages are guaranteed not +/// to contain newlines. +final _chunksToLines = StreamChannelTransformer<String, String>( + const LineSplitter(), + StreamSinkTransformer.fromHandlers( + handleData: (data, sink) => sink.add('$data\n')));
diff --git a/pkgs/test/lib/src/runner/node/socket_channel.dart b/pkgs/test/lib/src/runner/node/socket_channel.dart new file mode 100644 index 0000000..95e81de --- /dev/null +++ b/pkgs/test/lib/src/runner/node/socket_channel.dart
@@ -0,0 +1,43 @@ +// 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:convert'; +import 'dart:js_interop'; + +import 'package:stream_channel/stream_channel.dart'; + +@JS('process.argv') +external JSArray<JSString> get _args; + +extension type _Net._(JSObject _) { + external _Socket connect(int port); +} + +extension type _Socket._(JSObject _) { + external void setEncoding(JSString encoding); + external void on(JSString event, JSFunction callback); + external void write(JSString data); +} + +/// Returns a [StreamChannel] of JSON-encodable objects that communicates over a +/// socket whose port is given by `process.argv[2]`. +Future<StreamChannel<Object?>> socketChannel() async { + final net = (await importModule('node:net'.toJS).toDart) as _Net; + + var socket = net.connect(int.parse(_args.toDart[2].toDart)); + socket.setEncoding('utf8'.toJS); + + var socketSink = StreamController<Object?>(sync: true) + ..stream.listen((event) => socket.write('${jsonEncode(event)}\n'.toJS)); + + var socketStream = StreamController<String>(sync: true); + socket.on( + 'data'.toJS, + ((JSString chunk) => socketStream.add(chunk.toDart)).toJS, + ); + + return StreamChannel.withCloseGuarantee( + socketStream.stream.transform(const LineSplitter()).map(jsonDecode), + socketSink); +}
diff --git a/pkgs/test/lib/src/util/math.dart b/pkgs/test/lib/src/util/math.dart new file mode 100644 index 0000000..d6bbbc7 --- /dev/null +++ b/pkgs/test/lib/src/util/math.dart
@@ -0,0 +1,20 @@ +// Copyright (c) 2021, 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:math' as math; + +final _rand = math.Random(); + +/// Returns a random 32 character alphanumeric string ([a-zA-Z0-9]), which is +/// suitable as a url secret. +String randomUrlSecret() { + var buffer = StringBuffer(); + while (buffer.length < 32) { + buffer.write(_alphaChars[_rand.nextInt(_alphaChars.length)]); + } + return buffer.toString(); +} + +const _alphaChars = + '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
diff --git a/pkgs/test/lib/src/util/one_off_handler.dart b/pkgs/test/lib/src/util/one_off_handler.dart new file mode 100644 index 0000000..7667df3 --- /dev/null +++ b/pkgs/test/lib/src/util/one_off_handler.dart
@@ -0,0 +1,46 @@ +// Copyright (c) 2015, 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 'package:path/path.dart' as p; +import 'package:shelf/shelf.dart' as shelf; + +/// A Shelf handler that provides support for one-time handlers. +/// +/// This is useful for handlers that only expect to be hit once before becoming +/// invalid and don't need to have a persistent URL. +class OneOffHandler { + /// A map from URL paths to handlers. + final _handlers = <String, shelf.Handler>{}; + + /// The counter of handlers that have been activated. + var _counter = 0; + + /// The actual [shelf.Handler] that dispatches requests. + shelf.Handler get handler => _onRequest; + + /// Creates a new one-off handler that forwards to [handler]. + /// + /// Returns a string that's the URL path for hitting this handler, relative to + /// the URL for the one-off handler itself. + /// + /// [handler] will be unmounted as soon as it receives a request. + String create(shelf.Handler handler) { + var path = _counter.toString(); + _handlers[path] = handler; + _counter++; + return path; + } + + /// Dispatches [request] to the appropriate handler. + FutureOr<shelf.Response> _onRequest(shelf.Request request) { + var components = p.url.split(request.url.path); + if (components.isEmpty) return shelf.Response.notFound(null); + + var path = components.removeAt(0); + var handler = _handlers.remove(path); + if (handler == null) return shelf.Response.notFound(null); + return handler(request.change(path: path)); + } +}
diff --git a/pkgs/test/lib/src/util/package_map.dart b/pkgs/test/lib/src/util/package_map.dart new file mode 100644 index 0000000..6b30afa --- /dev/null +++ b/pkgs/test/lib/src/util/package_map.dart
@@ -0,0 +1,19 @@ +// Copyright (c) 2020, 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:package_config/package_config.dart'; + +/// Adds methods to convert to a package map on [PackageConfig]. +extension PackageMap on PackageConfig { + /// A package map exactly matching the current package config + Map<String, Uri> toPackageMap() => + {for (var package in packages) package.name: package.packageUriRoot}; + + /// A package map with all the current packages but where the uris are all + /// of the form 'packages/${package.name}'. + Map<String, Uri> toPackagesDirPackageMap() => { + for (var package in packages) + package.name: Uri.parse('packages/${package.name}') + }; +}
diff --git a/pkgs/test/lib/src/util/path_handler.dart b/pkgs/test/lib/src/util/path_handler.dart new file mode 100644 index 0000000..10a8c4e --- /dev/null +++ b/pkgs/test/lib/src/util/path_handler.dart
@@ -0,0 +1,62 @@ +// Copyright (c) 2015, 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 'package:path/path.dart' as p; +import 'package:shelf/shelf.dart' as shelf; + +/// A handler that routes to sub-handlers based on exact path prefixes. +class PathHandler { + /// A trie of path components to handlers. + final _paths = _Node(); + + /// The shelf handler. + shelf.Handler get handler => _onRequest; + + /// Returns middleware that nests all requests beneath the URL prefix + /// [beneath]. + static shelf.Middleware nestedIn(String beneath) { + return (handler) { + var pathHandler = PathHandler()..add(beneath, handler); + return pathHandler.handler; + }; + } + + /// Routes requests at or under [path] to [handler]. + /// + /// If [path] is a parent or child directory of another path in this handler, + /// the longest matching prefix wins. + void add(String path, shelf.Handler handler) { + var node = _paths; + for (var component in p.url.split(path)) { + node = node.children.putIfAbsent(component, _Node.new); + } + node.handler = handler; + } + + FutureOr<shelf.Response> _onRequest(shelf.Request request) { + shelf.Handler? handler; + int? handlerIndex; + _Node? node = _paths; + var components = p.url.split(request.url.path); + for (var i = 0; i < components.length; i++) { + node = node!.children[components[i]]; + if (node == null) break; + if (node.handler == null) continue; + handler = node.handler; + handlerIndex = i; + } + + if (handler == null) return shelf.Response.notFound('Not found.'); + + return handler(request.change( + path: p.url.joinAll(components.take(handlerIndex! + 1)))); + } +} + +/// A trie node. +class _Node { + shelf.Handler? handler; + final children = <String, _Node>{}; +}
diff --git a/pkgs/test/lib/test.dart b/pkgs/test/lib/test.dart new file mode 100644 index 0000000..7bcc673 --- /dev/null +++ b/pkgs/test/lib/test.dart
@@ -0,0 +1,15 @@ +// 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. + +export 'package:matcher/expect.dart'; +// Deprecated exports not surfaced through focused libraries. +// ignore: deprecated_member_use +export 'package:matcher/src/expect/expect.dart' show ErrorFormatter; +// ignore: deprecated_member_use +export 'package:matcher/src/expect/expect_async.dart' show expectAsync; +// ignore: deprecated_member_use +export 'package:matcher/src/expect/throws_matcher.dart' show Throws, throws; +// The non-deprecated API (through a deprecated import). +// ignore: deprecated_member_use +export 'package:test_core/test_core.dart';
diff --git a/pkgs/test/mono_pkg.yaml b/pkgs/test/mono_pkg.yaml new file mode 100644 index 0000000..2aaa4f9 --- /dev/null +++ b/pkgs/test/mono_pkg.yaml
@@ -0,0 +1,51 @@ +# See https://pub.dev/packages/mono_repo + +sdk: +- dev +- pubspec + +stages: +- analyze_and_format: + - group: + - format + - analyze: --fatal-infos + sdk: + - dev + os: + - linux +- unit_test: + - command: xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 0 + - command: xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 1 + - command: xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 2 + - command: xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 3 + - command: xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 4 + - command: dart test --preset travis --total-shards 5 --shard-index 0 + os: + - windows + - osx + sdk: + - pubspec + - command: dart test --preset travis --total-shards 5 --shard-index 1 + os: + - windows + - osx + sdk: + - pubspec + - command: dart test --preset travis --total-shards 5 --shard-index 2 + os: + - windows + - osx + sdk: + - pubspec + - command: dart test --preset travis --total-shards 5 --shard-index 3 + os: + - windows + - osx + sdk: + - pubspec + - command: dart test --preset travis --total-shards 5 --shard-index 4 + os: + - windows + - osx + sdk: + - pubspec
diff --git a/pkgs/test/pubspec.yaml b/pkgs/test/pubspec.yaml new file mode 100644 index 0000000..0ab65ad --- /dev/null +++ b/pkgs/test/pubspec.yaml
@@ -0,0 +1,53 @@ +name: test +version: 1.25.12 +description: >- + A full featured library for writing and running Dart tests across platforms. +repository: https://github.com/dart-lang/test/tree/master/pkgs/test +resolution: workspace + +environment: + sdk: ^3.5.0 + +dependencies: + analyzer: '>=6.0.0 <8.0.0' + async: ^2.5.0 + boolean_selector: ^2.1.0 + collection: ^1.15.0 + coverage: ^1.0.1 + http_multi_server: ^3.0.0 + io: ^1.0.0 + js: '>=0.6.4 <0.8.0' + + # Use a tight version constraint to ensure that a constraint on matcher + # properly constrains all features it provides. + matcher: '>=0.12.16 <0.12.17' + + node_preamble: ^2.0.0 + package_config: ^2.0.0 + path: ^1.8.0 + pool: ^1.5.0 + shelf: ^1.0.0 + shelf_packages_handler: ^3.0.0 + shelf_static: ^1.0.0 + shelf_web_socket: '>=1.0.0 <3.0.0' + source_span: ^1.8.0 + stack_trace: ^1.10.0 + stream_channel: ^2.1.0 + + # Use an exact version until the test_api and test_core package are stable. + test_api: 0.7.4 + test_core: 0.6.8 + + typed_data: ^1.3.0 + web_socket_channel: '>=2.0.0 <4.0.0' + webkit_inspection_protocol: ^1.0.0 + yaml: ^3.0.0 + +dev_dependencies: + fake_async: ^1.0.0 + glob: ^2.0.0 + test_descriptor: ^2.0.0 + test_process: ^2.0.0 + +topics: + - testing
diff --git a/pkgs/test/test/common.dart b/pkgs/test/test/common.dart new file mode 100644 index 0000000..1cad23f --- /dev/null +++ b/pkgs/test/test/common.dart
@@ -0,0 +1,6 @@ +// Copyright (c) 2020, 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'; + +void myTest(String name, void Function() testFn) => test(name, testFn);
diff --git a/pkgs/test/test/io.dart b/pkgs/test/test/io.dart new file mode 100644 index 0000000..b13f8bd --- /dev/null +++ b/pkgs/test/test/io.dart
@@ -0,0 +1,158 @@ +// Copyright (c) 2015, 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:isolate'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; +import 'package:test_process/test_process.dart'; + +/// The path to the root directory of the `test` package. +final Future<String> packageDir = + Isolate.resolvePackageUri(Uri(scheme: 'package', path: 'test/')) + .then((uri) { + var dir = p.dirname(uri!.path); + // If it starts with a `/C:` or other drive letter, remove the leading `/`. + if (dir[0] == '/' && dir[2] == ':') dir = dir.substring(1); + return dir; +}); + +/// The path to the `pub` executable in the current Dart SDK. +final _pubPath = p.absolute(p.join(p.dirname(Platform.resolvedExecutable), + Platform.isWindows ? 'pub.bat' : 'pub')); + +/// The platform-specific message emitted when a nonexistent file is loaded. +final String noSuchFileMessage = Platform.isWindows + ? 'The system cannot find the file specified.' + : 'No such file or directory'; + +/// An operating system name that's different than the current operating system. +final otherOS = Platform.isWindows ? 'mac-os' : 'windows'; + +/// Expects that the entire stdout stream of [test] equals [expected]. +void expectStdoutEquals(TestProcess test, String expected) => + _expectStreamEquals(test.stdoutStream(), expected); + +/// Expects that the entire stderr stream of [test] equals [expected]. +void expectStderrEquals(TestProcess test, String expected) => + _expectStreamEquals(test.stderrStream(), expected); + +/// Expects that the entirety of the line stream [stream] equals [expected]. +void _expectStreamEquals(Stream<String> stream, String expected) { + expect((() async { + var lines = await stream.toList(); + expect(lines.join('\n').trim(), equals(expected.trim())); + })(), completes); +} + +/// Returns a [StreamMatcher] that asserts that the stream emits strings +/// containing each string in [strings] in order. +/// +/// This expects each string in [strings] to match a different string in the +/// stream. +StreamMatcher containsInOrder(Iterable<String> strings) => + emitsInOrder(strings.map((string) => emitsThrough(contains(string)))); + +/// Lazily compile the test package to kernel and re-use that, initialized with +/// [precompileTestExecutable]. +String? _testExecutablePath; + +/// Must be invoked before any call to [runTests], should be invoked in a top +/// level `setUpAll` for best caching results. +Future<void> precompileTestExecutable() async { + if (_testExecutablePath != null) { + throw StateError('Test executable already precompiled'); + } + var tmpDirectory = await Directory.systemTemp.createTemp('test'); + var precompiledPath = p.join(tmpDirectory.path, 'test_runner.dill'); + var result = await Process.run(Platform.executable, [ + 'compile', + 'kernel', + p.url.join(await packageDir, 'bin', 'test.dart'), + '-o', + precompiledPath, + ]); + if (result.exitCode != 0) { + throw StateError( + 'Failed to compile test runner:\n${result.stdout}\n${result.stderr}'); + } + + addTearDown(() async { + await tmpDirectory.delete(recursive: true); + }); + _testExecutablePath = precompiledPath; +} + +/// Runs the test executable with the package root set properly. +/// +/// You must invoke [precompileTestExecutable] before invoking this function. +Future<TestProcess> runTest(Iterable<String> args, + {String? reporter, + String? fileReporter, + int? concurrency, + Map<String, String>? environment, + bool forwardStdio = false, + String? packageConfig, + Iterable<String>? vmArgs}) async { + concurrency ??= 1; + var testExecutablePath = _testExecutablePath; + if (testExecutablePath == null) { + throw StateError( + 'You must call `precompileTestExecutable` before calling `runTest`'); + } + + var allArgs = [ + ...?vmArgs, + testExecutablePath, + '--concurrency=$concurrency', + if (reporter != null) '--reporter=$reporter', + if (fileReporter != null) '--file-reporter=$fileReporter', + ...args, + ]; + + environment ??= {}; + environment.putIfAbsent('_DART_TEST_TESTING', () => 'true'); + + return await runDart(allArgs, + environment: environment, + description: 'dart bin/test.dart', + forwardStdio: forwardStdio, + packageConfig: packageConfig); +} + +/// Runs Dart. +/// +/// If [packageConfig] is provided then that is passed for the `--packages` +/// arg, otherwise the current isolate config is passed. +Future<TestProcess> runDart(Iterable<String> args, + {Map<String, String>? environment, + String? description, + bool forwardStdio = false, + String? packageConfig}) async { + var allArgs = <String>[ + ...Platform.executableArguments.where((arg) => + !arg.startsWith('--package-root=') && !arg.startsWith('--packages=')), + '--packages=${packageConfig ?? await Isolate.packageConfig}', + ...args + ]; + + return await TestProcess.start( + p.absolute(Platform.resolvedExecutable), allArgs, + workingDirectory: d.sandbox, + environment: environment, + description: description, + forwardStdio: forwardStdio); +} + +/// Runs Pub. +Future<TestProcess> runPub(Iterable<String> args, + {Map<String, String>? environment}) { + return TestProcess.start(_pubPath, args, + workingDirectory: d.sandbox, + environment: environment, + description: 'pub ${args.first}'); +}
diff --git a/pkgs/test/test/runner/browser/chrome_test.dart b/pkgs/test/test/runner/browser/chrome_test.dart new file mode 100644 index 0000000..ffec227 --- /dev/null +++ b/pkgs/test/test/runner/browser/chrome_test.dart
@@ -0,0 +1,100 @@ +// Copyright (c) 2015, 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') +@Tags(['chrome']) +library; + +import 'package:test/src/runner/browser/chrome.dart'; +import 'package:test/src/runner/executable_settings.dart'; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../io.dart'; +import '../../utils.dart'; +import 'code_server.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('starts Chrome with the given URL', () async { + var server = await CodeServer.start(); + + server.handleJavaScript(''' +var webSocket = new WebSocket(window.location.href.replace("http://", "ws://")); +webSocket.addEventListener("open", function() { + webSocket.send("loaded!"); +}); +'''); + var webSocket = server.handleWebSocket(); + + var chrome = Chrome(server.url, configuration()); + addTearDown(() => chrome.close()); + + expect(await (await webSocket).stream.first, equals('loaded!')); + }, + // It's not clear why, but this test in particular seems to time out + // when run in parallel with many other tests. + timeout: const Timeout.factor(2)); + + test("a process can be killed synchronously after it's started", () async { + var server = await CodeServer.start(); + var chrome = Chrome(server.url, configuration()); + await chrome.close(); + }); + + test('reports an error in onExit', () { + var chrome = Chrome(Uri.https('dart.dev'), configuration(), + settings: ExecutableSettings( + linuxExecutable: '_does_not_exist', + macOSExecutable: '_does_not_exist', + windowsExecutable: '_does_not_exist')); + expect( + chrome.onExit, + throwsA(isApplicationException( + startsWith('Failed to run Chrome: $noSuchFileMessage')))); + }); + + test('can run successful tests', () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('can run failing tests', () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + test("failure", () => throw TestFailure("oh no")); +} +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('-1: Some tests failed.'))); + await test.shouldExit(1); + }); + + test('can override chrome location with CHROME_EXECUTABLE var', () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +''').create(); + var test = await runTest(['-p', 'chrome', 'test.dart'], + environment: {'CHROME_EXECUTABLE': '/some/bad/path'}); + expect(test.stdout, emitsThrough(contains('Failed to run Chrome:'))); + await test.shouldExit(1); + }); +}
diff --git a/pkgs/test/test/runner/browser/code_server.dart b/pkgs/test/test/runner/browser/code_server.dart new file mode 100644 index 0000000..1aba5de --- /dev/null +++ b/pkgs/test/test/runner/browser/code_server.dart
@@ -0,0 +1,145 @@ +// Copyright (c) 2015, 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:collection'; + +import 'package:http_multi_server/http_multi_server.dart'; +import 'package:shelf/shelf.dart' as shelf; +import 'package:shelf/shelf_io.dart' as shelf_io; +import 'package:shelf_web_socket/shelf_web_socket.dart'; +import 'package:test/test.dart' show TestFailure, printOnFailure; +import 'package:web_socket_channel/web_socket_channel.dart'; + +/// A class that serves Dart and/or JS code and receives WebSocket connections. +class CodeServer { + final _Handler _handler; + + /// The URL of the server (including the port). + final Uri url; + + static Future<CodeServer> start() async { + var server = await HttpMultiServer.loopback(0); + var handler = _Handler(Zone.current.handleUncaughtError); + shelf_io.serveRequests(server, (request) { + if (request.method == 'GET' && request.url.path == 'favicon.ico') { + return shelf.Response.notFound(null); + } else { + return handler(request); + } + }); + + return CodeServer._(handler, Uri.parse('http://localhost:${server.port}')); + } + + CodeServer._(this._handler, this.url); + + /// Sets up a handler for the root of the server, "/", that serves a basic + /// HTML page with a script tag that will run [javaScript]. + void handleJavaScript(String javaScript) { + _handler.expect('GET', '/', (_) { + return shelf.Response.ok(''' +<!doctype html> +<html> +<head> + <script src="index.js"></script> +</head> +</html> +''', headers: {'content-type': 'text/html'}); + }); + + _handler.expect('GET', '/index.js', (_) { + return shelf.Response.ok(javaScript, + headers: {'content-type': 'application/javascript'}); + }); + } + + /// Handles a WebSocket connection to the root of the server, and returns a + /// future that will complete to the WebSocket. + Future<WebSocketChannel> handleWebSocket() { + var completer = Completer<WebSocketChannel>(); + // Note: the WebSocketChannel type below is needed for compatibility with + // package:shelf_web_socket v2. + _handler.expect('GET', '/', webSocketHandler((WebSocketChannel ws, _) { + completer.complete(ws); + })); + return completer.future; + } +} + +/// A [shelf.Handler] that handles requests as specified by [expect]. +class _Handler { + /// A callback called whenever an unexpected exception is thrown. + /// + /// This is used over throwing errors directly since the request handler might + /// not be running in the same error zone as the test. + final void Function(Object, StackTrace) _onError; + + /// The queue of expected requests to this handler. + final _expectations = Queue<_Expectation>(); + + /// Creates a new handler that handles requests using handlers provided by + /// [expect]. + _Handler(this._onError); + + /// Expects that a single HTTP request with the given [method] and [path] will + /// be made to [this]. + /// + /// The [path] should be root-relative; that is, it should start with "/". + /// + /// When a matching request is made, [handler] is used to handle that request. + /// + /// If this is called multiple times, the requests are expected to occur in + /// the same order. + void expect(String method, String path, shelf.Handler handler) { + _expectations.add(_Expectation(method, path, handler)); + } + + /// The implementation of [shelf.Handler]. + FutureOr<shelf.Response> call(shelf.Request request) async { + const description = 'ShelfTesthandler'; + var requestInfo = '${request.method} /${request.url}'; + printOnFailure('[$description] $requestInfo'); + + try { + if (_expectations.isEmpty) { + throw TestFailure( + '$description received unexpected request $requestInfo.'); + } + + var expectation = _expectations.removeFirst(); + if ((expectation.method != null && + expectation.method != request.method) || + (expectation.path != '/${request.url.path}' && + expectation.path != null)) { + var message = '$description received unexpected request $requestInfo.'; + if (expectation.method != null) { + message += '\nExpected ${expectation.method} ${expectation.path}.'; + } + throw TestFailure(message); + } + + return await expectation.handler(request); + } on shelf.HijackException catch (_) { + rethrow; + } catch (error, stackTrace) { + _onError(error, stackTrace); + return shelf.Response.internalServerError(body: '$error'); + } + } +} + +/// A single expectation for an HTTP request sent to a [_Handler]. +class _Expectation { + /// The expected request method, or [null] if this allows any requests. + final String? method; + + /// The expected request path, or [null] if this allows any requests. + final String? path; + + /// The handler to use for requests that match this expectation. + final shelf.Handler handler; + + _Expectation(this.method, this.path, this.handler); +}
diff --git a/pkgs/test/test/runner/browser/compact_reporter_test.dart b/pkgs/test/test/runner/browser/compact_reporter_test.dart new file mode 100644 index 0000000..753ffa1 --- /dev/null +++ b/pkgs/test/test/runner/browser/compact_reporter_test.dart
@@ -0,0 +1,35 @@ +// Copyright (c) 2015, 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 '../../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('prints the platform name when running on multiple platforms', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +''').create(); + + var test = await runTest( + ['-p', 'chrome', '-p', 'vm', '-j', '1', 'test.dart'], + reporter: 'compact'); + + expect(test.stdout, containsInOrder(['[Chrome, Dart2Js]', '[VM, Kernel]'])); + await test.shouldExit(0); + }, tags: 'chrome'); +}
diff --git a/pkgs/test/test/runner/browser/expanded_reporter_test.dart b/pkgs/test/test/runner/browser/expanded_reporter_test.dart new file mode 100644 index 0000000..875aaba --- /dev/null +++ b/pkgs/test/test/runner/browser/expanded_reporter_test.dart
@@ -0,0 +1,36 @@ +// Copyright (c) 2015, 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 '../../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('prints the platform name when running on multiple platforms', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +''').create(); + + var test = await runTest([ + '-r', 'expanded', '-p', 'chrome', '-p', 'vm', '-j', '1', // + 'test.dart' + ]); + + expect(test.stdoutStream(), emitsThrough(contains('[VM, Kernel]'))); + expect(test.stdout, emitsThrough(contains('[Chrome, Dart2Js]'))); + await test.shouldExit(0); + }, tags: ['chrome']); +}
diff --git a/pkgs/test/test/runner/browser/firefox_html_test.dart b/pkgs/test/test/runner/browser/firefox_html_test.dart new file mode 100644 index 0000000..c9a5302 --- /dev/null +++ b/pkgs/test/test/runner/browser/firefox_html_test.dart
@@ -0,0 +1,18 @@ +// Copyright (c) 2015, 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('firefox') +library; + +import 'package:test/src/runner/browser/dom.dart' as dom; +import 'package:test/test.dart'; + +void main() { + // Regression test for #274. Firefox doesn't compute styles within hidden + // iframes (https://bugzilla.mozilla.org/show_bug.cgi?id=548397), so we have + // to do some special stuff to make sure tests that care about that work. + test('getComputedStyle() works', () { + expect(dom.window.getComputedStyle(dom.document.body!), isNotNull); + }); +}
diff --git a/pkgs/test/test/runner/browser/firefox_test.dart b/pkgs/test/test/runner/browser/firefox_test.dart new file mode 100644 index 0000000..864f49c --- /dev/null +++ b/pkgs/test/test/runner/browser/firefox_test.dart
@@ -0,0 +1,115 @@ +// Copyright (c) 2015, 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') +@Tags(['firefox']) +library; + +import 'package:test/src/runner/browser/firefox.dart'; +import 'package:test/src/runner/executable_settings.dart'; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../io.dart'; +import '../../utils.dart'; +import 'code_server.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('starts Firefox with the given URL', () async { + var server = await CodeServer.start(); + + server.handleJavaScript(''' +var webSocket = new WebSocket(window.location.href.replace("http://", "ws://")); +webSocket.addEventListener("open", function() { + webSocket.send("loaded!"); +}); +'''); + var webSocket = server.handleWebSocket(); + + var firefox = Firefox(server.url); + addTearDown(() => firefox.close()); + + expect(await (await webSocket).stream.first, equals('loaded!')); + }); + + test("a process can be killed synchronously after it's started", () async { + var server = await CodeServer.start(); + + var firefox = Firefox(server.url); + await firefox.close(); + }); + + test('reports an error in onExit', () { + var firefox = Firefox(Uri.https('dart.dev'), + settings: ExecutableSettings( + linuxExecutable: '_does_not_exist', + macOSExecutable: '_does_not_exist', + windowsExecutable: '_does_not_exist')); + expect( + firefox.onExit, + throwsA(isApplicationException( + startsWith('Failed to run Firefox: $noSuchFileMessage')))); + }); + + test('can run successful tests', () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +''').create(); + + var test = await runTest(['-p', 'firefox', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('can run failing tests', () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + test("failure", () => throw TestFailure("oh no")); +} +''').create(); + + var test = await runTest(['-p', 'firefox', 'test.dart']); + expect(test.stdout, emitsThrough(contains('-1: Some tests failed.'))); + await test.shouldExit(1); + }); + + test('can override firefox location with FIREFOX_EXECUTABLE var', () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +''').create(); + var test = await runTest(['-p', 'firefox', 'test.dart'], + environment: {'FIREFOX_EXECUTABLE': '/some/bad/path'}); + expect(test.stdout, emitsThrough(contains('Failed to run Firefox:'))); + await test.shouldExit(1); + }); + + test('not impacted by CHROME_EXECUTABLE var', () async { + await d.file('test.dart', ''' +import 'dart:html'; + +import 'package:test/test.dart'; + +void main() { + test("success", () { + assert(window.navigator.vendor != 'Google Inc.'); + }); +} +''').create(); + var test = await runTest(['-p', 'firefox', 'test.dart'], + environment: {'CHROME_EXECUTABLE': '/some/bad/path'}); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); +}
diff --git a/pkgs/test/test/runner/browser/loader_test.dart b/pkgs/test/test/runner/browser/loader_test.dart new file mode 100644 index 0000000..17cdb9a --- /dev/null +++ b/pkgs/test/test/runner/browser/loader_test.dart
@@ -0,0 +1,181 @@ +// Copyright (c) 2015, 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') +@Tags(['chrome']) +library; + +import 'dart:io'; + +import 'package:path/path.dart' as p; +import 'package:test/src/runner/browser/platform.dart'; +import 'package:test/test.dart'; +import 'package:test_api/src/backend/runtime.dart'; +import 'package:test_api/src/backend/state.dart'; +import 'package:test_api/src/backend/test.dart'; +import 'package:test_core/src/runner/hack_register_platform.dart'; +import 'package:test_core/src/runner/loader.dart'; +import 'package:test_core/src/runner/runner_suite.dart'; +import 'package:test_core/src/runner/runner_test.dart'; +import 'package:test_core/src/runner/runtime_selection.dart'; +import 'package:test_core/src/runner/suite.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../utils.dart'; + +late Loader _loader; + +/// A configuration that loads suites on Chrome. +final _chrome = + SuiteConfiguration.runtimes([RuntimeSelection(Runtime.chrome.identifier)]); + +void main() { + setUp(() async { + // The default loader doesn't have the platforms registered. + registerPlatformPlugin([ + Runtime.chrome, + ], () => BrowserPlatform.start(root: d.sandbox)); + + _loader = Loader(); + + await d.file('a_test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + test("failure", () => throw TestFailure('oh no')); + test("error", () => throw 'oh no'); + } + ''').create(); + }); + + tearDown(() => _loader.close()); + + group('.loadFile()', () { + late RunnerSuite suite; + setUp(() async { + var suites = await _loader + .loadFile(p.join(d.sandbox, 'a_test.dart'), _chrome) + .toList(); + + expect(suites, hasLength(1)); + var loadSuite = suites.first; + suite = (await loadSuite.getSuite())!; + }); + + test('returns a suite with the file path and platform', () { + expect(suite.path, equals(p.join(d.sandbox, 'a_test.dart'))); + expect(suite.platform.runtime, equals(Runtime.chrome)); + expect(suite.platform.compiler, equals(Runtime.chrome.defaultCompiler)); + }); + + test('returns tests with the correct names', () { + expect(suite.group.entries, hasLength(3)); + expect(suite.group.entries[0].name, equals('success')); + expect(suite.group.entries[1].name, equals('failure')); + expect(suite.group.entries[2].name, equals('error')); + }); + + test('can load and run a successful test', () { + var liveTest = (suite.group.entries[0] as RunnerTest).load(suite); + + expectStates(liveTest, [ + const State(Status.running, Result.success), + const State(Status.complete, Result.success) + ]); + expectErrors(liveTest, []); + + return liveTest.run().whenComplete(() => liveTest.close()); + }); + + test('can load and run a failing test', () { + var liveTest = (suite.group.entries[1] as RunnerTest).load(suite); + expectSingleFailure(liveTest); + return liveTest.run().whenComplete(() => liveTest.close()); + }); + }); + + test('loads tests that are defined asynchronously', () async { + File(p.join(d.sandbox, 'a_test.dart')).writeAsStringSync(''' +import 'dart:async'; + +import 'package:test/test.dart'; + +Future main() { + return Future(() { + test("success", () {}); + + return Future(() { + test("failure", () => throw TestFailure('oh no')); + + return Future(() { + test("error", () => throw 'oh no'); + }); + }); + }); +} +'''); + + var suites = await _loader + .loadFile(p.join(d.sandbox, 'a_test.dart'), _chrome) + .toList(); + expect(suites, hasLength(1)); + var loadSuite = suites.first; + var suite = (await loadSuite.getSuite())!; + expect(suite.group.entries, hasLength(3)); + expect(suite.group.entries[0].name, equals('success')); + expect(suite.group.entries[1].name, equals('failure')); + expect(suite.group.entries[2].name, equals('error')); + }); + + test('loads a suite both in the browser and the VM', () async { + var path = p.join(d.sandbox, 'a_test.dart'); + + var suites = await _loader + .loadFile( + path, + SuiteConfiguration.runtimes([ + RuntimeSelection(Runtime.vm.identifier), + RuntimeSelection(Runtime.chrome.identifier) + ])) + .asyncMap((loadSuite) => loadSuite.getSuite()) + .cast<RunnerSuite>() + .toList(); + expect(suites[0].platform.runtime, equals(Runtime.vm)); + expect(suites[0].platform.compiler, equals(Runtime.vm.defaultCompiler)); + expect(suites[0].path, equals(path)); + expect(suites[1].platform.runtime, equals(Runtime.chrome)); + expect(suites[1].platform.compiler, equals(Runtime.chrome.defaultCompiler)); + expect(suites[1].path, equals(path)); + + for (var suite in suites) { + expect(suite.group.entries, hasLength(3)); + expect(suite.group.entries[0].name, equals('success')); + expect(suite.group.entries[1].name, equals('failure')); + expect(suite.group.entries[2].name, equals('error')); + } + }); + + test('a print in a loaded file is piped through the LoadSuite', () async { + File(p.join(d.sandbox, 'a_test.dart')).writeAsStringSync(''' +void main() { + print('print within test'); +} +'''); + var suites = await _loader + .loadFile(p.join(d.sandbox, 'a_test.dart'), _chrome) + .toList(); + expect(suites, hasLength(1)); + var loadSuite = suites.first; + + var liveTest = (loadSuite.group.entries.single as Test).load(loadSuite); + // Skip the "Compiled" message from dart2js. + expect(liveTest.onMessage.skip(1).first.then((message) => message.text), + completion(equals('print within test'))); + await liveTest.run(); + expectTestPassed(liveTest); + }); +}
diff --git a/pkgs/test/test/runner/browser/microsoft_edge_test.dart b/pkgs/test/test/runner/browser/microsoft_edge_test.dart new file mode 100644 index 0000000..a48bfd2 --- /dev/null +++ b/pkgs/test/test/runner/browser/microsoft_edge_test.dart
@@ -0,0 +1,91 @@ +// Copyright (c) 2023, 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') +@Tags(['edge']) +library; + +import 'package:test/src/runner/browser/microsoft_edge.dart'; +import 'package:test/src/runner/executable_settings.dart'; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../io.dart'; +import '../../utils.dart'; +import 'code_server.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('starts edge with the given URL', () async { + var server = await CodeServer.start(); + + server.handleJavaScript(''' +var webSocket = new WebSocket(window.location.href.replace("http://", "ws://")); +webSocket.addEventListener("open", function() { + webSocket.send("loaded!"); +}); +'''); + var webSocket = server.handleWebSocket(); + + var edge = MicrosoftEdge(server.url, configuration()); + addTearDown(() => edge.close()); + + expect(await (await webSocket).stream.first, equals('loaded!')); + }, timeout: const Timeout.factor(2)); + + test('reports an error in onExit', () { + var edge = MicrosoftEdge(Uri.parse('https://dart.dev'), configuration(), + settings: ExecutableSettings( + linuxExecutable: '_does_not_exist', + macOSExecutable: '_does_not_exist', + windowsExecutable: '_does_not_exist')); + expect( + edge.onExit, + throwsA(isApplicationException( + startsWith('Failed to run Edge: $noSuchFileMessage')))); + }); + + test('can run successful tests', () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +''').create(); + + var test = await runTest(['-p', 'edge', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('can run failing tests', () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + test("failure", () => throw TestFailure("oh no")); +} +''').create(); + + var test = await runTest(['-p', 'edge', 'test.dart']); + expect(test.stdout, emitsThrough(contains('-1: Some tests failed.'))); + await test.shouldExit(1); + }); + + test('can override edge location with MS_EDGE_EXECUTABLE var', () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +''').create(); + var test = await runTest(['-p', 'edge', 'test.dart'], + environment: {'MS_EDGE_EXECUTABLE': '/some/bad/path'}); + expect(test.stdout, emitsThrough(contains('Failed to run Edge:'))); + await test.shouldExit(1); + }); +}
diff --git a/pkgs/test/test/runner/browser/runner_test.dart b/pkgs/test/test/runner/browser/runner_test.dart new file mode 100644 index 0000000..c55cbb8 --- /dev/null +++ b/pkgs/test/test/runner/browser/runner_test.dart
@@ -0,0 +1,978 @@ +// Copyright (c) 2015, 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 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../io.dart'; + +final _success = ''' +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +'''; + +final _failure = ''' +import 'package:test/test.dart'; + +void main() { + test("failure", () => throw TestFailure("oh no")); +} +'''; + +void main() { + setUpAll(precompileTestExecutable); + + group('fails gracefully if', () { + test('a test file fails to compile', () async { + await d.file('test.dart', 'invalid Dart file').create(); + var test = await runTest(['-p', 'chrome', 'test.dart']); + + expect( + test.stdout, + containsInOrder([ + 'Error: Compilation failed.', + '-1: loading test.dart [E]', + 'Failed to load "test.dart": dart2js failed.' + ])); + await test.shouldExit(1); + }, tags: 'chrome'); + + test('a test file throws', () async { + await d.file('test.dart', "void main() => throw 'oh no';").create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": oh no' + ])); + await test.shouldExit(1); + }, tags: 'chrome'); + + test("a test file doesn't have a main defined", () async { + await d.file('test.dart', 'void foo() {}').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": No top-level main() function defined.' + ])); + await test.shouldExit(1); + }, tags: 'chrome', skip: 'https://github.com/dart-lang/test/issues/894'); + + test('a test file has a non-function main', () async { + await d.file('test.dart', 'int main;').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": Top-level main getter is not a function.' + ])); + await test.shouldExit(1); + }, tags: 'chrome', skip: 'https://github.com/dart-lang/test/issues/894'); + + test('a test file has a main with arguments', () async { + await d.file('test.dart', 'void main(arg) {}').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": Top-level main() function takes arguments.' + ])); + await test.shouldExit(1); + }, tags: 'chrome'); + + test('a custom HTML file has no script tag', () async { + await d.file('test.dart', 'void main() {}').create(); + + await d.file('test.html', ''' +<html> +<head> + <link rel="x-dart-test" href="test.dart"> +</head> +</html> +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": "test.html" must contain ' + '<script src="packages/test/dart.js"></script>.' + ])); + await test.shouldExit(1); + }, tags: 'chrome'); + + test('a custom HTML file has no link', () async { + await d.file('test.dart', 'void main() {}').create(); + + await d.file('test.html', ''' +<html> +<head> + <script src="packages/test/dart.js"></script> +</head> +</html> +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": Expected exactly 1 ' + '<link rel="x-dart-test"> in test.html, found 0.' + ])); + await test.shouldExit(1); + }, tags: 'chrome'); + + test('a custom HTML file has too many links', () async { + await d.file('test.dart', 'void main() {}').create(); + + await d.file('test.html', ''' +<html> +<head> + <link rel='x-dart-test' href='test.dart'> + <link rel='x-dart-test' href='test.dart'> + <script src="packages/test/dart.js"></script> +</head> +</html> +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": Expected exactly 1 ' + '<link rel="x-dart-test"> in test.html, found 2.' + ])); + await test.shouldExit(1); + }, tags: 'chrome'); + + test('a custom HTML file has no href in the link', () async { + await d.file('test.dart', 'void main() {}').create(); + + await d.file('test.html', ''' +<html> +<head> + <link rel='x-dart-test'> + <script src="packages/test/dart.js"></script> +</head> +</html> +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": Expected <link rel="x-dart-test"> in ' + 'test.html to have an "href" attribute.' + ])); + await test.shouldExit(1); + }, tags: 'chrome'); + + test('a custom HTML file has an invalid test URL', () async { + await d.file('test.dart', 'void main() {}').create(); + + await d.file('test.html', ''' +<html> +<head> + <link rel='x-dart-test' href='wrong.dart'> + <script src="packages/test/dart.js"></script> +</head> +</html> +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": Failed to load script at ' + ])); + await test.shouldExit(1); + }, tags: 'chrome'); + + test( + 'still errors even with a custom HTML template set since it will take precedence', + () async { + await d.file('test.dart', 'void main() {}').create(); + + await d.file('test.html', ''' +<html> +<head> + <link rel="x-dart-test" href="test.dart"> +</head> +</html> +''').create(); + + await d + .file( + 'global_test.yaml', + jsonEncode( + {'custom_html_template_path': 'html_template.html.tpl'})) + .create(); + + await d.file('html_template.html.tpl', ''' +<html> +<head> + {{testScript}} + <script src="packages/test/dart.js"></script> +</head> +<body> + <div id="foo"></div> +</body> +</html> +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart'], + environment: {'DART_TEST_CONFIG': 'global_test.yaml'}); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": "test.html" must contain ' + '<script src="packages/test/dart.js"></script>.' + ])); + await test.shouldExit(1); + }, tags: 'chrome'); + + group('with a custom HTML template', () { + setUp(() async { + await d.file('test.dart', _success).create(); + await d + .file( + 'global_test.yaml', + jsonEncode( + {'custom_html_template_path': 'html_template.html.tpl'})) + .create(); + }); + + test('that does not exist', () async { + var test = await runTest(['-p', 'chrome', 'test.dart'], + environment: {'DART_TEST_CONFIG': 'global_test.yaml'}); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": "html_template.html.tpl" does not exist or is not readable' + ])); + await test.shouldExit(1); + }, tags: 'chrome'); + + test("that doesn't contain the {{testScript}} tag", () async { + await d.file('html_template.html.tpl', ''' +<html> +<head> + <script src="packages/test/dart.js"></script> +</head> +<body> + <div id="foo"></div> +</body> +</html> +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart'], + environment: {'DART_TEST_CONFIG': 'global_test.yaml'}); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": "html_template.html.tpl" must contain exactly one {{testScript}} placeholder' + ])); + await test.shouldExit(1); + }, tags: 'chrome'); + + test('that contains more than one {{testScript}} tag', () async { + await d.file('html_template.html.tpl', ''' +<html> +<head> + {{testScript}} + {{testScript}} + <script src="packages/test/dart.js"></script> +</head> +<body> + <div id="foo"></div> +</body> +</html> +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart'], + environment: {'DART_TEST_CONFIG': 'global_test.yaml'}); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": "html_template.html.tpl" must contain exactly one {{testScript}} placeholder' + ])); + await test.shouldExit(1); + }, tags: 'chrome'); + + test('that has no script tag', () async { + await d.file('html_template.html.tpl', ''' +<html> +<head> + {{testScript}} +</head> +</html> +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart'], + environment: {'DART_TEST_CONFIG': 'global_test.yaml'}); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": "html_template.html.tpl" must contain ' + '<script src="packages/test/dart.js"></script>.' + ])); + await test.shouldExit(1); + }, tags: 'chrome'); + + test('that is named like the test file', () async { + await d.file('test.html', ''' +<html> +<head> + {{testScript}} + <script src="packages/test/dart.js"></script> +</head> +</html> +''').create(); + + await d + .file('global_test_2.yaml', + jsonEncode({'custom_html_template_path': 'test.html'})) + .create(); + var test = await runTest(['-p', 'chrome', 'test.dart'], + environment: {'DART_TEST_CONFIG': 'global_test_2.yaml'}); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": template file "test.html" cannot be named ' + 'like the test file.' + ])); + await test.shouldExit(1); + }); + }); + }); + + group('runs successful tests', () { + test('on a browser and the VM', () async { + await d.file('test.dart', _success).create(); + var test = await runTest(['-p', 'chrome', '-p', 'vm', 'test.dart']); + + expect(test.stdout, emitsThrough(contains('+2: All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + test('with setUpAll', () async { + await d.file('test.dart', r''' + import 'package:test/test.dart'; + + void main() { + setUpAll(() => print("in setUpAll")); + + test("test", () {}); + } + ''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+0: (setUpAll)'))); + expect(test.stdout, emits('in setUpAll')); + await test.shouldExit(0); + }, tags: 'chrome'); + + test('with tearDownAll', () async { + await d.file('test.dart', r''' + import 'package:test/test.dart'; + + void main() { + tearDownAll(() => print("in tearDownAll")); + + test("test", () {}); + } + ''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: (tearDownAll)'))); + expect(test.stdout, emits('in tearDownAll')); + await test.shouldExit(0); + }, tags: 'chrome'); + + // Regression test; this broke in 0.12.0-beta.9. + test('on a file in a subdirectory', () async { + await d.dir('dir', [d.file('test.dart', _success)]).create(); + + var test = await runTest(['-p', 'chrome', 'dir/test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + group('with a custom HTML template file', () { + group('without a {{testName}} tag', () { + setUp(() async { + await d + .file( + 'global_test.yaml', + jsonEncode( + {'custom_html_template_path': 'html_template.html.tpl'})) + .create(); + await d.file('html_template.html.tpl', ''' + <html> + <head> + {{testScript}} + <script src="packages/test/dart.js"></script> + </head> + <body> + <div id="foo"></div> + </body> + </html> + ''').create(); + + await d.file('test.dart', ''' + import 'package:test/src/runner/browser/dom.dart' as dom; + import 'package:test/test.dart'; + + void main() { + test("success", () { + expect(dom.document.querySelector('#foo'), isNotNull); + }); + } + ''').create(); + }); + + test('on Chrome', () async { + var test = await runTest(['-p', 'chrome', 'test.dart'], + environment: {'DART_TEST_CONFIG': 'global_test.yaml'}); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + }); + + group('with a {{testName}} tag', () { + setUp(() async { + await d + .file( + 'global_test.yaml', + jsonEncode( + {'custom_html_template_path': 'html_template.html.tpl'})) + .create(); + await d.file('html_template.html.tpl', ''' + <html> + <head> + <title>{{testName}}</title> + {{testScript}} + <script src="packages/test/dart.js"></script> + </head> + <body> + <div id="foo"></div> + </body> + </html> + ''').create(); + + await d.file('test-with-title.dart', ''' + import 'package:test/src/runner/browser/dom.dart' as dom; + import 'package:test/test.dart'; + + void main() { + test("success", () { + expect(dom.document.querySelector('#foo'), isNotNull); + }); + test("title", () { + expect(dom.document.title, 'test-with-title.dart'); + }); + } + ''').create(); + }); + + test('on Chrome', () async { + var test = await runTest(['-p', 'chrome', 'test-with-title.dart'], + environment: {'DART_TEST_CONFIG': 'global_test.yaml'}); + expect(test.stdout, emitsThrough(contains('+2: All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + }); + }); + + group('with a custom HTML file', () { + setUp(() async { + await d.file('test.dart', ''' +import 'package:test/src/runner/browser/dom.dart' as dom; +import 'package:test/test.dart'; + +void main() { + test("success", () { + expect(dom.document.querySelector('#foo'), isNotNull); + }); +} +''').create(); + + await d.file('test.html', ''' +<html> +<head> + <link rel='x-dart-test' href='test.dart'> + <script src="packages/test/dart.js"></script> +</head> +<body> + <div id="foo"></div> +</body> +</html> +''').create(); + }); + + test('on Chrome', () async { + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + // Regression test for https://github.com/dart-lang/test/issues/82. + test('ignores irrelevant link tags', () async { + await d.file('test.html', ''' +<html> +<head> + <link rel='x-dart-test-not'> + <link rel='other' href='test.dart'> + <link rel='x-dart-test' href='test.dart'> + <script src="packages/test/dart.js"></script> +</head> +<body> + <div id="foo"></div> +</body> +</html> +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + test('takes precedence over provided HTML template', () async { + await d + .file( + 'global_test.yaml', + jsonEncode( + {'custom_html_template_path': 'html_template.html.tpl'})) + .create(); + await d.file('html_template.html.tpl', ''' +<html> +<head> + {{testScript}} + <script src="packages/test/dart.js"></script> +</head> +<body> + <div id="not-foo"></div> +</body> +</html> +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart'], + environment: {'DART_TEST_CONFIG': 'global_test.yaml'}); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + }); + }); + + group('runs failing tests', () { + test('that fail only on the browser', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + test("test", () { + if (p.style == p.Style.url) throw TestFailure("oh no"); + }); +} +''').create(); + + var test = await runTest(['-p', 'chrome', '-p', 'vm', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1 -1: Some tests failed.'))); + await test.shouldExit(1); + }, tags: 'chrome'); + + test('that fail only on the VM', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + test("test", () { + if (p.style != p.Style.url) throw TestFailure("oh no"); + }); +} +''').create(); + + var test = await runTest(['-p', 'chrome', '-p', 'vm', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1 -1: Some tests failed.'))); + await test.shouldExit(1); + }, tags: 'chrome'); + + group('with a custom HTML file', () { + setUp(() async { + await d.file('test.dart', ''' +import 'package:test/src/runner/browser/dom.dart' as dom; +import 'package:test/test.dart'; + +void main() { + test("failure", () { + expect(dom.document.querySelector('#foo'), isNull); + }); +} +''').create(); + + await d.file('test.html', ''' +<html> +<head> + <link rel='x-dart-test' href='test.dart'> + <script src="packages/test/dart.js"></script> +</head> +<body> + <div id="foo"></div> +</body> +</html> +''').create(); + }); + + test('on Chrome', () async { + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('-1: Some tests failed.'))); + await test.shouldExit(1); + }, tags: 'chrome'); + }); + }); + + test('the compiler uses colors if the test runner uses colors', () async { + await d.file('test.dart', '{').create(); + + var test = await runTest(['--color', '-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('\u001b[31m'))); + await test.shouldExit(1); + }, tags: 'chrome'); + + test('forwards prints from the browser test', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("test", () { + print("Hello,"); + return Future(() => print("world!")); + }); +} +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsInOrder([emitsThrough('Hello,'), 'world!'])); + await test.shouldExit(0); + }, tags: 'chrome'); + + test('dartifies stack traces for JS-compiled tests by default', () async { + await d.file('test.dart', _failure).create(); + + var test = await runTest(['-p', 'chrome', '--verbose-trace', 'test.dart']); + expect(test.stdout, + containsInOrder([' main.<fn>', 'package:test', 'dart:async/zone.dart']), + skip: 'https://github.com/dart-lang/sdk/issues/41949'); + await test.shouldExit(1); + }, tags: 'chrome'); + + test("doesn't dartify stack traces for JS-compiled tests with --js-trace", + () async { + await d.file('test.dart', _failure).create(); + + var test = await runTest( + ['-p', 'chrome', '--verbose-trace', '--js-trace', 'test.dart']); + expect(test.stdoutStream(), neverEmits(endsWith(' main.<fn>'))); + expect(test.stdoutStream(), neverEmits(contains('package:test'))); + expect(test.stdoutStream(), neverEmits(contains('dart:async/zone.dart'))); + expect(test.stdout, emitsThrough(contains('-1: Some tests failed.'))); + await test.shouldExit(1); + }, tags: 'chrome'); + + test('respects top-level @Timeout declarations', () async { + await d.file('test.dart', ''' +@Timeout(const Duration(seconds: 0)) + +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("timeout", () => Future.delayed(Duration.zero)); +} +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder( + ['Test timed out after 0 seconds.', '-1: Some tests failed.'])); + await test.shouldExit(1); + }, tags: 'chrome'); + + group('with onPlatform', () { + test('respects matching Skips', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("fail", () => throw 'oh no', onPlatform: {"browser": Skip()}); +} +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+0 ~1: All tests skipped.'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + test('ignores non-matching Skips', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}, onPlatform: {"vm": Skip()}); +} +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + test('respects matching Timeouts', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("fail", () async { + await Future.delayed(Duration.zero); + throw 'oh no'; + }, onPlatform: { + "browser": Timeout(Duration.zero) + }); +} +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder( + ['Test timed out after 0 seconds.', '-1: Some tests failed.'])); + await test.shouldExit(1); + }, tags: 'chrome'); + + test('ignores non-matching Timeouts', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}, onPlatform: { + "vm": Timeout(Duration(seconds: 0)) + }); +} +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + test('applies matching platforms in order', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}, onPlatform: { + "browser": Skip("first"), + "browser || windows": Skip("second"), + "browser || linux": Skip("third"), + "browser || mac-os": Skip("fourth"), + "browser || android": Skip("fifth") + }); +} +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdoutStream(), neverEmits(contains('Skip: first'))); + expect(test.stdoutStream(), neverEmits(contains('Skip: second'))); + expect(test.stdoutStream(), neverEmits(contains('Skip: third'))); + expect(test.stdoutStream(), neverEmits(contains('Skip: fourth'))); + expect(test.stdout, emitsThrough(contains('Skip: fifth'))); + await test.shouldExit(0); + }, tags: 'chrome'); + }); + + group('with an @OnPlatform annotation', () { + test('respects matching Skips', () async { + await d.file('test.dart', ''' +@OnPlatform(const {"browser": const Skip()}) + +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("fail", () => throw 'oh no'); +} +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('~1: All tests skipped.'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + test('ignores non-matching Skips', () async { + await d.file('test.dart', ''' +@OnPlatform(const {"vm": const Skip()}) + +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + test('respects matching Timeouts', () async { + await d.file('test.dart', ''' +@OnPlatform(const { + "browser": const Timeout(const Duration(seconds: 0)) +}) + +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("fail", () async { + await Future.delayed(Duration.zero); + throw 'oh no'; + }); +} +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder( + ['Test timed out after 0 seconds.', '-1: Some tests failed.'])); + await test.shouldExit(1); + }, tags: 'chrome'); + + test('ignores non-matching Timeouts', () async { + await d.file('test.dart', ''' +@OnPlatform(const { + "vm": const Timeout(const Duration(seconds: 0)) +}) + +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + await test.shouldExit(0); + }, tags: 'chrome'); + }); + + group('deferred loading', () { + test('can run browser tests with deferred library imports', () async { + await d.file('deferred.dart', ''' +int x = 1; +''').create(); + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +import 'deferred.dart' deferred as d; + +void main() { + test("success", () async { + await d.loadLibrary(); + expect(d.x, 1); + }); +} +''').create(); + var test = await runTest(['-p', 'chrome', 'test.dart']); + + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + test('stack trace mapping works for deferred loaded libraries', () async { + await d.file('deferred.dart', ''' +int get x { + throw 'Oh no!'; +} +''').create(); + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +import 'deferred.dart' deferred as d; + +void main() { + test("failure", () async { + await d.loadLibrary(); + expect(d.x, 1); + }); +} +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + 'Oh no!', + 'deferred.dart', + 'test.dart', + ])); + await test.shouldExit(1); + }, tags: 'chrome'); + }); +}
diff --git a/pkgs/test/test/runner/browser/safari_test.dart b/pkgs/test/test/runner/browser/safari_test.dart new file mode 100644 index 0000000..28f4022 --- /dev/null +++ b/pkgs/test/test/runner/browser/safari_test.dart
@@ -0,0 +1,100 @@ +// Copyright (c) 2015, 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') +@Tags(['safari']) +library; + +import 'package:test/src/runner/browser/safari.dart'; +import 'package:test/src/runner/executable_settings.dart'; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../io.dart'; +import '../../utils.dart'; +import 'code_server.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('starts Safari with the given URL', + skip: 'https://github.com/dart-lang/test/issues/1253', () async { + var server = await CodeServer.start(); + + server.handleJavaScript(''' +var webSocket = new WebSocket(window.location.href.replace("http://", "ws://")); +webSocket.addEventListener("open", function() { + webSocket.send("loaded!"); +}); +'''); + var webSocket = server.handleWebSocket(); + + var safari = Safari(server.url); + addTearDown(() => safari.close()); + + expect(await (await webSocket).stream.first, equals('loaded!')); + }); + + test("a process can be killed synchronously after it's started", () async { + var server = await CodeServer.start(); + + var safari = Safari(server.url); + await safari.close(); + }); + + test('reports an error in onExit', () { + var safari = Safari(Uri.https('dart.dev'), + settings: ExecutableSettings( + linuxExecutable: '_does_not_exist', + macOSExecutable: '_does_not_exist', + windowsExecutable: '_does_not_exist')); + expect( + safari.onExit, + throwsA(isApplicationException( + startsWith('Failed to run Safari: $noSuchFileMessage')))); + }); + + test('can run successful tests', + skip: 'https://github.com/dart-lang/test/issues/1253', () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +''').create(); + + var test = await runTest(['-p', 'safari', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('can run failing tests', () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + test("failure", () => throw TestFailure("oh no")); +} +''').create(); + + var test = await runTest(['-p', 'safari', 'test.dart']); + expect(test.stdout, emitsThrough(contains('-1: Some tests failed.'))); + await test.shouldExit(1); + }); + + test('can override safari location with SAFARI_EXECUTABLE var', () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +''').create(); + var test = await runTest(['-p', 'safari', 'test.dart'], + environment: {'SAFARI_EXECUTABLE': '/some/bad/path'}); + expect(test.stdout, emitsThrough(contains('Failed to run Safari:'))); + await test.shouldExit(1); + }); +}
diff --git a/pkgs/test/test/runner/compact_reporter_test.dart b/pkgs/test/test/runner/compact_reporter_test.dart new file mode 100644 index 0000000..c935e02 --- /dev/null +++ b/pkgs/test/test/runner/compact_reporter_test.dart
@@ -0,0 +1,550 @@ +// Copyright (c) 2015, 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:io'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('reports when no tests are run', () async { + await d.file('test.dart', 'void main() {}').create(); + + var test = await runTest(['test.dart'], reporter: 'compact'); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }); + + test('runs several successful tests and reports when each completes', () { + return _expectReport(''' + test('success 1', () {}); + test('success 2', () {}); + test('success 3', () {});''', ''' + +0: loading test.dart + +0: success 1 + +1: success 1 + +1: success 2 + +2: success 2 + +2: success 3 + +3: success 3 + +3: All tests passed!'''); + }); + + test('runs several failing tests and reports when each fails', () { + return _expectReport(''' + test('failure 1', () => throw TestFailure('oh no')); + test('failure 2', () => throw TestFailure('oh no')); + test('failure 3', () => throw TestFailure('oh no'));''', ''' + +0: loading test.dart + +0: failure 1 + +0 -1: failure 1 [E] + oh no + test.dart 6:33 main.<fn> + + + +0 -1: failure 2 + +0 -2: failure 2 [E] + oh no + test.dart 7:33 main.<fn> + + + +0 -2: failure 3 + +0 -3: failure 3 [E] + oh no + test.dart 8:33 main.<fn> + + + +0 -3: Some tests failed.'''); + }); + + test('includes the full stack trace with --verbose-trace', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("failure", () => throw "oh no"); +} +''').create(); + + var test = + await runTest(['--verbose-trace', 'test.dart'], reporter: 'compact'); + expect(test.stdout, emitsThrough(contains('dart:async'))); + await test.shouldExit(1); + }); + + test('runs failing tests along with successful tests', () { + return _expectReport(''' + test('failure 1', () => throw TestFailure('oh no')); + test('success 1', () {}); + test('failure 2', () => throw TestFailure('oh no')); + test('success 2', () {});''', ''' + +0: loading test.dart + +0: failure 1 + +0 -1: failure 1 [E] + oh no + test.dart 6:33 main.<fn> + + + +0 -1: success 1 + +1 -1: success 1 + +1 -1: failure 2 + +1 -2: failure 2 [E] + oh no + test.dart 8:33 main.<fn> + + + +1 -2: success 2 + +2 -2: success 2 + +2 -2: Some tests failed.'''); + }); + + test('gracefully handles multiple test failures in a row', () { + return _expectReport(''' + // This completer ensures that the test isolate isn't killed until all + // errors have been thrown. + var completer = Completer(); + test('failures', () { + Future.microtask(() => throw 'first error'); + Future.microtask(() => throw 'second error'); + Future.microtask(() => throw 'third error'); + Future.microtask(completer.complete); + }); + test('wait', () => completer.future);''', ''' + +0: loading test.dart + +0: failures + +0 -1: failures [E] + first error + test.dart 10:34 main.<fn>.<fn> + ===== asynchronous gap =========================== + dart:async new Future.microtask + test.dart 10:18 main.<fn> + + second error + test.dart 11:34 main.<fn>.<fn> + ===== asynchronous gap =========================== + dart:async new Future.microtask + test.dart 11:18 main.<fn> + + third error + test.dart 12:34 main.<fn>.<fn> + ===== asynchronous gap =========================== + dart:async new Future.microtask + test.dart 12:18 main.<fn> + + + +0 -1: wait + +1 -1: wait + +1 -1: Some tests failed.'''); + }); + + test('prints the full test name before an error', () { + return _expectReport(''' + test( + 'really gosh dang long test name. Even longer than that. No, yet ' + 'longer. Even more. We have to get to at least 200 characters. ' + 'I know that seems like a lot, but I believe in you. A little ' + 'more... okay, that should do it.', + () => throw TestFailure('oh no'));''', ''' + +0: loading test.dart + +0: really ... than that. No, yet longer. Even more. We have to get to at least 200 characters. I know that seems like a lot, but I believe in you. A little more... okay, that should do it. + +0 -1: really gosh dang long test name. Even longer than that. No, yet longer. Even more. We have to get to at least 200 characters. I know that seems like a lot, but I believe in you. A little more... okay, that should do it. [E] + oh no + test.dart 11:18 main.<fn> + + + +0 -1: Some tests failed.'''); + }); + + group('print:', () { + test('handles multiple prints', () { + return _expectReport(''' + test('test', () { + print("one"); + print("two"); + print("three"); + print("four"); + });''', ''' + +0: loading test.dart + +0: test + one + two + three + four + + +1: test + +1: All tests passed!'''); + }); + + test('handles a print after the test completes', () { + return _expectReport(''' + // This completer ensures that the test isolate isn't killed until all + // prints have happened. + var testDone = Completer(); + var waitStarted = Completer(); + test('test', () { + waitStarted.future.then((_) { + Future(() => print("one")); + Future(() => print("two")); + Future(() => print("three")); + Future(() => print("four")); + Future(testDone.complete); + }); + }); + + test('wait', () { + waitStarted.complete(); + return testDone.future; + });''', ''' + +0: loading test.dart + +0: test + +1: test + +1: wait + +1: test + one + two + three + four + + +2: wait + +2: All tests passed!'''); + }); + + test('interleaves prints and errors', () { + return _expectReport(''' + // This completer ensures that the test isolate isn't killed until all + // prints have happened. + var completer = Completer(); + test('test', () { + scheduleMicrotask(() { + print("three"); + print("four"); + throw "second error"; + }); + + scheduleMicrotask(() { + print("five"); + print("six"); + completer.complete(); + }); + + print("one"); + print("two"); + throw "first error"; + }); + + test('wait', () => completer.future);''', ''' + +0: loading test.dart + +0: test + one + two + + +0 -1: test [E] + first error + test.dart 24:11 main.<fn> + + three + four + second error + test.dart 13:13 main.<fn>.<fn> + ===== asynchronous gap =========================== + dart:async scheduleMicrotask + test.dart 10:11 main.<fn> + + five + six + + +0 -1: wait + +1 -1: wait + +1 -1: Some tests failed.'''); + }); + + test('prints the full test name before a print', () { + return _expectReport(''' + test( + 'really gosh dang long test name. Even longer than that. No, yet ' + 'longer. Even more. We have to get to at least 200 ' + 'characters. I know that seems like a lot, but I believe in ' + 'you. A little more... okay, that should do it.', + () => print('hello'));''', ''' + +0: loading test.dart + +0: really ... than that. No, yet longer. Even more. We have to get to at least 200 characters. I know that seems like a lot, but I believe in you. A little more... okay, that should do it. + +0: really gosh dang long test name. Even longer than that. No, yet longer. Even more. We have to get to at least 200 characters. I know that seems like a lot, but I believe in you. A little more... okay, that should do it. + hello + + +1: really ... than that. No, yet longer. Even more. We have to get to at least 200 characters. I know that seems like a lot, but I believe in you. A little more... okay, that should do it. + +1: All tests passed!'''); + }); + + test("doesn't print a clock update between two prints", () { + return _expectReport(''' + test('slow', () async { + print('hello'); + await Future.delayed(Duration(seconds: 3)); + print('goodbye'); + });''', ''' + +0: loading test.dart + +0: slow + hello + goodbye + + +1: slow + +1: All tests passed!'''); + }); + }); + + group('skip:', () { + test('displays skipped tests separately', () { + return _expectReport(''' + test('skip 1', () {}, skip: true); + test('skip 2', () {}, skip: true); + test('skip 3', () {}, skip: true);''', ''' + +0: loading test.dart + +0: skip 1 + +0 ~1: skip 1 + +0 ~1: skip 2 + +0 ~2: skip 2 + +0 ~2: skip 3 + +0 ~3: skip 3 + +0 ~3: All tests skipped.'''); + }); + + test('displays a skipped group', () { + return _expectReport(''' + group('skip', () { + test('test 1', () {}); + test('test 2', () {}); + test('test 3', () {}); + }, skip: true);''', ''' + +0: loading test.dart + +0: skip test 1 + +0 ~1: skip test 1 + +0 ~1: skip test 2 + +0 ~2: skip test 2 + +0 ~2: skip test 3 + +0 ~3: skip test 3 + +0 ~3: All tests skipped.'''); + }); + + test('runs skipped tests along with successful tests', () { + return _expectReport(''' + test('skip 1', () {}, skip: true); + test('success 1', () {}); + test('skip 2', () {}, skip: true); + test('success 2', () {});''', ''' + +0: loading test.dart + +0: skip 1 + +0 ~1: skip 1 + +0 ~1: success 1 + +1 ~1: success 1 + +1 ~1: skip 2 + +1 ~2: skip 2 + +1 ~2: success 2 + +2 ~2: success 2 + +2 ~2: All tests passed!'''); + }); + + test('runs skipped tests along with successful and failing tests', () { + return _expectReport(''' + test('failure 1', () => throw TestFailure('oh no')); + test('skip 1', () {}, skip: true); + test('success 1', () {}); + test('failure 2', () => throw TestFailure('oh no')); + test('skip 2', () {}, skip: true); + test('success 2', () {});''', ''' + +0: loading test.dart + +0: failure 1 + +0 -1: failure 1 [E] + oh no + test.dart 6:35 main.<fn> + + + +0 -1: skip 1 + +0 ~1 -1: skip 1 + +0 ~1 -1: success 1 + +1 ~1 -1: success 1 + +1 ~1 -1: failure 2 + +1 ~1 -2: failure 2 [E] + oh no + test.dart 9:35 main.<fn> + + + +1 ~1 -2: skip 2 + +1 ~2 -2: skip 2 + +1 ~2 -2: success 2 + +2 ~2 -2: success 2 + +2 ~2 -2: Some tests failed.'''); + }); + + test('displays the skip reason if available', () { + return _expectReport(''' + test('skip 1', () {}, skip: 'some reason'); + test('skip 2', () {}, skip: 'or another');''', ''' + +0: loading test.dart + +0: skip 1 + Skip: some reason + + +0 ~1: skip 1 + +0 ~1: skip 2 + Skip: or another + + +0 ~2: skip 2 + +0 ~2: All tests skipped.'''); + }); + + test('runs skipped tests with --run-skipped', () { + return _expectReport(''' + test('skip 1', () {}, skip: 'some reason'); + test('skip 2', () {}, skip: 'or another');''', ''' + +0: loading test.dart + +0: skip 1 + +1: skip 1 + +1: skip 2 + +2: skip 2 + +2: All tests passed!''', args: ['--run-skipped']); + }); + }); + + test('Directs users to enable stack trace chaining if disabled', () async { + await _expectReport( + '''test('failure 1', () => throw TestFailure('oh no'));''', ''' + +0: loading test.dart + +0: failure 1 + +0 -1: failure 1 [E] + oh no + test.dart 6:25 main.<fn> + +0 -1: Some tests failed. + Consider enabling the flag chain-stack-traces to receive more detailed exceptions. + For example, 'dart test --chain-stack-traces'.''', + chainStackTraces: false); + }); + + group('gives non-windows users a way to re-run failed tests', () { + final executablePath = p.absolute(Platform.resolvedExecutable); + + test('with simple names', () { + return _expectReport(''' + test('failure', () { + expect(1, equals(2)); + });''', ''' + +0: loading test.dart + +0: failure + +0 -1: failure [E] + Expected: <2> + Actual: <1> + + To run this test again: $executablePath test test.dart -p vm --plain-name 'failure' + + +0 -1: Some tests failed.'''); + }); + + test('escapes names containing single quotes', () { + return _expectReport(''' + test("failure with a ' in the name", () { + expect(1, equals(2)); + });''', ''' + +0: loading test.dart + +0: failure with a ' in the name + +0 -1: failure with a ' in the name [E] + Expected: <2> + Actual: <1> + + To run this test again: $executablePath test test.dart -p vm --plain-name 'failure with a '\\'' in the name' + + +0 -1: Some tests failed.'''); + }); + }, testOn: '!windows'); + + group('gives windows users a way to re-run failed tests', () { + final executablePath = p.absolute(Platform.resolvedExecutable); + + test('with simple names', () { + return _expectReport(''' + test('failure', () { + expect(1, equals(2)); + });''', ''' + +0: loading test.dart + +0: failure + +0 -1: failure [E] + Expected: <2> + Actual: <1> + + To run this test again: $executablePath test test.dart -p vm --plain-name "failure" + + +0 -1: Some tests failed.'''); + }); + + test('escapes names containing double quotes', () { + return _expectReport(''' + test('failure with a " in the name', () { + expect(1, equals(2)); + });''', ''' + +0: loading test.dart + +0: failure with a " in the name + +0 -1: failure with a " in the name [E] + Expected: <2> + Actual: <1> + + To run this test again: $executablePath test test.dart -p vm --plain-name "failure with a """ in the name" + + +0 -1: Some tests failed.'''); + }); + }, testOn: 'windows'); +} + +Future<void> _expectReport(String tests, String expected, + {List<String> args = const [], bool chainStackTraces = true}) async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { +$tests + } + ''').create(); + + var test = await runTest([ + 'test.dart', + if (chainStackTraces) '--chain-stack-traces', + ...args, + ], reporter: 'compact'); + await test.shouldExit(); + + var stdoutLines = await test.stdout.rest.toList(); + + // Skip the first CR, remove excess trailing whitespace, and trim off + // timestamps. + String? lastLine; + var actual = stdoutLines.skip(1).map((line) { + if (line.startsWith(' ') || line.isEmpty) return line.trimRight(); + + var trimmed = line.trim().replaceFirst(RegExp('^[0-9]{2}:[0-9]{2} '), ''); + + // Trim identical lines so the test isn't dependent on how fast each test + // runs. + if (trimmed == lastLine) return null; + lastLine = trimmed; + return trimmed; + }).where((line) => line != null); + + // Un-indent the expected string. + var indentation = expected.indexOf(RegExp('[^ ]')); + var expectedLines = expected.split('\n').map((line) { + if (line.isEmpty) return line; + return line.substring(indentation); + }); + + expect(actual, containsAllInOrder(expectedLines)); +}
diff --git a/pkgs/test/test/runner/compiler_runtime_matrix_test.dart b/pkgs/test/test/runner/compiler_runtime_matrix_test.dart new file mode 100644 index 0000000..74d8802 --- /dev/null +++ b/pkgs/test/test/runner/compiler_runtime_matrix_test.dart
@@ -0,0 +1,174 @@ +// Copyright (c) 2023, 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') +@Timeout.factor(2) +library; + +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:test_api/backend.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; + +void main() { + setUpAll(() async { + await precompileTestExecutable(); + }); + + for (var runtime in Runtime.builtIn) { + for (var compiler in runtime.supportedCompilers) { + // Ignore the platforms we can't run on this OS. + if ((runtime == Runtime.edge && !Platform.isWindows) || + (runtime == Runtime.safari && !Platform.isMacOS)) { + continue; + } + String? skipReason; + if (runtime == Runtime.safari) { + skipReason = 'https://github.com/dart-lang/test/issues/1253'; + } else if (compiler == Compiler.dart2wasm) { + skipReason = 'Wasm tests are experimental and require special setup'; + } else if ([Runtime.firefox, Runtime.nodeJS].contains(runtime) && + Platform.isWindows) { + skipReason = 'https://github.com/dart-lang/test/issues/1942'; + } else if (runtime == Runtime.firefox && Platform.isMacOS) { + skipReason = 'https://github.com/dart-lang/test/pull/2276'; + } + group('--runtime ${runtime.identifier} --compiler ${compiler.identifier}', + skip: skipReason, () { + final testArgs = [ + 'test.dart', + '-p', + runtime.identifier, + '-c', + compiler.identifier + ]; + + test('can run passing tests', () async { + await d.file('test.dart', _goodTest).create(); + var test = await runTest(testArgs); + + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('fails gracefully for invalid code', () async { + await d.file('test.dart', _compileErrorTest).create(); + var test = await runTest(testArgs); + + expect( + test.stdout, + containsInOrder([ + "Error: A value of type 'String' can't be assigned to a variable of type 'int'.", + "int x = 'hello';", + ])); + + await test.shouldExit(1); + }); + + test('fails gracefully for test failures', () async { + await d.file('test.dart', _failingTest).create(); + var test = await runTest(testArgs); + + expect( + test.stdout, + containsInOrder([ + 'Expected: <2>', + 'Actual: <1>', + 'test.dart 5', + '+0 -1: Some tests failed.', + ])); + + await test.shouldExit(1); + }); + + test('fails gracefully if a test file throws in main', () async { + await d.file('test.dart', _throwingTest).create(); + var test = await runTest(testArgs); + expect( + test.stdout, + containsInOrder([ + '-1: [${runtime.name}, ${compiler.name}] loading test.dart [E]', + 'Failed to load "test.dart": oh no' + ])); + await test.shouldExit(1); + }); + + test('captures prints', () async { + await d.file('test.dart', _testWithPrints).create(); + var test = await runTest([...testArgs, '-r', 'json']); + + expect( + test.stdout, + containsInOrder([ + '"messageType":"print","message":"hello","type":"print"', + ])); + + await test.shouldExit(0); + }); + + if (runtime.isDartVM) { + test('forwards stdout/stderr', () async { + await d.file('test.dart', _testWithStdOutAndErr).create(); + var test = await runTest(testArgs, reporter: 'silent'); + + expect(test.stdout, emitsThrough('hello')); + expect(test.stderr, emits('world')); + await test.shouldExit(0); + }, + skip: Platform.isWindows && compiler == Compiler.exe + ? 'https://github.com/dart-lang/test/issues/2150' + : null); + } + }); + } + } +} + +final _goodTest = ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } +'''; + +final _failingTest = ''' + import 'package:test/test.dart'; + + void main() { + test("failure", () { + expect(1, 2); + }); + } +'''; + +final _compileErrorTest = ''' +int x = 'hello'; + +void main() {} +'''; + +final _throwingTest = "void main() => throw 'oh no';"; + +final _testWithPrints = ''' +import 'package:test/test.dart'; + +void main() { + print('hello'); + test('success', () {}); +}'''; + +final _testWithStdOutAndErr = ''' +import 'dart:io'; +import 'package:test/test.dart'; + +void main() async { + stdout.writeln('hello'); + await stdout.flush(); + stderr.writeln('world'); + test('success', () {}); +}''';
diff --git a/pkgs/test/test/runner/compiler_test.dart b/pkgs/test/test/runner/compiler_test.dart new file mode 100644 index 0000000..5cf7a47 --- /dev/null +++ b/pkgs/test/test/runner/compiler_test.dart
@@ -0,0 +1,77 @@ +// Copyright (c) 2023, 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 '../io.dart'; + +final _test = ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } +'''; + +void main() { + setUpAll(() async { + await precompileTestExecutable(); + await d.file('test.dart', _test).create(); + }); + + group('--compiler', () { + test( + 'uses the default compiler if none other is specified for the platform', + () async { + var test = + await runTest(['test.dart', '-p', 'chrome,vm', '-c', 'dart2js']); + + expect(test.stdout, emitsThrough(contains('[Chrome, Dart2Js]'))); + expect(test.stdout, emitsThrough(contains('[VM, Kernel]'))); + expect(test.stdout, emitsThrough(contains('+2: All tests passed!'))); + await test.shouldExit(0); + }); + + test('runs all supported compiler and platform combinations', () async { + var test = await runTest( + ['test.dart', '-p', 'chrome,vm', '-c', 'dart2js,kernel,source']); + + expect(test.stdout, emitsThrough(contains('[Chrome, Dart2Js]'))); + expect(test.stdout, emitsThrough(contains('[VM, Kernel]'))); + expect(test.stdout, emitsThrough(contains('[VM, Source]'))); + expect(test.stdout, emitsThrough(contains('+3: All tests passed!'))); + await test.shouldExit(0); + }); + + test('supports platform selectors', () async { + var test = await runTest( + ['test.dart', '-p', 'vm', '-c', 'vm:source,browser:kernel']); + + expect(test.stdout, emitsThrough(contains('[VM, Source]'))); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test( + 'will only run a given test once for each compiler, even if there are ' + 'multiple matches', () async { + var test = + await runTest(['test.dart', '-p', 'vm', '-c', 'vm:source,source']); + + expect(test.stdout, emitsThrough(contains('[VM, Source]'))); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('fails on unknown compilers', () async { + var test = await runTest(['test.dart', '-c', 'fake']); + expect(test.stderr, emitsThrough(contains('Invalid compiler `fake`'))); + await test.shouldExit(64); + }); + }); +}
diff --git a/pkgs/test/test/runner/configuration/compiler_test.dart b/pkgs/test/test/runner/configuration/compiler_test.dart new file mode 100644 index 0000000..d8f8d33 --- /dev/null +++ b/pkgs/test/test/runner/configuration/compiler_test.dart
@@ -0,0 +1,75 @@ +// Copyright (c) 2023, 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 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + group('compilers', () { + test('uses specified compilers for supporting platforms', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'compilers': ['source'] + })) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + var test = await runTest(['-p', 'chrome,vm', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '+0: [Chrome, Dart2Js]', + '+1: [VM, Source]', + '+2: All tests passed!', + ])); + await test.shouldExit(0); + }); + + test('supports platform selectors with compilers', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'compilers': ['vm:source', 'browser:kernel'] + })) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + var test = await runTest(['-p', 'chrome,vm', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '+0: [Chrome, Dart2Js]', + '+1: [VM, Source]', + '+2: All tests passed!', + ])); + await test.shouldExit(0); + }); + }); +}
diff --git a/pkgs/test/test/runner/configuration/configuration_test.dart b/pkgs/test/test/runner/configuration/configuration_test.dart new file mode 100644 index 0000000..03cc728 --- /dev/null +++ b/pkgs/test/test/runner/configuration/configuration_test.dart
@@ -0,0 +1,326 @@ +// Copyright (c) 2016, 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:boolean_selector/boolean_selector.dart'; +import 'package:test/test.dart'; +import 'package:test_core/src/runner/configuration/reporters.dart'; +import 'package:test_core/src/runner/suite.dart'; +import 'package:test_core/src/util/io.dart'; + +import '../../utils.dart'; + +void main() { + group('merge', () { + group('for most fields', () { + test('if neither is defined, preserves the default', () { + var merged = configuration().merge(configuration()); + expect(merged.help, isFalse); + expect(merged.version, isFalse); + expect(merged.pauseAfterLoad, isFalse); + expect(merged.debug, isFalse); + expect(merged.color, equals(canUseSpecialChars)); + expect(merged.configurationPath, equals('dart_test.yaml')); + expect(merged.reporter, equals(defaultReporter)); + expect(merged.fileReporters, isEmpty); + expect(merged.shardIndex, isNull); + expect(merged.totalShards, isNull); + expect(merged.testRandomizeOrderingSeed, isNull); + expect(merged.testSelections.keys.single, 'test'); + }); + + test("if only the old configuration's is defined, uses it", () { + var merged = configuration( + help: true, + version: true, + pauseAfterLoad: true, + debug: true, + color: true, + configurationPath: 'special_test.yaml', + reporter: 'json', + fileReporters: {'json': 'out.json'}, + shardIndex: 3, + totalShards: 10, + testRandomizeOrderingSeed: 123, + testSelections: const { + 'bar': {TestSelection()} + }).merge(configuration()); + + expect(merged.help, isTrue); + expect(merged.version, isTrue); + expect(merged.pauseAfterLoad, isTrue); + expect(merged.debug, isTrue); + expect(merged.color, isTrue); + expect(merged.configurationPath, equals('special_test.yaml')); + expect(merged.reporter, equals('json')); + expect(merged.fileReporters, equals({'json': 'out.json'})); + expect(merged.shardIndex, equals(3)); + expect(merged.totalShards, equals(10)); + expect(merged.testRandomizeOrderingSeed, 123); + expect(merged.testSelections.keys.single, 'bar'); + }); + + test("if only the new configuration's is defined, uses it", () { + var merged = configuration().merge(configuration( + help: true, + version: true, + pauseAfterLoad: true, + debug: true, + color: true, + configurationPath: 'special_test.yaml', + reporter: 'json', + fileReporters: {'json': 'out.json'}, + shardIndex: 3, + totalShards: 10, + testRandomizeOrderingSeed: 123, + testSelections: const { + 'bar': {TestSelection()} + })); + + expect(merged.help, isTrue); + expect(merged.version, isTrue); + expect(merged.pauseAfterLoad, isTrue); + expect(merged.debug, isTrue); + expect(merged.color, isTrue); + expect(merged.configurationPath, equals('special_test.yaml')); + expect(merged.reporter, equals('json')); + expect(merged.fileReporters, equals({'json': 'out.json'})); + expect(merged.shardIndex, equals(3)); + expect(merged.totalShards, equals(10)); + expect(merged.testRandomizeOrderingSeed, 123); + expect(merged.testSelections.keys.single, 'bar'); + }); + + test( + "if the two configurations conflict, uses the new configuration's " + 'values', () { + var older = configuration( + help: true, + version: false, + pauseAfterLoad: true, + debug: true, + color: false, + configurationPath: 'special_test.yaml', + reporter: 'json', + fileReporters: {'json': 'old.json'}, + shardIndex: 2, + totalShards: 4, + testRandomizeOrderingSeed: 0, + testSelections: const { + 'bar': {TestSelection()} + }); + var newer = configuration( + help: false, + version: true, + pauseAfterLoad: false, + debug: false, + color: true, + configurationPath: 'test_special.yaml', + reporter: 'compact', + fileReporters: {'json': 'new.json'}, + shardIndex: 3, + totalShards: 10, + testRandomizeOrderingSeed: 123, + testSelections: const { + 'blech': {TestSelection()} + }); + var merged = older.merge(newer); + + expect(merged.help, isFalse); + expect(merged.version, isTrue); + expect(merged.pauseAfterLoad, isFalse); + expect(merged.debug, isFalse); + expect(merged.color, isTrue); + expect(merged.configurationPath, equals('test_special.yaml')); + expect(merged.reporter, equals('compact')); + expect(merged.fileReporters, equals({'json': 'new.json'})); + expect(merged.shardIndex, equals(3)); + expect(merged.totalShards, equals(10)); + expect(merged.testRandomizeOrderingSeed, 123); + expect(merged.testSelections.keys.single, 'blech'); + }); + }); + + group('for chosenPresets', () { + test('if neither is defined, preserves the default', () { + var merged = configuration().merge(configuration()); + expect(merged.chosenPresets, isEmpty); + }); + + test("if only the old configuration's is defined, uses it", () { + var merged = configuration(chosenPresets: ['baz', 'bang']) + .merge(configuration()); + expect(merged.chosenPresets, equals(['baz', 'bang'])); + }); + + test("if only the new configuration's is defined, uses it", () { + var merged = configuration() + .merge(configuration(chosenPresets: ['baz', 'bang'])); + expect(merged.chosenPresets, equals(['baz', 'bang'])); + }); + + test('if both are defined, unions them', () { + var merged = configuration(chosenPresets: ['baz', 'bang']) + .merge(configuration(chosenPresets: ['qux'])); + expect(merged.chosenPresets, equals(['baz', 'bang', 'qux'])); + }); + }); + + group('for presets', () { + test('merges each nested configuration', () { + var merged = configuration(presets: { + 'bang': configuration(pauseAfterLoad: true), + 'qux': configuration(color: true) + }).merge(configuration(presets: { + 'qux': configuration(color: false), + 'zap': configuration(help: true) + })); + + expect(merged.presets['bang']!.pauseAfterLoad, isTrue); + expect(merged.presets['qux']!.color, isFalse); + expect(merged.presets['zap']!.help, isTrue); + }); + + test('automatically resolves a matching chosen preset', () { + var config = configuration( + presets: {'foo': configuration(color: true)}, + chosenPresets: ['foo']); + expect(config.presets, isEmpty); + expect(config.chosenPresets, equals(['foo'])); + expect(config.knownPresets, equals(['foo'])); + expect(config.color, isTrue); + }); + + test('resolves a chosen presets in order', () { + var config = configuration(presets: { + 'foo': configuration(color: true), + 'bar': configuration(color: false) + }, chosenPresets: [ + 'foo', + 'bar' + ]); + expect(config.presets, isEmpty); + expect(config.chosenPresets, equals(['foo', 'bar'])); + expect(config.knownPresets, unorderedEquals(['foo', 'bar'])); + expect(config.color, isFalse); + + config = configuration(presets: { + 'foo': configuration(color: true), + 'bar': configuration(color: false) + }, chosenPresets: [ + 'bar', + 'foo' + ]); + expect(config.presets, isEmpty); + expect(config.chosenPresets, equals(['bar', 'foo'])); + expect(config.knownPresets, unorderedEquals(['foo', 'bar'])); + expect(config.color, isTrue); + }); + + test('ignores inapplicable chosen presets', () { + var config = configuration(presets: {}, chosenPresets: ['baz']); + expect(config.presets, isEmpty); + expect(config.chosenPresets, equals(['baz'])); + expect(config.knownPresets, equals(isEmpty)); + }); + + test('resolves presets through merging', () { + var config = configuration(presets: {'foo': configuration(color: true)}) + .merge(configuration(chosenPresets: ['foo'])); + + expect(config.presets, isEmpty); + expect(config.chosenPresets, equals(['foo'])); + expect(config.knownPresets, equals(['foo'])); + expect(config.color, isTrue); + }); + + test('preserves known presets through merging', () { + var config = configuration( + presets: {'foo': configuration(color: true)}, + chosenPresets: ['foo']).merge(configuration()); + + expect(config.presets, isEmpty); + expect(config.chosenPresets, equals(['foo'])); + expect(config.knownPresets, equals(['foo'])); + expect(config.color, isTrue); + }); + }); + + group('for include and excludeTags', () { + test('if neither is defined, preserves the default', () { + var merged = configuration().merge(configuration()); + expect(merged.includeTags, equals(BooleanSelector.all)); + expect(merged.excludeTags, equals(BooleanSelector.none)); + }); + + test("if only the old configuration's is defined, uses it", () { + var merged = configuration( + includeTags: BooleanSelector.parse('foo || bar'), + excludeTags: BooleanSelector.parse('baz || bang')) + .merge(configuration()); + + expect(merged.includeTags, equals(BooleanSelector.parse('foo || bar'))); + expect( + merged.excludeTags, equals(BooleanSelector.parse('baz || bang'))); + }); + + test("if only the configuration's is defined, uses it", () { + var merged = configuration().merge(configuration( + includeTags: BooleanSelector.parse('foo || bar'), + excludeTags: BooleanSelector.parse('baz || bang'))); + + expect(merged.includeTags, equals(BooleanSelector.parse('foo || bar'))); + expect( + merged.excludeTags, equals(BooleanSelector.parse('baz || bang'))); + }); + + test('if both are defined, unions or intersects them', () { + var older = configuration( + includeTags: BooleanSelector.parse('foo || bar'), + excludeTags: BooleanSelector.parse('baz || bang')); + var newer = configuration( + includeTags: BooleanSelector.parse('blip'), + excludeTags: BooleanSelector.parse('qux')); + var merged = older.merge(newer); + + expect(merged.includeTags, + equals(BooleanSelector.parse('(foo || bar) && blip'))); + expect(merged.excludeTags, + equals(BooleanSelector.parse('(baz || bang) || qux'))); + }); + }); + + group('for globalPatterns', () { + test('if neither is defined, preserves the default', () { + var merged = configuration().merge(configuration()); + expect(merged.globalPatterns, isEmpty); + }); + + test("if only the old configuration's is defined, uses it", () { + var merged = configuration(globalPatterns: ['beep', 'boop']) + .merge(configuration()); + + expect(merged.globalPatterns, equals(['beep', 'boop'])); + }); + + test("if only the new configuration's is defined, uses it", () { + var merged = configuration() + .merge(configuration(globalPatterns: ['beep', 'boop'])); + + expect(merged.globalPatterns, equals(['beep', 'boop'])); + }); + + test('if both are defined, unions them', () { + var older = configuration(globalPatterns: ['beep', 'boop']); + var newer = configuration(globalPatterns: ['bonk']); + var merged = older.merge(newer); + + expect( + merged.globalPatterns, unorderedEquals(['beep', 'boop', 'bonk'])); + }); + }); + }); +}
diff --git a/pkgs/test/test/runner/configuration/custom_platform_test.dart b/pkgs/test/test/runner/configuration/custom_platform_test.dart new file mode 100644 index 0000000..ca9ec05 --- /dev/null +++ b/pkgs/test/test/runner/configuration/custom_platform_test.dart
@@ -0,0 +1,930 @@ +// 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:io'; + +import 'package:test/src/runner/browser/default_settings.dart'; +import 'package:test/test.dart'; +import 'package:test_api/src/backend/runtime.dart'; +import 'package:test_core/src/util/exit_codes.dart' as exit_codes; +import 'package:test_descriptor/test_descriptor.dart' as d; +import 'package:test_process/test_process.dart'; + +import '../../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + setUp(() async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + ''').create(); + }); + + group('override_platforms', () { + group('can override a browser', () { + test('without any changes', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + chrome: + settings: {} + ''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + test("that's user-defined", () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: {} + + override_platforms: + chromium: + settings: {} + ''').create(); + + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + test('with a basename-only executable', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + firefox: + settings: + executable: + linux: firefox + mac_os: firefox + windows: firefox.exe + ''').create(); + + var test = await runTest(['-p', 'firefox', 'test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }, tags: 'firefox'); + + test('with an absolute-path executable', () async { + String path; + if (Platform.isLinux) { + var process = await TestProcess.start('which', ['google-chrome']); + path = await process.stdout.next; + await process.shouldExit(0); + } else { + path = defaultSettings[Runtime.chrome]!.executable; + } + + await d.file('dart_test.yaml', ''' + override_platforms: + chrome: + settings: + executable: $path + ''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + test('with non-headless mode', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + chrome: + settings: + headless: false + ''').create(); + + await d.file('test.dart', ''' + import 'package:test/src/runner/browser/dom.dart' as dom; + import 'package:test/test.dart'; + + void main() { + test("is not headless", () { + expect(dom.window.navigator.userAgent, + isNot(contains('HeadlessChrome'))); + }); + } + ''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + }); + + test('can override Node.js without any changes', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + node: + settings: {} + ''').create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }, tags: 'node'); + + group('errors', () { + test('rejects a non-map value', () async { + await d.file('dart_test.yaml', 'override_platforms: 12').create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['override_platforms must be a map.', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects a non-string key', () async { + await d + .file('dart_test.yaml', 'override_platforms: {12: null}') + .create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['Platform identifier must be a string.', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects a non-identifier-like key', () async { + await d + .file('dart_test.yaml', 'override_platforms: {foo bar: null}') + .create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder([ + 'Platform identifier must be an (optionally hyphenated) Dart ' + 'identifier.', + '^^^^^^^' + ])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects a non-map definition', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + chrome: 12 + ''').create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['Platform definition must be a map.', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('requires a settings key', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + chrome: {} + ''').create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['Missing required field "settings".', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('settings must be a map', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + chrome: + settings: null + ''').create(); + + var test = await runTest([]); + expect(test.stderr, containsInOrder(['Must be a map.', '^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('the overridden platform must exist', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + chromium: + settings: {} + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['Unknown platform "chromium".', '^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test("uncustomizable platforms can't be overridden", () async { + await d.file('dart_test.yaml', ''' + override_platforms: + vm: + settings: {} + ''').create(); + + var test = await runTest(['-p', 'vm', 'test.dart']); + expect(test.stdout, + containsInOrder(['The "vm" platform can\'t be customized.', '^^'])); + await test.shouldExit(1); + }); + + group('when overriding browsers', () { + test('executable must be a string or map', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + chrome: + settings: + executable: 12 + ''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, + containsInOrder(['Must be a map or a string.', '^^'])); + await test.shouldExit(1); + }); + + test('executable string may not be relative on POSIX', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + chrome: + settings: + executable: foo/bar + ''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + 'Linux and Mac OS executables may not be relative paths.', + '^^^^^^^' + ])); + await test.shouldExit(1); + }, + // We allow relative executables for Windows so that Windows users + // can set a global executable without having to explicitly write + // `windows:`. + testOn: '!windows'); + + test('Linux executable must be a string', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + chrome: + settings: + executable: + linux: 12 + ''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, containsInOrder(['Must be a string.', '^^'])); + await test.shouldExit(1); + }); + + test('Linux executable may not be relative', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + chrome: + settings: + executable: + linux: foo/bar + ''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + 'Linux and Mac OS executables may not be relative paths.', + '^^^^^^^' + ])); + await test.shouldExit(1); + }); + + test('Mac OS executable must be a string', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + chrome: + settings: + executable: + mac_os: 12 + ''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, containsInOrder(['Must be a string.', '^^'])); + await test.shouldExit(1); + }); + + test('Mac OS executable may not be relative', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + chrome: + settings: + executable: + mac_os: foo/bar + ''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + 'Linux and Mac OS executables may not be relative paths.', + '^^^^^^^' + ])); + await test.shouldExit(1); + }); + + test('Windows executable must be a string', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + chrome: + settings: + executable: + windows: 12 + ''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, containsInOrder(['Must be a string.', '^^'])); + await test.shouldExit(1); + }); + + test('executable must exist', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + chrome: + settings: + executable: _does_not_exist + ''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + emitsThrough( + contains('Failed to run Chrome: $noSuchFileMessage'))); + await test.shouldExit(1); + }); + + test('executable must exist for Node.js', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + node: + settings: + executable: _does_not_exist + ''').create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect( + test.stdout, + emitsThrough( + contains('Failed to run Node.js: $noSuchFileMessage'))); + await test.shouldExit(1); + }, tags: 'node'); + + test('headless must be a boolean', () async { + await d.file('dart_test.yaml', ''' + override_platforms: + chrome: + settings: + headless: definitely + ''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stdout, + emitsThrough( + containsInOrder(['Must be a boolean.', '^^^^^^^^^^']))); + await test.shouldExit(1); + }); + }); + }); + }); + + group('define_platforms', () { + group('can define a new browser', () { + group('without any changes', () { + setUp(() async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: {} + ''').create(); + }); + + test('that can be used to run tests', () async { + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + test('that can be used in platform selectors', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}, testOn: "chromium"); + } + ''').create(); + + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + + test = await runTest(['-p', 'chrome', 'test.dart']); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }, tags: 'chrome'); + + test('that counts as its parent', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}, testOn: "chrome"); + } + ''').create(); + + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + }); + + test('with a basename-only executable', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + my-firefox: + name: My Firefox + extends: firefox + settings: + executable: + linux: firefox + mac_os: firefox + windows: firefox.exe + ''').create(); + + var test = await runTest(['-p', 'my-firefox', 'test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }, tags: 'firefox'); + + test('with an absolute-path executable', () async { + String path; + if (Platform.isLinux) { + var process = await TestProcess.start('which', ['google-chrome']); + path = await process.stdout.next; + await process.shouldExit(0); + } else { + path = defaultSettings[Runtime.chrome]!.executable; + } + + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: $path + ''').create(); + + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + }); + + group('errors', () { + test('rejects a non-map value', () async { + await d.file('dart_test.yaml', 'define_platforms: 12').create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['define_platforms must be a map.', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects a non-string key', () async { + await d.file('dart_test.yaml', 'define_platforms: {12: null}').create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['Platform identifier must be a string.', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects a non-identifier-like key', () async { + await d + .file('dart_test.yaml', 'define_platforms: {foo bar: null}') + .create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder([ + 'Platform identifier must be an (optionally hyphenated) Dart ' + 'identifier.', + '^^^^^^^' + ])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects a non-map definition', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: 12 + ''').create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['Platform definition must be a map.', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('requires a name key', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + extends: chrome + settings: {} + ''').create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder( + ['Missing required field "name".', 'extends: chrome'])); + await test.shouldExit(exit_codes.data); + }); + + test('name must be a string', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: null + extends: chrome + settings: {} + ''').create(); + + var test = await runTest([]); + expect(test.stderr, containsInOrder(['Must be a string.', '^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('requires an extends key', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + settings: {} + ''').create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder( + ['Missing required field "extends".', 'name: Chromium'])); + await test.shouldExit(exit_codes.data); + }); + + test('extends must be a string', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: null + settings: {} + ''').create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['Platform parent must be a string.', '^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('extends must be identifier-like', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: foo bar + settings: {} + ''').create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder([ + 'Platform parent must be an (optionally hyphenated) Dart ' + 'identifier.', + '^^^^^^^' + ])); + await test.shouldExit(exit_codes.data); + }); + + test('requires a settings key', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + ''').create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder( + ['Missing required field "settings".', 'name: Chromium'])); + await test.shouldExit(exit_codes.data); + }); + + test('settings must be a map', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: null + ''').create(); + + var test = await runTest([]); + expect(test.stderr, containsInOrder(['Must be a map.', '^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('the new platform may not override an existing platform', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chrome: + name: Chromium + extends: firefox + settings: {} + ''').create(); + + await d.dir('test').create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder([ + 'The platform "chrome" already exists. Use override_platforms to ' + 'override it.', + '^^^^^^' + ])); + await test.shouldExit(exit_codes.data); + }); + + test('the new platform must extend an existing platform', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: foobar + settings: {} + ''').create(); + + await d.dir('test').create(); + + var test = await runTest([]); + expect(test.stderr, containsInOrder(['Unknown platform.', '^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test("the new platform can't extend an uncustomizable platform", + () async { + await d.file('dart_test.yaml', ''' + define_platforms: + myvm: + name: My VM + extends: vm + settings: {} + ''').create(); + + var test = await runTest(['-p', 'myvm', 'test.dart']); + expect(test.stdout, + containsInOrder(['The "vm" platform can\'t be customized.', '^^'])); + await test.shouldExit(1); + }); + + group('when overriding browsers', () { + test('executable must be a string or map', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: 12 + ''').create(); + + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect(test.stdout, + containsInOrder(['Must be a map or a string.', '^^'])); + await test.shouldExit(1); + }); + + test('executable string may not be relative on POSIX', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: foo/bar + ''').create(); + + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + 'Linux and Mac OS executables may not be relative paths.', + '^^^^^^^' + ])); + await test.shouldExit(1); + }, + // We allow relative executables for Windows so that Windows users + // can set a global executable without having to explicitly write + // `windows:`. + testOn: '!windows'); + + test('Linux executable must be a string', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: + linux: 12 + ''').create(); + + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect(test.stdout, containsInOrder(['Must be a string.', '^^'])); + await test.shouldExit(1); + }); + + test('Linux executable may not be relative', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: + linux: foo/bar + ''').create(); + + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + 'Linux and Mac OS executables may not be relative paths.', + '^^^^^^^' + ])); + await test.shouldExit(1); + }); + + test('Mac OS executable must be a string', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: + mac_os: 12 + ''').create(); + + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect(test.stdout, containsInOrder(['Must be a string.', '^^'])); + await test.shouldExit(1); + }); + + test('Mac OS executable may not be relative', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: + mac_os: foo/bar + ''').create(); + + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + 'Linux and Mac OS executables may not be relative paths.', + '^^^^^^^' + ])); + await test.shouldExit(1); + }); + + test('Windows executable must be a string', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: + windows: 12 + ''').create(); + + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect(test.stdout, containsInOrder(['Must be a string.', '^^'])); + await test.shouldExit(1); + }); + + test('executable must exist', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + executable: _does_not_exist + ''').create(); + + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect( + test.stdout, + emitsThrough( + contains('Failed to run Chrome: $noSuchFileMessage'))); + await test.shouldExit(1); + }); + + test('arguments must be a string', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + arguments: 12 + ''').create(); + + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect(test.stdout, containsInOrder(['Must be a string.', '^^'])); + await test.shouldExit(1); + }); + + test('arguments must be shell parseable', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + arguments: --foo 'bar + ''').create(); + + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect(test.stdout, + containsInOrder(['Unmatched single quote.', '^^^^^^^^^^'])); + await test.shouldExit(1); + }); + + test('headless must be a boolean', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + chromium: + name: Chromium + extends: chrome + settings: + headless: definitely + ''').create(); + + var test = await runTest(['-p', 'chromium', 'test.dart']); + expect( + test.stdout, + emitsThrough( + containsInOrder(['Must be a boolean.', '^^^^^^^^^^']))); + await test.shouldExit(1); + }); + + test('with an argument that causes the browser to quit', () async { + await d.file('dart_test.yaml', ''' + define_platforms: + myfox: + name: My Firefox + extends: firefox + settings: + arguments: --version + ''').create(); + + var test = await runTest(['-p', 'myfox', 'test.dart']); + expect(test.stdout, + emitsThrough(contains('My Firefox exited before connecting.'))); + await test.shouldExit(1); + }, tags: 'firefox'); + }); + }); + }); +}
diff --git a/pkgs/test/test/runner/configuration/duplicate_names_test.dart b/pkgs/test/test/runner/configuration/duplicate_names_test.dart new file mode 100644 index 0000000..5a02f52 --- /dev/null +++ b/pkgs/test/test/runner/configuration/duplicate_names_test.dart
@@ -0,0 +1,82 @@ +// Copyright (c) 2021, 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 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + group('duplicate names', () { + group('can be disabled for', () { + for (var function in ['group', 'test']) { + test('${function}s', () async { + await d + .file('dart_test.yaml', + jsonEncode({'allow_duplicate_test_names': false})) + .create(); + + var testName = 'test'; + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + $function("$testName", () {}); + $function("$testName", () {}); + } + ''').create(); + + var test = await runTest([ + 'test.dart', + '--configuration', + p.join(d.sandbox, 'dart_test.yaml') + ]); + + expect( + test.stdout, + emitsThrough(contains( + 'A test with the name "$testName" was already declared.'))); + + await test.shouldExit(1); + }); + } + }); + group('are allowed by default for', () { + for (var function in ['group', 'test']) { + test('${function}s', () async { + var testName = 'test'; + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + $function("$testName", () {}); + $function("$testName", () {}); + + // Needed so at least one test runs when testing groups. + test('a test', () { + expect(true, isTrue); + }); + } + ''').create(); + + var test = await runTest( + ['test.dart'], + ); + + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + + await test.shouldExit(0); + }); + } + }); + }); +}
diff --git a/pkgs/test/test/runner/configuration/global_test.dart b/pkgs/test/test/runner/configuration/global_test.dart new file mode 100644 index 0000000..98e8741 --- /dev/null +++ b/pkgs/test/test/runner/configuration/global_test.dart
@@ -0,0 +1,122 @@ +// Copyright (c) 2016, 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 'package:test/test.dart'; +import 'package:test_core/src/util/exit_codes.dart' as exit_codes; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('ignores an empty file', () async { + await d.file('global_test.yaml', '').create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + ''').create(); + + var test = await runTest(['test.dart'], + environment: {'DART_TEST_CONFIG': 'global_test.yaml'}); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('uses supported test configuration', () async { + await d + .file('global_test.yaml', jsonEncode({'verbose_trace': true})) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("failure", () => throw "oh no"); + } + ''').create(); + + var test = await runTest(['test.dart'], + environment: {'DART_TEST_CONFIG': 'global_test.yaml'}); + expect(test.stdout, emitsThrough(contains('dart:async'))); + await test.shouldExit(1); + }); + + test('uses supported runner configuration', () async { + await d.file('global_test.yaml', jsonEncode({'reporter': 'json'})).create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + ''').create(); + + var test = await runTest(['test.dart'], + environment: {'DART_TEST_CONFIG': 'global_test.yaml'}); + expect(test.stdout, emitsThrough(contains('"testStart"'))); + await test.shouldExit(0); + }); + + test('local configuration takes precedence', () async { + await d + .file('global_test.yaml', jsonEncode({'verbose_trace': true})) + .create(); + + await d + .file('dart_test.yaml', jsonEncode({'verbose_trace': false})) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("failure", () => throw "oh no"); + } + ''').create(); + + var test = await runTest(['test.dart'], + environment: {'DART_TEST_CONFIG': 'global_test.yaml'}); + expect(test.stdout, neverEmits(contains('dart:isolate-patch'))); + await test.shouldExit(1); + }); + + group('disallows local-only configuration:', () { + for (var field in [ + 'skip', 'retry', 'test_on', 'paths', 'filename', 'names', 'tags', // + 'plain_names', 'include_tags', 'exclude_tags', 'add_tags', + 'define_platforms', 'allow_duplicate_test_names', + ]) { + test('for $field', () async { + await d.file('global_test.yaml', jsonEncode({field: null})).create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + ''').create(); + + var test = await runTest(['test.dart'], + environment: {'DART_TEST_CONFIG': 'global_test.yaml'}); + expect( + test.stderr, + containsInOrder( + ["of global_test.yaml: $field isn't supported here.", '^^'])); + await test.shouldExit(exit_codes.data); + }); + } + }); +}
diff --git a/pkgs/test/test/runner/configuration/include_test.dart b/pkgs/test/test/runner/configuration/include_test.dart new file mode 100644 index 0000000..5d064cf --- /dev/null +++ b/pkgs/test/test/runner/configuration/include_test.dart
@@ -0,0 +1,170 @@ +// 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 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_core/src/runner/configuration.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +void main() { + test('should merge with a base configuration', () async { + await d.dir('repo', [ + d.file('dart_test_base.yaml', 'filename: "test_*.dart"'), + d.dir('pkg', [ + d.file('dart_test.yaml', ''' + include: ../dart_test_base.yaml + concurrency: 3 + '''), + ]), + ]).create(); + + var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml'); + var config = Configuration.load(path); + expect(config.filename.pattern, equals('test_*.dart')); + expect(config.concurrency, equals(3)); + }); + + test('should merge fields with a base configuration', () async { + await d.dir('repo', [ + d.file('dart_test_base.yaml', ''' + tags: + hello: + '''), + d.dir('pkg', [ + d.file('dart_test.yaml', ''' + include: ../dart_test_base.yaml + tags: + world: + '''), + ]), + ]).create(); + + var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml'); + var config = Configuration.load(path); + expect(config.knownTags, unorderedEquals(['hello', 'world'])); + }); + + test('should allow an included file to include a file', () async { + await d.dir('repo', [ + d.file('dart_test_base_base.yaml', ''' + tags: + tag: + '''), + d.file('dart_test_base.yaml', ''' + include: dart_test_base_base.yaml + filename: "test_*.dart" + '''), + d.dir('pkg', [ + d.file('dart_test.yaml', ''' + include: ../dart_test_base.yaml + concurrency: 3 + '''), + ]), + ]).create(); + + var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml'); + var config = Configuration.load(path); + expect(config.knownTags, ['tag']); + expect(config.filename.pattern, 'test_*.dart'); + expect(config.concurrency, 3); + }); + + test('should not allow an include field in a test config context', () async { + await d.dir('repo', [ + d.dir('pkg', [ + d.file('dart_test.yaml', r''' + tags: + foo: + include: ../dart_test.yaml + '''), + ]), + ]).create(); + + var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml'); + expect( + () => Configuration.load(path), + throwsA(allOf( + isFormatException, + predicate((error) => + error.toString().contains("include isn't supported here"))))); + }); + + test('should allow an include field in a runner config context', () async { + await d.dir('repo', [ + d.dir('pkg', [ + d.file('dart_test.yaml', ''' + presets: + bar: + include: other_dart_test.yaml + pause_after_load: true + '''), + d.file('other_dart_test.yaml', 'reporter: expanded'), + ]), + ]).create(); + + var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml'); + var config = Configuration.load(path); + var presetBar = config.presets['bar']!; + expect(presetBar.pauseAfterLoad, isTrue); + expect(presetBar.reporter, 'expanded'); + }); + + test('local configuration should take precedence after merging', () async { + await d.dir('repo', [ + d.dir('pkg', [ + d.file('dart_test.yaml', ''' + include: other_dart_test.yaml + concurrency: 5 + '''), + d.file('other_dart_test.yaml', 'concurrency: 10'), + ]), + ]).create(); + + var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml'); + var config = Configuration.load(path); + expect(config.concurrency, 5); + }); + + group('gracefully handles', () { + test('a non-string include field', () async { + await d.dir('repo', [ + d.dir('pkg', [ + d.file('dart_test.yaml', 'include: 3'), + ]), + ]).create(); + + var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml'); + expect(() => Configuration.load(path), throwsFormatException); + }); + + test('a non-existent included file', () async { + await d.dir('repo', [ + d.dir('pkg', [ + d.file('dart_test.yaml', 'include: other_test.yaml'), + ]), + ]).create(); + + var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml'); + expect(() => Configuration.load(path), throwsFormatException); + }); + + test('an include field in a test config context', () async { + await d.dir('repo', [ + d.dir('pkg', [ + d.file('dart_test.yaml', ''' + tags: + foo: + include: ../dart_test.yaml + '''), + ]), + ]).create(); + + var path = p.join(d.sandbox, 'repo', 'pkg', 'dart_test.yaml'); + expect(() => Configuration.load(path), throwsFormatException); + }); + }); +}
diff --git a/pkgs/test/test/runner/configuration/platform_test.dart b/pkgs/test/test/runner/configuration/platform_test.dart new file mode 100644 index 0000000..014c0ed --- /dev/null +++ b/pkgs/test/test/runner/configuration/platform_test.dart
@@ -0,0 +1,310 @@ +// Copyright (c) 2016, 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 'package:test/test.dart'; +import 'package:test_core/src/util/exit_codes.dart' as exit_codes; +import 'package:test_core/src/util/io.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + group('on_platform', () { + test('applies platform-specific configuration to matching tests', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'on_platform': { + 'chrome': {'timeout': '0s'} + } + })) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test", () => Future.delayed(Duration.zero)); + } + ''').create(); + + var test = await runTest(['-p', 'chrome,vm', 'test.dart']); + expect( + test.stdout, + containsInOrder( + ['-1: [Chrome, Dart2Js] test [E]', '+1 -1: Some tests failed.'])); + await test.shouldExit(1); + }, tags: ['chrome']); + + test('supports platform selectors', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'on_platform': { + 'chrome || vm': {'timeout': '0s'} + } + })) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test", () => Future.delayed(Duration.zero)); + } + ''').create(); + + var test = await runTest(['-p', 'chrome,vm', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '-1: [Chrome, Dart2Js] test [E]', + '-2: [VM, Kernel] test [E]', + '-2: Some tests failed.' + ])); + await test.shouldExit(1); + }, tags: ['chrome']); + + group('errors', () { + test('rejects an invalid selector type', () async { + await d.file('dart_test.yaml', '{"on_platform": {12: null}}').create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['on_platform key must be a string', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid selector', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'on_platform': {'foo bar': null} + })) + .create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder([ + 'Invalid on_platform key: Expected end of input.', + '^^^^^^^^^' + ])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects a selector with an undefined variable', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'on_platform': {'foo': null} + })) + .create(); + + await d.dir('test').create(); + + var test = await runTest([]); + expect(test.stderr, containsInOrder(['Undefined variable.', '^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid map', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'on_platform': {'linux': 12} + })) + .create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['on_platform value must be a map.', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid configuration', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'on_platform': { + 'linux': {'timeout': '12p'} + } + })) + .create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['Invalid timeout: expected unit.', '^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects runner configuration', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'on_platform': { + 'linux': {'filename': '*_blorp'} + } + })) + .create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(["filename isn't supported here.", '^^^^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + }); + }); + + group('on_os', () { + test('applies OS-specific configuration on a matching OS', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'on_os': { + currentOS.identifier: {'filename': 'test_*.dart'} + } + })) + .create(); + + await d.file('foo_test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("foo_test", () {}); + } + ''').create(); + + await d.file('test_foo.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test_foo", () {}); + } + ''').create(); + + var test = await runTest(['.']); + expect( + test.stdout, + containsInOrder( + ['+0: ./test_foo.dart: test_foo', '+1: All tests passed!'])); + await test.shouldExit(0); + }); + + test("doesn't apply OS-specific configuration on a non-matching OS", + () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'on_os': { + otherOS: {'filename': 'test_*.dart'} + } + })) + .create(); + + await d.file('foo_test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("foo_test", () {}); + } + ''').create(); + + await d.file('test_foo.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test_foo", () {}); + } + ''').create(); + + var test = await runTest(['.']); + expect( + test.stdout, + containsInOrder( + ['+0: ./foo_test.dart: foo_test', '+1: All tests passed!'])); + await test.shouldExit(0); + }); + + group('errors', () { + test('rejects an invalid OS type', () async { + await d.file('dart_test.yaml', '{"on_os": {12: null}}').create(); + + var test = await runTest([]); + expect( + test.stderr, containsInOrder(['on_os key must be a string', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an unknown OS name', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'on_os': {'foo': null} + })) + .create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder( + ['Invalid on_os key: No such operating system.', '^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid map', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'on_os': {'linux': 12} + })) + .create(); + + var test = await runTest([]); + expect( + test.stderr, containsInOrder(['on_os value must be a map.', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid configuration', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'on_os': { + 'linux': {'timeout': '12p'} + } + })) + .create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['Invalid timeout: expected unit.', '^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + }); + }); +}
diff --git a/pkgs/test/test/runner/configuration/presets_test.dart b/pkgs/test/test/runner/configuration/presets_test.dart new file mode 100644 index 0000000..193f12e --- /dev/null +++ b/pkgs/test/test/runner/configuration/presets_test.dart
@@ -0,0 +1,515 @@ +// Copyright (c) 2016, 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 'package:test/test.dart'; +import 'package:test_core/src/util/exit_codes.dart' as exit_codes; +import 'package:test_core/src/util/io.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + group('presets', () { + test("don't do anything by default", () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'presets': { + 'foo': {'timeout': '0s'} + } + })) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test", () => Future.delayed(Duration.zero)); + } + ''').create(); + + await (await runTest(['test.dart'])).shouldExit(0); + }); + + test('can be selected on the command line', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'presets': { + 'foo': {'timeout': '0s'} + } + })) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test", () => Future.delayed(Duration.zero)); + } + ''').create(); + + var test = await runTest(['-P', 'foo', 'test.dart']); + expect(test.stdout, + containsInOrder(['-1: test [E]', '-1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('multiple presets can be selected', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'presets': { + 'foo': {'timeout': '0s'}, + 'bar': { + 'paths': ['test.dart'] + } + } + })) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test", () => Future.delayed(Duration.zero)); + } + ''').create(); + + var test = await runTest(['-P', 'foo,bar']); + expect(test.stdout, + containsInOrder(['-1: test [E]', '-1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('the latter preset takes precedence', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'presets': { + 'foo': {'timeout': '0s'}, + 'bar': {'timeout': '30s'} + } + })) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test", () => Future.delayed(Duration.zero)); + } + ''').create(); + + await (await runTest(['-P', 'foo,bar', 'test.dart'])).shouldExit(0); + + var test = await runTest(['-P', 'bar,foo', 'test.dart']); + expect(test.stdout, + containsInOrder(['-1: test [E]', '-1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('a preset takes precedence over the base configuration', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'presets': { + 'foo': {'timeout': '0s'} + }, + 'timeout': '30s' + })) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test", () => Future.delayed(Duration.zero)); + } + ''').create(); + + var test = await runTest(['-P', 'foo', 'test.dart']); + expect(test.stdout, + containsInOrder(['-1: test [E]', '-1: Some tests failed.'])); + await test.shouldExit(1); + + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'presets': { + 'foo': {'timeout': '30s'} + }, + 'timeout': '00s' + })) + .create(); + + await (await runTest(['-P', 'foo', 'test.dart'])).shouldExit(0); + }); + + test('a nested preset is activated', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'tags': { + 'foo': { + 'presets': { + 'bar': {'timeout': '0s'} + }, + }, + } + })) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test 1", () => Future.delayed(Duration.zero), tags: "foo"); + test("test 2", () => Future.delayed(Duration.zero)); + } + ''').create(); + + var test = await runTest(['-P', 'bar', 'test.dart']); + expect(test.stdout, + containsInOrder(['+0 -1: test 1 [E]', '+1 -1: Some tests failed.'])); + await test.shouldExit(1); + + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'presets': { + 'foo': {'timeout': '30s'} + }, + 'timeout': '00s' + })) + .create(); + + await (await runTest(['-P', 'foo', 'test.dart'])).shouldExit(0); + }); + }); + + group('add_presets', () { + test('selects a preset', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'presets': { + 'foo': {'timeout': '0s'} + }, + 'add_presets': ['foo'] + })) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test", () => Future.delayed(Duration.zero)); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, + containsInOrder(['-1: test [E]', '-1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('applies presets in selection order', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'presets': { + 'foo': {'timeout': '0s'}, + 'bar': {'timeout': '30s'} + }, + 'add_presets': ['foo', 'bar'] + })) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test", () => Future.delayed(Duration.zero)); + } + ''').create(); + + await (await runTest(['test.dart'])).shouldExit(0); + + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'presets': { + 'foo': {'timeout': '0s'}, + 'bar': {'timeout': '30s'} + }, + 'add_presets': ['bar', 'foo'] + })) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, + containsInOrder(['-1: test [E]', '-1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('allows preset inheritance via add_presets', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'presets': { + 'foo': { + 'add_presets': ['bar'] + }, + 'bar': {'timeout': '0s'} + } + })) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test", () => Future.delayed(Duration.zero)); + } + ''').create(); + + var test = await runTest(['-P', 'foo', 'test.dart']); + expect(test.stdout, + containsInOrder(['+0 -1: test [E]', '-1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('allows circular preset inheritance via add_presets', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'presets': { + 'foo': { + 'add_presets': ['bar'] + }, + 'bar': { + 'add_presets': ['foo'] + } + } + })) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + await (await runTest(['-P', 'foo', 'test.dart'])).shouldExit(0); + }); + }); + + group('errors', () { + group('presets', () { + test('rejects an invalid preset type', () async { + await d.file('dart_test.yaml', '{"presets": {12: null}}').create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['presets key must be a string', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid preset name', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'presets': {'foo bar': null} + })) + .create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder([ + 'presets key must be an (optionally hyphenated) Dart identifier.', + '^^^^^^^^^' + ])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid preset map', () async { + await d.file('dart_test.yaml', jsonEncode({'presets': 12})).create(); + + var test = await runTest([]); + expect(test.stderr, containsInOrder(['presets must be a map', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid preset configuration', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'presets': { + 'foo': {'timeout': '12p'} + } + })) + .create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['Invalid timeout: expected unit', '^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects runner configuration in a non-runner context', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'tags': { + 'foo': { + 'presets': { + 'bar': {'filename': '*_blorp.dart'} + } + } + } + })) + .create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(["filename isn't supported here.", '^^^^^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('fails if an undefined preset is passed', () async { + var test = await runTest(['-P', 'foo']); + expect(test.stderr, emitsThrough(contains('Undefined preset "foo".'))); + await test.shouldExit(exit_codes.usage); + }); + + test('fails if an undefined preset is added', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'add_presets': ['foo', 'bar'] + })) + .create(); + + var test = await runTest([]); + expect(test.stderr, + emitsThrough(contains('Undefined presets "foo" and "bar".'))); + await test.shouldExit(exit_codes.usage); + }); + + test('fails if an undefined preset is added in a nested context', + () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'on_os': { + currentOS.identifier: { + 'add_presets': ['bar'] + } + } + })) + .create(); + + var test = await runTest([]); + expect(test.stderr, emitsThrough(contains('Undefined preset "bar".'))); + await test.shouldExit(exit_codes.usage); + }); + }); + + group('add_presets', () { + test('rejects an invalid list type', () async { + await d + .file('dart_test.yaml', jsonEncode({'add_presets': 'foo'})) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['add_presets must be a list', '^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid preset type', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'add_presets': [12] + })) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['Preset name must be a string', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid preset name', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'add_presets': ['foo bar'] + })) + .create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, + containsInOrder([ + 'Preset name must be an (optionally hyphenated) Dart identifier.', + '^^^^^^^^^' + ])); + await test.shouldExit(exit_codes.data); + }); + }); + }); +}
diff --git a/pkgs/test/test/runner/configuration/randomize_order_test.dart b/pkgs/test/test/runner/configuration/randomize_order_test.dart new file mode 100644 index 0000000..690f54b --- /dev/null +++ b/pkgs/test/test/runner/configuration/randomize_order_test.dart
@@ -0,0 +1,234 @@ +// Copyright (c) 2016, 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 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('shuffles test order when passed a seed', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test 1", () {}); + test("test 2", () {}); + test("test 3", () {}); + test("test 4", () {}); + } + ''').create(); + + // Test with a given seed + var test = + await runTest(['test.dart', '--test-randomize-ordering-seed=987654']); + expect( + test.stdout, + containsInOrder([ + '+0: test 4', + '+1: test 3', + '+2: test 1', + '+3: test 2', + '+4: All tests passed!' + ])); + await test.shouldExit(0); + + // Do not shuffle when passed 0 + test = await runTest(['test.dart', '--test-randomize-ordering-seed=0']); + expect( + test.stdout, + containsInOrder([ + '+0: test 1', + '+1: test 2', + '+2: test 3', + '+3: test 4', + '+4: All tests passed!' + ])); + await test.shouldExit(0); + + // Do not shuffle when passed nothing + test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder([ + '+0: test 1', + '+1: test 2', + '+2: test 3', + '+3: test 4', + '+4: All tests passed!' + ])); + await test.shouldExit(0); + + // Shuffle when passed random + test = + await runTest(['test.dart', '--test-randomize-ordering-seed=random']); + expect( + test.stdout, + emitsInAnyOrder([ + contains('Shuffling test order with --test-randomize-ordering-seed'), + isNot(contains( + 'Shuffling test order with --test-randomize-ordering-seed=0')) + ])); + await test.shouldExit(0); + + // Doesn't log about shuffling with the json reporter + test = await runTest( + ['test.dart', '--test-randomize-ordering-seed=random', '-r', 'json']); + expect(test.stdout, neverEmits(contains('Shuffling test order'))); + await test.shouldExit(0); + }); + + test('test shuffling can be disabled in dart_test.yml', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'tags': { + 'doNotShuffle': {'allow_test_randomization': false} + } + })) + .create(); + + await d.file('test.dart', ''' + @Tags(['doNotShuffle']) + import 'package:test/test.dart'; + + void main() { + test("test 1", () {}); + test("test 2", () {}); + test("test 3", () {}); + test("test 4", () {}); + } + ''').create(); + + var test = + await runTest(['test.dart', '--test-randomize-ordering-seed=987654']); + expect( + test.stdout, + containsInOrder([ + '+0: test 1', + '+1: test 2', + '+2: test 3', + '+3: test 4', + '+4: All tests passed!' + ])); + await test.shouldExit(0); + }); + + test('shuffles each suite with the same seed', () async { + await d.file('1_test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test 1.1", () {}); + test("test 1.2", () {}); + test("test 1.3", () {}); + } + ''').create(); + + await d.file('2_test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test 2.1", () {}); + test("test 2.2", () {}); + test("test 2.3", () {}); + } + ''').create(); + + var test = await runTest(['.', '--test-randomize-ordering-seed=12345']); + expect( + test.stdout, + emitsInAnyOrder([ + containsInOrder([ + './1_test.dart: test 1.2', + './1_test.dart: test 1.3', + './1_test.dart: test 1.1' + ]), + containsInOrder([ + './2_test.dart: test 2.2', + './2_test.dart: test 2.3', + './2_test.dart: test 2.1' + ]), + contains('+6: All tests passed!') + ])); + await test.shouldExit(0); + }); + + test('shuffles groups as well as tests in groups', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + group("Group 1", () { + test("test 1.1", () {}); + test("test 1.2", () {}); + test("test 1.3", () {}); + test("test 1.4", () {}); + }); + group("Group 2", () { + test("test 2.1", () {}); + test("test 2.2", () {}); + test("test 2.3", () {}); + test("test 2.4", () {}); + }); + } + ''').create(); + + // Test with a given seed + var test = + await runTest(['test.dart', '--test-randomize-ordering-seed=123']); + expect( + test.stdout, + containsInOrder([ + '+0: Group 2 test 2.4', + '+1: Group 2 test 2.2', + '+2: Group 2 test 2.1', + '+3: Group 2 test 2.3', + '+4: Group 1 test 1.4', + '+5: Group 1 test 1.2', + '+6: Group 1 test 1.1', + '+7: Group 1 test 1.3', + '+8: All tests passed!' + ])); + await test.shouldExit(0); + }); + + test('shuffles nested groups', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + group("Group 1", () { + test("test 1.1", () {}); + test("test 1.2", () {}); + group("Group 2", () { + test("test 2.3", () {}); + test("test 2.4", () {}); + }); + }); + } + ''').create(); + + var test = + await runTest(['test.dart', '--test-randomize-ordering-seed=123']); + expect( + test.stdout, + containsInOrder([ + '+0: Group 1 test 1.1', + '+1: Group 1 Group 2 test 2.4', + '+2: Group 1 Group 2 test 2.3', + '+3: Group 1 test 1.2', + '+4: All tests passed!' + ])); + await test.shouldExit(0); + }); +}
diff --git a/pkgs/test/test/runner/configuration/suite_test.dart b/pkgs/test/test/runner/configuration/suite_test.dart new file mode 100644 index 0000000..1c2fc62 --- /dev/null +++ b/pkgs/test/test/runner/configuration/suite_test.dart
@@ -0,0 +1,147 @@ +// Copyright (c) 2016, 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:boolean_selector/boolean_selector.dart'; +import 'package:test/test.dart'; +import 'package:test_api/src/backend/platform_selector.dart'; +import 'package:test_api/src/backend/runtime.dart'; +import 'package:test_core/src/runner/compiler_selection.dart'; +import 'package:test_core/src/runner/runtime_selection.dart'; + +import '../../utils.dart'; + +void main() { + group('merge', () { + group('for most fields', () { + test('if neither is defined, preserves the default', () { + var merged = suiteConfiguration().merge(suiteConfiguration()); + expect(merged.jsTrace, isFalse); + expect(merged.runSkipped, isFalse); + expect(merged.precompiledPath, isNull); + expect(merged.runtimes, equals([Runtime.vm.identifier])); + expect(merged.compilerSelections, isNull); + }); + + test("if only the old configuration's is defined, uses it", () { + var merged = suiteConfiguration( + jsTrace: true, + runSkipped: true, + precompiledPath: '/tmp/js', + runtimes: [RuntimeSelection(Runtime.chrome.identifier)], + compilerSelections: [CompilerSelection.parse('dart2js')]) + .merge(suiteConfiguration()); + + expect(merged.jsTrace, isTrue); + expect(merged.runSkipped, isTrue); + expect(merged.precompiledPath, equals('/tmp/js')); + expect(merged.runtimes, equals([Runtime.chrome.identifier])); + expect(merged.compilerSelections, + equals([CompilerSelection.parse('dart2js')])); + }); + + test("if only the configuration's is defined, uses it", () { + var merged = suiteConfiguration().merge(suiteConfiguration( + jsTrace: true, + runSkipped: true, + precompiledPath: '/tmp/js', + runtimes: [RuntimeSelection(Runtime.chrome.identifier)], + compilerSelections: [CompilerSelection.parse('dart2js')])); + + expect(merged.jsTrace, isTrue); + expect(merged.runSkipped, isTrue); + expect(merged.precompiledPath, equals('/tmp/js')); + expect(merged.runtimes, equals([Runtime.chrome.identifier])); + expect(merged.compilerSelections, + equals([CompilerSelection.parse('dart2js')])); + }); + + test( + "if the two configurations conflict, uses the configuration's " + 'values', () { + var older = suiteConfiguration( + jsTrace: false, + runSkipped: true, + precompiledPath: '/tmp/js', + runtimes: [RuntimeSelection(Runtime.chrome.identifier)], + compilerSelections: [CompilerSelection.parse('dart2js')]); + var newer = suiteConfiguration( + jsTrace: true, + runSkipped: false, + precompiledPath: '../js', + runtimes: [RuntimeSelection(Runtime.firefox.identifier)], + compilerSelections: [CompilerSelection.parse('source')]); + var merged = older.merge(newer); + + expect(merged.jsTrace, isTrue); + expect(merged.runSkipped, isFalse); + expect(merged.precompiledPath, equals('../js')); + expect(merged.runtimes, equals([Runtime.firefox.identifier])); + expect(merged.compilerSelections, + equals([CompilerSelection.parse('source')])); + }); + }); + + group('for dart2jsArgs', () { + test('if neither is defined, preserves the default', () { + var merged = suiteConfiguration().merge(suiteConfiguration()); + expect(merged.dart2jsArgs, isEmpty); + }); + + test("if only the old configuration's is defined, uses it", () { + var merged = suiteConfiguration(dart2jsArgs: ['--foo', '--bar']) + .merge(suiteConfiguration()); + expect(merged.dart2jsArgs, equals(['--foo', '--bar'])); + }); + + test("if only the configuration's is defined, uses it", () { + var merged = suiteConfiguration() + .merge(suiteConfiguration(dart2jsArgs: ['--foo', '--bar'])); + expect(merged.dart2jsArgs, equals(['--foo', '--bar'])); + }); + + test('if both are defined, concatenates them', () { + var older = suiteConfiguration(dart2jsArgs: ['--foo', '--bar']); + var newer = suiteConfiguration(dart2jsArgs: ['--baz']); + var merged = older.merge(newer); + expect(merged.dart2jsArgs, equals(['--foo', '--bar', '--baz'])); + }); + }); + + group('for config maps', () { + test('merges each nested configuration', () { + var merged = suiteConfiguration(tags: { + BooleanSelector.parse('foo'): + suiteConfiguration(precompiledPath: 'path/'), + BooleanSelector.parse('bar'): suiteConfiguration(jsTrace: true) + }, onPlatform: { + PlatformSelector.parse('vm'): + suiteConfiguration(precompiledPath: 'path/'), + PlatformSelector.parse('chrome'): suiteConfiguration(jsTrace: true) + }).merge(suiteConfiguration(tags: { + BooleanSelector.parse('bar'): suiteConfiguration(jsTrace: false), + BooleanSelector.parse('baz'): suiteConfiguration(runSkipped: true) + }, onPlatform: { + PlatformSelector.parse('chrome'): suiteConfiguration(jsTrace: false), + PlatformSelector.parse('firefox'): + suiteConfiguration(runSkipped: true) + })); + + expect(merged.tags[BooleanSelector.parse('foo')]!.precompiledPath, + equals('path/')); + expect(merged.tags[BooleanSelector.parse('bar')]!.jsTrace, isFalse); + expect(merged.tags[BooleanSelector.parse('baz')]!.runSkipped, isTrue); + + expect(merged.onPlatform[PlatformSelector.parse('vm')]!.precompiledPath, + 'path/'); + expect(merged.onPlatform[PlatformSelector.parse('chrome')]!.jsTrace, + isFalse); + expect(merged.onPlatform[PlatformSelector.parse('firefox')]!.runSkipped, + isTrue); + }); + }); + }); +}
diff --git a/pkgs/test/test/runner/configuration/tags_test.dart b/pkgs/test/test/runner/configuration/tags_test.dart new file mode 100644 index 0000000..bbdb1d1 --- /dev/null +++ b/pkgs/test/test/runner/configuration/tags_test.dart
@@ -0,0 +1,401 @@ +// Copyright (c) 2016, 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 'package:test/test.dart'; +import 'package:test_core/src/util/exit_codes.dart' as exit_codes; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('adds the specified tags', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'add_tags': ['foo', 'bar'] + })) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + var test = await runTest(['--exclude-tag', 'foo', 'test.dart']); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + + test = await runTest(['--exclude-tag', 'bar', 'test.dart']); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + + test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + group('tags', () { + test("doesn't warn for tags that exist in the configuration", () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'tags': {'foo': null} + })) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, neverEmits(contains('Warning: Tags were used'))); + await test.shouldExit(0); + }); + + test('applies tag-specific configuration only to matching tests', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'tags': { + 'foo': {'timeout': '0s'} + } + })) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test 1", () => Future.delayed(Duration.zero), tags: ['foo']); + test("test 2", () => Future.delayed(Duration.zero)); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, + containsInOrder(['-1: test 1 [E]', '+1 -1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('supports tag selectors', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'tags': { + 'foo && bar': {'timeout': '0s'} + } + })) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test 1", () => Future.delayed(Duration.zero), tags: ['foo']); + test("test 2", () => Future.delayed(Duration.zero), tags: ['bar']); + test("test 3", () => Future.delayed(Duration.zero), + tags: ['foo', 'bar']); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, + containsInOrder(['+2 -1: test 3 [E]', '+2 -1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('allows tag inheritance via add_tags', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'tags': { + 'foo': null, + 'bar': { + 'add_tags': ['foo'] + } + } + })) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test 1", () {}, tags: ['bar']); + test("test 2", () {}); + } + ''').create(); + + var test = await runTest(['test.dart', '--tags', 'foo']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + // Regression test for #503. + test('skips tests whose tags are marked as skip', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'tags': { + 'foo': {'skip': 'some reason'} + } + })) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test 1", () => throw 'bad', tags: ['foo']); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, containsInOrder(['some reason', 'All tests skipped.'])); + await test.shouldExit(0); + }); + }); + + group('include_tags and exclude_tags', () { + test('only runs tests with the included tags', () async { + await d + .file('dart_test.yaml', jsonEncode({'include_tags': 'foo && bar'})) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("zip", () {}, tags: "foo"); + test("zap", () {}, tags: "bar"); + test("zop", () {}, tags: ["foo", "bar"]); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, containsInOrder(['+0: zop', '+1: All tests passed!'])); + await test.shouldExit(0); + }); + + test("doesn't run tests with the excluded tags", () async { + await d + .file('dart_test.yaml', jsonEncode({'exclude_tags': 'foo && bar'})) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("zip", () {}, tags: "foo"); + test("zap", () {}, tags: "bar"); + test("zop", () {}, tags: ["foo", "bar"]); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, + containsInOrder(['+0: zip', '+1: zap', '+2: All tests passed!'])); + await test.shouldExit(0); + }); + }); + + group('errors', () { + group('tags', () { + test('rejects an invalid tag type', () async { + await d.file('dart_test.yaml', '{"tags": {12: null}}').create(); + + var test = await runTest([]); + expect( + test.stderr, containsInOrder(['tags key must be a string', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid tag selector', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'tags': {'foo bar': null} + })) + .create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder( + ['Invalid tags key: Expected end of input.', '^^^^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid tag map', () async { + await d.file('dart_test.yaml', jsonEncode({'tags': 12})).create(); + + var test = await runTest([]); + expect(test.stderr, containsInOrder(['tags must be a map', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid tag configuration', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'tags': { + 'foo': {'timeout': '12p'} + } + })) + .create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(['Invalid timeout: expected unit', '^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects runner configuration', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'tags': { + 'foo': {'filename': '*_blorp.dart'} + } + })) + .create(); + + var test = await runTest([]); + expect(test.stderr, + containsInOrder(["filename isn't supported here.", '^^^^^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + }); + + group('add_tags', () { + test('rejects an invalid list type', () async { + await d + .file('dart_test.yaml', jsonEncode({'add_tags': 'foo'})) + .create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, containsInOrder(['add_tags must be a list', '^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid tag type', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'add_tags': [12] + })) + .create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, containsInOrder(['Tag name must be a string', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid tag name', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'add_tags': ['foo bar'] + })) + .create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, + containsInOrder([ + 'Tag name must be an (optionally hyphenated) Dart identifier.', + '^^^^^^^^^' + ])); + await test.shouldExit(exit_codes.data); + }); + }); + + group('include_tags', () { + test('rejects an invalid type', () async { + await d + .file('dart_test.yaml', jsonEncode({'include_tags': 12})) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['include_tags must be a string', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid selector', () async { + await d + .file('dart_test.yaml', jsonEncode({'include_tags': 'foo bar'})) + .create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder( + ['Invalid include_tags: Expected end of input.', '^^^^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + }); + + group('exclude_tags', () { + test('rejects an invalid type', () async { + await d + .file('dart_test.yaml', jsonEncode({'exclude_tags': 12})) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['exclude_tags must be a string', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid selector', () async { + await d + .file('dart_test.yaml', jsonEncode({'exclude_tags': 'foo bar'})) + .create(); + + var test = await runTest([]); + expect( + test.stderr, + containsInOrder( + ['Invalid exclude_tags: Expected end of input.', '^^^^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + }); + }); +}
diff --git a/pkgs/test/test/runner/configuration/top_level_error_test.dart b/pkgs/test/test/runner/configuration/top_level_error_test.dart new file mode 100644 index 0000000..56a717c --- /dev/null +++ b/pkgs/test/test/runner/configuration/top_level_error_test.dart
@@ -0,0 +1,417 @@ +// Copyright (c) 2016, 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 'package:test/test.dart'; +import 'package:test_core/src/util/exit_codes.dart' as exit_codes; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('rejects an invalid fold_stack_frames', () async { + await d + .file('dart_test.yaml', jsonEncode({'fold_stack_frames': 'flup'})) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['fold_stack_frames must be a map', '^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects multiple fold_stack_frames keys', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'fold_stack_frames': { + 'except': ['blah'], + 'only': ['blah'] + } + })) + .create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, + containsInOrder( + ['Can only contain one of "only" or "except".', '^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects invalid fold_stack_frames keys', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'fold_stack_frames': {'invalid': 'blah'} + })) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['Must be "only" or "except".', '^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects invalid fold_stack_frames values', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'fold_stack_frames': {'only': 'blah'} + })) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['Folded packages must be strings', '^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid pause_after_load', () async { + await d + .file('dart_test.yaml', jsonEncode({'pause_after_load': 'flup'})) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['pause_after_load must be a boolean', '^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid verbose_trace', () async { + await d + .file('dart_test.yaml', jsonEncode({'verbose_trace': 'flup'})) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['verbose_trace must be a boolean', '^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid chain_stack_traces', () async { + await d + .file('dart_test.yaml', jsonEncode({'chain_stack_traces': 'flup'})) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['chain_stack_traces must be a boolean', '^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid retry', () async { + await d.file('dart_test.yaml', jsonEncode({'retry': 'flup'})).create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['retry must be a non-negative int', '^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an negative retry values', () async { + await d.file('dart_test.yaml', jsonEncode({'retry': -1})).create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['retry must be a non-negative int', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid js_trace', () async { + await d.file('dart_test.yaml', jsonEncode({'js_trace': 'flup'})).create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, containsInOrder(['js_trace must be a boolean', '^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + group('reporter', () { + test('rejects an invalid type', () async { + await d.file('dart_test.yaml', jsonEncode({'reporter': 12})).create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, containsInOrder(['reporter must be a string', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid name', () async { + await d + .file('dart_test.yaml', jsonEncode({'reporter': 'non-existent'})) + .create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, + containsInOrder( + ['Unknown reporter "non-existent"', '^^^^^^^^^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + }); + + group('file_reporters', () { + test('rejects an invalid type', () async { + await d + .file('dart_test.yaml', jsonEncode({'file_reporters': 12})) + .create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, containsInOrder(['file_reporters must be a map', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid value type', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'file_reporters': {'json': 12} + })) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['file_reporters value must be a string', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid name', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'file_reporters': {'non-existent': 'out'} + })) + .create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, + containsInOrder( + ['Unknown reporter "non-existent"', '^^^^^^^^^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + }); + + test('rejects an invalid concurrency', () async { + await d.file('dart_test.yaml', jsonEncode({'concurrency': 'foo'})).create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, containsInOrder(['concurrency must be an int', '^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + group('timeout', () { + test('rejects an invalid type', () async { + await d.file('dart_test.yaml', jsonEncode({'timeout': 12})).create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, containsInOrder(['timeout must be a string', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid format', () async { + await d.file('dart_test.yaml', jsonEncode({'timeout': '12p'})).create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['Invalid timeout: expected unit', '^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + }); + + group('names', () { + test('rejects an invalid list type', () async { + await d.file('dart_test.yaml', jsonEncode({'names': 'vm'})).create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, containsInOrder(['names must be a list', '^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid member type', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'names': [12] + })) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, containsInOrder(['Names must be strings', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid RegExp', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'names': ['(foo'] + })) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['Invalid name: Unterminated group', '^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + }); + + group('plain_names', () { + test('rejects an invalid list type', () async { + await d + .file('dart_test.yaml', jsonEncode({'plain_names': 'vm'})) + .create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, containsInOrder(['plain_names must be a list', '^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid member type', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'plain_names': [12] + })) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, containsInOrder(['Names must be strings', '^^'])); + await test.shouldExit(exit_codes.data); + }); + }); + + group('platforms', () { + test('rejects an invalid list type', () async { + await d.file('dart_test.yaml', jsonEncode({'platforms': 'vm'})).create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, containsInOrder(['platforms must be a list', '^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid member type', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'platforms': [12] + })) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['Platform name must be a string', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid member name', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'platforms': ['foo'] + })) + .create(); + + await d.dir('test').create(); + + var test = await runTest([]); + expect(test.stderr, containsInOrder(['Unknown platform "foo"', '^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + }); + + group('paths', () { + test('rejects an invalid list type', () async { + await d.file('dart_test.yaml', jsonEncode({'paths': 'test'})).create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, containsInOrder(['paths must be a list', '^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid member type', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'paths': [12] + })) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, containsInOrder(['Paths must be strings', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an absolute path', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'paths': ['/foo'] + })) + .create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, containsInOrder(['Paths must be relative.', '^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid URI', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'paths': [':invalid'] + })) + .create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['Invalid path: Invalid empty scheme', '^^^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + }); + + group('filename', () { + test('rejects an invalid type', () async { + await d.file('dart_test.yaml', jsonEncode({'filename': 12})).create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, containsInOrder(['filename must be a string.', '^^'])); + await test.shouldExit(exit_codes.data); + }); + + test('rejects an invalid format', () async { + await d.file('dart_test.yaml', jsonEncode({'filename': '{foo'})).create(); + + var test = await runTest(['test.dart']); + expect(test.stderr, + containsInOrder(['Invalid filename: expected ",".', '^^^^^^'])); + await test.shouldExit(exit_codes.data); + }); + }); +}
diff --git a/pkgs/test/test/runner/configuration/top_level_test.dart b/pkgs/test/test/runner/configuration/top_level_test.dart new file mode 100644 index 0000000..9c749a4 --- /dev/null +++ b/pkgs/test/test/runner/configuration/top_level_test.dart
@@ -0,0 +1,578 @@ +// Copyright (c) 2016, 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 'package:test/test.dart'; +import 'package:test_core/src/util/io.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('ignores an empty file', () async { + await d.file('dart_test.yaml', '').create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('loads configuration from the path passed to --configuration', () async { + // Make sure dart_test.yaml is ignored. + await d.file('dart_test.yaml', jsonEncode({'run_skipped': true})).create(); + + await d.file('special_test.yaml', jsonEncode({'skip': true})).create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () => throw "oh no"); + } + ''').create(); + + var test = + await runTest(['--configuration', 'special_test.yaml', 'test.dart']); + expect(test.stdout, emitsThrough(contains('All tests skipped.'))); + await test.shouldExit(0); + }); + + test('pauses the test runner after a suite loads with pause_after_load: true', + () async { + await d + .file('dart_test.yaml', jsonEncode({'pause_after_load': true})) + .create(); + + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + print('loaded test!'); + + test("success", () {}); +} +''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + await expectLater(test.stdout, emitsThrough('loaded test!')); + await expectLater( + test.stdout, + emitsInOrder([ + '', + equalsIgnoringWhitespace(''' + The test runner is paused. Open the dev console in Chrome and set + breakpoints. Once you're finished, return to this terminal and press + Enter. + ''') + ])); + + var nextLineFired = false; + + unawaited(test.stdout.next.then(expectAsync1((line) { + expect(line, contains('+0: success')); + nextLineFired = true; + }))); + + // Wait a little bit to be sure that the tests don't start running without + // our input. + await Future<void>.delayed(const Duration(seconds: 2)); + expect(nextLineFired, isFalse); + + test.stdin.writeln(); + await expectLater( + test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome', onPlatform: const { + 'windows': Skip('https://github.com/dart-lang/test/issues/1613') + }); + + test('runs skipped tests with run_skipped: true', () async { + await d.file('dart_test.yaml', jsonEncode({'run_skipped': true})).create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("skip", () => print("In test!"), skip: true); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('In test!'))); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('includes the full stack with verbose_trace: true', () async { + await d + .file('dart_test.yaml', jsonEncode({'verbose_trace': true})) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("failure", () => throw "oh no"); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('dart:async'))); + await test.shouldExit(1); + }); + + test('disables stack trace chaining with chain_stack_traces: false', + () async { + await d + .file('dart_test.yaml', jsonEncode({'chain_stack_traces': false})) + .create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("failure", () async{ + await Future((){}); + await Future((){}); + throw "oh no"; + }); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder([ + '+0: failure', + '+0 -1: failure [E]', + 'oh no', + 'test.dart 9:15 main.<fn>', + ])); + await test.shouldExit(1); + }); + + test("doesn't dartify stack traces for JS-compiled tests with js_trace: true", + () async { + await d.file('dart_test.yaml', jsonEncode({'js_trace': true})).create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("failure", () => throw "oh no"); + } + ''').create(); + + var test = await runTest(['-p', 'chrome', '--verbose-trace', 'test.dart']); + expect(test.stdoutStream(), neverEmits(endsWith(' main.<fn>'))); + expect(test.stdoutStream(), neverEmits(contains('package:test'))); + expect(test.stdoutStream(), neverEmits(contains('dart:async/zone.dart'))); + expect(test.stdout, emitsThrough(contains('-1: Some tests failed.'))); + await test.shouldExit(1); + }, tags: 'chrome'); + + test('retries tests with retry: 1', () async { + await d.file('dart_test.yaml', jsonEncode({'retry': 1})).create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + import 'dart:async'; + + var attempt = 0; + void main() { + test("test", () { + attempt++; + if(attempt <= 1) { + throw 'Failure!'; + } + }); + } + + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed'))); + await test.shouldExit(0); + }); + + test('skips tests with skip: true', () async { + await d.file('dart_test.yaml', jsonEncode({'skip': true})).create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('All tests skipped.'))); + await test.shouldExit(0); + }); + + test('skips tests with skip: reason', () async { + await d + .file('dart_test.yaml', jsonEncode({'skip': 'Tests are boring.'})) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('Tests are boring.'))); + expect(test.stdout, emitsThrough(contains('All tests skipped.'))); + await test.shouldExit(0); + }); + + group('test_on', () { + test('runs tests on a platform matching platform', () async { + await d.file('dart_test.yaml', jsonEncode({'test_on': 'vm'})).create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }); + + test('warns about the VM when no OSes are supported', () async { + await d + .file('dart_test.yaml', jsonEncode({'test_on': 'chrome'})) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, + emits( + "Warning: this package doesn't support running tests on the Dart " + 'VM.')); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }); + + test('warns about the OS when some OSes are supported', () async { + await d.file('dart_test.yaml', jsonEncode({'test_on': otherOS})).create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stderr, + emits("Warning: this package doesn't support running tests on " + '${currentOS.name}.')); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }); + + test('warns about browsers in general when no browsers are supported', + () async { + await d.file('dart_test.yaml', jsonEncode({'test_on': 'vm'})).create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + var test = await runTest(['-p', 'chrome', 'test.dart']); + expect( + test.stderr, + emits( + "Warning: this package doesn't support running tests on browsers.")); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }); + + test( + 'warns about specific browsers when specific browsers are ' + 'supported', () async { + await d + .file('dart_test.yaml', jsonEncode({'test_on': 'safari'})) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + var test = await runTest(['-p', 'chrome,firefox', 'test.dart']); + expect( + test.stderr, + emits("Warning: this package doesn't support running tests on Chrome " + 'or Firefox.')); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }); + }); + + test('uses the specified reporter', () async { + await d.file('dart_test.yaml', jsonEncode({'reporter': 'json'})).create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('"testStart"'))); + await test.shouldExit(0); + }); + + test('uses the specified concurrency', () async { + await d.file('dart_test.yaml', jsonEncode({'concurrency': 2})).create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + ''').create(); + + // We can't reliably test the concurrency, but this at least ensures that + // it doesn't fail to parse. + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('uses the specified timeout', () async { + await d.file('dart_test.yaml', jsonEncode({'timeout': '0s'})).create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("success", () => Future.delayed(Duration.zero)); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder( + ['Test timed out after 0 seconds.', '-1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('runs on the specified platforms', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'platforms': ['vm', 'chrome'] + })) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, + containsInOrder(['[VM, Kernel] success', '[Chrome, Dart2Js] success'])); + await test.shouldExit(0); + }, tags: 'chrome'); + + test('command line args take precedence', () async { + await d.file('dart_test.yaml', jsonEncode({'timeout': '0s'})).create(); + + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("success", () => Future.delayed(Duration.zero)); + } + ''').create(); + + var test = await runTest(['--timeout=none', 'test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }); + + test('uses the specified regexp names', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'names': ['z[ia]p', 'a'] + })) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("zip", () {}); + test("zap", () {}); + test("zop", () {}); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, containsInOrder(['+0: zap', '+1: All tests passed!'])); + await test.shouldExit(0); + }); + + test('uses the specified plain names', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'names': ['z', 'a'] + })) + .create(); + + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("zip", () {}); + test("zap", () {}); + test("zop", () {}); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, containsInOrder(['+0: zap', '+1: All tests passed!'])); + await test.shouldExit(0); + }); + + test('uses the specified paths', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'paths': ['zip', 'zap'] + })) + .create(); + + await d.dir('zip', [ + d.file('zip_test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + ''') + ]).create(); + + await d.dir('zap', [ + d.file('zip_test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + ''') + ]).create(); + + await d.dir('zop', [ + d.file('zip_test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("failure", () => throw "oh no"); + } + ''') + ]).create(); + + var test = await runTest([]); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }); + + test('uses the specified filename', () async { + await d + .file('dart_test.yaml', jsonEncode({'filename': 'test_*.dart'})) + .create(); + + await d.dir('test', [ + d.file('test_foo.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + '''), + d.file('foo_test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("failure", () => throw "oh no"); + } + '''), + d.file('test_foo.bart', ''' + import 'package:test/test.dart'; + + void main() { + test("failure", () => throw "oh no"); + } + ''') + ]).create(); + + var test = await runTest([]); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }); +}
diff --git a/pkgs/test/test/runner/coverage_test.dart b/pkgs/test/test/runner/coverage_test.dart new file mode 100644 index 0000000..b149345 --- /dev/null +++ b/pkgs/test/test/runner/coverage_test.dart
@@ -0,0 +1,138 @@ +// Copyright (c) 2016, 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 'package:test_process/test_process.dart'; + +import '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + group('with the --coverage flag,', () { + late Directory coverageDirectory; + + Future<void> validateCoverage(TestProcess test, String coveragePath) async { + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + + final coverageFile = File(p.join(coverageDirectory.path, coveragePath)); + final coverage = await coverageFile.readAsString(); + final jsonCoverage = json.decode(coverage); + expect(jsonCoverage['coverage'], isNotEmpty); + } + + setUp(() async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test 1", () { + expect(true, isTrue); + }); + } + ''').create(); + + coverageDirectory = + await Directory.systemTemp.createTemp('test_coverage'); + }); + + tearDown(() async { + await coverageDirectory.delete(recursive: true); + }); + + test('gathers coverage for VM tests', () async { + var test = + await runTest(['--coverage', coverageDirectory.path, 'test.dart']); + await validateCoverage(test, 'test.dart.vm.json'); + }); + + test('gathers coverage for Chrome tests', () async { + var test = await runTest( + ['--coverage', coverageDirectory.path, 'test.dart', '-p', 'chrome']); + await validateCoverage(test, 'test.dart.chrome.json'); + }); + + test( + 'gathers coverage for Chrome tests when JS files contain unicode characters', + () async { + final sourceMapFileContent = + '{"version":3,"file":"","sources":[],"names":[],"mappings":""}'; + final jsContent = ''' + (function() { + '© ' + window.foo = function foo() { + return 'foo'; + }; + })({ + + '© ': '' + }); + '''; + await d.file('file_with_unicode.js', jsContent).create(); + await d.file('file_with_unicode.js.map', sourceMapFileContent).create(); + + await d.file('js_with_unicode_test.dart', ''' + import 'dart:async'; + + import 'package:js/js.dart'; + import 'package:js/js_util.dart'; + + import 'package:test/src/runner/browser/dom.dart' as dom; + import 'package:test/test.dart'; + + Future<void> loadScript(String src) async { + final controller = StreamController(); + final scriptLoaded = controller.stream.first; + final script = dom.createHTMLScriptElement()..src = src; + script.addEventListener('load', + allowInterop((_) { + controller.add('loaded'); + })); + dom.document.body!.appendChild(script); + await scriptLoaded.timeout(Duration(seconds: 1)); + } + + void main() { + test("test 1", () async { + await loadScript('file_with_unicode.js'); + expect(getProperty(dom.window, 'foo'), isNotNull); + callMethod(dom.window, 'foo', []); + expect(true, isTrue); + }); + } + ''').create(); + + final jsBytes = utf8.encode(jsContent); + final jsLatin1 = latin1.decode(jsBytes); + final jsUtf8 = utf8.decode(jsBytes); + expect(jsLatin1, isNot(jsUtf8), + reason: 'test setup: should have decoded differently'); + + const functionPattern = 'function foo'; + expect([jsLatin1, jsUtf8], everyElement(contains(functionPattern))); + expect(jsLatin1.indexOf(functionPattern), + isNot(jsUtf8.indexOf(functionPattern)), + reason: + 'test setup: decoding should have shifted the position of the function'); + + var test = await runTest([ + '--coverage', + coverageDirectory.path, + 'js_with_unicode_test.dart', + '-p', + 'chrome' + ]); + await validateCoverage(test, 'js_with_unicode_test.dart.chrome.json'); + }); + }); +}
diff --git a/pkgs/test/test/runner/engine_test.dart b/pkgs/test/test/runner/engine_test.dart new file mode 100644 index 0000000..6b7df23 --- /dev/null +++ b/pkgs/test/test/runner/engine_test.dart
@@ -0,0 +1,357 @@ +// Copyright (c) 2015, 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:math'; + +import 'package:test/test.dart'; +import 'package:test_api/src/backend/group.dart'; +import 'package:test_api/src/backend/state.dart'; +import 'package:test_core/src/runner/engine.dart'; + +import '../utils.dart'; + +void main() { + test('runs each test in each suite in order', () async { + var testsRun = 0; + var tests = declare(() { + for (var i = 0; i < 4; i++) { + test( + 'test ${i + 1}', + expectAsync0(() { + expect(testsRun, equals(i)); + testsRun++; + }, max: 1)); + } + }); + + var engine = Engine.withSuites([ + runnerSuite(Group.root(tests.take(2))), + runnerSuite(Group.root(tests.skip(2))) + ]); + + await engine.run(); + expect(testsRun, equals(4)); + }); + + test('runs tests in a suite added after run() was called', () { + var testsRun = 0; + var tests = declare(() { + for (var i = 0; i < 4; i++) { + test( + 'test ${i + 1}', + expectAsync0(() { + expect(testsRun, equals(i)); + testsRun++; + }, max: 1)); + } + }); + + var engine = Engine(); + expect( + engine.run().then((_) { + expect(testsRun, equals(4)); + }), + completes); + + engine.suiteSink.add(runnerSuite(Group.root(tests))); + engine.suiteSink.close(); + }); + + test('returns fail if any test does not complete', () async { + var completer = Completer<void>(); + var engine = declareEngine(() { + test('completes', () {}); + test('does not complete', () async { + await completer.future; + }); + }); + expect(engine.run(), completion(isFalse)); + await pumpEventQueue(); + unawaited(engine.close()); + // We need to complete this so the outer test finishes. + completer.complete(); + }); + + test( + 'emits each test before it starts running and after the previous test ' + 'finished', () { + var testsRun = 0; + var engine = declareEngine(() { + for (var i = 0; i < 3; i++) { + test('test ${i + 1}', expectAsync0(() => testsRun++, max: 1)); + } + }); + + engine.onTestStarted.listen(expectAsync1((liveTest) { + // [testsRun] should be one less than the test currently running. + expect(liveTest.test.name, equals('test ${testsRun + 1}')); + + // [Engine.onTestStarted] is guaranteed to fire before the first + // [LiveTest.onStateChange]. + expect(liveTest.onStateChange.first, + completion(equals(const State(Status.running, Result.success)))); + }, count: 3, max: 3)); + + return engine.run(); + }); + + test('.run() returns true if every test passes', () { + var engine = declareEngine(() { + for (var i = 0; i < 2; i++) { + test('test ${i + 1}', () {}); + } + }); + + expect(engine.run(), completion(isTrue)); + }); + + test('.run() returns false if any test fails', () { + var engine = declareEngine(() { + for (var i = 0; i < 2; i++) { + test('test ${i + 1}', () {}); + } + test('failure', () => throw TestFailure('oh no')); + }); + + expect(engine.run(), completion(isFalse)); + }); + + test('.run() returns false if any test errors', () { + var engine = declareEngine(() { + for (var i = 0; i < 2; i++) { + test('test ${i + 1}', () {}); + } + test('failure', () => throw 'oh no'); + }); + + expect(engine.run(), completion(isFalse)); + }); + + test('.run() does not run more tests after failure for stopOnFirstFailure', + () async { + var secondTestRan = false; + var engine = declareEngine(() { + test('failure', () => throw 'oh no'); + test('subsequent', () { + secondTestRan = true; + }); + }, stopOnFirstFailure: true); + await expectLater(engine.run(), completion(isFalse)); + expect(secondTestRan, false); + }); + + test('.run() may not be called more than once', () { + var engine = Engine.withSuites([]); + expect(engine.run(), completes); + expect(engine.run, throwsStateError); + }); + + test('runs tearDown after a test times out', () { + // Declare this here so the expect is in the context of this test, rather + // than the inner test. + var secondTestStarted = false; + var firstTestFinished = false; + var tearDownBody = expectAsync0(() { + expect(secondTestStarted, isFalse); + expect(firstTestFinished, isFalse); + }); + + var engine = declareEngine(() { + // This ensures that the first test doesn't actually finish until the + // second test runs. + var firstTestCompleter = Completer<void>(); + + group('group', () { + tearDown(tearDownBody); + + test('first test', () async { + await firstTestCompleter.future; + firstTestFinished = true; + }, timeout: const Timeout(Duration.zero)); + }); + + test('second test', () { + secondTestStarted = true; + firstTestCompleter.complete(); + }); + }); + + expect(engine.run(), completes); + }); + + group('for a skipped test', () { + test("doesn't run the test's body", () async { + var bodyRun = false; + var engine = declareEngine(() { + test('test', () => bodyRun = true, skip: true); + }); + + await engine.run(); + expect(bodyRun, isFalse); + }); + + test("runs the test's body with --run-skipped", () async { + var bodyRun = false; + var engine = declareEngine(() { + test('test', () => bodyRun = true, skip: true); + }, runSkipped: true); + + await engine.run(); + expect(bodyRun, isTrue); + }); + + test('exposes a LiveTest that emits the correct states', () { + var tests = declare(() { + test('test', () {}, skip: true); + }); + + var engine = Engine.withSuites([runnerSuite(Group.root(tests))]); + + engine.onTestStarted.listen(expectAsync1((liveTest) { + expect(liveTest, same(engine.liveTests.single)); + expect(liveTest.test.name, equals(tests.single.name)); + + var i = 0; + liveTest.onStateChange.listen(expectAsync1((state) { + if (i == 0) { + expect(state, equals(const State(Status.running, Result.success))); + } else if (i == 1) { + expect(state, equals(const State(Status.running, Result.skipped))); + } else if (i == 2) { + expect(state, equals(const State(Status.complete, Result.skipped))); + } + i++; + }, count: 3)); + + expect(liveTest.onComplete, completes); + })); + + return engine.run(); + }); + }); + + group('for a skipped group', () { + test("doesn't run a test in the group", () async { + var bodyRun = false; + var engine = declareEngine(() { + group('group', () { + test('test', () => bodyRun = true); + }, skip: true); + }); + + await engine.run(); + expect(bodyRun, isFalse); + }); + + test('runs tests in the group with --run-skipped', () async { + var bodyRun = false; + var engine = declareEngine(() { + group('group', () { + test('test', () => bodyRun = true); + }, skip: true); + }, runSkipped: true); + + await engine.run(); + expect(bodyRun, isTrue); + }); + + test('runs tests in the group when they are skip: false', () async { + var bodyRun = false; + var engine = declareEngine(() { + group('group', () { + test('test', skip: false, () => bodyRun = true); + }, skip: true); + }); + + await engine.run(); + expect(bodyRun, isTrue); + }); + + test('exposes a LiveTest that emits the correct states', () { + var entries = declare(() { + group('group', () { + test('test', () {}); + }, skip: true); + }); + + var engine = Engine.withSuites([runnerSuite(Group.root(entries))]); + + engine.onTestStarted.listen(expectAsync1((liveTest) { + expect(liveTest, same(engine.liveTests.single)); + expect(liveTest.test.name, equals('group test')); + + var i = 0; + liveTest.onStateChange.listen(expectAsync1((state) { + if (i == 0) { + expect(state, equals(const State(Status.running, Result.success))); + } else if (i == 1) { + expect(state, equals(const State(Status.running, Result.skipped))); + } else if (i == 2) { + expect(state, equals(const State(Status.complete, Result.skipped))); + } + i++; + }, count: 3)); + + expect(liveTest.onComplete, completes); + })); + + return engine.run(); + }); + }); + + group('concurrency', () { + test('is shared between runner and load suites', () async { + for (var concurrency = 1; concurrency < 5; concurrency++) { + var testsLoaded = 0; + var maxLoadConcurrency = 0; + var testsRunning = 0; + var maxTestConcurrency = 0; + var testCount = concurrency * 2; + + Future<void> updateAndCheckConcurrency( + {bool isLoadSuite = false}) async { + if (isLoadSuite) { + testsLoaded++; + maxLoadConcurrency = max(maxLoadConcurrency, testsLoaded); + expect(testsLoaded, lessThanOrEqualTo(concurrency)); + } else { + testsRunning++; + maxTestConcurrency = max(maxTestConcurrency, testsRunning); + expect(testsRunning, lessThanOrEqualTo(concurrency)); + } + // Simulate the test/loading taking some amount of time so that + // we actually reach max concurrency. + await Future<void>.delayed(const Duration(milliseconds: 100)); + if (!isLoadSuite) { + testsRunning--; + testsLoaded--; + } + } + + var tests = declare(() { + for (var i = 0; i < testCount; i++) { + test('test ${i + 1}', () async { + await updateAndCheckConcurrency(); + }); + } + }); + var engine = Engine.withSuites([ + for (var i = 0; i < testCount; i++) + loadSuite('group $i', () async { + await updateAndCheckConcurrency(isLoadSuite: true); + return runnerSuite(Group.root([tests[i]])); + }), + ], concurrency: concurrency); + + await engine.run(); + expect(engine.liveTests.length, testCount); + + // We should reach but not exceed max concurrency + expect(maxTestConcurrency, concurrency); + expect(maxLoadConcurrency, concurrency); + } + }); + }); +}
diff --git a/pkgs/test/test/runner/expanded_reporter_test.dart b/pkgs/test/test/runner/expanded_reporter_test.dart new file mode 100644 index 0000000..1c11686 --- /dev/null +++ b/pkgs/test/test/runner/expanded_reporter_test.dart
@@ -0,0 +1,386 @@ +// Copyright (c) 2015, 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 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('reports when no tests are run', () async { + await d.file('test.dart', 'void main() {}').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }); + + test('runs several successful tests and reports when each completes', () { + return _expectReport(''' + test('success 1', () {}); + test('success 2', () {}); + test('success 3', () {});''', ''' + +0: loading test.dart + +0: success 1 + +1: success 2 + +2: success 3 + +3: All tests passed!'''); + }); + + test('runs several failing tests and reports when each fails', () { + return _expectReport(''' + test('failure 1', () => throw TestFailure('oh no')); + test('failure 2', () => throw TestFailure('oh no')); + test('failure 3', () => throw TestFailure('oh no'));''', ''' + +0: loading test.dart + +0: failure 1 + +0 -1: failure 1 [E] + oh no + test.dart 6:33 main.<fn> + + +0 -1: failure 2 + +0 -2: failure 2 [E] + oh no + test.dart 7:33 main.<fn> + + +0 -2: failure 3 + +0 -3: failure 3 [E] + oh no + test.dart 8:33 main.<fn> + + +0 -3: Some tests failed.'''); + }); + + test('includes the full stack trace with --verbose-trace', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("failure", () => throw "oh no"); +} +''').create(); + + var test = await runTest(['--verbose-trace', 'test.dart']); + expect(test.stdout, emitsThrough(contains('dart:async'))); + await test.shouldExit(1); + }); + + test('runs failing tests along with successful tests', () { + return _expectReport(''' + test('failure 1', () => throw TestFailure('oh no')); + test('success 1', () {}); + test('failure 2', () => throw TestFailure('oh no')); + test('success 2', () {});''', ''' + +0: loading test.dart + +0: failure 1 + +0 -1: failure 1 [E] + oh no + test.dart 6:33 main.<fn> + + +0 -1: success 1 + +1 -1: failure 2 + +1 -2: failure 2 [E] + oh no + test.dart 8:33 main.<fn> + + +1 -2: success 2 + +2 -2: Some tests failed.'''); + }); + + test('always prints the full test name', () { + return _expectReport(''' + test( + 'really gosh dang long test name. Even longer than that. No, yet ' + 'longer. A little more... okay, that should do it.', + () {});''', ''' + +0: loading test.dart + +0: really gosh dang long test name. Even longer than that. No, yet longer. A little more... okay, that should do it. + +1: All tests passed!'''); + }); + + test('gracefully handles multiple test failures in a row', () { + return _expectReport(''' + // This completer ensures that the test isolate isn't killed until all + // errors have been thrown. + var completer = Completer(); + test('failures', () { + Future.microtask(() => throw 'first error'); + Future.microtask(() => throw 'second error'); + Future.microtask(() => throw 'third error'); + Future.microtask(completer.complete); + }); + test('wait', () => completer.future);''', ''' + +0: loading test.dart + +0: failures + +0 -1: failures [E] + first error + test.dart 10:34 main.<fn>.<fn> + ===== asynchronous gap =========================== + dart:async new Future.microtask + test.dart 10:18 main.<fn> + + second error + test.dart 11:34 main.<fn>.<fn> + ===== asynchronous gap =========================== + dart:async new Future.microtask + test.dart 11:18 main.<fn> + + third error + test.dart 12:34 main.<fn>.<fn> + ===== asynchronous gap =========================== + dart:async new Future.microtask + test.dart 12:18 main.<fn> + + +0 -1: wait + +1 -1: Some tests failed.'''); + }); + + group('print:', () { + test('handles multiple prints', () { + return _expectReport(''' + test('test', () { + print("one"); + print("two"); + print("three"); + print("four"); + });''', ''' + +0: loading test.dart + +0: test + one + two + three + four + +1: All tests passed!'''); + }); + + test('handles a print after the test completes', () { + return _expectReport(''' + // This completer ensures that the test isolate isn't killed until all + // prints have happened. + var testDone = Completer(); + var waitStarted = Completer(); + test('test', () async { + waitStarted.future.then((_) { + Future(() => print("one")); + Future(() => print("two")); + Future(() => print("three")); + Future(() => print("four")); + Future(testDone.complete); + }); + }); + + test('wait', () { + waitStarted.complete(); + return testDone.future; + });''', ''' + +0: loading test.dart + +0: test + +1: wait + +1: test + one + two + three + four + +2: All tests passed!'''); + }); + + test('interleaves prints and errors', () { + return _expectReport(''' + // This completer ensures that the test isolate isn't killed until all + // prints have happened. + var completer = Completer(); + test('test', () { + scheduleMicrotask(() { + print("three"); + print("four"); + throw "second error"; + }); + + scheduleMicrotask(() { + print("five"); + print("six"); + completer.complete(); + }); + + print("one"); + print("two"); + throw "first error"; + }); + + test('wait', () => completer.future);''', ''' + +0: loading test.dart + +0: test + one + two + +0 -1: test [E] + first error + test.dart 24:11 main.<fn> + + three + four + second error + test.dart 13:13 main.<fn>.<fn> + ===== asynchronous gap =========================== + dart:async scheduleMicrotask + test.dart 10:11 main.<fn> + + five + six + +0 -1: wait + +1 -1: Some tests failed.'''); + }); + }); + + group('skip:', () { + test('displays skipped tests separately', () { + return _expectReport(''' + test('skip 1', () {}, skip: true); + test('skip 2', () {}, skip: true); + test('skip 3', () {}, skip: true);''', ''' + +0: loading test.dart + +0: skip 1 + +0 ~1: skip 2 + +0 ~2: skip 3 + +0 ~3: All tests skipped.'''); + }); + + test('displays a skipped group', () { + return _expectReport(''' + group('skip', () { + test('test 1', () {}); + test('test 2', () {}); + test('test 3', () {}); + }, skip: true);''', ''' + +0: loading test.dart + +0: skip test 1 + +0 ~1: skip test 2 + +0 ~2: skip test 3 + +0 ~3: All tests skipped.'''); + }); + + test('runs skipped tests along with successful tests', () { + return _expectReport(''' + test('skip 1', () {}, skip: true); + test('success 1', () {}); + test('skip 2', () {}, skip: true); + test('success 2', () {});''', ''' + +0: loading test.dart + +0: skip 1 + +0 ~1: success 1 + +1 ~1: skip 2 + +1 ~2: success 2 + +2 ~2: All tests passed!'''); + }); + + test('runs skipped tests along with successful and failing tests', () { + return _expectReport(''' + test('failure 1', () => throw TestFailure('oh no')); + test('skip 1', () {}, skip: true); + test('success 1', () {}); + test('failure 2', () => throw TestFailure('oh no')); + test('skip 2', () {}, skip: true); + test('success 2', () {});''', ''' + +0: loading test.dart + +0: failure 1 + +0 -1: failure 1 [E] + oh no + test.dart 6:35 main.<fn> + + +0 -1: skip 1 + +0 ~1 -1: success 1 + +1 ~1 -1: failure 2 + +1 ~1 -2: failure 2 [E] + oh no + test.dart 9:35 main.<fn> + + +1 ~1 -2: skip 2 + +1 ~2 -2: success 2 + +2 ~2 -2: Some tests failed.'''); + }); + + test('displays the skip reason if available', () { + return _expectReport(''' + test('skip 1', () {}, skip: 'some reason'); + test('skip 2', () {}, skip: 'or another');''', ''' + +0: loading test.dart + +0: skip 1 + Skip: some reason + +0 ~1: skip 2 + Skip: or another + +0 ~2: All tests skipped.'''); + }); + + test('runs skipped tests with --run-skipped', () { + return _expectReport(''' + test('skip 1', () {}, skip: 'some reason'); + test('skip 2', () {}, skip: 'or another');''', ''' + +0: loading test.dart + +0: skip 1 + +1: skip 2 + +2: All tests passed!''', args: ['--run-skipped']); + }); + }); + + test('Directs users to enable stack trace chaining if disabled', () async { + await _expectReport( + '''test('failure 1', () => throw TestFailure('oh no'));''', ''' + +0: loading test.dart + +0: failure 1 + +0 -1: failure 1 [E] + oh no + test.dart 6:25 main.<fn> + + +0 -1: Some tests failed. + + Consider enabling the flag chain-stack-traces to receive more detailed exceptions. + For example, 'dart test --chain-stack-traces'.''', + chainStackTraces: false); + }); +} + +Future<void> _expectReport(String tests, String expected, + {List<String> args = const [], bool chainStackTraces = true}) async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { +$tests + } + ''').create(); + + var test = await runTest([ + 'test.dart', + if (chainStackTraces) '--chain-stack-traces', + ...args, + ]); + await test.shouldExit(); + + var stdoutLines = await test.stdoutStream().toList(); + + // Remove excess trailing whitespace and trim off timestamps. + var actual = stdoutLines.map((line) { + if (line.startsWith(' ') || line.isEmpty) return line.trimRight(); + return line.trim().replaceFirst(RegExp('^[0-9]{2}:[0-9]{2} '), ''); + }).join('\n'); + + // Un-indent the expected string. + var indentation = expected.indexOf(RegExp('[^ ]')); + expected = expected.split('\n').map((line) { + if (line.isEmpty) return line; + return line.substring(indentation); + }).join('\n'); + + expect(actual, equals(expected)); +}
diff --git a/pkgs/test/test/runner/failures_only_reporter_test.dart b/pkgs/test/test/runner/failures_only_reporter_test.dart new file mode 100644 index 0000000..3e2690a --- /dev/null +++ b/pkgs/test/test/runner/failures_only_reporter_test.dart
@@ -0,0 +1,257 @@ +// Copyright (c) 2024, 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 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('reports when no tests are run', () async { + await d.file('test.dart', 'void main() {}').create(); + + var test = await runTest(['test.dart'], reporter: 'failures-only'); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }); + + test('runs several successful tests and reports only at the end', () { + return _expectReport(''' + test('success 1', () {}); + test('success 2', () {}); + test('success 3', () {});''', ''' + +3: All tests passed!'''); + }); + + test('runs several failing tests and reports when each fails', () { + return _expectReport(''' + test('failure 1', () => throw TestFailure('oh no')); + test('failure 2', () => throw TestFailure('oh no')); + test('failure 3', () => throw TestFailure('oh no'));''', ''' + +0 -1: failure 1 [E] + oh no + test.dart 6:33 main.<fn> + + +0 -2: failure 2 [E] + oh no + test.dart 7:33 main.<fn> + + +0 -3: failure 3 [E] + oh no + test.dart 8:33 main.<fn> + + +0 -3: Some tests failed.'''); + }); + + test('includes the full stack trace with --verbose-trace', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("failure", () => throw "oh no"); +} +''').create(); + + var test = await runTest(['--verbose-trace', 'test.dart'], + reporter: 'failures-only'); + expect(test.stdout, emitsThrough(contains('dart:async'))); + await test.shouldExit(1); + }); + + test('reports only failing tests amid successful tests', () { + return _expectReport(''' + test('failure 1', () => throw TestFailure('oh no')); + test('success 1', () {}); + test('failure 2', () => throw TestFailure('oh no')); + test('success 2', () {});''', ''' + +0 -1: failure 1 [E] + oh no + test.dart 6:33 main.<fn> + + +1 -2: failure 2 [E] + oh no + test.dart 8:33 main.<fn> + + +2 -2: Some tests failed.'''); + }); + + group('print:', () { + test('handles multiple prints', () { + return _expectReport(''' + test('test', () { + print("one"); + print("two"); + print("three"); + print("four"); + });''', ''' + +0: test + one + two + three + four + +1: All tests passed!'''); + }); + + test('handles a print after the test completes', () { + return _expectReport(''' + // This completer ensures that the test isolate isn't killed until all + // prints have happened. + var testDone = Completer(); + var waitStarted = Completer(); + test('test', () async { + waitStarted.future.then((_) { + Future(() => print("one")); + Future(() => print("two")); + Future(() => print("three")); + Future(() => print("four")); + Future(testDone.complete); + }); + }); + + test('wait', () { + waitStarted.complete(); + return testDone.future; + });''', ''' + +1: test + one + two + three + four + +2: All tests passed!'''); + }); + + test('interleaves prints and errors', () { + return _expectReport(''' + // This completer ensures that the test isolate isn't killed until all + // prints have happened. + var completer = Completer(); + test('test', () { + scheduleMicrotask(() { + print("three"); + print("four"); + throw "second error"; + }); + + scheduleMicrotask(() { + print("five"); + print("six"); + completer.complete(); + }); + + print("one"); + print("two"); + throw "first error"; + }); + + test('wait', () => completer.future);''', ''' + +0: test + one + two + +0 -1: test [E] + first error + test.dart 24:11 main.<fn> + + three + four + second error + test.dart 13:13 main.<fn>.<fn> + ===== asynchronous gap =========================== + dart:async scheduleMicrotask + test.dart 10:11 main.<fn> + + five + six + +1 -1: Some tests failed.'''); + }); + }); + + group('skip:', () { + test('does not emit for skips', () { + return _expectReport(''' + test('skip 1', () {}, skip: true); + test('skip 2', () {}, skip: true); + test('skip 3', () {}, skip: true);''', ''' + +0 ~3: All tests skipped.'''); + }); + + test('runs skipped tests along with successful and failing tests', () { + return _expectReport(''' + test('failure 1', () => throw TestFailure('oh no')); + test('skip 1', () {}, skip: true); + test('success 1', () {}); + test('failure 2', () => throw TestFailure('oh no')); + test('skip 2', () {}, skip: true); + test('success 2', () {});''', ''' + +0 -1: failure 1 [E] + oh no + test.dart 6:35 main.<fn> + + +1 ~1 -2: failure 2 [E] + oh no + test.dart 9:35 main.<fn> + + +2 ~2 -2: Some tests failed.'''); + }); + }); + + test('Directs users to enable stack trace chaining if disabled', () async { + await _expectReport( + '''test('failure 1', () => throw TestFailure('oh no'));''', ''' + +0 -1: failure 1 [E] + oh no + test.dart 6:25 main.<fn> + + +0 -1: Some tests failed. + + Consider enabling the flag chain-stack-traces to receive more detailed exceptions. + For example, 'dart test --chain-stack-traces'.''', + chainStackTraces: false); + }); +} + +Future<void> _expectReport(String tests, String expected, + {List<String> args = const [], bool chainStackTraces = true}) async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { +$tests + } + ''').create(); + + var test = await runTest([ + 'test.dart', + if (chainStackTraces) '--chain-stack-traces', + ...args, + ], reporter: 'failures-only'); + await test.shouldExit(); + + var stdoutLines = await test.stdoutStream().toList(); + + // Remove excess trailing whitespace. + var actual = stdoutLines.map((line) { + if (line.startsWith(' ') || line.isEmpty) return line.trimRight(); + return line.trim(); + }).join('\n'); + + // Un-indent the expected string. + var indentation = expected.indexOf(RegExp('[^ ]')); + expected = expected.split('\n').map((line) { + if (line.isEmpty) return line; + return line.substring(indentation); + }).join('\n'); + + expect(actual, equals(expected)); +}
diff --git a/pkgs/test/test/runner/github_reporter_test.dart b/pkgs/test/test/runner/github_reporter_test.dart new file mode 100644 index 0000000..db50058 --- /dev/null +++ b/pkgs/test/test/runner/github_reporter_test.dart
@@ -0,0 +1,377 @@ +// 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. + +@TestOn('vm') +library; + +import 'dart:async'; + +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('reports when no tests are run', () async { + await d.file('test.dart', 'void main() {}').create(); + + var test = await runTest(['test.dart'], reporter: 'github'); + expect(test.stdout, emitsThrough(contains('0 tests passed'))); + await test.shouldExit(79); + }); + + test('runs several successful tests and reports when each completes', () { + return _expectReport(''' + test('success 1', () {}); + test('success 2', () {}); + test('success 3', () {});''', ''' + ✅ success 1 + ✅ success 2 + ✅ success 3 + 🎉 3 tests passed.'''); + }); + + test('includes the platform name when multiple platforms are run', () { + return _expectReportLines(''' + test('success 1', () {});''', [ + '✅ [VM, Kernel] success 1', + '✅ [Chrome, Dart2Js] success 1', + '🎉 2 tests passed.', + ], args: [ + '-p', + 'vm,chrome' + ]); + }); + + test('runs several failing tests and reports when each fails', () { + return _expectReport(''' + test('failure 1', () => throw TestFailure('oh no')); + test('failure 2', () => throw TestFailure('oh no')); + test('failure 3', () => throw TestFailure('oh no'));''', ''' + ::group::❌ failure 1 (failed) + oh no + test.dart 6:33 main.<fn> + ::endgroup:: + ::group::❌ failure 2 (failed) + oh no + test.dart 7:33 main.<fn> + ::endgroup:: + ::group::❌ failure 3 (failed) + oh no + test.dart 8:33 main.<fn> + ::endgroup:: + ::error::0 tests passed, 3 failed.'''); + }); + + test('includes the full stack trace with --verbose-trace', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("failure", () => throw "oh no"); +} +''').create(); + + var test = + await runTest(['--verbose-trace', 'test.dart'], reporter: 'github'); + expect(test.stdout, emitsThrough(contains('dart:async'))); + await test.shouldExit(1); + }); + + test('runs failing tests along with successful tests', () { + return _expectReport(''' + test('failure 1', () => throw TestFailure('oh no')); + test('success 1', () {}); + test('failure 2', () => throw TestFailure('oh no')); + test('success 2', () {});''', ''' + ::group::❌ failure 1 (failed) + oh no + test.dart 6:33 main.<fn> + ::endgroup:: + ✅ success 1 + ::group::❌ failure 2 (failed) + oh no + test.dart 8:33 main.<fn> + ::endgroup:: + ✅ success 2 + ::error::2 tests passed, 2 failed.'''); + }); + + test('always prints the full test name', () { + return _expectReport( + ''' + test( + 'really gosh dang long test name. Even longer than that. No, yet ' + 'longer. A little more... okay, that should do it.', + () {});''', + '✅ really gosh dang long test name. Even longer than that. No, yet longer. A little more... okay, that should do it.', + useContains: true, + ); + }); + + test('gracefully handles multiple test failures in a row', () { + return _expectReport(''' + // This completer ensures that the test isolate isn't killed until all + // errors have been thrown. + var completer = Completer(); + test('failures', () { + Future.microtask(() => throw 'first error'); + Future.microtask(() => throw 'second error'); + Future.microtask(() => throw 'third error'); + Future.microtask(completer.complete); + }); + test('wait', () => completer.future);''', ''' + ::group::❌ failures (failed) + first error + test.dart 10:34 main.<fn>.<fn> + second error + test.dart 11:34 main.<fn>.<fn> + third error + test.dart 12:34 main.<fn>.<fn> + ::endgroup:: + ✅ wait + ::error::1 test passed, 1 failed.'''); + }); + + test('displays failures occuring after a test completes', () { + return _expectReport( + ''' + test('fail after completion', () { + Future(() { + Zone.current.handleUncaughtError('foo', StackTrace.current); + }); + }); + + test('second test so that the first failure is reported', () {});''', + ''' + ✅ fail after completion + ::group::❌ fail after completion (failed after test completion) + foo + test.dart 8:62 main.<fn>.<fn> + ::endgroup:: + ::group::❌ fail after completion (failed after test completion) + This test failed after it had already completed. + Make sure to use a matching library which informs the test runner + of pending async work. + test.dart 8:62 main.<fn>.<fn> + ::endgroup:: + ✅ second test so that the first failure is reported + ::error::1 test passed, 1 failed.''', + ); + }); + + group('print:', () { + test('handles multiple prints', () { + return _expectReport( + ''' + test('test', () { + print("one"); + print("two"); + print("three"); + print("four"); + });''', + ''' + ::group::✅ test + one + two + three + four + ::endgroup::''', + useContains: true, + ); + }); + + test('handles a print after the test completes', () { + return _expectReport(''' + // This completer ensures that the test isolate isn't killed until all + // prints have happened. + var testDone = Completer(); + var waitStarted = Completer(); + test('test', () async { + waitStarted.future.then((_) { + Future(() => print("one")); + Future(() => print("two")); + Future(() => print("three")); + Future(() => print("four")); + Future(testDone.complete); + }); + }); + + test('wait', () { + waitStarted.complete(); + return testDone.future; + });''', ''' + ✅ test + one + two + three + four + ✅ wait + 🎉 2 tests passed.'''); + }); + }); + + group('skip:', () { + test('displays skipped tests', () { + return _expectReport(''' + test('skip 1', () {}, skip: true); + test('skip 2', () {}, skip: true); + test('skip 3', () {}, skip: true);''', ''' + ❎ skip 1 (skipped) + ❎ skip 2 (skipped) + ❎ skip 3 (skipped) + 🎉 0 tests passed, 3 skipped.'''); + }); + + test('displays a skipped group', () { + return _expectReport(''' + group('skip', () { + test('test 1', () {}); + test('test 2', () {}); + test('test 3', () {}); + }, skip: true);''', ''' + ❎ skip test 1 (skipped) + ❎ skip test 2 (skipped) + ❎ skip test 3 (skipped) + 🎉 0 tests passed, 3 skipped.'''); + }); + + test('runs skipped tests along with successful tests', () { + return _expectReport(''' + test('skip 1', () {}, skip: true); + test('success 1', () {}); + test('skip 2', () {}, skip: true); + test('success 2', () {});''', ''' + ❎ skip 1 (skipped) + ✅ success 1 + ❎ skip 2 (skipped) + ✅ success 2 + 🎉 2 tests passed, 2 skipped.'''); + }); + + test('runs skipped tests along with successful and failing tests', () { + return _expectReport(''' + test('failure 1', () => throw TestFailure('oh no')); + test('skip 1', () {}, skip: true); + test('success 1', () {}); + test('failure 2', () => throw TestFailure('oh no')); + test('skip 2', () {}, skip: true); + test('success 2', () {});''', ''' + ::group::❌ failure 1 (failed) + oh no + test.dart 6:35 main.<fn> + ::endgroup:: + ❎ skip 1 (skipped) + ✅ success 1 + ::group::❌ failure 2 (failed) + oh no + test.dart 9:35 main.<fn> + ::endgroup:: + ❎ skip 2 (skipped) + ✅ success 2 + ::error::2 tests passed, 2 failed, 2 skipped.'''); + }); + + test('displays the skip reason if available', () { + return _expectReport(''' + test('skip 1', () {}, skip: 'some reason'); + test('skip 2', () {}, skip: 'or another');''', ''' + ::group::❎ skip 1 (skipped) + Skip: some reason + ::endgroup:: + ::group::❎ skip 2 (skipped) + Skip: or another + ::endgroup:: + 🎉 0 tests passed, 2 skipped.'''); + }); + }); + + test('loadSuite, setUpAll, and tearDownAll hidden if no content', () { + return _expectReport(''' + group('one', () { + setUpAll(() {/* nothing to do here */}); + tearDownAll(() {/* nothing to do here */}); + test('test 1', () {}); + });''', ''' + ✅ one test 1 + 🎉 1 test passed.'''); + }); + + test('setUpAll and tearDownAll show when they have content', () { + return _expectReport(''' + group('one', () { + setUpAll(() { print('one'); }); + tearDownAll(() { print('two'); }); + test('test 1', () {}); + });''', ''' + ::group::✅ one (setUpAll) + one + ::endgroup:: + ✅ one test 1 + ::group::✅ one (tearDownAll) + two + ::endgroup:: + 🎉 1 test passed.'''); + }); +} + +/// Expects exactly [expected] to appear in the test output. +/// +/// If [useContains] is passed, then the output only must contain [expected]. +Future<void> _expectReport( + String tests, + String expected, { + List<String> args = const [], + bool useContains = false, +}) async { + expected = expected.split('\n').map(_unindent).join('\n'); + + var actual = (await _reportLines(tests, args)).join('\n'); + + expect(actual, useContains ? contains(expected) : equals(expected)); +} + +/// Expects all of [expected] lines to appear in the test output, but additional +/// output is allowed. +Future<void> _expectReportLines( + String tests, + List<String> expected, { + List<String> args = const [], +}) async { + expected = [for (var line in expected) _unindent(line)]; + var actual = await _reportLines(tests, args); + expect(actual, containsAllInOrder(expected)); +} + +/// All the output lines from running [tests]. +Future<List<String>> _reportLines(String tests, List<String> args) async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { +$tests + } + ''').create(); + + var test = await runTest([ + 'test.dart', + ...args, + ], reporter: 'github'); + await test.shouldExit(); + + var stdoutLines = await test.stdoutStream().toList(); + return stdoutLines + .map((line) => line.trim()) + .where((line) => line.isNotEmpty) + .toList(); +} + +/// Removes all leading space from [line]. +String _unindent(String line) => line.trimLeft();
diff --git a/pkgs/test/test/runner/json_file_reporter_test.dart b/pkgs/test/test/runner/json_file_reporter_test.dart new file mode 100644 index 0000000..ce3bd42 --- /dev/null +++ b/pkgs/test/test/runner/json_file_reporter_test.dart
@@ -0,0 +1,155 @@ +// Copyright (c) 2020, 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:io'; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_core/src/util/exit_codes.dart' as exit_codes; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; +import 'json_reporter_utils.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('runs successful tests with a stdout reporter and file reporter', () { + return _expectReports(''' + test('success 1', () {}); + test('success 2', () {}); + test('success 3', () {}); + ''', ''' + +0: loading test.dart + +0: success 1 + +1: success 2 + +2: success 3 + +3: All tests passed!''', [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 3), + testStartJson(3, 'success 1', line: 6, column: 7), + testDoneJson(3), + testStartJson(4, 'success 2', line: 7, column: 7), + testDoneJson(4), + testStartJson(5, 'success 3', line: 8, column: 7), + testDoneJson(5), + ] + ], doneJson()); + }); + + test('runs failing tests with a stdout reporter and file reporter', () { + return _expectReports(''' + test('failure 1', () => throw new TestFailure('oh no')); + test('failure 2', () => throw new TestFailure('oh no')); + test('failure 3', () => throw new TestFailure('oh no')); + ''', ''' + +0: loading test.dart + +0: failure 1 + +0 -1: failure 1 [E] + oh no + test.dart 6:31 main.<fn> + + +0 -1: failure 2 + +0 -2: failure 2 [E] + oh no + test.dart 7:31 main.<fn> + + +0 -2: failure 3 + +0 -3: failure 3 [E] + oh no + test.dart 8:31 main.<fn> + + +0 -3: Some tests failed.''', [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 3), + testStartJson(3, 'failure 1', line: 6, column: 7), + errorJson(3, 'oh no', isFailure: true), + testDoneJson(3, result: 'failure'), + testStartJson(4, 'failure 2', line: 7, column: 7), + errorJson(4, 'oh no', isFailure: true), + testDoneJson(4, result: 'failure'), + testStartJson(5, 'failure 3', line: 8, column: 7), + errorJson(5, 'oh no', isFailure: true), + testDoneJson(5, result: 'failure'), + ] + ], doneJson(success: false)); + }); + + group('reports an error if --file-reporter', () { + test('is not in the form <reporter>:<filepath>', () async { + var test = await runTest(['--file-reporter=json']); + expect(test.stderr, + emits(contains('option must be in the form <reporter>:<filepath>'))); + await test.shouldExit(exit_codes.usage); + }); + + test('targets a non-existent reporter', () async { + var test = await runTest(['--file-reporter=nope:output.txt']); + expect( + test.stderr, emits(contains('"nope" is not a supported reporter'))); + await test.shouldExit(exit_codes.usage); + }); + }); +} + +Future<void> _expectReports( + String tests, + String stdoutExpected, + List<List<Object /*Map|Matcher*/ >> jsonFileExpected, + Map<Object, Object> jsonFileDone, + {List<String> args = const []}) async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { +$tests + } + ''').create(); + + var test = await runTest(['test.dart', '--chain-stack-traces', ...args], + // Write to a file within a dir that doesn't yet exist to verify that the + // file is created recursively. + fileReporter: 'json:reports/tests.json'); + await test.shouldExit(); + + // ---- stdout reporter verification ---- + var stdoutLines = await test.stdoutStream().toList(); + + // Remove excess trailing whitespace and trim off timestamps. + var actual = stdoutLines.map((line) { + if (line.startsWith(' ') || line.isEmpty) return line.trimRight(); + return line.trim().replaceFirst(RegExp('^[0-9]{2}:[0-9]{2} '), ''); + }).join('\n'); + + // Un-indent the expected string. + var indentation = stdoutExpected.indexOf(RegExp('[^ ]')); + stdoutExpected = stdoutExpected.split('\n').map((line) { + if (line.isEmpty) return line; + return line.substring(indentation); + }).join('\n'); + + expect(actual, equals(stdoutExpected)); + + // ---- file reporter verification ---- + var fileOutputLines = + File(p.join(d.sandbox, 'reports', 'tests.json')).readAsLinesSync(); + await expectJsonReport( + fileOutputLines, test.pid, jsonFileExpected, jsonFileDone); +}
diff --git a/pkgs/test/test/runner/json_reporter_test.dart b/pkgs/test/test/runner/json_reporter_test.dart new file mode 100644 index 0000000..a771438 --- /dev/null +++ b/pkgs/test/test/runner/json_reporter_test.dart
@@ -0,0 +1,655 @@ +// Copyright (c) 2015, 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 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; +import 'json_reporter_utils.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('runs several successful tests and reports when each completes', () { + return _expectReport(''' + test('success 1', () {}); + test('success 2', () {}); + test('success 3', () {}); + ''', [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 3), + testStartJson(3, 'success 1', line: 6, column: 7), + testDoneJson(3), + testStartJson(4, 'success 2', line: 7, column: 7), + testDoneJson(4), + testStartJson(5, 'success 3', line: 8, column: 7), + testDoneJson(5), + ] + ], doneJson()); + }); + + test('runs several failing tests and reports when each fails', () { + return _expectReport(''' + test('failure 1', () => throw TestFailure('oh no')); + test('failure 2', () => throw TestFailure('oh no')); + test('failure 3', () => throw TestFailure('oh no')); + ''', [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 3), + testStartJson(3, 'failure 1', line: 6, column: 7), + errorJson(3, 'oh no', isFailure: true), + testDoneJson(3, result: 'failure'), + testStartJson(4, 'failure 2', line: 7, column: 7), + errorJson(4, 'oh no', isFailure: true), + testDoneJson(4, result: 'failure'), + testStartJson(5, 'failure 3', line: 8, column: 7), + errorJson(5, 'oh no', isFailure: true), + testDoneJson(5, result: 'failure'), + ] + ], doneJson(success: false)); + }); + + test('includes the full stack trace with --verbose-trace', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("failure", () => throw "oh no"); + } + ''').create(); + + var test = + await runTest(['--verbose-trace', 'test.dart'], reporter: 'json'); + expect(test.stdout, emitsThrough(contains('dart:async'))); + await test.shouldExit(1); + }); + + test('runs failing tests along with successful tests', () { + return _expectReport(''' + test('failure 1', () => throw TestFailure('oh no')); + test('success 1', () {}); + test('failure 2', () => throw TestFailure('oh no')); + test('success 2', () {}); + ''', [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 4), + testStartJson(3, 'failure 1', line: 6, column: 7), + errorJson(3, 'oh no', isFailure: true), + testDoneJson(3, result: 'failure'), + testStartJson(4, 'success 1', line: 7, column: 7), + testDoneJson(4), + testStartJson(5, 'failure 2', line: 8, column: 7), + errorJson(5, 'oh no', isFailure: true), + testDoneJson(5, result: 'failure'), + testStartJson(6, 'success 2', line: 9, column: 7), + testDoneJson(6), + ] + ], doneJson(success: false)); + }); + + test('gracefully handles multiple test failures in a row', () { + return _expectReport(''' + // This completer ensures that the test isolate isn't killed until all + // errors have been thrown. + var completer = Completer(); + test('failures', () { + Future.microtask(() => throw 'first error'); + Future.microtask(() => throw 'second error'); + Future.microtask(() => throw 'third error'); + Future.microtask(completer.complete); + }); + test('wait', () => completer.future); + ''', [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 2), + testStartJson(3, 'failures', line: 9, column: 7), + errorJson(3, 'first error'), + errorJson(3, 'second error'), + errorJson(3, 'third error'), + testDoneJson(3, result: 'error'), + testStartJson(4, 'wait', line: 15, column: 7), + testDoneJson(4), + ] + ], doneJson(success: false)); + }); + + test('gracefully handles a test failing after completion', () { + return _expectReport(''' + // These completers ensure that the first test won't fail until the second + // one is running, and that the test isolate isn't killed until all errors + // have been thrown. + var waitStarted = Completer(); + var testDone = Completer(); + test('failure', () { + waitStarted.future.then((_) { + Future.microtask(testDone.complete); + throw 'oh no'; + }); + }); + test('wait', () { + waitStarted.complete(); + return testDone.future; + }); + ''', [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 2), + testStartJson(3, 'failure', line: 11, column: 7), + testDoneJson(3), + testStartJson(4, 'wait', line: 17, column: 7), + errorJson(3, 'oh no'), + errorJson( + 3, + 'This test failed after it had already completed.\n' + 'Make sure to use a matching library which informs the ' + 'test runner\nof pending async work.'), + testDoneJson(4), + ] + ], doneJson(success: false)); + }); + + test('reports each test in its proper groups', () { + return _expectReport(''' + group('group 1', () { + group('.2', () { + group('.3', () { + test('success', () {}); + }); + }); + + test('success1', () {}); + test('success2', () {}); + }); + ''', [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 3), + groupJson(3, + name: 'group 1', parentID: 2, testCount: 3, line: 6, column: 7), + groupJson(4, name: 'group 1 .2', parentID: 3, line: 7, column: 9), + groupJson(5, name: 'group 1 .2 .3', parentID: 4, line: 8, column: 11), + testStartJson(6, 'group 1 .2 .3 success', + groupIDs: [2, 3, 4, 5], line: 9, column: 13), + testDoneJson(6), + testStartJson(7, 'group 1 success1', + groupIDs: [2, 3], line: 13, column: 9), + testDoneJson(7), + testStartJson(8, 'group 1 success2', + groupIDs: [2, 3], line: 14, column: 9), + testDoneJson(8), + ] + ], doneJson()); + }); + + group('print:', () { + test('handles multiple prints', () { + return _expectReport(''' + test('test', () { + print("one"); + print("two"); + print("three"); + print("four"); + }); + ''', [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2), + testStartJson(3, 'test', line: 6, column: 9), + printJson(3, 'one'), + printJson(3, 'two'), + printJson(3, 'three'), + printJson(3, 'four'), + testDoneJson(3), + ] + ], doneJson()); + }); + + test('handles a print after the test completes', () { + return _expectReport(''' + // This completer ensures that the test isolate isn't killed until all + // prints have happened. + var testDone = Completer(); + var waitStarted = Completer(); + test('test', () async { + waitStarted.future.then((_) { + Future(() => print("one")); + Future(() => print("two")); + Future(() => print("three")); + Future(() => print("four")); + Future(testDone.complete); + }); + }); + + test('wait', () { + waitStarted.complete(); + return testDone.future; + }); + ''', [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 2), + testStartJson(3, 'test', line: 10, column: 9), + testDoneJson(3), + testStartJson(4, 'wait', line: 20, column: 9), + printJson(3, 'one'), + printJson(3, 'two'), + printJson(3, 'three'), + printJson(3, 'four'), + testDoneJson(4), + ] + ], doneJson()); + }); + + test('interleaves prints and errors', () { + return _expectReport(''' + // This completer ensures that the test isolate isn't killed until all + // prints have happened. + var completer = Completer(); + test('test', () { + scheduleMicrotask(() { + print("three"); + print("four"); + throw "second error"; + }); + + scheduleMicrotask(() { + print("five"); + print("six"); + completer.complete(); + }); + + print("one"); + print("two"); + throw "first error"; + }); + + test('wait', () => completer.future); + ''', [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 2), + testStartJson(3, 'test', line: 9, column: 9), + printJson(3, 'one'), + printJson(3, 'two'), + errorJson(3, 'first error'), + printJson(3, 'three'), + printJson(3, 'four'), + errorJson(3, 'second error'), + printJson(3, 'five'), + printJson(3, 'six'), + testDoneJson(3, result: 'error'), + testStartJson(4, 'wait', line: 27, column: 9), + testDoneJson(4), + ] + ], doneJson(success: false)); + }); + }); + + group('skip:', () { + test('reports skipped tests', () { + return _expectReport(''' + test('skip 1', () {}, skip: true); + test('skip 2', () {}, skip: true); + test('skip 3', () {}, skip: true); + ''', [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 3), + testStartJson(3, 'skip 1', skip: true, line: 6, column: 9), + testDoneJson(3, skipped: true), + testStartJson(4, 'skip 2', skip: true, line: 7, column: 9), + testDoneJson(4, skipped: true), + testStartJson(5, 'skip 3', skip: true, line: 8, column: 9), + testDoneJson(5, skipped: true), + ] + ], doneJson()); + }); + + test('reports skipped groups', () { + return _expectReport(''' + group('skip', () { + test('success 1', () {}); + test('success 2', () {}); + test('success 3', () {}); + }, skip: true); + ''', [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 3), + groupJson(3, + name: 'skip', + parentID: 2, + skip: true, + testCount: 3, + line: 6, + column: 9), + testStartJson(4, 'skip success 1', + groupIDs: [2, 3], skip: true, line: 7, column: 11), + testDoneJson(4, skipped: true), + testStartJson(5, 'skip success 2', + groupIDs: [2, 3], skip: true, line: 8, column: 11), + testDoneJson(5, skipped: true), + testStartJson(6, 'skip success 3', + groupIDs: [2, 3], skip: true, line: 9, column: 11), + testDoneJson(6, skipped: true), + ] + ], doneJson()); + }); + + test('reports the skip reason if available', () { + return _expectReport(''' + test('skip 1', () {}, skip: 'some reason'); + test('skip 2', () {}, skip: 'or another'); + ''', [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 2), + testStartJson(3, 'skip 1', skip: 'some reason', line: 6, column: 9), + printJson(3, 'Skip: some reason', type: 'skip'), + testDoneJson(3, skipped: true), + testStartJson(4, 'skip 2', skip: 'or another', line: 7, column: 9), + printJson(4, 'Skip: or another', type: 'skip'), + testDoneJson(4, skipped: true), + ] + ], doneJson()); + }); + + test('runs skipped tests with --run-skipped', () { + return _expectReport( + ''' + test('skip 1', () {}, skip: 'some reason'); + test('skip 2', () {}, skip: 'or another'); + ''', + [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 2), + testStartJson(3, 'skip 1', line: 6, column: 9), + testDoneJson(3), + testStartJson(4, 'skip 2', line: 7, column: 9), + testDoneJson(4), + ] + ], + doneJson(), + args: ['--run-skipped']); + }); + }); + + group('reports line and column numbers for', () { + test('the first call to setUpAll()', () { + return _expectReport(''' + setUpAll(() {}); + setUpAll(() {}); + setUpAll(() {}); + test('success', () {}); + ''', [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 1), + testStartJson(3, '(setUpAll)', line: 6, column: 9), + testDoneJson(3, hidden: true), + testStartJson(4, 'success', line: 9, column: 9), + testDoneJson(4), + testStartJson(5, '(tearDownAll)'), + testDoneJson(5, hidden: true), + ] + ], doneJson()); + }); + + test('the first call to tearDownAll()', () { + return _expectReport(''' + tearDownAll(() {}); + tearDownAll(() {}); + tearDownAll(() {}); + test('success', () {}); + ''', [ + [ + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + suiteJson(0), + groupJson(2, testCount: 1), + testStartJson(3, 'success', line: 9, column: 9), + testDoneJson(3), + testStartJson(4, '(tearDownAll)', line: 6, column: 9), + testDoneJson(4, hidden: true), + ] + ], doneJson()); + }); + + test('a test compiled to JS', () { + return _expectReport( + ''' + test('success', () {}); + ''', + [ + [ + suiteJson(0, platform: 'chrome'), + testStartJson(1, 'loading test.dart', groupIDs: []), + printJson( + 1, + isA<String>().having((s) => s.split('\n'), 'lines', + contains(startsWith('Compiled')))), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 1), + testStartJson(3, 'success', line: 6, column: 9), + testDoneJson(3), + ] + ], + doneJson(), + args: ['-p', 'chrome']); + }, tags: ['chrome'], skip: 'https://github.com/dart-lang/test/issues/872'); + + test('the root suite from a relative path', () { + return _expectReport( + ''' + customTest('success 1', () {}); + test('success 2', () {}); + ''', + [ + [ + suiteJson(0), + testStartJson(1, 'loading test.dart', groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 2), + testStartJson(3, 'success 1', + line: 3, + column: 60, + url: p.toUri(p.join(d.sandbox, 'common.dart')).toString(), + rootColumn: 7, + rootLine: 7, + rootUrl: p.toUri(p.join(d.sandbox, 'test.dart')).toString()), + testDoneJson(3), + testStartJson(4, 'success 2', line: 8, column: 7), + testDoneJson(4), + ] + ], + doneJson(), + externalLibraries: { + 'common.dart': ''' +import 'package:test/test.dart'; + +void customTest(String name, dynamic Function() testFn) => test(name, testFn); +''', + }); + }); + + test('the root suite from an absolute path', () { + final path = p.prettyUri(p.join(d.sandbox, 'test.dart')); + return _expectReport( + ''' + customTest('success 1', () {}); + test('success 2', () {}); + ''', + useRelativePath: false, + [ + [ + suiteJson(0, path: equalsIgnoringCase(path)), + testStartJson( + 1, allOf(startsWith('loading '), endsWith('test.dart')), + groupIDs: []), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 2), + testStartJson(3, 'success 1', + line: 3, + column: 60, + url: p.toUri(p.join(d.sandbox, 'common.dart')).toString(), + rootColumn: 7, + rootLine: 7, + rootUrl: p.toUri(p.join(d.sandbox, 'test.dart')).toString()), + testDoneJson(3), + testStartJson(4, 'success 2', line: 8, column: 7), + testDoneJson(4), + ] + ], + doneJson(), + externalLibraries: { + 'common.dart': ''' +import 'package:test/test.dart'; + +void customTest(String name, dynamic Function() testFn) => test(name, testFn); +''', + }); + }); + }); + + test( + "doesn't report line and column information for a test compiled to JS " + 'with --js-trace', () { + return _expectReport( + ''' + test('success', () {}); + ''', + [ + [ + suiteJson(0, platform: 'chrome'), + testStartJson(1, 'loading test.dart', groupIDs: []), + printJson( + 1, + isA<String>().having((s) => s.split('\n'), 'lines', + contains(startsWith('Compiled')))), + testDoneJson(1, hidden: true), + ], + [ + groupJson(2, testCount: 1), + testStartJson(3, 'success'), + testDoneJson(3), + ], + ], + doneJson(), + args: ['-p', 'chrome', '--js-trace']); + }, tags: ['chrome']); +} + +/// Asserts that the tests defined by [tests] produce the JSON events in +/// [expected]. +/// +/// If [externalLibraries] are provided it should be a map of relative file +/// paths to contents. All libraries will be added as imports to the test, and +/// files will be created for them. +Future<void> _expectReport(String tests, + List<List<Object /*Map|Matcher*/ >> expected, Map<Object, Object> done, + {List<String> args = const [], + bool useRelativePath = true, + Map<String, String> externalLibraries = const {}}) async { + var testContent = StringBuffer(''' +import 'dart:async'; + +import 'package:test/test.dart'; + +'''); + for (var entry in externalLibraries.entries) { + testContent.writeln("import '${entry.key}';"); + await d.file(entry.key, entry.value).create(); + } + testContent + ..writeln('void main() {') + ..writeln(tests) + ..writeln('}'); + + await d.file('test.dart', testContent.toString()).create(); + var testPath = useRelativePath ? 'test.dart' : p.join(d.sandbox, 'test.dart'); + + var test = await runTest([testPath, '--chain-stack-traces', ...args], + reporter: 'json'); + await test.shouldExit(); + + var stdoutLines = await test.stdoutStream().toList(); + return expectJsonReport(stdoutLines, test.pid, expected, done); +}
diff --git a/pkgs/test/test/runner/json_reporter_utils.dart b/pkgs/test/test/runner/json_reporter_utils.dart new file mode 100644 index 0000000..bbdbb78 --- /dev/null +++ b/pkgs/test/test/runner/json_reporter_utils.dart
@@ -0,0 +1,209 @@ +// Copyright (c) 2020, 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:test/test.dart'; +import 'package:test_core/src/runner/version.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +/// Asserts that the outputs from running tests with a JSON reporter match the +/// given expectations. +/// +/// Verifies that [outputLines] matches each set of matchers in [expected], +/// includes the [testPid] from the test process, and ends with [done]. +Future<void> expectJsonReport( + List<String> outputLines, + int testPid, + List<List<Object /*Map|Matcher*/ >> expected, + Map<Object, Object> done) async { + // Ensure the output is of the same length, including start, done and all + // suites messages. + expect(outputLines, + hasLength(expected.fold<int>(3, (int a, m) => a + m.length))); + + final decoded = [ + for (final line in outputLines) + (jsonDecode(line) as Map) + ..remove('time') + ..remove('stackTrace') + ]; + + // Should contain all suites message. + expect(decoded, containsAll([_allSuitesJson()])); + + // A single start event is emitted first. + final start = { + 'type': 'start', + 'protocolVersion': '0.1.1', + 'runnerVersion': testVersion, + 'pid': testPid, + }; + expect(decoded.first, equals(start)); + + // A single done event is emitted last. + expect(decoded.last, equals(done)); + + for (var value in expected) { + expect(decoded, containsAllInOrder(value)); + } +} + +/// Returns the event emitted by the JSON reporter providing information about +/// all suites. +/// +/// The [count] defaults to 1. +Map<String, Object> _allSuitesJson({int count = 1}) => + {'type': 'allSuites', 'count': count}; + +/// Returns the event emitted by the JSON reporter indicating that a suite has +/// begun running. +/// +/// The [platform] defaults to `'vm'`. +/// The [path] defaults to `equals('test.dart')`. +Map<String, Object> suiteJson(int id, + {String platform = 'vm', Matcher? path}) => + { + 'type': 'suite', + 'suite': { + 'id': id, + 'platform': platform, + 'path': path ?? 'test.dart', + } + }; + +/// Returns the event emitted by the JSON reporter indicating that a group has +/// begun running. +/// +/// If [skip] is `true`, the group is expected to be marked as skipped without a +/// reason. If it's a [String], the group is expected to be marked as skipped +/// with that reason. +/// +/// The [testCount] parameter indicates the number of tests in the group. It +/// defaults to 1. +Map<String, Object> groupJson(int id, + {String? name, + int? suiteID, + int? parentID, + Object? skip, + int? testCount, + int? line, + int? column}) { + if ((line == null) != (column == null)) { + throw ArgumentError( + 'line and column must either both be null or both be passed'); + } + + return { + 'type': 'group', + 'group': { + 'id': id, + 'name': name ?? '', + 'suiteID': suiteID ?? 0, + 'parentID': parentID, + 'metadata': _metadataJson(skip: skip), + 'testCount': testCount ?? 1, + 'line': line, + 'column': column, + 'url': line == null + ? null + : p.toUri(p.join(d.sandbox, 'test.dart')).toString() + } + }; +} + +/// Returns the event emitted by the JSON reporter indicating that a test has +/// begun running. +/// +/// If [parentIDs] is passed, it's the IDs of groups containing this test. If +/// [skip] is `true`, the test is expected to be marked as skipped without a +/// reason. If it's a [String], the test is expected to be marked as skipped +/// with that reason. +Map<String, Object> testStartJson(int id, Object /*String|Matcher*/ name, + {int? suiteID, + Iterable<int>? groupIDs, + int? line, + int? column, + String? url, + Object? skip, + int? rootLine, + int? rootColumn, + String? rootUrl}) { + if ((line == null) != (column == null)) { + throw ArgumentError( + 'line and column must either both be null or both be passed'); + } + + url ??= + line == null ? null : p.toUri(p.join(d.sandbox, 'test.dart')).toString(); + return { + 'type': 'testStart', + 'test': { + 'id': id, + 'name': name, + 'suiteID': suiteID ?? 0, + 'groupIDs': groupIDs ?? [2], + 'metadata': _metadataJson(skip: skip), + 'line': line, + 'column': column, + 'url': url, + if (rootLine != null) 'root_line': rootLine, + if (rootColumn != null) 'root_column': rootColumn, + if (rootUrl != null) 'root_url': rootUrl, + } + }; +} + +/// Returns the event emitted by the JSON reporter indicating that a test +/// printed [message]. +Matcher printJson(int id, dynamic /*String|Matcher*/ message, + {String type = 'print'}) => + allOf( + hasLength(4), + containsPair('type', 'print'), + containsPair('testID', id), + containsPair('message', message), + containsPair('messageType', type), + ); + +/// Returns the event emitted by the JSON reporter indicating that a test +/// emitted [error]. +/// +/// The [isFailure] parameter indicates whether the error was a [TestFailure] or +/// not. +Map<String, Object> errorJson(int id, String error, {bool isFailure = false}) => + {'type': 'error', 'testID': id, 'error': error, 'isFailure': isFailure}; + +/// Returns the event emitted by the JSON reporter indicating that a test +/// finished. +/// +/// The [result] parameter indicates the result of the test. It defaults to +/// `"success"`. +/// +/// The [hidden] parameter indicates whether the test should not be displayed +/// after finishing. The [skipped] parameter indicates whether the test was +/// skipped. +Map<String, Object> testDoneJson(int id, + {String result = 'success', + bool hidden = false, + bool skipped = false}) => + { + 'type': 'testDone', + 'testID': id, + 'result': result, + 'hidden': hidden, + 'skipped': skipped + }; + +/// Returns the event emitted by the JSON reporter indicating that the entire +/// run finished. +Map<String, Object> doneJson({bool success = true}) => + {'type': 'done', 'success': success}; + +/// Returns the serialized metadata corresponding to [skip]. +Map<String, Object?> _metadataJson({Object? skip}) => { + 'skip': skip == true || skip is String, + 'skipReason': skip is String ? skip : null + };
diff --git a/pkgs/test/test/runner/line_and_col_test.dart b/pkgs/test/test/runner/line_and_col_test.dart new file mode 100644 index 0000000..c513cef --- /dev/null +++ b/pkgs/test/test/runner/line_and_col_test.dart
@@ -0,0 +1,386 @@ +// Copyright (c) 2021, 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_core/src/util/exit_codes.dart' as exit_codes; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + group('with test.dart?line=<line> query', () { + test('selects test with the matching line', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("a", () {}); + test("b", () => throw TestFailure("oh no")); + test("c", () {}); + } + ''').create(); + + var test = await runTest(['test.dart?line=6']); + + expect( + test.stdout, + emitsThrough(contains('+1: All tests passed!')), + ); + + await test.shouldExit(0); + }); + + test('selects multiple tests on the same line', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("a", () {}); test("b", () {}); + test("c", () => throw TestFailure("oh no")); + } + ''').create(); + + var test = await runTest(['test.dart?line=4']); + + expect( + test.stdout, + emitsThrough(contains('+2: All tests passed!')), + ); + + await test.shouldExit(0); + }); + + test('selects groups with a matching line', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + group("a", () { + test("b", () {}); + }); + group("b", () { + test("b", () => throw TestFailure("oh no")); + }); + } + ''').create(); + + var test = await runTest(['test.dart?line=4']); + + expect( + test.stdout, + emitsThrough(contains('+1: All tests passed!')), + ); + + await test.shouldExit(0); + }); + + test('No matching tests', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("a", () {}); + } + ''').create(); + + var test = await runTest(['test.dart?line=1']); + + expect( + test.stderr, + emitsThrough(contains('No tests were found.')), + ); + + await test.shouldExit(exit_codes.noTestsRan); + }); + + test('allows the line anywhere in the stack trace', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void runTest(String name) { + test(name, () {}); + } + + void main() { + runTest("a"); + test("b", () {}); + } + ''').create(); + + var test = await runTest(['test.dart?line=8']); + + expect( + test.stdout, + emitsThrough(contains('+1: All tests passed!')), + ); + + await test.shouldExit(0); + }); + }); + + group('with test.dart?col=<col> query', () { + test('selects single test with the matching column', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("a", () {}); + test("b", () => throw TestFailure("oh no")); + } + ''').create(); + + var test = await runTest(['test.dart?col=11']); + + expect( + test.stdout, + emitsThrough(contains('+1: All tests passed!')), + ); + + await test.shouldExit(0); + }); + + test('selects multiple tests starting on the same column', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("a", () {}); + test("b", () {}); + test("c", () => throw TestFailure("oh no")); + } + ''').create(); + + var test = await runTest(['test.dart?col=11']); + + expect( + test.stdout, + emitsThrough(contains('+2: All tests passed!')), + ); + + await test.shouldExit(0); + }); + + test('selects groups with a matching column', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + group("a", () { + test("b", () {}); + }); + group("b", () { + test("b", () => throw TestFailure("oh no")); + }); + } + ''').create(); + + var test = await runTest(['test.dart?col=11']); + + expect( + test.stdout, + emitsThrough(contains('+1: All tests passed!')), + ); + + await test.shouldExit(0); + }); + + test('No matching tests', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("a", () {}); + } + ''').create(); + + var test = await runTest(['test.dart?col=1']); + + expect( + test.stderr, + emitsThrough(contains('No tests were found.')), + ); + + await test.shouldExit(exit_codes.noTestsRan); + }); + + test('allows the col anywhere in the stack trace', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void runTest(String name) { + test(name, () {}); + } + + void main() { + runTest("a"); + test("b", () => throw TestFailure("oh no")); + } + ''').create(); + + var test = await runTest(['test.dart?col=13']); + + expect( + test.stdout, + emitsThrough(contains('+1: All tests passed!')), + ); + + await test.shouldExit(0); + }); + }); + + group('with test.dart?line=<line>&col=<col> query', () { + test('selects test with the matching line and col in the same frame', + () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + void runTests() { + test("a", () {});test("b", () => throw TestFailure("oh no")); + } + runTests(); + test("c", () => throw TestFailure("oh no")); + } + ''').create(); + + var test = await runTest(['test.dart?line=5&col=11']); + + expect( + test.stdout, + emitsThrough(contains('+1: All tests passed!')), + ); + + await test.shouldExit(0); + }); + + test('selects group with the matching line and col', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + group("a", () { + test("b", () {}); + test("c", () {}); + }); + group("d", () { + test("e", () => throw TestFailure("oh no")); + }); + } + ''').create(); + + var test = await runTest(['test.dart?line=4&col=11']); + + expect( + test.stdout, + emitsThrough(contains('+2: All tests passed!')), + ); + + await test.shouldExit(0); + }); + + test('no matching tests - col doesnt match', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("a", () {}); + } + ''').create(); + + var test = await runTest(['test.dart?line=4&col=1']); + + expect( + test.stderr, + emitsThrough(contains('No tests were found.')), + ); + + await test.shouldExit(exit_codes.noTestsRan); + }); + + test('no matching tests - line doesnt match', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("a", () {}); + } + ''').create(); + + var test = await runTest(['test.dart?line=1&col=11']); + + expect( + test.stderr, + emitsThrough(contains('No tests were found.')), + ); + + await test.shouldExit(exit_codes.noTestsRan); + }); + + test('supports browser tests', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("a", () {}); + test("b", () => throw TestFailure("oh no")); + } + ''').create(); + + var test = await runTest(['test.dart?line=4&col=11', '-p', 'chrome']); + + expect( + test.stdout, + emitsThrough(contains('+1: All tests passed!')), + ); + + await test.shouldExit(0); + }); + + test('supports node tests', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("a", () {}); + test("b", () => throw TestFailure("oh no")); + } + ''').create(); + + var test = await runTest(['test.dart?line=4&col=11', '-p', 'node']); + + expect( + test.stdout, + emitsThrough(contains('+1: All tests passed!')), + ); + + await test.shouldExit(0); + }, retry: 3); + }); + + test('bundles runs by suite, deduplicates tests that match multiple times', + () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test('a', () {}); + test('b', () => throw TestFailure('oh no')); + } + ''').create(); + + var test = await runTest(['test.dart?line=4', 'test.dart?full-name=a']); + + expect( + test.stdout, + emitsThrough(contains('+1: All tests passed!')), + ); + + await test.shouldExit(0); + }); +}
diff --git a/pkgs/test/test/runner/load_suite_test.dart b/pkgs/test/test/runner/load_suite_test.dart new file mode 100644 index 0000000..ab5fc79 --- /dev/null +++ b/pkgs/test/test/runner/load_suite_test.dart
@@ -0,0 +1,178 @@ +// Copyright (c) 2015, 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 'package:test/test.dart'; +import 'package:test_api/src/backend/group.dart'; +import 'package:test_api/src/backend/runtime.dart'; +import 'package:test_api/src/backend/state.dart'; +import 'package:test_api/src/backend/test.dart'; +import 'package:test_core/src/runner/load_exception.dart'; +import 'package:test_core/src/runner/load_suite.dart'; +import 'package:test_core/src/runner/runner_suite.dart'; +import 'package:test_core/src/runner/suite.dart'; + +import '../utils.dart'; + +void main() { + late RunnerSuite innerSuite; + setUp(() { + innerSuite = runnerSuite(Group.root([])); + }); + + test('running a load test causes LoadSuite.suite to emit a suite', () async { + var suite = LoadSuite('name', SuiteConfiguration.empty, suitePlatform, + () => Future.value(innerSuite)); + expect(suite.group.entries, hasLength(1)); + + expect(suite.suite, completion(equals(innerSuite))); + var liveTest = (suite.group.entries.single as Test).load(suite); + await liveTest.run(); + expectTestPassed(liveTest); + }); + + test("running a load suite's body may be synchronous", () async { + var suite = LoadSuite( + 'name', SuiteConfiguration.empty, suitePlatform, () => innerSuite); + expect(suite.group.entries, hasLength(1)); + + expect(suite.suite, completion(equals(innerSuite))); + var liveTest = (suite.group.entries.single as Test).load(suite); + await liveTest.run(); + expectTestPassed(liveTest); + }); + + test("a load test doesn't complete until the body returns", () async { + var completer = Completer<RunnerSuite>(); + var suite = LoadSuite('name', SuiteConfiguration.empty, suitePlatform, + () => completer.future); + expect(suite.group.entries, hasLength(1)); + + var liveTest = (suite.group.entries.single as Test).load(suite); + expect(liveTest.run(), completes); + await Future<void>.delayed(Duration.zero); + expect(liveTest.state.status, equals(Status.running)); + + completer.complete(innerSuite); + await Future<void>.delayed(Duration.zero); + expectTestPassed(liveTest); + }); + + test('a load test forwards errors and completes LoadSuite.suite to null', + () async { + var suite = LoadSuite('name', SuiteConfiguration.empty, suitePlatform, () { + return fail('error'); + }); + expect(suite.group.entries, hasLength(1)); + + expect(suite.suite, completion(isNull)); + + var liveTest = (suite.group.entries.single as Test).load(suite); + await liveTest.run(); + expectTestFailed(liveTest, 'error'); + }); + + test("a load test completes early if it's closed", () async { + var suite = LoadSuite('name', SuiteConfiguration.empty, suitePlatform, + () => Completer<RunnerSuite>().future); + expect(suite.group.entries, hasLength(1)); + + var liveTest = (suite.group.entries.single as Test).load(suite); + expect(liveTest.run(), completes); + await Future<void>.delayed(Duration.zero); + expect(liveTest.state.status, equals(Status.running)); + + expect(liveTest.close(), completes); + }); + + test('forLoadException() creates a suite that completes to a LoadException', + () async { + var exception = LoadException('path', 'error'); + var suite = LoadSuite.forLoadException(exception, SuiteConfiguration.empty); + expect(suite.group.entries, hasLength(1)); + + expect(suite.suite, completion(isNull)); + + var liveTest = (suite.group.entries.single as Test).load(suite); + await liveTest.run(); + expect(liveTest.state.status, equals(Status.complete)); + expect(liveTest.state.result, equals(Result.error)); + expect(liveTest.errors, hasLength(1)); + expect(liveTest.errors.first.error, equals(exception)); + }); + + test('forSuite() creates a load suite that completes to a test suite', + () async { + var suite = LoadSuite.forSuite(innerSuite); + expect(suite.group.entries, hasLength(1)); + + expect(suite.suite, completion(equals(innerSuite))); + var liveTest = (suite.group.entries.single as Test).load(suite); + await liveTest.run(); + expectTestPassed(liveTest); + }); + + group('changeSuite()', () { + test('returns a new load suite with the same properties', () { + var suite = LoadSuite( + 'name', SuiteConfiguration.empty, suitePlatform, () => innerSuite); + expect(suite.group.entries, hasLength(1)); + + var newSuite = suite.changeSuite((suite) => suite); + expect(newSuite.platform.runtime, equals(Runtime.vm)); + expect(newSuite.group.entries.single.name, + equals(suite.group.entries.single.name)); + }); + + test('changes the inner suite', () async { + var suite = LoadSuite( + 'name', SuiteConfiguration.empty, suitePlatform, () => innerSuite); + expect(suite.group.entries, hasLength(1)); + + var newInnerSuite = runnerSuite(Group.root([])); + var newSuite = suite.changeSuite((suite) => newInnerSuite); + expect(newSuite.suite, completion(equals(newInnerSuite))); + + var liveTest = (suite.group.entries.single as Test).load(suite); + await liveTest.run(); + expectTestPassed(liveTest); + }); + + test("doesn't run change() if the suite is null", () async { + var suite = LoadSuite( + 'name', SuiteConfiguration.empty, suitePlatform, () => null); + expect(suite.group.entries, hasLength(1)); + + var newSuite = suite.changeSuite(expectAsync1((_) { + return null; + }, count: 0)); + expect(newSuite.suite, completion(isNull)); + + var liveTest = (suite.group.entries.single as Test).load(suite); + await liveTest.run(); + expectTestPassed(liveTest); + }); + }); + + group('getSuite()', () { + test('runs the test and returns the suite', () { + var suite = LoadSuite.forSuite(innerSuite); + expect(suite.group.entries, hasLength(1)); + + expect(suite.getSuite(), completion(equals(innerSuite))); + }); + + test('forwards errors to the future', () { + var suite = LoadSuite( + 'name', SuiteConfiguration.empty, suitePlatform, () => throw 'error'); + expect(suite.group.entries, hasLength(1)); + + expect(suite.getSuite(), throwsA('error')); + }); + }); +}
diff --git a/pkgs/test/test/runner/loader_test.dart b/pkgs/test/test/runner/loader_test.dart new file mode 100644 index 0000000..8d1d4ad --- /dev/null +++ b/pkgs/test/test/runner/loader_test.dart
@@ -0,0 +1,293 @@ +// Copyright (c) 2015, 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 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_api/src/backend/compiler.dart'; +import 'package:test_api/src/backend/runtime.dart'; +import 'package:test_api/src/backend/state.dart'; +import 'package:test_api/src/backend/test.dart'; +import 'package:test_core/src/runner/compiler_selection.dart'; +import 'package:test_core/src/runner/load_suite.dart'; +import 'package:test_core/src/runner/loader.dart'; +import 'package:test_core/src/runner/runner_suite.dart'; +import 'package:test_core/src/runner/runner_test.dart'; +import 'package:test_core/src/runner/suite.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../utils.dart'; + +late Loader _loader; + +final _tests = ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}); + test("failure", () => throw TestFailure('oh no')); + test("error", () => throw 'oh no'); +} +'''; + +void main() { + setUp(() async { + _loader = Loader(); + }); + + tearDown(() => _loader.close()); + + group('.loadFile()', () { + late RunnerSuite suite; + group('with empty configuration', () { + setUp(() async { + await d.file('a_test.dart', _tests).create(); + var suites = await _loader + .loadFile( + p.join(d.sandbox, 'a_test.dart'), SuiteConfiguration.empty) + .toList(); + expect(suites, hasLength(1)); + var loadSuite = suites.first; + suite = (await loadSuite.getSuite())!; + }); + + test('returns a suite with the file path and platform', () { + expect(suite.path, equals(p.join(d.sandbox, 'a_test.dart'))); + expect(suite.platform.runtime, equals(Runtime.vm)); + expect(suite.platform.compiler, equals(Runtime.vm.defaultCompiler)); + }); + + test('returns entries with the correct names and platforms', () { + expect(suite.group.entries, hasLength(3)); + expect(suite.group.entries[0].name, equals('success')); + expect(suite.group.entries[1].name, equals('failure')); + expect(suite.group.entries[2].name, equals('error')); + }); + + test('can load and run a successful test', () { + var liveTest = (suite.group.entries[0] as RunnerTest).load(suite); + + expectStates(liveTest, [ + const State(Status.running, Result.success), + const State(Status.complete, Result.success) + ]); + expectErrors(liveTest, []); + + return liveTest.run().whenComplete(() => liveTest.close()); + }); + + test('can load and run a failing test', () { + var liveTest = (suite.group.entries[1] as RunnerTest).load(suite); + expectSingleFailure(liveTest); + return liveTest.run().whenComplete(() => liveTest.close()); + }); + }); + + group('with compiler selection', () { + Future<List<LoadSuite>> loadSuitesWithConfig( + SuiteConfiguration suiteConfiguration) async { + await d.file('a_test.dart', _tests).create(); + return _loader + .loadFile(p.join(d.sandbox, 'a_test.dart'), suiteConfiguration) + .toList(); + } + + test('with a single compiler selection, uses the selected compiler', + () async { + var suites = await loadSuitesWithConfig(suiteConfiguration( + compilerSelections: [CompilerSelection.parse('source')])); + expect(suites, hasLength(1)); + var loadSuite = suites.first; + suite = (await loadSuite.getSuite())!; + expect(suite.path, equals(p.join(d.sandbox, 'a_test.dart'))); + expect(suite.platform.runtime, equals(Runtime.vm)); + expect(suite.platform.compiler, equals(Compiler.source)); + }); + + test('with multiple compiler selections, returns a suite for each', + () async { + var suites = await loadSuitesWithConfig(suiteConfiguration( + compilerSelections: [ + CompilerSelection.parse('source'), + CompilerSelection.parse('kernel') + ])); + + expect(suites, hasLength(2)); + var runnerSuites = + await Future.wait([for (var suite in suites) suite.getSuite()]); + expect( + runnerSuites, + unorderedEquals([ + isA<RunnerSuite>() + .having( + (s) => s.platform.runtime, 'The vm runtime', Runtime.vm) + .having((s) => s.platform.compiler, 'The source compiler', + Compiler.source), + isA<RunnerSuite>() + .having( + (s) => s.platform.runtime, 'The vm runtime', Runtime.vm) + .having((s) => s.platform.compiler, 'The kernel compiler', + Compiler.kernel), + ])); + }); + + test('with unsupported compiler selections, uses the default compiler', + () async { + var suites = + await loadSuitesWithConfig(suiteConfiguration(compilerSelections: [ + CompilerSelection.parse('dart2js'), + ])); + expect(suites, hasLength(1)); + var loadSuite = suites.first; + suite = (await loadSuite.getSuite())!; + expect(suite.path, equals(p.join(d.sandbox, 'a_test.dart'))); + expect(suite.platform.runtime, equals(Runtime.vm)); + expect(suite.platform.compiler, equals(Runtime.vm.defaultCompiler)); + }); + + test('compiler selections support matching boolean selectors', () async { + var suites = + await loadSuitesWithConfig(suiteConfiguration(compilerSelections: [ + CompilerSelection.parse('vm:source'), + ])); + expect(suites, hasLength(1)); + var loadSuite = suites.first; + suite = (await loadSuite.getSuite())!; + expect(suite.path, equals(p.join(d.sandbox, 'a_test.dart'))); + expect(suite.platform.runtime, equals(Runtime.vm)); + expect(suite.platform.compiler, equals(Compiler.source)); + }); + + test('compiler selections support unmatched boolean selectors', () async { + var suites = + await loadSuitesWithConfig(suiteConfiguration(compilerSelections: [ + CompilerSelection.parse('browser:source'), + ])); + expect(suites, hasLength(1)); + var loadSuite = suites.first; + suite = (await loadSuite.getSuite())!; + expect(suite.path, equals(p.join(d.sandbox, 'a_test.dart'))); + expect(suite.platform.runtime, equals(Runtime.vm)); + expect(suite.platform.compiler, + allOf(Runtime.vm.defaultCompiler, isNot(Compiler.source))); + }); + }); + }); + + group('.loadDir()', () { + test('ignores non-Dart files', () async { + await d.file('a_test.txt', _tests).create(); + expect(_loader.loadDir(d.sandbox, SuiteConfiguration.empty).toList(), + completion(isEmpty)); + }); + + test("ignores files that don't end in _test.dart", () async { + await d.file('test.dart', _tests).create(); + expect(_loader.loadDir(d.sandbox, SuiteConfiguration.empty).toList(), + completion(isEmpty)); + }); + + group('with suites loaded from a directory', () { + late List<RunnerSuite> suites; + setUp(() async { + await d.file('a_test.dart', _tests).create(); + await d.file('another_test.dart', _tests).create(); + await d.dir('dir', [d.file('sub_test.dart', _tests)]).create(); + + suites = await _loader + .loadDir(d.sandbox, SuiteConfiguration.empty) + .asyncMap((loadSuite) async => (await loadSuite.getSuite())!) + .toList(); + }); + + test('gives those suites the correct paths', () { + expect( + suites.map((suite) => suite.path), + unorderedEquals([ + p.join(d.sandbox, 'a_test.dart'), + p.join(d.sandbox, 'another_test.dart'), + p.join(d.sandbox, 'dir', 'sub_test.dart') + ])); + }); + + test('can run tests in those suites', () { + var suite = + suites.firstWhere((suite) => suite.path!.contains('a_test')); + var liveTest = (suite.group.entries[1] as RunnerTest).load(suite); + expectSingleFailure(liveTest); + return liveTest.run().whenComplete(() => liveTest.close()); + }); + }); + }); + + test('a print in a loaded file is piped through the LoadSuite', () async { + await d.file('a_test.dart', ''' + void main() { + print('print within test'); + } + ''').create(); + var suites = await _loader + .loadFile(p.join(d.sandbox, 'a_test.dart'), SuiteConfiguration.empty) + .toList(); + expect(suites, hasLength(1)); + var loadSuite = suites.first; + + var liveTest = (loadSuite.group.entries.single as Test).load(loadSuite); + expect(liveTest.onMessage.first.then((message) => message.text), + completion(equals('print within test'))); + await liveTest.run(); + expectTestPassed(liveTest); + }); + + group('LoadException', () { + test('suites can be retried', () async { + var numRetries = 5; + + await d.file('a_test.dart', ''' + import 'hello.dart'; + + void main() {} + ''').create(); + + var firstFailureCompleter = Completer<void>(); + + // After the first load failure we create the missing dependency. + unawaited(firstFailureCompleter.future.then((_) async { + await d.file('hello.dart', ''' + String get message => 'hello'; + ''').create(); + })); + + await runZoned(() async { + var suites = await _loader + .loadFile(p.join(d.sandbox, 'a_test.dart'), + suiteConfiguration(retry: numRetries)) + .toList(); + expect(suites, hasLength(1)); + var loadSuite = suites.first; + var suite = (await loadSuite.getSuite())!; + expect(suite.path, equals(p.join(d.sandbox, 'a_test.dart'))); + expect(suite.platform.runtime, equals(Runtime.vm)); + }, zoneSpecification: + ZoneSpecification(print: (_, parent, zone, message) { + if (message.contains('Retrying load of') && + !firstFailureCompleter.isCompleted) { + firstFailureCompleter.complete(null); + } + parent.print(zone, message); + })); + + expect(firstFailureCompleter.isCompleted, true); + }); + }); + + // TODO: Test load suites. Don't forget to test that prints in loaded files + // are piped through the suite. Also for browser tests! +}
diff --git a/pkgs/test/test/runner/name_test.dart b/pkgs/test/test/runner/name_test.dart new file mode 100644 index 0000000..bd4dc21 --- /dev/null +++ b/pkgs/test/test/runner/name_test.dart
@@ -0,0 +1,460 @@ +// Copyright (c) 2016, 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_core/src/util/exit_codes.dart' as exit_codes; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + group('with test.dart?name="name" query', () { + test('selects tests with matching names', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected 1", () {}); + test("nope", () => throw TestFailure("oh no")); + test("selected 2", () {}); + } + ''').create(); + + var test = await runTest(['test.dart?name=selected']); + + expect( + test.stdout, + emitsThrough(contains('+2: All tests passed!')), + ); + + await test.shouldExit(0); + }); + + test('supports RegExp syntax', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test 1", () {}); + test("test 2", () => throw TestFailure("oh no")); + test("test 3", () {}); + } + ''').create(); + + var test = await runTest(['test.dart?name=test [13]']); + + expect( + test.stdout, + emitsThrough(contains('+2: All tests passed!')), + ); + + await test.shouldExit(0); + }); + + test('applies only to the associated file', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected 1", () {}); + test("selected 2", () => throw TestFailure("oh no")); + } + ''').create(); + + await d.file('test2.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected 1", () => throw TestFailure("oh no")); + test("selected 2", () {}); + } + ''').create(); + + var test = await runTest( + ['test.dart?name=selected 1', 'test2.dart?name=selected 2'], + ); + + expect( + test.stdout, + emitsThrough(contains('+2: All tests passed!')), + ); + await test.shouldExit(0); + }); + + test('selects more narrowly when passed multiple times', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected 1", () {}); + test("nope", () => throw TestFailure("oh no")); + test("selected 2", () {}); + } + ''').create(); + + var test = await runTest(['test.dart?name=selected&name=1']); + + expect( + test.stdout, + emitsThrough(contains('+1: All tests passed!')), + ); + await test.shouldExit(0); + }); + + test('applies to directories', () async { + await d.dir('dir', [ + d.file('first_test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected 1", () {}); + test("selected 2", () => throw TestFailure("oh no")); + } + '''), + d.file('second_test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected 1", () {}); + test("selected 2", () => throw TestFailure("oh no")); + } + ''') + ]).create(); + + var test = await runTest(['dir?name=selected 1']); + + expect( + test.stdout, + emitsThrough(contains('+2: All tests passed!')), + ); + await test.shouldExit(0); + }); + + test('produces an error when no tests match', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + var test = await runTest(['test.dart?name=no']); + + expect( + test.stderr, + emitsThrough(contains('No tests were found.')), + ); + + await test.shouldExit(exit_codes.noTestsRan); + }); + + test("doesn't filter out load exceptions", () async { + var test = await runTest(['file?name=name']); + expect( + test.stdout, + containsInOrder([ + '-1: loading file [E]', + ' Failed to load "file": Does not exist.' + ]), + ); + + await test.shouldExit(1); + }); + }); + + group('with test.dart?full-name query,', () { + test('matches with the complete test name', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected", () {}); + test("nope", () => throw TestFailure("oh no")); + test("selected nope", () => throw TestFailure("oh no")); + } + ''').create(); + + var test = await runTest(['test.dart?full-name=selected']); + + expect( + test.stdout, + emitsThrough(contains('+1: All tests passed!')), + ); + await test.shouldExit(0); + }); + + test("doesn't support RegExp syntax", () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test 1", () => throw TestFailure("oh no")); + test("test 2", () => throw TestFailure("oh no")); + test("test [12]", () {}); + } + ''').create(); + + var test = await runTest(['test.dart?full-name=test [12]']); + + expect( + test.stdout, + emitsThrough(contains('+1: All tests passed!')), + ); + await test.shouldExit(0); + }); + + test('applies only to the associated file', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected 1", () {}); + test("selected 2", () => throw TestFailure("oh no")); + } + ''').create(); + + await d.file('test2.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected 1", () => throw TestFailure("oh no")); + test("selected 2", () {}); + } + ''').create(); + + var test = await runTest( + ['test.dart?full-name=selected 1', 'test2.dart?full-name=selected 2'], + ); + + expect( + test.stdout, + emitsThrough(contains('+2: All tests passed!')), + ); + await test.shouldExit(0); + }); + + test('produces an error when no tests match', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + var test = await runTest(['test.dart?full-name=no match']); + + expect( + test.stderr, + emitsThrough(contains('No tests were found.')), + ); + await test.shouldExit(exit_codes.noTestsRan); + }); + }); + + test('test?name="name" and --name narrow the selection', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected 1", () {}); + test("nope 1", () => throw TestFailure("oh no")); + test("selected 2", () => throw TestFailure("oh no")); + test("nope 2", () => throw TestFailure("oh no")); + } + ''').create(); + + var test = await runTest(['--name', '1', 'test.dart?name=selected']); + + expect( + test.stdout, + emitsThrough(contains('+1: All tests passed!')), + ); + await test.shouldExit(0); + }); + + test('test?name="name" and test?full-name="name" throws', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected 1", () {}); + test("nope 1", () => throw TestFailure("oh no")); + test("selected 2", () => throw TestFailure("oh no")); + test("nope 2", () => throw TestFailure("oh no")); + } + ''').create(); + + var test = await runTest(['test.dart?name=selected&full-name=selected 1']); + + await test.shouldExit(64); + }); + + group('with the --name flag,', () { + test('selects tests with matching names', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected 1", () {}); + test("nope", () => throw TestFailure("oh no")); + test("selected 2", () {}); + } + ''').create(); + + var test = await runTest(['--name', 'selected', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+2: All tests passed!'))); + await test.shouldExit(0); + }); + + test('supports RegExp syntax', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test 1", () {}); + test("test 2", () => throw TestFailure("oh no")); + test("test 3", () {}); + } + ''').create(); + + var test = await runTest(['--name', 'test [13]', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+2: All tests passed!'))); + await test.shouldExit(0); + }); + + test('selects more narrowly when passed multiple times', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected 1", () {}); + test("nope", () => throw TestFailure("oh no")); + test("selected 2", () {}); + } + ''').create(); + + var test = + await runTest(['--name', 'selected', '--name', '1', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('produces an error when no tests match', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + var test = await runTest(['--name', 'no match', 'test.dart']); + expect( + test.stderr, + emitsThrough( + contains('No tests match regular expression "no match".'))); + await test.shouldExit(exit_codes.noTestsRan); + }); + + test("doesn't filter out load exceptions", () async { + var test = await runTest(['--name', 'name', 'file']); + expect( + test.stdout, + containsInOrder([ + '-1: loading file [E]', + ' Failed to load "file": Does not exist.' + ])); + await test.shouldExit(1); + }); + }); + + group('with the --plain-name flag,', () { + test('selects tests with matching names', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected 1", () {}); + test("nope", () => throw TestFailure("oh no")); + test("selected 2", () {}); + } + ''').create(); + + var test = await runTest(['--plain-name', 'selected', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+2: All tests passed!'))); + await test.shouldExit(0); + }); + + test("doesn't support RegExp syntax", () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test 1", () => throw TestFailure("oh no")); + test("test 2", () => throw TestFailure("oh no")); + test("test [12]", () {}); + } + ''').create(); + + var test = await runTest(['--plain-name', 'test [12]', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('selects more narrowly when passed multiple times', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected 1", () {}); + test("nope", () => throw TestFailure("oh no")); + test("selected 2", () {}); + } + ''').create(); + + var test = await runTest( + ['--plain-name', 'selected', '--plain-name', '1', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('produces an error when no tests match', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () {}); + } + ''').create(); + + var test = await runTest(['--plain-name', 'no match', 'test.dart']); + expect(test.stderr, emitsThrough(contains('No tests match "no match".'))); + await test.shouldExit(exit_codes.noTestsRan); + }); + }); + + test('--name and --plain-name together narrow the selection', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("selected 1", () {}); + test("nope", () => throw TestFailure("oh no")); + test("selected 2", () {}); + } + ''').create(); + + var test = + await runTest(['--name', '.....', '--plain-name', 'e', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+2: All tests passed!'))); + await test.shouldExit(0); + }); +}
diff --git a/pkgs/test/test/runner/node/runner_test.dart b/pkgs/test/test/runner/node/runner_test.dart new file mode 100644 index 0000000..82d2321 --- /dev/null +++ b/pkgs/test/test/runner/node/runner_test.dart
@@ -0,0 +1,469 @@ +// 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') +@Tags(['node']) +library; + +import 'dart:convert'; +import 'dart:io'; + +import 'package:test/src/runner/executable_settings.dart'; +import 'package:test/test.dart'; +import 'package:test_core/src/util/io.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../../io.dart'; + +final _success = ''' + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } +'''; + +final _failure = ''' + import 'package:test/test.dart'; + + void main() { + test("failure", () => throw TestFailure("oh no")); + } +'''; + +({int major, String full})? _nodeVersion; + +({int major, String full}) _readNodeVersion() { + final process = Process.runSync( + ExecutableSettings( + linuxExecutable: 'node', + macOSExecutable: 'node', + windowsExecutable: 'node.exe', + ).executable, + ['--version'], + stdoutEncoding: utf8, + ); + if (process.exitCode != 0) { + throw const OSError('Could not run node --version'); + } + + final version = RegExp(r'v(\d+)\..*'); + final parsed = version.firstMatch(process.stdout as String)!; + return (major: int.parse(parsed.group(1)!), full: process.stdout); +} + +String? skipBelowMajorNodeVersion(int minimumMajorVersion) { + final (:major, :full) = _nodeVersion ??= _readNodeVersion(); + if (major < minimumMajorVersion) { + return 'This test requires Node $minimumMajorVersion.x or later, ' + 'but is running on $full'; + } + + return null; +} + +String? skipAboveMajorNodeVersion(int maximumMajorVersion) { + final (:major, :full) = _nodeVersion ??= _readNodeVersion(); + if (major > maximumMajorVersion) { + return 'This test requires Node $maximumMajorVersion.x or older, ' + 'but is running on $full'; + } + + return null; +} + +void main() { + setUpAll(precompileTestExecutable); + + group('fails gracefully if', () { + test('a test file fails to compile', () async { + await d.file('test.dart', 'invalid Dart file').create(); + var test = await runTest(['-p', 'node', 'test.dart']); + + expect( + test.stdout, + containsInOrder([ + 'Error: Compilation failed.', + '-1: loading test.dart [E]', + 'Failed to load "test.dart": dart2js failed.' + ])); + await test.shouldExit(1); + }); + + test('a test file throws', () async { + await d.file('test.dart', "void main() => throw 'oh no';").create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": oh no' + ])); + await test.shouldExit(1); + }); + + test("a test file doesn't have a main defined", () async { + await d.file('test.dart', 'void foo() {}').create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": No top-level main() function defined.' + ])); + await test.shouldExit(1); + }, skip: 'https://github.com/dart-lang/test/issues/894'); + + test('a test file has a non-function main', () async { + await d.file('test.dart', 'int main;').create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": Top-level main getter is not a function.' + ])); + await test.shouldExit(1); + }, skip: 'https://github.com/dart-lang/test/issues/894'); + + test('a test file has a main with arguments', () async { + await d.file('test.dart', 'void main(arg) {}').create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": Top-level main() function takes arguments.' + ])); + await test.shouldExit(1); + }); + }); + + group('runs successful tests', () { + test('on Node and the VM', () async { + await d.file('test.dart', _success).create(); + var test = await runTest(['-p', 'node', '-p', 'vm', 'test.dart']); + + expect(test.stdout, emitsThrough(contains('+2: All tests passed!'))); + await test.shouldExit(0); + }); + + // Regression test; this broke in 0.12.0-beta.9. + test('on a file in a subdirectory', () async { + await d.dir('dir', [d.file('test.dart', _success)]).create(); + + var test = await runTest(['-p', 'node', 'dir/test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('compiled with dart2wasm', () async { + await d.file('test.dart', _success).create(); + var test = + await runTest(['-p', 'node', '--compiler', 'dart2wasm', 'test.dart']); + + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }, skip: skipBelowMajorNodeVersion(22)); + }); + + test('defines a node environment constant', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () { + expect(const bool.fromEnvironment("node"), isTrue); + }); + } + ''').create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('runs failing tests that fail only on node', () async { + await d.file('test.dart', ''' + import 'package:path/path.dart' as p; + import 'package:test/test.dart'; + + void main() { + test("test", () { + if (const bool.fromEnvironment("node")) { + throw TestFailure("oh no"); + } + }); + } + ''').create(); + + var test = + await runTest(['-p', 'node', '-p', 'vm', '-c', 'dart2js', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1 -1: Some tests failed.'))); + await test.shouldExit(1); + }); + + test('runs failing tests that fail only on node (with dart2wasm)', () async { + await d.file('test.dart', ''' + import 'package:path/path.dart' as p; + import 'package:test/test.dart'; + + void main() { + test("test", () { + if (const bool.fromEnvironment("node")) { + throw TestFailure("oh no"); + } + }); + } + ''').create(); + + var test = await runTest([ + '-p', + 'node', + '-p', + 'vm', + '-c', + 'dart2js', + '-c', + 'dart2wasm', + 'test.dart' + ]); + expect(test.stdout, emitsThrough(contains('+1 -2: Some tests failed.'))); + await test.shouldExit(1); + }, skip: skipBelowMajorNodeVersion(22)); + + test( + 'gracefully handles wasm errors on old node versions', + () async { + // Old Node.JS versions can't read the WebAssembly modules emitted by + // dart2wasm. The node process exits before connecting to the server + // opened by the test runner, leading to timeouts. So, this is a + // regression test for https://github.com/dart-lang/test/pull/2259#issuecomment-2307868442 + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test", () { + // Should pass on newer node versions + }); + } + ''').create(); + + var test = await runTest(['-p', 'node', '-c', 'dart2wasm', 'test.dart']); + expect( + test.stdout, + emitsInOrder([ + emitsThrough( + contains('Node exited before connecting to the test channel.')), + emitsThrough(contains('-1: Some tests failed.')), + ]), + ); + await test.shouldExit(1); + }, + skip: skipAboveMajorNodeVersion(21), + ); + + test('forwards prints from the Node test', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("test", () { + print("Hello,"); + return Future(() => print("world!")); + }); + } + ''').create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect(test.stdout, emitsInOrder([emitsThrough('Hello,'), 'world!'])); + await test.shouldExit(0); + }); + + test('forwards raw JS prints from the Node test', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:js/js.dart'; + import 'package:test/test.dart'; + + @JS("console.log") + external void log(value); + + void main() { + test("test", () { + log("Hello,"); + return Future(() => log("world!")); + }); + } + ''').create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect(test.stdout, emitsInOrder([emitsThrough('Hello,'), 'world!'])); + await test.shouldExit(0); + }); + + test('dartifies stack traces for JS-compiled tests by default', () async { + await d.file('test.dart', _failure).create(); + + var test = await runTest(['-p', 'node', '--verbose-trace', 'test.dart']); + expect(test.stdout, + containsInOrder([' main.<fn>', 'package:test', 'dart:async/zone.dart']), + skip: 'https://github.com/dart-lang/sdk/issues/41949'); + await test.shouldExit(1); + }); + + test("doesn't dartify stack traces for JS-compiled tests with --js-trace", + () async { + await d.file('test.dart', _failure).create(); + + var test = await runTest( + ['-p', 'node', '--verbose-trace', '--js-trace', 'test.dart']); + expect(test.stdoutStream(), neverEmits(endsWith(' main.<fn>'))); + expect(test.stdoutStream(), neverEmits(contains('package:test'))); + expect(test.stdoutStream(), neverEmits(contains('dart:async/zone.dart'))); + expect(test.stdout, emitsThrough(contains('-1: Some tests failed.'))); + await test.shouldExit(1); + }); + + test('supports node_modules in the package directory', () async { + await d.dir('node_modules', [ + d.dir('my_module', [d.file('index.js', 'module.exports.value = 12;')]) + ]).create(); + + await d.file('test.dart', ''' + import 'package:js/js.dart'; + import 'package:test/test.dart'; + + @JS() + external MyModule require(String name); + + @JS() + class MyModule { + external int get value; + } + + void main() { + test("can load from a module", () { + expect(require("my_module").value, equals(12)); + }); + } + ''').create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + group('with onPlatform', () { + test('respects matching Skips', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("fail", () => throw 'oh no', onPlatform: {"node": Skip()}); + } + ''').create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+0 ~1: All tests skipped.'))); + await test.shouldExit(0); + }); + + test('ignores non-matching Skips', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("success", () {}, onPlatform: {"browser": Skip()}); + } + ''').create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('matches the current OS', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("fail", () => throw 'oh no', + onPlatform: {"${currentOS.identifier}": Skip()}); + } + ''').create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+0 ~1: All tests skipped.'))); + await test.shouldExit(0); + }); + + test("doesn't match a different OS", () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("success", () {}, onPlatform: {"$otherOS": Skip()}); + } + ''').create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + }); + + group('with an @OnPlatform annotation', () { + test('respects matching Skips', () async { + await d.file('test.dart', ''' + @OnPlatform(const {"js": const Skip()}) + + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("fail", () => throw 'oh no'); + } + ''').create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect(test.stdout, emitsThrough(contains('~1: All tests skipped.'))); + await test.shouldExit(0); + }); + + test('ignores non-matching Skips', () async { + await d.file('test.dart', ''' + @OnPlatform(const {"vm": const Skip()}) + + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("success", () {}); + } + ''').create(); + + var test = await runTest(['-p', 'node', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + }); +}
diff --git a/pkgs/test/test/runner/parse_metadata_test.dart b/pkgs/test/test/runner/parse_metadata_test.dart new file mode 100644 index 0000000..a8ffa9c --- /dev/null +++ b/pkgs/test/test/runner/parse_metadata_test.dart
@@ -0,0 +1,326 @@ +// Copyright (c) 2015, 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_api/src/backend/platform_selector.dart'; +import 'package:test_api/src/backend/runtime.dart'; +import 'package:test_api/src/backend/suite_platform.dart'; +import 'package:test_core/src/runner/parse_metadata.dart'; + +final _path = 'test.dart'; + +void main() { + test('returns empty metadata for an empty file', () { + var metadata = parseMetadata(_path, '', {}); + expect(metadata.testOn, equals(PlatformSelector.all)); + expect(metadata.timeout.scaleFactor, equals(1)); + }); + + test('ignores irrelevant annotations', () { + var metadata = + parseMetadata(_path, '@Fblthp\n@Fblthp.foo\nlibrary foo;', {}); + expect(metadata.testOn, equals(PlatformSelector.all)); + }); + + test('parses a prefixed annotation', () { + var metadata = parseMetadata( + _path, + "@foo.TestOn('vm')\n" + "import 'package:test/test.dart' as foo;", + {}); + expect(metadata.testOn.evaluate(SuitePlatform(Runtime.vm, compiler: null)), + isTrue); + expect( + metadata.testOn.evaluate(SuitePlatform(Runtime.chrome, compiler: null)), + isFalse); + }); + + group('@TestOn:', () { + test('parses a valid annotation', () { + var metadata = parseMetadata(_path, "@TestOn('vm')\nlibrary foo;", {}); + expect( + metadata.testOn.evaluate(SuitePlatform(Runtime.vm, compiler: null)), + isTrue); + expect( + metadata.testOn + .evaluate(SuitePlatform(Runtime.chrome, compiler: null)), + isFalse); + }); + + test('ignores a constructor named TestOn', () { + var metadata = + parseMetadata(_path, "@foo.TestOn('foo')\nlibrary foo;", {}); + expect(metadata.testOn, equals(PlatformSelector.all)); + }); + + group('throws an error for', () { + test('multiple @TestOns', () { + expect( + () => parseMetadata( + _path, "@TestOn('foo')\n@TestOn('bar')\nlibrary foo;", {}), + throwsFormatException); + }); + }); + }); + + group('@Timeout:', () { + test('parses a valid duration annotation', () { + var metadata = parseMetadata(_path, ''' +@Timeout(const Duration( + hours: 1, + minutes: 2, + seconds: 3, + milliseconds: 4, + microseconds: 5)) + +library foo; +''', {}); + expect( + metadata.timeout.duration, + equals(const Duration( + hours: 1, + minutes: 2, + seconds: 3, + milliseconds: 4, + microseconds: 5))); + }); + + test('parses a valid duration omitting const', () { + var metadata = parseMetadata(_path, ''' +@Timeout(Duration( + hours: 1, + minutes: 2, + seconds: 3, + milliseconds: 4, + microseconds: 5)) + +library foo; +''', {}); + expect( + metadata.timeout.duration, + equals(const Duration( + hours: 1, + minutes: 2, + seconds: 3, + milliseconds: 4, + microseconds: 5))); + }); + + test('parses a valid duration with an import prefix', () { + var metadata = parseMetadata(_path, ''' +@Timeout(core.Duration( + hours: 1, + minutes: 2, + seconds: 3, + milliseconds: 4, + microseconds: 5)) +import 'dart:core' as core; +''', {}); + expect( + metadata.timeout.duration, + equals(const Duration( + hours: 1, + minutes: 2, + seconds: 3, + milliseconds: 4, + microseconds: 5))); + }); + + test('parses a valid int factor annotation', () { + var metadata = parseMetadata(_path, ''' +@Timeout.factor(1) + +library foo; +''', {}); + expect(metadata.timeout.scaleFactor, equals(1)); + }); + + test('parses a valid int factor annotation with an import prefix', () { + var metadata = parseMetadata(_path, ''' +@test.Timeout.factor(1) +import 'package:test/test.dart' as test; +''', {}); + expect(metadata.timeout.scaleFactor, equals(1)); + }); + + test('parses a valid double factor annotation', () { + var metadata = parseMetadata(_path, ''' +@Timeout.factor(0.5) + +library foo; +''', {}); + expect(metadata.timeout.scaleFactor, equals(0.5)); + }); + + test('parses a valid Timeout.none annotation', () { + var metadata = parseMetadata(_path, ''' +@Timeout.none + +library foo; +''', {}); + expect(metadata.timeout, same(Timeout.none)); + }); + + test('ignores a constructor named Timeout', () { + var metadata = + parseMetadata(_path, "@foo.Timeout('foo')\nlibrary foo;", {}); + expect(metadata.timeout.scaleFactor, equals(1)); + }); + + group('throws an error for', () { + test('multiple @Timeouts', () { + expect( + () => parseMetadata(_path, + '@Timeout.factor(1)\n@Timeout.factor(2)\nlibrary foo;', {}), + throwsFormatException); + }); + }); + }); + + group('@Skip:', () { + test('parses a valid annotation', () { + var metadata = parseMetadata(_path, '@Skip()\nlibrary foo;', {}); + expect(metadata.skip, isTrue); + expect(metadata.skipReason, isNull); + }); + + test('parses a valid annotation with a reason', () { + var metadata = parseMetadata(_path, "@Skip('reason')\nlibrary foo;", {}); + expect(metadata.skip, isTrue); + expect(metadata.skipReason, equals('reason')); + }); + + test('ignores a constructor named Skip', () { + var metadata = parseMetadata(_path, "@foo.Skip('foo')\nlibrary foo;", {}); + expect(metadata.skip, isFalse); + }); + + group('throws an error for', () { + test('multiple @Skips', () { + expect( + () => parseMetadata( + _path, "@Skip('foo')\n@Skip('bar')\nlibrary foo;", {}), + throwsFormatException); + }); + }); + }); + + group('@Tags:', () { + test('parses a valid annotation', () { + var metadata = parseMetadata(_path, "@Tags(['a'])\nlibrary foo;", {}); + expect(metadata.tags, equals(['a'])); + }); + + test('ignores a constructor named Tags', () { + var metadata = parseMetadata(_path, "@foo.Tags(['a'])\nlibrary foo;", {}); + expect(metadata.tags, isEmpty); + }); + + group('throws an error for', () { + test('multiple @Tags', () { + expect( + () => parseMetadata( + _path, "@Tags(['a'])\n@Tags(['b'])\nlibrary foo;", {}), + throwsFormatException); + }); + + test('String interpolation', () { + expect( + () => parseMetadata( + _path, "@Tags(['\$a'])\nlibrary foo;\nconst a = 'a';", {}), + throwsFormatException); + }); + }); + }); + + group('@OnPlatform:', () { + test('parses a valid annotation', () { + var metadata = parseMetadata(_path, ''' +@OnPlatform({ + 'chrome': Timeout.factor(2), + 'vm': [Skip(), Timeout.factor(3)] +}) +library foo;''', {}); + + var key = metadata.onPlatform.keys.first; + expect( + key.evaluate(SuitePlatform(Runtime.chrome, compiler: null)), isTrue); + expect(key.evaluate(SuitePlatform(Runtime.vm, compiler: null)), isFalse); + var value = metadata.onPlatform.values.first; + expect(value.timeout.scaleFactor, equals(2)); + + key = metadata.onPlatform.keys.last; + expect(key.evaluate(SuitePlatform(Runtime.vm, compiler: null)), isTrue); + expect( + key.evaluate(SuitePlatform(Runtime.chrome, compiler: null)), isFalse); + value = metadata.onPlatform.values.last; + expect(value.skip, isTrue); + expect(value.timeout.scaleFactor, equals(3)); + }); + + test('parses a valid annotation with an import prefix', () { + var metadata = parseMetadata(_path, ''' +@test.OnPlatform({ + 'chrome': test.Timeout.factor(2), + 'vm': [test.Skip(), test.Timeout.factor(3)] +}) +import 'package:test/test.dart' as test; +''', {}); + + var key = metadata.onPlatform.keys.first; + expect( + key.evaluate(SuitePlatform(Runtime.chrome, compiler: null)), isTrue); + expect(key.evaluate(SuitePlatform(Runtime.vm, compiler: null)), isFalse); + var value = metadata.onPlatform.values.first; + expect(value.timeout.scaleFactor, equals(2)); + + key = metadata.onPlatform.keys.last; + expect(key.evaluate(SuitePlatform(Runtime.vm, compiler: null)), isTrue); + expect( + key.evaluate(SuitePlatform(Runtime.chrome, compiler: null)), isFalse); + value = metadata.onPlatform.values.last; + expect(value.skip, isTrue); + expect(value.timeout.scaleFactor, equals(3)); + }); + + test('ignores a constructor named OnPlatform', () { + var metadata = + parseMetadata(_path, "@foo.OnPlatform('foo')\nlibrary foo;", {}); + expect(metadata.testOn, equals(PlatformSelector.all)); + }); + + group('throws an error for', () { + test('a map with a unparseable key', () { + expect( + () => parseMetadata( + _path, "@OnPlatform({'invalid': Skip()})\nlibrary foo;", {}), + throwsFormatException); + }); + + test('a map with an invalid value', () { + expect( + () => parseMetadata(_path, + "@OnPlatform({'vm': const TestOn('vm')})\nlibrary foo;", {}), + throwsFormatException); + }); + + test('a map with an invalid value in a list', () { + expect( + () => parseMetadata(_path, + "@OnPlatform({'vm': [const TestOn('vm')]})\nlibrary foo;", {}), + throwsFormatException); + }); + + test('multiple @OnPlatforms', () { + expect( + () => parseMetadata( + _path, '@OnPlatform({})\n@OnPlatform({})\nlibrary foo;', {}), + throwsFormatException); + }); + }); + }); +}
diff --git a/pkgs/test/test/runner/pause_after_load_test.dart b/pkgs/test/test/runner/pause_after_load_test.dart new file mode 100644 index 0000000..faba441 --- /dev/null +++ b/pkgs/test/test/runner/pause_after_load_test.dart
@@ -0,0 +1,283 @@ +// Copyright (c) 2015, 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') +@OnPlatform({'windows': Skip('https://github.com/dart-lang/test/issues/1613')}) +library; + +import 'dart:async'; +import 'dart:io'; + +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('pauses the test runner for each file until the user presses enter', + () async { + await d.file('test1.dart', ''' +import 'package:test/test.dart'; + +void main() { + print('loaded test 1!'); + + test("success", () {}); +} +''').create(); + + await d.file('test2.dart', ''' +import 'package:test/test.dart'; + +void main() { + + print('loaded test 2!'); + test("success", () {}); +} +''').create(); + + var test = await runTest( + ['--pause-after-load', '-p', 'chrome', 'test1.dart', 'test2.dart']); + await expectLater(test.stdout, emitsThrough('loaded test 1!')); + await expectLater(test.stdout, emitsThrough(equalsIgnoringWhitespace(''' + The test runner is paused. Open the dev console in Chrome and set + breakpoints. Once you're finished, return to this terminal and press + Enter. + '''))); + + var nextLineFired = false; + unawaited(test.stdout.next.then(expectAsync1((line) { + expect(line, contains('+0: test1.dart: success')); + nextLineFired = true; + }))); + + // Wait a little bit to be sure that the tests don't start running without + // our input. + await Future<void>.delayed(const Duration(seconds: 2)); + expect(nextLineFired, isFalse); + + test.stdin.writeln(); + + await expectLater(test.stdout, emitsThrough('loaded test 2!')); + await expectLater(test.stdout, emitsThrough(equalsIgnoringWhitespace(''' + The test runner is paused. Open the dev console in Chrome and set + breakpoints. Once you're finished, return to this terminal and press + Enter. + '''))); + + nextLineFired = false; + unawaited(test.stdout.next.then(expectAsync1((line) { + expect(line, contains('+1: test2.dart: success')); + nextLineFired = true; + }))); + + // Wait a little bit to be sure that the tests don't start running without + // our input. + await Future<void>.delayed(const Duration(seconds: 2)); + expect(nextLineFired, isFalse); + + test.stdin.writeln(); + await expectLater( + test.stdout, emitsThrough(contains('+2: All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + test('pauses the test runner for each platform until the user presses enter', + () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + print('loaded test!'); + + test("success", () {}); +} +''').create(); + + var test = await runTest([ + '--pause-after-load', + '-p', + 'firefox', + '-p', + 'chrome', + '-p', + 'vm', + 'test.dart' + ]); + await expectLater(test.stdout, emitsThrough('loaded test!')); + await expectLater(test.stdout, emitsThrough(equalsIgnoringWhitespace(''' + The test runner is paused. Open the dev console in Firefox and set + breakpoints. Once you're finished, return to this terminal and press + Enter. + '''))); + + var nextLineFired = false; + unawaited(test.stdout.next.then(expectAsync1((line) { + expect(line, contains('+0: [Firefox, Dart2Js] success')); + nextLineFired = true; + }))); + + // Wait a little bit to be sure that the tests don't start running without + // our input. + await Future<void>.delayed(const Duration(seconds: 2)); + expect(nextLineFired, isFalse); + + test.stdin.writeln(); + + await expectLater(test.stdout, emitsThrough('loaded test!')); + await expectLater( + test.stdout, + emitsThrough(emitsInOrder([ + 'The test runner is paused. Open the dev console in Chrome and set ' + "breakpoints. Once you're finished, return to this terminal and " + 'press Enter.' + ]))); + + nextLineFired = false; + unawaited(test.stdout.next.then(expectAsync1((line) { + expect(line, contains('+1: [Chrome, Dart2Js] success')); + nextLineFired = true; + }))); + + // Wait a little bit to be sure that the tests don't start running without + // our input. + await Future<void>.delayed(const Duration(seconds: 2)); + expect(nextLineFired, isFalse); + + test.stdin.writeln(); + await expectLater(test.stdout, emitsThrough('loaded test!')); + await expectLater( + test.stdout, + emitsThrough(emitsInOrder([ + 'The test runner is paused. Open the Observatory and set ' + "breakpoints. Once you're finished, return to this terminal " + 'and press Enter.' + ]))); + + nextLineFired = false; + unawaited(test.stdout.next.then(expectAsync1((line) { + expect(line, contains('+2: [VM, Kernel] success')); + nextLineFired = true; + }))); + + // Wait a little bit to be sure that the tests don't start running without + // our input. + await Future<void>.delayed(const Duration(seconds: 2)); + expect(nextLineFired, isFalse); + + test.stdin.writeln(); + + await expectLater( + test.stdout, emitsThrough(contains('+3: All tests passed!'))); + await test.shouldExit(0); + }, tags: ['firefox', 'chrome', 'vm']); + + test('stops immediately if killed while paused', () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + print('loaded test!'); + + test("success", () {}); +} +''').create(); + + var test = + await runTest(['--pause-after-load', '-p', 'chrome', 'test.dart']); + await expectLater(test.stdout, emitsThrough('loaded test!')); + await expectLater(test.stdout, emitsThrough(equalsIgnoringWhitespace(''' + The test runner is paused. Open the dev console in Chrome and set + breakpoints. Once you're finished, return to this terminal and press + Enter. + '''))); + + test.signal(ProcessSignal.sigterm); + await test.shouldExit(); + await expectLater(test.stderr, emitsDone); + }, tags: 'chrome', testOn: '!windows'); + + test('disables timeouts', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + print('loaded test 1!'); + + test("success", () async { + await Future.delayed(Duration.zero); + }, timeout: Timeout(Duration.zero)); +} +''').create(); + + var test = await runTest( + ['--pause-after-load', '-p', 'chrome', '-n', 'success', 'test.dart']); + await expectLater(test.stdout, emitsThrough('loaded test 1!')); + await expectLater(test.stdout, emitsThrough(equalsIgnoringWhitespace(''' + The test runner is paused. Open the dev console in Chrome and set + breakpoints. Once you're finished, return to this terminal and press + Enter. + '''))); + + var nextLineFired = false; + unawaited(test.stdout.next.then(expectAsync1((line) { + expect(line, contains('+0: success')); + nextLineFired = true; + }))); + + // Wait a little bit to be sure that the tests don't start running without + // our input. + await Future<void>.delayed(const Duration(seconds: 2)); + expect(nextLineFired, isFalse); + + test.stdin.writeln(); + await expectLater( + test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + // Regression test for #304. + test('supports test name patterns', () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + print('loaded test 1!'); + + test("failure 1", () {}); + test("success", () {}); + test("failure 2", () {}); +} +''').create(); + + var test = await runTest( + ['--pause-after-load', '-p', 'chrome', '-n', 'success', 'test.dart']); + await expectLater(test.stdout, emitsThrough('loaded test 1!')); + await expectLater(test.stdout, emitsThrough(equalsIgnoringWhitespace(''' + The test runner is paused. Open the dev console in Chrome and set + breakpoints. Once you're finished, return to this terminal and press + Enter. + '''))); + + var nextLineFired = false; + unawaited(test.stdout.next.then(expectAsync1((line) { + expect(line, contains('+0: success')); + nextLineFired = true; + }))); + + // Wait a little bit to be sure that the tests don't start running without + // our input. + await Future<void>.delayed(const Duration(seconds: 2)); + expect(nextLineFired, isFalse); + + test.stdin.writeln(); + await expectLater( + test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); +}
diff --git a/pkgs/test/test/runner/precompiled_test.dart b/pkgs/test/test/runner/precompiled_test.dart new file mode 100644 index 0000000..51dde28 --- /dev/null +++ b/pkgs/test/test/runner/precompiled_test.dart
@@ -0,0 +1,264 @@ +// Copyright (c) 2016, 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') +@OnPlatform({'windows': Skip('https://github.com/dart-lang/test/issues/1617')}) +library; + +import 'dart:async'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:node_preamble/preamble.dart' as preamble; +import 'package:package_config/package_config.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; +import 'package:test_process/test_process.dart'; + +import '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + group('browser tests', () { + setUpAll(() async { + await _precompileBrowserTest('test.dart'); + }); + + test('run a precompiled version of a test rather than recompiling', + () async { + var test = await runTest([ + '-p', + 'chrome', + '--precompiled=precompiled/', + 'test.dart', + ]); + expect(test.stdout, + containsInOrder(['+0: success', '+1: All tests passed!'])); + await test.shouldExit(0); + }); + + test('run two precompiled tests', () async { + await _precompileBrowserTest('test_2.dart'); + var test = await runTest(concurrency: 2, [ + '-p', + 'chrome', + '--precompiled=precompiled/', + 'test.dart', + 'test_2.dart', + ]); + expect(test.stdout, containsInOrder(['+2: All tests passed!'])); + await test.shouldExit(0); + }); + + test('can use the json reporter', () async { + var test = await runTest([ + '-p', + 'chrome', + '--precompiled=precompiled/', + 'test.dart', + '-r', + 'json' + ]); + expect( + test.stdout, + containsInOrder([ + '{"testID":3,"result":"success"', + '{"success":true,"type":"done"' + ])); + await test.shouldExit(0); + }); + }, tags: const ['chrome']); + + group('node tests', () { + setUp(() async { + await d.dir('test', [ + d.file('test.dart', ''' + import "package:test/src/bootstrap/node.dart"; + import "package:test/test.dart"; + + void main() { + internalBootstrapNodeTest(() => () => test("success", () { + expect(true, isTrue); + })); + }''') + ]).create(); + await _writePackagesFile(); + + var jsPath = p.join(d.sandbox, 'test', 'test.dart.node_test.dart.js'); + var dart2js = await TestProcess.start( + Platform.resolvedExecutable, + [ + 'compile', + 'js', + '--packages=${await Isolate.packageConfig}', + p.join('test', 'test.dart'), + '--out=$jsPath', + ], + workingDirectory: d.sandbox); + await dart2js.shouldExit(0); + + var jsFile = File(jsPath); + await jsFile.writeAsString( + preamble.getPreamble(minified: true) + await jsFile.readAsString()); + + await d.dir('test', [d.file('test.dart', 'invalid dart}')]).create(); + }); + + test('run a precompiled version of a test rather than recompiling', + () async { + var test = await runTest([ + '-p', + 'node', + '--precompiled', + d.sandbox, + p.join('test', 'test.dart') + ]); + expect(test.stdout, + containsInOrder(['+0: success', '+1: All tests passed!'])); + await test.shouldExit(0); + }); + + test('can use the json reporter', () async { + var test = await runTest([ + '-p', + 'node', + '--precompiled', + d.sandbox, + p.join('test', 'test.dart'), + '-r', + 'json' + ]); + expect( + test.stdout, + containsInOrder([ + '{"testID":3,"result":"success"', + '{"success":true,"type":"done"' + ])); + await test.shouldExit(0); + }); + }, tags: const ['node']); + + group('vm tests', () { + setUp(() async { + await d.dir('test', [ + d.file('test.dart', ''' + import "package:test/test.dart"; + void main() { + test("true is true", () { + expect(true, isTrue); + }); + } + '''), + d.file('test.dart.vm_test.dart', ''' + import "dart:isolate"; + import "package:test_core/src/bootstrap/vm.dart"; + import "test.dart" as test; + void main(_, SendPort message) { + internalBootstrapVmTest(() => test.main, message); + } + '''), + ]).create(); + await _writePackagesFile(); + }); + + test('run in the precompiled directory', () async { + var test = await runTest( + ['-p', 'vm', '--precompiled=${d.sandbox}', 'test/test.dart']); + expect(test.stdout, + containsInOrder(['+0: true is true', '+1: All tests passed!'])); + await test.shouldExit(0); + }); + + test('can load precompiled dill files if available', () async { + // Create the snapshot in the sandbox directory. + var snapshotProcess = await runDart([ + '--snapshot_kind=script', + '--snapshot=test/test.dart.vm_test.vm.app.dill', + 'test/test.dart.vm_test.dart' + ]); + await snapshotProcess.shouldExit(0); + + // Modify the original test so it would fail if it actually got ran, this + // makes sure the test fails if the dill file isn't loaded. + var testFile = File(p.join(d.sandbox, 'test', 'test.dart')); + expect(await testFile.exists(), isTrue); + var originalContent = await testFile.readAsString(); + await testFile + .writeAsString(originalContent.replaceAll('isTrue', 'isFalse')); + + // Actually invoke the test with the dill file. + var testProcess = await runTest( + ['-p', 'vm', '--precompiled=${d.sandbox}', 'test/test.dart']); + expect(testProcess.stdout, + containsInOrder(['+0: true is true', '+1: All tests passed!'])); + await testProcess.shouldExit(0); + }); + + test('can use the json reporter', () async { + var test = await runTest([ + '-p', + 'vm', + '--precompiled=${d.sandbox}', + 'test/test.dart', + '-r', + 'json' + ]); + expect( + test.stdout, + containsInOrder([ + '{"testID":3,"result":"success"', + '{"success":true,"type":"done"' + ])); + await test.shouldExit(0); + }); + }); +} + +Future<void> _writePackagesFile() async { + var config = (await findPackageConfig(Directory.current))!; + await d.dir('.dart_tool').create(); + await savePackageConfig(config, Directory(d.sandbox)); +} + +Future<void> _precompileBrowserTest(String testPath) async { + var tmpDir = await Directory.systemTemp.createTemp('browser_test'); + var file = File.fromUri(tmpDir.uri.resolve('precompiled.dart')); + await file.writeAsString(''' + import "package:test/bootstrap/browser.dart"; + import "package:test/test.dart"; + + main(_) { + internalBootstrapBrowserTest(() => () => test("success", () {})); + } + '''); + + await d.dir('precompiled', [ + d.file(p.setExtension(testPath, 'html'), ''' + <!DOCTYPE html> + <html> + <head> + <title>test Test</title> + <script src="$testPath.browser_test.dart.js"></script> + </head> + </html> + ''') + ]).create(); + + var dart2js = await TestProcess.start( + Platform.resolvedExecutable, + [ + 'compile', + 'js', + ...Platform.executableArguments, + '--packages=${(await Isolate.packageConfig)!.toFilePath()}', + file.path, + '--out=precompiled/$testPath.browser_test.dart.js' + ], + workingDirectory: d.sandbox); + await dart2js.shouldExit(0); + + await d.file(testPath, 'invalid dart}').create(); +}
diff --git a/pkgs/test/test/runner/retry_test.dart b/pkgs/test/test/runner/retry_test.dart new file mode 100644 index 0000000..0e16fef --- /dev/null +++ b/pkgs/test/test/runner/retry_test.dart
@@ -0,0 +1,231 @@ +// 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 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('respects --no-retry flag with retry option', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + var attempt = 0; + void main() { + test("eventually passes", () { + attempt++; + if(attempt <= 1 ) { + throw TestFailure("oh no"); + } + }, retry: 1); + } + ''').create(); + + var test = await runTest(['test.dart', '--no-retry']); + expect(test.stdout, emitsThrough(contains('-1: Some tests failed.'))); + await test.shouldExit(1); + }); + + test('respects --no-retry flag with @Retry declaration', () async { + await d.file('test.dart', ''' + @Retry(3) + + import 'dart:async'; + + import 'package:test/test.dart'; + + var attempt = 0; + void main() { + test("eventually passes", () { + attempt++; + if(attempt <= 1 ) { + throw TestFailure("oh no"); + } + }); + } + ''').create(); + + var test = await runTest(['test.dart', '--no-retry']); + expect(test.stdout, emitsThrough(contains('-1: Some tests failed.'))); + await test.shouldExit(1); + }); + + test('respects top-level @Retry declarations', () async { + await d.file('test.dart', ''' + @Retry(3) + + import 'dart:async'; + + import 'package:test/test.dart'; + + var attempt = 0; + void main() { + test("failure", () { + attempt++; + if(attempt <= 3) { + throw TestFailure("oh no"); + } + }); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('respects group retry declarations', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + var attempt = 0; + void main() { + group("retry", () { + test("failure", () { + attempt++; + if(attempt <= 3) { + throw TestFailure("oh no"); + } + }); + }, retry: 3); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('tests are not retried after they have already been reported successful', + () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + var completer1 = Completer(); + var completer2 = Completer(); + test("first", () { + completer1.future.then((_) { + completer2.complete(); + throw "oh no"; + }); + }, retry: 2); + + test("second", () async { + completer1.complete(); + await completer2.future; + }); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + emitsThrough( + contains('This test failed after it had already completed'))); + await test.shouldExit(1); + }); + + group('retries tests', () { + test('and eventually passes for valid tests', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + var attempt = 0; + void main() { + test("eventually passes", () { + attempt++; + if(attempt <= 2) { + throw TestFailure("oh no"); + } + }, retry: 2); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('and ignores previous errors', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + var attempt = 0; + Completer completer = Completer(); + void main() { + test("failure", () async { + attempt++; + if (attempt == 1) { + completer.future.then((_) => throw 'some error'); + throw TestFailure("oh no"); + } + completer.complete(null); + await Future((){}); + }, retry: 1); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('and eventually fails for invalid tests', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("failure", () { + throw TestFailure("oh no"); + }, retry: 2); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('-1: Some tests failed.'))); + await test.shouldExit(1); + }); + + test('only after a failure', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + var attempt = 0; + void main() { + test("eventually passes", () { + attempt++; + if (attempt != 2){ + throw TestFailure("oh no"); + } + }, retry: 5); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + }); +}
diff --git a/pkgs/test/test/runner/runner_test.dart b/pkgs/test/test/runner/runner_test.dart new file mode 100644 index 0000000..379e2a7 --- /dev/null +++ b/pkgs/test/test/runner/runner_test.dart
@@ -0,0 +1,871 @@ +// Copyright (c) 2015, 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 'dart:math' as math; + +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; +import 'package:test_core/src/util/exit_codes.dart' as exit_codes; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; + +final _success = ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +'''; + +final _failure = ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("failure", () => throw TestFailure("oh no")); +} +'''; + +final _asyncFailure = ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("failure", () async { + await Future(() {}).then((_) { + throw 'oh no'; + }); + }); +} +'''; + +final _defaultConcurrency = math.max(1, Platform.numberOfProcessors ~/ 2); + +final _usage = ''' +Usage: dart test [files or directories...] + +-h, --help Show this usage information. + --version Show the package:test version. + +Selecting Tests: +-n, --name A substring of the name of the test to run. + Regular expression syntax is supported. + If passed multiple times, tests must match all substrings. +-N, --plain-name A plain-text substring of the name of the test to run. + If passed multiple times, tests must match all substrings. +-t, --tags Run only tests with all of the specified tags. + Supports boolean selector syntax. +-x, --exclude-tags Don't run tests with any of the specified tags. + Supports boolean selector syntax. + --[no-]run-skipped Run skipped tests instead of skipping them. + +Running Tests: +-p, --platform The platform(s) on which to run the tests. + $_runtimes. + Each platform supports the following compilers: +$_runtimeCompilers +-c, --compiler The compiler(s) to use to run tests, supported compilers are [dart2js, dart2wasm, exe, kernel, source]. + Each platform has a default compiler but may support other compilers. + You can target a compiler to a specific platform using arguments of the following form [<platform-selector>:]<compiler>. + If a platform is specified but no given compiler is supported for that platform, then it will use its default compiler. +-P, --preset The configuration preset(s) to use. +-j, --concurrency=<threads> The number of concurrent test suites run. + (defaults to "$_defaultConcurrency") + --total-shards The total number of invocations of the test runner being run. + --shard-index The index of this test runner invocation (of --total-shards). + --timeout The default test timeout. For example: 15s, 2x, none + (defaults to "30s") + --ignore-timeouts Ignore all timeouts (useful if debugging) + --pause-after-load Pause for debugging before any tests execute. + Implies --concurrency=1, --debug, and --ignore-timeouts. + Currently only supported for browser tests. + --debug Run the VM and Chrome tests in debug mode. + --coverage=<directory> Gather coverage and output it to the specified directory. + Implies --debug. + --[no-]chain-stack-traces Use chained stack traces to provide greater exception details + especially for asynchronous code. It may be useful to disable + to provide improved test performance but at the cost of + debuggability. + --no-retry Don't rerun tests that have retry set. + --test-randomize-ordering-seed Use the specified seed to randomize the execution order of test cases. + Must be a 32bit unsigned integer or "random". + If "random", pick a random seed to use. + If not passed, do not randomize test case execution order. + --[no-]fail-fast Stop running tests after the first failure. + +Output: +-r, --reporter=<option> Set how to print test results. + + [compact] A single line, updated continuously. + [expanded] (default) A separate line for each update. + [failures-only] A separate line for failing tests with no output for passing tests + [github] A custom reporter for GitHub Actions (the default reporter when running on GitHub Actions). + [json] A machine-readable format (see https://dart.dev/go/test-docs/json_reporter.md). + [silent] A reporter with no output. May be useful when only the exit code is meaningful. + + --file-reporter Enable an additional reporter writing test results to a file. + Should be in the form <reporter>:<filepath>, Example: "json:reports/tests.json" + --verbose-trace Emit stack traces with core library frames. + --js-trace Emit raw JavaScript stack traces for browser tests. + --[no-]color Use terminal colors. + (auto-detected by default) +'''; + +final _runtimes = '[vm (default), chrome, firefox' + '${Platform.isMacOS ? ', safari' : ''}' + ', edge, node]'; + +final _runtimeCompilers = [ + '[vm]: kernel (default), source, exe', + '[chrome]: dart2js (default), dart2wasm', + '[firefox]: dart2js (default), dart2wasm', + if (Platform.isMacOS) '[safari]: dart2js (default)', + '[edge]: dart2js (default)', + '[node]: dart2js (default), dart2wasm', +].map((str) => ' $str').join('\n'); + +void main() { + setUpAll(precompileTestExecutable); + + test('prints help information', () async { + var test = await runTest(['--help']); + expectStdoutEquals(test, ''' +Runs tests in this package. + +$_usage'''); + await test.shouldExit(0); + }); + + group('fails gracefully if', () { + test('an invalid option is passed', () async { + var test = await runTest(['--asdf']); + expectStderrEquals(test, ''' +Could not find an option named "--asdf". + +$_usage'''); + await test.shouldExit(exit_codes.usage); + }); + + test('a non-existent file is passed', () async { + var test = await runTest(['file']); + expect( + test.stdout, + containsInOrder([ + '-1: loading file [E]', + 'Failed to load "file": Does not exist.' + ])); + await test.shouldExit(1); + }); + + test("the default directory doesn't exist", () async { + var test = await runTest([]); + expectStderrEquals(test, ''' +No test files were passed and the default "test/" directory doesn't exist. + +$_usage'''); + await test.shouldExit(exit_codes.data); + }); + + test('a test file fails to load', () async { + await d.file('test.dart', 'invalid Dart file').create(); + var test = await runTest(['test.dart']); + + expect( + test.stdout, + containsInOrder([ + 'Failed to load "test.dart":', + "test.dart:1:9: Error: Expected ';' after this.", + 'invalid Dart file' + ])); + + await test.shouldExit(1); + }); + + // This syntax error is detected lazily, and so requires some extra + // machinery to support. + test('a test file fails to parse due to a missing semicolon', () async { + await d.file('test.dart', 'void main() {foo}').create(); + var test = await runTest(['test.dart']); + + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart":', + "test.dart:1:14: Error: Expected ';' after this" + ])); + + await test.shouldExit(1); + }); + + // This is slightly different from the above test because it's an error + // that's caught first by the analyzer when it's used to parse the file. + test('a test file fails to parse', () async { + await d.file('test.dart', '@TestOn)').create(); + var test = await runTest(['test.dart']); + + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart":', + "test.dart:1:8: Error: Expected a declaration, but got ')'", + '@TestOn)', + ])); + + await test.shouldExit(1); + }); + + test("an annotation's contents are invalid", () async { + await d.file('test.dart', "@TestOn('zim')\nlibrary foo;").create(); + var test = await runTest(['test.dart']); + + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart":', + 'Error on line 1, column 10: Undefined variable.', + "@TestOn('zim')", + ' ^^^' + ])); + await test.shouldExit(1); + }); + + test('a test file throws', () async { + await d.file('test.dart', "void main() => throw 'oh no';").create(); + var test = await runTest(['test.dart']); + + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": oh no' + ])); + await test.shouldExit(1); + }); + + test("a test file doesn't have a main defined", () async { + await d.file('test.dart', 'void foo() {}').create(); + var test = await runTest(['test.dart']); + + expect( + test.stdout, + emitsThrough( + contains('-1: loading test.dart [E]'), + )); + expect( + test.stdout, + emitsThrough(anyOf([ + contains("Error: Getter not found: 'main'"), + contains("Error: Undefined name 'main'"), + ]))); + + await test.shouldExit(1); + }); + + test('a test file has a non-function main', () async { + await d.file('test.dart', 'int main = 0;').create(); + var test = await runTest(['test.dart']); + + expect(test.stdout, emitsThrough(contains('-1: loading test.dart [E]'))); + expect( + test.stdout, + emitsThrough(anyOf([ + contains( + "A value of type 'int' can't be assigned to a variable of type " + "'Function'", + ), + contains( + "A value of type 'int' can't be returned from a function with " + "return type 'Function'", + ), + ]))); + + await test.shouldExit(1); + }); + + test('a test file has a main with arguments', () async { + await d.file('test.dart', 'void main(arg) {}').create(); + var test = await runTest(['test.dart']); + + expect( + test.stdout, + containsInOrder([ + '-1: loading test.dart [E]', + 'Failed to load "test.dart": Top-level main() function takes arguments.' + ])); + await test.shouldExit(1); + }); + + test('multiple load errors occur', () async { + await d.file('test.dart', 'invalid Dart file').create(); + var test = await runTest(['test.dart', 'nonexistent.dart']); + + expect( + await test.stdoutStream().toList(), + containsAll([ + contains('loading nonexistent.dart [E]'), + contains('Failed to load "nonexistent.dart": Does not exist'), + contains('loading test.dart [E]'), + contains('Failed to load "test.dart"'), + ])); + + await test.shouldExit(1); + }); + + // TODO(nweiz): test what happens when a test file is unreadable once issue + // 15078 is fixed. + }); + + group('runs successful tests', () { + test('defined in a single file', () async { + await d.file('test.dart', _success).create(); + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('defined in a directory', () async { + for (var i = 0; i < 3; i++) { + await d.file('${i}_test.dart', _success).create(); + } + + var test = await runTest(['.']); + expect(test.stdout, emitsThrough(contains('+3: All tests passed!'))); + await test.shouldExit(0); + }); + + test('defaulting to the test directory', () async { + await d + .dir( + 'test', + Iterable.generate(3, (i) { + return d.file('${i}_test.dart', _success); + })) + .create(); + + var test = await runTest([]); + expect(test.stdout, emitsThrough(contains('+3: All tests passed!'))); + await test.shouldExit(0); + }); + + test('directly', () async { + await d.file('test.dart', _success).create(); + var test = await runDart(['test.dart']); + + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }); + + // Regression test; this broke in 0.12.0-beta.9. + test('on a file in a subdirectory', () async { + await d.dir('dir', [d.file('test.dart', _success)]).create(); + + var test = await runTest(['dir/test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('given a file: uri', () async { + await d.file('test.dart', _success).create(); + var fileUri = p.toUri(d.path('test.dart')).toString(); + expect(fileUri, startsWith('file:///')); + var test = await runTest([fileUri]); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('with platform specific relative paths', () async { + await d.dir('foo', [d.file('test.dart', _success)]).create(); + var test = await runTest([p.join('foo', 'test.dart')]); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('with platform specific absolute paths', () async { + await d.dir('foo', [d.file('test.dart', _success)]).create(); + var test = await runTest([d.path(p.join('foo', 'test.dart'))]); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('with platform specific relative paths containing query params', + () async { + await d.dir('foo', [d.file('test.dart', _success)]).create(); + var test = await runTest(['${p.join('foo', 'test.dart')}?line=6']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + }); + + group('runs successful tests with async setup', () { + setUp(() async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() async { + test("success 1", () {}); + + await () async {}; + + test("success 2", () {}); + } + ''').create(); + }); + + test('defined in a single file', () async { + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+2: All tests passed!'))); + await test.shouldExit(0); + }); + + test('directly', () async { + var test = await runDart(['test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }); + }); + + group('runs failing tests', () { + test('respects the chain-stack-traces flag', () async { + await d.file('test.dart', _asyncFailure).create(); + + var test = await runTest(['test.dart', '--chain-stack-traces']); + expect(test.stdout, emitsThrough(contains('asynchronous gap'))); + await test.shouldExit(1); + }); + + test('defaults to not chaining stack traces', () async { + await d.file('test.dart', _asyncFailure).create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder([ + '00:00 +0: failure', + '00:00 +0 -1: failure [E]', + 'oh no', + 'test.dart 8:7 main.<fn>.<fn>', + ])); + await test.shouldExit(1); + }); + + test('defined in a single file', () async { + await d.file('test.dart', _failure).create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('-1: Some tests failed.'))); + await test.shouldExit(1); + }); + + test('defined in a directory', () async { + for (var i = 0; i < 3; i++) { + await d.file('${i}_test.dart', _failure).create(); + } + + var test = await runTest(['.']); + expect(test.stdout, emitsThrough(contains('-3: Some tests failed.'))); + await test.shouldExit(1); + }); + + test('defaulting to the test directory', () async { + await d + .dir( + 'test', + Iterable.generate(3, (i) { + return d.file('${i}_test.dart', _failure); + })) + .create(); + + var test = await runTest([]); + expect(test.stdout, emitsThrough(contains('-3: Some tests failed.'))); + await test.shouldExit(1); + }); + + test('directly', () async { + await d.file('test.dart', _failure).create(); + var test = await runDart(['test.dart']); + expect(test.stdout, emitsThrough(contains('Some tests failed.'))); + await test.shouldExit(255); + }); + }); + + test('runs tests even when a file fails to load', () async { + await d.file('test.dart', _success).create(); + + var test = await runTest(['test.dart', 'nonexistent.dart']); + expect(test.stdout, emitsThrough(contains('+1 -1: Some tests failed.'))); + await test.shouldExit(1); + }); + + group('with a top-level @Skip declaration', () { + setUp(() async { + await d.file('test.dart', ''' + @Skip() + + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test('success', () {}); + test('explicitly unskipped', skip: false, () {}); + } + ''').create(); + }); + + test('skips all tests', () async { + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+0 ~1: All tests skipped.'))); + await test.shouldExit(0); + }); + + test('runs all tests with --run-skipped', () async { + var test = await runTest(['--run-skipped', 'test.dart']); + expect(test.stdout, emitsThrough(contains('+2: All tests passed!'))); + await test.shouldExit(0); + }); + }); + + group('with onPlatform', () { + test('respects matching Skips', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("fail", () => throw 'oh no', onPlatform: {"vm": Skip()}); +} +''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+0 ~1: All tests skipped.'))); + await test.shouldExit(0); + }); + + test('ignores non-matching Skips', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}, onPlatform: {"chrome": Skip()}); +} +''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('respects matching Timeouts', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("fail", () async { + await Future.delayed(Duration.zero); + throw 'oh no'; + }, onPlatform: { + "vm": Timeout(Duration.zero) + }); +} +''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder( + ['Test timed out after 0 seconds.', '-1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('ignores non-matching Timeouts', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}, onPlatform: { + "chrome": Timeout(Duration(seconds: 0)) + }); +} +''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('applies matching platforms in order', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}, onPlatform: { + "vm": Skip("first"), + "vm || windows": Skip("second"), + "vm || linux": Skip("third"), + "vm || mac-os": Skip("fourth"), + "vm || android": Skip("fifth") + }); +} +''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdoutStream(), neverEmits(contains('Skip: first'))); + expect(test.stdoutStream(), neverEmits(contains('Skip: second'))); + expect(test.stdoutStream(), neverEmits(contains('Skip: third'))); + expect(test.stdoutStream(), neverEmits(contains('Skip: fourth'))); + expect(test.stdout, emitsThrough(contains('Skip: fifth'))); + await test.shouldExit(0); + }); + + test('applies platforms to a group', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + group("group", () { + test("success", () {}); + }, onPlatform: { + "vm": Skip() + }); +} +''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('All tests skipped.'))); + await test.shouldExit(0); + }); + }); + + group('with an @OnPlatform annotation', () { + test('respects matching Skips', () async { + await d.file('test.dart', ''' +@OnPlatform(const {"vm": const Skip()}) + +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("fail", () => throw 'oh no'); +} +''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+0 ~1: All tests skipped.'))); + await test.shouldExit(0); + }); + + test('ignores non-matching Skips', () async { + await d.file('test.dart', ''' +@OnPlatform(const {"chrome": const Skip()}) + +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('respects matching Timeouts', () async { + await d.file('test.dart', ''' +@OnPlatform(const { + "vm": const Timeout(const Duration(seconds: 0)) +}) + +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("fail", () async { + await Future.delayed(Duration.zero); + throw 'oh no'; + }); +} +''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder( + ['Test timed out after 0 seconds.', '-1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('ignores non-matching Timeouts', () async { + await d.file('test.dart', ''' +@OnPlatform(const { + "chrome": const Timeout(const Duration(seconds: 0)) +}) + +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("success", () {}); +} +''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + }); + + test('with the --color flag, uses colors', () async { + await d.file('test.dart', _failure).create(); + var test = await runTest(['--color', 'test.dart']); + // This is the color code for red. + expect(test.stdout, emitsThrough(contains('\u001b[31m'))); + await test.shouldExit(); + }); + + group('runs tests successfully more than once when calling runTests', () { + test('defined in a single file', () async { + await d.file('test.dart', _success).create(); + await d.file('runner.dart', ''' +import 'package:test_core/src/executable.dart' as test; + +void main(List<String> args) async { + await test.runTests(args); + await test.runTests(args); + test.completeShutdown(); +}''').create(); + var test = await runDart([ + 'runner.dart', + '--no-color', + '--reporter', + 'compact', + '--', + 'test.dart', + ], description: 'dart runner.dart -- test.dart'); + expect( + test.stdout, + emitsThrough(containsInOrder([ + '+0: loading test.dart', + '+0: success', + '+1: success', + 'All tests passed!' + ]))); + expect( + test.stdout, + emitsThrough(containsInOrder([ + '+0: loading test.dart', + '+0: success', + '+1: success', + '+1: All tests passed!', + ]))); + await test.shouldExit(0); + }); + }, onPlatform: const { + 'windows': Skip('https://github.com/dart-lang/test/issues/1615') + }); + + group('language experiments', () { + group('are inherited from the executable arguments', () { + setUp(() async { + await d.file('test.dart', ''' +// @dart=2.10 +import 'package:test/test.dart'; + +// Compile time error if the experiment is enabled +int x; + +void main() { + test('x is null', () { + expect(x, isNull); + }); +} +''').create(); + }); + + for (var platform in ['vm', 'chrome']) { + test('on the $platform platform', () async { + var test = await runTest(['test.dart', '-p', platform], + vmArgs: ['--enable-experiment=non-nullable']); + + await expectLater(test.stdout, emitsThrough(contains('int x;'))); + await test.shouldExit(1); + + // Test that they can be removed on subsequent runs as well + test = await runTest(['test.dart', '-p', platform]); + await expectLater( + test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }, skip: 'https://github.com/dart-lang/test/issues/1813'); + } + }); + }); + + group('runs tests after changing directories', () { + setUp(() async { + await d.file('a_test.dart', ''' +@TestOn('vm') +import 'dart:io'; + +import 'package:test/test.dart'; + +void main() { + test('changes directory', () { + Directory.current = Directory.current.parent; + }); +} +''').create(); + await d.file('b_test.dart', ''' +import 'package:test/test.dart'; + +void main() { + test('passes', () { + expect(true, true); + }); +} +''').create(); + }); + test('on the VM platform', () async { + var test = await runTest(['-p', 'vm', 'a_test.dart', 'b_test.dart']); + await expectLater( + test.stdout, emitsThrough(contains('+2: All tests passed!'))); + await test.shouldExit(0); + }); + + test('on the browser platform', () async { + var test = + await runTest(['-p', 'vm,chrome', 'a_test.dart', 'b_test.dart']); + await expectLater( + test.stdout, emitsThrough(contains('+3: All tests passed!'))); + await test.shouldExit(0); + }, skip: 'https://github.com/dart-lang/test/issues/1803'); + }); +}
diff --git a/pkgs/test/test/runner/set_up_all_test.dart b/pkgs/test/test/runner/set_up_all_test.dart new file mode 100644 index 0000000..d1259e1 --- /dev/null +++ b/pkgs/test/test/runner/set_up_all_test.dart
@@ -0,0 +1,98 @@ +// Copyright (c) 2015, 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 '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('an error causes the run to fail', () async { + await d.file('test.dart', r''' + import 'package:test/test.dart'; + + void main() { + setUpAll(() => throw "oh no"); + + test("test", () {}); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('-1: (setUpAll) [E]'))); + expect(test.stdout, emitsThrough(contains('-1: Some tests failed.'))); + await test.shouldExit(1); + }); + + test("doesn't run if no tests in the group are selected", () async { + await d.file('test.dart', r''' + import 'package:test/test.dart'; + + void main() { + group("with setUpAll", () { + setUpAll(() => throw "oh no"); + + test("test", () {}); + }); + + group("without setUpAll", () { + test("test", () {}); + }); + } + ''').create(); + + var test = await runTest(['test.dart', '--name', 'without']); + expect(test.stdout, neverEmits(contains('(setUpAll)'))); + await test.shouldExit(0); + }); + + test("doesn't run if no tests in the group match the platform", () async { + await d.file('test.dart', r''' + import 'package:test/test.dart'; + + void main() { + group("group1", () { + setUpAll(() => throw "oh no"); + + test("with", () {}, testOn: "browser"); + }); + + group("group2", () { + test("without", () {}); + }); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, neverEmits(contains('(setUpAll)'))); + await test.shouldExit(0); + }); + + test("doesn't run if the group doesn't match the platform", () async { + await d.file('test.dart', r''' + import 'package:test/test.dart'; + + void main() { + group("group1", () { + setUpAll(() => throw "oh no"); + + test("with", () {}); + }, testOn: "browser"); + + group("group2", () { + test("without", () {}); + }); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, neverEmits(contains('(setUpAll)'))); + await test.shouldExit(0); + }); +}
diff --git a/pkgs/test/test/runner/shard_test.dart b/pkgs/test/test/runner/shard_test.dart new file mode 100644 index 0000000..8d4be86 --- /dev/null +++ b/pkgs/test/test/runner/shard_test.dart
@@ -0,0 +1,183 @@ +// Copyright (c) 2016, 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_core/src/util/exit_codes.dart' as exit_codes; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('divides all the tests among the available shards', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test 1", () {}); + test("test 2", () {}); + test("test 3", () {}); + test("test 4", () {}); + test("test 5", () {}); + test("test 6", () {}); + test("test 7", () {}); + test("test 8", () {}); + test("test 9", () {}); + test("test 10", () {}); + } + ''').create(); + + var test = + await runTest(['test.dart', '--shard-index=0', '--total-shards=3']); + expect( + test.stdout, + containsInOrder([ + '+0: test 1', + '+1: test 2', + '+2: test 3', + '+3: All tests passed!' + ])); + await test.shouldExit(0); + + test = await runTest(['test.dart', '--shard-index=1', '--total-shards=3']); + expect( + test.stdout, + containsInOrder([ + '+0: test 4', + '+1: test 5', + '+2: test 6', + '+3: test 7', + '+4: All tests passed!' + ])); + await test.shouldExit(0); + + test = await runTest(['test.dart', '--shard-index=2', '--total-shards=3']); + expect( + test.stdout, + containsInOrder([ + '+0: test 8', + '+1: test 9', + '+2: test 10', + '+3: All tests passed!' + ])); + await test.shouldExit(0); + }); + + test('shards each suite', () async { + await d.file('1_test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test 1.1", () {}); + test("test 1.2", () {}); + test("test 1.3", () {}); + } + ''').create(); + + await d.file('2_test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test 2.1", () {}); + test("test 2.2", () {}); + test("test 2.3", () {}); + } + ''').create(); + + var test = await runTest(['.', '--shard-index=0', '--total-shards=3']); + expect( + test.stdout, + emitsInOrder([ + emitsAnyOf([ + containsInOrder( + ['+0: ./1_test.dart: test 1.1', '+1: ./2_test.dart: test 2.1']), + containsInOrder( + ['+0: ./2_test.dart: test 2.1', '+1: ./1_test.dart: test 1.1']) + ]), + contains('+2: All tests passed!') + ])); + await test.shouldExit(0); + + test = await runTest(['.', '--shard-index=1', '--total-shards=3']); + expect( + test.stdout, + emitsInOrder([ + emitsAnyOf([ + containsInOrder( + ['+0: ./1_test.dart: test 1.2', '+1: ./2_test.dart: test 2.2']), + containsInOrder( + ['+0: ./2_test.dart: test 2.2', '+1: ./1_test.dart: test 1.2']) + ]), + contains('+2: All tests passed!') + ])); + await test.shouldExit(0); + + test = await runTest(['.', '--shard-index=2', '--total-shards=3']); + expect( + test.stdout, + emitsInOrder([ + emitsAnyOf([ + containsInOrder( + ['+0: ./1_test.dart: test 1.3', '+1: ./2_test.dart: test 2.3']), + containsInOrder( + ['+0: ./2_test.dart: test 2.3', '+1: ./1_test.dart: test 1.3']) + ]), + contains('+2: All tests passed!') + ])); + await test.shouldExit(0); + }); + + test('an empty shard reports success', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("test 1", () {}); + test("test 2", () {}); + } + ''').create(); + + var test = + await runTest(['test.dart', '--shard-index=1', '--total-shards=3']); + expect(test.stdout, emitsThrough('No tests ran.')); + await test.shouldExit(79); + }); + + group('reports an error if', () { + test('--shard-index is provided alone', () async { + var test = await runTest(['--shard-index=1']); + expect( + test.stderr, + emits( + '--shard-index and --total-shards may only be passed together.')); + await test.shouldExit(exit_codes.usage); + }); + + test('--total-shards is provided alone', () async { + var test = await runTest(['--total-shards=5']); + expect( + test.stderr, + emits( + '--shard-index and --total-shards may only be passed together.')); + await test.shouldExit(exit_codes.usage); + }); + + test('--shard-index is negative', () async { + var test = await runTest(['--shard-index=-1', '--total-shards=5']); + expect(test.stderr, emits('--shard-index may not be negative.')); + await test.shouldExit(exit_codes.usage); + }); + + test('--shard-index is equal to --total-shards', () async { + var test = await runTest(['--shard-index=5', '--total-shards=5']); + expect(test.stderr, + emits('--shard-index must be less than --total-shards.')); + await test.shouldExit(exit_codes.usage); + }); + }); +}
diff --git a/pkgs/test/test/runner/signal_test.dart b/pkgs/test/test/runner/signal_test.dart new file mode 100644 index 0000000..d14ee97 --- /dev/null +++ b/pkgs/test/test/runner/signal_test.dart
@@ -0,0 +1,236 @@ +// Copyright (c) 2015, 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. + +// Windows doesn't support sending signals. +@TestOn('vm && !windows') +library; + +import 'dart:async'; +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 'package:test_process/test_process.dart'; + +import '../io.dart'; + +String get _tempDir => p.join(d.sandbox, 'tmp'); + +// This test is inherently prone to race conditions. If it fails, it will likely +// do so flakily, but if it succeeds, it will succeed consistently. The tests +// represent a best effort to kill the test runner at certain times during its +// execution. +void main() { + setUpAll(precompileTestExecutable); + + setUp(() => d.dir('tmp').create()); + + group('during loading,', () { + test('cleans up if killed while loading a VM test', () async { + await d.file('test.dart', ''' +void main() { + print("in test.dart"); + // Spin for a long time so the test is probably killed while still loading. + for (var i = 0; i < 100000000; i++) {} +} +''').create(); + + var test = await _runTest(['test.dart']); + await expectLater(test.stdout, emitsThrough('in test.dart')); + await signalAndQuit(test); + + expectTempDirEmpty(); + }); + + test('cleans up if killed while loading a browser test', () async { + await d.file('test.dart', 'void main() {}').create(); + + var test = await _runTest(['-p', 'chrome', 'test.dart']); + await expectLater( + test.stdout, emitsThrough(endsWith('loading test.dart'))); + await signalAndQuit(test); + + expectTempDirEmpty(skip: 'Failing on Travis.'); + }, tags: 'chrome'); + + test('exits immediately if ^C is sent twice', () async { + await d.file('test.dart', ''' +void main() { + print("in test.dart"); + while (true) {} +} +''').create(); + + var test = await _runTest(['test.dart']); + await expectLater(test.stdout, emitsThrough('in test.dart')); + test.signal(ProcessSignal.sigterm); + + // TODO(nweiz): Sending two signals in close succession can cause the + // second one to be ignored, so we wait a bit before the second + // one. Remove this hack when issue 23047 is fixed. + await Future<void>.delayed(const Duration(seconds: 1)); + + await signalAndQuit(test); + }); + }); + + group('during test running', () { + test('waits for a VM test to finish running', () async { + await d.file('test.dart', ''' +import 'dart:async'; +import 'dart:io'; + +import 'package:test/test.dart'; + +void main() { + tearDownAll(() { + File("output_all").writeAsStringSync("ran tearDownAll"); + }); + + tearDown(() => File("output").writeAsStringSync("ran tearDown")); + + test("test", () { + print("running test"); + return Future.delayed(Duration(seconds: 1)); + }); +} +''').create(); + + var test = await _runTest(['test.dart']); + await expectLater(test.stdout, emitsThrough('running test')); + await signalAndQuit(test); + + await d.file('output', 'ran tearDown').validate(); + await d.file('output_all', 'ran tearDownAll').validate(); + expectTempDirEmpty(); + }); + + test('waits for an active tearDownAll to finish running', () async { + await d.file('test.dart', ''' +import 'dart:async'; +import 'dart:io'; + +import 'package:test/test.dart'; + +void main() { + tearDownAll(() async { + print("running tearDownAll"); + await Future.delayed(Duration(seconds: 1)); + File("output").writeAsStringSync("ran tearDownAll"); + }); + + test("test", () {}); +} +''').create(); + + var test = await _runTest(['test.dart']); + await expectLater(test.stdout, emitsThrough('running tearDownAll')); + await signalAndQuit(test); + + await d.file('output', 'ran tearDownAll').validate(); + expectTempDirEmpty(); + }); + + test('kills a browser test immediately', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("test", () { + print("running test"); + + // Allow an event loop to pass so the preceding print can be handled. + return Future(() { + // Loop forever so that if the test isn't stopped while running, it never + // stops. + while (true) {} + }); + }); +} +''').create(); + + var test = await _runTest(['-p', 'chrome', 'test.dart']); + await expectLater(test.stdout, emitsThrough('running test')); + await signalAndQuit(test); + + expectTempDirEmpty(skip: 'Failing on Travis.'); + }, tags: 'chrome'); + + test('kills a VM test immediately if ^C is sent twice', () async { + await d.file('test.dart', ''' +import 'package:test/test.dart'; + +void main() { + test("test", () { + print("running test"); + while (true) {} + }); +} +''').create(); + + var test = await _runTest(['test.dart']); + await expectLater(test.stdout, emitsThrough('running test')); + test.signal(ProcessSignal.sigterm); + + // TODO(nweiz): Sending two signals in close succession can cause the + // second one to be ignored, so we wait a bit before the second + // one. Remove this hack when issue 23047 is fixed. + await Future<void>.delayed(const Duration(seconds: 1)); + await signalAndQuit(test); + }); + + test('causes expectAsync() to always throw an error immediately', () async { + await d.file('test.dart', ''' +import 'dart:async'; +import 'dart:io'; + +import 'package:test/test.dart'; + +void main() { + var expectAsyncThrewError = false; + + tearDown(() { + File("output").writeAsStringSync(expectAsyncThrewError.toString()); + }); + + test("test", () async { + print("running test"); + + await Future.delayed(Duration(seconds: 1)); + try { + expectAsync0(() {}); + } catch (_) { + expectAsyncThrewError = true; + } + }); +} +''').create(); + + var test = await _runTest(['test.dart']); + await expectLater(test.stdout, emitsThrough('running test')); + await signalAndQuit(test); + + await d.file('output', 'true').validate(); + expectTempDirEmpty(); + }); + }); +} + +Future<TestProcess> _runTest(List<String> args, {bool forwardStdio = false}) => + runTest(args, + environment: {'_UNITTEST_TEMP_DIR': _tempDir}, + forwardStdio: forwardStdio); + +Future<void> signalAndQuit(TestProcess test) async { + test.signal(ProcessSignal.sigterm); + await test.shouldExit(); + await expectLater(test.stderr, emitsDone); +} + +void expectTempDirEmpty({Object? skip}) { + expect(Directory(_tempDir).listSync(), isEmpty, skip: skip); +}
diff --git a/pkgs/test/test/runner/skip_expect_test.dart b/pkgs/test/test/runner/skip_expect_test.dart new file mode 100644 index 0000000..e22332c --- /dev/null +++ b/pkgs/test/test/runner/skip_expect_test.dart
@@ -0,0 +1,270 @@ +// Copyright (c) 2016, 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 '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + group('a skipped expect', () { + test('marks the test as skipped', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("skipped", () => expect(1, equals(2), skip: true)); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('~1: All tests skipped.'))); + await test.shouldExit(0); + }); + + test('prints the skip reason if there is one', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("skipped", () => expect(1, equals(2), + reason: "1 is 2", skip: "is failing")); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder([ + '+0: skipped', + ' Skip expect: is failing', + '~1: All tests skipped.' + ])); + await test.shouldExit(0); + }); + + test("prints the expect reason if there's no skip reason", () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("skipped", () => expect(1, equals(2), + reason: "1 is 2", skip: true)); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder([ + '+0: skipped', + ' Skip expect (1 is 2).', + '~1: All tests skipped.' + ])); + await test.shouldExit(0); + }); + + test('prints the matcher description if there are no reasons', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("skipped", () => expect(1, equals(2), skip: true)); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder([ + '+0: skipped', + ' Skip expect (<2>).', + '~1: All tests skipped.' + ])); + await test.shouldExit(0); + }); + + test('still allows the test to fail', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("failing", () { + expect(1, equals(2), skip: true); + expect(1, equals(2)); + }); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder([ + '+0: failing', + ' Skip expect (<2>).', + '+0 -1: failing [E]', + ' Expected: <2>', + ' Actual: <1>', + '+0 -1: Some tests failed.' + ])); + await test.shouldExit(1); + }); + }); + + group('markTestSkipped', () { + test('prints the skip reason', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test('skipped', () { + markTestSkipped('some reason'); + }); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder([ + '+0: skipped', + ' some reason', + '~1: All tests skipped.', + ])); + await test.shouldExit(0); + }); + + test('still allows the test to fail', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test('failing', () { + markTestSkipped('some reason'); + expect(1, equals(2)); + }); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder([ + '+0: failing', + ' some reason', + '+0 -1: failing [E]', + ' Expected: <2>', + ' Actual: <1>', + '+0 -1: Some tests failed.' + ])); + await test.shouldExit(1); + }); + + test('error when called after the test succeeded', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + var skipCompleter = Completer(); + var waitCompleter = Completer(); + test('skip', () { + skipCompleter.future.then((_) { + waitCompleter.complete(); + markTestSkipped('some reason'); + }); + }); + + // Trigger the skip completer in a following test to ensure that it + // only fires after skip has completed successfully. + test('wait', () async { + skipCompleter.complete(); + await waitCompleter.future; + }); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder([ + '+0: skip', + '+1: wait', + '+0 -1: skip', + 'This test was marked as skipped after it had already completed.', + 'Make sure to use a matching library which informs the test runner', + 'of pending async work.', + '+1 -1: Some tests failed.' + ])); + await test.shouldExit(1); + }); + }); + + group('errors', () { + test('when called after the test succeeded', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + var skipCompleter = Completer(); + var waitCompleter = Completer(); + test("skip", () { + skipCompleter.future.then((_) { + waitCompleter.complete(); + expect(1, equals(2), skip: true); + }); + }); + + // Trigger the skip completer in a following test to ensure that it + // only fires after skip has completed successfully. + test("wait", () async { + skipCompleter.complete(); + await waitCompleter.future; + }); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder([ + '+0: skip', + '+1: wait', + '+0 -1: skip', + 'This test was marked as skipped after it had already completed.', + 'Make sure to use a matching library which informs the test runner', + 'of pending async work.', + '+1 -1: Some tests failed.' + ])); + await test.shouldExit(1); + }); + + test('when an invalid type is used for skip', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("failing", () { + expect(1, equals(2), skip: 10); + }); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder( + ['Invalid argument (skip)', '+0 -1: Some tests failed.'])); + await test.shouldExit(1); + }); + }); +}
diff --git a/pkgs/test/test/runner/solo_test.dart b/pkgs/test/test/runner/solo_test.dart new file mode 100644 index 0000000..87f6f25 --- /dev/null +++ b/pkgs/test/test/runner/solo_test.dart
@@ -0,0 +1,67 @@ +// 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 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('only runs the tests marked as solo', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("passes", () { + expect(true, isTrue); + }, solo: true); + test("failed", () { + throw 'some error'; + }); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+1 ~1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('only runs groups marked as solo', () async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + group('solo', () { + test("first pass", () { + expect(true, isTrue); + }); + test("second pass", () { + expect(true, isTrue); + }); + }, solo: true); + group('no solo', () { + test("failure", () { + throw 'some error'; + }); + test("another failure", () { + throw 'some error'; + }); + }); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('+2 ~1: All tests passed!'))); + await test.shouldExit(0); + }); +}
diff --git a/pkgs/test/test/runner/tag_test.dart b/pkgs/test/test/runner/tag_test.dart new file mode 100644 index 0000000..da9dca7 --- /dev/null +++ b/pkgs/test/test/runner/tag_test.dart
@@ -0,0 +1,370 @@ +// Copyright (c) 2015, 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 '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + setUp(() async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("no tags", () {}); + test("a", () {}, tags: "a"); + test("b", () {}, tags: "b"); + test("bc", () {}, tags: ["b", "c"]); + } + ''').create(); + }); + + group('--tags', () { + test('runs all tests when no tags are specified', () async { + var test = await runTest(['test.dart']); + expect(test.stdout, tagWarnings(['a', 'b', 'c'])); + expect(test.stdout, emitsThrough(contains(': no tags'))); + expect(test.stdout, emitsThrough(contains(': a'))); + expect(test.stdout, emitsThrough(contains(': b'))); + expect(test.stdout, emitsThrough(contains(': bc'))); + expect(test.stdout, emitsThrough(contains('+4: All tests passed!'))); + await test.shouldExit(0); + }); + + test('runs a test with only a specified tag', () async { + var test = await runTest(['--tags=a', 'test.dart']); + expect(test.stdout, tagWarnings(['b', 'c'])); + expect(test.stdout, emitsThrough(contains(': a'))); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('runs a test with a specified tag among others', () async { + var test = await runTest(['--tags=c', 'test.dart']); + expect(test.stdout, tagWarnings(['a', 'b'])); + expect(test.stdout, emitsThrough(contains(': bc'))); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('with multiple tags, runs only tests matching all of them', () async { + var test = await runTest(['--tags=b,c', 'test.dart']); + expect(test.stdout, tagWarnings(['a'])); + expect(test.stdout, emitsThrough(contains(': bc'))); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('supports boolean selector syntax', () async { + var test = await runTest(['--tags=b || c', 'test.dart']); + expect(test.stdout, tagWarnings(['a'])); + expect(test.stdout, emitsThrough(contains(': b'))); + expect(test.stdout, emitsThrough(contains(': bc'))); + expect(test.stdout, emitsThrough(contains('+2: All tests passed!'))); + await test.shouldExit(0); + }); + + test('prints no warnings when all tags are specified', () async { + var test = await runTest(['--tags=a,b,c', 'test.dart']); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }); + }); + + group('--exclude-tags', () { + test("dosn't run a test with only an excluded tag", () async { + var test = await runTest(['--exclude-tags=a', 'test.dart']); + expect(test.stdout, tagWarnings(['b', 'c'])); + expect(test.stdout, emitsThrough(contains(': no tags'))); + expect(test.stdout, emitsThrough(contains(': b'))); + expect(test.stdout, emitsThrough(contains(': bc'))); + expect(test.stdout, emitsThrough(contains('+3: All tests passed!'))); + await test.shouldExit(0); + }); + + test("doesn't run a test with an excluded tag among others", () async { + var test = await runTest(['--exclude-tags=c', 'test.dart']); + expect(test.stdout, tagWarnings(['a', 'b'])); + expect(test.stdout, emitsThrough(contains(': no tags'))); + expect(test.stdout, emitsThrough(contains(': a'))); + expect(test.stdout, emitsThrough(contains(': b'))); + expect(test.stdout, emitsThrough(contains('+3: All tests passed!'))); + await test.shouldExit(0); + }); + + test("dosn't load a suite with an excluded tag", () async { + await d.file('test.dart', ''' + @Tags(const ["a"]) + + import 'package:test/test.dart'; + + void main() { + throw "error"; + } + ''').create(); + + var test = await runTest(['--exclude-tags=a', 'test.dart']); + expect(test.stdout, emits('No tests ran.')); + await test.shouldExit(79); + }); + + test('allows unused tags', () async { + var test = await runTest(['--exclude-tags=b,z', 'test.dart']); + expect(test.stdout, tagWarnings(['a', 'c'])); + expect(test.stdout, emitsThrough(contains(': no tags'))); + expect(test.stdout, emitsThrough(contains(': a'))); + expect(test.stdout, emitsThrough(contains('+2: All tests passed!'))); + await test.shouldExit(0); + }); + + test('supports boolean selector syntax', () async { + var test = await runTest(['--exclude-tags=b && c', 'test.dart']); + expect(test.stdout, tagWarnings(['a'])); + expect(test.stdout, emitsThrough(contains(': no tags'))); + expect(test.stdout, emitsThrough(contains(': a'))); + expect(test.stdout, emitsThrough(contains(': b'))); + expect(test.stdout, emitsThrough(contains('+3: All tests passed!'))); + await test.shouldExit(0); + }); + + test('prints no warnings when all tags are specified', () async { + var test = await runTest(['--exclude-tags=a,b,c', 'test.dart']); + expect(test.stdout, emitsThrough(contains(': no tags'))); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + }); + + group('with a tagged group', () { + setUp(() async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + group("a", () { + test("in", () {}); + }, tags: "a"); + + test("out", () {}); + } + ''').create(); + }); + + test('includes tags specified on the group', () async { + var test = await runTest(['-x', 'a', 'test.dart']); + expect(test.stdout, emitsThrough(contains(': out'))); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + + test('excludes tags specified on the group', () async { + var test = await runTest(['-t', 'a', 'test.dart']); + expect(test.stdout, emitsThrough(contains(': a in'))); + expect(test.stdout, emitsThrough(contains('+1: All tests passed!'))); + await test.shouldExit(0); + }); + }); + + test('respects top-level @Tags annotations', () async { + await d.file('test.dart', ''' + @Tags(const ['a']) + import 'package:test/test.dart'; + + void main() { + test("foo", () {}); + } + ''').create(); + + var test = await runTest(['-x', 'a', 'test.dart']); + expect(test.stdout, emitsThrough(contains('No tests ran'))); + await test.shouldExit(79); + }); + + group('warning formatting', () { + test('for multiple tags', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("foo", () {}, tags: ["a", "b"]); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + emitsThrough(lines( + 'Warning: Tags were used that weren\'t specified in dart_test.yaml.\n' + ' a was used in the test "foo"\n' + ' b was used in the test "foo"'))); + await test.shouldExit(0); + }); + + test('for multiple tests', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("foo", () {}, tags: "a"); + test("bar", () {}, tags: "a"); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + emitsThrough(lines( + 'Warning: A tag was used that wasn\'t specified in dart_test.yaml.\n' + ' a was used in:\n' + ' the test "foo"\n' + ' the test "bar"'))); + await test.shouldExit(0); + }); + + test('for groups', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + group("group", () { + test("foo", () {}); + test("bar", () {}); + }, tags: "a"); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + emitsThrough(lines( + 'Warning: A tag was used that wasn\'t specified in dart_test.yaml.\n' + ' a was used in the group "group"'))); + await test.shouldExit(0); + }); + + test('for suites', () async { + await d.file('test.dart', ''' + @Tags(const ["a"]) + import 'package:test/test.dart'; + + void main() { + test("foo", () {}); + test("bar", () {}); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + emitsThrough(lines( + 'Warning: A tag was used that wasn\'t specified in dart_test.yaml.\n' + ' a was used in the suite itself'))); + await test.shouldExit(0); + }); + + test("doesn't double-print a tag warning", () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("foo", () {}, tags: "a"); + } + ''').create(); + + var test = await runTest(['-p', 'vm,chrome', 'test.dart']); + expect( + test.stdout, + emitsThrough(lines( + 'Warning: A tag was used that wasn\'t specified in dart_test.yaml.\n' + ' a was used in the test "foo"'))); + expect(test.stdout, neverEmits(startsWith('Warning:'))); + await test.shouldExit(0); + }, tags: 'chrome'); + }); + + group('invalid tags', () { + test('are disallowed by test()', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + test("foo", () {}, tags: "a b"); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + emitsThrough( + ' Failed to load "test.dart": Invalid argument(s): Invalid tag "a ' + 'b". Tags must be (optionally hyphenated) Dart identifiers.')); + await test.shouldExit(1); + }); + + test('are disallowed by group()', () async { + await d.file('test.dart', ''' + import 'package:test/test.dart'; + + void main() { + group("group", () { + test("foo", () {}); + }, tags: "a b"); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + emitsThrough( + ' Failed to load "test.dart": Invalid argument(s): Invalid tag "a ' + 'b". Tags must be (optionally hyphenated) Dart identifiers.')); + await test.shouldExit(1); + }); + + test('are disallowed by @Tags()', () async { + await d.file('test.dart', ''' + @Tags(const ["a b"]) + + import 'package:test/test.dart'; + + void main() { + test("foo", () {}); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + emitsThrough(lines(' Failed to load "test.dart":\n' + ' Error on line 1, column 22: Invalid tag name. Tags must be ' + '(optionally hyphenated) Dart identifiers.'))); + await test.shouldExit(1); + }); + }); +} + +/// Returns a [StreamMatcher] that asserts that a test emits warnings for [tags] +/// in order. +StreamMatcher tagWarnings(List<String> tags) => emitsInOrder([ + emitsThrough( + "Warning: ${tags.length == 1 ? 'A tag was' : 'Tags were'} used that " + "${tags.length == 1 ? "wasn't" : "weren't"} specified in " + 'dart_test.yaml.'), + + for (var tag in tags) emitsThrough(startsWith(' $tag was used in')), + + // Consume until the end of the warning block, and assert that it has no + // further tags than the ones we specified. + mayEmitMultiple(isNot(anyOf([contains(' was used in'), isEmpty]))), + isEmpty, + ]); + +/// Returns a [StreamMatcher] that matches the lines of [string] in order. +StreamMatcher lines(String string) => emitsInOrder(string.split('\n'));
diff --git a/pkgs/test/test/runner/tear_down_all_test.dart b/pkgs/test/test/runner/tear_down_all_test.dart new file mode 100644 index 0000000..93ede4f --- /dev/null +++ b/pkgs/test/test/runner/tear_down_all_test.dart
@@ -0,0 +1,98 @@ +// Copyright (c) 2015, 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 '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('an error causes the run to fail', () async { + await d.file('test.dart', r''' + import 'package:test/test.dart'; + + void main() { + tearDownAll(() => throw "oh no"); + + test("test", () {}); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, emitsThrough(contains('-1: (tearDownAll) [E]'))); + expect(test.stdout, emitsThrough(contains('-1: Some tests failed.'))); + await test.shouldExit(1); + }); + + test("doesn't run if no tests in the group are selected", () async { + await d.file('test.dart', r''' + import 'package:test/test.dart'; + + void main() { + group("with tearDownAll", () { + tearDownAll(() => throw "oh no"); + + test("test", () {}); + }); + + group("without tearDownAll", () { + test("test", () {}); + }); + } + ''').create(); + + var test = await runTest(['test.dart', '--name', 'without']); + expect(test.stdout, neverEmits(contains('(tearDownAll)'))); + await test.shouldExit(0); + }); + + test("doesn't run if no tests in the group match the platform", () async { + await d.file('test.dart', r''' + import 'package:test/test.dart'; + + void main() { + group("group1", () { + tearDownAll(() => throw "oh no"); + + test("with", () {}, testOn: "browser"); + }); + + group("group2", () { + test("without", () {}); + }); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, neverEmits(contains('(tearDownAll)'))); + await test.shouldExit(0); + }); + + test("doesn't run if the group doesn't match the platform", () async { + await d.file('test.dart', r''' + import 'package:test/test.dart'; + + void main() { + group("group1", () { + tearDownAll(() => throw "oh no"); + + test("with", () {}); + }, testOn: "browser"); + + group("group2", () { + test("without", () {}); + }); + } + ''').create(); + + var test = await runTest(['test.dart']); + expect(test.stdout, neverEmits(contains('(tearDownAll)'))); + await test.shouldExit(0); + }); +}
diff --git a/pkgs/test/test/runner/test_chain_test.dart b/pkgs/test/test/runner/test_chain_test.dart new file mode 100644 index 0000000..ad814de --- /dev/null +++ b/pkgs/test/test/runner/test_chain_test.dart
@@ -0,0 +1,84 @@ +// 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:convert'; + +import 'package:test/test.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + setUp(() async { + await d.file('test.dart', ''' + import 'dart:async'; + + import 'package:test/test.dart'; + + void main() { + test("failure", () async{ + await Future((){}); + await Future((){}); + throw "oh no"; + }); + } + ''').create(); + }); + test('folds packages contained in the except list', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'fold_stack_frames': { + 'except': ['stream_channel'] + } + })) + .create(); + var test = await runTest(['test.dart']); + expect(test.stdoutStream(), neverEmits(contains('package:stream_channel'))); + await test.shouldExit(1); + }); + + test('by default folds both stream_channel and test packages', () async { + var test = await runTest(['test.dart']); + expect(test.stdoutStream(), neverEmits(contains('package:test'))); + expect(test.stdoutStream(), neverEmits(contains('package:stream_channel'))); + await test.shouldExit(1); + }); + + test('folds all packages not contained in the only list', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'fold_stack_frames': { + 'only': ['test'] + } + })) + .create(); + var test = await runTest(['test.dart']); + expect(test.stdoutStream(), neverEmits(contains('package:stream_channel'))); + await test.shouldExit(1); + }); + + test('does not fold packages in the only list', () async { + await d + .file( + 'dart_test.yaml', + jsonEncode({ + 'fold_stack_frames': { + 'only': ['test_api'] + } + })) + .create(); + var test = await runTest(['test.dart']); + expect(test.stdoutStream(), emitsThrough(contains('package:test_api'))); + await test.shouldExit(1); + }); +}
diff --git a/pkgs/test/test/runner/test_on_test.dart b/pkgs/test/test/runner/test_on_test.dart new file mode 100644 index 0000000..96baf0b --- /dev/null +++ b/pkgs/test/test/runner/test_on_test.dart
@@ -0,0 +1,226 @@ +// Copyright (c) 2015, 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:isolate'; + +import 'package:package_config/package_config.dart'; +import 'package:test/test.dart'; +import 'package:test_core/src/util/io.dart'; +import 'package:test_descriptor/test_descriptor.dart' as d; + +import '../io.dart'; + +void main() { + late PackageConfig currentPackageConfig; + + setUpAll(() async { + await precompileTestExecutable(); + currentPackageConfig = + await loadPackageConfigUri((await Isolate.packageConfig)!); + }); + + setUp(() async { + await d + .file('package_config.json', + jsonEncode(PackageConfig.toJson(currentPackageConfig))) + .create(); + }); + + group('for suite', () { + test('runs a test suite on a matching platform', () async { + await _writeTestFile('vm_test.dart', suiteTestOn: 'vm'); + + var test = await runTest(['vm_test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }); + + test("doesn't run a test suite on a non-matching platform", () async { + await _writeTestFile('vm_test.dart', suiteTestOn: 'vm'); + + var test = await runTest(['--platform', 'chrome', 'vm_test.dart']); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }, tags: 'chrome'); + + test('runs a test suite on a matching operating system', () async { + await _writeTestFile('os_test.dart', suiteTestOn: currentOS.identifier); + + var test = await runTest(['os_test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }); + + test("doesn't run a test suite on a non-matching operating system", + () async { + await _writeTestFile('os_test.dart', + suiteTestOn: otherOS, loadable: false); + + var test = await runTest(['os_test.dart']); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }); + + test('only loads matching files when loading as a group', () async { + await _writeTestFile('vm_test.dart', suiteTestOn: 'vm'); + await _writeTestFile('browser_test.dart', + suiteTestOn: 'browser', loadable: false); + await _writeTestFile('this_os_test.dart', + suiteTestOn: currentOS.identifier); + await _writeTestFile('other_os_test.dart', + suiteTestOn: otherOS, loadable: false); + + var test = await runTest(['.']); + expect(test.stdout, emitsThrough(contains('+2: All tests passed!'))); + await test.shouldExit(0); + }); + }); + + group('for group', () { + test('runs a VM group on the VM', () async { + await _writeTestFile('vm_test.dart', groupTestOn: 'vm'); + + var test = await runTest(['vm_test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }); + + test("doesn't run a Browser group on the VM", () async { + await _writeTestFile('browser_test.dart', groupTestOn: 'browser'); + + var test = await runTest(['browser_test.dart']); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }); + + test('runs a browser group on a browser', () async { + await _writeTestFile('browser_test.dart', groupTestOn: 'browser'); + + var test = await runTest(['--platform', 'chrome', 'browser_test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + test("doesn't run a VM group on a browser", () async { + await _writeTestFile('vm_test.dart', groupTestOn: 'vm'); + + var test = await runTest(['--platform', 'chrome', 'vm_test.dart']); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }, tags: 'chrome'); + }); + + group('for test', () { + test('runs a VM test on the VM', () async { + await _writeTestFile('vm_test.dart', testTestOn: 'vm'); + + var test = await runTest(['vm_test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }); + + test("doesn't run a browser test on the VM", () async { + await _writeTestFile('browser_test.dart', testTestOn: 'browser'); + + var test = await runTest(['browser_test.dart']); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }); + + test('runs a browser test on a browser', () async { + await _writeTestFile('browser_test.dart', testTestOn: 'browser'); + + var test = await runTest(['--platform', 'chrome', 'browser_test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }, tags: 'chrome'); + + test("doesn't run a VM test on a browser", () async { + await _writeTestFile('vm_test.dart', testTestOn: 'vm'); + + var test = await runTest(['--platform', 'chrome', 'vm_test.dart']); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }, tags: 'chrome'); + }); + + group('with suite, group, and test selectors', () { + test('runs the test if all selectors match', () async { + await _writeTestFile('vm_test.dart', + suiteTestOn: '!browser', groupTestOn: '!js', testTestOn: 'vm'); + + var test = await runTest(['vm_test.dart']); + expect(test.stdout, emitsThrough(contains('All tests passed!'))); + await test.shouldExit(0); + }); + + test("doesn't runs the test if the suite doesn't match", () async { + await _writeTestFile('vm_test.dart', + suiteTestOn: 'browser', groupTestOn: '!js', testTestOn: 'vm'); + + var test = await runTest(['vm_test.dart']); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }); + + test("doesn't runs the test if the group doesn't match", () async { + await _writeTestFile('vm_test.dart', + suiteTestOn: '!browser', groupTestOn: 'browser', testTestOn: 'vm'); + + var test = await runTest(['vm_test.dart']); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }); + + test("doesn't runs the test if the test doesn't match", () async { + await _writeTestFile('vm_test.dart', + suiteTestOn: '!browser', groupTestOn: '!js', testTestOn: 'browser'); + + var test = await runTest(['vm_test.dart']); + expect(test.stdout, emitsThrough(contains('No tests ran.'))); + await test.shouldExit(79); + }); + }); +} + +/// Writes a test file with some platform selectors to [filename]. +/// +/// Each of [suiteTestOn], [groupTestOn], and [testTestOn] is a platform +/// selector that's suite-, group-, and test-level respectively. If [loadable] +/// is `false`, the test file will be made unloadable on the Dart VM. +Future<void> _writeTestFile(String filename, + {String? suiteTestOn, + String? groupTestOn, + String? testTestOn, + bool loadable = true}) { + var buffer = StringBuffer(); + if (suiteTestOn != null) buffer.writeln("@TestOn('$suiteTestOn')"); + if (!loadable) buffer.writeln("import 'dart:js_util';"); + + buffer + ..writeln("import 'package:test/test.dart';") + ..writeln('void main() {') + ..writeln(" group('group', () {"); + + if (testTestOn != null) { + buffer.writeln(" test('test', () {}, testOn: '$testTestOn');"); + } else { + buffer.writeln(" test('test', () {});"); + } + + if (groupTestOn != null) { + buffer.writeln(" }, testOn: '$groupTestOn');"); + } else { + buffer.writeln(' });'); + } + + buffer.writeln('}'); + + return d.file(filename, buffer.toString()).create(); +}
diff --git a/pkgs/test/test/runner/timeout_test.dart b/pkgs/test/test/runner/timeout_test.dart new file mode 100644 index 0000000..6f4b0e7 --- /dev/null +++ b/pkgs/test/test/runner/timeout_test.dart
@@ -0,0 +1,185 @@ +// Copyright (c) 2016, 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 '../io.dart'; + +void main() { + setUpAll(precompileTestExecutable); + + test('respects top-level @Timeout declarations', () async { + await d.file('test.dart', ''' +@Timeout(const Duration(seconds: 0)) + +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("timeout", () async { + await Future.delayed(Duration.zero); + }); +} +''').create(); + + var test = await runTest(['test.dart']); + expect( + test.stdout, + containsInOrder( + ['Test timed out after 0 seconds.', '-1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('respects the --timeout flag', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("timeout", () async { + await Future.delayed(Duration.zero); + }); +} +''').create(); + + var test = await runTest(['--timeout=0s', 'test.dart']); + expect( + test.stdout, + containsInOrder( + ['Test timed out after 0 seconds.', '-1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('timeout is reset with each retry', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + var runCount = 0; + test("timeout", () async { + runCount++; + if (runCount <=2) { + await Future.delayed(Duration(milliseconds: 1000)); + } + }, retry: 3); +} +''').create(); + + var test = await runTest(['--timeout=400ms', 'test.dart']); + expect( + test.stdout, + containsInOrder([ + 'Test timed out after 0.4 seconds.', + 'Test timed out after 0.4 seconds.', + '+1: All tests passed!' + ])); + await test.shouldExit(0); + }); + + test('the --timeout flag applies on top of the default 30s timeout', + () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("no timeout", () async { + await Future.delayed(Duration(milliseconds: 250)); + }); + + test("timeout", () async { + await Future.delayed(Duration(milliseconds: 750)); + }); +} +''').create(); + + // This should make the timeout about 500ms, which should cause exactly one + // test to fail. + var test = await runTest(['--timeout=0.016x', 'test.dart']); + expect( + test.stdout, + containsInOrder( + ['Test timed out after 0.4 seconds.', '-1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('times out teardown callbacks', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + tearDown(() async { + await Completer<void>().future; + }); + + test('timeout in teardown', () async { + // nothing + }); +} +''').create(); + + var test = await runTest(['--timeout=50ms', 'test.dart']); + expect( + test.stdout, + containsInOrder( + ['Test timed out after 0 seconds.', '-1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('times out after failing test', () async { + await d.file('test.dart', ''' +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + tearDown(() async { + await Completer<void>().future; + }); + + test('timeout in teardown', () async { + expect(true, false); + }); +} +''').create(); + + var test = await runTest(['--timeout=50ms', 'test.dart']); + expect( + test.stdout, + containsInOrder( + ['Test timed out after 0 seconds.', '-1: Some tests failed.'])); + await test.shouldExit(1); + }); + + test('are ignored with --ignore-timeouts', () async { + await d.file('test.dart', ''' +@Timeout(const Duration(seconds: 0)) + +import 'dart:async'; + +import 'package:test/test.dart'; + +void main() { + test("timeout", () async { + await Future.delayed(Duration(milliseconds: 10)); + }); +} +''').create(); + + var test = await runTest(['test.dart', '--ignore-timeouts']); + expect(test.stdout, containsInOrder(['+1: All tests passed!'])); + await test.shouldExit(0); + }); +}
diff --git a/pkgs/test/test/util/one_off_handler_test.dart b/pkgs/test/test/util/one_off_handler_test.dart new file mode 100644 index 0000000..c10a7af --- /dev/null +++ b/pkgs/test/test/util/one_off_handler_test.dart
@@ -0,0 +1,74 @@ +// Copyright (c) 2015, 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 'package:shelf/shelf.dart' as shelf; +import 'package:test/src/util/one_off_handler.dart'; +import 'package:test/test.dart'; + +void main() { + late OneOffHandler handler; + setUp(() => handler = OneOffHandler()); + + Future<shelf.Response> handle(shelf.Request request) => + Future.sync(() => handler.handler(request)); + + test('returns a 404 for a root URL', () async { + var request = shelf.Request('GET', Uri.parse('http://localhost/')); + expect((await handle(request)).statusCode, equals(404)); + }); + + test('returns a 404 for an unhandled URL', () async { + var request = shelf.Request('GET', Uri.parse('http://localhost/1')); + expect((await handle(request)).statusCode, equals(404)); + }); + + test('passes a request to a handler only once', () async { + var path = handler.create(expectAsync1((request) { + expect(request.method, equals('GET')); + return shelf.Response.ok('good job!'); + })); + + var request = shelf.Request('GET', Uri.parse('http://localhost/$path')); + var response = await handle(request); + expect(response.statusCode, equals(200)); + expect(response.readAsString(), completion(equals('good job!'))); + + request = shelf.Request('GET', Uri.parse('http://localhost/$path')); + expect((await handle(request)).statusCode, equals(404)); + }); + + test('passes requests to the correct handlers', () async { + var path1 = handler.create(expectAsync1((request) { + expect(request.method, equals('GET')); + return shelf.Response.ok('one'); + })); + + var path2 = handler.create(expectAsync1((request) { + expect(request.method, equals('GET')); + return shelf.Response.ok('two'); + })); + + var path3 = handler.create(expectAsync1((request) { + expect(request.method, equals('GET')); + return shelf.Response.ok('three'); + })); + + var request = shelf.Request('GET', Uri.parse('http://localhost/$path2')); + var response = await handle(request); + expect(response.statusCode, equals(200)); + expect(response.readAsString(), completion(equals('two'))); + + request = shelf.Request('GET', Uri.parse('http://localhost/$path1')); + response = await handle(request); + expect(response.statusCode, equals(200)); + expect(response.readAsString(), completion(equals('one'))); + + request = shelf.Request('GET', Uri.parse('http://localhost/$path3')); + response = await handle(request); + expect(response.statusCode, equals(200)); + expect(response.readAsString(), completion(equals('three'))); + }); +}
diff --git a/pkgs/test/test/util/path_handler_test.dart b/pkgs/test/test/util/path_handler_test.dart new file mode 100644 index 0000000..9a5d98b --- /dev/null +++ b/pkgs/test/test/util/path_handler_test.dart
@@ -0,0 +1,78 @@ +// Copyright (c) 2015, 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 'package:shelf/shelf.dart' as shelf; +import 'package:test/src/util/path_handler.dart'; +import 'package:test/test.dart'; + +void main() { + late PathHandler handler; + setUp(() => handler = PathHandler()); + + Future<shelf.Response> localHandler(shelf.Request request) => + Future.sync(() => handler.handler(request)); + + test('returns a 404 for a root URL', () async { + var request = shelf.Request('GET', Uri.parse('http://localhost/')); + expect((await localHandler(request)).statusCode, equals(404)); + }); + + test('returns a 404 for an unregistered URL', () async { + var request = shelf.Request('GET', Uri.parse('http://localhost/foo')); + expect((await localHandler(request)).statusCode, equals(404)); + }); + + test('runs a handler for an exact URL', () async { + var request = shelf.Request('GET', Uri.parse('http://localhost/foo')); + handler.add('foo', expectAsync1((request) { + expect(request.handlerPath, equals('/foo')); + expect(request.url.path, isEmpty); + return shelf.Response.ok('good job!'); + })); + + var response = await localHandler(request); + expect(response.statusCode, equals(200)); + expect(response.readAsString(), completion(equals('good job!'))); + }); + + test('runs a handler for a suffix', () async { + var request = shelf.Request('GET', Uri.parse('http://localhost/foo/bar')); + handler.add('foo', expectAsync1((request) { + expect(request.handlerPath, equals('/foo/')); + expect(request.url.path, 'bar'); + return shelf.Response.ok('good job!'); + })); + + var response = await localHandler(request); + expect(response.statusCode, equals(200)); + expect(response.readAsString(), completion(equals('good job!'))); + }); + + test('runs the longest matching handler', () async { + var request = + shelf.Request('GET', Uri.parse('http://localhost/foo/bar/baz')); + + handler.add( + 'foo', + expectAsync1((_) { + return shelf.Response.notFound('fake'); + }, count: 0)); + handler.add('foo/bar', expectAsync1((request) { + expect(request.handlerPath, equals('/foo/bar/')); + expect(request.url.path, 'baz'); + return shelf.Response.ok('good job!'); + })); + handler.add( + 'foo/bar/baz/bang', + expectAsync1((_) { + return shelf.Response.notFound('fake'); + }, count: 0)); + + var response = await localHandler(request); + expect(response.statusCode, equals(200)); + expect(response.readAsString(), completion(equals('good job!'))); + }); +}
diff --git a/pkgs/test/test/util/string_literal_iterator_test.dart b/pkgs/test/test/util/string_literal_iterator_test.dart new file mode 100644 index 0000000..03c8e34 --- /dev/null +++ b/pkgs/test/test/util/string_literal_iterator_test.dart
@@ -0,0 +1,250 @@ +// Copyright (c) 2015, 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:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:test/test.dart'; +import 'package:test_core/src/util/string_literal_iterator.dart'; + +final _offset = 'final str = '.length; + +void main() { + group('returns simple characters in', () { + test('a single simple string', () { + var iter = _parse('"abc"'); + + expect(() => iter.current, throwsA(isA<TypeError>())); + expect(iter.offset, equals(_offset)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('a')); + expect(iter.offset, equals(_offset + 1)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('b')); + expect(iter.offset, equals(_offset + 2)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('c')); + expect(iter.offset, equals(_offset + 3)); + + expect(iter.moveNext(), isFalse); + expect(() => iter.current, throwsA(isA<TypeError>())); + expect(iter.offset, equals(_offset + 4)); + }); + + test('a raw string', () { + var iter = _parse('r"abc"'); + + expect(() => iter.current, throwsA(isA<TypeError>())); + expect(iter.offset, equals(_offset + 1)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('a')); + expect(iter.offset, equals(_offset + 2)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('b')); + expect(iter.offset, equals(_offset + 3)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('c')); + expect(iter.offset, equals(_offset + 4)); + + expect(iter.moveNext(), isFalse); + expect(() => iter.current, throwsA(isA<TypeError>())); + expect(iter.offset, equals(_offset + 5)); + }); + + test('a multiline string', () { + var iter = _parse('"""ab\ncd"""'); + + expect(() => iter.current, throwsA(isA<TypeError>())); + expect(iter.offset, equals(_offset + 2)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('a')); + expect(iter.offset, equals(_offset + 3)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('b')); + expect(iter.offset, equals(_offset + 4)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('\n')); + expect(iter.offset, equals(_offset + 5)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('c')); + expect(iter.offset, equals(_offset + 6)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('d')); + expect(iter.offset, equals(_offset + 7)); + + expect(iter.moveNext(), isFalse); + expect(() => iter.current, throwsA(isA<TypeError>())); + expect(iter.offset, equals(_offset + 8)); + }); + + test('a raw multiline string', () { + var iter = _parse('r"""ab\ncd"""'); + + expect(() => iter.current, throwsA(isA<TypeError>())); + expect(iter.offset, equals(_offset + 3)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('a')); + expect(iter.offset, equals(_offset + 4)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('b')); + expect(iter.offset, equals(_offset + 5)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('\n')); + expect(iter.offset, equals(_offset + 6)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('c')); + expect(iter.offset, equals(_offset + 7)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('d')); + expect(iter.offset, equals(_offset + 8)); + + expect(iter.moveNext(), isFalse); + expect(() => iter.current, throwsA(isA<TypeError>())); + expect(iter.offset, equals(_offset + 9)); + }); + + test('adjacent strings', () { + var iter = _parse('"ab" r"cd" """ef\ngh"""'); + + expect(() => iter.current, throwsA(isA<TypeError>())); + expect(iter.offset, equals(_offset)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('a')); + expect(iter.offset, equals(_offset + 1)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('b')); + expect(iter.offset, equals(_offset + 2)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('c')); + expect(iter.offset, equals(_offset + 7)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('d')); + expect(iter.offset, equals(_offset + 8)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('e')); + expect(iter.offset, equals(_offset + 14)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('f')); + expect(iter.offset, equals(_offset + 15)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('\n')); + expect(iter.offset, equals(_offset + 16)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('g')); + expect(iter.offset, equals(_offset + 17)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('h')); + expect(iter.offset, equals(_offset + 18)); + + expect(iter.moveNext(), isFalse); + expect(() => iter.current, throwsA(isA<TypeError>())); + expect(iter.offset, equals(_offset + 19)); + }); + }); + + group('parses an escape sequence for', () { + test('a newline', () => _expectEscape(r'\n', '\n')); + test('a carriage return', () => _expectEscape(r'\r', '\r')); + test('a form feed', () => _expectEscape(r'\f', '\f')); + test('a backspace', () => _expectEscape(r'\b', '\b')); + test('a tab', () => _expectEscape(r'\t', '\t')); + test('a vertical tab', () => _expectEscape(r'\v', '\v')); + test('a quote', () => _expectEscape(r'\"', '"')); + test('a backslash', () => _expectEscape(r'\\', '\\')); + + test('a hex character', () { + _expectEscape(r'\x62', 'b'); + _expectEscape(r'\x7A', 'z'); + _expectEscape(r'\x7a', 'z'); + }); + + test('a fixed-length unicode character', + () => _expectEscape(r'\u0062', 'b')); + + test('a short variable-length unicode character', + () => _expectEscape(r'\u{62}', 'b')); + + test('a long variable-length unicode character', + () => _expectEscape(r'\u{000062}', 'b')); + }); + + group('throws an ArgumentError for', () { + test('interpolation', () { + expect(() => _parse(r'"$foo"'), throwsArgumentError); + }); + + test('interpolation in an adjacent string', () { + expect(() => _parse(r'"foo" "$bar" "baz"'), throwsArgumentError); + }); + }); +} + +/// Asserts that [escape] is parsed as [value]. +void _expectEscape(String escape, String value) { + var iter = _parse('"a${escape}b"'); + + expect(() => iter.current, throwsA(isA<TypeError>())); + expect(iter.offset, equals(_offset)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('a')); + expect(iter.offset, equals(_offset + 1)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune(value)); + expect(iter.offset, equals(_offset + 2)); + + expect(iter.moveNext(), isTrue); + expect(iter.current, _isRune('b')); + expect(iter.offset, equals(_offset + escape.length + 2)); + + expect(iter.moveNext(), isFalse); + expect(() => iter.current, throwsA(isA<TypeError>())); + expect(iter.offset, equals(_offset + escape.length + 3)); +} + +/// Returns a matcher that asserts that the given rune is the rune for [char]. +Matcher _isRune(String char) { + return predicate((rune) { + return rune is int && String.fromCharCode(rune) == char; + }, 'is the rune "$char"'); +} + +/// Parses [dart], which should be a string literal, into a +/// [StringLiteralIterator]. +StringLiteralIterator _parse(String dart) { + var declaration = parseString(content: 'final str = $dart;') + .unit + .declarations + .single as TopLevelVariableDeclaration; + var literal = declaration.variables.variables.single.initializer; + return StringLiteralIterator(literal as StringLiteral); +}
diff --git a/pkgs/test/test/utils.dart b/pkgs/test/test/utils.dart new file mode 100644 index 0000000..23d5b7e --- /dev/null +++ b/pkgs/test/test/utils.dart
@@ -0,0 +1,287 @@ +// Copyright (c) 2015, 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:collection'; + +import 'package:boolean_selector/boolean_selector.dart'; +import 'package:glob/glob.dart'; +import 'package:test/test.dart'; +import 'package:test_api/src/backend/declarer.dart'; +import 'package:test_api/src/backend/group.dart'; +import 'package:test_api/src/backend/group_entry.dart'; +import 'package:test_api/src/backend/live_test.dart'; +import 'package:test_api/src/backend/platform_selector.dart'; +import 'package:test_api/src/backend/runtime.dart'; +import 'package:test_api/src/backend/state.dart'; +import 'package:test_api/src/backend/suite_platform.dart'; +import 'package:test_core/src/runner/application_exception.dart'; +import 'package:test_core/src/runner/compiler_selection.dart'; +import 'package:test_core/src/runner/configuration.dart'; +import 'package:test_core/src/runner/configuration/custom_runtime.dart'; +import 'package:test_core/src/runner/configuration/runtime_settings.dart'; +import 'package:test_core/src/runner/engine.dart'; +import 'package:test_core/src/runner/load_suite.dart'; +import 'package:test_core/src/runner/plugin/environment.dart'; +import 'package:test_core/src/runner/runner_suite.dart'; +import 'package:test_core/src/runner/runtime_selection.dart'; +import 'package:test_core/src/runner/suite.dart'; + +/// A dummy suite platform to use for testing suites. +final suitePlatform = SuitePlatform(Runtime.vm, compiler: null); + +// The last state change detected via [expectStates]. +State? _lastState; + +/// Asserts that exactly [states] will be emitted via [liveTest.onStateChange]. +/// +/// The most recent emitted state is stored in [_lastState]. +void expectStates(LiveTest liveTest, Iterable<State> statesIter) { + var states = Queue.of(statesIter); + liveTest.onStateChange.listen(expectAsync1((state) { + _lastState = state; + expect(state, equals(states.removeFirst())); + }, count: states.length, max: states.length)); +} + +/// Asserts that errors will be emitted via [liveTest.onError] that match +/// [validators], in order. +void expectErrors( + LiveTest liveTest, Iterable<void Function(Object)> validatorsIter) { + var validators = Queue.of(validatorsIter); + liveTest.onError.listen(expectAsync1((error) { + validators.removeFirst()(error.error); + }, count: validators.length, max: validators.length)); +} + +/// Asserts that [liveTest] will have a single failure with message `"oh no"`. +void expectSingleFailure(LiveTest liveTest) { + expectStates(liveTest, [ + const State(Status.running, Result.success), + const State(Status.complete, Result.failure) + ]); + + expectErrors(liveTest, [ + (error) { + expect(_lastState!.status, equals(Status.complete)); + expect(error, _isTestFailure('oh no')); + } + ]); +} + +/// Returns a matcher that matches a [TestFailure] with the given [message]. +/// +/// [message] can be a string or a [Matcher]. +Matcher _isTestFailure(Object message) => const TypeMatcher<TestFailure>() + .having((e) => e.message, 'message', message); + +/// Returns a matcher that matches a [ApplicationException] with the given +/// [message]. +/// +/// [message] can be a string or a [Matcher]. +Matcher isApplicationException(Object message) => + const TypeMatcher<ApplicationException>() + .having((e) => e.message, 'message', message); + +/// Asserts that [liveTest] has completed and passed. +/// +/// If the test had any errors, they're surfaced nicely into the outer test. +void expectTestPassed(LiveTest liveTest) { + // Since the test is expected to pass, we forward any current or future errors + // to the outer test, because they're definitely unexpected. + for (var error in liveTest.errors) { + registerException(error.error, error.stackTrace); + } + liveTest.onError.listen((error) { + registerException(error.error, error.stackTrace); + }); + + expect(liveTest.state.status, equals(Status.complete)); + expect(liveTest.state.result, equals(Result.success)); +} + +/// Asserts that [liveTest] failed with a single [TestFailure] whose message +/// matches [message]. +void expectTestFailed(LiveTest liveTest, Object message) { + expect(liveTest.state.status, equals(Status.complete)); + expect(liveTest.state.result, equals(Result.failure)); + expect(liveTest.errors, hasLength(1)); + expect(liveTest.errors.first.error, _isTestFailure(message)); +} + +/// Runs [body] with a declarer and returns the declared entries. +List<GroupEntry> declare(void Function() body) { + var declarer = Declarer()..declare(body); + return declarer.build().entries; +} + +/// Runs [body] with a declarer and returns an engine that runs those tests. +Engine declareEngine( + void Function() body, { + bool runSkipped = false, + String? coverage, + bool stopOnFirstFailure = false, +}) { + var declarer = Declarer()..declare(body); + return Engine.withSuites( + [ + RunnerSuite( + const PluginEnvironment(), + SuiteConfiguration.runSkipped(runSkipped), + declarer.build(), + suitePlatform) + ], + coverage: coverage, + stopOnFirstFailure: stopOnFirstFailure, + ); +} + +/// Returns a [RunnerSuite] with a default environment and configuration. +RunnerSuite runnerSuite(Group root) => RunnerSuite( + const PluginEnvironment(), SuiteConfiguration.empty, root, suitePlatform); + +/// Returns a [LoadSuite] with a default configuration. +LoadSuite loadSuite(String name, FutureOr<RunnerSuite> Function() body) => + LoadSuite(name, SuiteConfiguration.empty, suitePlatform, body); + +SuiteConfiguration suiteConfiguration( + {bool? allowDuplicateTestNames, + bool? allowTestRandomization, + bool? jsTrace, + bool? runSkipped, + Iterable<String>? dart2jsArgs, + String? precompiledPath, + Iterable<CompilerSelection>? compilerSelections, + Iterable<RuntimeSelection>? runtimes, + Map<BooleanSelector, SuiteConfiguration>? tags, + Map<PlatformSelector, SuiteConfiguration>? onPlatform, + bool? ignoreTimeouts, + + // Test-level configuration + Timeout? timeout, + bool? verboseTrace, + bool? chainStackTraces, + bool? skip, + int? retry, + String? skipReason, + PlatformSelector? testOn, + Iterable<String>? addTags}) => + SuiteConfiguration( + allowDuplicateTestNames: allowDuplicateTestNames, + allowTestRandomization: allowTestRandomization, + jsTrace: jsTrace, + runSkipped: runSkipped, + dart2jsArgs: dart2jsArgs, + precompiledPath: precompiledPath, + compilerSelections: compilerSelections, + runtimes: runtimes, + tags: tags, + onPlatform: onPlatform, + ignoreTimeouts: ignoreTimeouts, + timeout: timeout, + verboseTrace: verboseTrace, + chainStackTraces: chainStackTraces, + skip: skip, + retry: retry, + skipReason: skipReason, + testOn: testOn, + addTags: addTags); + +Configuration configuration( + {bool? help, + String? customHtmlTemplatePath, + bool? version, + bool? pauseAfterLoad, + bool? debug, + bool? color, + String? configurationPath, + String? reporter, + Map<String, String>? fileReporters, + String? coverage, + int? concurrency, + int? shardIndex, + int? totalShards, + Map<String, Set<TestSelection>>? testSelections, + Iterable<String>? foldTraceExcept, + Iterable<String>? foldTraceOnly, + Glob? filename, + Iterable<String>? chosenPresets, + Map<String, Configuration>? presets, + Map<String, RuntimeSettings>? overrideRuntimes, + Map<String, CustomRuntime>? defineRuntimes, + bool? noRetry, + bool? ignoreTimeouts, + + // Suite-level configuration + bool? allowDuplicateTestNames, + bool? allowTestRandomization, + bool? jsTrace, + bool? runSkipped, + Iterable<String>? dart2jsArgs, + String? precompiledPath, + Iterable<Pattern>? globalPatterns, + Iterable<CompilerSelection>? compilerSelections, + Iterable<RuntimeSelection>? runtimes, + BooleanSelector? includeTags, + BooleanSelector? excludeTags, + Map<BooleanSelector, SuiteConfiguration>? tags, + Map<PlatformSelector, SuiteConfiguration>? onPlatform, + int? testRandomizeOrderingSeed, + + // Test-level configuration + Timeout? timeout, + bool? verboseTrace, + bool? chainStackTraces, + bool? skip, + int? retry, + String? skipReason, + PlatformSelector? testOn, + Iterable<String>? addTags}) => + Configuration( + help: help, + customHtmlTemplatePath: customHtmlTemplatePath, + version: version, + pauseAfterLoad: pauseAfterLoad, + debug: debug, + color: color, + configurationPath: configurationPath, + reporter: reporter, + fileReporters: fileReporters, + coverage: coverage, + concurrency: concurrency, + shardIndex: shardIndex, + totalShards: totalShards, + testSelections: testSelections, + foldTraceExcept: foldTraceExcept, + foldTraceOnly: foldTraceOnly, + filename: filename, + chosenPresets: chosenPresets, + presets: presets, + overrideRuntimes: overrideRuntimes, + defineRuntimes: defineRuntimes, + noRetry: noRetry, + ignoreTimeouts: ignoreTimeouts, + allowDuplicateTestNames: allowDuplicateTestNames, + allowTestRandomization: allowTestRandomization, + jsTrace: jsTrace, + runSkipped: runSkipped, + dart2jsArgs: dart2jsArgs, + precompiledPath: precompiledPath, + globalPatterns: globalPatterns, + compilerSelections: compilerSelections, + runtimes: runtimes, + includeTags: includeTags, + excludeTags: excludeTags, + tags: tags, + onPlatform: onPlatform, + testRandomizeOrderingSeed: testRandomizeOrderingSeed, + stopOnFirstFailure: false, + timeout: timeout, + verboseTrace: verboseTrace, + chainStackTraces: chainStackTraces, + skip: skip, + retry: retry, + skipReason: skipReason, + testOn: testOn, + addTags: addTags);
diff --git a/pkgs/test/tool/README.md b/pkgs/test/tool/README.md new file mode 100644 index 0000000..bd9ee8b --- /dev/null +++ b/pkgs/test/tool/README.md
@@ -0,0 +1,7 @@ +`host.dart` is the source for `lib/src/runner/browser/static/host.dart.js`. + +To updated it, run: + +```console +dart compile js tool/host.dart -o lib/src/runner/browser/static/host.dart.js +```
diff --git a/pkgs/test/tool/host.dart b/pkgs/test/tool/host.dart new file mode 100644 index 0000000..dc3e5ab --- /dev/null +++ b/pkgs/test/tool/host.dart
@@ -0,0 +1,252 @@ +// Copyright (c) 2015, 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. + +@JS() +library; + +import 'dart:async'; +import 'dart:convert'; + +import 'package:js/js.dart'; +import 'package:stack_trace/stack_trace.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test/src/runner/browser/dom.dart' as dom; + +/// A class that exposes the test API to JS. +/// +/// These are exposed so that tools like IDEs can interact with them via remote +/// debugging. +@JS() +@anonymous +@staticInterop +class _JSApi { + external factory _JSApi( + {void Function() resume, void Function() restartCurrent}); +} + +extension _JSApiExtension on _JSApi { + /// Causes the test runner to resume running, as though the user had clicked + /// the "play" button. + // ignore: unused_element + external Function get resume; + + /// Causes the test runner to restart the current test once it finishes + /// running. + // ignore: unused_element + external Function get restartCurrent; +} + +/// Sets the top-level `dartTest` object so that it's visible to JS. +@JS('dartTest') +external set _jsApi(_JSApi api); + +/// The iframes created for each loaded test suite, indexed by the suite id. +final _iframes = <int, dom.HTMLIFrameElement>{}; + +/// Subscriptions created for each loaded test suite, indexed by the suite id. +final _subscriptions = <int, StreamSubscription<void>>{}; +final _domSubscriptions = <int, dom.Subscription>{}; + +/// The URL for the current page. +final _currentUrl = Uri.parse(dom.window.location.href); + +/// Code that runs in the browser and loads test suites at the server's behest. +/// +/// One instance of this runs for each browser. When the server tells it to load +/// a test, it starts an iframe pointing at that test's code; from then on, it +/// just relays messages between the two. +/// +/// The browser uses two layers of [MultiChannel]s when communicating with the +/// server: +/// +/// server +/// │ +/// (WebSocket) +/// │ +/// ┏━ host.html ━━━━━━━━┿━━━━━━━━━━━━━━━━━┓ +/// ┃ │ ┃ +/// ┃ ┌──────┬───MultiChannel─────┐ ┃ +/// ┃ │ │ │ │ │ ┃ +/// ┃ host suite suite suite suite ┃ +/// ┃ │ │ │ │ ┃ +/// ┗━━━━━━━━━━━┿━━━━━━┿━━━━━━┿━━━━━━┿━━━━━┛ +/// │ │ │ │ +/// │ ... ... ... +/// │ +/// (MessageChannel) +/// │ +/// ┏━ suite.html (in iframe) ┿━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +/// ┃ │ ┃ +/// ┃ ┌──────────MultiChannel┬─────────┐ ┃ +/// ┃ │ │ │ │ │ ┃ +/// ┃ RemoteListener test test test running test ┃ +/// ┃ ┃ +/// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +/// +/// The host (this code) has a [MultiChannel] that splits the WebSocket +/// connection with the server. One connection is used for the host itself to +/// receive messages like "load a suite at this URL", and the rest are +/// connected to each test suite's iframe via a [MessageChannel]. +/// +/// Each iframe runs a `RemoteListener` which creates its own [MultiChannel] on +/// top of a [MessageChannel] connection. One connection is used for +/// the `RemoteListener`, which sends messages like "here are all the tests in +/// this suite". The rest are used for each test, receiving messages like +/// "start running". A new connection is also created whenever a test begins +/// running to send status messages about its progress. +/// +/// It's of particular note that the suite's [MultiChannel] connection uses the +/// host's purely as a transport layer; neither is aware that the other is also +/// using [MultiChannel]. This is necessary, since the host doesn't share memory +/// with the suites and thus can't share its [MultiChannel] with them, but it +/// does mean that the server needs to be sure to nest its [MultiChannel]s at +/// the same place the client does. +void main() { + dom.window.console.log('Dart test runner browser host running'); + if (_currentUrl.queryParameters['debug'] == 'true') { + dom.document.body!.classList.add('debug'); + } + + runZonedGuarded(() { + var serverChannel = _connectToServer(); + serverChannel.stream.listen((message) { + switch (message) { + case { + 'command': 'loadSuite', + 'channel': final num channel, + 'url': final String url, + 'id': final num id + }: + var suiteChannel = serverChannel.virtualChannel(channel.toInt()); + var iframeChannel = _connectToIframe(url, id.toInt()); + suiteChannel.pipe(iframeChannel); + case {'command': 'displayPause'}: + dom.document.body!.classList.add('paused'); + case {'command': 'resume'}: + dom.document.body!.classList.remove('paused'); + case {'command': 'closeSuite', 'id': final id}: + _iframes.remove(id)!.remove(); + _subscriptions.remove(id)?.cancel(); + _domSubscriptions.remove(id)?.cancel(); + default: + dom.window.console + .warn('Unhandled message from test runner: $message'); + } + }); + + // Send periodic pings to the test runner so it can know when the browser is + // paused for debugging. + Timer.periodic(const Duration(seconds: 1), + (_) => serverChannel.sink.add({'command': 'ping'})); + + var play = dom.document.querySelector('#play'); + play!.addEventListener('click', allowInterop((_) { + if (!dom.document.body!.classList.contains('paused')) return; + dom.document.body!.classList.remove('paused'); + serverChannel.sink.add({'command': 'resume'}); + })); + + _jsApi = _JSApi(resume: allowInterop(() { + if (!dom.document.body!.classList.contains('paused')) return; + dom.document.body!.classList.remove('paused'); + serverChannel.sink.add({'command': 'resume'}); + }), restartCurrent: allowInterop(() { + serverChannel.sink.add({'command': 'restart'}); + })); + }, (error, stackTrace) { + dom.window.console.warn('$error\n${Trace.from(stackTrace).terse}'); + }); +} + +/// Creates a [MultiChannel] connection to the server, using a [WebSocket] as +/// the underlying protocol. +MultiChannel<dynamic> _connectToServer() { + // The `managerUrl` query parameter contains the WebSocket URL of the remote + // [BrowserManager] with which this communicates. + var webSocket = + dom.createWebSocket(_currentUrl.queryParameters['managerUrl']!); + + var controller = StreamChannelController<Object?>(sync: true); + webSocket.addEventListener('message', allowInterop((message) { + controller.local.sink + .add(jsonDecode((message as dom.MessageEvent).data as String)); + })); + + controller.local.stream + .listen((message) => webSocket.send(jsonEncode(message))); + + return MultiChannel(controller.foreign); +} + +/// Creates an iframe with `src` [url] and expects a message back to connect a +/// message channel with the suite running in the frame. +/// +/// [id] identifies the suite loaded in this iframe. +/// +/// Before the frame is attached, adds a listener for `window.onMessage` which +/// filters to only the messages coming from this frame (by it's URL) and +/// expects the first message to be either an initialization message, (coming +/// from the browser bootstrap message channel initialization), or a map with +/// the key 'exception' set to true and details in the value for 'data' (coming +/// from `dart.js` due to a load exception). +/// +/// Legacy bootstrap implementations send a `{'ready': true}` message as a +/// signal for this host to create a [MessageChannel] and send the port through +/// the frame's `window.onMessage` channel. +/// +/// Upcoming bootstrap implementations will send the string 'port' and include a +/// port for a prepared [MessageChannel]. +/// +/// Returns a [StreamChannel] which will be connected to the frame once the +/// message channel port is active. +StreamChannel<dynamic> _connectToIframe(String url, int id) { + var suiteUrl = Uri.parse(url).removeFragment(); + dom.window.console.log('Starting suite $suiteUrl'); + var iframe = dom.createHTMLIFrameElement(); + _iframes[id] = iframe; + var controller = StreamChannelController<Object?>(sync: true); + + late dom.Subscription windowSubscription; + windowSubscription = + dom.Subscription(dom.window, 'message', allowInterop((dom.Event event) { + // A message on the Window can theoretically come from any website. It's + // very unlikely that a malicious site would care about hacking someone's + // unit tests, let alone be able to find the test server while it's + // running, but it's good practice to check the origin anyway. + var message = event as dom.MessageEvent; + if (message.origin != dom.window.location.origin) return; + // Disambiguate between frames for different test suites. + // Depending on the source type, the `location.href` may be missing. + if (message.source.location?.href != iframe.src) return; + + message.stopPropagation(); + windowSubscription.cancel(); + + switch (message.data) { + case 'port': + dom.window.console.log('Connecting channel for suite $suiteUrl'); + // The frame is starting and sending a port to forward for the suite. + final port = message.ports.first; + assert(!_domSubscriptions.containsKey(id)); + _domSubscriptions[id] = + dom.Subscription(port, 'message', allowInterop((event) { + controller.local.sink.add((event as dom.MessageEvent).data); + })); + port.start(); + + assert(!_subscriptions.containsKey(id)); + _subscriptions[id] = controller.local.stream.listen(port.postMessage); + case {'exception': true, 'data': final data}: + // This message from `dart.js` indicates that an exception occurred + // loading the test. + controller.local.sink.add(data); + } + })); + + iframe.src = url; + dom.document.body!.appendChild(iframe); + dom.window.console.log('Appended iframe with src $url'); + + return controller.foreign; +}
diff --git a/pkgs/test_api/CHANGELOG.md b/pkgs/test_api/CHANGELOG.md new file mode 100644 index 0000000..98d65ba --- /dev/null +++ b/pkgs/test_api/CHANGELOG.md
@@ -0,0 +1,339 @@ +## 0.7.4 + +* Allow `analyzer: '>=6.0.0 <8.0.0'` +* Increase SDK constraint to ^3.5.0. +* Support running Node.js tests compiled with dart2wasm. + +## 0.7.3 + +* Increase SDK constraint to ^3.4.0. + +## 0.7.2 + +* Update min SDK constraint to 3.2.0. + +## 0.7.1 + +- Added [`@doNotSubmit`](https://pub.dev/documentation/meta/latest/meta/doNotSubmit-constant.html) to `test(solo: ...)` and `group(solo: ...)`. In + practice, this means that code that was relying on ignoring deprecation + warnings and using `solo` or `group` with a `skip` parameter will now fail if + `dart analyze --fatal-infos` (or similar) is enabled. + +## 0.7.0 + +- Deprecate `Runtime.internetExplorer`. +- Added `dart2wasm` as a supported compiler for the `chrome` runtime. +- **BREAKING**: Removed the `experimentalChromeWasm` runtime. +- **BREAKING**: Removed `Runtime.isJS` and `Runtime.isWasm`, as this is now + based on the compiler and not the runtime. + +## 0.6.1 + +- Drop support for null unsafe Dart, bump SDK constraint to `3.0.0`. +- Make some implementation classes `final`. These classes were never intended to + be extended or implemented. `Metadata`, `PlatformSelector`, `RemoteListener`, + `Runtime`, `StackTraceFormatter`, `SuitePlatform`, `RemoteException`, + `TestHandle`, `OutstandingWork`, `OutsideTestException`, `OnPlatform`, + `Retry`, `Skip`, `Tags`, `TestOn`, `Timeout`. +- Mark an implementation class `interface`: `StackTraceMapper`. +- Change the `Compiler` class into an `enum`. +- Make `Fake` a `mixin class`. +- Allow the latest analyzer (6.x.x). + +## 0.6.0 + +- Remove the `package:test_api/expect.dart' library. `test`will export from`package:matcher` directly. +- Fix compatibility with wasm number semantics. + +## 0.5.2 + +- Remove deprecation for the `scaffolding.dart` and `backend.dart` libraries. +- Export `registerException` from the `scaffolding.dart` library. + +## 0.5.1 + +- Handle a missing `'compiler'` value when running a test compiled against a + newer `test_api` than the runner back end is using. The expectation was that + the json protocol is only used across packages compatible with the same major + version of the `test_api` package, but `flutter test` does not check the + version of packages in the pub solve for user test code. + +## 0.5.0 + +- Add `Compiler` class, exposed through `backend.dart`. +- Support compiler identifiers in platform selectors. +- Add `compiler` field to `SuitePlatform`. This will become required in the next + major release. +- **BREAKING** Add required `defaultCompiler` and `supportedCompilers` fields + to `Runtime`. +- Add `package:test_api/hooks_testing.dart` library for writing tests against + code that uses `package:test_api/hooks.dart`. +- **BREAKING** Remove `ErrorFormatter`, `expectAsync`, `throws`, and `Throws` + from `package:test_api/test_api.dart`. + +## 0.4.18 + +- Don't run `tearDown` until the test body and outstanding work is complete, + even if the test has already failed. + +## 0.4.17 + +- Deprecate `throwsNullThrownError`, use `throwsA(isA<TypeError>())` instead. + The implementation has been changed to ease migrations. +- Deprecate `throwsCyclicInitializationError` and replace the implementation + with `Throws(TypeMatcher<Error>())`. The specific exception no longer exists + and there is no guarantee about what type of error will be thrown. + +## 0.4.16 + +- Add the `experimental-chrome-wasm` runtime. This is very unstable and will + eventually be deleted, to be replaced by a `--compiler` flag. See + https://github.com/dart-lang/test/issues/1776 for more information on future + plans +- Add `isWasm` field to `Runtime` (defaults to `false`). + +## 0.4.15 + +- Expand the pubspec description. +- Support `package:matcher` version `0.12.13`. + +## 0.4.14 + +- Require Dart >= 2.18.0 +- Support the latest `package:analyzer`. + +## 0.4.13 + +- Fix `printOnFailure` output to be associated with the correct test. + +## 0.4.12 + +- Internal cleanup. + +## 0.4.11 + +- Support the latest version of `package:matcher`. + +## 0.4.10 + +- Add `Target` to restrict `TestOn` annotation to library level. + +## 0.4.9 + +- Add `ignoreTimeouts` option to `Suite`, which disables all timeouts for all + tests in that suite. + +## 0.4.8 + +- `TestFailure` implements `Exception` for compatibility with + `only_throw_exceptions`. + +## 0.4.7 + +- Remove logging about enabling the chain-stack-traces flag from the invoker. + +## 0.4.6 + +- Give a better exception when using `markTestSkipped` outside of a test. +- Format stack traces if a formatter is available when serializing tests + and groups from the remote listener. + +## 0.4.5 + +- Add defaulting for older test backends that don't pass a configuration for + the `allow_duplicate_test_names` parameter to the remote listener. + +## 0.4.4 + +- Allow disabling duplicate test or group names in the `Declarer`. + +## 0.4.3 + +- Use the latest `package:matcher`. + +## 0.4.2 + +- Update `analyzer` constraint to `>=1.5.0 <3.0.0`. + +## 0.4.1 + +- Give a better error when `printOnFailure` is called from outside a test + zone. + +## 0.4.0 + +- Add libraries `scaffolding.dart`, and `expect.dart` to allow importing as + subset of the normal surface area. +- Add new APIs in `hooks.dart` to allow writing custom expectation frameworks + which integrate with the test runner. +- Add examples to `throwsA` and make top-level `throws...` matchers refer to it. +- Disable stack trace chaining by default. +- Fix `expectAsync` function type checks. +- Add `RemoteException`, `RemoteListener`, `StackTraceFormatter`, and + `StackTraceMapper` to `backend.dart`. +- **Breaking** remove `Runtime.phantomJS` +- **Breaking** Add callback to get the suite channel in the `beforeLoad` + callback of `RemoteListener.start`. This is now used in place of using zones + to communicate the value. + +## 0.3.0 + +- **Breaking** `TestException.message` is now nullable. + - Fixes handling of `null` messages in remote exceptions. + +## 0.2.20 + +- Fix some strong null safety mode errors in the original migration. + +## 0.2.19 + +- Stable release for null safety. + +## 0.2.19-nullsafety.7 + +- Expand upper bound constraints for some null safe migrated packages. + +## 0.2.19-nullsafety.6 + +- Fix `spawnHybridUri` to respect language versioning of the spawned uri. + +## 0.2.19-nullsafety.5 + +- Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release + guidelines. + +## 0.2.19-nullsafety.4 + +- Allow prerelease versions of the 2.12 sdk. + +## 0.2.19-nullsafety.3 + +- Add capability to filter to a single exact test name in `Declarer`. +- Add `markTestSkipped` API. + +## 0.2.19-nullsafety.2 + +- Allow `2.10` stable and `2.11.0-dev` SDKs. +- Annotate the classes used as annotations to restrict their usage to library + level. + +## 0.2.19-nullsafety + +- Migrate to NNBD. + - The vast majority of changes are intended to express the pre-existing + behavior of the code regarding to handling of nulls. + - **Breaking Change**: `GroupEntry.name` is no longer nullable, the root + group now has the empty string as its name. +- Add the `Fake` class, available through `package:test_api/fake.dart`. This + was previously part of the Mockito package, but with null safety it is useful + enough that we decided to make it available through `package:test`. In a + future release it will be made available directly through + `package:test_api/test_api.dart` (and hence through + `package:test_core/test_core.dart` and `package:test/test.dart`). + +## 0.2.18+1 (Backport) + +- Fix `spawnHybridUri` to respect language versioning of the spawned uri. + +## 0.2.18 + +- Update to `matcher` version `0.12.9`. + +## 0.2.17 + +- Add `languageVersionComment` on the `MetaData` class. This should only be + present for test suites. + +## 0.2.16 + +- Deprecate `LiveTestController.liveTest`, the `LiveTestController` instance now + implements `LiveTest` and can be used directly. + +## 0.2.15 + +- Cancel any StreamQueue that is created as a part of a stream matcher once it + is done matching. + - This fixes a bug where using a matcher on a custom stream controller and + then awaiting the `close()` method on that controller would hang. +- Avoid causing the test runner to hang if there is a timeout during a + `tearDown` callback following a failing test case. + +## 0.2.14 + +- Bump minimum SDK to `2.4.0` for safer usage of for-loop elements. + +## 0.2.13 + +- Work around a bug in the `2.3.0` SDK by avoiding for-loop elements at the top + level. + +## 0.2.12 + +- Link to docs on setting timeout when a test times out with the default + duration. +- No longer directly depend on `package:pedantic`. + +## 0.2.11 + +- Extend the timeout for synthetic tests, e.g. `tearDownAll`. + +## 0.2.10 + +- Update to latest `package:matcher`. Improves output for instances of private + classes. + +## 0.2.9 + +- Treat non-solo tests as skipped so they are properly reported. + +## 0.2.8 + +- Remove logic which accounted for a race condition in state change. The logic + was required because `package:sse` used to not guarantee order. This is no + longer the case. + +## 0.2.7 + +- Prepare for upcoming `Stream<List<int>>` changes in the Dart SDK. +- Mark `package:test_api` as deprecated to prevent accidental use. + +## 0.2.6 + +- Don't swallow exceptions from callbacks in `expectAsync*`. +- Internal cleanup - fix lints. +- Fixed a race condition that caused tests to occasionally fail during + `tearDownAll` with the message `(tearDownAll) - did not complete [E]`. + +## 0.2.5 + +- Expose the `Metadata`, `PlatformSelector`, `Runtime`, and `SuitePlatform` + classes publicly through a new `backend.dart` import. + +## 0.2.4 + +- Allow `stream_channel` version `2.0.0`. + +## 0.2.3 + +- Update to matcher version `0.12.5`. + +## 0.2.2 + +- Require Dart SDK `>=2.1.0`. + +## 0.2.1 + +- Add `remote_listener.dart` and `suite_channel_manager.dart`. + +## 0.2.0 + +- Remove "runner" extensions. + +## 0.1.1 + +- Update `stack_trace_formatter` to fold `test_api` frames by default. + +## 0.1.0 + +- Initial release of `test_api`. Provides the basic API for writing tests and + touch points for implementing a custom test runner.
diff --git a/pkgs/test_api/LICENSE b/pkgs/test_api/LICENSE new file mode 100644 index 0000000..9972f6e --- /dev/null +++ b/pkgs/test_api/LICENSE
@@ -0,0 +1,27 @@ +Copyright 2018, 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_api/README.md b/pkgs/test_api/README.md new file mode 100644 index 0000000..aa1af49 --- /dev/null +++ b/pkgs/test_api/README.md
@@ -0,0 +1,8 @@ +[](https://pub.dev/packages/test_api) +[](https://pub.dev/packages/test_api/publisher) + +A minimal package for writing tests. At this time this package is not intended +to be publicly used as the API will take time to stabilize. + +If you're interested in testing Dart code, you likely want to use +[package:test](https://pub.dev/packages/test).
diff --git a/pkgs/test_api/dart_test.yaml b/pkgs/test_api/dart_test.yaml new file mode 100644 index 0000000..faff3bb --- /dev/null +++ b/pkgs/test_api/dart_test.yaml
@@ -0,0 +1,46 @@ +# Fold frames from helper packages we use in our tests, but not from test +# itself. +fold_stack_frames: + except: + - shelf_test_handler + - stream_channel + - test_descriptor + - test_process + +presets: + # "-P terse-trace" folds frames from test's implementation to make the output + # less verbose when + terse-trace: + fold_stack_frames: + except: [test] + +tags: + browser: + timeout: 2x + + # Browsers can sometimes randomly time out while starting, especially on + # Travis which is pretty slow. Don't retry locally because it makes + # debugging more annoying. + presets: {travis: {retry: 3}} + + dart2js: + add_tags: [browser] + timeout: 2x + + firefox: {add_tags: [dart2js]} + chrome: {add_tags: [dart2js]} + phantomjs: {add_tags: [dart2js]} + + safari: + add_tags: [dart2js] + test_on: mac-os + + # Tests that run pub. These tests may need to be excluded when there are local + # dependency_overrides. + pub: + timeout: 2x + + # Tests that use Node.js. These tests may need to be excluded on systems that + # don't have Node installed. + node: + timeout: 2x
diff --git a/pkgs/test_api/lib/backend.dart b/pkgs/test_api/lib/backend.dart new file mode 100644 index 0000000..5da47b6 --- /dev/null +++ b/pkgs/test_api/lib/backend.dart
@@ -0,0 +1,13 @@ +// Copyright (c) 2019, 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. + +export 'src/backend/compiler.dart' show Compiler; +export 'src/backend/metadata.dart' show Metadata; +export 'src/backend/platform_selector.dart' show PlatformSelector; +export 'src/backend/remote_exception.dart' show RemoteException; +export 'src/backend/remote_listener.dart' show RemoteListener; +export 'src/backend/runtime.dart' show Runtime; +export 'src/backend/stack_trace_formatter.dart' show StackTraceFormatter; +export 'src/backend/stack_trace_mapper.dart' show StackTraceMapper; +export 'src/backend/suite_platform.dart' show SuitePlatform;
diff --git a/pkgs/test_api/lib/fake.dart b/pkgs/test_api/lib/fake.dart new file mode 100644 index 0000000..f8b2d80 --- /dev/null +++ b/pkgs/test_api/lib/fake.dart
@@ -0,0 +1,13 @@ +// Copyright (c) 2020, 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. + +// Note: eventually we would like to fold this into test_api.dart, but we can't +// do so until Mockito stops implementing its own version of `Fake`, because +// there is code in the wild that imports both test_api.dart and Mockito. + +@Deprecated('package:test_api is not intended for general use. ' + 'Please use package:test.') +library; + +export 'src/frontend/fake.dart';
diff --git a/pkgs/test_api/lib/hooks.dart b/pkgs/test_api/lib/hooks.dart new file mode 100644 index 0000000..126d322 --- /dev/null +++ b/pkgs/test_api/lib/hooks.dart
@@ -0,0 +1,82 @@ +// Copyright (c) 2021, 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 'package:stack_trace/stack_trace.dart'; + +import 'src/backend/closed_exception.dart'; +import 'src/backend/invoker.dart'; +import 'src/backend/stack_trace_formatter.dart'; + +export 'src/backend/test_failure.dart' show TestFailure; +export 'src/scaffolding/utils.dart' show pumpEventQueue; + +final class TestHandle { + /// Returns handle for the currently running test. + /// + /// This must be called from within the zone that the test is running in. If + /// the current zone is not a test's zone throws [OutsideTestException]. + static TestHandle get current { + final invoker = Invoker.current; + if (invoker == null) throw OutsideTestException(); + return TestHandle._( + invoker, StackTraceFormatter.current ?? _defaultFormatter); + } + + static final _defaultFormatter = StackTraceFormatter(); + + final Invoker _invoker; + final StackTraceFormatter _stackTraceFormatter; + TestHandle._(this._invoker, this._stackTraceFormatter); + + String get name => _invoker.liveTest.test.name; + + /// Whether this test has already completed successfully. + /// + /// If a callback originating from a test case is invoked after the test has + /// already passed it may be an indication of a test that fails to wait for + /// all work to be finished, or of an asynchronous callback that is called + /// more times or later than expected. + bool get shouldBeDone => _invoker.liveTest.state.shouldBeDone; + + /// Marks this test as skipped. + /// + /// A skipped test may still fail if any exception is thrown, including + /// uncaught asynchronous errors. + void markSkipped(String message) { + if (_invoker.closed) throw ClosedException(); + _invoker.skip(message); + } + + /// Indicates that this test should not be considered done until the returned + /// [OutstandingWork] is marked as complete. + /// + /// The test may time out before the outstanding work completes. + OutstandingWork markPending() { + if (_invoker.closed) throw ClosedException(); + return OutstandingWork._(_invoker, Zone.current); + } + + /// Converts [stackTrace] to a [Chain] according to the current test's + /// configuration. + Chain formatStackTrace(StackTrace stackTrace) => + _stackTraceFormatter.formatStackTrace(stackTrace); +} + +final class OutstandingWork { + final Invoker _invoker; + final Zone _zone; + var _isComplete = false; + OutstandingWork._(this._invoker, this._zone) { + _invoker.addOutstandingCallback(); + } + void complete() { + if (_isComplete) return; + _isComplete = true; + _zone.run(_invoker.removeOutstandingCallback); + } +} + +final class OutsideTestException implements Exception {}
diff --git a/pkgs/test_api/lib/hooks_testing.dart b/pkgs/test_api/lib/hooks_testing.dart new file mode 100644 index 0000000..b0a6154 --- /dev/null +++ b/pkgs/test_api/lib/hooks_testing.dart
@@ -0,0 +1,157 @@ +// Copyright (c) 2023, 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 'src/backend/group.dart'; +import 'src/backend/invoker.dart'; +import 'src/backend/live_test.dart'; +import 'src/backend/metadata.dart'; +import 'src/backend/runtime.dart'; +import 'src/backend/state.dart'; +import 'src/backend/suite.dart'; +import 'src/backend/suite_platform.dart'; + +@Deprecated('These classes are unused for monitoring tests. Use `State`.') +export 'src/backend/state.dart' show Result, Status; +export 'src/backend/test_failure.dart' show TestFailure; + +/// A monitor for the behavior of a callback when it is run as the body of a +/// test case. +/// +/// Allows running a callback as the body of a local test case and querying for +/// the current [state], and [errors], and subscribing to future errors. +/// +/// Use [run] to run a test body and query for the success or failure. +/// +/// Use [start] to start a test and query for whether it has finished running. +final class TestCaseMonitor { + final LiveTest _liveTest; + final _done = Completer<void>(); + TestCaseMonitor._(FutureOr<void> Function() body) + : _liveTest = _createTest(body); + + /// Run [body] as a test case and return a [TestCaseMonitor] with the result. + /// + /// The [state] will either [State.passed], [State.skipped], or + /// [State.failed], the test will no longer be running. + /// + /// {@template result-late-fail} + /// Note that a test can change state from [State.passed] to [State.failed] + /// if the test surfaces an unawaited asynchronous error. + /// {@endtemplate} + /// + /// ```dart + /// final monitor = await TestCaseMonitor.run(() { + /// fail('oh no!'); + /// }); + /// assert(monitor.state == State.failed); + /// assert((monitor.errors.single.error as TestFailure).message == 'oh no!'); + /// ``` + static Future<TestCaseMonitor> run(FutureOr<void> Function() body) async { + final monitor = TestCaseMonitor.start(body); + await monitor.onDone; + return monitor; + } + + /// Start [body] as a test case and return a [TestCaseMonitor] with the status + /// and result. + /// + /// The [state] will start as [State.pending] if queried synchronously, but it + /// will switch to [State.running]. After `onDone` completes the state will be + /// one of [State.passed], [State.skipped], or [State.failed]. + /// + /// {@macro result-late-fail} + /// + /// ```dart + /// late void Function() completeWork; + /// final monitor = TestCaseMonitor.start(() { + /// final outstandingWork = TestHandle.current.markPending(); + /// completeWork = outstandingWork.complete; + /// }); + /// await pumpEventQueue(); + /// assert(monitor.state == State.running); + /// completeWork(); + /// await monitor.onDone; + /// assert(monitor.state == State.passed); + /// ``` + static TestCaseMonitor start(FutureOr<void> Function() body) => + TestCaseMonitor._(body).._start(); + + void _start() { + _liveTest.run().whenComplete(_done.complete); + } + + /// A future that completes after this test has finished running, or has + /// surfaced an error. + Future<void> get onDone => _done.future; + + /// The running and success or failure status for the test case. + State get state { + final status = _liveTest.state.status; + if (status == Status.pending) return State.pending; + if (status == Status.running) return State.running; + final result = _liveTest.state.result; + if (result == Result.skipped) return State.skipped; + if (result == Result.success) return State.passed; + // result == Result.failure || result == Result.error + return State.failed; + } + + /// The errors surfaced by the test. + /// + /// A test with any errors will have a [state] of [State.failed]. + /// + /// {@macro result-late-fail} + /// + /// A test may have more than one error if there were unhandled asynchronous + /// errors surfaced after the test is done. + Iterable<AsyncError> get errors => _liveTest.errors; + + /// A stream of errors surfaced by the test. + /// + /// This stream will not close, asynchronous errors may be surfaced within the + /// test's error zone at any point. + Stream<AsyncError> get onError => _liveTest.onError; +} + +/// Returns a local [LiveTest] that runs [body]. +LiveTest _createTest(FutureOr<void> Function() body) { + var test = LocalTest('test', Metadata(chainStackTraces: true), body); + var suite = Suite(Group.root([test]), _suitePlatform, ignoreTimeouts: false); + return test.load(suite); +} + +/// A dummy suite platform to use for testing suites. +final _suitePlatform = + SuitePlatform(Runtime.vm, compiler: Runtime.vm.defaultCompiler); + +/// The running and success state of a test monitored by a [TestCaseMonitor]. +enum State { + /// The test is has not yet started. + pending, + + /// The test is running and has not yet failed. + running, + + /// The test has completed without any error. + /// + /// This implies that the test body has completed, and no error has surfaced + /// *yet*. However, it this doesn't mean that the test won't fail in the + /// future. + passed, + + /// The test, or some part of it, has been skipped. + /// + /// This does not imply that the test has not had an error, but if there are + /// errors they are ignored. + skipped, + + /// The test has failed. + /// + /// An test fails when any exception, typically a [TestFailure], is thrown in + /// the test's zone. A test that has failed may still have additional errors + /// that surface as unhandled asynchronous errors. + failed, +}
diff --git a/pkgs/test_api/lib/scaffolding.dart b/pkgs/test_api/lib/scaffolding.dart new file mode 100644 index 0000000..9d864bb --- /dev/null +++ b/pkgs/test_api/lib/scaffolding.dart
@@ -0,0 +1,23 @@ +// Copyright (c) 2021, 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. + +/// {@canonicalFor on_platform.OnPlatform} +/// {@canonicalFor retry.Retry} +/// {@canonicalFor skip.Skip} +/// {@canonicalFor tags.Tags} +/// {@canonicalFor test_on.TestOn} +/// {@canonicalFor timeout.Timeout} +library; + +export 'src/backend/configuration/on_platform.dart' show OnPlatform; +export 'src/backend/configuration/retry.dart' show Retry; +export 'src/backend/configuration/skip.dart' show Skip; +export 'src/backend/configuration/tags.dart' show Tags; +export 'src/backend/configuration/test_on.dart' show TestOn; +export 'src/backend/configuration/timeout.dart' show Timeout; +export 'src/scaffolding/spawn_hybrid.dart' show spawnHybridCode, spawnHybridUri; +export 'src/scaffolding/test_structure.dart' + show addTearDown, group, setUp, setUpAll, tearDown, tearDownAll, test; +export 'src/scaffolding/utils.dart' + show markTestSkipped, printOnFailure, pumpEventQueue, registerException;
diff --git a/pkgs/test_api/lib/src/backend/closed_exception.dart b/pkgs/test_api/lib/src/backend/closed_exception.dart new file mode 100644 index 0000000..08a45a0 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/closed_exception.dart
@@ -0,0 +1,12 @@ +// Copyright (c) 2015, 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. + +/// An exception thrown by various front-end methods when the test framework has +/// been closed and a test must shut down as soon as possible. +class ClosedException implements Exception { + ClosedException(); + + @override + String toString() => 'This test has been closed.'; +}
diff --git a/pkgs/test_api/lib/src/backend/compiler.dart b/pkgs/test_api/lib/src/backend/compiler.dart new file mode 100644 index 0000000..41c253d --- /dev/null +++ b/pkgs/test_api/lib/src/backend/compiler.dart
@@ -0,0 +1,59 @@ +// Copyright (c) 2023, 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. + +/// All the Dart compilers supported by the test runner. +enum Compiler { + /// The production Dart to Javascript compiler (whole world, optimizing). + dart2js('Dart2Js', 'dart2js'), + + /// Experimental Dart to Wasm compiler. + dart2wasm('Dart2Wasm', 'dart2wasm'), + + /// Compiles dart code to a native executable. + exe('Exe', 'exe'), + + /// The standard compiler for vm tests, compiles tests to kernel before + /// running them on the VM. + kernel('Kernel', 'kernel'), + + /// Runs tests directly from source, with no precompilation. + source('Source', 'source'); + + /// The compilers that are supported by the test runner by default. + static const List<Compiler> builtIn = [ + Compiler.dart2js, + Compiler.dart2wasm, + Compiler.exe, + Compiler.kernel, + Compiler.source, + ]; + + /// The human-friendly name of the compiler. + final String name; + + /// The identifier used to look up the compiler. + final String identifier; + + const Compiler(this.name, this.identifier); + + /// Converts a JSON-safe representation generated by [serialize] back into a + /// [Compiler]. + /// + /// Note that custom [Compiler] implementations are not supported. + factory Compiler.deserialize(Object serialized) => builtIn + .firstWhere((compiler) => compiler.identifier == serialized as String); + + /// Converts [this] into a JSON-safe object that can be converted back to a + /// [Compiler] using [Compiler.deserialize]. + Object serialize() => identifier; + + @override + String toString() => name; +} + +extension CompileTarget on Compiler { + bool get isJS => this == Compiler.dart2js; + + bool get isWasm => this == Compiler.dart2wasm; +}
diff --git a/pkgs/test_api/lib/src/backend/configuration/on_platform.dart b/pkgs/test_api/lib/src/backend/configuration/on_platform.dart new file mode 100644 index 0000000..02a5254 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/configuration/on_platform.dart
@@ -0,0 +1,17 @@ +// Copyright (c) 2015, 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:meta/meta_meta.dart'; + +/// An annotation for platform-specific customizations for a test suite. +/// +/// See [the README][onPlatform]. +/// +/// [onPlatform]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-specific-configuration +@Target({TargetKind.library}) +final class OnPlatform { + final Map<String, dynamic> annotationsByPlatform; + + const OnPlatform(this.annotationsByPlatform); +}
diff --git a/pkgs/test_api/lib/src/backend/configuration/retry.dart b/pkgs/test_api/lib/src/backend/configuration/retry.dart new file mode 100644 index 0000000..1811814 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/configuration/retry.dart
@@ -0,0 +1,18 @@ +// 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. + +import 'package:meta/meta_meta.dart'; + +/// An annotation for marking a test suite to be retried. +/// +/// A suite-level retry configuration will enable retries for every test in the +/// suite, unless the group or test is configured with a more specific retry. +@Target({TargetKind.library}) +final class Retry { + /// The number of times the tests in the suite will be retried. + final int count; + + /// Marks all tests in a test suite to be retried. + const Retry(this.count); +}
diff --git a/pkgs/test_api/lib/src/backend/configuration/skip.dart b/pkgs/test_api/lib/src/backend/configuration/skip.dart new file mode 100644 index 0000000..3927994 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/configuration/skip.dart
@@ -0,0 +1,18 @@ +// Copyright (c) 2015, 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:meta/meta_meta.dart'; + +/// An annotation for marking a test suite as skipped. +@Target({TargetKind.library}) +final class Skip { + /// The reason the test suite is skipped, or `null` if no reason is given. + final String? reason; + + /// Marks a suite as skipped. + /// + /// If [reason] is passed, it's included in the test output as the reason the + /// test is skipped. + const Skip([this.reason]); +}
diff --git a/pkgs/test_api/lib/src/backend/configuration/tags.dart b/pkgs/test_api/lib/src/backend/configuration/tags.dart new file mode 100644 index 0000000..dbd03f3 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/configuration/tags.dart
@@ -0,0 +1,21 @@ +// Copyright (c) 2015, 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:meta/meta_meta.dart'; + +/// An annotation for applying a set of user-defined tags to a test suite. +/// +/// See [the documentation on tagging tests][tagging tests]. +/// +/// [tagging tests]: https://github.com/dart-lang/test/blob/master/pkgs/test/README.md#tagging-tests +@Target({TargetKind.library}) +final class Tags { + /// The tags for the test suite. + Set<String> get tags => _tags.toSet(); + + final Iterable<String> _tags; + + /// Applies a set of user-defined tags to a test suite. + const Tags(this._tags); +}
diff --git a/pkgs/test_api/lib/src/backend/configuration/test_on.dart b/pkgs/test_api/lib/src/backend/configuration/test_on.dart new file mode 100644 index 0000000..dee7f40 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/configuration/test_on.dart
@@ -0,0 +1,18 @@ +// Copyright (c) 2015, 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:meta/meta_meta.dart'; + +/// An annotation indicating which platforms a test suite supports. +/// +/// For the full syntax of [expression], see [the README][]. +/// +/// [the README]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors +@Target({TargetKind.library}) +final class TestOn { + /// The expression specifying the platform. + final String expression; + + const TestOn(this.expression); +}
diff --git a/pkgs/test_api/lib/src/backend/configuration/timeout.dart b/pkgs/test_api/lib/src/backend/configuration/timeout.dart new file mode 100644 index 0000000..e51d520 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/configuration/timeout.dart
@@ -0,0 +1,155 @@ +// Copyright (c) 2015, 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:meta/meta_meta.dart'; +import 'package:string_scanner/string_scanner.dart'; + +/// A regular expression that matches text until a letter or whitespace. +/// +/// This is intended to scan through a number without actually encoding the full +/// Dart number grammar. It doesn't stop on "e" because that can be a component +/// of numbers. +final _untilUnit = RegExp(r'[^a-df-z\s]+', caseSensitive: false); + +/// A regular expression that matches a time unit. +final _unit = RegExp(r'([um]s|[dhms])', caseSensitive: false); + +/// A regular expression that matches a section of whitespace. +final _whitespace = RegExp(r'\s+'); + +/// A class representing a modification to the default timeout for a test. +/// +/// By default, a test will time out after 30 seconds. With [Timeout], that +/// can be overridden entirely; with [Timeout.factor], it can be scaled +/// relative to the default. +@Target({TargetKind.library}) +final class Timeout { + /// A constant indicating that a test should never time out. + static const none = Timeout._none(); + + /// The timeout duration. + /// + /// If set, this overrides the default duration entirely. It's `null` for + /// timeouts with a non-null [scaleFactor] and for [Timeout.none]. + final Duration? duration; + + /// The timeout factor. + /// + /// The default timeout will be multiplied by this to get the new timeout. + /// Thus a factor of 2 means that the test will take twice as long to time + /// out, and a factor of 0.5 means that it will time out twice as quickly. + /// + /// This is `null` for timeouts with a non-null [duration] and for + /// [Timeout.none]. + final num? scaleFactor; + + /// Declares an absolute timeout that overrides the default. + const Timeout(this.duration) : scaleFactor = null; + + /// Declares a relative timeout that scales the default. + const Timeout.factor(this.scaleFactor) : duration = null; + + const Timeout._none() + : scaleFactor = null, + duration = null; + + /// Parse the timeout from a user-provided string. + /// + /// This supports the following formats: + /// + /// * `Number "x"`, which produces a relative timeout with the given scale + /// factor. + /// + /// * `(Number ("d" | "h" | "m" | "s" | "ms" | "us") (" ")?)+`, which produces + /// an absolute timeout with the duration given by the sum of the given + /// units. + /// + /// * `"none"`, which produces [Timeout.none]. + /// + /// Throws a [FormatException] if [timeout] is not in a valid format + factory Timeout.parse(String timeout) { + var scanner = StringScanner(timeout); + + // First check for the string "none". + if (scanner.scan('none')) { + scanner.expectDone(); + return Timeout.none; + } + + // Scan a number. This will be either a time unit or a scale factor. + scanner.expect(_untilUnit, name: 'number'); + var number = double.parse(scanner.lastMatch![0]!); + + // A number followed by "x" is a scale factor. + if (scanner.scan('x') || scanner.scan('X')) { + scanner.expectDone(); + return Timeout.factor(number); + } + + // Parse time units until none are left. The condition is in the middle of + // the loop because we've already parsed the first number. + var microseconds = 0.0; + while (true) { + scanner.expect(_unit, name: 'unit'); + microseconds += _microsecondsFor(number, scanner.lastMatch![0]!); + + scanner.scan(_whitespace); + + // Scan the next number, if it's available. + if (!scanner.scan(_untilUnit)) break; + number = double.parse(scanner.lastMatch![0]!); + } + + scanner.expectDone(); + return Timeout(Duration(microseconds: microseconds.round())); + } + + /// Returns the number of microseconds in [number] [unit]s. + static double _microsecondsFor(double number, String unit) => switch (unit) { + 'd' => number * 24 * 60 * 60 * 1000000, + 'h' => number * 60 * 60 * 1000000, + 'm' => number * 60 * 1000000, + 's' => number * 1000000, + 'ms' => number * 1000, + 'us' => number, + _ => throw ArgumentError('Unknown unit $unit.'), + }; + + /// Returns a new [Timeout] that merges [this] with [other]. + /// + /// [Timeout.none] takes precedence over everything. If timeout is + /// [Timeout.none] and [other] declares a [duration], that takes precedence. + /// Otherwise, this timeout's [duration] or [factor] are multiplied by + /// [other]'s [factor]. + Timeout merge(Timeout other) { + if (this == none || other == none) return none; + if (other.duration != null) return Timeout(other.duration); + if (duration != null) return Timeout(duration! * other.scaleFactor!); + return Timeout.factor(scaleFactor! * other.scaleFactor!); + } + + /// Returns a new [Duration] from applying [this] to [base]. + /// + /// If this is [none], returns `null`. + Duration? apply(Duration base) { + if (this == none) return null; + return duration ?? base * scaleFactor!; + } + + @override + int get hashCode => duration.hashCode ^ 5 * scaleFactor.hashCode; + + @override + bool operator ==(Object other) => + other is Timeout && + other.duration == duration && + other.scaleFactor == scaleFactor; + + @override + String toString() { + if (duration != null) return duration.toString(); + if (scaleFactor != null) return '${scaleFactor}x'; + return 'none'; + } +}
diff --git a/pkgs/test_api/lib/src/backend/declarer.dart b/pkgs/test_api/lib/src/backend/declarer.dart new file mode 100644 index 0000000..76f7563 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/declarer.dart
@@ -0,0 +1,433 @@ +// Copyright (c) 2015, 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 'package:collection/collection.dart'; +import 'package:stack_trace/stack_trace.dart'; + +import 'configuration/timeout.dart'; +import 'group.dart'; +import 'group_entry.dart'; +import 'invoker.dart'; +import 'metadata.dart'; +import 'test.dart'; + +/// A class that manages the state of tests as they're declared. +/// +/// A nested tree of Declarers tracks the current group, set-up, and tear-down +/// functions. Each Declarer in the tree corresponds to a group. This tree is +/// tracked by a zone-scoped "current" Declarer; the current declarer can be set +/// for a block using [Declarer.declare], and it can be accessed using +/// [Declarer.current]. +class Declarer { + /// The parent declarer, or `null` if this corresponds to the root group. + final Declarer? _parent; + + /// The name of the current test group, including the name of any parent + /// groups. + /// + /// This is `null` if this is the root group. + final String? _name; + + /// The metadata for this group, including the metadata of any parent groups + /// and of the test suite. + final Metadata _metadata; + + /// The set of variables that are valid for platform selectors, in addition to + /// the built-in variables that are allowed everywhere. + final Set<String> _platformVariables; + + /// The stack trace for this group. + /// + /// This is `null` for the root (implicit) group. + final Trace? _trace; + + /// Whether to collect stack traces for [GroupEntry]s. + final bool _collectTraces; + + /// Whether to disable retries of tests. + final bool _noRetry; + + /// The set-up functions to run for each test in this group. + final _setUps = <dynamic Function()>[]; + + /// The tear-down functions to run for each test in this group. + final _tearDowns = <dynamic Function()>[]; + + /// The set-up functions to run once for this group. + final _setUpAlls = <dynamic Function()>[]; + + /// The default timeout for synthetic tests. + final _timeout = const Timeout(Duration(minutes: 12)); + + /// The trace for the first call to [setUpAll]. + /// + /// All [setUpAll]s are run in a single logical test, so they can only have + /// one trace. The first trace is most often correct, since the first + /// [setUpAll] is always run and the rest are only run if that one succeeds. + Trace? _setUpAllTrace; + + /// The tear-down functions to run once for this group. + final _tearDownAlls = <void Function()>[]; + + /// The trace for the first call to [tearDownAll]. + /// + /// All [tearDownAll]s are run in a single logical test, so they can only have + /// one trace. The first trace matches [_setUpAllTrace]. + Trace? _tearDownAllTrace; + + /// The children of this group, either tests or sub-groups. + /// + /// All modifications to this must go through [_addEntry]. + final _entries = <GroupEntry>[]; + + /// Whether [build] has been called for this declarer. + bool _built = false; + + /// The tests and/or groups that have been flagged as solo. + final _soloEntries = <GroupEntry>[]; + + /// Whether any tests and/or groups have been flagged as solo. + bool get _solo => _soloEntries.isNotEmpty; + + /// An exact full test name to match. + /// + /// When non-null only tests with exactly this name will be considered. The + /// full test name is the combination of the test case name with all group + /// prefixes. All other tests, including their metadata like `solo`, is + /// ignored. Uniqueness is not guaranteed so this may match more than one + /// test. + /// + /// Groups which are not a strict prefix of this name will be ignored. + final String? _fullTestName; + + /// The current zone-scoped declarer. + static Declarer? get current => Zone.current[#test.declarer] as Declarer?; + + /// All the test and group names that have been declared in the entire suite. + /// + /// If duplicate test names are allowed, this is not tracked and it will be + /// `null`. + final Set<String>? _seenNames; + + /// Whether this declarer is running in a standalone test executation. + /// + /// The full test runner awaits asynchronous `main` declarations, and so + /// asynchronous work can be performed in between calls to `group`, and `test` + /// etc. When running as a standalone file tests are run synchronously + /// following the first call to declare a test, so all tests must be declared + /// synchronously starting at that point. Track whether we are running in this + /// more limited mode to customize the error message for tests declared late. + final bool _isStandalone; + + /// Creates a new declarer for the root group. + /// + /// This is the implicit group that exists outside of any calls to `group()`. + /// If [metadata] is passed, it's used as the metadata for the implicit root + /// group. + /// + /// The [platformVariables] are the set of variables that are valid for + /// platform selectors in test and group metadata, in addition to the built-in + /// variables that are allowed everywhere. + /// + /// If [collectTraces] is `true`, this will set [GroupEntry.trace] for all + /// entries built by the declarer. Note that this can be noticeably slow when + /// thousands of tests are being declared (see #457). + /// + /// If [noRetry] is `true` tests will be run at most once. + /// + /// If [allowDuplicateTestNames] is `false`, then a + /// [DuplicateTestNameException] will be thrown if two tests (or groups) have + /// the same name. + Declarer({ + Metadata? metadata, + Set<String>? platformVariables, + bool collectTraces = false, + bool noRetry = false, + String? fullTestName, + // TODO: Change the default https://github.com/dart-lang/test/issues/1571 + bool allowDuplicateTestNames = true, + bool isStandalone = false, + }) : this._( + null, + null, + metadata ?? Metadata(), + platformVariables ?? const UnmodifiableSetView.empty(), + collectTraces, + null, + noRetry, + fullTestName, + allowDuplicateTestNames ? null : <String>{}, + isStandalone, + ); + + Declarer._( + this._parent, + this._name, + this._metadata, + this._platformVariables, + this._collectTraces, + this._trace, + this._noRetry, + this._fullTestName, + this._seenNames, + this._isStandalone, + ); + + /// Runs [body] with this declarer as [Declarer.current]. + /// + /// Returns the return value of [body]. + T declare<T>(T Function() body) => + runZoned(body, zoneValues: {#test.declarer: this}); + + /// Defines a test case with the given name and body. + void test(String name, dynamic Function() body, + {String? testOn, + Timeout? timeout, + Object? skip, + Map<String, dynamic>? onPlatform, + Object? tags, + int? retry, + bool solo = false}) { + _checkNotBuilt('test'); + + final fullName = _prefix(name); + if (_fullTestName != null && fullName != _fullTestName) { + return; + } + + var newMetadata = Metadata.parse( + testOn: testOn, + timeout: timeout, + skip: skip, + onPlatform: onPlatform, + tags: tags, + retry: _noRetry ? 0 : retry); + newMetadata.validatePlatformSelectors(_platformVariables); + var metadata = _metadata.merge(newMetadata); + _addEntry(LocalTest(fullName, metadata, () async { + var parents = <Declarer>[]; + for (Declarer? declarer = this; + declarer != null; + declarer = declarer._parent) { + parents.add(declarer); + } + + // Register all tear-down functions in all declarers. Iterate through + // parents outside-in so that the Invoker gets the functions in the order + // they were declared in source. + for (var declarer in parents.reversed) { + for (var tearDown in declarer._tearDowns) { + Invoker.current!.addTearDown(tearDown); + } + } + + await runZoned(() async { + await _runSetUps(); + await body(); + }, + // Make the declarer visible to running tests so that they'll throw + // useful errors when calling `test()` and `group()` within a test. + zoneValues: {#test.declarer: this}); + }, trace: _collectTraces ? Trace.current(2) : null, guarded: false)); + + if (solo) { + _soloEntries.add(_entries.last); + } + } + + /// Creates a group of tests. + void group(String name, void Function() body, + {String? testOn, + Timeout? timeout, + Object? skip, + Map<String, dynamic>? onPlatform, + Object? tags, + int? retry, + bool solo = false}) { + _checkNotBuilt('group'); + + final fullTestPrefix = _prefix(name); + if (_fullTestName != null && !_fullTestName.startsWith(fullTestPrefix)) { + return; + } + + var newMetadata = Metadata.parse( + testOn: testOn, + timeout: timeout, + skip: skip, + onPlatform: onPlatform, + tags: tags, + retry: _noRetry ? 0 : retry); + newMetadata.validatePlatformSelectors(_platformVariables); + var metadata = _metadata.merge(newMetadata); + var trace = _collectTraces ? Trace.current(2) : null; + + var declarer = Declarer._( + this, + fullTestPrefix, + metadata, + _platformVariables, + _collectTraces, + trace, + _noRetry, + _fullTestName, + _seenNames, + _isStandalone, + ); + declarer.declare(() { + // Cast to dynamic to avoid the analyzer complaining about us using the + // result of a void method. + var result = (body as dynamic)(); + if (result is! Future) return; + throw ArgumentError('Groups may not be async.'); + }); + _addEntry(declarer.build()); + + if (solo || declarer._solo) { + _soloEntries.add(_entries.last); + } + } + + /// Returns [name] prefixed with this declarer's group name. + String _prefix(String name) => _name == null ? name : '$_name $name'; + + /// Registers a function to be run before each test in this group. + void setUp(dynamic Function() callback) { + _checkNotBuilt('setUp'); + _setUps.add(callback); + } + + /// Registers a function to be run after each test in this group. + void tearDown(dynamic Function() callback) { + _checkNotBuilt('tearDown'); + _tearDowns.add(callback); + } + + /// Registers a function to be run once before all tests. + void setUpAll(dynamic Function() callback) { + _checkNotBuilt('setUpAll'); + if (_collectTraces) _setUpAllTrace ??= Trace.current(2); + _setUpAlls.add(callback); + } + + /// Registers a function to be run once after all tests. + void tearDownAll(dynamic Function() callback) { + _checkNotBuilt('tearDownAll'); + if (_collectTraces) _tearDownAllTrace ??= Trace.current(2); + _tearDownAlls.add(callback); + } + + /// Like [tearDownAll], but called from within a running [setUpAll] test to + /// dynamically add a [tearDownAll]. + void addTearDownAll(dynamic Function() callback) => + _tearDownAlls.add(callback); + + /// Finalizes and returns the group being declared. + /// + /// **Note**: The tests in this group must be run in a [Invoker.guard] + /// context; otherwise, test errors won't be captured. + Group build() { + _checkNotBuilt('build'); + + _built = true; + var entries = _entries.map((entry) { + if (_solo && !_soloEntries.contains(entry)) { + entry = LocalTest( + entry.name, + entry.metadata + .change(skip: true, skipReason: 'does not have "solo"'), + () {}); + } + return entry; + }).toList(); + + return Group(_name ?? '', entries, + metadata: _metadata, + trace: _trace, + setUpAll: _setUpAll, + tearDownAll: _tearDownAll); + } + + /// Throws a [StateError] if [build] has been called. + /// + /// [name] should be the name of the method being called. + void _checkNotBuilt(String name) { + if (!_built) return; + final restrictionMessage = _isStandalone + ? 'When running a test as an executable directly ' + '(not as a suite by the test runner), ' + 'tests must be declared in a synchronous block.\n' + 'If async work is required before any tests are run ' + 'use a `setUpAll` callback.\n' + 'If async work cannot be avoided before declaring tests, ' + 'all async events must be complete before declaring the first test.' + : 'If async work is required before any tests are run ' + 'use a `setUpAll` callback.\n' + 'If async work cannot be avoided before declaring tests it must ' + 'all be awaited within the Future returned from `main`.'; + throw StateError("Can't call $name() once tests have begun running.\n" + '$restrictionMessage'); + } + + /// Run the set-up functions for this and any parent groups. + /// + /// If no set-up functions are declared, this returns a [Future] that + /// completes immediately. + Future _runSetUps() async { + if (_parent != null) await _parent._runSetUps(); + // TODO: why does type inference not work here? + await Future.forEach<Function>(_setUps, (setUp) => setUp()); + } + + /// Returns a [Test] that runs the callbacks in [_setUpAll], or `null`. + Test? get _setUpAll { + if (_setUpAlls.isEmpty) return null; + + return LocalTest(_prefix('(setUpAll)'), _metadata.change(timeout: _timeout), + () { + return runZoned( + () => Future.forEach<Function>(_setUpAlls, (setUp) => setUp()), + // Make the declarer visible to running scaffolds so they can add to + // the declarer's `tearDownAll()` list. + zoneValues: {#test.declarer: this}); + }, trace: _setUpAllTrace, guarded: false, isScaffoldAll: true); + } + + /// Returns a [Test] that runs the callbacks in [_tearDownAll], or `null`. + Test? get _tearDownAll { + // We have to create a tearDownAll if there's a setUpAll, since it might + // dynamically add tear-down code using [addTearDownAll]. + if (_setUpAlls.isEmpty && _tearDownAlls.isEmpty) return null; + + return LocalTest( + _prefix('(tearDownAll)'), _metadata.change(timeout: _timeout), () { + return runZoned(() => Invoker.current!.runTearDowns(_tearDownAlls), + // Make the declarer visible to running scaffolds so they can add to + // the declarer's `tearDownAll()` list. + zoneValues: {#test.declarer: this}); + }, trace: _tearDownAllTrace, guarded: false, isScaffoldAll: true); + } + + void _addEntry(GroupEntry entry) { + if (_seenNames?.add(entry.name) == false) { + throw DuplicateTestNameException(entry.name); + } + _entries.add(entry); + } +} + +/// An exception thrown when two test cases in the same test suite (same `main`) +/// have an identical name. +class DuplicateTestNameException implements Exception { + final String name; + DuplicateTestNameException(this.name); + + @override + String toString() => 'A test with the name "$name" was already declared. ' + 'Test cases must have unique names.\n\n' + 'See https://github.com/dart-lang/test/blob/master/pkgs/test/doc/' + 'configuration.md#allow_test_randomization for info on enabling this.'; +}
diff --git a/pkgs/test_api/lib/src/backend/group.dart b/pkgs/test_api/lib/src/backend/group.dart new file mode 100644 index 0000000..9a28875 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/group.dart
@@ -0,0 +1,90 @@ +// Copyright (c) 2015, 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:stack_trace/stack_trace.dart'; + +import 'group_entry.dart'; +import 'metadata.dart'; +import 'suite_platform.dart'; +import 'test.dart'; + +/// A group contains one or more tests and subgroups. +/// +/// It includes metadata that applies to all contained tests. +class Group implements GroupEntry { + @override + final String name; + + @override + final Metadata metadata; + + @override + final Trace? trace; + + /// The children of this group. + final List<GroupEntry> entries; + + /// Returns a new root-level group. + Group.root(Iterable<GroupEntry> entries, {Metadata? metadata}) + : this('', entries, metadata: metadata); + + /// A test to run before all tests in the group. + /// + /// This is `null` if no `setUpAll` callbacks were declared. + final Test? setUpAll; + + /// A test to run after all tests in the group. + /// + /// This is `null` if no `tearDown` callbacks were declared. + final Test? tearDownAll; + + /// The number of tests (recursively) in this group. + int get testCount { + if (_testCount != null) return _testCount!; + _testCount = entries.fold<int>( + 0, (count, entry) => count + (entry is Group ? entry.testCount : 1)); + return _testCount!; + } + + int? _testCount; + + Group(this.name, Iterable<GroupEntry> entries, + {Metadata? metadata, this.trace, this.setUpAll, this.tearDownAll}) + : entries = List<GroupEntry>.unmodifiable(entries), + metadata = metadata ?? Metadata(); + + @override + Group? forPlatform(SuitePlatform platform) { + if (!metadata.testOn.evaluate(platform)) return null; + var newMetadata = metadata.forPlatform(platform); + var filtered = _map((entry) => entry.forPlatform(platform)); + if (filtered.isEmpty && entries.isNotEmpty) return null; + return Group(name, filtered, + metadata: newMetadata, + trace: trace, + setUpAll: setUpAll, + tearDownAll: tearDownAll); + } + + @override + Group? filter(bool Function(Test) callback) { + var filtered = _map((entry) => entry.filter(callback)); + if (filtered.isEmpty && entries.isNotEmpty) return null; + return Group(name, filtered, + metadata: metadata, + trace: trace, + setUpAll: setUpAll, + tearDownAll: tearDownAll); + } + + /// Returns the entries of this group mapped using [callback]. + /// + /// Any `null` values returned by [callback] will be removed. + List<GroupEntry> _map(GroupEntry? Function(GroupEntry) callback) { + return entries + .map((entry) => callback(entry)) + .whereType<GroupEntry>() + .toList(); + } +}
diff --git a/pkgs/test_api/lib/src/backend/group_entry.dart b/pkgs/test_api/lib/src/backend/group_entry.dart new file mode 100644 index 0000000..a6f30bd --- /dev/null +++ b/pkgs/test_api/lib/src/backend/group_entry.dart
@@ -0,0 +1,39 @@ +// Copyright (c) 2015, 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:stack_trace/stack_trace.dart'; + +import 'metadata.dart'; +import 'suite_platform.dart'; +import 'test.dart'; + +/// A [Test] or [Group]. +abstract class GroupEntry { + /// The name of the entry, including the prefixes from any containing + /// [Group]s. + /// + /// This will be empty for the root group. + String get name; + + /// The metadata for the entry, including the metadata from any containing + /// [Group]s. + Metadata get metadata; + + /// The stack trace for the call to `test()` or `group()` that defined this + /// entry, or `null` if the entry was defined in a different way. + Trace? get trace; + + /// Returns a copy of [this] with all platform-specific metadata resolved. + /// + /// Removes any tests and groups with [Metadata.testOn] selectors that don't + /// match [platform]. Returns `null` if this entry's selector doesn't match. + GroupEntry? forPlatform(SuitePlatform platform); + + /// Returns a copy of [this] with all tests that don't match [callback] + /// removed. + /// + /// Returns `null` if this is a test that doesn't match [callback] or a group + /// where no child tests match [callback]. + GroupEntry? filter(bool Function(Test) callback); +}
diff --git a/pkgs/test_api/lib/src/backend/invoker.dart b/pkgs/test_api/lib/src/backend/invoker.dart new file mode 100644 index 0000000..58f1001 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/invoker.dart
@@ -0,0 +1,464 @@ +// Copyright (c) 2015, 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 'package:stack_trace/stack_trace.dart'; + +import 'closed_exception.dart'; +import 'declarer.dart'; +import 'group.dart'; +import 'live_test.dart'; +import 'live_test_controller.dart'; +import 'message.dart'; +import 'metadata.dart'; +import 'state.dart'; +import 'suite.dart'; +import 'suite_platform.dart'; +import 'test.dart'; +import 'test_failure.dart'; +import 'util/pretty_print.dart'; + +/// A test in this isolate. +class LocalTest extends Test { + @override + final String name; + + @override + final Metadata metadata; + + @override + final Trace? trace; + + /// Whether this is a test defined using `setUpAll()` or `tearDownAll()`. + final bool isScaffoldAll; + + /// The test body. + final void Function() _body; + + /// Whether the test is run in its own error zone. + final bool _guarded; + + /// Creates a new [LocalTest]. + /// + /// If [guarded] is `true`, the test is run in its own error zone, and any + /// errors that escape that zone cause the test to fail. If it's `false`, it's + /// the caller's responsibility to invoke [LiveTest.run] in the context of a + /// call to [Invoker.guard]. + LocalTest(this.name, this.metadata, this._body, + {this.trace, bool guarded = true, this.isScaffoldAll = false}) + : _guarded = guarded; + + LocalTest._(this.name, this.metadata, this._body, this.trace, this._guarded, + this.isScaffoldAll); + + /// Loads a single runnable instance of this test. + @override + LiveTest load(Suite suite, {Iterable<Group>? groups}) { + var invoker = Invoker._(suite, this, groups: groups, guarded: _guarded); + return invoker.liveTest; + } + + @override + Test? forPlatform(SuitePlatform platform) { + if (!metadata.testOn.evaluate(platform)) return null; + return LocalTest._(name, metadata.forPlatform(platform), _body, trace, + _guarded, isScaffoldAll); + } +} + +/// The class responsible for managing the lifecycle of a single local test. +/// +/// The current invoker is accessible within the zone scope of the running test +/// using [Invoker.current]. It's used to track asynchronous callbacks and +/// report asynchronous errors. +class Invoker { + /// The live test being driven by the invoker. + /// + /// This provides a view into the state of the test being executed. + LiveTest get liveTest => _controller; + late final LiveTestController _controller; + + /// Whether to run this test in its own error zone. + final bool _guarded; + + /// Whether the user code is allowed to interact with the invoker despite it + /// being closed. + /// + /// A test is generally closed because the runner is shutting down (in + /// response to a signal) or because the test's suite is finished. + /// Typically calls to [addTearDown] and [addOutstandingCallback] are only + /// allowed before the test is closed. Tear down callbacks, however, are + /// allowed to perform these interactions to facilitate resource cleanup on a + /// best-effort basis, so the invoker is made to appear open only within the + /// zones running the teardown callbacks. + bool get _forceOpen => Zone.current[_forceOpenForTearDownKey] as bool; + + /// An opaque object used as a key in the zone value map to identify + /// [_forceOpen]. + /// + /// This is an instance variable to ensure that multiple invokers don't step + /// on one anothers' toes. + final _forceOpenForTearDownKey = Object(); + + /// Whether the test has been closed. + /// + /// Once the test is closed, [expect] and [expectAsync] will throw + /// [ClosedException]s whenever accessed to help the test stop executing as + /// soon as possible. + bool get closed => !_forceOpen && _onCloseCompleter.isCompleted; + + /// A future that completes once the test has been closed. + Future<void> get onClose => _onCloseCompleter.future; + final _onCloseCompleter = Completer<void>(); + + /// The test being run. + LocalTest get _test => liveTest.test as LocalTest; + + /// The outstanding callback counter for the current zone. + _AsyncCounter get _outstandingCallbacks { + var counter = Zone.current[_counterKey] as _AsyncCounter?; + if (counter != null) return counter; + throw StateError("Can't add or remove outstanding callbacks outside " + 'of a test body.'); + } + + /// All the zones created by [_waitForOutstandingCallbacks], in the order they + /// were created. + /// + /// This is used to throw timeout errors in the most recent zone. + final _outstandingCallbackZones = <Zone>[]; + + /// An opaque object used as a key in the zone value map to identify + /// [_outstandingCallbacks]. + /// + /// This is an instance variable to ensure that multiple invokers don't step + /// on one anothers' toes. + final _counterKey = Object(); + + /// The number of times this [liveTest] has been run. + int _runCount = 0; + + /// The current invoker, or `null` if none is defined. + /// + /// An invoker is only set within the zone scope of a running test. + static Invoker? get current { + // TODO(nweiz): Use a private symbol when dart2js supports it (issue 17526). + return Zone.current[#test.invoker] as Invoker?; + } + + /// Runs [callback] in a zone where unhandled errors from [LiveTest]s are + /// caught and dispatched to the appropriate [Invoker]. + static T? guard<T>(T Function() callback) => + runZoned<T?>(callback, zoneSpecification: ZoneSpecification( + // Use [handleUncaughtError] rather than [onError] so we can + // capture [zone] and with it the outstanding callback counter for + // the zone in which [error] was thrown. + handleUncaughtError: (self, _, zone, error, stackTrace) { + var invoker = zone[#test.invoker] as Invoker?; + if (invoker != null) { + self.parent!.run(() => invoker._handleError(zone, error, stackTrace)); + } else { + self.parent!.handleUncaughtError(error, stackTrace); + } + })); + + /// The timer for tracking timeouts. + /// + /// This will be `null` until the test starts running. + Timer? _timeoutTimer; + + /// The tear-down functions to run when this test finishes. + final _tearDowns = <void Function()>[]; + + /// Messages to print if and when this test fails. + final _printsOnFailure = <String>[]; + + Invoker._(Suite suite, LocalTest test, + {Iterable<Group>? groups, bool guarded = true}) + : _guarded = guarded { + _controller = LiveTestController( + suite, test, _onRun, _onCloseCompleter.complete, + groups: groups); + } + + /// Runs [callback] after this test completes. + /// + /// The [callback] may return a [Future]. Like all tear-downs, callbacks are + /// run in the reverse of the order they're declared. + void addTearDown(dynamic Function() callback) { + if (closed) throw ClosedException(); + + if (_test.isScaffoldAll) { + Declarer.current!.addTearDownAll(callback); + } else { + _tearDowns.add(callback); + } + } + + /// Tells the invoker that there's a callback running that it should wait for + /// before considering the test successful. + /// + /// Each call to [addOutstandingCallback] should be followed by a call to + /// [removeOutstandingCallback] once the callback is no longer running. Note + /// that only successful tests wait for outstanding callbacks; as soon as a + /// test experiences an error, any further calls to [addOutstandingCallback] + /// or [removeOutstandingCallback] will do nothing. + /// + /// Throws a [ClosedException] if this test has been closed. + void addOutstandingCallback() { + if (closed) throw ClosedException(); + _outstandingCallbacks.increment(); + } + + /// Tells the invoker that a callback declared with [addOutstandingCallback] + /// is no longer running. + void removeOutstandingCallback() { + heartbeat(); + _outstandingCallbacks.decrement(); + } + + /// Run [tearDowns] in reverse order. + /// + /// An exception thrown in a tearDown callback will cause the test to fail, if + /// it isn't already failing, but it won't prevent the remaining callbacks + /// from running. This invoker will not be closeable within the zone that the + /// teardowns are running in. + Future<void> runTearDowns(List<FutureOr<void> Function()> tearDowns) { + heartbeat(); + return runZoned(() async { + while (tearDowns.isNotEmpty) { + var completer = Completer<void>(); + + addOutstandingCallback(); + _waitForOutstandingCallbacks(() { + Future.sync(tearDowns.removeLast()).whenComplete(completer.complete); + }).then((_) => removeOutstandingCallback()).unawaited; + + await completer.future; + } + }, zoneValues: {_forceOpenForTearDownKey: true}); + } + + /// Runs [fn] and completes once [fn] and all outstanding callbacks registered + /// within [fn] have completed. + /// + /// Outstanding callbacks registered within [fn] will *not* be registered as + /// outstanding callback outside of [fn]. + Future<void> _waitForOutstandingCallbacks(FutureOr<void> Function() fn) { + heartbeat(); + + Zone? zone; + var counter = _AsyncCounter(); + runZoned(() async { + zone = Zone.current; + _outstandingCallbackZones.add(zone!); + try { + await fn(); + } finally { + counter.decrement(); + } + }, zoneValues: {_counterKey: counter}); + + return counter.onZero.whenComplete(() { + _outstandingCallbackZones.remove(zone!); + }); + } + + /// Notifies the invoker that progress is being made. + /// + /// Each heartbeat resets the timeout timer. This helps ensure that + /// long-running tests that still make progress don't time out. + void heartbeat() { + if (liveTest.isComplete) return; + if (_timeoutTimer != null) _timeoutTimer!.cancel(); + if (liveTest.suite.ignoreTimeouts == true) return; + + const defaultTimeout = Duration(seconds: 30); + var timeout = liveTest.test.metadata.timeout.apply(defaultTimeout); + if (timeout == null) return; + String message() { + var message = 'Test timed out after ${niceDuration(timeout)}.'; + if (timeout == defaultTimeout) { + message += ' See https://pub.dev/packages/test#timeouts'; + } + return message; + } + + _timeoutTimer = Zone.root.createTimer(timeout, () { + _outstandingCallbackZones.last.run(() { + _handleError(Zone.current, TimeoutException(message(), timeout)); + _outstandingCallbacks.complete(); + }); + }); + } + + /// Marks the current test as skipped. + /// + /// If passed, [message] is emitted as a skip message. + /// + /// Note that this *does not* mark the test as complete. That is, it sets + /// the result to [Result.skipped], but doesn't change the state. + void skip([String? message]) { + if (liveTest.state.shouldBeDone) { + // Set the state explicitly so we don't get an extra error about the test + // failing after being complete. + _controller.setState(const State(Status.complete, Result.error)); + throw 'This test was marked as skipped after it had already completed.\n' + 'Make sure to use a matching library which informs the test runner\n' + 'of pending async work.'; + } + + if (message != null) _controller.message(Message.skip(message)); + // TODO: error if the test is already complete. + _controller.setState(const State(Status.pending, Result.skipped)); + } + + /// Prints [message] if and when this test fails. + void printOnFailure(String message) { + message = message.trim(); + if (liveTest.state.result.isFailing) { + _print('\n$message'); + } else { + _printsOnFailure.add(message); + } + } + + /// Notifies the invoker of an asynchronous error. + /// + /// The [zone] is the zone in which the error was thrown. + void _handleError(Zone zone, Object error, [StackTrace? stackTrace]) { + // Ignore errors propagated from previous test runs + if (_runCount != zone[#runCount]) return; + + // Get the chain information from the zone in which the error was thrown. + zone.run(() { + if (stackTrace == null) { + stackTrace = Chain.current(); + } else { + stackTrace = Chain.forTrace(stackTrace!); + } + }); + + // Store these here because they'll change when we set the state below. + var shouldBeDone = liveTest.state.shouldBeDone; + + if (error is! TestFailure) { + _controller.setState(const State(Status.complete, Result.error)); + } else if (liveTest.state.result != Result.error) { + _controller.setState(const State(Status.complete, Result.failure)); + } + + _controller.addError(error, stackTrace!); + + if (_printsOnFailure.isNotEmpty) { + _print(_printsOnFailure.join('\n\n')); + _printsOnFailure.clear(); + } + + // If a test was supposed to be done but then had an error, that indicates + // that it was poorly-written and could be flaky. + if (!shouldBeDone) return; + + // However, users don't think of load tests as "tests", so the error isn't + // helpful for them. + if (liveTest.suite.isLoadSuite) return; + + _handleError( + zone, + 'This test failed after it had already completed.\n' + 'Make sure to use a matching library which informs the test runner\n' + 'of pending async work.', + stackTrace); + } + + /// The method that's run when the test is started. + void _onRun() { + _controller.setState(const State(Status.running, Result.success)); + + _runCount++; + Chain.capture(() { + _guardIfGuarded(() { + runZoned(() async { + // Run the test asynchronously so that the "running" state change + // has a chance to hit its event handler(s) before the test produces + // an error. If an error is emitted before the first state change is + // handled, we can end up with [onError] callbacks firing before the + // corresponding [onStateChange], which violates the timing + // guarantees. + // + // Use the event loop over the microtask queue to avoid starvation. + await Future(() {}); + + await _waitForOutstandingCallbacks(_test._body); + await _waitForOutstandingCallbacks(() => runTearDowns(_tearDowns)); + + if (_timeoutTimer != null) _timeoutTimer!.cancel(); + + if (liveTest.state.result != Result.success && + _runCount < liveTest.test.metadata.retry + 1) { + _controller.message(Message.print('Retry: ${liveTest.test.name}')); + _onRun(); + return; + } + + _controller.setState(State(Status.complete, liveTest.state.result)); + + _controller.completer.complete(); + }, + zoneValues: { + #test.invoker: this, + _forceOpenForTearDownKey: false, + #runCount: _runCount, + }, + zoneSpecification: + ZoneSpecification(print: (_, __, ___, line) => _print(line))); + }); + }, when: liveTest.test.metadata.chainStackTraces, errorZone: false); + } + + /// Runs [callback], in a [Invoker.guard] context if [_guarded] is `true`. + void _guardIfGuarded(void Function() callback) { + if (_guarded) { + Invoker.guard(callback); + } else { + callback(); + } + } + + /// Prints [text] as a message to [_controller]. + void _print(String text) => _controller.message(Message.print(text)); +} + +/// A manually incremented/decremented counter that completes a [Future] the +/// first time it reaches zero or is forcefully completed. +class _AsyncCounter { + var _count = 1; + + /// A Future that completes the first time the counter reaches 0. + Future<void> get onZero => _completer.future; + final _completer = Completer<void>(); + + void increment() { + _count++; + } + + void decrement() { + _count--; + if (_count != 0) return; + if (_completer.isCompleted) return; + _completer.complete(); + } + + /// Force [onZero] to complete. + /// + /// No effect if [onZero] has already completed. + void complete() { + if (!_completer.isCompleted) _completer.complete(); + } +} + +extension<T> on Future<T> { + void get unawaited {} +}
diff --git a/pkgs/test_api/lib/src/backend/live_test.dart b/pkgs/test_api/lib/src/backend/live_test.dart new file mode 100644 index 0000000..683e87c --- /dev/null +++ b/pkgs/test_api/lib/src/backend/live_test.dart
@@ -0,0 +1,148 @@ +// Copyright (c) 2015, 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 'group.dart'; +import 'message.dart'; +import 'state.dart'; +import 'suite.dart'; +import 'test.dart'; + +/// A runnable instance of a test. +/// +/// This is distinct from [Test] in order to keep [Test] immutable. Running a +/// test requires state, and [LiveTest] provides a view of the state of the test +/// as it runs. +/// +/// If the state changes, [state] will be updated before [onStateChange] fires. +/// Likewise, if an error is caught, it will be added to [errors] before being +/// emitted via [onError]. If an error causes a state change, [onStateChange] +/// will fire before [onError]. If an error or other state change causes the +/// test to complete, [onComplete] will complete after [onStateChange] and +/// [onError] fire. +abstract class LiveTest { + /// The suite within which this test is being run. + Suite get suite; + + /// The groups within which this test is being run, from the outermost to the + /// innermost. + /// + /// This will always contain at least the implicit top-level group. + List<Group> get groups; + + /// The running test. + Test get test; + + /// The current state of the running test. + /// + /// This starts as [Status.pending] and [Result.success]. It will be updated + /// before [onStateChange] fires. + /// + /// Note that even if this is marked [Status.complete], the test may still be + /// running code asynchronously. A test is considered complete either once it + /// hits its first error or when all [expectAsync] callbacks have been called + /// and any returned [Future] has completed, but it's possible for further + /// processing to happen, which may cause further errors. It's even possible + /// for a test that was marked [Status.complete] and [Result.success] to be + /// marked as [Result.error] later. + State get state; + + /// Returns whether this test has completed. + /// + /// This is equivalent to [state.status] being [Status.complete]. + /// + /// Note that even if this returns `true`, the test may still be + /// running code asynchronously. A test is considered complete either once it + /// hits its first error or when all [expectAsync] callbacks have been called + /// and any returned [Future] has completed, but it's possible for further + /// processing to happen, which may cause further errors. + bool get isComplete => state.status == Status.complete; + + // A stream that emits a new [State] whenever [state] changes. + // + // This will only ever emit a [State] if it's different than the previous + // [state]. It will emit an event after [state] has been updated. Note that + // since this is an asynchronous stream, it's possible for [state] not to + // match the [State] that it emits within the [Stream.listen] callback. + Stream<State> get onStateChange; + + /// An unmodifiable list of all errors that have been caught while running + /// this test. + /// + /// This will be updated before [onError] fires. These errors are not + /// guaranteed to have the same types as when they were thrown; for example, + /// they may need to be serialized across isolate boundaries. The stack traces + /// will be [Chain]s. + List<AsyncError> get errors; + + /// A stream that emits a new [AsyncError] whenever an error is caught. + /// + /// This will emit an event after [errors] is updated. These errors are not + /// guaranteed to have the same types as when they were thrown; for example, + /// they may need to be serialized across isolate boundaries. The stack traces + /// will be [Chain]s. + Stream<AsyncError> get onError; + + /// A stream that emits messages produced by the test. + Stream<Message> get onMessage; + + /// A [Future] that completes once the test is complete. + /// + /// This will complete after [onStateChange] has fired, and after [onError] + /// has fired if the test completes because of an error. It's the same as the + /// [Future] returned by [run]. + /// + /// Note that even once this completes, the test may still be running code + /// asynchronously. A test is considered complete either once it hits its + /// first error or when all [expectAsync] callbacks have been called and any + /// returned [Future] has completed, but it's possible for further processing + /// to happen, which may cause further errors. + Future get onComplete; + + /// The name of this live test without any group prefixes. + String get individualName { + var group = groups.last; + if (group.name.isEmpty) return test.name; + if (!test.name.startsWith(group.name)) return test.name; + + // The test will have the same name as the group for virtual tests created + // to represent skipping the entire group. + if (test.name.length == group.name.length) return ''; + + return test.name.substring(group.name.length + 1); + } + + /// Loads a copy of this [LiveTest] that's able to be run again. + LiveTest copy() => test.load(suite, groups: groups); + + /// Signals that this test should start running as soon as possible. + /// + /// A test may not start running immediately for various reasons specific to + /// the means by which it's defined. Until it starts running, [state] will + /// continue to be marked [Status.pending]. + /// + /// This returns the same [Future] as [onComplete]. It may not be called more + /// than once. + Future run(); + + /// Signals that this test should stop emitting events and release any + /// resources it may have allocated. + /// + /// Once [close] is called, [onComplete] will complete if it hasn't already + /// and [onStateChange] and [onError] will close immediately. This means that, + /// if the test was running at the time [close] is called, it will never emit + /// a [Status.complete] state-change event. Once a test is closed, [expect] + /// and [expectAsync] will throw a [ClosedException] to help the test + /// terminate as quickly as possible. + /// + /// This doesn't automatically happen after the test completes because there + /// may be more asynchronous work going on in the background that could + /// produce new errors. + /// + /// Returns a [Future] that completes once all resources are released *and* + /// the test has completed. This allows the caller to wait until the test's + /// tear-down logic has run. + Future close(); +}
diff --git a/pkgs/test_api/lib/src/backend/live_test_controller.dart b/pkgs/test_api/lib/src/backend/live_test_controller.dart new file mode 100644 index 0000000..956c293 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/live_test_controller.dart
@@ -0,0 +1,178 @@ +// Copyright (c) 2015, 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:collection'; + +import 'package:stack_trace/stack_trace.dart'; + +import 'group.dart'; +import 'live_test.dart'; +import 'message.dart'; +import 'state.dart'; +import 'suite.dart'; +import 'test.dart'; + +/// A concrete [LiveTest] that enforces some lifecycle guarantees. +/// +/// This automatically handles some of [LiveTest]'s guarantees, but for the most +/// part it's the caller's responsibility to make sure everything gets +/// dispatched in the correct order. +class LiveTestController extends LiveTest { + @Deprecated('Use this instance instead') + LiveTest get liveTest => this; + + @override + final Suite suite; + + @override + final List<Group> groups; + + @override + final Test test; + + /// The function that will actually start the test running. + final void Function() _onRun; + + /// A function to run when the test is closed. + /// + /// This may be `null`. + final void Function() _onClose; + + /// The list of errors caught by the test. + final _errors = <AsyncError>[]; + + @override + List<AsyncError> get errors => UnmodifiableListView(_errors); + + /// The current state of the test. + @override + State state = const State(Status.pending, Result.success); + + /// The controller for [onStateChange]. + /// + /// This is synchronous to ensure that events are well-ordered across multiple + /// streams. + final _onStateChange = StreamController<State>.broadcast(sync: true); + @override + Stream<State> get onStateChange => _onStateChange.stream; + + /// The controller for [onError]. + /// + /// This is synchronous to ensure that events are well-ordered across multiple + /// streams. + final _onError = StreamController<AsyncError>.broadcast(sync: true); + @override + Stream<AsyncError> get onError => _onError.stream; + + /// The controller for [onMessage]. + /// + /// This is synchronous to ensure that events are well-ordered across multiple + /// streams. + final _onMessage = StreamController<Message>.broadcast(sync: true); + @override + Stream<Message> get onMessage => _onMessage.stream; + + final completer = Completer<void>(); + + /// Whether [run] has been called. + var _runCalled = false; + + /// Whether [close] has been called. + bool get _isClosed => _onError.isClosed; + + /// Creates a new controller for a [LiveTest]. + /// + /// [test] is the test being run; [suite] is the suite that contains it. + /// + /// [onRun] is a function that's called from [LiveTest.run]. It should start + /// the test running. The controller takes care of ensuring that + /// [LiveTest.run] isn't called more than once and that [LiveTest.onComplete] + /// is returned. + /// + /// [onClose] is a function that's called the first time [LiveTest.close] is + /// called. It should clean up any resources that have been allocated for the + /// test and ensure that the test finishes quickly if it's still running. It + /// will only be called if [onRun] has been called first. + /// + /// If [groups] is passed, it's used to populate the list of groups that + /// contain this test. Otherwise, `suite.group` is used. + LiveTestController(this.suite, this.test, this._onRun, this._onClose, + {Iterable<Group>? groups}) + : groups = groups == null ? [suite.group] : List.unmodifiable(groups); + + /// Adds an error to the [LiveTest]. + /// + /// This both adds the error to [LiveTest.errors] and emits it via + /// [LiveTest.onError]. [stackTrace] is automatically converted into a [Chain] + /// if it's not one already. + void addError(Object error, StackTrace? stackTrace) { + if (_isClosed) return; + + var asyncError = AsyncError( + error, Chain.forTrace(stackTrace ?? StackTrace.fromString(''))); + _errors.add(asyncError); + _onError.add(asyncError); + } + + /// Sets the current state of the [LiveTest] to [newState]. + /// + /// If [newState] is different than the old state, this both sets + /// [LiveTest.state] and emits the new state via [LiveTest.onStateChanged]. If + /// it's not different, this does nothing. + void setState(State newState) { + if (_isClosed) return; + if (state == newState) return; + + state = newState; + _onStateChange.add(newState); + } + + /// Emits message over [LiveTest.onMessage]. + void message(Message message) { + if (_onMessage.hasListener) { + _onMessage.add(message); + } else { + // Make sure all messages get surfaced one way or another to aid in + // debugging. + Zone.root.print(message.text); + } + } + + @override + Future<void> run() { + if (_runCalled) { + throw StateError('LiveTest.run() may not be called more than once.'); + } else if (_isClosed) { + throw StateError('LiveTest.run() may not be called for a closed ' + 'test.'); + } + _runCalled = true; + + _onRun(); + return onComplete; + } + + /// Returns a future that completes when the test is complete. + /// + /// We also wait for the state to transition to Status.complete. + @override + Future<void> get onComplete => completer.future; + + @override + Future<void> close() { + if (_isClosed) return onComplete; + + _onStateChange.close(); + _onError.close(); + + if (_runCalled) { + _onClose(); + } else { + completer.complete(); + } + + return onComplete; + } +}
diff --git a/pkgs/test_api/lib/src/backend/message.dart b/pkgs/test_api/lib/src/backend/message.dart new file mode 100644 index 0000000..a027460 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/message.dart
@@ -0,0 +1,41 @@ +// Copyright (c) 2016, 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. + +/// A message emitted by a test. +/// +/// A message encompasses any textual information that should be presented to +/// the user. Reporters are encouraged to visually distinguish different message +/// types. +class Message { + final MessageType type; + + final String text; + + Message(this.type, this.text); + + Message.print(this.text) : type = MessageType.print; + Message.skip(this.text) : type = MessageType.skip; +} + +class MessageType { + /// A message explicitly printed by the user's test. + static const print = MessageType._('print'); + + /// A message indicating that a test, or some portion of one, was skipped. + static const skip = MessageType._('skip'); + + /// The name of the message type. + final String name; + + factory MessageType.parse(String name) => switch (name) { + 'print' => MessageType.print, + 'skip' => MessageType.skip, + _ => throw ArgumentError('Invalid message type "$name".'), + }; + + const MessageType._(this.name); + + @override + String toString() => name; +}
diff --git a/pkgs/test_api/lib/src/backend/metadata.dart b/pkgs/test_api/lib/src/backend/metadata.dart new file mode 100644 index 0000000..d0663b1 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/metadata.dart
@@ -0,0 +1,420 @@ +// Copyright (c) 2015, 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:boolean_selector/boolean_selector.dart'; +import 'package:collection/collection.dart'; + +import 'configuration/skip.dart'; +import 'configuration/timeout.dart'; +import 'platform_selector.dart'; +import 'suite_platform.dart'; +import 'util/identifier_regex.dart'; +import 'util/pretty_print.dart'; + +/// Metadata for a test or test suite. +/// +/// This metadata comes from declarations on the test itself; it doesn't include +/// configuration from the user. +final class Metadata { + /// Empty metadata with only default values. + /// + /// Using this is slightly more efficient than manually constructing a new + /// metadata with no arguments. + static final empty = Metadata._(); + + /// The selector indicating which platforms the suite supports. + final PlatformSelector testOn; + + /// The modification to the timeout for the test or suite. + final Timeout timeout; + + /// Whether the test or suite should be skipped. + bool get skip => _skip ?? false; + final bool? _skip; + + /// The reason the test or suite should be skipped, if given. + final String? skipReason; + + /// Whether to use verbose stack traces. + bool get verboseTrace => _verboseTrace ?? false; + final bool? _verboseTrace; + + /// Whether to chain stack traces. + bool get chainStackTraces => _chainStackTraces ?? _verboseTrace ?? false; + final bool? _chainStackTraces; + + /// The user-defined tags attached to the test or suite. + final Set<String> tags; + + /// The number of times to re-run a test before being marked as a failure. + int get retry => _retry ?? 0; + final int? _retry; + + /// Platform-specific metadata. + /// + /// Each key identifies a platform, and its value identifies the specific + /// metadata for that platform. These can be applied by calling [forPlatform]. + final Map<PlatformSelector, Metadata> onPlatform; + + /// Metadata that applies only when specific tags are applied. + /// + /// Tag-specific metadata is applied when merging this with other metadata. + /// Note that unlike [onPlatform], the base metadata takes precedence over any + /// tag-specific metadata. + /// + /// This is guaranteed not to have any keys that match [tags]; those are + /// resolved when the metadata is constructed. + final Map<BooleanSelector, Metadata> forTag; + + /// The language version comment, if one is present. + /// + /// Only available for test suites and not individual tests. + final String? languageVersionComment; + + /// Parses a user-provided map into the value for [onPlatform]. + static Map<PlatformSelector, Metadata> _parseOnPlatform( + Map<String, dynamic>? onPlatform) { + if (onPlatform == null) return {}; + + var result = <PlatformSelector, Metadata>{}; + onPlatform.forEach((platform, metadata) { + var selector = PlatformSelector.parse(platform); + if (metadata is Timeout || metadata is Skip) { + result[selector] = _parsePlatformOptions(platform, [metadata]); + } else if (metadata is List) { + result[selector] = _parsePlatformOptions(platform, metadata); + } else { + throw ArgumentError('Metadata for platform "$platform" must be a ' + 'Timeout, Skip, or List of those; was "$metadata".'); + } + }); + return result; + } + + static Metadata _parsePlatformOptions( + String platform, List<dynamic> metadata) { + Timeout? timeout; + dynamic skip; + for (var metadatum in metadata) { + if (metadatum is Timeout) { + if (timeout != null) { + throw ArgumentError('Only a single Timeout may be declared for ' + '"$platform".'); + } + + timeout = metadatum; + } else if (metadatum is Skip) { + if (skip != null) { + throw ArgumentError('Only a single Skip may be declared for ' + '"$platform".'); + } + + skip = metadatum.reason ?? true; + } else { + throw ArgumentError('Metadata for platform "$platform" must be a ' + 'Timeout, Skip, or List of those; was "$metadata".'); + } + } + + return Metadata.parse(timeout: timeout, skip: skip); + } + + /// Parses a user-provided [String] or [Iterable] into the value for [tags]. + /// + /// Throws an [ArgumentError] if [tags] is not a [String] or an [Iterable]. + static Set<String> _parseTags(Object? tags) { + if (tags == null) return {}; + if (tags is String) return {tags}; + if (tags is! Iterable) { + throw ArgumentError.value( + tags, 'tags', 'must be either a String or an Iterable.'); + } + + if (tags.any((tag) => tag is! String)) { + throw ArgumentError.value(tags, 'tags', 'must contain only Strings.'); + } + + return Set.from(tags); + } + + /// Creates new Metadata. + /// + /// [testOn] defaults to [PlatformSelector.all]. + /// + /// If [forTag] contains metadata that applies to [tags], that metadata is + /// included inline in the returned value. The values directly passed to the + /// constructor take precedence over tag-specific metadata. + factory Metadata( + {PlatformSelector? testOn, + Timeout? timeout, + bool? skip, + bool? verboseTrace, + bool? chainStackTraces, + int? retry, + String? skipReason, + Iterable<String>? tags, + Map<PlatformSelector, Metadata>? onPlatform, + Map<BooleanSelector, Metadata>? forTag, + String? languageVersionComment}) { + // Returns metadata without forTag resolved at all. + Metadata unresolved() => Metadata._( + testOn: testOn, + timeout: timeout, + skip: skip, + verboseTrace: verboseTrace, + chainStackTraces: chainStackTraces, + retry: retry, + skipReason: skipReason, + tags: tags, + onPlatform: onPlatform, + forTag: forTag, + languageVersionComment: languageVersionComment); + + // If there's no tag-specific metadata, or if none of it applies, just + // return the metadata as-is. + if (forTag == null || tags == null) return unresolved(); + tags = Set.from(tags); + forTag = Map.from(forTag); + + // Otherwise, resolve the tag-specific components. Doing this eagerly means + // we only have to resolve suite- or group-level tags once, rather than + // doing it for every test individually. + var empty = Metadata._(); + var merged = forTag.keys.toList().fold(empty, (Metadata merged, selector) { + if (!selector.evaluate(tags!.contains)) return merged; + return merged.merge(forTag!.remove(selector)!); + }); + + if (merged == empty) return unresolved(); + return merged.merge(unresolved()); + } + + /// Creates new Metadata. + /// + /// Unlike [Metadata], this assumes [forTag] is already resolved. + Metadata._({ + PlatformSelector? testOn, + Timeout? timeout, + bool? skip, + this.skipReason, + bool? verboseTrace, + bool? chainStackTraces, + int? retry, + Iterable<String>? tags, + Map<PlatformSelector, Metadata>? onPlatform, + Map<BooleanSelector, Metadata>? forTag, + this.languageVersionComment, + }) : testOn = testOn ?? PlatformSelector.all, + timeout = timeout ?? const Timeout.factor(1), + _skip = skip, + _verboseTrace = verboseTrace, + _chainStackTraces = chainStackTraces, + _retry = retry, + tags = UnmodifiableSetView(tags == null ? {} : tags.toSet()), + onPlatform = + onPlatform == null ? const {} : UnmodifiableMapView(onPlatform), + forTag = forTag == null ? const {} : UnmodifiableMapView(forTag) { + if (retry != null) RangeError.checkNotNegative(retry, 'retry'); + _validateTags(); + } + + /// Creates a new Metadata, but with fields parsed from caller-friendly values + /// where applicable. + /// + /// Throws a [FormatException] if any field is invalid. + Metadata.parse( + {String? testOn, + Timeout? timeout, + dynamic skip, + bool? verboseTrace, + bool? chainStackTraces, + int? retry, + Map<String, dynamic>? onPlatform, + Object? /* String|Iterable<String> */ tags, + this.languageVersionComment}) + : testOn = testOn == null + ? PlatformSelector.all + : PlatformSelector.parse(testOn), + timeout = timeout ?? const Timeout.factor(1), + _skip = skip == null ? null : skip != false, + _verboseTrace = verboseTrace, + _chainStackTraces = chainStackTraces, + _retry = retry, + skipReason = skip is String ? skip : null, + onPlatform = _parseOnPlatform(onPlatform), + tags = _parseTags(tags), + forTag = const {} { + if (skip != null && skip is! String && skip is! bool) { + throw ArgumentError('"skip" must be a String or a bool, was "$skip".'); + } + + if (retry != null) RangeError.checkNotNegative(retry, 'retry'); + + _validateTags(); + } + + /// Deserializes the result of [Metadata.serialize] into a new [Metadata]. + Metadata.deserialize(Map serialized) + : testOn = serialized['testOn'] == null + ? PlatformSelector.all + : PlatformSelector.parse(serialized['testOn'] as String), + timeout = _deserializeTimeout(serialized['timeout']), + _skip = serialized['skip'] as bool?, + skipReason = serialized['skipReason'] as String?, + _verboseTrace = serialized['verboseTrace'] as bool?, + _chainStackTraces = serialized['chainStackTraces'] as bool?, + _retry = (serialized['retry'] as num?)?.toInt(), + tags = Set.from(serialized['tags'] as Iterable), + onPlatform = { + for (var pair in serialized['onPlatform'] as List) + PlatformSelector.parse(pair.first as String): + Metadata.deserialize(pair.last as Map) + }, + forTag = (serialized['forTag'] as Map).map((key, nested) => MapEntry( + BooleanSelector.parse(key as String), + Metadata.deserialize(nested as Map))), + languageVersionComment = + serialized['languageVersionComment'] as String?; + + /// Deserializes timeout from the format returned by [_serializeTimeout]. + static Timeout _deserializeTimeout(Object? serialized) { + if (serialized == 'none') return Timeout.none; + var scaleFactor = (serialized as Map)['scaleFactor']; + if (scaleFactor != null) return Timeout.factor(scaleFactor as num); + return Timeout( + Duration(microseconds: (serialized['duration'] as num).toInt())); + } + + /// Throws an [ArgumentError] if any tags in [tags] aren't hyphenated + /// identifiers. + void _validateTags() { + var invalidTags = tags + .where((tag) => !tag.contains(anchoredHyphenatedIdentifier)) + .map((tag) => '"$tag"') + .toList(); + + if (invalidTags.isEmpty) return; + + throw ArgumentError("Invalid ${pluralize('tag', invalidTags.length)} " + '${toSentence(invalidTags)}. Tags must be (optionally hyphenated) ' + 'Dart identifiers.'); + } + + /// Throws a [FormatException] if any [PlatformSelector]s use any variables + /// that don't appear either in [validVariables] or in the set of variables + /// that are known to be valid for all selectors. + void validatePlatformSelectors(Set<String> validVariables) { + testOn.validate(validVariables); + onPlatform.forEach((selector, metadata) { + selector.validate(validVariables); + metadata.validatePlatformSelectors(validVariables); + }); + } + + /// Return a new [Metadata] that merges [this] with [other]. + /// + /// If the two [Metadata]s have conflicting properties, [other] wins. If + /// either has a [forTag] metadata for one of the other's tags, that metadata + /// is merged as well. + Metadata merge(Metadata other) => Metadata( + testOn: testOn.intersection(other.testOn), + timeout: timeout.merge(other.timeout), + skip: other._skip ?? _skip, + skipReason: other.skipReason ?? skipReason, + verboseTrace: other._verboseTrace ?? _verboseTrace, + chainStackTraces: other._chainStackTraces ?? _chainStackTraces, + retry: other._retry ?? _retry, + tags: tags.union(other.tags), + onPlatform: mergeMaps(onPlatform, other.onPlatform, + value: (metadata1, metadata2) => metadata1.merge(metadata2)), + forTag: mergeMaps(forTag, other.forTag, + value: (metadata1, metadata2) => metadata1.merge(metadata2)), + languageVersionComment: + other.languageVersionComment ?? languageVersionComment); + + /// Returns a copy of [this] with the given fields changed. + Metadata change( + {PlatformSelector? testOn, + Timeout? timeout, + bool? skip, + bool? verboseTrace, + bool? chainStackTraces, + int? retry, + String? skipReason, + Map<PlatformSelector, Metadata>? onPlatform, + Set<String>? tags, + Map<BooleanSelector, Metadata>? forTag, + String? languageVersionComment}) { + testOn ??= this.testOn; + timeout ??= this.timeout; + skip ??= _skip; + verboseTrace ??= _verboseTrace; + chainStackTraces ??= _chainStackTraces; + retry ??= _retry; + skipReason ??= this.skipReason; + onPlatform ??= this.onPlatform; + tags ??= this.tags; + forTag ??= this.forTag; + languageVersionComment ??= this.languageVersionComment; + return Metadata( + testOn: testOn, + timeout: timeout, + skip: skip, + verboseTrace: verboseTrace, + chainStackTraces: chainStackTraces, + skipReason: skipReason, + onPlatform: onPlatform, + tags: tags, + forTag: forTag, + retry: retry, + languageVersionComment: languageVersionComment); + } + + /// Returns a copy of [this] with all platform-specific metadata from + /// [onPlatform] resolved. + Metadata forPlatform(SuitePlatform platform) { + if (onPlatform.isEmpty) return this; + + var metadata = this; + onPlatform.forEach((platformSelector, platformMetadata) { + if (!platformSelector.evaluate(platform)) return; + metadata = metadata.merge(platformMetadata); + }); + return metadata.change(onPlatform: {}); + } + + /// Serializes [this] into a JSON-safe object that can be deserialized using + /// [Metadata.deserialize]. + Map<String, dynamic> serialize() { + // Make this a list to guarantee that the order is preserved. + var serializedOnPlatform = <List<Object>>[]; + onPlatform.forEach((key, value) { + serializedOnPlatform.add([key.toString(), value.serialize()]); + }); + + return { + 'testOn': testOn == PlatformSelector.all ? null : testOn.toString(), + 'timeout': _serializeTimeout(timeout), + 'skip': _skip, + 'skipReason': skipReason, + 'verboseTrace': _verboseTrace, + 'chainStackTraces': _chainStackTraces, + 'retry': _retry, + 'tags': tags.toList(), + 'onPlatform': serializedOnPlatform, + 'forTag': forTag.map((selector, metadata) => + MapEntry(selector.toString(), metadata.serialize())), + 'languageVersionComment': languageVersionComment, + }; + } + + /// Serializes timeout into a JSON-safe object. + dynamic _serializeTimeout(Timeout timeout) { + if (timeout == Timeout.none) return 'none'; + return { + 'duration': timeout.duration?.inMicroseconds, + 'scaleFactor': timeout.scaleFactor + }; + } +}
diff --git a/pkgs/test_api/lib/src/backend/operating_system.dart b/pkgs/test_api/lib/src/backend/operating_system.dart new file mode 100644 index 0000000..a86c1e7 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/operating_system.dart
@@ -0,0 +1,74 @@ +// Copyright (c) 2015, 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. + +/// An enum of all operating systems supported by Dart. +/// +/// This is used for selecting which operating systems a test can run on. Even +/// for browser tests, this indicates the operating system of the machine +/// running the test runner. +class OperatingSystem { + /// Microsoft Windows. + static const windows = OperatingSystem._('Windows', 'windows'); + + /// Mac OS X. + static const macOS = OperatingSystem._('OS X', 'mac-os'); + + /// GNU/Linux. + static const linux = OperatingSystem._('Linux', 'linux'); + + /// Android. + /// + /// Since this is the operating system the test runner is running on, this + /// won't be true when testing remotely on an Android browser. + static const android = OperatingSystem._('Android', 'android'); + + /// iOS. + /// + /// Since this is the operating system the test runner is running on, this + /// won't be true when testing remotely on an iOS browser. + static const iOS = OperatingSystem._('iOS', 'ios'); + + /// No operating system. + /// + /// This is used when running in the browser, or if an unrecognized operating + /// system is used. It can't be referenced by name in platform selectors. + static const none = OperatingSystem._('none', 'none'); + + /// A list of all instances of [OperatingSystem] other than [none]. + static const all = [windows, macOS, linux, android, iOS]; + + /// Finds an operating system by its name. + /// + /// If no operating system is found, returns [none]. + static OperatingSystem find(String identifier) => + all.firstWhere((platform) => platform.identifier == identifier, + orElse: () => none); + + /// Finds an operating system by the return value from `dart:io`'s + /// `Platform.operatingSystem`. + /// + /// If no operating system is found, returns [none]. + static OperatingSystem findByIoName(String name) => switch (name) { + 'windows' => windows, + 'macos' => macOS, + 'linux' => linux, + 'android' => android, + 'ios' => iOS, + _ => none, + }; + + /// The human-friendly of the operating system. + final String name; + + /// The identifier used to look up the operating system. + final String identifier; + + /// Whether this is a POSIX-ish operating system. + bool get isPosix => this != windows && this != none; + + const OperatingSystem._(this.name, this.identifier); + + @override + String toString() => name; +}
diff --git a/pkgs/test_api/lib/src/backend/platform_selector.dart b/pkgs/test_api/lib/src/backend/platform_selector.dart new file mode 100644 index 0000000..c65dc8b --- /dev/null +++ b/pkgs/test_api/lib/src/backend/platform_selector.dart
@@ -0,0 +1,116 @@ +// Copyright (c) 2015, 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:boolean_selector/boolean_selector.dart'; +import 'package:source_span/source_span.dart'; + +import 'compiler.dart'; +import 'operating_system.dart'; +import 'runtime.dart'; +import 'suite_platform.dart'; + +/// The set of variable names that are valid for all platform selectors. +final _universalValidVariables = { + 'posix', + 'dart-vm', + 'browser', + 'js', + 'blink', + 'google', + 'wasm', + for (var runtime in Runtime.builtIn) runtime.identifier, + for (var compiler in Compiler.builtIn) compiler.identifier, + for (var os in OperatingSystem.all) os.identifier, +}; + +/// An expression for selecting certain platforms, including operating systems +/// and browsers. +/// +/// This uses the [boolean selector][] syntax. +/// +/// [boolean selector]: https://pub.dev/packages/boolean_selector +final class PlatformSelector { + /// A selector that declares that a test can be run on all platforms. + static const all = PlatformSelector._(BooleanSelector.all); + + /// The boolean selector used to implement this selector. + final BooleanSelector _inner; + + /// The source span from which this selector was parsed. + final SourceSpan? _span; + + /// Parses [selector]. + /// + /// If [span] is passed, it indicates the location of the text for [selector] + /// in a larger document. It's used for error reporting. + PlatformSelector.parse(String selector, [SourceSpan? span]) + : _inner = + _wrapFormatException(() => BooleanSelector.parse(selector), span), + _span = span; + + const PlatformSelector._(this._inner) : _span = null; + + /// Runs [body] and wraps any [FormatException] it throws in a + /// [SourceSpanFormatException] using [span]. + /// + /// If [span] is `null`, runs [body] as-is. + static T _wrapFormatException<T>(T Function() body, [SourceSpan? span]) { + if (span == null) return body(); + + try { + return body(); + } on FormatException catch (error) { + throw SourceSpanFormatException(error.message, span); + } + } + + /// Throws a [FormatException] if this selector uses any variables that don't + /// appear either in [validVariables] or in the set of variables that are + /// known to be valid for all selectors. + void validate(Set<String> validVariables) { + if (identical(this, all)) return; + + _wrapFormatException( + () => _inner.validate((name) => + _universalValidVariables.contains(name) || + validVariables.contains(name)), + _span); + } + + /// Returns whether the selector matches the given [platform]. + bool evaluate(SuitePlatform platform) => + _inner.evaluate((String variable) => switch (variable) { + _ + when variable == platform.runtime.identifier || + variable == platform.runtime.parent?.identifier || + variable == platform.os.identifier || + variable == platform.compiler.identifier => + true, + 'dart-vm' => platform.runtime.isDartVM, + 'browser' => platform.runtime.isBrowser, + 'js' => platform.compiler.isJS, + 'blink' => platform.runtime.isBlink, + 'posix' => platform.os.isPosix, + 'google' => platform.inGoogle, + 'wasm' => platform.compiler.isWasm, + _ => false, + }); + + /// Returns a new [PlatformSelector] that matches only platforms matched by + /// both [this] and [other]. + PlatformSelector intersection(PlatformSelector other) { + if (other == PlatformSelector.all) return this; + return PlatformSelector._(_inner.intersection(other._inner)); + } + + @override + String toString() => _inner.toString(); + + @override + bool operator ==(Object other) => + other is PlatformSelector && _inner == other._inner; + + @override + int get hashCode => _inner.hashCode; +}
diff --git a/pkgs/test_api/lib/src/backend/remote_exception.dart b/pkgs/test_api/lib/src/backend/remote_exception.dart new file mode 100644 index 0000000..0d70561 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/remote_exception.dart
@@ -0,0 +1,88 @@ +// Copyright (c) 2015, 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 'package:stack_trace/stack_trace.dart'; + +import 'test_failure.dart'; + +/// An exception that was thrown remotely. +/// +/// This could be an exception thrown in a different isolate, a different +/// process, or on an entirely different computer. +final class RemoteException implements Exception { + /// The original exception's message, if it had one. + /// + /// If the original exception was a plain string, this will contain that + /// string. + final String? message; + + /// The value of the original exception's `runtimeType.toString()`. + final String type; + + /// The value of the original exception's `toString()`. + final String _toString; + + /// Serializes [error] and [stackTrace] into a JSON-safe object. + /// + /// Other than JSON- and isolate-safety, no guarantees are made about the + /// serialized format. + static Map<String, dynamic> serialize(dynamic error, StackTrace stackTrace) { + String? message; + if (error is String) { + message = error; + } else { + try { + message = error.message.toString(); + } on NoSuchMethodError catch (_) { + // Do nothing. + } + } + + final supertype = (error is TestFailure) ? 'TestFailure' : null; + + return { + 'message': message, + 'type': error.runtimeType.toString(), + 'supertype': supertype, + 'toString': error.toString(), + 'stackChain': Chain.forTrace(stackTrace).toString() + }; + } + + /// Deserializes an exception serialized with [RemoteException.serialize]. + /// + /// The returned [AsyncError] is guaranteed to have a [RemoteException] as its + /// error and a [Chain] as its stack trace. + static AsyncError deserialize(Map serialized) { + return AsyncError(_deserializeException(serialized), + Chain.parse(serialized['stackChain'] as String)); + } + + /// Deserializes the exception portion of [serialized]. + static RemoteException _deserializeException(Map serialized) { + final message = serialized['message'] as String?; + final type = serialized['type'] as String; + final toString = serialized['toString'] as String; + + return switch (serialized['supertype'] as String?) { + 'TestFailure' => _RemoteTestFailure(message, type, toString), + _ => RemoteException._(message, type, toString), + }; + } + + RemoteException._(this.message, this.type, this._toString); + + @override + String toString() => _toString; +} + +/// A subclass of [RemoteException] that implements [TestFailure]. +/// +/// It's important to preserve [TestFailure]-ness, because tests have different +/// results depending on whether an exception was a failure or an error. +final class _RemoteTestFailure extends RemoteException implements TestFailure { + _RemoteTestFailure(super.message, super.type, super.toString) : super._(); +}
diff --git a/pkgs/test_api/lib/src/backend/remote_listener.dart b/pkgs/test_api/lib/src/backend/remote_listener.dart new file mode 100644 index 0000000..66a0814 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/remote_listener.dart
@@ -0,0 +1,276 @@ +// Copyright (c) 2016, 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 'package:async/async.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:term_glyph/term_glyph.dart' as glyph; + +import 'declarer.dart'; +import 'group.dart'; +import 'invoker.dart'; +import 'live_test.dart'; +import 'metadata.dart'; +import 'remote_exception.dart'; +import 'stack_trace_formatter.dart'; +import 'suite.dart'; +import 'suite_channel_manager.dart'; +import 'suite_platform.dart'; +import 'test.dart'; + +final class RemoteListener { + /// The test suite to run. + final Suite _suite; + + /// The zone to forward prints to, or `null` if prints shouldn't be forwarded. + final Zone? _printZone; + + /// Extracts metadata about all the tests in the function returned by + /// [getMain] and returns a channel that will send information about them. + /// + /// The main function is wrapped in a closure so that we can handle it being + /// undefined here rather than in the generated code. + /// + /// Once that's done, this starts listening for commands about which tests to + /// run. + /// + /// If [hidePrints] is `true` (the default), calls to `print()` within this + /// suite will not be forwarded to the parent zone's print handler. However, + /// the caller may want them to be forwarded in (for example) a browser + /// context where they'll be visible in the development console. + /// + /// If [beforeLoad] is passed, it's called before the tests have been declared + /// for this worker. + static StreamChannel<Object?> start(Function Function() getMain, + {bool hidePrints = true, + Future Function( + StreamChannel<Object?> Function(String name) suiteChannel)? + beforeLoad}) { + // Synchronous in order to allow `print` output to show up immediately, even + // if they are followed by long running synchronous work. + var controller = + StreamChannelController<Object?>(allowForeignErrors: false, sync: true); + var channel = MultiChannel<Object?>(controller.local); + + var verboseChain = true; + + var printZone = hidePrints ? null : Zone.current; + var spec = ZoneSpecification(print: (_, __, ___, line) { + if (printZone != null) printZone.print(line); + channel.sink.add({'type': 'print', 'line': line}); + }); + + final suiteChannelManager = SuiteChannelManager(); + StackTraceFormatter().asCurrent(() { + runZonedGuarded(() async { + Function? main; + try { + main = getMain(); + } on NoSuchMethodError catch (_) { + _sendLoadException(channel, 'No top-level main() function defined.'); + return; + } catch (error, stackTrace) { + _sendError(channel, error, stackTrace, verboseChain); + return; + } + + if (main is! FutureOr<void> Function()) { + _sendLoadException( + channel, 'Top-level main() function takes arguments.'); + return; + } + + var queue = StreamQueue(channel.stream); + var message = await queue.next as Map; + assert(message['type'] == 'initial'); + + queue.rest.cast<Map>().listen((message) { + if (message['type'] == 'close') { + controller.local.sink.close(); + return; + } + + assert(message['type'] == 'suiteChannel'); + suiteChannelManager.connectIn(message['name'] as String, + channel.virtualChannel((message['id'] as num).toInt())); + }); + + if ((message['asciiGlyphs'] as bool?) ?? false) glyph.ascii = true; + var metadata = Metadata.deserialize(message['metadata'] as Map); + verboseChain = metadata.verboseTrace; + var declarer = Declarer( + metadata: metadata, + platformVariables: Set.from(message['platformVariables'] as Iterable), + collectTraces: message['collectTraces'] as bool, + noRetry: message['noRetry'] as bool, + // TODO: Change to non-nullable https://github.com/dart-lang/test/issues/1591 + allowDuplicateTestNames: + message['allowDuplicateTestNames'] as bool? ?? true, + ); + StackTraceFormatter.current!.configure( + except: _deserializeSet(message['foldTraceExcept'] as List), + only: _deserializeSet(message['foldTraceOnly'] as List)); + + if (beforeLoad != null) { + await beforeLoad(suiteChannelManager.connectOut); + } + + await declarer.declare(main); + + var suite = Suite( + declarer.build(), + SuitePlatform.deserialize(message['platform'] as Object), + path: message['path'] as String, + ignoreTimeouts: message['ignoreTimeouts'] as bool? ?? false, + ); + + runZoned(() { + Invoker.guard( + () => RemoteListener._(suite, printZone)._listen(channel)); + }, + // Make the declarer visible to running tests so that they'll throw + // useful errors when calling `test()` and `group()` within a test, + // and so they can add to the declarer's `tearDownAll()` list. + zoneValues: {#test.declarer: declarer}); + }, (error, stackTrace) { + _sendError(channel, error, stackTrace, verboseChain); + }, zoneSpecification: spec); + }); + + return controller.foreign; + } + + /// Returns a [Set] from a JSON serialized list of strings, or `null` if the + /// list is empty or `null`. + static Set<String>? _deserializeSet(List? list) { + if (list == null) return null; + if (list.isEmpty) return null; + return Set.from(list); + } + + /// Sends a message over [channel] indicating that the tests failed to load. + /// + /// [message] should describe the failure. + static void _sendLoadException(StreamChannel channel, String message) { + channel.sink.add({'type': 'loadException', 'message': message}); + } + + /// Sends a message over [channel] indicating an error from user code. + static void _sendError(StreamChannel channel, Object error, + StackTrace stackTrace, bool verboseChain) { + channel.sink.add({ + 'type': 'error', + 'error': RemoteException.serialize( + error, + StackTraceFormatter.current! + .formatStackTrace(stackTrace, verbose: verboseChain)) + }); + } + + RemoteListener._(this._suite, this._printZone); + + /// Send information about [_suite] across [channel] and start listening for + /// commands to run the tests. + void _listen(MultiChannel channel) { + channel.sink.add({ + 'type': 'success', + 'root': _serializeGroup(channel, _suite.group, []) + }); + } + + /// Serializes [group] into a JSON-safe map. + /// + /// [parents] lists the groups that contain [group]. + Map _serializeGroup( + MultiChannel channel, Group group, Iterable<Group> parents) { + parents = parents.toList()..add(group); + return { + 'type': 'group', + 'name': group.name, + 'metadata': group.metadata.serialize(), + 'trace': group.trace == null + ? null + : StackTraceFormatter.current + ?.formatStackTrace(group.trace!) + .toString() ?? + group.trace?.toString(), + 'setUpAll': _serializeTest(channel, group.setUpAll, parents), + 'tearDownAll': _serializeTest(channel, group.tearDownAll, parents), + 'entries': group.entries.map((entry) { + return entry is Group + ? _serializeGroup(channel, entry, parents) + : _serializeTest(channel, entry as Test, parents); + }).toList() + }; + } + + /// Serializes [test] into a JSON-safe map. + /// + /// [groups] lists the groups that contain [test]. Returns `null` if [test] + /// is `null`. + Map? _serializeTest( + MultiChannel channel, Test? test, Iterable<Group>? groups) { + if (test == null) return null; + + var testChannel = channel.virtualChannel(); + testChannel.stream.listen((message) { + assert(message['command'] == 'run'); + _runLiveTest(test.load(_suite, groups: groups), + channel.virtualChannel((message['channel'] as num).toInt())); + }); + + return { + 'type': 'test', + 'name': test.name, + 'metadata': test.metadata.serialize(), + 'trace': test.trace == null + ? null + : StackTraceFormatter.current + ?.formatStackTrace(test.trace!) + .toString() ?? + test.trace?.toString(), + 'channel': testChannel.id + }; + } + + /// Runs [liveTest] and sends the results across [channel]. + void _runLiveTest(LiveTest liveTest, MultiChannel channel) { + channel.stream.listen((message) { + assert(message['command'] == 'close'); + liveTest.close(); + }); + + liveTest.onStateChange.listen((state) { + channel.sink.add({ + 'type': 'state-change', + 'status': state.status.name, + 'result': state.result.name + }); + }); + + liveTest.onError.listen((asyncError) { + channel.sink.add({ + 'type': 'error', + 'error': RemoteException.serialize( + asyncError.error, + StackTraceFormatter.current!.formatStackTrace(asyncError.stackTrace, + verbose: liveTest.test.metadata.verboseTrace)) + }); + }); + + liveTest.onMessage.listen((message) { + if (_printZone != null) _printZone.print(message.text); + channel.sink.add({ + 'type': 'message', + 'message-type': message.type.name, + 'text': message.text + }); + }); + + runZoned(() { + liveTest.run().then((_) => channel.sink.add({'type': 'complete'})); + }, zoneValues: {#test.runner.test_channel: channel}); + } +}
diff --git a/pkgs/test_api/lib/src/backend/runtime.dart b/pkgs/test_api/lib/src/backend/runtime.dart new file mode 100644 index 0000000..ceef227 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/runtime.dart
@@ -0,0 +1,193 @@ +// Copyright (c) 2015, 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 'compiler.dart'; + +/// An enum of all Dart runtimes supported by the test runner. +final class Runtime { + // When adding new runtimes, be sure to update the baseline and derived + // variable tests in test/backend/platform_selector/evaluate_test. + + /// The command-line Dart VM. + static const Runtime vm = Runtime('VM', 'vm', Compiler.kernel, + [Compiler.kernel, Compiler.source, Compiler.exe], + isDartVM: true); + + /// Google Chrome. + static const Runtime chrome = Runtime('Chrome', 'chrome', Compiler.dart2js, + [Compiler.dart2js, Compiler.dart2wasm], + isBrowser: true, isBlink: true); + + /// Mozilla Firefox. + static const Runtime firefox = Runtime('Firefox', 'firefox', Compiler.dart2js, + [Compiler.dart2js, Compiler.dart2wasm], + isBrowser: true); + + /// Apple Safari. + static const Runtime safari = Runtime( + 'Safari', 'safari', Compiler.dart2js, [Compiler.dart2js], + isBrowser: true); + + /// Microsoft Internet Explorer. + @Deprecated('Internet Explorer is no longer supported') + static const Runtime internetExplorer = Runtime( + 'Internet Explorer', 'ie', Compiler.dart2js, [Compiler.dart2js], + isBrowser: true); + + /// Microsoft Edge (based on Chromium). + static const Runtime edge = Runtime( + 'Microsoft Edge', 'edge', Compiler.dart2js, [Compiler.dart2js], + isBrowser: true, isBlink: true); + + /// The command-line Node.js VM. + static const Runtime nodeJS = Runtime('Node.js', 'node', Compiler.dart2js, + [Compiler.dart2js, Compiler.dart2wasm]); + + /// The platforms that are supported by the test runner by default. + static const List<Runtime> builtIn = [ + Runtime.vm, + Runtime.chrome, + Runtime.firefox, + Runtime.safari, + Runtime.edge, + Runtime.nodeJS, + ]; + + /// The human-friendly name of the platform. + final String name; + + /// The identifier used to look up the platform. + final String identifier; + + /// The parent platform that this is based on, or `null` if there is no + /// parent. + final Runtime? parent; + + /// Returns whether this is a child of another platform. + bool get isChild => parent != null; + + /// Whether this platform runs the Dart VM in any capacity. + final bool isDartVM; + + /// Whether this platform is a browser. + final bool isBrowser; + + /// Whether this platform uses the Blink rendering engine. + final bool isBlink; + + /// Whether this platform has no visible window. + final bool isHeadless; + + /// Returns the platform this is based on, or [this] if it's not based on + /// anything. + /// + /// That is, returns [parent] if it's non-`null` or [this] if it's `null`. + Runtime get root => parent ?? this; + + /// The default compiler to use with this runtime. + final Compiler defaultCompiler; + + /// All the supported compilers for this runtime. + final List<Compiler> supportedCompilers; + + const Runtime( + this.name, this.identifier, this.defaultCompiler, this.supportedCompilers, + {this.isDartVM = false, + this.isBrowser = false, + this.isBlink = false, + this.isHeadless = false}) + : parent = null; + + Runtime._child(this.name, this.identifier, this.defaultCompiler, + this.supportedCompilers, Runtime this.parent) + : isDartVM = parent.isDartVM, + isBrowser = parent.isBrowser, + isBlink = parent.isBlink, + isHeadless = parent.isHeadless; + + /// Converts a JSON-safe representation generated by [serialize] back into a + /// [Runtime]. + factory Runtime.deserialize(Object serialized) { + if (serialized is String) { + return builtIn + .firstWhere((platform) => platform.identifier == serialized); + } + + var map = serialized as Map; + var name = map['name'] as String; + var identifier = map['identifier'] as String; + var defaultCompiler = + Compiler.deserialize(map['defaultCompiler'] as Object); + var supportedCompilers = [ + for (var compiler in map['supportedCompilers'] as List) + Compiler.deserialize(compiler as Object), + ]; + + var parent = map['parent']; + if (parent != null) { + // Note that the returned platform's [parent] won't necessarily be `==` to + // a separately-deserialized parent platform. This should be fine, though, + // since we only deserialize platforms in the remote execution context + // where they're only used to evaluate platform selectors. + return Runtime._child(name, identifier, defaultCompiler, + supportedCompilers, Runtime.deserialize(parent as Object)); + } + + return Runtime(name, identifier, defaultCompiler, supportedCompilers, + isDartVM: map['isDartVM'] as bool, + isBrowser: map['isBrowser'] as bool, + isBlink: map['isBlink'] as bool, + isHeadless: map['isHeadless'] as bool); + } + + /// Converts [this] into a JSON-safe object that can be converted back to a + /// [Runtime] using [Runtime.deserialize]. + Object serialize() { + if (builtIn.contains(this)) return identifier; + + if (parent != null) { + return { + 'name': name, + 'defaultCompiler': defaultCompiler.serialize(), + 'supportedCompilers': [ + for (var compiler in supportedCompilers) compiler.serialize(), + ], + 'identifier': identifier, + 'parent': parent!.serialize() + }; + } + + return { + 'name': name, + 'defaultCompiler': defaultCompiler.serialize(), + 'supportedCompilers': [ + for (var compiler in supportedCompilers) compiler.serialize(), + ], + 'identifier': identifier, + 'isDartVM': isDartVM, + 'isBrowser': isBrowser, + 'isBlink': isBlink, + 'isHeadless': isHeadless, + // TODO(https://github.com/dart-lang/test/issues/2146): Remove this. + 'isJS': isBrowser || this == Runtime.nodeJS, + // TODO(https://github.com/dart-lang/test/issues/2146): Remove this. + 'isWasm': false, + }; + } + + /// Returns a child of [this] that counts as both this platform's identifier + /// and the new [identifier]. + /// + /// This may not be called on a platform that's already a child. + Runtime extend(String name, String identifier) { + if (parent == null) { + return Runtime._child( + name, identifier, defaultCompiler, supportedCompilers, this); + } + throw StateError('A child platform may not be extended.'); + } + + @override + String toString() => name; +}
diff --git a/pkgs/test_api/lib/src/backend/stack_trace_formatter.dart b/pkgs/test_api/lib/src/backend/stack_trace_formatter.dart new file mode 100644 index 0000000..a495e89 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/stack_trace_formatter.dart
@@ -0,0 +1,75 @@ +// 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. + +import 'dart:async'; + +import 'package:stack_trace/stack_trace.dart'; + +import 'invoker.dart'; +import 'stack_trace_mapper.dart'; + +/// The key used to look up [StackTraceFormatter.current] in a zone. +final _currentKey = Object(); + +/// A class that tracks how to format a stack trace according to the user's +/// configuration. +/// +/// This can convert JavaScript stack traces to Dart using source maps, and fold +/// irrelevant frames out of the stack trace. +final class StackTraceFormatter { + /// A class that converts [trace] into a Dart stack trace, or `null` to use it + /// as-is. + StackTraceMapper? _mapper; + + /// The set of packages to fold when producing terse [Chain]s. + var _except = {'matcher', 'stream_channel', 'test', 'test_api'}; + + /// If non-empty, all packages not in this list will be folded when producing + /// terse [Chain]s. + var _only = <String>{}; + + /// Returns the current manager, or `null` if this isn't called within a call + /// to [asCurrent]. + static StackTraceFormatter? get current => + Zone.current[_currentKey] as StackTraceFormatter?; + + /// Runs [body] with this as [StackTraceFormatter.current]. + /// + /// This is zone-scoped, so this will be the current configuration in any + /// asynchronous callbacks transitively created by [body]. + T asCurrent<T>(T Function() body) => + runZoned(body, zoneValues: {_currentKey: this}); + + /// Configure how stack traces are formatted. + /// + /// The [mapper] is used to convert JavaScript traces into Dart traces. The + /// [except] set indicates packages whose frames should be folded away. If + /// [only] is non-empty, it indicates packages whose frames should *not* be + /// folded away. + void configure( + {StackTraceMapper? mapper, Set<String>? except, Set<String>? only}) { + if (mapper != null) _mapper = mapper; + if (except != null) _except = except; + if (only != null) _only = only; + } + + /// Converts [stackTrace] to a [Chain] and formats it according to the user's + /// preferences. + /// + /// If [verbose] is `true`, this doesn't fold out irrelevant stack frames. It + /// defaults to the current test's [Metadata.verboseTrace] configuration, or + /// `false` if there is no current test. + Chain formatStackTrace(StackTrace stackTrace, {bool? verbose}) { + verbose ??= Invoker.current?.liveTest.test.metadata.verboseTrace ?? false; + + var chain = + Chain.forTrace(_mapper?.mapStackTrace(stackTrace) ?? stackTrace); + if (verbose) return chain; + + return chain.foldFrames((frame) { + if (_only.isNotEmpty) return !_only.contains(frame.package); + return _except.contains(frame.package); + }, terse: true); + } +}
diff --git a/pkgs/test_api/lib/src/backend/stack_trace_mapper.dart b/pkgs/test_api/lib/src/backend/stack_trace_mapper.dart new file mode 100644 index 0000000..b091bbe --- /dev/null +++ b/pkgs/test_api/lib/src/backend/stack_trace_mapper.dart
@@ -0,0 +1,12 @@ +// Copyright (c) 2015, 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. + +/// A class for mapping JS stack traces to Dart stack traces using source maps. +abstract interface class StackTraceMapper { + /// Converts [trace] into a Dart stack trace. + StackTrace mapStackTrace(StackTrace trace); + + /// Returns a Map representation which is suitable for JSON serialization. + Map<String, dynamic> serialize(); +}
diff --git a/pkgs/test_api/lib/src/backend/state.dart b/pkgs/test_api/lib/src/backend/state.dart new file mode 100644 index 0000000..5a37654 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/state.dart
@@ -0,0 +1,109 @@ +// Copyright (c) 2015, 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. + +/// The state of a [LiveTest]. +/// +/// A test's state is made up of two components, its [status] and its [result]. +/// The [status] represents where the test is in its process of running; the +/// [result] represents the outcome as far as its known. +class State { + /// Where the test is in its process of running. + final Status status; + + /// The outcome of the test, as far as it's known. + /// + /// Note that if [status] is [Status.pending], [result] will always be + /// [Result.success] since the test hasn't yet had a chance to fail. + final Result result; + + /// Whether a test in this state is expected to be done running code. + /// + /// If [status] is [Status.complete] and [result] doesn't indicate an error, a + /// properly-written test case should not be running any more code. However, + /// it may have started asynchronous processes without notifying the test + /// runner. + bool get shouldBeDone => status == Status.complete && result.isPassing; + + const State(this.status, this.result); + + @override + bool operator ==(Object other) => + other is State && status == other.status && result == other.result; + + @override + int get hashCode => status.hashCode ^ (7 * result.hashCode); + + @override + String toString() { + if (status == Status.pending) return 'pending'; + if (status == Status.complete) return result.toString(); + if (result == Result.success) return 'running'; + return 'running with $result'; + } +} + +/// Where the test is in its process of running. +enum Status { + /// The test has not yet begun running. + pending, + + /// The test is currently running. + running, + + /// The test has finished running. + /// + /// Note that even if the test is marked [complete], it may still be running + /// code asynchronously. A test is considered complete either once it hits its + /// first error or when all [expectAsync] callbacks have been called and any + /// returned [Future] has completed, but it's possible for further processing + /// to happen, which may cause further errors. + complete; + + factory Status.parse(String name) => Status.values.byName(name); + + @override + String toString() => name; +} + +/// The outcome of the test, as far as it's known. +enum Result { + /// The test has not yet failed in any way. + /// + /// Note that this doesn't mean that the test won't fail in the future. + success, + + /// The test, or some part of it, has been skipped. + /// + /// This implies that the test hasn't failed *yet*. However, it this doesn't + /// mean that the test won't fail in the future. + skipped, + + /// The test has failed. + /// + /// A failure is specifically caused by a [TestFailure] being thrown; any + /// other exception causes an error. + failure, + + /// The test has crashed. + /// + /// Any exception other than a [TestFailure] is considered to be an error. + error; + + /// Whether this is a passing result. + /// + /// A test is considered to have passed if it's a success or if it was + /// skipped. + bool get isPassing => this == success || this == skipped; + + /// Whether this is a failing result. + /// + /// A test is considered to have failed if it experiences a failure or an + /// error. + bool get isFailing => !isPassing; + + factory Result.parse(String name) => Result.values.byName(name); + + @override + String toString() => name; +}
diff --git a/pkgs/test_api/lib/src/backend/suite.dart b/pkgs/test_api/lib/src/backend/suite.dart new file mode 100644 index 0000000..910f184 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/suite.dart
@@ -0,0 +1,62 @@ +// Copyright (c) 2015, 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 'group.dart'; +import 'metadata.dart'; +import 'suite_platform.dart'; +import 'test.dart'; + +/// A test suite. +/// +/// A test suite is a set of tests that are intended to be run together and that +/// share default configuration. +class Suite { + /// The platform on which the suite is running. + final SuitePlatform platform; + + /// The path to the Dart test suite, or `null` if that path is unknown. + final String? path; + + /// The metadata associated with this test suite. + /// + /// This is a shortcut for [group.metadata]. + Metadata get metadata => group.metadata; + + /// The top-level group for this test suite. + final Group group; + + /// Whether or not to ignore test timeouts. + final bool ignoreTimeouts; + + /// Creates a new suite containing [group]. + /// + /// If [platform] and/or [os] are passed, [group] is filtered to match that + /// platform information. + /// + /// If [os] is passed without [platform], throws an [ArgumentError]. + Suite(Group group, this.platform, {this.ignoreTimeouts = false, this.path}) + : group = _filterGroup(group, platform); + + /// Returns [entries] filtered according to [platform] and [os]. + /// + /// Gracefully handles [platform] being null. + static Group _filterGroup(Group group, SuitePlatform platform) { + var filtered = group.forPlatform(platform); + if (filtered != null) return filtered; + return Group.root([], metadata: group.metadata); + } + + /// Returns a new suite with all tests matching [test] removed. + /// + /// Unlike [GroupEntry.filter], this never returns `null`. If all entries are + /// filtered out, it returns an empty suite. + Suite filter(bool Function(Test) callback) { + var filtered = group.filter(callback); + filtered ??= Group.root([], metadata: metadata); + return Suite(filtered, platform, + ignoreTimeouts: ignoreTimeouts, path: path); + } + + bool get isLoadSuite => false; +}
diff --git a/pkgs/test_api/lib/src/backend/suite_channel_manager.dart b/pkgs/test_api/lib/src/backend/suite_channel_manager.dart new file mode 100644 index 0000000..26a21c9 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/suite_channel_manager.dart
@@ -0,0 +1,44 @@ +// 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. + +import 'package:stream_channel/stream_channel.dart'; + +/// A class that connects incoming and outgoing channels with the same names. +class SuiteChannelManager { + /// Connections from the test runner that have yet to connect to corresponding + /// calls to [connectOut]. + final _incomingConnections = <String, StreamChannel<Object?>>{}; + + /// Connections from calls to [connectOut] that have yet to connect to + /// corresponding connections from the test runner. + final _outgoingConnections = <String, StreamChannelCompleter<Object?>>{}; + + /// The channel names that have already been used. + final _names = <String>{}; + + /// Creates a connection to the test runnner's channel with the given [name]. + StreamChannel<Object?> connectOut(String name) { + if (_incomingConnections.containsKey(name)) { + return _incomingConnections[name]!; + } else if (_names.contains(name)) { + throw StateError('Duplicate suiteChannel() connection "$name".'); + } else { + _names.add(name); + var completer = StreamChannelCompleter<Object?>(); + _outgoingConnections[name] = completer; + return completer.channel; + } + } + + /// Connects [channel] to this worker's channel with the given [name]. + void connectIn(String name, StreamChannel<Object?> channel) { + if (_outgoingConnections.containsKey(name)) { + _outgoingConnections.remove(name)!.setChannel(channel); + } else if (_incomingConnections.containsKey(name)) { + throw StateError('Duplicate RunnerSuite.channel() connection "$name".'); + } else { + _incomingConnections[name] = channel; + } + } +}
diff --git a/pkgs/test_api/lib/src/backend/suite_platform.dart b/pkgs/test_api/lib/src/backend/suite_platform.dart new file mode 100644 index 0000000..baab6ce --- /dev/null +++ b/pkgs/test_api/lib/src/backend/suite_platform.dart
@@ -0,0 +1,70 @@ +// 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. + +import 'compiler.dart'; +import 'operating_system.dart'; +import 'runtime.dart'; + +/// The platform on which a test suite is loaded. +final class SuitePlatform { + /// The runtime that hosts the suite. + final Runtime runtime; + + /// The operating system on which the suite is running. + /// + /// This will always be [OperatingSystem.none] if `runtime.isBrowser` is + /// true. + final OperatingSystem os; + + /// Whether we're running on Google-internal infrastructure. + final bool inGoogle; + + /// The compiler that should used for this platform. + final Compiler compiler; + + /// Creates a new platform with the given [runtime] and [os], which defaults + /// to [OperatingSystem.none]. + /// + /// Throws an [ArgumentError] if [runtime] is a browser and [os] is not + /// `null` or [OperatingSystem.none]. + /// + /// If [compiler] is `null`, then the default compiler for [runtime] will be + /// used. + SuitePlatform(this.runtime, + { + // TODO(https://github.com/dart-lang/test/issues/1935): make required + Compiler? compiler, + this.os = OperatingSystem.none, + this.inGoogle = false}) + : compiler = compiler ?? runtime.defaultCompiler { + if (runtime.isBrowser && os != OperatingSystem.none) { + throw ArgumentError('No OS should be passed for runtime "$runtime".'); + } + if (!runtime.supportedCompilers.contains(this.compiler)) { + throw ArgumentError( + 'The platform $runtime does not support the compiler ${this.compiler}'); + } + } + + /// Converts a JSON-safe representation generated by [serialize] back into a + /// [SuitePlatform]. + factory SuitePlatform.deserialize(Object serialized) { + var map = serialized as Map; + return SuitePlatform(Runtime.deserialize(map['runtime'] as Object), + compiler: map.containsKey('compiler') + ? Compiler.deserialize(map['compiler'] as Object) + : null, + os: OperatingSystem.find(map['os'] as String), + inGoogle: map['inGoogle'] as bool); + } + + /// Converts [this] into a JSON-safe object that can be converted back to a + /// [SuitePlatform] using [SuitePlatform.deserialize]. + Object serialize() => { + 'runtime': runtime.serialize(), + 'compiler': compiler.serialize(), + 'os': os.identifier, + 'inGoogle': inGoogle + }; +}
diff --git a/pkgs/test_api/lib/src/backend/test.dart b/pkgs/test_api/lib/src/backend/test.dart new file mode 100644 index 0000000..5b5e73a --- /dev/null +++ b/pkgs/test_api/lib/src/backend/test.dart
@@ -0,0 +1,42 @@ +// Copyright (c) 2015, 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:stack_trace/stack_trace.dart'; + +import 'group.dart'; +import 'group_entry.dart'; +import 'live_test.dart'; +import 'metadata.dart'; +import 'suite.dart'; +import 'suite_platform.dart'; + +/// A single test. +/// +/// A test is immutable and stateless, which means that it can't be run +/// directly. To run one, load a live version using [Test.load] and run it using +/// [LiveTest.run]. +abstract class Test implements GroupEntry { + @override + String get name; + + @override + Metadata get metadata; + + @override + Trace? get trace; + + /// Loads a live version of this test, which can be used to run it a single + /// time. + /// + /// [suite] is the suite within which this test is being run. If [groups] is + /// passed, it's the list of groups containing this test; otherwise, it + /// defaults to just containing `suite.group`. + LiveTest load(Suite suite, {Iterable<Group>? groups}); + + @override + Test? forPlatform(SuitePlatform platform); + + @override + Test? filter(bool Function(Test) callback) => callback(this) ? this : null; +}
diff --git a/pkgs/test_api/lib/src/backend/test_failure.dart b/pkgs/test_api/lib/src/backend/test_failure.dart new file mode 100644 index 0000000..4d9fc79 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/test_failure.dart
@@ -0,0 +1,13 @@ +// Copyright (c) 2021, 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. + +/// An exception thrown when a test assertion fails. +class TestFailure implements Exception { + final String? message; + + TestFailure(this.message); + + @override + String toString() => message.toString(); +}
diff --git a/pkgs/test_api/lib/src/backend/util/identifier_regex.dart b/pkgs/test_api/lib/src/backend/util/identifier_regex.dart new file mode 100644 index 0000000..6426641 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/util/identifier_regex.dart
@@ -0,0 +1,9 @@ +// Copyright (c) 2021, 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. + +/// A regular expression matching a full string as a hyphenated identifier. +/// +/// This is like a standard Dart identifier, except that it can also contain +/// hyphens. +final anchoredHyphenatedIdentifier = RegExp(r'^[a-zA-Z_-][a-zA-Z0-9_-]*$');
diff --git a/pkgs/test_api/lib/src/backend/util/pretty_print.dart b/pkgs/test_api/lib/src/backend/util/pretty_print.dart new file mode 100644 index 0000000..5c011c5 --- /dev/null +++ b/pkgs/test_api/lib/src/backend/util/pretty_print.dart
@@ -0,0 +1,45 @@ +// Copyright (c) 2021, 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. + +/// Returns [name] if [number] is 1, or the plural of [name] otherwise. +/// +/// By default, this just adds "s" to the end of [name] to get the plural. If +/// [plural] is passed, that's used instead. +String pluralize(String name, int number, {String? plural}) { + if (number == 1) return name; + if (plural != null) return plural; + return '${name}s'; +} + +/// Returns a sentence fragment listing the elements of [iter]. +/// +/// This converts each element of [iter] to a string and separates them with +/// commas and/or [conjunction] where appropriate. The [conjunction] defaults to +/// "and". +String toSentence(Iterable iter, {String conjunction = 'and'}) { + if (iter.length == 1) return iter.first.toString(); + + var result = iter.take(iter.length - 1).join(', '); + if (iter.length > 2) result += ','; + return '$result $conjunction ${iter.last}'; +} + +/// Returns a human-friendly representation of [duration]. +String niceDuration(Duration duration) { + var minutes = duration.inMinutes; + var seconds = duration.inSeconds % 60; + var decaseconds = (duration.inMilliseconds % 1000) ~/ 100; + + var buffer = StringBuffer(); + if (minutes != 0) buffer.write('$minutes minutes'); + + if (minutes == 0 || seconds != 0) { + if (minutes != 0) buffer.write(', '); + buffer.write(seconds); + if (decaseconds != 0) buffer.write('.$decaseconds'); + buffer.write(' seconds'); + } + + return buffer.toString(); +}
diff --git a/pkgs/test_api/lib/src/frontend/fake.dart b/pkgs/test_api/lib/src/frontend/fake.dart new file mode 100644 index 0000000..e3e0ea6 --- /dev/null +++ b/pkgs/test_api/lib/src/frontend/fake.dart
@@ -0,0 +1,52 @@ +// Copyright (c) 2020, 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. + +/// A stand-in for another object which cannot be used except for specifically +/// overridden methods. +/// +/// A fake has a default behavior for every field and method of throwing +/// [UnimplementedError]. Fields and methods that are exercised by the code +/// under test should be manually overridden in the implementing class. +/// +/// A fake does not have any support for verification or defining behavior from +/// the test, it cannot be used as a mock. +/// +/// In most cases a shared full fake implementation without a `noSuchMethod` is +/// preferable to `extends Fake`, however `extends Fake` is preferred against +/// `extends Mock` mixed with manual `@override` implementations. +/// +/// __Example use__: +/// +/// // Real class. +/// class Cat { +/// String meow(String suffix) => 'Meow$suffix'; +/// String hiss(String suffix) => 'Hiss$suffix'; +/// } +/// +/// // Fake class. +/// class FakeCat extends Fake implements Cat { +/// @override +/// String meow(String suffix) => 'FakeMeow$suffix'; +/// } +/// +/// void main() { +/// // Create a new fake Cat at runtime. +/// var cat = new FakeCat(); +/// +/// // Try making a Cat sound... +/// print(cat.meow('foo')); // Prints 'FakeMeowfoo' +/// print(cat.hiss('foo')); // Throws +/// } +/// +/// **WARNING**: [Fake] uses [noSuchMethod](goo.gl/r3IQUH), which is a _form_ of +/// runtime reflection, and causes sub-standard code to be generated. As such, +/// [Fake] should strictly _not_ be used in any production code, especially if +/// used within the context of Dart for Web (dart2js, DDC) and Dart for Mobile +/// (Flutter). +abstract mixin class Fake { + @override + dynamic noSuchMethod(Invocation invocation) { + throw UnimplementedError(invocation.memberName.toString().split('"')[1]); + } +}
diff --git a/pkgs/test_api/lib/src/remote_listener.dart b/pkgs/test_api/lib/src/remote_listener.dart new file mode 100644 index 0000000..f2df134 --- /dev/null +++ b/pkgs/test_api/lib/src/remote_listener.dart
@@ -0,0 +1,5 @@ +// Copyright (c) 2021, 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. + +export 'backend/remote_listener.dart' show RemoteListener;
diff --git a/pkgs/test_api/lib/src/scaffolding/spawn_hybrid.dart b/pkgs/test_api/lib/src/scaffolding/spawn_hybrid.dart new file mode 100644 index 0000000..7222d52 --- /dev/null +++ b/pkgs/test_api/lib/src/scaffolding/spawn_hybrid.dart
@@ -0,0 +1,179 @@ +// Copyright (c) 2016, 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:convert'; + +import 'package:async/async.dart'; +import 'package:stream_channel/stream_channel.dart'; + +import '../backend/remote_exception.dart'; +import '../utils.dart'; +import 'test_structure.dart' show addTearDown; + +/// A transformer that handles messages from the spawned isolate and ensures +/// that messages sent to it are JSON-encodable. +/// +/// The spawned isolate sends three kinds of messages. Data messages are emitted +/// as data events, error messages are emitted as error events, and print +/// messages are printed using `print()`. +// package:test will only send a `Map` across this channel, but users of +// `hybridMain` can send any json encodeable type. +final _transformer = StreamChannelTransformer<dynamic, dynamic>( + StreamTransformer.fromHandlers(handleData: (message, sink) { + switch (message['type'] as String) { + case 'data': + sink.add(message['data']); + break; + + case 'print': + print(message['line']); + break; + + case 'error': + var error = RemoteException.deserialize(message['error'] as Map); + sink.addError(error.error, error.stackTrace); + break; + } +}), StreamSinkTransformer.fromHandlers(handleData: (message, sink) { + // This is called synchronously from the user's `Sink.add()` call, so if + // [ensureJsonEncodable] throws here they'll get a helpful stack trace. + ensureJsonEncodable(message); + sink.add(message); +})); + +/// Spawns a VM isolate for the given [uri], which may be a [Uri] or a [String]. +/// +/// This allows browser tests to spawn servers with which they can communicate +/// to test client/server interactions. It can also be used by VM tests to +/// easily spawn an isolate. +/// +/// The Dart file at [uri] must define a top-level `hybridMain()` function that +/// takes a `StreamChannel` argument and, optionally, an `Object` argument to +/// which [message] will be passed. Note that [message] must be JSON-encodable. +/// For example: +/// +/// ```dart +/// import "package:stream_channel/stream_channel.dart"; +/// +/// hybridMain(StreamChannel channel, Object message) { +/// // ... +/// } +/// ``` +/// +/// If [uri] is relative, it will be interpreted relative to the `file:` URL for +/// the test suite being executed. If it's root-relative (that is, if it begins +/// with `/`) it will be interpreted relative to the root of the package (the +/// directory that contains `pubspec.yaml`, *not* the `test/` directory). If +/// it's a `package:` URL, it will be resolved using the current package's +/// dependency constellation. +/// +/// Returns a [StreamChannel] that's connected to the channel passed to +/// `hybridMain()`. Only JSON-encodable objects may be sent through this +/// channel. If the channel is closed, the hybrid isolate is killed. If the +/// isolate is killed, the channel's stream will emit a "done" event. +/// +/// Any unhandled errors loading or running the hybrid isolate will be emitted +/// as errors over the channel's stream. Any calls to `print()` in the hybrid +/// isolate will be printed as though they came from the test that created the +/// isolate. +/// +/// Code in the hybrid isolate is not considered to be running in a test +/// context, so it can't access test functions like `expect()` and +/// `expectAsync()`. +/// +/// By default, the hybrid isolate is automatically killed when the test +/// finishes running. If [stayAlive] is `true`, it won't be killed until the +/// entire test suite finishes running. +/// +/// **Note**: If you use this API, be sure to add a dependency on the +/// **`stream_channel` package, since you're using its API as well! +StreamChannel spawnHybridUri( + Object uri, { + Object? message, + bool stayAlive = false, +}) { + if (uri is String) { + // Ensure that it can be parsed as a uri. + Uri.parse(uri); + } else if (uri is! Uri) { + throw ArgumentError.value(uri, 'uri', 'must be a Uri or a String.'); + } + return _spawn(uri.toString(), message, stayAlive: stayAlive); +} + +/// Spawns a VM isolate that runs the given [dartCode], which is loaded as the +/// contents of a Dart library. +/// +/// This allows browser tests to spawn servers with which they can communicate +/// to test client/server interactions. It can also be used by VM tests to +/// easily spawn an isolate. +/// +/// The [dartCode] must define a top-level `hybridMain()` function that takes a +/// `StreamChannel` argument and, optionally, an `Object` argument to which +/// [message] will be passed. Note that [message] must be JSON-encodable. For +/// example: +/// +/// ```dart +/// import "package:stream_channel/stream_channel.dart"; +/// +/// hybridMain(StreamChannel channel, Object message) { +/// // ... +/// } +/// ``` +/// +/// Returns a [StreamChannel] that's connected to the channel passed to +/// `hybridMain()`. Only JSON-encodable objects may be sent through this +/// channel. If the channel is closed, the hybrid isolate is killed. If the +/// isolate is killed, the channel's stream will emit a "done" event. +/// +/// Any unhandled errors loading or running the hybrid isolate will be emitted +/// as errors over the channel's stream. Any calls to `print()` in the hybrid +/// isolate will be printed as though they came from the test that created the +/// isolate. +/// +/// Code in the hybrid isolate is not considered to be running in a test +/// context, so it can't access test functions like `expect()` and +/// `expectAsync()`. +/// +/// By default, the hybrid isolate is automatically killed when the test +/// finishes running. If [stayAlive] is `true`, it won't be killed until the +/// entire test suite finishes running. +/// +/// **Note**: If you use this API, be sure to add a dependency on the +/// **`stream_channel` package, since you're using its API as well! +StreamChannel spawnHybridCode(String dartCode, + {Object? message, bool stayAlive = false}) { + var uri = Uri.dataFromString(dartCode, + encoding: utf8, mimeType: 'application/dart'); + return _spawn(uri.toString(), message, stayAlive: stayAlive); +} + +/// Like [spawnHybridUri], but doesn't take [Uri] objects. +StreamChannel _spawn(String uri, Object? message, {bool stayAlive = false}) { + var channel = Zone.current[#test.runner.test_channel] as MultiChannel?; + if (channel == null) { + throw UnsupportedError("Can't connect to the test runner.\n" + 'spawnHybridUri() is currently only supported within "dart test".'); + } + + ensureJsonEncodable(message); + + var virtualChannel = channel.virtualChannel(); + StreamChannel isolateChannel = virtualChannel; + channel.sink.add({ + 'type': 'spawn-hybrid-uri', + 'url': uri, + 'message': message, + 'channel': virtualChannel.id + }); + + if (!stayAlive) { + var disconnector = Disconnector<void>(); + addTearDown(() => disconnector.disconnect()); + isolateChannel = isolateChannel.transform(disconnector); + } + + return isolateChannel.transform(_transformer); +}
diff --git a/pkgs/test_api/lib/src/scaffolding/test_structure.dart b/pkgs/test_api/lib/src/scaffolding/test_structure.dart new file mode 100644 index 0000000..642e013 --- /dev/null +++ b/pkgs/test_api/lib/src/scaffolding/test_structure.dart
@@ -0,0 +1,251 @@ +// Copyright (c) 2021, 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 'package:meta/meta.dart'; + +import '../backend/configuration/timeout.dart'; +import '../backend/declarer.dart'; +import '../backend/invoker.dart'; + +// test_core does not support running tests directly, so the Declarer should +// always be on the Zone. +Declarer get _declarer => Zone.current[#test.declarer] as Declarer; + +// TODO(nweiz): This and other top-level functions should throw exceptions if +// they're called after the declarer has finished declaring. +/// Creates a new test case with the given description (converted to a string) +/// and body. +/// +/// The description will be added to the descriptions of any surrounding +/// [group]s. If [testOn] is passed, it's parsed as a [platform selector][]; the +/// test will only be run on matching platforms. +/// +/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors +/// +/// If [timeout] is passed, it's used to modify or replace the default timeout +/// of 30 seconds. Timeout modifications take precedence in suite-group-test +/// order, so [timeout] will also modify any timeouts set on the group or suite. +/// +/// If [skip] is a String or `true`, the test is skipped. If it's a String, it +/// should explain why the test is skipped; this reason will be printed instead +/// of running the test. +/// +/// If [tags] is passed, it declares user-defined tags that are applied to the +/// test. These tags can be used to select or skip the test on the command line, +/// or to do bulk test configuration. All tags should be declared in the +/// [package configuration file][configuring tags]. The parameter can be an +/// [Iterable] of tag names, or a [String] representing a single tag. +/// +/// If [retry] is passed, the test will be retried the provided number of times +/// before being marked as a failure. +/// +/// [configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags +/// +/// [onPlatform] allows tests to be configured on a platform-by-platform +/// basis. It's a map from strings that are parsed as [PlatformSelector]s to +/// annotation classes: [Timeout], [Skip], or lists of those. These +/// annotations apply only on the given platforms. For example: +/// +/// test('potentially slow test', () { +/// // ... +/// }, onPlatform: { +/// // This test is especially slow on Windows. +/// 'windows': Timeout.factor(2), +/// 'browser': [ +/// Skip('TODO: add browser support'), +/// // This will be slow on browsers once it works on them. +/// Timeout.factor(2) +/// ] +/// }); +/// +/// If multiple platforms match, the annotations apply in order as through +/// they were in nested groups. +/// +/// If the `solo` flag is `true`, only tests and groups marked as +/// "solo" will be be run. This only restricts tests *within this test +/// suite*—tests in other suites will run as normal. We recommend that users +/// avoid this flag if possible and instead use the test runner flag `-n` to +/// filter tests by name. +@isTest +void test(Object? description, dynamic Function() body, + {String? testOn, + Timeout? timeout, + Object? skip, + Object? tags, + Map<String, dynamic>? onPlatform, + int? retry, + // TODO(https://github.com/dart-lang/test/issues/2205): Remove deprecated. + @Deprecated('Debug only') @doNotSubmit bool solo = false}) { + _declarer.test(description.toString(), body, + testOn: testOn, + timeout: timeout, + skip: skip, + onPlatform: onPlatform, + tags: tags, + retry: retry, + solo: solo); + + // Force dart2js not to inline this function. We need it to be separate from + // `main()` in JS stack traces in order to properly determine the line and + // column where the test was defined. See sdk#26705. + return; + return; // ignore: dead_code +} + +/// Creates a group of tests. +/// +/// A group's description (converted to a string) is included in the descriptions +/// of any tests or sub-groups it contains. [setUp] and [tearDown] are also scoped +/// to the containing group. +/// +/// If [testOn] is passed, it's parsed as a [platform selector][]; the test will +/// only be run on matching platforms. +/// +/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors +/// +/// If [timeout] is passed, it's used to modify or replace the default timeout +/// of 30 seconds. Timeout modifications take precedence in suite-group-test +/// order, so [timeout] will also modify any timeouts set on the suite, and will +/// be modified by any timeouts set on individual tests. +/// +/// If [skip] is a String or `true`, the group is skipped. If it's a String, it +/// should explain why the group is skipped; this reason will be printed instead +/// of running the group's tests. +/// +/// If [tags] is passed, it declares user-defined tags that are applied to the +/// test. These tags can be used to select or skip the test on the command line, +/// or to do bulk test configuration. All tags should be declared in the +/// [package configuration file][configuring tags]. The parameter can be an +/// [Iterable] of tag names, or a [String] representing a single tag. +/// +/// [configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags +/// +/// [onPlatform] allows groups to be configured on a platform-by-platform +/// basis. It's a map from strings that are parsed as [PlatformSelector]s to +/// annotation classes: [Timeout], [Skip], or lists of those. These +/// annotations apply only on the given platforms. For example: +/// +/// group('potentially slow tests', () { +/// // ... +/// }, onPlatform: { +/// // These tests are especially slow on Windows. +/// 'windows': Timeout.factor(2), +/// 'browser': [ +/// Skip('TODO: add browser support'), +/// // They'll be slow on browsers once it works on them. +/// Timeout.factor(2) +/// ] +/// }); +/// +/// If multiple platforms match, the annotations apply in order as through +/// they were in nested groups. +/// +/// If the `solo` flag is `true`, only tests and groups marked as +/// "solo" will be be run. This only restricts tests *within this test +/// suite*—tests in other suites will run as normal. We recommend that users +/// avoid this flag if possible, and instead use the test runner flag `-n` to +/// filter tests by name. +@isTestGroup +void group(Object? description, dynamic Function() body, + {String? testOn, + Timeout? timeout, + Object? skip, + Object? tags, + Map<String, dynamic>? onPlatform, + int? retry, + // TODO(https://github.com/dart-lang/test/issues/2205): Remove deprecated. + @Deprecated('Debug only') @doNotSubmit bool solo = false}) { + _declarer.group(description.toString(), body, + testOn: testOn, + timeout: timeout, + skip: skip, + tags: tags, + onPlatform: onPlatform, + retry: retry, + solo: solo); + + // Force dart2js not to inline this function. We need it to be separate from + // `main()` in JS stack traces in order to properly determine the line and + // column where the test was defined. See sdk#26705. + return; + return; // ignore: dead_code +} + +/// Registers a function to be run before tests. +/// +/// This function will be called before each test is run. [callback] may be +/// asynchronous; if so, it must return a [Future]. +/// +/// If this is called within a test group, it applies only to tests in that +/// group. [callback] will be run after any set-up callbacks in parent groups or +/// at the top level. +/// +/// Each callback at the top level or in a given group will be run in the order +/// they were declared. +void setUp(dynamic Function() callback) => _declarer.setUp(callback); + +/// Registers a function to be run after tests. +/// +/// This function will be called after each test is run. [callback] may be +/// asynchronous; if so, it must return a [Future]. +/// +/// If this is called within a test group, it applies only to tests in that +/// group. [callback] will be run before any tear-down callbacks in parent +/// groups or at the top level. +/// +/// Each callback at the top level or in a given group will be run in the +/// reverse of the order they were declared. +/// +/// See also [addTearDown], which adds tear-downs to a running test. +void tearDown(dynamic Function() callback) => _declarer.tearDown(callback); + +/// Registers a function to be run after the current test. +/// +/// This is called within a running test, and adds a tear-down only for the +/// current test. It allows testing libraries to add cleanup logic as soon as +/// there's something to clean up. +/// +/// The [callback] is run before any callbacks registered with [tearDown]. Like +/// [tearDown], the most recently registered callback is run first. +/// +/// If this is called from within a [setUpAll] or [tearDownAll] callback, it +/// instead runs the function after *all* tests in the current test suite. +void addTearDown(dynamic Function() callback) { + if (Invoker.current == null) { + throw StateError('addTearDown() may only be called within a test.'); + } + + Invoker.current!.addTearDown(callback); +} + +/// Registers a function to be run once before all tests. +/// +/// [callback] may be asynchronous; if so, it must return a [Future]. +/// +/// If this is called within a test group, [callback] will run before all tests +/// in that group. It will be run after any [setUpAll] callbacks in parent +/// groups or at the top level. It won't be run if none of the tests in the +/// group are run. +/// +/// **Note**: This function makes it very easy to accidentally introduce hidden +/// dependencies between tests that should be isolated. In general, you should +/// prefer [setUp], and only use [setUpAll] if the callback is prohibitively +/// slow. +void setUpAll(dynamic Function() callback) => _declarer.setUpAll(callback); + +/// Registers a function to be run once after all tests. +/// +/// If this is called within a test group, [callback] will run after all tests +/// in that group. It will be run before any [tearDownAll] callbacks in parent +/// groups or at the top level. It won't be run if none of the tests in the +/// group are run. +/// +/// **Note**: This function makes it very easy to accidentally introduce hidden +/// dependencies between tests that should be isolated. In general, you should +/// prefer [tearDown], and only use [tearDownAll] if the callback is +/// prohibitively slow. +void tearDownAll(dynamic Function() callback) => + _declarer.tearDownAll(callback);
diff --git a/pkgs/test_api/lib/src/scaffolding/utils.dart b/pkgs/test_api/lib/src/scaffolding/utils.dart new file mode 100644 index 0000000..134dc49 --- /dev/null +++ b/pkgs/test_api/lib/src/scaffolding/utils.dart
@@ -0,0 +1,51 @@ +// Copyright (c) 2021, 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 '../backend/invoker.dart'; + +/// Returns a [Future] that completes after the [event loop][] has run the given +/// number of [times] (20 by default). +/// +/// [event loop]: https://medium.com/dartlang/dart-asynchronous-programming-isolates-and-event-loops-bffc3e296a6a +/// +/// Awaiting this approximates waiting until all asynchronous work (other than +/// work that's waiting for external resources) completes. +Future pumpEventQueue({int times = 20}) { + if (times == 0) return Future.value(); + // Use the event loop to allow the microtask queue to finish. + return Future(() => pumpEventQueue(times: times - 1)); +} + +/// Registers an exception that was caught for the current test. +void registerException(Object error, + [StackTrace stackTrace = StackTrace.empty]) { + // This will usually forward directly to [Invoker.current.handleError], but + // going through the zone API allows other zones to consistently see errors. + Zone.current.handleUncaughtError(error, stackTrace); +} + +/// Prints [message] if and when the current test fails. +/// +/// This is intended for test infrastructure to provide debugging information +/// without cluttering the output for successful tests. Note that unlike +/// [print], each individual message passed to [printOnFailure] will be +/// separated by a blank line. +void printOnFailure(String message) { + _currentInvoker.printOnFailure(message); +} + +/// Marks the current test as skipped. +/// +/// A skipped test may still fail if any exception is thrown, including uncaught +/// asynchronous errors. If the entire test should be skipped `return` from the +/// test body after marking it as skipped. +void markTestSkipped(String message) => _currentInvoker..skip(message); + +Invoker get _currentInvoker => + Invoker.current ?? + (throw StateError( + 'There is no current invoker. Please make sure that you are making the ' + 'call inside a test zone.'));
diff --git a/pkgs/test_api/lib/src/utils.dart b/pkgs/test_api/lib/src/utils.dart new file mode 100644 index 0000000..3bf7518 --- /dev/null +++ b/pkgs/test_api/lib/src/utils.dart
@@ -0,0 +1,27 @@ +// 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. + +/// Throws an [ArgumentError] if [message] isn't recursively JSON-safe. +void ensureJsonEncodable(Object? message) { + if (message == null || + message is String || + message is num || + message is bool) { + // JSON-encodable, hooray! + } else if (message is List) { + for (var element in message) { + ensureJsonEncodable(element); + } + } else if (message is Map) { + message.forEach((key, value) { + if (key is! String) { + throw ArgumentError("$message can't be JSON-encoded."); + } + + ensureJsonEncodable(value); + }); + } else { + throw ArgumentError.value("$message can't be JSON-encoded."); + } +}
diff --git a/pkgs/test_api/lib/test_api.dart b/pkgs/test_api/lib/test_api.dart new file mode 100644 index 0000000..75c1540 --- /dev/null +++ b/pkgs/test_api/lib/test_api.dart
@@ -0,0 +1,10 @@ +// 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. + +@Deprecated('package:test_api is not intended for general use. ' + 'Please use package:test.') +library; + +export 'hooks.dart' show TestFailure; +export 'scaffolding.dart';
diff --git a/pkgs/test_api/mono_pkg.yaml b/pkgs/test_api/mono_pkg.yaml new file mode 100644 index 0000000..534d9c3 --- /dev/null +++ b/pkgs/test_api/mono_pkg.yaml
@@ -0,0 +1,19 @@ +# See https://pub.dev/packages/mono_repo + +sdk: +- dev +- pubspec + +stages: +- analyze_and_format: + - group: + - format + - analyze: --fatal-infos + sdk: + - dev +- unit_test: + - group: + - command: dart test --preset travis -x browser + os: + - linux + - windows
diff --git a/pkgs/test_api/pubspec.yaml b/pkgs/test_api/pubspec.yaml new file mode 100644 index 0000000..af5cb93 --- /dev/null +++ b/pkgs/test_api/pubspec.yaml
@@ -0,0 +1,29 @@ +name: test_api +version: 0.7.4 +description: >- + The user facing API for structuring Dart tests and checking expectations. +repository: https://github.com/dart-lang/test/tree/master/pkgs/test_api +resolution: workspace + +environment: + sdk: ^3.5.0 + +dependencies: + async: ^2.5.0 + boolean_selector: ^2.1.0 + collection: ^1.15.0 + meta: ^1.14.0 + source_span: ^1.8.0 + stack_trace: ^1.10.0 + stream_channel: ^2.1.0 + string_scanner: ^1.1.0 + term_glyph: ^1.2.0 + +dev_dependencies: + analyzer: '>=6.0.0 <8.0.0' + fake_async: ^1.2.0 + glob: ^2.0.0 + graphs: ^2.0.0 + path: ^1.8.0 + test: any + test_core: any
diff --git a/pkgs/test_api/test/backend/declarer_test.dart b/pkgs/test_api/test/backend/declarer_test.dart new file mode 100644 index 0000000..f095ad0 --- /dev/null +++ b/pkgs/test_api/test/backend/declarer_test.dart
@@ -0,0 +1,703 @@ +// Copyright (c) 2015, 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 'package:test/test.dart'; +import 'package:test_api/src/backend/declarer.dart'; +import 'package:test_api/src/backend/group.dart'; +import 'package:test_api/src/backend/invoker.dart'; +import 'package:test_api/src/backend/suite.dart'; +import 'package:test_api/src/backend/test.dart'; + +import '../utils.dart'; + +late Suite _suite; + +void main() { + setUp(() { + _suite = Suite(Group.root([]), suitePlatform, ignoreTimeouts: false); + }); + + group('.test()', () { + test('declares a test with a description and body', () async { + var bodyRun = false; + var tests = declare(() { + test('description', () { + bodyRun = true; + }); + }); + + expect(tests, hasLength(1)); + expect(tests.single.name, equals('description')); + + await _runTest(tests[0] as Test); + expect(bodyRun, isTrue); + }); + + test('declares a test with an object as the description', () async { + var tests = declare(() { + test(Object, () {}); + }); + + expect(tests.single.name, equals('Object')); + }); + + test('declares multiple tests', () { + var tests = declare(() { + test('description 1', () {}); + test('description 2', () {}); + test('description 3', () {}); + }); + + expect(tests, hasLength(3)); + expect(tests[0].name, equals('description 1')); + expect(tests[1].name, equals('description 2')); + expect(tests[2].name, equals('description 3')); + }); + }); + + group('.setUp()', () { + test('is run before all tests', () async { + var setUpRun = false; + var tests = declare(() { + setUp(() => setUpRun = true); + + test( + 'description 1', + expectAsync0(() { + expect(setUpRun, isTrue); + setUpRun = false; + }, max: 1)); + + test( + 'description 2', + expectAsync0(() { + expect(setUpRun, isTrue); + setUpRun = false; + }, max: 1)); + }); + + await _runTest(tests[0] as Test); + await _runTest(tests[1] as Test); + }); + + test('can return a Future', () { + var setUpRun = false; + var tests = declare(() { + setUp(() { + return Future(() => setUpRun = true); + }); + + test( + 'description', + expectAsync0(() { + expect(setUpRun, isTrue); + }, max: 1)); + }); + + return _runTest(tests.single as Test); + }); + + test('runs in call order within a group', () async { + var firstSetUpRun = false; + var secondSetUpRun = false; + var thirdSetUpRun = false; + var tests = declare(() { + setUp(expectAsync0(() async { + expect(secondSetUpRun, isFalse); + expect(thirdSetUpRun, isFalse); + firstSetUpRun = true; + })); + + setUp(expectAsync0(() async { + expect(firstSetUpRun, isTrue); + expect(thirdSetUpRun, isFalse); + secondSetUpRun = true; + })); + + setUp(expectAsync0(() async { + expect(firstSetUpRun, isTrue); + expect(secondSetUpRun, isTrue); + thirdSetUpRun = true; + })); + + test('description', expectAsync0(() { + expect(firstSetUpRun, isTrue); + expect(secondSetUpRun, isTrue); + expect(thirdSetUpRun, isTrue); + })); + }); + + await _runTest(tests.single as Test); + }); + }); + + group('.tearDown()', () { + test('is run after all tests', () async { + late bool tearDownRun; + var tests = declare(() { + setUp(() => tearDownRun = false); + tearDown(() => tearDownRun = true); + + test( + 'description 1', + expectAsync0(() { + expect(tearDownRun, isFalse); + }, max: 1)); + + test( + 'description 2', + expectAsync0(() { + expect(tearDownRun, isFalse); + }, max: 1)); + }); + + await _runTest(tests[0] as Test); + expect(tearDownRun, isTrue); + await _runTest(tests[1] as Test); + expect(tearDownRun, isTrue); + }); + + test('is run after an out-of-band failure', () async { + late bool tearDownRun; + var tests = declare(() { + setUp(() => tearDownRun = false); + tearDown(() => tearDownRun = true); + + test( + 'description 1', + expectAsync0(() { + Invoker.current!.addOutstandingCallback(); + Future(() => throw TestFailure('oh no')) + .whenComplete(Invoker.current!.removeOutstandingCallback); + }, max: 1)); + }); + + await _runTest(tests.single as Test, shouldFail: true); + expect(tearDownRun, isTrue); + }); + + test('can return a Future', () async { + var tearDownRun = false; + var tests = declare(() { + tearDown(() { + return Future(() => tearDownRun = true); + }); + + test( + 'description', + expectAsync0(() { + expect(tearDownRun, isFalse); + }, max: 1)); + }); + + await _runTest(tests.single as Test); + expect(tearDownRun, isTrue); + }); + + test("isn't run until there are no outstanding callbacks", () async { + var outstandingCallbackRemoved = false; + var outstandingCallbackRemovedBeforeTeardown = false; + var tests = declare(() { + tearDown(() { + outstandingCallbackRemovedBeforeTeardown = outstandingCallbackRemoved; + }); + + test('description', () { + Invoker.current!.addOutstandingCallback(); + pumpEventQueue().then((_) { + outstandingCallbackRemoved = true; + Invoker.current!.removeOutstandingCallback(); + }); + }); + }); + + await _runTest(tests.single as Test); + expect(outstandingCallbackRemovedBeforeTeardown, isTrue); + }); + + test("isn't run until test body completes after out-of-band error", + () async { + var hasTestFinished = false; + var hasTestFinishedBeforeTeardown = false; + var tests = declare(() { + tearDown(() { + hasTestFinishedBeforeTeardown = hasTestFinished; + }); + + test('description', () { + Future<Never>.error('oh no'); + return pumpEventQueue().then((_) { + hasTestFinished = true; + }); + }); + }); + + await _runTest(tests.single as Test, shouldFail: true); + expect(hasTestFinishedBeforeTeardown, isTrue); + }); + + test("doesn't complete until there are no outstanding callbacks", () async { + var outstandingCallbackRemoved = false; + var tests = declare(() { + tearDown(() { + Invoker.current!.addOutstandingCallback(); + pumpEventQueue().then((_) { + outstandingCallbackRemoved = true; + Invoker.current!.removeOutstandingCallback(); + }); + }); + + test('description', () {}); + }); + + await _runTest(tests.single as Test); + expect(outstandingCallbackRemoved, isTrue); + }); + + test('runs in reverse call order within a group', () async { + var firstTearDownRun = false; + var secondTearDownRun = false; + var thirdTearDownRun = false; + var tests = declare(() { + tearDown(expectAsync0(() async { + expect(secondTearDownRun, isTrue); + expect(thirdTearDownRun, isTrue); + firstTearDownRun = true; + })); + + tearDown(expectAsync0(() async { + expect(firstTearDownRun, isFalse); + expect(thirdTearDownRun, isTrue); + secondTearDownRun = true; + })); + + tearDown(expectAsync0(() async { + expect(firstTearDownRun, isFalse); + expect(secondTearDownRun, isFalse); + thirdTearDownRun = true; + })); + + test( + 'description', + expectAsync0(() { + expect(firstTearDownRun, isFalse); + expect(secondTearDownRun, isFalse); + expect(thirdTearDownRun, isFalse); + }, max: 1)); + }); + + await _runTest(tests.single as Test); + }); + + test('runs further tearDowns in a group even if one fails', () async { + var tests = declare(() { + tearDown(expectAsync0(() {})); + + tearDown(() async { + throw 'error'; + }); + + test('description', expectAsync0(() {})); + }); + + await _runTest(tests.single as Test, shouldFail: true); + }); + + test('runs in the same error zone as the test', () { + return expectTestsPass(() { + late Zone testBodyZone; + + tearDown(() { + final tearDownZone = Zone.current; + expect(tearDownZone.inSameErrorZone(testBodyZone), isTrue, + reason: 'The tear down callback is in a different error zone ' + 'than the test body.'); + }); + + test('test', () { + testBodyZone = Zone.current; + }); + }); + }); + }); + + group('in a group,', () { + test("tests inherit the group's description", () { + var entries = declare(() { + group('group', () { + test('description', () {}); + }); + }); + + expect(entries, hasLength(1)); + var testGroup = entries.single as Group; + expect(testGroup.name, equals('group')); + expect(testGroup.entries, hasLength(1)); + expect(testGroup.entries.single, const TypeMatcher<Test>()); + expect(testGroup.entries.single.name, 'group description'); + }); + + test("tests inherit the group's description when it's not a string", () { + var entries = declare(() { + group(Object, () { + test('description', () {}); + }); + }); + + expect(entries, hasLength(1)); + var testGroup = entries.single as Group; + expect(testGroup.name, equals('Object')); + expect(testGroup.entries, hasLength(1)); + expect(testGroup.entries.single, const TypeMatcher<Test>()); + expect(testGroup.entries.single.name, 'Object description'); + }); + + test("a test's timeout factor is applied to the group's", () { + var entries = declare(() { + group('group', () { + test('test', () {}, timeout: const Timeout.factor(3)); + }, timeout: const Timeout.factor(2)); + }); + + expect(entries, hasLength(1)); + var testGroup = entries.single as Group; + expect(testGroup.metadata.timeout.scaleFactor, equals(2)); + expect(testGroup.entries, hasLength(1)); + expect(testGroup.entries.single, const TypeMatcher<Test>()); + expect(testGroup.entries.single.metadata.timeout.scaleFactor, equals(6)); + }); + + test("a test's timeout factor is applied to the group's duration", () { + var entries = declare(() { + group('group', () { + test('test', () {}, timeout: const Timeout.factor(2)); + }, timeout: const Timeout(Duration(seconds: 10))); + }); + + expect(entries, hasLength(1)); + var testGroup = entries.single as Group; + expect(testGroup.metadata.timeout.duration, + equals(const Duration(seconds: 10))); + expect(testGroup.entries, hasLength(1)); + expect(testGroup.entries.single, const TypeMatcher<Test>()); + expect(testGroup.entries.single.metadata.timeout.duration, + equals(const Duration(seconds: 20))); + }); + + test("a test's timeout duration is applied over the group's", () { + var entries = declare(() { + group('group', () { + test('test', () {}, timeout: const Timeout(Duration(seconds: 15))); + }, timeout: const Timeout(Duration(seconds: 10))); + }); + + expect(entries, hasLength(1)); + var testGroup = entries.single as Group; + expect(testGroup.metadata.timeout.duration, + equals(const Duration(seconds: 10))); + expect(testGroup.entries, hasLength(1)); + expect(testGroup.entries.single, const TypeMatcher<Test>()); + expect(testGroup.entries.single.metadata.timeout.duration, + equals(const Duration(seconds: 15))); + }); + + test('disallows asynchronous groups', () async { + declare(() { + expect(() => group('group', () async {}), throwsArgumentError); + }); + }); + + group('.setUp()', () { + test('is scoped to the group', () async { + var setUpRun = false; + var entries = declare(() { + group('group', () { + setUp(() => setUpRun = true); + + test( + 'description 1', + expectAsync0(() { + expect(setUpRun, isTrue); + setUpRun = false; + }, max: 1)); + }); + + test( + 'description 2', + expectAsync0(() { + expect(setUpRun, isFalse); + setUpRun = false; + }, max: 1)); + }); + + await _runTest((entries[0] as Group).entries.single as Test); + await _runTest(entries[1] as Test); + }); + + test('runs from the outside in', () { + var outerSetUpRun = false; + var middleSetUpRun = false; + var innerSetUpRun = false; + var entries = declare(() { + setUp(expectAsync0(() { + expect(middleSetUpRun, isFalse); + expect(innerSetUpRun, isFalse); + outerSetUpRun = true; + }, max: 1)); + + group('middle', () { + setUp(expectAsync0(() { + expect(outerSetUpRun, isTrue); + expect(innerSetUpRun, isFalse); + middleSetUpRun = true; + }, max: 1)); + + group('inner', () { + setUp(expectAsync0(() { + expect(outerSetUpRun, isTrue); + expect(middleSetUpRun, isTrue); + innerSetUpRun = true; + }, max: 1)); + + test( + 'description', + expectAsync0(() { + expect(outerSetUpRun, isTrue); + expect(middleSetUpRun, isTrue); + expect(innerSetUpRun, isTrue); + }, max: 1)); + }); + }); + }); + + var middleGroup = entries.single as Group; + var innerGroup = middleGroup.entries.single as Group; + return _runTest(innerGroup.entries.single as Test); + }); + + test('handles Futures when chained', () { + var outerSetUpRun = false; + var innerSetUpRun = false; + var entries = declare(() { + setUp(expectAsync0(() { + expect(innerSetUpRun, isFalse); + return Future(() => outerSetUpRun = true); + }, max: 1)); + + group('inner', () { + setUp(expectAsync0(() { + expect(outerSetUpRun, isTrue); + return Future(() => innerSetUpRun = true); + }, max: 1)); + + test( + 'description', + expectAsync0(() { + expect(outerSetUpRun, isTrue); + expect(innerSetUpRun, isTrue); + }, max: 1)); + }); + }); + + var innerGroup = entries.single as Group; + return _runTest(innerGroup.entries.single as Test); + }); + + test("inherits group's tags", () { + var tests = declare(() { + group('outer', () { + group('inner', () { + test('with tags', () {}, tags: 'd'); + }, tags: ['b', 'c']); + }, tags: 'a'); + }); + + var outerGroup = tests.single as Group; + var innerGroup = outerGroup.entries.single as Group; + var testWithTags = innerGroup.entries.single; + expect(outerGroup.metadata.tags, unorderedEquals(['a'])); + expect(innerGroup.metadata.tags, unorderedEquals(['a', 'b', 'c'])); + expect( + testWithTags.metadata.tags, unorderedEquals(['a', 'b', 'c', 'd'])); + }); + + test('throws on invalid tags', () { + expect(() { + declare(() { + group('a', () {}, tags: 1); + }); + }, throwsArgumentError); + }); + }); + + group('.tearDown()', () { + test('is scoped to the group', () async { + late bool tearDownRun; + var entries = declare(() { + setUp(() => tearDownRun = false); + + group('group', () { + tearDown(() => tearDownRun = true); + + test( + 'description 1', + expectAsync0(() { + expect(tearDownRun, isFalse); + }, max: 1)); + }); + + test( + 'description 2', + expectAsync0(() { + expect(tearDownRun, isFalse); + }, max: 1)); + }); + + var testGroup = entries[0] as Group; + await _runTest(testGroup.entries.single as Test); + expect(tearDownRun, isTrue); + await _runTest(entries[1] as Test); + expect(tearDownRun, isFalse); + }); + + test('runs from the inside out', () async { + var innerTearDownRun = false; + var middleTearDownRun = false; + var outerTearDownRun = false; + var entries = declare(() { + tearDown(expectAsync0(() { + expect(innerTearDownRun, isTrue); + expect(middleTearDownRun, isTrue); + outerTearDownRun = true; + }, max: 1)); + + group('middle', () { + tearDown(expectAsync0(() { + expect(innerTearDownRun, isTrue); + expect(outerTearDownRun, isFalse); + middleTearDownRun = true; + }, max: 1)); + + group('inner', () { + tearDown(expectAsync0(() { + expect(outerTearDownRun, isFalse); + expect(middleTearDownRun, isFalse); + innerTearDownRun = true; + }, max: 1)); + + test( + 'description', + expectAsync0(() { + expect(outerTearDownRun, isFalse); + expect(middleTearDownRun, isFalse); + expect(innerTearDownRun, isFalse); + }, max: 1)); + }); + }); + }); + + var middleGroup = entries.single as Group; + var innerGroup = middleGroup.entries.single as Group; + await _runTest(innerGroup.entries.single as Test); + expect(innerTearDownRun, isTrue); + expect(middleTearDownRun, isTrue); + expect(outerTearDownRun, isTrue); + }); + + test('handles Futures when chained', () async { + var outerTearDownRun = false; + var innerTearDownRun = false; + var entries = declare(() { + tearDown(expectAsync0(() { + expect(innerTearDownRun, isTrue); + return Future(() => outerTearDownRun = true); + }, max: 1)); + + group('inner', () { + tearDown(expectAsync0(() { + expect(outerTearDownRun, isFalse); + return Future(() => innerTearDownRun = true); + }, max: 1)); + + test( + 'description', + expectAsync0(() { + expect(outerTearDownRun, isFalse); + expect(innerTearDownRun, isFalse); + }, max: 1)); + }); + }); + + var innerGroup = entries.single as Group; + await _runTest(innerGroup.entries.single as Test); + expect(innerTearDownRun, isTrue); + expect(outerTearDownRun, isTrue); + }); + + test('runs outer callbacks even when inner ones fail', () async { + var outerTearDownRun = false; + var entries = declare(() { + tearDown(() { + return Future(() => outerTearDownRun = true); + }); + + group('inner', () { + tearDown(() { + throw 'inner error'; + }); + + test( + 'description', + expectAsync0(() { + expect(outerTearDownRun, isFalse); + }, max: 1)); + }); + }); + + var innerGroup = entries.single as Group; + await _runTest(innerGroup.entries.single as Test, shouldFail: true); + expect(outerTearDownRun, isTrue); + }); + }); + }); + + group('duplicate names', () { + test('can be enabled', () { + expect( + () => declare(() { + test('a', expectAsync0(() {}, count: 0)); + test('a', expectAsync0(() {}, count: 0)); + }, allowDuplicateTestNames: false), + throwsA(isA<DuplicateTestNameException>() + .having((e) => e.name, 'name', 'a'))); + }); + + test('are allowed by default', () { + expect( + declare(() { + test('a', expectAsync0(() {}, count: 0)); + test('a', expectAsync0(() {}, count: 0)); + }).map((e) => e.name), + equals(['a', 'a'])); + }); + }); +} + +/// Runs [test]. +/// +/// This automatically sets up an `onError` listener to ensure that the test +/// doesn't throw any invisible exceptions. +Future _runTest(Test test, {bool shouldFail = false}) { + var liveTest = test.load(_suite); + + if (shouldFail) { + liveTest.onError.listen(expectAsync1((_) {})); + } else { + liveTest.onError.listen((e) => registerException(e.error, e.stackTrace)); + } + + return liveTest.run(); +}
diff --git a/pkgs/test_api/test/backend/invoker_test.dart b/pkgs/test_api/test/backend/invoker_test.dart new file mode 100644 index 0000000..ef08b38 --- /dev/null +++ b/pkgs/test_api/test/backend/invoker_test.dart
@@ -0,0 +1,581 @@ +// Copyright (c) 2015, 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 'package:test/test.dart'; +import 'package:test_api/src/backend/group.dart'; +import 'package:test_api/src/backend/invoker.dart'; +import 'package:test_api/src/backend/live_test.dart'; +import 'package:test_api/src/backend/message.dart'; +import 'package:test_api/src/backend/metadata.dart'; +import 'package:test_api/src/backend/state.dart'; +import 'package:test_api/src/backend/suite.dart'; + +import '../utils.dart'; + +void main() { + late Suite suite; + setUp(() { + lastState = null; + suite = Suite(Group.root([]), suitePlatform, ignoreTimeouts: false); + }); + + group('Invoker.current', () { + var invoker = Invoker.current; + test('returns null outside of a test body', () { + expect(invoker, isNull); + }); + + test('returns the current invoker in a test body', () async { + late Invoker invoker; + var liveTest = _localTest(() { + invoker = Invoker.current!; + }).load(suite); + liveTest.onError.listen(expectAsync1((_) {}, count: 0)); + + await liveTest.run(); + expect(invoker.liveTest, equals(liveTest)); + }); + + test('returns the current invoker in a test body after the test completes', + () async { + Status? status; + var completer = Completer<Invoker>(); + var liveTest = _localTest(() { + // Use the event loop to wait longer than a microtask for the test to + // complete. + Future(() { + status = Invoker.current!.liveTest.state.status; + completer.complete(Invoker.current); + }); + }).load(suite); + liveTest.onError.listen(expectAsync1((_) {}, count: 0)); + + expect(liveTest.run(), completes); + var invoker = await completer.future; + expect(invoker.liveTest, equals(liveTest)); + expect(status, equals(Status.complete)); + }); + }); + + group('in a successful test,', () { + test('the state changes from pending to running to complete', () async { + late State stateInTest; + late LiveTest liveTest; + liveTest = _localTest(() { + stateInTest = liveTest.state; + }).load(suite); + liveTest.onError.listen(expectAsync1((_) {}, count: 0)); + + expect(liveTest.state.status, equals(Status.pending)); + expect(liveTest.state.result, equals(Result.success)); + + var future = liveTest.run(); + + expect(liveTest.state.status, equals(Status.running)); + expect(liveTest.state.result, equals(Result.success)); + + await future; + + expect(stateInTest.status, equals(Status.running)); + expect(stateInTest.result, equals(Result.success)); + + expect(liveTest.state.status, equals(Status.complete)); + expect(liveTest.state.result, equals(Result.success)); + }); + + test('onStateChange fires for each state change', () { + var liveTest = _localTest(() {}).load(suite); + liveTest.onError.listen(expectAsync1((_) {}, count: 0)); + + var first = true; + liveTest.onStateChange.listen(expectAsync1((state) { + if (first) { + expect(state.status, equals(Status.running)); + first = false; + } else { + expect(state.status, equals(Status.complete)); + } + expect(state.result, equals(Result.success)); + }, count: 2, max: 2)); + + return liveTest.run(); + }); + + test('onComplete completes once the test body is done', () { + var testRun = false; + var liveTest = _localTest(() { + testRun = true; + }).load(suite); + + expect(liveTest.onComplete.then((_) { + expect(testRun, isTrue); + }), completes); + + return liveTest.run(); + }); + }); + + group('in a test with failures,', () { + test('a synchronous throw is reported and causes the test to fail', () { + var liveTest = _localTest(() { + throw TestFailure('oh no'); + }).load(suite); + + expectSingleFailure(liveTest); + return liveTest.run(); + }); + + test('a synchronous reported failure causes the test to fail', () { + var liveTest = _localTest(() { + registerException(TestFailure('oh no')); + }).load(suite); + + expectSingleFailure(liveTest); + return liveTest.run(); + }); + + test('a failure reported asynchronously during the test causes it to fail', + () { + var liveTest = _localTest(() { + Invoker.current!.addOutstandingCallback(); + Future(() => registerException(TestFailure('oh no'))) + .whenComplete(Invoker.current!.removeOutstandingCallback); + }).load(suite); + + expectSingleFailure(liveTest); + return liveTest.run(); + }); + + test('a failure thrown asynchronously during the test causes it to fail', + () { + var liveTest = _localTest(() { + Invoker.current!.addOutstandingCallback(); + Future(() => throw TestFailure('oh no')) + .whenComplete(Invoker.current!.removeOutstandingCallback); + }).load(suite); + + expectSingleFailure(liveTest); + return liveTest.run(); + }); + + test('a failure reported asynchronously after the test causes it to error', + () { + var liveTest = _localTest(() { + Future(() => registerException(TestFailure('oh no'))); + }).load(suite); + + expectStates(liveTest, [ + const State(Status.running, Result.success), + const State(Status.complete, Result.success), + const State(Status.complete, Result.failure), + const State(Status.complete, Result.error) + ]); + + expectErrors(liveTest, [ + (error) { + expect( + lastState, equals(const State(Status.complete, Result.failure))); + expect(error, isTestFailure('oh no')); + }, + (error) { + expect(lastState, equals(const State(Status.complete, Result.error))); + expect( + error, + equals('This test failed after it had already completed.\n' + 'Make sure to use a matching library which informs the ' + 'test runner\nof pending async work.')); + } + ]); + + return liveTest.run(); + }); + + test('multiple asynchronous failures are reported', () { + var liveTest = _localTest(() { + Invoker.current!.addOutstandingCallback(); + Future(() => throw TestFailure('one')); + Future(() => throw TestFailure('two')); + Future(() => throw TestFailure('three')); + Future(() => throw TestFailure('four')) + .whenComplete(Invoker.current!.removeOutstandingCallback); + }).load(suite); + + expectStates(liveTest, [ + const State(Status.running, Result.success), + const State(Status.complete, Result.failure) + ]); + + expectErrors(liveTest, [ + (error) { + expect(lastState?.status, equals(Status.complete)); + expect(error, isTestFailure('one')); + }, + (error) { + expect(error, isTestFailure('two')); + }, + (error) { + expect(error, isTestFailure('three')); + }, + (error) { + expect(error, isTestFailure('four')); + } + ]); + + return liveTest.run(); + }); + + test("a failure after an error doesn't change the state of the test", () { + var liveTest = _localTest(() { + Future(() => throw TestFailure('fail')); + throw 'error'; + }).load(suite); + + expectStates(liveTest, [ + const State(Status.running, Result.success), + const State(Status.complete, Result.error) + ]); + + expectErrors(liveTest, [ + (error) { + expect(lastState, equals(const State(Status.complete, Result.error))); + expect(error, equals('error')); + }, + (error) { + expect(error, isTestFailure('fail')); + } + ]); + + return liveTest.run(); + }); + }); + + group('in a test with errors,', () { + test('a synchronous throw is reported and causes the test to error', () { + var liveTest = _localTest(() { + throw 'oh no'; + }).load(suite); + + expectSingleError(liveTest); + return liveTest.run(); + }); + + test('a synchronous reported error causes the test to error', () { + var liveTest = _localTest(() { + registerException('oh no'); + }).load(suite); + + expectSingleError(liveTest); + return liveTest.run(); + }); + + test('an error reported asynchronously during the test causes it to error', + () { + var liveTest = _localTest(() { + Invoker.current!.addOutstandingCallback(); + Future(() => registerException('oh no')) + .whenComplete(Invoker.current!.removeOutstandingCallback); + }).load(suite); + + expectSingleError(liveTest); + return liveTest.run(); + }); + + test('an error thrown asynchronously during the test causes it to error', + () { + var liveTest = _localTest(() { + Invoker.current!.addOutstandingCallback(); + Future(() => throw 'oh no') + .whenComplete(Invoker.current!.removeOutstandingCallback); + }).load(suite); + + expectSingleError(liveTest); + return liveTest.run(); + }); + + test('an error reported asynchronously after the test causes it to error', + () { + var liveTest = _localTest(() { + Future(() => registerException('oh no')); + }).load(suite); + + expectStates(liveTest, [ + const State(Status.running, Result.success), + const State(Status.complete, Result.success), + const State(Status.complete, Result.error) + ]); + + expectErrors(liveTest, [ + (error) { + expect(lastState, equals(const State(Status.complete, Result.error))); + expect(error, equals('oh no')); + }, + (error) { + expect( + error, + equals('This test failed after it had already completed.\n' + 'Make sure to use a matching library which informs the ' + 'test runner\nof pending async work.')); + } + ]); + + return liveTest.run(); + }); + + test('multiple asynchronous errors are reported', () { + var liveTest = _localTest(() { + Invoker.current!.addOutstandingCallback(); + Future(() => throw 'one'); + Future(() => throw 'two'); + Future(() => throw 'three'); + Future(() => throw 'four') + .whenComplete(Invoker.current!.removeOutstandingCallback); + }).load(suite); + + expectStates(liveTest, [ + const State(Status.running, Result.success), + const State(Status.complete, Result.error) + ]); + + expectErrors(liveTest, [ + (error) { + expect(lastState?.status, equals(Status.complete)); + expect(error, equals('one')); + }, + (error) { + expect(error, equals('two')); + }, + (error) { + expect(error, equals('three')); + }, + (error) { + expect(error, equals('four')); + } + ]); + + return liveTest.run(); + }); + + test('an error after a failure changes the state of the test', () { + var liveTest = _localTest(() { + Future(() => throw 'error'); + throw TestFailure('fail'); + }).load(suite); + + expectStates(liveTest, [ + const State(Status.running, Result.success), + const State(Status.complete, Result.failure), + const State(Status.complete, Result.error) + ]); + + expectErrors(liveTest, [ + (error) { + expect( + lastState, equals(const State(Status.complete, Result.failure))); + expect(error, isTestFailure('fail')); + }, + (error) { + expect(lastState, equals(const State(Status.complete, Result.error))); + expect(error, equals('error')); + } + ]); + + return liveTest.run(); + }); + }); + + test("a test doesn't complete until there are no outstanding callbacks", + () async { + var outstandingCallbackRemoved = false; + var liveTest = _localTest(() { + Invoker.current!.addOutstandingCallback(); + + // Pump the event queue to make sure the test isn't coincidentally + // completing after the outstanding callback is removed. + pumpEventQueue().then((_) { + outstandingCallbackRemoved = true; + Invoker.current!.removeOutstandingCallback(); + }); + }).load(suite); + + liveTest.onError.listen(expectAsync1((_) {}, count: 0)); + + await liveTest.run(); + expect(outstandingCallbackRemoved, isTrue); + }); + + test("a test's prints are captured and reported", () { + expect(() { + var liveTest = _localTest(() { + print('Hello,'); + return Future(() => print('world!')); + }).load(suite); + + expect( + liveTest.onMessage.take(2).toList().then((messages) { + expect(messages[0].type, equals(MessageType.print)); + expect(messages[0].text, equals('Hello,')); + expect(messages[1].type, equals(MessageType.print)); + expect(messages[1].text, equals('world!')); + }), + completes); + + return liveTest.run(); + }, prints(isEmpty)); + }); + + group('timeout:', () { + test('A test can be timed out', () { + var liveTest = _localTest(() { + Invoker.current!.addOutstandingCallback(); + }, + metadata: Metadata( + chainStackTraces: true, + timeout: const Timeout(Duration.zero))) + .load(suite); + + expectStates(liveTest, [ + const State(Status.running, Result.success), + const State(Status.complete, Result.error) + ]); + + expectErrors(liveTest, [ + (error) { + expect(lastState!.status, equals(Status.complete)); + expect(error, const TypeMatcher<TimeoutException>()); + } + ]); + + liveTest.run(); + }); + + test('can be ignored', () { + suite = Suite(Group.root([]), suitePlatform, ignoreTimeouts: true); + var liveTest = _localTest(() async { + await Future<void>.delayed(const Duration(milliseconds: 10)); + }, + metadata: Metadata( + chainStackTraces: true, + timeout: const Timeout(Duration.zero))) + .load(suite); + + expectStates(liveTest, [ + const State(Status.running, Result.success), + const State(Status.complete, Result.success) + ]); + + liveTest.run(); + }); + }); + + group('runTearDowns', () { + test('runs multiple tear downs', () async { + var firstTearDownStarted = false; + var secondTearDownStarted = false; + await Invoker.current!.runTearDowns([ + () { + firstTearDownStarted = true; + }, + () { + secondTearDownStarted = true; + } + ]); + expect(secondTearDownStarted, isTrue); + expect(firstTearDownStarted, isTrue); + }); + + test('waits for the future returned tear downs to complete', () async { + var firstTearDownWork = Completer<void>(); + var secondTearDownStarted = false; + var result = Invoker.current!.runTearDowns([ + () { + secondTearDownStarted = true; + }, + () async { + await firstTearDownWork.future; + }, + ]); + await pumpEventQueue(); + expect(secondTearDownStarted, isFalse); + firstTearDownWork.complete(); + await result; + expect(secondTearDownStarted, isTrue); + }); + + test('allows next tear down to run while there are still prior callbacks', + () async { + var firstTearDownAsyncWork = Completer<void>(); + var secondTearDownStarted = false; + unawaited(Invoker.current!.runTearDowns([ + () { + secondTearDownStarted = true; + }, + () { + Invoker.current!.addOutstandingCallback(); + firstTearDownAsyncWork.future + .whenComplete(Invoker.current!.removeOutstandingCallback); + }, + ])); + await pumpEventQueue(); + expect(secondTearDownStarted, isTrue); + firstTearDownAsyncWork.complete(); + }); + + test('forwards errors to the enclosing test but does not end it', () async { + var liveTest = _localTest(() async { + Invoker.current!.addOutstandingCallback(); + await Invoker.current!.runTearDowns([ + () { + throw 'oh no'; + } + ]); + }).load(suite); + + expectStates(liveTest, [ + const State(Status.running, Result.success), + const State(Status.complete, Result.error) + ]); + + var isComplete = false; + unawaited(liveTest.run().then((_) => isComplete = true)); + await pumpEventQueue(); + expect(liveTest.state.status, equals(Status.complete)); + expect(isComplete, isFalse); + }); + }); + + group('printOnFailure:', () { + test("doesn't print anything if the test succeeds", () async { + var liveTest = _localTest(() { + Invoker.current!.printOnFailure('only on failure'); + }).load(suite); + liveTest.onError.listen(expectAsync1((_) {}, count: 0)); + + liveTest.onMessage.listen(expectAsync1((_) {}, count: 0)); + + await liveTest.run(); + }); + + test('prints if the test fails', () async { + var liveTest = _localTest(() { + Invoker.current!.printOnFailure('only on failure'); + expect(true, isFalse); + }).load(suite); + liveTest.onError.listen(expectAsync1((_) {}, count: 1)); + + liveTest.onMessage.listen(expectAsync1((message) { + expect(message.type, equals(MessageType.print)); + expect(message.text, equals('only on failure')); + }, count: 1)); + + await liveTest.run(); + }); + }); +} + +LocalTest _localTest(dynamic Function() body, {Metadata? metadata}) { + metadata ??= Metadata(chainStackTraces: true); + return LocalTest('test', metadata, body); +}
diff --git a/pkgs/test_api/test/backend/metadata_test.dart b/pkgs/test_api/test/backend/metadata_test.dart new file mode 100644 index 0000000..f42a535 --- /dev/null +++ b/pkgs/test_api/test/backend/metadata_test.dart
@@ -0,0 +1,257 @@ +// Copyright (c) 2015, 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:boolean_selector/boolean_selector.dart'; +import 'package:test/test.dart'; +import 'package:test_api/src/backend/metadata.dart'; +import 'package:test_api/src/backend/platform_selector.dart'; +import 'package:test_api/src/backend/runtime.dart'; +import 'package:test_api/src/backend/suite_platform.dart'; + +void main() { + group('tags', () { + test('parses an Iterable', () { + expect( + Metadata.parse(tags: ['a', 'b']).tags, unorderedEquals(['a', 'b'])); + }); + + test('parses a String', () { + expect(Metadata.parse(tags: 'a').tags, unorderedEquals(['a'])); + }); + + test('parses null', () { + expect(Metadata.parse().tags, unorderedEquals([])); + }); + + test('parse refuses an invalid type', () { + expect(() => Metadata.parse(tags: 1), throwsArgumentError); + }); + + test('parse refuses an invalid type in a list', () { + expect(() => Metadata.parse(tags: [1]), throwsArgumentError); + }); + + test('merges tags by computing the union of the two tag sets', () { + var merged = Metadata(tags: ['a', 'b']).merge(Metadata(tags: ['b', 'c'])); + expect(merged.tags, unorderedEquals(['a', 'b', 'c'])); + }); + + test('serializes and deserializes tags', () { + var metadata = Metadata(tags: ['a', 'b']).serialize(); + expect(Metadata.deserialize(metadata).tags, unorderedEquals(['a', 'b'])); + }); + }); + + group('constructor', () { + test("returns the normal metadata if there's no forTag", () { + var metadata = Metadata(verboseTrace: true, tags: ['foo', 'bar']); + expect(metadata.verboseTrace, isTrue); + expect(metadata.tags, equals(['foo', 'bar'])); + }); + + test("returns the normal metadata if there's no tags", () { + var metadata = Metadata( + verboseTrace: true, + forTag: {BooleanSelector.parse('foo'): Metadata(skip: true)}); + expect(metadata.verboseTrace, isTrue); + expect(metadata.skip, isFalse); + expect(metadata.forTag, contains(BooleanSelector.parse('foo'))); + expect(metadata.forTag[BooleanSelector.parse('foo')]?.skip, isTrue); + }); + + test("returns the normal metadata if forTag doesn't match tags", () { + var metadata = Metadata( + verboseTrace: true, + tags: ['bar', 'baz'], + forTag: {BooleanSelector.parse('foo'): Metadata(skip: true)}); + + expect(metadata.verboseTrace, isTrue); + expect(metadata.skip, isFalse); + expect(metadata.tags, unorderedEquals(['bar', 'baz'])); + expect(metadata.forTag, contains(BooleanSelector.parse('foo'))); + expect(metadata.forTag[BooleanSelector.parse('foo')]?.skip, isTrue); + }); + + test('resolves forTags that match tags', () { + var metadata = Metadata(verboseTrace: true, tags: [ + 'foo', + 'bar', + 'baz' + ], forTag: { + BooleanSelector.parse('foo'): Metadata(skip: true), + BooleanSelector.parse('baz'): Metadata(timeout: Timeout.none), + BooleanSelector.parse('qux'): Metadata(skipReason: 'blah') + }); + + expect(metadata.verboseTrace, isTrue); + expect(metadata.skip, isTrue); + expect(metadata.skipReason, isNull); + expect(metadata.timeout, equals(Timeout.none)); + expect(metadata.tags, unorderedEquals(['foo', 'bar', 'baz'])); + expect(metadata.forTag.keys, equals([BooleanSelector.parse('qux')])); + }); + + test('resolves forTags that adds a behavioral tag', () { + var metadata = Metadata(tags: [ + 'foo' + ], forTag: { + BooleanSelector.parse('baz'): Metadata(skip: true), + BooleanSelector.parse('bar'): + Metadata(verboseTrace: true, tags: ['baz']), + BooleanSelector.parse('foo'): Metadata(tags: ['bar']) + }); + + expect(metadata.verboseTrace, isTrue); + expect(metadata.skip, isTrue); + expect(metadata.tags, unorderedEquals(['foo', 'bar', 'baz'])); + expect(metadata.forTag, isEmpty); + }); + + test('resolves forTags that adds circular tags', () { + var metadata = Metadata(tags: [ + 'foo' + ], forTag: { + BooleanSelector.parse('foo'): Metadata(tags: ['bar']), + BooleanSelector.parse('bar'): Metadata(tags: ['baz']), + BooleanSelector.parse('baz'): Metadata(tags: ['foo']) + }); + + expect(metadata.tags, unorderedEquals(['foo', 'bar', 'baz'])); + expect(metadata.forTag, isEmpty); + }); + + test('base metadata takes precedence over forTags', () { + var metadata = Metadata(verboseTrace: true, tags: [ + 'foo' + ], forTag: { + BooleanSelector.parse('foo'): Metadata(verboseTrace: false) + }); + + expect(metadata.verboseTrace, isTrue); + }); + }); + + group('onPlatform', () { + test('parses a valid map', () { + var metadata = Metadata.parse(onPlatform: { + 'chrome': const Timeout.factor(2), + 'vm': [const Skip(), const Timeout.factor(3)] + }); + + var key = metadata.onPlatform.keys.first; + expect( + key.evaluate(SuitePlatform(Runtime.chrome, compiler: null)), isTrue); + expect(key.evaluate(SuitePlatform(Runtime.vm, compiler: null)), isFalse); + var value = metadata.onPlatform.values.first; + expect(value.timeout.scaleFactor, equals(2)); + + key = metadata.onPlatform.keys.last; + expect(key.evaluate(SuitePlatform(Runtime.vm, compiler: null)), isTrue); + expect( + key.evaluate(SuitePlatform(Runtime.chrome, compiler: null)), isFalse); + value = metadata.onPlatform.values.last; + expect(value.skip, isTrue); + expect(value.timeout.scaleFactor, equals(3)); + }); + + test('refuses an invalid value', () { + expect(() { + Metadata.parse(onPlatform: {'chrome': const TestOn('chrome')}); + }, throwsArgumentError); + }); + + test('refuses an invalid value in a list', () { + expect(() { + Metadata.parse(onPlatform: { + 'chrome': [const TestOn('chrome')] + }); + }, throwsArgumentError); + }); + + test('refuses an invalid platform selector', () { + expect(() { + Metadata.parse(onPlatform: {'vm &&': const Skip()}); + }, throwsFormatException); + }); + + test('refuses multiple Timeouts', () { + expect(() { + Metadata.parse(onPlatform: { + 'chrome': [const Timeout.factor(2), const Timeout.factor(3)] + }); + }, throwsArgumentError); + }); + + test('refuses multiple Skips', () { + expect(() { + Metadata.parse(onPlatform: { + 'chrome': [const Skip(), const Skip()] + }); + }, throwsArgumentError); + }); + }); + + group('validatePlatformSelectors', () { + test('succeeds if onPlatform uses valid platforms', () { + Metadata.parse(onPlatform: {'vm || browser': const Skip()}) + .validatePlatformSelectors({'vm'}); + }); + + test('succeeds if testOn uses valid platforms', () { + Metadata.parse(testOn: 'vm || browser').validatePlatformSelectors({'vm'}); + }); + + test('succeeds if testOn uses valid compilers', () { + Metadata.parse(testOn: 'dart2js || kernel').validatePlatformSelectors({}); + }); + + test('fails if onPlatform uses an invalid platform', () { + expect(() { + Metadata.parse(onPlatform: {'unknown': const Skip()}) + .validatePlatformSelectors({'vm'}); + }, throwsFormatException); + }); + + test('fails if testOn uses an invalid platform', () { + expect(() { + Metadata.parse(testOn: 'unknown').validatePlatformSelectors({'vm'}); + }, throwsFormatException); + }); + + test('fails if testOn uses an invalid compiler', () { + expect(() { + Metadata.parse(testOn: 'foo2bar').validatePlatformSelectors({}); + }, throwsFormatException); + }); + }); + + group('change', () { + test('preserves all fields if no parameters are passed', () { + var metadata = Metadata( + testOn: PlatformSelector.parse('linux'), + timeout: const Timeout.factor(2), + skip: true, + skipReason: 'just because', + verboseTrace: true, + tags: [ + 'foo', + 'bar' + ], + onPlatform: { + PlatformSelector.parse('mac-os'): Metadata(skip: false) + }, + forTag: { + BooleanSelector.parse('slow'): + Metadata(timeout: const Timeout.factor(4)) + }); + expect(metadata.serialize(), equals(metadata.change().serialize())); + }); + + test('updates a changed field', () { + var metadata = Metadata(timeout: const Timeout.factor(2)); + expect(metadata.change(timeout: const Timeout.factor(3)).timeout, + equals(const Timeout.factor(3))); + }); + }); +}
diff --git a/pkgs/test_api/test/frontend/add_tear_down_test.dart b/pkgs/test_api/test/frontend/add_tear_down_test.dart new file mode 100644 index 0000000..de4bb55 --- /dev/null +++ b/pkgs/test_api/test/frontend/add_tear_down_test.dart
@@ -0,0 +1,798 @@ +// Copyright (c) 2016, 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 'package:async/async.dart'; +import 'package:test/test.dart'; + +import '../utils.dart'; + +void main() { + group('in a test', () { + test('runs after the test body', () { + return expectTestsPass(() { + var test1Run = false; + var tearDownRun = false; + test('test 1', () { + addTearDown(() { + expect(test1Run, isTrue); + expect(tearDownRun, isFalse); + tearDownRun = true; + }); + + expect(tearDownRun, isFalse); + test1Run = true; + }); + + test('test 2', () { + expect(tearDownRun, isTrue); + }); + }); + }); + + test('multiples run in reverse order', () { + return expectTestsPass(() { + var tearDown1Run = false; + var tearDown2Run = false; + var tearDown3Run = false; + + test('test 1', () { + addTearDown(() { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isTrue); + tearDown1Run = true; + }); + + addTearDown(() { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isTrue); + tearDown2Run = true; + }); + + addTearDown(() { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + tearDown3Run = true; + }); + + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + }); + + test('test 2', () { + expect(tearDown1Run, isTrue); + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isTrue); + }); + }); + }); + + test('can be called in addTearDown', () { + return expectTestsPass(() { + var tearDown2Run = false; + var tearDown3Run = false; + + test('test 1', () { + addTearDown(() { + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isFalse); + tearDown3Run = true; + }); + + addTearDown(() { + addTearDown(() { + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + tearDown2Run = true; + }); + }); + }); + + test('test 2', () { + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isTrue); + }); + }); + }); + + test('can be called in tearDown', () { + return expectTestsPass(() { + var tearDown2Run = false; + var tearDown3Run = false; + + tearDown(() { + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isFalse); + tearDown3Run = true; + }); + + tearDown(() { + tearDown2Run = false; + tearDown3Run = false; + + addTearDown(() { + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + tearDown2Run = true; + }); + }); + + test('test 1', () {}); + + test('test 2', () { + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isTrue); + }); + }); + }); + + test('runs before a normal tearDown', () { + return expectTestsPass(() { + var groupTearDownRun = false; + var testTearDownRun = false; + group('group', () { + tearDown(() { + expect(testTearDownRun, isTrue); + expect(groupTearDownRun, isFalse); + groupTearDownRun = true; + }); + + test('test 1', () { + addTearDown(() { + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isFalse); + testTearDownRun = true; + }); + + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isFalse); + }); + }); + + test('test 2', () { + expect(groupTearDownRun, isTrue); + expect(testTearDownRun, isTrue); + }); + }); + }); + + test('runs in the same error zone as the test', () { + return expectTestsPass(() { + test('test', () { + final testBodyZone = Zone.current; + + addTearDown(() { + final tearDownZone = Zone.current; + expect(tearDownZone.inSameErrorZone(testBodyZone), isTrue, + reason: 'The tear down callback is in a different error zone ' + 'than the test body.'); + }); + }); + }); + }); + + group('asynchronously', () { + test('blocks additional test tearDowns on in-band async', () { + return expectTestsPass(() { + var tearDown1Run = false; + var tearDown2Run = false; + var tearDown3Run = false; + test('test', () { + addTearDown(() async { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isTrue); + await pumpEventQueue(); + tearDown1Run = true; + }); + + addTearDown(() async { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isTrue); + await pumpEventQueue(); + tearDown2Run = true; + }); + + addTearDown(() async { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + await pumpEventQueue(); + tearDown3Run = true; + }); + + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + }); + }); + }); + + test("doesn't block additional test tearDowns on out-of-band async", () { + return expectTestsPass(() { + var tearDown1Run = false; + var tearDown2Run = false; + var tearDown3Run = false; + test('test', () { + addTearDown(() { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + + expect(Future(() { + tearDown1Run = true; + }), completes); + }); + + addTearDown(() { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + + expect(Future(() { + tearDown2Run = true; + }), completes); + }); + + addTearDown(() { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + + expect(Future(() { + tearDown3Run = true; + }), completes); + }); + + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + }); + }); + }); + + test('blocks additional group tearDowns on in-band async', () { + return expectTestsPass(() { + var groupTearDownRun = false; + var testTearDownRun = false; + tearDown(() async { + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isTrue); + await pumpEventQueue(); + groupTearDownRun = true; + }); + + test('test', () { + addTearDown(() async { + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isFalse); + await pumpEventQueue(); + testTearDownRun = true; + }); + + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isFalse); + }); + }); + }); + + test("doesn't block additional group tearDowns on out-of-band async", () { + return expectTestsPass(() { + var groupTearDownRun = false; + var testTearDownRun = false; + tearDown(() { + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isFalse); + + expect(Future(() { + groupTearDownRun = true; + }), completes); + }); + + test('test', () { + addTearDown(() { + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isFalse); + + expect(Future(() { + testTearDownRun = true; + }), completes); + }); + + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isFalse); + }); + }); + }); + + test('blocks further tests on in-band async', () { + return expectTestsPass(() { + var tearDownRun = false; + test('test 1', () { + addTearDown(() async { + expect(tearDownRun, isFalse); + await pumpEventQueue(); + tearDownRun = true; + }); + }); + + test('test 2', () { + expect(tearDownRun, isTrue); + }); + }); + }); + + test('blocks further tests on out-of-band async', () { + return expectTestsPass(() { + var tearDownRun = false; + test('test 1', () { + addTearDown(() async { + expect(tearDownRun, isFalse); + expect( + pumpEventQueue().then((_) { + tearDownRun = true; + }), + completes); + }); + }); + + test('after', () { + expect(tearDownRun, isTrue); + }); + }); + }); + }); + + group('with an error', () { + test('reports the error', () async { + var engine = declareEngine(() { + test('test', () { + addTearDown(() => throw TestFailure('fail')); + }); + }); + + var queue = StreamQueue(engine.onTestStarted); + var liveTestFuture = queue.next; + + expect(await engine.run(), isFalse); + + var liveTest = await liveTestFuture; + expect(liveTest.test.name, equals('test')); + expectTestFailed(liveTest, 'fail'); + }); + + test('runs further test tearDowns', () async { + // Declare this in the outer test so if it doesn't run, the outer test + // will fail. + var shouldRun = expectAsync0(() {}); + + var engine = declareEngine(() { + test('test', () { + addTearDown(() => throw 'error'); + addTearDown(shouldRun); + }); + }); + + expect(await engine.run(), isFalse); + }); + + test('runs further group tearDowns', () async { + // Declare this in the outer test so if it doesn't run, the outer test + // will fail. + var shouldRun = expectAsync0(() {}); + + var engine = declareEngine(() { + tearDown(shouldRun); + + test('test', () { + addTearDown(() => throw 'error'); + }); + }); + + expect(await engine.run(), isFalse); + }); + }); + }); + + group('in setUpAll()', () { + test('runs after all tests', () async { + var test1Run = false; + var test2Run = false; + var tearDownRun = false; + await expectTestsPass(() { + setUpAll(() { + addTearDown(() { + expect(test1Run, isTrue); + expect(test2Run, isTrue); + expect(tearDownRun, isFalse); + tearDownRun = true; + }); + }); + + test('test 1', () { + test1Run = true; + expect(tearDownRun, isFalse); + }); + + test('test 2', () { + test2Run = true; + expect(tearDownRun, isFalse); + }); + }); + + expect(test1Run, isTrue); + expect(test2Run, isTrue); + expect(tearDownRun, isTrue); + }); + + test('multiples run in reverse order', () async { + var tearDown1Run = false; + var tearDown2Run = false; + var tearDown3Run = false; + await expectTestsPass(() { + setUpAll(() { + addTearDown(() { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isTrue); + tearDown1Run = true; + }); + + addTearDown(() { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isTrue); + tearDown2Run = true; + }); + + addTearDown(() { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + tearDown3Run = true; + }); + + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + }); + + test('test', () { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + }); + }); + + expect(tearDown1Run, isTrue); + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isTrue); + }); + + test('can be called in addTearDown', () async { + var tearDown2Run = false; + var tearDown3Run = false; + await expectTestsPass(() { + setUpAll(() { + addTearDown(() { + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isFalse); + tearDown3Run = true; + }); + + addTearDown(() { + addTearDown(() { + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + tearDown2Run = true; + }); + }); + }); + + test('test', () { + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + }); + }); + + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isTrue); + }); + + test('can be called in tearDownAll', () async { + var tearDown2Run = false; + var tearDown3Run = false; + await expectTestsPass(() { + tearDownAll(() { + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isFalse); + tearDown3Run = true; + }); + + tearDownAll(() { + tearDown2Run = false; + tearDown3Run = false; + + addTearDown(() { + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + tearDown2Run = true; + }); + }); + + test('test', () {}); + }); + + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isTrue); + }); + + test('runs before a normal tearDownAll', () async { + var groupTearDownRun = false; + var testTearDownRun = false; + await expectTestsPass(() { + tearDownAll(() { + expect(testTearDownRun, isTrue); + expect(groupTearDownRun, isFalse); + groupTearDownRun = true; + }); + + setUpAll(() { + addTearDown(() { + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isFalse); + testTearDownRun = true; + }); + }); + + test('test', () { + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isFalse); + }); + }); + + expect(groupTearDownRun, isTrue); + expect(testTearDownRun, isTrue); + }); + + test('runs in the same error zone as the setUpAll', () async { + return expectTestsPass(() { + setUpAll(() { + final setUpAllZone = Zone.current; + + addTearDown(() { + final tearDownZone = Zone.current; + expect(tearDownZone.inSameErrorZone(setUpAllZone), isTrue, + reason: 'The tear down callback is in a different error zone ' + 'than the set up all callback.'); + }); + }); + + test('test', () {}); + }); + }); + + group('asynchronously', () { + test('blocks additional tearDowns on in-band async', () async { + var tearDown1Run = false; + var tearDown2Run = false; + var tearDown3Run = false; + await expectTestsPass(() { + setUpAll(() { + addTearDown(() async { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isTrue); + await pumpEventQueue(); + tearDown1Run = true; + }); + + addTearDown(() async { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isTrue); + await pumpEventQueue(); + tearDown2Run = true; + }); + + addTearDown(() async { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + await pumpEventQueue(); + tearDown3Run = true; + }); + }); + + test('test', () { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + }); + }); + + expect(tearDown1Run, isTrue); + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isTrue); + }); + + test("doesn't block additional tearDowns on out-of-band async", () async { + var tearDown1Run = false; + var tearDown2Run = false; + var tearDown3Run = false; + await expectTestsPass(() { + setUpAll(() { + addTearDown(() { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + + expect(Future(() { + tearDown1Run = true; + }), completes); + }); + + addTearDown(() { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + + expect(Future(() { + tearDown2Run = true; + }), completes); + }); + + addTearDown(() { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + + expect(Future(() { + tearDown3Run = true; + }), completes); + }); + }); + + test('test', () { + expect(tearDown1Run, isFalse); + expect(tearDown2Run, isFalse); + expect(tearDown3Run, isFalse); + }); + }); + + expect(tearDown1Run, isTrue); + expect(tearDown2Run, isTrue); + expect(tearDown3Run, isTrue); + }); + + test('blocks additional tearDownAlls on in-band async', () async { + var groupTearDownRun = false; + var testTearDownRun = false; + await expectTestsPass(() { + tearDownAll(() async { + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isTrue); + await pumpEventQueue(); + groupTearDownRun = true; + }); + + setUpAll(() { + addTearDown(() async { + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isFalse); + await pumpEventQueue(); + testTearDownRun = true; + }); + }); + + test('test', () { + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isFalse); + }); + }); + + expect(groupTearDownRun, isTrue); + expect(testTearDownRun, isTrue); + }); + + test("doesn't block additional tearDownAlls on out-of-band async", + () async { + var groupTearDownRun = false; + var testTearDownRun = false; + await expectTestsPass(() { + tearDownAll(() { + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isFalse); + + expect(Future(() { + groupTearDownRun = true; + }), completes); + }); + + setUpAll(() { + addTearDown(() { + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isFalse); + + expect(Future(() { + testTearDownRun = true; + }), completes); + }); + }); + + test('test', () { + expect(groupTearDownRun, isFalse); + expect(testTearDownRun, isFalse); + }); + }); + + expect(groupTearDownRun, isTrue); + expect(testTearDownRun, isTrue); + }); + }); + + group('with an error', () { + test('reports the error', () async { + var engine = declareEngine(() { + setUpAll(() { + addTearDown(() => throw TestFailure('fail')); + }); + + test('test', () {}); + }); + + var queue = StreamQueue(engine.onTestStarted); + unawaited(queue.skip(2)); + var liveTestFuture = queue.next; + + expect(await engine.run(), isFalse); + + var liveTest = await liveTestFuture; + expect(liveTest.test.name, equals('(tearDownAll)')); + expectTestFailed(liveTest, 'fail'); + }); + + test('runs further tearDowns', () async { + // Declare this in the outer test so if it doesn't run, the outer test + // will fail. + var shouldRun = expectAsync0(() {}); + + var engine = declareEngine(() { + setUpAll(() { + addTearDown(() => throw 'error'); + addTearDown(shouldRun); + }); + + test('test', () {}); + }); + + expect(await engine.run(), isFalse); + }); + + test('runs further tearDownAlls', () async { + // Declare this in the outer test so if it doesn't run, the outer test + // will fail. + var shouldRun = expectAsync0(() {}); + + var engine = declareEngine(() { + tearDownAll(shouldRun); + + setUpAll(() { + addTearDown(() => throw 'error'); + }); + + test('test', () {}); + }); + + expect(await engine.run(), isFalse); + }); + }); + }); +}
diff --git a/pkgs/test_api/test/frontend/fake_test.dart b/pkgs/test_api/test/frontend/fake_test.dart new file mode 100644 index 0000000..7dfb306 --- /dev/null +++ b/pkgs/test_api/test/frontend/fake_test.dart
@@ -0,0 +1,37 @@ +// Copyright (c) 2020, 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'; +import 'package:test_api/fake.dart' as test_api; + +void main() { + late _FakeSample fake; + setUp(() { + fake = _FakeSample(); + }); + test('method invocation', () { + expect(() => fake.f(), throwsA(const TypeMatcher<UnimplementedError>())); + }); + test('getter', () { + expect(() => fake.x, throwsA(const TypeMatcher<UnimplementedError>())); + }); + test('setter', () { + expect(() => fake.x = 0, throwsA(const TypeMatcher<UnimplementedError>())); + }); + test('operator', () { + expect(() => fake + 1, throwsA(const TypeMatcher<UnimplementedError>())); + }); +} + +class _Sample { + void f() {} + + int get x => 0; + + set x(int value) {} + + int operator +(int other) => 0; +} + +class _FakeSample extends test_api.Fake implements _Sample {}
diff --git a/pkgs/test_api/test/frontend/set_up_all_test.dart b/pkgs/test_api/test/frontend/set_up_all_test.dart new file mode 100644 index 0000000..60d4dc8 --- /dev/null +++ b/pkgs/test_api/test/frontend/set_up_all_test.dart
@@ -0,0 +1,328 @@ +// Copyright (c) 2015, 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:async/async.dart'; +import 'package:test/test.dart'; + +import '../utils.dart'; + +void main() { + test('runs once before all tests', () { + return expectTestsPass(() { + var setUpAllRun = false; + setUpAll(() { + expect(setUpAllRun, isFalse); + setUpAllRun = true; + }); + + test('test 1', () { + expect(setUpAllRun, isTrue); + }); + + test('test 2', () { + expect(setUpAllRun, isTrue); + }); + }); + }); + + test('runs once per group, outside-in', () { + return expectTestsPass(() { + var setUpAll1Run = false; + var setUpAll2Run = false; + var setUpAll3Run = false; + setUpAll(() { + expect(setUpAll1Run, isFalse); + expect(setUpAll2Run, isFalse); + expect(setUpAll3Run, isFalse); + setUpAll1Run = true; + }); + + group('mid', () { + setUpAll(() { + expect(setUpAll1Run, isTrue); + expect(setUpAll2Run, isFalse); + expect(setUpAll3Run, isFalse); + setUpAll2Run = true; + }); + + group('inner', () { + setUpAll(() { + expect(setUpAll1Run, isTrue); + expect(setUpAll2Run, isTrue); + expect(setUpAll3Run, isFalse); + setUpAll3Run = true; + }); + + test('test', () { + expect(setUpAll1Run, isTrue); + expect(setUpAll2Run, isTrue); + expect(setUpAll3Run, isTrue); + }); + }); + }); + }); + }); + + test('runs before setUps', () { + return expectTestsPass(() { + var setUpAllRun = false; + setUp(() { + expect(setUpAllRun, isTrue); + }); + + setUpAll(() { + expect(setUpAllRun, isFalse); + setUpAllRun = true; + }); + + setUp(() { + expect(setUpAllRun, isTrue); + }); + + test('test', () { + expect(setUpAllRun, isTrue); + }); + }); + }); + + test('multiples run in order', () { + return expectTestsPass(() { + var setUpAll1Run = false; + var setUpAll2Run = false; + var setUpAll3Run = false; + setUpAll(() { + expect(setUpAll1Run, isFalse); + expect(setUpAll2Run, isFalse); + expect(setUpAll3Run, isFalse); + setUpAll1Run = true; + }); + + setUpAll(() { + expect(setUpAll1Run, isTrue); + expect(setUpAll2Run, isFalse); + expect(setUpAll3Run, isFalse); + setUpAll2Run = true; + }); + + setUpAll(() { + expect(setUpAll1Run, isTrue); + expect(setUpAll2Run, isTrue); + expect(setUpAll3Run, isFalse); + setUpAll3Run = true; + }); + + test('test', () { + expect(setUpAll1Run, isTrue); + expect(setUpAll2Run, isTrue); + expect(setUpAll3Run, isTrue); + }); + }); + }); + + group('asynchronously', () { + test('blocks additional setUpAlls on in-band async', () { + return expectTestsPass(() { + var setUpAll1Run = false; + var setUpAll2Run = false; + var setUpAll3Run = false; + setUpAll(() async { + expect(setUpAll1Run, isFalse); + expect(setUpAll2Run, isFalse); + expect(setUpAll3Run, isFalse); + await pumpEventQueue(); + setUpAll1Run = true; + }); + + setUpAll(() async { + expect(setUpAll1Run, isTrue); + expect(setUpAll2Run, isFalse); + expect(setUpAll3Run, isFalse); + await pumpEventQueue(); + setUpAll2Run = true; + }); + + setUpAll(() async { + expect(setUpAll1Run, isTrue); + expect(setUpAll2Run, isTrue); + expect(setUpAll3Run, isFalse); + await pumpEventQueue(); + setUpAll3Run = true; + }); + + test('test', () { + expect(setUpAll1Run, isTrue); + expect(setUpAll2Run, isTrue); + expect(setUpAll3Run, isTrue); + }); + }); + }); + + test("doesn't block additional setUpAlls on out-of-band async", () { + return expectTestsPass(() { + var setUpAll1Run = false; + var setUpAll2Run = false; + var setUpAll3Run = false; + setUpAll(() { + expect(setUpAll1Run, isFalse); + expect(setUpAll2Run, isFalse); + expect(setUpAll3Run, isFalse); + + expect( + pumpEventQueue().then((_) { + setUpAll1Run = true; + }), + completes); + }); + + setUpAll(() { + expect(setUpAll1Run, isFalse); + expect(setUpAll2Run, isFalse); + expect(setUpAll3Run, isFalse); + + expect( + pumpEventQueue().then((_) { + setUpAll2Run = true; + }), + completes); + }); + + setUpAll(() { + expect(setUpAll1Run, isFalse); + expect(setUpAll2Run, isFalse); + expect(setUpAll3Run, isFalse); + + expect( + pumpEventQueue().then((_) { + setUpAll3Run = true; + }), + completes); + }); + + test('test', () { + expect(setUpAll1Run, isTrue); + expect(setUpAll2Run, isTrue); + expect(setUpAll3Run, isTrue); + }); + }); + }); + }); + + test("isn't run for a skipped group", () async { + // Declare this in the outer test so if it runs, the outer test will fail. + var shouldNotRun = expectAsync0(() {}, count: 0); + + var engine = declareEngine(() { + group('skipped', () { + setUpAll(shouldNotRun); + + test('test', () {}); + }, skip: true); + }); + + await engine.run(); + expect(engine.liveTests, hasLength(1)); + expect(engine.skipped, hasLength(1)); + expect(engine.liveTests, equals(engine.skipped)); + }); + + test('is emitted through Engine.onTestStarted', () async { + var engine = declareEngine(() { + setUpAll(() {}); + + test('test', () {}); + }); + + var queue = StreamQueue(engine.onTestStarted); + var setUpAllFuture = queue.next; + var liveTestFuture = queue.next; + + await engine.run(); + + var setUpAllLiveTest = await setUpAllFuture; + expect(setUpAllLiveTest.test.name, equals('(setUpAll)')); + expectTestPassed(setUpAllLiveTest); + + // The fake test for setUpAll should be removed from the engine's live + // test list so that reporters don't display it as a passed test. + expect(engine.liveTests, isNot(contains(setUpAllLiveTest))); + expect(engine.passed, isNot(contains(setUpAllLiveTest))); + expect(engine.failed, isNot(contains(setUpAllLiveTest))); + expect(engine.skipped, isNot(contains(setUpAllLiveTest))); + expect(engine.active, isNot(contains(setUpAllLiveTest))); + + var liveTest = await liveTestFuture; + expectTestPassed(await liveTestFuture); + expect(engine.liveTests, contains(liveTest)); + expect(engine.passed, contains(liveTest)); + }); + + group('with an error', () { + test('reports the error and remains in Engine.liveTests', () async { + var engine = declareEngine(() { + setUpAll(() => throw TestFailure('fail')); + + test('test', () {}); + }); + + var queue = StreamQueue(engine.onTestStarted); + var setUpAllFuture = queue.next; + + expect(await engine.run(), isFalse); + + var setUpAllLiveTest = await setUpAllFuture; + expect(setUpAllLiveTest.test.name, equals('(setUpAll)')); + expectTestFailed(setUpAllLiveTest, 'fail'); + + // The fake test for setUpAll should be removed from the engine's live + // test list so that reporters don't display it as a passed test. + expect(engine.liveTests, contains(setUpAllLiveTest)); + expect(engine.failed, contains(setUpAllLiveTest)); + expect(engine.passed, isNot(contains(setUpAllLiveTest))); + expect(engine.skipped, isNot(contains(setUpAllLiveTest))); + expect(engine.active, isNot(contains(setUpAllLiveTest))); + }); + + test("doesn't run tests in the group", () async { + // Declare this in the outer test so if it runs, the outer test will fail. + var shouldNotRun = expectAsync0(() {}, count: 0); + + var engine = declareEngine(() { + setUpAll(() => throw 'error'); + + test('test', shouldNotRun); + }); + + expect(await engine.run(), isFalse); + }); + + test("doesn't run inner groups", () async { + // Declare this in the outer test so if it runs, the outer test will fail. + var shouldNotRun = expectAsync0(() {}, count: 0); + + var engine = declareEngine(() { + setUpAll(() => throw 'error'); + + group('group', () { + test('test', shouldNotRun); + }); + }); + + expect(await engine.run(), isFalse); + }); + + test("doesn't run further setUpAlls", () async { + // Declare this in the outer test so if it runs, the outer test will fail. + var shouldNotRun = expectAsync0(() {}, count: 0); + + var engine = declareEngine(() { + setUpAll(() => throw 'error'); + setUpAll(shouldNotRun); + + test('test', shouldNotRun); + }); + + expect(await engine.run(), isFalse); + }); + }); +}
diff --git a/pkgs/test_api/test/frontend/tear_down_all_test.dart b/pkgs/test_api/test/frontend/tear_down_all_test.dart new file mode 100644 index 0000000..d6d209f --- /dev/null +++ b/pkgs/test_api/test/frontend/tear_down_all_test.dart
@@ -0,0 +1,372 @@ +// Copyright (c) 2015, 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:async/async.dart'; +import 'package:test/test.dart'; + +import '../utils.dart'; + +void main() { + test('runs once after all tests', () { + return expectTestsPass(() { + var test1Run = false; + var test2Run = false; + var tearDownAllRun = false; + tearDownAll(() { + expect(test1Run, isTrue); + expect(test2Run, isTrue); + expect(tearDownAllRun, isFalse); + tearDownAllRun = true; + }); + + test('test 1', () { + expect(tearDownAllRun, isFalse); + test1Run = true; + }); + + test('test 2', () { + expect(tearDownAllRun, isFalse); + test2Run = true; + }); + }); + }); + + test('runs once per group, inside-out', () { + return expectTestsPass(() { + var tearDownAll1Run = false; + var tearDownAll2Run = false; + var tearDownAll3Run = false; + var testRun = false; + tearDownAll(() { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isTrue); + expect(tearDownAll3Run, isTrue); + expect(testRun, isTrue); + tearDownAll1Run = true; + }); + + group('mid', () { + tearDownAll(() { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isFalse); + expect(tearDownAll3Run, isTrue); + expect(testRun, isTrue); + tearDownAll2Run = true; + }); + + group('inner', () { + tearDownAll(() { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isFalse); + expect(tearDownAll3Run, isFalse); + expect(testRun, isTrue); + tearDownAll3Run = true; + }); + + test('test', () { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isFalse); + expect(tearDownAll3Run, isFalse); + testRun = true; + }); + }); + }); + }); + }); + + test('runs after tearDowns', () { + return expectTestsPass(() { + var tearDown1Run = false; + var tearDown2Run = false; + var tearDownAllRun = false; + tearDown(() { + expect(tearDownAllRun, isFalse); + tearDown1Run = true; + }); + + tearDownAll(() { + expect(tearDown1Run, isTrue); + expect(tearDown2Run, isTrue); + expect(tearDownAllRun, isFalse); + tearDownAllRun = true; + }); + + tearDown(() { + expect(tearDownAllRun, isFalse); + tearDown2Run = true; + }); + + test('test', () { + expect(tearDownAllRun, isFalse); + }); + }); + }); + + test('multiples run in reverse order', () { + return expectTestsPass(() { + var tearDownAll1Run = false; + var tearDownAll2Run = false; + var tearDownAll3Run = false; + tearDownAll(() { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isTrue); + expect(tearDownAll3Run, isTrue); + tearDownAll1Run = true; + }); + + tearDownAll(() { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isFalse); + expect(tearDownAll3Run, isTrue); + tearDownAll2Run = true; + }); + + tearDownAll(() { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isFalse); + expect(tearDownAll3Run, isFalse); + tearDownAll3Run = true; + }); + + test('test', () { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isFalse); + expect(tearDownAll3Run, isFalse); + }); + }); + }); + + group('asynchronously', () { + test('blocks additional tearDownAlls on in-band async', () { + return expectTestsPass(() { + var tearDownAll1Run = false; + var tearDownAll2Run = false; + var tearDownAll3Run = false; + tearDownAll(() async { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isTrue); + expect(tearDownAll3Run, isTrue); + await pumpEventQueue(); + tearDownAll1Run = true; + }); + + tearDownAll(() async { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isFalse); + expect(tearDownAll3Run, isTrue); + await pumpEventQueue(); + tearDownAll2Run = true; + }); + + tearDownAll(() async { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isFalse); + expect(tearDownAll3Run, isFalse); + await pumpEventQueue(); + tearDownAll3Run = true; + }); + + test('test', () { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isFalse); + expect(tearDownAll3Run, isFalse); + }); + }); + }); + + test("doesn't block additional tearDownAlls on out-of-band async", () { + return expectTestsPass(() { + var tearDownAll1Run = false; + var tearDownAll2Run = false; + var tearDownAll3Run = false; + tearDownAll(() { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isFalse); + expect(tearDownAll3Run, isFalse); + + expect(Future(() { + tearDownAll1Run = true; + }), completes); + }); + + tearDownAll(() { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isFalse); + expect(tearDownAll3Run, isFalse); + + expect(Future(() { + tearDownAll2Run = true; + }), completes); + }); + + tearDownAll(() { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isFalse); + expect(tearDownAll3Run, isFalse); + + expect(Future(() { + tearDownAll3Run = true; + }), completes); + }); + + test('test', () { + expect(tearDownAll1Run, isFalse); + expect(tearDownAll2Run, isFalse); + expect(tearDownAll3Run, isFalse); + }); + }); + }); + + test('blocks further tests on in-band async', () { + return expectTestsPass(() { + var tearDownAllRun = false; + group('group', () { + tearDownAll(() async { + expect(tearDownAllRun, isFalse); + await pumpEventQueue(); + tearDownAllRun = true; + }); + + test('test', () {}); + }); + + test('after', () { + expect(tearDownAllRun, isTrue); + }); + }); + }); + + test('blocks further tests on out-of-band async', () { + return expectTestsPass(() { + var tearDownAllRun = false; + group('group', () { + tearDownAll(() async { + expect(tearDownAllRun, isFalse); + expect( + pumpEventQueue().then((_) { + tearDownAllRun = true; + }), + completes); + }); + + test('test', () {}); + }); + + test('after', () { + expect(tearDownAllRun, isTrue); + }); + }); + }); + }); + + test("isn't run for a skipped group", () async { + // Declare this in the outer test so if it runs, the outer test will fail. + var shouldNotRun = expectAsync0(() {}, count: 0); + + var engine = declareEngine(() { + group('skipped', () { + tearDownAll(shouldNotRun); + + test('test', () {}); + }, skip: true); + }); + + await engine.run(); + expect(engine.liveTests, hasLength(1)); + expect(engine.skipped, hasLength(1)); + expect(engine.liveTests, equals(engine.skipped)); + }); + + test('is emitted through Engine.onTestStarted', () async { + var engine = declareEngine(() { + tearDownAll(() {}); + + test('test', () {}); + }); + + var queue = StreamQueue(engine.onTestStarted); + var liveTestFuture = queue.next; + var tearDownAllFuture = queue.next; + + await engine.run(); + + var tearDownAllLiveTest = await tearDownAllFuture; + expect(tearDownAllLiveTest.test.name, equals('(tearDownAll)')); + expectTestPassed(tearDownAllLiveTest); + + // The fake test for tearDownAll should be removed from the engine's live + // test list so that reporters don't display it as a passed test. + expect(engine.liveTests, isNot(contains(tearDownAllLiveTest))); + expect(engine.passed, isNot(contains(tearDownAllLiveTest))); + expect(engine.failed, isNot(contains(tearDownAllLiveTest))); + expect(engine.skipped, isNot(contains(tearDownAllLiveTest))); + expect(engine.active, isNot(contains(tearDownAllLiveTest))); + + var liveTest = await liveTestFuture; + expectTestPassed(await liveTestFuture); + expect(engine.liveTests, contains(liveTest)); + expect(engine.passed, contains(liveTest)); + }); + + group('with an error', () { + test('reports the error and remains in Engine.liveTests', () async { + var engine = declareEngine(() { + tearDownAll(() => throw TestFailure('fail')); + + test('test', () {}); + }); + + var queue = StreamQueue(engine.onTestStarted); + expect(queue.next, completes); + var tearDownAllFuture = queue.next; + + expect(await engine.run(), isFalse); + + var tearDownAllLiveTest = await tearDownAllFuture; + expect(tearDownAllLiveTest.test.name, equals('(tearDownAll)')); + expectTestFailed(tearDownAllLiveTest, 'fail'); + + // The fake test for tearDownAll should be removed from the engine's live + // test list so that reporters don't display it as a passed test. + expect(engine.liveTests, contains(tearDownAllLiveTest)); + expect(engine.failed, contains(tearDownAllLiveTest)); + expect(engine.passed, isNot(contains(tearDownAllLiveTest))); + expect(engine.skipped, isNot(contains(tearDownAllLiveTest))); + expect(engine.active, isNot(contains(tearDownAllLiveTest))); + }); + + test('runs further tearDownAlls', () async { + // Declare this in the outer test so if it doesn't runs, the outer test + // will fail. + var shouldRun = expectAsync0(() {}); + + var engine = declareEngine(() { + tearDownAll(() => throw 'error'); + tearDownAll(shouldRun); + + test('test', () {}); + }); + + expect(await engine.run(), isFalse); + }); + + test('runs outer tearDownAlls', () async { + // Declare this in the outer test so if it doesn't runs, the outer test + // will fail. + var shouldRun = expectAsync0(() {}); + + var engine = declareEngine(() { + tearDownAll(shouldRun); + + group('group', () { + tearDownAll(() => throw 'error'); + + test('test', () {}); + }); + }); + + expect(await engine.run(), isFalse); + }); + }); +}
diff --git a/pkgs/test_api/test/frontend/timeout_test.dart b/pkgs/test_api/test/frontend/timeout_test.dart new file mode 100644 index 0000000..fdfd611 --- /dev/null +++ b/pkgs/test_api/test/frontend/timeout_test.dart
@@ -0,0 +1,81 @@ +// Copyright (c) 2016, 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'; + +void main() { + group('Timeout.parse', () { + group('for "none"', () { + test('successfully parses', () { + expect(Timeout.parse('none'), equals(Timeout.none)); + }); + + test('rejects invalid input', () { + expect(() => Timeout.parse(' none'), throwsFormatException); + expect(() => Timeout.parse('none '), throwsFormatException); + expect(() => Timeout.parse('xnone'), throwsFormatException); + expect(() => Timeout.parse('nonex'), throwsFormatException); + expect(() => Timeout.parse('noxe'), throwsFormatException); + }); + }); + + group('for a relative timeout', () { + test('successfully parses', () { + expect(Timeout.parse('1x'), equals(const Timeout.factor(1))); + expect(Timeout.parse('2.5x'), equals(const Timeout.factor(2.5))); + expect(Timeout.parse('1.2e3x'), equals(const Timeout.factor(1.2e3))); + }); + + test('rejects invalid input', () { + expect(() => Timeout.parse('.x'), throwsFormatException); + expect(() => Timeout.parse('x'), throwsFormatException); + expect(() => Timeout.parse('ax'), throwsFormatException); + expect(() => Timeout.parse('1x '), throwsFormatException); + expect(() => Timeout.parse('1x5m'), throwsFormatException); + }); + }); + + group('for an absolute timeout', () { + test('successfully parses all supported units', () { + expect(Timeout.parse('2d'), equals(const Timeout(Duration(days: 2)))); + expect(Timeout.parse('2h'), equals(const Timeout(Duration(hours: 2)))); + expect( + Timeout.parse('2m'), equals(const Timeout(Duration(minutes: 2)))); + expect( + Timeout.parse('2s'), equals(const Timeout(Duration(seconds: 2)))); + expect(Timeout.parse('2ms'), + equals(const Timeout(Duration(milliseconds: 2)))); + expect(Timeout.parse('2us'), + equals(const Timeout(Duration(microseconds: 2)))); + }); + + test('supports non-integer units', () { + expect(Timeout.parse('2.73d'), + equals(Timeout(const Duration(days: 1) * 2.73))); + }); + + test('supports multiple units', () { + expect( + Timeout.parse('1d 2h3m 4s5ms\t6us'), + equals(const Timeout(Duration( + days: 1, + hours: 2, + minutes: 3, + seconds: 4, + milliseconds: 5, + microseconds: 6)))); + }); + + test('rejects invalid input', () { + expect(() => Timeout.parse('.d'), throwsFormatException); + expect(() => Timeout.parse('d'), throwsFormatException); + expect(() => Timeout.parse('ad'), throwsFormatException); + expect(() => Timeout.parse('1z'), throwsFormatException); + expect(() => Timeout.parse('1u'), throwsFormatException); + expect(() => Timeout.parse('1d5x'), throwsFormatException); + expect(() => Timeout.parse('1d*5m'), throwsFormatException); + }); + }); + }); +}
diff --git a/pkgs/test_api/test/import_restrictions_test.dart b/pkgs/test_api/test/import_restrictions_test.dart new file mode 100644 index 0000000..3fd9ffd --- /dev/null +++ b/pkgs/test_api/test/import_restrictions_test.dart
@@ -0,0 +1,126 @@ +// Copyright (c) 2021, 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:isolate'; + +import 'package:analyzer/dart/analysis/analysis_context.dart'; +import 'package:analyzer/dart/analysis/analysis_context_collection.dart'; +import 'package:analyzer/dart/analysis/results.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:glob/glob.dart'; +import 'package:glob/list_local_fs.dart'; +import 'package:graphs/graphs.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +void main() { + late _ImportCheck importCheck; + setUpAll(() async { + importCheck = await _ImportCheck.create(); + }); + group('backend', () { + test('must not import from other subdirectories', () async { + final entryPoints = [ + _testApiLibrary('backend.dart'), + ...await _ImportCheck.findEntrypointsUnder( + _testApiLibrary('src/backend')) + ]; + await for (final source + in importCheck.transitiveSamePackageSources(entryPoints)) { + for (final import in source.imports) { + expect(import.pathSegments.skip(1).take(2), ['src', 'backend'], + reason: 'Invalid import from ${source.uri} : $import'); + } + } + }); + }); +} + +Uri _testApiLibrary(String path) => Uri.parse('package:test_api/$path'); + +class _ImportCheck { + final AnalysisContext _context; + + static Future<Iterable<Uri>> findEntrypointsUnder(Uri uri) async { + if (!uri.path.endsWith('/')) { + uri = uri.replace(path: '${uri.path}/'); + } + final directory = p.fromUri(await Isolate.resolvePackageUri(uri)); + return Glob('./**') + .listSync(root: directory) + .whereType<File>() + .map((f) => uri.resolve(p.url.relative(f.path, from: directory))); + } + + static Future<_ImportCheck> create() async { + final context = await _createAnalysisContext(); + return _ImportCheck._(context); + } + + static Future<AnalysisContext> _createAnalysisContext() async { + final libUri = Uri.parse('package:graphs/'); + final libPath = await _pathForUri(libUri); + final packagePath = p.dirname(libPath); + + final contexts = + AnalysisContextCollection(includedPaths: [packagePath]).contexts; + if (contexts.length != 1) { + throw StateError('Expected to find exactly one context, got $contexts'); + } + return contexts.first; + } + + static Future<String> _pathForUri(Uri uri) async { + final fileUri = await Isolate.resolvePackageUri(uri); + if (fileUri == null || !fileUri.isScheme('file')) { + throw StateError('Expected to resolve $uri to a file URI, got $fileUri'); + } + return p.fromUri(fileUri); + } + + _ImportCheck._(this._context); + + Stream<_Source> transitiveSamePackageSources(Iterable<Uri> entryPoints) { + assert(entryPoints.every((e) => e.scheme == 'package')); + final package = entryPoints.first.pathSegments.first; + assert(entryPoints.skip(1).every((e) => e.pathSegments.first == package)); + return crawlAsync<Uri, _Source>( + entryPoints, + (uri) async => _Source(uri, await _findImports(uri, package)), + (_, source) => source.imports); + } + + Future<Set<Uri>> _findImports(Uri uri, String restrictToPackage) async { + var path = await _pathForUri(uri); + var analysisSession = _context.currentSession; + var parseResult = analysisSession.getParsedUnit(path) as ParsedUnitResult; + assert(parseResult.content.isNotEmpty, + 'Tried to read an invalid library $uri'); + return parseResult.unit.directives + .whereType<UriBasedDirective>() + .map((d) => d.uri.stringValue!) + .where((uri) => !uri.startsWith('dart:')) + .map((import) => _resolveImport(import, uri)) + .where((import) => import.pathSegments.first == restrictToPackage) + .toSet(); + } + + static Uri _resolveImport(String import, Uri from) { + if (import.startsWith('package:')) return Uri.parse(import); + assert(from.scheme == 'package'); + final package = from.pathSegments.first; + final fromPath = p.joinAll(from.pathSegments.skip(1)); + final path = p.normalize(p.join(p.dirname(fromPath), import)); + return Uri.parse('package:${p.join(package, path)}'); + } +} + +class _Source { + final Uri uri; + final Set<Uri> imports; + + _Source(this.uri, this.imports); +}
diff --git a/pkgs/test_api/test/utils.dart b/pkgs/test_api/test/utils.dart new file mode 100644 index 0000000..97058e3 --- /dev/null +++ b/pkgs/test_api/test/utils.dart
@@ -0,0 +1,146 @@ +// Copyright (c) 2015, 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:collection'; + +import 'package:test/test.dart'; +import 'package:test_api/src/backend/declarer.dart'; +import 'package:test_api/src/backend/group_entry.dart'; +import 'package:test_api/src/backend/live_test.dart'; +import 'package:test_api/src/backend/runtime.dart'; +import 'package:test_api/src/backend/state.dart'; +import 'package:test_api/src/backend/suite_platform.dart'; +import 'package:test_core/src/runner/engine.dart'; +import 'package:test_core/src/runner/plugin/environment.dart'; +import 'package:test_core/src/runner/runner_suite.dart'; +import 'package:test_core/src/runner/suite.dart'; + +/// A dummy suite platform to use for testing suites. +final suitePlatform = + SuitePlatform(Runtime.vm, compiler: Runtime.vm.defaultCompiler); + +// The last state change detected via [expectStates]. +State? lastState; + +/// Asserts that exactly [states] will be emitted via [liveTest.onStateChange]. +/// +/// The most recent emitted state is stored in [_lastState]. +void expectStates(LiveTest liveTest, Iterable<State> statesIter) { + var states = Queue.of(statesIter); + liveTest.onStateChange.listen(expectAsync1((state) { + lastState = state; + expect(state, equals(states.removeFirst())); + }, count: states.length, max: states.length)); +} + +/// Asserts that errors will be emitted via [liveTest.onError] that match +/// [validators], in order. +void expectErrors( + LiveTest liveTest, Iterable<void Function(Object)> validatorsIter) { + var validators = Queue.of(validatorsIter); + liveTest.onError.listen(expectAsync1((error) { + validators.removeFirst()(error.error); + }, count: validators.length, max: validators.length)); +} + +/// Asserts that [liveTest] will have a single failure with message `"oh no"`. +void expectSingleFailure(LiveTest liveTest) { + expectStates(liveTest, [ + const State(Status.running, Result.success), + const State(Status.complete, Result.failure) + ]); + + expectErrors(liveTest, [ + (error) { + expect(lastState?.status, equals(Status.complete)); + expect(error, isTestFailure('oh no')); + } + ]); +} + +/// Asserts that [liveTest] will have a single error, the string `"oh no"`. +void expectSingleError(LiveTest liveTest) { + expectStates(liveTest, [ + const State(Status.running, Result.success), + const State(Status.complete, Result.error) + ]); + + expectErrors(liveTest, [ + (error) { + expect(lastState?.status, equals(Status.complete)); + expect(error, equals('oh no')); + } + ]); +} + +/// Returns a matcher that matches a [TestFailure] with the given [message]. +/// +/// [message] can be a string or a [Matcher]. +Matcher isTestFailure(Object message) => const TypeMatcher<TestFailure>() + .having((e) => e.message, 'message', message); + +/// Asserts that [liveTest] has completed and passed. +/// +/// If the test had any errors, they're surfaced nicely into the outer test. +void expectTestPassed(LiveTest liveTest) { + // Since the test is expected to pass, we forward any current or future errors + // to the outer test, because they're definitely unexpected. + for (var error in liveTest.errors) { + registerException(error.error, error.stackTrace); + } + liveTest.onError.listen((error) { + registerException(error.error, error.stackTrace); + }); + + expect(liveTest.state.status, equals(Status.complete)); + expect(liveTest.state.result, equals(Result.success)); +} + +/// Asserts that [liveTest] failed with a single [TestFailure] whose message +/// matches [message]. +void expectTestFailed(LiveTest liveTest, Object message) { + expect(liveTest.state.status, equals(Status.complete)); + expect(liveTest.state.result, equals(Result.failure)); + expect(liveTest.errors, hasLength(1)); + expect(liveTest.errors.first.error, isTestFailure(message)); +} + +/// Runs [body] with a declarer, runs all the declared tests, and asserts that +/// they pass. +/// +/// This is typically used to run multiple tests where later tests make +/// assertions about the results of previous ones. +Future expectTestsPass(void Function() body) async { + var engine = declareEngine(body); + var success = await engine.run(); + + for (var test in engine.liveTests) { + expectTestPassed(test); + } + + expect(success, isTrue); +} + +/// Runs [body] with a declarer and returns the declared entries. +List<GroupEntry> declare( + void Function() body, { + // TODO: Change the default https://github.com/dart-lang/test/issues/1571 + bool allowDuplicateTestNames = true, +}) { + var declarer = Declarer(allowDuplicateTestNames: allowDuplicateTestNames) + ..declare(body); + return declarer.build().entries; +} + +/// Runs [body] with a declarer and returns an engine that runs those tests. +Engine declareEngine(void Function() body, {bool runSkipped = false}) { + var declarer = Declarer()..declare(body); + return Engine.withSuites([ + RunnerSuite( + const PluginEnvironment(), + SuiteConfiguration.runSkipped(runSkipped), + declarer.build(), + suitePlatform) + ]); +}
diff --git a/pkgs/test_core/CHANGELOG.md b/pkgs/test_core/CHANGELOG.md new file mode 100644 index 0000000..4c305bc --- /dev/null +++ b/pkgs/test_core/CHANGELOG.md
@@ -0,0 +1,668 @@ +## 0.6.8 + +* Fix hang when running multiple precompiled browser tests. + +## 0.6.7 + +* Update the `package:vm_service` constraint to allow version `15.x`. + +## 0.6.6 + +* Allow `analyzer: '>=6.0.0 <8.0.0'` +* Fix dart2wasm tests on windows. +* Increase SDK constraint to ^3.5.0. +* Allow passing additional arguments to `dart compile wasm`. + +## 0.6.5 + +* Increase SDK constraint to ^3.4.0. +* Ensure we don't create files ending in a `.`, this breaks windows. + +## 0.6.4 + +* Enable asserts for `dart2wasm` tests. + +## 0.6.3 + +* Update min SDK constraint to 3.2.0. +* Fix testing with `dart2wasm` - use `dart compile wasm` instead of depending on + SDK internals + +## 0.6.2 + +* Add `@doNotSubmit` to more declarations of the `solo` parameter. + +## 0.6.1 + +* Handle missing package configs. +* Document the silent reporter in CLI help output. +* Support enabling experiments with the dart2wasm compiler. + +## 0.6.0 + +* Handle paths with leading `/` when spawning test isolates. +* Added `dart2wasm` as a supported compiler for the `chrome` runtime. +* **BREAKING**: Removed the `experimentalChromeWasm` runtime. +* **BREAKING**: Removed `Runtime.isJS` and `Runtime.isWasm`, as this is now + based on the compiler and not the runtime. +* **BREAKING**: Removed `Configuration.pubServeUrl` and support for it. +* Fix running of tests defined under `lib/` with relative imports to other + libraries in the package. +* Update the `package:frontend_server_client` constraint to allow version + `4.0.0`. +* Update the `package:vm_service` constraint to allow version `14.x`. + +## 0.5.9 + +* Update the vm_service constraint to allow version `13.x`. + +## 0.5.8 + +* Move scaffolding definitions to a non-deprecated library. +* Allow omitting the `Compiler` argument to `currentPlatform`. + +## 0.5.7 + +* Pass --disable-program-split to dart2js to fix tests which use deferred + loading. +* Add a 'silent' reporter option. Keep it hidden in the CLI args help since it + is not useful in the general case, but can be useful for tests of the test + runner. +* Update to `package:vm_service` `12.0.0` + +## 0.5.6 + +* Add support for discontinuing after the first failing test with `--fail-fast`. + +## 0.5.5 + +* Change "compiling <path>" to "loading <path>" message in all cases. Surface + the "loading" messages in the situations where previously only "compiling" + message would be used. + +## 0.5.4 + +* Drop support for null unsafe Dart, bump SDK constraint to `3.0.0`. +* Add `final` modifier on some implementation classes: `Configuration`, + `CustomRuntime`,`RuntimeSettings`, `SuiteConfiguration`. +* Fix the `root_` fields in the JSON reporter when running a test on Windows + with an absolute path. +* Allow the latest analyzer (6.x.x). + +## 0.5.3 + +* Fix compatibility with wasm number semantics. + +## 0.5.2 + +* Use the version `0.5.2` of `packge:test_api`. + +## 0.5.1 + +* Start adding experimental support for native_assets.yaml, when + `--enable-experiment=native_assets` is passed. + +## 0.5.0 + +* Support the `--compiler` flag, which can be used to configure which compiler + to use. + * To specify a compiler by platform, the argument supports platform selectors + through this syntax `[<platform>:]<compiler>`. For example the command line + argument `--compiler vm:source` would run all vm tests from source instead + of compiling to kernel first. + * If no given compiler is compatible for a platform, it will use its default + compiler instead. +* Add support for `-c exe` (the native executable compiler) to the vm platform. +* Add `Compiler` class, exposed through `backend.dart`. +* Support compiler identifiers in platform selectors. +* List the supported compilers for each platform in the usage text. +* Update all reporters to print the compiler along with the platform name + when configured to print the platform. Extend the logic for printing platofrm + information to do so if any compilers are explicitly configured. +* Deprecate `--use-data-isolate-strategy`. It is now an alias for `-c vm:source` + which is roughly equivalent. If this is breaking for you please file an issue. +* **BREAKING** Add required `defaultCompiler` and `supportedCompilers` fields + to `Runtime`. +* **BREAKING** Add required `compiler` field to `SuitePlatform`. +* **BREAKING** Add required `compilerSelections` argument to some + `Configuration` and `SuiteConfiguration` constructors. +* **BREAKING** Custom platform plugins need to respect the compiler option + given through the `SuitePlatform` argument to `PlatformPlugin.load`. This is + not statically breaking but it will be confusing for users if it isn't + supported. +* **BREAKING** Remove `useDataIsolateStrategy` field from `Configuration`. +* **BREAKING** Stop exporting APIs from `package:matcher/expect.dart`. + +## 0.4.24 + +* Fix running paths by absolute path (with drive letter) on windows. + +## 0.4.23 + +* Avoid empty expandable groups for tests without extra output in Github + reporter. +* Support running tests by absolute file uri. +* Update `vm_service` constraint to `>=6.0.0 <12.0.0`. + +## 0.4.22 + +* Don't run `tearDown` until the test body and outstanding work is complete, + even if the test has already failed. +* Update `vm_service` constraint to `>=6.0.0 <11.0.0`. + +## 0.4.21 + +* Move `includeTags` and `excludeTags` from `SuiteConfiguration` to + `Configuration`. +* Merge command lines args repeating the same test path to run the suite one + time with all the test cases across the different arguments. +* Fix VM tests which run after some test has changed the working directory. + There are still issues with browser tests after changing directory. + +## 0.4.20 + +* Fix an issue with the github reporter where tests that fail asynchronously + after they've completed would show up as succeeded tests. +* Support the latest `package:test_api`. +* Refactor `CompilerPool` to be abstract, add wasm compiler pool. + +## 0.4.19 + +* Support `package:matcher` version `0.12.13`. +* Require Dart SDK version 2.18. + +## 0.4.18 + +* Support the latest `package:test_api`. +* Support the latest `package:analyzer`. + +## 0.4.17 + +* Support the latest `package:test_api`. +* Support the latest `package:frontend_server_client`. + +## 0.4.16 + +* Make the labels for test loading more readable in the compact and expanded + reporters, use gray instead of black. +* Print a command to re-run the failed test after each failure in the compact + reporter. +* Fix the package config path used when running pre-compiled vm tests. + +## 0.4.15 + +* Support the latest `package:test_api`. + +## 0.4.14 + +* Update the github reporter to output the platform in the test names when + multiple platforms are used. +* Fix `spawnHybridUri` support for `package:` uris. + +## 0.4.13 + +* Re-publish changes from 0.4.12. +* Stop relying on setUpAllName and tearDownAllName constants from test_api. + +## 0.4.12 (retracted) + +* Remove wait for VM platform isolate exits. +* Drop `dart2jsPath` configuration support. +* Allow loading tests under a path with the directory named `packages`. +* Require analyzer version `3.3.0`, and allow version `4.x`. + +## 0.4.11 + +* Update `vm_service` constraint to `>=6.0.0 <9.0.0`. + +## 0.4.10 + +* Update `analyzer` constraint to `>=2.14.0 <3.0.0`. +* Add an `--ignore-timeouts` command line flag, which disables all timeouts + for all tests. +* Experimental: Add a VM service extension `ext.test.pauseAfterTests` which + configures VM platform tests to pause for debugging after tests are run, + before the test isolates are killed. + +## 0.4.9 + +* Wait for paused VM platform isolates before shutdown. + +## 0.4.8 + +* Add logging about enabling stack trace chaining to the compact and expanded + reporters (moved from the invoker). This will now only be logged once after + all tests have ran. + +## 0.4.7 + +* Fix parsing of file paths into a URI on windows. + +## 0.4.6 + +* Support query parameters `name`, `full-name`, `line`, and `col` on test paths, + which will apply the filters to only those test suites. + * All specified filters must match for a test to run. + * Global filters (ie: `--name`) are also still respected and must match. + * The `line` and `col` will match if any frame from the test trace matches + (the test trace is the current stack trace where `test` is invoked). +* Support the latest `test_api`. + +## 0.4.5 + +* Use newer analyzer APIs. + +## 0.4.4 + +* Support the latest `test_api`. + +## 0.4.3 + +* Add an option to disallow duplicate test or group names in `directRunTests`. +* Add configuration to disallow duplicate test and group names by default. See + the [docs][allow_duplicate_test_names] for more information. +* Remove dependency on pedantic. + +[allow_duplicate_test_names]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#allow_duplicate_test_names + +## 0.4.2 + +* Re-use the cached dill file from previous runs on subsequent runs. + +## 0.4.1 + +* Use the latest `package:matcher`. + +## 0.4.0 + +* **BREAKING**: All parameters to the `SuiteConfiguration` and `Configuration` + constructors are now required. Some specialized constructors have been added + for the common cases where a subset are intended to be provided. +* **BREAKING**: Remove support for `FORCE_TEST_EXIT`. +* Report incomplete tests as errors in the JSON reporter when the run is + canceled early. +* Don't log the --test-randomization-ordering-seed if using the json reporter. +* Add a new exit code, 79, which is used when no tests were ran. + * Previously you would have gotten either exit code 1 or 65 (65 if you had + provided a test name regex). +* When no tests were ran but tags were provided, list the tag configuration. +* Update `analyzer` constraint to `>=1.0.0 <3.0.0`. + +## 0.3.29 + +* Fix a bug where a tag level configuration would cause test suites with that + tag to ignore the `--test-randomize-ordering-seed` argument. + +## 0.3.28 + +* Add `time` field to the json reporters `allSuites` event type so that all + event types can be unified. + +## 0.3.27 + +* Restore the `Configuration.loadFromString` constructor. + +## 0.3.26 + +* Give a better error when `printOnFailure` is called from outside a test + zone. + +## 0.3.25 + +* Support the latest vm_service release (`7.0.0`). + +## 0.3.24 + +* Fix race condition between compilation of vm tests and the running of + isolates. + +## 0.3.23 + +* Forward experiment args from the runner executable to the compiler with the + new vm test loading strategy. + +## 0.3.22 + +* Fix a windows issue with the new loading strategy. + +## 0.3.21 + +* Fix an issue where you couldn't have tests compiled in both sound and + unsound null safety modes. + +## 0.3.20 + +* Add library `scaffolding.dart` to allow importing a subset of the normal + surface area. +* Remove `suiteChannel`. This is now handled by an additional argument to the + `beforeLoad` callback in `serializeSuite`. +* Disable stack trace chaining by default. +* Change the default way VM tests are launched and ran to greatly speed up + loading performance. + * You can force the old strategy with `--use-data-isolate-strategy` flag if + you run into issues, but please also file a bug. +* Improve the error message for `hybridMain` functions with an incompatible + StreamChannel parameter type. +* Change the `message` argument to `PlatformPlugin.load` to `Map<String, + Object?>`. In an upcoming release this will be required as the type for this + argument when passed through to `deserializeSuite`. + +## 0.3.19 + +* ~~Disable stack trace chaining by default.~~ + +## 0.3.18 + +* Update `spawnHybridCode` to default to the current packages language version. +* Update to the latest `test_api`. + +## 0.3.17 + +* Complete the null safety migration. + +## 0.3.16 + +* Allow package:io version 1.0.0. + +## 0.3.14 + +* Handle issue closing `stdin` during shutdown. + +## 0.3.13 + +* Allow the latest analyzer `1.0.0`. + +## 0.3.12 + +* Stable null safety release. + +## 0.3.12-nullsafety.17 + +* Use the `test_api` for stable null safety. + +## 0.3.12-nullsafety.16 + +* Expand upper bound constraints for some null safe migrated packages. + +## 0.3.12-nullsafety.15 + +* Support the latest vm_service release (`6.x.x`). + +## 0.3.12-nullsafety.14 + +* Support the latest coverage release (`0.15.x`). + +## 0.3.12-nullsafety.13 + +* Allow the latest args release (`2.x`). + +## 0.3.12-nullsafety.12 + +* Allow the latest glob release (`2.x`). + +## 0.3.12-nullsafety.11 + +* Fix `spawnHybridUri` on windows. +* Allow `package:yaml` version `3.x.x`. + +## 0.3.12-nullsafety.10 + +* Allow `package:analyzer` version `0.41.x`. + +## 0.3.12-nullsafety.9 + +* Fix `spawnHybridUri` to respect language versioning of the spawned uri. +* Pre-emptively fix legacy library import lint violations, and unmigrate some + libraries as necessary. + +## 0.3.12-nullsafety.8 + +* Fix a bug where the test runner could crash when printing the elapsed time. +* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release + guidelines. + + +## 0.3.12-nullsafety.7 + +* Allow prerelease versions of the 2.12 sdk. + +## 0.3.12-nullsafety.6 + +* Add experimental `directRunTests`, `directRunSingle`, and `enumerateTestCases` + APIs to enable test runners written around a single executable that can report + and run any single test case. + +## 0.3.12-nullsafety.5 + +* Allow `2.10` stable and `2.11.0-dev` SDKs. +* Add `src/platform.dart` library to consolidate the necessary imports required + to write a custom platform. +* Stop required a `SILENT_OBSERVATORY` environment variable to run with + debugging and the JSON reporter. + +## 0.3.12-nullsafety.4 + +* Support latest `package:vm_service`. + +## 0.3.12-nullsafety.3 + +* Clean up `--help` output. + +## 0.3.12-nullsafety.2 + +* Allow version `0.40.x` of `analyzer`. + +## 0.3.12-nullsafety.1 + +* Update source_maps constraint. + +## 0.3.12-nullsafety + +* Migrate to null safety. + +## 0.3.11+4 (Backport) + +* Fix `spawnHybridUri` on windows. + +## 0.3.11+3 (Backport) + +* Support `package:analyzer` version `0.41.x`. + +## 0.3.11+2 (Backport) + +* Fix `spawnHybridUri` to respect language versioning of the spawned uri. + +## 0.3.11+1 + +* Allow analyzer 0.40.x. + +## 0.3.11 + +* Update to `matcher` version `0.12.9`. + +## 0.3.10 + +* Prepare for `unawaited` from `package:meta`. + +## 0.3.9 + +* Ignore a null `RunnerSuite` rather than throw an error. + +## 0.3.8 + +* Update vm bootstrapping logic to ensure the bootstrap library has the same + language version as the test. +* Populate `languageVersionComment` in the `Metadata` returned from + `parseMetadata`. + +## 0.3.7 + +* Support the latest `package:coverage`. + +## 0.3.6 + +* Expose the `Configuration` class and related classes through `backend.dart`. + +## 0.3.5 + +* Add additional information to an exception when we end up with a null + `RunnerSuite`. + +* Update vm bootstrapping logic to ensure the bootstrap library has the same + language version as the test. +* Populate `languageVersionComment` in the `Metadata` returned from + `parseMetadata`. + +## 0.3.4 + +* Fix error messages for incorrect string literals in test annotations. + +## 0.3.3 + +* Support latest `package:vm_service`. + +## 0.3.2 + +* Drop the `package_resolver` dependency. + +## 0.3.1 + +* Support latest `package:vm_service`. +* Enable asserts in code running through `spawnHybrid` APIs. +* Exit with a non-zero code if no tests were ran, whether due to skips or having + no tests defined. + +## 0.3.0 + +* Bump minimum SDK to `2.4.0` for safer usage of for-loop elements. +* Deprecate `PhantomJS` and provide warning when used. Support for `PhantomJS` + will be removed in version `2.0.0`. +* Differentiate between test-randomize-ordering-seed not set and 0 being chosen + as the random seed. +* `deserializeSuite` now takes an optional `gatherCoverage` callback. +* Support retrying of entire test suites when they fail to load. +* Fix the `compiling` message in precompiled mode so it says `loading` instead, + which is more accurate. +* Change the behavior of the concurrency setting so that loading and running + don't have separate pools. + * The loading and running of a test are now done with the same resource, and + the concurrency setting uniformly affects each. With `-j1` only a single + test will ever be loaded at a time. + * Previously the loading pool was 2x larger than the actual concurrency + setting which could cause flaky tests due to tests being loaded while + other tests were running, even with `-j1`. +* Avoid printing uncaught errors within `spawnHybridUri`. + +## 0.2.18 + +* Allow `test_api` `0.2.13` to work around a bug in the SDK version `2.3.0`. + +## 0.2.17 + +* Add `file_reporters` configuration option and `--file-reporter` CLI option to + allow specifying a separate reporter that writes to a file. + +## 0.2.16 + +* Internal cleanup. +* Add `customHtmlTemplateFile` configuration option to allow sharing an + html template between tests +* Depend on the latest `test_api`. + +## 0.2.15 + +* Add a `StringSink` argument to reporters to prepare for reporting to a file. +* Add --test-randomize-ordering-seed` argument to randomize test +execution order based on a provided seed +* Depend on the latest `test_api`. + +## 0.2.14 + +* Support the latest `package:analyzer`. +* Update to latest `package:matcher`. Improves output for instances of private + classes. + +## 0.2.13 + +* Depend on the latest `package:test_api`. + +## 0.2.12 + +* Conditionally import coverage logic in `engine.dart`. This ensures the engine + is platform agnostic. + +## 0.2.11 + +* Implement code coverage gathering for VM tests. + +## 0.2.10 + +* Add a `--debug` argument for running the VM/Chrome in debug mode. + +## 0.2.9+2 + +* Depend on the latest `test_api`. + +## 0.2.9+1 + +* Allow the latest `package:vm_service`. + +## 0.2.9 + +* Mark `package:test_core` as deprecated to prevent accidental use. +* Depend on the latest `test_api`. + +## 0.2.8 + +* Depend on `vm_service` instead of `vm_service_lib`. +* Drop dependency on `pub_semver`. +* Allow `analyzer` version `0.38.x`. + +## 0.2.7 + +* Depend on `vm_service_lib` instead of `vm_service_client`. +* Depend on latest `package:analyzer`. + +## 0.2.6 + +* Internal cleanup - fix lints. +* Use the latest `test_api`. + +## 0.2.5 + +* Fix an issue where non-completed tests were considered passing. +* Updated `compact` and `expanded` reporters to display non-completed tests. + +## 0.2.4 + +* Avoid `dart:isolate` imports on code loaded in tests. +* Expose the `parseMetadata` function publicly through a new `backend.dart` + import, as well as re-exporting `package:test_api/backend.dart`. + +## 0.2.3 + +* Switch import for `IsolateChannel` for forwards compatibility with `2.0.0`. + +## 0.2.2 + +* Allow `analyzer` version `0.36.x`. +* Update to matcher version `0.12.5`. + +## 0.2.1+1 + +* Allow `analyzer` version `0.35.x`. + +## 0.2.1 + +* Require Dart SDK `>=2.1.0`. +* Require latest `test_api`. + +## 0.2.0 + +* Remove `remote_listener.dart` and `suite_channel_manager.dart` from runner + and depend on them from `test_api`. + +## 0.1.0 + +* Initial release of `test_core`. Provides the basic API for writing and running + tests on the VM.
diff --git a/pkgs/test_core/LICENSE b/pkgs/test_core/LICENSE new file mode 100644 index 0000000..9972f6e --- /dev/null +++ b/pkgs/test_core/LICENSE
@@ -0,0 +1,27 @@ +Copyright 2018, 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_core/README.md b/pkgs/test_core/README.md new file mode 100644 index 0000000..67f21a8 --- /dev/null +++ b/pkgs/test_core/README.md
@@ -0,0 +1,11 @@ +[](https://pub.dev/packages/test_core) +[](https://pub.dev/packages/test_core/publisher) + +A minimal package for writing and running tests as well as extensions for +implementing a custom test runner. + +If you're interested in testing Dart code, you likely want to use +[package:test](https://pub.dev/packages/test). + +At this time this package is not intended to be used publicly. The API is not +yet stable.
diff --git a/pkgs/test_core/lib/backend.dart b/pkgs/test_core/lib/backend.dart new file mode 100644 index 0000000..f5a3b46 --- /dev/null +++ b/pkgs/test_core/lib/backend.dart
@@ -0,0 +1,16 @@ +// Copyright (c) 2019, 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. + +@Deprecated('package:test_core is not intended for general use. ' + 'Please use package:test.') +library; + +export 'package:test_api/backend.dart' + show Compiler, Metadata, PlatformSelector, Runtime, SuitePlatform; + +export 'src/runner/configuration.dart' show Configuration; +export 'src/runner/configuration/custom_runtime.dart' show CustomRuntime; +export 'src/runner/configuration/runtime_settings.dart' show RuntimeSettings; +export 'src/runner/parse_metadata.dart' show parseMetadata; +export 'src/runner/suite.dart' show SuiteConfiguration;
diff --git a/pkgs/test_core/lib/scaffolding.dart b/pkgs/test_core/lib/scaffolding.dart new file mode 100644 index 0000000..90304d2 --- /dev/null +++ b/pkgs/test_core/lib/scaffolding.dart
@@ -0,0 +1,9 @@ +// Copyright (c) 2021, 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. + +@Deprecated('package:test_core is not intended for general use. ' + 'Please use package:test.') +library; + +export 'src/scaffolding.dart';
diff --git a/pkgs/test_core/lib/src/bootstrap/vm.dart b/pkgs/test_core/lib/src/bootstrap/vm.dart new file mode 100644 index 0000000..05604f1 --- /dev/null +++ b/pkgs/test_core/lib/src/bootstrap/vm.dart
@@ -0,0 +1,50 @@ +// 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:developer'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:stream_channel/isolate_channel.dart'; +import 'package:stream_channel/stream_channel.dart'; + +import '../runner/plugin/remote_platform_helpers.dart'; +import '../runner/plugin/shared_platform_helpers.dart'; + +/// Bootstraps a vm test to communicate with the test runner over an isolate. +void internalBootstrapVmTest(Function Function() getMain, SendPort sendPort) { + var platformChannel = + MultiChannel<Object?>(IsolateChannel<Object?>.connectSend(sendPort)); + var testControlChannel = platformChannel.virtualChannel() + ..pipe(serializeSuite(getMain)); + platformChannel.sink.add(testControlChannel.id); + + platformChannel.stream.forEach((message) { + assert(message == 'debug'); + debugger(message: 'Paused by test runner'); + platformChannel.sink.add('done'); + }); +} + +/// Bootstraps a native executable test to communicate with the test runner over +/// a socket. +void internalBootstrapNativeTest( + Function Function() getMain, List<String> args) async { + if (args.length != 2) { + throw StateError( + 'Expected exactly two args, a host and a port, but got $args'); + } + var socket = await Socket.connect(args[0], int.parse(args[1])); + var platformChannel = MultiChannel<Object?>(jsonSocketStreamChannel(socket)); + var testControlChannel = platformChannel.virtualChannel() + ..pipe(serializeSuite(getMain)); + platformChannel.sink.add(testControlChannel.id); + + unawaited(platformChannel.stream.forEach((message) { + assert(message == 'debug'); + debugger(message: 'Paused by test runner'); + platformChannel.sink.add('done'); + })); +}
diff --git a/pkgs/test_core/lib/src/direct_run.dart b/pkgs/test_core/lib/src/direct_run.dart new file mode 100644 index 0000000..c574257 --- /dev/null +++ b/pkgs/test_core/lib/src/direct_run.dart
@@ -0,0 +1,134 @@ +// Copyright (c) 2020, 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:collection'; + +import 'package:path/path.dart' as p; +import 'package:test_api/backend.dart'; +import 'package:test_api/src/backend/declarer.dart'; //ignore: implementation_imports +import 'package:test_api/src/backend/group.dart'; //ignore: implementation_imports +import 'package:test_api/src/backend/group_entry.dart'; //ignore: implementation_imports +import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/test.dart'; //ignore: implementation_imports + +import 'runner/configuration.dart'; +import 'runner/engine.dart'; +import 'runner/plugin/environment.dart'; +import 'runner/reporter.dart'; +import 'runner/reporter/expanded.dart'; +import 'runner/runner_suite.dart'; +import 'runner/suite.dart'; +import 'util/os.dart'; +import 'util/print_sink.dart'; + +/// Runs all unskipped test cases declared in [testMain]. +/// +/// Test suite level metadata defined in annotations is not read. No filtering +/// is applied except for the filtering defined by `solo` or `skip` arguments to +/// `group` and `test`. Returns [true] if all tests passed. +Future<bool> directRunTests(FutureOr<void> Function() testMain, + {Reporter Function(Engine)? reporterFactory, + // TODO: Change the default https://github.com/dart-lang/test/issues/1571 + bool allowDuplicateTestNames = true}) => + _directRunTests(testMain, + reporterFactory: reporterFactory, + allowDuplicateTestNames: allowDuplicateTestNames); + +/// Runs a single test declared in [testMain] matched by it's full test name. +/// +/// There must be exactly one test defined with the name [fullTestName]. Note +/// that not all tests and groups are checked, so a test case that is not be +/// intended to be run (due to a `solo` on a different test) may still be run +/// with this API. Only the test names returned by [enumerateTestCases] should +/// be used to prevent running skipped tests. +/// +/// Return [true] if the test passes. +/// +/// If there are no tests matching [fullTestName] a [MissingTestException] is +/// thrown. If there is more than one test with the name [fullTestName] they +/// will both be run, then a [DuplicateTestnameException] will be thrown. +Future<bool> directRunSingleTest( + FutureOr<void> Function() testMain, String fullTestName, + {Reporter Function(Engine)? reporterFactory}) => + _directRunTests(testMain, + reporterFactory: reporterFactory, + fullTestName: fullTestName, + allowDuplicateTestNames: false); + +Future<bool> _directRunTests(FutureOr<void> Function() testMain, + {Reporter Function(Engine)? reporterFactory, + String? fullTestName, + required bool allowDuplicateTestNames}) async { + reporterFactory ??= (engine) => ExpandedReporter.watch(engine, PrintSink(), + color: Configuration.empty.color, printPath: false, printPlatform: false); + final declarer = Declarer( + fullTestName: fullTestName, + allowDuplicateTestNames: allowDuplicateTestNames); + await declarer.declare(testMain); + + final suite = RunnerSuite( + const PluginEnvironment(), + SuiteConfiguration.empty, + declarer.build(), + SuitePlatform(Runtime.vm, compiler: null, os: currentOSGuess), + path: p.prettyUri(Uri.base)); + + final engine = Engine() + ..suiteSink.add(suite) + ..suiteSink.close(); + + reporterFactory(engine); + + final success = await runZoned(() => Invoker.guard(engine.run), + zoneValues: {#test.declarer: declarer}) ?? + false; + + if (fullTestName != null) { + final testCount = engine.liveTests.length; + if (testCount == 0) { + throw MissingTestException(fullTestName); + } + } + return success; +} + +/// Runs [testMain] and returns the names of all declared tests. +/// +/// Test names declared must be unique. If any test repeats the full name, +/// including group prefixes, of a prior test a [DuplicateTestNameException] +/// will be thrown. +/// +/// Skipped tests are ignored. +Future<Set<String>> enumerateTestCases( + FutureOr<void> Function() testMain) async { + final declarer = Declarer(); + await declarer.declare(testMain); + + final toVisit = Queue<GroupEntry>.of([declarer.build()]); + final unskippedTestNames = <String>{}; + while (toVisit.isNotEmpty) { + final current = toVisit.removeLast(); + if (current is Group) { + toVisit.addAll(current.entries.reversed); + } else if (current is Test) { + if (current.metadata.skip) continue; + unskippedTestNames.add(current.name); + } else { + throw StateError('Unhandled Group Entry: ${current.runtimeType}'); + } + } + return unskippedTestNames; +} + +/// An exception thrown when a specific test was requested by name that does not +/// exist. +class MissingTestException implements Exception { + final String name; + MissingTestException(this.name); + + @override + String toString() => + 'A test with the name "$name" was not declared in the test suite.'; +}
diff --git a/pkgs/test_core/lib/src/executable.dart b/pkgs/test_core/lib/src/executable.dart new file mode 100644 index 0000000..eeefd16 --- /dev/null +++ b/pkgs/test_core/lib/src/executable.dart
@@ -0,0 +1,196 @@ +// Copyright (c) 2015, 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:source_span/source_span.dart'; +import 'package:stack_trace/stack_trace.dart'; +import 'package:test_api/src/backend/util/pretty_print.dart'; // ignore: implementation_imports + +import 'runner.dart'; +import 'runner/application_exception.dart'; +import 'runner/configuration.dart'; +import 'runner/no_tests_found_exception.dart'; +import 'runner/version.dart'; +import 'util/errors.dart'; +import 'util/exit_codes.dart' as exit_codes; +import 'util/io.dart'; + +StreamSubscription? signalSubscription; +bool isShutdown = false; + +/// Returns the path to the global test configuration file. +final String _globalConfigPath = () { + if (Platform.environment.containsKey('DART_TEST_CONFIG')) { + return Platform.environment['DART_TEST_CONFIG']!; + } else if (Platform.operatingSystem == 'windows') { + return p.join(Platform.environment['LOCALAPPDATA']!, 'DartTest.yaml'); + } else { + return '${Platform.environment['HOME']}/.dart_test.yaml'; + } +}(); + +Future<void> main(List<String> args) async { + await _execute(args); + completeShutdown(); +} + +// ignore: unreachable_from_main +Future<void> runTests(List<String> args) async { + await _execute(args); +} + +void completeShutdown() { + if (isShutdown) return; + if (signalSubscription != null) { + signalSubscription!.cancel(); + signalSubscription = null; + } + isShutdown = true; + cancelStdinLines(); +} + +Future<void> _execute(List<String> args) async { + /// A merged stream of all signals that tell the test runner to shut down + /// gracefully. + /// + /// Signals will only be captured as long as this has an active subscription. + /// Otherwise, they'll be handled by Dart's default signal handler, which + /// terminates the program immediately. + final signals = Platform.isWindows + ? ProcessSignal.sigint.watch() + : Platform.isFuchsia // Signals don't exist on Fuchsia. + ? const Stream<Never>.empty() + : StreamGroup.merge( + [ProcessSignal.sigterm.watch(), ProcessSignal.sigint.watch()]); + + Configuration configuration; + try { + configuration = Configuration.parse(args); + } on FormatException catch (error) { + _printUsage(error.message); + exitCode = exit_codes.usage; + return; + } + + if (configuration.help) { + _printUsage(); + return; + } + + if (configuration.version) { + var version = testVersion; + if (version == null) { + stderr.writeln("Couldn't find version number."); + exitCode = exit_codes.data; + } else { + print(version); + } + return; + } + + try { + var fileConfiguration = Configuration.empty; + if (File(_globalConfigPath).existsSync()) { + fileConfiguration = fileConfiguration + .merge(Configuration.load(_globalConfigPath, global: true)); + } + + if (File(configuration.configurationPath).existsSync()) { + fileConfiguration = fileConfiguration + .merge(Configuration.load(configuration.configurationPath)); + } + + configuration = fileConfiguration.merge(configuration); + } on SourceSpanFormatException catch (error) { + stderr.writeln(error.toString(color: configuration.color)); + exitCode = exit_codes.data; + return; + } on FormatException catch (error) { + stderr.writeln(error.message); + exitCode = exit_codes.data; + return; + } on IOException catch (error) { + stderr.writeln(error.toString()); + exitCode = exit_codes.noInput; + return; + } + + var undefinedPresets = configuration.chosenPresets + .where((preset) => !configuration.knownPresets.contains(preset)) + .toList(); + if (undefinedPresets.isNotEmpty) { + _printUsage("Undefined ${pluralize('preset', undefinedPresets.length)} " + "${toSentence(undefinedPresets.map((preset) => '"$preset"'))}."); + exitCode = exit_codes.usage; + return; + } + + if (!configuration.explicitPaths && + !Directory(configuration.testSelections.keys.single).existsSync()) { + _printUsage('No test files were passed and the default "test/" ' + "directory doesn't exist."); + exitCode = exit_codes.data; + return; + } + + Runner? runner; + + signalSubscription ??= signals.listen((signal) async { + completeShutdown(); + await runner?.close(); + }); + + try { + runner = Runner(configuration); + exitCode = (await runner.run()) ? 0 : 1; + } on ApplicationException catch (error) { + stderr.writeln(error.message); + exitCode = exit_codes.data; + } on SourceSpanFormatException catch (error) { + stderr.writeln(error.toString(color: configuration.color)); + exitCode = exit_codes.data; + } on FormatException catch (error) { + stderr.writeln(error.message); + exitCode = exit_codes.data; + } on NoTestsFoundException catch (error) { + stderr.writeln(error.message); + exitCode = exit_codes.noTestsRan; + } catch (error, stackTrace) { + stderr.writeln(getErrorMessage(error)); + stderr.writeln(Trace.from(stackTrace).terse); + stderr.writeln('This is an unexpected error. Please file an issue at ' + 'http://github.com/dart-lang/test\n' + 'with the stack trace and instructions for reproducing the error.'); + exitCode = exit_codes.software; + } finally { + await runner?.close(); + } + + return; +} + +/// Print usage information for this command. +/// +/// If [error] is passed, it's used in place of the usage message and the whole +/// thing is printed to stderr instead of stdout. +void _printUsage([String? error]) { + var output = stdout; + + var message = 'Runs tests in this package.'; + if (error != null) { + message = error; + output = stderr; + } + + output.write('''${wordWrap(message)} + +Usage: dart test [files or directories...] + +${Configuration.usage} +'''); +}
diff --git a/pkgs/test_core/lib/src/platform.dart b/pkgs/test_core/lib/src/platform.dart new file mode 100644 index 0000000..af457b4 --- /dev/null +++ b/pkgs/test_core/lib/src/platform.dart
@@ -0,0 +1,16 @@ +// Copyright (c) 2020, 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. + +export 'package:test_api/backend.dart' show Runtime, SuitePlatform; +export 'package:test_core/src/runner/configuration.dart' show Configuration; +export 'package:test_core/src/runner/environment.dart' + show Environment, PluginEnvironment; +export 'package:test_core/src/runner/hack_register_platform.dart' + show registerPlatformPlugin; +export 'package:test_core/src/runner/platform.dart' show PlatformPlugin; +export 'package:test_core/src/runner/plugin/platform_helpers.dart' + show deserializeSuite; +export 'package:test_core/src/runner/runner_suite.dart' + show RunnerSuite, RunnerSuiteController; +export 'package:test_core/src/runner/suite.dart' show SuiteConfiguration;
diff --git a/pkgs/test_core/lib/src/runner.dart b/pkgs/test_core/lib/src/runner.dart new file mode 100644 index 0000000..ee648d4 --- /dev/null +++ b/pkgs/test_core/lib/src/runner.dart
@@ -0,0 +1,475 @@ +// Copyright (c) 2015, 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:boolean_selector/boolean_selector.dart'; +import 'package:stack_trace/stack_trace.dart'; +import 'package:test_api/backend.dart' + show PlatformSelector, Runtime, SuitePlatform; +import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/group_entry.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/operating_system.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/util/pretty_print.dart'; // ignore: implementation_imports + +import 'runner/configuration.dart'; +import 'runner/configuration/reporters.dart'; +import 'runner/debugger.dart'; +import 'runner/engine.dart'; +import 'runner/load_exception.dart'; +import 'runner/load_suite.dart'; +import 'runner/loader.dart'; +import 'runner/no_tests_found_exception.dart'; +import 'runner/reporter.dart'; +import 'runner/reporter/compact.dart'; +import 'runner/reporter/expanded.dart'; +import 'runner/reporter/multiplex.dart'; +import 'runner/runner_suite.dart'; +import 'util/io.dart'; + +/// A class that loads and runs tests based on a [Configuration]. +/// +/// This maintains a [Loader] and an [Engine] and passes test suites from one to +/// the other, as well as printing out tests with a [CompactReporter] or an +/// [ExpandedReporter]. +class Runner { + /// The test runner configuration. + final _config = Configuration.current; + + /// The loader that loads the test suites from the filesystem. + final _loader = Loader(); + + /// The engine that runs the test suites. + final Engine _engine; + + /// The reporter that's emitting the test runner's results. + final Reporter _reporter; + + /// The subscription to the stream returned by [_loadSuites]. + StreamSubscription? _suiteSubscription; + + /// The set of suite paths for which [_warnForUnknownTags] has already been + /// called. + /// + /// This is used to avoid printing duplicate warnings when a suite is loaded + /// on multiple platforms. + final _tagWarningSuites = <String>{}; + + /// The current debug operation, if any. + /// + /// This is stored so that we can cancel it when the runner is closed. + CancelableOperation? _debugOperation; + + /// The memoizer for ensuring [close] only runs once. + final _closeMemo = AsyncMemoizer<void>(); + bool get _closed => _closeMemo.hasRun; + + /// Sinks created for each file reporter (if there are any). + final List<IOSink> _sinks; + + /// Creates a new runner based on [configuration]. + factory Runner(Configuration config) => config.asCurrent(() { + var engine = Engine( + concurrency: config.concurrency, + coverage: config.coverage, + testRandomizeOrderingSeed: config.testRandomizeOrderingSeed, + stopOnFirstFailure: config.stopOnFirstFailure, + ); + + var sinks = <IOSink>[]; + Reporter createFileReporter(String reporterName, String filepath) { + final sink = + (File(filepath)..createSync(recursive: true)).openWrite(); + sinks.add(sink); + return allReporters[reporterName]!.factory(config, engine, sink); + } + + return Runner._( + engine, + MultiplexReporter([ + // Standard reporter. + allReporters[config.reporter]!.factory(config, engine, stdout), + // File reporters. + for (var reporter in config.fileReporters.keys) + createFileReporter(reporter, config.fileReporters[reporter]!), + ]), + sinks, + ); + }); + + Runner._(this._engine, this._reporter, this._sinks); + + /// Starts the runner. + /// + /// This starts running tests and printing their progress. It returns whether + /// or not they ran successfully. + Future<bool> run() => _config.asCurrent(() async { + if (_closed) { + throw StateError('run() may not be called on a closed Runner.'); + } + + _warnForUnsupportedPlatforms(); + + var suites = _loadSuites(); + + if (_config.coverage != null) { + await Directory(_config.coverage!).create(recursive: true); + } + + bool? success; + if (_config.pauseAfterLoad) { + success = await _loadThenPause(suites); + } else { + var subscription = + _suiteSubscription = suites.listen(_engine.suiteSink.add); + var results = await Future.wait(<Future>[ + subscription + .asFuture<void>() + .then((_) => _engine.suiteSink.close()), + _engine.run() + ], eagerError: true); + success = results.last as bool?; + } + + if (_closed) return false; + + if (_engine.passed.isEmpty && + _engine.failed.isEmpty && + _engine.skipped.isEmpty) { + if (_config.globalPatterns.isNotEmpty) { + var patterns = toSentence(_config.globalPatterns.map((pattern) => + pattern is RegExp + ? 'regular expression "${pattern.pattern}"' + : '"$pattern"')); + throw NoTestsFoundException('No tests match $patterns.'); + } else if (_config.includeTags != BooleanSelector.all || + _config.excludeTags != BooleanSelector.none) { + throw NoTestsFoundException( + 'No tests match the requested tag selectors:\n' + ' include: "${_config.includeTags}"\n' + ' exclude: "${_config.excludeTags}"'); + } else { + throw NoTestsFoundException('No tests were found.'); + } + } + + return (success ?? false) && + (_engine.passed.isNotEmpty || _engine.skipped.isNotEmpty); + }); + + /// Emits a warning if the user is trying to run on a platform that's + /// unsupported for the entire package. + void _warnForUnsupportedPlatforms() { + var testOn = _config.suiteDefaults.metadata.testOn; + if (testOn == PlatformSelector.all) return; + + var unsupportedRuntimes = _config.suiteDefaults.runtimes + .map(_loader.findRuntime) + .whereType<Runtime>() + .where((runtime) => !testOn.evaluate(currentPlatform(runtime, null))) + .toList(); + + if (unsupportedRuntimes.isEmpty) return; + + // Human-readable names for all unsupported runtimes. + var unsupportedNames = <String>[]; + + // If the user tried to run on one or more unsupported browsers, figure out + // whether we should warn about the individual browsers or whether all + // browsers are unsupported. + var unsupportedBrowsers = + unsupportedRuntimes.where((platform) => platform.isBrowser); + if (unsupportedBrowsers.isNotEmpty) { + var supportsAnyBrowser = _loader.allRuntimes + .where((runtime) => runtime.isBrowser) + .any((runtime) => testOn.evaluate(currentPlatform(runtime, null))); + + if (supportsAnyBrowser) { + unsupportedNames + .addAll(unsupportedBrowsers.map((runtime) => runtime.name)); + } else { + unsupportedNames.add('browsers'); + } + } + + // If the user tried to run on the VM and it's not supported, figure out if + // that's because of the current OS or whether the VM is unsupported. + if (unsupportedRuntimes.contains(Runtime.vm)) { + var supportsAnyOS = OperatingSystem.all.any((os) => testOn.evaluate( + SuitePlatform(Runtime.vm, + compiler: null, os: os, inGoogle: inGoogle))); + + if (supportsAnyOS) { + unsupportedNames.add(currentOS.name); + } else { + unsupportedNames.add('the Dart VM'); + } + } + + warn("this package doesn't support running tests on " + '${toSentence(unsupportedNames, conjunction: 'or')}.'); + } + + /// Closes the runner. + /// + /// This stops any future test suites from running. It will wait for any + /// currently-running VM tests, in case they have stuff to clean up on the + /// filesystem. + Future close() => _closeMemo.runOnce(() async { + Timer? timer; + if (!_engine.isIdle) { + // Wait a bit to print this message, since printing it eagerly looks weird + // if the tests then finish immediately. + timer = Timer(const Duration(seconds: 1), () { + // Pause the reporter while we print to ensure that we don't interfere + // with its output. + _reporter.pause(); + print('Waiting for current test(s) to finish.'); + print('Press Control-C again to terminate immediately.'); + _reporter.resume(); + }); + } + + await _debugOperation?.cancel(); + await _suiteSubscription?.cancel(); + + _suiteSubscription = null; + + // Make sure we close the engine *before* the loader. Otherwise, + // LoadSuites provided by the loader may get into bad states. + // + // We close the loader's browsers while we're closing the engine because + // browser tests don't store any state we care about and we want them to + // shut down without waiting for their tear-downs. + await Future.wait([_loader.closeEphemeral(), _engine.close()]); + timer?.cancel(); + await _loader.close(); + + // Flush any IOSinks created for file reporters. + await Future.wait(_sinks.map((s) => s.flush().then((_) => s.close()))); + _sinks.clear(); + }); + + /// Return a stream of [LoadSuite]s in [_config.testSelections]. + /// + /// Only tests that match [_config.patterns] will be included in the + /// suites once they're loaded. + Stream<LoadSuite> _loadSuites() { + return StreamGroup.merge(_config.testSelections.entries.map((pathEntry) { + final testPath = pathEntry.key; + final testSelections = pathEntry.value; + final suiteConfig = _config.suiteDefaults.selectTests(testSelections); + if (Directory(testPath).existsSync()) { + return _loader.loadDir(testPath, suiteConfig); + } else if (File(testPath).existsSync()) { + return _loader.loadFile(testPath, suiteConfig); + } else { + return Stream.fromIterable([ + LoadSuite.forLoadException( + LoadException(testPath, 'Does not exist.'), + suiteConfig, + ), + ]); + } + })).map((loadSuite) { + return loadSuite.changeSuite((suite) { + _warnForUnknownTags(suite); + + return _shardSuite(suite.filter((test) { + // Skip any tests that don't match all the global patterns. + if (!_config.globalPatterns + .every((pattern) => test.name.contains(pattern))) { + return false; + } + + // If the user provided tags, skip tests that don't match all of them. + if (!_config.includeTags.evaluate(test.metadata.tags.contains)) { + return false; + } + + // Skip tests that do match any tags the user wants to exclude. + if (_config.excludeTags.evaluate(test.metadata.tags.contains)) { + return false; + } + + final testSelections = suite.config.testSelections; + assert(testSelections.isNotEmpty, 'Tests should have been selected'); + return testSelections.any((selection) { + // Skip tests that don't match all the suite specific patterns. + if (!selection.testPatterns + .every((pattern) => test.name.contains(pattern))) { + return false; + } + // Skip tests that don't start on `line` or `col` if specified. + var line = selection.line; + var col = selection.col; + if (line == null && col == null) return true; + var trace = test.trace; + if (trace == null) { + throw StateError( + 'Cannot filter by line/column for this test suite, no stack' + 'trace available.'); + } + var path = suite.path; + if (path == null) { + throw StateError( + 'Cannot filter by line/column for this test suite, no suite' + 'path available.'); + } + // The absolute path as it will appear in stack traces. + var absoluteSuitePath = File(path).absolute.uri.toFilePath(); + + bool matchLineAndCol(Frame frame) { + switch (frame.uri.scheme) { + case 'file': + if (frame.uri.toFilePath() != absoluteSuitePath) { + return false; + } + case 'package': + // It is unlikely that the test case is specified in a + // package: URI. Ignore this case. + return false; + default: + // Now we can assume that the kernel is compiled using + // --filesystem-scheme. + // In this case, because we don't know the --filesystem-root, as + // long as the file path matches we assume it is the same file. + if (!absoluteSuitePath.endsWith(frame.uri.path)) { + return false; + } + } + if (line != null && frame.line != line) { + return false; + } + if (col != null && frame.column != col) { + return false; + } + return true; + } + + return trace.frames.any(matchLineAndCol); + }); + })); + }); + }); + } + + /// Prints a warning for any unknown tags referenced in [suite] or its + /// children. + void _warnForUnknownTags(Suite suite) { + if (_tagWarningSuites.contains(suite.path)) return; + _tagWarningSuites.add(suite.path!); + + var unknownTags = _collectUnknownTags(suite); + if (unknownTags.isEmpty) return; + + var yellow = _config.color ? '\u001b[33m' : ''; + var bold = _config.color ? '\u001b[1m' : ''; + var noColor = _config.color ? '\u001b[0m' : ''; + + var buffer = StringBuffer() + ..write('${yellow}Warning:$noColor ') + ..write(unknownTags.length == 1 ? 'A tag was ' : 'Tags were ') + ..write('used that ') + ..write(unknownTags.length == 1 ? "wasn't " : "weren't ") + ..writeln('specified in dart_test.yaml.'); + + unknownTags.forEach((tag, entries) { + buffer.write(' $bold$tag$noColor was used in'); + + if (entries.length == 1) { + buffer.writeln(' ${_entryDescription(entries.single)}'); + return; + } + + buffer.write(':'); + for (var entry in entries) { + buffer.write('\n ${_entryDescription(entry)}'); + } + buffer.writeln(); + }); + + print(buffer.toString()); + } + + /// Collects all tags used by [suite] or its children that aren't also passed + /// on the command line. + /// + /// This returns a map from tag names to lists of entries that use those tags. + Map<String, List<GroupEntry>> _collectUnknownTags(Suite suite) { + var unknownTags = <String, List<GroupEntry>>{}; + var currentTags = <String>{}; + + void collect(GroupEntry entry) { + var newTags = <String>{}; + for (var unknownTag + in entry.metadata.tags.difference(_config.knownTags)) { + if (currentTags.contains(unknownTag)) continue; + unknownTags.putIfAbsent(unknownTag, () => []).add(entry); + newTags.add(unknownTag); + } + + if (entry is! Group) return; + + currentTags.addAll(newTags); + for (var child in entry.entries) { + collect(child); + } + currentTags.removeAll(newTags); + } + + collect(suite.group); + return unknownTags; + } + + /// Returns a human-readable description of [entry], including its type. + String _entryDescription(GroupEntry entry) { + if (entry is Test) return 'the test "${entry.name}"'; + if (entry.name.isNotEmpty) return 'the group "${entry.name}"'; + return 'the suite itself'; + } + + /// If sharding is enabled, filters [suite] to only include the tests that + /// should be run in this shard. + /// + /// We just take a slice of the tests in each suite corresponding to the shard + /// index. This makes the tests pretty tests across shards, and since the + /// tests are continuous, makes us more likely to be able to re-use + /// `setUpAll()` logic. + RunnerSuite _shardSuite(RunnerSuite suite) { + if (_config.totalShards == null) return suite; + + var shardSize = suite.group.testCount / _config.totalShards!; + var shardIndex = _config.shardIndex!; + var shardStart = (shardSize * shardIndex).round(); + var shardEnd = (shardSize * (shardIndex + 1)).round(); + + var count = -1; + var filtered = suite.filter((test) { + count++; + return count >= shardStart && count < shardEnd; + }); + + return filtered; + } + + /// Loads each suite in [suites] in order, pausing after load for runtimes + /// that support debugging. + Future<bool> _loadThenPause(Stream<LoadSuite> suites) async { + var subscription = _suiteSubscription = suites.asyncMap((loadSuite) async { + var operation = _debugOperation = debug(_engine, _reporter, loadSuite); + await operation.valueOrCancellation(); + }).listen(null); + + var results = await Future.wait(<Future>[ + subscription.asFuture<void>().then((_) => _engine.suiteSink.close()), + _engine.run() + ], eagerError: true); + return results.last as bool; + } +}
diff --git a/pkgs/test_core/lib/src/runner/application_exception.dart b/pkgs/test_core/lib/src/runner/application_exception.dart new file mode 100644 index 0000000..23155a2 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/application_exception.dart
@@ -0,0 +1,13 @@ +// Copyright (c) 2015, 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. + +/// An expected exception caused by user-controllable circumstances. +class ApplicationException implements Exception { + final String message; + + ApplicationException(this.message); + + @override + String toString() => message; +}
diff --git a/pkgs/test_core/lib/src/runner/compiler_pool.dart b/pkgs/test_core/lib/src/runner/compiler_pool.dart new file mode 100644 index 0000000..52aa238 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/compiler_pool.dart
@@ -0,0 +1,57 @@ +// Copyright (c) 2015, 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 'package:async/async.dart'; +import 'package:meta/meta.dart'; +import 'package:pool/pool.dart'; + +import 'configuration.dart'; +import 'suite.dart'; + +/// A pool of compiler instances. +/// +/// This limits the number of compiler instances running concurrently. +abstract class CompilerPool { + /// The test runner configuration. + final config = Configuration.current; + + /// The internal pool that controls the number of process running at once. + final Pool _pool; + + /// Whether [close] has been called. + bool get closed => _closeMemo.hasRun; + + /// The memoizer for running [close] exactly once. + final _closeMemo = AsyncMemoizer<void>(); + + /// Creates a compiler pool that multiple instances of a compiler at once. + CompilerPool() : _pool = Pool(Configuration.current.concurrency); + + /// Compiles [code] to [path] using [_pool] and [compileInternal]. + /// + /// Should not be overridden. + Future<void> compile( + String code, String path, SuiteConfiguration suiteConfig) => + _pool.withResource(() { + if (closed) return null; + return compileInternal(code, path, suiteConfig); + }); + + /// The actual function a given compiler pool should implement to compile a + /// suite. + @protected + Future<void> compileInternal( + String code, String path, SuiteConfiguration suiteConfig); + + /// Shuts down the compiler pool, invoking `closeInternal` exactly once. + /// + /// Should not be overridden. + Future<void> close() => _closeMemo.runOnce(closeInternal); + + /// The actual function to shut down the compiler pool, invoked exactly once. + @protected + Future<void> closeInternal(); +}
diff --git a/pkgs/test_core/lib/src/runner/compiler_selection.dart b/pkgs/test_core/lib/src/runner/compiler_selection.dart new file mode 100644 index 0000000..70ab553 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/compiler_selection.dart
@@ -0,0 +1,61 @@ +// Copyright (c) 2023, 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:source_span/source_span.dart'; +import 'package:test_api/backend.dart'; + +/// A compiler with which the user has chosen to run tests. +class CompilerSelection { + /// The chosen compiler to use. + final Compiler compiler; + + /// The location in the configuration file of this compiler string, or `null` + /// if it was defined outside a configuration file (for example, on the + /// command line). + final SourceSpan? span; + + /// The platform selector for which platforms this compiler should apply to, + /// if specified. Defaults to all platforms where the compiler is supported. + final PlatformSelector? platformSelector; + + CompilerSelection(String compiler, + {required this.platformSelector, required this.span}) + : compiler = Compiler.builtIn.firstWhere((c) => c.identifier == compiler); + + factory CompilerSelection.parse(String option, {SourceSpan? parentSpan}) { + var parts = option.split(':'); + switch (parts.length) { + case 1: + _checkValidCompiler(option, parentSpan); + return CompilerSelection(option, + platformSelector: null, span: parentSpan); + case 2: + var compiler = parts[1]; + _checkValidCompiler(compiler, parentSpan); + return CompilerSelection(compiler, + platformSelector: PlatformSelector.parse(parts[0]), + span: parentSpan); + default: + throw ArgumentError.value( + option, + '--compiler', + 'Must be of the format [<boolean-selector>:]<compiler>, but got ' + 'more than one `:`.'); + } + } + + @override + bool operator ==(Object other) => + other is CompilerSelection && other.compiler == compiler; + + @override + int get hashCode => compiler.hashCode; +} + +void _checkValidCompiler(String compiler, SourceSpan? span) { + if (Compiler.builtIn.any((c) => c.identifier == compiler)) return; + throw SourceSpanFormatException( + 'Invalid compiler `$compiler`, must be one of ${Compiler.builtIn.map((c) => c.identifier).join(', ')}', + span); +}
diff --git a/pkgs/test_core/lib/src/runner/configuration.dart b/pkgs/test_core/lib/src/runner/configuration.dart new file mode 100644 index 0000000..8ba4aa8 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/configuration.dart
@@ -0,0 +1,1088 @@ +// Copyright (c) 2015, 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:boolean_selector/boolean_selector.dart'; +import 'package:collection/collection.dart'; +import 'package:glob/glob.dart'; +import 'package:path/path.dart' as p; +import 'package:source_span/source_span.dart'; +import 'package:test_api/scaffolding.dart' show Timeout; +import 'package:test_api/src/backend/platform_selector.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports + +import '../util/io.dart'; +import 'compiler_selection.dart'; +import 'configuration/args.dart' as args; +import 'configuration/custom_runtime.dart'; +import 'configuration/load.dart'; +import 'configuration/reporters.dart'; +import 'configuration/runtime_settings.dart'; +import 'configuration/utils.dart'; +import 'configuration/values.dart'; +import 'runtime_selection.dart'; +import 'suite.dart'; + +export 'suite.dart' show TestSelection; + +/// The key used to look up [Configuration.current] in a zone. +final _currentKey = Object(); + +/// A class that encapsulates the command-line configuration of the test runner. +class Configuration { + /// An empty configuration with only default values. + /// + /// Using this is slightly more efficient than manually constructing a new + /// configuration with no arguments. + static final empty = Configuration._unsafe(); + + /// The usage string for the command-line arguments. + static String get usage => args.usage; + + /// Whether `--help` was passed. + bool get help => _help ?? false; + final bool? _help; + + /// Custom HTML template file. + final String? customHtmlTemplatePath; + + /// Whether `--version` was passed. + bool get version => _version ?? false; + final bool? _version; + + /// Whether to pause for debugging after loading each test suite. + bool get pauseAfterLoad => _pauseAfterLoad ?? false; + final bool? _pauseAfterLoad; + + /// Whether to run browsers in their respective debug modes + bool get debug => pauseAfterLoad || (_debug ?? false) || coverage != null; + final bool? _debug; + + /// The output folder for coverage gathering + final String? coverage; + + /// The path to the file from which to load more configuration information. + /// + /// This is *not* resolved automatically. + String get configurationPath => _configurationPath ?? 'dart_test.yaml'; + final String? _configurationPath; + + /// The name of the reporter to use to display results. + String get reporter => _reporter ?? defaultReporter; + final String? _reporter; + + /// The map of file reporters where the key is the name of the reporter and + /// the value is the filepath to which its output should be written. + final Map<String, String> fileReporters; + + /// Whether to disable retries of tests. + bool get noRetry => _noRetry ?? false; + final bool? _noRetry; + + /// Whether to use command-line color escapes. + bool get color => _color ?? canUseSpecialChars; + final bool? _color; + + /// How many tests to run concurrently. + int get concurrency => + pauseAfterLoad ? 1 : (_concurrency ?? defaultConcurrency); + final int? _concurrency; + + /// The index of the current shard, if sharding is in use, or `null` if it's + /// not. + /// + /// Sharding is a technique that allows the Google internal test framework to + /// easily split a test run across multiple workers without requiring the + /// tests to be modified by the user. When sharding is in use, the runner gets + /// a shard index (this field) and a total number of shards, and is expected + /// to provide the following guarantees: + /// + /// * Running the same invocation of the runner, with the same shard index and + /// total shards, will run the same set of tests. + /// * Across all shards, each test must be run exactly once. + /// + /// In addition, tests should be balanced across shards as much as possible. + final int? shardIndex; + + /// The total number of shards, if sharding is in use, or `null` if it's not. + /// + /// See [shardIndex] for details. + final int? totalShards; + + /// The list of packages to fold when producing [StackTrace]s. + Set<String> get foldTraceExcept => _foldTraceExcept ?? {}; + final Set<String>? _foldTraceExcept; + + /// If non-empty, all packages not in this list will be folded when producing + /// [StackTrace]s. + Set<String> get foldTraceOnly => _foldTraceOnly ?? {}; + final Set<String>? _foldTraceOnly; + + /// The paths from which to load tests, and the test cases to run. + Map<String, Set<TestSelection>> get testSelections => + _testSelections ?? + const { + 'test': {TestSelection()} + }; + final Map<String, Set<TestSelection>>? _testSelections; + + /// Whether the load paths were passed explicitly or the default was used. + bool get explicitPaths => _testSelections != null; + + /// The glob matching the basename of tests to run. + /// + /// This is used to find tests within a directory. + Glob get filename => _filename ?? defaultFilename; + final Glob? _filename; + + /// The set of presets to use. + /// + /// Any chosen presets for the parent configuration are added to the chosen + /// preset sets for child configurations as well. + /// + /// Note that the order of this set matters. + final Set<String> chosenPresets; + + /// The set of tags that have been declared in any way in this configuration. + late final Set<String> knownTags = UnmodifiableSetView({ + ...includeTags.variables, + ...excludeTags.variables, + ...suiteDefaults.knownTags, + for (var configuration in presets.values) ...configuration.knownTags + }); + + /// Only run tests whose tags match this selector. + /// + /// When [merge]d, this is intersected with the other configuration's included + /// tags. + final BooleanSelector includeTags; + + /// Do not run tests whose tags match this selector. + /// + /// When [merge]d, this is unioned with the other configuration's + /// excluded tags. + final BooleanSelector excludeTags; + + /// Configuration presets. + /// + /// These are configurations that can be explicitly selected by the user via + /// the command line. Preset configuration takes precedence over the base + /// configuration. + /// + /// This is guaranteed not to have any keys that match [chosenPresets]; those + /// are resolved when the configuration is constructed. + final Map<String, Configuration> presets; + + /// All preset names that are known to be valid. + /// + /// This includes presets that have already been resolved. + Set<String> get knownPresets => _knownPresets ??= UnmodifiableSetView({ + ...presets.keys, + for (var configuration in presets.values) ...configuration.knownPresets + }); + Set<String>? _knownPresets; + + /// Built-in runtimes whose settings are overridden by the user. + final Map<String, RuntimeSettings> overrideRuntimes; + + /// Runtimes defined by the user in terms of existing runtimes. + final Map<String, CustomRuntime> defineRuntimes; + + /// The default suite-level configuration. + final SuiteConfiguration suiteDefaults; + + /// The set of patterns to check test names against in all suites that run. + final Set<Pattern> globalPatterns; + + /// The seed used to generate randomness for test case shuffling. + /// + /// If null or zero no shuffling will occur. + /// The same seed will shuffle the tests in the same way every time. + final int? testRandomizeOrderingSeed; + + final bool? _stopOnFirstFailure; + + /// Whether to stop running subsequent tests after a test fails. + bool get stopOnFirstFailure => _stopOnFirstFailure ?? false; + + /// Returns the current configuration, or a default configuration if no + /// current configuration is set. + /// + /// The current configuration is set using [asCurrent]. + static Configuration get current => + Zone.current[_currentKey] as Configuration? ?? Configuration._unsafe(); + + /// Parses the configuration from [args]. + /// + /// Throws a [FormatException] if [args] are invalid. + factory Configuration.parse(List<String> arguments) => args.parse(arguments); + + /// Loads configuration from [path]. + /// + /// If [global] is `true`, this restricts the configuration to rules that are + /// supported globally. + /// + /// Throws an [IOException] if [path] does not exist or cannot be read. Throws + /// a [FormatException] if the file contents are invalid. + factory Configuration.load(String path, {bool global = false}) { + final content = File(path).readAsStringSync(); + final sourceUrl = p.toUri(path); + return parse(content, global: global, sourceUrl: sourceUrl); + } + + /// Parses configuration from YAML formatted [content]. + /// + /// If [global] is `true`, this restricts the configuration to rules that are + /// supported globally. + /// + /// If [sourceUrl] is provided it will be set as the source url for the yaml + /// document. + /// + /// Throws a [FormatException] if the content is invalid. + factory Configuration.loadFromString(String content, + {bool global = false, Uri? sourceUrl}) => + parse(content, global: global, sourceUrl: sourceUrl); + + factory Configuration( + {required bool? help, + required String? customHtmlTemplatePath, + required bool? version, + required bool? pauseAfterLoad, + required bool? debug, + required bool? color, + required String? configurationPath, + required String? reporter, + required Map<String, String>? fileReporters, + required String? coverage, + required int? concurrency, + required int? shardIndex, + required int? totalShards, + required Map<String, Set<TestSelection>>? testSelections, + required Iterable<String>? foldTraceExcept, + required Iterable<String>? foldTraceOnly, + required Glob? filename, + required Iterable<String>? chosenPresets, + required Map<String, Configuration>? presets, + required Map<String, RuntimeSettings>? overrideRuntimes, + required Map<String, CustomRuntime>? defineRuntimes, + required bool? noRetry, + required int? testRandomizeOrderingSeed, + required bool? stopOnFirstFailure, + + // Suite-level configuration + required bool? allowDuplicateTestNames, + required bool? allowTestRandomization, + required bool? jsTrace, + required bool? runSkipped, + required Iterable<String>? dart2jsArgs, + required String? precompiledPath, + required Iterable<Pattern>? globalPatterns, + required Iterable<CompilerSelection>? compilerSelections, + required Iterable<RuntimeSelection>? runtimes, + required BooleanSelector? includeTags, + required BooleanSelector? excludeTags, + required Map<BooleanSelector, SuiteConfiguration>? tags, + required Map<PlatformSelector, SuiteConfiguration>? onPlatform, + required bool? ignoreTimeouts, + + // Test-level configuration + required Timeout? timeout, + required bool? verboseTrace, + required bool? chainStackTraces, + required bool? skip, + required int? retry, + required String? skipReason, + required PlatformSelector? testOn, + required Iterable<String>? addTags}) { + var chosenPresetSet = chosenPresets?.toSet(); + var configuration = Configuration._( + help: help, + customHtmlTemplatePath: customHtmlTemplatePath, + version: version, + pauseAfterLoad: pauseAfterLoad, + debug: debug, + color: color, + configurationPath: configurationPath, + reporter: reporter, + fileReporters: fileReporters, + coverage: coverage, + concurrency: concurrency, + shardIndex: shardIndex, + totalShards: totalShards, + testSelections: testSelections, + foldTraceExcept: foldTraceExcept, + foldTraceOnly: foldTraceOnly, + filename: filename, + chosenPresets: chosenPresetSet, + presets: _withChosenPresets(presets, chosenPresetSet), + overrideRuntimes: overrideRuntimes, + defineRuntimes: defineRuntimes, + noRetry: noRetry, + testRandomizeOrderingSeed: testRandomizeOrderingSeed, + stopOnFirstFailure: stopOnFirstFailure, + includeTags: includeTags, + excludeTags: excludeTags, + globalPatterns: globalPatterns, + suiteDefaults: SuiteConfiguration( + allowDuplicateTestNames: allowDuplicateTestNames, + allowTestRandomization: allowTestRandomization, + jsTrace: jsTrace, + runSkipped: runSkipped, + dart2jsArgs: dart2jsArgs, + precompiledPath: precompiledPath, + compilerSelections: compilerSelections, + runtimes: runtimes, + tags: tags, + onPlatform: onPlatform, + ignoreTimeouts: ignoreTimeouts, + + // Test-level configuration + timeout: timeout, + verboseTrace: verboseTrace, + chainStackTraces: chainStackTraces, + skip: skip, + retry: retry, + skipReason: skipReason, + testOn: testOn, + addTags: addTags)); + return configuration._resolvePresets(); + } + + /// A constructor that doesn't require all of its options to be passed. + /// + /// This should only be used in situations where you really only want to + /// configure a specific restricted set of options. + factory Configuration._unsafe( + {bool? help, + String? customHtmlTemplatePath, + bool? version, + bool? pauseAfterLoad, + bool? debug, + bool? color, + String? configurationPath, + String? reporter, + Map<String, String>? fileReporters, + String? coverage, + int? concurrency, + int? shardIndex, + int? totalShards, + Map<String, Set<TestSelection>>? testSelections, + Iterable<String>? foldTraceExcept, + Iterable<String>? foldTraceOnly, + Glob? filename, + Iterable<String>? chosenPresets, + Map<String, Configuration>? presets, + Map<String, RuntimeSettings>? overrideRuntimes, + Map<String, CustomRuntime>? defineRuntimes, + bool? noRetry, + int? testRandomizeOrderingSeed, + bool? stopOnFirstFailure, + + // Suite-level configuration + bool? allowDuplicateTestNames, + bool? allowTestRandomization, + bool? jsTrace, + bool? runSkipped, + Iterable<String>? dart2jsArgs, + String? precompiledPath, + Iterable<Pattern>? globalPatterns, + Iterable<CompilerSelection>? compilerSelections, + Iterable<RuntimeSelection>? runtimes, + BooleanSelector? includeTags, + BooleanSelector? excludeTags, + Map<BooleanSelector, SuiteConfiguration>? tags, + Map<PlatformSelector, SuiteConfiguration>? onPlatform, + bool? ignoreTimeouts, + + // Test-level configuration + Timeout? timeout, + bool? verboseTrace, + bool? chainStackTraces, + bool? skip, + int? retry, + String? skipReason, + PlatformSelector? testOn, + Iterable<String>? addTags}) => + Configuration( + help: help, + customHtmlTemplatePath: customHtmlTemplatePath, + version: version, + pauseAfterLoad: pauseAfterLoad, + debug: debug, + color: color, + configurationPath: configurationPath, + reporter: reporter, + fileReporters: fileReporters, + coverage: coverage, + concurrency: concurrency, + shardIndex: shardIndex, + totalShards: totalShards, + testSelections: testSelections, + foldTraceExcept: foldTraceExcept, + foldTraceOnly: foldTraceOnly, + filename: filename, + chosenPresets: chosenPresets, + presets: presets, + overrideRuntimes: overrideRuntimes, + defineRuntimes: defineRuntimes, + noRetry: noRetry, + testRandomizeOrderingSeed: testRandomizeOrderingSeed, + stopOnFirstFailure: stopOnFirstFailure, + allowDuplicateTestNames: allowDuplicateTestNames, + allowTestRandomization: allowTestRandomization, + jsTrace: jsTrace, + runSkipped: runSkipped, + dart2jsArgs: dart2jsArgs, + precompiledPath: precompiledPath, + globalPatterns: globalPatterns, + compilerSelections: compilerSelections, + runtimes: runtimes, + includeTags: includeTags, + excludeTags: excludeTags, + tags: tags, + onPlatform: onPlatform, + ignoreTimeouts: ignoreTimeouts, + timeout: timeout, + verboseTrace: verboseTrace, + chainStackTraces: chainStackTraces, + skip: skip, + retry: retry, + skipReason: skipReason, + testOn: testOn, + addTags: addTags); + + /// Suite level configuration allowed in the global test config file. + /// + /// This is per-user configuration and should be limited as such, it should + /// not contain options that would change the pass/fail result of any given + /// test, or change which tests would run. + factory Configuration.globalTest({ + required bool? verboseTrace, + required bool? jsTrace, + required Timeout? timeout, + required Map<String, Configuration>? presets, + required bool? chainStackTraces, + required Iterable<String>? foldTraceExcept, + required Iterable<String>? foldTraceOnly, + }) => + Configuration( + foldTraceExcept: foldTraceExcept, + foldTraceOnly: foldTraceOnly, + jsTrace: jsTrace, + timeout: timeout, + verboseTrace: verboseTrace, + chainStackTraces: chainStackTraces, + help: null, + customHtmlTemplatePath: null, + version: null, + pauseAfterLoad: null, + debug: null, + color: null, + configurationPath: null, + reporter: null, + fileReporters: null, + coverage: null, + concurrency: null, + shardIndex: null, + totalShards: null, + testSelections: null, + filename: null, + chosenPresets: null, + presets: presets, + overrideRuntimes: null, + defineRuntimes: null, + noRetry: null, + testRandomizeOrderingSeed: null, + stopOnFirstFailure: null, + ignoreTimeouts: null, + allowDuplicateTestNames: null, + allowTestRandomization: null, + runSkipped: null, + dart2jsArgs: null, + precompiledPath: null, + globalPatterns: null, + compilerSelections: null, + runtimes: null, + includeTags: null, + excludeTags: null, + tags: null, + onPlatform: null, + skip: null, + retry: null, + skipReason: null, + testOn: null, + addTags: null, + ); + + /// Suite level configuration that is not allowed in the global test + /// config file. + /// + /// This configuration may alter the pass/fail result of a test run, and thus + /// should only be configured per package and not at the global level (global + /// config is user specific). + factory Configuration.localTest({ + required bool? skip, + required int? retry, + required String? skipReason, + required PlatformSelector? testOn, + required Iterable<String>? addTags, + required bool? allowDuplicateTestNames, + required bool? allowTestRandomization, + }) => + Configuration( + allowDuplicateTestNames: allowDuplicateTestNames, + allowTestRandomization: allowTestRandomization, + skip: skip, + retry: retry, + skipReason: skipReason, + testOn: testOn, + addTags: addTags, + help: null, + customHtmlTemplatePath: null, + version: null, + pauseAfterLoad: null, + debug: null, + color: null, + configurationPath: null, + reporter: null, + fileReporters: null, + coverage: null, + concurrency: null, + shardIndex: null, + totalShards: null, + testSelections: null, + foldTraceExcept: null, + foldTraceOnly: null, + filename: null, + chosenPresets: null, + presets: null, + overrideRuntimes: null, + defineRuntimes: null, + noRetry: null, + testRandomizeOrderingSeed: null, + stopOnFirstFailure: null, + jsTrace: null, + runSkipped: null, + dart2jsArgs: null, + precompiledPath: null, + globalPatterns: null, + compilerSelections: null, + runtimes: null, + includeTags: null, + excludeTags: null, + tags: null, + onPlatform: null, + ignoreTimeouts: null, + timeout: null, + verboseTrace: null, + chainStackTraces: null, + ); + + /// Runner configuration that is allowed in the global test config file. + /// + /// This is per-user configuration and should be limited as such, it should + /// not contain options that would change the pass/fail result of any given + /// test, or change which tests would run. + /// + /// Note that [customHtmlTemplatePath] violates this rule, and really should + /// not be configurable globally. + factory Configuration.globalRunner( + {required bool? pauseAfterLoad, + required String? customHtmlTemplatePath, + required bool? runSkipped, + required String? reporter, + required Map<String, String>? fileReporters, + required int? concurrency, + required Iterable<CompilerSelection>? compilerSelections, + required Iterable<RuntimeSelection>? runtimes, + required Iterable<String>? chosenPresets, + required Map<String, RuntimeSettings>? overrideRuntimes}) => + Configuration( + customHtmlTemplatePath: customHtmlTemplatePath, + pauseAfterLoad: pauseAfterLoad, + runSkipped: runSkipped, + reporter: reporter, + fileReporters: fileReporters, + concurrency: concurrency, + compilerSelections: compilerSelections, + runtimes: runtimes, + chosenPresets: chosenPresets, + overrideRuntimes: overrideRuntimes, + help: null, + version: null, + debug: null, + color: null, + configurationPath: null, + coverage: null, + shardIndex: null, + totalShards: null, + testSelections: null, + foldTraceExcept: null, + foldTraceOnly: null, + filename: null, + presets: null, + defineRuntimes: null, + noRetry: null, + testRandomizeOrderingSeed: null, + stopOnFirstFailure: null, + allowDuplicateTestNames: null, + allowTestRandomization: null, + jsTrace: null, + dart2jsArgs: null, + precompiledPath: null, + globalPatterns: null, + includeTags: null, + excludeTags: null, + tags: null, + onPlatform: null, + ignoreTimeouts: null, + timeout: null, + verboseTrace: null, + chainStackTraces: null, + skip: null, + retry: null, + skipReason: null, + testOn: null, + addTags: null, + ); + + /// Runner configuration that is not allowed in the global test config file. + /// + /// This configuration may alter the pass/fail result of a test run, and thus + /// should only be configured per package and not at the global level (global + /// config is user specific). + factory Configuration.localRunner( + {required Iterable<Pattern>? globalPatterns, + required Map<String, Set<TestSelection>>? testSelections, + required Glob? filename, + required BooleanSelector? includeTags, + required BooleanSelector? excludeTags, + required Map<String, CustomRuntime>? defineRuntimes}) => + Configuration( + globalPatterns: globalPatterns, + testSelections: testSelections, + filename: filename, + includeTags: includeTags, + excludeTags: excludeTags, + defineRuntimes: defineRuntimes, + help: null, + customHtmlTemplatePath: null, + version: null, + pauseAfterLoad: null, + debug: null, + color: null, + configurationPath: null, + reporter: null, + fileReporters: null, + coverage: null, + concurrency: null, + shardIndex: null, + totalShards: null, + foldTraceExcept: null, + foldTraceOnly: null, + chosenPresets: null, + presets: null, + overrideRuntimes: null, + noRetry: null, + testRandomizeOrderingSeed: null, + stopOnFirstFailure: null, + allowDuplicateTestNames: null, + allowTestRandomization: null, + jsTrace: null, + runSkipped: null, + dart2jsArgs: null, + precompiledPath: null, + compilerSelections: null, + runtimes: null, + tags: null, + onPlatform: null, + ignoreTimeouts: null, + timeout: null, + verboseTrace: null, + chainStackTraces: null, + skip: null, + retry: null, + skipReason: null, + testOn: null, + addTags: null); + + /// A specialized constructor for configuring only `onPlatform`. + factory Configuration.onPlatform( + Map<PlatformSelector, SuiteConfiguration> onPlatform) => + Configuration._unsafe(onPlatform: onPlatform); + + factory Configuration.tags(Map<BooleanSelector, SuiteConfiguration> tags) => + Configuration._unsafe(tags: tags); + + static Map<String, Configuration>? _withChosenPresets( + Map<String, Configuration>? map, Set<String>? chosenPresets) { + if (map == null || chosenPresets == null) return map; + return map.map((key, config) => MapEntry( + key, + config.change( + chosenPresets: config.chosenPresets.union(chosenPresets)))); + } + + /// Creates new Configuration. + /// + /// Unlike [Configuration.new], this assumes [presets] is already resolved. + Configuration._( + {required bool? help, + required this.customHtmlTemplatePath, + required bool? version, + required bool? pauseAfterLoad, + required bool? debug, + required bool? color, + required String? configurationPath, + required String? reporter, + required Map<String, String>? fileReporters, + required this.coverage, + required int? concurrency, + required this.shardIndex, + required this.totalShards, + required Map<String, Set<TestSelection>>? testSelections, + required Iterable<String>? foldTraceExcept, + required Iterable<String>? foldTraceOnly, + required Glob? filename, + required Iterable<String>? chosenPresets, + required Map<String, Configuration>? presets, + required Map<String, RuntimeSettings>? overrideRuntimes, + required Map<String, CustomRuntime>? defineRuntimes, + required bool? noRetry, + required this.testRandomizeOrderingSeed, + required bool? stopOnFirstFailure, + required BooleanSelector? includeTags, + required BooleanSelector? excludeTags, + required Iterable<Pattern>? globalPatterns, + required SuiteConfiguration? suiteDefaults}) + : _help = help, + _version = version, + _pauseAfterLoad = pauseAfterLoad, + _debug = debug, + _color = color, + _configurationPath = configurationPath, + _reporter = reporter, + fileReporters = fileReporters ?? {}, + _concurrency = concurrency, + _testSelections = testSelections == null || testSelections.isEmpty + ? null + : Map.unmodifiable(testSelections), + _foldTraceExcept = _set(foldTraceExcept), + _foldTraceOnly = _set(foldTraceOnly), + _filename = filename, + chosenPresets = UnmodifiableSetView(chosenPresets?.toSet() ?? {}), + presets = _map(presets), + overrideRuntimes = _map(overrideRuntimes), + defineRuntimes = _map(defineRuntimes), + _noRetry = noRetry, + includeTags = includeTags ?? BooleanSelector.all, + excludeTags = excludeTags ?? BooleanSelector.none, + globalPatterns = globalPatterns == null + ? const {} + : UnmodifiableSetView(globalPatterns.toSet()), + _stopOnFirstFailure = stopOnFirstFailure, + suiteDefaults = (() { + var config = suiteDefaults ?? SuiteConfiguration.empty; + if (pauseAfterLoad == true) { + return config.change(ignoreTimeouts: true); + } + return config; + }()) { + if (_filename != null && _filename.context.style != p.style) { + throw ArgumentError( + "filename's context must match the current operating system, was " + '${_filename.context.style}.'); + } + + if ((shardIndex == null) != (totalShards == null)) { + throw ArgumentError( + 'shardIndex and totalShards may only be passed together.'); + } else if (shardIndex != null) { + RangeError.checkValueInInterval( + shardIndex!, 0, totalShards! - 1, 'shardIndex'); + } + } + + /// Creates a new [Configuration] that takes its configuration from + /// [SuiteConfiguration]. + factory Configuration.fromSuiteConfiguration( + SuiteConfiguration suiteConfig) => + Configuration._( + suiteDefaults: suiteConfig, + globalPatterns: null, + help: null, + customHtmlTemplatePath: null, + version: null, + pauseAfterLoad: null, + debug: null, + color: null, + configurationPath: null, + reporter: null, + fileReporters: null, + coverage: null, + concurrency: null, + shardIndex: null, + totalShards: null, + testSelections: null, + foldTraceExcept: null, + foldTraceOnly: null, + filename: null, + chosenPresets: null, + presets: null, + overrideRuntimes: null, + defineRuntimes: null, + noRetry: null, + testRandomizeOrderingSeed: null, + stopOnFirstFailure: null, + includeTags: null, + excludeTags: null, + ); + + /// Returns a set from [input]. + /// + /// If [input] is `null` or empty, this returns `null`. + static Set<T>? _set<T>(Iterable<T>? input) { + if (input == null) return null; + var set = Set<T>.from(input); + if (set.isEmpty) return null; + return set; + } + + /// Returns an unmodifiable copy of [input] or an empty unmodifiable map. + static Map<K, V> _map<K, V>(Map<K, V>? input) { + input ??= {}; + return Map.unmodifiable(input); + } + + /// Runs [body] with this as [Configuration.current]. + /// + /// This is zone-scoped, so this will be the current configuration in any + /// asynchronous callbacks transitively created by [body]. + T asCurrent<T>(T Function() body) => + runZoned(body, zoneValues: {_currentKey: this}); + + /// Throws a [FormatException] if this refers to any undefined runtimes. + void validateRuntimes(List<Runtime> allRuntimes) { + // We don't need to verify [customRuntimes] here because those runtimes + // already need to be verified and resolved to create [allRuntimes]. + + for (var settings in overrideRuntimes.values) { + if (!allRuntimes + .any((runtime) => runtime.identifier == settings.identifier)) { + throw SourceSpanFormatException( + 'Unknown platform "${settings.identifier}".', + settings.identifierSpan); + } + } + + suiteDefaults.validateRuntimes(allRuntimes); + for (var config in presets.values) { + config.validateRuntimes(allRuntimes); + } + } + + /// Merges this with [other]. + /// + /// For most fields, if both configurations have values set, [other]'s value + /// takes precedence. However, certain fields are merged together instead. + /// This is indicated in those fields' documentation. + Configuration merge(Configuration other) { + if (this == Configuration.empty) return other; + if (other == Configuration.empty) return this; + + var foldTraceOnly = other._foldTraceOnly ?? _foldTraceOnly; + var foldTraceExcept = other._foldTraceExcept ?? _foldTraceExcept; + if (_foldTraceOnly != null) { + if (other._foldTraceExcept != null) { + foldTraceOnly = _foldTraceOnly.difference(other._foldTraceExcept); + } else if (other._foldTraceOnly != null) { + foldTraceOnly = other._foldTraceOnly.intersection(_foldTraceOnly); + } + } else if (_foldTraceExcept != null) { + if (other._foldTraceOnly != null) { + foldTraceOnly = other._foldTraceOnly.difference(_foldTraceExcept); + } else if (other._foldTraceExcept != null) { + foldTraceExcept = other._foldTraceExcept.union(_foldTraceExcept); + } + } + + var result = Configuration._( + help: other._help ?? _help, + customHtmlTemplatePath: + other.customHtmlTemplatePath ?? customHtmlTemplatePath, + version: other._version ?? _version, + pauseAfterLoad: other._pauseAfterLoad ?? _pauseAfterLoad, + debug: other._debug ?? _debug, + color: other._color ?? _color, + configurationPath: other._configurationPath ?? _configurationPath, + reporter: other._reporter ?? _reporter, + fileReporters: mergeMaps(fileReporters, other.fileReporters), + coverage: other.coverage ?? coverage, + concurrency: other._concurrency ?? _concurrency, + shardIndex: other.shardIndex ?? shardIndex, + totalShards: other.totalShards ?? totalShards, + testSelections: other._testSelections ?? _testSelections, + foldTraceExcept: foldTraceExcept, + foldTraceOnly: foldTraceOnly, + filename: other._filename ?? _filename, + chosenPresets: chosenPresets.union(other.chosenPresets), + presets: _mergeConfigMaps(presets, other.presets), + overrideRuntimes: mergeUnmodifiableMaps( + overrideRuntimes, other.overrideRuntimes, + value: (settings1, settings2) => RuntimeSettings( + settings1.identifier, + settings1.identifierSpan, + [...settings1.settings, ...settings2.settings])), + defineRuntimes: + mergeUnmodifiableMaps(defineRuntimes, other.defineRuntimes), + noRetry: other._noRetry ?? _noRetry, + testRandomizeOrderingSeed: + other.testRandomizeOrderingSeed ?? testRandomizeOrderingSeed, + stopOnFirstFailure: other._stopOnFirstFailure ?? _stopOnFirstFailure, + includeTags: includeTags.intersection(other.includeTags), + excludeTags: excludeTags.union(other.excludeTags), + globalPatterns: globalPatterns.union(other.globalPatterns), + suiteDefaults: suiteDefaults.merge(other.suiteDefaults)); + result = result._resolvePresets(); + + // Make sure the merged config preserves any presets that were chosen and + // discarded. + result._knownPresets = knownPresets.union(other.knownPresets); + return result; + } + + /// Returns a copy of this configuration with the given fields updated. + /// + /// Note that unlike [merge], this has no merging behavior—the old value is + /// always replaced by the new one. + Configuration change( + {bool? help, + String? customHtmlTemplatePath, + bool? version, + bool? pauseAfterLoad, + bool? debug, + bool? color, + String? configurationPath, + String? reporter, + Map<String, String>? fileReporters, + String? coverage, + int? concurrency, + int? shardIndex, + int? totalShards, + Map<String, Set<TestSelection>>? testSelections, + Iterable<String>? exceptPackages, + Iterable<String>? onlyPackages, + Glob? filename, + Iterable<String>? chosenPresets, + Map<String, Configuration>? presets, + Map<String, RuntimeSettings>? overrideRuntimes, + Map<String, CustomRuntime>? defineRuntimes, + bool? noRetry, + int? testRandomizeOrderingSeed, + bool? ignoreTimeouts, + + // Suite-level configuration + bool? allowDuplicateTestNames, + bool? jsTrace, + bool? runSkipped, + Iterable<String>? dart2jsArgs, + String? precompiledPath, + Iterable<RuntimeSelection>? runtimes, + BooleanSelector? includeTags, + BooleanSelector? excludeTags, + Map<BooleanSelector, SuiteConfiguration>? tags, + Map<PlatformSelector, SuiteConfiguration>? onPlatform, + + // Test-level configuration + Timeout? timeout, + bool? verboseTrace, + bool? chainStackTraces, + bool? skip, + String? skipReason, + PlatformSelector? testOn, + Iterable<String>? addTags}) { + var config = Configuration._( + help: help ?? _help, + customHtmlTemplatePath: + customHtmlTemplatePath ?? this.customHtmlTemplatePath, + version: version ?? _version, + pauseAfterLoad: pauseAfterLoad ?? _pauseAfterLoad, + debug: debug ?? _debug, + color: color ?? _color, + configurationPath: configurationPath ?? _configurationPath, + reporter: reporter ?? _reporter, + fileReporters: fileReporters ?? this.fileReporters, + coverage: coverage ?? this.coverage, + concurrency: concurrency ?? _concurrency, + shardIndex: shardIndex ?? this.shardIndex, + totalShards: totalShards ?? this.totalShards, + testSelections: testSelections ?? _testSelections, + foldTraceExcept: exceptPackages ?? _foldTraceExcept, + foldTraceOnly: onlyPackages ?? _foldTraceOnly, + filename: filename ?? _filename, + chosenPresets: chosenPresets ?? this.chosenPresets, + presets: presets ?? this.presets, + overrideRuntimes: overrideRuntimes ?? this.overrideRuntimes, + defineRuntimes: defineRuntimes ?? this.defineRuntimes, + noRetry: noRetry ?? _noRetry, + testRandomizeOrderingSeed: + testRandomizeOrderingSeed ?? this.testRandomizeOrderingSeed, + stopOnFirstFailure: _stopOnFirstFailure, + includeTags: includeTags, + excludeTags: excludeTags, + globalPatterns: globalPatterns, + suiteDefaults: suiteDefaults.change( + allowDuplicateTestNames: allowDuplicateTestNames, + jsTrace: jsTrace, + runSkipped: runSkipped, + dart2jsArgs: dart2jsArgs, + precompiledPath: precompiledPath, + runtimes: runtimes, + tags: tags, + onPlatform: onPlatform, + timeout: timeout, + verboseTrace: verboseTrace, + chainStackTraces: chainStackTraces, + skip: skip, + skipReason: skipReason, + testOn: testOn, + addTags: addTags, + ignoreTimeouts: ignoreTimeouts, + )); + return config._resolvePresets(); + } + + /// Merges two maps whose values are [Configuration]s. + /// + /// Any overlapping keys in the maps have their configurations merged in the + /// returned map. + Map<String, Configuration> _mergeConfigMaps( + Map<String, Configuration> map1, Map<String, Configuration> map2) => + mergeMaps(map1, map2, + value: (config1, config2) => config1.merge(config2)); + + /// Returns a copy of this [Configuration] with all [chosenPresets] resolved + /// against [presets]. + Configuration _resolvePresets() { + if (chosenPresets.isEmpty || presets.isEmpty) return this; + + var newPresets = Map<String, Configuration>.from(presets); + var merged = chosenPresets.fold( + empty, + (Configuration merged, preset) => + merged.merge(newPresets.remove(preset) ?? Configuration.empty)); + + if (merged == empty) return this; + var result = change(presets: newPresets).merge(merged); + + // Make sure the configuration knows about presets that were selected and + // thus removed from [newPresets]. + result._knownPresets = + UnmodifiableSetView({...result.knownPresets, ...presets.keys}); + + return result; + } +}
diff --git a/pkgs/test_core/lib/src/runner/configuration/args.dart b/pkgs/test_core/lib/src/runner/configuration/args.dart new file mode 100644 index 0000000..e981bed --- /dev/null +++ b/pkgs/test_core/lib/src/runner/configuration/args.dart
@@ -0,0 +1,426 @@ +// Copyright (c) 2016, 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 'dart:math'; + +import 'package:args/args.dart'; +import 'package:boolean_selector/boolean_selector.dart'; +import 'package:test_api/backend.dart'; +import 'package:test_api/scaffolding.dart' show Timeout; + +import '../../util/io.dart'; +import '../compiler_selection.dart'; +import '../configuration.dart'; +import '../runtime_selection.dart'; +import 'reporters.dart'; +import 'values.dart'; + +/// The parser used to parse the command-line arguments. +final ArgParser _parser = (() { + var parser = ArgParser(allowTrailingOptions: true); + + var allRuntimes = Runtime.builtIn.toList()..remove(Runtime.vm); + if (!Platform.isMacOS) allRuntimes.remove(Runtime.safari); + + parser.addFlag('help', + abbr: 'h', negatable: false, help: 'Show this usage information.'); + parser.addFlag('version', + negatable: false, help: 'Show the package:test version.'); + + // Note that defaultsTo declarations here are only for documentation purposes. + // We pass null instead of the default so that it merges properly with the + // config file. + + parser.addSeparator('Selecting Tests:'); + parser.addMultiOption('name', + abbr: 'n', + help: 'A substring of the name of the test to run.\n' + 'Regular expression syntax is supported.\n' + 'If passed multiple times, tests must match all substrings.', + splitCommas: false); + parser.addMultiOption('plain-name', + abbr: 'N', + help: 'A plain-text substring of the name of the test to run.\n' + 'If passed multiple times, tests must match all substrings.', + splitCommas: false); + parser.addMultiOption('tags', + abbr: 't', + help: 'Run only tests with all of the specified tags.\n' + 'Supports boolean selector syntax.'); + parser.addMultiOption('tag', hide: true); + parser.addMultiOption('exclude-tags', + abbr: 'x', + help: "Don't run tests with any of the specified tags.\n" + 'Supports boolean selector syntax.'); + parser.addMultiOption('exclude-tag', hide: true); + parser.addFlag('run-skipped', + help: 'Run skipped tests instead of skipping them.'); + + parser.addSeparator('Running Tests:'); + + // The UI term "platform" corresponds with the implementation term "runtime". + // The [Runtime] class used to be called [TestPlatform], but it was changed to + // avoid conflicting with [SuitePlatform]. We decided not to also change the + // UI to avoid a painful migration. + parser.addMultiOption('platform', + abbr: 'p', + help: 'The platform(s) on which to run the tests.\n' + '[vm (default), ' + '${allRuntimes.map((runtime) => runtime.identifier).join(", ")}].\n' + 'Each platform supports the following compilers:\n' + '${Runtime.vm.supportedCompilersText}\n' + '${allRuntimes.map((r) => r.supportedCompilersText).join('\n')}'); + parser.addMultiOption('compiler', + abbr: 'c', + help: 'The compiler(s) to use to run tests, supported compilers are ' + '[${Compiler.builtIn.map((c) => c.identifier).join(', ')}].\n' + 'Each platform has a default compiler but may support other ' + 'compilers.\n' + 'You can target a compiler to a specific platform using arguments ' + 'of the following form [<platform-selector>:]<compiler>.\n' + 'If a platform is specified but no given compiler is supported for ' + 'that platform, then it will use its default compiler.'); + parser.addMultiOption('preset', + abbr: 'P', help: 'The configuration preset(s) to use.'); + parser.addOption('concurrency', + abbr: 'j', + help: 'The number of concurrent test suites run.', + defaultsTo: defaultConcurrency.toString(), + valueHelp: 'threads'); + parser.addOption('total-shards', + help: 'The total number of invocations of the test runner being run.'); + parser.addOption('shard-index', + help: 'The index of this test runner invocation (of --total-shards).'); + parser.addOption('pub-serve', + help: '[Removed] The port of a pub serve instance serving "test/".', + valueHelp: 'port', + hide: true); + parser.addOption('timeout', + help: 'The default test timeout. For example: 15s, 2x, none', + defaultsTo: '30s'); + parser.addFlag('ignore-timeouts', + help: 'Ignore all timeouts (useful if debugging)', negatable: false); + parser.addFlag('pause-after-load', + help: 'Pause for debugging before any tests execute.\n' + 'Implies --concurrency=1, --debug, and --ignore-timeouts.\n' + 'Currently only supported for browser tests.', + negatable: false); + parser.addFlag('debug', + help: 'Run the VM and Chrome tests in debug mode.', negatable: false); + parser.addOption('coverage', + help: 'Gather coverage and output it to the specified directory.\n' + 'Implies --debug.', + valueHelp: 'directory'); + parser.addFlag('chain-stack-traces', + help: 'Use chained stack traces to provide greater exception details\n' + 'especially for asynchronous code. It may be useful to disable\n' + 'to provide improved test performance but at the cost of\n' + 'debuggability.', + defaultsTo: false); + parser.addFlag('no-retry', + help: "Don't rerun tests that have retry set.", + defaultsTo: false, + negatable: false); + parser.addFlag('use-data-isolate-strategy', + help: '**DEPRECATED**: This is now just an alias for --compiler source.', + defaultsTo: false, + hide: true, + negatable: false); + parser.addOption('test-randomize-ordering-seed', + help: 'Use the specified seed to randomize the execution order of test' + ' cases.\n' + 'Must be a 32bit unsigned integer or "random".\n' + 'If "random", pick a random seed to use.\n' + 'If not passed, do not randomize test case execution order.'); + parser.addFlag('fail-fast', + help: 'Stop running tests after the first failure.\n'); + + var reporterDescriptions = <String, String>{ + for (final MapEntry(:key, :value) in allReporters.entries) + key: value.description + }; + + parser.addSeparator('Output:'); + parser.addOption('reporter', + abbr: 'r', + help: 'Set how to print test results.', + defaultsTo: defaultReporter, + allowed: allReporters.keys, + allowedHelp: reporterDescriptions, + valueHelp: 'option'); + parser.addOption('file-reporter', + help: 'Enable an additional reporter writing test results to a file.\n' + 'Should be in the form <reporter>:<filepath>, ' + 'Example: "json:reports/tests.json"'); + parser.addFlag('verbose-trace', + negatable: false, help: 'Emit stack traces with core library frames.'); + parser.addFlag('js-trace', + negatable: false, + help: 'Emit raw JavaScript stack traces for browser tests.'); + parser.addFlag('color', + help: 'Use terminal colors.\n(auto-detected by default)'); + + /// The following options are used only by the internal Google test runner. + /// They're hidden and not supported as stable API surface outside Google. + + parser.addOption('configuration', + help: 'The path to the configuration file.', hide: true); + parser.addMultiOption('dart2js-args', + help: 'Extra arguments to pass to dart2js.', hide: true); + + // If we're running test/dir/my_test.dart, we'll look for + // test/dir/my_test.dart.html in the precompiled directory. + parser.addOption('precompiled', + help: 'The path to a mirror of the package directory containing HTML ' + 'that points to precompiled JS.', + hide: true); + + return parser; +})(); + +/// The usage string for the command-line arguments. +String get usage => _parser.usage; + +/// Parses the configuration from [args]. +/// +/// Throws a [FormatException] if [args] are invalid. +Configuration parse(List<String> args) => _Parser(args).parse(); + +void _parseTestSelection( + String option, Map<String, Set<TestSelection>> selections) { + if (Platform.isWindows) { + // If given a path that starts with what looks like a drive letter, convert it + // into a file scheme URI. We can't parse using `Uri.file` because we do + // support query parameters which aren't valid file uris. + if (option.indexOf(':') == 1) { + option = 'file:///$option'; + } + } + final uri = Uri.parse(option); + final path = Uri.decodeComponent(uri.path).stripDriveLetterLeadingSlash; + final names = uri.queryParametersAll['name']; + final fullName = uri.queryParameters['full-name']; + final line = uri.queryParameters['line']; + final col = uri.queryParameters['col']; + + if (names != null && names.isNotEmpty && fullName != null) { + throw const FormatException( + 'Cannot specify both "name=<...>" and "full-name=<...>".', + ); + } + final selection = TestSelection( + testPatterns: fullName != null + ? {RegExp('^${RegExp.escape(fullName)}\$')} + : { + if (names != null) + for (var name in names) RegExp(name) + }, + line: line == null ? null : int.parse(line), + col: col == null ? null : int.parse(col), + ); + + selections.update(path, (selections) => selections..add(selection), + ifAbsent: () => {selection}); +} + +/// A class for parsing an argument list. +/// +/// This is used to provide access to the arg results across helper methods. +class _Parser { + /// The parsed options. + final ArgResults _options; + + _Parser(List<String> args) : _options = _parser.parse(args); + + List<String> _readMulti(String name) => _options[name] as List<String>; + + /// Returns the parsed configuration. + Configuration parse() { + var patterns = [ + for (var value in _readMulti('name')) + _wrapFormatException(value, () => RegExp(value), optionName: 'name'), + ..._readMulti('plain-name'), + ]; + + var includeTags = {..._readMulti('tags'), ..._readMulti('tag')} + .fold<BooleanSelector>(BooleanSelector.all, (selector, tag) { + return selector.intersection(BooleanSelector.parse(tag)); + }); + + var excludeTags = { + ..._readMulti('exclude-tags'), + ..._readMulti('exclude-tag') + }.fold<BooleanSelector>(BooleanSelector.none, (selector, tag) { + return selector.union(BooleanSelector.parse(tag)); + }); + + var shardIndex = _parseOption('shard-index', int.parse); + var totalShards = _parseOption('total-shards', int.parse); + if ((shardIndex == null) != (totalShards == null)) { + throw const FormatException( + '--shard-index and --total-shards may only be passed together.'); + } else if (shardIndex != null) { + if (shardIndex < 0) { + throw const FormatException('--shard-index may not be negative.'); + } else if (shardIndex >= totalShards!) { + throw const FormatException( + '--shard-index must be less than --total-shards.'); + } + } + + var reporter = _ifParsed('reporter') as String?; + + var testRandomizeOrderingSeed = + _parseOption('test-randomize-ordering-seed', (value) { + var seed = value == 'random' + ? Random().nextInt(4294967295) + : int.parse(value).toUnsigned(32); + + // TODO(#1547): Less hacky way of not breaking the json reporter + if (reporter != 'json') { + print('Shuffling test order with --test-randomize-ordering-seed=$seed'); + } + + return seed; + }); + + var color = _ifParsed<bool>('color') ?? canUseSpecialChars; + + var runtimes = + _ifParsed<List<String>>('platform')?.map(RuntimeSelection.new).toList(); + var compilerSelections = _ifParsed<List<String>>('compiler') + ?.map(CompilerSelection.parse) + .toList(); + if (_ifParsed<bool>('use-data-isolate-strategy') == true) { + compilerSelections ??= []; + compilerSelections.add(CompilerSelection.parse('vm:source')); + } + + final paths = _options.rest.isEmpty ? null : _options.rest; + + Map<String, Set<TestSelection>>? selections; + if (paths != null) { + selections = {}; + for (final path in paths) { + _parseTestSelection(path, selections); + } + } + + if (_options.wasParsed('pub-serve')) { + throw ArgumentError( + 'The --pub-serve is no longer supported, if you require it please ' + 'open an issue at https://github.com/dart-lang/test/issues/new.'); + } + + return Configuration( + help: _ifParsed('help'), + version: _ifParsed('version'), + verboseTrace: _ifParsed('verbose-trace'), + chainStackTraces: _ifParsed('chain-stack-traces'), + jsTrace: _ifParsed('js-trace'), + pauseAfterLoad: _ifParsed('pause-after-load'), + debug: _ifParsed('debug'), + color: color, + configurationPath: _ifParsed('configuration'), + dart2jsArgs: _ifParsed('dart2js-args'), + precompiledPath: _ifParsed<String>('precompiled'), + reporter: reporter, + fileReporters: _parseFileReporterOption(), + coverage: _ifParsed('coverage'), + concurrency: _parseOption('concurrency', int.parse), + shardIndex: shardIndex, + totalShards: totalShards, + timeout: _parseOption('timeout', Timeout.parse), + globalPatterns: patterns, + compilerSelections: compilerSelections, + runtimes: runtimes, + runSkipped: _ifParsed('run-skipped'), + chosenPresets: _ifParsed('preset'), + testSelections: selections, + includeTags: includeTags, + excludeTags: excludeTags, + noRetry: _ifParsed('no-retry'), + testRandomizeOrderingSeed: testRandomizeOrderingSeed, + ignoreTimeouts: _ifParsed('ignore-timeouts'), + stopOnFirstFailure: _ifParsed('fail-fast'), + // Config that isn't supported on the command line + addTags: null, + allowTestRandomization: null, + allowDuplicateTestNames: null, + customHtmlTemplatePath: null, + defineRuntimes: null, + filename: null, + foldTraceExcept: null, + foldTraceOnly: null, + onPlatform: null, + overrideRuntimes: null, + presets: null, + retry: null, + skip: null, + skipReason: null, + testOn: null, + tags: null); + } + + /// Returns the parsed option for [name], or `null` if none was parsed. + /// + /// If the user hasn't explicitly chosen a value, we want to pass null values + /// to [Configuration.new] so that it considers those fields unset when + /// merging with configuration from the config file. + T? _ifParsed<T>(String name) => + _options.wasParsed(name) ? _options[name] as T : null; + + /// Runs [parse] on the value of the option [name], and wraps any + /// [FormatException] it throws with additional information. + T? _parseOption<T>(String name, T Function(String) parse) { + if (!_options.wasParsed(name)) return null; + + var value = _options[name]; + if (value == null) return null; + + return _wrapFormatException(value, () => parse(value as String), + optionName: name); + } + + Map<String, String>? _parseFileReporterOption() => + _parseOption('file-reporter', (value) { + if (!value.contains(':')) { + throw const FormatException( + 'option must be in the form <reporter>:<filepath>, e.g. ' + '"json:reports/tests.json"'); + } + final sep = value.indexOf(':'); + final reporter = value.substring(0, sep); + if (!allReporters.containsKey(reporter)) { + throw FormatException('"$reporter" is not a supported reporter'); + } + return {reporter: value.substring(sep + 1)}; + }); + + /// Runs [parse], and wraps any [FormatException] it throws with additional + /// information. + T _wrapFormatException<T>(Object? value, T Function() parse, + {String? optionName}) { + try { + return parse(); + } on FormatException catch (error) { + throw FormatException( + 'Couldn\'t parse ${optionName == null ? '' : '--$optionName '}"$value": ' + '${error.message}'); + } + } +} + +extension _RuntimeDescription on Runtime { + String get supportedCompilersText { + var message = StringBuffer('[$identifier]: '); + message.write('${defaultCompiler.identifier} (default)'); + for (var compiler in supportedCompilers) { + if (compiler == defaultCompiler) continue; + message.write(', ${compiler.identifier}'); + } + return message.toString(); + } +}
diff --git a/pkgs/test_core/lib/src/runner/configuration/custom_runtime.dart b/pkgs/test_core/lib/src/runner/configuration/custom_runtime.dart new file mode 100644 index 0000000..d1bd028 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/configuration/custom_runtime.dart
@@ -0,0 +1,34 @@ +// 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:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +/// A user-defined test runtime, based on an existing runtime but with +/// different configuration. +final class CustomRuntime { + /// The human-friendly name of the runtime. + final String name; + + /// The location that [name] was defined in the configuration file. + final SourceSpan nameSpan; + + /// The identifier used to look up the runtime. + final String identifier; + + /// The location that [identifier] was defined in the configuration file. + final SourceSpan identifierSpan; + + /// The identifier of the runtime that this extends. + final String parent; + + /// The location that [parent] was defined in the configuration file. + final SourceSpan parentSpan; + + /// The user's settings for this runtime. + final YamlMap settings; + + CustomRuntime(this.name, this.nameSpan, this.identifier, this.identifierSpan, + this.parent, this.parentSpan, this.settings); +}
diff --git a/pkgs/test_core/lib/src/runner/configuration/load.dart b/pkgs/test_core/lib/src/runner/configuration/load.dart new file mode 100644 index 0000000..9877db6 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/configuration/load.dart
@@ -0,0 +1,656 @@ +// Copyright (c) 2016, 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:boolean_selector/boolean_selector.dart'; +import 'package:glob/glob.dart'; +import 'package:path/path.dart' as p; +import 'package:source_span/source_span.dart'; +import 'package:test_api/scaffolding.dart' show Timeout; +import 'package:test_api/src/backend/operating_system.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/platform_selector.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/util/identifier_regex.dart'; // ignore: implementation_imports +import 'package:yaml/yaml.dart'; + +import '../../util/errors.dart'; +import '../../util/io.dart'; +import '../../util/pretty_print.dart'; +import '../compiler_selection.dart'; +import '../configuration.dart'; +import '../runtime_selection.dart'; +import '../suite.dart'; +import 'custom_runtime.dart'; +import 'reporters.dart'; +import 'runtime_settings.dart'; + +/// A regular expression matching a Dart identifier. +/// +/// This also matches a package name, since they must be Dart identifiers. +final _identifierRegExp = RegExp(r'[a-zA-Z_]\w*'); + +/// A regular expression matching allowed package names. +/// +/// This allows dot-separated valid Dart identifiers. The dots are there for +/// compatibility with Google's internal Dart packages, but they may not be used +/// when publishing a package to pub.dev. +final _packageName = + RegExp('^${_identifierRegExp.pattern}(\\.${_identifierRegExp.pattern})*\$'); + +/// Parses configuration from YAML formatted [content]. +/// +/// If [global] is `true`, this restricts the configuration file to only rules +/// that are supported globally. +/// +/// If [sourceUrl] is provided then that will be set as the source url for +/// the yaml document. +/// +/// Throws a [FormatException] if the configuration is invalid. +Configuration parse(String content, {Uri? sourceUrl, bool global = false}) { + var document = loadYamlNode(content, sourceUrl: sourceUrl); + + if (document.value == null) return Configuration.empty; + + if (document is! Map) { + throw SourceSpanFormatException( + 'The configuration must be a YAML map.', document.span, content); + } + + var loader = + _ConfigurationLoader(document as YamlMap, content, global: global); + return loader.load(); +} + +/// A helper for [load] that tracks the YAML document. +class _ConfigurationLoader { + /// The parsed configuration document. + final YamlMap _document; + + /// The source string for [_document]. + /// + /// Used for error reporting. + final String _source; + + /// Whether this is parsing the global configuration file. + final bool _global; + + /// Whether runner configuration is allowed at this level. + final bool _runnerConfig; + + _ConfigurationLoader(this._document, this._source, + {bool global = false, bool runnerConfig = true}) + : _global = global, + _runnerConfig = runnerConfig; + + /// Loads the configuration in [_document]. + Configuration load() => _loadIncludeConfig() + .merge(_loadGlobalTestConfig()) + .merge(_loadLocalTestConfig()) + .merge(_loadGlobalRunnerConfig()) + .merge(_loadLocalRunnerConfig()); + + /// If an `include` node is contained in [node], merges and returns [config]. + Configuration _loadIncludeConfig() { + if (!_runnerConfig) { + _disallow('include'); + return Configuration.empty; + } + + var includeNode = _document.nodes['include']; + if (includeNode == null) return Configuration.empty; + + var includePath = _parseNode(includeNode, 'include path', p.fromUri); + var basePath = + p.join(p.dirname(p.fromUri(_document.span.sourceUrl)), includePath); + try { + return Configuration.load(basePath); + } on FileSystemException catch (error) { + throw SourceSpanFormatException( + getErrorMessage(error), includeNode.span, _source); + } + } + + /// Loads test configuration that's allowed in the global configuration file. + Configuration _loadGlobalTestConfig() { + var verboseTrace = _getBool('verbose_trace'); + var chainStackTraces = _getBool('chain_stack_traces'); + var foldStackFrames = _loadFoldedStackFrames(); + var jsTrace = _getBool('js_trace'); + + var timeout = _parseValue('timeout', Timeout.parse); + + var onPlatform = _getMap('on_platform', + key: (keyNode) => _parseNode(keyNode, 'on_platform key', + (value) => PlatformSelector.parse(value, keyNode.span)), + value: (valueNode) => + _nestedConfig(valueNode, 'on_platform value', runnerConfig: false)); + + var onOS = _getMap('on_os', + key: (keyNode) { + _validate(keyNode, 'on_os key must be a string.', + (value) => value is String); + + var os = OperatingSystem.find(keyNode.value as String); + if (os != OperatingSystem.none) return os; + + throw SourceSpanFormatException( + 'Invalid on_os key: No such operating system.', + keyNode.span, + _source); + }, + value: (valueNode) => _nestedConfig(valueNode, 'on_os value')); + + var presets = _getMap('presets', + key: (keyNode) => _parseIdentifierLike(keyNode, 'presets key'), + value: (valueNode) => _nestedConfig(valueNode, 'presets value')); + + var config = Configuration.globalTest( + verboseTrace: verboseTrace, + jsTrace: jsTrace, + timeout: timeout, + presets: presets, + chainStackTraces: chainStackTraces, + foldTraceExcept: foldStackFrames['except'], + foldTraceOnly: foldStackFrames['only']) + .merge(_extractPresets<PlatformSelector>( + onPlatform, Configuration.onPlatform)); + + var osConfig = onOS[currentOS]; + return osConfig == null ? config : config.merge(osConfig); + } + + /// Loads test configuration that's not allowed in the global configuration + /// file. + /// + /// If [_global] is `true`, this will error if there are any local test-level + /// configuration fields. + Configuration _loadLocalTestConfig() { + if (_global) { + _disallow('skip'); + _disallow('retry'); + _disallow('test_on'); + _disallow('add_tags'); + _disallow('tags'); + _disallow('allow_test_randomization'); + _disallow('allow_duplicate_test_names'); + return Configuration.empty; + } + + var skipRaw = _getValue('skip', 'boolean or string', + (value) => (value is bool?) || value is String?); + String? skipReason; + bool? skip; + if (skipRaw is String) { + skipReason = skipRaw; + skip = true; + } else { + skip = skipRaw as bool?; + } + + var testOn = _parsePlatformSelector('test_on'); + + var addTags = _getList( + 'add_tags', (tagNode) => _parseIdentifierLike(tagNode, 'Tag name')); + + var tags = _getMap('tags', + key: (keyNode) => + _parseNode(keyNode, 'tags key', BooleanSelector.parse), + value: (valueNode) => + _nestedConfig(valueNode, 'tag value', runnerConfig: false)); + + var retry = _getNonNegativeInt('retry'); + + var allowTestRandomization = _getBool('allow_test_randomization'); + + var allowDuplicateTestNames = _getBool('allow_duplicate_test_names'); + + return Configuration.localTest( + skip: skip, + retry: retry, + skipReason: skipReason, + testOn: testOn, + addTags: addTags, + allowTestRandomization: allowTestRandomization, + allowDuplicateTestNames: allowDuplicateTestNames) + .merge(_extractPresets<BooleanSelector>(tags, Configuration.tags)); + } + + /// Loads runner configuration that's allowed in the global configuration + /// file. + /// + /// If [_runnerConfig] is `false`, this will error if there are any + /// runner-level configuration fields. + Configuration _loadGlobalRunnerConfig() { + if (!_runnerConfig) { + _disallow('pause_after_load'); + _disallow('reporter'); + _disallow('file_reporters'); + _disallow('concurrency'); + _disallow('names'); + _disallow('plain_names'); + _disallow('platforms'); + _disallow('add_presets'); + _disallow('override_platforms'); + _disallow('include'); + return Configuration.empty; + } + + var pauseAfterLoad = _getBool('pause_after_load'); + var runSkipped = _getBool('run_skipped'); + + var reporter = _getString('reporter'); + if (reporter != null && !allReporters.keys.contains(reporter)) { + _error('Unknown reporter "$reporter".', 'reporter'); + } + + var fileReporters = _getMap('file_reporters', key: (keyNode) { + _validate(keyNode, 'file_reporters key must be a string', + (value) => value is String); + final reporter = keyNode.value as String; + if (!allReporters.keys.contains(reporter)) { + _error('Unknown reporter "$reporter".', 'file_reporters'); + } + return reporter; + }, value: (valueNode) { + _validate(valueNode, 'file_reporters value must be a string', + (value) => value is String); + return valueNode.value as String; + }); + + var concurrency = _getInt('concurrency'); + + // The UI term "platform" corresponds with the implementation term + // "runtime". The [Runtime] class used to be called [TestPlatform], but it + // was changed to avoid conflicting with [SuitePlatform]. We decided not to + // also change the UI to avoid a painful migration. + var runtimes = _getList( + 'platforms', + (runtimeNode) => RuntimeSelection( + _parseIdentifierLike(runtimeNode, 'Platform name'), + runtimeNode.span)); + + var compilerSelections = _getList( + 'compilers', + (node) => _parseNode( + node, + 'compiler', + (option) => + CompilerSelection.parse(option, parentSpan: node.span))); + + var chosenPresets = _getList('add_presets', + (presetNode) => _parseIdentifierLike(presetNode, 'Preset name')); + + var overrideRuntimes = _loadOverrideRuntimes(); + + var customHtmlTemplatePath = _getString('custom_html_template_path'); + + return Configuration.globalRunner( + pauseAfterLoad: pauseAfterLoad, + customHtmlTemplatePath: customHtmlTemplatePath, + runSkipped: runSkipped, + reporter: reporter, + fileReporters: fileReporters, + concurrency: concurrency, + compilerSelections: compilerSelections, + runtimes: runtimes, + chosenPresets: chosenPresets, + overrideRuntimes: overrideRuntimes); + } + + /// Loads the `override_platforms` field. + Map<String, RuntimeSettings> _loadOverrideRuntimes() { + var runtimesNode = + _getNode('override_platforms', 'map', (value) => value is Map) + as YamlMap?; + if (runtimesNode == null) return const {}; + + var runtimes = <String, RuntimeSettings>{}; + runtimesNode.nodes.forEach((identifierNode, valueNode) { + var yamlNode = identifierNode as YamlNode; + var identifier = _parseIdentifierLike(yamlNode, 'Platform identifier'); + + _validate(valueNode, 'Platform definition must be a map.', + (value) => value is Map); + var map = valueNode as YamlMap; + + var settings = _expect(map, 'settings'); + _validate(settings, 'Must be a map.', (value) => value is Map); + + runtimes[identifier] = + RuntimeSettings(identifier, yamlNode.span, [settings as YamlMap]); + }); + return runtimes; + } + + /// Loads runner configuration that's not allowed in the global configuration + /// file. + /// + /// If [_runnerConfig] is `false` or if [_global] is `true`, this will error + /// if there are any local test-level configuration fields. + Configuration _loadLocalRunnerConfig() { + if (!_runnerConfig || _global) { + _disallow('names'); + _disallow('plain_names'); + _disallow('paths'); + _disallow('filename'); + _disallow('include_tags'); + _disallow('exclude_tags'); + _disallow('define_platforms'); + return Configuration.empty; + } + + var patterns = _getList('names', (nameNode) { + _validate(nameNode, 'Names must be strings.', (value) => value is String); + return _parseNode(nameNode, 'name', RegExp.new); + }) + ..addAll(_getList('plain_names', (nameNode) { + _validate( + nameNode, 'Names must be strings.', (value) => value is String); + return _parseNode(nameNode, 'name', RegExp.new); + })); + + var paths = _getList('paths', (pathNode) { + _validate(pathNode, 'Paths must be strings.', (value) => value is String); + _validate(pathNode, 'Paths must be relative.', + (value) => p.url.isRelative(value as String)); + return _parseNode(pathNode, 'path', p.fromUri); + }); + + var filename = _parseValue('filename', Glob.new); + + var includeTags = _parseBooleanSelector('include_tags'); + var excludeTags = _parseBooleanSelector('exclude_tags'); + + var defineRuntimes = _loadDefineRuntimes(); + + return Configuration.localRunner( + globalPatterns: patterns, + testSelections: { + for (var path in paths) path: const {TestSelection()} + }, + filename: filename, + includeTags: includeTags, + excludeTags: excludeTags, + defineRuntimes: defineRuntimes); + } + + /// Returns a map representation of the `fold_stack_frames` configuration. + /// + /// The key `except` will correspond to the list of packages to fold. + /// The key `only` will correspond to the list of packages to keep in a + /// test [Chain]. + Map<String, List<String>> _loadFoldedStackFrames() { + var foldOptionSet = false; + return _getMap('fold_stack_frames', key: (keyNode) { + _validate(keyNode, 'Must be a string', (value) => value is String); + _validate(keyNode, 'Must be "only" or "except".', + (value) => value == 'only' || value == 'except'); + + if (foldOptionSet) { + throw SourceSpanFormatException( + 'Can only contain one of "only" or "except".', + keyNode.span, + _source); + } + foldOptionSet = true; + return keyNode.value as String; + }, value: (valueNode) { + _validate( + valueNode, + 'Folded packages must be strings.', + (valueList) => + valueList is YamlList && + valueList.every((value) => value is String)); + + _validate( + valueNode, + 'Invalid package name.', + (valueList) => (valueList as Iterable) + .every((value) => _packageName.hasMatch(value as String))); + + return List<String>.from(valueNode.value as Iterable); + }); + } + + /// Loads the `define_platforms` field. + Map<String, CustomRuntime> _loadDefineRuntimes() { + var runtimesNode = + _getNode('define_platforms', 'map', (value) => value is Map) + as YamlMap?; + if (runtimesNode == null) return const {}; + + var runtimes = <String, CustomRuntime>{}; + runtimesNode.nodes.forEach((identifierNode, valueNode) { + var yamlNode = identifierNode as YamlNode; + var identifier = _parseIdentifierLike(yamlNode, 'Platform identifier'); + + _validate(valueNode, 'Platform definition must be a map.', + (value) => value is Map); + var map = valueNode as YamlMap; + + var nameNode = _expect(map, 'name'); + _validate(nameNode, 'Must be a string.', (value) => value is String); + var name = nameNode.value as String; + + var parentNode = _expect(map, 'extends'); + var parent = _parseIdentifierLike(parentNode, 'Platform parent'); + + var settings = _expect(map, 'settings'); + _validate(settings, 'Must be a map.', (value) => value is Map); + + runtimes[identifier] = CustomRuntime(name, nameNode.span, identifier, + yamlNode.span, parent, parentNode.span, settings as YamlMap); + }); + return runtimes; + } + + /// Throws an exception with [message] if [test] returns `false` when passed + /// [node]'s value. + void _validate(YamlNode node, String message, bool Function(dynamic) test) { + if (test(node.value)) return; + throw SourceSpanFormatException(message, node.span, _source); + } + + /// Returns the value of the node at [field]. + /// + /// If [typeTest] returns `false` for that value, instead throws an error + /// complaining that the field is not a [typeName]. + Object? _getValue( + String field, String typeName, bool Function(dynamic) typeTest) { + var value = _document[field]; + if (value == null || typeTest(value)) return value; + _error('$field must be ${a(typeName)}.', field); + } + + /// Returns the YAML node at [field]. + /// + /// If [typeTest] returns `false` for that node's value, instead throws an + /// error complaining that the field is not a [typeName]. + /// + /// Returns `null` if [field] does not have a node in [_document]. + YamlNode? _getNode( + String field, String typeName, bool Function(dynamic) typeTest) { + var node = _document.nodes[field]; + if (node == null) return null; + _validate(node, '$field must be ${a(typeName)}.', typeTest); + return node; + } + + /// Asserts that [field] is an int and returns its value. + int? _getInt(String field) => + _getValue(field, 'int', (value) => value is int?) as int?; + + /// Asserts that [field] is a non-negative int and returns its value. + int? _getNonNegativeInt(String field) => + _getValue(field, 'non-negative int', (value) { + if (value == null) return true; + return value is int && value >= 0; + }) as int?; + + /// Asserts that [field] is a boolean and returns its value. + bool? _getBool(String field) => + _getValue(field, 'boolean', (value) => value is bool?) as bool?; + + /// Asserts that [field] is a string and returns its value. + String? _getString(String field) => + _getValue(field, 'string', (value) => value is String?) as String?; + + /// Asserts that [field] is a list and runs [forElement] for each element it + /// contains. + /// + /// Returns a list of values returned by [forElement]. + List<T> _getList<T>(String field, T Function(YamlNode) forElement) { + var node = _getNode(field, 'list', (value) => value is List) as YamlList?; + if (node == null) return []; + return node.nodes.map(forElement).toList(); + } + + /// Asserts that [field] is a map and runs [key] and [value] for each pair. + /// + /// Returns a map with the keys and values returned by [key] and [value]. Each + /// of these defaults to asserting that the value is a string. + Map<K, V> _getMap<K, V>(String field, + {K Function(YamlNode)? key, V Function(YamlNode)? value}) { + var node = _getNode(field, 'map', (value) => value is Map) as YamlMap?; + if (node == null) return {}; + + key ??= (keyNode) { + _validate( + keyNode, '$field keys must be strings.', (value) => value is String); + + return keyNode.value as K; + }; + + value ??= (valueNode) { + _validate(valueNode, '$field values must be strings.', + (value) => value is String); + + return valueNode.value as V; + }; + + return node.nodes.map((keyNode, valueNode) => + MapEntry(key!(keyNode as YamlNode), value!(valueNode))); + } + + /// Verifies that [node]'s value is an optionally hyphenated Dart identifier, + /// and returns it + String _parseIdentifierLike(YamlNode node, String name) { + _validate(node, '$name must be a string.', (value) => value is String); + _validate(node, '$name must be an (optionally hyphenated) Dart identifier.', + (value) => (value as String).contains(anchoredHyphenatedIdentifier)); + return node.value as String; + } + + /// Parses [node]'s value as a boolean selector. + BooleanSelector? _parseBooleanSelector(String name) => + _parseValue(name, BooleanSelector.parse); + + /// Parses [node]'s value as a platform selector. + PlatformSelector? _parsePlatformSelector(String field) { + var node = _document.nodes[field]; + if (node == null) return null; + return _parseNode( + node, field, (value) => PlatformSelector.parse(value, node.span)); + } + + /// Asserts that [node] is a string, passes its value to [parse], and returns + /// the result. + /// + /// If [parse] throws a [FormatException], it's wrapped to include [node]'s + /// span. + T _parseNode<T>(YamlNode node, String name, T Function(String) parse) { + _validate(node, '$name must be a string.', (value) => value is String); + + try { + return parse(node.value as String); + } on FormatException catch (error) { + throw SourceSpanFormatException( + 'Invalid $name: ${error.message}', node.span, _source); + } + } + + /// Asserts that [field] is a string, passes it to [parse], and returns the + /// result. + /// + /// If [parse] throws a [FormatException], it's wrapped to include [field]'s + /// span. + T? _parseValue<T>(String field, T Function(String) parse) { + var node = _document.nodes[field]; + if (node == null) return null; + return _parseNode(node, field, parse); + } + + /// Parses a nested configuration document. + /// + /// [name] is the name of the field, which is used for error-handling. + /// [runnerConfig] controls whether runner configuration is allowed in the + /// nested configuration. It defaults to [_runnerConfig]. + Configuration _nestedConfig(YamlNode? node, String name, + {bool? runnerConfig}) { + if (node == null || node.value == null) return Configuration.empty; + + _validate(node, '$name must be a map.', (value) => value is Map); + var loader = _ConfigurationLoader(node as YamlMap, _source, + global: _global, runnerConfig: runnerConfig ?? _runnerConfig); + return loader.load(); + } + + /// Takes a map that contains [Configuration]s and extracts any + /// preset-specific configuration into a parent [Configuration]. + /// + /// This is needed because parameters to [Configuration.new] such as + /// `onPlatform` take maps to [SuiteConfiguration]s. [SuiteConfiguration] + /// doesn't support preset-specific configuration, so this extracts the preset + /// logic into a parent [Configuration], leaving only maps to + /// [SuiteConfiguration]s. The [create] function is used to construct + /// [Configuration]s from the resolved maps. + Configuration _extractPresets<T>(Map<T, Configuration> map, + Configuration Function(Map<T, SuiteConfiguration>) create) { + if (map.isEmpty) return Configuration.empty; + + var base = <T, SuiteConfiguration>{}; + var presets = <String, Map<T, SuiteConfiguration>>{}; + map.forEach((key, config) { + base[key] = config.suiteDefaults; + config.presets.forEach((preset, presetConfig) { + presets.putIfAbsent(preset, () => {})[key] = presetConfig.suiteDefaults; + }); + }); + + if (presets.isEmpty) { + return base.isEmpty ? Configuration.empty : create(base); + } else { + var newPresets = presets.map((key, map) => MapEntry(key, create(map))); + return create(base).change(presets: newPresets); + } + } + + /// Asserts that [map] has a field named [field] and returns it. + YamlNode _expect(YamlMap map, String field) { + var node = map.nodes[field]; + if (node != null) return node; + + throw SourceSpanFormatException( + 'Missing required field "$field".', map.span, _source); + } + + /// Throws an error if a field named [field] exists at this level. + void _disallow(String field) { + if (!_document.containsKey(field)) return; + + throw SourceSpanFormatException( + "$field isn't supported here.", + // We need the key as a [YamlNode] to get its span. + (_document.nodes.keys.firstWhere((key) => key.value == field) + as YamlNode) + .span, + _source); + } + + /// Throws a [SourceSpanFormatException] with [message] about [field]. + Never _error(String message, String field) { + throw SourceSpanFormatException( + message, _document.nodes[field]!.span, _source); + } +}
diff --git a/pkgs/test_core/lib/src/runner/configuration/reporters.dart b/pkgs/test_core/lib/src/runner/configuration/reporters.dart new file mode 100644 index 0000000..0f0fe01 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/configuration/reporters.dart
@@ -0,0 +1,91 @@ +// 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:collection'; +import 'dart:io'; + +import '../../util/io.dart'; +import '../configuration.dart'; +import '../engine.dart'; +import '../reporter.dart'; +import '../reporter/compact.dart'; +import '../reporter/expanded.dart'; +import '../reporter/failures_only.dart'; +import '../reporter/github.dart'; +import '../reporter/json.dart'; + +/// Constructs a reporter for the provided engine with the provided +/// configuration. +typedef ReporterFactory = Reporter Function(Configuration, Engine, StringSink); + +/// Container for a reporter description and corresponding factory. +class ReporterDetails { + final String description; + final ReporterFactory factory; + ReporterDetails(this.description, this.factory); +} + +/// All reporters and their corresponding details. +final UnmodifiableMapView<String, ReporterDetails> allReporters = + UnmodifiableMapView<String, ReporterDetails>(_allReporters); + +final _allReporters = <String, ReporterDetails>{ + 'expanded': ReporterDetails( + 'A separate line for each update.', + (config, engine, sink) => ExpandedReporter.watch(engine, sink, + color: config.color, + printPath: config.testSelections.length > 1 || + Directory(config.testSelections.keys.single).existsSync(), + printPlatform: config.suiteDefaults.runtimes.length > 1 || + config.suiteDefaults.compilerSelections != null)), + 'compact': ReporterDetails( + 'A single line, updated continuously.', + (config, engine, sink) => CompactReporter.watch(engine, sink, + color: config.color, + printPath: config.testSelections.length > 1 || + Directory(config.testSelections.keys.single).existsSync(), + printPlatform: config.suiteDefaults.runtimes.length > 1 || + config.suiteDefaults.compilerSelections != null)), + 'failures-only': ReporterDetails( + 'A separate line for failing tests with no output for passing tests', + (config, engine, sink) => FailuresOnlyReporter.watch(engine, sink, + color: config.color, + printPath: config.testSelections.length > 1 || + Directory(config.testSelections.keys.single).existsSync(), + printPlatform: config.suiteDefaults.runtimes.length > 1 || + config.suiteDefaults.compilerSelections != null)), + 'github': ReporterDetails( + 'A custom reporter for GitHub Actions ' + '(the default reporter when running on GitHub Actions).', + (config, engine, sink) => GithubReporter.watch(engine, sink, + printPath: config.testSelections.length > 1 || + Directory(config.testSelections.keys.single).existsSync(), + printPlatform: config.suiteDefaults.runtimes.length > 1 || + config.suiteDefaults.compilerSelections != null)), + 'json': ReporterDetails( + 'A machine-readable format (see ' + 'https://dart.dev/go/test-docs/json_reporter.md).', + (config, engine, sink) => + JsonReporter.watch(engine, sink, isDebugRun: config.debug)), + 'silent': ReporterDetails( + 'A reporter with no output. ' + 'May be useful when only the exit code is meaningful.', + (config, engine, sink) => SilentReporter()), +}; + +final defaultReporter = inTestTests + ? 'expanded' + : inGithubContext + ? 'github' + : canUseSpecialChars + ? 'compact' + : 'expanded'; + +/// **Do not call this function without express permission from the test package +/// authors**. +/// +/// This globally registers a reporter. +void registerReporter(String name, ReporterDetails reporterDetails) { + _allReporters[name] = reporterDetails; +}
diff --git a/pkgs/test_core/lib/src/runner/configuration/runtime_settings.dart b/pkgs/test_core/lib/src/runner/configuration/runtime_settings.dart new file mode 100644 index 0000000..e366b4d --- /dev/null +++ b/pkgs/test_core/lib/src/runner/configuration/runtime_settings.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 'package:source_span/source_span.dart'; +import 'package:yaml/yaml.dart'; + +import '../plugin/customizable_platform.dart'; + +/// User-defined settings for a built-in test runtime. +final class RuntimeSettings { + /// The identifier used to look up the runtime being overridden. + final String identifier; + + /// The location that [identifier] was defined in the configuration file. + final SourceSpan identifierSpan; + + /// The user's settings for this runtime. + /// + /// This is a list of settings, from most global to most specific, that will + /// eventually be merged using [CustomizablePlatform.mergePlatformSettings]. + final List<YamlMap> settings; + + RuntimeSettings(this.identifier, this.identifierSpan, List<YamlMap> settings) + : settings = List.unmodifiable(settings); +}
diff --git a/pkgs/test_core/lib/src/runner/configuration/utils.dart b/pkgs/test_core/lib/src/runner/configuration/utils.dart new file mode 100644 index 0000000..d47526c --- /dev/null +++ b/pkgs/test_core/lib/src/runner/configuration/utils.dart
@@ -0,0 +1,16 @@ +// Copyright (c) 2021, 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:collection/collection.dart'; + +/// Like [mergeMaps], but assumes both maps are unmodifiable and so avoids +/// creating a new map unnecessarily. +/// +/// The return value *may or may not* be unmodifiable. +Map<K, V> mergeUnmodifiableMaps<K, V>(Map<K, V> map1, Map<K, V> map2, + {V Function(V, V)? value}) { + if (map1.isEmpty) return map2; + if (map2.isEmpty) return map1; + return mergeMaps(map1, map2, value: value); +}
diff --git a/pkgs/test_core/lib/src/runner/configuration/values.dart b/pkgs/test_core/lib/src/runner/configuration/values.dart new file mode 100644 index 0000000..6e2f50a --- /dev/null +++ b/pkgs/test_core/lib/src/runner/configuration/values.dart
@@ -0,0 +1,19 @@ +// Copyright (c) 2016, 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 'dart:math' as math; + +import 'package:glob/glob.dart'; + +/// The default number of test suites to run at once. +/// +/// This defaults to half the available processors, since presumably some of +/// them will be used for the OS and other processes. +final defaultConcurrency = math.max(1, Platform.numberOfProcessors ~/ 2); + +/// The default filename pattern. +/// +/// This is stored here so that we don't have to recompile it multiple times. +final defaultFilename = Glob('*_test.dart');
diff --git a/pkgs/test_core/lib/src/runner/console.dart b/pkgs/test_core/lib/src/runner/console.dart new file mode 100644 index 0000000..3a4d6fa --- /dev/null +++ b/pkgs/test_core/lib/src/runner/console.dart
@@ -0,0 +1,109 @@ +// Copyright (c) 2016, 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:async/async.dart'; + +import '../util/io.dart'; + +/// An interactive console for taking user commands. +class Console { + /// The registered commands. + final _commands = <String, _Command>{}; + + /// The pending next line of standard input, if we're waiting on one. + CancelableOperation? _nextLine; + + /// Whether the console is currently running. + bool _running = false; + + /// The terminal escape for red text, or the empty string if this is Windows + /// or not outputting to a terminal. + final String _red; + + /// The terminal escape for bold text, or the empty string if this is + /// Windows or not outputting to a terminal. + final String _bold; + + /// The terminal escape for removing test coloring, or the empty string if + /// this is Windows or not outputting to a terminal. + final String _noColor; + + /// Creates a new [Console]. + /// + /// If [color] is true, this uses Unix terminal colors. + Console({bool color = true}) + : _red = color ? '\u001b[31m' : '', + _bold = color ? '\u001b[1m' : '', + _noColor = color ? '\u001b[0m' : '' { + registerCommand('help', 'Displays this help information.', _displayHelp); + } + + /// Registers a command to be run whenever the user types [name]. + /// + /// The [description] should be a one-line description of the command to print + /// in the help output. The [body] callback will be called when the user types + /// the command. + void registerCommand( + String name, String description, FutureOr<void> Function() body) { + if (_commands.containsKey(name)) { + throw ArgumentError('The console already has a command named "$name".'); + } + + _commands[name] = (name: name, description: description, body: body); + } + + /// Starts running the console. + /// + /// This prints the initial prompt and loops while waiting for user input. + void start() { + _running = true; + unawaited(() async { + while (_running) { + stdout.write('> '); + _nextLine = stdinLines.cancelable((queue) => queue.next); + var commandName = await _nextLine!.value; + _nextLine = null; + + var command = _commands[commandName]; + if (command == null) { + stderr.writeln( + '${_red}Unknown command $_bold$commandName$_noColor$_red.' + '$_noColor'); + } else { + await command.body(); + } + } + }()); + } + + /// Stops the console running. + void stop() { + _running = false; + if (_nextLine != null) { + stdout.writeln(); + _nextLine!.cancel(); + } + } + + /// Displays the help info for the console commands. + void _displayHelp() { + var maxCommandLength = + _commands.values.map((command) => command.name.length).reduce(math.max); + + for (var command in _commands.values) { + var name = command.name.padRight(maxCommandLength + 4); + print('$_bold$name$_noColor${command.description}'); + } + } +} + +typedef _Command = ({ + String name, + String description, + FutureOr<void> Function() body, +});
diff --git a/pkgs/test_core/lib/src/runner/coverage.dart b/pkgs/test_core/lib/src/runner/coverage.dart new file mode 100644 index 0000000..841bdd7 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/coverage.dart
@@ -0,0 +1,24 @@ +// Copyright (c) 2019, 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 'dart:io'; + +import 'package:path/path.dart' as p; + +import 'live_suite_controller.dart'; + +/// Collects coverage and outputs to the [coveragePath] path. +Future<void> writeCoverage( + String coveragePath, LiveSuiteController controller) async { + var suite = controller.liveSuite.suite; + var coverage = await controller.liveSuite.suite.gatherCoverage(); + final outfile = File(p.join(coveragePath, + '${suite.path}.${suite.platform.runtime.name.toLowerCase()}.json')) + ..createSync(recursive: true); + final out = outfile.openWrite(); + out.write(json.encode(coverage)); + await out.flush(); + await out.close(); +}
diff --git a/pkgs/test_core/lib/src/runner/coverage_stub.dart b/pkgs/test_core/lib/src/runner/coverage_stub.dart new file mode 100644 index 0000000..64f69c7 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/coverage_stub.dart
@@ -0,0 +1,10 @@ +// Copyright (c) 2019, 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 'live_suite_controller.dart'; + +Future<void> writeCoverage( + String coveragePath, LiveSuiteController controller) => + throw UnsupportedError( + 'Coverage is only supported through the test runner.');
diff --git a/pkgs/test_core/lib/src/runner/dart2js_compiler_pool.dart b/pkgs/test_core/lib/src/runner/dart2js_compiler_pool.dart new file mode 100644 index 0000000..a13a65a --- /dev/null +++ b/pkgs/test_core/lib/src/runner/dart2js_compiler_pool.dart
@@ -0,0 +1,130 @@ +// 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:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; + +import '../util/dart.dart'; +import '../util/io.dart'; +import '../util/package_config.dart'; +import 'compiler_pool.dart'; +import 'suite.dart'; + +/// A regular expression matching the first status line printed by dart2js. +final _dart2jsStatus = + RegExp(r'^Dart file \(.*\) compiled to JavaScript: .*\n?'); + +/// A pool of `dart2js` instances. +/// +/// This limits the number of compiler instances running concurrently. +class Dart2JsCompilerPool extends CompilerPool { + /// Extra arguments to pass to dart2js. + final List<String> _extraArgs; + + /// The currently-active dart2js processes. + final _processes = <Process>{}; + + /// Creates a compiler pool that multiple instances of `dart2js` at once. + Dart2JsCompilerPool([Iterable<String>? extraArgs]) + : _extraArgs = extraArgs?.toList() ?? const []; + + /// Compiles [code] to [path]. + /// + /// This wraps the Dart code in the standard browser-testing wrapper. + /// + /// The returned [Future] will complete once the `dart2js` process completes + /// *and* all its output has been printed to the command line. + @override + Future compileInternal( + String code, String path, SuiteConfiguration suiteConfig) { + return withTempDir((dir) async { + var wrapperPath = p.join(dir, 'runInBrowser.dart'); + File(wrapperPath).writeAsStringSync(code); + + var args = [ + 'compile', + 'js', + for (var experiment in enabledExperiments) + '--enable-experiment=$experiment', + '--enable-asserts', + wrapperPath, + '--out=$path', + '--packages=${await packageConfigUri}', + '--disable-program-split', + ..._extraArgs, + ...suiteConfig.dart2jsArgs + ]; + + if (config.color) args.add('--enable-diagnostic-colors'); + + var process = await Process.start(Platform.resolvedExecutable, args); + if (closed) { + process.kill(); + return; + } + + _processes.add(process); + + /// Wait until the process is entirely done to print out any output. + /// This can produce a little extra time for users to wait with no + /// update, but it also avoids some really nasty-looking interleaved + /// output. Write both stdout and stderr to the same buffer in case + /// they're intended to be printed in order. + var buffer = StringBuffer(); + + await Future.wait([ + process.stdout.transform(utf8.decoder).forEach(buffer.write), + process.stderr.transform(utf8.decoder).forEach(buffer.write), + ]); + + var exitCode = await process.exitCode; + _processes.remove(process); + if (closed) return; + + var output = buffer.toString().replaceFirst(_dart2jsStatus, ''); + if (output.isNotEmpty) print(output); + + if (exitCode != 0) throw 'dart2js failed.'; + + _fixSourceMap('$path.map'); + }); + } + + /// Fix up the source map at [mapPath] so that it points to absolute file: + /// URIs that are resolvable by the browser. + void _fixSourceMap(String mapPath) { + var map = jsonDecode(File(mapPath).readAsStringSync()) as Map; + var root = map['sourceRoot'] as String; + + final mapUri = p.toUri(mapPath); + map.cast<String, List<Object?>>().update( + 'sources', + (sources) => [ + for (var source in sources) + switch (Uri.parse('$root$source')) { + Uri(hasScheme: true) && final uri => uri.toString(), + Uri(:final path) when path.endsWith('/runInBrowser.dart') => '', + final uri => mapUri.resolveUri(uri).toString(), + } + ], + ); + + File(mapPath).writeAsStringSync(jsonEncode(map)); + } + + /// Closes the compiler pool. + /// + /// This kills all currently-running compilers and ensures that no more will + /// be started. It returns a [Future] that completes once all the compilers + /// have been killed and all resources released. + @override + Future<void> closeInternal() async { + await Future.wait(_processes.map((process) async { + process.kill(); + await process.exitCode; + })); + } +}
diff --git a/pkgs/test_core/lib/src/runner/debugger.dart b/pkgs/test_core/lib/src/runner/debugger.dart new file mode 100644 index 0000000..5e59d88 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/debugger.dart
@@ -0,0 +1,219 @@ +// Copyright (c) 2016, 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 'package:async/async.dart'; + +import '../util/async.dart'; +import '../util/io.dart'; +import 'configuration.dart'; +import 'console.dart'; +import 'engine.dart'; +import 'load_suite.dart'; +import 'reporter.dart'; +import 'runner_suite.dart'; + +/// Runs [loadSuite] in debugging mode. +/// +/// Runs the suite's tests using [engine]. The [reporter] should already be +/// watching [engine], and the [config] should contain the user configuration +/// for the test runner. +/// +/// Returns a [CancelableOperation] that will complete once the suite has +/// finished running. If the operation is canceled, the debugger will clean up +/// any resources it allocated. +CancelableOperation debug( + Engine engine, Reporter reporter, LoadSuite loadSuite) { + _Debugger? debugger; + var canceled = false; + return CancelableOperation.fromFuture(() async { + engine.suiteSink.add(loadSuite.changeSuite((runnerSuite) { + engine.pause(); + return runnerSuite; + })); + + var suite = await loadSuite.suite; + if (canceled || suite == null) return; + + await (debugger = _Debugger(engine, reporter, suite)).run(); + }(), onCancel: () { + canceled = true; + // Make sure the load test finishes so the engine can close. + engine.resume(); + debugger?.close(); + }); +} + +// TODO(nweiz): Test using the console and restarting a test once sdk#25369 is +// fixed and the VM service client is released +/// A debugger for a single test suite. +class _Debugger { + /// The test runner configuration. + final _config = Configuration.current; + + /// The engine that will run the suite. + final Engine _engine; + + /// The reporter that's reporting [_engine]'s progress. + final Reporter _reporter; + + /// The suite to run. + final RunnerSuite _suite; + + /// The console through which the user can control the debugger. + /// + /// This is only visible when the test environment is paused, so as not to + /// overlap with the reporter's reporting. + final Console _console; + + /// A completer that's used to manually unpause the test if the debugger is + /// closed. + final _pauseCompleter = CancelableCompleter<void>(); + + /// The subscription to [_suite.onDebugging]. + StreamSubscription<bool>? _onDebuggingSubscription; + + /// The subscription to [_suite.environment.onRestart]. + late final StreamSubscription _onRestartSubscription; + + /// Whether [close] has been called. + bool _closed = false; + + bool get _json => _config.reporter == 'json'; + + _Debugger(this._engine, this._reporter, this._suite) + : _console = Console(color: Configuration.current.color) { + _console.registerCommand('restart', + 'Restart the current test after it finishes running.', _restartTest); + + _onRestartSubscription = _suite.environment.onRestart.listen((_) { + _restartTest(); + }); + } + + /// Runs the debugger. + /// + /// This prints information about the suite's debugger, then once the user has + /// had a chance to set breakpoints, runs the suite's tests. + Future run() async { + try { + await _pause(); + if (_closed) return; + + _onDebuggingSubscription = _suite.onDebugging.listen((debugging) { + if (debugging) { + _onDebugging(); + } else { + _onNotDebugging(); + } + }); + + _engine.resume(); + await _engine.onIdle.first; + } finally { + close(); + } + } + + /// Prints URLs for the [_suite]'s debugger and waits for the user to tell the + /// suite to run. + Future _pause() async { + if (!_suite.environment.supportsDebugging) return; + + try { + if (!_json) { + _reporter.pause(); + + var bold = _config.color ? '\u001b[1m' : ''; + var yellow = _config.color ? '\u001b[33m' : ''; + var noColor = _config.color ? '\u001b[0m' : ''; + print(''); + + var runtime = _suite.platform.runtime; + if (runtime.isDartVM) { + var url = _suite.environment.observatoryUrl; + if (url == null) { + print('${yellow}Observatory URL not found.$noColor'); + } else { + print('Observatory URL: $bold$url$noColor'); + } + } + + if (runtime.isHeadless && !runtime.isDartVM) { + var url = _suite.environment.remoteDebuggerUrl; + if (url == null) { + print('${yellow}Remote debugger URL not found.$noColor'); + } else { + print('Remote debugger URL: $bold$url$noColor'); + } + } + + var buffer = StringBuffer('${bold}The test runner is paused.$noColor '); + if (runtime.isDartVM) { + buffer.write('Open the Observatory '); + } else { + if (!runtime.isHeadless) { + buffer.write('Open the dev console in $runtime '); + } else { + buffer.write('Open the remote debugger '); + } + } + + buffer.write("and set breakpoints. Once you're finished, return to " + 'this terminal and press Enter.'); + + print(wordWrap(buffer.toString())); + } + + await inCompletionOrder([ + _suite.environment.displayPause(), + stdinLines.cancelable((queue) => queue.next), + _pauseCompleter.operation + ]).first; + } finally { + if (!_json) _reporter.resume(); + } + } + + /// Handles the environment pausing to debug. + /// + /// This starts the interactive console. + void _onDebugging() { + if (!_json) _reporter.pause(); + + if (!_json) { + print('\nEntering debugging console. Type "help" for help.'); + } + _console.start(); + } + + /// Handles the environment starting up again. + /// + /// This closes the interactive console. + void _onNotDebugging() { + if (!_json) _reporter.resume(); + _console.stop(); + } + + /// Restarts the current test. + void _restartTest() { + if (_engine.active.isEmpty) return; + var liveTest = _engine.active.single; + _engine.restartTest(liveTest); + if (!_json) { + print(wordWrap( + 'Will restart "${liveTest.test.name}" once it finishes running.')); + } + } + + /// Closes the debugger and releases its resources. + void close() { + _pauseCompleter.complete(); + _closed = true; + _onDebuggingSubscription?.cancel(); + _onRestartSubscription.cancel(); + _console.stop(); + } +}
diff --git a/pkgs/test_core/lib/src/runner/engine.dart b/pkgs/test_core/lib/src/runner/engine.dart new file mode 100644 index 0000000..6a63627 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/engine.dart
@@ -0,0 +1,553 @@ +// Copyright (c) 2015, 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:math'; + +import 'package:async/async.dart' hide Result; +import 'package:collection/collection.dart'; +import 'package:pool/pool.dart'; +import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/live_test_controller.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports + +import 'coverage_stub.dart' if (dart.library.io) 'coverage.dart'; +import 'live_suite.dart'; +import 'live_suite_controller.dart'; +import 'load_suite.dart'; +import 'runner_suite.dart'; +import 'util/iterable_set.dart'; + +/// An [Engine] manages a run that encompasses multiple test suites. +/// +/// Test suites are provided by passing them into [suiteSink]. Once all suites +/// have been provided, the user should close [suiteSink] to indicate this. +/// [run] won't terminate until [suiteSink] is closed. Suites will be run in the +/// order they're provided to [suiteSink]. Tests within those suites will +/// likewise be run in the order they're declared. +/// +/// The current status of every test is visible via [liveTests]. [onTestStarted] +/// can also be used to be notified when a test is about to be run. +/// +/// The engine has some special logic for [LoadSuite]s and the tests they +/// contain, referred to as "load tests". Load tests exist to provide visibility +/// into the process of loading test files. As long as that process is +/// proceeding normally users usually don't care about it, so the engine does +/// not include them in [liveTests] and other collections. +/// If a load test fails, it will be added to [failed] and [liveTests]. +/// +/// The test suite loaded by a load suite will be automatically be run by the +/// engine; it doesn't need to be added to [suiteSink] manually. +/// +/// Load tests will always be emitted through [onTestStarted] so users can watch +/// their event streams once they start running. +class Engine { + /// Whether [run] has been called yet. + var _runCalled = false; + + /// Whether [close] has been called. + var _closed = false; + + /// Whether [close] was called before all the tests finished running. + /// + /// This is `null` if close hasn't been called and the tests are still + /// running, `true` if close was called before the tests finished running, and + /// `false` if the tests finished running before close was called. + bool? _closedBeforeDone; + + /// The coverage output directory. + String? _coverage; + + /// The seed used to generate randomness for test case shuffling. + /// + /// If null or zero no shuffling will occur. + /// The same seed will shuffle the tests in the same way every time. + int? testRandomizeOrderingSeed; + + /// Whether to stop running tests after a failure. + bool _stopOnFirstFailure; + + /// A pool that limits the number of test suites running concurrently. + final Pool _runPool; + + /// A completer that will complete when this engine is unpaused. + /// + /// `null` if this engine is not paused. + Completer? _pauseCompleter; + + /// A future that completes once this is unpaused. + /// + /// If this engine isn't paused, this future completes immediately. + Future get _onUnpaused => + _pauseCompleter == null ? Future.value() : _pauseCompleter!.future; + + /// Whether all tests passed or were skipped. + /// + /// This fires once all tests have completed and [suiteSink] has been closed. + /// This will be `null` if [close] was called before all the tests finished + /// running. + Future<bool?> get success async { + await Future.wait(<Future>[_group.future, _runPool.done], eagerError: true); + if (_closedBeforeDone!) return null; + return liveTests.every((liveTest) => + liveTest.state.result.isPassing && + liveTest.state.status == Status.complete); + } + + /// A group of futures for each test suite. + final _group = FutureGroup<void>(); + + /// All of the engine's stream subscriptions. + final _subscriptions = <StreamSubscription>{}; + + /// A sink used to pass [RunnerSuite]s in to the engine to run. + /// + /// Suites may be added as quickly as they're available; the Engine will only + /// run as many as necessary at a time based on its concurrency settings. + /// + /// Suites added to the sink will be closed by the engine based on its + /// internal logic. + Sink<RunnerSuite> get suiteSink => DelegatingSink(_suiteController.sink); + final _suiteController = StreamController<RunnerSuite>(); + + /// All the [RunnerSuite]s added to [suiteSink] so far. + /// + /// Note that if a [LoadSuite] is added, this will only contain that suite, + /// not the suite it loads. + Set<RunnerSuite> get addedSuites => UnmodifiableSetView(_addedSuites); + final _addedSuites = <RunnerSuite>{}; + + /// A broadcast stream that emits each [RunnerSuite] as it's added to the + /// engine via [suiteSink]. + /// + /// Note that if a [LoadSuite] is added, this will only return that suite, not + /// the suite it loads. + /// + /// This is guaranteed to fire after the suite is added to [addedSuites]. + Stream<RunnerSuite> get onSuiteAdded => _onSuiteAddedController.stream; + final _onSuiteAddedController = StreamController<RunnerSuite>.broadcast(); + + /// A broadcast stream that emits each [LiveSuite] as it's loaded. + /// + /// Note that unlike [onSuiteAdded], for suites that are loaded using + /// [LoadSuite]s, both the [LoadSuite] and the suite it loads will eventually + /// be emitted by this stream. + Stream<LiveSuite> get onSuiteStarted => _onSuiteStartedController.stream; + final _onSuiteStartedController = StreamController<LiveSuite>.broadcast(); + + /// All the currently-known tests that have run or are running. + /// + /// These are [LiveTest]s, representing the in-progress state of each test. + /// Tests that have not yet begun running are marked [Status.pending]; tests + /// that have finished are marked [Status.complete]. + /// + /// This is guaranteed to contain the same tests as the union of [passed], + /// [skipped], [failed], and [active]. + /// + /// [LiveTest.run] must not be called on these tests. + Set<LiveTest> get liveTests => + UnionSet.from([passed, skipped, failed, IterableSet(active)], + disjoint: true); + + /// A stream that emits each [LiveTest] as it's about to start running. + /// + /// This is guaranteed to fire before [LiveTest.onStateChange] first fires. + Stream<LiveTest> get onTestStarted => _onTestStartedGroup.stream; + final _onTestStartedGroup = StreamGroup<LiveTest>.broadcast(); + + /// The set of tests that have completed and been marked as passing. + Set<LiveTest> get passed => _passedGroup.set; + final _passedGroup = UnionSetController<LiveTest>(disjoint: true); + + /// The set of tests that have completed and been marked as skipped. + Set<LiveTest> get skipped => _skippedGroup.set; + final _skippedGroup = UnionSetController<LiveTest>(disjoint: true); + + /// The set of tests that have completed and been marked as failing or error. + Set<LiveTest> get failed => _failedGroup.set; + final _failedGroup = UnionSetController<LiveTest>(disjoint: true); + + /// The tests that are still running, in the order they began running. + List<LiveTest> get active => UnmodifiableListView(_active); + final _active = QueueList<LiveTest>(); + + /// The suites that are still loading, in the order they began. + List<LiveTest> get activeSuiteLoads => + UnmodifiableListView(_activeSuiteLoads); + final _activeSuiteLoads = <LiveTest>{}; + + /// The set of tests that have been marked for restarting. + /// + /// This is always a subset of [active]. Once a test in here has finished + /// running, it's run again. + final _restarted = <LiveTest>{}; + + /// Whether this engine is idle—that is, not currently executing a test. + bool get isIdle => _group.isIdle; + + /// A broadcast stream that fires an event whenever [isIdle] switches from + /// `false` to `true`. + Stream get onIdle => _group.onIdle; + + /// Creates an [Engine] that will run all tests provided via [suiteSink]. + /// + /// [concurrency] controls how many suites are loaded and ran at once, and + /// defaults to 1. + /// + /// [testRandomizeOrderingSeed] configures test case shuffling within each + /// test suite. + /// Any non-zero value will enable shuffling using this value as a seed. + /// Omitting this argument or passing `0` disables shuffling. + /// + /// [coverage] specifies a directory to output coverage information. + /// + /// If [stopOnFirstFailure] then a single failing test will cause the engine + /// to [close] and stop ruunning further tests. + Engine({ + int? concurrency, + String? coverage, + this.testRandomizeOrderingSeed, + bool stopOnFirstFailure = false, + }) : _runPool = Pool(concurrency ?? 1), + _stopOnFirstFailure = stopOnFirstFailure, + _coverage = coverage { + _group.future.then((_) { + _onTestStartedGroup.close(); + _onSuiteStartedController.close(); + _closedBeforeDone ??= false; + }).onError((_, __) { + // Don't top-level errors. They'll be thrown via [success] anyway. + }); + } + + /// Creates an [Engine] that will run all tests in [suites]. + /// + /// An engine constructed this way will automatically close its [suiteSink], + /// meaning that no further suites may be provided. + /// + /// [concurrency] controls how many suites are run at once. If [runSkipped] is + /// `true`, skipped tests will be run as though they weren't skipped. + factory Engine.withSuites(List<RunnerSuite> suites, + {int? concurrency, String? coverage, bool stopOnFirstFailure = false}) { + var engine = Engine( + concurrency: concurrency, + coverage: coverage, + stopOnFirstFailure: stopOnFirstFailure, + ); + for (var suite in suites) { + engine.suiteSink.add(suite); + } + engine.suiteSink.close(); + return engine; + } + + /// Runs all tests in all suites defined by this engine. + /// + /// This returns `true` if all tests succeed, and `false` otherwise. It will + /// only return once all tests have finished running and [suiteSink] has been + /// closed. + /// + /// If [success] completes with `null` this will complete with `null`. + Future<bool?> run() { + if (_runCalled) { + throw StateError('Engine.run() may not be called more than once.'); + } + _runCalled = true; + + var subscription = _suiteController.stream.listen(null); + subscription + ..onData((suite) { + _addedSuites.add(suite); + _onSuiteAddedController.add(suite); + + _group.add(() async { + var resource = await _runPool.request(); + LiveSuiteController? controller; + try { + if (suite is LoadSuite) { + await _onUnpaused; + controller = await _addLoadSuite(suite); + if (controller == null) return; + } else { + controller = LiveSuiteController(suite); + } + + _addLiveSuite(controller.liveSuite); + + if (_closed) return; + await _runGroup(controller, controller.liveSuite.suite.group, []); + controller.noMoreLiveTests(); + if (_coverage != null) await writeCoverage(_coverage!, controller); + } finally { + resource.allowRelease(() => controller?.close()); + } + }()); + }) + ..onDone(() { + _subscriptions.remove(subscription); + _onSuiteAddedController.close(); + _group.close(); + _runPool.close(); + }); + _subscriptions.add(subscription); + + return success; + } + + /// Runs all the entries in [group] in sequence. + /// + /// [suiteController] is the controller fo the suite that contains [group]. + /// [parents] is a list of groups that contain [group]. It may be modified, + /// but it's guaranteed to be in its original state once this function has + /// finished. + Future _runGroup(LiveSuiteController suiteController, Group group, + List<Group> parents) async { + parents.add(group); + try { + var suiteConfig = suiteController.liveSuite.suite.config; + var skipGroup = !suiteConfig.runSkipped && group.metadata.skip; + var setUpAllSucceeded = true; + if (!skipGroup && group.setUpAll != null) { + var liveTest = group.setUpAll! + .load(suiteController.liveSuite.suite, groups: parents); + await _runLiveTest(suiteController, liveTest, countSuccess: false); + setUpAllSucceeded = liveTest.state.result.isPassing; + } + + if (!_closed && setUpAllSucceeded) { + // shuffle the group entries + var entries = group.entries.toList(); + if (suiteConfig.allowTestRandomization && + testRandomizeOrderingSeed != null && + testRandomizeOrderingSeed! > 0) { + entries.shuffle(Random(testRandomizeOrderingSeed)); + } + + for (var entry in entries) { + if (_closed) return; + + if (entry is Group) { + await _runGroup(suiteController, entry, parents); + } else if (!suiteConfig.runSkipped && entry.metadata.skip) { + await _runSkippedTest(suiteController, entry as Test, parents); + } else { + var test = entry as Test; + await _runLiveTest(suiteController, + test.load(suiteController.liveSuite.suite, groups: parents)); + } + } + } + + // Even if we're closed or setUpAll failed, we want to run all the + // teardowns to ensure that any state is properly cleaned up. + if (!skipGroup && group.tearDownAll != null) { + var liveTest = group.tearDownAll! + .load(suiteController.liveSuite.suite, groups: parents); + await _runLiveTest(suiteController, liveTest, countSuccess: false); + if (_closed) await liveTest.close(); + } + } finally { + parents.remove(group); + } + } + + /// Runs [liveTest] using [suiteController]. + /// + /// If [countSuccess] is `true` (the default), the test is put into [passed] + /// if it succeeds. Otherwise, it's removed from [liveTests] entirely. + Future _runLiveTest(LiveSuiteController suiteController, LiveTest liveTest, + {bool countSuccess = true}) async { + await _onUnpaused; + _active.add(liveTest); + + var subscription = liveTest.onStateChange.listen(null); + subscription + ..onData((state) { + if (state.status != Status.complete) return; + _active.remove(liveTest); + }) + ..onDone(() { + _subscriptions.remove(subscription); + }); + _subscriptions.add(subscription); + + suiteController.reportLiveTest(liveTest, countSuccess: countSuccess); + + // Schedule a microtask to ensure that [onTestStarted] fires before the + // first [LiveTest.onStateChange] event. + await Future.microtask(liveTest.run); + + // Once the test finishes, use [new Future] to do a coarse-grained event + // loop pump to avoid starving non-microtask events. + await Future(() {}); + + if (!_restarted.contains(liveTest)) { + if (_stopOnFirstFailure && liveTest.state.result.isFailing) { + unawaited(close()); + } + return; + } + await _runLiveTest(suiteController, liveTest.copy(), + countSuccess: countSuccess); + _restarted.remove(liveTest); + } + + /// Runs a dummy [LiveTest] for a test marked as "skip". + /// + /// [suiteController] is the controller for the suite that contains [test]. + /// [parents] is a list of groups that contain [test]. + Future _runSkippedTest(LiveSuiteController suiteController, Test test, + List<Group> parents) async { + await _onUnpaused; + var skipped = LocalTest(test.name, test.metadata, () {}, trace: test.trace); + + late LiveTestController controller; + controller = + LiveTestController(suiteController.liveSuite.suite, skipped, () { + controller.setState(const State(Status.running, Result.success)); + controller.setState(const State(Status.running, Result.skipped)); + + if (skipped.metadata.skipReason != null) { + controller + .message(Message.skip('Skip: ${skipped.metadata.skipReason}')); + } + + controller.setState(const State(Status.complete, Result.skipped)); + controller.completer.complete(); + }, () {}, groups: parents); + + return await _runLiveTest(suiteController, controller); + } + + /// Closes [liveTest] and tells the engine to re-run it once it's done + /// running. + /// + /// Returns the same future as [LiveTest.close]. + Future restartTest(LiveTest liveTest) async { + if (_activeSuiteLoads.contains(liveTest)) { + throw ArgumentError("Can't restart a load test."); + } + + if (!_active.contains(liveTest)) { + throw StateError("Can't restart inactive test " + '"${liveTest.test.name}".'); + } + + _restarted.add(liveTest); + _active.remove(liveTest); + await liveTest.close(); + } + + /// Runs [suite] and returns the [LiveSuiteController] for the suite it loads. + /// + /// Returns `null` if the suite fails to load. + Future<LiveSuiteController?> _addLoadSuite(LoadSuite suite) async { + var controller = LiveSuiteController(suite); + _addLiveSuite(controller.liveSuite); + + var liveTest = suite.test.load(suite); + _activeSuiteLoads.add(liveTest); + + var subscription = liveTest.onStateChange.listen(null); + subscription + ..onData((state) { + if (state.status != Status.complete) return; + _activeSuiteLoads.remove(liveTest); + }) + ..onDone(() { + _subscriptions.remove(subscription); + }); + _subscriptions.add(subscription); + + controller.reportLiveTest(liveTest, countSuccess: false); + controller.noMoreLiveTests(); + + // Schedule a microtask to ensure that [onTestStarted] fires before the + // first [LiveTest.onStateChange] event. + await Future.microtask(liveTest.run); + + var innerSuite = await suite.suite; + if (innerSuite == null) return null; + + var innerController = LiveSuiteController(innerSuite); + unawaited(innerController.liveSuite.onClose.whenComplete(() { + // When the main suite is closed, close the load suite and its test as + // well. This doesn't release any resources, but it does close streams + // which indicates that the load test won't experience an error in the + // future. + liveTest.close(); + controller.close(); + })); + + return innerController; + } + + /// Add [liveSuite] and the information it exposes to the engine's + /// informational streams and collections. + void _addLiveSuite(LiveSuite liveSuite) { + _onSuiteStartedController.add(liveSuite); + + _onTestStartedGroup.add(liveSuite.onTestStarted); + _passedGroup.add(liveSuite.passed); + _skippedGroup.add(liveSuite.skipped); + _failedGroup.add(liveSuite.failed); + } + + /// Pauses the engine. + /// + /// This pauses all streams and keeps any new suites from being loaded or + /// tests from being run until [resume] is called. + /// + /// This does nothing if the engine is already paused. Pauses are *not* + /// cumulative. + void pause() { + if (_pauseCompleter != null) return; + _pauseCompleter = Completer(); + for (var subscription in _subscriptions) { + subscription.pause(); + } + } + + void resume() { + if (_pauseCompleter == null) return; + _pauseCompleter!.complete(); + _pauseCompleter = null; + for (var subscription in _subscriptions) { + subscription.resume(); + } + } + + /// Signals that the caller is done paying attention to test results and the + /// engine should release any resources it has allocated. + /// + /// Any actively-running tests are also closed. VM tests are allowed to finish + /// running so that any modifications they've made to the filesystem can be + /// cleaned up. + /// + /// **Note that closing the engine is not the same as closing [suiteSink].** + /// Closing [suiteSink] indicates that no more input will be provided, closing + /// the engine indicates that no more output should be emitted. + Future close() async { + _closed = true; + if (_closedBeforeDone != null) _closedBeforeDone = true; + await _suiteController.close(); + await _onSuiteAddedController.close(); + + // Close the running tests first so that we're sure to wait for them to + // finish before we close their suites and cause them to become unloaded. + var allLiveTests = liveTests.toSet()..addAll(_activeSuiteLoads); + var futures = allLiveTests.map((liveTest) => liveTest.close()).toList(); + + // Closing the run pool will close the test suites as soon as their tests + // are done. For browser suites this is effectively immediate since their + // tests shut down as soon as they're closed, but for VM suites we may need + // to wait for tearDowns or tearDownAlls to run. + futures.add(_runPool.close()); + await Future.wait(futures, eagerError: true); + } +}
diff --git a/pkgs/test_core/lib/src/runner/environment.dart b/pkgs/test_core/lib/src/runner/environment.dart new file mode 100644 index 0000000..7fcba66 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/environment.dart
@@ -0,0 +1,55 @@ +// Copyright (c) 2015, 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 'package:async/async.dart'; + +/// The abstract class of environments in which test suites are +/// loaded—specifically, browsers and the Dart VM. +abstract class Environment { + /// Whether this environment supports interactive debugging. + bool get supportsDebugging; + + /// The URL of the Dart VM Observatory for this environment, or `null` if this + /// environment doesn't run the Dart VM or the URL couldn't be detected. + Uri? get observatoryUrl; + + /// The URL of the remote debugger for this environment, or `null` if it isn't + /// enabled. + Uri? get remoteDebuggerUrl; + + /// A broadcast stream that emits a `null` event whenever the user tells the + /// environment to restart the current test once it's finished. + /// + /// Never emits an error, and never closes. + Stream get onRestart; + + /// Displays information indicating that the test runner is paused. + /// + /// The returned operation will complete when the user takes action within the + /// environment that should unpause the runner. If the runner is unpaused + /// elsewhere, the operation should be canceled. + CancelableOperation displayPause(); +} + +/// The default environment for platform plugins. +class PluginEnvironment implements Environment { + @override + final supportsDebugging = false; + @override + Stream get onRestart => StreamController<void>.broadcast().stream; + + const PluginEnvironment(); + + @override + Uri? get observatoryUrl => null; + + @override + Uri? get remoteDebuggerUrl => null; + + @override + CancelableOperation displayPause() => throw UnsupportedError( + 'PluginEnvironment.displayPause is not supported.'); +}
diff --git a/pkgs/test_core/lib/src/runner/hack_register_platform.dart b/pkgs/test_core/lib/src/runner/hack_register_platform.dart new file mode 100644 index 0000000..f5a618d --- /dev/null +++ b/pkgs/test_core/lib/src/runner/hack_register_platform.dart
@@ -0,0 +1,36 @@ +// Copyright (c) 2016, 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:collection'; + +import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports +import 'platform.dart'; + +/// The functions to use to load [_platformPlugins] in all loaders. +/// +/// **Do not access this outside the test package**. +final platformCallbacks = + UnmodifiableMapView<Runtime, FutureOr<PlatformPlugin> Function()>( + _platformCallbacks); +final _platformCallbacks = <Runtime, FutureOr<PlatformPlugin> Function()>{}; + +/// **Do not call this function without express permission from the test package +/// authors**. +/// +/// Registers a [PlatformPlugin] for [runtimes]. +/// +/// This globally registers a plugin for all [Loader]s. When the runner first +/// requests that a suite be loaded for one of the given runtimes, this will +/// call [plugin] to load the platform plugin. It may return either a +/// [PlatformPlugin] or a `Future<PlatformPlugin>`. That plugin is then +/// preserved and used to load all suites for all matching runtimes. +/// +/// This overwrites the default plugins for those runtimes. +void registerPlatformPlugin( + Iterable<Runtime> runtimes, FutureOr<PlatformPlugin> Function() plugin) { + for (var runtime in runtimes) { + _platformCallbacks[runtime] = plugin; + } +}
diff --git a/pkgs/test_core/lib/src/runner/hybrid_listener.dart b/pkgs/test_core/lib/src/runner/hybrid_listener.dart new file mode 100644 index 0000000..6a521db --- /dev/null +++ b/pkgs/test_core/lib/src/runner/hybrid_listener.dart
@@ -0,0 +1,96 @@ +// Copyright (c) 2016, 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:isolate'; + +import 'package:async/async.dart'; +import 'package:stack_trace/stack_trace.dart'; +import 'package:stream_channel/isolate_channel.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test_api/backend.dart' show RemoteException; +import 'package:test_api/src/utils.dart'; // ignore: implementation_imports + +/// A sink transformer that wraps data and error events so that errors can be +/// decoded after being JSON-serialized. +final _transformer = StreamSinkTransformer<dynamic, dynamic>.fromHandlers( + handleData: (data, sink) { + ensureJsonEncodable(data); + sink.add({'type': 'data', 'data': data}); +}, handleError: (error, stackTrace, sink) { + sink.add( + {'type': 'error', 'error': RemoteException.serialize(error, stackTrace)}); +}); + +/// Runs the body of a hybrid isolate and communicates its messages, errors, and +/// prints to the main isolate. +/// +/// The [getMain] function returns the `hybridMain()` method. It's wrapped in a +/// closure so that, if the method undefined, we can catch the error and notify +/// the caller of it. +/// +/// The [data] argument contains two values: a [SendPort] that communicates with +/// the main isolate, and a message to pass to `hybridMain()`. +void listen(Function Function() getMain, List data) { + var channel = IsolateChannel<Object?>.connectSend(data.first as SendPort); + var message = data.last; + + Chain.capture(() { + runZoned(() { + dynamic /*Function*/ main; + try { + main = getMain(); + } on NoSuchMethodError catch (_) { + _sendError(channel, 'No top-level hybridMain() function defined.'); + return; + } catch (error, stackTrace) { + _sendError(channel, error, stackTrace); + return; + } + + if (main is! Function) { + _sendError(channel, 'Top-level hybridMain is not a function.'); + return; + } else if (main is! void Function(StreamChannel) && + main is! void Function(StreamChannel, Never)) { + if (main is void Function(StreamChannel<Never>) || + main is void Function(StreamChannel<Never>, Never)) { + _sendError( + channel, + 'The first parameter to the top-level hybridMain() must be a ' + 'StreamChannel<dynamic> or StreamChannel<Object?>. More specific ' + 'types such as StreamChannel<Object> are not supported.'); + } else { + _sendError(channel, + 'Top-level hybridMain() function must take one or two arguments.'); + } + return; + } + + // Wrap [channel] before passing it to user code so that we can wrap + // errors and distinguish user data events from control events sent by the + // listener. + var transformedChannel = channel.transformSink(_transformer); + if (main is void Function(StreamChannel)) { + main(transformedChannel); + } else { + main(transformedChannel, message); + } + }, zoneSpecification: ZoneSpecification(print: (_, __, ___, line) { + channel.sink.add({'type': 'print', 'line': line}); + })); + }, onError: (error, stackTrace) async { + _sendError(channel, error, stackTrace); + await channel.sink.close(); + Isolate.current.kill(); + }); +} + +/// Sends a message over [channel] indicating an error from user code. +void _sendError(StreamChannel channel, Object error, [StackTrace? stackTrace]) { + channel.sink.add({ + 'type': 'error', + 'error': RemoteException.serialize(error, stackTrace ?? Chain.current()) + }); +}
diff --git a/pkgs/test_core/lib/src/runner/live_suite.dart b/pkgs/test_core/lib/src/runner/live_suite.dart new file mode 100644 index 0000000..771655d --- /dev/null +++ b/pkgs/test_core/lib/src/runner/live_suite.dart
@@ -0,0 +1,77 @@ +// Copyright (c) 2016, 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:collection/collection.dart'; + +import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports + +import 'runner_suite.dart'; + +/// A view of the execution of a test suite. +/// +/// This is distinct from [Suite] because it represents the progress of running +/// a suite rather than the suite's contents. It provides events and collections +/// that give the caller a view into the suite's current state. +abstract class LiveSuite { + /// The suite that's being run. + RunnerSuite get suite; + + /// A [Future] that completes once the suite is complete. + /// + /// Note that even once this completes, the suite may still be running code + /// asynchronously. A suite is considered complete once all of its tests are + /// complete, but it's possible for a test to continue running even after it's + /// been marked complete—see [LiveTest.isComplete] for details. + /// + /// The [onClose] future can be used to determine when the suite and its tests + /// are guaranteed to emit no more events. + Future get onComplete; + + /// Whether the suite has been closed. + /// + /// If this is `true`, no code is running for the suite or any of its tests. + /// At this point, the caller can be sure that the suites' tests are all in + /// fixed states that will not change in the future. + bool get isClosed; + + /// A [Future] that completes when the suite has been closed. + /// + /// Once this completes, no code is running for the suite or any of its tests. + /// At this point, the caller can be sure that the suites' tests are all in + /// fixed states that will not change in the future. + Future get onClose; + + /// All the currently-known tests in this suite that have run or are running. + /// + /// This is guaranteed to contain the same tests as the union of [passed], + /// [skipped], [failed], and [active]. + Set<LiveTest> get liveTests => UnionSet.from([ + passed, + skipped, + failed, + if (active != null) {active!} + ]); + + /// A stream that emits each [LiveTest] in this suite as it's about to start + /// running. + /// + /// This is guaranteed to fire before [LiveTest.onStateChange] first fires. It + /// will close once all tests the user has selected are run. + Stream<LiveTest> get onTestStarted; + + /// The set of tests in this suite that have completed and been marked as + /// passing. + Set<LiveTest> get passed; + + /// The set of tests in this suite that have completed and been marked as + /// skipped. + Set<LiveTest> get skipped; + + /// The set of tests in this suite that have completed and been marked as + /// failing or error. + Set<LiveTest> get failed; + + /// The currently running test in this suite, or `null` if no test is running. + LiveTest? get active; +}
diff --git a/pkgs/test_core/lib/src/runner/live_suite_controller.dart b/pkgs/test_core/lib/src/runner/live_suite_controller.dart new file mode 100644 index 0000000..15bc797 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/live_suite_controller.dart
@@ -0,0 +1,154 @@ +// Copyright (c) 2016, 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 'package:async/async.dart' hide Result; +import 'package:collection/collection.dart'; +import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports + +import 'live_suite.dart'; +import 'runner_suite.dart'; + +/// An implementation of [LiveSuite] that's controlled by a +/// [LiveSuiteController]. +class _LiveSuite extends LiveSuite { + final LiveSuiteController _controller; + + @override + RunnerSuite get suite => _controller._suite; + + @override + Future get onComplete => _controller._onCompleteGroup.future; + + @override + bool get isClosed => _controller._onCloseCompleter.isCompleted; + + @override + Future get onClose => _controller._onCloseCompleter.future; + + @override + Stream<LiveTest> get onTestStarted => + _controller._onTestStartedController.stream; + + @override + Set<LiveTest> get passed => UnmodifiableSetView(_controller._passed); + + @override + Set<LiveTest> get skipped => UnmodifiableSetView(_controller._skipped); + + @override + Set<LiveTest> get failed => UnmodifiableSetView(_controller._failed); + + @override + LiveTest? get active => _controller._active; + + _LiveSuite(this._controller); +} + +/// A controller that drives a [LiveSuite]. +/// +/// This is a utility class to make it easier for [Engine] to create the +/// [LiveSuite]s exposed by various APIs. The [LiveSuite] is accessible through +/// [LiveSuiteController.liveSuite]. When a live test is run, it should be +/// passed to [reportLiveTest], and once tests are finished being run for this +/// suite, [noMoreLiveTests] should be called. Once the suite should be torn +/// down, [close] should be called. +class LiveSuiteController { + /// The [LiveSuite] being controlled. + late final liveSuite = _LiveSuite(this); + + /// The suite that's being run. + final RunnerSuite _suite; + + /// The future group that backs [LiveSuite.onComplete]. + /// + /// This contains all the futures from tests that are run in this suite. + final _onCompleteGroup = FutureGroup<void>(); + + /// The completer that backs [LiveSuite.onClose]. + /// + /// This is completed when the live suite is closed. + final _onCloseCompleter = Completer<void>(); + + /// The controller for [LiveSuite.onTestStarted]. + final _onTestStartedController = + StreamController<LiveTest>.broadcast(sync: true); + + /// The set that backs [LiveTest.passed]. + final _passed = <LiveTest>{}; + + /// The set that backs [LiveTest.skipped]. + final _skipped = <LiveTest>{}; + + /// The set that backs [LiveTest.failed]. + final _failed = <LiveTest>{}; + + /// The test exposed through [LiveTest.active]. + LiveTest? _active; + + /// Creates a controller for a live suite representing running the tests in + /// [suite]. + /// + /// Once this is called, the controller assumes responsibility for closing the + /// suite. The caller should call [LiveSuiteController.close] rather than + /// calling [RunnerSuite.close] directly. + LiveSuiteController(this._suite); + + /// Reports the status of [liveTest] through [liveSuite]. + /// + /// The live test is assumed to be a member of this suite. If [countSuccess] + /// is `true` (the default), the test is put into [passed] if it succeeds. + /// Otherwise, it's removed from [liveTests] entirely. + /// + /// Throws a [StateError] if called after [noMoreLiveTests]. + void reportLiveTest(LiveTest liveTest, {bool countSuccess = true}) { + if (_onTestStartedController.isClosed) { + throw StateError("Can't call reportLiveTest() after noMoreTests()."); + } + + assert(liveTest.suite == _suite); + assert(_active == null); + + _active = liveTest; + + liveTest.onStateChange.listen((state) { + if (state.status != Status.complete) return; + _active = null; + + if (state.result == Result.skipped) { + _skipped.add(liveTest); + } else if (state.result != Result.success) { + _passed.remove(liveTest); + _failed.add(liveTest); + } else if (countSuccess) { + _passed.add(liveTest); + // A passing test that was once failing was retried + _failed.remove(liveTest); + } + }); + + _onTestStartedController.add(liveTest); + + _onCompleteGroup.add(liveTest.onComplete); + } + + /// Indicates that all the live tests that are going to be provided for this + /// suite have already been provided. + void noMoreLiveTests() { + _onTestStartedController.close(); + _onCompleteGroup.close(); + } + + /// Closes the underlying suite. + Future close() => _closeMemo.runOnce(() async { + try { + await _suite.close(); + } finally { + _onCloseCompleter.complete(); + } + }); + final _closeMemo = AsyncMemoizer<void>(); +}
diff --git a/pkgs/test_core/lib/src/runner/load_exception.dart b/pkgs/test_core/lib/src/runner/load_exception.dart new file mode 100644 index 0000000..3fbd462 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/load_exception.dart
@@ -0,0 +1,34 @@ +// Copyright (c) 2015, 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:source_span/source_span.dart'; + +import '../util/errors.dart'; + +class LoadException implements Exception { + final String path; + + final Object innerError; + + LoadException(this.path, this.innerError); + + @override + String toString({bool color = false}) { + var buffer = StringBuffer(); + if (color) buffer.write('\u001b[31m'); // red + buffer.write('Failed to load "$path":'); + if (color) buffer.write('\u001b[0m'); // no color + + var innerString = getErrorMessage(innerError); + if (innerError is SourceSpanException) { + innerString = (innerError as SourceSpanException) + .toString(color: color) + .replaceFirst(' of $path', ''); + } + + buffer.write(innerString.contains('\n') ? '\n' : ' '); + buffer.write(innerString); + return buffer.toString(); + } +}
diff --git a/pkgs/test_core/lib/src/runner/load_suite.dart b/pkgs/test_core/lib/src/runner/load_suite.dart new file mode 100644 index 0000000..633739a --- /dev/null +++ b/pkgs/test_core/lib/src/runner/load_suite.dart
@@ -0,0 +1,220 @@ +// Copyright (c) 2015, 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 'package:stack_trace/stack_trace.dart'; +import 'package:test_api/scaffolding.dart' show Timeout; +import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports + +import '../util/io_stub.dart' if (dart.library.io) '../util/io.dart'; +import 'load_exception.dart'; +import 'plugin/environment.dart'; +import 'runner_suite.dart'; +import 'suite.dart'; + +/// The timeout for loading a test suite. +/// +/// We want this to be long enough that even a very large application being +/// compiled with dart2js doesn't trigger it, but short enough that it fires +/// before the host kills it. For example, Google's Forge service has a +/// 15-minute timeout. +final _timeout = const Duration(minutes: 12); + +/// A [Suite] emitted by a [Loader] that provides a test-like interface for +/// loading a test file. +/// +/// This is used to expose the current status of test loading to the user. It's +/// important to provide users visibility into what's taking a long time and +/// where failures occur. And since some tests may be loaded at the same time as +/// others are run, it's useful to provide that visibility in the form of a test +/// suite so that it can integrate well into the existing reporting interface +/// without too much extra logic. +/// +/// A suite is constructed with logic necessary to produce a test suite. As with +/// a normal test body, this logic isn't run until [LiveTest.run] is called. The +/// suite itself is returned by [suite] once it's available, but any errors or +/// prints will be emitted through the running [LiveTest]. +class LoadSuite extends Suite implements RunnerSuite { + @override + final environment = const PluginEnvironment(); + @override + final SuiteConfiguration config; + @override + final isDebugging = false; + @override + final onDebugging = StreamController<bool>().stream; + + @override + bool get isLoadSuite => true; + + /// A future that completes to the loaded suite once the suite's test has been + /// run and completed successfully. + /// + /// This will return `null` if the suite is unavailable for some reason (for + /// example if an error occurred while loading it). + Future<RunnerSuite?> get suite async => (await _suiteAndZone)?.suite; + + /// A future that completes to a pair of [suite] and the load test's [Zone]. + /// + /// This will return `null` if the suite is unavailable for some reason (for + /// example if an error occurred while loading it). + final Future<({RunnerSuite suite, Zone zone})?> _suiteAndZone; + + /// Returns the test that loads the suite. + /// + /// Load suites are guaranteed to only contain one test. This is a utility + /// method for accessing it directly. + Test get test => group.entries.single as Test; + + /// Creates a load suite named [name] on [platform]. + /// + /// [body] may return either a [RunnerSuite] or a [Future] that completes to a + /// [RunnerSuite]. Its return value is forwarded through [suite], although if + /// it throws an error that will be forwarded through the suite's test. + /// + /// If the the load test is closed before [body] is complete, it will close + /// the suite returned by [body] once it completes. + factory LoadSuite(String name, SuiteConfiguration config, + SuitePlatform platform, FutureOr<RunnerSuite?> Function() body, + {String? path}) { + var completer = Completer<({RunnerSuite suite, Zone zone})?>.sync(); + return LoadSuite._(name, config, platform, () { + var invoker = Invoker.current; + invoker!.addOutstandingCallback(); + + unawaited(() async { + RunnerSuite? suite; + try { + suite = await body(); + } catch (_) { + invoker.removeOutstandingCallback(); + rethrow; + } + if (completer.isCompleted) { + // If the load test has already been closed, close the suite it + // generated. + await suite?.close(); + return; + } + + completer.complete( + suite == null ? null : (suite: suite, zone: Zone.current)); + invoker.removeOutstandingCallback(); + }()); + + // If the test completes before the body callback, either an out-of-band + // error occurred or the test was canceled. Either way, we return a `null` + // suite. + invoker.liveTest.onComplete.then((_) { + if (!completer.isCompleted) completer.complete(); + }); + + // If the test is forcibly closed, let it complete, since load tests don't + // have timeouts. + invoker.onClose.then((_) => invoker.removeOutstandingCallback()); + }, completer.future, path: path, ignoreTimeouts: config.ignoreTimeouts); + } + + /// A utility constructor for a load suite that just throws [exception]. + /// + /// The suite's name will be based on [exception]'s path. + factory LoadSuite.forLoadException( + LoadException exception, SuiteConfiguration? config, + {SuitePlatform? platform, StackTrace? stackTrace}) { + stackTrace ??= Trace.current(); + + return LoadSuite( + 'loading ${exception.path}', + config ?? SuiteConfiguration.empty, + platform ?? currentPlatform(Runtime.vm, null), + () => Future.error(exception, stackTrace), + path: exception.path); + } + + /// A utility constructor for a load suite that just emits [suite]. + factory LoadSuite.forSuite(RunnerSuite suite) { + return LoadSuite( + 'loading ${suite.path}', suite.config, suite.platform, () => suite, + path: suite.path); + } + + LoadSuite._(String name, this.config, SuitePlatform platform, + void Function() body, this._suiteAndZone, + {required bool ignoreTimeouts, String? path}) + : super( + Group.root( + [LocalTest(name, Metadata(timeout: Timeout(_timeout)), body)]), + platform, + path: path, + ignoreTimeouts: ignoreTimeouts); + + /// A constructor used by [changeSuite]. + LoadSuite._changeSuite(LoadSuite old, this._suiteAndZone) + : config = old.config, + super(old.group, old.platform, + path: old.path, ignoreTimeouts: old.ignoreTimeouts); + + /// A constructor used by [filter]. + LoadSuite._filtered(LoadSuite old, Group filtered) + : config = old.config, + _suiteAndZone = old._suiteAndZone, + super(old.group, old.platform, + path: old.path, ignoreTimeouts: old.ignoreTimeouts); + + /// Creates a new [LoadSuite] that's identical to this one, but that + /// transforms [suite] once it's loaded. + /// + /// If [suite] completes to `null`, [change] won't be run. [change] is run + /// within the load test's zone, so any errors or prints it emits will be + /// associated with that test. + LoadSuite changeSuite(RunnerSuite? Function(RunnerSuite) change) { + return LoadSuite._changeSuite(this, _suiteAndZone.then((pair) { + if (pair == null) return null; + + var (:suite, :zone) = pair; + RunnerSuite? newSuite; + zone.runGuarded(() { + newSuite = change(suite); + }); + return newSuite == null ? null : (suite: newSuite!, zone: zone); + })); + } + + /// Runs the test and returns the suite. + /// + /// Rather than emitting errors through a [LiveTest], this just pipes them + /// through the return value. + Future<RunnerSuite?> getSuite() async { + var liveTest = test.load(this); + liveTest.onMessage.listen((message) => print(message.text)); + await liveTest.run(); + + if (liveTest.errors.isEmpty) return await suite; + + var error = liveTest.errors.first; + await Future<void>.error(error.error, error.stackTrace); + throw 'unreachable'; + } + + @override + LoadSuite filter(bool Function(Test) callback) { + var filtered = group.filter(callback); + filtered ??= Group.root([], metadata: metadata); + return LoadSuite._filtered(this, filtered); + } + + @override + Future close() async {} + + @override + Future<Map<String, dynamic>> gatherCoverage() => + throw UnsupportedError('Coverage is not supported for LoadSuite tests.'); +}
diff --git a/pkgs/test_core/lib/src/runner/loader.dart b/pkgs/test_core/lib/src/runner/loader.dart new file mode 100644 index 0000000..c37105d --- /dev/null +++ b/pkgs/test_core/lib/src/runner/loader.dart
@@ -0,0 +1,297 @@ +// Copyright (c) 2015, 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:source_span/source_span.dart'; +import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports +import 'package:yaml/yaml.dart'; + +import '../util/io.dart'; +import 'compiler_selection.dart'; +import 'configuration.dart'; +import 'hack_register_platform.dart'; +import 'load_exception.dart'; +import 'load_suite.dart'; +import 'parse_metadata.dart'; +import 'platform.dart'; +import 'plugin/customizable_platform.dart'; +import 'plugin/environment.dart'; +import 'runner_suite.dart'; +import 'suite.dart'; +import 'vm/platform.dart'; + +/// A class for finding test files and loading them into a runnable form. +class Loader { + /// The test runner configuration. + final _config = Configuration.current; + + /// All suites that have been created by the loader. + final _suites = <RunnerSuite>{}; + + /// Memoizers for platform plugins, indexed by the runtimes they support. + final _platformPlugins = <Runtime, AsyncMemoizer<PlatformPlugin>>{}; + + /// The functions to use to load [_platformPlugins]. + /// + /// These are passed to the plugins' async memoizers when a plugin is needed. + final _platformCallbacks = <Runtime, FutureOr<PlatformPlugin> Function()>{}; + + /// A map of all runtimes registered in [_platformCallbacks], indexed by + /// their string identifiers. + final _runtimesByIdentifier = <String, Runtime>{}; + + /// The user-provided settings for runtimes, as a list of settings that will + /// be merged together using [CustomizablePlatform.mergePlatformSettings]. + final _runtimeSettings = <Runtime, List<YamlMap>>{}; + + /// The user-provided settings for runtimes. + final _parsedRuntimeSettings = <Runtime, Object>{}; + + /// All plaforms supported by this [Loader]. + List<Runtime> get allRuntimes => List.unmodifiable(_platformCallbacks.keys); + + /// The runtime variables supported by this loader, in addition the default + /// variables that are always supported. + Iterable<String> get _runtimeVariables => + _platformCallbacks.keys.map((runtime) => runtime.identifier); + + /// Creates a new loader that loads tests on platforms defined in + /// [Configuration.current]. + Loader() { + _registerPlatformPlugin([Runtime.vm], VMPlatform.new); + + platformCallbacks.forEach((runtime, plugin) { + _registerPlatformPlugin([runtime], plugin); + }); + + _registerCustomRuntimes(); + + _config.validateRuntimes(allRuntimes); + + _registerRuntimeOverrides(); + } + + /// Registers a [PlatformPlugin] for [runtimes]. + void _registerPlatformPlugin( + Iterable<Runtime> runtimes, FutureOr<PlatformPlugin> Function() plugin) { + var memoizer = AsyncMemoizer<PlatformPlugin>(); + for (var runtime in runtimes) { + _platformPlugins[runtime] = memoizer; + _platformCallbacks[runtime] = plugin; + _runtimesByIdentifier[runtime.identifier] = runtime; + } + } + + /// Registers user-defined runtimes from [Configuration.defineRuntimes]. + void _registerCustomRuntimes() { + for (var customRuntime in _config.defineRuntimes.values) { + if (_runtimesByIdentifier.containsKey(customRuntime.identifier)) { + throw SourceSpanFormatException( + wordWrap( + 'The platform "${customRuntime.identifier}" already exists. ' + 'Use override_platforms to override it.'), + customRuntime.identifierSpan); + } + + var parent = _runtimesByIdentifier[customRuntime.parent]; + if (parent == null) { + throw SourceSpanFormatException( + 'Unknown platform.', customRuntime.parentSpan); + } + + var runtime = parent.extend(customRuntime.name, customRuntime.identifier); + _platformPlugins[runtime] = _platformPlugins[parent]!; + _platformCallbacks[runtime] = _platformCallbacks[parent]!; + _runtimesByIdentifier[runtime.identifier] = runtime; + + _runtimeSettings[runtime] = [customRuntime.settings]; + } + } + + /// Registers users' runtime settings from [Configuration.overrideRuntimes]. + void _registerRuntimeOverrides() { + for (var settings in _config.overrideRuntimes.values) { + var runtime = _runtimesByIdentifier[settings.identifier]; + _runtimeSettings + .putIfAbsent(runtime!, () => []) + .addAll(settings.settings); + } + } + + /// Returns the [Runtime] registered with this loader that's identified + /// by [identifier], or `null` if none can be found. + Runtime? findRuntime(String identifier) => _runtimesByIdentifier[identifier]; + + /// Loads all test suites in [dir] according to [suiteConfig]. + /// + /// This will load tests from files that match the global configuration's + /// filename glob. Any tests that fail to load will be emitted as + /// [LoadException]s. + /// + /// This emits [LoadSuite]s that must then be run to emit the actual + /// [RunnerSuite]s defined in the file. + Stream<LoadSuite> loadDir(String dir, SuiteConfiguration suiteConfig) { + return StreamGroup.merge( + Directory(dir).listSync(recursive: true).map((entry) { + if (entry is! File || !_config.filename.matches(p.basename(entry.path))) { + return const Stream.empty(); + } + + return loadFile(entry.path, suiteConfig); + })); + } + + /// Loads a test suite from the file at [path] according to [suiteConfig]. + /// + /// This emits [LoadSuite]s that must then be run to emit the actual + /// [RunnerSuite]s defined in the file. + /// + /// This will emit a [LoadException] if the file fails to load. + Stream<LoadSuite> loadFile( + String path, SuiteConfiguration suiteConfig) async* { + try { + suiteConfig = suiteConfig.merge(SuiteConfiguration.fromMetadata( + parseMetadata( + path, File(path).readAsStringSync(), _runtimeVariables.toSet()))); + } on ArgumentError catch (_) { + // Ignore the analyzer's error, since its formatting is much worse than + // the VM's or dart2js's. + } on FormatException catch (error, stackTrace) { + yield LoadSuite.forLoadException(LoadException(path, error), suiteConfig, + stackTrace: stackTrace); + return; + } + + if (_config.excludeTags.evaluate(suiteConfig.metadata.tags.contains)) { + return; + } + + for (var runtimeName in suiteConfig.runtimes) { + var runtime = findRuntime(runtimeName); + if (runtime == null) { + throw ArgumentError.value(runtimeName, 'platform', 'Unknown platform'); + } + final compilers = { + for (var selection + in suiteConfig.compilerSelections ?? <CompilerSelection>[]) + if (runtime.supportedCompilers.contains(selection.compiler) && + (selection.platformSelector == null || + selection.platformSelector! + .evaluate(currentPlatform(runtime, selection.compiler)))) + selection.compiler, + }; + if (compilers.isEmpty) compilers.add(runtime.defaultCompiler); + + for (var compiler in compilers) { + var platform = currentPlatform(runtime, compiler); + if (!suiteConfig.metadata.testOn.evaluate(platform)) continue; + + var platformConfig = suiteConfig.forPlatform(platform); + + // Don't load a skipped suite. + if (platformConfig.metadata.skip && !platformConfig.runSkipped) { + yield LoadSuite.forSuite(RunnerSuite( + const PluginEnvironment(), + platformConfig, + Group.root([LocalTest('(suite)', platformConfig.metadata, () {})], + metadata: platformConfig.metadata), + platform, + path: path)); + continue; + } + + yield LoadSuite('loading $path', platformConfig, platform, () async { + var memo = _platformPlugins[platform.runtime]!; + + var retriesLeft = suiteConfig.metadata.retry; + while (true) { + try { + var plugin = + await memo.runOnce(_platformCallbacks[platform.runtime]!); + _customizePlatform(plugin, platform.runtime); + var suite = await plugin.load(path, platform, platformConfig, + {'platformVariables': _runtimeVariables.toList()}); + if (suite != null) _suites.add(suite); + return suite; + } on Object catch (error, stackTrace) { + if (retriesLeft > 0) { + retriesLeft--; + print('Retrying load of $path in 1s ($retriesLeft remaining)'); + await Future<void>.delayed(const Duration(seconds: 1)); + continue; + } + if (error is LoadException) { + rethrow; + } + await Future<void>.error(LoadException(path, error), stackTrace); + return null; + } + } + }, path: path); + } + } + } + + /// Passes user-defined settings to [plugin] if necessary. + void _customizePlatform(PlatformPlugin plugin, Runtime runtime) { + var parsed = _parsedRuntimeSettings[runtime]; + if (parsed != null) { + (plugin as CustomizablePlatform).customizePlatform(runtime, parsed); + return; + } + + var settings = _runtimeSettings[runtime]; + if (settings == null) return; + + if (plugin is CustomizablePlatform) { + parsed = settings + .map(plugin.parsePlatformSettings) + .reduce(plugin.mergePlatformSettings); + plugin.customizePlatform(runtime, parsed); + _parsedRuntimeSettings[runtime] = parsed; + } else { + String identifier; + SourceSpan span; + if (runtime.isChild) { + identifier = runtime.parent!.identifier; + span = _config.defineRuntimes[runtime.identifier]!.parentSpan; + } else { + identifier = runtime.identifier; + span = _config.overrideRuntimes[runtime.identifier]!.identifierSpan; + } + + throw SourceSpanFormatException( + 'The "$identifier" platform can\'t be customized.', span); + } + } + + Future closeEphemeral() async { + await Future.wait(_platformPlugins.values.map((memo) async { + if (!memo.hasRun) return; + await (await memo.future).closeEphemeral(); + })); + } + + /// Closes the loader and releases all resources allocated by it. + Future close() => _closeMemo.runOnce(() async { + await Future.wait([ + Future.wait(_platformPlugins.values.map((memo) async { + if (!memo.hasRun) return; + await (await memo.future).close(); + })), + Future.wait(_suites.map((suite) => suite.close())) + ]); + + _platformPlugins.clear(); + _platformCallbacks.clear(); + _suites.clear(); + }); + final _closeMemo = AsyncMemoizer<void>(); +}
diff --git a/pkgs/test_core/lib/src/runner/no_tests_found_exception.dart b/pkgs/test_core/lib/src/runner/no_tests_found_exception.dart new file mode 100644 index 0000000..0f65837 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/no_tests_found_exception.dart
@@ -0,0 +1,13 @@ +// Copyright (c) 2021, 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. + +/// An expected exception thrown when there were no tests found to run. +class NoTestsFoundException implements Exception { + final String message; + + NoTestsFoundException(this.message); + + @override + String toString() => message; +}
diff --git a/pkgs/test_core/lib/src/runner/package_version.dart b/pkgs/test_core/lib/src/runner/package_version.dart new file mode 100644 index 0000000..0ce77d3 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/package_version.dart
@@ -0,0 +1,23 @@ +// Copyright (c) 2020, 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:package_config/package_config.dart'; +import 'package:path/path.dart' as p; + +import '../util/detaching_future.dart'; +import '../util/package_config.dart'; + +/// A comment which forces the language version to be that of the current +/// packages default. +/// +/// If the cwd is not a package, this returns an empty string which ends up +/// defaulting to the current sdk version. +Future<String> get rootPackageLanguageVersionComment => + _rootPackageLanguageVersionComment.asFuture; +final _rootPackageLanguageVersionComment = DetachingFuture(() async { + var packageConfig = await loadPackageConfigUri(await packageConfigUri); + var rootPackage = packageConfig.packageOf(Uri.file(p.absolute('foo.dart'))); + if (rootPackage == null) return '// <unknown-language-version>'; + return '// @dart=${rootPackage.languageVersion}'; +}());
diff --git a/pkgs/test_core/lib/src/runner/parse_metadata.dart b/pkgs/test_core/lib/src/runner/parse_metadata.dart new file mode 100644 index 0000000..fd2b5ee --- /dev/null +++ b/pkgs/test_core/lib/src/runner/parse_metadata.dart
@@ -0,0 +1,514 @@ +// Copyright (c) 2015, 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:analyzer/dart/analysis/utilities.dart'; +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:path/path.dart' as p; +import 'package:source_span/source_span.dart'; +import 'package:test_api/scaffolding.dart' show Timeout; +import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/platform_selector.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/util/identifier_regex.dart'; // ignore: implementation_imports + +import '../util/dart.dart'; + +/// Parse the test metadata for the test file at [path] with [contents]. +/// +/// The [platformVariables] are the set of variables that are valid for platform +/// selectors in suite metadata, in addition to the built-in variables that are +/// allowed everywhere. +/// +/// Throws an [AnalysisError] if parsing fails or a [FormatException] if the +/// test annotations are incorrect. +Metadata parseMetadata( + String path, String contents, Set<String> platformVariables) => + _Parser(path, contents, platformVariables).parse(); + +/// A parser for test suite metadata. +class _Parser { + /// The path to the test suite. + final String _path; + + /// The set of variables that are valid for platform selectors, in addition to + /// the built-in variables that are allowed everywhere. + final Set<String> _platformVariables; + + /// All annotations at the top of the file. + late final List<Annotation> _annotations; + + /// All prefixes defined by imports in this file. + late final Set<String> _prefixes; + + /// The actual contents of the file. + final String _contents; + + /// The language version override comment if one was present, otherwise null. + String? _languageVersionComment; + + _Parser(this._path, this._contents, this._platformVariables) { + var result = + parseString(content: _contents, path: _path, throwIfDiagnostics: false); + var directives = result.unit.directives; + _annotations = directives.isEmpty ? [] : directives.first.metadata; + _languageVersionComment = result.unit.languageVersionToken?.value(); + + // We explicitly *don't* just look for "package:test" imports here, + // because it could be re-exported from another library. + _prefixes = directives + .map((directive) { + if (directive is ImportDirective) { + return directive.prefix?.name; + } else { + return null; + } + }) + .whereType<String>() + .toSet(); + } + + /// Parses the metadata. + Metadata parse() { + Timeout? timeout; + PlatformSelector? testOn; + Object? /*String|bool*/ skip; + Map<PlatformSelector, Metadata>? onPlatform; + Set<String>? tags; + int? retry; + + for (var annotation in _annotations) { + var pair = + _resolveConstructor(annotation.name, annotation.constructorName); + var (name, constructorName) = pair; + + if (name == 'TestOn') { + _assertSingle(testOn, 'TestOn', annotation); + testOn = _parseTestOn(annotation); + } else if (name == 'Timeout') { + _assertSingle(timeout, 'Timeout', annotation); + timeout = _parseTimeout(annotation, constructorName); + } else if (name == 'Skip') { + _assertSingle(skip, 'Skip', annotation); + skip = _parseSkip(annotation); + } else if (name == 'OnPlatform') { + _assertSingle(onPlatform, 'OnPlatform', annotation); + onPlatform = _parseOnPlatform(annotation); + } else if (name == 'Tags') { + _assertSingle(tags, 'Tags', annotation); + tags = _parseTags(annotation); + } else if (name == 'Retry') { + retry = _parseRetry(annotation); + } + } + + return Metadata( + testOn: testOn, + timeout: timeout, + skip: skip == null ? null : true, + skipReason: skip is String ? skip : null, + onPlatform: onPlatform, + tags: tags, + retry: retry, + languageVersionComment: _languageVersionComment); + } + + /// Parses a `@TestOn` annotation. + /// + /// [annotation] is the annotation. + PlatformSelector _parseTestOn(Annotation annotation) => + _parsePlatformSelector(annotation.arguments!.arguments.first); + + /// Parses an [expression] that should contain a string representing a + /// [PlatformSelector]. + PlatformSelector _parsePlatformSelector(Expression expression) { + var literal = _parseString(expression); + return _contextualize( + literal, + () => PlatformSelector.parse(literal.stringValue!) + ..validate(_platformVariables)); + } + + /// Parses a `@Retry` annotation. + /// + /// [annotation] is the annotation. + int _parseRetry(Annotation annotation) => + _parseInt(annotation.arguments!.arguments.first); + + /// Parses a `@Timeout` annotation. + /// + /// [annotation] is the annotation. [constructorName] is the name of the named + /// constructor for the annotation, if any. + Timeout _parseTimeout(Annotation annotation, String? constructorName) { + if (constructorName == 'none') { + return Timeout.none; + } + + var args = annotation.arguments!.arguments; + if (constructorName == null) return Timeout(_parseDuration(args.first)); + return Timeout.factor(_parseNum(args.first)); + } + + /// Parses a `Timeout` constructor. + Timeout _parseTimeoutConstructor(Expression constructor) { + var name = _findConstructorName(constructor, 'Timeout'); + var arguments = _parseArguments(constructor); + if (name == null) return Timeout(_parseDuration(arguments.first)); + if (name == 'factor') return Timeout.factor(_parseNum(arguments.first)); + throw SourceSpanFormatException('Invalid timeout', _spanFor(constructor)); + } + + /// Parses a `@Skip` annotation. + /// + /// [annotation] is the annotation. + /// + /// Returns either `true` or a reason string. + dynamic _parseSkip(Annotation annotation) { + var args = annotation.arguments!.arguments; + return args.isEmpty ? true : _parseString(args.first).stringValue; + } + + /// Parses a `Skip` constructor. + /// + /// Returns either `true` or a reason string. + dynamic _parseSkipConstructor(Expression constructor) { + _findConstructorName(constructor, 'Skip'); + var arguments = _parseArguments(constructor); + return arguments.isEmpty ? true : _parseString(arguments.first).stringValue; + } + + /// Parses a `@Tags` annotation. + /// + /// [annotation] is the annotation. + Set<String> _parseTags(Annotation annotation) { + return _parseList(annotation.arguments!.arguments.first) + .map((tagExpression) { + var name = _parseString(tagExpression).stringValue!; + if (name.contains(anchoredHyphenatedIdentifier)) return name; + + throw SourceSpanFormatException( + 'Invalid tag name. Tags must be (optionally hyphenated) Dart ' + 'identifiers.', + _spanFor(tagExpression)); + }).toSet(); + } + + /// Parses an `@OnPlatform` annotation. + /// + /// [annotation] is the annotation. + Map<PlatformSelector, Metadata> _parseOnPlatform(Annotation annotation) { + return _parseMap(annotation.arguments!.arguments.first, + key: _parsePlatformSelector, value: (value) { + var expressions = <AstNode>[]; + if (value is ListLiteral) { + expressions = _parseList(value); + } else if (value is InstanceCreationExpression || + value is PrefixedIdentifier || + value is MethodInvocation) { + expressions = [value]; + } else { + throw SourceSpanFormatException( + 'Expected a Timeout, Skip, or List of those.', _spanFor(value)); + } + + Timeout? timeout; + Object? skip; + for (var expression in expressions) { + if (expression is InstanceCreationExpression) { + var className = expression.constructorName.type.name2.lexeme; + + if (className == 'Timeout') { + _assertSingle(timeout, 'Timeout', expression); + timeout = _parseTimeoutConstructor(expression); + continue; + } else if (className == 'Skip') { + _assertSingle(skip, 'Skip', expression); + skip = _parseSkipConstructor(expression); + continue; + } + } else if (expression is PrefixedIdentifier && + expression.prefix.name == 'Timeout') { + if (expression.identifier.name != 'none') { + throw SourceSpanFormatException( + 'Undefined value.', _spanFor(expression)); + } + + _assertSingle(timeout, 'Timeout', expression); + timeout = Timeout.none; + continue; + } else if (expression is MethodInvocation) { + var className = + _typeNameFromMethodInvocation(expression, ['Timeout', 'Skip']); + if (className == 'Timeout') { + _assertSingle(timeout, 'Timeout', expression); + timeout = _parseTimeoutConstructor(expression); + continue; + } else if (className == 'Skip') { + _assertSingle(skip, 'Skip', expression); + skip = _parseSkipConstructor(expression); + continue; + } + } + + throw SourceSpanFormatException( + 'Expected a Timeout or Skip.', _spanFor(expression)); + } + + return Metadata.parse(timeout: timeout, skip: skip); + }); + } + + /// Parses a `const Duration` expression. + Duration _parseDuration(Expression expression) { + _findConstructorName(expression, 'Duration'); + + var arguments = _parseArguments(expression); + var values = _parseNamedArguments(arguments) + .map((key, value) => MapEntry(key, _parseInt(value))); + + return Duration( + days: values['days'] ?? 0, + hours: values['hours'] ?? 0, + minutes: values['minutes'] ?? 0, + seconds: values['seconds'] ?? 0, + milliseconds: values['milliseconds'] ?? 0, + microseconds: values['microseconds'] ?? 0); + } + + Map<String, Expression> _parseNamedArguments( + NodeList<Expression> arguments) => + { + for (var a in arguments.whereType<NamedExpression>()) + a.name.label.name: a.expression + }; + + /// Asserts that [existing] is null. + /// + /// [name] is the name of the annotation and [node] is its location, used for + /// error reporting. + void _assertSingle(Object? existing, String name, AstNode node) { + if (existing == null) return; + throw SourceSpanFormatException( + 'Only a single $name may be used.', _spanFor(node)); + } + + NodeList<Expression> _parseArguments(Expression expression) { + if (expression is InstanceCreationExpression) { + return expression.argumentList.arguments; + } + if (expression is MethodInvocation) { + return expression.argumentList.arguments; + } + throw SourceSpanFormatException( + 'Expected an instantiation', _spanFor(expression)); + } + + /// Resolves a constructor name from its type [identifier] and its + /// [constructorName]. + /// + /// Since the parsed file isn't fully resolved, this is necessary to + /// disambiguate between prefixed names and named constructors. + (String, String?) _resolveConstructor( + Identifier identifier, SimpleIdentifier? constructorName) { + // The syntax is ambiguous between named constructors and prefixed + // annotations, so we need to resolve that ambiguity using the known + // prefixes. The analyzer parses "new x.y()" as prefix "x", annotation "y", + // and named constructor null. It parses "new x.y.z()" as prefix "x", + // annotation "y", and named constructor "z". + String className; + String? namedConstructor; + if (identifier is PrefixedIdentifier && + !_prefixes.contains(identifier.prefix.name) && + constructorName == null) { + className = identifier.prefix.name; + namedConstructor = identifier.identifier.name; + } else { + className = identifier is PrefixedIdentifier + ? identifier.identifier.name + : identifier.name; + if (constructorName != null) namedConstructor = constructorName.name; + } + return (className, namedConstructor); + } + + /// Parses a constructor invocation for [className]. + /// + /// Returns the name of the named constructor used, or null if the default + /// constructor is used. + /// If [expression] is not an instantiation of a [className] throws. + String? _findConstructorName(Expression expression, String className) { + if (expression is InstanceCreationExpression) { + return _findConstructorNameFromInstantiation(expression, className); + } + if (expression is MethodInvocation) { + return _findConstructorNameFromMethod(expression, className); + } + throw SourceSpanFormatException( + 'Expected a $className.', _spanFor(expression)); + } + + String? _findConstructorNameFromInstantiation( + InstanceCreationExpression constructor, String className) { + var actualClassName = constructor.constructorName.type.name2.lexeme; + var constructorName = constructor.constructorName.name?.name; + + if (actualClassName != className) { + throw SourceSpanFormatException( + 'Expected a $className.', _spanFor(constructor)); + } + + return constructorName; + } + + String? _findConstructorNameFromMethod( + MethodInvocation constructor, String className) { + var target = constructor.target; + if (target != null) { + // target could be an import prefix or a different class. Assume that + // named constructor on a different class won't match the class name we + // are looking for. + // Example: `test.Timeout()` + if (constructor.methodName.name == className) return null; + // target is an optionally prefixed class, method is named constructor + // Examples: `Timeout.factor(2)`, `test.Timeout.factor(2)` + String? parsedName; + if (target is SimpleIdentifier) parsedName = target.name; + if (target is PrefixedIdentifier) parsedName = target.identifier.name; + if (parsedName != className) { + throw SourceSpanFormatException( + 'Expected a $className.', _spanFor(constructor)); + } + return constructor.methodName.name; + } + // No target, must be an unnamed constructor + // Example `Timeout()` + if (constructor.methodName.name != className) { + throw SourceSpanFormatException( + 'Expected a $className.', _spanFor(constructor)); + } + return null; + } + + /// Returns a type from [candidates] that _may_ be a type instantiated by + /// [constructor]. + /// + /// This can be fooled - for instance the invocation `foo.Bar()` may look like + /// a prefixed instantiation of a `Bar` even though it is a named constructor + /// instantiation of a `foo`, or a method invocation on a variable `foo`, or + /// ... + /// + /// Similarly `Baz.another` may look like the named constructor invocation of + /// a `Baz`even though it is a prefixed instantiation of an `another`, or a + /// method invocation on a variable `Baz`, or ... + String? _typeNameFromMethodInvocation( + MethodInvocation constructor, List<String> candidates) { + var methodName = constructor.methodName.name; + // Examples: `Timeout()`, `test.Timeout()` + if (candidates.contains(methodName)) return methodName; + var target = constructor.target; + // Example: `SomeOtherClass()` + if (target == null) return null; + if (target is SimpleIdentifier) { + // Example: `Timeout.factor()` + if (candidates.contains(target.name)) return target.name; + } + if (target is PrefixedIdentifier) { + // Looks like `some_prefix.SomeTarget.someMethod` - "SomeTarget" is the + // only potential type name. + // Example: `test.Timeout.factor()` + if (candidates.contains(target.identifier.name)) { + return target.identifier.name; + } + } + return null; + } + + /// Parses a Map literal. + /// + /// By default, returns [Expression] keys and values. These can be overridden + /// with the [key] and [value] parameters. + Map<K, V> _parseMap<K, V>(Expression expression, + {K Function(Expression)? key, V Function(Expression)? value}) { + key ??= (expression) => expression as K; + value ??= (expression) => expression as V; + + if (expression is! SetOrMapLiteral) { + throw SourceSpanFormatException('Expected a Map.', _spanFor(expression)); + } + + var map = <K, V>{}; + for (var element in expression.elements) { + if (element is MapLiteralEntry) { + map[key(element.key)] = value(element.value); + } else { + throw SourceSpanFormatException( + 'Expected a map entry.', _spanFor(element)); + } + } + return map; + } + + /// Parses a List literal. + List<Expression> _parseList(Expression expression) { + if (expression is! ListLiteral) { + throw SourceSpanFormatException('Expected a List.', _spanFor(expression)); + } + + var list = expression; + + return list.elements.map((e) { + if (e is! Expression) { + throw SourceSpanFormatException( + 'Expected only literal elements.', _spanFor(e)); + } + return e; + }).toList(); + } + + /// Parses a constant number literal. + num _parseNum(Expression expression) { + if (expression is IntegerLiteral && expression.value != null) { + return expression.value!; + } + if (expression is DoubleLiteral) return expression.value; + throw SourceSpanFormatException('Expected a number.', _spanFor(expression)); + } + + /// Parses a constant int literal. + int _parseInt(Expression expression) { + if (expression is IntegerLiteral && expression.value != null) { + return expression.value!; + } + throw SourceSpanFormatException( + 'Expected an integer.', _spanFor(expression)); + } + + /// Parses a constant String literal. + StringLiteral _parseString(Expression expression) { + if (expression is StringLiteral && expression.stringValue != null) { + return expression; + } + throw SourceSpanFormatException( + 'Expected a String literal.', _spanFor(expression)); + } + + /// Creates a [SourceSpan] for [node]. + SourceSpan _spanFor(AstNode node) { + // Load a SourceFile from scratch here since we're only ever going to emit + // one error per file anyway. + return SourceFile.fromString(_contents, url: p.toUri(_path)) + .span(node.offset, node.end); + } + + /// Runs [fn] and contextualizes any [SourceSpanFormatException]s that occur + /// in it relative to [literal]. + T _contextualize<T>(StringLiteral literal, T Function() fn) { + try { + return fn(); + } on SourceSpanFormatException catch (error) { + var file = SourceFile.fromString(_contents, url: p.toUri(_path)); + var span = contextualizeSpan(error.span!, literal, file); + if (span == null) rethrow; + throw SourceSpanFormatException(error.message, span); + } + } +}
diff --git a/pkgs/test_core/lib/src/runner/platform.dart b/pkgs/test_core/lib/src/runner/platform.dart new file mode 100644 index 0000000..3048052 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/platform.dart
@@ -0,0 +1,43 @@ +// Copyright (c) 2016, 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_api/src/backend/suite_platform.dart'; // ignore: implementation_imports + +import 'environment.dart'; +import 'runner_suite.dart'; +import 'suite.dart'; + +/// A class that defines a platform for which test suites can be loaded. +/// +/// A minimal plugin must define [loadChannel], which connects to a client in +/// which the tests are defined. This is enough to support most of the test +/// runner's functionality. +/// +/// In order to support interactive debugging, a plugin must override [load] as +/// well, which returns a [RunnerSuite] that can contain a custom [Environment] +/// and control debugging metadata such as [RunnerSuite.isDebugging] and +/// [RunnerSuite.onDebugging]. The plugin must create this suite by calling the +/// [deserializeSuite] helper function. +/// +/// A platform plugin can be registered by passing it to [Loader.new]'s +/// `plugins` parameter. +abstract class PlatformPlugin { + /// Loads the runner suite for the test file at [path] using [platform], with + /// [suiteConfig] encoding the suite-specific configuration. + /// + /// By default, this just calls [loadChannel] and passes its result to + /// [deserializeSuite]. However, it can be overridden to provide more + /// fine-grained control over the [RunnerSuite], including providing a custom + /// implementation of [Environment]. + /// + /// Subclasses overriding this method must call [deserializeSuite] in + /// `platform_helpers.dart` to obtain a [RunnerSuiteController]. They must + /// pass the opaque [message] parameter to the [deserializeSuite] call. + Future<RunnerSuite?> load(String path, SuitePlatform platform, + SuiteConfiguration suiteConfig, Map<String, Object?> message); + + Future closeEphemeral() async {} + + Future close() async {} +}
diff --git a/pkgs/test_core/lib/src/runner/plugin/customizable_platform.dart b/pkgs/test_core/lib/src/runner/plugin/customizable_platform.dart new file mode 100644 index 0000000..0ded7ae --- /dev/null +++ b/pkgs/test_core/lib/src/runner/plugin/customizable_platform.dart
@@ -0,0 +1,55 @@ +// 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:test_api/src/backend/runtime.dart'; // ignore: implementation_imports +import 'package:yaml/yaml.dart'; + +import './../platform.dart'; + +/// An interface for [PlatformPlugin]s that support per-platform customization. +/// +/// If a [PlatformPlugin] implements this, the user will be able to override the +/// [Runtime]s it supports using the +/// [`override_platforms`][override_platforms] configuration field, and define +/// new runtimes based on them using the [`define_platforms`][define_platforms] +/// field. The custom settings will be passed to the plugin using +/// [customizePlatform]. +/// +/// [override_platforms]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#override_platforms +/// [define_platforms]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#define_platforms +/// +/// Plugins that implement this **must** support children of recognized runtimes +/// (created by [Runtime.extend]) in their [load] methods. +abstract class CustomizablePlatform<T extends Object> extends PlatformPlugin { + /// Parses user-provided [settings] for a custom platform into a + /// plugin-defined format. + /// + /// The [settings] come from a user's configuration file. The parsed output + /// will be passed to [customizePlatform]. + /// + /// Subclasses should throw [SourceSpanFormatException]s if [settings] + /// contains invalid configuration. Unrecognized fields should be ignored if + /// possible. + T parsePlatformSettings(YamlMap settings); + + /// Merges [settings1] with [settings2] and returns a new settings object that + /// includes the configuration of both. + /// + /// When the settings conflict, [settings2] should take priority. + /// + /// This is used to merge global settings with local settings, or a custom + /// platform's settings with its parent's. + T mergePlatformSettings(T settings1, T settings2); + + /// Defines user-provided [settings] for [runtime]. + /// + /// The [runtime] is a runtime this plugin was declared to accept when + /// registered with [Loader.registerPlatformPlugin], or a runtime whose + /// [Runtime.parent] is one of those runtimes. Subclasses should customize the + /// behavior for these runtimes when [loadChannel] or [load] is called with + /// the given [runtime], using the [settings] which are parsed by + /// [parsePlatformSettings]. This is guaranteed to be called before either + /// `load` method. + void customizePlatform(Runtime runtime, T settings); +}
diff --git a/pkgs/test_core/lib/src/runner/plugin/environment.dart b/pkgs/test_core/lib/src/runner/plugin/environment.dart new file mode 100644 index 0000000..7558d62 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/plugin/environment.dart
@@ -0,0 +1,29 @@ +// Copyright (c) 2016, 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 'package:async/async.dart'; + +import '../environment.dart'; + +/// The default environment for platform plugins. +class PluginEnvironment implements Environment { + @override + final supportsDebugging = false; + @override + Stream<void> get onRestart => StreamController<void>.broadcast().stream; + + const PluginEnvironment(); + + @override + Uri? get observatoryUrl => null; + + @override + Uri? get remoteDebuggerUrl => null; + + @override + CancelableOperation displayPause() => throw UnsupportedError( + 'PluginEnvironment.displayPause is not supported.'); +}
diff --git a/pkgs/test_core/lib/src/runner/plugin/platform_helpers.dart b/pkgs/test_core/lib/src/runner/plugin/platform_helpers.dart new file mode 100644 index 0000000..450dedb --- /dev/null +++ b/pkgs/test_core/lib/src/runner/plugin/platform_helpers.dart
@@ -0,0 +1,162 @@ +// Copyright (c) 2016, 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:stack_trace/stack_trace.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test_api/backend.dart' + show Metadata, RemoteException, SuitePlatform; +import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports + +import '../configuration.dart'; +import '../environment.dart'; +import '../load_exception.dart'; +import '../runner_suite.dart'; +import '../runner_test.dart'; +import '../suite.dart'; + +/// A helper method for creating a [RunnerSuiteController] containing tests +/// that communicate over [channel]. +/// +/// This returns a controller so that the caller has a chance to control the +/// runner suite's debugging state based on plugin-specific logic. +/// +/// If the suite is closed, this will close [channel]. +/// +/// The [message] parameter is an opaque object passed from the runner to +/// [PlatformPlugin.load]. Plugins shouldn't interact with it other than to pass +/// it on to [deserializeSuite]. +/// +/// If [mapper] is passed, it will be used to adjust stack traces for any errors +/// emitted by tests. +/// +/// [gatherCoverage] is a callback which returns a hit-map containing merged +/// coverage report suitable for use with `package:coverage`. +RunnerSuiteController deserializeSuite( + String path, + SuitePlatform platform, + SuiteConfiguration suiteConfig, + Environment environment, + StreamChannel<Object?> channel, + Object /*Map<String, Object?>*/ message, + {Future<Map<String, dynamic>> Function()? gatherCoverage}) { + var disconnector = Disconnector<Object?>(); + var suiteChannel = MultiChannel<Object?>(channel.transform(disconnector)); + + suiteChannel.sink.add(<String, Object?>{ + 'type': 'initial', + 'platform': platform.serialize(), + 'metadata': suiteConfig.metadata.serialize(), + 'asciiGlyphs': Platform.isWindows, + 'path': path, + 'collectTraces': Configuration.current.reporter == 'json' || + Configuration.current.fileReporters.containsKey('json') || + suiteConfig.testSelections.any( + (selection) => selection.line != null || selection.col != null), + 'noRetry': Configuration.current.noRetry, + 'foldTraceExcept': Configuration.current.foldTraceExcept.toList(), + 'foldTraceOnly': Configuration.current.foldTraceOnly.toList(), + 'allowDuplicateTestNames': suiteConfig.allowDuplicateTestNames, + 'ignoreTimeouts': suiteConfig.ignoreTimeouts, + ...message as Map<String, dynamic>, + }); + + var completer = Completer<Group>(); + + var loadSuiteZone = Zone.current; + void handleError(Object error, StackTrace stackTrace) { + disconnector.disconnect(); + + if (completer.isCompleted) { + // If we've already provided a controller, send the error to the + // LoadSuite. This will cause the virtual load test to fail, which will + // notify the user of the error. + loadSuiteZone.handleUncaughtError(error, stackTrace); + } else { + completer.completeError(error, stackTrace); + } + } + + suiteChannel.stream.cast<Map<String, Object?>>().listen( + (response) { + switch (response['type'] as String) { + case 'print': + print(response['line']); + break; + + case 'loadException': + handleError(LoadException(path, response['message'] as Object), + Trace.current()); + break; + + case 'error': + var asyncError = RemoteException.deserialize( + response['error'] as Map<String, dynamic>); + handleError( + LoadException(path, asyncError.error), asyncError.stackTrace); + break; + + case 'success': + var deserializer = _Deserializer(suiteChannel); + completer.complete( + deserializer.deserializeGroup(response['root'] as Map)); + break; + } + }, + onError: handleError, + onDone: () { + if (completer.isCompleted) return; + completer.completeError( + LoadException(path, 'Connection closed before test suite loaded.'), + Trace.current()); + }); + + return RunnerSuiteController( + environment, suiteConfig, suiteChannel, completer.future, platform, + path: path, + onClose: () => disconnector.disconnect().onError(handleError), + gatherCoverage: gatherCoverage); +} + +/// A utility class for storing state while deserializing tests. +class _Deserializer { + /// The channel over which tests communicate. + final MultiChannel _channel; + + _Deserializer(this._channel); + + /// Deserializes [group] into a concrete [Group]. + Group deserializeGroup(Map group) { + var metadata = Metadata.deserialize(group['metadata'] as Map); + return Group( + group['name'] as String, + (group['entries'] as List).map((entry) { + var map = entry as Map; + if (map['type'] == 'group') return deserializeGroup(map); + return _deserializeTest(map)!; + }), + metadata: metadata, + trace: group['trace'] == null + ? null + : Trace.parse(group['trace'] as String), + setUpAll: _deserializeTest(group['setUpAll'] as Map?), + tearDownAll: _deserializeTest(group['tearDownAll'] as Map?)); + } + + /// Deserializes [test] into a concrete [Test] class. + /// + /// Returns `null` if [test] is `null`. + Test? _deserializeTest(Map? test) { + if (test == null) return null; + + var metadata = Metadata.deserialize(test['metadata'] as Map); + var trace = + test['trace'] == null ? null : Trace.parse(test['trace'] as String); + var testChannel = _channel.virtualChannel((test['channel'] as num).toInt()); + return RunnerTest(test['name'] as String, metadata, trace, testChannel); + } +}
diff --git a/pkgs/test_core/lib/src/runner/plugin/remote_platform_helpers.dart b/pkgs/test_core/lib/src/runner/plugin/remote_platform_helpers.dart new file mode 100644 index 0000000..8eb2b76 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/plugin/remote_platform_helpers.dart
@@ -0,0 +1,51 @@ +// Copyright (c) 2016, 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:stream_channel/stream_channel.dart'; +import 'package:test_api/backend.dart' + show RemoteListener, StackTraceFormatter, StackTraceMapper; + +/// Returns a channel that will emit a serialized representation of the tests +/// defined in [getMain]. +/// +/// This channel is used to control the tests. Platform plugins should forward +/// it `deserializeSuite`. It's guaranteed to communicate using only +/// JSON-serializable values. +/// +/// Any errors thrown within [getMain], synchronously or not, will be forwarded +/// to the load test for this suite. Prints will similarly be forwarded to that +/// test's print stream. +/// +/// If [hidePrints] is `true` (the default), calls to `print()` within this +/// suite will not be forwarded to the parent zone's print handler. However, the +/// caller may want them to be forwarded in (for example) a browser context +/// where they'll be visible in the development console. +/// +/// If [beforeLoad] is passed, it's called before the tests have been declared +/// for this worker. +StreamChannel<Object?> serializeSuite(Function Function() getMain, + {bool hidePrints = true, + Future Function( + StreamChannel<Object?> Function(String name) suiteChannel)? + beforeLoad}) => + RemoteListener.start( + getMain, + hidePrints: hidePrints, + beforeLoad: beforeLoad, + ); + +/// Sets the stack trace mapper for the current test suite. +/// +/// This is used to convert JavaScript stack traces into their Dart equivalents +/// using source maps. It should be set before any tests run, usually in the +/// `onLoad()` callback to [serializeSuite]. +void setStackTraceMapper(StackTraceMapper mapper) { + var formatter = StackTraceFormatter.current; + if (formatter == null) { + throw StateError( + 'setStackTraceMapper() may only be called within a test worker.'); + } + + formatter.configure(mapper: mapper); +}
diff --git a/pkgs/test_core/lib/src/runner/plugin/shared_platform_helpers.dart b/pkgs/test_core/lib/src/runner/plugin/shared_platform_helpers.dart new file mode 100644 index 0000000..4fdcf23 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/plugin/shared_platform_helpers.dart
@@ -0,0 +1,21 @@ +// Copyright (c) 2023, 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 'dart:io'; + +import 'package:async/async.dart'; +import 'package:stream_channel/stream_channel.dart'; + +/// Converts a raw [Socket] into a [StreamChannel] of JSON objects. +/// +/// JSON messages are separated by newlines. +StreamChannel<Object?> jsonSocketStreamChannel(Socket socket) => + StreamChannel.withGuarantees(socket, socket) + .cast<List<int>>() + .transform(StreamChannelTransformer.fromCodec(utf8)) + .transformStream(const LineSplitter()) + .transformSink(StreamSinkTransformer.fromHandlers( + handleData: (original, sink) => sink.add('$original\n'))) + .transform(jsonDocument);
diff --git a/pkgs/test_core/lib/src/runner/reporter.dart b/pkgs/test_core/lib/src/runner/reporter.dart new file mode 100644 index 0000000..b179337 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/reporter.dart
@@ -0,0 +1,31 @@ +// Copyright (c) 2015, 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. + +/// An interface for classes that watch the progress of an Engine and report it +/// to the user. +/// +/// A reporter should subscribe to the Engine's events as soon as it's created. +abstract class Reporter { + /// Pauses the reporter's output. + /// + /// Subclasses should buffer any events from the engine while they're paused. + /// They should also ensure that this does nothing if the reporter is already + /// paused. + void pause(); + + /// Resumes the reporter's output after being [paused]. + /// + /// Subclasses should ensure that this does nothing if the reporter isn't + /// paused. + void resume(); +} + +/// A reporter that prints nothing. +class SilentReporter implements Reporter { + @override + void pause() {} + + @override + void resume() {} +}
diff --git a/pkgs/test_core/lib/src/runner/reporter/compact.dart b/pkgs/test_core/lib/src/runner/reporter/compact.dart new file mode 100644 index 0000000..13ccd11 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/reporter/compact.dart
@@ -0,0 +1,446 @@ +// Copyright (c) 2015, 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:isolate'; + +import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports + +import '../../util/io.dart'; +import '../../util/pretty_print.dart'; +import '../../util/pretty_print.dart' as utils; +import '../engine.dart'; +import '../load_exception.dart'; +import '../load_suite.dart'; +import '../reporter.dart'; + +/// A reporter that prints test results to the console in a single +/// continuously-updating line. +class CompactReporter implements Reporter { + /// Whether the reporter should emit terminal color escapes. + final bool _color; + + /// The terminal escape for green text, or the empty string if this is Windows + /// or not outputting to a terminal. + final String _green; + + /// The terminal escape for red text, or the empty string if this is Windows + /// or not outputting to a terminal. + final String _red; + + /// The terminal escape for yellow text, or the empty string if this is + /// Windows or not outputting to a terminal. + final String _yellow; + + /// The terminal escape for gray text, or the empty string if this is + /// Windows or not outputting to a terminal. + final String _gray; + + /// The terminal escape code for cyan text, or the empty string if this is + /// Windows or not outputting to a terminal. + final String _cyan; + + /// The terminal escape for bold text, or the empty string if this is + /// Windows or not outputting to a terminal. + final String _bold; + + /// The terminal escape for removing test coloring, or the empty string if + /// this is Windows or not outputting to a terminal. + final String _noColor; + + /// The engine used to run the tests. + final Engine _engine; + + /// Whether the path to each test's suite should be printed. + final bool _printPath; + + /// Whether the platform each test is running on should be printed. + final bool _printPlatform; + + /// A stopwatch that tracks the duration of the full run. + final _stopwatch = Stopwatch(); + + /// Whether we've started [_stopwatch]. + /// + /// We can't just use `_stopwatch.isRunning` because the stopwatch is stopped + /// when the reporter is paused. + var _stopwatchStarted = false; + + /// The size of `_engine.passed` last time a progress notification was + /// printed. + int _lastProgressPassed = 0; + + /// The size of `_engine.skipped` last time a progress notification was + /// printed. + int? _lastProgressSkipped; + + /// The size of `_engine.failed` last time a progress notification was + /// printed. + int? _lastProgressFailed; + + /// The duration of the test run in seconds last time a progress notification + /// was printed. + int? _lastProgressElapsed; + + /// The message printed for the last progress notification. + String? _lastProgressMessage; + + /// The suffix added to the last progress notification. + String? _lastProgressSuffix; + + /// Whether the message printed for the last progress notification was + /// truncated. + bool? _lastProgressTruncated; + + // Whether a newline has been printed since the last progress line. + var _printedNewline = true; + + /// Whether the reporter is paused. + var _paused = false; + + // Whether a notice should be logged about enabling stack trace chaining at + // the end of all tests running. + var _shouldPrintStackTraceChainingNotice = false; + + /// The set of all subscriptions to various streams. + final _subscriptions = <StreamSubscription>{}; + + final StringSink _sink; + + /// Watches the tests run by [engine] and prints their results to the + /// terminal. + /// + /// If [color] is `true`, this will use terminal colors; if it's `false`, it + /// won't. If [printPath] is `true`, this will print the path name as part of + /// the test description. Likewise, if [printPlatform] is `true`, this will + /// print the platform as part of the test description. + static CompactReporter watch(Engine engine, StringSink sink, + {required bool color, + required bool printPath, + required bool printPlatform}) => + CompactReporter._(engine, sink, + color: color, printPath: printPath, printPlatform: printPlatform); + + CompactReporter._(this._engine, this._sink, + {required bool color, + required bool printPath, + required bool printPlatform}) + : _printPath = printPath, + _printPlatform = printPlatform, + _color = color, + _green = color ? '\u001b[32m' : '', + _red = color ? '\u001b[31m' : '', + _yellow = color ? '\u001b[33m' : '', + _gray = color ? '\u001b[90m' : '', + _cyan = color ? '\u001b[36m' : '', + _bold = color ? '\u001b[1m' : '', + _noColor = color ? '\u001b[0m' : '' { + _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted)); + + // Convert the future to a stream so that the subscription can be paused or + // canceled. + _subscriptions.add(_engine.success.asStream().listen(_onDone)); + } + + @override + void pause() { + if (_paused) return; + _paused = true; + + if (!_printedNewline) _sink.writeln(''); + _printedNewline = true; + _stopwatch.stop(); + + // Force the next message to be printed, even if it's identical to the + // previous one. If the reporter was paused, text was probably printed + // during the pause. + _lastProgressMessage = null; + + for (var subscription in _subscriptions) { + subscription.pause(); + } + } + + @override + void resume() { + if (!_paused) return; + _paused = false; + + if (_stopwatchStarted) _stopwatch.start(); + + for (var subscription in _subscriptions) { + subscription.resume(); + } + } + + void _cancel() { + for (var subscription in _subscriptions) { + subscription.cancel(); + } + _subscriptions.clear(); + } + + /// A callback called when the engine begins running [liveTest]. + void _onTestStarted(LiveTest liveTest) { + if (!_stopwatchStarted) { + _stopwatchStarted = true; + _stopwatch.start(); + + // Keep updating the time even when nothing else is happening. + _subscriptions.add(Stream<void>.periodic(const Duration(seconds: 1)) + .listen((_) => _progressLine(_lastProgressMessage ?? ''))); + } + + // If this is the first test or suite load to start, print a progress line + // so the user knows what's running. + if ((_engine.active.length == 1 && _engine.active.first == liveTest) || + (_engine.active.isEmpty && + _engine.activeSuiteLoads.length == 1 && + _engine.activeSuiteLoads.first == liveTest)) { + _progressLine(_description(liveTest)); + } + + _subscriptions.add(liveTest.onStateChange + .listen((state) => _onStateChange(liveTest, state))); + + _subscriptions.add(liveTest.onError + .listen((error) => _onError(liveTest, error.error, error.stackTrace))); + + _subscriptions.add(liveTest.onMessage.listen((message) { + _progressLine(_description(liveTest), truncate: false); + if (!_printedNewline) _sink.writeln(''); + _printedNewline = true; + + var text = message.text; + if (message.type == MessageType.skip) text = ' $_yellow$text$_noColor'; + _sink.writeln(text); + })); + + liveTest.onComplete.then((_) { + var result = liveTest.state.result; + if (result != Result.error && result != Result.failure) return; + var quotedName = Platform.isWindows + ? '"${liveTest.test.name.replaceAll('"', '"""')}"' + : "'${liveTest.test.name.replaceAll("'", r"'\''")}'"; + _sink.writeln(''); + _sink.writeln('$_bold${_cyan}To run this test again:$_noColor ' + '${Platform.executable} test ${liveTest.suite.path} ' + '-p ${liveTest.suite.platform.runtime.identifier} ' + '--plain-name $quotedName'); + }); + } + + /// A callback called when [liveTest]'s state becomes [state]. + void _onStateChange(LiveTest liveTest, State state) { + if (state.status != Status.complete) return; + + // Errors are printed in [onError]; no need to print them here as well. + if (state.result == Result.failure) return; + if (state.result == Result.error) return; + + // Always display the name of the oldest active test, unless testing + // is finished in which case display the last test to complete. + if (_engine.active.isEmpty) { + _progressLine(_description(liveTest)); + } else { + _progressLine(_description(_engine.active.first)); + } + } + + /// A callback called when [liveTest] throws [error]. + void _onError(LiveTest liveTest, Object error, StackTrace stackTrace) { + if (!liveTest.test.metadata.chainStackTraces && + !liveTest.suite.isLoadSuite) { + _shouldPrintStackTraceChainingNotice = true; + } + + if (liveTest.state.status != Status.complete) return; + + _progressLine(_description(liveTest), + truncate: false, suffix: ' $_bold$_red[E]$_noColor'); + if (!_printedNewline) _sink.writeln(''); + _printedNewline = true; + + if (error is! LoadException) { + _sink.writeln(indent(error.toString())); + _sink.writeln(indent('$stackTrace')); + return; + } + + // TODO - what type is this? + _sink.writeln(indent(error.toString(color: _color))); + + // Only print stack traces for load errors that come from the user's code. + if (error.innerError is! IOException && + error.innerError is! IsolateSpawnException && + error.innerError is! FormatException && + error.innerError is! String) { + _sink.writeln(indent('$stackTrace')); + } + } + + /// A callback called when the engine is finished running tests. + /// + /// [success] will be `true` if all tests passed, `false` if some tests + /// failed, and `null` if the engine was closed prematurely. + void _onDone(bool? success) { + _cancel(); + _stopwatch.stop(); + + // A null success value indicates that the engine was closed before the + // tests finished running, probably because of a signal from the user. We + // shouldn't print summary information, we should just make sure the + // terminal cursor is on its own line. + if (success == null) { + if (!_printedNewline) _sink.writeln(''); + _printedNewline = true; + return; + } + + if (_engine.liveTests.isEmpty) { + if (!_printedNewline) _sink.write('\r'); + var message = 'No tests ran.'; + _sink.write(message); + + // Add extra padding to overwrite any load messages. + if (!_printedNewline) _sink.write(' ' * (lineLength - message.length)); + _sink.writeln(''); + } else if (!success) { + for (var liveTest in _engine.active) { + _progressLine(_description(liveTest), + truncate: false, + suffix: ' - did not complete $_bold$_red[E]$_noColor'); + _sink.writeln(''); + } + _progressLine('Some tests failed.', color: _red); + _sink.writeln(''); + } else if (_engine.passed.isEmpty) { + _progressLine('All tests skipped.'); + _sink.writeln(''); + } else { + _progressLine('All tests passed!'); + _sink.writeln(''); + } + + if (_shouldPrintStackTraceChainingNotice) { + _sink + ..writeln('') + ..writeln('Consider enabling the flag chain-stack-traces to ' + 'receive more detailed exceptions.\n' + "For example, 'dart test --chain-stack-traces'."); + } + } + + /// Prints a line representing the current state of the tests. + /// + /// [message] goes after the progress report, and may be truncated to fit the + /// entire line within [lineLength]. If [color] is passed, it's used as the + /// color for [message]. If [suffix] is passed, it's added to the end of + /// [message]. + bool _progressLine(String message, + {String? color, bool truncate = true, String? suffix}) { + var elapsed = _stopwatch.elapsed.inSeconds; + + // Print nothing if nothing has changed since the last progress line. + if (_engine.passed.length == _lastProgressPassed && + _engine.skipped.length == _lastProgressSkipped && + _engine.failed.length == _lastProgressFailed && + message == _lastProgressMessage && + // Don't re-print just because a suffix was removed. + (suffix == null || suffix == _lastProgressSuffix) && + // Don't re-print just because the message became re-truncated, because + // that doesn't add information. + (truncate || !_lastProgressTruncated!) && + // If we printed a newline, that means the last line *wasn't* a progress + // line. In that case, we don't want to print a new progress line just + // because the elapsed time changed. + (_printedNewline || elapsed == _lastProgressElapsed)) { + return false; + } + + _lastProgressPassed = _engine.passed.length; + _lastProgressSkipped = _engine.skipped.length; + _lastProgressFailed = _engine.failed.length; + _lastProgressElapsed = elapsed; + _lastProgressMessage = message; + _lastProgressSuffix = suffix; + _lastProgressTruncated = truncate; + + if (suffix != null) message += suffix; + color ??= ''; + var duration = _stopwatch.elapsed; + var buffer = StringBuffer(); + + // \r moves back to the beginning of the current line. + buffer.write('\r${_timeString(duration)} '); + buffer.write(_green); + buffer.write('+'); + buffer.write(_engine.passed.length); + buffer.write(_noColor); + + if (_engine.skipped.isNotEmpty) { + buffer.write(_yellow); + buffer.write(' ~'); + buffer.write(_engine.skipped.length); + buffer.write(_noColor); + } + + if (_engine.failed.isNotEmpty) { + buffer.write(_red); + buffer.write(' -'); + buffer.write(_engine.failed.length); + buffer.write(_noColor); + } + + buffer.write(': '); + buffer.write(color); + + // Ensure the line fits within [lineLength]. [buffer] includes the color + // escape sequences too. Because these sequences are not visible characters, + // we make sure they are not counted towards the limit. + var length = withoutColors(buffer.toString()).length; + if (truncate) message = utils.truncate(message, lineLength - length); + buffer.write(message); + buffer.write(_noColor); + + // Pad the rest of the line so that it looks erased. + buffer.write(' ' * (lineLength - withoutColors(buffer.toString()).length)); + _sink.write(buffer.toString()); + + _printedNewline = false; + return true; + } + + /// Returns a representation of [duration] as `MM:SS`. + String _timeString(Duration duration) { + return "${duration.inMinutes.toString().padLeft(2, '0')}:" + "${(duration.inSeconds % 60).toString().padLeft(2, '0')}"; + } + + /// Returns a description of [liveTest]. + /// + /// This differs from the test's own description in that it may also include + /// the suite's name. + String _description(LiveTest liveTest) { + var name = liveTest.test.name; + + if (_printPath && + liveTest.suite is! LoadSuite && + liveTest.suite.path != null) { + name = '${liveTest.suite.path}: $name'; + } + + if (_printPlatform) { + name = '[${liveTest.suite.platform.runtime.name}, ' + '${liveTest.suite.platform.compiler.name}] $name'; + } + + if (liveTest.suite is LoadSuite) name = '$_bold$_gray$name$_noColor'; + + return name; + } +}
diff --git a/pkgs/test_core/lib/src/runner/reporter/expanded.dart b/pkgs/test_core/lib/src/runner/reporter/expanded.dart new file mode 100644 index 0000000..be2016c --- /dev/null +++ b/pkgs/test_core/lib/src/runner/reporter/expanded.dart
@@ -0,0 +1,347 @@ +// Copyright (c) 2015, 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 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports + +import '../../util/pretty_print.dart'; +import '../engine.dart'; +import '../load_exception.dart'; +import '../load_suite.dart'; +import '../reporter.dart'; + +/// A reporter that prints each test on its own line. +/// +/// This is currently used in place of [CompactReporter] by `lib/test.dart`, +/// which can't transitively import `dart:io` but still needs access to a runner +/// so that test files can be run directly. This means that until issue 6943 is +/// fixed, this must not import `dart:io`. +class ExpandedReporter implements Reporter { + /// Whether the reporter should emit terminal color escapes. + final bool _color; + + /// The terminal escape for green text, or the empty string if this is Windows + /// or not outputting to a terminal. + final String _green; + + /// The terminal escape for red text, or the empty string if this is Windows + /// or not outputting to a terminal. + final String _red; + + /// The terminal escape for yellow text, or the empty string if this is + /// Windows or not outputting to a terminal. + final String _yellow; + + /// The terminal escape for gray text, or the empty string if this is + /// Windows or not outputting to a terminal. + final String _gray; + + /// The terminal escape for bold text, or the empty string if this is + /// Windows or not outputting to a terminal. + final String _bold; + + /// The terminal escape for removing test coloring, or the empty string if + /// this is Windows or not outputting to a terminal. + final String _noColor; + + /// The engine used to run the tests. + final Engine _engine; + + /// Whether the path to each test's suite should be printed. + final bool _printPath; + + /// Whether the platform each test is running on should be printed. + final bool _printPlatform; + + /// A stopwatch that tracks the duration of the full run. + final _stopwatch = Stopwatch(); + + /// The size of `_engine.passed` last time a progress notification was + /// printed. + int _lastProgressPassed = 0; + + /// The size of `_engine.skipped` last time a progress notification was + /// printed. + int _lastProgressSkipped = 0; + + /// The size of `_engine.failed` last time a progress notification was + /// printed. + int _lastProgressFailed = 0; + + /// The message printed for the last progress notification. + String _lastProgressMessage = ''; + + /// The suffix added to the last progress notification. + String? _lastProgressSuffix; + + /// Whether the reporter is paused. + var _paused = false; + + // Whether a notice should be logged about enabling stack trace chaining at + // the end of all tests running. + var _shouldPrintStackTraceChainingNotice = false; + + /// The set of all subscriptions to various streams. + final _subscriptions = <StreamSubscription>{}; + + final StringSink _sink; + + /// Watches the tests run by [engine] and prints their results to the + /// terminal. + /// + /// If [color] is `true`, this will use terminal colors; if it's `false`, it + /// won't. If [printPath] is `true`, this will print the path name as part of + /// the test description. Likewise, if [printPlatform] is `true`, this will + /// print the platform as part of the test description. + static ExpandedReporter watch(Engine engine, StringSink sink, + {required bool color, + required bool printPath, + required bool printPlatform}) => + ExpandedReporter._(engine, sink, + color: color, printPath: printPath, printPlatform: printPlatform); + + ExpandedReporter._(this._engine, this._sink, + {required bool color, + required bool printPath, + required bool printPlatform}) + : _printPath = printPath, + _printPlatform = printPlatform, + _color = color, + _green = color ? '\u001b[32m' : '', + _red = color ? '\u001b[31m' : '', + _yellow = color ? '\u001b[33m' : '', + _gray = color ? '\u001b[90m' : '', + _bold = color ? '\u001b[1m' : '', + _noColor = color ? '\u001b[0m' : '' { + _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted)); + + // Convert the future to a stream so that the subscription can be paused or + // canceled. + _subscriptions.add(_engine.success.asStream().listen(_onDone)); + } + + @override + void pause() { + if (_paused) return; + _paused = true; + + _stopwatch.stop(); + + for (var subscription in _subscriptions) { + subscription.pause(); + } + } + + @override + void resume() { + if (!_paused) return; + + _stopwatch.start(); + + for (var subscription in _subscriptions) { + subscription.resume(); + } + } + + void _cancel() { + for (var subscription in _subscriptions) { + subscription.cancel(); + } + _subscriptions.clear(); + } + + /// A callback called when the engine begins running [liveTest]. + void _onTestStarted(LiveTest liveTest) { + if (liveTest.suite is! LoadSuite) { + if (!_stopwatch.isRunning) _stopwatch.start(); + + // If this is the first non-load test to start, print a progress line so + // the user knows what's running. + if (_engine.active.length == 1) _progressLine(_description(liveTest)); + + // The engine surfaces load tests when there are no other tests running, + // but because the expanded reporter's output is always visible, we don't + // emit information about them unless they fail. + _subscriptions.add(liveTest.onStateChange + .listen((state) => _onStateChange(liveTest, state))); + } else if (_engine.active.isEmpty && + _engine.activeSuiteLoads.length == 1 && + _engine.activeSuiteLoads.first == liveTest && + liveTest.test.name.startsWith('loading ')) { + // Print a progress line for ongoing suite loading synthetic test since it + // may be slow (or stuck) depending on the platform. + _progressLine(_description(liveTest)); + } + + _subscriptions.add(liveTest.onError + .listen((error) => _onError(liveTest, error.error, error.stackTrace))); + + _subscriptions.add(liveTest.onMessage.listen((message) { + _progressLine(_description(liveTest)); + var text = message.text; + if (message.type == MessageType.skip) text = ' $_yellow$text$_noColor'; + _sink.writeln(text); + })); + } + + /// A callback called when [liveTest]'s state becomes [state]. + void _onStateChange(LiveTest liveTest, State state) { + if (state.status != Status.complete) return; + + // If any tests are running, display the name of the oldest active + // test. + if (_engine.active.isNotEmpty) { + _progressLine(_description(_engine.active.first)); + } + } + + /// A callback called when [liveTest] throws [error]. + void _onError(LiveTest liveTest, Object error, StackTrace stackTrace) { + if (!liveTest.test.metadata.chainStackTraces && + !liveTest.suite.isLoadSuite) { + _shouldPrintStackTraceChainingNotice = true; + } + + if (liveTest.state.status != Status.complete) return; + + _progressLine(_description(liveTest), suffix: ' $_bold$_red[E]$_noColor'); + + if (error is! LoadException) { + _sink + ..writeln(indent('$error')) + ..writeln(indent('$stackTrace')); + return; + } + + // TODO - what type is this? + _sink.writeln(indent(error.toString(color: _color))); + + // Only print stack traces for load errors that come from the user's code. + if (error.innerError is! FormatException && error.innerError is! String) { + _sink.writeln(indent('$stackTrace')); + } + } + + /// A callback called when the engine is finished running tests. + /// + /// [success] will be `true` if all tests passed, `false` if some tests + /// failed, and `null` if the engine was closed prematurely. + void _onDone(bool? success) { + _cancel(); + // A null success value indicates that the engine was closed before the + // tests finished running, probably because of a signal from the user, in + // which case we shouldn't print summary information. + if (success == null) return; + + if (_engine.liveTests.isEmpty) { + _sink.writeln('No tests ran.'); + } else if (!success) { + for (var liveTest in _engine.active) { + _progressLine(_description(liveTest), + suffix: ' - did not complete $_bold$_red[E]$_noColor'); + } + _progressLine('Some tests failed.', color: _red); + } else if (_engine.passed.isEmpty) { + _progressLine('All tests skipped.'); + } else { + _progressLine('All tests passed!'); + } + + if (_shouldPrintStackTraceChainingNotice) { + _sink + ..writeln('') + ..writeln('Consider enabling the flag chain-stack-traces to ' + 'receive more detailed exceptions.\n' + "For example, 'dart test --chain-stack-traces'."); + } + } + + /// Prints a line representing the current state of the tests. + /// + /// [message] goes after the progress report. If [color] is passed, it's used + /// as the color for [message]. If [suffix] is passed, it's added to the end + /// of [message]. + void _progressLine(String message, {String? color, String? suffix}) { + // Print nothing if nothing has changed since the last progress line. + if (_engine.passed.length == _lastProgressPassed && + _engine.skipped.length == _lastProgressSkipped && + _engine.failed.length == _lastProgressFailed && + message == _lastProgressMessage && + // Don't re-print just because a suffix was removed. + (suffix == null || suffix == _lastProgressSuffix)) { + return; + } + + _lastProgressPassed = _engine.passed.length; + _lastProgressSkipped = _engine.skipped.length; + _lastProgressFailed = _engine.failed.length; + _lastProgressMessage = message; + _lastProgressSuffix = suffix; + + if (suffix != null) message += suffix; + color ??= ''; + var duration = _stopwatch.elapsed; + var buffer = StringBuffer(); + + // \r moves back to the beginning of the current line. + buffer.write('${_timeString(duration)} '); + buffer.write(_green); + buffer.write('+'); + buffer.write(_engine.passed.length); + buffer.write(_noColor); + + if (_engine.skipped.isNotEmpty) { + buffer.write(_yellow); + buffer.write(' ~'); + buffer.write(_engine.skipped.length); + buffer.write(_noColor); + } + + if (_engine.failed.isNotEmpty) { + buffer.write(_red); + buffer.write(' -'); + buffer.write(_engine.failed.length); + buffer.write(_noColor); + } + + buffer.write(': '); + buffer.write(color); + buffer.write(message); + buffer.write(_noColor); + + _sink.writeln(buffer.toString()); + } + + /// Returns a representation of [duration] as `MM:SS`. + String _timeString(Duration duration) { + return "${duration.inMinutes.toString().padLeft(2, '0')}:" + "${(duration.inSeconds % 60).toString().padLeft(2, '0')}"; + } + + /// Returns a description of [liveTest]. + /// + /// This differs from the test's own description in that it may also include + /// the suite's name. + String _description(LiveTest liveTest) { + var name = liveTest.test.name; + + if (_printPath && + liveTest.suite is! LoadSuite && + liveTest.suite.path != null) { + name = '${liveTest.suite.path}: $name'; + } + + if (_printPlatform) { + name = '[${liveTest.suite.platform.runtime.name}, ' + '${liveTest.suite.platform.compiler.name}] $name'; + } + + if (liveTest.suite is LoadSuite) name = '$_bold$_gray$name$_noColor'; + + return name; + } +}
diff --git a/pkgs/test_core/lib/src/runner/reporter/failures_only.dart b/pkgs/test_core/lib/src/runner/reporter/failures_only.dart new file mode 100644 index 0000000..b6b85e7 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/reporter/failures_only.dart
@@ -0,0 +1,295 @@ +// Copyright (c) 2024, 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 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports + +import '../../util/pretty_print.dart'; +import '../engine.dart'; +import '../load_exception.dart'; +import '../load_suite.dart'; +import '../reporter.dart'; + +/// A reporter that only prints when a test fails. +class FailuresOnlyReporter implements Reporter { + /// Whether the reporter should emit terminal color escapes. + final bool _color; + + /// The terminal escape for green text, or the empty string if this is Windows + /// or not outputting to a terminal. + final String _green; + + /// The terminal escape for red text, or the empty string if this is Windows + /// or not outputting to a terminal. + final String _red; + + /// The terminal escape for yellow text, or the empty string if this is + /// Windows or not outputting to a terminal. + final String _yellow; + + /// The terminal escape for gray text, or the empty string if this is + /// Windows or not outputting to a terminal. + final String _gray; + + /// The terminal escape for bold text, or the empty string if this is + /// Windows or not outputting to a terminal. + final String _bold; + + /// The terminal escape for removing test coloring, or the empty string if + /// this is Windows or not outputting to a terminal. + final String _noColor; + + /// The engine used to run the tests. + final Engine _engine; + + /// Whether the path to each test's suite should be printed. + final bool _printPath; + + /// Whether the platform each test is running on should be printed. + final bool _printPlatform; + + /// The size of `_engine.passed` last time a progress notification was + /// printed. + int _lastProgressPassed = 0; + + /// The size of `_engine.skipped` last time a progress notification was + /// printed. + int _lastProgressSkipped = 0; + + /// The size of `_engine.failed` last time a progress notification was + /// printed. + int _lastProgressFailed = 0; + + /// The message printed for the last progress notification. + String _lastProgressMessage = ''; + + /// The suffix added to the last progress notification. + String? _lastProgressSuffix; + + /// Whether the reporter is paused. + var _paused = false; + + // Whether a notice should be logged about enabling stack trace chaining at + // the end of all tests running. + var _shouldPrintStackTraceChainingNotice = false; + + /// The set of all subscriptions to various streams. + final _subscriptions = <StreamSubscription>{}; + + final StringSink _sink; + + /// Watches the tests run by [engine] and prints their results to the + /// terminal. + /// + /// If [color] is `true`, this will use terminal colors; if it's `false`, it + /// won't. If [printPath] is `true`, this will print the path name as part of + /// the test description. Likewise, if [printPlatform] is `true`, this will + /// print the platform as part of the test description. + static FailuresOnlyReporter watch(Engine engine, StringSink sink, + {required bool color, + required bool printPath, + required bool printPlatform}) => + FailuresOnlyReporter._(engine, sink, + color: color, printPath: printPath, printPlatform: printPlatform); + + FailuresOnlyReporter._(this._engine, this._sink, + {required bool color, + required bool printPath, + required bool printPlatform}) + : _printPath = printPath, + _printPlatform = printPlatform, + _color = color, + _green = color ? '\u001b[32m' : '', + _red = color ? '\u001b[31m' : '', + _yellow = color ? '\u001b[33m' : '', + _gray = color ? '\u001b[90m' : '', + _bold = color ? '\u001b[1m' : '', + _noColor = color ? '\u001b[0m' : '' { + _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted)); + + // Convert the future to a stream so that the subscription can be paused or + // canceled. + _subscriptions.add(_engine.success.asStream().listen(_onDone)); + } + + @override + void pause() { + if (_paused) return; + _paused = true; + + for (var subscription in _subscriptions) { + subscription.pause(); + } + } + + @override + void resume() { + if (!_paused) return; + + for (var subscription in _subscriptions) { + subscription.resume(); + } + } + + void _cancel() { + for (var subscription in _subscriptions) { + subscription.cancel(); + } + _subscriptions.clear(); + } + + /// A callback called when the engine begins running [liveTest]. + void _onTestStarted(LiveTest liveTest) { + _subscriptions.add(liveTest.onError + .listen((error) => _onError(liveTest, error.error, error.stackTrace))); + + _subscriptions.add(liveTest.onMessage.listen((message) { + // TODO - Should this suppress output? Behave like printOnFailure? + _progressLine(_description(liveTest)); + var text = message.text; + if (message.type == MessageType.skip) text = ' $_yellow$text$_noColor'; + _sink.writeln(text); + })); + } + + /// A callback called when [liveTest] throws [error]. + void _onError(LiveTest liveTest, Object error, StackTrace stackTrace) { + if (!liveTest.test.metadata.chainStackTraces && + !liveTest.suite.isLoadSuite) { + _shouldPrintStackTraceChainingNotice = true; + } + + if (liveTest.state.status != Status.complete) return; + + _progressLine(_description(liveTest), suffix: ' $_bold$_red[E]$_noColor'); + + if (error is! LoadException) { + _sink + ..writeln(indent('$error')) + ..writeln(indent('$stackTrace')); + return; + } + + // TODO - what type is this? + _sink.writeln(indent(error.toString(color: _color))); + + // Only print stack traces for load errors that come from the user's code. + if (error.innerError is! FormatException && error.innerError is! String) { + _sink.writeln(indent('$stackTrace')); + } + } + + /// A callback called when the engine is finished running tests. + /// + /// [success] will be `true` if all tests passed, `false` if some tests + /// failed, and `null` if the engine was closed prematurely. + void _onDone(bool? success) { + _cancel(); + // A null success value indicates that the engine was closed before the + // tests finished running, probably because of a signal from the user, in + // which case we shouldn't print summary information. + if (success == null) return; + + if (_engine.liveTests.isEmpty) { + _sink.writeln('No tests ran.'); + } else if (!success) { + for (var liveTest in _engine.active) { + _progressLine(_description(liveTest), + suffix: ' - did not complete $_bold$_red[E]$_noColor'); + } + _progressLine('Some tests failed.', color: _red); + } else if (_engine.passed.isEmpty) { + _progressLine('All tests skipped.'); + } else { + _progressLine('All tests passed!'); + } + + if (_shouldPrintStackTraceChainingNotice) { + _sink + ..writeln('') + ..writeln('Consider enabling the flag chain-stack-traces to ' + 'receive more detailed exceptions.\n' + "For example, 'dart test --chain-stack-traces'."); + } + } + + /// Prints a line representing the current state of the tests. + /// + /// [message] goes after the progress report. If [color] is passed, it's used + /// as the color for [message]. If [suffix] is passed, it's added to the end + /// of [message]. + void _progressLine(String message, {String? color, String? suffix}) { + // Print nothing if nothing has changed since the last progress line. + if (_engine.passed.length == _lastProgressPassed && + _engine.skipped.length == _lastProgressSkipped && + _engine.failed.length == _lastProgressFailed && + message == _lastProgressMessage && + // Don't re-print just because a suffix was removed. + (suffix == null || suffix == _lastProgressSuffix)) { + return; + } + + _lastProgressPassed = _engine.passed.length; + _lastProgressSkipped = _engine.skipped.length; + _lastProgressFailed = _engine.failed.length; + _lastProgressMessage = message; + _lastProgressSuffix = suffix; + + if (suffix != null) message += suffix; + color ??= ''; + var buffer = StringBuffer(); + + buffer.write(_green); + buffer.write('+'); + buffer.write(_engine.passed.length); + buffer.write(_noColor); + + if (_engine.skipped.isNotEmpty) { + buffer.write(_yellow); + buffer.write(' ~'); + buffer.write(_engine.skipped.length); + buffer.write(_noColor); + } + + if (_engine.failed.isNotEmpty) { + buffer.write(_red); + buffer.write(' -'); + buffer.write(_engine.failed.length); + buffer.write(_noColor); + } + + buffer.write(': '); + buffer.write(color); + buffer.write(message); + buffer.write(_noColor); + + _sink.writeln(buffer.toString()); + } + + /// Returns a description of [liveTest]. + /// + /// This differs from the test's own description in that it may also include + /// the suite's name. + String _description(LiveTest liveTest) { + var name = liveTest.test.name; + + if (_printPath && + liveTest.suite is! LoadSuite && + liveTest.suite.path != null) { + name = '${liveTest.suite.path}: $name'; + } + + if (_printPlatform) { + name = '[${liveTest.suite.platform.runtime.name}, ' + '${liveTest.suite.platform.compiler.name}] $name'; + } + + if (liveTest.suite is LoadSuite) name = '$_bold$_gray$name$_noColor'; + + return name; + } +}
diff --git a/pkgs/test_core/lib/src/runner/reporter/github.dart b/pkgs/test_core/lib/src/runner/reporter/github.dart new file mode 100644 index 0000000..5249817 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/reporter/github.dart
@@ -0,0 +1,239 @@ +// 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. + +// ignore_for_file: implementation_imports + +import 'dart:async'; + +import 'package:test_api/src/backend/live_test.dart'; +import 'package:test_api/src/backend/message.dart'; +import 'package:test_api/src/backend/state.dart'; +import 'package:test_api/src/backend/util/pretty_print.dart'; + +import '../engine.dart'; +import '../load_suite.dart'; +import '../reporter.dart'; + +/// A reporter that prints test output using formatting for Github Actions. +/// +/// See +/// https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions +/// for a description of the output format, and +/// https://github.com/dart-lang/test/issues/1415 for discussions about this +/// implementation. +class GithubReporter implements Reporter { + /// The engine used to run the tests. + final Engine _engine; + + /// Whether the path to each test's suite should be printed. + final bool _printPath; + + /// Whether the platform each test is running on should be printed. + final bool _printPlatform; + + /// Whether the reporter is paused. + var _paused = false; + + /// The set of all subscriptions to various streams. + final _subscriptions = <StreamSubscription>{}; + + final StringSink _sink; + + final Map<LiveTest, List<Message>> _testMessages = {}; + + final Set<LiveTest> _completedTests = {}; + + /// Watches the tests run by [engine] and prints their results as JSON. + static GithubReporter watch( + Engine engine, + StringSink sink, { + required bool printPath, + required bool printPlatform, + }) => + GithubReporter._(engine, sink, printPath, printPlatform); + + GithubReporter._( + this._engine, this._sink, this._printPath, this._printPlatform) { + _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted)); + _subscriptions.add(_engine.success.asStream().listen(_onDone)); + + // Add a spacer between pre-test output and the test results. + _sink.writeln(); + } + + @override + void pause() { + if (_paused) return; + _paused = true; + + for (var subscription in _subscriptions) { + subscription.pause(); + } + } + + @override + void resume() { + if (!_paused) return; + _paused = false; + + for (var subscription in _subscriptions) { + subscription.resume(); + } + } + + void _cancel() { + for (var subscription in _subscriptions) { + subscription.cancel(); + } + _subscriptions.clear(); + } + + /// A callback called when the engine begins running [liveTest]. + void _onTestStarted(LiveTest liveTest) { + // Convert the future to a stream so that the subscription can be paused or + // canceled. + _subscriptions.add( + liveTest.onComplete.asStream().listen((_) => _onComplete(liveTest))); + + _subscriptions.add(liveTest.onError + .listen((error) => _onError(liveTest, error.error, error.stackTrace))); + + // Collect messages from tests as they are emitted. + _subscriptions.add(liveTest.onMessage.listen((message) { + if (_completedTests.contains(liveTest)) { + // The test has already completed and it's previous messages were + // written out; ensure this post-completion output is not lost. + _sink.writeln(message.text); + } else { + _testMessages.putIfAbsent(liveTest, () => []).add(message); + } + })); + } + + /// A callback called when [liveTest] finishes running. + void _onComplete(LiveTest test) { + final errors = test.errors; + final messages = _testMessages[test] ?? []; + final skipped = test.state.result == Result.skipped; + final failed = errors.isNotEmpty; + final loadSuite = test.suite is LoadSuite; + final synthetic = loadSuite || + test.individualName == '(setUpAll)' || + test.individualName == '(tearDownAll)'; + + // Mark this test as having completed. + _completedTests.add(test); + + // Don't emit any info for loadSuite, setUpAll, or tearDownAll tests + // unless they contain errors or other info. + if (synthetic && (errors.isEmpty && messages.isEmpty)) { + return; + } + + // For now, we use the same icon for both tests and test-like structures + // (loadSuite, setUpAll, tearDownAll). + var defaultIcon = synthetic ? _GithubMarkup.passed : _GithubMarkup.passed; + final prefix = failed + ? _GithubMarkup.failed + : skipped + ? _GithubMarkup.skipped + : defaultIcon; + final statusSuffix = failed + ? ' (failed)' + : skipped + ? ' (skipped)' + : ''; + + var name = test.test.name; + if (!loadSuite) { + if (_printPath && test.suite.path != null) { + name = '${test.suite.path}: $name'; + } + } + if (_printPlatform) { + name = '[${test.suite.platform.runtime.name}, ' + '${test.suite.platform.compiler.name}] $name'; + } + if (messages.isEmpty && errors.isEmpty) { + _sink.writeln('$prefix $name$statusSuffix'); + } else { + _sink.writeln(_GithubMarkup.startGroup('$prefix $name$statusSuffix')); + for (var message in messages) { + _sink.writeln(message.text); + } + for (var error in errors) { + _sink.writeln('${error.error}'); + _sink.writeln(error.stackTrace.toString().trimRight()); + } + _sink.writeln(_GithubMarkup.endGroup); + } + } + + /// A callback called when [test] throws [error]. + void _onError(LiveTest test, Object error, StackTrace stackTrace) { + if (_completedTests.contains(test)) { + final loadSuite = test.suite is LoadSuite; + + final prefix = _GithubMarkup.failed; + final statusSuffix = ' (failed after test completion)'; + + var name = test.test.name; + if (!loadSuite) { + if (_printPath && test.suite.path != null) { + name = '${test.suite.path}: $name'; + } + } + if (_printPlatform) { + name = '[${test.suite.platform.runtime.name}, ' + '${test.suite.platform.compiler.name}] $name'; + } + + _sink.writeln(_GithubMarkup.startGroup('$prefix $name$statusSuffix')); + _sink.writeln('$error'); + _sink.writeln(stackTrace.toString().trimRight()); + _sink.writeln(_GithubMarkup.endGroup); + } + } + + void _onDone(bool? success) { + _cancel(); + + _sink.writeln(); + + final hadFailures = _engine.failed.isNotEmpty; + final message = StringBuffer('${_engine.passed.length} ' + '${pluralize('test', _engine.passed.length)} passed'); + if (_engine.failed.isNotEmpty) { + message.write(', ${_engine.failed.length} failed'); + } + if (_engine.skipped.isNotEmpty) { + message.write(', ${_engine.skipped.length} skipped'); + } + message.write('.'); + _sink.writeln( + hadFailures + ? _GithubMarkup.error(message.toString()) + : '${_GithubMarkup.success} $message', + ); + } +} + +abstract class _GithubMarkup { + // Char sets avilable at https://www.compart.com/en/unicode/. + static const String passed = '✅'; + static const String skipped = '❎'; + static const String failed = '❌'; + // The 'synthetic' icon is currently not used but is something to consider in + // order to draw a distinction between user tests and test-like supporting + // infrastructure. + // static const String synthetic = '⏺'; + static const String success = '🎉'; + + static String startGroup(String title) => + '::group::${title.replaceAll('\n', ' ')}'; + + static final String endGroup = '::endgroup::'; + + static String error(String message) => '::error::$message'; +}
diff --git a/pkgs/test_core/lib/src/runner/reporter/json.dart b/pkgs/test_core/lib/src/runner/reporter/json.dart new file mode 100644 index 0000000..672f806 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/reporter/json.dart
@@ -0,0 +1,324 @@ +// Copyright (c) 2015, 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:convert'; +import 'dart:io' show pid; + +import 'package:collection/collection.dart'; +import 'package:path/path.dart' as p; +import 'package:stack_trace/stack_trace.dart'; +import 'package:test_api/hooks.dart' // ignore: implementation_imports + show + TestFailure; +import 'package:test_api/src/backend/compiler.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports + +import '../../platform.dart'; +import '../engine.dart'; +import '../load_suite.dart'; +import '../reporter.dart'; +import '../version.dart'; + +/// A reporter that prints machine-readable JSON-formatted test results. +class JsonReporter implements Reporter { + /// Whether the test runner will pause for debugging. + final bool _isDebugRun; + + /// The engine used to run the tests. + final Engine _engine; + + /// A stopwatch that tracks the duration of the full run. + final _stopwatch = Stopwatch(); + + /// Whether we've started [_stopwatch]. + /// + /// We can't just use `_stopwatch.isRunning` because the stopwatch is stopped + /// when the reporter is paused. + var _stopwatchStarted = false; + + /// An expando that associates unique IDs with [LiveTest]s. + final _liveTestIDs = <LiveTest, int>{}; + + /// An expando that associates unique IDs with [Suite]s. + final _suiteIDs = <Suite, int>{}; + + /// An expando that associates unique IDs with [Group]s. + final _groupIDs = <Group, int>{}; + + /// The next ID to associate with a [LiveTest]. + var _nextID = 0; + + /// Whether the reporter is paused. + var _paused = false; + + /// The set of all subscriptions to various streams. + final _subscriptions = <StreamSubscription>{}; + + final StringSink _sink; + + /// Watches the tests run by [engine] and prints their results as JSON. + static JsonReporter watch(Engine engine, StringSink sink, + {required bool isDebugRun}) => + JsonReporter._(engine, sink, isDebugRun); + + JsonReporter._(this._engine, this._sink, this._isDebugRun) { + _subscriptions.add(_engine.onTestStarted.listen(_onTestStarted)); + + // Convert the future to a stream so that the subscription can be paused or + // canceled. + _subscriptions.add(_engine.success.asStream().listen(_onDone)); + + _subscriptions.add(_engine.onSuiteAdded.listen(null, onDone: () { + _emit('allSuites', { + 'count': _engine.addedSuites.length, + 'time': _stopwatch.elapsed.inMilliseconds + }); + })); + + _emit('start', + {'protocolVersion': '0.1.1', 'runnerVersion': testVersion, 'pid': pid}); + } + + @override + void pause() { + if (_paused) return; + _paused = true; + + _stopwatch.stop(); + + for (var subscription in _subscriptions) { + subscription.pause(); + } + } + + @override + void resume() { + if (!_paused) return; + _paused = false; + + if (_stopwatchStarted) _stopwatch.start(); + + for (var subscription in _subscriptions) { + subscription.resume(); + } + } + + void _cancel() { + for (var subscription in _subscriptions) { + subscription.cancel(); + } + _subscriptions.clear(); + } + + /// A callback called when the engine begins running [liveTest]. + void _onTestStarted(LiveTest liveTest) { + if (!_stopwatchStarted) { + _stopwatchStarted = true; + _stopwatch.start(); + } + + var suiteID = _idForSuite(liveTest.suite); + + // Don't emit groups for load suites. They're always empty and they provide + // unnecessary clutter. + var groupIDs = liveTest.suite is LoadSuite + ? <int>[] + : _idsForGroups(liveTest.groups, liveTest.suite); + + var suiteConfig = _configFor(liveTest.suite); + var id = _nextID++; + _liveTestIDs[liveTest] = id; + _emit('testStart', { + 'test': { + 'id': id, + 'name': liveTest.test.name, + 'suiteID': suiteID, + 'groupIDs': groupIDs, + 'metadata': _serializeMetadata(suiteConfig, liveTest.test.metadata), + ..._frameInfo(suiteConfig, liveTest.test.trace, liveTest.suite.platform, + liveTest.suite.path!), + } + }); + + // Convert the future to a stream so that the subscription can be paused or + // canceled. + _subscriptions.add( + liveTest.onComplete.asStream().listen((_) => _onComplete(liveTest))); + + _subscriptions.add(liveTest.onError + .listen((error) => _onError(liveTest, error.error, error.stackTrace))); + + _subscriptions.add(liveTest.onMessage.listen((message) { + _emit('print', { + 'testID': id, + 'messageType': message.type.name, + 'message': message.text + }); + })); + } + + /// Returns an ID for [suite]. + /// + /// If [suite] doesn't have an ID yet, this assigns one and emits a new event + /// for that suite. + int _idForSuite(Suite suite) { + if (_suiteIDs.containsKey(suite)) return _suiteIDs[suite]!; + + var id = _nextID++; + _suiteIDs[suite] = id; + + // Give the load suite's suite the same ID, because it doesn't have any + // different metadata. + if (suite is LoadSuite) { + suite.suite.then((runnerSuite) { + if (runnerSuite == null) return; + _suiteIDs[runnerSuite] = id; + if (!_isDebugRun) return; + + // TODO(nweiz): test this when we have a library for communicating with + // the Chrome remote debugger, or when we have VM debug support. + _emit('debug', { + 'suiteID': id, + 'observatory': runnerSuite.environment.observatoryUrl?.toString(), + 'remoteDebugger': + runnerSuite.environment.remoteDebuggerUrl?.toString(), + }); + }); + } + + _emit('suite', { + 'suite': <String, Object?>{ + 'id': id, + 'platform': suite.platform.runtime.identifier, + 'path': suite.path + } + }); + return id; + } + + /// Returns a list of the IDs for all the groups in [groups], which are + /// contained in the suite identified by [suiteID]. + /// + /// If a group doesn't have an ID yet, this assigns one and emits a new event + /// for that group. + List<int> _idsForGroups(Iterable<Group> groups, Suite suite) { + int? parentID; + return groups.map((group) { + if (_groupIDs.containsKey(group)) { + return parentID = _groupIDs[group]!; + } + + var id = _nextID++; + _groupIDs[group] = id; + + var suiteConfig = _configFor(suite); + _emit('group', { + 'group': { + 'id': id, + 'suiteID': _idForSuite(suite), + 'parentID': parentID, + 'name': group.name, + 'metadata': _serializeMetadata(suiteConfig, group.metadata), + 'testCount': group.testCount, + ..._frameInfo(suiteConfig, group.trace, suite.platform, suite.path!) + } + }); + parentID = id; + return id; + }).toList(); + } + + /// Serializes [metadata] into a JSON-protocol-compatible map. + Map _serializeMetadata(SuiteConfiguration suiteConfig, Metadata metadata) => + suiteConfig.runSkipped + ? {'skip': false, 'skipReason': null} + : {'skip': metadata.skip, 'skipReason': metadata.skipReason}; + + /// A callback called when [liveTest] finishes running. + void _onComplete(LiveTest liveTest) { + _emit('testDone', { + 'testID': _liveTestIDs[liveTest], + 'result': _normalizeTestResult(liveTest), + 'skipped': liveTest.state.result == Result.skipped, + 'hidden': !_engine.liveTests.contains(liveTest) + }); + } + + String _normalizeTestResult(LiveTest liveTest) { + // For backwards-compatibility, report skipped tests as successes. + if (liveTest.state.result == Result.skipped) return 'success'; + // if test is still active, it was probably cancelled + if (_engine.active.contains(liveTest)) return 'error'; + return liveTest.state.result.toString(); + } + + /// A callback called when [liveTest] throws [error]. + void _onError(LiveTest liveTest, Object error, StackTrace stackTrace) { + _emit('error', { + 'testID': _liveTestIDs[liveTest], + 'error': error.toString(), + 'stackTrace': '$stackTrace', + 'isFailure': error is TestFailure + }); + } + + /// A callback called when the engine is finished running tests. + /// + /// [success] will be `true` if all tests passed, `false` if some tests + /// failed, and `null` if the engine was closed prematurely. + void _onDone(bool? success) { + _cancel(); + _stopwatch.stop(); + + _emit('done', {'success': success}); + } + + /// Returns the configuration for [suite]. + /// + /// If [suite] is a [RunnerSuite], this returns [RunnerSuite.config]. + /// Otherwise, it returns [SuiteConfiguration.empty]. + SuiteConfiguration _configFor(Suite suite) => + suite is RunnerSuite ? suite.config : SuiteConfiguration.empty; + + /// Emits an event with the given type and attributes. + void _emit(String type, Map attributes) { + attributes['type'] = type; + attributes['time'] = _stopwatch.elapsed.inMilliseconds; + _sink.writeln(jsonEncode(attributes)); + } + + /// Returns a map with the line, column, and URL information for the first + /// frame of [trace], as well as the first line in the original file. + /// + /// If javascript traces are enabled and the test is on a javascript platform, + /// or if the [trace] is null or empty, then the line, column, and url will + /// all be `null`. + Map<String, dynamic> _frameInfo(SuiteConfiguration suiteConfig, Trace? trace, + SuitePlatform platform, String suitePath) { + var absoluteSuitePath = p.canonicalize(p.absolute(suitePath)); + var frame = trace?.frames.first; + if (frame == null || (suiteConfig.jsTrace && platform.compiler.isJS)) { + return {'line': null, 'column': null, 'url': null}; + } + + var rootFrame = trace?.frames.firstWhereOrNull((frame) => + frame.uri.scheme == 'file' && + p.canonicalize(frame.uri.toFilePath()) == absoluteSuitePath); + return { + 'line': frame.line, + 'column': frame.column, + 'url': frame.uri.toString(), + if (rootFrame != null && rootFrame != frame) ...{ + 'root_line': rootFrame.line, + 'root_column': rootFrame.column, + 'root_url': rootFrame.uri.toString(), + } + }; + } +}
diff --git a/pkgs/test_core/lib/src/runner/reporter/multiplex.dart b/pkgs/test_core/lib/src/runner/reporter/multiplex.dart new file mode 100644 index 0000000..59a9841 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/reporter/multiplex.dart
@@ -0,0 +1,25 @@ +// Copyright (c) 2020, 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 '../reporter.dart'; + +class MultiplexReporter implements Reporter { + Iterable<Reporter> delegates; + + MultiplexReporter(this.delegates); + + @override + void pause() { + for (var d in delegates) { + d.pause(); + } + } + + @override + void resume() { + for (var d in delegates) { + d.resume(); + } + } +}
diff --git a/pkgs/test_core/lib/src/runner/runner_suite.dart b/pkgs/test_core/lib/src/runner/runner_suite.dart new file mode 100644 index 0000000..dcceb6b --- /dev/null +++ b/pkgs/test_core/lib/src/runner/runner_suite.dart
@@ -0,0 +1,175 @@ +// Copyright (c) 2015, 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 'package:async/async.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports + +import 'environment.dart'; +import 'suite.dart'; + +/// A suite produced and consumed by the test runner that has runner-specific +/// logic and lifecycle management. +/// +/// This is separated from [Suite] because the backend library (which will +/// eventually become its own package) is primarily for test code itself to use, +/// for which the [RunnerSuite] APIs don't make sense. +/// +/// A [RunnerSuite] can be produced and controlled using a +/// [RunnerSuiteController]. +class RunnerSuite extends Suite { + final RunnerSuiteController _controller; + + /// The environment in which this suite runs. + Environment get environment => _controller._environment; + + /// The configuration for this suite. + SuiteConfiguration get config => _controller._config; + + /// Whether the suite is paused for debugging. + /// + /// When using a dev inspector, this may also mean that the entire browser is + /// paused. + bool get isDebugging => _controller._isDebugging; + + /// A broadcast stream that emits an event whenever the suite is paused for + /// debugging or resumed afterwards. + /// + /// The event is `true` when debugging starts and `false` when it ends. + Stream<bool> get onDebugging => _controller._onDebuggingController.stream; + + /// A shortcut constructor for creating a [RunnerSuite] that never goes into + /// debugging mode and doesn't support suite channels. + factory RunnerSuite(Environment environment, SuiteConfiguration config, + Group group, SuitePlatform platform, + {String? path, void Function()? onClose}) { + var controller = + RunnerSuiteController._local(environment, config, onClose: onClose); + var suite = RunnerSuite._(controller, group, platform, path: path); + controller._suite = Future.value(suite); + return suite; + } + + RunnerSuite._(this._controller, Group group, SuitePlatform platform, + {String? path}) + : super(group, platform, + path: path, ignoreTimeouts: _controller._config.ignoreTimeouts); + + @override + RunnerSuite filter(bool Function(Test) callback) { + var filtered = group.filter(callback); + filtered ??= Group.root([], metadata: metadata); + return RunnerSuite._(_controller, filtered, platform, path: path); + } + + /// Closes the suite and releases any resources associated with it. + Future close() => _controller._close(); + + /// Collects a hit-map containing merged coverage. + /// + /// Result is suitable for input to the coverage formatters provided by + /// `package:coverage`. + Future<Map<String, dynamic>> gatherCoverage() async => + (await _controller._gatherCoverage?.call()) ?? {}; +} + +/// A class that exposes and controls a [RunnerSuite]. +class RunnerSuiteController { + /// The suite controlled by this controller. + Future<RunnerSuite> get suite => _suite; + late final Future<RunnerSuite> _suite; + + /// The backing value for [suite.environment]. + final Environment _environment; + + /// The configuration for this suite. + final SuiteConfiguration _config; + + /// A channel that communicates with the remote suite. + final MultiChannel? _suiteChannel; + + /// The function to call when the suite is closed. + final FutureOr<void> Function()? _onClose; + + /// The backing value for [suite.isDebugging]. + bool _isDebugging = false; + + /// The controller for [suite.onDebugging]. + final _onDebuggingController = StreamController<bool>.broadcast(); + + /// The channel names that have already been used. + final _channelNames = <String>{}; + + /// Collects a hit-map containing merged coverage. + final Future<Map<String, dynamic>> Function()? _gatherCoverage; + + RunnerSuiteController(this._environment, this._config, this._suiteChannel, + Future<Group> groupFuture, SuitePlatform platform, + {String? path, + void Function()? onClose, + Future<Map<String, dynamic>> Function()? gatherCoverage}) + : _onClose = onClose, + _gatherCoverage = gatherCoverage { + _suite = groupFuture + .then((group) => RunnerSuite._(this, group, platform, path: path)); + } + + /// Used by [RunnerSuite.new] to create a runner suite that's not loaded from + /// an external source. + RunnerSuiteController._local(this._environment, this._config, + {void Function()? onClose, + Future<Map<String, dynamic>> Function()? gatherCoverage}) + : _suiteChannel = null, + _onClose = onClose, + _gatherCoverage = gatherCoverage; + + /// Sets whether the suite is paused for debugging. + /// + /// If this is different than [suite.isDebugging], this will automatically + /// send out an event along [suite.onDebugging]. + void setDebugging(bool debugging) { + if (debugging == _isDebugging) return; + _isDebugging = debugging; + _onDebuggingController.add(debugging); + } + + /// Returns a channel that communicates with the remote suite. + /// + /// This connects to a channel created by code in the test worker calling the + /// `suiteChannel` argument from a `beforeLoad` callback to `serializeSuite` + /// with the same name. + /// It can be used used to send and receive any JSON-serializable object. + /// + /// This is exposed on the [RunnerSuiteController] so that runner plugins can + /// communicate with the workers they spawn before the associated [suite] is + /// fully loaded. + StreamChannel channel(String name) { + if (!_channelNames.add(name)) { + throw StateError('Duplicate RunnerSuite.channel() connection "$name".'); + } + + var suiteChannel = _suiteChannel; + if (suiteChannel == null) { + throw StateError('No suite channel set up but one was requested.'); + } + + var channel = suiteChannel.virtualChannel(); + suiteChannel.sink + .add({'type': 'suiteChannel', 'name': name, 'id': channel.id}); + return channel; + } + + /// The backing function for [suite.close]. + Future _close() => _closeMemo.runOnce(() async { + await _onDebuggingController.close(); + var onClose = _onClose; + if (onClose != null) await onClose(); + }); + final _closeMemo = AsyncMemoizer<void>(); +}
diff --git a/pkgs/test_core/lib/src/runner/runner_test.dart b/pkgs/test_core/lib/src/runner/runner_test.dart new file mode 100644 index 0000000..2823983 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/runner_test.dart
@@ -0,0 +1,108 @@ +// Copyright (c) 2015, 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 'package:stack_trace/stack_trace.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test_api/backend.dart' + show Metadata, RemoteException, SuitePlatform; +import 'package:test_api/src/backend/group.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/live_test.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/live_test_controller.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/message.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/state.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/test.dart'; // ignore: implementation_imports + +import 'spawn_hybrid.dart'; + +/// A test running remotely, controlled by a stream channel. +class RunnerTest extends Test { + @override + final String name; + @override + final Metadata metadata; + @override + final Trace? trace; + + /// The channel used to communicate with the test's `RemoteListener`. + final MultiChannel _channel; + + RunnerTest(this.name, this.metadata, this.trace, this._channel); + + @override + LiveTest load(Suite suite, {Iterable<Group>? groups}) { + late final LiveTestController controller; + late final VirtualChannel testChannel; + controller = LiveTestController(suite, this, () { + controller.setState(const State(Status.running, Result.success)); + + testChannel = _channel.virtualChannel(); + _channel.sink.add({'command': 'run', 'channel': testChannel.id}); + + testChannel.stream.listen((message) { + final msg = message as Map; + switch (msg['type'] as String) { + case 'error': + var asyncError = RemoteException.deserialize( + msg['error'] as Map<String, dynamic>); + var stackTrace = asyncError.stackTrace; + controller.addError(asyncError.error, stackTrace); + break; + + case 'state-change': + controller.setState(State(Status.parse(msg['status'] as String), + Result.parse(msg['result'] as String))); + break; + + case 'message': + controller.message(Message( + MessageType.parse(msg['message-type'] as String), + msg['text'] as String)); + break; + + case 'complete': + controller.completer.complete(); + break; + + case 'spawn-hybrid-uri': + // When we kill the isolate that the test lives in, that will close + // this virtual channel and cause the spawned isolate to close as + // well. + spawnHybridUri(msg['url'] as String, msg['message'], suite).pipe( + testChannel.virtualChannel((msg['channel'] as num).toInt())); + break; + } + }, onDone: () { + // When the test channel closes—presumably because the browser + // closed—mark the test as complete no matter what. + if (controller.completer.isCompleted) return; + controller.completer.complete(); + }); + }, () { + // If the test has finished running, just disconnect the channel. + if (controller.completer.isCompleted) { + testChannel.sink.close(); + return; + } + + unawaited(() async { + // If the test is still running, send it a message telling it to shut + // down ASAP. This causes the [Invoker] to eagerly throw exceptions + // whenever the test touches it. + testChannel.sink.add({'command': 'close'}); + await controller.completer.future; + await testChannel.sink.close(); + }()); + }, groups: groups); + return controller; + } + + @override + Test? forPlatform(SuitePlatform platform) { + if (!metadata.testOn.evaluate(platform)) return null; + return RunnerTest(name, metadata.forPlatform(platform), trace, _channel); + } +}
diff --git a/pkgs/test_core/lib/src/runner/runtime_selection.dart b/pkgs/test_core/lib/src/runner/runtime_selection.dart new file mode 100644 index 0000000..7f64a5c --- /dev/null +++ b/pkgs/test_core/lib/src/runner/runtime_selection.dart
@@ -0,0 +1,25 @@ +// 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:source_span/source_span.dart'; + +/// A runtime on which the user has chosen to run tests. +class RuntimeSelection { + /// The name of the runtime. + final String name; + + /// The location in the configuration file of this runtime string, or `null` + /// if it was defined outside a configuration file (for example, on the + /// command line). + final SourceSpan? span; + + RuntimeSelection(this.name, [this.span]); + + @override + bool operator ==(Object other) => + other is RuntimeSelection && other.name == name; + + @override + int get hashCode => name.hashCode; +}
diff --git a/pkgs/test_core/lib/src/runner/spawn_hybrid.dart b/pkgs/test_core/lib/src/runner/spawn_hybrid.dart new file mode 100644 index 0000000..ed236b0 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/spawn_hybrid.dart
@@ -0,0 +1,167 @@ +// Copyright (c) 2016, 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 'dart:isolate'; + +import 'package:analyzer/dart/analysis/utilities.dart'; +import 'package:async/async.dart'; +import 'package:path/path.dart' as p; +import 'package:stream_channel/isolate_channel.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test_api/backend.dart' show RemoteException; +import 'package:test_api/src/backend/suite.dart'; // ignore: implementation_imports + +import '../util/dart.dart' as dart; +import '../util/package_config.dart'; +import 'package_version.dart'; + +/// Spawns a hybrid isolate from [url] with the given [message], and returns a +/// [StreamChannel] that communicates with it. +/// +/// This connects the main isolate to the hybrid isolate, whereas +/// `lib/src/frontend/spawn_hybrid.dart` connects the test isolate to the main +/// isolate. +/// +/// If [uri] is relative, it will be interpreted relative to the `file:` URL +/// for [suite]. If it's root-relative (that is, if it begins with `/`) it will +/// be interpreted relative to the root of the package (the directory that +/// contains `pubspec.yaml`, *not* the `test/` directory). If it's a `package:` +/// URL, it will be resolved using the current package's dependency +/// constellation. +StreamChannel spawnHybridUri(String url, Object? message, Suite suite) { + return StreamChannelCompleter.fromFuture(() async { + url = await _normalizeUrl(url, suite); + var port = ReceivePort(); + var onExitPort = ReceivePort(); + try { + var code = ''' + ${await _languageVersionCommentFor(url)} + + import "package:test_core/src/runner/hybrid_listener.dart"; + + import "${url.replaceAll(r'$', '%24')}" as lib; + + void main(_, List data) => listen(() => lib.hybridMain, data); + '''; + + var isolate = await dart.runInIsolate(code, [port.sendPort, message], + onExit: onExitPort.sendPort); + + // Ensure that we close [port] and [channel] when the isolate exits. + var disconnector = Disconnector<void>(); + onExitPort.listen((_) { + disconnector.disconnect(); + port.close(); + onExitPort.close(); + }); + + return IsolateChannel<Object?>.connectReceive(port) + .transform(disconnector) + .transformSink(StreamSinkTransformer.fromHandlers(handleDone: (sink) { + // If the user closes the stream channel, kill the isolate. + isolate.kill(); + port.close(); + onExitPort.close(); + sink.close(); + })); + } catch (error, stackTrace) { + port.close(); + onExitPort.close(); + + // Make sure any errors in spawning the isolate are forwarded to the test. + return StreamChannel( + Stream.fromFuture(Future.value({ + 'type': 'error', + 'error': RemoteException.serialize(error, stackTrace) + })), + NullStreamSink<void>()); + } + }()); +} + +/// Normalizes [url] to an absolute url, resolving `package:` urls with the +/// current package config. +/// +/// If [url] has a scheme other than `package:`, then it is returned as is. +/// +/// Follows the rules for relative/absolute paths outlined in [spawnHybridUri]. +Future<String> _normalizeUrl(String url, Suite suite) async { + final parsedUri = Uri.parse(url); + + switch (parsedUri.scheme) { + case '': + var isRootRelative = parsedUri.path.startsWith('/'); + + if (isRootRelative) { + // We assume that the current path is the package root. `pub run` + // enforces this currently, but at some point it would probably be good + // to pass in an explicit root. + return p.url + .join(p.toUri(p.current).toString(), parsedUri.path.substring(1)); + } else { + var suitePath = suite.path!; + return p.url.join( + p.url.dirname(p.toUri(p.absolute(suitePath)).toString()), + parsedUri.toString()); + } + case 'package': + final resolvedUri = await Isolate.resolvePackageUri(parsedUri); + if (resolvedUri == null) { + throw ArgumentError.value( + url, 'uri', 'Could not resolve the package URI'); + } + return resolvedUri.toString(); + default: + return url; + } +} + +/// Computes the a language version comment for the library at [uri]. +/// +/// If there is a language version comment in the file, that is returned. +/// +/// If the URI has a `data` scheme, a comment representing the language version of +/// the current package is returned. +/// +/// Otherwise a comment representing the default version from the +/// [currentPackageConfig] is returned. +/// +/// If no default language version is known (the URI scheme is not recognized +/// for instance), then an empty string is returned. +Future<String> _languageVersionCommentFor(String url) async { + var parsedUri = Uri.parse(url); + + // Returns the explicit language version comment if one exists. + var result = parseString( + content: await _readUri(parsedUri), + path: parsedUri.scheme == 'data' ? null : p.fromUri(parsedUri), + throwIfDiagnostics: false); + var languageVersionComment = result.unit.languageVersionToken?.value(); + if (languageVersionComment != null) return languageVersionComment.toString(); + + // Returns the default language version for the package if one exists. + if (parsedUri.scheme.isEmpty || parsedUri.scheme == 'file') { + var packageConfig = await currentPackageConfig; + var package = packageConfig.packageOf(parsedUri); + var version = package?.languageVersion; + if (version != null) return '// @dart=$version'; + } + + // Returns the root package language version for `data` URIs. These are + // assumed to be from `spawnHybridCode` calls. + if (parsedUri.scheme == 'data') { + return await rootPackageLanguageVersionComment; + } + + // Fall back on no language comment. + return ''; +} + +Future<String> _readUri(Uri uri) async => switch (uri.scheme) { + '' || 'file' => await File.fromUri(uri).readAsString(), + 'data' => uri.data!.contentAsString(), + _ => throw ArgumentError.value(uri, 'uri', + 'Only data and file uris (as well as relative paths) are supported'), + };
diff --git a/pkgs/test_core/lib/src/runner/suite.dart b/pkgs/test_core/lib/src/runner/suite.dart new file mode 100644 index 0000000..b5ee0be --- /dev/null +++ b/pkgs/test_core/lib/src/runner/suite.dart
@@ -0,0 +1,498 @@ +// Copyright (c) 2016, 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:boolean_selector/boolean_selector.dart'; +import 'package:collection/collection.dart'; +import 'package:source_span/source_span.dart'; +import 'package:test_api/scaffolding.dart' show Timeout; +import 'package:test_api/src/backend/metadata.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/platform_selector.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports + +import 'compiler_selection.dart'; +import 'runtime_selection.dart'; + +/// A filter on tests cases to run within a test suite. +/// +/// The tests that match a test selection should match all available criteria. A +/// suite run may include multiple test selections, and a test should run if it +/// matches any test selection. +/// +/// An empty [TestSelection()] will run all tests in the suite. +final class TestSelection { + /// The patterns to check against test case names. + /// + /// Only run tests which match all the patterns. + final Set<Pattern> testPatterns; + + /// Only run tests that originate from this line in the test suite. + final int? line; + + /// Only run tests that originate from this column in the test suite. + final int? col; + + const TestSelection({this.testPatterns = const {}, this.line, this.col}); +} + +/// Suite-level configuration. +/// +/// This tracks configuration that can differ from suite to suite. +final class SuiteConfiguration { + /// Empty configuration with only default values. + /// + /// Using this is slightly more efficient than manually constructing a new + /// configuration with no arguments. + static final empty = SuiteConfiguration._( + allowDuplicateTestNames: null, + allowTestRandomization: null, + jsTrace: null, + runSkipped: null, + dart2jsArgs: null, + testSelections: const {}, + precompiledPath: null, + runtimes: null, + compilerSelections: null, + tags: null, + onPlatform: null, + metadata: null, + ignoreTimeouts: null); + + /// Whether or not duplicate test (or group) names are allowed within the same + /// test suite. + // + // TODO: Change the default https://github.com/dart-lang/test/issues/1571 + bool get allowDuplicateTestNames => _allowDuplicateTestNames ?? true; + final bool? _allowDuplicateTestNames; + + /// Whether test randomization should be allowed for this test. + bool get allowTestRandomization => _allowTestRandomization ?? true; + final bool? _allowTestRandomization; + + /// Whether JavaScript stack traces should be left as-is or converted to + /// Dart-like traces. + bool get jsTrace => _jsTrace ?? false; + final bool? _jsTrace; + + /// Whether skipped tests should be run. + bool get runSkipped => _runSkipped ?? false; + final bool? _runSkipped; + + /// The path to a mirror of this package containing HTML that points to + /// precompiled JS. + /// + /// This is used by the internal Google test runner so that test compilation + /// can more effectively make use of Google's build tools. + final String? precompiledPath; + + /// Additional arguments to pass to dart2js. + /// + /// Note that this if multiple suites run the same JavaScript on different + /// runtimes, and they have different [dart2jsArgs], only one (undefined) + /// suite's arguments will be used. + final List<String> dart2jsArgs; + + /// The selections for which tests to run in a suite. + /// + /// If empty, no tests have been selected for this suite. + /// Tests must be selected once for a suite before the suite can run. + /// When merging suite configurations, only one, or neither, should have tests + /// already selected. + /// + /// A test should run within a suite if it matches any selection in + /// [testSelections]. + final Set<TestSelection> testSelections; + + /// The set of compiler selections for running tests. + final List<CompilerSelection>? compilerSelections; + + /// The set of runtimes on which to run tests. + List<String> get runtimes => _runtimes == null + ? const ['vm'] + : List.unmodifiable(_runtimes.map((runtime) => runtime.name)); + final List<RuntimeSelection>? _runtimes; + + /// Configuration for particular tags. + /// + /// The keys are tag selectors, and the values are configurations for tests + /// whose tags match those selectors. + final Map<BooleanSelector, SuiteConfiguration> tags; + + /// Configuration for particular platforms. + /// + /// The keys are platform selectors, and the values are configurations for + /// those platforms. These configuration should only contain test-level + /// configuration fields, but that isn't enforced. + final Map<PlatformSelector, SuiteConfiguration> onPlatform; + + /// The global test metadata derived from this configuration. + Metadata get metadata { + if (tags.isEmpty && onPlatform.isEmpty) return _metadata; + return _metadata.change( + forTag: tags.map((key, config) => MapEntry(key, config.metadata)), + onPlatform: + onPlatform.map((key, config) => MapEntry(key, config.metadata))); + } + + final Metadata _metadata; + + /// The set of tags that have been declared in any way in this configuration. + late final Set<String> knownTags = UnmodifiableSetView({ + ..._metadata.tags, + for (var selector in tags.keys) ...selector.variables, + for (var configuration in tags.values) ...configuration.knownTags, + for (var configuration in onPlatform.values) ...configuration.knownTags, + }); + + /// Whether or not timeouts should be ignored. + final bool? _ignoreTimeouts; + bool get ignoreTimeouts => _ignoreTimeouts ?? false; + + factory SuiteConfiguration( + {required bool? allowDuplicateTestNames, + required bool? allowTestRandomization, + required bool? jsTrace, + required bool? runSkipped, + required Iterable<String>? dart2jsArgs, + required String? precompiledPath, + required Iterable<CompilerSelection>? compilerSelections, + required Iterable<RuntimeSelection>? runtimes, + required Map<BooleanSelector, SuiteConfiguration>? tags, + required Map<PlatformSelector, SuiteConfiguration>? onPlatform, + required bool? ignoreTimeouts, + + // Test-level configuration + required Timeout? timeout, + required bool? verboseTrace, + required bool? chainStackTraces, + required bool? skip, + required int? retry, + required String? skipReason, + required PlatformSelector? testOn, + required Iterable<String>? addTags}) { + var config = SuiteConfiguration._( + allowDuplicateTestNames: allowDuplicateTestNames, + allowTestRandomization: allowTestRandomization, + jsTrace: jsTrace, + runSkipped: runSkipped, + dart2jsArgs: dart2jsArgs, + testSelections: const {}, + precompiledPath: precompiledPath, + compilerSelections: compilerSelections, + runtimes: runtimes, + tags: tags, + onPlatform: onPlatform, + ignoreTimeouts: ignoreTimeouts, + metadata: Metadata( + timeout: timeout, + verboseTrace: verboseTrace, + chainStackTraces: chainStackTraces, + skip: skip, + retry: retry, + skipReason: skipReason, + testOn: testOn, + tags: addTags)); + return config._resolveTags(); + } + + /// A constructor that doesn't require all of its options to be passed. + /// + /// This should only be used in situations where you really only want to + /// configure a specific restricted set of options. + factory SuiteConfiguration._unsafe( + {bool? allowDuplicateTestNames, + bool? allowTestRandomization, + bool? jsTrace, + bool? runSkipped, + Iterable<String>? dart2jsArgs, + String? precompiledPath, + Iterable<CompilerSelection>? compilerSelections, + Iterable<RuntimeSelection>? runtimes, + Map<BooleanSelector, SuiteConfiguration>? tags, + Map<PlatformSelector, SuiteConfiguration>? onPlatform, + bool? ignoreTimeouts, + + // Test-level configuration + Timeout? timeout, + bool? verboseTrace, + bool? chainStackTraces, + bool? skip, + int? retry, + String? skipReason, + PlatformSelector? testOn, + Iterable<String>? addTags}) => + SuiteConfiguration( + allowDuplicateTestNames: allowDuplicateTestNames, + allowTestRandomization: allowTestRandomization, + jsTrace: jsTrace, + runSkipped: runSkipped, + dart2jsArgs: dart2jsArgs, + precompiledPath: precompiledPath, + compilerSelections: compilerSelections, + runtimes: runtimes, + tags: tags, + onPlatform: onPlatform, + ignoreTimeouts: ignoreTimeouts, + timeout: timeout, + verboseTrace: verboseTrace, + chainStackTraces: chainStackTraces, + skip: skip, + retry: retry, + skipReason: skipReason, + testOn: testOn, + addTags: addTags); + + /// A specialized constructor for only configuring the runtimes. + factory SuiteConfiguration.runtimes(Iterable<RuntimeSelection> runtimes) => + SuiteConfiguration._unsafe(runtimes: runtimes); + + /// A specialized constructor for only configuring runSkipped. + factory SuiteConfiguration.runSkipped(bool runSkipped) => + SuiteConfiguration._unsafe(runSkipped: runSkipped); + + /// A specialized constructor for only configuring the timeout. + factory SuiteConfiguration.timeout(Timeout timeout) => + SuiteConfiguration._unsafe(timeout: timeout); + + /// Creates new SuiteConfiguration. + /// + /// Unlike [SuiteConfiguration.new], this assumes [tags] is already + /// resolved. + SuiteConfiguration._({ + required bool? allowDuplicateTestNames, + required bool? allowTestRandomization, + required bool? jsTrace, + required bool? runSkipped, + required Iterable<String>? dart2jsArgs, + required this.testSelections, + required this.precompiledPath, + required Iterable<CompilerSelection>? compilerSelections, + required Iterable<RuntimeSelection>? runtimes, + required Map<BooleanSelector, SuiteConfiguration>? tags, + required Map<PlatformSelector, SuiteConfiguration>? onPlatform, + required Metadata? metadata, + required bool? ignoreTimeouts, + }) : _allowDuplicateTestNames = allowDuplicateTestNames, + _allowTestRandomization = allowTestRandomization, + _jsTrace = jsTrace, + _runSkipped = runSkipped, + dart2jsArgs = _list(dart2jsArgs) ?? const [], + _runtimes = _list(runtimes), + compilerSelections = _list(compilerSelections), + tags = _map(tags), + onPlatform = _map(onPlatform), + _ignoreTimeouts = ignoreTimeouts, + _metadata = metadata ?? Metadata.empty; + + /// Creates a new [SuiteConfiguration] that takes its configuration from + /// [metadata]. + factory SuiteConfiguration.fromMetadata(Metadata metadata) => + SuiteConfiguration._( + tags: metadata.forTag.map((key, child) => + MapEntry(key, SuiteConfiguration.fromMetadata(child))), + onPlatform: metadata.onPlatform.map((key, child) => + MapEntry(key, SuiteConfiguration.fromMetadata(child))), + metadata: metadata.change(forTag: {}, onPlatform: {}), + allowDuplicateTestNames: null, + allowTestRandomization: null, + jsTrace: null, + runSkipped: null, + dart2jsArgs: null, + testSelections: const {}, + precompiledPath: null, + runtimes: null, + compilerSelections: null, + ignoreTimeouts: null, + ); + + /// Returns an unmodifiable copy of [input]. + /// + /// If [input] is `null` or empty, this returns `null`. + static List<T>? _list<T>(Iterable<T>? input) { + if (input == null) return null; + var list = List<T>.unmodifiable(input); + if (list.isEmpty) return null; + return list; + } + + /// Returns an unmodifiable copy of [input] or an empty unmodifiable map. + static Map<K, V> _map<K, V>(Map<K, V>? input) { + if (input == null || input.isEmpty) return const <Never, Never>{}; + return Map.unmodifiable(input); + } + + /// Merges this with [other]. + /// + /// For most fields, if both configurations have values set, [other]'s value + /// takes precedence. However, certain fields are merged together instead. + /// This is indicated in those fields' documentation. + SuiteConfiguration merge(SuiteConfiguration other) { + if (this == SuiteConfiguration.empty) return other; + if (other == SuiteConfiguration.empty) return this; + assert(testSelections.isEmpty || other.testSelections.isEmpty); + + var config = SuiteConfiguration._( + allowDuplicateTestNames: + other._allowDuplicateTestNames ?? _allowDuplicateTestNames, + allowTestRandomization: + other._allowTestRandomization ?? _allowTestRandomization, + jsTrace: other._jsTrace ?? _jsTrace, + runSkipped: other._runSkipped ?? _runSkipped, + dart2jsArgs: dart2jsArgs.toList()..addAll(other.dart2jsArgs), + testSelections: + testSelections.isEmpty ? other.testSelections : testSelections, + precompiledPath: other.precompiledPath ?? precompiledPath, + compilerSelections: other.compilerSelections ?? compilerSelections, + runtimes: other._runtimes ?? _runtimes, + tags: _mergeConfigMaps(tags, other.tags), + onPlatform: _mergeConfigMaps(onPlatform, other.onPlatform), + ignoreTimeouts: other._ignoreTimeouts ?? _ignoreTimeouts, + metadata: metadata.merge(other.metadata)); + return config._resolveTags(); + } + + /// Returns a copy of this configuration with the given fields updated. + /// + /// Note that unlike [merge], this has no merging behavior—the old value is + /// always replaced by the new one. + SuiteConfiguration change( + {bool? allowDuplicateTestNames, + bool? allowTestRandomization, + bool? jsTrace, + bool? runSkipped, + Iterable<String>? dart2jsArgs, + String? precompiledPath, + Iterable<CompilerSelection>? compilerSelections, + Iterable<RuntimeSelection>? runtimes, + Map<BooleanSelector, SuiteConfiguration>? tags, + Map<PlatformSelector, SuiteConfiguration>? onPlatform, + bool? ignoreTimeouts, + + // Test-level configuration + Timeout? timeout, + bool? verboseTrace, + bool? chainStackTraces, + bool? skip, + int? retry, + String? skipReason, + PlatformSelector? testOn, + Iterable<String>? addTags}) { + var config = SuiteConfiguration._( + allowDuplicateTestNames: + allowDuplicateTestNames ?? _allowDuplicateTestNames, + allowTestRandomization: + allowTestRandomization ?? _allowTestRandomization, + jsTrace: jsTrace ?? _jsTrace, + runSkipped: runSkipped ?? _runSkipped, + dart2jsArgs: dart2jsArgs?.toList() ?? this.dart2jsArgs, + testSelections: testSelections, + precompiledPath: precompiledPath ?? this.precompiledPath, + compilerSelections: compilerSelections ?? this.compilerSelections, + runtimes: runtimes ?? _runtimes, + tags: tags ?? this.tags, + onPlatform: onPlatform ?? this.onPlatform, + ignoreTimeouts: ignoreTimeouts ?? _ignoreTimeouts, + metadata: _metadata.change( + timeout: timeout, + verboseTrace: verboseTrace, + chainStackTraces: chainStackTraces, + skip: skip, + retry: retry, + skipReason: skipReason, + testOn: testOn, + tags: addTags?.toSet())); + return config._resolveTags(); + } + + /// Assign the selection of tests that should run for this suite. + /// + /// Test selections must be chosen only once per suite, once a + /// SuiteConfiguration has test slections this method should not be called + /// again. + /// + /// [testSelections] must not be empty. + SuiteConfiguration selectTests(Set<TestSelection> testSelections) { + assert(this.testSelections.isEmpty); + assert(testSelections.isNotEmpty); + return SuiteConfiguration._( + testSelections: testSelections, + allowDuplicateTestNames: _allowDuplicateTestNames, + allowTestRandomization: _allowTestRandomization, + jsTrace: _jsTrace, + runSkipped: _runSkipped, + dart2jsArgs: dart2jsArgs, + precompiledPath: precompiledPath, + compilerSelections: compilerSelections, + runtimes: _runtimes, + tags: tags, + onPlatform: onPlatform, + ignoreTimeouts: _ignoreTimeouts, + metadata: _metadata); + } + + /// Throws a [FormatException] if this refers to any undefined runtimes. + void validateRuntimes(List<Runtime> allRuntimes) { + var validVariables = + allRuntimes.map((runtime) => runtime.identifier).toSet(); + _metadata.validatePlatformSelectors(validVariables); + + var runtimes = _runtimes; + if (runtimes != null) { + for (var selection in runtimes) { + if (!allRuntimes + .any((runtime) => runtime.identifier == selection.name)) { + if (selection.span != null) { + throw SourceSpanFormatException( + 'Unknown platform "${selection.name}".', selection.span); + } else { + throw FormatException('Unknown platform "${selection.name}".'); + } + } + } + } + + onPlatform.forEach((selector, config) { + selector.validate(validVariables); + config.validateRuntimes(allRuntimes); + }); + } + + /// Returns a copy of this with all platform-specific configuration from + /// [onPlatform] resolved. + SuiteConfiguration forPlatform(SuitePlatform platform) { + if (onPlatform.isEmpty) return this; + + var config = this; + onPlatform.forEach((platformSelector, platformConfig) { + if (!platformSelector.evaluate(platform)) return; + config = config.merge(platformConfig); + }); + return config.change(onPlatform: {}); + } + + /// Merges two maps whose values are [SuiteConfiguration]s. + /// + /// Any overlapping keys in the maps have their configurations merged in the + /// returned map. + Map<T, SuiteConfiguration> _mergeConfigMaps<T>( + Map<T, SuiteConfiguration> map1, Map<T, SuiteConfiguration> map2) => + mergeMaps(map1, map2, + value: (config1, config2) => config1.merge(config2)); + + SuiteConfiguration _resolveTags() { + // If there's no tag-specific configuration, or if none of it applies, just + // return the configuration as-is. + if (_metadata.tags.isEmpty || tags.isEmpty) return this; + + // Otherwise, resolve the tag-specific components. + var newTags = Map<BooleanSelector, SuiteConfiguration>.from(tags); + var merged = tags.keys.fold(empty, (SuiteConfiguration merged, selector) { + if (!selector.evaluate(_metadata.tags.contains)) return merged; + return merged.merge(newTags.remove(selector)!); + }); + + if (merged == empty) return this; + return change(tags: newTags).merge(merged); + } +}
diff --git a/pkgs/test_core/lib/src/runner/util/iterable_set.dart b/pkgs/test_core/lib/src/runner/util/iterable_set.dart new file mode 100644 index 0000000..09335ba --- /dev/null +++ b/pkgs/test_core/lib/src/runner/util/iterable_set.dart
@@ -0,0 +1,44 @@ +// Copyright (c) 2016, 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:collection'; + +import 'package:collection/collection.dart'; + +/// An unmodifiable [Set] view backed by an arbitrary [Iterable]. +/// +/// Note that contrary to most APIs that take iterables, this does not convert +/// its argument to another collection before use. This means that if it's +/// lazily-generated, that generation will happen for every operation. +/// +/// Note also that set operations that are usually expected to be `O(1)` or +/// `O(log(n))`, such as [contains], may be `O(n)` for many underlying iterable +/// types. As such, this should only be used for small iterables. +class IterableSet<E> with SetMixin<E>, UnmodifiableSetMixin<E> { + /// The base iterable that set operations forward to. + final Iterable<E> _base; + + @override + int get length => _base.length; + + @override + Iterator<E> get iterator => _base.iterator; + + /// Creates a [Set] view of [base]. + IterableSet(this._base); + + @override + bool contains(Object? element) => _base.contains(element); + + @override + E? lookup(Object? element) { + for (var e in _base) { + if (e == element) return e; + } + return null; + } + + @override + Set<E> toSet() => _base.toSet(); +}
diff --git a/pkgs/test_core/lib/src/runner/version.dart b/pkgs/test_core/lib/src/runner/version.dart new file mode 100644 index 0000000..3eab242 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/version.dart
@@ -0,0 +1,60 @@ +// Copyright (c) 2015, 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:yaml/yaml.dart'; + +/// The version number of the test runner, or `null` if it couldn't be loaded. +/// +/// This is a semantic version, optionally followed by a space and additional +/// data about its source. +final String? testVersion = (() { + dynamic lockfile; + try { + lockfile = loadYaml(File('pubspec.lock').readAsStringSync()); + } on FormatException catch (_) { + return null; + } on IOException catch (_) { + return null; + } + + if (lockfile is! Map) return null; + var packages = lockfile['packages']; + if (packages is! Map) return null; + var package = packages['test']; + if (package is! Map) return null; + + var source = package['source']; + if (source is! String) return null; + + switch (source) { + case 'hosted': + var version = package['version']; + return (version is String) ? version : null; + + case 'git': + var version = package['version']; + if (version is! String) return null; + var description = package['description']; + if (description is! Map) return null; + var ref = description['resolved-ref']; + if (ref is! String) return null; + + return '$version (${ref.substring(0, 7)})'; + + case 'path': + var version = package['version']; + if (version is! String) return null; + var description = package['description']; + if (description is! Map) return null; + var path = description['path']; + if (path is! String) return null; + + return '$version (from $path)'; + + default: + return null; + } +})();
diff --git a/pkgs/test_core/lib/src/runner/vm/environment.dart b/pkgs/test_core/lib/src/runner/vm/environment.dart new file mode 100644 index 0000000..76cc2fd --- /dev/null +++ b/pkgs/test_core/lib/src/runner/vm/environment.dart
@@ -0,0 +1,42 @@ +// 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. + +import 'dart:async'; + +import 'package:async/async.dart'; +import 'package:vm_service/vm_service.dart'; + +import '../environment.dart'; // ignore: implementation_imports + +/// The environment in which VM tests are loaded. +class VMEnvironment implements Environment { + @override + final supportsDebugging = true; + @override + final Uri observatoryUrl; + + /// The VM service isolate object used to control this isolate. + final IsolateRef _isolate; + final VmService _client; + + VMEnvironment(this.observatoryUrl, this._isolate, this._client); + + @override + Uri? get remoteDebuggerUrl => null; + + @override + Stream<void> get onRestart => StreamController<void>.broadcast().stream; + + @override + CancelableOperation<void> displayPause() { + var completer = + CancelableCompleter<void>(onCancel: () => _client.resume(_isolate.id!)); + + completer.complete(_client.pause(_isolate.id!).then((_) => _client + .onDebugEvent + .firstWhere((event) => event.kind == EventKind.kResume))); + + return completer.operation; + } +}
diff --git a/pkgs/test_core/lib/src/runner/vm/platform.dart b/pkgs/test_core/lib/src/runner/vm/platform.dart new file mode 100644 index 0000000..506909d --- /dev/null +++ b/pkgs/test_core/lib/src/runner/vm/platform.dart
@@ -0,0 +1,357 @@ +// Copyright (c) 2016, 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:convert'; +import 'dart:developer'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:async/async.dart'; +import 'package:coverage/coverage.dart'; +import 'package:path/path.dart' as p; +import 'package:stream_channel/isolate_channel.dart'; +import 'package:stream_channel/stream_channel.dart'; +import 'package:test_api/backend.dart'; +import 'package:vm_service/vm_service.dart' hide Isolate; +import 'package:vm_service/vm_service_io.dart'; + +import '../../runner/configuration.dart'; +import '../../runner/environment.dart'; +import '../../runner/load_exception.dart'; +import '../../runner/platform.dart'; +import '../../runner/plugin/platform_helpers.dart'; +import '../../runner/plugin/shared_platform_helpers.dart'; +import '../../runner/runner_suite.dart'; +import '../../runner/suite.dart'; +import '../../util/io.dart'; +import '../../util/package_config.dart'; +import '../package_version.dart'; +import 'environment.dart'; +import 'test_compiler.dart'; + +var _shouldPauseAfterTests = false; + +/// A platform that loads tests in isolates spawned within this Dart process. +class VMPlatform extends PlatformPlugin { + /// The test runner configuration. + final _config = Configuration.current; + final _compiler = TestCompiler( + p.join(p.current, '.dart_tool', 'test', 'incremental_kernel')); + final _closeMemo = AsyncMemoizer<void>(); + final _tempDir = Directory.systemTemp.createTempSync('dart_test.vm.'); + + @override + Future<RunnerSuite?> load(String path, SuitePlatform platform, + SuiteConfiguration suiteConfig, Map<String, Object?> message) async { + assert(platform.runtime == Runtime.vm); + + _setupPauseAfterTests(); + + MultiChannel outerChannel; + var cleanupCallbacks = <void Function()>[]; + Isolate? isolate; + if (platform.compiler == Compiler.exe) { + var serverSocket = await ServerSocket.bind('localhost', 0); + Process process; + try { + process = + await _spawnExecutable(path, suiteConfig.metadata, serverSocket); + } catch (error) { + unawaited(serverSocket.close()); + rethrow; + } + process.stdout.listen(stdout.add); + process.stderr.listen(stderr.add); + var socket = await serverSocket.first; + outerChannel = MultiChannel<Object?>(jsonSocketStreamChannel(socket)); + cleanupCallbacks + ..add(serverSocket.close) + ..add(process.kill); + } else { + var receivePort = ReceivePort(); + try { + isolate = await _spawnIsolate(path, receivePort.sendPort, + suiteConfig.metadata, platform.compiler); + if (isolate == null) return null; + } catch (error) { + receivePort.close(); + rethrow; + } + outerChannel = MultiChannel(IsolateChannel.connectReceive(receivePort)); + cleanupCallbacks.add(isolate.kill); + } + cleanupCallbacks.add(outerChannel.sink.close); + + VmService? client; + StreamSubscription<Event>? eventSub; + // Typical test interaction will go across `channel`, `outerChannel` adds + // additional communication directly between the test bootstrapping and this + // platform to enable pausing after tests for debugging. + var outerQueue = StreamQueue(outerChannel.stream); + var channelId = (await outerQueue.next) as int; + var channel = outerChannel.virtualChannel(channelId).transformStream( + StreamTransformer.fromHandlers(handleDone: (sink) async { + if (_shouldPauseAfterTests) { + outerChannel.sink.add('debug'); + await outerQueue.next; + } + for (var fn in cleanupCallbacks) { + fn(); + } + unawaited(eventSub?.cancel()); + unawaited(client?.dispose()); + sink.close(); + })); + + Environment? environment; + IsolateRef? isolateRef; + if (_config.debug) { + if (platform.compiler == Compiler.exe) { + throw UnsupportedError( + 'Unable to debug tests compiled to `exe` (tried to debug $path with ' + 'the `exe` compiler).'); + } + var info = + await Service.controlWebServer(enable: true, silenceOutput: true); + // ignore: deprecated_member_use, Remove when SDK constraint is at 3.2.0 + var isolateID = Service.getIsolateID(isolate!)!; + + var libraryPath = (await absoluteUri(path)).toString(); + var serverUri = info.serverUri!; + client = await vmServiceConnectUri(_wsUriFor(serverUri).toString()); + var isolateNumber = int.parse(isolateID.split('/').last); + isolateRef = (await client.getVM()) + .isolates! + .firstWhere((isolate) => isolate.number == isolateNumber.toString()); + await client.setName(isolateRef.id!, path); + var libraryRef = (await client.getIsolate(isolateRef.id!)) + .libraries! + .firstWhere((library) => library.uri == libraryPath); + var url = _observatoryUrlFor(serverUri, isolateRef.id!, libraryRef.id!); + environment = VMEnvironment(url, isolateRef, client); + } + + environment ??= const PluginEnvironment(); + + var controller = deserializeSuite( + path, platform, suiteConfig, environment, channel.cast(), message, + gatherCoverage: () => _gatherCoverage(environment!)); + + if (isolateRef != null) { + await client!.streamListen('Debug'); + eventSub = client.onDebugEvent.listen((event) { + if (event.kind == EventKind.kResume) { + controller.setDebugging(false); + } else if (event.kind == EventKind.kPauseInterrupted || + event.kind == EventKind.kPauseBreakpoint || + event.kind == EventKind.kPauseException) { + controller.setDebugging(true); + } + }); + } + + return await controller.suite; + } + + @override + Future close() => _closeMemo.runOnce(() => Future.wait([ + _compiler.dispose(), + _tempDir.deleteWithRetry(), + ])); + + /// Compiles [path] to a native executable and spawns it as a process. + /// + /// Sets up a communication channel as well by passing command line arguments + /// for the host and port of [socket]. + Future<Process> _spawnExecutable( + String path, Metadata suiteMetadata, ServerSocket socket) async { + if (_config.suiteDefaults.precompiledPath != null) { + throw UnsupportedError( + 'Precompiled native executable tests are not supported at this time'); + } + var executable = await _compileToNative(path, suiteMetadata); + return await Process.start( + executable, [socket.address.host, socket.port.toString()]); + } + + /// Compiles [path] to a native executable using `dart compile exe`. + Future<String> _compileToNative(String path, Metadata suiteMetadata) async { + var bootstrapPath = await _bootstrapNativeTestFile( + path, + suiteMetadata.languageVersionComment ?? + await rootPackageLanguageVersionComment); + var output = File(p.setExtension(bootstrapPath, '.exe')); + var processResult = await Process.run(Platform.resolvedExecutable, [ + 'compile', + 'exe', + bootstrapPath, + '--output', + output.path, + '--packages', + (await packageConfigUri).toFilePath(), + ]); + if (processResult.exitCode != 0 || !(await output.exists())) { + throw LoadException(path, ''' +exitCode: ${processResult.exitCode} +stdout: ${processResult.stdout} +stderr: ${processResult.stderr}'''); + } + return output.path; + } + + /// Spawns an isolate with the current configuration and passes it [message]. + /// + /// This isolate connects an [IsolateChannel] to [message] and sends the + /// serialized tests over that channel. + /// + /// Returns `null` if an exception occurs but [close] has already been called. + Future<Isolate?> _spawnIsolate(String path, SendPort message, + Metadata suiteMetadata, Compiler compiler) async { + try { + var precompiledPath = _config.suiteDefaults.precompiledPath; + if (precompiledPath != null) { + return _spawnPrecompiledIsolate( + path, message, precompiledPath, compiler); + } + return switch (compiler) { + Compiler.kernel => _spawnIsolateWithUri( + await _compileToKernel(path, suiteMetadata), message), + Compiler.source => _spawnIsolateWithUri( + await _bootstrapIsolateTestFile( + path, + suiteMetadata.languageVersionComment ?? + await rootPackageLanguageVersionComment), + message), + _ => throw StateError( + 'Unsupported compiler $compiler for the VM platform'), + }; + } catch (_) { + if (_closeMemo.hasRun) return null; + rethrow; + } + } + + /// Compiles [path] to kernel and returns the uri to the compiled dill. + Future<Uri> _compileToKernel(String path, Metadata suiteMetadata) async { + final response = + await _compiler.compile(await absoluteUri(path), suiteMetadata); + var compiledDill = response.kernelOutputUri?.toFilePath(); + if (compiledDill == null || response.errorCount > 0) { + throw LoadException(path, response.compilerOutput ?? 'unknown error'); + } + return absoluteUri(compiledDill); + } + + /// Runs [uri] in an isolate, passing [message]. + Future<Isolate> _spawnIsolateWithUri(Uri uri, SendPort message) async { + return await Isolate.spawnUri(uri, [], message, + packageConfig: await packageConfigUri, checked: true); + } + + Future<Isolate> _spawnPrecompiledIsolate(String testPath, SendPort message, + String precompiledPath, Compiler compiler) async { + var testUri = + await absoluteUri('${p.join(precompiledPath, testPath)}.vm_test.dart'); + testUri = testUri.replace(path: testUri.path.stripDriveLetterLeadingSlash); + + switch (compiler) { + case Compiler.kernel: + // Load `.dill` files from their absolute file path. + var dillUri = (await Isolate.resolvePackageUri(testUri.replace( + path: + '${testUri.path.substring(0, testUri.path.length - '.dart'.length)}' + '.vm.app.dill')))!; + if (await File.fromUri(dillUri).exists()) { + testUri = dillUri; + } + // TODO: Compile to kernel manually here? Otherwise we aren't compiling + // with kernel when we technically should be, based on the compiler + // setting. + break; + case Compiler.source: + // Just leave test uri as is. + break; + default: + throw StateError('Unsupported compiler for the VM platform $compiler.'); + } + File? packageConfig = + File(p.join(precompiledPath, '.dart_tool/package_config.json')); + if (!(await packageConfig.exists())) { + packageConfig = File(p.join(precompiledPath, '.packages')); + if (!(await packageConfig.exists())) { + packageConfig = null; + } + } + return await Isolate.spawnUri(testUri, [], message, + packageConfig: packageConfig?.uri, checked: true); + } + + /// Bootstraps the test at [testPath] and writes its contents to a temporary + /// file. + /// + /// Returns the [Uri] to the created file. + Future<Uri> _bootstrapIsolateTestFile( + String testPath, String languageVersionComment) async { + var file = File(p.join( + _tempDir.path, p.setExtension(testPath, '.bootstrap.isolate.dart'))); + if (!file.existsSync()) { + file + ..createSync(recursive: true) + ..writeAsStringSync(testBootstrapContents( + testUri: await absoluteUri(testPath), + languageVersionComment: languageVersionComment, + packageConfigUri: await packageConfigUri, + testType: VmTestType.isolate, + )); + } + return file.uri; + } + + /// Bootstraps the test at [testPath] for native execution and writes its + /// contents to a temporary file. + /// + /// Returns the path to the created file. + Future<String> _bootstrapNativeTestFile( + String testPath, String languageVersionComment) async { + var file = File(p.join( + _tempDir.path, p.setExtension(testPath, '.bootstrap.native.dart'))); + if (!file.existsSync()) { + file + ..createSync(recursive: true) + ..writeAsStringSync(testBootstrapContents( + testUri: await absoluteUri(testPath), + languageVersionComment: languageVersionComment, + packageConfigUri: await packageConfigUri, + testType: VmTestType.process, + )); + } + return file.path; + } +} + +Future<Map<String, dynamic>> _gatherCoverage(Environment environment) async { + final isolateId = Uri.parse(environment.observatoryUrl!.fragment) + .queryParameters['isolateId']; + return await collect(environment.observatoryUrl!, false, false, false, {}, + isolateIds: {isolateId!}); +} + +Uri _wsUriFor(Uri observatoryUrl) => + observatoryUrl.replace(scheme: 'ws').resolve('ws'); + +Uri _observatoryUrlFor(Uri base, String isolateId, String id) => base.replace( + fragment: Uri( + path: '/inspect', + queryParameters: {'isolateId': isolateId, 'objectId': id}).toString()); + +var _hasRegistered = false; +void _setupPauseAfterTests() { + if (_hasRegistered) return; + _hasRegistered = true; + registerExtension('ext.test.pauseAfterTests', (_, __) async { + _shouldPauseAfterTests = true; + return ServiceExtensionResponse.result(jsonEncode({})); + }); +}
diff --git a/pkgs/test_core/lib/src/runner/vm/test_compiler.dart b/pkgs/test_core/lib/src/runner/vm/test_compiler.dart new file mode 100644 index 0000000..56fbbd2 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/vm/test_compiler.dart
@@ -0,0 +1,238 @@ +// Copyright (c) 2020, 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:convert'; +import 'dart:io'; + +import 'package:async/async.dart'; +import 'package:frontend_server_client/frontend_server_client.dart'; +import 'package:path/path.dart' as p; +import 'package:pool/pool.dart'; +import 'package:test_api/backend.dart'; + +import '../../util/dart.dart'; +import '../../util/io.dart'; +import '../../util/package_config.dart'; +import '../package_version.dart'; + +class CompilationResponse { + final String? compilerOutput; + final int errorCount; + final Uri? kernelOutputUri; + + const CompilationResponse( + {this.compilerOutput, this.errorCount = 0, this.kernelOutputUri}); + + static const _wasShutdown = CompilationResponse( + errorCount: 1, compilerOutput: 'Compiler no longer active.'); +} + +class TestCompiler { + final _closeMemo = AsyncMemoizer<void>(); + + /// Each language version that appears in test files gets its own compiler, + /// to ensure that all language modes are supported (such as sound and + /// unsound null safety). + final _compilerForLanguageVersion = + <String, _TestCompilerForLanguageVersion>{}; + + /// A prefix used for the dill files for each compiler that is created. + final String _dillCachePrefix; + + /// No work is done until the first call to [compile] is received, at which + /// point the compiler process is started. + TestCompiler(this._dillCachePrefix); + + /// Compiles [mainDart], using a separate compiler per language version of + /// the tests. + Future<CompilationResponse> compile(Uri mainDart, Metadata metadata) async { + if (_closeMemo.hasRun) return CompilationResponse._wasShutdown; + var languageVersionComment = metadata.languageVersionComment ?? + await rootPackageLanguageVersionComment; + var compiler = _compilerForLanguageVersion.putIfAbsent( + languageVersionComment, + () => _TestCompilerForLanguageVersion( + _dillCachePrefix, languageVersionComment)); + return compiler.compile(mainDart); + } + + Future<void> dispose() => _closeMemo.runOnce(() => Future.wait([ + for (var compiler in _compilerForLanguageVersion.values) + compiler.dispose(), + ])); +} + +class _TestCompilerForLanguageVersion { + final _closeMemo = AsyncMemoizer<void>(); + final _compilePool = Pool(1); + final String _dillCachePath; + FrontendServerClient? _frontendServerClient; + final String _languageVersionComment; + late final _outputDill = + File(p.join(_outputDillDirectory.path, 'output.dill')); + final _outputDillDirectory = + Directory.systemTemp.createTempSync('dart_test.kernel.'); + // Used to create unique file names for final kernel files. + int _compileNumber = 0; + // The largest incremental dill file we created, will be cached under + // the `.dart_tool` dir at the end of compilation. + File? _dillToCache; + + _TestCompilerForLanguageVersion( + String dillCachePrefix, this._languageVersionComment) + : _dillCachePath = '$dillCachePrefix.' + '${_dillCacheSuffix(_languageVersionComment, enabledExperiments)}'; + + Future<CompilationResponse> compile(Uri mainUri) => + _compilePool.withResource(() => _compile(mainUri)); + + Future<CompilationResponse> _compile(Uri mainUri) async { + _compileNumber++; + if (_closeMemo.hasRun) return CompilationResponse._wasShutdown; + CompileResult? compilerOutput; + final tempFile = File(p.join(_outputDillDirectory.path, 'test.dart')) + ..writeAsStringSync(testBootstrapContents( + testUri: mainUri, + packageConfigUri: await packageConfigUri, + languageVersionComment: _languageVersionComment, + testType: VmTestType.isolate, + )); + final testCache = File(_dillCachePath); + + try { + if (_frontendServerClient == null) { + if (await testCache.exists()) { + await testCache.copy(_outputDill.path); + } + compilerOutput = await _createCompiler(tempFile.uri); + } else { + compilerOutput = + await _frontendServerClient!.compile(<Uri>[tempFile.uri]); + } + } catch (e, s) { + if (_closeMemo.hasRun) return CompilationResponse._wasShutdown; + return CompilationResponse(errorCount: 1, compilerOutput: '$e\n$s'); + } finally { + _frontendServerClient?.accept(); + _frontendServerClient?.reset(); + } + + // The client is guaranteed initialized at this point. + final outputPath = compilerOutput?.dillOutput; + if (outputPath == null) { + return CompilationResponse( + compilerOutput: compilerOutput?.compilerOutputLines.join('\n'), + errorCount: compilerOutput?.errorCount ?? 0); + } + + final outputFile = File(outputPath); + final kernelReadyToRun = + await outputFile.copy('${tempFile.path}_$_compileNumber.dill'); + // Keep the `_dillToCache` file up-to-date and use the size of the + // kernel file as an approximation for how many packages are included. + // Larger files are preferred, since re-using more packages will reduce the + // number of files the frontend server needs to load and parse. + if (_dillToCache == null || + (_dillToCache!.lengthSync() < kernelReadyToRun.lengthSync())) { + _dillToCache = kernelReadyToRun; + } + + return CompilationResponse( + compilerOutput: compilerOutput?.compilerOutputLines.join('\n'), + errorCount: compilerOutput?.errorCount ?? 0, + kernelOutputUri: kernelReadyToRun.absolute.uri); + } + + Future<CompileResult?> _createCompiler(Uri testUri) async { + final platformDill = 'lib/_internal/vm_platform_strong.dill'; + final sdkRoot = + p.relative(p.dirname(p.dirname(Platform.resolvedExecutable))); + final packageConfigUriAwaited = await packageConfigUri; + + // If we have native assets for the host os in JIT mode, they are here. + Uri? nativeAssetsYaml; + if (enabledExperiments.contains('native-assets')) { + nativeAssetsYaml = packageConfigUriAwaited.resolve('native_assets.yaml'); + if (!await File.fromUri(nativeAssetsYaml).exists()) { + nativeAssetsYaml = null; + } + } + + var client = _frontendServerClient = await FrontendServerClient.start( + testUri.toString(), + _outputDill.path, + platformDill, + enabledExperiments: enabledExperiments, + sdkRoot: sdkRoot, + packagesJson: packageConfigUriAwaited.toFilePath(), + nativeAssets: nativeAssetsYaml?.toFilePath(), + printIncrementalDependencies: false, + ); + return client.compile(); + } + + Future<void> dispose() => _closeMemo.runOnce(() async { + await _compilePool.close(); + if (_dillToCache != null) { + var testCache = File(_dillCachePath); + if (!testCache.parent.existsSync()) { + testCache.parent.createSync(recursive: true); + } + _dillToCache!.copySync(_dillCachePath); + } + _frontendServerClient?.kill(); + _frontendServerClient = null; + if (_outputDillDirectory.existsSync()) { + await _outputDillDirectory.deleteWithRetry(); + } + }); +} + +/// Computes a unique dill cache suffix for each [languageVersionComment] +/// and [enabledExperiments] combination. +String _dillCacheSuffix( + String languageVersionComment, List<String> enabledExperiments) { + var identifierString = + StringBuffer(languageVersionComment.replaceAll(' ', '')); + for (var experiment in enabledExperiments) { + identifierString.writeln(experiment); + } + return base64.encode(utf8.encode(identifierString.toString())); +} + +/// Creates bootstrap file contents for running [testUri]. +/// +/// The [bootstrapType] argument should be either 'Vm' or 'Native' depending on +/// which `internalBootstrap*Test` function should be used. +String testBootstrapContents({ + required Uri testUri, + required String languageVersionComment, + required Uri packageConfigUri, + required VmTestType testType, +}) { + final (mainArgs, forwardedArgName, bootstrapType) = switch (testType) { + VmTestType.isolate => ('_, SendPort sendPort', 'sendPort', 'Vm'), + VmTestType.process => ('List<String> args', 'args', 'Native'), + }; + return ''' + $languageVersionComment + + import 'dart:isolate'; + + import 'package:test_core/src/bootstrap/vm.dart'; + + import '$testUri' as test; + + // This variable is read at runtime through the VM service and is unsafe to + // remove. + const packageConfigLocation = '$packageConfigUri'; + + void main($mainArgs) { + internalBootstrap${bootstrapType}Test(() => test.main, $forwardedArgName); + } + '''; +} + +enum VmTestType { isolate, process }
diff --git a/pkgs/test_core/lib/src/runner/wasm_compiler_pool.dart b/pkgs/test_core/lib/src/runner/wasm_compiler_pool.dart new file mode 100644 index 0000000..c4318f5 --- /dev/null +++ b/pkgs/test_core/lib/src/runner/wasm_compiler_pool.dart
@@ -0,0 +1,96 @@ +// 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:convert'; +import 'dart:io'; + +import 'package:path/path.dart' as p; + +import '../util/dart.dart'; +import '../util/io.dart'; +import '../util/package_config.dart'; +import 'compiler_pool.dart'; +import 'suite.dart'; + +/// A pool of `dart2wasm` compiler instances. +/// +/// This limits the number of compiler instances running concurrently. +class WasmCompilerPool extends CompilerPool { + /// Extra arguments to pass to `dart compile js`. + final List<String> _extraArgs; + + /// The currently-active dart2wasm processes. + final _processes = <Process>{}; + + WasmCompilerPool([this._extraArgs = const []]); + + /// Compiles [code] to [path]. + /// + /// This wraps the Dart code in the standard browser-testing wrapper. + /// + /// The returned [Future] will complete once the `dart2wasm` process completes + /// *and* all its output has been printed to the command line. + @override + Future compileInternal( + String code, String path, SuiteConfiguration suiteConfig) { + return withTempDir((dir) async { + final wrapperPath = p.join(dir, 'main.dart'); + File(wrapperPath).writeAsStringSync(code); + final outWasmPath = '$path.wasm'; + final process = await Process.start(Platform.resolvedExecutable, [ + 'compile', + 'wasm', + '--enable-asserts', + '--packages=${(await packageConfigUri).toFilePath()}', + for (var experiment in enabledExperiments) + '--enable-experiment=$experiment', + '-O0', + ..._extraArgs, + '-o', + outWasmPath, + wrapperPath, + ]); + if (closed) { + process.kill(); + return; + } + + _processes.add(process); + + /// Wait until the process is entirely done to print out any output. + /// This can produce a little extra time for users to wait with no + /// update, but it also avoids some really nasty-looking interleaved + /// output. Write both stdout and stderr to the same buffer in case + /// they're intended to be printed in order. + var buffer = StringBuffer(); + + await Future.wait([ + process.stdout.transform(utf8.decoder).forEach(buffer.write), + process.stderr.transform(utf8.decoder).forEach(buffer.write), + ]); + + var exitCode = await process.exitCode; + _processes.remove(process); + if (closed) return; + + var output = buffer.toString(); + if (output.isNotEmpty) print(output); + + if (exitCode != 0) throw StateError('dart2wasm failed.'); + }); + } + + /// Closes the compiler pool. + /// + /// This kills all currently-running compilers and ensures that no more will + /// be started. It returns a [Future] that completes once all the compilers + /// have been killed and all resources released. + @override + Future<void> closeInternal() async { + await Future.wait(_processes.map((process) async { + process.kill(); + await process.exitCode; + })); + } +}
diff --git a/pkgs/test_core/lib/src/scaffolding.dart b/pkgs/test_core/lib/src/scaffolding.dart new file mode 100644 index 0000000..31b3d89 --- /dev/null +++ b/pkgs/test_core/lib/src/scaffolding.dart
@@ -0,0 +1,295 @@ +// Copyright (c) 2021, 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 'package:meta/meta.dart' show doNotSubmit, isTest, isTestGroup; +import 'package:path/path.dart' as p; +import 'package:test_api/backend.dart'; +import 'package:test_api/scaffolding.dart' show Timeout, pumpEventQueue; +import 'package:test_api/src/backend/declarer.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/invoker.dart'; // ignore: implementation_imports + +import 'runner/engine.dart'; +import 'runner/plugin/environment.dart'; +import 'runner/reporter/expanded.dart'; +import 'runner/runner_suite.dart'; +import 'runner/suite.dart'; +import 'util/os.dart'; +import 'util/print_sink.dart'; + +// Hide implementations which don't support being run directly. +// This file is an almost direct copy of import below, but with the global +// declarer added. +export 'package:test_api/scaffolding.dart' + hide group, setUp, setUpAll, tearDown, tearDownAll, test; + +/// The global declarer. +/// +/// This is used if a test file is run directly, rather than through the runner. +Declarer? _globalDeclarer; + +/// Gets the declarer for the current scope. +/// +/// When using the runner, this returns the [Zone]-scoped declarer that's set by +/// [RemoteListener]. If the test file is run directly, this returns +/// [_globalDeclarer] (and sets it up on the first call). +Declarer get _declarer { + var declarer = Declarer.current; + if (declarer != null) return declarer; + if (_globalDeclarer != null) return _globalDeclarer!; + + // Since there's no Zone-scoped declarer, the test file is being run directly. + // In order to run the tests, we set up our own Declarer via + // [_globalDeclarer], and pump the event queue as a best effort to wait for + // all tests to be defined before starting them. + _globalDeclarer = Declarer(isStandalone: true); + + () async { + await pumpEventQueue(); + + var suite = RunnerSuite( + const PluginEnvironment(), + SuiteConfiguration.empty, + _globalDeclarer!.build(), + SuitePlatform(Runtime.vm, compiler: null, os: currentOSGuess), + path: p.prettyUri(Uri.base)); + + var engine = Engine(); + engine.suiteSink.add(suite); + engine.suiteSink.close(); + ExpandedReporter.watch(engine, PrintSink(), + color: true, printPath: false, printPlatform: false); + + var success = await runZoned(() => Invoker.guard(engine.run), + zoneValues: {#test.declarer: _globalDeclarer}); + if (success == true) return null; + print(''); + unawaited(Future.error('Dummy exception to set exit code.')); + }(); + + return _globalDeclarer!; +} + +// TODO(nweiz): This and other top-level functions should throw exceptions if +// they're called after the declarer has finished declaring. +/// Creates a new test case with the given description (converted to a string) +/// and body. +/// +/// The description will be added to the descriptions of any surrounding +/// [group]s. If [testOn] is passed, it's parsed as a [platform selector][]; the +/// test will only be run on matching platforms. +/// +/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors +/// +/// If [timeout] is passed, it's used to modify or replace the default timeout +/// of 30 seconds. Timeout modifications take precedence in suite-group-test +/// order, so [timeout] will also modify any timeouts set on the group or suite. +/// +/// If [skip] is a String or `true`, the test is skipped. If it's a String, it +/// should explain why the test is skipped; this reason will be printed instead +/// of running the test. If a call to [test] is nested within a [group], a +/// non-null `skip` parameter for the `test` will take precedence over the skip +/// parameter in the `group`. For instance, if a `group` is set to `skip: true`, +/// but a `test` within it is configured as `skip: false`, the `test` will not +/// be skipped. A suite level `@Skip()` annotation cannot be overridden with +/// `skip` arguments to `test` or `group`. +/// +/// If [tags] is passed, it declares user-defined tags that are applied to the +/// test. These tags can be used to select or skip the test on the command line, +/// or to do bulk test configuration. All tags should be declared in the +/// [package configuration file][configuring tags]. The parameter can be an +/// [Iterable] of tag names, or a [String] representing a single tag. +/// +/// If [retry] is passed, the test will be retried the provided number of times +/// before being marked as a failure. +/// +/// [configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags +/// +/// [onPlatform] allows tests to be configured on a platform-by-platform +/// basis. It's a map from strings that are parsed as [PlatformSelector]s to +/// annotation classes: [Timeout], [Skip], or lists of those. These +/// annotations apply only on the given platforms. For example: +/// +/// test('potentially slow test', () { +/// // ... +/// }, onPlatform: { +/// // This test is especially slow on Windows. +/// 'windows': Timeout.factor(2), +/// 'browser': [ +/// Skip('TODO: add browser support'), +/// // This will be slow on browsers once it works on them. +/// Timeout.factor(2) +/// ] +/// }); +/// +/// If multiple platforms match, the annotations apply in order as through +/// they were in nested groups. +/// +/// If the `solo` flag is `true`, only tests and groups marked as +/// "solo" will be be run. This only restricts tests *within this test +/// suite*—tests in other suites will run as normal. We recommend that users +/// avoid this flag if possible and instead use the test runner flag `-n` to +/// filter tests by name. +@isTest +void test(Object? description, dynamic Function() body, + {String? testOn, + Timeout? timeout, + Object? skip, + Object? tags, + Map<String, dynamic>? onPlatform, + int? retry, + // TODO(https://github.com/dart-lang/test/issues/2205): Remove deprecated. + @Deprecated('Debug only') @doNotSubmit bool solo = false}) { + _declarer.test(description.toString(), body, + testOn: testOn, + timeout: timeout, + skip: skip, + onPlatform: onPlatform, + tags: tags, + retry: retry, + solo: solo); + + // Force dart2js not to inline this function. We need it to be separate from + // `main()` in JS stack traces in order to properly determine the line and + // column where the test was defined. See sdk#26705. + return; + return; // ignore: dead_code +} + +/// Creates a group of tests. +/// +/// A group's description (converted to a string) is included in the descriptions +/// of any tests or sub-groups it contains. [setUp] and [tearDown] are also scoped +/// to the containing group. +/// +/// If [testOn] is passed, it's parsed as a [platform selector][]; the test will +/// only be run on matching platforms. +/// +/// [platform selector]: https://github.com/dart-lang/test/tree/master/pkgs/test#platform-selectors +/// +/// If [timeout] is passed, it's used to modify or replace the default timeout +/// of 30 seconds. Timeout modifications take precedence in suite-group-test +/// order, so [timeout] will also modify any timeouts set on the suite, and will +/// be modified by any timeouts set on individual tests. +/// +/// If [skip] is a String or `true`, the group is skipped. If it's a String, it +/// should explain why the group is skipped; this reason will be printed instead +/// of running the group's tests. +/// +/// If [tags] is passed, it declares user-defined tags that are applied to the +/// test. These tags can be used to select or skip the test on the command line, +/// or to do bulk test configuration. All tags should be declared in the +/// [package configuration file][configuring tags]. The parameter can be an +/// [Iterable] of tag names, or a [String] representing a single tag. +/// +/// [configuring tags]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/configuration.md#configuring-tags +/// +/// [onPlatform] allows groups to be configured on a platform-by-platform +/// basis. It's a map from strings that are parsed as [PlatformSelector]s to +/// annotation classes: [Timeout], [Skip], or lists of those. These +/// annotations apply only on the given platforms. For example: +/// +/// group('potentially slow tests', () { +/// // ... +/// }, onPlatform: { +/// // These tests are especially slow on Windows. +/// 'windows': Timeout.factor(2), +/// 'browser': [ +/// Skip('TODO: add browser support'), +/// // They'll be slow on browsers once it works on them. +/// Timeout.factor(2) +/// ] +/// }); +/// +/// If multiple platforms match, the annotations apply in order as through +/// they were in nested groups. +/// +/// If the `solo` flag is `true`, only tests and groups marked as +/// "solo" will be be run. This only restricts tests *within this test +/// suite*—tests in other suites will run as normal. We recommend that users +/// avoid this flag if possible, and instead use the test runner flag `-n` to +/// filter tests by name. +@isTestGroup +void group(Object? description, dynamic Function() body, + {String? testOn, + Timeout? timeout, + Object? skip, + Object? tags, + Map<String, dynamic>? onPlatform, + int? retry, + // TODO(https://github.com/dart-lang/test/issues/2205): Remove deprecated. + @Deprecated('Debug only') @doNotSubmit bool solo = false}) { + _declarer.group(description.toString(), body, + testOn: testOn, + timeout: timeout, + skip: skip, + tags: tags, + onPlatform: onPlatform, + retry: retry, + solo: solo); + + // Force dart2js not to inline this function. We need it to be separate from + // `main()` in JS stack traces in order to properly determine the line and + // column where the test was defined. See sdk#26705. + return; + return; // ignore: dead_code +} + +/// Registers a function to be run before tests. +/// +/// This function will be called before each test is run. [callback] may be +/// asynchronous; if so, it must return a [Future]. +/// +/// If this is called within a test group, it applies only to tests in that +/// group. [callback] will be run after any set-up callbacks in parent groups or +/// at the top level. +/// +/// Each callback at the top level or in a given group will be run in the order +/// they were declared. +void setUp(dynamic Function() callback) => _declarer.setUp(callback); + +/// Registers a function to be run after tests. +/// +/// This function will be called after each test is run. [callback] may be +/// asynchronous; if so, it must return a [Future]. +/// +/// If this is called within a test group, it applies only to tests in that +/// group. [callback] will be run before any tear-down callbacks in parent +/// groups or at the top level. +/// +/// Each callback at the top level or in a given group will be run in the +/// reverse of the order they were declared. +/// +/// See also [addTearDown], which adds tear-downs to a running test. +void tearDown(dynamic Function() callback) => _declarer.tearDown(callback); + +/// Registers a function to be run once before all tests. +/// +/// [callback] may be asynchronous; if so, it must return a [Future]. +/// +/// If this is called within a test group, [callback] will run before all tests +/// in that group. It will be run after any [setUpAll] callbacks in parent +/// groups or at the top level. It won't be run if none of the tests in the +/// group are run. +/// +/// **Note**: This function makes it very easy to accidentally introduce hidden +/// dependencies between tests that should be isolated. In general, you should +/// prefer [setUp], and only use [setUpAll] if the callback is prohibitively +/// slow. +void setUpAll(dynamic Function() callback) => _declarer.setUpAll(callback); + +/// Registers a function to be run once after all tests. +/// +/// If this is called within a test group, [callback] will run after all tests +/// in that group. It will be run before any [tearDownAll] callbacks in parent +/// groups or at the top level. It won't be run if none of the tests in the +/// group are run. +/// +/// **Note**: This function makes it very easy to accidentally introduce hidden +/// dependencies between tests that should be isolated. In general, you should +/// prefer [tearDown], and only use [tearDownAll] if the callback is +/// prohibitively slow. +void tearDownAll(dynamic Function() callback) => + _declarer.tearDownAll(callback);
diff --git a/pkgs/test_core/lib/src/util/async.dart b/pkgs/test_core/lib/src/util/async.dart new file mode 100644 index 0000000..036d2b1 --- /dev/null +++ b/pkgs/test_core/lib/src/util/async.dart
@@ -0,0 +1,32 @@ +// Copyright (c) 2021, 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 'package:async/async.dart'; + +/// Returns a single-subscription stream that emits the results of [operations] +/// in the order they complete. +/// +/// If the subscription is canceled, any pending operations are canceled as +/// well. +Stream<T> inCompletionOrder<T>(Iterable<CancelableOperation<T>> operations) { + var operationSet = operations.toSet(); + var controller = StreamController<T>( + sync: true, + onCancel: () => + Future.wait(operationSet.map((operation) => operation.cancel()))); + + for (var operation in operationSet) { + operation.value + .then((value) => controller.add(value)) + .onError(controller.addError) + .whenComplete(() { + operationSet.remove(operation); + if (operationSet.isEmpty) controller.close(); + }); + } + + return controller.stream; +}
diff --git a/pkgs/test_core/lib/src/util/dart.dart b/pkgs/test_core/lib/src/util/dart.dart new file mode 100644 index 0000000..5a1546c --- /dev/null +++ b/pkgs/test_core/lib/src/util/dart.dart
@@ -0,0 +1,91 @@ +// Copyright (c) 2015, 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 'dart:io'; +import 'dart:isolate'; + +import 'package:analyzer/dart/ast/ast.dart'; +import 'package:source_span/source_span.dart'; + +import '../util/package_config.dart'; +import 'string_literal_iterator.dart'; + +/// Runs [code] in an isolate. +/// +/// [code] should be the contents of a Dart entrypoint. It may contain imports; +/// they will be resolved in the same context as the host isolate. [message] is +/// passed to the [main] method of the code being run; the caller is responsible +/// for using this to establish communication with the isolate. +Future<Isolate> runInIsolate(String code, Object message, + {SendPort? onExit}) async => + Isolate.spawnUri( + Uri.dataFromString(code, mimeType: 'application/dart', encoding: utf8), + [], + message, + packageConfig: await packageConfigUri, + checked: true, + onExit: onExit); + +/// Takes a span whose source is the value of a string that has been parsed from +/// a Dart file and returns the corresponding span from within that Dart file. +/// +/// For example, suppose a Dart file contains `@Eval("1 + a")`. The +/// [StringLiteral] `"1 + a"` is extracted; this is [context]. Its contents are +/// then parsed, producing an error pointing to [span]: +/// +/// line 1, column 5: +/// 1 + a +/// ^ +/// +/// This span isn't very useful, since it only shows the location within the +/// [StringLiteral]'s value. So it's passed to [contextualizeSpan] along with +/// [context] and [file] (which contains the source of the entire Dart file), +/// which then returns: +/// +/// line 4, column 12 of file.dart: +/// @Eval("1 + a") +/// ^ +/// +/// This properly handles multiline literals, adjacent literals, and literals +/// containing escape sequences. It does not support interpolated literals. +/// +/// This will return `null` if [context] contains an invalid string or does not +/// contain [span]. +SourceSpan? contextualizeSpan( + SourceSpan span, StringLiteral context, SourceFile file) { + var contextRunes = StringLiteralIterator(context)..moveNext(); + + for (var i = 0; i < span.start.offset; i++) { + if (!contextRunes.moveNext()) return null; + } + + var start = contextRunes.offset; + for (var spanRune in span.text.runes) { + if (spanRune != contextRunes.current) return null; + contextRunes.moveNext(); + } + + return file.span(start, contextRunes.offset); +} + +/// Parses and returns the currently enabled experiments from +/// [Platform.executableArguments]. +final List<String> enabledExperiments = () { + var experiments = <String>[]; + var itr = Platform.executableArguments.iterator; + while (itr.moveNext()) { + var arg = itr.current; + if (arg == '--enable-experiment') { + if (!itr.moveNext()) break; + experiments.add(itr.current); + } else if (arg.startsWith('--enable-experiment=')) { + var parts = arg.split('='); + if (parts.length == 2) { + experiments.addAll(parts[1].split(',')); + } + } + } + return experiments; +}();
diff --git a/pkgs/test_core/lib/src/util/detaching_future.dart b/pkgs/test_core/lib/src/util/detaching_future.dart new file mode 100644 index 0000000..594f313 --- /dev/null +++ b/pkgs/test_core/lib/src/util/detaching_future.dart
@@ -0,0 +1,32 @@ +// Copyright (c) 2020, 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. + +/// A handle on a [Future] that detaches from the original evaluation context +/// after it has resolved. +/// +/// A [Future] holds the Zone it was created in and may hold references to +/// `async` functions that `await` it. When a future is stored in a static +/// variable it won't be garbage collected which means all zone variables and +/// variables in async functions get leaked. [DetachingFuture] works around this +/// by forgetting the original [Future] after it has resolved, and wrapping the +/// resolved value with `Future.value` for later calls. +/// +/// https://github.com/dart-lang/sdk/issues/42457 +/// https://github.com/dart-lang/sdk/issues/42458 +/// +/// In the case of a future that resolves to an error the original future is +/// retained. +class DetachingFuture<T> { + late T _value; + Future<T>? _inProgress; + + DetachingFuture(Future<T> inProgress) : _inProgress = inProgress { + inProgress.then((result) { + _value = result; + _inProgress = null; + }); + } + + Future<T> get asFuture => _inProgress ?? Future.value(_value); +}
diff --git a/pkgs/test_core/lib/src/util/errors.dart b/pkgs/test_core/lib/src/util/errors.dart new file mode 100644 index 0000000..7ebea6e --- /dev/null +++ b/pkgs/test_core/lib/src/util/errors.dart
@@ -0,0 +1,14 @@ +// Copyright (c) 2021, 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. + +/// A regular expression to match the exception prefix that some exceptions' +/// [Object.toString] values contain. +final _exceptionPrefix = RegExp(r'^([A-Z][a-zA-Z]*)?(Exception|Error): '); + +/// Get a string description of an exception. +/// +/// Many exceptions include the exception class name at the beginning of their +/// [toString], so we remove that if it exists. +String getErrorMessage(Object error) => + error.toString().replaceFirst(_exceptionPrefix, '');
diff --git a/pkgs/test_core/lib/src/util/exit_codes.dart b/pkgs/test_core/lib/src/util/exit_codes.dart new file mode 100644 index 0000000..7c8b2db --- /dev/null +++ b/pkgs/test_core/lib/src/util/exit_codes.dart
@@ -0,0 +1,59 @@ +// Copyright (c) 2015, 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. + +/// Exit code constants. +/// +/// From [the BSD sysexits manpage][manpage]. Not every constant here is used. +/// +/// [manpage]: http://www.freebsd.org/cgi/man.cgi?query=sysexits +/// The command completely successfully. +const success = 0; + +/// The command was used incorrectly. +const usage = 64; + +/// The input data was incorrect. +const data = 65; + +/// An input file did not exist or was unreadable. +const noInput = 66; + +/// The user specified did not exist. +const noUser = 67; + +/// The host specified did not exist. +const noHost = 68; + +/// A service is unavailable. +const unavailable = 69; + +/// An internal software error has been detected. +const software = 70; + +/// An operating system error has been detected. +const os = 71; + +/// Some system file did not exist or was unreadable. +const osFile = 72; + +/// A user-specified output file cannot be created. +const cantCreate = 73; + +/// An error occurred while doing I/O on some file. +const io = 74; + +/// Temporary failure, indicating something that is not really an error. +const tempFail = 75; + +/// The remote system returned something invalid during a protocol exchange. +const protocol = 76; + +/// The user did not have sufficient permissions. +const noPerm = 77; + +/// Something was unconfigured or mis-configured. +const config = 78; + +/// No tests were ran. +const noTestsRan = 79;
diff --git a/pkgs/test_core/lib/src/util/io.dart b/pkgs/test_core/lib/src/util/io.dart new file mode 100644 index 0000000..a082dd3 --- /dev/null +++ b/pkgs/test_core/lib/src/util/io.dart
@@ -0,0 +1,267 @@ +// Copyright (c) 2015, 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:convert'; +import 'dart:core' as core; +import 'dart:core'; +import 'dart:io'; +import 'dart:math'; + +import 'package:async/async.dart'; +import 'package:path/path.dart' as p; +import 'package:test_api/src/backend/compiler.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/operating_system.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports + +import 'pretty_print.dart'; + +/// The default line length for output when there isn't a terminal attached to +/// stdout. +const _defaultLineLength = 200; + +/// Whether the test runner is running on Google-internal infrastructure. +final bool inGoogle = Platform.version.contains('(google3)'); + +/// The maximum line length for output. +final int lineLength = () { + try { + return stdout.terminalColumns; + } on UnsupportedError { + // This can throw an [UnsupportedError] if we're running in a JS context + // where `dart:io` is unavailable. + return _defaultLineLength; + } on StdoutException { + return _defaultLineLength; + } +}(); + +/// The root directory of the Dart SDK. +final String sdkDir = p.dirname(p.dirname(Platform.resolvedExecutable)); + +/// The current operating system. +final currentOS = OperatingSystem.findByIoName(Platform.operatingSystem); + +/// Returns a [SuitePlatform] with the given [runtime], and with +/// [SuitePlatform.os] and [inGoogle] determined automatically. +/// +/// If [runtime] is a browser, this will set [SuitePlatform.os] to +/// [OperatingSystem.none]. +// TODO: https://github.com/dart-lang/test/issues/2119 - require compiler +SuitePlatform currentPlatform(Runtime runtime, [Compiler? compiler]) => + SuitePlatform(runtime, + compiler: compiler, + os: runtime.isBrowser ? OperatingSystem.none : currentOS, + inGoogle: inGoogle); + +/// A transformer that decodes bytes using UTF-8 and splits them on newlines. +final lineSplitter = StreamTransformer<List<int>, String>( + (stream, cancelOnError) => utf8.decoder + .bind(stream) + .transform(const LineSplitter()) + .listen(null, cancelOnError: cancelOnError)); + +/// A queue of lines of standard input. +/// +/// Also returns an empty stream for Fuchsia since Fuchsia components can't +/// access stdin. +StreamQueue<String> get stdinLines => + _stdinLines ??= StreamQueue(Platform.isFuchsia + ? const Stream<String>.empty() + : lineSplitter.bind(stdin)); + +StreamQueue<String>? _stdinLines; + +/// Call cancel on [stdinLines], but only if it's been accessed previously. +void cancelStdinLines() => _stdinLines?.cancel(immediate: true); + +/// Whether this is being run as a subprocess in the test package's own tests. +bool inTestTests = Platform.environment['_DART_TEST_TESTING'] == 'true'; + +/// The root directory below which to nest temporary directories created by the +/// test runner. +/// +/// This is configurable so that the test code can validate that the runner +/// cleans up after itself fully. +final _tempDir = Platform.environment.containsKey('_UNITTEST_TEMP_DIR') + ? Platform.environment['_UNITTEST_TEMP_DIR']! + : Directory.systemTemp.path; + +/// Whether or not the current terminal supports ansi escape codes. +/// +/// Otherwise only printable ASCII characters should be used. +bool get canUseSpecialChars => + (!Platform.isWindows || stdout.supportsAnsiEscapes) && !inTestTests; + +/// Detect whether we're running in a Github Actions context. +/// +/// See +/// https://docs.github.com/en/actions/learn-github-actions/environment-variables. +bool get inGithubContext => Platform.environment['GITHUB_ACTIONS'] == 'true'; + +/// Creates a temporary directory and returns its path. +String createTempDir() => + Directory(_tempDir).createTempSync('dart_test_').resolveSymbolicLinksSync(); + +/// Creates a temporary directory and passes its path to [fn]. +/// +/// Once the [Future] returned by [fn] completes, the temporary directory and +/// all its contents are deleted. [fn] can also return `null`, in which case +/// the temporary directory is deleted immediately afterwards. +/// +/// Returns a future that completes to the value that the future returned from +/// [fn] completes to. +Future withTempDir(Future Function(String) fn) { + return Future.sync(() { + var tempDir = createTempDir(); + return Future.sync(() => fn(tempDir)) + .whenComplete(() => Directory(tempDir).deleteWithRetry()); + }); +} + +/// Wraps [text] so that it fits within [lineLength]. +/// +/// This preserves existing newlines and doesn't consider terminal color escapes +/// part of a word's length. It only splits words on spaces, not on other sorts +/// of whitespace. +String wordWrap(String text) { + return text.split('\n').map((originalLine) { + var buffer = StringBuffer(); + var lengthSoFar = 0; + for (var word in originalLine.split(' ')) { + var wordLength = withoutColors(word).length; + if (wordLength > lineLength) { + if (lengthSoFar != 0) buffer.writeln(); + buffer.writeln(word); + } else if (lengthSoFar == 0) { + buffer.write(word); + lengthSoFar = wordLength; + } else if (lengthSoFar + 1 + wordLength > lineLength) { + buffer.writeln(); + buffer.write(word); + lengthSoFar = wordLength; + } else { + buffer.write(' $word'); + lengthSoFar += 1 + wordLength; + } + } + return buffer.toString(); + }).join('\n'); +} + +/// Print a warning containing [message]. +/// +/// This automatically wraps lines if they get too long. If [color] is passed, +/// it controls whether the warning header is color; otherwise, it defaults to +/// [canUseSpecialChars]. +/// +/// If [print] is `true`, this prints the message using [print] to associate it +/// with the current test. Otherwise, it prints it using [stderr]. +void warn(String message, {bool? color, bool print = false}) { + color ??= canUseSpecialChars; + var header = color ? '\u001b[33mWarning:\u001b[0m' : 'Warning:'; + (print ? core.print : stderr.writeln)(wordWrap('$header $message\n')); +} + +/// Repeatedly finds a probably-unused port on localhost and passes it to +/// [tryPort] until it binds successfully. +/// +/// [tryPort] should return a non-`null` value or a Future completing to a +/// non-`null` value once it binds successfully. This value will be returned +/// by [getUnusedPort] in turn. +/// +/// This is necessary for ensuring that our port binding isn't flaky for +/// applications that don't print out the bound port. +Future<T> getUnusedPort<T extends Object>( + FutureOr<T> Function(int port) tryPort) async { + T? value; + await Future.doWhile(() async { + value = await tryPort(await getUnsafeUnusedPort()); + return value == null; + }); + return value!; +} + +/// Whether this computer supports binding to IPv6 addresses. +var _maySupportIPv6 = true; + +/// Returns a port that is probably, but not definitely, not in use. +/// +/// This has a built-in race condition: another process may bind this port at +/// any time after this call has returned. If at all possible, callers should +/// use [getUnusedPort] instead. +Future<int> getUnsafeUnusedPort() async { + late int port; + if (_maySupportIPv6) { + try { + final socket = await ServerSocket.bind(InternetAddress.loopbackIPv6, 0, + v6Only: true); + port = socket.port; + await socket.close(); + } on SocketException { + _maySupportIPv6 = false; + } + } + if (!_maySupportIPv6) { + final socket = await RawServerSocket.bind(InternetAddress.loopbackIPv4, 0); + port = socket.port; + await socket.close(); + } + return port; +} + +/// Returns the full URL of the Chrome remote debugger for the main page. +/// +/// This takes the [base] remote debugger URL (which points to a browser-wide +/// page) and uses its JSON API to find the resolved URL for debugging the host +/// page. +Future<Uri> getRemoteDebuggerUrl(Uri base) async { + try { + var client = HttpClient(); + var request = await client.getUrl(base.resolve('/json/list')); + var response = await request.close(); + var jsonObject = + await json.fuse(utf8).decoder.bind(response).single as List; + return base + .resolve((jsonObject.first as Map)['devtoolsFrontendUrl'] as String); + } catch (_) { + // If we fail to talk to the remote debugger protocol, give up and return + // the raw URL rather than crashing. + return base; + } +} + +extension RetryDelete on FileSystemEntity { + Future<void> deleteWithRetry() async { + var attempt = 0; + while (true) { + try { + await delete(recursive: true); + return; + } on FileSystemException { + if (attempt == 2) rethrow; + attempt++; + await Future<void>.delayed( + Duration(milliseconds: pow(10, attempt).toInt())); + } + } + } +} + +extension WindowsFilePaths on String { + /// Strip out the leading slash before the drive letter on windows. + /// + /// In some windows environments full paths get passed with `/` before the + /// drive letter. Normalize paths to exclude this slash when it exists. + String get stripDriveLetterLeadingSlash { + if (Platform.isWindows && + startsWith('/') && + length >= 3 && + this[2] == ':') { + return substring(1); + } + return this; + } +}
diff --git a/pkgs/test_core/lib/src/util/io_stub.dart b/pkgs/test_core/lib/src/util/io_stub.dart new file mode 100644 index 0000000..7953877 --- /dev/null +++ b/pkgs/test_core/lib/src/util/io_stub.dart
@@ -0,0 +1,11 @@ +// 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. + +import 'package:test_api/src/backend/compiler.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports +import 'package:test_api/src/backend/suite_platform.dart'; // ignore: implementation_imports + +SuitePlatform currentPlatform(Runtime runtime, Compiler? compiler) => + throw UnsupportedError( + 'Getting the current platform is only supported where dart:io exists');
diff --git a/pkgs/test_core/lib/src/util/os.dart b/pkgs/test_core/lib/src/util/os.dart new file mode 100644 index 0000000..3db1608 --- /dev/null +++ b/pkgs/test_core/lib/src/util/os.dart
@@ -0,0 +1,30 @@ +// Copyright (c) 2021, 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_api/src/backend/operating_system.dart'; // ignore: implementation_imports + +/// Directories that are specific to OS X. +/// +/// This is used to try to distinguish OS X and Linux in [currentOSGuess]. +final _macOSDirectories = { + '/Applications', + '/Library', + '/Network', + '/System', + '/Users', +}; + +/// Returns the best guess for the current operating system without using +/// `dart:io`. +/// +/// This is useful for running test files directly and skipping tests as +/// appropriate. The only OS-specific information we have is the current path, +/// which we try to use to figure out the OS. +final OperatingSystem currentOSGuess = (() { + if (p.style == p.Style.url) return OperatingSystem.none; + if (p.style == p.Style.windows) return OperatingSystem.windows; + if (_macOSDirectories.any(p.current.startsWith)) return OperatingSystem.macOS; + return OperatingSystem.linux; +})();
diff --git a/pkgs/test_core/lib/src/util/package_config.dart b/pkgs/test_core/lib/src/util/package_config.dart new file mode 100644 index 0000000..c33dda6 --- /dev/null +++ b/pkgs/test_core/lib/src/util/package_config.dart
@@ -0,0 +1,39 @@ +// Copyright (c) 2020, 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 'dart:isolate'; + +import 'package:package_config/package_config.dart'; +import 'package:path/path.dart' as p; + +/// The [PackageConfig] parsed from the current isolates package config file. +final Future<PackageConfig> currentPackageConfig = () async { + return loadPackageConfigUri(await packageConfigUri); +}(); + +final Future<Uri> packageConfigUri = () async { + var uri = await Isolate.packageConfig; + if (uri == null) { + throw StateError('Unable to find a package config'); + } + return uri; +}(); + +final _originalWorkingDirectory = Directory.current.uri; + +/// Returns an `package:` URI for [path] if it is in a package, otherwise +/// returns an absolute file URI. +Future<Uri> absoluteUri(String path) async { + final uri = p.toUri(path); + final absoluteUri = + uri.isAbsolute ? uri : _originalWorkingDirectory.resolveUri(uri); + try { + final packageConfig = await currentPackageConfig; + return packageConfig.toPackageUri(absoluteUri) ?? absoluteUri; + } on StateError { + // Workaround for a missing package config. + return absoluteUri; + } +}
diff --git a/pkgs/test_core/lib/src/util/pretty_print.dart b/pkgs/test_core/lib/src/util/pretty_print.dart new file mode 100644 index 0000000..f425629 --- /dev/null +++ b/pkgs/test_core/lib/src/util/pretty_print.dart
@@ -0,0 +1,71 @@ +// Copyright (c) 2021, 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. + +/// A regular expression matching terminal color codes. +final _colorCode = RegExp('\u001b\\[[0-9;]+m'); + +/// Returns [str] without any color codes. +String withoutColors(String str) => str.replaceAll(_colorCode, ''); + +/// A regular expression matching a single vowel. +final _vowel = RegExp('[aeiou]'); + +/// Returns [noun] with an indefinite article ("a" or "an") added, based on +/// whether its first letter is a vowel. +String a(String noun) => noun.startsWith(_vowel) ? 'an $noun' : 'a $noun'; + +/// Indent each line in [string] by 2 spaces. +String indent(String text) { + var lines = text.split('\n'); + if (lines.length == 1) return ' $text'; + + var buffer = StringBuffer(); + + for (var line in lines.take(lines.length - 1)) { + buffer.writeln(' $line'); + } + buffer.write(' ${lines.last}'); + return buffer.toString(); +} + +/// Truncates [text] to fit within [maxLength]. +/// +/// This will try to truncate along word boundaries and preserve words both at +/// the beginning and the end of [text]. +String truncate(String text, int maxLength) { + // Return the full message if it fits. + if (text.length <= maxLength) return text; + + // If we can fit the first and last three words, do so. + var words = text.split(' '); + if (words.length > 1) { + var i = words.length; + var length = words.first.length + 4; + do { + i--; + length += 1 + words[i].length; + } while (length <= maxLength && i > 0); + if (length > maxLength || i == 0) i++; + if (i < words.length - 4) { + // Require at least 3 words at the end. + var buffer = StringBuffer(); + buffer.write(words.first); + buffer.write(' ...'); + for (; i < words.length; i++) { + buffer.write(' '); + buffer.write(words[i]); + } + return buffer.toString(); + } + } + + // Otherwise truncate to return the trailing text, but attempt to start at + // the beginning of a word. + var result = text.substring(text.length - maxLength + 4); + var firstSpace = result.indexOf(' '); + if (firstSpace > 0) { + result = result.substring(firstSpace); + } + return '...$result'; +}
diff --git a/pkgs/test_core/lib/src/util/print_sink.dart b/pkgs/test_core/lib/src/util/print_sink.dart new file mode 100644 index 0000000..ebb2fe8 --- /dev/null +++ b/pkgs/test_core/lib/src/util/print_sink.dart
@@ -0,0 +1,39 @@ +// Copyright (c) 2019, 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. + +class PrintSink implements StringSink { + final _buffer = StringBuffer(); + + @override + void write(Object? obj) { + _buffer.write(obj); + _flush(); + } + + @override + void writeAll(Iterable objects, [String separator = '']) { + _buffer.writeAll(objects, separator); + _flush(); + } + + @override + void writeCharCode(int charCode) { + _buffer.writeCharCode(charCode); + _flush(); + } + + @override + void writeln([Object? obj = '']) { + _buffer.writeln(obj ?? ''); + _flush(); + } + + /// [print] if the content available ends with a newline. + void _flush() { + if ('$_buffer'.endsWith('\n')) { + print(_buffer); + _buffer.clear(); + } + } +}
diff --git a/pkgs/test_core/lib/src/util/stack_trace_mapper.dart b/pkgs/test_core/lib/src/util/stack_trace_mapper.dart new file mode 100644 index 0000000..9d4248d --- /dev/null +++ b/pkgs/test_core/lib/src/util/stack_trace_mapper.dart
@@ -0,0 +1,81 @@ +// Copyright (c) 2015, 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:source_map_stack_trace/source_map_stack_trace.dart' as mapper; +import 'package:source_maps/source_maps.dart'; +import 'package:test_api/backend.dart' show StackTraceMapper; + +/// A class for mapping JS stack traces to Dart stack traces using source maps. +class JSStackTraceMapper implements StackTraceMapper { + /// The parsed source map. + /// + /// This is initialized lazily in `mapStackTrace()`. + Mapping? _mapping; + + /// The same package resolution information as was passed to dart2js. + final Map<String, Uri>? _packageMap; + + /// The URL of the SDK root from which dart2js loaded its sources. + final Uri? _sdkRoot; + + /// The contents of the source map. + final String _mapContents; + + /// The URL of the source map. + final Uri? _mapUrl; + + JSStackTraceMapper(this._mapContents, + {Uri? mapUrl, Map<String, Uri>? packageMap, Uri? sdkRoot}) + : _mapUrl = mapUrl, + _packageMap = packageMap, + _sdkRoot = sdkRoot; + + /// Converts [trace] into a Dart stack trace. + @override + StackTrace mapStackTrace(StackTrace trace) { + var mapping = _mapping ??= parseExtended(_mapContents, mapUrl: _mapUrl); + return mapper.mapStackTrace(mapping, trace, + packageMap: _packageMap, sdkRoot: _sdkRoot); + } + + /// Returns a Map representation which is suitable for JSON serialization. + @override + Map<String, dynamic> serialize() { + return { + 'mapContents': _mapContents, + 'sdkRoot': _sdkRoot?.toString(), + 'packageConfigMap': _serializePackageConfigMap(_packageMap), + 'mapUrl': _mapUrl?.toString(), + }; + } + + /// Returns a [StackTraceMapper] contained in the provided serialized + /// representation. + static StackTraceMapper? deserialize(Map? serialized) { + if (serialized == null) return null; + var deserialized = _deserializePackageConfigMap( + (serialized['packageConfigMap'] as Map).cast<String, String>()); + + return JSStackTraceMapper(serialized['mapContents'] as String, + sdkRoot: Uri.parse(serialized['sdkRoot'] as String), + packageMap: deserialized, + mapUrl: Uri.parse(serialized['mapUrl'] as String)); + } + + /// Converts a [packageConfigMap] into a format suitable for JSON + /// serialization. + static Map<String, String>? _serializePackageConfigMap( + Map<String, Uri>? packageConfigMap) { + if (packageConfigMap == null) return null; + return packageConfigMap.map((key, value) => MapEntry(key, '$value')); + } + + /// Converts a serialized package config map into a format suitable for + /// the [PackageResolver] + static Map<String, Uri>? _deserializePackageConfigMap( + Map<String, String>? serialized) { + if (serialized == null) return null; + return serialized.map((key, value) => MapEntry(key, Uri.parse(value))); + } +}
diff --git a/pkgs/test_core/lib/src/util/string_literal_iterator.dart b/pkgs/test_core/lib/src/util/string_literal_iterator.dart new file mode 100644 index 0000000..d58f64a --- /dev/null +++ b/pkgs/test_core/lib/src/util/string_literal_iterator.dart
@@ -0,0 +1,245 @@ +// Copyright (c) 2015, 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:collection'; + +import 'package:analyzer/dart/ast/ast.dart'; + +// ASCII character codes. + +const _zero = 0x30; +const _nine = 0x39; +const _backslash = 0x5C; +const _openCurly = 0x7B; +const _closeCurly = 0x7D; +const _capitalA = 0x41; +const _capitalZ = 0x5A; +const _a = 0x61; +const _n = 0x6E; +const _r = 0x72; +const _f = 0x66; +const _b = 0x62; +const _t = 0x74; +const _u = 0x75; +const _v = 0x76; +const _x = 0x78; +const _z = 0x7A; +const _newline = 0xA; +const _carriageReturn = 0xD; +const _formFeed = 0xC; +const _backspace = 0x8; +const _tab = 0x9; +const _verticalTab = 0xB; + +/// An iterator over the runes in the value of a [StringLiteral]. +/// +/// In addition to exposing the values of the runes themselves, this also +/// exposes the offset of the current rune in the Dart source file. +class StringLiteralIterator implements Iterator<int> { + @override + int get current => _current!; + int? _current; + + /// The offset of the beginning of [current] in the Dart source file that + /// contains the string literal. + /// + /// Before iteration begins, this points to the character before the first + /// rune. + int get offset => _offset; + late int _offset; + + /// The offset of the next rune. + /// + /// This isn't necessarily just `offset + 1`, since a single rune may be + /// represented by multiple characters in the source file, or a string literal + /// may be composed of several adjacent string literals. + int? _nextOffset; + + /// All [SimpleStringLiteral]s that compose the input literal. + /// + /// If the input literal is itself a [SimpleStringLiteral], this just contains + /// that literal; otherwise, the literal is an [AdjacentStrings], and this + /// contains its component literals. + final _strings = Queue<SimpleStringLiteral>(); + + /// Whether this is a raw string that begins with `r`. + /// + /// This is necessary for knowing how to parse escape sequences. + bool? _isRaw; + + /// The iterator over the runes in the Dart source file. + /// + /// When switching to a new string in [_strings], this is updated to point to + /// that string's component runes. + Iterator<int>? _runes; + + /// The result of the last call to `_runes.moveNext`. + bool _runesHasCurrent = false; + + /// Creates a new [StringLiteralIterator] iterating over the contents of + /// [literal]. + /// + /// Throws an [ArgumentError] if [literal] contains interpolated strings. + StringLiteralIterator(StringLiteral literal) { + if (literal is StringInterpolation) { + throw ArgumentError("Can't iterate over an interpolated string."); + } else if (literal is SimpleStringLiteral) { + _strings.add(literal); + } else { + assert(literal is AdjacentStrings); + + for (var string in (literal as AdjacentStrings).strings) { + if (string is StringInterpolation) { + throw ArgumentError("Can't iterate over an interpolated string."); + } + _strings.add(string as SimpleStringLiteral); + } + } + + _offset = _strings.first.contentsOffset - 1; + } + + @override + bool moveNext() { + // If we're at beginning of a [SimpleStringLiteral], move forward until + // there's actually text to consume. + while (_runes == null || !_runesHasCurrent) { + if (_strings.isEmpty) { + // Move the offset past the end of the text. + _offset = _nextOffset!; + _current = null; + return false; + } + + var string = _strings.removeFirst(); + var start = string.contentsOffset - string.offset; + + // Compensate for the opening and closing quotes. + var end = start + + string.literal.lexeme.length - + 2 * (string.isMultiline ? 3 : 1) - + (string.isRaw ? 1 : 0); + var text = string.literal.lexeme.substring(start, end); + + _nextOffset = string.contentsOffset; + _isRaw = string.isRaw; + _runes = text.runes.iterator; + _runesHasCurrent = _runes!.moveNext(); + } + + _offset = _nextOffset!; + _current = _nextRune(); + if (_current != null) return true; + + // If we encounter a parse failure, stop moving forward immediately. + _strings.clear(); + return false; + } + + /// Consume and return the next rune. + int? _nextRune() { + if (_isRaw! || _runes!.current != _backslash) { + var rune = _runes!.current; + _moveRunesNext(); + return (rune < 0) ? null : rune; + } + + if (!_moveRunesNext()) return null; + return _parseEscapeSequence(); + } + + /// Parse an escape sequence in the underlying Dart text. + /// + /// This assumes that a backslash has already been consumed. It leaves the + /// [_runes] cursor on the first character after the escape sequence. + int? _parseEscapeSequence() { + switch (_runes!.current) { + case _n: + _moveRunesNext(); + return _newline; + case _r: + _moveRunesNext(); + return _carriageReturn; + case _f: + _moveRunesNext(); + return _formFeed; + case _b: + _moveRunesNext(); + return _backspace; + case _t: + _moveRunesNext(); + return _tab; + case _v: + _moveRunesNext(); + return _verticalTab; + case _x: + if (!_moveRunesNext()) return null; + return _parseHex(2); + case _u: + if (!_moveRunesNext()) return null; + if (_runes!.current != _openCurly) return _parseHex(4); + if (!_moveRunesNext()) return null; + + var number = _parseHexSequence(); + if (_runes!.current != _closeCurly) return null; + if (!_moveRunesNext()) return null; + return number; + default: + var rune = _runes!.current; + _moveRunesNext(); + return rune; + } + } + + /// Parse a variable-length sequence of hexadecimal digits and returns their + /// value as an [int]. + /// + /// This parses digits as they appear in a unicode escape sequence: one to six + /// hex digits. + int? _parseHexSequence() { + var number = _parseHexDigit(_runes!.current); + if (number == null) return null; + if (!_moveRunesNext()) return null; + + for (var i = 0; i < 5; i++) { + var digit = _parseHexDigit(_runes!.current); + if (digit == null) break; + number = number! * 16 + digit; + if (!_moveRunesNext()) return null; + } + + return number; + } + + /// Parses [digits] hexadecimal digits and returns their value as an [int]. + int? _parseHex(int digits) { + var number = 0; + for (var i = 0; i < digits; i++) { + if (_runes!.current == -1) return null; + var digit = _parseHexDigit(_runes!.current); + if (digit == null) return null; + number = number * 16 + digit; + _moveRunesNext(); + } + return number; + } + + /// Parses a single hexadecimal digit. + int? _parseHexDigit(int rune) { + if (rune < _zero) return null; + if (rune <= _nine) return rune - _zero; + if (rune < _capitalA) return null; + if (rune <= _capitalZ) return 10 + rune - _capitalA; + if (rune < _a) return null; + if (rune <= _z) return 10 + rune - _a; + return null; + } + + /// Move [_runes] to the next rune and update [_nextOffset]. + bool _moveRunesNext() { + var result = _runesHasCurrent = _runes!.moveNext(); + _nextOffset = _nextOffset! + 1; + return result; + } +}
diff --git a/pkgs/test_core/lib/test_core.dart b/pkgs/test_core/lib/test_core.dart new file mode 100644 index 0000000..b1df09d --- /dev/null +++ b/pkgs/test_core/lib/test_core.dart
@@ -0,0 +1,11 @@ +// 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. + +@Deprecated('package:test_core is not intended for general use. ' + 'Please use package:test.') +library; + +export 'package:test_api/hooks.dart' show TestFailure; + +export 'scaffolding.dart';
diff --git a/pkgs/test_core/mono_pkg.yaml b/pkgs/test_core/mono_pkg.yaml new file mode 100644 index 0000000..9ef7927 --- /dev/null +++ b/pkgs/test_core/mono_pkg.yaml
@@ -0,0 +1,17 @@ +# See https://pub.dev/packages/mono_repo + +sdk: +- dev + +stages: +- analyze_and_format: + - group: + - format + - analyze: --fatal-infos + - group: + - analyze + sdk: pubspec +- unit_test: + - group: + - command: dart test + sdk: [dev, pubspec]
diff --git a/pkgs/test_core/pubspec.yaml b/pkgs/test_core/pubspec.yaml new file mode 100644 index 0000000..d08e24c --- /dev/null +++ b/pkgs/test_core/pubspec.yaml
@@ -0,0 +1,35 @@ +name: test_core +version: 0.6.8 +description: A basic library for writing tests and running them on the VM. +repository: https://github.com/dart-lang/test/tree/master/pkgs/test_core +resolution: workspace + +environment: + sdk: ^3.5.0 + +dependencies: + analyzer: '>=6.0.0 <8.0.0' + args: ^2.0.0 + async: ^2.5.0 + boolean_selector: ^2.1.0 + collection: ^1.15.0 + coverage: ^1.0.0 + frontend_server_client: '>=3.2.0 <5.0.0' + glob: ^2.0.0 + io: ^1.0.0 + meta: ^1.3.0 + package_config: ^2.0.0 + path: ^1.8.0 + pool: ^1.5.0 + source_map_stack_trace: ^2.1.0 + source_maps: ^0.10.10 + source_span: ^1.8.0 + stack_trace: ^1.10.0 + stream_channel: ^2.1.0 + # Use an exact version until the test_api package is stable. + test_api: 0.7.4 + vm_service: ">=6.0.0 <16.0.0" + yaml: ^3.0.0 + +dev_dependencies: + test: any
diff --git a/pkgs/test_core/test/runner/vm/test_compiler_test.dart b/pkgs/test_core/test/runner/vm/test_compiler_test.dart new file mode 100644 index 0000000..a6e50ef --- /dev/null +++ b/pkgs/test_core/test/runner/vm/test_compiler_test.dart
@@ -0,0 +1,25 @@ +// Copyright (c) 2024, 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:test/test.dart'; +import 'package:test_core/src/runner/vm/test_compiler.dart'; + +void main() { + group('VM test templates', () { + test('include package config URI variable', () async { + // This variable is read through the VM service and should not be removed. + final template = testBootstrapContents( + testUri: Uri.file('foo.dart'), + languageVersionComment: '// version comment', + packageConfigUri: Uri.file('package_config.json'), + testType: VmTestType.isolate, + ); + final lines = LineSplitter.split(template).map((line) => line.trim()); + expect(lines, + contains("const packageConfigLocation = 'package_config.json';")); + }); + }); +}
diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..2e08374 --- /dev/null +++ b/pubspec.yaml
@@ -0,0 +1,14 @@ +name: test_workspace +publish_to: none +environment: + sdk: ^3.5.0 +workspace: + - integration_tests/regression + - integration_tests/spawn_hybrid + - integration_tests/wasm + - pkgs/checks + - pkgs/test + - pkgs/test_api + - pkgs/test_core +dev_dependencies: + dart_flutter_team_lints: ^3.1.0
diff --git a/tool/ci.sh b/tool/ci.sh new file mode 100755 index 0000000..040c70d --- /dev/null +++ b/tool/ci.sh
@@ -0,0 +1,167 @@ +#!/bin/bash +# Created with package:mono_repo v6.6.2 + +# Support built in commands on windows out of the box. + +# When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") +# then "flutter pub" is called instead of "dart pub". +# This assumes that the Flutter SDK has been installed in a previous step. +function pub() { + if grep -Fq "sdk: flutter" "${PWD}/pubspec.yaml"; then + command flutter pub "$@" + else + command dart pub "$@" + fi +} + +function format() { + command dart format "$@" +} + +# When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") +# then "flutter analyze" is called instead of "dart analyze". +# This assumes that the Flutter SDK has been installed in a previous step. +function analyze() { + if grep -Fq "sdk: flutter" "${PWD}/pubspec.yaml"; then + command flutter analyze "$@" + else + command dart analyze "$@" + fi +} + +if [[ -z ${PKGS} ]]; then + echo -e '\033[31mPKGS environment variable must be set! - TERMINATING JOB\033[0m' + exit 64 +fi + +if [[ "$#" == "0" ]]; then + echo -e '\033[31mAt least one task argument must be provided! - TERMINATING JOB\033[0m' + exit 64 +fi + +SUCCESS_COUNT=0 +declare -a FAILURES + +for PKG in ${PKGS}; do + echo -e "\033[1mPKG: ${PKG}\033[22m" + EXIT_CODE=0 + pushd "${PKG}" >/dev/null || EXIT_CODE=$? + + if [[ ${EXIT_CODE} -ne 0 ]]; then + echo -e "\033[31mPKG: '${PKG}' does not exist - TERMINATING JOB\033[0m" + exit 64 + fi + + dart pub upgrade || EXIT_CODE=$? + + if [[ ${EXIT_CODE} -ne 0 ]]; then + echo -e "\033[31mPKG: ${PKG}; 'dart pub upgrade' - FAILED (${EXIT_CODE})\033[0m" + FAILURES+=("${PKG}; 'dart pub upgrade'") + else + for TASK in "$@"; do + EXIT_CODE=0 + echo + echo -e "\033[1mPKG: ${PKG}; TASK: ${TASK}\033[22m" + case ${TASK} in + analyze_0) + echo 'dart analyze --fatal-infos' + dart analyze --fatal-infos || EXIT_CODE=$? + ;; + analyze_1) + echo 'dart analyze' + dart analyze || EXIT_CODE=$? + ;; + command_00) + echo 'dart test' + dart test || EXIT_CODE=$? + ;; + command_01) + echo 'xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 0' + xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 0 || EXIT_CODE=$? + ;; + command_02) + echo 'xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 1' + xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 1 || EXIT_CODE=$? + ;; + command_03) + echo 'xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 2' + xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 2 || EXIT_CODE=$? + ;; + command_04) + echo 'xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 3' + xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 3 || EXIT_CODE=$? + ;; + command_05) + echo 'xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 4' + xvfb-run -s "-screen 0 1024x768x24" dart test --preset travis --total-shards 5 --shard-index 4 || EXIT_CODE=$? + ;; + command_06) + echo 'dart test --preset travis --total-shards 5 --shard-index 0' + dart test --preset travis --total-shards 5 --shard-index 0 || EXIT_CODE=$? + ;; + command_07) + echo 'dart test --preset travis --total-shards 5 --shard-index 1' + dart test --preset travis --total-shards 5 --shard-index 1 || EXIT_CODE=$? + ;; + command_08) + echo 'dart test --preset travis --total-shards 5 --shard-index 2' + dart test --preset travis --total-shards 5 --shard-index 2 || EXIT_CODE=$? + ;; + command_09) + echo 'dart test --preset travis --total-shards 5 --shard-index 3' + dart test --preset travis --total-shards 5 --shard-index 3 || EXIT_CODE=$? + ;; + command_10) + echo 'dart test --preset travis --total-shards 5 --shard-index 4' + dart test --preset travis --total-shards 5 --shard-index 4 || EXIT_CODE=$? + ;; + command_11) + echo 'dart test --preset travis -x browser' + dart test --preset travis -x browser || EXIT_CODE=$? + ;; + format) + echo 'dart format --output=none --set-exit-if-changed .' + dart format --output=none --set-exit-if-changed . || EXIT_CODE=$? + ;; + test_1) + echo 'dart test -p chrome,vm,node' + dart test -p chrome,vm,node || EXIT_CODE=$? + ;; + test_2) + echo 'dart test --timeout=60s' + dart test --timeout=60s || EXIT_CODE=$? + ;; + *) + echo -e "\033[31mUnknown TASK '${TASK}' - TERMINATING JOB\033[0m" + exit 64 + ;; + esac + + if [[ ${EXIT_CODE} -ne 0 ]]; then + echo -e "\033[31mPKG: ${PKG}; TASK: ${TASK} - FAILED (${EXIT_CODE})\033[0m" + FAILURES+=("${PKG}; TASK: ${TASK}") + else + echo -e "\033[32mPKG: ${PKG}; TASK: ${TASK} - SUCCEEDED\033[0m" + SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) + fi + + done + fi + + echo + echo -e "\033[32mSUCCESS COUNT: ${SUCCESS_COUNT}\033[0m" + + if [ ${#FAILURES[@]} -ne 0 ]; then + echo -e "\033[31mFAILURES: ${#FAILURES[@]}\033[0m" + for i in "${FAILURES[@]}"; do + echo -e "\033[31m $i\033[0m" + done + fi + + popd >/dev/null || exit 70 + echo +done + +if [ ${#FAILURES[@]} -ne 0 ]; then + exit 1 +fi