Merge branch 'main' into merge-yaml-package
diff --git a/.github/ISSUE_TEMPLATE/pubspec_parse.md b/.github/ISSUE_TEMPLATE/pubspec_parse.md
new file mode 100644
index 0000000..2d65881
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/pubspec_parse.md
@@ -0,0 +1,5 @@
+---
+name: "package:pubspec_parse"
+about: "Create a bug or file a feature request against package:pubspec_parse."
+labels: "package:pubspec_parse"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/stack_trace.md b/.github/ISSUE_TEMPLATE/stack_trace.md
new file mode 100644
index 0000000..417362b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/stack_trace.md
@@ -0,0 +1,5 @@
+---
+name: "package:stack_trace"
+about: "Create a bug or file a feature request against package:stack_trace."
+labels: "package:stack_trace"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/stream_channel.md b/.github/ISSUE_TEMPLATE/stream_channel.md
new file mode 100644
index 0000000..76b5994
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/stream_channel.md
@@ -0,0 +1,5 @@
+---
+name: "package:stream_channel"
+about: "Create a bug or file a feature request against package:stream_channel."
+labels: "package:stream_channel"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/stream_transform.md b/.github/ISSUE_TEMPLATE/stream_transform.md
new file mode 100644
index 0000000..475bd83
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/stream_transform.md
@@ -0,0 +1,5 @@
+---
+name: "package:stream_transform"
+about: "Create a bug or file a feature request against package:stream_transform."
+labels: "package:stream_transform"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/string_scanner.md b/.github/ISSUE_TEMPLATE/string_scanner.md
new file mode 100644
index 0000000..ad89f1b
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/string_scanner.md
@@ -0,0 +1,5 @@
+---
+name: "package:string_scanner"
+about: "Create a bug or file a feature request against package:string_scanner."
+labels: "package:string_scanner"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/term_glyph.md b/.github/ISSUE_TEMPLATE/term_glyph.md
new file mode 100644
index 0000000..b6a4766
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/term_glyph.md
@@ -0,0 +1,5 @@
+---
+name: "package:term_glyph"
+about: "Create a bug or file a feature request against package:term_glyph."
+labels: "package:term_glyph"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/test_reflective_loader.md b/.github/ISSUE_TEMPLATE/test_reflective_loader.md
new file mode 100644
index 0000000..bde03fe
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/test_reflective_loader.md
@@ -0,0 +1,5 @@
+---
+name: "package:test_reflective_loader"
+about: "Create a bug or file a feature request against package:test_reflective_loader."
+labels: "package:test_reflective_loader"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/timing.md b/.github/ISSUE_TEMPLATE/timing.md
new file mode 100644
index 0000000..38a0015
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/timing.md
@@ -0,0 +1,5 @@
+---
+name: "package:timing"
+about: "Create a bug or file a feature request against package:timing."
+labels: "package:timing"
+---
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/watcher.md b/.github/ISSUE_TEMPLATE/watcher.md
new file mode 100644
index 0000000..2578819
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/watcher.md
@@ -0,0 +1,5 @@
+---
+name: "package:watcher"
+about: "Create a bug or file a feature request against package:watcher."
+labels: "package:watcher"
+---
\ No newline at end of file
diff --git a/.github/labeler.yml b/.github/labeler.yml
index 24a7095..8492622 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -92,6 +92,10 @@
- changed-files:
- any-glob-to-any-file: 'pkgs/pub_semver/**'
+'package:pubspec_parse':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/pubspec_parse/**'
+
'package:source_map_stack_trace':
- changed-files:
- any-glob-to-any-file: 'pkgs/source_map_stack_trace/**'
@@ -108,10 +112,38 @@
- changed-files:
- any-glob-to-any-file: 'pkgs/sse/**'
+'package:stack_trace':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/stack_trace/**'
+
+'package:stream_channel':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/stream_channel/**'
+
+'package:stream_transform':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/stream_transform/**'
+
+'package:term_glyph':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/term_glyph/**'
+
+'package:test_reflective_loader':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/test_reflective_loader/**'
+
+'package:timing':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/timing/**'
+
'package:unified_analytics':
- changed-files:
- any-glob-to-any-file: 'pkgs/unified_analytics/**'
+'package:watcher':
+ - changed-files:
+ - any-glob-to-any-file: 'pkgs/watcher/**'
+
'package:yaml':
- changed-files:
- - any-glob-to-any-file: 'pkgs/yaml/**'
+ - any-glob-to-any-file: 'pkgs/yaml/**'
\ No newline at end of file
diff --git a/.github/workflows/bazel_worker.yaml b/.github/workflows/bazel_worker.yaml
index 0eb06da..b448219 100644
--- a/.github/workflows/bazel_worker.yaml
+++ b/.github/workflows/bazel_worker.yaml
@@ -36,6 +36,8 @@
- uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
with:
sdk: ${{ matrix.sdk }}
+ - run: dart pub get
- run: "dart format --output=none --set-exit-if-changed ."
+ if: ${{ matrix.sdk == 'dev' }}
- name: Test
run: ./tool/travis.sh
diff --git a/.github/workflows/clock.yaml b/.github/workflows/clock.yaml
index aef0895..a09a601 100644
--- a/.github/workflows/clock.yaml
+++ b/.github/workflows/clock.yaml
@@ -5,12 +5,12 @@
push:
branches: [ main ]
paths:
- - '.github/workflows/clock.yml'
+ - '.github/workflows/clock.yaml'
- 'pkgs/clock/**'
pull_request:
branches: [ main ]
paths:
- - '.github/workflows/clock.yml'
+ - '.github/workflows/clock.yaml'
- 'pkgs/clock/**'
schedule:
- cron: "0 0 * * 0"
diff --git a/.github/workflows/pubspec_parse.yaml b/.github/workflows/pubspec_parse.yaml
new file mode 100644
index 0000000..ebe7059
--- /dev/null
+++ b/.github/workflows/pubspec_parse.yaml
@@ -0,0 +1,71 @@
+name: package:pubspec_parse
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/pubspec_parse.yaml'
+ - 'pkgs/pubspec_parse/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/pubspec_parse.yaml'
+ - 'pkgs/pubspec_parse/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+ run:
+ working-directory: pkgs/pubspec_parse/
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ sdk: [3.2, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm --run-skipped
+ if: always() && steps.install.outcome == 'success'
diff --git a/.github/workflows/stack_trace.yaml b/.github/workflows/stack_trace.yaml
new file mode 100644
index 0000000..7435967
--- /dev/null
+++ b/.github/workflows/stack_trace.yaml
@@ -0,0 +1,75 @@
+name: package:stack_trace
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/stack_trace.yaml'
+ - 'pkgs/stack_trace/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/stack_trace.yaml'
+ - 'pkgs/stack_trace/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+ run:
+ working-directory: pkgs/stack_trace/
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [3.4, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
+ - name: Run browser tests
+ run: dart test --platform chrome
+ if: always() && steps.install.outcome == 'success'
diff --git a/.github/workflows/stream_channel.yaml b/.github/workflows/stream_channel.yaml
new file mode 100644
index 0000000..c39424d
--- /dev/null
+++ b/.github/workflows/stream_channel.yaml
@@ -0,0 +1,74 @@
+name: package:stream_channel
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/stream_channel.yaml'
+ - 'pkgs/stream_channel/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/stream_channel.yaml'
+ - 'pkgs/stream_channel/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+defaults:
+ run:
+ working-directory: pkgs/stream_channel/
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [3.3, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
+ - name: Run Chrome tests
+ run: dart test --platform chrome
+ if: always() && steps.install.outcome == 'success'
diff --git a/.github/workflows/stream_transform.yaml b/.github/workflows/stream_transform.yaml
new file mode 100644
index 0000000..a36a776
--- /dev/null
+++ b/.github/workflows/stream_transform.yaml
@@ -0,0 +1,73 @@
+name: package:stream_transform
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/stream_transform.yaml'
+ - 'pkgs/stream_transform/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/stream_transform.yaml'
+ - 'pkgs/stream_transform/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+ run:
+ working-directory: pkgs/stream_transform/
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ # Bump SDK for Legacy tests when changing min SDK.
+ sdk: [3.1, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run tests
+ run: dart test -p chrome,vm --test-randomize-ordering-seed=random
+ if: always() && steps.install.outcome == 'success'
diff --git a/.github/workflows/term_glyph.yaml b/.github/workflows/term_glyph.yaml
new file mode 100644
index 0000000..5b3b320
--- /dev/null
+++ b/.github/workflows/term_glyph.yaml
@@ -0,0 +1,72 @@
+name: package:term_glyph
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/term_glyph.yaml'
+ - 'pkgs/term_glyph/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/term_glyph.yaml'
+ - 'pkgs/term_glyph/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+ run:
+ working-directory: pkgs/term_glyph/
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [3.1, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
diff --git a/.github/workflows/test_reflective_loader.yaml b/.github/workflows/test_reflective_loader.yaml
new file mode 100644
index 0000000..975c970
--- /dev/null
+++ b/.github/workflows/test_reflective_loader.yaml
@@ -0,0 +1,43 @@
+name: package:test_reflective_loader
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/test_reflective_loader.yaml'
+ - 'pkgs/test_reflective_loader/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/test_reflective_loader.yaml'
+ - 'pkgs/test_reflective_loader/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+defaults:
+ run:
+ working-directory: pkgs/test_reflective_loader/
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev, 3.1]
+
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+
+ - run: dart pub get
+ - name: dart format
+ run: dart format --output=none --set-exit-if-changed .
+ - run: dart analyze --fatal-infos
+ - run: dart test
diff --git a/.github/workflows/timing.yaml b/.github/workflows/timing.yaml
new file mode 100644
index 0000000..df77b13
--- /dev/null
+++ b/.github/workflows/timing.yaml
@@ -0,0 +1,67 @@
+name: package:timing
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/timing.yaml'
+ - 'pkgs/timing/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/timing.yaml'
+ - 'pkgs/timing/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+ run:
+ working-directory: pkgs/timing/
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [3.4, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ run: dart pub get
+ - run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev, 2.2.0
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [3.4, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ run: dart pub get
+ - run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
diff --git a/.github/workflows/watcher.yaml b/.github/workflows/watcher.yaml
new file mode 100644
index 0000000..a04483c
--- /dev/null
+++ b/.github/workflows/watcher.yaml
@@ -0,0 +1,71 @@
+name: package:watcher
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/watcher.yaml'
+ - 'pkgs/watcher/**'
+ pull_request:
+ branches: [ main ]
+ paths:
+ - '.github/workflows/watcher.yaml'
+ - 'pkgs/watcher/**'
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+
+defaults:
+ run:
+ working-directory: pkgs/watcher/
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, macos-latest, windows-latest
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macos-latest, windows-latest]
+ sdk: [3.1, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
diff --git a/README.md b/README.md
index f463351..a9608b7 100644
--- a/README.md
+++ b/README.md
@@ -36,11 +36,19 @@
| [package_config](pkgs/package_config/) | Support for reading and writing Dart Package Configuration files. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apackage_config) | [](https://pub.dev/packages/package_config) |
| [pool](pkgs/pool/) | Manage a finite pool of resources. Useful for controlling concurrent file system or network requests. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apool) | [](https://pub.dev/packages/pool) |
| [pub_semver](pkgs/pub_semver/) | Versions and version constraints implementing pub's versioning policy. This is very similar to vanilla semver, with a few corner cases. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apub_semver) | [](https://pub.dev/packages/pub_semver) |
+| [pubspec_parse](pkgs/pubspec_parse/) | Simple package for parsing pubspec.yaml files with a type-safe API and rich error reporting. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apubspec_parse) | [](https://pub.dev/packages/pubspec_parse) |
| [source_map_stack_trace](pkgs/source_map_stack_trace/) | A package for applying source maps to stack traces. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_map_stack_trace) | [](https://pub.dev/packages/source_map_stack_trace) |
| [source_maps](pkgs/source_maps/) | A library to programmatically manipulate source map files. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_maps) | [](https://pub.dev/packages/source_maps) |
| [source_span](pkgs/source_span/) | Provides a standard representation for source code locations and spans. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_span) | [](https://pub.dev/packages/source_span) |
| [sse](pkgs/sse/) | Provides client and server functionality for setting up bi-directional communication through Server Sent Events (SSE) and corresponding POST requests. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asse) | [](https://pub.dev/packages/sse) |
+| [stack_trace](pkgs/stack_trace/) | A package for manipulating stack traces and printing them readably. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Astack_trace) | [](https://pub.dev/packages/stack_trace) |
+| [stream_channel](pkgs/stream_channel/) | An abstraction for two-way communication channels based on the Dart Stream class. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Astream_channel) | [](https://pub.dev/packages/stream_channel) |
+| [stream_transform](pkgs/stream_transform/) | A collection of utilities to transform and manipulate streams. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Astream_transform) | [](https://pub.dev/packages/stream_transform) |
+| [term_glyph](pkgs/term_glyph/) | Useful Unicode glyphs and ASCII substitutes. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aterm_glyph) | [](https://pub.dev/packages/term_glyph) |
+| [test_reflective_loader](pkgs/test_reflective_loader/) | Support for discovering tests and test suites using reflection. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Atest_reflective_loader) | [](https://pub.dev/packages/test_reflective_loader) |
+| [timing](pkgs/timing/) | A simple package for tracking the performance of synchronous and asynchronous actions. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Atiming) | [](https://pub.dev/packages/timing) |
| [unified_analytics](pkgs/unified_analytics/) | A package for logging analytics for all Dart and Flutter related tooling to Google Analytics. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aunified_analytics) | [](https://pub.dev/packages/unified_analytics) |
+| [watcher](pkgs/watcher/) | Monitor directories and send notifications when the contents change. | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Awatcher) | [](https://pub.dev/packages/watcher) |
| [yaml](pkgs/yaml/) | A parser for YAML, a human-friendly data serialization standard | [](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Ayaml) | [](https://pub.dev/packages/yaml) |
## Publishing automation
diff --git a/pkgs/bazel_worker/benchmark/benchmark.dart b/pkgs/bazel_worker/benchmark/benchmark.dart
index 035e2b8..0a03122 100644
--- a/pkgs/bazel_worker/benchmark/benchmark.dart
+++ b/pkgs/bazel_worker/benchmark/benchmark.dart
@@ -12,10 +12,7 @@
var path = 'blaze-bin/some/path/to/a/file/that/is/an/input/$i';
workRequest
..arguments.add('--input=$path')
- ..inputs.add(Input(
- path: '',
- digest: List.filled(70, 0x11),
- ));
+ ..inputs.add(Input(path: '', digest: List.filled(70, 0x11)));
}
// Serialize it.
@@ -24,14 +21,20 @@
print('Request has $length requestBytes.');
// Add the length in front base 128 encoded as in the worker protocol.
- requestBytes =
- Uint8List.fromList(requestBytes.toList()..insertAll(0, _varInt(length)));
+ requestBytes = Uint8List.fromList(
+ requestBytes.toList()..insertAll(0, _varInt(length)),
+ );
// Split into 10000 byte chunks.
var lists = <Uint8List>[];
for (var i = 0; i < requestBytes.length; i += 10000) {
- lists.add(Uint8List.sublistView(
- requestBytes, i, min(i + 10000, requestBytes.length)));
+ lists.add(
+ Uint8List.sublistView(
+ requestBytes,
+ i,
+ min(i + 10000, requestBytes.length),
+ ),
+ );
}
// Time `AsyncMessageGrouper` and deserialization.
diff --git a/pkgs/bazel_worker/e2e_test/bin/async_worker_in_isolate.dart b/pkgs/bazel_worker/e2e_test/bin/async_worker_in_isolate.dart
index a94875d..285b03d 100644
--- a/pkgs/bazel_worker/e2e_test/bin/async_worker_in_isolate.dart
+++ b/pkgs/bazel_worker/e2e_test/bin/async_worker_in_isolate.dart
@@ -17,7 +17,10 @@
Future main(List<String> args, [SendPort? message]) async {
var receivePort = ReceivePort();
await Isolate.spawnUri(
- Uri.file('async_worker.dart'), [], receivePort.sendPort);
+ Uri.file('async_worker.dart'),
+ [],
+ receivePort.sendPort,
+ );
var worker = await ForwardsToIsolateAsyncWorker.create(receivePort);
await worker.run();
diff --git a/pkgs/bazel_worker/e2e_test/lib/async_worker.dart b/pkgs/bazel_worker/e2e_test/lib/async_worker.dart
index d48d87c..55f5171 100644
--- a/pkgs/bazel_worker/e2e_test/lib/async_worker.dart
+++ b/pkgs/bazel_worker/e2e_test/lib/async_worker.dart
@@ -16,9 +16,6 @@
@override
Future<WorkResponse> performRequest(WorkRequest request) async {
- return WorkResponse(
- exitCode: 0,
- output: request.arguments.join('\n'),
- );
+ return WorkResponse(exitCode: 0, output: request.arguments.join('\n'));
}
}
diff --git a/pkgs/bazel_worker/e2e_test/lib/forwards_to_isolate_async_worker.dart b/pkgs/bazel_worker/e2e_test/lib/forwards_to_isolate_async_worker.dart
index bb937b2..a4845cf 100644
--- a/pkgs/bazel_worker/e2e_test/lib/forwards_to_isolate_async_worker.dart
+++ b/pkgs/bazel_worker/e2e_test/lib/forwards_to_isolate_async_worker.dart
@@ -13,9 +13,11 @@
final IsolateDriverConnection _isolateDriverConnection;
static Future<ForwardsToIsolateAsyncWorker> create(
- ReceivePort receivePort) async {
+ ReceivePort receivePort,
+ ) async {
return ForwardsToIsolateAsyncWorker(
- await IsolateDriverConnection.create(receivePort));
+ await IsolateDriverConnection.create(receivePort),
+ );
}
ForwardsToIsolateAsyncWorker(this._isolateDriverConnection);
diff --git a/pkgs/bazel_worker/e2e_test/pubspec.yaml b/pkgs/bazel_worker/e2e_test/pubspec.yaml
index 56f00cd..7eaa89a 100644
--- a/pkgs/bazel_worker/e2e_test/pubspec.yaml
+++ b/pkgs/bazel_worker/e2e_test/pubspec.yaml
@@ -10,6 +10,6 @@
dev_dependencies:
cli_util: ^0.4.2
- dart_flutter_team_lints: ^1.0.0
+ dart_flutter_team_lints: ^3.0.0
path: ^1.8.0
test: ^1.16.0
diff --git a/pkgs/bazel_worker/e2e_test/test/e2e_test.dart b/pkgs/bazel_worker/e2e_test/test/e2e_test.dart
index caa813a..6b79b5e 100644
--- a/pkgs/bazel_worker/e2e_test/test/e2e_test.dart
+++ b/pkgs/bazel_worker/e2e_test/test/e2e_test.dart
@@ -12,14 +12,18 @@
void main() {
var dart = p.join(sdkPath, 'bin', 'dart');
- runE2eTestForWorker('sync worker',
- () => Process.start(dart, [p.join('bin', 'sync_worker.dart')]));
- runE2eTestForWorker('async worker',
- () => Process.start(dart, [p.join('bin', 'async_worker.dart')]));
runE2eTestForWorker(
- 'async worker in isolate',
- () =>
- Process.start(dart, [p.join('bin', 'async_worker_in_isolate.dart')]));
+ 'sync worker',
+ () => Process.start(dart, [p.join('bin', 'sync_worker.dart')]),
+ );
+ runE2eTestForWorker(
+ 'async worker',
+ () => Process.start(dart, [p.join('bin', 'async_worker.dart')]),
+ );
+ runE2eTestForWorker(
+ 'async worker in isolate',
+ () => Process.start(dart, [p.join('bin', 'async_worker_in_isolate.dart')]),
+ );
}
void runE2eTestForWorker(String groupName, SpawnWorker spawnWorker) {
diff --git a/pkgs/bazel_worker/example/client.dart b/pkgs/bazel_worker/example/client.dart
index 7147fcb..326bb18 100644
--- a/pkgs/bazel_worker/example/client.dart
+++ b/pkgs/bazel_worker/example/client.dart
@@ -5,10 +5,14 @@
void main() async {
var scratchSpace = await Directory.systemTemp.createTemp();
var driver = BazelWorkerDriver(
- () => Process.start(Platform.resolvedExecutable,
- [Platform.script.resolve('worker.dart').toFilePath()],
- workingDirectory: scratchSpace.path),
- maxWorkers: 4);
+ () => Process.start(
+ Platform.resolvedExecutable,
+ [
+ Platform.script.resolve('worker.dart').toFilePath(),
+ ],
+ workingDirectory: scratchSpace.path),
+ maxWorkers: 4,
+ );
var response = await driver.doWork(WorkRequest(arguments: ['foo']));
if (response.exitCode != EXIT_CODE_OK) {
print('Worker request failed');
diff --git a/pkgs/bazel_worker/lib/src/async_message_grouper.dart b/pkgs/bazel_worker/lib/src/async_message_grouper.dart
index e1f0dea..8fc4778 100644
--- a/pkgs/bazel_worker/lib/src/async_message_grouper.dart
+++ b/pkgs/bazel_worker/lib/src/async_message_grouper.dart
@@ -86,13 +86,18 @@
// Copy as much as possible from the input buffer. Limit is the
// smaller of the remaining length to fill in the message and the
// remaining length in the buffer.
- var lengthToCopy = min(_message.length - _messagePos,
- _inputBuffer.length - _inputBufferPos);
+ var lengthToCopy = min(
+ _message.length - _messagePos,
+ _inputBuffer.length - _inputBufferPos,
+ );
_message.setRange(
- _messagePos,
- _messagePos + lengthToCopy,
- _inputBuffer.sublist(
- _inputBufferPos, _inputBufferPos + lengthToCopy));
+ _messagePos,
+ _messagePos + lengthToCopy,
+ _inputBuffer.sublist(
+ _inputBufferPos,
+ _inputBufferPos + lengthToCopy,
+ ),
+ );
_messagePos += lengthToCopy;
_inputBufferPos += lengthToCopy;
diff --git a/pkgs/bazel_worker/lib/src/driver/driver.dart b/pkgs/bazel_worker/lib/src/driver/driver.dart
index 4a78020..06cf0fe 100644
--- a/pkgs/bazel_worker/lib/src/driver/driver.dart
+++ b/pkgs/bazel_worker/lib/src/driver/driver.dart
@@ -44,9 +44,12 @@
/// Factory method that spawns a worker process.
final SpawnWorker _spawnWorker;
- BazelWorkerDriver(this._spawnWorker,
- {int? maxIdleWorkers, int? maxWorkers, int? maxRetries})
- : _maxIdleWorkers = maxIdleWorkers ?? 4,
+ BazelWorkerDriver(
+ this._spawnWorker, {
+ int? maxIdleWorkers,
+ int? maxWorkers,
+ int? maxRetries,
+ }) : _maxIdleWorkers = maxIdleWorkers ?? 4,
_maxWorkers = maxWorkers ?? 4,
_maxRetries = maxRetries ?? 4;
@@ -56,8 +59,10 @@
/// [request] has been actually sent to the worker. This allows the caller
/// to determine when actual work is being done versus just waiting for an
/// available worker.
- Future<WorkResponse> doWork(WorkRequest request,
- {void Function(Future<WorkResponse?>)? trackWork}) {
+ Future<WorkResponse> doWork(
+ WorkRequest request, {
+ void Function(Future<WorkResponse?>)? trackWork,
+ }) {
var attempt = _WorkAttempt(request, trackWork: trackWork);
_workQueue.add(attempt);
_runWorkQueue();
@@ -69,9 +74,11 @@
for (var worker in _readyWorkers.toList()) {
_killWorker(worker);
}
- await Future.wait(_spawningWorkers.map((worker) async {
- _killWorker(await worker);
- }));
+ await Future.wait(
+ _spawningWorkers.map((worker) async {
+ _killWorker(await worker);
+ }),
+ );
}
/// Runs as many items in [_workQueue] as possible given the number of
@@ -88,8 +95,10 @@
if (_workQueue.isEmpty) return;
if (_numWorkers == _maxWorkers && _idleWorkers.isEmpty) return;
if (_numWorkers > _maxWorkers) {
- throw StateError('Internal error, created to many workers. Please '
- 'file a bug at https://github.com/dart-lang/bazel_worker/issues/new');
+ throw StateError(
+ 'Internal error, created to many workers. Please '
+ 'file a bug at https://github.com/dart-lang/bazel_worker/issues/new',
+ );
}
// At this point we definitely want to run a task, we just need to decide
@@ -137,48 +146,51 @@
void _runWorker(Process worker, _WorkAttempt attempt) {
var rescheduled = false;
- runZonedGuarded(() async {
- var connection = _workerConnections[worker]!;
+ runZonedGuarded(
+ () async {
+ var connection = _workerConnections[worker]!;
- connection.writeRequest(attempt.request);
- var responseFuture = connection.readResponse();
- if (attempt.trackWork != null) {
- attempt.trackWork!(responseFuture);
- }
- var response = await responseFuture;
+ connection.writeRequest(attempt.request);
+ var responseFuture = connection.readResponse();
+ if (attempt.trackWork != null) {
+ attempt.trackWork!(responseFuture);
+ }
+ var response = await responseFuture;
- // It is possible for us to complete with an error response due to an
- // unhandled async error before we get here.
- if (!attempt.responseCompleter.isCompleted) {
- if (response.exitCode == EXIT_CODE_BROKEN_PIPE) {
+ // It is possible for us to complete with an error response due to an
+ // unhandled async error before we get here.
+ if (!attempt.responseCompleter.isCompleted) {
+ if (response.exitCode == EXIT_CODE_BROKEN_PIPE) {
+ rescheduled = _tryReschedule(attempt);
+ if (rescheduled) return;
+ stderr.writeln('Failed to run request ${attempt.request}');
+ response = WorkResponse(
+ exitCode: EXIT_CODE_ERROR,
+ output:
+ 'Invalid response from worker, this probably means it wrote '
+ 'invalid output or died.',
+ );
+ }
+ attempt.responseCompleter.complete(response);
+ _cleanUp(worker);
+ }
+ },
+ (e, s) {
+ // Note that we don't need to do additional cleanup here on failures. If
+ // the worker dies that is already handled in a generic fashion, we just
+ // need to make sure we complete with a valid response.
+ if (!attempt.responseCompleter.isCompleted) {
rescheduled = _tryReschedule(attempt);
if (rescheduled) return;
- stderr.writeln('Failed to run request ${attempt.request}');
- response = WorkResponse(
+ var response = WorkResponse(
exitCode: EXIT_CODE_ERROR,
- output:
- 'Invalid response from worker, this probably means it wrote '
- 'invalid output or died.',
+ output: 'Error running worker:\n$e\n$s',
);
+ attempt.responseCompleter.complete(response);
+ _cleanUp(worker);
}
- attempt.responseCompleter.complete(response);
- _cleanUp(worker);
- }
- }, (e, s) {
- // Note that we don't need to do additional cleanup here on failures. If
- // the worker dies that is already handled in a generic fashion, we just
- // need to make sure we complete with a valid response.
- if (!attempt.responseCompleter.isCompleted) {
- rescheduled = _tryReschedule(attempt);
- if (rescheduled) return;
- var response = WorkResponse(
- exitCode: EXIT_CODE_ERROR,
- output: 'Error running worker:\n$e\n$s',
- );
- attempt.responseCompleter.complete(response);
- _cleanUp(worker);
- }
- });
+ },
+ );
}
/// Performs post-work cleanup for [worker].
diff --git a/pkgs/bazel_worker/lib/src/driver/driver_connection.dart b/pkgs/bazel_worker/lib/src/driver/driver_connection.dart
index b419deb..80d5c98 100644
--- a/pkgs/bazel_worker/lib/src/driver/driver_connection.dart
+++ b/pkgs/bazel_worker/lib/src/driver/driver_connection.dart
@@ -34,13 +34,16 @@
Future<void> get done => _messageGrouper.done;
- StdDriverConnection(
- {Stream<List<int>>? inputStream, StreamSink<List<int>>? outputStream})
- : _messageGrouper = AsyncMessageGrouper(inputStream ?? stdin),
+ StdDriverConnection({
+ Stream<List<int>>? inputStream,
+ StreamSink<List<int>>? outputStream,
+ }) : _messageGrouper = AsyncMessageGrouper(inputStream ?? stdin),
_outputStream = outputStream ?? stdout;
factory StdDriverConnection.forWorker(Process worker) => StdDriverConnection(
- inputStream: worker.stdout, outputStream: worker.stdin);
+ inputStream: worker.stdout,
+ outputStream: worker.stdin,
+ );
/// Note: This will attempts to recover from invalid proto messages by parsing
/// them as strings. This is a common error case for workers (they print a
diff --git a/pkgs/bazel_worker/lib/src/utils.dart b/pkgs/bazel_worker/lib/src/utils.dart
index 609b435..f67bbac 100644
--- a/pkgs/bazel_worker/lib/src/utils.dart
+++ b/pkgs/bazel_worker/lib/src/utils.dart
@@ -13,8 +13,9 @@
var delimiterBuffer = CodedBufferWriter();
delimiterBuffer.writeInt32NoTag(messageBuffer.lengthInBytes);
- var result =
- Uint8List(messageBuffer.lengthInBytes + delimiterBuffer.lengthInBytes);
+ var result = Uint8List(
+ messageBuffer.lengthInBytes + delimiterBuffer.lengthInBytes,
+ );
delimiterBuffer.writeTo(result);
messageBuffer.writeTo(result, delimiterBuffer.lengthInBytes);
diff --git a/pkgs/bazel_worker/lib/src/worker/async_worker_loop.dart b/pkgs/bazel_worker/lib/src/worker/async_worker_loop.dart
index 5182b55..a95d09a 100644
--- a/pkgs/bazel_worker/lib/src/worker/async_worker_loop.dart
+++ b/pkgs/bazel_worker/lib/src/worker/async_worker_loop.dart
@@ -32,20 +32,20 @@
var request = await connection.readRequest();
if (request == null) break;
var printMessages = StringBuffer();
- response = await runZoned(() => performRequest(request),
- zoneSpecification:
- ZoneSpecification(print: (self, parent, zone, message) {
- printMessages.writeln();
- printMessages.write(message);
- }));
+ response = await runZoned(
+ () => performRequest(request),
+ zoneSpecification: ZoneSpecification(
+ print: (self, parent, zone, message) {
+ printMessages.writeln();
+ printMessages.write(message);
+ },
+ ),
+ );
if (printMessages.isNotEmpty) {
response.output = '${response.output}$printMessages';
}
} catch (e, s) {
- response = WorkResponse(
- exitCode: EXIT_CODE_ERROR,
- output: '$e\n$s',
- );
+ response = WorkResponse(exitCode: EXIT_CODE_ERROR, output: '$e\n$s');
}
connection.writeResponse(response);
diff --git a/pkgs/bazel_worker/lib/src/worker/sync_worker_loop.dart b/pkgs/bazel_worker/lib/src/worker/sync_worker_loop.dart
index a857105..51da684 100644
--- a/pkgs/bazel_worker/lib/src/worker/sync_worker_loop.dart
+++ b/pkgs/bazel_worker/lib/src/worker/sync_worker_loop.dart
@@ -30,19 +30,20 @@
var request = connection.readRequest();
if (request == null) break;
var printMessages = StringBuffer();
- response = runZoned(() => performRequest(request), zoneSpecification:
- ZoneSpecification(print: (self, parent, zone, message) {
- printMessages.writeln();
- printMessages.write(message);
- }));
+ response = runZoned(
+ () => performRequest(request),
+ zoneSpecification: ZoneSpecification(
+ print: (self, parent, zone, message) {
+ printMessages.writeln();
+ printMessages.write(message);
+ },
+ ),
+ );
if (printMessages.isNotEmpty) {
response.output = '${response.output}$printMessages';
}
} catch (e, s) {
- response = WorkResponse(
- exitCode: EXIT_CODE_ERROR,
- output: '$e\n$s',
- );
+ response = WorkResponse(exitCode: EXIT_CODE_ERROR, output: '$e\n$s');
}
connection.writeResponse(response);
diff --git a/pkgs/bazel_worker/lib/src/worker/worker_connection.dart b/pkgs/bazel_worker/lib/src/worker/worker_connection.dart
index b395316..fd5508e 100644
--- a/pkgs/bazel_worker/lib/src/worker/worker_connection.dart
+++ b/pkgs/bazel_worker/lib/src/worker/worker_connection.dart
@@ -29,13 +29,16 @@
/// Creates a [StdAsyncWorkerConnection] with the specified [inputStream]
/// and [outputStream], unless [sendPort] is specified, in which case
/// creates a [SendPortAsyncWorkerConnection].
- factory AsyncWorkerConnection(
- {Stream<List<int>>? inputStream,
- StreamSink<List<int>>? outputStream,
- SendPort? sendPort}) =>
+ factory AsyncWorkerConnection({
+ Stream<List<int>>? inputStream,
+ StreamSink<List<int>>? outputStream,
+ SendPort? sendPort,
+ }) =>
sendPort == null
? StdAsyncWorkerConnection(
- inputStream: inputStream, outputStream: outputStream)
+ inputStream: inputStream,
+ outputStream: outputStream,
+ )
: SendPortAsyncWorkerConnection(sendPort);
@override
@@ -53,9 +56,10 @@
final AsyncMessageGrouper _messageGrouper;
final StreamSink<List<int>> _outputStream;
- StdAsyncWorkerConnection(
- {Stream<List<int>>? inputStream, StreamSink<List<int>>? outputStream})
- : _messageGrouper = AsyncMessageGrouper(inputStream ?? stdin),
+ StdAsyncWorkerConnection({
+ Stream<List<int>>? inputStream,
+ StreamSink<List<int>>? outputStream,
+ }) : _messageGrouper = AsyncMessageGrouper(inputStream ?? stdin),
_outputStream = outputStream ?? stdout;
@override
diff --git a/pkgs/bazel_worker/lib/testing.dart b/pkgs/bazel_worker/lib/testing.dart
index 3ae4c1f..7aefabb 100644
--- a/pkgs/bazel_worker/lib/testing.dart
+++ b/pkgs/bazel_worker/lib/testing.dart
@@ -72,10 +72,18 @@
}
@override
- StreamSubscription<Uint8List> listen(void Function(Uint8List bytes)? onData,
- {Function? onError, void Function()? onDone, bool? cancelOnError}) {
- return _controller.stream.listen(onData,
- onError: onError, onDone: onDone, cancelOnError: cancelOnError);
+ StreamSubscription<Uint8List> listen(
+ void Function(Uint8List bytes)? onData, {
+ Function? onError,
+ void Function()? onDone,
+ bool? cancelOnError,
+ }) {
+ return _controller.stream.listen(
+ onData,
+ onError: onError,
+ onDone: onDone,
+ cancelOnError: cancelOnError,
+ );
}
@override
@@ -165,8 +173,9 @@
final List<WorkResponse> responses = <WorkResponse>[];
TestAsyncWorkerConnection(
- Stream<List<int>> inputStream, StreamSink<List<int>> outputStream)
- : super(inputStream: inputStream, outputStream: outputStream);
+ Stream<List<int>> inputStream,
+ StreamSink<List<int>> outputStream,
+ ) : super(inputStream: inputStream, outputStream: outputStream);
@override
void writeResponse(WorkResponse response) {
diff --git a/pkgs/bazel_worker/test/driver_test.dart b/pkgs/bazel_worker/test/driver_test.dart
index c397830..c3db55c 100644
--- a/pkgs/bazel_worker/test/driver_test.dart
+++ b/pkgs/bazel_worker/test/driver_test.dart
@@ -23,27 +23,37 @@
await _doRequests(count: 1);
});
- test('can run multiple batches of requests through multiple workers',
- () async {
- var maxWorkers = 4;
- var maxIdleWorkers = 2;
- driver = BazelWorkerDriver(MockWorker.spawn,
- maxWorkers: maxWorkers, maxIdleWorkers: maxIdleWorkers);
- for (var i = 0; i < 10; i++) {
- await _doRequests(driver: driver);
- expect(MockWorker.liveWorkers.length, maxIdleWorkers);
- // No workers should be killed while there is ongoing work, but they
- // should be cleaned up once there isn't any more work to do.
- expect(MockWorker.deadWorkers.length,
- (maxWorkers - maxIdleWorkers) * (i + 1));
- }
- });
+ test(
+ 'can run multiple batches of requests through multiple workers',
+ () async {
+ var maxWorkers = 4;
+ var maxIdleWorkers = 2;
+ driver = BazelWorkerDriver(
+ MockWorker.spawn,
+ maxWorkers: maxWorkers,
+ maxIdleWorkers: maxIdleWorkers,
+ );
+ for (var i = 0; i < 10; i++) {
+ await _doRequests(driver: driver);
+ expect(MockWorker.liveWorkers.length, maxIdleWorkers);
+ // No workers should be killed while there is ongoing work, but they
+ // should be cleaned up once there isn't any more work to do.
+ expect(
+ MockWorker.deadWorkers.length,
+ (maxWorkers - maxIdleWorkers) * (i + 1),
+ );
+ }
+ },
+ );
test('can run multiple requests through one worker', () async {
var maxWorkers = 1;
var maxIdleWorkers = 1;
- driver = BazelWorkerDriver(MockWorker.spawn,
- maxWorkers: maxWorkers, maxIdleWorkers: maxIdleWorkers);
+ driver = BazelWorkerDriver(
+ MockWorker.spawn,
+ maxWorkers: maxWorkers,
+ maxIdleWorkers: maxIdleWorkers,
+ );
for (var i = 0; i < 10; i++) {
await _doRequests(driver: driver);
expect(MockWorker.liveWorkers.length, 1);
@@ -52,8 +62,11 @@
});
test('can run one request through multiple workers', () async {
- driver =
- BazelWorkerDriver(MockWorker.spawn, maxWorkers: 4, maxIdleWorkers: 4);
+ driver = BazelWorkerDriver(
+ MockWorker.spawn,
+ maxWorkers: 4,
+ maxIdleWorkers: 4,
+ );
for (var i = 0; i < 10; i++) {
await _doRequests(driver: driver, count: 1);
expect(MockWorker.liveWorkers.length, 1);
@@ -63,8 +76,11 @@
test('can run with maxIdleWorkers == 0', () async {
var maxWorkers = 4;
- driver = BazelWorkerDriver(MockWorker.spawn,
- maxWorkers: maxWorkers, maxIdleWorkers: 0);
+ driver = BazelWorkerDriver(
+ MockWorker.spawn,
+ maxWorkers: maxWorkers,
+ maxIdleWorkers: 0,
+ );
for (var i = 0; i < 10; i++) {
await _doRequests(driver: driver);
expect(MockWorker.liveWorkers.length, 0);
@@ -77,14 +93,15 @@
driver = BazelWorkerDriver(MockWorker.spawn, maxWorkers: maxWorkers);
var tracking = <Future>[];
await _doRequests(
- driver: driver,
- count: 10,
- trackWork: (Future response) {
- // We should never be tracking more than `maxWorkers` jobs at a time.
- expect(tracking.length, lessThan(maxWorkers));
- tracking.add(response);
- response.then((_) => tracking.remove(response));
- });
+ driver: driver,
+ count: 10,
+ trackWork: (Future response) {
+ // We should never be tracking more than `maxWorkers` jobs at a time.
+ expect(tracking.length, lessThan(maxWorkers));
+ tracking.add(response);
+ response.then((_) => tracking.remove(response));
+ },
+ );
});
group('failing workers', () {
@@ -93,27 +110,39 @@
void createDriver({int maxRetries = 2, int numBadWorkers = 2}) {
var numSpawned = 0;
driver = BazelWorkerDriver(
- () async => MockWorker(workerLoopFactory: (MockWorker worker) {
- var connection = StdAsyncWorkerConnection(
- inputStream: worker._stdinController.stream,
- outputStream: worker._stdoutController.sink);
- if (numSpawned < numBadWorkers) {
- numSpawned++;
- return ThrowingMockWorkerLoop(
- worker, MockWorker.responseQueue, connection);
- } else {
- return MockWorkerLoop(MockWorker.responseQueue,
- connection: connection);
- }
- }),
- maxRetries: maxRetries);
+ () async => MockWorker(
+ workerLoopFactory: (MockWorker worker) {
+ var connection = StdAsyncWorkerConnection(
+ inputStream: worker._stdinController.stream,
+ outputStream: worker._stdoutController.sink,
+ );
+ if (numSpawned < numBadWorkers) {
+ numSpawned++;
+ return ThrowingMockWorkerLoop(
+ worker,
+ MockWorker.responseQueue,
+ connection,
+ );
+ } else {
+ return MockWorkerLoop(
+ MockWorker.responseQueue,
+ connection: connection,
+ );
+ }
+ },
+ ),
+ maxRetries: maxRetries,
+ );
}
test('should retry up to maxRetries times', () async {
createDriver();
var expectedResponse = WorkResponse();
- MockWorker.responseQueue.addAll(
- [disconnectedResponse, disconnectedResponse, expectedResponse]);
+ MockWorker.responseQueue.addAll([
+ disconnectedResponse,
+ disconnectedResponse,
+ expectedResponse,
+ ]);
var actualResponse = await driver!.doWork(WorkRequest());
// The first 2 null responses are thrown away, and we should get the
// third one.
@@ -125,23 +154,29 @@
test('should fail if it exceeds maxRetries failures', () async {
createDriver(maxRetries: 2, numBadWorkers: 3);
- MockWorker.responseQueue.addAll(
- [disconnectedResponse, disconnectedResponse, WorkResponse()]);
+ MockWorker.responseQueue.addAll([
+ disconnectedResponse,
+ disconnectedResponse,
+ WorkResponse(),
+ ]);
var actualResponse = await driver!.doWork(WorkRequest());
// Should actually get a bad response.
expect(actualResponse.exitCode, 15);
expect(
- actualResponse.output,
- 'Invalid response from worker, this probably means it wrote '
- 'invalid output or died.');
+ actualResponse.output,
+ 'Invalid response from worker, this probably means it wrote '
+ 'invalid output or died.',
+ );
expect(MockWorker.deadWorkers.length, 3);
});
});
test('handles spawnWorker failures', () async {
- driver = BazelWorkerDriver(() async => throw StateError('oh no!'),
- maxRetries: 0);
+ driver = BazelWorkerDriver(
+ () async => throw StateError('oh no!'),
+ maxRetries: 0,
+ );
expect(driver!.doWork(WorkRequest()), throwsA(isA<StateError>()));
});
@@ -156,10 +191,11 @@
/// Runs [count] of fake work requests through [driver], and asserts that they
/// all completed.
-Future _doRequests(
- {BazelWorkerDriver? driver,
- int count = 100,
- void Function(Future<WorkResponse?>)? trackWork}) async {
+Future _doRequests({
+ BazelWorkerDriver? driver,
+ int count = 100,
+ void Function(Future<WorkResponse?>)? trackWork,
+}) async {
// If we create a driver, we need to make sure and terminate it.
var terminateDriver = driver == null;
driver ??= BazelWorkerDriver(MockWorker.spawn);
@@ -167,7 +203,8 @@
var responses = List.generate(count, (_) => WorkResponse());
MockWorker.responseQueue.addAll(responses);
var actualResponses = await Future.wait(
- requests.map((request) => driver!.doWork(request, trackWork: trackWork)));
+ requests.map((request) => driver!.doWork(request, trackWork: trackWork)),
+ );
expect(actualResponses, unorderedEquals(responses));
if (terminateDriver) await driver.terminateWorkers();
}
@@ -191,9 +228,11 @@
class ThrowingMockWorkerLoop extends MockWorkerLoop {
final MockWorker _mockWorker;
- ThrowingMockWorkerLoop(this._mockWorker, Queue<WorkResponse> responseQueue,
- AsyncWorkerConnection connection)
- : super(responseQueue, connection: connection);
+ ThrowingMockWorkerLoop(
+ this._mockWorker,
+ Queue<WorkResponse> responseQueue,
+ AsyncWorkerConnection connection,
+ ) : super(responseQueue, connection: connection);
/// Run the worker loop. The returned [Future] doesn't complete until
/// [connection#readRequest] returns `null`.
@@ -234,10 +273,13 @@
liveWorkers.add(this);
var workerLoop = workerLoopFactory != null
? workerLoopFactory(this)
- : MockWorkerLoop(responseQueue,
+ : MockWorkerLoop(
+ responseQueue,
connection: StdAsyncWorkerConnection(
- inputStream: _stdinController.stream,
- outputStream: _stdoutController.sink));
+ inputStream: _stdinController.stream,
+ outputStream: _stdoutController.sink,
+ ),
+ );
workerLoop.run();
}
@@ -260,8 +302,10 @@
int get pid => throw UnsupportedError('Not needed.');
@override
- bool kill(
- [ProcessSignal processSignal = ProcessSignal.sigterm, int exitCode = 0]) {
+ bool kill([
+ ProcessSignal processSignal = ProcessSignal.sigterm,
+ int exitCode = 0,
+ ]) {
if (_killed) return false;
() async {
await _stdoutController.close();
diff --git a/pkgs/bazel_worker/test/message_grouper_test.dart b/pkgs/bazel_worker/test/message_grouper_test.dart
index 475190e..fd99911 100644
--- a/pkgs/bazel_worker/test/message_grouper_test.dart
+++ b/pkgs/bazel_worker/test/message_grouper_test.dart
@@ -18,8 +18,10 @@
});
}
-void runTests(TestStdin Function() stdinFactory,
- MessageGrouper Function(Stdin) messageGrouperFactory) {
+void runTests(
+ TestStdin Function() stdinFactory,
+ MessageGrouper Function(Stdin) messageGrouperFactory,
+) {
late MessageGrouper messageGrouper;
late TestStdin stdinStream;
@@ -52,16 +54,12 @@
});
test('Short message', () async {
- await check([
- 5,
- 10,
- 20,
- 30,
- 40,
- 50
- ], [
- [10, 20, 30, 40, 50]
- ]);
+ await check(
+ [5, 10, 20, 30, 40, 50],
+ [
+ [10, 20, 30, 40, 50],
+ ],
+ );
});
test('Message with 2-byte length', () async {
@@ -79,57 +77,44 @@
});
test('Multiple messages', () async {
- await check([
- 2,
- 10,
- 20,
- 2,
- 30,
- 40
- ], [
- [10, 20],
- [30, 40]
- ]);
+ await check(
+ [2, 10, 20, 2, 30, 40],
+ [
+ [10, 20],
+ [30, 40],
+ ],
+ );
});
test('Empty message at start', () async {
- await check([
- 0,
- 2,
- 10,
- 20
- ], [
- [],
- [10, 20]
- ]);
+ await check(
+ [0, 2, 10, 20],
+ [
+ [],
+ [10, 20],
+ ],
+ );
});
test('Empty message at end', () async {
- await check([
- 2,
- 10,
- 20,
- 0
- ], [
- [10, 20],
- []
- ]);
+ await check(
+ [2, 10, 20, 0],
+ [
+ [10, 20],
+ [],
+ ],
+ );
});
test('Empty message in the middle', () async {
- await check([
- 2,
- 10,
- 20,
- 0,
- 2,
- 30,
- 40
- ], [
- [10, 20],
- [],
- [30, 40]
- ]);
+ await check(
+ [2, 10, 20, 0, 2, 30, 40],
+ [
+ [10, 20],
+ [],
+ [30, 40],
+ ],
+ );
});
test('Handles the case when stdin gives an error instead of EOF', () async {
diff --git a/pkgs/bazel_worker/test/worker_loop_test.dart b/pkgs/bazel_worker/test/worker_loop_test.dart
index 50d2151..24068b1 100644
--- a/pkgs/bazel_worker/test/worker_loop_test.dart
+++ b/pkgs/bazel_worker/test/worker_loop_test.dart
@@ -11,36 +11,45 @@
void main() {
group('SyncWorkerLoop', () {
- runTests(TestStdinSync.new, TestSyncWorkerConnection.new,
- TestSyncWorkerLoop.new);
+ runTests(
+ TestStdinSync.new,
+ TestSyncWorkerConnection.new,
+ TestSyncWorkerLoop.new,
+ );
});
group('AsyncWorkerLoop', () {
- runTests(TestStdinAsync.new, TestAsyncWorkerConnection.new,
- TestAsyncWorkerLoop.new);
+ runTests(
+ TestStdinAsync.new,
+ TestAsyncWorkerConnection.new,
+ TestAsyncWorkerLoop.new,
+ );
});
group('SyncWorkerLoopWithPrint', () {
runTests(
- TestStdinSync.new,
- TestSyncWorkerConnection.new,
- (TestSyncWorkerConnection connection) =>
- TestSyncWorkerLoop(connection, printMessage: 'Goodbye!'));
+ TestStdinSync.new,
+ TestSyncWorkerConnection.new,
+ (TestSyncWorkerConnection connection) =>
+ TestSyncWorkerLoop(connection, printMessage: 'Goodbye!'),
+ );
});
group('AsyncWorkerLoopWithPrint', () {
runTests(
- TestStdinAsync.new,
- TestAsyncWorkerConnection.new,
- (TestAsyncWorkerConnection connection) =>
- TestAsyncWorkerLoop(connection, printMessage: 'Goodbye!'));
+ TestStdinAsync.new,
+ TestAsyncWorkerConnection.new,
+ (TestAsyncWorkerConnection connection) =>
+ TestAsyncWorkerLoop(connection, printMessage: 'Goodbye!'),
+ );
});
}
void runTests<T extends TestWorkerConnection>(
- TestStdin Function() stdinFactory,
- T Function(Stdin, Stdout) workerConnectionFactory,
- TestWorkerLoop Function(T) workerLoopFactory) {
+ TestStdin Function() stdinFactory,
+ T Function(Stdin, Stdout) workerConnectionFactory,
+ TestWorkerLoop Function(T) workerLoopFactory,
+) {
late TestStdin stdinStream;
late TestStdoutStream stdoutStream;
late T connection;
@@ -63,19 +72,29 @@
// Make sure `print` never gets called in the parent zone.
var printMessages = <String>[];
- await runZoned(() => workerLoop.run(), zoneSpecification:
- ZoneSpecification(print: (self, parent, zone, message) {
- printMessages.add(message);
- }));
- expect(printMessages, isEmpty,
- reason: 'The worker loop should hide all print calls from the parent '
- 'zone.');
+ await runZoned(
+ () => workerLoop.run(),
+ zoneSpecification: ZoneSpecification(
+ print: (self, parent, zone, message) {
+ printMessages.add(message);
+ },
+ ),
+ );
+ expect(
+ printMessages,
+ isEmpty,
+ reason: 'The worker loop should hide all print calls from the parent '
+ 'zone.',
+ );
expect(connection.responses, hasLength(1));
expect(connection.responses[0], response);
if (workerLoop.printMessage != null) {
- expect(response.output, endsWith(workerLoop.printMessage!),
- reason: 'Print messages should get appended to the response output.');
+ expect(
+ response.output,
+ endsWith(workerLoop.printMessage!),
+ reason: 'Print messages should get appended to the response output.',
+ );
}
// Check that a serialized version was written to std out.
diff --git a/pkgs/clock/analysis_options.yaml b/pkgs/clock/analysis_options.yaml
index 9ee7c2b..db6072d 100644
--- a/pkgs/clock/analysis_options.yaml
+++ b/pkgs/clock/analysis_options.yaml
@@ -11,4 +11,3 @@
rules:
- avoid_private_typedef_functions
- avoid_redundant_argument_values
- - use_super_parameters
diff --git a/pkgs/coverage/analysis_options.yaml b/pkgs/coverage/analysis_options.yaml
index 82ce5e0..bb1afe0 100644
--- a/pkgs/coverage/analysis_options.yaml
+++ b/pkgs/coverage/analysis_options.yaml
@@ -9,14 +9,9 @@
linter:
rules:
- - always_declare_return_types
- avoid_slow_async_io
- cancel_subscriptions
- - comment_references
- literal_only_boolean_expressions
- prefer_final_locals
- sort_constructors_first
- sort_unnamed_constructors_first
- - test_types_in_equals
- - throw_in_finally
- - type_annotate_public_apis
diff --git a/pkgs/file/CHANGELOG.md b/pkgs/file/CHANGELOG.md
index 50c96c4..3a3969c 100644
--- a/pkgs/file/CHANGELOG.md
+++ b/pkgs/file/CHANGELOG.md
@@ -1,3 +1,5 @@
+## 7.0.2-wip
+
## 7.0.1
* Update the pubspec repository field to reflect the new package repository.
diff --git a/pkgs/file/analysis_options.yaml b/pkgs/file/analysis_options.yaml
index 8fbd2e4..d978f81 100644
--- a/pkgs/file/analysis_options.yaml
+++ b/pkgs/file/analysis_options.yaml
@@ -1,6 +1 @@
-include: package:lints/recommended.yaml
-
-analyzer:
- errors:
- # Allow having TODOs in the code
- todo: ignore
+include: package:dart_flutter_team_lints/analysis_options.yaml
diff --git a/pkgs/file/example/main.dart b/pkgs/file/example/main.dart
index 7ca0bc7..b03b363 100644
--- a/pkgs/file/example/main.dart
+++ b/pkgs/file/example/main.dart
@@ -7,8 +7,8 @@
Future<void> main() async {
final FileSystem fs = MemoryFileSystem();
- final Directory tmp = await fs.systemTempDirectory.createTemp('example_');
- final File outputFile = tmp.childFile('output');
+ final tmp = await fs.systemTempDirectory.createTemp('example_');
+ final outputFile = tmp.childFile('output');
await outputFile.writeAsString('Hello world!');
print(outputFile.readAsStringSync());
}
diff --git a/pkgs/file/lib/chroot.dart b/pkgs/file/lib/chroot.dart
index 56d2bd5..6992ad0 100644
--- a/pkgs/file/lib/chroot.dart
+++ b/pkgs/file/lib/chroot.dart
@@ -3,4 +3,6 @@
// BSD-style license that can be found in the LICENSE file.
/// A file system that provides a view into _another_ `FileSystem` via a path.
+library;
+
export 'src/backends/chroot.dart';
diff --git a/pkgs/file/lib/file.dart b/pkgs/file/lib/file.dart
index cdde9fe..c2e97b2 100644
--- a/pkgs/file/lib/file.dart
+++ b/pkgs/file/lib/file.dart
@@ -4,5 +4,7 @@
/// Core interfaces containing the abstract `FileSystem` interface definition
/// and all associated types used by `FileSystem`.
+library;
+
export 'src/forwarding.dart';
export 'src/interface.dart';
diff --git a/pkgs/file/lib/local.dart b/pkgs/file/lib/local.dart
index 74f506e..5b1e3cd 100644
--- a/pkgs/file/lib/local.dart
+++ b/pkgs/file/lib/local.dart
@@ -4,4 +4,6 @@
/// A local file system implementation. This relies on the use of `dart:io`
/// and is thus not suitable for use in the browser.
+library;
+
export 'src/backends/local.dart';
diff --git a/pkgs/file/lib/memory.dart b/pkgs/file/lib/memory.dart
index c5705ef..690b65f 100644
--- a/pkgs/file/lib/memory.dart
+++ b/pkgs/file/lib/memory.dart
@@ -4,5 +4,7 @@
/// An implementation of `FileSystem` that exists entirely in memory with an
/// internal representation loosely based on the Filesystem Hierarchy Standard.
+library;
+
export 'src/backends/memory.dart';
export 'src/backends/memory/operations.dart';
diff --git a/pkgs/file/lib/src/backends/chroot.dart b/pkgs/file/lib/src/backends/chroot.dart
index 6082e80..402dbec 100644
--- a/pkgs/file/lib/src/backends/chroot.dart
+++ b/pkgs/file/lib/src/backends/chroot.dart
@@ -2,16 +2,16 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-library file.src.backends.chroot;
-
import 'dart:convert';
import 'dart:typed_data';
-import 'package:file/file.dart';
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
import 'package:path/path.dart' as p;
+import '../common.dart' as common;
+import '../forwarding.dart';
+import '../interface.dart';
+import '../io.dart' as io;
+
part 'chroot/chroot_directory.dart';
part 'chroot/chroot_file.dart';
part 'chroot/chroot_file_system.dart';
diff --git a/pkgs/file/lib/src/backends/chroot/chroot_directory.dart b/pkgs/file/lib/src/backends/chroot/chroot_directory.dart
index 8fec7b1..e094193 100644
--- a/pkgs/file/lib/src/backends/chroot/chroot_directory.dart
+++ b/pkgs/file/lib/src/backends/chroot/chroot_directory.dart
@@ -2,18 +2,18 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-part of file.src.backends.chroot;
+part of '../chroot.dart';
class _ChrootDirectory extends _ChrootFileSystemEntity<Directory, io.Directory>
with ForwardingDirectory<Directory>, common.DirectoryAddOnsMixin {
- _ChrootDirectory(ChrootFileSystem fs, String path) : super(fs, path);
+ _ChrootDirectory(super.fs, super.path);
factory _ChrootDirectory.wrapped(
ChrootFileSystem fs,
Directory delegate, {
bool relative = false,
}) {
- String localPath = fs._local(delegate.path, relative: relative);
+ var localPath = fs._local(delegate.path, relative: relative);
return _ChrootDirectory(fs, localPath);
}
@@ -32,7 +32,7 @@
if (await fileSystem.type(path) != expectedType) {
throw common.notADirectory(path);
}
- FileSystemEntityType type = await fileSystem.type(newPath);
+ var type = await fileSystem.type(newPath);
if (type != FileSystemEntityType.notFound) {
if (type != expectedType) {
throw common.notADirectory(newPath);
@@ -44,7 +44,7 @@
throw common.directoryNotEmpty(newPath);
}
}
- String target = await fileSystem.link(path).target();
+ var target = await fileSystem.link(path).target();
await fileSystem.link(path).delete();
await fileSystem.link(newPath).create(target);
return fileSystem.directory(newPath);
@@ -60,7 +60,7 @@
if (fileSystem.typeSync(path) != expectedType) {
throw common.notADirectory(path);
}
- FileSystemEntityType type = fileSystem.typeSync(newPath);
+ var type = fileSystem.typeSync(newPath);
if (type != FileSystemEntityType.notFound) {
if (type != expectedType) {
throw common.notADirectory(newPath);
@@ -72,7 +72,7 @@
throw common.directoryNotEmpty(newPath);
}
}
- String target = fileSystem.link(path).targetSync();
+ var target = fileSystem.link(path).targetSync();
fileSystem.link(path).deleteSync();
fileSystem.link(newPath).createSync(target);
return fileSystem.directory(newPath);
@@ -97,17 +97,15 @@
@override
Future<Directory> create({bool recursive = false}) async {
if (_isLink) {
- switch (await fileSystem.type(path)) {
- case FileSystemEntityType.notFound:
- throw common.noSuchFileOrDirectory(path);
- case FileSystemEntityType.file:
- throw common.fileExists(path);
- case FileSystemEntityType.directory:
+ return switch (await fileSystem.type(path)) {
+ FileSystemEntityType.notFound =>
+ throw common.noSuchFileOrDirectory(path),
+ FileSystemEntityType.file => throw common.fileExists(path),
+ FileSystemEntityType.directory =>
// Nothing to do.
- return this;
- default:
- throw AssertionError();
- }
+ this,
+ _ => throw AssertionError()
+ };
} else {
return wrap(await delegate.create(recursive: recursive));
}
@@ -137,8 +135,8 @@
bool recursive = false,
bool followLinks = true,
}) {
- Directory delegate = this.delegate as Directory;
- String dirname = delegate.path;
+ var delegate = this.delegate as Directory;
+ var dirname = delegate.path;
return delegate
.list(recursive: recursive, followLinks: followLinks)
.map((io.FileSystemEntity entity) => _denormalize(entity, dirname));
@@ -149,8 +147,8 @@
bool recursive = false,
bool followLinks = true,
}) {
- Directory delegate = this.delegate as Directory;
- String dirname = delegate.path;
+ var delegate = this.delegate as Directory;
+ var dirname = delegate.path;
return delegate
.listSync(recursive: recursive, followLinks: followLinks)
.map((io.FileSystemEntity entity) => _denormalize(entity, dirname))
@@ -158,9 +156,9 @@
}
FileSystemEntity _denormalize(io.FileSystemEntity entity, String dirname) {
- p.Context ctx = fileSystem.path;
- String relativePart = ctx.relative(entity.path, from: dirname);
- String entityPath = ctx.join(path, relativePart);
+ var ctx = fileSystem.path;
+ var relativePart = ctx.relative(entity.path, from: dirname);
+ var entityPath = ctx.join(path, relativePart);
if (entity is io.File) {
return _ChrootFile(fileSystem, entityPath);
} else if (entity is io.Directory) {
diff --git a/pkgs/file/lib/src/backends/chroot/chroot_file.dart b/pkgs/file/lib/src/backends/chroot/chroot_file.dart
index 4b67bc1..d6c29fc 100644
--- a/pkgs/file/lib/src/backends/chroot/chroot_file.dart
+++ b/pkgs/file/lib/src/backends/chroot/chroot_file.dart
@@ -2,20 +2,20 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-part of file.src.backends.chroot;
+part of '../chroot.dart';
typedef _SetupCallback = dynamic Function();
class _ChrootFile extends _ChrootFileSystemEntity<File, io.File>
with ForwardingFile {
- _ChrootFile(ChrootFileSystem fs, String path) : super(fs, path);
+ _ChrootFile(super.fs, super.path);
factory _ChrootFile.wrapped(
ChrootFileSystem fs,
io.File delegate, {
bool relative = false,
}) {
- String localPath = fs._local(delegate.path, relative: relative);
+ var localPath = fs._local(delegate.path, relative: relative);
return _ChrootFile(fs, localPath);
}
@@ -126,7 +126,7 @@
@override
Future<File> create({bool recursive = false, bool exclusive = false}) async {
- String path = fileSystem._resolve(
+ var path = fileSystem._resolve(
this.path,
followLinks: false,
notFound: recursive ? _NotFoundBehavior.mkdir : _NotFoundBehavior.allow,
@@ -158,7 +158,7 @@
@override
void createSync({bool recursive = false, bool exclusive = false}) {
- String path = fileSystem._resolve(
+ var path = fileSystem._resolve(
this.path,
followLinks: false,
notFound: recursive ? _NotFoundBehavior.mkdir : _NotFoundBehavior.allow,
diff --git a/pkgs/file/lib/src/backends/chroot/chroot_file_system.dart b/pkgs/file/lib/src/backends/chroot/chroot_file_system.dart
index 6889c98..503821f 100644
--- a/pkgs/file/lib/src/backends/chroot/chroot_file_system.dart
+++ b/pkgs/file/lib/src/backends/chroot/chroot_file_system.dart
@@ -2,7 +2,7 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-part of file.src.backends.chroot;
+part of '../chroot.dart';
const String _thisDir = '.';
const String _parentDir = '..';
@@ -107,7 +107,7 @@
}
value = _resolve(value, notFound: _NotFoundBehavior.throwError);
- String realPath = _real(value, resolve: false);
+ var realPath = _real(value, resolve: false);
switch (delegate.typeSync(realPath, followLinks: false)) {
case FileSystemEntityType.directory:
break;
@@ -117,7 +117,7 @@
throw common.notADirectory(path as String);
}
assert(() {
- p.Context ctx = delegate.path;
+ var ctx = delegate.path;
return ctx.isAbsolute(value) && value == ctx.canonicalize(value);
}());
_cwd = value;
@@ -201,7 +201,7 @@
throw _ChrootJailException();
}
// TODO(tvolkert): See if _context.relative() works here
- String result = realPath.substring(root.length);
+ var result = realPath.substring(root.length);
if (result.isEmpty) {
result = _localRoot;
}
@@ -263,8 +263,8 @@
throw common.noSuchFileOrDirectory(path);
}
- p.Context ctx = this.path;
- String root = _localRoot;
+ var ctx = this.path;
+ var root = _localRoot;
List<String> parts, ledger;
if (ctx.isAbsolute(path)) {
parts = ctx.split(path).sublist(1);
@@ -277,9 +277,9 @@
}
String getCurrentPath() => root + ctx.joinAll(ledger);
- Set<String> breadcrumbs = <String>{};
+ var breadcrumbs = <String>{};
while (parts.isNotEmpty) {
- String segment = parts.removeAt(0);
+ var segment = parts.removeAt(0);
if (segment == _thisDir) {
continue;
} else if (segment == _parentDir) {
@@ -290,8 +290,8 @@
}
ledger.add(segment);
- String currentPath = getCurrentPath();
- String realPath = _real(currentPath, resolve: false);
+ var currentPath = getCurrentPath();
+ var realPath = _real(currentPath, resolve: false);
switch (delegate.typeSync(realPath, followLinks: false)) {
case FileSystemEntityType.directory:
@@ -333,7 +333,7 @@
if (!breadcrumbs.add(currentPath)) {
throw common.tooManyLevelsOfSymbolicLinks(path);
}
- String target = delegate.link(realPath).targetSync();
+ var target = delegate.link(realPath).targetSync();
if (ctx.isAbsolute(target)) {
ledger.clear();
parts.insertAll(0, ctx.split(target).sublist(1));
diff --git a/pkgs/file/lib/src/backends/chroot/chroot_file_system_entity.dart b/pkgs/file/lib/src/backends/chroot/chroot_file_system_entity.dart
index 8e859ac..18e37cd 100644
--- a/pkgs/file/lib/src/backends/chroot/chroot_file_system_entity.dart
+++ b/pkgs/file/lib/src/backends/chroot/chroot_file_system_entity.dart
@@ -2,7 +2,7 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-part of file.src.backends.chroot;
+part of '../chroot.dart';
abstract class _ChrootFileSystemEntity<T extends FileSystemEntity,
D extends io.FileSystemEntity> extends ForwardingFileSystemEntity<T, D> {
@@ -103,7 +103,7 @@
@override
Future<T> delete({bool recursive = false}) async {
- String path = fileSystem._resolve(this.path,
+ var path = fileSystem._resolve(this.path,
followLinks: false, notFound: _NotFoundBehavior.throwError);
String real(String path) => fileSystem._real(path, resolve: false);
@@ -114,7 +114,7 @@
if (expectedType == FileSystemEntityType.link) {
await fileSystem.delegate.link(real(path)).delete();
} else {
- String resolvedPath = fileSystem._resolve(p.basename(path),
+ var resolvedPath = fileSystem._resolve(p.basename(path),
from: p.dirname(path), notFound: _NotFoundBehavior.allowAtTail);
if (!recursive && await type(resolvedPath) != expectedType) {
throw expectedType == FileSystemEntityType.file
@@ -132,7 +132,7 @@
@override
void deleteSync({bool recursive = false}) {
- String path = fileSystem._resolve(this.path,
+ var path = fileSystem._resolve(this.path,
followLinks: false, notFound: _NotFoundBehavior.throwError);
String real(String path) => fileSystem._real(path, resolve: false);
@@ -143,7 +143,7 @@
if (expectedType == FileSystemEntityType.link) {
fileSystem.delegate.link(real(path)).deleteSync();
} else {
- String resolvedPath = fileSystem._resolve(p.basename(path),
+ var resolvedPath = fileSystem._resolve(p.basename(path),
from: p.dirname(path), notFound: _NotFoundBehavior.allowAtTail);
if (!recursive && type(resolvedPath) != expectedType) {
throw expectedType == FileSystemEntityType.file
diff --git a/pkgs/file/lib/src/backends/chroot/chroot_link.dart b/pkgs/file/lib/src/backends/chroot/chroot_link.dart
index acbeda6..1620df9 100644
--- a/pkgs/file/lib/src/backends/chroot/chroot_link.dart
+++ b/pkgs/file/lib/src/backends/chroot/chroot_link.dart
@@ -2,18 +2,18 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-part of file.src.backends.chroot;
+part of '../chroot.dart';
class _ChrootLink extends _ChrootFileSystemEntity<Link, io.Link>
with ForwardingLink {
- _ChrootLink(ChrootFileSystem fs, String path) : super(fs, path);
+ _ChrootLink(super.fs, super.path);
factory _ChrootLink.wrapped(
ChrootFileSystem fs,
io.Link delegate, {
bool relative = false,
}) {
- String localPath = fs._local(delegate.path, relative: relative);
+ var localPath = fs._local(delegate.path, relative: relative);
return _ChrootLink(fs, localPath);
}
diff --git a/pkgs/file/lib/src/backends/chroot/chroot_random_access_file.dart b/pkgs/file/lib/src/backends/chroot/chroot_random_access_file.dart
index 4105ac8..10bbd70 100644
--- a/pkgs/file/lib/src/backends/chroot/chroot_random_access_file.dart
+++ b/pkgs/file/lib/src/backends/chroot/chroot_random_access_file.dart
@@ -2,7 +2,7 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-part of file.src.backends.chroot;
+part of '../chroot.dart';
class _ChrootRandomAccessFile with ForwardingRandomAccessFile {
_ChrootRandomAccessFile(this.path, this.delegate);
diff --git a/pkgs/file/lib/src/backends/local/local_directory.dart b/pkgs/file/lib/src/backends/local/local_directory.dart
index e23e68f..3e1db61 100644
--- a/pkgs/file/lib/src/backends/local/local_directory.dart
+++ b/pkgs/file/lib/src/backends/local/local_directory.dart
@@ -2,10 +2,10 @@
// for 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:file/file.dart';
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
-
+import '../../common.dart' as common;
+import '../../forwarding.dart';
+import '../../interface.dart';
+import '../../io.dart' as io;
import 'local_file_system_entity.dart';
/// [Directory] implementation that forwards all calls to `dart:io`.
@@ -13,7 +13,7 @@
with ForwardingDirectory<LocalDirectory>, common.DirectoryAddOnsMixin {
/// Instantiates a new [LocalDirectory] tied to the specified file system
/// and delegating to the specified [delegate].
- LocalDirectory(FileSystem fs, io.Directory delegate) : super(fs, delegate);
+ LocalDirectory(super.fs, super.delegate);
@override
String toString() => "LocalDirectory: '$path'";
diff --git a/pkgs/file/lib/src/backends/local/local_file.dart b/pkgs/file/lib/src/backends/local/local_file.dart
index 36293ba..a4bc106 100644
--- a/pkgs/file/lib/src/backends/local/local_file.dart
+++ b/pkgs/file/lib/src/backends/local/local_file.dart
@@ -2,9 +2,9 @@
// for 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:file/file.dart';
-import 'package:file/src/io.dart' as io;
-
+import '../../forwarding.dart';
+import '../../interface.dart';
+import '../../io.dart' as io;
import 'local_file_system_entity.dart';
/// [File] implementation that forwards all calls to `dart:io`.
@@ -12,7 +12,7 @@
with ForwardingFile {
/// Instantiates a new [LocalFile] tied to the specified file system
/// and delegating to the specified [delegate].
- LocalFile(FileSystem fs, io.File delegate) : super(fs, delegate);
+ LocalFile(super.fs, super.delegate);
@override
String toString() => "LocalFile: '$path'";
diff --git a/pkgs/file/lib/src/backends/local/local_file_system.dart b/pkgs/file/lib/src/backends/local/local_file_system.dart
index 635998e..7541c37 100644
--- a/pkgs/file/lib/src/backends/local/local_file_system.dart
+++ b/pkgs/file/lib/src/backends/local/local_file_system.dart
@@ -2,10 +2,10 @@
// for 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:file/src/io.dart' as io;
-import 'package:file/file.dart';
import 'package:path/path.dart' as p;
+import '../../interface.dart';
+import '../../io.dart' as io;
import 'local_directory.dart';
import 'local_file.dart';
import 'local_link.dart';
diff --git a/pkgs/file/lib/src/backends/local/local_file_system_entity.dart b/pkgs/file/lib/src/backends/local/local_file_system_entity.dart
index ca4617b..d0da559 100644
--- a/pkgs/file/lib/src/backends/local/local_file_system_entity.dart
+++ b/pkgs/file/lib/src/backends/local/local_file_system_entity.dart
@@ -2,9 +2,9 @@
// for 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:file/file.dart';
-import 'package:file/src/io.dart' as io;
-
+import '../../forwarding.dart';
+import '../../interface.dart';
+import '../../io.dart' as io;
import 'local_directory.dart';
import 'local_file.dart';
import 'local_link.dart';
diff --git a/pkgs/file/lib/src/backends/local/local_link.dart b/pkgs/file/lib/src/backends/local/local_link.dart
index fc67d5e..2ce4791 100644
--- a/pkgs/file/lib/src/backends/local/local_link.dart
+++ b/pkgs/file/lib/src/backends/local/local_link.dart
@@ -2,9 +2,9 @@
// for 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:file/file.dart';
-import 'package:file/src/io.dart' as io;
-
+import '../../forwarding.dart';
+import '../../interface.dart';
+import '../../io.dart' as io;
import 'local_file_system_entity.dart';
/// [Link] implementation that forwards all calls to `dart:io`.
@@ -12,7 +12,7 @@
with ForwardingLink {
/// Instantiates a new [LocalLink] tied to the specified file system
/// and delegating to the specified [delegate].
- LocalLink(FileSystem fs, io.Link delegate) : super(fs, delegate);
+ LocalLink(super.fs, super.delegate);
@override
String toString() => "LocalLink: '$path'";
diff --git a/pkgs/file/lib/src/backends/memory/clock.dart b/pkgs/file/lib/src/backends/memory/clock.dart
index 98d5434..57c1b72 100644
--- a/pkgs/file/lib/src/backends/memory/clock.dart
+++ b/pkgs/file/lib/src/backends/memory/clock.dart
@@ -2,6 +2,8 @@
// for 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: comment_references
+
/// Interface describing clocks used by the [MemoryFileSystem].
///
/// The [MemoryFileSystem] uses a clock to determine the modification times of
diff --git a/pkgs/file/lib/src/backends/memory/common.dart b/pkgs/file/lib/src/backends/memory/common.dart
index 80e3c38..eb4ca43 100644
--- a/pkgs/file/lib/src/backends/memory/common.dart
+++ b/pkgs/file/lib/src/backends/memory/common.dart
@@ -2,7 +2,7 @@
// for 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:file/src/common.dart' as common;
+import '../../common.dart' as common;
/// Generates a path to use in error messages.
typedef PathGenerator = dynamic Function();
diff --git a/pkgs/file/lib/src/backends/memory/memory_directory.dart b/pkgs/file/lib/src/backends/memory/memory_directory.dart
index 95fe542..e73b967 100644
--- a/pkgs/file/lib/src/backends/memory/memory_directory.dart
+++ b/pkgs/file/lib/src/backends/memory/memory_directory.dart
@@ -2,11 +2,11 @@
// for 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:file/file.dart';
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
import 'package:meta/meta.dart';
+import '../../common.dart' as common;
+import '../../interface.dart';
+import '../../io.dart' as io;
import 'common.dart';
import 'memory_file.dart';
import 'memory_file_system_entity.dart';
@@ -25,8 +25,7 @@
with common.DirectoryAddOnsMixin
implements Directory {
/// Instantiates a new [MemoryDirectory].
- MemoryDirectory(NodeBasedFileSystem fileSystem, String path)
- : super(fileSystem, path);
+ MemoryDirectory(super.fileSystem, super.path);
@override
io.FileSystemEntityType get expectedType => io.FileSystemEntityType.directory;
@@ -52,7 +51,7 @@
@override
void createSync({bool recursive = false}) {
fileSystem.opHandle(path, FileSystemOp.create);
- Node? node = internalCreateSync(
+ var node = internalCreateSync(
followTailLink: true,
visitLinks: true,
createChild: (DirectoryNode parent, bool isFinalSegment) {
@@ -75,19 +74,19 @@
@override
Directory createTempSync([String? prefix]) {
prefix = '${prefix ?? ''}rand';
- String fullPath = fileSystem.path.join(path, prefix);
- String dirname = fileSystem.path.dirname(fullPath);
- String basename = fileSystem.path.basename(fullPath);
- DirectoryNode? node = fileSystem.findNode(dirname) as DirectoryNode?;
+ var fullPath = fileSystem.path.join(path, prefix);
+ var dirname = fileSystem.path.dirname(fullPath);
+ var basename = fileSystem.path.basename(fullPath);
+ var node = fileSystem.findNode(dirname) as DirectoryNode?;
checkExists(node, () => dirname);
utils.checkIsDir(node!, () => dirname);
- int tempCounter = _systemTempCounter[fileSystem] ?? 0;
+ var tempCounter = _systemTempCounter[fileSystem] ?? 0;
String name() => '$basename$tempCounter';
while (node.children.containsKey(name())) {
tempCounter++;
}
_systemTempCounter[fileSystem] = tempCounter;
- DirectoryNode tempDir = DirectoryNode(node);
+ var tempDir = DirectoryNode(node);
node.children[name()] = tempDir;
return MemoryDirectory(fileSystem, fileSystem.path.join(dirname, name()))
..createSync();
@@ -128,9 +127,9 @@
bool recursive = false,
bool followLinks = true,
}) {
- DirectoryNode node = backing as DirectoryNode;
- List<FileSystemEntity> listing = <FileSystemEntity>[];
- List<_PendingListTask> tasks = <_PendingListTask>[
+ var node = backing as DirectoryNode;
+ var listing = <FileSystemEntity>[];
+ var tasks = <_PendingListTask>[
_PendingListTask(
node,
path.endsWith(fileSystem.path.separator)
@@ -140,14 +139,14 @@
),
];
while (tasks.isNotEmpty) {
- _PendingListTask task = tasks.removeLast();
+ var task = tasks.removeLast();
task.dir.children.forEach((String name, Node child) {
- Set<LinkNode> breadcrumbs = Set<LinkNode>.from(task.breadcrumbs);
- String childPath = fileSystem.path.join(task.path, name);
+ var breadcrumbs = Set<LinkNode>.from(task.breadcrumbs);
+ var childPath = fileSystem.path.join(task.path, name);
while (followLinks &&
utils.isLink(child) &&
breadcrumbs.add(child as LinkNode)) {
- Node? referent = child.referentOrNull;
+ var referent = child.referentOrNull;
if (referent != null) {
child = referent;
}
diff --git a/pkgs/file/lib/src/backends/memory/memory_file.dart b/pkgs/file/lib/src/backends/memory/memory_file.dart
index ba4faab..1a8f5f9 100644
--- a/pkgs/file/lib/src/backends/memory/memory_file.dart
+++ b/pkgs/file/lib/src/backends/memory/memory_file.dart
@@ -7,26 +7,25 @@
import 'dart:math' as math show min;
import 'dart:typed_data';
-import 'package:file/file.dart';
-import 'package:file/src/backends/memory/operations.dart';
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
import 'package:meta/meta.dart';
+import '../../common.dart' as common;
+import '../../interface.dart';
+import '../../io.dart' as io;
import 'common.dart';
import 'memory_file_system_entity.dart';
import 'memory_random_access_file.dart';
import 'node.dart';
+import 'operations.dart';
import 'utils.dart' as utils;
/// Internal implementation of [File].
class MemoryFile extends MemoryFileSystemEntity implements File {
/// Instantiates a new [MemoryFile].
- const MemoryFile(NodeBasedFileSystem fileSystem, String path)
- : super(fileSystem, path);
+ const MemoryFile(super.fileSystem, super.path);
FileNode get _resolvedBackingOrCreate {
- Node? node = backingOrNull;
+ var node = backingOrNull;
if (node == null) {
node = _doCreate();
} else {
@@ -61,7 +60,7 @@
}
Node? _doCreate({bool recursive = false}) {
- Node? node = internalCreateSync(
+ var node = internalCreateSync(
followTailLink: true,
createChild: (DirectoryNode parent, bool isFinalSegment) {
if (isFinalSegment) {
@@ -88,7 +87,7 @@
newPath,
followTailLink: true,
checkType: (Node node) {
- FileSystemEntityType actualType = node.stat.type;
+ var actualType = node.stat.type;
if (actualType != expectedType) {
throw actualType == FileSystemEntityType.notFound
? common.noSuchFileOrDirectory(path)
@@ -103,7 +102,7 @@
@override
File copySync(String newPath) {
fileSystem.opHandle(path, FileSystemOp.copy);
- FileNode sourceNode = resolvedBacking as FileNode;
+ var sourceNode = resolvedBacking as FileNode;
fileSystem.findNode(
newPath,
segmentVisitor: (
@@ -116,7 +115,7 @@
if (currentSegment == finalSegment) {
if (child != null) {
if (utils.isLink(child)) {
- List<String> ledger = <String>[];
+ var ledger = <String>[];
child = utils.resolveLinks(child as LinkNode, () => newPath,
ledger: ledger);
checkExists(child, () => newPath);
@@ -127,7 +126,7 @@
utils.checkType(expectedType, child.type, () => newPath);
parent.children.remove(childName);
}
- FileNode newNode = FileNode(parent);
+ var newNode = FileNode(parent);
newNode.copyFrom(sourceNode);
parent.children[childName] = newNode;
}
@@ -158,7 +157,7 @@
@override
void setLastAccessedSync(DateTime time) {
- FileNode node = resolvedBacking as FileNode;
+ var node = resolvedBacking as FileNode;
node.accessed = time.millisecondsSinceEpoch;
}
@@ -174,7 +173,7 @@
@override
void setLastModifiedSync(DateTime time) {
- FileNode node = resolvedBacking as FileNode;
+ var node = resolvedBacking as FileNode;
node.modified = time.millisecondsSinceEpoch;
}
@@ -199,8 +198,8 @@
Stream<List<int>> openRead([int? start, int? end]) {
fileSystem.opHandle(path, FileSystemOp.open);
try {
- FileNode node = resolvedBacking as FileNode;
- Uint8List content = node.content;
+ var node = resolvedBacking as FileNode;
+ var content = node.content;
if (start != null) {
content = end == null
? content.sublist(start)
@@ -253,13 +252,13 @@
@override
List<String> readAsLinesSync({Encoding encoding = utf8}) {
- String str = readAsStringSync(encoding: encoding);
+ var str = readAsStringSync(encoding: encoding);
if (str.isEmpty) {
return <String>[];
}
- final List<String> lines = str.split('\n');
+ final lines = str.split('\n');
if (str.endsWith('\n')) {
// A final newline should not create an additional line.
lines.removeLast();
@@ -287,7 +286,7 @@
if (!utils.isWriteMode(mode)) {
throw common.badFileDescriptor(path);
}
- FileNode node = _resolvedBackingOrCreate;
+ var node = _resolvedBackingOrCreate;
_truncateIfNecessary(node, mode);
fileSystem.opHandle(path, FileSystemOp.write);
node.write(bytes);
@@ -349,7 +348,7 @@
deferredException = e;
}
- Future<FileNode> future = Future<FileNode>.microtask(() {
+ var future = Future<FileNode>.microtask(() {
if (deferredException != null) {
throw deferredException;
}
@@ -387,7 +386,7 @@
@override
void writeAll(Iterable<dynamic> objects, [String separator = '']) {
- bool firstIter = true;
+ var firstIter = true;
for (dynamic obj in objects) {
if (!firstIter) {
write(separator);
@@ -418,7 +417,7 @@
_streamCompleter = Completer<void>();
stream.listen(
- (List<int> data) => _addData(data),
+ _addData,
cancelOnError: true,
onError: (Object error, StackTrace stackTrace) {
_streamCompleter!.completeError(error, stackTrace);
@@ -445,8 +444,7 @@
_isClosed = true;
_pendingWrites.then(
(_) => _completer.complete(),
- onError: (Object error, StackTrace stackTrace) =>
- _completer.completeError(error, stackTrace),
+ onError: _completer.completeError,
);
}
return _completer.future;
diff --git a/pkgs/file/lib/src/backends/memory/memory_file_stat.dart b/pkgs/file/lib/src/backends/memory/memory_file_stat.dart
index 94f86d1..ce6beda 100644
--- a/pkgs/file/lib/src/backends/memory/memory_file_stat.dart
+++ b/pkgs/file/lib/src/backends/memory/memory_file_stat.dart
@@ -2,7 +2,7 @@
// for 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:file/src/io.dart' as io;
+import '../../io.dart' as io;
/// Internal implementation of [io.FileStat].
class MemoryFileStat implements io.FileStat {
@@ -47,8 +47,8 @@
@override
String modeString() {
- int permissions = mode & 0xFFF;
- List<String> codes = const <String>[
+ var permissions = mode & 0xFFF;
+ var codes = const <String>[
'---',
'--x',
'-w-',
@@ -58,7 +58,7 @@
'rw-',
'rwx',
];
- List<String> result = <String>[];
+ var result = <String>[];
result
..add(codes[(permissions >> 6) & 0x7])
..add(codes[(permissions >> 3) & 0x7])
diff --git a/pkgs/file/lib/src/backends/memory/memory_file_system.dart b/pkgs/file/lib/src/backends/memory/memory_file_system.dart
index f3cdaee..dd359f0 100644
--- a/pkgs/file/lib/src/backends/memory/memory_file_system.dart
+++ b/pkgs/file/lib/src/backends/memory/memory_file_system.dart
@@ -2,11 +2,10 @@
// for 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:file/file.dart';
-import 'package:file/src/backends/memory/operations.dart';
-import 'package:file/src/io.dart' as io;
import 'package:path/path.dart' as p;
+import '../../interface.dart';
+import '../../io.dart' as io;
import 'clock.dart';
import 'common.dart';
import 'memory_directory.dart';
@@ -14,6 +13,7 @@
import 'memory_file_stat.dart';
import 'memory_link.dart';
import 'node.dart';
+import 'operations.dart';
import 'style.dart';
import 'utils.dart' as utils;
@@ -91,7 +91,7 @@
p.Context _context;
@override
- final Function(String context, FileSystemOp operation) opHandle;
+ final void Function(String context, FileSystemOp operation) opHandle;
@override
final Clock clock;
@@ -141,7 +141,7 @@
}
value = directory(value).resolveSymbolicLinksSync();
- Node? node = findNode(value);
+ var node = findNode(value);
checkExists(node, () => value);
utils.checkIsDir(node!, () => value);
assert(_context.isAbsolute(value));
@@ -166,9 +166,9 @@
@override
bool identicalSync(String path1, String path2) {
- Node? node1 = findNode(path1);
+ var node1 = findNode(path1);
checkExists(node1, () => path1);
- Node? node2 = findNode(path2);
+ var node2 = findNode(path2);
checkExists(node2, () => path2);
return node1 != null && node1 == node2;
}
@@ -220,14 +220,13 @@
reference ??= _current;
}
- List<String> parts = path.split(style.separator)
- ..removeWhere(utils.isEmpty);
- DirectoryNode? directory = reference?.directory;
+ var parts = path.split(style.separator)..removeWhere(utils.isEmpty);
+ var directory = reference?.directory;
Node? child = directory;
- int finalSegment = parts.length - 1;
- for (int i = 0; i <= finalSegment; i++) {
- String basename = parts[i];
+ var finalSegment = parts.length - 1;
+ for (var i = 0; i <= finalSegment; i++) {
+ var basename = parts[i];
assert(basename.isNotEmpty);
switch (basename) {
diff --git a/pkgs/file/lib/src/backends/memory/memory_file_system_entity.dart b/pkgs/file/lib/src/backends/memory/memory_file_system_entity.dart
index ad987d7..1990abc 100644
--- a/pkgs/file/lib/src/backends/memory/memory_file_system_entity.dart
+++ b/pkgs/file/lib/src/backends/memory/memory_file_system_entity.dart
@@ -2,11 +2,11 @@
// for 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:file/file.dart';
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
import 'package:meta/meta.dart';
+import '../../common.dart' as common;
+import '../../interface.dart';
+import '../../io.dart' as io;
import 'common.dart';
import 'memory_directory.dart';
import 'node.dart';
@@ -60,7 +60,7 @@
/// The type of the node is not guaranteed to match [expectedType].
@protected
Node get backing {
- Node? node = fileSystem.findNode(path);
+ var node = fileSystem.findNode(path);
checkExists(node, () => path);
return node!;
}
@@ -71,7 +71,7 @@
/// doesn't match, this will throw a [io.FileSystemException].
@protected
Node get resolvedBacking {
- Node node = backing;
+ var node = backing;
node = utils.isLink(node)
? utils.resolveLinks(node as LinkNode, () => path)
: node;
@@ -107,14 +107,14 @@
if (path.isEmpty) {
throw common.noSuchFileOrDirectory(path);
}
- List<String> ledger = <String>[];
+ var ledger = <String>[];
if (isAbsolute) {
ledger.add(fileSystem.style.drive);
}
- Node? node = fileSystem.findNode(path,
+ var node = fileSystem.findNode(path,
pathWithSymlinks: ledger, followTailLink: true);
checkExists(node, () => path);
- String resolved = ledger.join(fileSystem.path.separator);
+ var resolved = ledger.join(fileSystem.path.separator);
if (resolved == fileSystem.style.drive) {
resolved = fileSystem.style.root;
} else if (!fileSystem.path.isAbsolute(resolved)) {
@@ -151,7 +151,7 @@
@override
FileSystemEntity get absolute {
- String absolutePath = path;
+ var absolutePath = path;
if (!fileSystem.path.isAbsolute(absolutePath)) {
absolutePath = fileSystem.path.join(fileSystem.cwd, absolutePath);
}
@@ -242,7 +242,7 @@
bool followTailLink = false,
utils.TypeChecker? checkType,
}) {
- Node node = backing;
+ var node = backing;
(checkType ?? defaultCheckType)(node);
fileSystem.findNode(
newPath,
@@ -256,7 +256,7 @@
if (currentSegment == finalSegment) {
if (child != null) {
if (followTailLink) {
- FileSystemEntityType childType = child.stat.type;
+ var childType = child.stat.type;
if (childType != FileSystemEntityType.notFound) {
utils.checkType(expectedType, child.stat.type, () => newPath);
}
@@ -289,7 +289,7 @@
utils.TypeChecker? checkType,
}) {
fileSystem.opHandle(path, FileSystemOp.delete);
- Node node = backing;
+ var node = backing;
if (!recursive) {
if (node is DirectoryNode && node.children.isNotEmpty) {
throw common.directoryNotEmpty(path);
diff --git a/pkgs/file/lib/src/backends/memory/memory_link.dart b/pkgs/file/lib/src/backends/memory/memory_link.dart
index 7d5afb4..a599fe8 100644
--- a/pkgs/file/lib/src/backends/memory/memory_link.dart
+++ b/pkgs/file/lib/src/backends/memory/memory_link.dart
@@ -2,11 +2,11 @@
// for 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:file/file.dart';
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
import 'package:meta/meta.dart';
+import '../../common.dart' as common;
+import '../../interface.dart';
+import '../../io.dart' as io;
import 'memory_file_system_entity.dart';
import 'node.dart';
import 'operations.dart';
@@ -15,8 +15,7 @@
/// Internal implementation of [Link].
class MemoryLink extends MemoryFileSystemEntity implements Link {
/// Instantiates a new [MemoryLink].
- const MemoryLink(NodeBasedFileSystem fileSystem, String path)
- : super(fileSystem, path);
+ const MemoryLink(super.fileSystem, super.path);
@override
io.FileSystemEntityType get expectedType => io.FileSystemEntityType.link;
@@ -50,7 +49,7 @@
@override
void createSync(String target, {bool recursive = false}) {
- bool preexisting = true;
+ var preexisting = true;
fileSystem.opHandle(path, FileSystemOp.create);
internalCreateSync(
createChild: (DirectoryNode parent, bool isFinalSegment) {
@@ -76,7 +75,7 @@
@override
void updateSync(String target) {
- Node node = backing;
+ var node = backing;
utils.checkType(expectedType, node.type, () => path);
(node as LinkNode).target = target;
}
@@ -93,7 +92,7 @@
@override
String targetSync() {
- Node node = backing;
+ var node = backing;
if (node.type != expectedType) {
// Note: this may change; https://github.com/dart-lang/sdk/issues/28204
throw common.noSuchFileOrDirectory(path);
diff --git a/pkgs/file/lib/src/backends/memory/memory_random_access_file.dart b/pkgs/file/lib/src/backends/memory/memory_random_access_file.dart
index d4fe73d..190f0a1 100644
--- a/pkgs/file/lib/src/backends/memory/memory_random_access_file.dart
+++ b/pkgs/file/lib/src/backends/memory/memory_random_access_file.dart
@@ -6,10 +6,11 @@
import 'dart:math' as math show min;
import 'dart:typed_data';
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
-
+import '../../common.dart' as common;
+import '../../io.dart' as io;
+import '../memory.dart' show MemoryFileSystem;
import 'memory_file.dart';
+import 'memory_file_system.dart' show MemoryFileSystem;
import 'node.dart';
import 'utils.dart' as utils;
@@ -106,8 +107,8 @@
/// Wraps a synchronous function to make it appear asynchronous.
///
/// [_asyncOperationPending], [_checkAsync], and [_asyncWrapper] are used to
- /// mimic [RandomAccessFile]'s enforcement that only one asynchronous
- /// operation is pending for a [RandomAccessFile] instance. Since
+ /// mimic [io.RandomAccessFile]'s enforcement that only one asynchronous
+ /// operation is pending for a [io.RandomAccessFile] instance. Since
/// [MemoryFileSystem]-based classes are likely to be used in tests, fidelity
/// is important to catch errors that might occur in production.
///
@@ -211,7 +212,7 @@
_checkReadable('read');
// TODO(jamesderlin): Check for integer overflow.
final int end = math.min(_position + bytes, lengthSync());
- final Uint8List copy = _node.content.sublist(_position, end);
+ final copy = _node.content.sublist(_position, end);
_position = end;
return copy;
}
@@ -243,7 +244,7 @@
end = RangeError.checkValidRange(start, end, buffer.length);
- final int length = lengthSync();
+ final length = lengthSync();
int i;
for (i = start; i < end && _position < length; i += 1, _position += 1) {
buffer[i] = _node.content[_position];
@@ -288,7 +289,7 @@
'truncate failed', path, common.invalidArgument(path).osError);
}
- final int oldLength = lengthSync();
+ final oldLength = lengthSync();
if (length < oldLength) {
_node.truncate(length);
@@ -329,7 +330,7 @@
// [Uint8List] will truncate values to 8-bits automatically, so we don't
// need to check [value].
- int length = lengthSync();
+ var length = lengthSync();
if (_position >= length) {
// If [_position] is out of bounds, [RandomAccessFile] zero-fills the
// file.
@@ -363,8 +364,8 @@
end = RangeError.checkValidRange(start, end, buffer.length);
- final int writeByteCount = end - start;
- final int endPosition = _position + writeByteCount;
+ final writeByteCount = end - start;
+ final endPosition = _position + writeByteCount;
if (endPosition > lengthSync()) {
truncateSync(endPosition);
diff --git a/pkgs/file/lib/src/backends/memory/node.dart b/pkgs/file/lib/src/backends/memory/node.dart
index ae4d3f7..eea72b5 100644
--- a/pkgs/file/lib/src/backends/memory/node.dart
+++ b/pkgs/file/lib/src/backends/memory/node.dart
@@ -4,13 +4,12 @@
import 'dart:typed_data';
-import 'package:file/file.dart';
-import 'package:file/src/backends/memory/operations.dart';
-import 'package:file/src/io.dart' as io;
-
+import '../../interface.dart';
+import '../../io.dart' as io;
import 'clock.dart';
import 'common.dart';
import 'memory_file_stat.dart';
+import 'operations.dart';
import 'style.dart';
/// Visitor callback for use with [NodeBasedFileSystem.findNode].
@@ -115,7 +114,7 @@
/// Reparents this node to live in the specified directory.
set parent(DirectoryNode parent) {
- DirectoryNode ancestor = parent;
+ var ancestor = parent;
while (!ancestor.isRoot) {
if (ancestor == this) {
throw const io.FileSystemException(
@@ -149,8 +148,8 @@
/// you call [stat] on them).
abstract class RealNode extends Node {
/// Constructs a new [RealNode] as a child of the specified [parent].
- RealNode(DirectoryNode? parent) : super(parent) {
- int now = clock.now.millisecondsSinceEpoch;
+ RealNode(super.parent) {
+ var now = clock.now.millisecondsSinceEpoch;
changed = now;
modified = now;
accessed = now;
@@ -195,7 +194,7 @@
/// Class that represents the backing for an in-memory directory.
class DirectoryNode extends RealNode {
/// Constructs a new [DirectoryNode] as a child of the specified [parent].
- DirectoryNode(DirectoryNode? parent) : super(parent);
+ DirectoryNode(super.parent);
/// Child nodes, indexed by their basename.
final Map<String, Node> children = <String, Node>{};
@@ -237,7 +236,7 @@
/// Class that represents the backing for an in-memory regular file.
class FileNode extends RealNode {
/// Constructs a new [FileNode] as a child of the specified [parent].
- FileNode(DirectoryNode parent) : super(parent);
+ FileNode(DirectoryNode super.parent);
/// File contents in bytes.
Uint8List get content => _content;
@@ -251,7 +250,7 @@
/// Appends the specified bytes to the end of this node's [content].
void write(List<int> bytes) {
- Uint8List existing = _content;
+ var existing = _content;
_content = Uint8List(existing.length + bytes.length);
_content.setRange(0, existing.length, existing);
_content.setRange(existing.length, _content.length, bytes);
@@ -286,9 +285,7 @@
class LinkNode extends Node {
/// Constructs a new [LinkNode] as a child of the specified [parent] and
/// linking to the specified [target] path.
- LinkNode(DirectoryNode parent, this.target)
- : assert(target.isNotEmpty),
- super(parent);
+ LinkNode(DirectoryNode super.parent, this.target) : assert(target.isNotEmpty);
/// The path to which this link points.
String target;
@@ -309,7 +306,7 @@
Node? Function(DirectoryNode parent, String childName, Node? child)?
tailVisitor,
}) {
- Node? referent = fs.findNode(
+ var referent = fs.findNode(
target,
reference: this,
segmentVisitor: (
@@ -349,7 +346,7 @@
}
_reentrant = true;
try {
- Node? node = referentOrNull;
+ var node = referentOrNull;
return node == null ? MemoryFileStat.notFound : node.stat;
} finally {
_reentrant = false;
diff --git a/pkgs/file/lib/src/backends/memory/operations.dart b/pkgs/file/lib/src/backends/memory/operations.dart
index 9fc7462..57d118b 100644
--- a/pkgs/file/lib/src/backends/memory/operations.dart
+++ b/pkgs/file/lib/src/backends/memory/operations.dart
@@ -2,6 +2,8 @@
// for 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: comment_references
+
/// A file system operation used by the [MemoryFileSytem] to allow
/// tests to insert errors for certain operations.
///
@@ -64,23 +66,15 @@
@override
String toString() {
- switch (_value) {
- case 0:
- return 'FileSystemOp.read';
- case 1:
- return 'FileSystemOp.write';
- case 2:
- return 'FileSystemOp.delete';
- case 3:
- return 'FileSystemOp.create';
- case 4:
- return 'FileSystemOp.open';
- case 5:
- return 'FileSystemOp.copy';
- case 6:
- return 'FileSystemOp.exists';
- default:
- throw StateError('Invalid FileSytemOp type: $this');
- }
+ return switch (_value) {
+ 0 => 'FileSystemOp.read',
+ 1 => 'FileSystemOp.write',
+ 2 => 'FileSystemOp.delete',
+ 3 => 'FileSystemOp.create',
+ 4 => 'FileSystemOp.open',
+ 5 => 'FileSystemOp.copy',
+ 6 => 'FileSystemOp.exists',
+ _ => throw StateError('Invalid FileSytemOp type: $this')
+ };
}
}
diff --git a/pkgs/file/lib/src/backends/memory/style.dart b/pkgs/file/lib/src/backends/memory/style.dart
index 701c9d0..f4bd33f 100644
--- a/pkgs/file/lib/src/backends/memory/style.dart
+++ b/pkgs/file/lib/src/backends/memory/style.dart
@@ -2,9 +2,10 @@
// for 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:file/file.dart';
import 'package:path/path.dart' as p;
+import '../../interface.dart';
+
/// Class that represents the path style that a memory file system should
/// adopt.
///
diff --git a/pkgs/file/lib/src/backends/memory/utils.dart b/pkgs/file/lib/src/backends/memory/utils.dart
index eec9980..aa24cfb 100644
--- a/pkgs/file/lib/src/backends/memory/utils.dart
+++ b/pkgs/file/lib/src/backends/memory/utils.dart
@@ -2,20 +2,19 @@
// for 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:file/file.dart';
-import 'package:file/src/common.dart' as common;
-import 'package:file/src/io.dart' as io;
-
+import '../../common.dart' as common;
+import '../../interface.dart';
+import '../../io.dart' as io;
import 'common.dart';
import 'node.dart';
-/// Checks if `node.type` returns [io.FileSystemEntityType.FILE].
+/// Checks if `node.type` returns [io.FileSystemEntityType.file].
bool isFile(Node? node) => node?.type == io.FileSystemEntityType.file;
-/// Checks if `node.type` returns [io.FileSystemEntityType.DIRECTORY].
+/// Checks if `node.type` returns [io.FileSystemEntityType.directory].
bool isDirectory(Node? node) => node?.type == io.FileSystemEntityType.directory;
-/// Checks if `node.type` returns [io.FileSystemEntityType.LINK].
+/// Checks if `node.type` returns [io.FileSystemEntityType.link].
bool isLink(Node? node) => node?.type == io.FileSystemEntityType.link;
/// Validator function that is expected to throw a [FileSystemException] if
@@ -86,7 +85,7 @@
tailVisitor,
}) {
// Record a breadcrumb trail to guard against symlink loops.
- Set<LinkNode> breadcrumbs = <LinkNode>{};
+ var breadcrumbs = <LinkNode>{};
Node node = link;
while (isLink(node)) {
diff --git a/pkgs/file/lib/src/forwarding/forwarding_directory.dart b/pkgs/file/lib/src/forwarding/forwarding_directory.dart
index dba0c8e..ad1c548 100644
--- a/pkgs/file/lib/src/forwarding/forwarding_directory.dart
+++ b/pkgs/file/lib/src/forwarding/forwarding_directory.dart
@@ -2,8 +2,9 @@
// for 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:file/src/io.dart' as io;
-import 'package:file/file.dart';
+import '../forwarding.dart';
+import '../interface.dart';
+import '../io.dart' as io;
/// A directory that forwards all methods and properties to a delegate.
mixin ForwardingDirectory<T extends Directory>
diff --git a/pkgs/file/lib/src/forwarding/forwarding_file.dart b/pkgs/file/lib/src/forwarding/forwarding_file.dart
index 49c211d..d6cfe3b 100644
--- a/pkgs/file/lib/src/forwarding/forwarding_file.dart
+++ b/pkgs/file/lib/src/forwarding/forwarding_file.dart
@@ -5,8 +5,9 @@
import 'dart:convert';
import 'dart:typed_data';
-import 'package:file/src/io.dart' as io;
-import 'package:file/file.dart';
+import '../forwarding.dart';
+import '../interface.dart';
+import '../io.dart' as io;
/// A file that forwards all methods and properties to a delegate.
mixin ForwardingFile
diff --git a/pkgs/file/lib/src/forwarding/forwarding_file_system.dart b/pkgs/file/lib/src/forwarding/forwarding_file_system.dart
index d864db9..885fdb6 100644
--- a/pkgs/file/lib/src/forwarding/forwarding_file_system.dart
+++ b/pkgs/file/lib/src/forwarding/forwarding_file_system.dart
@@ -2,11 +2,12 @@
// for 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:file/src/io.dart' as io;
-import 'package:file/file.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
+import '../interface.dart';
+import '../io.dart' as io;
+
/// A file system that forwards all methods and properties to a delegate.
abstract class ForwardingFileSystem extends FileSystem {
/// Creates a new [ForwardingFileSystem] that forwards all methods and
diff --git a/pkgs/file/lib/src/forwarding/forwarding_file_system_entity.dart b/pkgs/file/lib/src/forwarding/forwarding_file_system_entity.dart
index 3c41b39..1c0628e 100644
--- a/pkgs/file/lib/src/forwarding/forwarding_file_system_entity.dart
+++ b/pkgs/file/lib/src/forwarding/forwarding_file_system_entity.dart
@@ -2,10 +2,11 @@
// for 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:file/src/io.dart' as io;
-import 'package:file/file.dart';
import 'package:meta/meta.dart';
+import '../interface.dart';
+import '../io.dart' as io;
+
/// A file system entity that forwards all methods and properties to a delegate.
abstract class ForwardingFileSystemEntity<T extends FileSystemEntity,
D extends io.FileSystemEntity> implements FileSystemEntity {
diff --git a/pkgs/file/lib/src/forwarding/forwarding_link.dart b/pkgs/file/lib/src/forwarding/forwarding_link.dart
index 7a60ecb..915e710 100644
--- a/pkgs/file/lib/src/forwarding/forwarding_link.dart
+++ b/pkgs/file/lib/src/forwarding/forwarding_link.dart
@@ -2,8 +2,9 @@
// for 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:file/src/io.dart' as io;
-import 'package:file/file.dart';
+import '../forwarding.dart';
+import '../interface.dart';
+import '../io.dart' as io;
/// A link that forwards all methods and properties to a delegate.
mixin ForwardingLink
diff --git a/pkgs/file/lib/src/forwarding/forwarding_random_access_file.dart b/pkgs/file/lib/src/forwarding/forwarding_random_access_file.dart
index 9dd4079..3847b91 100644
--- a/pkgs/file/lib/src/forwarding/forwarding_random_access_file.dart
+++ b/pkgs/file/lib/src/forwarding/forwarding_random_access_file.dart
@@ -5,11 +5,12 @@
import 'dart:convert';
import 'dart:typed_data';
-import 'package:file/src/io.dart' as io;
import 'package:meta/meta.dart';
-/// A [RandomAccessFile] implementation that forwards all methods and properties
-/// to a delegate.
+import '../io.dart' as io;
+
+/// A [io.RandomAccessFile] implementation that forwards all methods and
+/// properties to a delegate.
mixin ForwardingRandomAccessFile implements io.RandomAccessFile {
/// The entity to which this entity will forward all methods and properties.
@protected
diff --git a/pkgs/file/lib/src/interface.dart b/pkgs/file/lib/src/interface.dart
index 4662e35..d9b7ed5 100644
--- a/pkgs/file/lib/src/interface.dart
+++ b/pkgs/file/lib/src/interface.dart
@@ -2,8 +2,6 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
-library file.src.interface;
-
export 'interface/directory.dart';
export 'interface/error_codes.dart';
export 'interface/file.dart';
diff --git a/pkgs/file/lib/src/interface/error_codes.dart b/pkgs/file/lib/src/interface/error_codes.dart
index 8943538..4836b56 100644
--- a/pkgs/file/lib/src/interface/error_codes.dart
+++ b/pkgs/file/lib/src/interface/error_codes.dart
@@ -168,7 +168,7 @@
static int get EXDEV => _platform((_Codes codes) => codes.exdev);
static int _platform(int Function(_Codes codes) getCode) {
- _Codes codes = (_platforms[operatingSystem] ?? _platforms['linux'])!;
+ var codes = (_platforms[operatingSystem] ?? _platforms['linux'])!;
return getCode(codes);
}
}
diff --git a/pkgs/file/lib/src/interface/file_system.dart b/pkgs/file/lib/src/interface/file_system.dart
index ecc01a8..2d4e4aa 100644
--- a/pkgs/file/lib/src/interface/file_system.dart
+++ b/pkgs/file/lib/src/interface/file_system.dart
@@ -6,7 +6,6 @@
import 'package:path/path.dart' as p;
import '../io.dart' as io;
-
import 'directory.dart';
import 'file.dart';
import 'file_system_entity.dart';
@@ -99,9 +98,9 @@
bool get isWatchSupported;
/// Finds the type of file system object that a [path] points to. Returns
- /// a Future<FileSystemEntityType> that completes with the result.
+ /// a `Future<FileSystemEntityType>` that completes with the result.
///
- /// [io.FileSystemEntityType.LINK] will only be returned if [followLinks] is
+ /// [io.FileSystemEntityType.link] will only be returned if [followLinks] is
/// `false`, and [path] points to a link
///
/// If the [path] does not point to a file system object or an error occurs
@@ -111,37 +110,38 @@
/// Syncronously finds the type of file system object that a [path] points
/// to. Returns a [io.FileSystemEntityType].
///
- /// [io.FileSystemEntityType.LINK] will only be returned if [followLinks] is
+ /// [io.FileSystemEntityType.link] will only be returned if [followLinks] is
/// `false`, and [path] points to a link
///
/// If the [path] does not point to a file system object or an error occurs
/// then [io.FileSystemEntityType.notFound] is returned.
io.FileSystemEntityType typeSync(String path, {bool followLinks = true});
- /// Checks if [`type(path)`](type) returns [io.FileSystemEntityType.FILE].
+ /// Checks if [`type(path)`](type) returns [io.FileSystemEntityType.file].
Future<bool> isFile(String path) async =>
await type(path) == io.FileSystemEntityType.file;
/// Synchronously checks if [`type(path)`](type) returns
- /// [io.FileSystemEntityType.FILE].
+ /// [io.FileSystemEntityType.file].
bool isFileSync(String path) =>
typeSync(path) == io.FileSystemEntityType.file;
- /// Checks if [`type(path)`](type) returns [io.FileSystemEntityType.DIRECTORY].
+ /// Checks if [`type(path)`](type) returns
+ /// [io.FileSystemEntityType.directory].
Future<bool> isDirectory(String path) async =>
await type(path) == io.FileSystemEntityType.directory;
/// Synchronously checks if [`type(path)`](type) returns
- /// [io.FileSystemEntityType.DIRECTORY].
+ /// [io.FileSystemEntityType.directory].
bool isDirectorySync(String path) =>
typeSync(path) == io.FileSystemEntityType.directory;
- /// Checks if [`type(path)`](type) returns [io.FileSystemEntityType.LINK].
+ /// Checks if [`type(path)`](type) returns [io.FileSystemEntityType.link].
Future<bool> isLink(String path) async =>
await type(path, followLinks: false) == io.FileSystemEntityType.link;
/// Synchronously checks if [`type(path)`](type) returns
- /// [io.FileSystemEntityType.LINK].
+ /// [io.FileSystemEntityType.link].
bool isLinkSync(String path) =>
typeSync(path, followLinks: false) == io.FileSystemEntityType.link;
diff --git a/pkgs/file/lib/src/io.dart b/pkgs/file/lib/src/io.dart
index 9d57e78..28c1d6d 100644
--- a/pkgs/file/lib/src/io.dart
+++ b/pkgs/file/lib/src/io.dart
@@ -8,6 +8,8 @@
/// the `file` package. The `file` package re-exports these interfaces (or in
/// some cases, implementations of these interfaces by the same name), so this
/// file need not be exposes publicly and exists for internal use only.
+library;
+
export 'dart:io'
show
Directory,
diff --git a/pkgs/file/pubspec.yaml b/pkgs/file/pubspec.yaml
index 5de5d37..0ad65b0 100644
--- a/pkgs/file/pubspec.yaml
+++ b/pkgs/file/pubspec.yaml
@@ -1,5 +1,5 @@
name: file
-version: 7.0.1
+version: 7.0.2-wip
description: A pluggable, mockable file system abstraction for Dart.
repository: https://github.com/dart-lang/tools/tree/main/pkgs/file
issue_tracker: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Afile
@@ -12,6 +12,10 @@
path: ^1.8.3
dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
file_testing: ^3.0.0
- lints: ^2.0.1
test: ^1.23.1
+
+dependency_overrides:
+ file_testing:
+ path: ../file_testing
diff --git a/pkgs/file/test/chroot_test.dart b/pkgs/file/test/chroot_test.dart
index 6c34ff2..cf23f47 100644
--- a/pkgs/file/test/chroot_test.dart
+++ b/pkgs/file/test/chroot_test.dart
@@ -3,6 +3,8 @@
// BSD-style license that can be found in the LICENSE file.
@TestOn('vm')
+library;
+
import 'dart:io' as io;
import 'package:file/chroot.dart';
@@ -17,14 +19,15 @@
void main() {
group('ChrootFileSystem', () {
ChrootFileSystem createMemoryBackedChrootFileSystem() {
- MemoryFileSystem fs = MemoryFileSystem();
+ var fs = MemoryFileSystem();
fs.directory('/tmp').createSync();
return ChrootFileSystem(fs, '/tmp');
}
// TODO(jamesderlin): Make ChrootFile.openSync return a delegating
// RandomAccessFile that uses the chroot'd path.
- List<String> skipCommon = <String>[
+ var skipCommon = <String>[
+ // ignore: lines_longer_than_80_chars
'File > open > .* > RandomAccessFile > read > openReadHandleDoesNotChange',
'File > open > .* > RandomAccessFile > openWriteHandleDoesNotChange',
];
@@ -137,6 +140,7 @@
test('referencesRootEntityForJailbreakPath', () {
mem.file('/foo').createSync();
dynamic f = fs.file('../foo');
+ // ignore: avoid_dynamic_calls
expect(f.delegate.path, '/tmp/foo');
});
});
@@ -151,7 +155,7 @@
group('copy', () {
test('copiesToRootDirectoryIfDestinationIsJailbreakPath', () {
- File f = fs.file('/foo')..createSync();
+ var f = fs.file('/foo')..createSync();
f.copySync('../bar');
expect(mem.file('/bar'), isNot(exists));
expect(mem.file('/tmp/bar'), exists);
diff --git a/pkgs/file/test/common_tests.dart b/pkgs/file/test/common_tests.dart
index 6028c77..491d4f9 100644
--- a/pkgs/file/test/common_tests.dart
+++ b/pkgs/file/test/common_tests.dart
@@ -3,6 +3,8 @@
// BSD-style license that can be found in the LICENSE file.
@TestOn('vm')
+library;
+
import 'dart:async';
import 'dart:convert';
import 'dart:io' as io;
@@ -10,8 +12,8 @@
import 'package:file/file.dart';
import 'package:file_testing/file_testing.dart';
import 'package:path/path.dart' as p;
-import 'package:test/test.dart';
import 'package:test/test.dart' as testpkg show group, setUp, tearDown, test;
+import 'package:test/test.dart';
import 'utils.dart';
@@ -54,7 +56,7 @@
List<String> skip = const <String>[],
FileSystemGenerator? replay,
}) {
- RootPathGenerator? rootfn = root;
+ var rootfn = root;
group('common', () {
late FileSystemGenerator createFs;
@@ -62,7 +64,7 @@
late List<SetUpTearDown> tearDowns;
late FileSystem fs;
late String root;
- List<String> stack = <String>[];
+ var stack = <String>[];
void skipIfNecessary(String description, void Function() callback) {
stack.add(description);
@@ -105,7 +107,7 @@
testpkg.setUp(() async {
await Future.forEach(setUps, (SetUpTearDown setUp) => setUp());
await body();
- for (SetUpTearDown tearDown in tearDowns) {
+ for (var tearDown in tearDowns) {
await tearDown();
}
createFs = replay;
@@ -115,7 +117,7 @@
testpkg.test(description, body, skip: skip);
testpkg.tearDown(() async {
- for (SetUpTearDown tearDown in tearDowns) {
+ for (var tearDown in tearDowns) {
await tearDown();
}
});
@@ -126,13 +128,13 @@
/// Returns [path] prefixed by the [root] namespace.
/// This is only intended for absolute paths.
String ns(String path) {
- p.Context posix = p.Context(style: p.Style.posix);
- List<String> parts = posix.split(path);
+ var posix = p.Context(style: p.Style.posix);
+ var parts = posix.split(path);
parts[0] = root;
path = fs.path.joinAll(parts);
- String rootPrefix = fs.path.rootPrefix(path);
+ var rootPrefix = fs.path.rootPrefix(path);
assert(rootPrefix.isNotEmpty);
- String result = root == rootPrefix
+ var result = root == rootPrefix
? path
: (path == rootPrefix
? root
@@ -160,7 +162,7 @@
test('succeedsWithUriArgument', () {
fs.directory(ns('/foo')).createSync();
- Uri uri = fs.path.toUri(ns('/foo'));
+ var uri = fs.path.toUri(ns('/foo'));
expect(fs.directory(uri), exists);
});
@@ -173,11 +175,11 @@
});
// Fails due to
- // https://github.com/google/file.dart/issues/112
+ // https://github.com/dart-lang/tools/issues/632
test('considersBothSlashesEquivalent', () {
fs.directory(r'foo\bar_dir').createSync(recursive: true);
expect(fs.directory(r'foo/bar_dir'), exists);
- }, skip: 'Fails due to https://github.com/google/file.dart/issues/112');
+ }, skip: 'Fails due to https://github.com/dart-lang/tools/issues/632');
});
group('file', () {
@@ -191,7 +193,7 @@
test('succeedsWithUriArgument', () {
fs.file(ns('/foo')).createSync();
- Uri uri = fs.path.toUri(ns('/foo'));
+ var uri = fs.path.toUri(ns('/foo'));
expect(fs.file(uri), exists);
});
@@ -204,11 +206,11 @@
});
// Fails due to
- // https://github.com/google/file.dart/issues/112
+ // https://github.com/dart-lang/tools/issues/632
test('considersBothSlashesEquivalent', () {
fs.file(r'foo\bar_file').createSync(recursive: true);
expect(fs.file(r'foo/bar_file'), exists);
- }, skip: 'Fails due to https://github.com/google/file.dart/issues/112');
+ }, skip: 'Fails due to https://github.com/dart-lang/tools/issues/632');
});
group('link', () {
@@ -223,7 +225,7 @@
test('succeedsWithUriArgument', () {
fs.file(ns('/foo')).createSync();
fs.link(ns('/bar')).createSync(ns('/foo'));
- Uri uri = fs.path.toUri(ns('/bar'));
+ var uri = fs.path.toUri(ns('/bar'));
expect(fs.link(uri), exists);
});
@@ -248,7 +250,7 @@
group('systemTempDirectory', () {
test('existsAsDirectory', () {
- Directory tmp = fs.systemTempDirectory;
+ var tmp = fs.systemTempDirectory;
expect(tmp, isDirectory);
expect(tmp, exists);
});
@@ -318,7 +320,7 @@
test('staysAtRootIfSetToParentOfRoot', () {
fs.currentDirectory =
List<String>.filled(20, '..').join(fs.path.separator);
- String cwd = fs.currentDirectory.path;
+ var cwd = fs.currentDirectory.path;
expect(cwd, fs.path.rootPrefix(cwd));
});
@@ -371,36 +373,36 @@
group('stat', () {
test('isNotFoundForEmptyPath', () {
- FileStat stat = fs.statSync('');
+ var stat = fs.statSync('');
expect(stat.type, FileSystemEntityType.notFound);
});
test('isNotFoundForPathToNonExistentEntityAtTail', () {
- FileStat stat = fs.statSync(ns('/foo'));
+ var stat = fs.statSync(ns('/foo'));
expect(stat.type, FileSystemEntityType.notFound);
});
test('isNotFoundForPathToNonExistentEntityInTraversal', () {
- FileStat stat = fs.statSync(ns('/foo/bar'));
+ var stat = fs.statSync(ns('/foo/bar'));
expect(stat.type, FileSystemEntityType.notFound);
});
test('isDirectoryForDirectory', () {
fs.directory(ns('/foo')).createSync();
- FileStat stat = fs.statSync(ns('/foo'));
+ var stat = fs.statSync(ns('/foo'));
expect(stat.type, FileSystemEntityType.directory);
});
test('isFileForFile', () {
fs.file(ns('/foo')).createSync();
- FileStat stat = fs.statSync(ns('/foo'));
+ var stat = fs.statSync(ns('/foo'));
expect(stat.type, FileSystemEntityType.file);
});
test('isFileForLinkToFile', () {
fs.file(ns('/foo')).createSync();
fs.link(ns('/bar')).createSync(ns('/foo'));
- FileStat stat = fs.statSync(ns('/bar'));
+ var stat = fs.statSync(ns('/bar'));
expect(stat.type, FileSystemEntityType.file);
});
@@ -408,7 +410,7 @@
fs.link(ns('/foo')).createSync(ns('/bar'));
fs.link(ns('/bar')).createSync(ns('/baz'));
fs.link(ns('/baz')).createSync(ns('/foo'));
- FileStat stat = fs.statSync(ns('/foo'));
+ var stat = fs.statSync(ns('/foo'));
expect(stat.type, FileSystemEntityType.notFound);
});
});
@@ -454,18 +456,18 @@
group('type', () {
test('isFileForFile', () {
fs.file(ns('/foo')).createSync();
- FileSystemEntityType type = fs.typeSync(ns('/foo'));
+ var type = fs.typeSync(ns('/foo'));
expect(type, FileSystemEntityType.file);
});
test('isDirectoryForDirectory', () {
fs.directory(ns('/foo')).createSync();
- FileSystemEntityType type = fs.typeSync(ns('/foo'));
+ var type = fs.typeSync(ns('/foo'));
expect(type, FileSystemEntityType.directory);
});
test('isDirectoryForAncestorOfRoot', () {
- FileSystemEntityType type = fs
+ var type = fs
.typeSync(List<String>.filled(20, '..').join(fs.path.separator));
expect(type, FileSystemEntityType.directory);
});
@@ -473,15 +475,14 @@
test('isFileForLinkToFileAndFollowLinksTrue', () {
fs.file(ns('/foo')).createSync();
fs.link(ns('/bar')).createSync(ns('/foo'));
- FileSystemEntityType type = fs.typeSync(ns('/bar'));
+ var type = fs.typeSync(ns('/bar'));
expect(type, FileSystemEntityType.file);
});
test('isLinkForLinkToFileAndFollowLinksFalse', () {
fs.file(ns('/foo')).createSync();
fs.link(ns('/bar')).createSync(ns('/foo'));
- FileSystemEntityType type =
- fs.typeSync(ns('/bar'), followLinks: false);
+ var type = fs.typeSync(ns('/bar'), followLinks: false);
expect(type, FileSystemEntityType.link);
});
@@ -489,17 +490,17 @@
fs.link(ns('/foo')).createSync(ns('/bar'));
fs.link(ns('/bar')).createSync(ns('/baz'));
fs.link(ns('/baz')).createSync(ns('/foo'));
- FileSystemEntityType type = fs.typeSync(ns('/foo'));
+ var type = fs.typeSync(ns('/foo'));
expect(type, FileSystemEntityType.notFound);
});
test('isNotFoundForNoEntityAtTail', () {
- FileSystemEntityType type = fs.typeSync(ns('/foo'));
+ var type = fs.typeSync(ns('/foo'));
expect(type, FileSystemEntityType.notFound);
});
test('isNotFoundForNoDirectoryInTraversal', () {
- FileSystemEntityType type = fs.typeSync(ns('/foo/bar/baz'));
+ var type = fs.typeSync(ns('/foo/bar/baz'));
expect(type, FileSystemEntityType.notFound);
});
});
@@ -676,8 +677,8 @@
});
test('succeedsIfDestinationDoesntExist', () {
- Directory src = fs.directory(ns('/foo'))..createSync();
- Directory dest = src.renameSync(ns('/bar'));
+ var src = fs.directory(ns('/foo'))..createSync();
+ var dest = src.renameSync(ns('/bar'));
expect(dest.path, ns('/bar'));
expect(dest, exists);
});
@@ -686,8 +687,8 @@
'succeedsIfDestinationIsEmptyDirectory',
() {
fs.directory(ns('/bar')).createSync();
- Directory src = fs.directory(ns('/foo'))..createSync();
- Directory dest = src.renameSync(ns('/bar'));
+ var src = fs.directory(ns('/foo'))..createSync();
+ var dest = src.renameSync(ns('/bar'));
expect(src, isNot(exists));
expect(dest, exists);
},
@@ -697,14 +698,14 @@
test('throwsIfDestinationIsFile', () {
fs.file(ns('/bar')).createSync();
- Directory src = fs.directory(ns('/foo'))..createSync();
+ var src = fs.directory(ns('/foo'))..createSync();
expectFileSystemException(ErrorCodes.ENOTDIR, () {
src.renameSync(ns('/bar'));
});
});
test('throwsIfDestinationParentFolderDoesntExist', () {
- Directory src = fs.directory(ns('/foo'))..createSync();
+ var src = fs.directory(ns('/foo'))..createSync();
expectFileSystemException(ErrorCodes.ENOENT, () {
src.renameSync(ns('/bar/baz'));
});
@@ -712,7 +713,7 @@
test('throwsIfDestinationIsNonEmptyDirectory', () {
fs.file(ns('/bar/baz')).createSync(recursive: true);
- Directory src = fs.directory(ns('/foo'))..createSync();
+ var src = fs.directory(ns('/foo'))..createSync();
// The error will be 'Directory not empty' on OS X, but it will be
// 'File exists' on Linux.
expectFileSystemException(
@@ -749,7 +750,7 @@
});
test('throwsIfDestinationIsLinkToNotFound', () {
- Directory src = fs.directory(ns('/foo'))..createSync();
+ var src = fs.directory(ns('/foo'))..createSync();
fs.link(ns('/bar')).createSync(ns('/baz'));
expectFileSystemException(ErrorCodes.ENOTDIR, () {
src.renameSync(ns('/bar'));
@@ -757,7 +758,7 @@
});
test('throwsIfDestinationIsLinkToEmptyDirectory', () {
- Directory src = fs.directory(ns('/foo'))..createSync();
+ var src = fs.directory(ns('/foo'))..createSync();
fs.directory(ns('/bar')).createSync();
fs.link(ns('/baz')).createSync(ns('/bar'));
expectFileSystemException(ErrorCodes.ENOTDIR, () {
@@ -766,7 +767,7 @@
});
test('succeedsIfDestinationIsInDifferentDirectory', () {
- Directory src = fs.directory(ns('/foo'))..createSync();
+ var src = fs.directory(ns('/foo'))..createSync();
fs.directory(ns('/bar')).createSync();
src.renameSync(ns('/bar/baz'));
expect(fs.typeSync(ns('/foo')), FileSystemEntityType.notFound);
@@ -790,24 +791,24 @@
group('delete', () {
test('returnsCovariantType', () async {
- Directory dir = fs.directory(ns('/foo'))..createSync();
+ var dir = fs.directory(ns('/foo'))..createSync();
expect(await dir.delete(), isDirectory);
});
test('succeedsIfEmptyDirectoryExistsAndRecursiveFalse', () {
- Directory dir = fs.directory(ns('/foo'))..createSync();
+ var dir = fs.directory(ns('/foo'))..createSync();
dir.deleteSync();
expect(dir, isNot(exists));
});
test('succeedsIfEmptyDirectoryExistsAndRecursiveTrue', () {
- Directory dir = fs.directory(ns('/foo'))..createSync();
+ var dir = fs.directory(ns('/foo'))..createSync();
dir.deleteSync(recursive: true);
expect(dir, isNot(exists));
});
test('throwsIfNonEmptyDirectoryExistsAndRecursiveFalse', () {
- Directory dir = fs.directory(ns('/foo'))..createSync();
+ var dir = fs.directory(ns('/foo'))..createSync();
fs.file(ns('/foo/bar')).createSync();
expectFileSystemException(ErrorCodes.ENOTEMPTY, () {
dir.deleteSync();
@@ -815,7 +816,7 @@
});
test('succeedsIfNonEmptyDirectoryExistsAndRecursiveTrue', () {
- Directory dir = fs.directory(ns('/foo'))..createSync();
+ var dir = fs.directory(ns('/foo'))..createSync();
fs.file(ns('/foo/bar')).createSync();
dir.deleteSync(recursive: true);
expect(fs.directory(ns('/foo')), isNot(exists));
@@ -997,7 +998,7 @@
test('handlesParentAndThisFolderReferences', () {
fs.directory(ns('/foo/bar/baz')).createSync(recursive: true);
fs.link(ns('/foo/bar/baz/qux')).createSync(fs.path.join('..', '..'));
- String resolved = fs
+ var resolved = fs
.directory(ns('/foo/./bar/baz/../baz/qux/bar'))
.resolveSymbolicLinksSync();
expect(resolved, ns('/foo/bar'));
@@ -1015,7 +1016,7 @@
.createSync(fs.path.join('..', '..', 'qux'), recursive: true);
fs.link(ns('/qux')).createSync('quux');
fs.link(ns('/quux/quuz')).createSync(ns('/foo'), recursive: true);
- String resolved = fs
+ var resolved = fs
.directory(ns('/foo//bar/./baz/quuz/bar/..///bar/baz/'))
.resolveSymbolicLinksSync();
expect(resolved, ns('/quux'));
@@ -1069,29 +1070,29 @@
test('resolvesNameCollisions', () {
fs.directory(ns('/foo/bar')).createSync(recursive: true);
- Directory tmp = fs.directory(ns('/foo')).createTempSync('bar');
+ var tmp = fs.directory(ns('/foo')).createTempSync('bar');
expect(tmp.path,
allOf(isNot(ns('/foo/bar')), startsWith(ns('/foo/bar'))));
});
test('succeedsWithoutPrefix', () {
- Directory dir = fs.directory(ns('/foo'))..createSync();
+ var dir = fs.directory(ns('/foo'))..createSync();
expect(dir.createTempSync().path, startsWith(ns('/foo/')));
});
test('succeedsWithPrefix', () {
- Directory dir = fs.directory(ns('/foo'))..createSync();
+ var dir = fs.directory(ns('/foo'))..createSync();
expect(dir.createTempSync('bar').path, startsWith(ns('/foo/bar')));
});
test('succeedsWithNestedPathPrefixThatExists', () {
fs.directory(ns('/foo/bar')).createSync(recursive: true);
- Directory tmp = fs.directory(ns('/foo')).createTempSync('bar/baz');
+ var tmp = fs.directory(ns('/foo')).createTempSync('bar/baz');
expect(tmp.path, startsWith(ns('/foo/bar/baz')));
});
test('throwsWithNestedPathPrefixThatDoesntExist', () {
- Directory dir = fs.directory(ns('/foo'))..createSync();
+ var dir = fs.directory(ns('/foo'))..createSync();
expectFileSystemException(ErrorCodes.ENOENT, () {
dir.createTempSync('bar/baz');
});
@@ -1123,7 +1124,7 @@
});
test('returnsEmptyListForEmptyDirectory', () {
- Directory empty = fs.directory(ns('/bar'))..createSync();
+ var empty = fs.directory(ns('/bar'))..createSync();
expect(empty.listSync(), isEmpty);
});
@@ -1134,7 +1135,7 @@
});
test('returnsLinkObjectsIfFollowLinksFalse', () {
- List<FileSystemEntity> list = dir.listSync(followLinks: false);
+ var list = dir.listSync(followLinks: false);
expect(list, hasLength(3));
expect(list, contains(allOf(isFile, hasPath(ns('/foo/bar')))));
expect(list, contains(allOf(isDirectory, hasPath(ns('/foo/baz')))));
@@ -1142,7 +1143,7 @@
});
test('followsLinksIfFollowLinksTrue', () {
- List<FileSystemEntity> list = dir.listSync();
+ var list = dir.listSync();
expect(list, hasLength(3));
expect(list, contains(allOf(isFile, hasPath(ns('/foo/bar')))));
expect(list, contains(allOf(isDirectory, hasPath(ns('/foo/baz')))));
@@ -1189,8 +1190,7 @@
test('childEntriesNotNormalized', () {
dir = fs.directory(ns('/bar/baz'))..createSync(recursive: true);
fs.file(ns('/bar/baz/qux')).createSync();
- List<FileSystemEntity> list =
- fs.directory(ns('/bar//../bar/./baz')).listSync();
+ var list = fs.directory(ns('/bar//../bar/./baz')).listSync();
expect(list, hasLength(1));
expect(list[0], allOf(isFile, hasPath(ns('/bar//../bar/./baz/qux'))));
});
@@ -1198,9 +1198,8 @@
test('symlinksToNotFoundAlwaysReturnedAsLinks', () {
dir = fs.directory(ns('/bar'))..createSync();
fs.link(ns('/bar/baz')).createSync('qux');
- for (bool followLinks in const <bool>[true, false]) {
- List<FileSystemEntity> list =
- dir.listSync(followLinks: followLinks);
+ for (var followLinks in const <bool>[true, false]) {
+ var list = dir.listSync(followLinks: followLinks);
expect(list, hasLength(1));
expect(list[0], allOf(isLink, hasPath(ns('/bar/baz'))));
}
@@ -1208,7 +1207,7 @@
});
test('childEntities', () {
- Directory dir = fs.directory(ns('/foo'))..createSync();
+ var dir = fs.directory(ns('/foo'))..createSync();
dir.childDirectory('bar').createSync();
dir.childFile('baz').createSync();
dir.childLink('qux').createSync('bar');
@@ -1321,22 +1320,22 @@
});
test('succeedsIfDestinationDoesntExistAtTail', () {
- File src = fs.file(ns('/foo'))..createSync();
- File dest = src.renameSync(ns('/bar'));
+ var src = fs.file(ns('/foo'))..createSync();
+ var dest = src.renameSync(ns('/bar'));
expect(fs.file(ns('/foo')), isNot(exists));
expect(fs.file(ns('/bar')), exists);
expect(dest.path, ns('/bar'));
});
test('throwsIfDestinationDoesntExistViaTraversal', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
expectFileSystemException(ErrorCodes.ENOENT, () {
f.renameSync(ns('/bar/baz'));
});
});
test('succeedsIfDestinationExistsAsFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
fs.file(ns('/bar')).createSync();
f.renameSync(ns('/bar'));
expect(fs.file(ns('/foo')), isNot(exists));
@@ -1344,7 +1343,7 @@
});
test('throwsIfDestinationExistsAsDirectory', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
fs.directory(ns('/bar')).createSync();
expectFileSystemException(ErrorCodes.EISDIR, () {
f.renameSync(ns('/bar'));
@@ -1352,7 +1351,7 @@
});
test('succeedsIfDestinationExistsAsLinkToFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
fs.file(ns('/bar')).createSync();
fs.link(ns('/baz')).createSync(ns('/bar'));
f.renameSync(ns('/baz'));
@@ -1364,7 +1363,7 @@
});
test('throwsIfDestinationExistsAsLinkToDirectory', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
fs.directory(ns('/bar')).createSync();
fs.link(ns('/baz')).createSync(ns('/bar'));
expectFileSystemException(ErrorCodes.EISDIR, () {
@@ -1373,7 +1372,7 @@
});
test('succeedsIfDestinationExistsAsLinkToNotFound', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
fs.link(ns('/bar')).createSync(ns('/baz'));
f.renameSync(ns('/bar'));
expect(fs.typeSync(ns('/foo')), FileSystemEntityType.notFound);
@@ -1429,7 +1428,7 @@
});
test('succeedsIfDestinationDoesntExistAtTail', () {
- File f = fs.file(ns('/foo'))
+ var f = fs.file(ns('/foo'))
..createSync()
..writeAsStringSync('foo');
f.copySync(ns('/bar'));
@@ -1439,14 +1438,14 @@
});
test('throwsIfDestinationDoesntExistViaTraversal', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
expectFileSystemException(ErrorCodes.ENOENT, () {
f.copySync(ns('/bar/baz'));
});
});
test('succeedsIfDestinationExistsAsFile', () {
- File f = fs.file(ns('/foo'))
+ var f = fs.file(ns('/foo'))
..createSync()
..writeAsStringSync('foo');
fs.file(ns('/bar'))
@@ -1460,7 +1459,7 @@
});
test('throwsIfDestinationExistsAsDirectory', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
fs.directory(ns('/bar')).createSync();
expectFileSystemException(ErrorCodes.EISDIR, () {
f.copySync(ns('/bar'));
@@ -1468,7 +1467,7 @@
});
test('succeedsIfDestinationExistsAsLinkToFile', () {
- File f = fs.file(ns('/foo'))
+ var f = fs.file(ns('/foo'))
..createSync()
..writeAsStringSync('foo');
fs.file(ns('/bar'))
@@ -1487,7 +1486,7 @@
}, skip: io.Platform.isWindows /* No links on Windows */);
test('throwsIfDestinationExistsAsLinkToDirectory', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
fs.directory(ns('/bar')).createSync();
fs.link(ns('/baz')).createSync(ns('/bar'));
expectFileSystemException(ErrorCodes.EISDIR, () {
@@ -1525,7 +1524,7 @@
});
test('succeedsIfDestinationIsInDifferentDirectoryThanSource', () {
- File f = fs.file(ns('/foo/bar'))
+ var f = fs.file(ns('/foo/bar'))
..createSync(recursive: true)
..writeAsStringSync('foo');
fs.directory(ns('/baz')).createSync();
@@ -1587,12 +1586,12 @@
});
test('returnsZeroForNewlyCreatedFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
expect(f.lengthSync(), 0);
});
test('writeNBytesReturnsLengthN', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsBytesSync(<int>[1, 2, 3, 4], flush: true);
expect(f.lengthSync(), 4);
});
@@ -1616,10 +1615,10 @@
group('lastAccessed', () {
test('isNowForNewlyCreatedFile', () {
- DateTime before = downstairs();
- File f = fs.file(ns('/foo'))..createSync();
- DateTime after = ceil();
- DateTime accessed = f.lastAccessedSync();
+ var before = downstairs();
+ var f = fs.file(ns('/foo'))..createSync();
+ var after = ceil();
+ var accessed = f.lastAccessedSync();
expect(accessed, isSameOrAfter(before));
expect(accessed, isSameOrBefore(after));
});
@@ -1638,18 +1637,18 @@
});
test('succeedsIfExistsAsLinkToFile', () {
- DateTime before = downstairs();
+ var before = downstairs();
fs.file(ns('/foo')).createSync();
fs.link(ns('/bar')).createSync(ns('/foo'));
- DateTime after = ceil();
- DateTime accessed = fs.file(ns('/bar')).lastAccessedSync();
+ var after = ceil();
+ var accessed = fs.file(ns('/bar')).lastAccessedSync();
expect(accessed, isSameOrAfter(before));
expect(accessed, isSameOrBefore(after));
});
});
group('setLastAccessed', () {
- final DateTime time = DateTime(1999);
+ final time = DateTime(1999);
test('throwsIfDoesntExist', () {
expectFileSystemException(ErrorCodes.ENOENT, () {
@@ -1665,13 +1664,13 @@
});
test('succeedsIfExistsAsFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.setLastAccessedSync(time);
expect(fs.file(ns('/foo')).lastAccessedSync(), time);
});
test('succeedsIfExistsAsLinkToFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
fs.link(ns('/bar')).createSync(ns('/foo'));
f.setLastAccessedSync(time);
expect(fs.file(ns('/bar')).lastAccessedSync(), time);
@@ -1680,10 +1679,10 @@
group('lastModified', () {
test('isNowForNewlyCreatedFile', () {
- DateTime before = downstairs();
- File f = fs.file(ns('/foo'))..createSync();
- DateTime after = ceil();
- DateTime modified = f.lastModifiedSync();
+ var before = downstairs();
+ var f = fs.file(ns('/foo'))..createSync();
+ var after = ceil();
+ var modified = f.lastModifiedSync();
expect(modified, isSameOrAfter(before));
expect(modified, isSameOrBefore(after));
});
@@ -1702,18 +1701,18 @@
});
test('succeedsIfExistsAsLinkToFile', () {
- DateTime before = downstairs();
+ var before = downstairs();
fs.file(ns('/foo')).createSync();
fs.link(ns('/bar')).createSync(ns('/foo'));
- DateTime after = ceil();
- DateTime modified = fs.file(ns('/bar')).lastModifiedSync();
+ var after = ceil();
+ var modified = fs.file(ns('/bar')).lastModifiedSync();
expect(modified, isSameOrAfter(before));
expect(modified, isSameOrBefore(after));
});
});
group('setLastModified', () {
- final DateTime time = DateTime(1999);
+ final time = DateTime(1999);
test('throwsIfDoesntExist', () {
expectFileSystemException(ErrorCodes.ENOENT, () {
@@ -1729,13 +1728,13 @@
});
test('succeedsIfExistsAsFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.setLastModifiedSync(time);
expect(fs.file(ns('/foo')).lastModifiedSync(), time);
});
test('succeedsIfExistsAsLinkToFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
fs.link(ns('/bar')).createSync(ns('/foo'));
f.setLastModifiedSync(time);
expect(fs.file(ns('/bar')).lastModifiedSync(), time);
@@ -1752,7 +1751,7 @@
});
} else {
test('createsFileIfDoesntExistAtTail', () {
- RandomAccessFile raf = fs.file(ns('/bar')).openSync(mode: mode);
+ var raf = fs.file(ns('/bar')).openSync(mode: mode);
raf.closeSync();
expect(fs.file(ns('/bar')), exists);
});
@@ -1877,39 +1876,39 @@
});
test('readIntoWithBufferLargerThanContent', () {
- List<int> buffer = List<int>.filled(1024, 0);
- int numRead = raf.readIntoSync(buffer);
+ var buffer = List<int>.filled(1024, 0);
+ var numRead = raf.readIntoSync(buffer);
expect(numRead, 21);
expect(utf8.decode(buffer.sublist(0, 21)),
'pre-existing content\n');
});
test('readIntoWithBufferSmallerThanContent', () {
- List<int> buffer = List<int>.filled(10, 0);
- int numRead = raf.readIntoSync(buffer);
+ var buffer = List<int>.filled(10, 0);
+ var numRead = raf.readIntoSync(buffer);
expect(numRead, 10);
expect(utf8.decode(buffer), 'pre-existi');
});
test('readIntoWithStart', () {
- List<int> buffer = List<int>.filled(10, 0);
- int numRead = raf.readIntoSync(buffer, 2);
+ var buffer = List<int>.filled(10, 0);
+ var numRead = raf.readIntoSync(buffer, 2);
expect(numRead, 8);
expect(utf8.decode(buffer.sublist(2)), 'pre-exis');
});
test('readIntoWithStartAndEnd', () {
- List<int> buffer = List<int>.filled(10, 0);
- int numRead = raf.readIntoSync(buffer, 2, 5);
+ var buffer = List<int>.filled(10, 0);
+ var numRead = raf.readIntoSync(buffer, 2, 5);
expect(numRead, 3);
expect(utf8.decode(buffer.sublist(2, 5)), 'pre');
});
test('openReadHandleDoesNotChange', () {
- final String initial = utf8.decode(raf.readSync(4));
+ final initial = utf8.decode(raf.readSync(4));
expect(initial, 'pre-');
- final File newFile = f.renameSync(ns('/bar'));
- String rest = utf8.decode(raf.readSync(1024));
+ final newFile = f.renameSync(ns('/bar'));
+ var rest = utf8.decode(raf.readSync(1024));
expect(rest, 'existing content\n');
assert(newFile.path != f.path);
@@ -1942,13 +1941,13 @@
});
} else {
test('lengthGrowsAsDataIsWritten', () {
- int lengthBefore = f.lengthSync();
+ var lengthBefore = f.lengthSync();
raf.writeByteSync(0xFACE);
expect(raf.lengthSync(), lengthBefore + 1);
});
test('flush', () {
- int lengthBefore = f.lengthSync();
+ var lengthBefore = f.lengthSync();
raf.writeByteSync(0xFACE);
raf.flushSync();
expect(f.lengthSync(), lengthBefore + 1);
@@ -2009,10 +2008,10 @@
test('openWriteHandleDoesNotChange', () {
raf.writeStringSync('Hello ');
- final File newFile = f.renameSync(ns('/bar'));
+ final newFile = f.renameSync(ns('/bar'));
raf.writeStringSync('world');
- final String contents = newFile.readAsStringSync();
+ final contents = newFile.readAsStringSync();
if (mode == FileMode.write || mode == FileMode.writeOnly) {
expect(contents, 'Hello world');
} else {
@@ -2067,7 +2066,7 @@
});
} else {
test('growsAfterWrite', () {
- int positionBefore = raf.positionSync();
+ var positionBefore = raf.positionSync();
raf.writeStringSync('Hello world');
expect(raf.positionSync(), positionBefore + 11);
});
@@ -2165,42 +2164,42 @@
group('openRead', () {
test('throwsIfDoesntExist', () {
- Stream<List<int>> stream = fs.file(ns('/foo')).openRead();
+ var stream = fs.file(ns('/foo')).openRead();
expect(stream.drain<void>(),
throwsFileSystemException(ErrorCodes.ENOENT));
});
test('succeedsIfExistsAsFile', () async {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsStringSync('Hello world', flush: true);
- Stream<List<int>> stream = f.openRead();
- List<List<int>> data = await stream.toList();
+ var stream = f.openRead();
+ var data = await stream.toList();
expect(data, hasLength(1));
expect(utf8.decode(data[0]), 'Hello world');
});
test('throwsIfExistsAsDirectory', () {
fs.directory(ns('/foo')).createSync();
- Stream<List<int>> stream = fs.file(ns('/foo')).openRead();
+ var stream = fs.file(ns('/foo')).openRead();
expect(stream.drain<void>(),
throwsFileSystemException(ErrorCodes.EISDIR));
});
test('succeedsIfExistsAsLinkToFile', () async {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
fs.link(ns('/bar')).createSync(ns('/foo'));
f.writeAsStringSync('Hello world', flush: true);
- Stream<List<int>> stream = fs.file(ns('/bar')).openRead();
- List<List<int>> data = await stream.toList();
+ var stream = fs.file(ns('/bar')).openRead();
+ var data = await stream.toList();
expect(data, hasLength(1));
expect(utf8.decode(data[0]), 'Hello world');
});
test('respectsStartAndEndParameters', () async {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsStringSync('Hello world', flush: true);
- Stream<List<int>> stream = f.openRead(2);
- List<List<int>> data = await stream.toList();
+ var stream = f.openRead(2);
+ var data = await stream.toList();
expect(data, hasLength(1));
expect(utf8.decode(data[0]), 'llo world');
stream = f.openRead(2, 5);
@@ -2210,24 +2209,24 @@
});
test('throwsIfStartParameterIsNegative', () async {
- File f = fs.file(ns('/foo'))..createSync();
- Stream<List<int>> stream = f.openRead(-2);
+ var f = fs.file(ns('/foo'))..createSync();
+ var stream = f.openRead(-2);
expect(stream.drain<void>(), throwsRangeError);
});
test('stopsAtEndOfFileIfEndParameterIsPastEndOfFile', () async {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsStringSync('Hello world', flush: true);
- Stream<List<int>> stream = f.openRead(2, 1024);
- List<List<int>> data = await stream.toList();
+ var stream = f.openRead(2, 1024);
+ var data = await stream.toList();
expect(data, hasLength(1));
expect(utf8.decode(data[0]), 'llo world');
});
test('providesSingleSubscriptionStream', () async {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsStringSync('Hello world', flush: true);
- Stream<List<int>> stream = f.openRead();
+ var stream = f.openRead();
expect(stream.isBroadcast, isFalse);
await stream.drain<void>();
});
@@ -2237,20 +2236,20 @@
// split across multiple chunks in the [Stream]. However, there
// doesn't seem to be a good way to determine the chunk size used by
// [io.File].
- final List<int> data = List<int>.generate(
+ final data = List<int>.generate(
1024 * 256,
(int index) => index & 0xFF,
growable: false,
);
- final File f = fs.file(ns('/foo'))..createSync();
+ final f = fs.file(ns('/foo'))..createSync();
f.writeAsBytesSync(data, flush: true);
- final Stream<List<int>> stream = f.openRead();
+ final stream = f.openRead();
File? newFile;
List<int>? initialChunk;
- final List<int> remainingChunks = <int>[];
+ final remainingChunks = <int>[];
await for (List<int> chunk in stream) {
if (initialChunk == null) {
@@ -2276,7 +2275,7 @@
test('openReadCompatibleWithUtf8Decoder', () async {
const content = 'Hello world!';
- File file = fs.file(ns('/foo'))
+ var file = fs.file(ns('/foo'))
..createSync()
..writeAsStringSync(content);
expect(
@@ -2315,8 +2314,8 @@
});
test('succeedsIfExistsAsEmptyFile', () async {
- File f = fs.file(ns('/foo'))..createSync();
- IOSink sink = f.openWrite();
+ var f = fs.file(ns('/foo'))..createSync();
+ var sink = f.openWrite();
sink.write('Hello world');
await sink.flush();
await sink.close();
@@ -2326,7 +2325,7 @@
test('succeedsIfExistsAsLinkToFile', () async {
fs.file(ns('/foo')).createSync();
fs.link(ns('/bar')).createSync(ns('/foo'));
- IOSink sink = fs.file(ns('/bar')).openWrite();
+ var sink = fs.file(ns('/bar')).openWrite();
sink.write('Hello world');
await sink.flush();
await sink.close();
@@ -2334,9 +2333,9 @@
});
test('overwritesContentInWriteMode', () async {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsStringSync('Hello');
- IOSink sink = f.openWrite();
+ var sink = f.openWrite();
sink.write('Goodbye');
await sink.flush();
await sink.close();
@@ -2344,9 +2343,9 @@
});
test('appendsContentInAppendMode', () async {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsStringSync('Hello');
- IOSink sink = f.openWrite(mode: FileMode.append);
+ var sink = f.openWrite(mode: FileMode.append);
sink.write('Goodbye');
await sink.flush();
await sink.close();
@@ -2354,12 +2353,12 @@
});
test('openWriteHandleDoesNotChange', () async {
- File f = fs.file(ns('/foo'))..createSync();
- IOSink sink = f.openWrite();
+ var f = fs.file(ns('/foo'))..createSync();
+ var sink = f.openWrite();
sink.write('Hello');
await sink.flush();
- final File newFile = f.renameSync(ns('/bar'));
+ final newFile = f.renameSync(ns('/bar'));
sink.write('Goodbye');
await sink.flush();
await sink.close();
@@ -2377,7 +2376,7 @@
late bool isSinkClosed;
Future<dynamic> closeSink() {
- Future<dynamic> future = sink.close();
+ var future = sink.close();
isSinkClosed = true;
return future;
}
@@ -2448,13 +2447,13 @@
test('ignoresCloseAfterAlreadyClosed', () async {
sink.write('Hello world');
- Future<dynamic> f1 = closeSink();
- Future<dynamic> f2 = closeSink();
+ var f1 = closeSink();
+ var f2 = closeSink();
await Future.wait<dynamic>(<Future<dynamic>>[f1, f2]);
});
test('returnsAccurateDoneFuture', () async {
- bool done = false;
+ var done = false;
// ignore: unawaited_futures
sink.done.then((dynamic _) => done = true);
expect(done, isFalse);
@@ -2469,7 +2468,7 @@
late bool isControllerClosed;
Future<dynamic> closeController() {
- Future<dynamic> future = controller.close();
+ var future = controller.close();
isControllerClosed = true;
return future;
}
@@ -2543,7 +2542,7 @@
});
test('succeedsIfExistsAsFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsBytesSync(<int>[1, 2, 3, 4]);
expect(f.readAsBytesSync(), <int>[1, 2, 3, 4]);
});
@@ -2556,12 +2555,12 @@
});
test('returnsEmptyListForZeroByteFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
expect(f.readAsBytesSync(), isEmpty);
});
test('returns a copy, not a view, of the file content', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsBytesSync(<int>[1, 2, 3, 4]);
List<int> result = f.readAsBytesSync();
expect(result, <int>[1, 2, 3, 4]);
@@ -2593,7 +2592,7 @@
});
test('succeedsIfExistsAsFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsStringSync('Hello world');
expect(f.readAsStringSync(), 'Hello world');
});
@@ -2606,14 +2605,14 @@
});
test('returnsEmptyStringForZeroByteFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
expect(f.readAsStringSync(), isEmpty);
});
});
group('readAsLines', () {
- const String testString = 'Hello world\nHow are you?\nI am fine';
- final List<String> expectedLines = <String>[
+ const testString = 'Hello world\nHow are you?\nI am fine';
+ final expectedLines = <String>[
'Hello world',
'How are you?',
'I am fine',
@@ -2641,25 +2640,25 @@
});
test('succeedsIfExistsAsFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsStringSync(testString);
expect(f.readAsLinesSync(), expectedLines);
});
test('succeedsIfExistsAsLinkToFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
fs.link(ns('/bar')).createSync(ns('/foo'));
f.writeAsStringSync(testString);
expect(f.readAsLinesSync(), expectedLines);
});
test('returnsEmptyListForZeroByteFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
expect(f.readAsLinesSync(), isEmpty);
});
test('isTrailingNewlineAgnostic', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsStringSync('$testString\n');
expect(f.readAsLinesSync(), expectedLines);
@@ -2677,7 +2676,7 @@
});
test('createsFileIfDoesntExist', () {
- File f = fs.file(ns('/foo'));
+ var f = fs.file(ns('/foo'));
expect(f, isNot(exists));
f.writeAsBytesSync(<int>[1, 2, 3, 4]);
expect(f, exists);
@@ -2699,21 +2698,21 @@
});
test('succeedsIfExistsAsLinkToFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
fs.link(ns('/bar')).createSync(ns('/foo'));
fs.file(ns('/bar')).writeAsBytesSync(<int>[1, 2, 3, 4]);
expect(f.readAsBytesSync(), <int>[1, 2, 3, 4]);
});
test('throwsIfFileModeRead', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
expectFileSystemException(ErrorCodes.EBADF, () {
f.writeAsBytesSync(<int>[1], mode: FileMode.read);
});
});
test('overwritesContentIfFileModeWrite', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsBytesSync(<int>[1, 2]);
expect(f.readAsBytesSync(), <int>[1, 2]);
f.writeAsBytesSync(<int>[3, 4]);
@@ -2721,7 +2720,7 @@
});
test('appendsContentIfFileModeAppend', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsBytesSync(<int>[1, 2], mode: FileMode.append);
expect(f.readAsBytesSync(), <int>[1, 2]);
f.writeAsBytesSync(<int>[3, 4], mode: FileMode.append);
@@ -2729,17 +2728,17 @@
});
test('acceptsEmptyBytesList', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsBytesSync(<int>[]);
expect(f.readAsBytesSync(), <int>[]);
});
test('updatesLastModifiedTime', () async {
- File f = fs.file(ns('/foo'))..createSync();
- DateTime before = f.statSync().modified;
+ var f = fs.file(ns('/foo'))..createSync();
+ var before = f.statSync().modified;
await Future<void>.delayed(const Duration(seconds: 2));
f.writeAsBytesSync(<int>[1, 2, 3]);
- DateTime after = f.statSync().modified;
+ var after = f.statSync().modified;
expect(after, isAfter(before));
});
});
@@ -2750,7 +2749,7 @@
});
test('createsFileIfDoesntExist', () {
- File f = fs.file(ns('/foo'));
+ var f = fs.file(ns('/foo'));
expect(f, isNot(exists));
f.writeAsStringSync('Hello world');
expect(f, exists);
@@ -2772,21 +2771,21 @@
});
test('succeedsIfExistsAsLinkToFile', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
fs.link(ns('/bar')).createSync(ns('/foo'));
fs.file(ns('/bar')).writeAsStringSync('Hello world');
expect(f.readAsStringSync(), 'Hello world');
});
test('throwsIfFileModeRead', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
expectFileSystemException(ErrorCodes.EBADF, () {
f.writeAsStringSync('Hello world', mode: FileMode.read);
});
});
test('overwritesContentIfFileModeWrite', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsStringSync('Hello world');
expect(f.readAsStringSync(), 'Hello world');
f.writeAsStringSync('Goodbye cruel world');
@@ -2794,7 +2793,7 @@
});
test('appendsContentIfFileModeAppend', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsStringSync('Hello', mode: FileMode.append);
expect(f.readAsStringSync(), 'Hello');
f.writeAsStringSync('Goodbye', mode: FileMode.append);
@@ -2802,7 +2801,7 @@
});
test('acceptsEmptyString', () {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
f.writeAsStringSync('');
expect(f.readAsStringSync(), isEmpty);
});
@@ -2847,38 +2846,38 @@
group('stat', () {
test('isNotFoundIfDoesntExistAtTail', () {
- FileStat stat = fs.file(ns('/foo')).statSync();
+ var stat = fs.file(ns('/foo')).statSync();
expect(stat.type, FileSystemEntityType.notFound);
});
test('isNotFoundIfDoesntExistViaTraversal', () {
- FileStat stat = fs.file(ns('/foo/bar')).statSync();
+ var stat = fs.file(ns('/foo/bar')).statSync();
expect(stat.type, FileSystemEntityType.notFound);
});
test('isDirectoryIfExistsAsDirectory', () {
fs.directory(ns('/foo')).createSync();
- FileStat stat = fs.file(ns('/foo')).statSync();
+ var stat = fs.file(ns('/foo')).statSync();
expect(stat.type, FileSystemEntityType.directory);
});
test('isFileIfExistsAsFile', () {
fs.file(ns('/foo')).createSync();
- FileStat stat = fs.file(ns('/foo')).statSync();
+ var stat = fs.file(ns('/foo')).statSync();
expect(stat.type, FileSystemEntityType.file);
});
test('isFileIfExistsAsLinkToFile', () {
fs.file(ns('/foo')).createSync();
fs.link(ns('/bar')).createSync(ns('/foo'));
- FileStat stat = fs.file(ns('/bar')).statSync();
+ var stat = fs.file(ns('/bar')).statSync();
expect(stat.type, FileSystemEntityType.file);
});
});
group('delete', () {
test('returnsCovariantType', () async {
- File f = fs.file(ns('/foo'))..createSync();
+ var f = fs.file(ns('/foo'))..createSync();
expect(await f.delete(), isFile);
});
@@ -2953,14 +2952,14 @@
group('uri', () {
test('whenTargetIsDirectory', () {
fs.directory(ns('/foo')).createSync();
- Link l = fs.link(ns('/bar'))..createSync(ns('/foo'));
+ var l = fs.link(ns('/bar'))..createSync(ns('/foo'));
expect(l.uri, fs.path.toUri(ns('/bar')));
expect(fs.link('bar').uri.toString(), 'bar');
});
test('whenTargetIsFile', () {
fs.file(ns('/foo')).createSync();
- Link l = fs.link(ns('/bar'))..createSync(ns('/foo'));
+ var l = fs.link(ns('/bar'))..createSync(ns('/foo'));
expect(l.uri, fs.path.toUri(ns('/bar')));
expect(fs.link('bar').uri.toString(), 'bar');
});
@@ -2991,24 +2990,24 @@
});
test('isTrueIfTargetIsNotFound', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
expect(l, exists);
});
test('isTrueIfTargetIsFile', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.file(ns('/bar')).createSync();
expect(l, exists);
});
test('isTrueIfTargetIsDirectory', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.directory(ns('/bar')).createSync();
expect(l, exists);
});
test('isTrueIfTargetIsLinkLoop', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.link(ns('/bar')).createSync(ns('/foo'));
expect(l, exists);
});
@@ -3038,29 +3037,29 @@
});
test('isNotFoundIfTargetNotFoundAtTail', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
expect(l.statSync().type, FileSystemEntityType.notFound);
});
test('isNotFoundIfTargetNotFoundViaTraversal', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar/baz'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar/baz'));
expect(l.statSync().type, FileSystemEntityType.notFound);
});
test('isNotFoundIfTargetIsLinkLoop', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.link(ns('/bar')).createSync(ns('/foo'));
expect(l.statSync().type, FileSystemEntityType.notFound);
});
test('isFileIfTargetIsFile', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.file(ns('/bar')).createSync();
expect(l.statSync().type, FileSystemEntityType.file);
});
test('isDirectoryIfTargetIsDirectory', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.directory(ns('/bar')).createSync();
expect(l.statSync().type, FileSystemEntityType.directory);
});
@@ -3068,7 +3067,7 @@
group('delete', () {
test('returnsCovariantType', () async {
- Link link = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var link = fs.link(ns('/foo'))..createSync(ns('/bar'));
expect(await link.delete(), isLink);
});
@@ -3118,7 +3117,7 @@
});
test('unlinksIfTargetIsFileAndRecursiveFalse', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.file(ns('/bar')).createSync();
l.deleteSync();
expect(fs.typeSync(ns('/foo'), followLinks: false),
@@ -3128,7 +3127,7 @@
});
test('unlinksIfTargetIsFileAndRecursiveTrue', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.file(ns('/bar')).createSync();
l.deleteSync(recursive: true);
expect(fs.typeSync(ns('/foo'), followLinks: false),
@@ -3138,7 +3137,7 @@
});
test('unlinksIfTargetIsDirectoryAndRecursiveFalse', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.directory(ns('/bar')).createSync();
l.deleteSync();
expect(fs.typeSync(ns('/foo'), followLinks: false),
@@ -3148,7 +3147,7 @@
});
test('unlinksIfTargetIsDirectoryAndRecursiveTrue', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.directory(ns('/bar')).createSync();
l.deleteSync(recursive: true);
expect(fs.typeSync(ns('/foo'), followLinks: false),
@@ -3158,7 +3157,7 @@
});
test('unlinksIfTargetIsLinkLoop', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.link(ns('/bar')).createSync(ns('/foo'));
l.deleteSync();
expect(fs.typeSync(ns('/foo'), followLinks: false),
@@ -3178,7 +3177,7 @@
});
test('ignoresLinkTarget', () {
- Link l = fs.link(ns('/foo/bar'))
+ var l = fs.link(ns('/foo/bar'))
..createSync(ns('/baz/qux'), recursive: true);
expect(l.parent.path, ns('/foo'));
});
@@ -3190,7 +3189,7 @@
});
test('succeedsIfLinkDoesntExistAtTail', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
expect(fs.typeSync(ns('/foo'), followLinks: false),
FileSystemEntityType.link);
expect(l.targetSync(), ns('/bar'));
@@ -3203,7 +3202,7 @@
});
test('succeedsIfLinkDoesntExistViaTraversalAndRecursiveTrue', () {
- Link l = fs.link(ns('/foo/bar'))..createSync('baz', recursive: true);
+ var l = fs.link(ns('/foo/bar'))..createSync('baz', recursive: true);
expect(fs.typeSync(ns('/foo'), followLinks: false),
FileSystemEntityType.directory);
expect(fs.typeSync(ns('/foo/bar'), followLinks: false),
@@ -3242,7 +3241,7 @@
group('update', () {
test('returnsCovariantType', () async {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
expect(await l.update(ns('/baz')), isLink);
});
@@ -3336,24 +3335,24 @@
});
test('succeedsIfTargetIsNotFound', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
expect(l.targetSync(), ns('/bar'));
});
test('succeedsIfTargetIsFile', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.file(ns('/bar')).createSync();
expect(l.targetSync(), ns('/bar'));
});
test('succeedsIfTargetIsDirectory', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.directory(ns('/bar')).createSync();
expect(l.targetSync(), ns('/bar'));
});
test('succeedsIfTargetIsLinkLoop', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.link(ns('/bar')).createSync(ns('/foo'));
expect(l.targetSync(), ns('/bar'));
});
@@ -3393,9 +3392,9 @@
});
test('succeedsIfSourceIsLinkToFile', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.file(ns('/bar')).createSync();
- Link renamed = l.renameSync(ns('/baz'));
+ var renamed = l.renameSync(ns('/baz'));
expect(renamed.path, ns('/baz'));
expect(fs.typeSync(ns('/foo'), followLinks: false),
FileSystemEntityType.notFound);
@@ -3407,8 +3406,8 @@
});
test('succeedsIfSourceIsLinkToNotFound', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
- Link renamed = l.renameSync(ns('/baz'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var renamed = l.renameSync(ns('/baz'));
expect(renamed.path, ns('/baz'));
expect(fs.typeSync(ns('/foo'), followLinks: false),
FileSystemEntityType.notFound);
@@ -3418,9 +3417,9 @@
});
test('succeedsIfSourceIsLinkToDirectory', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.directory(ns('/bar')).createSync();
- Link renamed = l.renameSync(ns('/baz'));
+ var renamed = l.renameSync(ns('/baz'));
expect(renamed.path, ns('/baz'));
expect(fs.typeSync(ns('/foo'), followLinks: false),
FileSystemEntityType.notFound);
@@ -3432,9 +3431,9 @@
});
test('succeedsIfSourceIsLinkLoop', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.link(ns('/bar')).createSync(ns('/foo'));
- Link renamed = l.renameSync(ns('/baz'));
+ var renamed = l.renameSync(ns('/baz'));
expect(renamed.path, ns('/baz'));
expect(fs.typeSync(ns('/foo'), followLinks: false),
FileSystemEntityType.notFound);
@@ -3446,22 +3445,22 @@
});
test('succeedsIfDestinationDoesntExistAtTail', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
- Link renamed = l.renameSync(ns('/baz'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var renamed = l.renameSync(ns('/baz'));
expect(renamed.path, ns('/baz'));
expect(fs.link(ns('/foo')), isNot(exists));
expect(fs.link(ns('/baz')), exists);
});
test('throwsIfDestinationDoesntExistViaTraversal', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
expectFileSystemException(ErrorCodes.ENOENT, () {
l.renameSync(ns('/baz/qux'));
});
});
test('throwsIfDestinationExistsAsFile', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.file(ns('/baz')).createSync();
expectFileSystemException(ErrorCodes.EINVAL, () {
l.renameSync(ns('/baz'));
@@ -3469,7 +3468,7 @@
});
test('throwsIfDestinationExistsAsDirectory', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.directory(ns('/baz')).createSync();
expectFileSystemException(ErrorCodes.EINVAL, () {
l.renameSync(ns('/baz'));
@@ -3477,7 +3476,7 @@
});
test('succeedsIfDestinationExistsAsLinkToFile', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.file(ns('/baz')).createSync();
fs.link(ns('/qux')).createSync(ns('/baz'));
l.renameSync(ns('/qux'));
@@ -3490,7 +3489,7 @@
});
test('throwsIfDestinationExistsAsLinkToDirectory', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.directory(ns('/baz')).createSync();
fs.link(ns('/qux')).createSync(ns('/baz'));
l.renameSync(ns('/qux'));
@@ -3503,7 +3502,7 @@
});
test('succeedsIfDestinationExistsAsLinkToNotFound', () {
- Link l = fs.link(ns('/foo'))..createSync(ns('/bar'));
+ var l = fs.link(ns('/foo'))..createSync(ns('/bar'));
fs.link(ns('/baz')).createSync(ns('/qux'));
l.renameSync(ns('/baz'));
expect(fs.typeSync(ns('/foo')), FileSystemEntityType.notFound);
diff --git a/pkgs/file/test/local_test.dart b/pkgs/file/test/local_test.dart
index e1618d2..b794ccd 100644
--- a/pkgs/file/test/local_test.dart
+++ b/pkgs/file/test/local_test.dart
@@ -2,7 +2,11 @@
// for 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
+
@TestOn('vm')
+library;
+
import 'dart:io' as io;
import 'package:file/local.dart';
@@ -33,7 +37,7 @@
setUpAll(() {
if (io.Platform.isWindows) {
// TODO(tvolkert): Remove once all more serious test failures are fixed
- // https://github.com/google/file.dart/issues/56
+ // https://github.com/dart-lang/tools/issues/618
ignoreOsErrorCodes = true;
}
});
@@ -42,7 +46,7 @@
ignoreOsErrorCodes = false;
});
- Map<String, List<String>> skipOnPlatform = <String, List<String>>{
+ var skipOnPlatform = <String, List<String>>{
'windows': <String>[
'FileSystem > currentDirectory > throwsIfHasNonExistentPathInComplexChain',
'FileSystem > currentDirectory > resolvesLinksIfEncountered',
diff --git a/pkgs/file/test/memory_operations_test.dart b/pkgs/file/test/memory_operations_test.dart
index 5e27843..916707c 100644
--- a/pkgs/file/test/memory_operations_test.dart
+++ b/pkgs/file/test/memory_operations_test.dart
@@ -2,22 +2,21 @@
// for 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:file/file.dart';
import 'package:file/memory.dart';
import 'package:test/test.dart';
void main() {
test('Read operations invoke opHandle', () async {
- List<String> contexts = <String>[];
- List<FileSystemOp> operations = <FileSystemOp>[];
- MemoryFileSystem fs = MemoryFileSystem.test(
+ var contexts = <String>[];
+ var operations = <FileSystemOp>[];
+ var fs = MemoryFileSystem.test(
opHandle: (String context, FileSystemOp operation) {
if (operation == FileSystemOp.read) {
contexts.add(context);
operations.add(operation);
}
});
- final File file = fs.file('test')..createSync();
+ final file = fs.file('test')..createSync();
await file.readAsBytes();
file.readAsBytesSync();
@@ -34,16 +33,16 @@
});
test('Write operations invoke opHandle', () async {
- List<String> contexts = <String>[];
- List<FileSystemOp> operations = <FileSystemOp>[];
- MemoryFileSystem fs = MemoryFileSystem.test(
+ var contexts = <String>[];
+ var operations = <FileSystemOp>[];
+ var fs = MemoryFileSystem.test(
opHandle: (String context, FileSystemOp operation) {
if (operation == FileSystemOp.write) {
contexts.add(context);
operations.add(operation);
}
});
- final File file = fs.file('test')..createSync();
+ final file = fs.file('test')..createSync();
await file.writeAsBytes(<int>[]);
file.writeAsBytesSync(<int>[]);
@@ -60,18 +59,18 @@
});
test('Delete operations invoke opHandle', () async {
- List<String> contexts = <String>[];
- List<FileSystemOp> operations = <FileSystemOp>[];
- MemoryFileSystem fs = MemoryFileSystem.test(
+ var contexts = <String>[];
+ var operations = <FileSystemOp>[];
+ var fs = MemoryFileSystem.test(
opHandle: (String context, FileSystemOp operation) {
if (operation == FileSystemOp.delete) {
contexts.add(context);
operations.add(operation);
}
});
- final File file = fs.file('test')..createSync();
- final Directory directory = fs.directory('testDir')..createSync();
- final Link link = fs.link('testLink')..createSync('foo');
+ final file = fs.file('test')..createSync();
+ final directory = fs.directory('testDir')..createSync();
+ final link = fs.link('testLink')..createSync('foo');
await file.delete();
file.createSync();
@@ -98,9 +97,9 @@
});
test('Create operations invoke opHandle', () async {
- List<String> contexts = <String>[];
- List<FileSystemOp> operations = <FileSystemOp>[];
- MemoryFileSystem fs = MemoryFileSystem.test(
+ var contexts = <String>[];
+ var operations = <FileSystemOp>[];
+ var fs = MemoryFileSystem.test(
opHandle: (String context, FileSystemOp operation) {
if (operation == FileSystemOp.create) {
contexts.add(context);
@@ -139,16 +138,16 @@
});
test('Open operations invoke opHandle', () async {
- List<String> contexts = <String>[];
- List<FileSystemOp> operations = <FileSystemOp>[];
- MemoryFileSystem fs = MemoryFileSystem.test(
+ var contexts = <String>[];
+ var operations = <FileSystemOp>[];
+ var fs = MemoryFileSystem.test(
opHandle: (String context, FileSystemOp operation) {
if (operation == FileSystemOp.open) {
contexts.add(context);
operations.add(operation);
}
});
- final File file = fs.file('test')..createSync();
+ final file = fs.file('test')..createSync();
await file.open();
file.openSync();
@@ -165,16 +164,16 @@
});
test('Copy operations invoke opHandle', () async {
- List<String> contexts = <String>[];
- List<FileSystemOp> operations = <FileSystemOp>[];
- MemoryFileSystem fs = MemoryFileSystem.test(
+ var contexts = <String>[];
+ var operations = <FileSystemOp>[];
+ var fs = MemoryFileSystem.test(
opHandle: (String context, FileSystemOp operation) {
if (operation == FileSystemOp.copy) {
contexts.add(context);
operations.add(operation);
}
});
- final File file = fs.file('test')..createSync();
+ final file = fs.file('test')..createSync();
await file.copy('A');
file.copySync('B');
@@ -187,9 +186,9 @@
});
test('Exists operations invoke opHandle', () async {
- List<String> contexts = <String>[];
- List<FileSystemOp> operations = <FileSystemOp>[];
- MemoryFileSystem fs = MemoryFileSystem.test(
+ var contexts = <String>[];
+ var operations = <FileSystemOp>[];
+ var fs = MemoryFileSystem.test(
opHandle: (String context, FileSystemOp operation) {
if (operation == FileSystemOp.exists) {
contexts.add(context);
diff --git a/pkgs/file/test/memory_test.dart b/pkgs/file/test/memory_test.dart
index f3b324e..ce8675f 100644
--- a/pkgs/file/test/memory_test.dart
+++ b/pkgs/file/test/memory_test.dart
@@ -66,8 +66,7 @@
});
test('MemoryFileSystem.test', () {
- final MemoryFileSystem fs =
- MemoryFileSystem.test(); // creates root directory
+ final fs = MemoryFileSystem.test(); // creates root directory
fs.file('/test1.txt').createSync(); // creates file
fs.file('/test2.txt').createSync(); // creates file
expect(fs.directory('/').statSync().modified, DateTime(2000, 1, 1, 0, 1));
@@ -95,10 +94,10 @@
});
test('MemoryFile.openSync returns a MemoryRandomAccessFile', () async {
- final MemoryFileSystem fs = MemoryFileSystem.test();
+ final fs = MemoryFileSystem.test();
final io.File file = fs.file('/test1')..createSync();
- io.RandomAccessFile raf = file.openSync();
+ var raf = file.openSync();
try {
expect(raf, isA<MemoryRandomAccessFile>());
} finally {
@@ -114,7 +113,7 @@
});
test('MemoryFileSystem.systemTempDirectory test', () {
- final MemoryFileSystem fs = MemoryFileSystem.test();
+ final fs = MemoryFileSystem.test();
final io.Directory fooA = fs.systemTempDirectory.createTempSync('foo');
final io.Directory fooB = fs.systemTempDirectory.createTempSync('foo');
@@ -122,7 +121,7 @@
expect(fooA.path, '/.tmp_rand0/foorand0');
expect(fooB.path, '/.tmp_rand0/foorand1');
- final MemoryFileSystem secondFs = MemoryFileSystem.test();
+ final secondFs = MemoryFileSystem.test();
final io.Directory fooAA =
secondFs.systemTempDirectory.createTempSync('foo');
@@ -136,16 +135,16 @@
test('Failed UTF8 decoding in MemoryFileSystem throws a FileSystemException',
() {
- final MemoryFileSystem fileSystem = MemoryFileSystem.test();
- final File file = fileSystem.file('foo')
+ final fileSystem = MemoryFileSystem.test();
+ final file = fileSystem.file('foo')
..writeAsBytesSync(<int>[0xFFFE]); // Invalid UTF8
expect(file.readAsStringSync, throwsA(isA<FileSystemException>()));
});
test('Creating a temporary directory actually creates the directory', () {
- final MemoryFileSystem fileSystem = MemoryFileSystem.test();
- final Directory tempDir = fileSystem.currentDirectory.createTempSync('foo');
+ final fileSystem = MemoryFileSystem.test();
+ final tempDir = fileSystem.currentDirectory.createTempSync('foo');
expect(tempDir.existsSync(), true);
});
diff --git a/pkgs/file/test/utils.dart b/pkgs/file/test/utils.dart
index 231312f..797ec9d 100644
--- a/pkgs/file/test/utils.dart
+++ b/pkgs/file/test/utils.dart
@@ -25,7 +25,7 @@
/// If [time] is not specified, it will default to the current time.
DateTime ceil([DateTime? time]) {
time ??= DateTime.now();
- int microseconds = (1000 * time.millisecond) + time.microsecond;
+ var microseconds = (1000 * time.millisecond) + time.microsecond;
return (microseconds == 0)
? time
// Add just enough milliseconds and microseconds to reach the next second.
@@ -78,7 +78,7 @@
bool verbose,
) {
if (item is DateTime) {
- Duration diff = item.difference(_time).abs();
+ var diff = item.difference(_time).abs();
return description.add('is $mismatchAdjective $_time by $diff');
} else {
return description.add('is not a DateTime');
diff --git a/pkgs/file/test/utils_test.dart b/pkgs/file/test/utils_test.dart
index 75293bf..23788e9 100644
--- a/pkgs/file/test/utils_test.dart
+++ b/pkgs/file/test/utils_test.dart
@@ -8,9 +8,9 @@
void main() {
test('floorAndCeilProduceExactSecondDateTime', () {
- DateTime time = DateTime.fromMicrosecondsSinceEpoch(1001);
- DateTime lower = floor(time);
- DateTime upper = ceil(time);
+ var time = DateTime.fromMicrosecondsSinceEpoch(1001);
+ var lower = floor(time);
+ var upper = ceil(time);
expect(lower.millisecond, 0);
expect(upper.millisecond, 0);
expect(lower.microsecond, 0);
@@ -18,26 +18,26 @@
});
test('floorAndCeilWorkWithNow', () {
- DateTime time = DateTime.now();
- int lower = time.difference(floor(time)).inMicroseconds;
- int upper = ceil(time).difference(time).inMicroseconds;
+ var time = DateTime.now();
+ var lower = time.difference(floor(time)).inMicroseconds;
+ var upper = ceil(time).difference(time).inMicroseconds;
expect(lower, lessThan(1000000));
expect(upper, lessThanOrEqualTo(1000000));
});
test('floorAndCeilWorkWithExactSecondDateTime', () {
- DateTime time = DateTime.parse('1999-12-31 23:59:59');
- DateTime lower = floor(time);
- DateTime upper = ceil(time);
+ var time = DateTime.parse('1999-12-31 23:59:59');
+ var lower = floor(time);
+ var upper = ceil(time);
expect(lower, time);
expect(upper, time);
});
test('floorAndCeilWorkWithInexactSecondDateTime', () {
- DateTime time = DateTime.parse('1999-12-31 23:59:59.500');
- DateTime lower = floor(time);
- DateTime upper = ceil(time);
- Duration difference = upper.difference(lower);
+ var time = DateTime.parse('1999-12-31 23:59:59.500');
+ var lower = floor(time);
+ var upper = ceil(time);
+ var difference = upper.difference(lower);
expect(difference.inMicroseconds, 1000000);
});
}
diff --git a/pkgs/file_testing/CHANGELOG.md b/pkgs/file_testing/CHANGELOG.md
index 0af779d..17039ee 100644
--- a/pkgs/file_testing/CHANGELOG.md
+++ b/pkgs/file_testing/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 3.1.0-wip
+
+* Changed the type of several matchers to `TypeMatcher` which allows cascading
+ their usage with `.having` and similar.
+
## 3.0.2
* Require Dart 3.1.
diff --git a/pkgs/file_testing/analysis_options.yaml b/pkgs/file_testing/analysis_options.yaml
index 8fbd2e4..d978f81 100644
--- a/pkgs/file_testing/analysis_options.yaml
+++ b/pkgs/file_testing/analysis_options.yaml
@@ -1,6 +1 @@
-include: package:lints/recommended.yaml
-
-analyzer:
- errors:
- # Allow having TODOs in the code
- todo: ignore
+include: package:dart_flutter_team_lints/analysis_options.yaml
diff --git a/pkgs/file_testing/lib/src/testing/core_matchers.dart b/pkgs/file_testing/lib/src/testing/core_matchers.dart
index f58539f..801209e 100644
--- a/pkgs/file_testing/lib/src/testing/core_matchers.dart
+++ b/pkgs/file_testing/lib/src/testing/core_matchers.dart
@@ -2,6 +2,8 @@
// for 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: comment_references
+
import 'dart:io';
import 'package:test/test.dart';
@@ -9,26 +11,27 @@
import 'internal.dart';
/// Matcher that successfully matches against any instance of [Directory].
-const Matcher isDirectory = TypeMatcher<Directory>();
+const isDirectory = TypeMatcher<Directory>();
/// Matcher that successfully matches against any instance of [File].
-const Matcher isFile = TypeMatcher<File>();
+const isFile = TypeMatcher<File>();
/// Matcher that successfully matches against any instance of [Link].
-const Matcher isLink = TypeMatcher<Link>();
+const isLink = TypeMatcher<Link>();
/// Matcher that successfully matches against any instance of
/// [FileSystemEntity].
-const Matcher isFileSystemEntity = TypeMatcher<FileSystemEntity>();
+const isFileSystemEntity = TypeMatcher<FileSystemEntity>();
/// Matcher that successfully matches against any instance of [FileStat].
-const Matcher isFileStat = TypeMatcher<FileStat>();
+const isFileStat = TypeMatcher<FileStat>();
/// Returns a [Matcher] that matches [path] against an entity's path.
///
/// [path] may be a String, a predicate function, or a [Matcher]. If it is
/// a String, it will be wrapped in an equality matcher.
-Matcher hasPath(dynamic path) => _HasPath(path);
+TypeMatcher<FileSystemEntity> hasPath(dynamic path) =>
+ isFileSystemEntity.having((e) => e.path, 'path', path);
/// Returns a [Matcher] that successfully matches against an instance of
/// [FileSystemException].
@@ -39,7 +42,8 @@
/// [osErrorCode] may be an `int`, a predicate function, or a [Matcher]. If it
/// is an `int`, it will be wrapped in an equality matcher.
Matcher isFileSystemException([dynamic osErrorCode]) =>
- _FileSystemException(osErrorCode);
+ const TypeMatcher<FileSystemException>().having((e) => e.osError?.errorCode,
+ 'osError.errorCode', _fileExceptionWrapMatcher(osErrorCode));
/// Returns a matcher that successfully matches against a future or function
/// that throws a [FileSystemException].
@@ -67,89 +71,10 @@
/// Matcher that successfully matches against a [FileSystemEntity] that
/// exists ([FileSystemEntity.existsSync] returns true).
-const Matcher exists = _Exists();
+final TypeMatcher<FileSystemEntity> exists =
+ isFileSystemEntity.having((e) => e.existsSync(), 'existsSync', true);
-class _FileSystemException extends Matcher {
- _FileSystemException(dynamic osErrorCode)
- : _matcher = _wrapMatcher(osErrorCode);
-
- final Matcher? _matcher;
-
- static Matcher? _wrapMatcher(dynamic osErrorCode) {
- if (osErrorCode == null) {
- return null;
- }
- return ignoreOsErrorCodes ? anything : wrapMatcher(osErrorCode);
- }
-
- @override
- bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
- if (item is FileSystemException) {
- return _matcher == null ||
- _matcher!.matches(item.osError?.errorCode, matchState);
- }
- return false;
- }
-
- @override
- Description describe(Description desc) {
- if (_matcher == null) {
- return desc.add('FileSystemException');
- } else {
- desc.add('FileSystemException with osError.errorCode: ');
- return _matcher!.describe(desc);
- }
- }
-}
-
-class _HasPath extends Matcher {
- _HasPath(dynamic path) : _matcher = wrapMatcher(path);
-
- final Matcher _matcher;
-
- @override
- bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
- _matcher.matches(item.path, matchState);
-
- @override
- Description describe(Description desc) {
- desc.add('has path: ');
- return _matcher.describe(desc);
- }
-
- @override
- Description describeMismatch(
- dynamic item,
- Description desc,
- Map<dynamic, dynamic> matchState,
- bool verbose,
- ) {
- desc.add('has path: \'${item.path}\'').add('\n Which: ');
- final Description pathDesc = StringDescription();
- _matcher.describeMismatch(item.path, pathDesc, matchState, verbose);
- desc.add(pathDesc.toString());
- return desc;
- }
-}
-
-class _Exists extends Matcher {
- const _Exists();
-
- @override
- bool matches(dynamic item, Map<dynamic, dynamic> matchState) =>
- item is FileSystemEntity && item.existsSync();
-
- @override
- Description describe(Description description) =>
- description.add('a file system entity that exists');
-
- @override
- Description describeMismatch(
- dynamic item,
- Description description,
- Map<dynamic, dynamic> matchState,
- bool verbose,
- ) {
- return description.add('does not exist');
- }
-}
+Matcher? _fileExceptionWrapMatcher(dynamic osErrorCode) =>
+ (osErrorCode == null || ignoreOsErrorCodes)
+ ? anything
+ : wrapMatcher(osErrorCode);
diff --git a/pkgs/file_testing/pubspec.yaml b/pkgs/file_testing/pubspec.yaml
index 691efa0..895826a 100644
--- a/pkgs/file_testing/pubspec.yaml
+++ b/pkgs/file_testing/pubspec.yaml
@@ -1,5 +1,5 @@
name: file_testing
-version: 3.0.2
+version: 3.1.0-wip
description: Testing utilities for package:file.
repository: https://github.com/dart-lang/tools/tree/main/pkgs/file_testing
issue_tracker: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Afile_testing
@@ -10,5 +10,5 @@
dependencies:
test: ^1.23.1
-dev_dependencies:
- lints: ^5.0.0
+dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
diff --git a/pkgs/pubspec_parse/.gitignore b/pkgs/pubspec_parse/.gitignore
new file mode 100644
index 0000000..ec8eae3
--- /dev/null
+++ b/pkgs/pubspec_parse/.gitignore
@@ -0,0 +1,4 @@
+# Don’t commit the following directories created by pub.
+.dart_tool/
+.packages
+pubspec.lock
diff --git a/pkgs/pubspec_parse/CHANGELOG.md b/pkgs/pubspec_parse/CHANGELOG.md
new file mode 100644
index 0000000..a5f0f1a
--- /dev/null
+++ b/pkgs/pubspec_parse/CHANGELOG.md
@@ -0,0 +1,104 @@
+## 1.4.0
+
+- Require Dart 3.2
+- Seal the `Dependency` class.
+- Set `Pubspec.environment` to non-nullable.
+- Remove deprecated package_api_docs rule
+- Move to `dart-lang/tools` monorepo.
+
+## 1.3.0
+
+- Require Dart 3.0
+- Added support for `ignored_advisories` field.
+- Added structural equality for `Dependency` subclasses and `HostedDetails`.
+
+## 1.2.3
+
+- Added topics to `pubspec.yaml`.
+
+## 1.2.2
+
+- Require Dart SDK >= 2.18.0
+- Required `json_annotation: ^4.8.0`
+- Added support for `topics` field.
+
+## 1.2.1
+
+- Added support for `funding` field.
+
+## 1.2.0
+
+- Added support for `screenshots` field.
+- Update `HostedDetails` to reflect how `hosted` dependencies are parsed in
+ Dart 2.15:
+ - Add `HostedDetails.declaredName` as the (optional) `name` property in a
+ `hosted` block.
+ - `HostedDetails.name` now falls back to the name of the dependency if no
+ name is declared in the block.
+- Require Dart SDK >= 2.14.0
+
+## 1.1.0
+
+- Export `HostedDetails` publicly.
+
+## 1.0.0
+
+- Migrate to null-safety.
+- Pubspec: `author` and `authors` are both now deprecated.
+ See https://dart.dev/tools/pub/pubspec#authorauthors
+
+## 0.1.8
+
+- Allow the latest `package:pub_semver`.
+
+## 0.1.7
+
+- Allow `package:yaml` `v3.x`.
+
+## 0.1.6
+
+- Update SDK requirement to `>=2.7.0 <3.0.0`.
+- Allow `package:json_annotation` `v4.x`.
+
+## 0.1.5
+
+- Update SDK requirement to `>=2.2.0 <3.0.0`.
+- Support the latest `package:json_annotation`.
+
+## 0.1.4
+
+- Added `lenient` named argument to `Pubspec.fromJson` to ignore format and type errors.
+
+## 0.1.3
+
+- Added support for `flutter`, `issue_tracker`, `publish_to`, and `repository`
+ fields.
+
+## 0.1.2+3
+
+- Support the latest version of `package:json_annotation`.
+
+## 0.1.2+2
+
+- Support `package:json_annotation` v1.
+
+## 0.1.2+1
+
+- Support the Dart 2 stable release.
+
+## 0.1.2
+
+- Allow superfluous `version` keys with `git` and `path` dependencies.
+- Improve errors when unsupported keys are provided in dependencies.
+- Provide better errors with invalid `sdk` dependency values.
+- Support "scp-like syntax" for Git SSH URIs in the form
+ `[user@]host.xz:path/to/repo.git/`.
+
+## 0.1.1
+
+- Fixed name collision with error type in latest `package:json_annotation`.
+- Improved parsing of hosted dependencies and environment constraints.
+
+## 0.1.0
+
+- Initial release.
diff --git a/pkgs/pubspec_parse/LICENSE b/pkgs/pubspec_parse/LICENSE
new file mode 100644
index 0000000..4d1ad40
--- /dev/null
+++ b/pkgs/pubspec_parse/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/pubspec_parse/README.md b/pkgs/pubspec_parse/README.md
new file mode 100644
index 0000000..1d04aa4
--- /dev/null
+++ b/pkgs/pubspec_parse/README.md
@@ -0,0 +1,12 @@
+[](https://github.com/dart-lang/tools/actions/workflows/pubspec_parse.yaml)
+[](https://pub.dev/packages/pubspec_parse)
+[](https://pub.dev/packages/pubspec_parse/publisher)
+
+## What's this?
+
+Supports parsing `pubspec.yaml` files with robust error reporting and support
+for most of the documented features.
+
+## More information
+
+Read more about the [pubspec format](https://dart.dev/tools/pub/pubspec).
diff --git a/pkgs/pubspec_parse/analysis_options.yaml b/pkgs/pubspec_parse/analysis_options.yaml
new file mode 100644
index 0000000..93eeebf
--- /dev/null
+++ b/pkgs/pubspec_parse/analysis_options.yaml
@@ -0,0 +1,30 @@
+# https://dart.dev/guides/language/analysis-options
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
+ strict-inference: true
+
+linter:
+ rules:
+ - avoid_bool_literals_in_conditional_expressions
+ - avoid_classes_with_only_static_members
+ - avoid_private_typedef_functions
+ - avoid_redundant_argument_values
+ - avoid_returning_this
+ - avoid_unused_constructor_parameters
+ - avoid_void_async
+ - cancel_subscriptions
+ - cascade_invocations
+ - join_return_with_assignment
+ - literal_only_boolean_expressions
+ - missing_whitespace_between_adjacent_strings
+ - no_adjacent_strings_in_list
+ - no_runtimeType_toString
+ - prefer_const_declarations
+ - prefer_expression_function_bodies
+ - prefer_final_locals
+ - require_trailing_commas
+ - unnecessary_await_in_return
+ - use_string_buffers
diff --git a/pkgs/pubspec_parse/build.yaml b/pkgs/pubspec_parse/build.yaml
new file mode 100644
index 0000000..2003bc2
--- /dev/null
+++ b/pkgs/pubspec_parse/build.yaml
@@ -0,0 +1,25 @@
+# Read about `build.yaml` at https://pub.dev/packages/build_config
+# To update generated code, run `pub run build_runner build`
+targets:
+ $default:
+ builders:
+ json_serializable:
+ generate_for:
+ - lib/src/pubspec.dart
+ - lib/src/dependency.dart
+ options:
+ any_map: true
+ checked: true
+ create_to_json: false
+ field_rename: snake
+
+ # The end-user of a builder which applies "source_gen|combining_builder"
+ # may configure the builder to ignore specific lints for their project
+ source_gen|combining_builder:
+ options:
+ ignore_for_file:
+ - deprecated_member_use_from_same_package
+ - lines_longer_than_80_chars
+ - require_trailing_commas
+ # https://github.com/google/json_serializable.dart/issues/945
+ - unnecessary_cast
diff --git a/pkgs/pubspec_parse/dart_test.yaml b/pkgs/pubspec_parse/dart_test.yaml
new file mode 100644
index 0000000..1d7ac69
--- /dev/null
+++ b/pkgs/pubspec_parse/dart_test.yaml
@@ -0,0 +1,3 @@
+tags:
+ presubmit-only:
+ skip: "Should only be run during presubmit"
diff --git a/pkgs/pubspec_parse/lib/pubspec_parse.dart b/pkgs/pubspec_parse/lib/pubspec_parse.dart
new file mode 100644
index 0000000..b5c12e4
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/pubspec_parse.dart
@@ -0,0 +1,14 @@
+// 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/dependency.dart'
+ show
+ Dependency,
+ GitDependency,
+ HostedDependency,
+ HostedDetails,
+ PathDependency,
+ SdkDependency;
+export 'src/pubspec.dart' show Pubspec;
+export 'src/screenshot.dart' show Screenshot;
diff --git a/pkgs/pubspec_parse/lib/src/dependency.dart b/pkgs/pubspec_parse/lib/src/dependency.dart
new file mode 100644
index 0000000..24c65ea
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/dependency.dart
@@ -0,0 +1,277 @@
+// 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:collection/collection.dart';
+import 'package:json_annotation/json_annotation.dart';
+import 'package:pub_semver/pub_semver.dart';
+import 'package:yaml/yaml.dart';
+
+part 'dependency.g.dart';
+
+Map<String, Dependency> parseDeps(Map? source) =>
+ source?.map((k, v) {
+ final key = k as String;
+ Dependency? value;
+ try {
+ value = _fromJson(v, k);
+ } on CheckedFromJsonException catch (e) {
+ if (e.map is! YamlMap) {
+ // This is likely a "synthetic" map created from a String value
+ // Use `source` to throw this exception with an actual YamlMap and
+ // extract the associated error information.
+ throw CheckedFromJsonException(source, key, e.className!, e.message);
+ }
+ rethrow;
+ }
+
+ if (value == null) {
+ throw CheckedFromJsonException(
+ source,
+ key,
+ 'Pubspec',
+ 'Not a valid dependency value.',
+ );
+ }
+ return MapEntry(key, value);
+ }) ??
+ {};
+
+const _sourceKeys = ['sdk', 'git', 'path', 'hosted'];
+
+/// Returns `null` if the data could not be parsed.
+Dependency? _fromJson(Object? data, String name) {
+ if (data is String || data == null) {
+ return _$HostedDependencyFromJson({'version': data});
+ }
+
+ if (data is Map) {
+ final matchedKeys =
+ data.keys.cast<String>().where((key) => key != 'version').toList();
+
+ if (data.isEmpty || (matchedKeys.isEmpty && data.containsKey('version'))) {
+ return _$HostedDependencyFromJson(data);
+ } else {
+ final firstUnrecognizedKey =
+ matchedKeys.firstWhereOrNull((k) => !_sourceKeys.contains(k));
+
+ return $checkedNew<Dependency>('Dependency', data, () {
+ if (firstUnrecognizedKey != null) {
+ throw UnrecognizedKeysException(
+ [firstUnrecognizedKey],
+ data,
+ _sourceKeys,
+ );
+ }
+ if (matchedKeys.length > 1) {
+ throw CheckedFromJsonException(
+ data,
+ matchedKeys[1],
+ 'Dependency',
+ 'A dependency may only have one source.',
+ );
+ }
+
+ final key = matchedKeys.single;
+
+ return switch (key) {
+ 'git' => GitDependency.fromData(data[key]),
+ 'path' => PathDependency.fromData(data[key]),
+ 'sdk' => _$SdkDependencyFromJson(data),
+ 'hosted' => _$HostedDependencyFromJson(data)
+ ..hosted?._nameOfPackage = name,
+ _ => throw StateError('There is a bug in pubspec_parse.'),
+ };
+ });
+ }
+ }
+
+ // Not a String or a Map – return null so parent logic can throw proper error
+ return null;
+}
+
+sealed class Dependency {}
+
+@JsonSerializable()
+class SdkDependency extends Dependency {
+ final String sdk;
+ @JsonKey(fromJson: _constraintFromString)
+ final VersionConstraint version;
+
+ SdkDependency(this.sdk, {VersionConstraint? version})
+ : version = version ?? VersionConstraint.any;
+
+ @override
+ bool operator ==(Object other) =>
+ other is SdkDependency && other.sdk == sdk && other.version == version;
+
+ @override
+ int get hashCode => Object.hash(sdk, version);
+
+ @override
+ String toString() => 'SdkDependency: $sdk';
+}
+
+@JsonSerializable()
+class GitDependency extends Dependency {
+ @JsonKey(fromJson: parseGitUri)
+ final Uri url;
+ final String? ref;
+ final String? path;
+
+ GitDependency(this.url, {this.ref, this.path});
+
+ factory GitDependency.fromData(Object? data) {
+ if (data is String) {
+ data = {'url': data};
+ }
+
+ if (data is Map) {
+ return _$GitDependencyFromJson(data);
+ }
+
+ throw ArgumentError.value(data, 'git', 'Must be a String or a Map.');
+ }
+
+ @override
+ bool operator ==(Object other) =>
+ other is GitDependency &&
+ other.url == url &&
+ other.ref == ref &&
+ other.path == path;
+
+ @override
+ int get hashCode => Object.hash(url, ref, path);
+
+ @override
+ String toString() => 'GitDependency: url@$url';
+}
+
+Uri? parseGitUriOrNull(String? value) =>
+ value == null ? null : parseGitUri(value);
+
+Uri parseGitUri(String value) => _tryParseScpUri(value) ?? Uri.parse(value);
+
+/// Supports URIs like `[user@]host.xz:path/to/repo.git/`
+/// See https://git-scm.com/docs/git-clone#_git_urls_a_id_urls_a
+Uri? _tryParseScpUri(String value) {
+ final colonIndex = value.indexOf(':');
+
+ if (colonIndex < 0) {
+ return null;
+ } else if (colonIndex == value.indexOf('://')) {
+ // If the first colon is part of a scheme, it's not an scp-like URI
+ return null;
+ }
+ final slashIndex = value.indexOf('/');
+
+ if (slashIndex >= 0 && slashIndex < colonIndex) {
+ // Per docs: This syntax is only recognized if there are no slashes before
+ // the first colon. This helps differentiate a local path that contains a
+ // colon. For example the local path foo:bar could be specified as an
+ // absolute path or ./foo:bar to avoid being misinterpreted as an ssh url.
+ return null;
+ }
+
+ final atIndex = value.indexOf('@');
+ if (colonIndex > atIndex) {
+ final user = atIndex >= 0 ? value.substring(0, atIndex) : null;
+ final host = value.substring(atIndex + 1, colonIndex);
+ final path = value.substring(colonIndex + 1);
+ return Uri(scheme: 'ssh', userInfo: user, host: host, path: path);
+ }
+ return null;
+}
+
+class PathDependency extends Dependency {
+ final String path;
+
+ PathDependency(this.path);
+
+ factory PathDependency.fromData(Object? data) {
+ if (data is String) {
+ return PathDependency(data);
+ }
+ throw ArgumentError.value(data, 'path', 'Must be a String.');
+ }
+
+ @override
+ bool operator ==(Object other) =>
+ other is PathDependency && other.path == path;
+
+ @override
+ int get hashCode => path.hashCode;
+
+ @override
+ String toString() => 'PathDependency: path@$path';
+}
+
+@JsonSerializable(disallowUnrecognizedKeys: true)
+class HostedDependency extends Dependency {
+ @JsonKey(fromJson: _constraintFromString)
+ final VersionConstraint version;
+
+ @JsonKey(disallowNullValue: true)
+ final HostedDetails? hosted;
+
+ HostedDependency({VersionConstraint? version, this.hosted})
+ : version = version ?? VersionConstraint.any;
+
+ @override
+ bool operator ==(Object other) =>
+ other is HostedDependency &&
+ other.version == version &&
+ other.hosted == hosted;
+
+ @override
+ int get hashCode => Object.hash(version, hosted);
+
+ @override
+ String toString() => 'HostedDependency: $version';
+}
+
+@JsonSerializable(disallowUnrecognizedKeys: true)
+class HostedDetails {
+ /// The name of the target dependency as declared in a `hosted` block.
+ ///
+ /// This may be null if no explicit name is present, for instance because the
+ /// hosted dependency was declared as a string (`hosted: pub.example.org`).
+ @JsonKey(name: 'name')
+ final String? declaredName;
+
+ @JsonKey(fromJson: parseGitUriOrNull, disallowNullValue: true)
+ final Uri? url;
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ String? _nameOfPackage;
+
+ /// The name of this package on the package repository.
+ ///
+ /// If this hosted block has a [declaredName], that one will be used.
+ /// Otherwise, the name will be inferred from the surrounding package name.
+ String get name => declaredName ?? _nameOfPackage!;
+
+ HostedDetails(this.declaredName, this.url);
+
+ factory HostedDetails.fromJson(Object data) {
+ if (data is String) {
+ data = {'url': data};
+ }
+
+ if (data is Map) {
+ return _$HostedDetailsFromJson(data);
+ }
+
+ throw ArgumentError.value(data, 'hosted', 'Must be a Map or String.');
+ }
+
+ @override
+ bool operator ==(Object other) =>
+ other is HostedDetails && other.name == name && other.url == url;
+
+ @override
+ int get hashCode => Object.hash(name, url);
+}
+
+VersionConstraint _constraintFromString(String? input) =>
+ input == null ? VersionConstraint.any : VersionConstraint.parse(input);
diff --git a/pkgs/pubspec_parse/lib/src/dependency.g.dart b/pkgs/pubspec_parse/lib/src/dependency.g.dart
new file mode 100644
index 0000000..1a504f1
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/dependency.g.dart
@@ -0,0 +1,72 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+// ignore_for_file: deprecated_member_use_from_same_package, lines_longer_than_80_chars, require_trailing_commas, unnecessary_cast
+
+part of 'dependency.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+SdkDependency _$SdkDependencyFromJson(Map json) => $checkedCreate(
+ 'SdkDependency',
+ json,
+ ($checkedConvert) {
+ final val = SdkDependency(
+ $checkedConvert('sdk', (v) => v as String),
+ version: $checkedConvert(
+ 'version', (v) => _constraintFromString(v as String?)),
+ );
+ return val;
+ },
+ );
+
+GitDependency _$GitDependencyFromJson(Map json) => $checkedCreate(
+ 'GitDependency',
+ json,
+ ($checkedConvert) {
+ final val = GitDependency(
+ $checkedConvert('url', (v) => parseGitUri(v as String)),
+ ref: $checkedConvert('ref', (v) => v as String?),
+ path: $checkedConvert('path', (v) => v as String?),
+ );
+ return val;
+ },
+ );
+
+HostedDependency _$HostedDependencyFromJson(Map json) => $checkedCreate(
+ 'HostedDependency',
+ json,
+ ($checkedConvert) {
+ $checkKeys(
+ json,
+ allowedKeys: const ['version', 'hosted'],
+ disallowNullValues: const ['hosted'],
+ );
+ final val = HostedDependency(
+ version: $checkedConvert(
+ 'version', (v) => _constraintFromString(v as String?)),
+ hosted: $checkedConvert('hosted',
+ (v) => v == null ? null : HostedDetails.fromJson(v as Object)),
+ );
+ return val;
+ },
+ );
+
+HostedDetails _$HostedDetailsFromJson(Map json) => $checkedCreate(
+ 'HostedDetails',
+ json,
+ ($checkedConvert) {
+ $checkKeys(
+ json,
+ allowedKeys: const ['name', 'url'],
+ disallowNullValues: const ['url'],
+ );
+ final val = HostedDetails(
+ $checkedConvert('name', (v) => v as String?),
+ $checkedConvert('url', (v) => parseGitUriOrNull(v as String?)),
+ );
+ return val;
+ },
+ fieldKeyMap: const {'declaredName': 'name'},
+ );
diff --git a/pkgs/pubspec_parse/lib/src/pubspec.dart b/pkgs/pubspec_parse/lib/src/pubspec.dart
new file mode 100644
index 0000000..1317a23
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/pubspec.dart
@@ -0,0 +1,226 @@
+// 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:checked_yaml/checked_yaml.dart';
+import 'package:json_annotation/json_annotation.dart';
+import 'package:pub_semver/pub_semver.dart';
+
+import 'dependency.dart';
+import 'screenshot.dart';
+
+part 'pubspec.g.dart';
+
+@JsonSerializable()
+class Pubspec {
+ // TODO: executables
+
+ final String name;
+
+ @JsonKey(fromJson: _versionFromString)
+ final Version? version;
+
+ final String? description;
+
+ /// This should be a URL pointing to the website for the package.
+ final String? homepage;
+
+ /// Specifies where to publish this package.
+ ///
+ /// Accepted values: `null`, `'none'` or an `http` or `https` URL.
+ ///
+ /// [More information](https://dart.dev/tools/pub/pubspec#publish_to).
+ final String? publishTo;
+
+ /// Optional field to specify the source code repository of the package.
+ /// Useful when a package has both a home page and a repository.
+ final Uri? repository;
+
+ /// Optional field to a web page where developers can report new issues or
+ /// view existing ones.
+ final Uri? issueTracker;
+
+ /// Optional field to list the URLs where the package authors accept
+ /// support or funding.
+ final List<Uri>? funding;
+
+ /// Optional field to list the topics that this packages belongs to.
+ final List<String>? topics;
+
+ /// Optional field to list advisories to be ignored by the client.
+ final List<String>? ignoredAdvisories;
+
+ /// Optional field for specifying included screenshot files.
+ @JsonKey(fromJson: parseScreenshots)
+ final List<Screenshot>? screenshots;
+
+ /// If there is exactly 1 value in [authors], returns it.
+ ///
+ /// If there are 0 or more than 1, returns `null`.
+ @Deprecated(
+ 'See https://dart.dev/tools/pub/pubspec#authorauthors',
+ )
+ String? get author {
+ if (authors.length == 1) {
+ return authors.single;
+ }
+ return null;
+ }
+
+ @Deprecated(
+ 'See https://dart.dev/tools/pub/pubspec#authorauthors',
+ )
+ final List<String> authors;
+ final String? documentation;
+
+ @JsonKey(fromJson: _environmentMap)
+ final Map<String, VersionConstraint?> environment;
+
+ @JsonKey(fromJson: parseDeps)
+ final Map<String, Dependency> dependencies;
+
+ @JsonKey(fromJson: parseDeps)
+ final Map<String, Dependency> devDependencies;
+
+ @JsonKey(fromJson: parseDeps)
+ final Map<String, Dependency> dependencyOverrides;
+
+ /// Optional configuration specific to [Flutter](https://flutter.io/)
+ /// packages.
+ ///
+ /// May include
+ /// [assets](https://flutter.io/docs/development/ui/assets-and-images)
+ /// and other settings.
+ final Map<String, dynamic>? flutter;
+
+ /// If [author] and [authors] are both provided, their values are combined
+ /// with duplicates eliminated.
+ Pubspec(
+ this.name, {
+ this.version,
+ this.publishTo,
+ @Deprecated(
+ 'See https://dart.dev/tools/pub/pubspec#authorauthors',
+ )
+ String? author,
+ @Deprecated(
+ 'See https://dart.dev/tools/pub/pubspec#authorauthors',
+ )
+ List<String>? authors,
+ Map<String, VersionConstraint?>? environment,
+ this.homepage,
+ this.repository,
+ this.issueTracker,
+ this.funding,
+ this.topics,
+ this.ignoredAdvisories,
+ this.screenshots,
+ this.documentation,
+ this.description,
+ Map<String, Dependency>? dependencies,
+ Map<String, Dependency>? devDependencies,
+ Map<String, Dependency>? dependencyOverrides,
+ this.flutter,
+ }) :
+ // ignore: deprecated_member_use_from_same_package
+ authors = _normalizeAuthors(author, authors),
+ environment = environment ?? const {},
+ dependencies = dependencies ?? const {},
+ devDependencies = devDependencies ?? const {},
+ dependencyOverrides = dependencyOverrides ?? const {} {
+ if (name.isEmpty) {
+ throw ArgumentError.value(name, 'name', '"name" cannot be empty.');
+ }
+
+ if (publishTo != null && publishTo != 'none') {
+ try {
+ final targetUri = Uri.parse(publishTo!);
+ if (!(targetUri.isScheme('http') || targetUri.isScheme('https'))) {
+ throw const FormatException('Must be an http or https URL.');
+ }
+ } on FormatException catch (e) {
+ throw ArgumentError.value(publishTo, 'publishTo', e.message);
+ }
+ }
+ }
+
+ factory Pubspec.fromJson(Map json, {bool lenient = false}) {
+ if (lenient) {
+ while (json.isNotEmpty) {
+ // Attempting to remove top-level properties that cause parsing errors.
+ try {
+ return _$PubspecFromJson(json);
+ } on CheckedFromJsonException catch (e) {
+ if (e.map == json && json.containsKey(e.key)) {
+ json = Map.from(json)..remove(e.key);
+ continue;
+ }
+ rethrow;
+ }
+ }
+ }
+
+ return _$PubspecFromJson(json);
+ }
+
+ /// Parses source [yaml] into [Pubspec].
+ ///
+ /// When [lenient] is set, top-level property-parsing or type cast errors are
+ /// ignored and `null` values are returned.
+ factory Pubspec.parse(String yaml, {Uri? sourceUrl, bool lenient = false}) =>
+ checkedYamlDecode(
+ yaml,
+ (map) => Pubspec.fromJson(map!, lenient: lenient),
+ sourceUrl: sourceUrl,
+ );
+
+ static List<String> _normalizeAuthors(String? author, List<String>? authors) {
+ final value = <String>{
+ if (author != null) author,
+ ...?authors,
+ };
+ return value.toList();
+ }
+}
+
+Version? _versionFromString(String? input) =>
+ input == null ? null : Version.parse(input);
+
+Map<String, VersionConstraint?> _environmentMap(Map? source) =>
+ source?.map((k, value) {
+ final key = k as String;
+ if (key == 'dart') {
+ // github.com/dart-lang/pub/blob/d84173eeb03c3/lib/src/pubspec.dart#L342
+ // 'dart' is not allowed as a key!
+ throw CheckedFromJsonException(
+ source,
+ 'dart',
+ 'VersionConstraint',
+ 'Use "sdk" to for Dart SDK constraints.',
+ badKey: true,
+ );
+ }
+
+ VersionConstraint? constraint;
+ if (value == null) {
+ constraint = null;
+ } else if (value is String) {
+ try {
+ constraint = VersionConstraint.parse(value);
+ } on FormatException catch (e) {
+ throw CheckedFromJsonException(source, key, 'Pubspec', e.message);
+ }
+
+ return MapEntry(key, constraint);
+ } else {
+ throw CheckedFromJsonException(
+ source,
+ key,
+ 'VersionConstraint',
+ '`$value` is not a String.',
+ );
+ }
+
+ return MapEntry(key, constraint);
+ }) ??
+ {};
diff --git a/pkgs/pubspec_parse/lib/src/pubspec.g.dart b/pkgs/pubspec_parse/lib/src/pubspec.g.dart
new file mode 100644
index 0000000..fc28571
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/pubspec.g.dart
@@ -0,0 +1,64 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+// ignore_for_file: deprecated_member_use_from_same_package, lines_longer_than_80_chars, require_trailing_commas, unnecessary_cast
+
+part of 'pubspec.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+Pubspec _$PubspecFromJson(Map json) => $checkedCreate(
+ 'Pubspec',
+ json,
+ ($checkedConvert) {
+ final val = Pubspec(
+ $checkedConvert('name', (v) => v as String),
+ version: $checkedConvert(
+ 'version', (v) => _versionFromString(v as String?)),
+ publishTo: $checkedConvert('publish_to', (v) => v as String?),
+ author: $checkedConvert('author', (v) => v as String?),
+ authors: $checkedConvert('authors',
+ (v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
+ environment:
+ $checkedConvert('environment', (v) => _environmentMap(v as Map?)),
+ homepage: $checkedConvert('homepage', (v) => v as String?),
+ repository: $checkedConvert(
+ 'repository', (v) => v == null ? null : Uri.parse(v as String)),
+ issueTracker: $checkedConvert('issue_tracker',
+ (v) => v == null ? null : Uri.parse(v as String)),
+ funding: $checkedConvert(
+ 'funding',
+ (v) => (v as List<dynamic>?)
+ ?.map((e) => Uri.parse(e as String))
+ .toList()),
+ topics: $checkedConvert('topics',
+ (v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
+ ignoredAdvisories: $checkedConvert('ignored_advisories',
+ (v) => (v as List<dynamic>?)?.map((e) => e as String).toList()),
+ screenshots: $checkedConvert(
+ 'screenshots', (v) => parseScreenshots(v as List?)),
+ documentation: $checkedConvert('documentation', (v) => v as String?),
+ description: $checkedConvert('description', (v) => v as String?),
+ dependencies:
+ $checkedConvert('dependencies', (v) => parseDeps(v as Map?)),
+ devDependencies:
+ $checkedConvert('dev_dependencies', (v) => parseDeps(v as Map?)),
+ dependencyOverrides: $checkedConvert(
+ 'dependency_overrides', (v) => parseDeps(v as Map?)),
+ flutter: $checkedConvert(
+ 'flutter',
+ (v) => (v as Map?)?.map(
+ (k, e) => MapEntry(k as String, e),
+ )),
+ );
+ return val;
+ },
+ fieldKeyMap: const {
+ 'publishTo': 'publish_to',
+ 'issueTracker': 'issue_tracker',
+ 'ignoredAdvisories': 'ignored_advisories',
+ 'devDependencies': 'dev_dependencies',
+ 'dependencyOverrides': 'dependency_overrides'
+ },
+ );
diff --git a/pkgs/pubspec_parse/lib/src/screenshot.dart b/pkgs/pubspec_parse/lib/src/screenshot.dart
new file mode 100644
index 0000000..f5f0be2
--- /dev/null
+++ b/pkgs/pubspec_parse/lib/src/screenshot.dart
@@ -0,0 +1,65 @@
+// 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:json_annotation/json_annotation.dart';
+
+@JsonSerializable()
+class Screenshot {
+ final String description;
+ final String path;
+
+ Screenshot(this.description, this.path);
+}
+
+List<Screenshot> parseScreenshots(List? input) {
+ final res = <Screenshot>[];
+ if (input == null) {
+ return res;
+ }
+
+ for (final e in input) {
+ if (e is! Map) continue;
+
+ final description = e['description'];
+ if (description == null) {
+ throw CheckedFromJsonException(
+ e,
+ 'description',
+ 'Screenshot',
+ 'Missing required key `description`',
+ );
+ }
+
+ if (description is! String) {
+ throw CheckedFromJsonException(
+ e,
+ 'description',
+ 'Screenshot',
+ '`$description` is not a String',
+ );
+ }
+
+ final path = e['path'];
+ if (path == null) {
+ throw CheckedFromJsonException(
+ e,
+ 'path',
+ 'Screenshot',
+ 'Missing required key `path`',
+ );
+ }
+
+ if (path is! String) {
+ throw CheckedFromJsonException(
+ e,
+ 'path',
+ 'Screenshot',
+ '`$path` is not a String',
+ );
+ }
+
+ res.add(Screenshot(description, path));
+ }
+ return res;
+}
diff --git a/pkgs/pubspec_parse/pubspec.yaml b/pkgs/pubspec_parse/pubspec.yaml
new file mode 100644
index 0000000..ad0c55e
--- /dev/null
+++ b/pkgs/pubspec_parse/pubspec.yaml
@@ -0,0 +1,32 @@
+name: pubspec_parse
+version: 1.4.0
+description: >-
+ Simple package for parsing pubspec.yaml files with a type-safe API and rich
+ error reporting.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/pubspec_parse
+
+topics:
+- dart-pub
+
+environment:
+ sdk: ^3.2.0
+
+dependencies:
+ checked_yaml: ^2.0.1
+ collection: ^1.15.0
+ json_annotation: ^4.8.0
+ pub_semver: ^2.0.0
+ yaml: ^3.0.0
+
+dev_dependencies:
+ build_runner: ^2.2.1
+ build_verify: ^3.0.0
+ dart_flutter_team_lints: ^3.0.0
+ json_serializable: ^6.6.0
+ path: ^1.8.0
+ # Needed because we are configuring `combining_builder`
+ source_gen: ^1.2.3
+ stack_trace: ^1.10.0
+ test: ^1.21.6
+ test_descriptor: ^2.0.0
+ test_process: ^2.0.0
diff --git a/pkgs/pubspec_parse/test/dependency_test.dart b/pkgs/pubspec_parse/test/dependency_test.dart
new file mode 100644
index 0000000..f1e4f57
--- /dev/null
+++ b/pkgs/pubspec_parse/test/dependency_test.dart
@@ -0,0 +1,446 @@
+// 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:io';
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:pubspec_parse/pubspec_parse.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+void main() {
+ group('hosted', _hostedDependency);
+ group('git', _gitDependency);
+ group('sdk', _sdkDependency);
+ group('path', _pathDependency);
+
+ group('errors', () {
+ test('List', () {
+ _expectThrows(
+ [],
+ r'''
+line 4, column 10: Unsupported value for "dep". Not a valid dependency value.
+ ╷
+4 │ "dep": []
+ │ ^^
+ ╵''',
+ );
+ });
+
+ test('int', () {
+ _expectThrows(
+ 42,
+ r'''
+line 4, column 10: Unsupported value for "dep". Not a valid dependency value.
+ ╷
+4 │ "dep": 42
+ │ ┌──────────^
+5 │ │ }
+ │ └─^
+ ╵''',
+ );
+ });
+
+ test('map with too many keys', () {
+ _expectThrows(
+ {'path': 'a', 'git': 'b'},
+ r'''
+line 6, column 11: Unsupported value for "git". A dependency may only have one source.
+ ╷
+6 │ "git": "b"
+ │ ^^^
+ ╵''',
+ );
+ });
+
+ test('map with unsupported keys', () {
+ _expectThrows(
+ {'bob': 'a', 'jones': 'b'},
+ r'''
+line 5, column 4: Unrecognized keys: [bob]; supported keys: [sdk, git, path, hosted]
+ ╷
+5 │ "bob": "a",
+ │ ^^^^^
+ ╵''',
+ );
+ });
+ });
+}
+
+void _hostedDependency() {
+ test('null', () async {
+ final dep = await _dependency<HostedDependency>(null);
+ expect(dep.version.toString(), 'any');
+ expect(dep.hosted, isNull);
+ expect(dep.toString(), 'HostedDependency: any');
+ });
+
+ test('empty map', () async {
+ final dep = await _dependency<HostedDependency>({});
+ expect(dep.hosted, isNull);
+ expect(dep.toString(), 'HostedDependency: any');
+ });
+
+ test('string version', () async {
+ final dep = await _dependency<HostedDependency>('^1.0.0');
+ expect(dep.version.toString(), '^1.0.0');
+ expect(dep.hosted, isNull);
+ expect(dep.toString(), 'HostedDependency: ^1.0.0');
+ });
+
+ test('bad string version', () {
+ _expectThrows(
+ 'not a version',
+ r'''
+line 4, column 10: Unsupported value for "dep". Could not parse version "not a version". Unknown text at "not a version".
+ ╷
+4 │ "dep": "not a version"
+ │ ^^^^^^^^^^^^^^^
+ ╵''',
+ );
+ });
+
+ test('map w/ just version', () async {
+ final dep = await _dependency<HostedDependency>({'version': '^1.0.0'});
+ expect(dep.version.toString(), '^1.0.0');
+ expect(dep.hosted, isNull);
+ expect(dep.toString(), 'HostedDependency: ^1.0.0');
+ });
+
+ test('map w/ version and hosted as Map', () async {
+ final dep = await _dependency<HostedDependency>({
+ 'version': '^1.0.0',
+ 'hosted': {'name': 'hosted_name', 'url': 'https://hosted_url'},
+ });
+ expect(dep.version.toString(), '^1.0.0');
+ expect(dep.hosted!.name, 'hosted_name');
+ expect(dep.hosted!.url.toString(), 'https://hosted_url');
+ expect(dep.toString(), 'HostedDependency: ^1.0.0');
+ });
+
+ test('map /w hosted as a map without name', () async {
+ final dep = await _dependency<HostedDependency>(
+ {
+ 'version': '^1.0.0',
+ 'hosted': {'url': 'https://hosted_url'},
+ },
+ skipTryPub: true, // todo: Unskip once pub supports this syntax
+ );
+ expect(dep.version.toString(), '^1.0.0');
+ expect(dep.hosted!.declaredName, isNull);
+ expect(dep.hosted!.name, 'dep');
+ expect(dep.hosted!.url.toString(), 'https://hosted_url');
+ expect(dep.toString(), 'HostedDependency: ^1.0.0');
+ });
+
+ test('map w/ bad version value', () {
+ _expectThrows(
+ {
+ 'version': 'not a version',
+ 'hosted': {'name': 'hosted_name', 'url': 'hosted_url'},
+ },
+ r'''
+line 5, column 15: Unsupported value for "version". Could not parse version "not a version". Unknown text at "not a version".
+ ╷
+5 │ "version": "not a version",
+ │ ^^^^^^^^^^^^^^^
+ ╵''',
+ );
+ });
+
+ test('map w/ extra keys should fail', () {
+ _expectThrows(
+ {
+ 'version': '^1.0.0',
+ 'hosted': {'name': 'hosted_name', 'url': 'hosted_url'},
+ 'not_supported': null,
+ },
+ r'''
+line 10, column 4: Unrecognized keys: [not_supported]; supported keys: [sdk, git, path, hosted]
+ ╷
+10 │ "not_supported": null
+ │ ^^^^^^^^^^^^^^^
+ ╵''',
+ );
+ });
+
+ test('map w/ version and hosted as String', () async {
+ final dep = await _dependency<HostedDependency>(
+ {'version': '^1.0.0', 'hosted': 'hosted_url'},
+ skipTryPub: true, // todo: Unskip once put supports this
+ );
+ expect(dep.version.toString(), '^1.0.0');
+ expect(dep.hosted!.declaredName, isNull);
+ expect(dep.hosted!.name, 'dep');
+ expect(dep.hosted!.url, Uri.parse('hosted_url'));
+ expect(dep.toString(), 'HostedDependency: ^1.0.0');
+ });
+
+ test('map w/ hosted as String', () async {
+ final dep = await _dependency<HostedDependency>({'hosted': 'hosted_url'});
+ expect(dep.version, VersionConstraint.any);
+ expect(dep.hosted!.declaredName, isNull);
+ expect(dep.hosted!.name, 'dep');
+ expect(dep.hosted!.url, Uri.parse('hosted_url'));
+ expect(dep.toString(), 'HostedDependency: any');
+ });
+
+ test('map w/ null hosted should error', () {
+ _expectThrows(
+ {'hosted': null},
+ r'''
+line 5, column 4: These keys had `null` values, which is not allowed: [hosted]
+ ╷
+5 │ "hosted": null
+ │ ^^^^^^^^
+ ╵''',
+ );
+ });
+
+ test('map w/ null version is fine', () async {
+ final dep = await _dependency<HostedDependency>({'version': null});
+ expect(dep.version, VersionConstraint.any);
+ expect(dep.hosted, isNull);
+ expect(dep.toString(), 'HostedDependency: any');
+ });
+}
+
+void _sdkDependency() {
+ test('without version', () async {
+ final dep = await _dependency<SdkDependency>({'sdk': 'flutter'});
+ expect(dep.sdk, 'flutter');
+ expect(dep.version, VersionConstraint.any);
+ expect(dep.toString(), 'SdkDependency: flutter');
+ });
+
+ test('with version', () async {
+ final dep = await _dependency<SdkDependency>(
+ {'sdk': 'flutter', 'version': '>=1.2.3 <2.0.0'},
+ );
+ expect(dep.sdk, 'flutter');
+ expect(dep.version.toString(), '>=1.2.3 <2.0.0');
+ expect(dep.toString(), 'SdkDependency: flutter');
+ });
+
+ test('null content', () {
+ _expectThrowsContaining(
+ {'sdk': null},
+ r"type 'Null' is not a subtype of type 'String'",
+ );
+ });
+
+ test('number content', () {
+ _expectThrowsContaining(
+ {'sdk': 42},
+ r"type 'int' is not a subtype of type 'String'",
+ );
+ });
+}
+
+void _gitDependency() {
+ test('string', () async {
+ final dep = await _dependency<GitDependency>({'git': 'url'});
+ expect(dep.url.toString(), 'url');
+ expect(dep.path, isNull);
+ expect(dep.ref, isNull);
+ expect(dep.toString(), 'GitDependency: url@url');
+ });
+
+ test('string with version key is ignored', () async {
+ // Regression test for https://github.com/dart-lang/pubspec_parse/issues/13
+ final dep =
+ await _dependency<GitDependency>({'git': 'url', 'version': '^1.2.3'});
+ expect(dep.url.toString(), 'url');
+ expect(dep.path, isNull);
+ expect(dep.ref, isNull);
+ expect(dep.toString(), 'GitDependency: url@url');
+ });
+
+ test('string with user@ URL', () async {
+ final skipTryParse = Platform.environment.containsKey('TRAVIS');
+ if (skipTryParse) {
+ print('FYI: not validating git@ URI on travis due to failure');
+ }
+ final dep = await _dependency<GitDependency>(
+ {'git': 'git@localhost:dep.git'},
+ skipTryPub: skipTryParse,
+ );
+ expect(dep.url.toString(), 'ssh://git@localhost/dep.git');
+ expect(dep.path, isNull);
+ expect(dep.ref, isNull);
+ expect(dep.toString(), 'GitDependency: url@ssh://git@localhost/dep.git');
+ });
+
+ test('string with random extra key fails', () {
+ _expectThrows(
+ {'git': 'url', 'bob': '^1.2.3'},
+ r'''
+line 6, column 4: Unrecognized keys: [bob]; supported keys: [sdk, git, path, hosted]
+ ╷
+6 │ "bob": "^1.2.3"
+ │ ^^^^^
+ ╵''',
+ );
+ });
+
+ test('map', () async {
+ final dep = await _dependency<GitDependency>({
+ 'git': {'url': 'url', 'path': 'path', 'ref': 'ref'},
+ });
+ expect(dep.url.toString(), 'url');
+ expect(dep.path, 'path');
+ expect(dep.ref, 'ref');
+ expect(dep.toString(), 'GitDependency: url@url');
+ });
+
+ test('git - null content', () {
+ _expectThrows(
+ {'git': null},
+ r'''
+line 5, column 11: Unsupported value for "git". Must be a String or a Map.
+ ╷
+5 │ "git": null
+ │ ┌───────────^
+6 │ │ }
+ │ └──^
+ ╵''',
+ );
+ });
+
+ test('git - int content', () {
+ _expectThrows(
+ {'git': 42},
+ r'''
+line 5, column 11: Unsupported value for "git". Must be a String or a Map.
+ ╷
+5 │ "git": 42
+ │ ┌───────────^
+6 │ │ }
+ │ └──^
+ ╵''',
+ );
+ });
+
+ test('git - empty map', () {
+ _expectThrowsContaining(
+ {'git': <String, dynamic>{}},
+ r"type 'Null' is not a subtype of type 'String'",
+ );
+ });
+
+ test('git - null url', () {
+ _expectThrowsContaining(
+ {
+ 'git': {'url': null},
+ },
+ r"type 'Null' is not a subtype of type 'String'",
+ );
+ });
+
+ test('git - int url', () {
+ _expectThrowsContaining(
+ {
+ 'git': {'url': 42},
+ },
+ r"type 'int' is not a subtype of type 'String'",
+ );
+ });
+}
+
+void _pathDependency() {
+ test('valid', () async {
+ final dep = await _dependency<PathDependency>({'path': '../path'});
+ expect(dep.path, '../path');
+ expect(dep.toString(), 'PathDependency: path@../path');
+ });
+
+ test('valid with version key is ignored', () async {
+ final dep = await _dependency<PathDependency>(
+ {'path': '../path', 'version': '^1.2.3'},
+ );
+ expect(dep.path, '../path');
+ expect(dep.toString(), 'PathDependency: path@../path');
+ });
+
+ test('valid with random extra key fails', () {
+ _expectThrows(
+ {'path': '../path', 'bob': '^1.2.3'},
+ r'''
+line 6, column 4: Unrecognized keys: [bob]; supported keys: [sdk, git, path, hosted]
+ ╷
+6 │ "bob": "^1.2.3"
+ │ ^^^^^
+ ╵''',
+ );
+ });
+
+ test('null content', () {
+ _expectThrows(
+ {'path': null},
+ r'''
+line 5, column 12: Unsupported value for "path". Must be a String.
+ ╷
+5 │ "path": null
+ │ ┌────────────^
+6 │ │ }
+ │ └──^
+ ╵''',
+ );
+ });
+
+ test('int content', () {
+ _expectThrows(
+ {'path': 42},
+ r'''
+line 5, column 12: Unsupported value for "path". Must be a String.
+ ╷
+5 │ "path": 42
+ │ ┌────────────^
+6 │ │ }
+ │ └──^
+ ╵''',
+ );
+ });
+}
+
+void _expectThrows(Object content, String expectedError) {
+ expectParseThrows(
+ {
+ 'name': 'sample',
+ 'dependencies': {'dep': content},
+ },
+ expectedError,
+ );
+}
+
+void _expectThrowsContaining(Object content, String errorText) {
+ expectParseThrowsContaining(
+ {
+ 'name': 'sample',
+ 'dependencies': {'dep': content},
+ },
+ errorText,
+ );
+}
+
+Future<T> _dependency<T extends Dependency>(
+ Object? content, {
+ bool skipTryPub = false,
+}) async {
+ final value = await parse(
+ {
+ ...defaultPubspec,
+ 'dependencies': {'dep': content},
+ },
+ skipTryPub: skipTryPub,
+ );
+ expect(value.name, 'sample');
+ expect(value.dependencies, hasLength(1));
+
+ final entry = value.dependencies.entries.single;
+ expect(entry.key, 'dep');
+
+ return entry.value as T;
+}
diff --git a/pkgs/pubspec_parse/test/ensure_build_test.dart b/pkgs/pubspec_parse/test/ensure_build_test.dart
new file mode 100644
index 0000000..0e4371c
--- /dev/null
+++ b/pkgs/pubspec_parse/test/ensure_build_test.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.
+
+@Timeout.factor(2)
+@TestOn('vm')
+@Tags(['presubmit-only'])
+library;
+
+import 'package:build_verify/build_verify.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test(
+ 'ensure_build',
+ () => expectBuildClean(packageRelativeDirectory: 'pkgs/pubspec_parse/'),
+ );
+}
diff --git a/pkgs/pubspec_parse/test/git_uri_test.dart b/pkgs/pubspec_parse/test/git_uri_test.dart
new file mode 100644
index 0000000..be89ba8
--- /dev/null
+++ b/pkgs/pubspec_parse/test/git_uri_test.dart
@@ -0,0 +1,25 @@
+import 'package:pubspec_parse/src/dependency.dart';
+import 'package:test/test.dart';
+
+void main() {
+ for (var item in {
+ 'git@github.com:google/grinder.dart.git':
+ 'ssh://git@github.com/google/grinder.dart.git',
+ 'host.xz:path/to/repo.git/': 'ssh://host.xz/path/to/repo.git/',
+ 'http:path/to/repo.git/': 'ssh://http/path/to/repo.git/',
+ 'file:path/to/repo.git/': 'ssh://file/path/to/repo.git/',
+ './foo:bar': 'foo%3Abar',
+ '/path/to/repo.git/': '/path/to/repo.git/',
+ 'file:///path/to/repo.git/': 'file:///path/to/repo.git/',
+ }.entries) {
+ test(item.key, () {
+ final uri = parseGitUri(item.key);
+
+ printOnFailure(
+ [uri.scheme, uri.userInfo, uri.host, uri.port, uri.path].join('\n'),
+ );
+
+ expect(uri, Uri.parse(item.value));
+ });
+ }
+}
diff --git a/pkgs/pubspec_parse/test/parse_test.dart b/pkgs/pubspec_parse/test/parse_test.dart
new file mode 100644
index 0000000..6251f41
--- /dev/null
+++ b/pkgs/pubspec_parse/test/parse_test.dart
@@ -0,0 +1,715 @@
+// 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: deprecated_member_use_from_same_package
+// ignore_for_file: lines_longer_than_80_chars
+
+import 'package:pub_semver/pub_semver.dart';
+import 'package:test/test.dart';
+
+import 'test_utils.dart';
+
+void main() {
+ test('minimal set values', () async {
+ final value = await parse(defaultPubspec);
+ expect(value.name, 'sample');
+ expect(value.version, isNull);
+ expect(value.publishTo, isNull);
+ expect(value.description, isNull);
+ expect(value.homepage, isNull);
+ expect(value.author, isNull);
+ expect(value.authors, isEmpty);
+ expect(
+ value.environment,
+ {'sdk': VersionConstraint.parse('>=2.12.0 <3.0.0')},
+ );
+ expect(value.documentation, isNull);
+ expect(value.dependencies, isEmpty);
+ expect(value.devDependencies, isEmpty);
+ expect(value.dependencyOverrides, isEmpty);
+ expect(value.flutter, isNull);
+ expect(value.repository, isNull);
+ expect(value.issueTracker, isNull);
+ expect(value.screenshots, isEmpty);
+ });
+
+ test('all fields set', () async {
+ final version = Version.parse('1.2.3');
+ final sdkConstraint = VersionConstraint.parse('>=2.12.0 <3.0.0');
+ final value = await parse({
+ 'name': 'sample',
+ 'version': version.toString(),
+ 'publish_to': 'none',
+ 'author': 'name@example.com',
+ 'environment': {'sdk': sdkConstraint.toString()},
+ 'description': 'description',
+ 'homepage': 'homepage',
+ 'documentation': 'documentation',
+ 'repository': 'https://github.com/example/repo',
+ 'issue_tracker': 'https://github.com/example/repo/issues',
+ 'funding': [
+ 'https://patreon.com/example',
+ ],
+ 'topics': ['widget', 'button'],
+ 'ignored_advisories': ['111', '222'],
+ 'screenshots': [
+ {'description': 'my screenshot', 'path': 'path/to/screenshot'},
+ ],
+ });
+ expect(value.name, 'sample');
+ expect(value.version, version);
+ expect(value.publishTo, 'none');
+ expect(value.description, 'description');
+ expect(value.homepage, 'homepage');
+ expect(value.author, 'name@example.com');
+ expect(value.authors, ['name@example.com']);
+ expect(value.environment, hasLength(1));
+ expect(value.environment, containsPair('sdk', sdkConstraint));
+ expect(value.documentation, 'documentation');
+ expect(value.dependencies, isEmpty);
+ expect(value.devDependencies, isEmpty);
+ expect(value.dependencyOverrides, isEmpty);
+ expect(value.repository, Uri.parse('https://github.com/example/repo'));
+ expect(
+ value.issueTracker,
+ Uri.parse('https://github.com/example/repo/issues'),
+ );
+ expect(value.funding, hasLength(1));
+ expect(value.funding!.single.toString(), 'https://patreon.com/example');
+ expect(value.topics, hasLength(2));
+ expect(value.topics!.first, 'widget');
+ expect(value.topics!.last, 'button');
+ expect(value.ignoredAdvisories, hasLength(2));
+ expect(value.ignoredAdvisories!.first, '111');
+ expect(value.ignoredAdvisories!.last, '222');
+ expect(value.screenshots, hasLength(1));
+ expect(value.screenshots!.first.description, 'my screenshot');
+ expect(value.screenshots!.first.path, 'path/to/screenshot');
+ });
+
+ test('environment values can be null', () async {
+ final value = await parse(
+ {
+ 'name': 'sample',
+ 'environment': {
+ 'sdk': '>=2.12.0 <3.0.0',
+ 'bob': null,
+ },
+ },
+ skipTryPub: true,
+ );
+ expect(value.name, 'sample');
+ expect(value.environment, hasLength(2));
+ expect(value.environment, containsPair('bob', isNull));
+ });
+
+ group('publish_to', () {
+ for (var entry in {
+ 42: "Unsupported value for \"publish_to\". type 'int' is not a subtype of type 'String?'",
+ '##not a uri!': r'''
+line 3, column 16: Unsupported value for "publish_to". Must be an http or https URL.
+ ╷
+3 │ "publish_to": "##not a uri!"
+ │ ^^^^^^^^^^^^^^
+ ╵''',
+ '/cool/beans': r'''
+line 3, column 16: Unsupported value for "publish_to". Must be an http or https URL.
+ ╷
+3 │ "publish_to": "/cool/beans"
+ │ ^^^^^^^^^^^^^
+ ╵''',
+ 'file:///Users/kevmoo/': r'''
+line 3, column 16: Unsupported value for "publish_to". Must be an http or https URL.
+ ╷
+3 │ "publish_to": "file:///Users/kevmoo/"
+ │ ^^^^^^^^^^^^^^^^^^^^^^^
+ ╵''',
+ }.entries) {
+ test('cannot be `${entry.key}`', () {
+ expectParseThrowsContaining(
+ {'name': 'sample', 'publish_to': entry.key},
+ entry.value,
+ skipTryPub: true,
+ );
+ });
+ }
+
+ for (var entry in {
+ null: null,
+ 'http': 'http://example.com',
+ 'https': 'https://example.com',
+ 'none': 'none',
+ }.entries) {
+ test('can be ${entry.key}', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'publish_to': entry.value,
+ });
+ expect(value.publishTo, entry.value);
+ });
+ }
+ });
+
+ group('author, authors', () {
+ test('one author', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'author': 'name@example.com',
+ });
+ expect(value.author, 'name@example.com');
+ expect(value.authors, ['name@example.com']);
+ });
+
+ test('one author, via authors', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'authors': ['name@example.com'],
+ });
+ expect(value.author, 'name@example.com');
+ expect(value.authors, ['name@example.com']);
+ });
+
+ test('many authors', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'authors': ['name@example.com', 'name2@example.com'],
+ });
+ expect(value.author, isNull);
+ expect(value.authors, ['name@example.com', 'name2@example.com']);
+ });
+
+ test('author and authors', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'author': 'name@example.com',
+ 'authors': ['name2@example.com'],
+ });
+ expect(value.author, isNull);
+ expect(value.authors, ['name@example.com', 'name2@example.com']);
+ });
+
+ test('duplicate author values', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'author': 'name@example.com',
+ 'authors': ['name@example.com', 'name@example.com'],
+ });
+ expect(value.author, 'name@example.com');
+ expect(value.authors, ['name@example.com']);
+ });
+
+ test('flutter', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'flutter': {'key': 'value'},
+ });
+ expect(value.flutter, {'key': 'value'});
+ });
+ });
+
+ group('invalid', () {
+ test('null', () {
+ expectParseThrows(
+ null,
+ r'''
+line 1, column 1: Not a map
+ ╷
+1 │ null
+ │ ^^^^
+ ╵''',
+ );
+ });
+ test('empty string', () {
+ expectParseThrows(
+ '',
+ r'''
+line 1, column 1: Not a map
+ ╷
+1 │ ""
+ │ ^^
+ ╵''',
+ );
+ });
+ test('array', () {
+ expectParseThrows(
+ [],
+ r'''
+line 1, column 1: Not a map
+ ╷
+1 │ []
+ │ ^^
+ ╵''',
+ );
+ });
+
+ test('missing name', () {
+ expectParseThrowsContaining(
+ {},
+ "Missing key \"name\". type 'Null' is not a subtype of type 'String'",
+ );
+ });
+
+ test('null name value', () {
+ expectParseThrowsContaining(
+ {'name': null},
+ "Unsupported value for \"name\". type 'Null' is not a subtype of type 'String'",
+ );
+ });
+
+ test('empty name value', () {
+ expectParseThrows(
+ {'name': ''},
+ r'''
+line 2, column 10: Unsupported value for "name". "name" cannot be empty.
+ ╷
+2 │ "name": ""
+ │ ^^
+ ╵''',
+ );
+ });
+
+ test('"dart" is an invalid environment key', () {
+ expectParseThrows(
+ {
+ 'name': 'sample',
+ 'environment': {'dart': 'cool'},
+ },
+ r'''
+line 4, column 3: Use "sdk" to for Dart SDK constraints.
+ ╷
+4 │ "dart": "cool"
+ │ ^^^^^^
+ ╵''',
+ );
+ });
+
+ test('environment values cannot be int', () {
+ expectParseThrows(
+ {
+ 'name': 'sample',
+ 'environment': {'sdk': 42},
+ },
+ r'''
+line 4, column 10: Unsupported value for "sdk". `42` is not a String.
+ ╷
+4 │ "sdk": 42
+ │ ┌──────────^
+5 │ │ }
+ │ └─^
+ ╵''',
+ );
+ });
+
+ test('version', () {
+ expectParseThrows(
+ {'name': 'sample', 'version': 'invalid'},
+ r'''
+line 3, column 13: Unsupported value for "version". Could not parse "invalid".
+ ╷
+3 │ "version": "invalid"
+ │ ^^^^^^^^^
+ ╵''',
+ );
+ });
+
+ test('invalid environment value', () {
+ expectParseThrows(
+ {
+ 'name': 'sample',
+ 'environment': {'sdk': 'silly'},
+ },
+ r'''
+line 4, column 10: Unsupported value for "sdk". Could not parse version "silly". Unknown text at "silly".
+ ╷
+4 │ "sdk": "silly"
+ │ ^^^^^^^
+ ╵''',
+ );
+ });
+
+ test('bad repository url', () {
+ expectParseThrowsContaining(
+ {
+ ...defaultPubspec,
+ 'repository': {'x': 'y'},
+ },
+ "Unsupported value for \"repository\". type 'YamlMap' is not a subtype of type 'String'",
+ skipTryPub: true,
+ );
+ });
+
+ test('bad issue_tracker url', () {
+ expectParseThrowsContaining(
+ {
+ 'name': 'sample',
+ 'issue_tracker': {'x': 'y'},
+ },
+ "Unsupported value for \"issue_tracker\". type 'YamlMap' is not a subtype of type 'String'",
+ skipTryPub: true,
+ );
+ });
+ });
+
+ group('funding', () {
+ test('not a list', () {
+ expectParseThrowsContaining(
+ {
+ ...defaultPubspec,
+ 'funding': 1,
+ },
+ "Unsupported value for \"funding\". type 'int' is not a subtype of type 'List<dynamic>?'",
+ skipTryPub: true,
+ );
+ });
+
+ test('not an uri', () {
+ expectParseThrowsContaining(
+ {
+ ...defaultPubspec,
+ 'funding': [1],
+ },
+ "Unsupported value for \"funding\". type 'int' is not a subtype of type 'String'",
+ skipTryPub: true,
+ );
+ });
+
+ test('not an uri', () {
+ expectParseThrows(
+ {
+ ...defaultPubspec,
+ 'funding': ['ht tps://example.com/'],
+ },
+ r'''
+line 6, column 13: Unsupported value for "funding". Illegal scheme character at offset 2.
+ ╷
+6 │ "funding": [
+ │ ┌─────────────^
+7 │ │ "ht tps://example.com/"
+8 │ └ ]
+ ╵''',
+ skipTryPub: true,
+ );
+ });
+ });
+ group('topics', () {
+ test('not a list', () {
+ expectParseThrowsContaining(
+ {
+ ...defaultPubspec,
+ 'topics': 1,
+ },
+ "Unsupported value for \"topics\". type 'int' is not a subtype of type 'List<dynamic>?'",
+ skipTryPub: true,
+ );
+ });
+
+ test('not a string', () {
+ expectParseThrowsContaining(
+ {
+ ...defaultPubspec,
+ 'topics': [1],
+ },
+ "Unsupported value for \"topics\". type 'int' is not a subtype of type 'String'",
+ skipTryPub: true,
+ );
+ });
+
+ test('invalid data - lenient', () async {
+ final value = await parse(
+ {
+ ...defaultPubspec,
+ 'topics': [1],
+ },
+ skipTryPub: true,
+ lenient: true,
+ );
+ expect(value.name, 'sample');
+ expect(value.topics, isNull);
+ });
+ });
+
+ group('ignored_advisories', () {
+ test('not a list', () {
+ expectParseThrowsContaining(
+ {
+ ...defaultPubspec,
+ 'ignored_advisories': 1,
+ },
+ "Unsupported value for \"ignored_advisories\". type 'int' is not a subtype of type 'List<dynamic>?'",
+ skipTryPub: true,
+ );
+ });
+
+ test('not a string', () {
+ expectParseThrowsContaining(
+ {
+ ...defaultPubspec,
+ 'ignored_advisories': [1],
+ },
+ "Unsupported value for \"ignored_advisories\". type 'int' is not a subtype of type 'String'",
+ skipTryPub: true,
+ );
+ });
+
+ test('invalid data - lenient', () async {
+ final value = await parse(
+ {
+ ...defaultPubspec,
+ 'ignored_advisories': [1],
+ },
+ skipTryPub: true,
+ lenient: true,
+ );
+ expect(value.name, 'sample');
+ expect(value.ignoredAdvisories, isNull);
+ });
+ });
+
+ group('screenshots', () {
+ test('one screenshot', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'screenshots': [
+ {'description': 'my screenshot', 'path': 'path/to/screenshot'},
+ ],
+ });
+ expect(value.screenshots, hasLength(1));
+ expect(value.screenshots!.first.description, 'my screenshot');
+ expect(value.screenshots!.first.path, 'path/to/screenshot');
+ });
+
+ test('many screenshots', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'screenshots': [
+ {'description': 'my screenshot', 'path': 'path/to/screenshot'},
+ {
+ 'description': 'my second screenshot',
+ 'path': 'path/to/screenshot2',
+ },
+ ],
+ });
+ expect(value.screenshots, hasLength(2));
+ expect(value.screenshots!.first.description, 'my screenshot');
+ expect(value.screenshots!.first.path, 'path/to/screenshot');
+ expect(value.screenshots!.last.description, 'my second screenshot');
+ expect(value.screenshots!.last.path, 'path/to/screenshot2');
+ });
+
+ test('one screenshot plus invalid entries', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'screenshots': [
+ 42,
+ {
+ 'description': 'my screenshot',
+ 'path': 'path/to/screenshot',
+ 'extraKey': 'not important',
+ },
+ 'not a screenshot',
+ ],
+ });
+ expect(value.screenshots, hasLength(1));
+ expect(value.screenshots!.first.description, 'my screenshot');
+ expect(value.screenshots!.first.path, 'path/to/screenshot');
+ });
+
+ test('invalid entries', () async {
+ final value = await parse({
+ ...defaultPubspec,
+ 'screenshots': [
+ 42,
+ 'not a screenshot',
+ ],
+ });
+ expect(value.screenshots, isEmpty);
+ });
+
+ test('missing key `dessription', () {
+ expectParseThrows(
+ {
+ ...defaultPubspec,
+ 'screenshots': [
+ {'path': 'my/path'},
+ ],
+ },
+ r'''
+line 7, column 3: Missing key "description". Missing required key `description`
+ ╷
+7 │ ┌ {
+8 │ │ "path": "my/path"
+9 │ └ }
+ ╵''',
+ skipTryPub: true,
+ );
+ });
+
+ test('missing key `path`', () {
+ expectParseThrows(
+ {
+ ...defaultPubspec,
+ 'screenshots': [
+ {'description': 'my screenshot'},
+ ],
+ },
+ r'''
+line 7, column 3: Missing key "path". Missing required key `path`
+ ╷
+7 │ ┌ {
+8 │ │ "description": "my screenshot"
+9 │ └ }
+ ╵''',
+ skipTryPub: true,
+ );
+ });
+
+ test('Value of description not a String`', () {
+ expectParseThrows(
+ {
+ ...defaultPubspec,
+ 'screenshots': [
+ {'description': 42},
+ ],
+ },
+ r'''
+line 8, column 19: Unsupported value for "description". `42` is not a String
+ ╷
+8 │ "description": 42
+ │ ┌───────────────────^
+9 │ │ }
+ │ └──^
+ ╵''',
+ skipTryPub: true,
+ );
+ });
+
+ test('Value of path not a String`', () {
+ expectParseThrows(
+ {
+ ...defaultPubspec,
+ 'screenshots': [
+ {
+ 'description': '',
+ 'path': 42,
+ },
+ ],
+ },
+ r'''
+line 9, column 12: Unsupported value for "path". `42` is not a String
+ ╷
+9 │ "path": 42
+ │ ┌────────────^
+10 │ │ }
+ │ └──^
+ ╵''',
+ skipTryPub: true,
+ );
+ });
+
+ test('invalid screenshot - lenient', () async {
+ final value = await parse(
+ {
+ ...defaultPubspec,
+ 'screenshots': 'Invalid value',
+ },
+ lenient: true,
+ );
+ expect(value.name, 'sample');
+ expect(value.screenshots, isEmpty);
+ });
+ });
+
+ group('lenient', () {
+ test('null', () {
+ expectParseThrows(
+ null,
+ r'''
+line 1, column 1: Not a map
+ ╷
+1 │ null
+ │ ^^^^
+ ╵''',
+ lenient: true,
+ );
+ });
+
+ test('empty string', () {
+ expectParseThrows(
+ '',
+ r'''
+line 1, column 1: Not a map
+ ╷
+1 │ ""
+ │ ^^
+ ╵''',
+ lenient: true,
+ );
+ });
+
+ test('name cannot be empty', () {
+ expectParseThrowsContaining(
+ {},
+ "Missing key \"name\". type 'Null' is not a subtype of type 'String'",
+ lenient: true,
+ );
+ });
+
+ test('bad repository url', () async {
+ final value = await parse(
+ {
+ ...defaultPubspec,
+ 'repository': {'x': 'y'},
+ },
+ lenient: true,
+ );
+ expect(value.name, 'sample');
+ expect(value.repository, isNull);
+ });
+
+ test('bad issue_tracker url', () async {
+ final value = await parse(
+ {
+ ...defaultPubspec,
+ 'issue_tracker': {'x': 'y'},
+ },
+ lenient: true,
+ );
+ expect(value.name, 'sample');
+ expect(value.issueTracker, isNull);
+ });
+
+ test('multiple bad values', () async {
+ final value = await parse(
+ {
+ ...defaultPubspec,
+ 'repository': {'x': 'y'},
+ 'issue_tracker': {'x': 'y'},
+ },
+ lenient: true,
+ );
+ expect(value.name, 'sample');
+ expect(value.repository, isNull);
+ expect(value.issueTracker, isNull);
+ });
+
+ test('deep error throws with lenient', () {
+ expect(
+ () => parse(
+ {
+ 'name': 'sample',
+ 'dependencies': {
+ 'foo': {
+ 'git': {'url': 1},
+ },
+ },
+ 'issue_tracker': {'x': 'y'},
+ },
+ skipTryPub: true,
+ lenient: true,
+ ),
+ throwsException,
+ );
+ });
+ });
+}
diff --git a/pkgs/pubspec_parse/test/pub_utils.dart b/pkgs/pubspec_parse/test/pub_utils.dart
new file mode 100644
index 0000000..a60aa2a
--- /dev/null
+++ b/pkgs/pubspec_parse/test/pub_utils.dart
@@ -0,0 +1,88 @@
+// 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 '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';
+
+Future<ProcResult> tryPub(String content) async {
+ await d.file('pubspec.yaml', content).create();
+
+ final proc = await TestProcess.start(
+ Platform.resolvedExecutable,
+ ['pub', 'get', '--offline'],
+ workingDirectory: d.sandbox,
+ // Don't pass current process VM options to child
+ environment: Map.from(Platform.environment)..remove('DART_VM_OPTIONS'),
+ );
+
+ final result = await ProcResult.fromTestProcess(proc);
+
+ printOnFailure(
+ [
+ '-----BEGIN pub output-----',
+ result.toString().trim(),
+ '-----END pub output-----',
+ ].join('\n'),
+ );
+
+ if (result.exitCode == 0) {
+ final lockContent =
+ File(p.join(d.sandbox, 'pubspec.lock')).readAsStringSync();
+
+ printOnFailure(
+ [
+ '-----BEGIN pubspec.lock-----',
+ lockContent.trim(),
+ '-----END pubspec.lock-----',
+ ].join('\n'),
+ );
+ }
+
+ return result;
+}
+
+class ProcResult {
+ final int exitCode;
+ final List<ProcLine> lines;
+
+ bool get cleanParse => exitCode == 0 || exitCode == 66 || exitCode == 69;
+
+ ProcResult(this.exitCode, this.lines);
+
+ static Future<ProcResult> fromTestProcess(TestProcess proc) async {
+ final items = <ProcLine>[];
+
+ final values = await Future.wait([
+ proc.exitCode,
+ proc.stdoutStream().forEach((line) => items.add(ProcLine(false, line))),
+ proc.stderrStream().forEach((line) => items.add(ProcLine(true, line))),
+ ]);
+
+ return ProcResult(values[0] as int, items);
+ }
+
+ @override
+ String toString() {
+ final buffer = StringBuffer('Exit code: $exitCode');
+ for (var line in lines) {
+ buffer.write('\n$line');
+ }
+ return buffer.toString();
+ }
+}
+
+class ProcLine {
+ final bool isError;
+ final String line;
+
+ ProcLine(this.isError, this.line);
+
+ @override
+ String toString() => '${isError ? 'err' : 'out'} $line';
+}
diff --git a/pkgs/pubspec_parse/test/test_utils.dart b/pkgs/pubspec_parse/test/test_utils.dart
new file mode 100644
index 0000000..cc46522
--- /dev/null
+++ b/pkgs/pubspec_parse/test/test_utils.dart
@@ -0,0 +1,157 @@
+// 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:convert';
+
+import 'package:checked_yaml/checked_yaml.dart';
+import 'package:json_annotation/json_annotation.dart';
+import 'package:pubspec_parse/pubspec_parse.dart';
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import 'pub_utils.dart';
+
+const defaultPubspec = {
+ 'name': 'sample',
+ 'environment': {'sdk': '>=2.12.0 <3.0.0'},
+};
+
+String _encodeJson(Object? input) =>
+ const JsonEncoder.withIndent(' ').convert(input);
+
+Matcher _throwsParsedYamlException(String prettyValue) => throwsA(
+ const TypeMatcher<ParsedYamlException>().having(
+ (e) {
+ final message = e.formattedMessage;
+ printOnFailure("Actual error format:\nr'''\n$message'''");
+ _printDebugParsedYamlException(e);
+ return message;
+ },
+ 'formattedMessage',
+ prettyValue,
+ ),
+ );
+
+void _printDebugParsedYamlException(ParsedYamlException e) {
+ var innerError = e.innerError;
+ StackTrace? innerStack;
+
+ if (innerError is CheckedFromJsonException) {
+ final cfje = innerError;
+
+ if (cfje.innerError != null) {
+ innerError = cfje.innerError;
+ innerStack = cfje.innerStack;
+ }
+ }
+
+ if (innerError != null) {
+ final items = [innerError];
+ if (innerStack != null) {
+ items.add(Trace.format(innerStack));
+ }
+
+ final content =
+ LineSplitter.split(items.join('\n')).map((e) => ' $e').join('\n');
+
+ printOnFailure('Inner error details:\n$content');
+ }
+}
+
+Future<Pubspec> parse(
+ Object? content, {
+ bool quietOnError = false,
+ bool skipTryPub = false,
+ bool lenient = false,
+}) async {
+ final encoded = _encodeJson(content);
+
+ ProcResult? pubResult;
+ if (!skipTryPub) {
+ // ignore: deprecated_member_use
+ pubResult = await tryPub(encoded);
+ expect(pubResult, isNotNull);
+ }
+
+ try {
+ final value = Pubspec.parse(encoded, lenient: lenient);
+
+ if (pubResult != null) {
+ addTearDown(() {
+ expect(
+ pubResult!.cleanParse,
+ isTrue,
+ reason:
+ 'On success, parsing from the pub client should also succeed.',
+ );
+ });
+ }
+ return value;
+ } catch (e) {
+ if (pubResult != null) {
+ addTearDown(() {
+ expect(
+ pubResult!.cleanParse,
+ isFalse,
+ reason: 'On failure, parsing from the pub client should also fail.',
+ );
+ });
+ }
+ if (e is ParsedYamlException) {
+ if (!quietOnError) {
+ _printDebugParsedYamlException(e);
+ }
+ }
+ rethrow;
+ }
+}
+
+void expectParseThrows(
+ Object? content,
+ String expectedError, {
+ bool skipTryPub = false,
+ bool lenient = false,
+}) =>
+ expect(
+ () => parse(
+ content,
+ lenient: lenient,
+ quietOnError: true,
+ skipTryPub: skipTryPub,
+ ),
+ _throwsParsedYamlException(expectedError),
+ );
+
+void expectParseThrowsContaining(
+ Object? content,
+ String errorFragment, {
+ bool skipTryPub = false,
+ bool lenient = false,
+}) {
+ expect(
+ () => parse(
+ content,
+ lenient: lenient,
+ quietOnError: true,
+ skipTryPub: skipTryPub,
+ ),
+ _throwsParsedYamlExceptionContaining(errorFragment),
+ );
+}
+
+// ignore: prefer_expression_function_bodies
+Matcher _throwsParsedYamlExceptionContaining(String errorFragment) {
+ return throwsA(
+ const TypeMatcher<ParsedYamlException>().having(
+ (e) {
+ final message = e.formattedMessage;
+ printOnFailure("Actual error format:\nr'''\n$message'''");
+ _printDebugParsedYamlException(e);
+ return message;
+ },
+ 'formattedMessage',
+ contains(errorFragment),
+ ),
+ );
+}
diff --git a/pkgs/source_maps/CHANGELOG.md b/pkgs/source_maps/CHANGELOG.md
index ae7711e..b06ac72 100644
--- a/pkgs/source_maps/CHANGELOG.md
+++ b/pkgs/source_maps/CHANGELOG.md
@@ -1,3 +1,5 @@
+## 0.10.14-wip
+
## 0.10.13
* Require Dart 3.3
diff --git a/pkgs/source_maps/lib/builder.dart b/pkgs/source_maps/lib/builder.dart
index 54ba743..9043c63 100644
--- a/pkgs/source_maps/lib/builder.dart
+++ b/pkgs/source_maps/lib/builder.dart
@@ -3,7 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
/// Contains a builder object useful for creating source maps programatically.
-library source_maps.builder;
+library;
// TODO(sigmund): add a builder for multi-section mappings.
diff --git a/pkgs/source_maps/lib/parser.dart b/pkgs/source_maps/lib/parser.dart
index b699ac7..590dfc6 100644
--- a/pkgs/source_maps/lib/parser.dart
+++ b/pkgs/source_maps/lib/parser.dart
@@ -3,7 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
/// Contains the top-level function to parse source maps version 3.
-library source_maps.parser;
+library;
import 'dart:convert';
diff --git a/pkgs/source_maps/lib/printer.dart b/pkgs/source_maps/lib/printer.dart
index 17733cd..32523d6 100644
--- a/pkgs/source_maps/lib/printer.dart
+++ b/pkgs/source_maps/lib/printer.dart
@@ -3,7 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
/// Contains a code printer that generates code by recording the source maps.
-library source_maps.printer;
+library;
import 'package:source_span/source_span.dart';
diff --git a/pkgs/source_maps/lib/refactor.dart b/pkgs/source_maps/lib/refactor.dart
index 98e0c93..a518a0c 100644
--- a/pkgs/source_maps/lib/refactor.dart
+++ b/pkgs/source_maps/lib/refactor.dart
@@ -6,7 +6,7 @@
///
/// [TextEditTransaction] supports making a series of changes to a text buffer.
/// [guessIndent] helps to guess the appropriate indentiation for the new code.
-library source_maps.refactor;
+library;
import 'package:source_span/source_span.dart';
diff --git a/pkgs/source_maps/lib/source_maps.dart b/pkgs/source_maps/lib/source_maps.dart
index 58f805a..244dee7 100644
--- a/pkgs/source_maps/lib/source_maps.dart
+++ b/pkgs/source_maps/lib/source_maps.dart
@@ -24,7 +24,7 @@
/// var mapping = parse(json);
/// mapping.spanFor(outputSpan1.line, outputSpan1.column)
/// ```
-library source_maps;
+library;
import 'package:source_span/source_span.dart';
diff --git a/pkgs/source_maps/lib/src/utils.dart b/pkgs/source_maps/lib/src/utils.dart
index f70531e..ba04fbb 100644
--- a/pkgs/source_maps/lib/src/utils.dart
+++ b/pkgs/source_maps/lib/src/utils.dart
@@ -3,7 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
/// Utilities that shouldn't be in this package.
-library source_maps.utils;
+library;
/// Find the first entry in a sorted [list] that matches a monotonic predicate.
/// Given a result `n`, that all items before `n` will not match, `n` matches,
diff --git a/pkgs/source_maps/lib/src/vlq.dart b/pkgs/source_maps/lib/src/vlq.dart
index 61b4768..3b0562d 100644
--- a/pkgs/source_maps/lib/src/vlq.dart
+++ b/pkgs/source_maps/lib/src/vlq.dart
@@ -10,7 +10,7 @@
/// represented by using the least significant bit of the value as the sign bit.
///
/// For more details see the source map [version 3 documentation](https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?usp=sharing).
-library source_maps.src.vlq;
+library;
import 'dart:math';
diff --git a/pkgs/source_maps/pubspec.yaml b/pkgs/source_maps/pubspec.yaml
index 8518fa7..32cbf4f 100644
--- a/pkgs/source_maps/pubspec.yaml
+++ b/pkgs/source_maps/pubspec.yaml
@@ -1,5 +1,5 @@
name: source_maps
-version: 0.10.13
+version: 0.10.14-wip
description: A library to programmatically manipulate source map files.
repository: https://github.com/dart-lang/tools/tree/main/pkgs/source_maps
@@ -10,6 +10,6 @@
source_span: ^1.8.0
dev_dependencies:
- dart_flutter_team_lints: ^2.0.0
+ dart_flutter_team_lints: ^3.0.0
term_glyph: ^1.2.0
test: ^1.16.0
diff --git a/pkgs/source_maps/test/common.dart b/pkgs/source_maps/test/common.dart
index f6139de..e225ff5 100644
--- a/pkgs/source_maps/test/common.dart
+++ b/pkgs/source_maps/test/common.dart
@@ -3,7 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
/// Common input/output used by builder, parser and end2end tests
-library test.common;
+library;
import 'package:source_maps/source_maps.dart';
import 'package:source_span/source_span.dart';
diff --git a/pkgs/source_maps/test/utils_test.dart b/pkgs/source_maps/test/utils_test.dart
index 4abdce2..2516d1e 100644
--- a/pkgs/source_maps/test/utils_test.dart
+++ b/pkgs/source_maps/test/utils_test.dart
@@ -3,7 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
/// Tests for the binary search utility algorithm.
-library test.utils_test;
+library;
import 'package:source_maps/src/utils.dart';
import 'package:test/test.dart';
diff --git a/pkgs/stack_trace/.gitignore b/pkgs/stack_trace/.gitignore
new file mode 100644
index 0000000..f023015
--- /dev/null
+++ b/pkgs/stack_trace/.gitignore
@@ -0,0 +1,6 @@
+# See https://dart.dev/guides/libraries/private-files
+# Don’t commit the following directories created by pub.
+.dart_tool/
+.packages
+.pub/
+pubspec.lock
diff --git a/pkgs/stack_trace/CHANGELOG.md b/pkgs/stack_trace/CHANGELOG.md
new file mode 100644
index 0000000..e92cf9c
--- /dev/null
+++ b/pkgs/stack_trace/CHANGELOG.md
@@ -0,0 +1,363 @@
+## 1.12.1
+
+* Move to `dart-lang/tools` monorepo.
+
+## 1.12.0
+
+* Added support for parsing Wasm frames of Chrome (V8), Firefox, Safari.
+* Require Dart 3.4 or greater
+
+## 1.11.1
+
+* Make use of `@pragma('vm:awaiter-link')` to make package work better with
+ Dart VM's builtin awaiter stack unwinding. No other changes.
+
+## 1.11.0
+
+* Added the parameter `zoneValues` to `Chain.capture` to be able to use custom
+ zone values with the `runZoned` internal calls.
+* Populate the pubspec `repository` field.
+* Require Dart 2.18 or greater
+
+## 1.10.0
+
+* Stable release for null safety.
+* Fix broken test, `test/chain/vm_test.dart`, which incorrectly handles
+ asynchronous suspension gap markers at the end of stack traces.
+
+## 1.10.0-nullsafety.6
+
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+ traces, when parsing with `Trace.parse` and `Chain.parse`.
+* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release
+ guidelines.
+
+## 1.10.0-nullsafety.5
+
+* Allow prerelease versions of the 2.12 sdk.
+
+## 1.10.0-nullsafety.4
+
+* Allow the `2.10.0` stable and dev SDKs.
+
+## 1.10.0-nullsafety.3
+
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+ traces.
+
+## 1.10.0-nullsafety.2
+
+* Forward fix for a change in SDK type promotion behavior.
+
+## 1.10.0-nullsafety.1
+
+* Allow 2.10 stable and 2.11.0 dev SDK versions.
+
+## 1.10.0-nullsafety
+
+* Opt in to null safety.
+
+## 1.9.6 (backpublish)
+
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+ traces. (Also fixed separately in 1.10.0-nullsafety.3)
+* Fix bug parsing asynchronous suspension gap markers at the end of stack
+ traces, when parsing with `Trace.parse` and `Chain.parse`. (Also fixed
+ separately in 1.10.0-nullsafety.6)
+
+## 1.9.5
+
+* Parse the format for `data:` URIs that the Dart VM has used since `2.2.0`.
+
+## 1.9.4
+
+* Add support for firefox anonymous stack traces.
+* Add support for chrome eval stack traces without a column.
+* Change the argument type to `Chain.capture` from `Function(dynamic, Chain)` to
+ `Function(Object, Chain)`. Existing functions which take `dynamic` are still
+ fine, but new uses can have a safer type.
+
+## 1.9.3
+
+* Set max SDK version to `<3.0.0`.
+
+## 1.9.2
+
+* Fix Dart 2.0 runtime cast failure in test.
+
+## 1.9.1
+
+* Preserve the original chain for a trace to handle cases where an
+ error is rethrown.
+
+## 1.9.0
+
+* Add an `errorZone` parameter to `Chain.capture()` that makes it avoid creating
+ an error zone.
+
+## 1.8.3
+
+* `Chain.forTrace()` now returns a full stack chain for *all* `StackTrace`s
+ within `Chain.capture()`, even those that haven't been processed by
+ `dart:async` yet.
+
+* `Chain.forTrace()` now uses the Dart VM's stack chain information when called
+ synchronously within `Chain.capture()`. This matches the existing behavior
+ outside `Chain.capture()`.
+
+* `Chain.forTrace()` now trims the VM's stack chains for the innermost stack
+ trace within `Chain.capture()` (unless it's called synchronously, as above).
+ This avoids duplicated frames and makes the format of the innermost traces
+ consistent with the other traces in the chain.
+
+## 1.8.2
+
+* Update to use strong-mode clean Zone API.
+
+## 1.8.1
+
+* Use official generic function syntax.
+
+* Updated minimum SDK to 1.23.0.
+
+## 1.8.0
+
+* Add a `Trace.original` field to provide access to the original `StackTrace`s
+ from which the `Trace` was created, and a matching constructor parameter to
+ `new Trace()`.
+
+## 1.7.4
+
+* Always run `onError` callbacks for `Chain.capture()` in the parent zone.
+
+## 1.7.3
+
+* Fix broken links in the README.
+
+## 1.7.2
+
+* `Trace.foldFrames()` and `Chain.foldFrames()` now remove the outermost folded
+ frame. This matches the behavior of `.terse` with core frames.
+
+* Fix bug parsing a friendly frame with spaces in the member name.
+
+* Fix bug parsing a friendly frame where the location is a data url.
+
+## 1.7.1
+
+* Make `Trace.parse()`, `Chain.parse()`, treat the VM's new causal asynchronous
+ stack traces as chains. Outside of a `Chain.capture()` block, `new
+ Chain.current()` will return a stack chain constructed from the asynchronous
+ stack traces.
+
+## 1.7.0
+
+* Add a `Chain.disable()` function that disables stack-chain tracking.
+
+* Fix a bug where `Chain.capture(..., when: false)` would throw if an error was
+ emitted without a stack trace.
+
+## 1.6.8
+
+* Add a note to the documentation of `Chain.terse` and `Trace.terse`.
+
+## 1.6.7
+
+* Fix a bug where `new Frame.caller()` returned the wrong depth of frame on
+ Dartium.
+
+## 1.6.6
+
+* `new Trace.current()` and `new Chain.current()` now skip an extra frame when
+ run in a JS context. This makes their return values match the VM context.
+
+## 1.6.5
+
+* Really fix strong mode warnings.
+
+## 1.6.4
+
+* Fix a syntax error introduced in 1.6.3.
+
+## 1.6.3
+
+* Make `Chain.capture()` generic. Its signature is now `T Chain.capture<T>(T
+ callback(), ...)`.
+
+## 1.6.2
+
+* Fix all strong mode warnings.
+
+## 1.6.1
+
+* Use `StackTrace.current` in Dart SDK 1.14 to get the current stack trace.
+
+## 1.6.0
+
+* Add a `when` parameter to `Chain.capture()`. This allows capturing to be
+ easily enabled and disabled based on whether the application is running in
+ debug/development mode or not.
+
+* Deprecate the `ChainHandler` typedef. This didn't provide any value over
+ directly annotating the function argument, and it made the documentation less
+ clear.
+
+## 1.5.1
+
+* Fix a crash in `Chain.foldFrames()` and `Chain.terse` when one of the chain's
+ traces has no frames.
+
+## 1.5.0
+
+* `new Chain.parse()` now parses all the stack trace formats supported by `new
+ Trace.parse()`. Formats other than that emitted by `Chain.toString()` will
+ produce single-element chains.
+
+* `new Trace.parse()` now parses the output of `Chain.toString()`. It produces
+ the same result as `Chain.parse().toTrace()`.
+
+## 1.4.2
+
+* Improve the display of `data:` URIs in stack traces.
+
+## 1.4.1
+
+* Fix a crashing bug in `UnparsedFrame.toString()`.
+
+## 1.4.0
+
+* `new Trace.parse()` and related constructors will no longer throw an exception
+ if they encounter an unparseable stack frame. Instead, they will generate an
+ `UnparsedFrame`, which exposes no metadata but preserves the frame's original
+ text.
+
+* Properly parse native-code V8 frames.
+
+## 1.3.5
+
+* Properly shorten library names for pathnames of folded frames on Windows.
+
+## 1.3.4
+
+* No longer say that stack chains aren't supported on dart2js now that
+ [sdk#15171][] is fixed. Note that this fix only applies to Dart 1.12.
+
+[sdk#15171]: https://github.com/dart-lang/sdk/issues/15171
+
+## 1.3.3
+
+* When a `null` stack trace is passed to a completer or stream controller in
+ nested `Chain.capture()` blocks, substitute the inner block's chain rather
+ than the outer block's.
+
+* Add support for empty chains and chains of empty traces to `Chain.parse()`.
+
+* Don't crash when parsing stack traces from Dart VM stack overflows.
+
+## 1.3.2
+
+* Don't crash when running `Trace.terse` on empty stack traces.
+
+## 1.3.1
+
+* Support more types of JavaScriptCore stack frames.
+
+## 1.3.0
+
+* Support stack traces generated by JavaScriptCore. They can be explicitly
+ parsed via `new Trace.parseJSCore` and `new Frame.parseJSCore`.
+
+## 1.2.4
+
+* Fix a type annotation in `LazyTrace`.
+
+## 1.2.3
+
+* Fix a crash in `Chain.parse`.
+
+## 1.2.2
+
+* Don't print the first folded frame of terse stack traces. This frame
+ is always just an internal isolate message handler anyway. This
+ improves the readability of stack traces, especially in stack chains.
+
+* Remove the line numbers and specific files in all terse folded frames, not
+ just those from core libraries.
+
+* Make padding consistent across all stack traces for `Chain.toString()`.
+
+## 1.2.1
+
+* Add `terse` to `LazyTrace.foldFrames()`.
+
+* Further improve stack chains when using the VM's async/await implementation.
+
+## 1.2.0
+
+* Add a `terse` argument to `Trace.foldFrames()` and `Chain.foldFrames()`. This
+ allows them to inherit the behavior of `Trace.terse` and `Chain.terse` without
+ having to duplicate the logic.
+
+## 1.1.3
+
+* Produce nicer-looking stack chains when using the VM's async/await
+ implementation.
+
+## 1.1.2
+
+* Support VM frames without line *or* column numbers, which async/await programs
+ occasionally generate.
+
+* Replace `<<anonymous closure>_async_body>` in VM frames' members with the
+ terser `<async>`.
+
+## 1.1.1
+
+* Widen the SDK constraint to include 1.7.0-dev.4.0.
+
+## 1.1.0
+
+* Unify the parsing of Safari and Firefox stack traces. This fixes an error in
+ Firefox trace parsing.
+
+* Deprecate `Trace.parseSafari6_0`, `Trace.parseSafari6_1`,
+ `Frame.parseSafari6_0`, and `Frame.parseSafari6_1`.
+
+* Add `Frame.parseSafari`.
+
+## 1.0.3
+
+* Use `Zone.errorCallback` to attach stack chains to all errors without the need
+ for `Chain.track`, which is now deprecated.
+
+## 1.0.2
+
+* Remove a workaround for [issue 17083][].
+
+[issue 17083]: https://github.com/dart-lang/sdk/issues/17083
+
+## 1.0.1
+
+* Synchronous errors in the [Chain.capture] callback are now handled correctly.
+
+## 1.0.0
+
+* No API changes, just declared stable.
+
+## 0.9.3+2
+
+* Update the dependency on path.
+
+* Improve the formatting of library URIs in stack traces.
+
+## 0.9.3+1
+
+* If an error is thrown in `Chain.capture`'s `onError` handler, that error is
+ handled by the parent zone. This matches the behavior of `runZoned` in
+ `dart:async`.
+
+## 0.9.3
+
+* Add a `Chain.foldFrames` method that parallels `Trace.foldFrames`.
+
+* Record anonymous method frames in IE10 as "<fn>".
diff --git a/pkgs/stack_trace/LICENSE b/pkgs/stack_trace/LICENSE
new file mode 100644
index 0000000..162572a
--- /dev/null
+++ b/pkgs/stack_trace/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/stack_trace/README.md b/pkgs/stack_trace/README.md
new file mode 100644
index 0000000..b10a556
--- /dev/null
+++ b/pkgs/stack_trace/README.md
@@ -0,0 +1,169 @@
+[](https://github.com/dart-lang/tools/actions/workflows/stack_trace.yaml)
+[](https://pub.dev/packages/stack_trace)
+[](https://pub.dev/packages/stack_trace/publisher)
+
+This library provides the ability to parse, inspect, and manipulate stack traces
+produced by the underlying Dart implementation. It also provides functions to
+produce string representations of stack traces in a more readable format than
+the native [StackTrace] implementation.
+
+`Trace`s can be parsed from native [StackTrace]s using `Trace.from`, or captured
+using `Trace.current`. Native [StackTrace]s can also be directly converted to
+human-readable strings using `Trace.format`.
+
+[StackTrace]: https://api.dart.dev/stable/dart-core/StackTrace-class.html
+
+Here's an example native stack trace from debugging this library:
+
+ #0 Object.noSuchMethod (dart:core-patch:1884:25)
+ #1 Trace.terse.<anonymous closure> (file:///usr/local/google-old/home/goog/dart/dart/pkg/stack_trace/lib/src/trace.dart:47:21)
+ #2 IterableMixinWorkaround.reduce (dart:collection:29:29)
+ #3 List.reduce (dart:core-patch:1247:42)
+ #4 Trace.terse (file:///usr/local/google-old/home/goog/dart/dart/pkg/stack_trace/lib/src/trace.dart:40:35)
+ #5 format (file:///usr/local/google-old/home/goog/dart/dart/pkg/stack_trace/lib/stack_trace.dart:24:28)
+ #6 main.<anonymous closure> (file:///usr/local/google-old/home/goog/dart/dart/test.dart:21:29)
+ #7 _CatchErrorFuture._sendError (dart:async:525:24)
+ #8 _FutureImpl._setErrorWithoutAsyncTrace (dart:async:393:26)
+ #9 _FutureImpl._setError (dart:async:378:31)
+ #10 _ThenFuture._sendValue (dart:async:490:16)
+ #11 _FutureImpl._handleValue.<anonymous closure> (dart:async:349:28)
+ #12 Timer.run.<anonymous closure> (dart:async:2402:21)
+ #13 Timer.Timer.<anonymous closure> (dart:async-patch:15:15)
+
+and its human-readable representation:
+
+ dart:core-patch 1884:25 Object.noSuchMethod
+ pkg/stack_trace/lib/src/trace.dart 47:21 Trace.terse.<fn>
+ dart:collection 29:29 IterableMixinWorkaround.reduce
+ dart:core-patch 1247:42 List.reduce
+ pkg/stack_trace/lib/src/trace.dart 40:35 Trace.terse
+ pkg/stack_trace/lib/stack_trace.dart 24:28 format
+ test.dart 21:29 main.<fn>
+ dart:async 525:24 _CatchErrorFuture._sendError
+ dart:async 393:26 _FutureImpl._setErrorWithoutAsyncTrace
+ dart:async 378:31 _FutureImpl._setError
+ dart:async 490:16 _ThenFuture._sendValue
+ dart:async 349:28 _FutureImpl._handleValue.<fn>
+ dart:async 2402:21 Timer.run.<fn>
+ dart:async-patch 15:15 Timer.Timer.<fn>
+
+You can further clean up the stack trace using `Trace.terse`. This folds
+together multiple stack frames from the Dart core libraries, so that only the
+core library method that was directly called from user code is visible. For
+example:
+
+ dart:core Object.noSuchMethod
+ pkg/stack_trace/lib/src/trace.dart 47:21 Trace.terse.<fn>
+ dart:core List.reduce
+ pkg/stack_trace/lib/src/trace.dart 40:35 Trace.terse
+ pkg/stack_trace/lib/stack_trace.dart 24:28 format
+ test.dart 21:29 main.<fn>
+
+## Stack Chains
+
+This library also provides the ability to capture "stack chains" with the
+`Chain` class. When writing asynchronous code, a single stack trace isn't very
+useful, since the call stack is unwound every time something async happens. A
+stack chain tracks stack traces through asynchronous calls, so that you can see
+the full path from `main` down to the error.
+
+To use stack chains, just wrap the code that you want to track in
+`Chain.capture`. This will create a new [Zone][] in which stack traces are
+recorded and woven into chains every time an asynchronous call occurs. Zones are
+sticky, too, so any asynchronous operations started in the `Chain.capture`
+callback will have their chains tracked, as will asynchronous operations they
+start and so on.
+
+Here's an example of some code that doesn't capture its stack chains:
+
+```dart
+import 'dart:async';
+
+void main() {
+ _scheduleAsync();
+}
+
+void _scheduleAsync() {
+ Future.delayed(Duration(seconds: 1)).then((_) => _runAsync());
+}
+
+void _runAsync() {
+ throw 'oh no!';
+}
+```
+
+If we run this, it prints the following:
+
+ Unhandled exception:
+ oh no!
+ #0 _runAsync (file:///Users/kevmoo/github/stack_trace/example/example.dart:12:3)
+ #1 _scheduleAsync.<anonymous closure> (file:///Users/kevmoo/github/stack_trace/example/example.dart:8:52)
+ <asynchronous suspension>
+
+Notice how there's no mention of `main` in that stack trace. All we know is that
+the error was in `runAsync`; we don't know why `runAsync` was called.
+
+Now let's look at the same code with stack chains captured:
+
+```dart
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+
+void main() {
+ Chain.capture(_scheduleAsync);
+}
+
+void _scheduleAsync() {
+ Future.delayed(Duration(seconds: 1)).then((_) => _runAsync());
+}
+
+void _runAsync() {
+ throw 'oh no!';
+}
+```
+
+Now if we run it, it prints this:
+
+ Unhandled exception:
+ oh no!
+ example/example.dart 14:3 _runAsync
+ example/example.dart 10:52 _scheduleAsync.<fn>
+ package:stack_trace/src/stack_zone_specification.dart 126:26 StackZoneSpecification._registerUnaryCallback.<fn>.<fn>
+ package:stack_trace/src/stack_zone_specification.dart 208:15 StackZoneSpecification._run
+ package:stack_trace/src/stack_zone_specification.dart 126:14 StackZoneSpecification._registerUnaryCallback.<fn>
+ dart:async/zone.dart 1406:47 _rootRunUnary
+ dart:async/zone.dart 1307:19 _CustomZone.runUnary
+ ===== asynchronous gap ===========================
+ dart:async/zone.dart 1328:19 _CustomZone.registerUnaryCallback
+ dart:async/future_impl.dart 315:23 Future.then
+ example/example.dart 10:40 _scheduleAsync
+ package:stack_trace/src/chain.dart 97:24 Chain.capture.<fn>
+ dart:async/zone.dart 1398:13 _rootRun
+ dart:async/zone.dart 1300:19 _CustomZone.run
+ dart:async/zone.dart 1803:10 _runZoned
+ dart:async/zone.dart 1746:10 runZoned
+ package:stack_trace/src/chain.dart 95:12 Chain.capture
+ example/example.dart 6:9 main
+ dart:isolate-patch/isolate_patch.dart 297:19 _delayEntrypointInvocation.<fn>
+ dart:isolate-patch/isolate_patch.dart 192:12 _RawReceivePortImpl._handleMessage
+
+That's a lot of text! If you look closely, though, you can see that `main` is
+listed in the first trace in the chain.
+
+Thankfully, you can call `Chain.terse` just like `Trace.terse` to get rid of all
+the frames you don't care about. The terse version of the stack chain above is
+this:
+
+ test.dart 17:3 runAsync
+ test.dart 13:28 scheduleAsync.<fn>
+ ===== asynchronous gap ===========================
+ dart:async _Future.then
+ test.dart 13:12 scheduleAsync
+ test.dart 7:18 main.<fn>
+ package:stack_trace Chain.capture
+ test.dart 6:16 main
+
+That's a lot easier to understand!
+
+[Zone]: https://api.dart.dev/stable/dart-async/Zone-class.html
diff --git a/pkgs/stack_trace/analysis_options.yaml b/pkgs/stack_trace/analysis_options.yaml
new file mode 100644
index 0000000..4eb82ce
--- /dev/null
+++ b/pkgs/stack_trace/analysis_options.yaml
@@ -0,0 +1,22 @@
+# https://dart.dev/tools/analysis#the-analysis-options-file
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
+ strict-raw-types: true
+
+linter:
+ rules:
+ - avoid_private_typedef_functions
+ - avoid_redundant_argument_values
+ - avoid_unused_constructor_parameters
+ - avoid_void_async
+ - cancel_subscriptions
+ - literal_only_boolean_expressions
+ - missing_whitespace_between_adjacent_strings
+ - no_adjacent_strings_in_list
+ - no_runtimeType_toString
+ - prefer_const_declarations
+ - unnecessary_await_in_return
+ - use_string_buffers
diff --git a/pkgs/stack_trace/example/example.dart b/pkgs/stack_trace/example/example.dart
new file mode 100644
index 0000000..d601ca4
--- /dev/null
+++ b/pkgs/stack_trace/example/example.dart
@@ -0,0 +1,15 @@
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+
+void main() {
+ Chain.capture(_scheduleAsync);
+}
+
+void _scheduleAsync() {
+ Future<void>.delayed(const Duration(seconds: 1)).then((_) => _runAsync());
+}
+
+void _runAsync() {
+ throw StateError('oh no!');
+}
diff --git a/pkgs/stack_trace/lib/src/chain.dart b/pkgs/stack_trace/lib/src/chain.dart
new file mode 100644
index 0000000..6a815c6
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/chain.dart
@@ -0,0 +1,264 @@
+// 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:async';
+import 'dart:math' as math;
+
+import 'frame.dart';
+import 'lazy_chain.dart';
+import 'stack_zone_specification.dart';
+import 'trace.dart';
+import 'utils.dart';
+
+/// A function that handles errors in the zone wrapped by [Chain.capture].
+@Deprecated('Will be removed in stack_trace 2.0.0.')
+typedef ChainHandler = void Function(dynamic error, Chain chain);
+
+/// An opaque key used to track the current [StackZoneSpecification].
+final _specKey = Object();
+
+/// A chain of stack traces.
+///
+/// A stack chain is a collection of one or more stack traces that collectively
+/// represent the path from `main` through nested function calls to a particular
+/// code location, usually where an error was thrown. Multiple stack traces are
+/// necessary when using asynchronous functions, since the program's stack is
+/// reset before each asynchronous callback is run.
+///
+/// Stack chains can be automatically tracked using [Chain.capture]. This sets
+/// up a new [Zone] in which the current stack chain is tracked and can be
+/// accessed using [Chain.current]. Any errors that would be top-leveled in
+/// the zone can be handled, along with their associated chains, with the
+/// `onError` callback. For example:
+///
+/// Chain.capture(() {
+/// // ...
+/// }, onError: (error, stackChain) {
+/// print("Caught error $error\n"
+/// "$stackChain");
+/// });
+class Chain implements StackTrace {
+ /// The stack traces that make up this chain.
+ ///
+ /// Like the frames in a stack trace, the traces are ordered from most local
+ /// to least local. The first one is the trace where the actual exception was
+ /// raised, the second one is where that callback was scheduled, and so on.
+ final List<Trace> traces;
+
+ /// The [StackZoneSpecification] for the current zone.
+ static StackZoneSpecification? get _currentSpec =>
+ Zone.current[_specKey] as StackZoneSpecification?;
+
+ /// If [when] is `true`, runs [callback] in a [Zone] in which the current
+ /// stack chain is tracked and automatically associated with (most) errors.
+ ///
+ /// If [when] is `false`, this does not track stack chains. Instead, it's
+ /// identical to [runZoned], except that it wraps any errors in
+ /// [Chain.forTrace]—which will only wrap the trace unless there's a different
+ /// [Chain.capture] active. This makes it easy for the caller to only capture
+ /// stack chains in debug mode or during development.
+ ///
+ /// If [onError] is passed, any error in the zone that would otherwise go
+ /// unhandled is passed to it, along with the [Chain] associated with that
+ /// error. Note that if [callback] produces multiple unhandled errors,
+ /// [onError] may be called more than once. If [onError] isn't passed, the
+ /// parent Zone's `unhandledErrorHandler` will be called with the error and
+ /// its chain.
+ ///
+ /// The zone this creates will be an error zone if either [onError] is
+ /// not `null` and [when] is false,
+ /// or if both [when] and [errorZone] are `true`.
+ /// If [errorZone] is `false`, [onError] must be `null`.
+ ///
+ /// If [callback] returns a value, it will be returned by [capture] as well.
+ ///
+ /// [zoneValues] is added to the [runZoned] calls.
+ static T capture<T>(T Function() callback,
+ {void Function(Object error, Chain)? onError,
+ bool when = true,
+ bool errorZone = true,
+ Map<Object?, Object?>? zoneValues}) {
+ if (!errorZone && onError != null) {
+ throw ArgumentError.value(
+ onError, 'onError', 'must be null if errorZone is false');
+ }
+
+ if (!when) {
+ if (onError == null) return runZoned(callback, zoneValues: zoneValues);
+ return runZonedGuarded(callback, (error, stackTrace) {
+ onError(error, Chain.forTrace(stackTrace));
+ }, zoneValues: zoneValues) as T;
+ }
+
+ var spec = StackZoneSpecification(onError, errorZone: errorZone);
+ return runZoned(() {
+ try {
+ return callback();
+ } on Object catch (error, stackTrace) {
+ // Forward synchronous errors through the async error path to match the
+ // behavior of `runZonedGuarded`.
+ Zone.current.handleUncaughtError(error, stackTrace);
+
+ // If the expected return type of capture() is not nullable, this will
+ // throw a cast exception. But the only other alternative is to throw
+ // some other exception. Casting null to T at least lets existing uses
+ // where T is a nullable type continue to work.
+ return null as T;
+ }
+ }, zoneSpecification: spec.toSpec(), zoneValues: {
+ ...?zoneValues,
+ _specKey: spec,
+ StackZoneSpecification.disableKey: false
+ });
+ }
+
+ /// If [when] is `true` and this is called within a [Chain.capture] zone, runs
+ /// [callback] in a [Zone] in which chain capturing is disabled.
+ ///
+ /// If [callback] returns a value, it will be returned by [disable] as well.
+ static T disable<T>(T Function() callback, {bool when = true}) {
+ var zoneValues =
+ when ? {_specKey: null, StackZoneSpecification.disableKey: true} : null;
+
+ return runZoned(callback, zoneValues: zoneValues);
+ }
+
+ /// Returns [futureOrStream] unmodified.
+ ///
+ /// Prior to Dart 1.7, this was necessary to ensure that stack traces for
+ /// exceptions reported with [Completer.completeError] and
+ /// [StreamController.addError] were tracked correctly.
+ @Deprecated('Chain.track is not necessary in Dart 1.7+.')
+ static dynamic track(Object? futureOrStream) => futureOrStream;
+
+ /// Returns the current stack chain.
+ ///
+ /// By default, the first frame of the first trace will be the line where
+ /// [Chain.current] is called. If [level] is passed, the first trace will
+ /// start that many frames up instead.
+ ///
+ /// If this is called outside of a [capture] zone, it just returns a
+ /// single-trace chain.
+ factory Chain.current([int level = 0]) {
+ if (_currentSpec != null) return _currentSpec!.currentChain(level + 1);
+
+ var chain = Chain.forTrace(StackTrace.current);
+ return LazyChain(() {
+ // JS includes a frame for the call to StackTrace.current, but the VM
+ // doesn't, so we skip an extra frame in a JS context.
+ var first = Trace(chain.traces.first.frames.skip(level + (inJS ? 2 : 1)),
+ original: chain.traces.first.original.toString());
+ return Chain([first, ...chain.traces.skip(1)]);
+ });
+ }
+
+ /// Returns the stack chain associated with [trace].
+ ///
+ /// The first stack trace in the returned chain will always be [trace]
+ /// (converted to a [Trace] if necessary). If there is no chain associated
+ /// with [trace] or if this is called outside of a [capture] zone, this just
+ /// returns a single-trace chain containing [trace].
+ ///
+ /// If [trace] is already a [Chain], it will be returned as-is.
+ factory Chain.forTrace(StackTrace trace) {
+ if (trace is Chain) return trace;
+ if (_currentSpec != null) return _currentSpec!.chainFor(trace);
+ if (trace is Trace) return Chain([trace]);
+ return LazyChain(() => Chain.parse(trace.toString()));
+ }
+
+ /// Parses a string representation of a stack chain.
+ ///
+ /// If [chain] is the output of a call to [Chain.toString], it will be parsed
+ /// as a full stack chain. Otherwise, it will be parsed as in [Trace.parse]
+ /// and returned as a single-trace chain.
+ factory Chain.parse(String chain) {
+ if (chain.isEmpty) return Chain([]);
+ if (chain.contains(vmChainGap)) {
+ return Chain(chain
+ .split(vmChainGap)
+ .where((line) => line.isNotEmpty)
+ .map(Trace.parseVM));
+ }
+ if (!chain.contains(chainGap)) return Chain([Trace.parse(chain)]);
+
+ return Chain(chain.split(chainGap).map(Trace.parseFriendly));
+ }
+
+ /// Returns a new [Chain] comprised of [traces].
+ Chain(Iterable<Trace> traces) : traces = List<Trace>.unmodifiable(traces);
+
+ /// Returns a terser version of this chain.
+ ///
+ /// This calls [Trace.terse] on every trace in [traces], and discards any
+ /// trace that contain only internal frames.
+ ///
+ /// This won't do anything with a raw JavaScript trace, since there's no way
+ /// to determine which frames come from which Dart libraries. However, the
+ /// [`source_map_stack_trace`](https://pub.dev/packages/source_map_stack_trace)
+ /// package can be used to convert JavaScript traces into Dart-style traces.
+ Chain get terse => foldFrames((_) => false, terse: true);
+
+ /// Returns a new [Chain] based on this chain where multiple stack frames
+ /// matching [predicate] are folded together.
+ ///
+ /// This means that whenever there are multiple frames in a row that match
+ /// [predicate], only the last one is kept. In addition, traces that are
+ /// composed entirely of frames matching [predicate] are omitted.
+ ///
+ /// This is useful for limiting the amount of library code that appears in a
+ /// stack trace by only showing user code and code that's called by user code.
+ ///
+ /// If [terse] is true, this will also fold together frames from the core
+ /// library or from this package, and simplify core library frames as in
+ /// [Trace.terse].
+ Chain foldFrames(bool Function(Frame) predicate, {bool terse = false}) {
+ var foldedTraces =
+ traces.map((trace) => trace.foldFrames(predicate, terse: terse));
+ var nonEmptyTraces = foldedTraces.where((trace) {
+ // Ignore traces that contain only folded frames.
+ if (trace.frames.length > 1) return true;
+ if (trace.frames.isEmpty) return false;
+
+ // In terse mode, the trace may have removed an outer folded frame,
+ // leaving a single non-folded frame. We can detect a folded frame because
+ // it has no line information.
+ if (!terse) return false;
+ return trace.frames.single.line != null;
+ });
+
+ // If all the traces contain only internal processing, preserve the last
+ // (top-most) one so that the chain isn't empty.
+ if (nonEmptyTraces.isEmpty && foldedTraces.isNotEmpty) {
+ return Chain([foldedTraces.last]);
+ }
+
+ return Chain(nonEmptyTraces);
+ }
+
+ /// Converts this chain to a [Trace].
+ ///
+ /// The trace version of a chain is just the concatenation of all the traces
+ /// in the chain.
+ Trace toTrace() => Trace(traces.expand((trace) => trace.frames));
+
+ @override
+ String toString() {
+ // Figure out the longest path so we know how much to pad.
+ var longest = traces
+ .map((trace) => trace.frames
+ .map((frame) => frame.location.length)
+ .fold(0, math.max))
+ .fold(0, math.max);
+
+ // Don't call out to [Trace.toString] here because that doesn't ensure that
+ // padding is consistent across all traces.
+ return traces
+ .map((trace) => trace.frames
+ .map((frame) =>
+ '${frame.location.padRight(longest)} ${frame.member}\n')
+ .join())
+ .join(chainGap);
+ }
+}
diff --git a/pkgs/stack_trace/lib/src/frame.dart b/pkgs/stack_trace/lib/src/frame.dart
new file mode 100644
index 0000000..d4043b7
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/frame.dart
@@ -0,0 +1,458 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as path;
+
+import 'trace.dart';
+import 'unparsed_frame.dart';
+
+// #1 Foo._bar (file:///home/nweiz/code/stuff.dart:42:21)
+// #1 Foo._bar (file:///home/nweiz/code/stuff.dart:42)
+// #1 Foo._bar (file:///home/nweiz/code/stuff.dart)
+final _vmFrame = RegExp(r'^#\d+\s+(\S.*) \((.+?)((?::\d+){0,2})\)$');
+
+// at Object.stringify (native)
+// at VW.call$0 (https://example.com/stuff.dart.js:560:28)
+// at VW.call$0 (eval as fn
+// (https://example.com/stuff.dart.js:560:28), efn:3:28)
+// at https://example.com/stuff.dart.js:560:28
+final _v8JsFrame =
+ RegExp(r'^\s*at (?:(\S.*?)(?: \[as [^\]]+\])? \((.*)\)|(.*))$');
+
+// https://example.com/stuff.dart.js:560:28
+// https://example.com/stuff.dart.js:560
+//
+// Group 1: URI, required
+// Group 2: line number, required
+// Group 3: column number, optional
+final _v8JsUrlLocation = RegExp(r'^(.*?):(\d+)(?::(\d+))?$|native$');
+
+// With names:
+//
+// at Error.f (wasm://wasm/0006d966:wasm-function[119]:0xbb13)
+// at g (wasm://wasm/0006d966:wasm-function[796]:0x143b4)
+//
+// Without names:
+//
+// at wasm://wasm/0005168a:wasm-function[119]:0xbb13
+// at wasm://wasm/0005168a:wasm-function[796]:0x143b4
+//
+// Matches named groups:
+//
+// - "member": optional, `Error.f` in the first example, NA in the second.
+// - "uri": `wasm://wasm/0006d966`.
+// - "index": `119`.
+// - "offset": (hex number) `bb13`.
+//
+// To avoid having multiple groups for the same part of the frame, this regex
+// matches unmatched parentheses after the member name.
+final _v8WasmFrame = RegExp(r'^\s*at (?:(?<member>.+) )?'
+ r'(?:\(?(?:(?<uri>\S+):wasm-function\[(?<index>\d+)\]'
+ r'\:0x(?<offset>[0-9a-fA-F]+))\)?)$');
+
+// eval as function (https://example.com/stuff.dart.js:560:28), efn:3:28
+// eval as function (https://example.com/stuff.dart.js:560:28)
+// eval as function (eval as otherFunction
+// (https://example.com/stuff.dart.js:560:28))
+final _v8EvalLocation =
+ RegExp(r'^eval at (?:\S.*?) \((.*)\)(?:, .*?:\d+:\d+)?$');
+
+// anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+// anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
+final _firefoxEvalLocation =
+ RegExp(r'(\S+)@(\S+) line (\d+) >.* (Function|eval):\d+:\d+');
+
+// .VW.call$0@https://example.com/stuff.dart.js:560
+// .VW.call$0("arg")@https://example.com/stuff.dart.js:560
+// .VW.call$0/name<@https://example.com/stuff.dart.js:560
+// .VW.call$0@https://example.com/stuff.dart.js:560:36
+// https://example.com/stuff.dart.js:560
+final _firefoxSafariJSFrame = RegExp(r'^'
+ r'(?:' // Member description. Not present in some Safari frames.
+ r'([^@(/]*)' // The actual name of the member.
+ r'(?:\(.*\))?' // Arguments to the member, sometimes captured by Firefox.
+ r'((?:/[^/]*)*)' // Extra characters indicating a nested closure.
+ r'(?:\(.*\))?' // Arguments to the closure.
+ r'@'
+ r')?'
+ r'(.*?)' // The frame's URL.
+ r':'
+ r'(\d*)' // The line number. Empty in Safari if it's unknown.
+ r'(?::(\d*))?' // The column number. Not present in older browsers and
+ // empty in Safari if it's unknown.
+ r'$');
+
+// With names:
+//
+// g@http://localhost:8080/test.wasm:wasm-function[796]:0x143b4
+// f@http://localhost:8080/test.wasm:wasm-function[795]:0x143a8
+// main@http://localhost:8080/test.wasm:wasm-function[792]:0x14390
+//
+// Without names:
+//
+// @http://localhost:8080/test.wasm:wasm-function[796]:0x143b4
+// @http://localhost:8080/test.wasm:wasm-function[795]:0x143a8
+// @http://localhost:8080/test.wasm:wasm-function[792]:0x14390
+//
+// JSShell in the command line uses a different format, which this regex also
+// parses.
+//
+// With names:
+//
+// main@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[792]:0x14378
+//
+// Without names:
+//
+// @/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[792]:0x14378
+//
+// Matches named groups:
+//
+// - "member": Function name, may be empty: `g`.
+// - "uri": `http://localhost:8080/test.wasm`.
+// - "index": `796`.
+// - "offset": (in hex) `143b4`.
+final _firefoxWasmFrame =
+ RegExp(r'^(?<member>.*?)@(?:(?<uri>\S+).*?:wasm-function'
+ r'\[(?<index>\d+)\]:0x(?<offset>[0-9a-fA-F]+))$');
+
+// With names:
+//
+// (Note: Lines below are literal text, e.g. <?> is not a placeholder, it's a
+// part of the stack frame.)
+//
+// <?>.wasm-function[g]@[wasm code]
+// <?>.wasm-function[f]@[wasm code]
+// <?>.wasm-function[main]@[wasm code]
+//
+// Without names:
+//
+// <?>.wasm-function[796]@[wasm code]
+// <?>.wasm-function[795]@[wasm code]
+// <?>.wasm-function[792]@[wasm code]
+//
+// Matches named group "member": `g` or `796`.
+final _safariWasmFrame =
+ RegExp(r'^.*?wasm-function\[(?<member>.*)\]@\[wasm code\]$');
+
+// foo/bar.dart 10:11 Foo._bar
+// foo/bar.dart 10:11 (anonymous function).dart.fn
+// https://dart.dev/foo/bar.dart Foo._bar
+// data:... 10:11 Foo._bar
+final _friendlyFrame = RegExp(r'^(\S+)(?: (\d+)(?::(\d+))?)?\s+([^\d].*)$');
+
+/// A regular expression that matches asynchronous member names generated by the
+/// VM.
+final _asyncBody = RegExp(r'<(<anonymous closure>|[^>]+)_async_body>');
+
+final _initialDot = RegExp(r'^\.');
+
+/// A single stack frame. Each frame points to a precise location in Dart code.
+class Frame {
+ /// The URI of the file in which the code is located.
+ ///
+ /// This URI will usually have the scheme `dart`, `file`, `http`, or `https`.
+ final Uri uri;
+
+ /// The line number on which the code location is located.
+ ///
+ /// This can be null, indicating that the line number is unknown or
+ /// unimportant.
+ final int? line;
+
+ /// The column number of the code location.
+ ///
+ /// This can be null, indicating that the column number is unknown or
+ /// unimportant.
+ final int? column;
+
+ /// The name of the member in which the code location occurs.
+ ///
+ /// Anonymous closures are represented as `<fn>` in this member string.
+ final String? member;
+
+ /// Whether this stack frame comes from the Dart core libraries.
+ bool get isCore => uri.scheme == 'dart';
+
+ /// Returns a human-friendly description of the library that this stack frame
+ /// comes from.
+ ///
+ /// This will usually be the string form of [uri], but a relative URI will be
+ /// used if possible. Data URIs will be truncated.
+ String get library {
+ if (uri.scheme == 'data') return 'data:...';
+ return path.prettyUri(uri);
+ }
+
+ /// Returns the name of the package this stack frame comes from, or `null` if
+ /// this stack frame doesn't come from a `package:` URL.
+ String? get package {
+ if (uri.scheme != 'package') return null;
+ return uri.path.split('/').first;
+ }
+
+ /// A human-friendly description of the code location.
+ String get location {
+ if (line == null) return library;
+ if (column == null) return '$library $line';
+ return '$library $line:$column';
+ }
+
+ /// Returns a single frame of the current stack.
+ ///
+ /// By default, this will return the frame above the current method. If
+ /// [level] is `0`, it will return the current method's frame; if [level] is
+ /// higher than `1`, it will return higher frames.
+ factory Frame.caller([int level = 1]) {
+ if (level < 0) {
+ throw ArgumentError('Argument [level] must be greater than or equal '
+ 'to 0.');
+ }
+
+ return Trace.current(level + 1).frames.first;
+ }
+
+ /// Parses a string representation of a Dart VM stack frame.
+ factory Frame.parseVM(String frame) => _catchFormatException(frame, () {
+ // The VM sometimes folds multiple stack frames together and replaces
+ // them with "...".
+ if (frame == '...') {
+ return Frame(Uri(), null, null, '...');
+ }
+
+ var match = _vmFrame.firstMatch(frame);
+ if (match == null) return UnparsedFrame(frame);
+
+ // Get the pieces out of the regexp match. Function, URI and line should
+ // always be found. The column is optional.
+ var member = match[1]!
+ .replaceAll(_asyncBody, '<async>')
+ .replaceAll('<anonymous closure>', '<fn>');
+ var uri = match[2]!.startsWith('<data:')
+ ? Uri.dataFromString('')
+ : Uri.parse(match[2]!);
+
+ var lineAndColumn = match[3]!.split(':');
+ var line =
+ lineAndColumn.length > 1 ? int.parse(lineAndColumn[1]) : null;
+ var column =
+ lineAndColumn.length > 2 ? int.parse(lineAndColumn[2]) : null;
+ return Frame(uri, line, column, member);
+ });
+
+ /// Parses a string representation of a Chrome/V8 stack frame.
+ factory Frame.parseV8(String frame) => _catchFormatException(frame, () {
+ // Try to match a Wasm frame first: the Wasm frame regex won't match a
+ // JS frame but the JS frame regex may match a Wasm frame.
+ var match = _v8WasmFrame.firstMatch(frame);
+ if (match != null) {
+ final member = match.namedGroup('member');
+ final uri = _uriOrPathToUri(match.namedGroup('uri')!);
+ final functionIndex = match.namedGroup('index')!;
+ final functionOffset =
+ int.parse(match.namedGroup('offset')!, radix: 16);
+ return Frame(uri, 1, functionOffset + 1, member ?? functionIndex);
+ }
+
+ match = _v8JsFrame.firstMatch(frame);
+ if (match != null) {
+ // v8 location strings can be arbitrarily-nested, since it adds a
+ // layer of nesting for each eval performed on that line.
+ Frame parseJsLocation(String location, String member) {
+ var evalMatch = _v8EvalLocation.firstMatch(location);
+ while (evalMatch != null) {
+ location = evalMatch[1]!;
+ evalMatch = _v8EvalLocation.firstMatch(location);
+ }
+
+ if (location == 'native') {
+ return Frame(Uri.parse('native'), null, null, member);
+ }
+
+ var urlMatch = _v8JsUrlLocation.firstMatch(location);
+ if (urlMatch == null) return UnparsedFrame(frame);
+
+ final uri = _uriOrPathToUri(urlMatch[1]!);
+ final line = int.parse(urlMatch[2]!);
+ final columnMatch = urlMatch[3];
+ final column = columnMatch != null ? int.parse(columnMatch) : null;
+ return Frame(uri, line, column, member);
+ }
+
+ // V8 stack frames can be in two forms.
+ if (match[2] != null) {
+ // The first form looks like " at FUNCTION (LOCATION)". V8 proper
+ // lists anonymous functions within eval as "<anonymous>", while
+ // IE10 lists them as "Anonymous function".
+ return parseJsLocation(
+ match[2]!,
+ match[1]!
+ .replaceAll('<anonymous>', '<fn>')
+ .replaceAll('Anonymous function', '<fn>')
+ .replaceAll('(anonymous function)', '<fn>'));
+ } else {
+ // The second form looks like " at LOCATION", and is used for
+ // anonymous functions.
+ return parseJsLocation(match[3]!, '<fn>');
+ }
+ }
+
+ return UnparsedFrame(frame);
+ });
+
+ /// Parses a string representation of a JavaScriptCore stack trace.
+ factory Frame.parseJSCore(String frame) => Frame.parseV8(frame);
+
+ /// Parses a string representation of an IE stack frame.
+ ///
+ /// IE10+ frames look just like V8 frames. Prior to IE10, stack traces can't
+ /// be retrieved.
+ factory Frame.parseIE(String frame) => Frame.parseV8(frame);
+
+ /// Parses a Firefox 'eval' or 'function' stack frame.
+ ///
+ /// For example:
+ ///
+ /// ```
+ /// anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+ /// anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
+ /// ```
+ factory Frame._parseFirefoxEval(String frame) =>
+ _catchFormatException(frame, () {
+ final match = _firefoxEvalLocation.firstMatch(frame);
+ if (match == null) return UnparsedFrame(frame);
+ var member = match[1]!.replaceAll('/<', '');
+ final uri = _uriOrPathToUri(match[2]!);
+ final line = int.parse(match[3]!);
+ if (member.isEmpty || member == 'anonymous') {
+ member = '<fn>';
+ }
+ return Frame(uri, line, null, member);
+ });
+
+ /// Parses a string representation of a Firefox or Safari stack frame.
+ factory Frame.parseFirefox(String frame) => _catchFormatException(frame, () {
+ var match = _firefoxSafariJSFrame.firstMatch(frame);
+ if (match != null) {
+ if (match[3]!.contains(' line ')) {
+ return Frame._parseFirefoxEval(frame);
+ }
+
+ // Normally this is a URI, but in a jsshell trace it can be a path.
+ var uri = _uriOrPathToUri(match[3]!);
+
+ var member = match[1];
+ if (member != null) {
+ member +=
+ List.filled('/'.allMatches(match[2]!).length, '.<fn>').join();
+ if (member == '') member = '<fn>';
+
+ // Some Firefox members have initial dots. We remove them for
+ // consistency with other platforms.
+ member = member.replaceFirst(_initialDot, '');
+ } else {
+ member = '<fn>';
+ }
+
+ var line = match[4] == '' ? null : int.parse(match[4]!);
+ var column =
+ match[5] == null || match[5] == '' ? null : int.parse(match[5]!);
+ return Frame(uri, line, column, member);
+ }
+
+ match = _firefoxWasmFrame.firstMatch(frame);
+ if (match != null) {
+ final member = match.namedGroup('member')!;
+ final uri = _uriOrPathToUri(match.namedGroup('uri')!);
+ final functionIndex = match.namedGroup('index')!;
+ final functionOffset =
+ int.parse(match.namedGroup('offset')!, radix: 16);
+ return Frame(uri, 1, functionOffset + 1,
+ member.isNotEmpty ? member : functionIndex);
+ }
+
+ match = _safariWasmFrame.firstMatch(frame);
+ if (match != null) {
+ final member = match.namedGroup('member')!;
+ return Frame(Uri(path: 'wasm code'), null, null, member);
+ }
+
+ return UnparsedFrame(frame);
+ });
+
+ /// Parses a string representation of a Safari 6.0 stack frame.
+ @Deprecated('Use Frame.parseSafari instead.')
+ factory Frame.parseSafari6_0(String frame) => Frame.parseFirefox(frame);
+
+ /// Parses a string representation of a Safari 6.1+ stack frame.
+ @Deprecated('Use Frame.parseSafari instead.')
+ factory Frame.parseSafari6_1(String frame) => Frame.parseFirefox(frame);
+
+ /// Parses a string representation of a Safari stack frame.
+ factory Frame.parseSafari(String frame) => Frame.parseFirefox(frame);
+
+ /// Parses this package's string representation of a stack frame.
+ factory Frame.parseFriendly(String frame) => _catchFormatException(frame, () {
+ var match = _friendlyFrame.firstMatch(frame);
+ if (match == null) {
+ throw FormatException(
+ "Couldn't parse package:stack_trace stack trace line '$frame'.");
+ }
+ // Fake truncated data urls generated by the friendly stack trace format
+ // cause Uri.parse to throw an exception so we have to special case
+ // them.
+ var uri = match[1] == 'data:...'
+ ? Uri.dataFromString('')
+ : Uri.parse(match[1]!);
+ // If there's no scheme, this is a relative URI. We should interpret it
+ // as relative to the current working directory.
+ if (uri.scheme == '') {
+ uri = path.toUri(path.absolute(path.fromUri(uri)));
+ }
+
+ var line = match[2] == null ? null : int.parse(match[2]!);
+ var column = match[3] == null ? null : int.parse(match[3]!);
+ return Frame(uri, line, column, match[4]);
+ });
+
+ /// A regular expression matching an absolute URI.
+ static final _uriRegExp = RegExp(r'^[a-zA-Z][-+.a-zA-Z\d]*://');
+
+ /// A regular expression matching a Windows path.
+ static final _windowsRegExp = RegExp(r'^([a-zA-Z]:[\\/]|\\\\)');
+
+ /// Converts [uriOrPath], which can be a URI, a Windows path, or a Posix path,
+ /// to a URI (absolute if possible).
+ static Uri _uriOrPathToUri(String uriOrPath) {
+ if (uriOrPath.contains(_uriRegExp)) {
+ return Uri.parse(uriOrPath);
+ } else if (uriOrPath.contains(_windowsRegExp)) {
+ return Uri.file(uriOrPath, windows: true);
+ } else if (uriOrPath.startsWith('/')) {
+ return Uri.file(uriOrPath, windows: false);
+ }
+
+ // As far as I've seen, Firefox and V8 both always report absolute paths in
+ // their stack frames. However, if we do get a relative path, we should
+ // handle it gracefully.
+ if (uriOrPath.contains('\\')) return path.windows.toUri(uriOrPath);
+ return Uri.parse(uriOrPath);
+ }
+
+ /// Runs [body] and returns its result.
+ ///
+ /// If [body] throws a [FormatException], returns an [UnparsedFrame] with
+ /// [text] instead.
+ static Frame _catchFormatException(String text, Frame Function() body) {
+ try {
+ return body();
+ } on FormatException catch (_) {
+ return UnparsedFrame(text);
+ }
+ }
+
+ Frame(this.uri, this.line, this.column, this.member);
+
+ @override
+ String toString() => '$location in $member';
+}
diff --git a/pkgs/stack_trace/lib/src/lazy_chain.dart b/pkgs/stack_trace/lib/src/lazy_chain.dart
new file mode 100644
index 0000000..063ed59
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/lazy_chain.dart
@@ -0,0 +1,33 @@
+// 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 'chain.dart';
+import 'frame.dart';
+import 'lazy_trace.dart';
+import 'trace.dart';
+
+/// A thunk for lazily constructing a [Chain].
+typedef ChainThunk = Chain Function();
+
+/// A wrapper around a [ChainThunk]. This works around issue 9579 by avoiding
+/// the conversion of native [StackTrace]s to strings until it's absolutely
+/// necessary.
+class LazyChain implements Chain {
+ final ChainThunk _thunk;
+ late final Chain _chain = _thunk();
+
+ LazyChain(this._thunk);
+
+ @override
+ List<Trace> get traces => _chain.traces;
+ @override
+ Chain get terse => _chain.terse;
+ @override
+ Chain foldFrames(bool Function(Frame) predicate, {bool terse = false}) =>
+ LazyChain(() => _chain.foldFrames(predicate, terse: terse));
+ @override
+ Trace toTrace() => LazyTrace(_chain.toTrace);
+ @override
+ String toString() => _chain.toString();
+}
diff --git a/pkgs/stack_trace/lib/src/lazy_trace.dart b/pkgs/stack_trace/lib/src/lazy_trace.dart
new file mode 100644
index 0000000..3ecaa2d
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/lazy_trace.dart
@@ -0,0 +1,33 @@
+// 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 'frame.dart';
+import 'trace.dart';
+
+/// A thunk for lazily constructing a [Trace].
+typedef TraceThunk = Trace Function();
+
+/// A wrapper around a [TraceThunk]. This works around issue 9579 by avoiding
+/// the conversion of native [StackTrace]s to strings until it's absolutely
+/// necessary.
+class LazyTrace implements Trace {
+ final TraceThunk _thunk;
+ late final Trace _trace = _thunk();
+
+ LazyTrace(this._thunk);
+
+ @override
+ List<Frame> get frames => _trace.frames;
+ @override
+ StackTrace get original => _trace.original;
+ @override
+ StackTrace get vmTrace => _trace.vmTrace;
+ @override
+ Trace get terse => LazyTrace(() => _trace.terse);
+ @override
+ Trace foldFrames(bool Function(Frame) predicate, {bool terse = false}) =>
+ LazyTrace(() => _trace.foldFrames(predicate, terse: terse));
+ @override
+ String toString() => _trace.toString();
+}
diff --git a/pkgs/stack_trace/lib/src/stack_zone_specification.dart b/pkgs/stack_trace/lib/src/stack_zone_specification.dart
new file mode 100644
index 0000000..901a5ee
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/stack_zone_specification.dart
@@ -0,0 +1,262 @@
+// 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:async';
+
+import 'chain.dart';
+import 'lazy_chain.dart';
+import 'lazy_trace.dart';
+import 'trace.dart';
+import 'utils.dart';
+
+/// A class encapsulating the zone specification for a [Chain.capture] zone.
+///
+/// Until they're materialized and exposed to the user, stack chains are tracked
+/// as linked lists of [Trace]s using the [_Node] class. These nodes are stored
+/// in three distinct ways:
+///
+/// * When a callback is registered, a node is created and stored as a captured
+/// local variable until the callback is run.
+///
+/// * When a callback is run, its captured node is set as the [_currentNode] so
+/// it can be available to [Chain.current] and to be linked into additional
+/// chains when more callbacks are scheduled.
+///
+/// * When a callback throws an error or a Future or Stream emits an error, the
+/// current node is associated with that error's stack trace using the
+/// [_chains] expando.
+///
+/// Since [ZoneSpecification] can't be extended or even implemented, in order to
+/// get a real [ZoneSpecification] instance it's necessary to call [toSpec].
+class StackZoneSpecification {
+ /// An opaque object used as a zone value to disable chain tracking in a given
+ /// zone.
+ ///
+ /// If `Zone.current[disableKey]` is `true`, no stack chains will be tracked.
+ static final disableKey = Object();
+
+ /// Whether chain-tracking is disabled in the current zone.
+ bool get _disabled => Zone.current[disableKey] == true;
+
+ /// The expando that associates stack chains with [StackTrace]s.
+ ///
+ /// The chains are associated with stack traces rather than errors themselves
+ /// because it's a common practice to throw strings as errors, which can't be
+ /// used with expandos.
+ ///
+ /// The chain associated with a given stack trace doesn't contain a node for
+ /// that stack trace.
+ final _chains = Expando<_Node>('stack chains');
+
+ /// The error handler for the zone.
+ ///
+ /// If this is null, that indicates that any unhandled errors should be passed
+ /// to the parent zone.
+ final void Function(Object error, Chain)? _onError;
+
+ /// The most recent node of the current stack chain.
+ _Node? _currentNode;
+
+ /// Whether this is an error zone.
+ final bool _errorZone;
+
+ StackZoneSpecification(this._onError, {bool errorZone = true})
+ : _errorZone = errorZone;
+
+ /// Converts this specification to a real [ZoneSpecification].
+ ZoneSpecification toSpec() => ZoneSpecification(
+ handleUncaughtError: _errorZone ? _handleUncaughtError : null,
+ registerCallback: _registerCallback,
+ registerUnaryCallback: _registerUnaryCallback,
+ registerBinaryCallback: _registerBinaryCallback,
+ errorCallback: _errorCallback);
+
+ /// Returns the current stack chain.
+ ///
+ /// By default, the first frame of the first trace will be the line where
+ /// [currentChain] is called. If [level] is passed, the first trace will start
+ /// that many frames up instead.
+ Chain currentChain([int level = 0]) => _createNode(level + 1).toChain();
+
+ /// Returns the stack chain associated with [trace], if one exists.
+ ///
+ /// The first stack trace in the returned chain will always be [trace]
+ /// (converted to a [Trace] if necessary). If there is no chain associated
+ /// with [trace], this just returns a single-trace chain containing [trace].
+ Chain chainFor(StackTrace? trace) {
+ if (trace is Chain) return trace;
+ trace ??= StackTrace.current;
+
+ var previous = _chains[trace] ?? _currentNode;
+ if (previous == null) {
+ // If there's no [_currentNode], we're running synchronously beneath
+ // [Chain.capture] and we should fall back to the VM's stack chaining. We
+ // can't use [Chain.from] here because it'll just call [chainFor] again.
+ if (trace is Trace) return Chain([trace]);
+ return LazyChain(() => Chain.parse(trace!.toString()));
+ } else {
+ if (trace is! Trace) {
+ var original = trace;
+ trace = LazyTrace(() => Trace.parse(_trimVMChain(original)));
+ }
+
+ return _Node(trace, previous).toChain();
+ }
+ }
+
+ /// Tracks the current stack chain so it can be set to [_currentNode] when
+ /// [f] is run.
+ ZoneCallback<R> _registerCallback<R>(
+ Zone self, ZoneDelegate parent, Zone zone, R Function() f) {
+ if (_disabled) return parent.registerCallback(zone, f);
+ var node = _createNode(1);
+ return parent.registerCallback(zone, () => _run(f, node));
+ }
+
+ /// Tracks the current stack chain so it can be set to [_currentNode] when
+ /// [f] is run.
+ ZoneUnaryCallback<R, T> _registerUnaryCallback<R, T>(
+ Zone self,
+ ZoneDelegate parent,
+ Zone zone,
+ @pragma('vm:awaiter-link') R Function(T) f) {
+ if (_disabled) return parent.registerUnaryCallback(zone, f);
+ var node = _createNode(1);
+ return parent.registerUnaryCallback(
+ zone, (arg) => _run(() => f(arg), node));
+ }
+
+ /// Tracks the current stack chain so it can be set to [_currentNode] when
+ /// [f] is run.
+ ZoneBinaryCallback<R, T1, T2> _registerBinaryCallback<R, T1, T2>(
+ Zone self, ZoneDelegate parent, Zone zone, R Function(T1, T2) f) {
+ if (_disabled) return parent.registerBinaryCallback(zone, f);
+
+ var node = _createNode(1);
+ return parent.registerBinaryCallback(
+ zone, (arg1, arg2) => _run(() => f(arg1, arg2), node));
+ }
+
+ /// Looks up the chain associated with [stackTrace] and passes it either to
+ /// [_onError] or [parent]'s error handler.
+ void _handleUncaughtError(Zone self, ZoneDelegate parent, Zone zone,
+ Object error, StackTrace stackTrace) {
+ if (_disabled) {
+ parent.handleUncaughtError(zone, error, stackTrace);
+ return;
+ }
+
+ var stackChain = chainFor(stackTrace);
+ if (_onError == null) {
+ parent.handleUncaughtError(zone, error, stackChain);
+ return;
+ }
+
+ // TODO(nweiz): Currently this copies a lot of logic from [runZoned]. Just
+ // allow [runBinary] to throw instead once issue 18134 is fixed.
+ try {
+ // TODO(rnystrom): Is the null-assertion correct here? It is nullable in
+ // Zone. Should we check for that here?
+ self.parent!.runBinary(_onError, error, stackChain);
+ } on Object catch (newError, newStackTrace) {
+ if (identical(newError, error)) {
+ parent.handleUncaughtError(zone, error, stackChain);
+ } else {
+ parent.handleUncaughtError(zone, newError, newStackTrace);
+ }
+ }
+ }
+
+ /// Attaches the current stack chain to [stackTrace], replacing it if
+ /// necessary.
+ AsyncError? _errorCallback(Zone self, ZoneDelegate parent, Zone zone,
+ Object error, StackTrace? stackTrace) {
+ if (_disabled) return parent.errorCallback(zone, error, stackTrace);
+
+ // Go up two levels to get through [_CustomZone.errorCallback].
+ if (stackTrace == null) {
+ stackTrace = _createNode(2).toChain();
+ } else {
+ if (_chains[stackTrace] == null) _chains[stackTrace] = _createNode(2);
+ }
+
+ var asyncError = parent.errorCallback(zone, error, stackTrace);
+ return asyncError ?? AsyncError(error, stackTrace);
+ }
+
+ /// Creates a [_Node] with the current stack trace and linked to
+ /// [_currentNode].
+ ///
+ /// By default, the first frame of the first trace will be the line where
+ /// [_createNode] is called. If [level] is passed, the first trace will start
+ /// that many frames up instead.
+ _Node _createNode([int level = 0]) =>
+ _Node(_currentTrace(level + 1), _currentNode);
+
+ // TODO(nweiz): use a more robust way of detecting and tracking errors when
+ // issue 15105 is fixed.
+ /// Runs [f] with [_currentNode] set to [node].
+ ///
+ /// If [f] throws an error, this associates [node] with that error's stack
+ /// trace.
+ T _run<T>(T Function() f, _Node node) {
+ var previousNode = _currentNode;
+ _currentNode = node;
+ try {
+ return f();
+ } catch (e, stackTrace) {
+ // We can see the same stack trace multiple times if it's rethrown through
+ // guarded callbacks. The innermost chain will have the most
+ // information so it should take precedence.
+ _chains[stackTrace] ??= node;
+ rethrow;
+ } finally {
+ _currentNode = previousNode;
+ }
+ }
+
+ /// Like [Trace.current], but if the current stack trace has VM chaining
+ /// enabled, this only returns the innermost sub-trace.
+ Trace _currentTrace([int? level]) {
+ var stackTrace = StackTrace.current;
+ return LazyTrace(() {
+ var text = _trimVMChain(stackTrace);
+ var trace = Trace.parse(text);
+ // JS includes a frame for the call to StackTrace.current, but the VM
+ // doesn't, so we skip an extra frame in a JS context.
+ return Trace(trace.frames.skip((level ?? 0) + (inJS ? 2 : 1)),
+ original: text);
+ });
+ }
+
+ /// Removes the VM's stack chains from the native [trace], since we're
+ /// generating our own and we don't want duplicate frames.
+ String _trimVMChain(StackTrace trace) {
+ var text = trace.toString();
+ var index = text.indexOf(vmChainGap);
+ return index == -1 ? text : text.substring(0, index);
+ }
+}
+
+/// A linked list node representing a single entry in a stack chain.
+class _Node {
+ /// The stack trace for this link of the chain.
+ final Trace trace;
+
+ /// The previous node in the chain.
+ final _Node? previous;
+
+ _Node(StackTrace trace, [this.previous]) : trace = Trace.from(trace);
+
+ /// Converts this to a [Chain].
+ Chain toChain() {
+ var nodes = <Trace>[];
+ _Node? node = this;
+ while (node != null) {
+ nodes.add(node.trace);
+ node = node.previous;
+ }
+ return Chain(nodes);
+ }
+}
diff --git a/pkgs/stack_trace/lib/src/trace.dart b/pkgs/stack_trace/lib/src/trace.dart
new file mode 100644
index 0000000..b8c62f5
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/trace.dart
@@ -0,0 +1,341 @@
+// 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:math' as math;
+
+import 'chain.dart';
+import 'frame.dart';
+import 'lazy_trace.dart';
+import 'unparsed_frame.dart';
+import 'utils.dart';
+import 'vm_trace.dart';
+
+final _terseRegExp = RegExp(r'(-patch)?([/\\].*)?$');
+
+/// A RegExp to match V8's stack traces.
+///
+/// V8's traces start with a line that's either just "Error" or else is a
+/// description of the exception that occurred. That description can be multiple
+/// lines, so we just look for any line other than the first that begins with
+/// three or four spaces and "at".
+final _v8Trace = RegExp(r'\n ?at ');
+
+/// A RegExp to match indidual lines of V8's stack traces.
+///
+/// This is intended to filter out the leading exception details of the trace
+/// though it is possible for the message to match this as well.
+final _v8TraceLine = RegExp(r' ?at ');
+
+/// A RegExp to match Firefox's eval and Function stack traces.
+///
+/// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack
+///
+/// These stack traces look like:
+///
+/// ````
+/// anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+/// anonymous/<@https://example.com/stuff.js line 693 > eval:3:40
+/// ````
+final _firefoxEvalTrace = RegExp(r'@\S+ line \d+ >.* (Function|eval):\d+:\d+');
+
+/// A RegExp to match Firefox and Safari's stack traces.
+///
+/// Firefox and Safari have very similar stack trace formats, so we use the same
+/// logic for parsing them.
+///
+/// Firefox's trace frames start with the name of the function in which the
+/// error occurred, possibly including its parameters inside `()`. For example,
+/// `.VW.call$0("arg")@https://example.com/stuff.dart.js:560`.
+///
+/// Safari traces occasionally don't include the initial method name followed by
+/// "@", and they always have both the line and column number (or just a
+/// trailing colon if no column number is available). They can also contain
+/// empty lines or lines consisting only of `[native code]`.
+final _firefoxSafariTrace = RegExp(
+ r'^'
+ r'(' // Member description. Not present in some Safari frames.
+ r'([.0-9A-Za-z_$/<]|\(.*\))*' // Member name and arguments.
+ r'@'
+ r')?'
+ r'[^\s]*' // Frame URL.
+ r':\d*' // Line or column number. Some older frames only have a line number.
+ r'$',
+ multiLine: true);
+
+/// A RegExp to match this package's stack traces.
+final _friendlyTrace =
+ RegExp(r'^[^\s<][^\s]*( \d+(:\d+)?)?[ \t]+[^\s]+$', multiLine: true);
+
+/// A stack trace, comprised of a list of stack frames.
+class Trace implements StackTrace {
+ /// The stack frames that comprise this stack trace.
+ final List<Frame> frames;
+
+ /// The original stack trace from which this trace was parsed.
+ final StackTrace original;
+
+ /// Returns a human-readable representation of [stackTrace]. If [terse] is
+ /// set, this folds together multiple stack frames from the Dart core
+ /// libraries, so that only the core library method directly called from user
+ /// code is visible (see [Trace.terse]).
+ static String format(StackTrace stackTrace, {bool terse = true}) {
+ var trace = Trace.from(stackTrace);
+ if (terse) trace = trace.terse;
+ return trace.toString();
+ }
+
+ /// Returns the current stack trace.
+ ///
+ /// By default, the first frame of this trace will be the line where
+ /// [Trace.current] is called. If [level] is passed, the trace will start that
+ /// many frames up instead.
+ factory Trace.current([int level = 0]) {
+ if (level < 0) {
+ throw ArgumentError('Argument [level] must be greater than or equal '
+ 'to 0.');
+ }
+
+ var trace = Trace.from(StackTrace.current);
+ return LazyTrace(
+ () =>
+ // JS includes a frame for the call to StackTrace.current, but the VM
+ // doesn't, so we skip an extra frame in a JS context.
+ Trace(trace.frames.skip(level + (inJS ? 2 : 1)),
+ original: trace.original.toString()),
+ );
+ }
+
+ /// Returns a new stack trace containing the same data as [trace].
+ ///
+ /// If [trace] is a native [StackTrace], its data will be parsed out; if it's
+ /// a [Trace], it will be returned as-is.
+ factory Trace.from(StackTrace trace) {
+ if (trace is Trace) return trace;
+ if (trace is Chain) return trace.toTrace();
+ return LazyTrace(() => Trace.parse(trace.toString()));
+ }
+
+ /// Parses a string representation of a stack trace.
+ ///
+ /// [trace] should be formatted in the same way as a Dart VM or browser stack
+ /// trace. If it's formatted as a stack chain, this will return the equivalent
+ /// of [Chain.toTrace].
+ factory Trace.parse(String trace) {
+ try {
+ if (trace.isEmpty) return Trace(<Frame>[]);
+ if (trace.contains(_v8Trace)) return Trace.parseV8(trace);
+ if (trace.contains('\tat ')) return Trace.parseJSCore(trace);
+ if (trace.contains(_firefoxSafariTrace) ||
+ trace.contains(_firefoxEvalTrace)) {
+ return Trace.parseFirefox(trace);
+ }
+ if (trace.contains(chainGap)) return Chain.parse(trace).toTrace();
+ if (trace.contains(_friendlyTrace)) {
+ return Trace.parseFriendly(trace);
+ }
+
+ // Default to parsing the stack trace as a VM trace. This is also hit on
+ // IE and Safari, where the stack trace is just an empty string (issue
+ // 11257).
+ return Trace.parseVM(trace);
+ } on FormatException catch (error) {
+ throw FormatException('${error.message}\nStack trace:\n$trace');
+ }
+ }
+
+ /// Parses a string representation of a Dart VM stack trace.
+ Trace.parseVM(String trace) : this(_parseVM(trace), original: trace);
+
+ static List<Frame> _parseVM(String trace) {
+ // Ignore [vmChainGap]. This matches the behavior of
+ // `Chain.parse().toTrace()`.
+ var lines = trace
+ .trim()
+ .replaceAll(vmChainGap, '')
+ .split('\n')
+ .where((line) => line.isNotEmpty);
+
+ if (lines.isEmpty) {
+ return [];
+ }
+
+ var frames = lines.take(lines.length - 1).map(Frame.parseVM).toList();
+
+ // TODO(nweiz): Remove this when issue 23614 is fixed.
+ if (!lines.last.endsWith('.da')) {
+ frames.add(Frame.parseVM(lines.last));
+ }
+
+ return frames;
+ }
+
+ /// Parses a string representation of a Chrome/V8 stack trace.
+ Trace.parseV8(String trace)
+ : this(
+ trace
+ .split('\n')
+ .skip(1)
+ // It's possible that an Exception's description contains a line
+ // that looks like a V8 trace line, which will screw this up.
+ // Unfortunately, that's impossible to detect.
+ .skipWhile((line) => !line.startsWith(_v8TraceLine))
+ .map(Frame.parseV8),
+ original: trace);
+
+ /// Parses a string representation of a JavaScriptCore stack trace.
+ Trace.parseJSCore(String trace)
+ : this(
+ trace
+ .split('\n')
+ .where((line) => line != '\tat ')
+ .map(Frame.parseV8),
+ original: trace);
+
+ /// Parses a string representation of an Internet Explorer stack trace.
+ ///
+ /// IE10+ traces look just like V8 traces. Prior to IE10, stack traces can't
+ /// be retrieved.
+ Trace.parseIE(String trace) : this.parseV8(trace);
+
+ /// Parses a string representation of a Firefox stack trace.
+ Trace.parseFirefox(String trace)
+ : this(
+ trace
+ .trim()
+ .split('\n')
+ .where((line) => line.isNotEmpty && line != '[native code]')
+ .map(Frame.parseFirefox),
+ original: trace);
+
+ /// Parses a string representation of a Safari stack trace.
+ Trace.parseSafari(String trace) : this.parseFirefox(trace);
+
+ /// Parses a string representation of a Safari 6.1+ stack trace.
+ @Deprecated('Use Trace.parseSafari instead.')
+ Trace.parseSafari6_1(String trace) : this.parseSafari(trace);
+
+ /// Parses a string representation of a Safari 6.0 stack trace.
+ @Deprecated('Use Trace.parseSafari instead.')
+ Trace.parseSafari6_0(String trace)
+ : this(
+ trace
+ .trim()
+ .split('\n')
+ .where((line) => line != '[native code]')
+ .map(Frame.parseFirefox),
+ original: trace);
+
+ /// Parses this package's string representation of a stack trace.
+ ///
+ /// This also parses string representations of [Chain]s. They parse to the
+ /// same trace that [Chain.toTrace] would return.
+ Trace.parseFriendly(String trace)
+ : this(
+ trace.isEmpty
+ ? []
+ : trace
+ .trim()
+ .split('\n')
+ // Filter out asynchronous gaps from [Chain]s.
+ .where((line) => !line.startsWith('====='))
+ .map(Frame.parseFriendly),
+ original: trace);
+
+ /// Returns a new [Trace] comprised of [frames].
+ Trace(Iterable<Frame> frames, {String? original})
+ : frames = List<Frame>.unmodifiable(frames),
+ original = StackTrace.fromString(original ?? '');
+
+ /// Returns a VM-style [StackTrace] object.
+ ///
+ /// The return value's [toString] method will always return a string
+ /// representation in the Dart VM's stack trace format, regardless of what
+ /// platform is being used.
+ StackTrace get vmTrace => VMTrace(frames);
+
+ /// Returns a terser version of this trace.
+ ///
+ /// This is accomplished by folding together multiple stack frames from the
+ /// core library or from this package, as in [foldFrames]. Remaining core
+ /// library frames have their libraries, "-patch" suffixes, and line numbers
+ /// removed. If the outermost frame of the stack trace is a core library
+ /// frame, it's removed entirely.
+ ///
+ /// This won't do anything with a raw JavaScript trace, since there's no way
+ /// to determine which frames come from which Dart libraries. However, the
+ /// [`source_map_stack_trace`][https://pub.dev/packages/source_map_stack_trace]
+ /// package can be used to convert JavaScript traces into Dart-style traces.
+ ///
+ /// For custom folding, see [foldFrames].
+ Trace get terse => foldFrames((_) => false, terse: true);
+
+ /// Returns a new [Trace] based on `this` where multiple stack frames matching
+ /// [predicate] are folded together.
+ ///
+ /// This means that whenever there are multiple frames in a row that match
+ /// [predicate], only the last one is kept. This is useful for limiting the
+ /// amount of library code that appears in a stack trace by only showing user
+ /// code and code that's called by user code.
+ ///
+ /// If [terse] is true, this will also fold together frames from the core
+ /// library or from this package, simplify core library frames, and
+ /// potentially remove the outermost frame as in [Trace.terse].
+ Trace foldFrames(bool Function(Frame) predicate, {bool terse = false}) {
+ if (terse) {
+ var oldPredicate = predicate;
+ predicate = (frame) {
+ if (oldPredicate(frame)) return true;
+
+ if (frame.isCore) return true;
+ if (frame.package == 'stack_trace') return true;
+
+ // Ignore async stack frames without any line or column information.
+ // These come from the VM's async/await implementation and represent
+ // internal frames. They only ever show up in stack chains and are
+ // always surrounded by other traces that are actually useful, so we can
+ // just get rid of them.
+ // TODO(nweiz): Get rid of this logic some time after issue 22009 is
+ // fixed.
+ if (!frame.member!.contains('<async>')) return false;
+ return frame.line == null;
+ };
+ }
+
+ var newFrames = <Frame>[];
+ for (var frame in frames.reversed) {
+ if (frame is UnparsedFrame || !predicate(frame)) {
+ newFrames.add(frame);
+ } else if (newFrames.isEmpty || !predicate(newFrames.last)) {
+ newFrames.add(Frame(frame.uri, frame.line, frame.column, frame.member));
+ }
+ }
+
+ if (terse) {
+ newFrames = newFrames.map((frame) {
+ if (frame is UnparsedFrame || !predicate(frame)) return frame;
+ var library = frame.library.replaceAll(_terseRegExp, '');
+ return Frame(Uri.parse(library), null, null, frame.member);
+ }).toList();
+
+ if (newFrames.length > 1 && predicate(newFrames.first)) {
+ newFrames.removeAt(0);
+ }
+ }
+
+ return Trace(newFrames.reversed, original: original.toString());
+ }
+
+ @override
+ String toString() {
+ // Figure out the longest path so we know how much to pad.
+ var longest =
+ frames.map((frame) => frame.location.length).fold(0, math.max);
+
+ // Print out the stack trace nicely formatted.
+ return frames.map((frame) {
+ if (frame is UnparsedFrame) return '$frame\n';
+ return '${frame.location.padRight(longest)} ${frame.member}\n';
+ }).join();
+ }
+}
diff --git a/pkgs/stack_trace/lib/src/unparsed_frame.dart b/pkgs/stack_trace/lib/src/unparsed_frame.dart
new file mode 100644
index 0000000..27e97f6
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/unparsed_frame.dart
@@ -0,0 +1,33 @@
+// 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 'frame.dart';
+
+/// A frame that failed to parse.
+///
+/// The [member] property contains the original frame's contents.
+class UnparsedFrame implements Frame {
+ @override
+ final Uri uri = Uri(path: 'unparsed');
+ @override
+ final int? line = null;
+ @override
+ final int? column = null;
+ @override
+ final bool isCore = false;
+ @override
+ final String library = 'unparsed';
+ @override
+ final String? package = null;
+ @override
+ final String location = 'unparsed';
+
+ @override
+ final String member;
+
+ UnparsedFrame(this.member);
+
+ @override
+ String toString() => member;
+}
diff --git a/pkgs/stack_trace/lib/src/utils.dart b/pkgs/stack_trace/lib/src/utils.dart
new file mode 100644
index 0000000..bd971fe
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/utils.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.
+
+/// The line used in the string representation of stack chains to represent
+/// the gap between traces.
+const chainGap = '===== asynchronous gap ===========================\n';
+
+/// The line used in the string representation of VM stack chains to represent
+/// the gap between traces.
+final vmChainGap = RegExp(r'^<asynchronous suspension>\n?$', multiLine: true);
+
+// TODO(nweiz): When cross-platform imports work, use them to set this.
+/// Whether we're running in a JS context.
+const bool inJS = 0.0 is int;
diff --git a/pkgs/stack_trace/lib/src/vm_trace.dart b/pkgs/stack_trace/lib/src/vm_trace.dart
new file mode 100644
index 0000000..005b7af
--- /dev/null
+++ b/pkgs/stack_trace/lib/src/vm_trace.dart
@@ -0,0 +1,32 @@
+// 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 'frame.dart';
+
+/// An implementation of [StackTrace] that emulates the behavior of the VM's
+/// implementation.
+///
+/// In particular, when [toString] is called, this returns a string in the VM's
+/// stack trace format.
+class VMTrace implements StackTrace {
+ /// The stack frames that comprise this stack trace.
+ final List<Frame> frames;
+
+ VMTrace(this.frames);
+
+ @override
+ String toString() {
+ var i = 1;
+ return frames.map((frame) {
+ var number = '#${i++}'.padRight(8);
+ var member = frame.member!
+ .replaceAllMapped(RegExp(r'[^.]+\.<async>'),
+ (match) => '${match[1]}.<${match[1]}_async_body>')
+ .replaceAll('<fn>', '<anonymous closure>');
+ var line = frame.line ?? 0;
+ var column = frame.column ?? 0;
+ return '$number$member (${frame.uri}:$line:$column)\n';
+ }).join();
+ }
+}
diff --git a/pkgs/stack_trace/lib/stack_trace.dart b/pkgs/stack_trace/lib/stack_trace.dart
new file mode 100644
index 0000000..fad30ce
--- /dev/null
+++ b/pkgs/stack_trace/lib/stack_trace.dart
@@ -0,0 +1,8 @@
+// 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 'src/chain.dart';
+export 'src/frame.dart';
+export 'src/trace.dart';
+export 'src/unparsed_frame.dart';
diff --git a/pkgs/stack_trace/pubspec.yaml b/pkgs/stack_trace/pubspec.yaml
new file mode 100644
index 0000000..4f387b1
--- /dev/null
+++ b/pkgs/stack_trace/pubspec.yaml
@@ -0,0 +1,14 @@
+name: stack_trace
+version: 1.12.1
+description: A package for manipulating stack traces and printing them readably.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/stack_trace
+
+environment:
+ sdk: ^3.4.0
+
+dependencies:
+ path: ^1.8.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
+ test: ^1.16.6
diff --git a/pkgs/stack_trace/test/chain/chain_test.dart b/pkgs/stack_trace/test/chain/chain_test.dart
new file mode 100644
index 0000000..d5426dd
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/chain_test.dart
@@ -0,0 +1,375 @@
+// 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:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ group('Chain.parse()', () {
+ test('parses a real Chain', () async {
+ // ignore: only_throw_errors
+ final chain = await captureFuture(() => inMicrotask(() => throw 'error'));
+
+ expect(
+ Chain.parse(chain.toString()).toString(),
+ equals(chain.toString()),
+ );
+ });
+
+ test('parses an empty string', () {
+ var chain = Chain.parse('');
+ expect(chain.traces, isEmpty);
+ });
+
+ test('parses a chain containing empty traces', () {
+ var chain =
+ Chain.parse('===== asynchronous gap ===========================\n'
+ '===== asynchronous gap ===========================\n');
+ expect(chain.traces, hasLength(3));
+ expect(chain.traces[0].frames, isEmpty);
+ expect(chain.traces[1].frames, isEmpty);
+ expect(chain.traces[2].frames, isEmpty);
+ });
+
+ test('parses a chain with VM gaps', () {
+ final chain =
+ Chain.parse('#1 MyClass.run (package:my_lib.dart:134:5)\n'
+ '<asynchronous suspension>\n'
+ '#2 main (file:///my_app.dart:9:3)\n'
+ '<asynchronous suspension>\n');
+ expect(chain.traces, hasLength(2));
+ expect(chain.traces[0].frames, hasLength(1));
+ expect(chain.traces[0].frames[0].toString(),
+ equals('package:my_lib.dart 134:5 in MyClass.run'));
+ expect(chain.traces[1].frames, hasLength(1));
+ expect(
+ chain.traces[1].frames[0].toString(),
+ anyOf(
+ equals('/my_app.dart 9:3 in main'), // VM
+ equals('file:///my_app.dart 9:3 in main'), // Browser
+ ),
+ );
+ });
+ });
+
+ group('Chain.capture()', () {
+ test('with onError blocks errors', () {
+ Chain.capture(() {
+ return Future<void>.error('oh no');
+ }, onError: expectAsync2((error, chain) {
+ expect(error, equals('oh no'));
+ expect(chain, isA<Chain>());
+ })).then(expectAsync1((_) {}, count: 0),
+ onError: expectAsync2((_, __) {}, count: 0));
+ });
+
+ test('with no onError blocks errors', () {
+ runZonedGuarded(() {
+ Chain.capture(() => Future<void>.error('oh no')).then(
+ expectAsync1((_) {}, count: 0),
+ onError: expectAsync2((_, __) {}, count: 0));
+ }, expectAsync2((error, chain) {
+ expect(error, equals('oh no'));
+ expect(chain, isA<Chain>());
+ }));
+ });
+
+ test("with errorZone: false doesn't block errors", () {
+ expect(Chain.capture(() => Future<void>.error('oh no'), errorZone: false),
+ throwsA('oh no'));
+ });
+
+ test("doesn't allow onError and errorZone: false", () {
+ expect(() => Chain.capture(() {}, onError: (_, __) {}, errorZone: false),
+ throwsArgumentError);
+ });
+
+ group('with when: false', () {
+ test("with no onError doesn't block errors", () {
+ expect(Chain.capture(() => Future<void>.error('oh no'), when: false),
+ throwsA('oh no'));
+ });
+
+ test('with onError blocks errors', () {
+ Chain.capture(() {
+ return Future<void>.error('oh no');
+ }, onError: expectAsync2((error, chain) {
+ expect(error, equals('oh no'));
+ expect(chain, isA<Chain>());
+ }), when: false);
+ });
+
+ test("doesn't enable chain-tracking", () {
+ return Chain.disable(() {
+ return Chain.capture(() {
+ var completer = Completer<Chain>();
+ inMicrotask(() {
+ completer.complete(Chain.current());
+ });
+
+ return completer.future.then((chain) {
+ expect(chain.traces, hasLength(1));
+ });
+ }, when: false);
+ });
+ });
+ });
+ });
+
+ test('Chain.capture() with custom zoneValues', () {
+ return Chain.capture(() {
+ expect(Zone.current[#enabled], true);
+ }, zoneValues: {#enabled: true});
+ });
+
+ group('Chain.disable()', () {
+ test('disables chain-tracking', () {
+ return Chain.disable(() {
+ var completer = Completer<Chain>();
+ inMicrotask(() => completer.complete(Chain.current()));
+
+ return completer.future.then((chain) {
+ expect(chain.traces, hasLength(1));
+ });
+ });
+ });
+
+ test('Chain.capture() re-enables chain-tracking', () {
+ return Chain.disable(() {
+ return Chain.capture(() {
+ var completer = Completer<Chain>();
+ inMicrotask(() => completer.complete(Chain.current()));
+
+ return completer.future.then((chain) {
+ expect(chain.traces, hasLength(2));
+ });
+ });
+ });
+ });
+
+ test('preserves parent zones of the capture zone', () {
+ // The outer disable call turns off the test package's chain-tracking.
+ return Chain.disable(() {
+ return runZoned(() {
+ return Chain.capture(() {
+ expect(Chain.disable(() => Zone.current[#enabled]), isTrue);
+ });
+ }, zoneValues: {#enabled: true});
+ });
+ });
+
+ test('preserves child zones of the capture zone', () {
+ // The outer disable call turns off the test package's chain-tracking.
+ return Chain.disable(() {
+ return Chain.capture(() {
+ return runZoned(() {
+ expect(Chain.disable(() => Zone.current[#enabled]), isTrue);
+ }, zoneValues: {#enabled: true});
+ });
+ });
+ });
+
+ test("with when: false doesn't disable", () {
+ return Chain.capture(() {
+ return Chain.disable(() {
+ var completer = Completer<Chain>();
+ inMicrotask(() => completer.complete(Chain.current()));
+
+ return completer.future.then((chain) {
+ expect(chain.traces, hasLength(2));
+ });
+ }, when: false);
+ });
+ });
+ });
+
+ test('toString() ensures that all traces are aligned', () {
+ var chain = Chain([
+ Trace.parse('short 10:11 Foo.bar\n'),
+ Trace.parse('loooooooooooong 10:11 Zop.zoop')
+ ]);
+
+ expect(
+ chain.toString(),
+ equals('short 10:11 Foo.bar\n'
+ '===== asynchronous gap ===========================\n'
+ 'loooooooooooong 10:11 Zop.zoop\n'));
+ });
+
+ var userSlashCode = p.join('user', 'code.dart');
+ group('Chain.terse', () {
+ test('makes each trace terse', () {
+ var chain = Chain([
+ Trace.parse('dart:core 10:11 Foo.bar\n'
+ 'dart:core 10:11 Bar.baz\n'
+ 'user/code.dart 10:11 Bang.qux\n'
+ 'dart:core 10:11 Zip.zap\n'
+ 'dart:core 10:11 Zop.zoop'),
+ Trace.parse('user/code.dart 10:11 Bang.qux\n'
+ 'dart:core 10:11 Foo.bar\n'
+ 'package:stack_trace/stack_trace.dart 10:11 Bar.baz\n'
+ 'dart:core 10:11 Zip.zap\n'
+ 'user/code.dart 10:11 Zop.zoop')
+ ]);
+
+ expect(
+ chain.terse.toString(),
+ equals('dart:core Bar.baz\n'
+ '$userSlashCode 10:11 Bang.qux\n'
+ '===== asynchronous gap ===========================\n'
+ '$userSlashCode 10:11 Bang.qux\n'
+ 'dart:core Zip.zap\n'
+ '$userSlashCode 10:11 Zop.zoop\n'));
+ });
+
+ test('eliminates internal-only traces', () {
+ var chain = Chain([
+ Trace.parse('user/code.dart 10:11 Foo.bar\n'
+ 'dart:core 10:11 Bar.baz'),
+ Trace.parse('dart:core 10:11 Foo.bar\n'
+ 'package:stack_trace/stack_trace.dart 10:11 Bar.baz\n'
+ 'dart:core 10:11 Zip.zap'),
+ Trace.parse('user/code.dart 10:11 Foo.bar\n'
+ 'dart:core 10:11 Bar.baz')
+ ]);
+
+ expect(
+ chain.terse.toString(),
+ equals('$userSlashCode 10:11 Foo.bar\n'
+ '===== asynchronous gap ===========================\n'
+ '$userSlashCode 10:11 Foo.bar\n'));
+ });
+
+ test("doesn't return an empty chain", () {
+ var chain = Chain([
+ Trace.parse('dart:core 10:11 Foo.bar\n'
+ 'package:stack_trace/stack_trace.dart 10:11 Bar.baz\n'
+ 'dart:core 10:11 Zip.zap'),
+ Trace.parse('dart:core 10:11 A.b\n'
+ 'package:stack_trace/stack_trace.dart 10:11 C.d\n'
+ 'dart:core 10:11 E.f')
+ ]);
+
+ expect(chain.terse.toString(), equals('dart:core E.f\n'));
+ });
+
+ // Regression test for #9
+ test("doesn't crash on empty traces", () {
+ var chain = Chain([
+ Trace.parse('user/code.dart 10:11 Bang.qux'),
+ Trace([]),
+ Trace.parse('user/code.dart 10:11 Bang.qux')
+ ]);
+
+ expect(
+ chain.terse.toString(),
+ equals('$userSlashCode 10:11 Bang.qux\n'
+ '===== asynchronous gap ===========================\n'
+ '$userSlashCode 10:11 Bang.qux\n'));
+ });
+ });
+
+ group('Chain.foldFrames', () {
+ test('folds each trace', () {
+ var chain = Chain([
+ Trace.parse('a.dart 10:11 Foo.bar\n'
+ 'a.dart 10:11 Bar.baz\n'
+ 'b.dart 10:11 Bang.qux\n'
+ 'a.dart 10:11 Zip.zap\n'
+ 'a.dart 10:11 Zop.zoop'),
+ Trace.parse('a.dart 10:11 Foo.bar\n'
+ 'a.dart 10:11 Bar.baz\n'
+ 'a.dart 10:11 Bang.qux\n'
+ 'a.dart 10:11 Zip.zap\n'
+ 'b.dart 10:11 Zop.zoop')
+ ]);
+
+ var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
+ expect(
+ folded.toString(),
+ equals('a.dart 10:11 Bar.baz\n'
+ 'b.dart 10:11 Bang.qux\n'
+ 'a.dart 10:11 Zop.zoop\n'
+ '===== asynchronous gap ===========================\n'
+ 'a.dart 10:11 Zip.zap\n'
+ 'b.dart 10:11 Zop.zoop\n'));
+ });
+
+ test('with terse: true, folds core frames as well', () {
+ var chain = Chain([
+ Trace.parse('a.dart 10:11 Foo.bar\n'
+ 'dart:async-patch/future.dart 10:11 Zip.zap\n'
+ 'b.dart 10:11 Bang.qux\n'
+ 'dart:core 10:11 Bar.baz\n'
+ 'a.dart 10:11 Zop.zoop'),
+ Trace.parse('a.dart 10:11 Foo.bar\n'
+ 'a.dart 10:11 Bar.baz\n'
+ 'a.dart 10:11 Bang.qux\n'
+ 'a.dart 10:11 Zip.zap\n'
+ 'b.dart 10:11 Zop.zoop')
+ ]);
+
+ var folded =
+ chain.foldFrames((frame) => frame.library == 'a.dart', terse: true);
+ expect(
+ folded.toString(),
+ equals('dart:async Zip.zap\n'
+ 'b.dart 10:11 Bang.qux\n'
+ '===== asynchronous gap ===========================\n'
+ 'a.dart Zip.zap\n'
+ 'b.dart 10:11 Zop.zoop\n'));
+ });
+
+ test('eliminates completely-folded traces', () {
+ var chain = Chain([
+ Trace.parse('a.dart 10:11 Foo.bar\n'
+ 'b.dart 10:11 Bang.qux'),
+ Trace.parse('a.dart 10:11 Foo.bar\n'
+ 'a.dart 10:11 Bang.qux'),
+ Trace.parse('a.dart 10:11 Zip.zap\n'
+ 'b.dart 10:11 Zop.zoop')
+ ]);
+
+ var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
+ expect(
+ folded.toString(),
+ equals('a.dart 10:11 Foo.bar\n'
+ 'b.dart 10:11 Bang.qux\n'
+ '===== asynchronous gap ===========================\n'
+ 'a.dart 10:11 Zip.zap\n'
+ 'b.dart 10:11 Zop.zoop\n'));
+ });
+
+ test("doesn't return an empty trace", () {
+ var chain = Chain([
+ Trace.parse('a.dart 10:11 Foo.bar\n'
+ 'a.dart 10:11 Bang.qux')
+ ]);
+
+ var folded = chain.foldFrames((frame) => frame.library == 'a.dart');
+ expect(folded.toString(), equals('a.dart 10:11 Bang.qux\n'));
+ });
+ });
+
+ test('Chain.toTrace eliminates asynchronous gaps', () {
+ var trace = Chain([
+ Trace.parse('user/code.dart 10:11 Foo.bar\n'
+ 'dart:core 10:11 Bar.baz'),
+ Trace.parse('user/code.dart 10:11 Foo.bar\n'
+ 'dart:core 10:11 Bar.baz')
+ ]).toTrace();
+
+ expect(
+ trace.toString(),
+ equals('$userSlashCode 10:11 Foo.bar\n'
+ 'dart:core 10:11 Bar.baz\n'
+ '$userSlashCode 10:11 Foo.bar\n'
+ 'dart:core 10:11 Bar.baz\n'));
+ });
+}
diff --git a/pkgs/stack_trace/test/chain/dart2js_test.dart b/pkgs/stack_trace/test/chain/dart2js_test.dart
new file mode 100644
index 0000000..abb842d
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/dart2js_test.dart
@@ -0,0 +1,337 @@
+// 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
+
+// dart2js chain tests are separated out because dart2js stack traces are
+// inconsistent due to inlining and browser differences. These tests don't
+// assert anything about the content of the traces, just the number of traces in
+// a chain.
+@TestOn('js')
+library;
+
+import 'dart:async';
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ group('capture() with onError catches exceptions', () {
+ test('thrown synchronously', () async {
+ var chain = await captureFuture(() => throw 'error');
+ expect(chain.traces, hasLength(1));
+ });
+
+ test('thrown in a microtask', () async {
+ var chain = await captureFuture(() => inMicrotask(() => throw 'error'));
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('thrown in a one-shot timer', () async {
+ var chain =
+ await captureFuture(() => inOneShotTimer(() => throw 'error'));
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('thrown in a periodic timer', () async {
+ var chain =
+ await captureFuture(() => inPeriodicTimer(() => throw 'error'));
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('thrown in a nested series of asynchronous operations', () async {
+ var chain = await captureFuture(() {
+ inPeriodicTimer(() {
+ inOneShotTimer(() => inMicrotask(() => throw 'error'));
+ });
+ });
+
+ expect(chain.traces, hasLength(4));
+ });
+
+ test('thrown in a long future chain', () async {
+ var chain = await captureFuture(() => inFutureChain(() => throw 'error'));
+
+ // Despite many asynchronous operations, there's only one level of
+ // nested calls, so there should be only two traces in the chain. This
+ // is important; programmers expect stack trace memory consumption to be
+ // O(depth of program), not O(length of program).
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('thrown in new Future()', () async {
+ var chain = await captureFuture(() => inNewFuture(() => throw 'error'));
+ expect(chain.traces, hasLength(3));
+ });
+
+ test('thrown in new Future.sync()', () async {
+ var chain = await captureFuture(() {
+ inMicrotask(() => inSyncFuture(() => throw 'error'));
+ });
+
+ expect(chain.traces, hasLength(3));
+ });
+
+ test('multiple times', () {
+ var completer = Completer<void>();
+ var first = true;
+
+ Chain.capture(() {
+ inMicrotask(() => throw 'first error');
+ inPeriodicTimer(() => throw 'second error');
+ }, onError: (error, chain) {
+ try {
+ if (first) {
+ expect(error, equals('first error'));
+ expect(chain.traces, hasLength(2));
+ first = false;
+ } else {
+ expect(error, equals('second error'));
+ expect(chain.traces, hasLength(2));
+ completer.complete();
+ }
+ } on Object catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ return completer.future;
+ });
+
+ test('passed to a completer', () async {
+ var trace = Trace.current();
+ var chain = await captureFuture(() {
+ inMicrotask(() => completerErrorFuture(trace));
+ });
+
+ expect(chain.traces, hasLength(3));
+
+ // The first trace is the trace that was manually reported for the
+ // error.
+ expect(chain.traces.first.toString(), equals(trace.toString()));
+ });
+
+ test('passed to a completer with no stack trace', () async {
+ var chain = await captureFuture(() {
+ inMicrotask(completerErrorFuture);
+ });
+
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('passed to a stream controller', () async {
+ var trace = Trace.current();
+ var chain = await captureFuture(() {
+ inMicrotask(() => controllerErrorStream(trace).listen(null));
+ });
+
+ expect(chain.traces, hasLength(3));
+ expect(chain.traces.first.toString(), equals(trace.toString()));
+ });
+
+ test('passed to a stream controller with no stack trace', () async {
+ var chain = await captureFuture(() {
+ inMicrotask(() => controllerErrorStream().listen(null));
+ });
+
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('and relays them to the parent zone', () {
+ var completer = Completer<void>();
+
+ runZonedGuarded(() {
+ Chain.capture(() {
+ inMicrotask(() => throw 'error');
+ }, onError: (error, chain) {
+ expect(error, equals('error'));
+ expect(chain.traces, hasLength(2));
+ throw error;
+ });
+ }, (error, chain) {
+ try {
+ expect(error, equals('error'));
+ expect(chain,
+ isA<Chain>().having((c) => c.traces, 'traces', hasLength(2)));
+ completer.complete();
+ } on Object catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ return completer.future;
+ });
+ });
+
+ test('capture() without onError passes exceptions to parent zone', () {
+ var completer = Completer<void>();
+
+ runZonedGuarded(() {
+ Chain.capture(() => inMicrotask(() => throw 'error'));
+ }, (error, chain) {
+ try {
+ expect(error, equals('error'));
+ expect(chain,
+ isA<Chain>().having((c) => c.traces, 'traces', hasLength(2)));
+ completer.complete();
+ } on Object catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ return completer.future;
+ });
+
+ group('current() within capture()', () {
+ test('called in a microtask', () async {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inMicrotask(() => completer.complete(Chain.current()));
+ });
+
+ var chain = await completer.future;
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('called in a one-shot timer', () async {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inOneShotTimer(() => completer.complete(Chain.current()));
+ });
+
+ var chain = await completer.future;
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('called in a periodic timer', () async {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inPeriodicTimer(() => completer.complete(Chain.current()));
+ });
+
+ var chain = await completer.future;
+ expect(chain.traces, hasLength(2));
+ });
+
+ test('called in a nested series of asynchronous operations', () async {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inPeriodicTimer(() {
+ inOneShotTimer(() {
+ inMicrotask(() => completer.complete(Chain.current()));
+ });
+ });
+ });
+
+ var chain = await completer.future;
+ expect(chain.traces, hasLength(4));
+ });
+
+ test('called in a long future chain', () async {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inFutureChain(() => completer.complete(Chain.current()));
+ });
+
+ var chain = await completer.future;
+ expect(chain.traces, hasLength(2));
+ });
+ });
+
+ test(
+ 'current() outside of capture() returns a chain wrapping the current trace',
+ () =>
+ // The test runner runs all tests with chains enabled.
+ Chain.disable(() async {
+ var completer = Completer<Chain>();
+ inMicrotask(() => completer.complete(Chain.current()));
+
+ var chain = await completer.future;
+ // Since the chain wasn't loaded within [Chain.capture], the full stack
+ // chain isn't available and it just returns the current stack when
+ // called.
+ expect(chain.traces, hasLength(1));
+ }),
+ );
+
+ group('forTrace() within capture()', () {
+ test('called for a stack trace from a microtask', () async {
+ var chain = await Chain.capture(
+ () => chainForTrace(inMicrotask, () => throw 'error'));
+
+ // Because [chainForTrace] has to set up a future chain to capture the
+ // stack trace while still showing it to the zone specification, it adds
+ // an additional level of async nesting and so an additional trace.
+ expect(chain.traces, hasLength(3));
+ });
+
+ test('called for a stack trace from a one-shot timer', () async {
+ var chain = await Chain.capture(
+ () => chainForTrace(inOneShotTimer, () => throw 'error'));
+
+ expect(chain.traces, hasLength(3));
+ });
+
+ test('called for a stack trace from a periodic timer', () async {
+ var chain = await Chain.capture(
+ () => chainForTrace(inPeriodicTimer, () => throw 'error'));
+
+ expect(chain.traces, hasLength(3));
+ });
+
+ test(
+ 'called for a stack trace from a nested series of asynchronous '
+ 'operations', () async {
+ var chain = await Chain.capture(() => chainForTrace((callback) {
+ inPeriodicTimer(() => inOneShotTimer(() => inMicrotask(callback)));
+ }, () => throw 'error'));
+
+ expect(chain.traces, hasLength(5));
+ });
+
+ test('called for a stack trace from a long future chain', () async {
+ var chain = await Chain.capture(
+ () => chainForTrace(inFutureChain, () => throw 'error'));
+
+ expect(chain.traces, hasLength(3));
+ });
+
+ test(
+ 'called for an unregistered stack trace returns a chain wrapping that '
+ 'trace', () {
+ late StackTrace trace;
+ var chain = Chain.capture(() {
+ try {
+ throw 'error';
+ } catch (_, stackTrace) {
+ trace = stackTrace;
+ return Chain.forTrace(stackTrace);
+ }
+ });
+
+ expect(chain.traces, hasLength(1));
+ expect(
+ chain.traces.first.toString(), equals(Trace.from(trace).toString()));
+ });
+ });
+
+ test(
+ 'forTrace() outside of capture() returns a chain wrapping the given '
+ 'trace', () {
+ late StackTrace trace;
+ var chain = Chain.capture(() {
+ try {
+ throw 'error';
+ } catch (_, stackTrace) {
+ trace = stackTrace;
+ return Chain.forTrace(stackTrace);
+ }
+ });
+
+ expect(chain.traces, hasLength(1));
+ expect(chain.traces.first.toString(), equals(Trace.from(trace).toString()));
+ });
+}
diff --git a/pkgs/stack_trace/test/chain/utils.dart b/pkgs/stack_trace/test/chain/utils.dart
new file mode 100644
index 0000000..27fb0e6
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/utils.dart
@@ -0,0 +1,94 @@
+// 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/test.dart';
+
+/// Runs [callback] in a microtask callback.
+void inMicrotask(void Function() callback) => scheduleMicrotask(callback);
+
+/// Runs [callback] in a one-shot timer callback.
+void inOneShotTimer(void Function() callback) => Timer.run(callback);
+
+/// Runs [callback] once in a periodic timer callback.
+void inPeriodicTimer(void Function() callback) {
+ var count = 0;
+ Timer.periodic(const Duration(milliseconds: 1), (timer) {
+ count++;
+ if (count != 5) return;
+ timer.cancel();
+ callback();
+ });
+}
+
+/// Runs [callback] within a long asynchronous Future chain.
+void inFutureChain(void Function() callback) {
+ Future(() {})
+ .then((_) => Future(() {}))
+ .then((_) => Future(() {}))
+ .then((_) => Future(() {}))
+ .then((_) => Future(() {}))
+ .then((_) => callback())
+ .then((_) => Future(() {}));
+}
+
+void inNewFuture(void Function() callback) {
+ Future(callback);
+}
+
+void inSyncFuture(void Function() callback) {
+ Future.sync(callback);
+}
+
+/// Returns a Future that completes to an error using a completer.
+///
+/// If [trace] is passed, it's used as the stack trace for the error.
+Future<void> completerErrorFuture([StackTrace? trace]) {
+ var completer = Completer<void>();
+ completer.completeError('error', trace);
+ return completer.future;
+}
+
+/// Returns a Stream that emits an error using a controller.
+///
+/// If [trace] is passed, it's used as the stack trace for the error.
+Stream<void> controllerErrorStream([StackTrace? trace]) {
+ var controller = StreamController<void>();
+ controller.addError('error', trace);
+ return controller.stream;
+}
+
+/// Runs [callback] within [asyncFn], then converts any errors raised into a
+/// [Chain] with [Chain.forTrace].
+Future<Chain> chainForTrace(
+ void Function(void Function()) asyncFn, void Function() callback) {
+ var completer = Completer<Chain>();
+ asyncFn(() {
+ // We use `new Future.value().then(...)` here as opposed to [new Future] or
+ // [new Future.sync] because those methods don't pass the exception through
+ // the zone specification before propagating it, so there's no chance to
+ // attach a chain to its stack trace. See issue 15105.
+ Future<void>.value()
+ .then((_) => callback())
+ .catchError(completer.completeError);
+ });
+
+ return completer.future
+ .catchError((_, StackTrace stackTrace) => Chain.forTrace(stackTrace));
+}
+
+/// Runs [callback] in a [Chain.capture] zone and returns a Future that
+/// completes to the stack chain for an error thrown by [callback].
+///
+/// [callback] is expected to throw the string `"error"`.
+Future<Chain> captureFuture(void Function() callback) {
+ var completer = Completer<Chain>();
+ Chain.capture(callback, onError: (error, chain) {
+ expect(error, equals('error'));
+ completer.complete(chain);
+ });
+ return completer.future;
+}
diff --git a/pkgs/stack_trace/test/chain/vm_test.dart b/pkgs/stack_trace/test/chain/vm_test.dart
new file mode 100644
index 0000000..5c6c0b7
--- /dev/null
+++ b/pkgs/stack_trace/test/chain/vm_test.dart
@@ -0,0 +1,508 @@
+// 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
+
+// VM chain tests can rely on stronger guarantees about the contents of the
+// stack traces than dart2js.
+@TestOn('dart-vm')
+library;
+
+import 'dart:async';
+
+import 'package:stack_trace/src/utils.dart';
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+import '../utils.dart';
+import 'utils.dart';
+
+void main() {
+ group('capture() with onError catches exceptions', () {
+ test('thrown synchronously', () async {
+ late StackTrace vmTrace;
+ var chain = await captureFuture(() {
+ try {
+ throw 'error';
+ } catch (_, stackTrace) {
+ vmTrace = stackTrace;
+ rethrow;
+ }
+ });
+
+ // Because there's no chain context for a synchronous error, we fall back
+ // on the VM's stack chain tracking.
+ expect(
+ chain.toString(), equals(Chain.parse(vmTrace.toString()).toString()));
+ });
+
+ test('thrown in a microtask', () {
+ return captureFuture(() => inMicrotask(() => throw 'error'))
+ .then((chain) {
+ // Since there was only one asynchronous operation, there should be only
+ // two traces in the chain.
+ expect(chain.traces, hasLength(2));
+
+ // The first frame of the first trace should be the line on which the
+ // actual error was thrown.
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+
+ // The second trace should describe the stack when the error callback
+ // was scheduled.
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ });
+ });
+
+ test('thrown in a one-shot timer', () {
+ return captureFuture(() => inOneShotTimer(() => throw 'error'))
+ .then((chain) {
+ expect(chain.traces, hasLength(2));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inOneShotTimer'))));
+ });
+ });
+
+ test('thrown in a periodic timer', () {
+ return captureFuture(() => inPeriodicTimer(() => throw 'error'))
+ .then((chain) {
+ expect(chain.traces, hasLength(2));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inPeriodicTimer'))));
+ });
+ });
+
+ test('thrown in a nested series of asynchronous operations', () {
+ return captureFuture(() {
+ inPeriodicTimer(() {
+ inOneShotTimer(() => inMicrotask(() => throw 'error'));
+ });
+ }).then((chain) {
+ expect(chain.traces, hasLength(4));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inOneShotTimer'))));
+ expect(chain.traces[3].frames,
+ contains(frameMember(startsWith('inPeriodicTimer'))));
+ });
+ });
+
+ test('thrown in a long future chain', () {
+ return captureFuture(() => inFutureChain(() => throw 'error'))
+ .then((chain) {
+ // Despite many asynchronous operations, there's only one level of
+ // nested calls, so there should be only two traces in the chain. This
+ // is important; programmers expect stack trace memory consumption to be
+ // O(depth of program), not O(length of program).
+ expect(chain.traces, hasLength(2));
+
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inFutureChain'))));
+ });
+ });
+
+ test('thrown in new Future()', () {
+ return captureFuture(() => inNewFuture(() => throw 'error'))
+ .then((chain) {
+ expect(chain.traces, hasLength(3));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+
+ // The second trace is the one captured by
+ // [StackZoneSpecification.errorCallback]. Because that runs
+ // asynchronously within [new Future], it doesn't actually refer to the
+ // source file at all.
+ expect(chain.traces[1].frames,
+ everyElement(frameLibrary(isNot(contains('chain_test')))));
+
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inNewFuture'))));
+ });
+ });
+
+ test('thrown in new Future.sync()', () {
+ return captureFuture(() {
+ inMicrotask(() => inSyncFuture(() => throw 'error'));
+ }).then((chain) {
+ expect(chain.traces, hasLength(3));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inSyncFuture'))));
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ });
+ });
+
+ test('multiple times', () {
+ var completer = Completer<void>();
+ var first = true;
+
+ Chain.capture(() {
+ inMicrotask(() => throw 'first error');
+ inPeriodicTimer(() => throw 'second error');
+ }, onError: (error, chain) {
+ try {
+ if (first) {
+ expect(error, equals('first error'));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ first = false;
+ } else {
+ expect(error, equals('second error'));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inPeriodicTimer'))));
+ completer.complete();
+ }
+ } on Object catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ return completer.future;
+ });
+
+ test('passed to a completer', () {
+ var trace = Trace.current();
+ return captureFuture(() {
+ inMicrotask(() => completerErrorFuture(trace));
+ }).then((chain) {
+ expect(chain.traces, hasLength(3));
+
+ // The first trace is the trace that was manually reported for the
+ // error.
+ expect(chain.traces.first.toString(), equals(trace.toString()));
+
+ // The second trace is the trace that was captured when
+ // [Completer.addError] was called.
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('completerErrorFuture'))));
+
+ // The third trace is the automatically-captured trace from when the
+ // microtask was scheduled.
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ });
+ });
+
+ test('passed to a completer with no stack trace', () {
+ return captureFuture(() {
+ inMicrotask(completerErrorFuture);
+ }).then((chain) {
+ expect(chain.traces, hasLength(2));
+
+ // The first trace is the one captured when [Completer.addError] was
+ // called.
+ expect(chain.traces[0].frames,
+ contains(frameMember(startsWith('completerErrorFuture'))));
+
+ // The second trace is the automatically-captured trace from when the
+ // microtask was scheduled.
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ });
+ });
+
+ test('passed to a stream controller', () {
+ var trace = Trace.current();
+ return captureFuture(() {
+ inMicrotask(() => controllerErrorStream(trace).listen(null));
+ }).then((chain) {
+ expect(chain.traces, hasLength(3));
+ expect(chain.traces.first.toString(), equals(trace.toString()));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('controllerErrorStream'))));
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ });
+ });
+
+ test('passed to a stream controller with no stack trace', () {
+ return captureFuture(() {
+ inMicrotask(() => controllerErrorStream().listen(null));
+ }).then((chain) {
+ expect(chain.traces, hasLength(2));
+ expect(chain.traces[0].frames,
+ contains(frameMember(startsWith('controllerErrorStream'))));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ });
+ });
+
+ test('and relays them to the parent zone', () {
+ var completer = Completer<void>();
+
+ runZonedGuarded(() {
+ Chain.capture(() {
+ inMicrotask(() => throw 'error');
+ }, onError: (error, chain) {
+ expect(error, equals('error'));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ throw error;
+ });
+ }, (error, chain) {
+ try {
+ expect(error, equals('error'));
+ expect(
+ chain,
+ isA<Chain>().having((c) => c.traces[1].frames, 'traces[1].frames',
+ contains(frameMember(startsWith('inMicrotask')))));
+ completer.complete();
+ } on Object catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ return completer.future;
+ });
+ });
+
+ test('capture() without onError passes exceptions to parent zone', () {
+ var completer = Completer<void>();
+
+ runZonedGuarded(() {
+ Chain.capture(() => inMicrotask(() => throw 'error'));
+ }, (error, chain) {
+ try {
+ expect(error, equals('error'));
+ expect(
+ chain,
+ isA<Chain>().having((c) => c.traces[1].frames, 'traces[1].frames',
+ contains(frameMember(startsWith('inMicrotask')))));
+ completer.complete();
+ } on Object catch (error, stackTrace) {
+ completer.completeError(error, stackTrace);
+ }
+ });
+
+ return completer.future;
+ });
+
+ group('current() within capture()', () {
+ test('called in a microtask', () {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inMicrotask(() => completer.complete(Chain.current()));
+ });
+
+ return completer.future.then((chain) {
+ expect(chain.traces, hasLength(2));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ });
+ });
+
+ test('called in a one-shot timer', () {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inOneShotTimer(() => completer.complete(Chain.current()));
+ });
+
+ return completer.future.then((chain) {
+ expect(chain.traces, hasLength(2));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inOneShotTimer'))));
+ });
+ });
+
+ test('called in a periodic timer', () {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inPeriodicTimer(() => completer.complete(Chain.current()));
+ });
+
+ return completer.future.then((chain) {
+ expect(chain.traces, hasLength(2));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inPeriodicTimer'))));
+ });
+ });
+
+ test('called in a nested series of asynchronous operations', () {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inPeriodicTimer(() {
+ inOneShotTimer(() {
+ inMicrotask(() => completer.complete(Chain.current()));
+ });
+ });
+ });
+
+ return completer.future.then((chain) {
+ expect(chain.traces, hasLength(4));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inOneShotTimer'))));
+ expect(chain.traces[3].frames,
+ contains(frameMember(startsWith('inPeriodicTimer'))));
+ });
+ });
+
+ test('called in a long future chain', () {
+ var completer = Completer<Chain>();
+ Chain.capture(() {
+ inFutureChain(() => completer.complete(Chain.current()));
+ });
+
+ return completer.future.then((chain) {
+ expect(chain.traces, hasLength(2));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('inFutureChain'))));
+ });
+ });
+ });
+
+ test(
+ 'current() outside of capture() returns a chain wrapping the current '
+ 'trace', () {
+ // The test runner runs all tests with chains enabled.
+ return Chain.disable(() {
+ var completer = Completer<Chain>();
+ inMicrotask(() => completer.complete(Chain.current()));
+
+ return completer.future.then((chain) {
+ // Since the chain wasn't loaded within [Chain.capture], the full stack
+ // chain isn't available and it just returns the current stack when
+ // called.
+ expect(chain.traces, hasLength(1));
+ expect(
+ chain.traces.first.frames.first, frameMember(startsWith('main')));
+ });
+ });
+ });
+
+ group('forTrace() within capture()', () {
+ test('called for a stack trace from a microtask', () {
+ return Chain.capture(() {
+ return chainForTrace(inMicrotask, () => throw 'error');
+ }).then((chain) {
+ // Because [chainForTrace] has to set up a future chain to capture the
+ // stack trace while still showing it to the zone specification, it adds
+ // an additional level of async nesting and so an additional trace.
+ expect(chain.traces, hasLength(3));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('chainForTrace'))));
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ });
+ });
+
+ test('called for a stack trace from a one-shot timer', () {
+ return Chain.capture(() {
+ return chainForTrace(inOneShotTimer, () => throw 'error');
+ }).then((chain) {
+ expect(chain.traces, hasLength(3));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('chainForTrace'))));
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inOneShotTimer'))));
+ });
+ });
+
+ test('called for a stack trace from a periodic timer', () {
+ return Chain.capture(() {
+ return chainForTrace(inPeriodicTimer, () => throw 'error');
+ }).then((chain) {
+ expect(chain.traces, hasLength(3));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('chainForTrace'))));
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inPeriodicTimer'))));
+ });
+ });
+
+ test(
+ 'called for a stack trace from a nested series of asynchronous '
+ 'operations', () {
+ return Chain.capture(() {
+ return chainForTrace((callback) {
+ inPeriodicTimer(() => inOneShotTimer(() => inMicrotask(callback)));
+ }, () => throw 'error');
+ }).then((chain) {
+ expect(chain.traces, hasLength(5));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('chainForTrace'))));
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inMicrotask'))));
+ expect(chain.traces[3].frames,
+ contains(frameMember(startsWith('inOneShotTimer'))));
+ expect(chain.traces[4].frames,
+ contains(frameMember(startsWith('inPeriodicTimer'))));
+ });
+ });
+
+ test('called for a stack trace from a long future chain', () {
+ return Chain.capture(() {
+ return chainForTrace(inFutureChain, () => throw 'error');
+ }).then((chain) {
+ expect(chain.traces, hasLength(3));
+ expect(chain.traces[0].frames.first, frameMember(startsWith('main')));
+ expect(chain.traces[1].frames,
+ contains(frameMember(startsWith('chainForTrace'))));
+ expect(chain.traces[2].frames,
+ contains(frameMember(startsWith('inFutureChain'))));
+ });
+ });
+
+ test('called for an unregistered stack trace uses the current chain',
+ () async {
+ late StackTrace trace;
+ var chain = await Chain.capture(() async {
+ try {
+ throw 'error';
+ } catch (_, stackTrace) {
+ trace = stackTrace;
+ return Chain.forTrace(stackTrace);
+ }
+ });
+
+ expect(chain.traces, hasLength(greaterThan(1)));
+
+ // Assert that we've trimmed the VM's stack chains here to avoid
+ // duplication.
+ expect(chain.traces.first.toString(),
+ equals(Chain.parse(trace.toString()).traces.first.toString()));
+ });
+ });
+
+ test(
+ 'forTrace() outside of capture() returns a chain describing the VM stack '
+ 'chain', () {
+ // Disable the test package's chain-tracking.
+ return Chain.disable(() async {
+ late StackTrace trace;
+ await Chain.capture(() async {
+ try {
+ throw 'error';
+ } catch (_, stackTrace) {
+ trace = stackTrace;
+ }
+ });
+
+ final chain = Chain.forTrace(trace);
+ final traceStr = trace.toString();
+ final gaps = vmChainGap.allMatches(traceStr);
+ // If the trace ends on a gap, there's no sub-trace following the gap.
+ final expectedLength =
+ (gaps.last.end == traceStr.length) ? gaps.length : gaps.length + 1;
+ expect(chain.traces, hasLength(expectedLength));
+ expect(
+ chain.traces.first.frames, contains(frameMember(startsWith('main'))));
+ });
+ });
+}
diff --git a/pkgs/stack_trace/test/frame_test.dart b/pkgs/stack_trace/test/frame_test.dart
new file mode 100644
index 0000000..a5dfc20
--- /dev/null
+++ b/pkgs/stack_trace/test/frame_test.dart
@@ -0,0 +1,729 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as path;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('.parseVM', () {
+ test('parses a stack frame with column correctly', () {
+ var frame = Frame.parseVM('#1 Foo._bar '
+ '(file:///home/nweiz/code/stuff.dart:42:21)');
+ expect(
+ frame.uri, equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+ expect(frame.line, equals(42));
+ expect(frame.column, equals(21));
+ expect(frame.member, equals('Foo._bar'));
+ });
+
+ test('parses a stack frame without column correctly', () {
+ var frame = Frame.parseVM('#1 Foo._bar '
+ '(file:///home/nweiz/code/stuff.dart:24)');
+ expect(
+ frame.uri, equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+ expect(frame.line, equals(24));
+ expect(frame.column, null);
+ expect(frame.member, equals('Foo._bar'));
+ });
+
+ // This can happen with async stack traces. See issue 22009.
+ test('parses a stack frame without line or column correctly', () {
+ var frame = Frame.parseVM('#1 Foo._bar '
+ '(file:///home/nweiz/code/stuff.dart)');
+ expect(
+ frame.uri, equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+ expect(frame.line, isNull);
+ expect(frame.column, isNull);
+ expect(frame.member, equals('Foo._bar'));
+ });
+
+ test('converts "<anonymous closure>" to "<fn>"', () {
+ String? parsedMember(String member) =>
+ Frame.parseVM('#0 $member (foo:0:0)').member;
+
+ expect(parsedMember('Foo.<anonymous closure>'), equals('Foo.<fn>'));
+ expect(parsedMember('<anonymous closure>.<anonymous closure>.bar'),
+ equals('<fn>.<fn>.bar'));
+ });
+
+ test('converts "<<anonymous closure>_async_body>" to "<async>"', () {
+ var frame =
+ Frame.parseVM('#0 Foo.<<anonymous closure>_async_body> (foo:0:0)');
+ expect(frame.member, equals('Foo.<async>'));
+ });
+
+ test('converts "<function_name_async_body>" to "<async>"', () {
+ var frame = Frame.parseVM('#0 Foo.<function_name_async_body> (foo:0:0)');
+ expect(frame.member, equals('Foo.<async>'));
+ });
+
+ test('parses a folded frame correctly', () {
+ var frame = Frame.parseVM('...');
+
+ expect(frame.member, equals('...'));
+ expect(frame.uri, equals(Uri()));
+ expect(frame.line, isNull);
+ expect(frame.column, isNull);
+ });
+ });
+
+ group('.parseV8', () {
+ test('returns an UnparsedFrame for malformed frames', () {
+ expectIsUnparsed(Frame.parseV8, '');
+ expectIsUnparsed(Frame.parseV8, '#1');
+ expectIsUnparsed(Frame.parseV8, '#1 Foo');
+ expectIsUnparsed(Frame.parseV8, '#1 (dart:async/future.dart:10:15)');
+ expectIsUnparsed(Frame.parseV8, 'Foo (dart:async/future.dart:10:15)');
+ });
+
+ test('parses a stack frame correctly', () {
+ var frame = Frame.parseV8(' at VW.call\$0 '
+ '(https://example.com/stuff.dart.js:560:28)');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with a : in the authority', () {
+ var frame = Frame.parseV8(' at VW.call\$0 '
+ '(http://localhost:8080/stuff.dart.js:560:28)');
+ expect(
+ frame.uri, equals(Uri.parse('http://localhost:8080/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with an absolute POSIX path correctly', () {
+ var frame = Frame.parseV8(' at VW.call\$0 '
+ '(/path/to/stuff.dart.js:560:28)');
+ expect(frame.uri, equals(Uri.parse('file:///path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with an absolute Windows path correctly', () {
+ var frame = Frame.parseV8(' at VW.call\$0 '
+ r'(C:\path\to\stuff.dart.js:560:28)');
+ expect(frame.uri, equals(Uri.parse('file:///C:/path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with a Windows UNC path correctly', () {
+ var frame = Frame.parseV8(' at VW.call\$0 '
+ r'(\\mount\path\to\stuff.dart.js:560:28)');
+ expect(
+ frame.uri, equals(Uri.parse('file://mount/path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with a relative POSIX path correctly', () {
+ var frame = Frame.parseV8(' at VW.call\$0 '
+ '(path/to/stuff.dart.js:560:28)');
+ expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with a relative Windows path correctly', () {
+ var frame = Frame.parseV8(' at VW.call\$0 '
+ r'(path\to\stuff.dart.js:560:28)');
+ expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses an anonymous stack frame correctly', () {
+ var frame =
+ Frame.parseV8(' at https://example.com/stuff.dart.js:560:28');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('<fn>'));
+ });
+
+ test('parses a native stack frame correctly', () {
+ var frame = Frame.parseV8(' at Object.stringify (native)');
+ expect(frame.uri, Uri.parse('native'));
+ expect(frame.line, isNull);
+ expect(frame.column, isNull);
+ expect(frame.member, equals('Object.stringify'));
+ });
+
+ test('parses a stack frame with [as ...] correctly', () {
+ // Ignore "[as ...]", since other stack trace formats don't support a
+ // similar construct.
+ var frame = Frame.parseV8(' at VW.call\$0 [as call\$4] '
+ '(https://example.com/stuff.dart.js:560:28)');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a basic eval stack frame correctly', () {
+ var frame = Frame.parseV8(' at eval (eval at <anonymous> '
+ '(https://example.com/stuff.dart.js:560:28))');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('eval'));
+ });
+
+ test('parses an IE10 eval stack frame correctly', () {
+ var frame = Frame.parseV8(' at eval (eval at Anonymous function '
+ '(https://example.com/stuff.dart.js:560:28))');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('eval'));
+ });
+
+ test('parses an eval stack frame with inner position info correctly', () {
+ var frame = Frame.parseV8(' at eval (eval at <anonymous> '
+ '(https://example.com/stuff.dart.js:560:28), <anonymous>:3:28)');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('eval'));
+ });
+
+ test('parses a nested eval stack frame correctly', () {
+ var frame = Frame.parseV8(' at eval (eval at <anonymous> '
+ '(eval at sub (https://example.com/stuff.dart.js:560:28)))');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, equals(28));
+ expect(frame.member, equals('eval'));
+ });
+
+ test('converts "<anonymous>" to "<fn>"', () {
+ String? parsedMember(String member) =>
+ Frame.parseV8(' at $member (foo:0:0)').member;
+
+ expect(parsedMember('Foo.<anonymous>'), equals('Foo.<fn>'));
+ expect(
+ parsedMember('<anonymous>.<anonymous>.bar'), equals('<fn>.<fn>.bar'));
+ });
+
+ test('returns an UnparsedFrame for malformed frames', () {
+ expectIsUnparsed(Frame.parseV8, '');
+ expectIsUnparsed(Frame.parseV8, ' at');
+ expectIsUnparsed(Frame.parseV8, ' at Foo');
+ expectIsUnparsed(Frame.parseV8, ' at Foo (dart:async/future.dart)');
+ expectIsUnparsed(Frame.parseV8, ' at (dart:async/future.dart:10:15)');
+ expectIsUnparsed(Frame.parseV8, 'Foo (dart:async/future.dart:10:15)');
+ expectIsUnparsed(Frame.parseV8, ' at dart:async/future.dart');
+ expectIsUnparsed(Frame.parseV8, 'dart:async/future.dart:10:15');
+ });
+ });
+
+ group('.parseFirefox/.parseSafari', () {
+ test('parses a Firefox stack trace with anonymous function', () {
+ var trace = Trace.parse('''
+Foo._bar@https://example.com/stuff.js:18056:12
+anonymous/<@https://example.com/stuff.js line 693 > Function:3:40
+baz@https://pub.dev/buz.js:56355:55
+ ''');
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[0].line, equals(18056));
+ expect(trace.frames[0].column, equals(12));
+ expect(trace.frames[0].member, equals('Foo._bar'));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].line, equals(693));
+ expect(trace.frames[1].column, isNull);
+ expect(trace.frames[1].member, equals('<fn>'));
+ expect(trace.frames[2].uri, equals(Uri.parse('https://pub.dev/buz.js')));
+ expect(trace.frames[2].line, equals(56355));
+ expect(trace.frames[2].column, equals(55));
+ expect(trace.frames[2].member, equals('baz'));
+ });
+
+ test('parses a Firefox stack trace with nested evals in anonymous function',
+ () {
+ var trace = Trace.parse('''
+ Foo._bar@https://example.com/stuff.js:18056:12
+ anonymous@file:///C:/example.html line 7 > eval line 1 > eval:1:1
+ anonymous@file:///C:/example.html line 45 > Function:1:1
+ ''');
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[0].line, equals(18056));
+ expect(trace.frames[0].column, equals(12));
+ expect(trace.frames[0].member, equals('Foo._bar'));
+ expect(trace.frames[1].uri, equals(Uri.parse('file:///C:/example.html')));
+ expect(trace.frames[1].line, equals(7));
+ expect(trace.frames[1].column, isNull);
+ expect(trace.frames[1].member, equals('<fn>'));
+ expect(trace.frames[2].uri, equals(Uri.parse('file:///C:/example.html')));
+ expect(trace.frames[2].line, equals(45));
+ expect(trace.frames[2].column, isNull);
+ expect(trace.frames[2].member, equals('<fn>'));
+ });
+
+ test('parses a simple stack frame correctly', () {
+ var frame = Frame.parseFirefox(
+ '.VW.call\$0@https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with an absolute POSIX path correctly', () {
+ var frame = Frame.parseFirefox('.VW.call\$0@/path/to/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('file:///path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with an absolute Windows path correctly', () {
+ var frame =
+ Frame.parseFirefox(r'.VW.call$0@C:\path\to\stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('file:///C:/path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with a Windows UNC path correctly', () {
+ var frame =
+ Frame.parseFirefox(r'.VW.call$0@\\mount\path\to\stuff.dart.js:560');
+ expect(
+ frame.uri, equals(Uri.parse('file://mount/path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with a relative POSIX path correctly', () {
+ var frame = Frame.parseFirefox('.VW.call\$0@path/to/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a stack frame with a relative Windows path correctly', () {
+ var frame = Frame.parseFirefox(r'.VW.call$0@path\to\stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('path/to/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('VW.call\$0'));
+ });
+
+ test('parses a simple anonymous stack frame correctly', () {
+ var frame = Frame.parseFirefox('@https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('<fn>'));
+ });
+
+ test('parses a nested anonymous stack frame correctly', () {
+ var frame =
+ Frame.parseFirefox('.foo/<@https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('foo.<fn>'));
+
+ frame = Frame.parseFirefox('.foo/@https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('foo.<fn>'));
+ });
+
+ test('parses a named nested anonymous stack frame correctly', () {
+ var frame = Frame.parseFirefox(
+ '.foo/.name<@https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('foo.<fn>'));
+
+ frame = Frame.parseFirefox(
+ '.foo/.name@https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('foo.<fn>'));
+ });
+
+ test('parses a stack frame with parameters correctly', () {
+ var frame = Frame.parseFirefox(
+ '.foo(12, "@)()/<")@https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('foo'));
+ });
+
+ test('parses a nested anonymous stack frame with parameters correctly', () {
+ var frame = Frame.parseFirefox(
+ '.foo(12, "@)()/<")/.fn<@https://example.com/stuff.dart.js:560',
+ );
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('foo.<fn>'));
+ });
+
+ test(
+ 'parses a deeply-nested anonymous stack frame with parameters '
+ 'correctly', () {
+ var frame = Frame.parseFirefox('.convertDartClosureToJS/\$function</<@'
+ 'https://example.com/stuff.dart.js:560');
+ expect(frame.uri, equals(Uri.parse('https://example.com/stuff.dart.js')));
+ expect(frame.line, equals(560));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('convertDartClosureToJS.<fn>.<fn>'));
+ });
+
+ test('returns an UnparsedFrame for malformed frames', () {
+ expectIsUnparsed(Frame.parseFirefox, '');
+ expectIsUnparsed(Frame.parseFirefox, '.foo');
+ expectIsUnparsed(Frame.parseFirefox, '.foo@dart:async/future.dart');
+ expectIsUnparsed(Frame.parseFirefox, '.foo(@dart:async/future.dart:10');
+ expectIsUnparsed(Frame.parseFirefox, '@dart:async/future.dart');
+ });
+
+ test('parses a simple stack frame correctly', () {
+ var frame =
+ Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart:10:11');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, equals(10));
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('foo\$bar'));
+ });
+
+ test('parses an anonymous stack frame correctly', () {
+ var frame = Frame.parseFirefox('https://dart.dev/foo/bar.dart:10:11');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, equals(10));
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('<fn>'));
+ });
+
+ test('parses a stack frame with no line correctly', () {
+ var frame =
+ Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart::11');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, isNull);
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('foo\$bar'));
+ });
+
+ test('parses a stack frame with no column correctly', () {
+ var frame =
+ Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart:10:');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, equals(10));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('foo\$bar'));
+ });
+
+ test('parses a stack frame with no line or column correctly', () {
+ var frame =
+ Frame.parseFirefox('foo\$bar@https://dart.dev/foo/bar.dart:10:11');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, equals(10));
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('foo\$bar'));
+ });
+ });
+
+ group('.parseFriendly', () {
+ test('parses a simple stack frame correctly', () {
+ var frame = Frame.parseFriendly(
+ 'https://dart.dev/foo/bar.dart 10:11 Foo.<fn>.bar');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, equals(10));
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('Foo.<fn>.bar'));
+ });
+
+ test('parses a stack frame with no line or column correctly', () {
+ var frame =
+ Frame.parseFriendly('https://dart.dev/foo/bar.dart Foo.<fn>.bar');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, isNull);
+ expect(frame.column, isNull);
+ expect(frame.member, equals('Foo.<fn>.bar'));
+ });
+
+ test('parses a stack frame with no column correctly', () {
+ var frame =
+ Frame.parseFriendly('https://dart.dev/foo/bar.dart 10 Foo.<fn>.bar');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, equals(10));
+ expect(frame.column, isNull);
+ expect(frame.member, equals('Foo.<fn>.bar'));
+ });
+
+ test('parses a stack frame with a relative path correctly', () {
+ var frame = Frame.parseFriendly('foo/bar.dart 10:11 Foo.<fn>.bar');
+ expect(frame.uri,
+ equals(path.toUri(path.absolute(path.join('foo', 'bar.dart')))));
+ expect(frame.line, equals(10));
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('Foo.<fn>.bar'));
+ });
+
+ test('returns an UnparsedFrame for malformed frames', () {
+ expectIsUnparsed(Frame.parseFriendly, '');
+ expectIsUnparsed(Frame.parseFriendly, 'foo/bar.dart');
+ expectIsUnparsed(Frame.parseFriendly, 'foo/bar.dart 10:11');
+ });
+
+ test('parses a data url stack frame with no line or column correctly', () {
+ var frame = Frame.parseFriendly('data:... main');
+ expect(frame.uri.scheme, equals('data'));
+ expect(frame.line, isNull);
+ expect(frame.column, isNull);
+ expect(frame.member, equals('main'));
+ });
+
+ test('parses a data url stack frame correctly', () {
+ var frame = Frame.parseFriendly('data:... 10:11 main');
+ expect(frame.uri.scheme, equals('data'));
+ expect(frame.line, equals(10));
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('main'));
+ });
+
+ test('parses a stack frame with spaces in the member name correctly', () {
+ var frame = Frame.parseFriendly(
+ 'foo/bar.dart 10:11 (anonymous function).dart.fn');
+ expect(frame.uri,
+ equals(path.toUri(path.absolute(path.join('foo', 'bar.dart')))));
+ expect(frame.line, equals(10));
+ expect(frame.column, equals(11));
+ expect(frame.member, equals('(anonymous function).dart.fn'));
+ });
+
+ test(
+ 'parses a stack frame with spaces in the member name and no line or '
+ 'column correctly', () {
+ var frame = Frame.parseFriendly(
+ 'https://dart.dev/foo/bar.dart (anonymous function).dart.fn');
+ expect(frame.uri, equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(frame.line, isNull);
+ expect(frame.column, isNull);
+ expect(frame.member, equals('(anonymous function).dart.fn'));
+ });
+ });
+
+ test('only considers dart URIs to be core', () {
+ bool isCore(String library) =>
+ Frame.parseVM('#0 Foo ($library:0:0)').isCore;
+
+ expect(isCore('dart:core'), isTrue);
+ expect(isCore('dart:async'), isTrue);
+ expect(isCore('dart:core/uri.dart'), isTrue);
+ expect(isCore('dart:async/future.dart'), isTrue);
+ expect(isCore('bart:core'), isFalse);
+ expect(isCore('sdart:core'), isFalse);
+ expect(isCore('darty:core'), isFalse);
+ expect(isCore('bart:core/uri.dart'), isFalse);
+ });
+
+ group('.library', () {
+ test('returns the URI string for non-file URIs', () {
+ expect(Frame.parseVM('#0 Foo (dart:async/future.dart:0:0)').library,
+ equals('dart:async/future.dart'));
+ expect(
+ Frame.parseVM('#0 Foo '
+ '(https://dart.dev/stuff/thing.dart:0:0)')
+ .library,
+ equals('https://dart.dev/stuff/thing.dart'));
+ });
+
+ test('returns the relative path for file URIs', () {
+ expect(Frame.parseVM('#0 Foo (foo/bar.dart:0:0)').library,
+ equals(path.join('foo', 'bar.dart')));
+ });
+
+ test('truncates legacy data: URIs', () {
+ var frame = Frame.parseVM(
+ '#0 Foo (data:application/dart;charset=utf-8,blah:0:0)');
+ expect(frame.library, equals('data:...'));
+ });
+
+ test('truncates data: URIs', () {
+ var frame = Frame.parseVM(
+ '#0 main (<data:application/dart;charset=utf-8>:1:15)');
+ expect(frame.library, equals('data:...'));
+ });
+ });
+
+ group('.location', () {
+ test(
+ 'returns the library and line/column numbers for non-core '
+ 'libraries', () {
+ expect(
+ Frame.parseVM('#0 Foo '
+ '(https://dart.dev/thing.dart:5:10)')
+ .location,
+ equals('https://dart.dev/thing.dart 5:10'));
+ expect(Frame.parseVM('#0 Foo (foo/bar.dart:1:2)').location,
+ equals('${path.join('foo', 'bar.dart')} 1:2'));
+ });
+ });
+
+ group('.package', () {
+ test('returns null for non-package URIs', () {
+ expect(
+ Frame.parseVM('#0 Foo (dart:async/future.dart:0:0)').package, isNull);
+ expect(
+ Frame.parseVM('#0 Foo '
+ '(https://dart.dev/stuff/thing.dart:0:0)')
+ .package,
+ isNull);
+ });
+
+ test('returns the package name for package: URIs', () {
+ expect(Frame.parseVM('#0 Foo (package:foo/foo.dart:0:0)').package,
+ equals('foo'));
+ expect(Frame.parseVM('#0 Foo (package:foo/zap/bar.dart:0:0)').package,
+ equals('foo'));
+ });
+ });
+
+ group('.toString()', () {
+ test(
+ 'returns the library and line/column numbers for non-core '
+ 'libraries', () {
+ expect(
+ Frame.parseVM('#0 Foo (https://dart.dev/thing.dart:5:10)').toString(),
+ equals('https://dart.dev/thing.dart 5:10 in Foo'));
+ });
+
+ test('converts "<anonymous closure>" to "<fn>"', () {
+ expect(
+ Frame.parseVM('#0 Foo.<anonymous closure> '
+ '(dart:core/uri.dart:5:10)')
+ .toString(),
+ equals('dart:core/uri.dart 5:10 in Foo.<fn>'));
+ });
+
+ test('prints a frame without a column correctly', () {
+ expect(Frame.parseVM('#0 Foo (dart:core/uri.dart:5)').toString(),
+ equals('dart:core/uri.dart 5 in Foo'));
+ });
+
+ test('prints relative paths as relative', () {
+ var relative = path.normalize('relative/path/to/foo.dart');
+ expect(Frame.parseFriendly('$relative 5:10 Foo').toString(),
+ equals('$relative 5:10 in Foo'));
+ });
+ });
+
+ test('parses a V8 Wasm frame with a name', () {
+ var frame = Frame.parseV8(' at Error._throwWithCurrentStackTrace '
+ '(wasm://wasm/0006d966:wasm-function[119]:0xbb13)');
+ expect(frame.uri, Uri.parse('wasm://wasm/0006d966'));
+ expect(frame.line, 1);
+ expect(frame.column, 0xbb13 + 1);
+ expect(frame.member, 'Error._throwWithCurrentStackTrace');
+ });
+
+ test('parses a V8 Wasm frame with a name with spaces', () {
+ var frame = Frame.parseV8(' at main tear-off trampoline '
+ '(wasm://wasm/0017fbea:wasm-function[863]:0x23cc8)');
+ expect(frame.uri, Uri.parse('wasm://wasm/0017fbea'));
+ expect(frame.line, 1);
+ expect(frame.column, 0x23cc8 + 1);
+ expect(frame.member, 'main tear-off trampoline');
+ });
+
+ test('parses a V8 Wasm frame with a name with colons and parens', () {
+ var frame = Frame.parseV8(' at a::b::c() '
+ '(https://a.b.com/x/y/z.wasm:wasm-function[66334]:0x12c28ad)');
+ expect(frame.uri, Uri.parse('https://a.b.com/x/y/z.wasm'));
+ expect(frame.line, 1);
+ expect(frame.column, 0x12c28ad + 1);
+ expect(frame.member, 'a::b::c()');
+ });
+
+ test('parses a V8 Wasm frame without a name', () {
+ var frame =
+ Frame.parseV8(' at wasm://wasm/0006d966:wasm-function[119]:0xbb13');
+ expect(frame.uri, Uri.parse('wasm://wasm/0006d966'));
+ expect(frame.line, 1);
+ expect(frame.column, 0xbb13 + 1);
+ expect(frame.member, '119');
+ });
+
+ test('parses a Firefox Wasm frame with a name', () {
+ var frame = Frame.parseFirefox(
+ 'g@http://localhost:8080/test.wasm:wasm-function[796]:0x143b4');
+ expect(frame.uri, Uri.parse('http://localhost:8080/test.wasm'));
+ expect(frame.line, 1);
+ expect(frame.column, 0x143b4 + 1);
+ expect(frame.member, 'g');
+ });
+
+ test('parses a Firefox Wasm frame with a name with spaces', () {
+ var frame = Frame.parseFirefox(
+ 'main tear-off trampoline@http://localhost:8080/test.wasm:wasm-function[794]:0x14387');
+ expect(frame.uri, Uri.parse('http://localhost:8080/test.wasm'));
+ expect(frame.line, 1);
+ expect(frame.column, 0x14387 + 1);
+ expect(frame.member, 'main tear-off trampoline');
+ });
+
+ test('parses a Firefox Wasm frame without a name', () {
+ var frame = Frame.parseFirefox(
+ '@http://localhost:8080/test.wasm:wasm-function[796]:0x143b4');
+ expect(frame.uri, Uri.parse('http://localhost:8080/test.wasm'));
+ expect(frame.line, 1);
+ expect(frame.column, 0x143b4 + 1);
+ expect(frame.member, '796');
+ });
+
+ test('parses a Safari Wasm frame with a name', () {
+ var frame = Frame.parseSafari('<?>.wasm-function[g]@[wasm code]');
+ expect(frame.uri, Uri.parse('wasm code'));
+ expect(frame.line, null);
+ expect(frame.column, null);
+ expect(frame.member, 'g');
+ });
+
+ test('parses a Safari Wasm frame with a name', () {
+ var frame = Frame.parseSafari(
+ '<?>.wasm-function[main tear-off trampoline]@[wasm code]');
+ expect(frame.uri, Uri.parse('wasm code'));
+ expect(frame.line, null);
+ expect(frame.column, null);
+ expect(frame.member, 'main tear-off trampoline');
+ });
+
+ test('parses a Safari Wasm frame without a name', () {
+ var frame = Frame.parseSafari('<?>.wasm-function[796]@[wasm code]');
+ expect(frame.uri, Uri.parse('wasm code'));
+ expect(frame.line, null);
+ expect(frame.column, null);
+ expect(frame.member, '796');
+ });
+}
+
+void expectIsUnparsed(Frame Function(String) constructor, String text) {
+ var frame = constructor(text);
+ expect(frame, isA<UnparsedFrame>());
+ expect(frame.toString(), equals(text));
+}
diff --git a/pkgs/stack_trace/test/trace_test.dart b/pkgs/stack_trace/test/trace_test.dart
new file mode 100644
index 0000000..e09de95
--- /dev/null
+++ b/pkgs/stack_trace/test/trace_test.dart
@@ -0,0 +1,615 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as path;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+void main() {
+ // This just shouldn't crash.
+ test('a native stack trace is parseable', Trace.current);
+
+ group('.parse', () {
+ test('.parse parses a V8 stack trace with eval statment correctly', () {
+ var trace = Trace.parse(r'''Error
+ at Object.eval (eval at Foo (main.dart.js:588), <anonymous>:3:47)''');
+ expect(trace.frames[0].uri, Uri.parse('main.dart.js'));
+ expect(trace.frames[0].member, equals('Object.eval'));
+ expect(trace.frames[0].line, equals(588));
+ expect(trace.frames[0].column, isNull);
+ });
+
+ test('.parse parses a VM stack trace correctly', () {
+ var trace = Trace.parse(
+ '#0 Foo._bar (file:///home/nweiz/code/stuff.dart:42:21)\n'
+ '#1 zip.<anonymous closure>.zap (dart:async/future.dart:0:2)\n'
+ '#2 zip.<anonymous closure>.zap (https://pub.dev/thing.dart:1:100)',
+ );
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('file:///home/nweiz/code/stuff.dart')));
+ expect(trace.frames[1].uri, equals(Uri.parse('dart:async/future.dart')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.dart')));
+ });
+
+ test('parses a V8 stack trace correctly', () {
+ var trace = Trace.parse('Error\n'
+ ' at Foo._bar (https://example.com/stuff.js:42:21)\n'
+ ' at https://example.com/stuff.js:0:2\n'
+ ' at zip.<anonymous>.zap '
+ '(https://pub.dev/thing.js:1:100)');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+ trace = Trace.parse('Exception: foo\n'
+ ' at Foo._bar (https://example.com/stuff.js:42:21)\n'
+ ' at https://example.com/stuff.js:0:2\n'
+ ' at zip.<anonymous>.zap '
+ '(https://pub.dev/thing.js:1:100)');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+ trace = Trace.parse('Exception: foo\n'
+ ' bar\n'
+ ' at Foo._bar (https://example.com/stuff.js:42:21)\n'
+ ' at https://example.com/stuff.js:0:2\n'
+ ' at zip.<anonymous>.zap '
+ '(https://pub.dev/thing.js:1:100)');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+ trace = Trace.parse('Exception: foo\n'
+ ' bar\n'
+ ' at Foo._bar (https://example.com/stuff.js:42:21)\n'
+ ' at https://example.com/stuff.js:0:2\n'
+ ' at (anonymous function).zip.zap '
+ '(https://pub.dev/thing.js:1:100)');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].member, equals('<fn>'));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+ expect(trace.frames[2].member, equals('<fn>.zip.zap'));
+ });
+
+ // JavaScriptCore traces are just like V8, except that it doesn't have a
+ // header and it starts with a tab rather than spaces.
+ test('parses a JavaScriptCore stack trace correctly', () {
+ var trace =
+ Trace.parse('\tat Foo._bar (https://example.com/stuff.js:42:21)\n'
+ '\tat https://example.com/stuff.js:0:2\n'
+ '\tat zip.<anonymous>.zap '
+ '(https://pub.dev/thing.js:1:100)');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+ trace = Trace.parse('\tat Foo._bar (https://example.com/stuff.js:42:21)\n'
+ '\tat \n'
+ '\tat zip.<anonymous>.zap '
+ '(https://pub.dev/thing.js:1:100)');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[1].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+ });
+
+ test('parses a Firefox/Safari stack trace correctly', () {
+ var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42\n'
+ 'zip/<@https://example.com/stuff.js:0\n'
+ 'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+ trace = Trace.parse('zip/<@https://example.com/stuff.js:0\n'
+ 'Foo._bar@https://example.com/stuff.js:42\n'
+ 'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+
+ trace = Trace.parse('zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1\n'
+ 'zip/<@https://example.com/stuff.js:0\n'
+ 'Foo._bar@https://example.com/stuff.js:42');
+
+ expect(
+ trace.frames[0].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[2].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ });
+
+ test('parses a Firefox/Safari stack trace containing native code correctly',
+ () {
+ var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42\n'
+ 'zip/<@https://example.com/stuff.js:0\n'
+ 'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1\n'
+ '[native code]');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+ expect(trace.frames.length, equals(3));
+ });
+
+ test('parses a Firefox/Safari stack trace without a method name correctly',
+ () {
+ var trace = Trace.parse('https://example.com/stuff.js:42\n'
+ 'zip/<@https://example.com/stuff.js:0\n'
+ 'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[0].member, equals('<fn>'));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+ });
+
+ test('parses a Firefox/Safari stack trace with an empty line correctly',
+ () {
+ var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42\n'
+ '\n'
+ 'zip/<@https://example.com/stuff.js:0\n'
+ 'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+ });
+
+ test('parses a Firefox/Safari stack trace with a column number correctly',
+ () {
+ var trace = Trace.parse('Foo._bar@https://example.com/stuff.js:42:2\n'
+ 'zip/<@https://example.com/stuff.js:0\n'
+ 'zip.zap(12, "@)()/<")@https://pub.dev/thing.js:1');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(trace.frames[0].line, equals(42));
+ expect(trace.frames[0].column, equals(2));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://example.com/stuff.js')));
+ expect(
+ trace.frames[2].uri, equals(Uri.parse('https://pub.dev/thing.js')));
+ });
+
+ test('parses a package:stack_trace stack trace correctly', () {
+ var trace =
+ Trace.parse('https://dart.dev/foo/bar.dart 10:11 Foo.<fn>.bar\n'
+ 'https://dart.dev/foo/baz.dart Foo.<fn>.bar');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://dart.dev/foo/baz.dart')));
+ });
+
+ test('parses a package:stack_trace stack chain correctly', () {
+ var trace =
+ Trace.parse('https://dart.dev/foo/bar.dart 10:11 Foo.<fn>.bar\n'
+ 'https://dart.dev/foo/baz.dart Foo.<fn>.bar\n'
+ '===== asynchronous gap ===========================\n'
+ 'https://dart.dev/foo/bang.dart 10:11 Foo.<fn>.bar\n'
+ 'https://dart.dev/foo/quux.dart Foo.<fn>.bar');
+
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://dart.dev/foo/baz.dart')));
+ expect(trace.frames[2].uri,
+ equals(Uri.parse('https://dart.dev/foo/bang.dart')));
+ expect(trace.frames[3].uri,
+ equals(Uri.parse('https://dart.dev/foo/quux.dart')));
+ });
+
+ test('parses a package:stack_trace stack chain with end gap correctly', () {
+ var trace = Trace.parse(
+ 'https://dart.dev/foo/bar.dart 10:11 Foo.<fn>.bar\n'
+ 'https://dart.dev/foo/baz.dart Foo.<fn>.bar\n'
+ 'https://dart.dev/foo/bang.dart 10:11 Foo.<fn>.bar\n'
+ 'https://dart.dev/foo/quux.dart Foo.<fn>.bar===== asynchronous gap ===========================\n',
+ );
+
+ expect(trace.frames.length, 4);
+ expect(trace.frames[0].uri,
+ equals(Uri.parse('https://dart.dev/foo/bar.dart')));
+ expect(trace.frames[1].uri,
+ equals(Uri.parse('https://dart.dev/foo/baz.dart')));
+ expect(trace.frames[2].uri,
+ equals(Uri.parse('https://dart.dev/foo/bang.dart')));
+ expect(trace.frames[3].uri,
+ equals(Uri.parse('https://dart.dev/foo/quux.dart')));
+ });
+
+ test('parses a real package:stack_trace stack trace correctly', () {
+ var traceString = Trace.current().toString();
+ expect(Trace.parse(traceString).toString(), equals(traceString));
+ });
+
+ test('parses an empty string correctly', () {
+ var trace = Trace.parse('');
+ expect(trace.frames, isEmpty);
+ expect(trace.toString(), equals(''));
+ });
+
+ test('parses trace with async gap correctly', () {
+ var trace = Trace.parse('#0 bop (file:///pull.dart:42:23)\n'
+ '<asynchronous suspension>\n'
+ '#1 twist (dart:the/future.dart:0:2)\n'
+ '#2 main (dart:my/file.dart:4:6)\n');
+
+ expect(trace.frames.length, 3);
+ expect(trace.frames[0].uri, equals(Uri.parse('file:///pull.dart')));
+ expect(trace.frames[1].uri, equals(Uri.parse('dart:the/future.dart')));
+ expect(trace.frames[2].uri, equals(Uri.parse('dart:my/file.dart')));
+ });
+
+ test('parses trace with async gap at end correctly', () {
+ var trace = Trace.parse('#0 bop (file:///pull.dart:42:23)\n'
+ '#1 twist (dart:the/future.dart:0:2)\n'
+ '<asynchronous suspension>\n');
+
+ expect(trace.frames.length, 2);
+ expect(trace.frames[0].uri, equals(Uri.parse('file:///pull.dart')));
+ expect(trace.frames[1].uri, equals(Uri.parse('dart:the/future.dart')));
+ });
+
+ test('parses a V8 stack frace with Wasm frames correctly', () {
+ var trace = Trace.parse(
+ '\tat Error._throwWithCurrentStackTrace (wasm://wasm/0006d892:wasm-function[119]:0xbaf8)\n'
+ '\tat main (wasm://wasm/0006d892:wasm-function[792]:0x14378)\n'
+ '\tat main tear-off trampoline (wasm://wasm/0006d892:wasm-function[794]:0x14387)\n'
+ '\tat _invokeMain (wasm://wasm/0006d892:wasm-function[70]:0xa56c)\n'
+ '\tat InstantiatedApp.invokeMain (/home/user/test.mjs:361:37)\n'
+ '\tat main (/home/user/run_wasm.js:416:21)\n'
+ '\tat async action (/home/user/run_wasm.js:353:38)\n'
+ '\tat async eventLoop (/home/user/run_wasm.js:329:9)');
+
+ expect(trace.frames.length, 8);
+
+ for (final frame in trace.frames) {
+ expect(frame is UnparsedFrame, false);
+ }
+
+ expect(trace.frames[0].uri, Uri.parse('wasm://wasm/0006d892'));
+ expect(trace.frames[0].line, 1);
+ expect(trace.frames[0].column, 0xbaf8 + 1);
+ expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+ expect(trace.frames[4].uri, Uri.parse('file:///home/user/test.mjs'));
+ expect(trace.frames[4].line, 361);
+ expect(trace.frames[4].column, 37);
+ expect(trace.frames[4].member, 'InstantiatedApp.invokeMain');
+
+ expect(trace.frames[5].uri, Uri.parse('file:///home/user/run_wasm.js'));
+ expect(trace.frames[5].line, 416);
+ expect(trace.frames[5].column, 21);
+ expect(trace.frames[5].member, 'main');
+ });
+
+ test('parses Firefox stack frace with Wasm frames correctly', () {
+ var trace = Trace.parse(
+ 'Error._throwWithCurrentStackTrace@http://localhost:8080/test.wasm:wasm-function[119]:0xbaf8\n'
+ 'main@http://localhost:8080/test.wasm:wasm-function[792]:0x14378\n'
+ 'main tear-off trampoline@http://localhost:8080/test.wasm:wasm-function[794]:0x14387\n'
+ '_invokeMain@http://localhost:8080/test.wasm:wasm-function[70]:0xa56c\n'
+ 'invoke@http://localhost:8080/test.mjs:48:26');
+
+ expect(trace.frames.length, 5);
+
+ for (final frame in trace.frames) {
+ expect(frame is UnparsedFrame, false);
+ }
+
+ expect(trace.frames[0].uri, Uri.parse('http://localhost:8080/test.wasm'));
+ expect(trace.frames[0].line, 1);
+ expect(trace.frames[0].column, 0xbaf8 + 1);
+ expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+ expect(trace.frames[4].uri, Uri.parse('http://localhost:8080/test.mjs'));
+ expect(trace.frames[4].line, 48);
+ expect(trace.frames[4].column, 26);
+ expect(trace.frames[4].member, 'invoke');
+ });
+
+ test('parses JSShell stack frace with Wasm frames correctly', () {
+ var trace = Trace.parse(
+ 'Error._throwWithCurrentStackTrace@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[119]:0xbaf8\n'
+ 'main@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[792]:0x14378\n'
+ 'main tear-off trampoline@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[794]:0x14387\n'
+ '_invokeMain@/home/user/test.mjs line 29 > WebAssembly.compile:wasm-function[70]:0xa56c\n'
+ 'invokeMain@/home/user/test.mjs:361:37\n'
+ 'main@/home/user/run_wasm.js:416:21\n'
+ 'async*action@/home/user/run_wasm.js:353:44\n'
+ 'eventLoop@/home/user/run_wasm.js:329:15\n'
+ 'self.dartMainRunner@/home/user/run_wasm.js:354:14\n'
+ '@/home/user/run_wasm.js:419:15');
+
+ expect(trace.frames.length, 10);
+
+ for (final frame in trace.frames) {
+ expect(frame is UnparsedFrame, false);
+ }
+
+ expect(trace.frames[0].uri, Uri.parse('file:///home/user/test.mjs'));
+ expect(trace.frames[0].line, 1);
+ expect(trace.frames[0].column, 0xbaf8 + 1);
+ expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+ expect(trace.frames[4].uri, Uri.parse('file:///home/user/test.mjs'));
+ expect(trace.frames[4].line, 361);
+ expect(trace.frames[4].column, 37);
+ expect(trace.frames[4].member, 'invokeMain');
+
+ expect(trace.frames[9].uri, Uri.parse('file:///home/user/run_wasm.js'));
+ expect(trace.frames[9].line, 419);
+ expect(trace.frames[9].column, 15);
+ expect(trace.frames[9].member, '<fn>');
+ });
+
+ test('parses Safari stack frace with Wasm frames correctly', () {
+ var trace = Trace.parse(
+ '<?>.wasm-function[Error._throwWithCurrentStackTrace]@[wasm code]\n'
+ '<?>.wasm-function[main]@[wasm code]\n'
+ '<?>.wasm-function[main tear-off trampoline]@[wasm code]\n'
+ '<?>.wasm-function[_invokeMain]@[wasm code]\n'
+ 'invokeMain@/home/user/test.mjs:361:48\n'
+ '@/home/user/run_wasm.js:416:31');
+
+ expect(trace.frames.length, 6);
+
+ for (final frame in trace.frames) {
+ expect(frame is UnparsedFrame, false);
+ }
+
+ expect(trace.frames[0].uri, Uri.parse('wasm code'));
+ expect(trace.frames[0].line, null);
+ expect(trace.frames[0].column, null);
+ expect(trace.frames[0].member, 'Error._throwWithCurrentStackTrace');
+
+ expect(trace.frames[4].uri, Uri.parse('file:///home/user/test.mjs'));
+ expect(trace.frames[4].line, 361);
+ expect(trace.frames[4].column, 48);
+ expect(trace.frames[4].member, 'invokeMain');
+
+ expect(trace.frames[5].uri, Uri.parse('file:///home/user/run_wasm.js'));
+ expect(trace.frames[5].line, 416);
+ expect(trace.frames[5].column, 31);
+ expect(trace.frames[5].member, '<fn>');
+ });
+ });
+
+ test('.toString() nicely formats the stack trace', () {
+ var trace = Trace.parse('''
+#0 Foo._bar (foo/bar.dart:42:21)
+#1 zip.<anonymous closure>.zap (dart:async/future.dart:0:2)
+#2 zip.<anonymous closure>.zap (https://pub.dev/thing.dart:1:100)
+''');
+
+ expect(trace.toString(), equals('''
+${path.join('foo', 'bar.dart')} 42:21 Foo._bar
+dart:async/future.dart 0:2 zip.<fn>.zap
+https://pub.dev/thing.dart 1:100 zip.<fn>.zap
+'''));
+ });
+
+ test('.vmTrace returns a native-style trace', () {
+ var uri = path.toUri(path.absolute('foo'));
+ var trace = Trace([
+ Frame(uri, 10, 20, 'Foo.<fn>'),
+ Frame(Uri.parse('https://dart.dev/foo.dart'), null, null, 'bar'),
+ Frame(Uri.parse('dart:async'), 15, null, 'baz'),
+ ]);
+
+ expect(
+ trace.vmTrace.toString(),
+ equals('#1 Foo.<anonymous closure> ($uri:10:20)\n'
+ '#2 bar (https://dart.dev/foo.dart:0:0)\n'
+ '#3 baz (dart:async:15:0)\n'));
+ });
+
+ group('folding', () {
+ group('.terse', () {
+ test('folds core frames together bottom-up', () {
+ var trace = Trace.parse('''
+#1 top (dart:async/future.dart:0:2)
+#2 bottom (dart:core/uri.dart:1:100)
+#0 notCore (foo.dart:42:21)
+#3 top (dart:io:5:10)
+#4 bottom (dart:async-patch/future.dart:9:11)
+#5 alsoNotCore (bar.dart:10:20)
+''');
+
+ expect(trace.terse.toString(), equals('''
+dart:core bottom
+foo.dart 42:21 notCore
+dart:async bottom
+bar.dart 10:20 alsoNotCore
+'''));
+ });
+
+ test('folds empty async frames', () {
+ var trace = Trace.parse('''
+#0 top (dart:async/future.dart:0:2)
+#1 empty.<<anonymous closure>_async_body> (bar.dart)
+#2 bottom (dart:async-patch/future.dart:9:11)
+#3 notCore (foo.dart:42:21)
+''');
+
+ expect(trace.terse.toString(), equals('''
+dart:async bottom
+foo.dart 42:21 notCore
+'''));
+ });
+
+ test('removes the bottom-most async frame', () {
+ var trace = Trace.parse('''
+#0 notCore (foo.dart:42:21)
+#1 top (dart:async/future.dart:0:2)
+#2 bottom (dart:core/uri.dart:1:100)
+#3 top (dart:io:5:10)
+#4 bottom (dart:async-patch/future.dart:9:11)
+''');
+
+ expect(trace.terse.toString(), equals('''
+foo.dart 42:21 notCore
+'''));
+ });
+
+ test("won't make a trace empty", () {
+ var trace = Trace.parse('''
+#1 top (dart:async/future.dart:0:2)
+#2 bottom (dart:core/uri.dart:1:100)
+''');
+
+ expect(trace.terse.toString(), equals('''
+dart:core bottom
+'''));
+ });
+
+ test("won't panic on an empty trace", () {
+ expect(Trace.parse('').terse.toString(), equals(''));
+ });
+ });
+
+ group('.foldFrames', () {
+ test('folds frames together bottom-up', () {
+ var trace = Trace.parse('''
+#0 notFoo (foo.dart:42:21)
+#1 fooTop (bar.dart:0:2)
+#2 fooBottom (foo.dart:1:100)
+#3 alsoNotFoo (bar.dart:10:20)
+#4 fooTop (dart:io/socket.dart:5:10)
+#5 fooBottom (dart:async-patch/future.dart:9:11)
+''');
+
+ var folded =
+ trace.foldFrames((frame) => frame.member!.startsWith('foo'));
+ expect(folded.toString(), equals('''
+foo.dart 42:21 notFoo
+foo.dart 1:100 fooBottom
+bar.dart 10:20 alsoNotFoo
+dart:async-patch/future.dart 9:11 fooBottom
+'''));
+ });
+
+ test('will never fold unparsed frames', () {
+ var trace = Trace.parse(r'''
+.g"cs$#:b";a#>sw{*{ul$"$xqwr`p
+%+j-?uppx<([j@#nu{{>*+$%x-={`{
+!e($b{nj)zs?cgr%!;bmw.+$j+pfj~
+''');
+
+ expect(trace.foldFrames((frame) => true).toString(), equals(r'''
+.g"cs$#:b";a#>sw{*{ul$"$xqwr`p
+%+j-?uppx<([j@#nu{{>*+$%x-={`{
+!e($b{nj)zs?cgr%!;bmw.+$j+pfj~
+'''));
+ });
+
+ group('with terse: true', () {
+ test('folds core frames as well', () {
+ var trace = Trace.parse('''
+#0 notFoo (foo.dart:42:21)
+#1 fooTop (bar.dart:0:2)
+#2 coreBottom (dart:async/future.dart:0:2)
+#3 alsoNotFoo (bar.dart:10:20)
+#4 fooTop (foo.dart:9:11)
+#5 coreBottom (dart:async-patch/future.dart:9:11)
+''');
+
+ var folded = trace.foldFrames(
+ (frame) => frame.member!.startsWith('foo'),
+ terse: true);
+ expect(folded.toString(), equals('''
+foo.dart 42:21 notFoo
+dart:async coreBottom
+bar.dart 10:20 alsoNotFoo
+'''));
+ });
+
+ test('shortens folded frames', () {
+ var trace = Trace.parse('''
+#0 notFoo (foo.dart:42:21)
+#1 fooTop (bar.dart:0:2)
+#2 fooBottom (package:foo/bar.dart:0:2)
+#3 alsoNotFoo (bar.dart:10:20)
+#4 fooTop (foo.dart:9:11)
+#5 fooBottom (foo/bar.dart:9:11)
+#6 againNotFoo (bar.dart:20:20)
+''');
+
+ var folded = trace.foldFrames(
+ (frame) => frame.member!.startsWith('foo'),
+ terse: true);
+ expect(folded.toString(), equals('''
+foo.dart 42:21 notFoo
+package:foo fooBottom
+bar.dart 10:20 alsoNotFoo
+foo fooBottom
+bar.dart 20:20 againNotFoo
+'''));
+ });
+
+ test('removes the bottom-most folded frame', () {
+ var trace = Trace.parse('''
+#2 fooTop (package:foo/bar.dart:0:2)
+#3 notFoo (bar.dart:10:20)
+#5 fooBottom (foo/bar.dart:9:11)
+''');
+
+ var folded = trace.foldFrames(
+ (frame) => frame.member!.startsWith('foo'),
+ terse: true);
+ expect(folded.toString(), equals('''
+package:foo fooTop
+bar.dart 10:20 notFoo
+'''));
+ });
+ });
+ });
+ });
+}
diff --git a/pkgs/stack_trace/test/utils.dart b/pkgs/stack_trace/test/utils.dart
new file mode 100644
index 0000000..98cb5ed
--- /dev/null
+++ b/pkgs/stack_trace/test/utils.dart
@@ -0,0 +1,14 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+/// Returns a matcher that runs [matcher] against a [Frame]'s `member` field.
+Matcher frameMember(Object? matcher) =>
+ isA<Frame>().having((p0) => p0.member, 'member', matcher);
+
+/// Returns a matcher that runs [matcher] against a [Frame]'s `library` field.
+Matcher frameLibrary(Object? matcher) =>
+ isA<Frame>().having((p0) => p0.library, 'library', matcher);
diff --git a/pkgs/stack_trace/test/vm_test.dart b/pkgs/stack_trace/test/vm_test.dart
new file mode 100644
index 0000000..70ac014
--- /dev/null
+++ b/pkgs/stack_trace/test/vm_test.dart
@@ -0,0 +1,112 @@
+// 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.
+
+/// This file tests stack_trace's ability to parse live stack traces. It's a
+/// dual of dartium_test.dart, since method names can differ somewhat from
+/// platform to platform. No similar file exists for dart2js since the specific
+/// method names there are implementation details.
+@TestOn('vm')
+library;
+
+import 'package:path/path.dart' as path;
+import 'package:stack_trace/stack_trace.dart';
+import 'package:test/test.dart';
+
+// The name of this (trivial) function is verified as part of the test
+String getStackTraceString() => StackTrace.current.toString();
+
+// The name of this (trivial) function is verified as part of the test
+StackTrace getStackTraceObject() => StackTrace.current;
+
+Frame getCaller([int? level]) {
+ if (level == null) return Frame.caller();
+ return Frame.caller(level);
+}
+
+Frame nestedGetCaller(int level) => getCaller(level);
+
+Trace getCurrentTrace([int level = 0]) => Trace.current(level);
+
+Trace nestedGetCurrentTrace(int level) => getCurrentTrace(level);
+
+void main() {
+ group('Trace', () {
+ test('.parse parses a real stack trace correctly', () {
+ var string = getStackTraceString();
+ var trace = Trace.parse(string);
+ expect(path.url.basename(trace.frames.first.uri.path),
+ equals('vm_test.dart'));
+ expect(trace.frames.first.member, equals('getStackTraceString'));
+ });
+
+ test('converts from a native stack trace correctly', () {
+ var trace = Trace.from(getStackTraceObject());
+ expect(path.url.basename(trace.frames.first.uri.path),
+ equals('vm_test.dart'));
+ expect(trace.frames.first.member, equals('getStackTraceObject'));
+ });
+
+ test('.from handles a stack overflow trace correctly', () {
+ void overflow() => overflow();
+
+ late Trace? trace;
+ try {
+ overflow();
+ } catch (_, stackTrace) {
+ trace = Trace.from(stackTrace);
+ }
+
+ expect(trace!.frames.first.member, equals('main.<fn>.<fn>.overflow'));
+ });
+
+ group('.current()', () {
+ test('with no argument returns a trace starting at the current frame',
+ () {
+ var trace = Trace.current();
+ expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+ });
+
+ test('at level 0 returns a trace starting at the current frame', () {
+ var trace = Trace.current();
+ expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+ });
+
+ test('at level 1 returns a trace starting at the parent frame', () {
+ var trace = getCurrentTrace(1);
+ expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+ });
+
+ test('at level 2 returns a trace starting at the grandparent frame', () {
+ var trace = nestedGetCurrentTrace(2);
+ expect(trace.frames.first.member, equals('main.<fn>.<fn>.<fn>'));
+ });
+
+ test('throws an ArgumentError for negative levels', () {
+ expect(() => Trace.current(-1), throwsArgumentError);
+ });
+ });
+ });
+
+ group('Frame.caller()', () {
+ test('with no argument returns the parent frame', () {
+ expect(getCaller().member, equals('main.<fn>.<fn>'));
+ });
+
+ test('at level 0 returns the current frame', () {
+ expect(getCaller(0).member, equals('getCaller'));
+ });
+
+ test('at level 1 returns the current frame', () {
+ expect(getCaller(1).member, equals('main.<fn>.<fn>'));
+ });
+
+ test('at level 2 returns the grandparent frame', () {
+ expect(nestedGetCaller(2).member, equals('main.<fn>.<fn>'));
+ });
+
+ test('throws an ArgumentError for negative levels', () {
+ expect(() => Frame.caller(-1), throwsArgumentError);
+ });
+ });
+}
diff --git a/pkgs/stream_channel/.gitignore b/pkgs/stream_channel/.gitignore
new file mode 100644
index 0000000..1447012
--- /dev/null
+++ b/pkgs/stream_channel/.gitignore
@@ -0,0 +1,10 @@
+.buildlog
+.dart_tool/
+.DS_Store
+.idea
+.pub/
+.settings/
+build/
+packages
+.packages
+pubspec.lock
diff --git a/pkgs/stream_channel/AUTHORS b/pkgs/stream_channel/AUTHORS
new file mode 100644
index 0000000..e8063a8
--- /dev/null
+++ b/pkgs/stream_channel/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/stream_channel/CHANGELOG.md b/pkgs/stream_channel/CHANGELOG.md
new file mode 100644
index 0000000..30f7d32
--- /dev/null
+++ b/pkgs/stream_channel/CHANGELOG.md
@@ -0,0 +1,158 @@
+## 2.1.3
+
+* Require Dart 3.3
+* Move to `dart-lang/tools` monorepo.
+
+## 2.1.2
+
+* Require Dart 2.19
+* Add an example.
+* Fix a race condition in `IsolateChannel.connectReceive()` where the channel
+ could hang forever if its sink was closed before the connection was established.
+
+## 2.1.1
+
+* Require Dart 2.14
+* Populate the pubspec `repository` field.
+* Handle multichannel messages where the ID element is a `double` at runtime
+ instead of an `int`. When reading an array with `dart2wasm` numbers within the
+ array are parsed as `double`.
+
+## 2.1.0
+
+* Stable release for null safety.
+
+## 2.0.0
+
+**Breaking changes**
+
+* `IsolateChannel` requires a separate import
+ `package:stram_channel/isolate_channel.dart`.
+ `package:stream_channel/stream_channel.dart` will now not trigger any platform
+ concerns due to importing `dart:isolate`.
+* Remove `JsonDocumentTransformer` class. The `jsonDocument` top level is still
+ available.
+* Remove `StreamChannelTransformer.typed`. Use `.cast` on the transformed
+ channel instead.
+* Change `Future<dynamic>` returns to `Future<void>`.
+
+## 1.7.0
+
+* Make `IsolateChannel` available through
+ `package:stream_channel/isolate_channel.dart`. This will be the required
+ import in the next release.
+* Require `2.0.0` or newer SDK.
+* Internal style changes.
+
+## 1.6.8
+
+* Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 1.6.7+1
+
+* Fix Dart 2 runtime types in `IsolateChannel`.
+
+## 1.6.7
+
+* Update SDK version to 2.0.0-dev.17.0.
+* Add a type argument to `MultiChannel`.
+
+## 1.6.6
+
+* Fix a Dart 2 issue with inner stream transformation in `GuaranteeChannel`.
+
+* Fix a Dart 2 issue with `StreamChannelTransformer.fromCodec()`.
+
+## 1.6.5
+
+* Fix an issue with `JsonDocumentTransformer.bind` where it created an internal
+ stream channel which didn't get a properly inferred type for its `sink`.
+
+## 1.6.4
+
+* Fix a race condition in `MultiChannel` where messages from a remote virtual
+ channel could get dropped if the corresponding local channel wasn't registered
+ quickly enough.
+
+## 1.6.3
+
+* Use `pumpEventQueue()` from test.
+
+## 1.6.2
+
+* Declare support for `async` 2.0.0.
+
+## 1.6.1
+
+* Fix the type of `StreamChannel.transform()`. This previously inverted the
+ generic parameters, so it only really worked with transformers where both
+ generic types were identical.
+
+## 1.6.0
+
+* `Disconnector.disconnect()` now returns a future that completes when all the
+ inner `StreamSink.close()` futures have completed.
+
+## 1.5.0
+
+* Add `new StreamChannel.withCloseGuarantee()` to provide the specific guarantee
+ that closing the sink causes the stream to close before it emits any more
+ events. This is the only guarantee that isn't automatically preserved when
+ transforming a channel.
+
+* `StreamChannelTransformer`s provided by the `stream_channel` package now
+ properly provide the guarantee that closing the sink causes the stream to
+ close before it emits any more events
+
+## 1.4.0
+
+* Add `StreamChannel.cast()`, which soundly coerces the generic type of a
+ channel.
+
+* Add `StreamChannelTransformer.typed()`, which soundly coerces the generic type
+ of a transformer.
+
+## 1.3.2
+
+* Fix all strong-mode errors and warnings.
+
+## 1.3.1
+
+* Make `IsolateChannel` slightly more efficient.
+
+* Make `MultiChannel` follow the stream channel rules.
+
+## 1.3.0
+
+* Add `Disconnector`, a transformer that allows the caller to disconnect the
+ transformed channel.
+
+## 1.2.0
+
+* Add `new StreamChannel.withGuarantees()`, which creates a channel with extra
+ wrapping to ensure that it obeys the stream channel guarantees.
+
+* Add `StreamChannelController`, which can be used to create custom
+ `StreamChannel` objects.
+
+## 1.1.1
+
+* Fix the type annotation for `StreamChannel.transform()`'s parameter.
+
+## 1.1.0
+
+* Add `StreamChannel.transformStream()`, `StreamChannel.transformSink()`,
+ `StreamChannel.changeStream()`, and `StreamChannel.changeSink()` to support
+ changing only the stream or only the sink of a channel.
+
+* Be more explicit about `JsonDocumentTransformer`'s error-handling behavior.
+
+## 1.0.1
+
+* Fix `MultiChannel`'s constructor to take a `StreamChannel`. This is
+ technically a breaking change, but since 1.0.0 was only released an hour ago,
+ we're treating it as a bug fix.
+
+## 1.0.0
+
+* Initial version
diff --git a/pkgs/stream_channel/LICENSE b/pkgs/stream_channel/LICENSE
new file mode 100644
index 0000000..dbd2843
--- /dev/null
+++ b/pkgs/stream_channel/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2015, 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/stream_channel/README.md b/pkgs/stream_channel/README.md
new file mode 100644
index 0000000..3677ccf
--- /dev/null
+++ b/pkgs/stream_channel/README.md
@@ -0,0 +1,20 @@
+[](https://github.com/dart-lang/tools/actions/workflows/stream_channel.yaml)
+[](https://pub.dev/packages/stream_channel)
+[](https://pub.dev/packages/stream_channel/publisher)
+
+This package exposes the `StreamChannel` interface, which represents a two-way
+communication channel. Each `StreamChannel` exposes a `Stream` for receiving
+data and a `StreamSink` for sending it.
+
+`StreamChannel` helps abstract communication logic away from the underlying
+protocol. For example, the [`test`][test] package re-uses its test suite
+communication protocol for both WebSocket connections to browser suites and
+Isolate connections to VM tests.
+
+[test]: https://pub.dev/packages/test
+
+This package also contains utilities for dealing with `StreamChannel`s and with
+two-way communications in general. For documentation of these utilities, see
+[the API docs][api].
+
+[api]: https://pub.dev/documentation/stream_channel/latest/
diff --git a/pkgs/stream_channel/analysis_options.yaml b/pkgs/stream_channel/analysis_options.yaml
new file mode 100644
index 0000000..44cda4d
--- /dev/null
+++ b/pkgs/stream_channel/analysis_options.yaml
@@ -0,0 +1,5 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
diff --git a/pkgs/stream_channel/example/example.dart b/pkgs/stream_channel/example/example.dart
new file mode 100644
index 0000000..b41d8d9
--- /dev/null
+++ b/pkgs/stream_channel/example/example.dart
@@ -0,0 +1,110 @@
+// 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 'dart:isolate';
+
+import 'package:stream_channel/isolate_channel.dart';
+import 'package:stream_channel/stream_channel.dart';
+
+Future<void> main() async {
+ // A StreamChannel<T>, is in simplest terms, a wrapper around a Stream<T> and
+ // a StreamSink<T>. For example, you can create a channel that wraps standard
+ // IO:
+ var stdioChannel = StreamChannel(stdin, stdout);
+ stdioChannel.sink.add('Hello!\n'.codeUnits);
+
+ // Like a Stream<T> can be transformed with a StreamTransformer<T>, a
+ // StreamChannel<T> can be transformed with a StreamChannelTransformer<T>.
+ // For example, we can handle standard input as strings:
+ var stringChannel = stdioChannel
+ .transform(StreamChannelTransformer.fromCodec(utf8))
+ .transformStream(const LineSplitter());
+ stringChannel.sink.add('world!\n');
+
+ // You can implement StreamChannel<T> by extending StreamChannelMixin<T>, but
+ // it's much easier to use a StreamChannelController<T>. A controller has two
+ // StreamChannel<T> members: `local` and `foreign`. The creator of a
+ // controller should work with the `local` channel, while the recipient should
+ // work with the `foreign` channel, and usually will not have direct access to
+ // the underlying controller.
+ var ctrl = StreamChannelController<String>();
+ ctrl.local.stream.listen((event) {
+ // Do something useful here...
+ });
+
+ // You can also pipe events from one channel to another.
+ ctrl
+ ..foreign.pipe(stringChannel)
+ ..local.sink.add('Piped!\n');
+ await ctrl.local.sink.close();
+
+ // The StreamChannel<T> interface provides several guarantees, which can be
+ // found here:
+ // https://pub.dev/documentation/stream_channel/latest/stream_channel/StreamChannel-class.html
+ //
+ // By calling `StreamChannel<T>.withGuarantees()`, you can create a
+ // StreamChannel<T> that provides all guarantees.
+ var dummyCtrl0 = StreamChannelController<String>();
+ var guaranteedChannel = StreamChannel.withGuarantees(
+ dummyCtrl0.foreign.stream, dummyCtrl0.foreign.sink);
+
+ // To close a StreamChannel, use `sink.close()`.
+ await guaranteedChannel.sink.close();
+
+ // A MultiChannel<T> multiplexes multiple virtual channels across a single
+ // underlying transport layer. For example, an application listening over
+ // standard I/O can still support multiple clients if it has a mechanism to
+ // separate events from different clients.
+ //
+ // A MultiChannel<T> splits events into numbered channels, which are
+ // instances of VirtualChannel<T>.
+ var dummyCtrl1 = StreamChannelController<String>();
+ var multiChannel = MultiChannel<String>(dummyCtrl1.foreign);
+ var channel1 = multiChannel.virtualChannel();
+ await multiChannel.sink.close();
+
+ // The client/peer should also create its own MultiChannel<T>, connected to
+ // the underlying transport, use the corresponding ID's to handle events in
+ // their respective channels. It is up to you how to communicate channel ID's
+ // across different endpoints.
+ var dummyCtrl2 = StreamChannelController<String>();
+ var multiChannel2 = MultiChannel<String>(dummyCtrl2.foreign);
+ var channel2 = multiChannel2.virtualChannel(channel1.id);
+ await channel2.sink.close();
+ await multiChannel2.sink.close();
+
+ // Multiple instances of a Dart application can communicate easily across
+ // `SendPort`/`ReceivePort` pairs by means of the `IsolateChannel<T>` class.
+ // Typically, one endpoint will create a `ReceivePort`, and call the
+ // `IsolateChannel.connectReceive` constructor. The other endpoint will be
+ // given the corresponding `SendPort`, and then call
+ // `IsolateChannel.connectSend`.
+ var recv = ReceivePort();
+ var recvChannel = IsolateChannel<void>.connectReceive(recv);
+ var sendChannel = IsolateChannel<void>.connectSend(recv.sendPort);
+
+ // You must manually close `IsolateChannel<T>` sinks, however.
+ await recvChannel.sink.close();
+ await sendChannel.sink.close();
+
+ // You can use the `Disconnector` transformer to cause a channel to act as
+ // though the remote end of its transport had disconnected.
+ var disconnector = Disconnector<String>();
+ var disconnectable = stringChannel.transform(disconnector);
+ disconnectable.sink.add('Still connected!');
+ await disconnector.disconnect();
+
+ // Additionally:
+ // * The `DelegatingStreamController<T>` class can be extended to build a
+ // basis for wrapping other `StreamChannel<T>` objects.
+ // * The `jsonDocument` transformer converts events to/from JSON, using
+ // the `json` codec from `dart:convert`.
+ // * `package:json_rpc_2` directly builds on top of
+ // `package:stream_channel`, so any compatible transport can be used to
+ // create interactive client/server or peer-to-peer applications (i.e.
+ // language servers, microservices, etc.
+}
diff --git a/pkgs/stream_channel/lib/isolate_channel.dart b/pkgs/stream_channel/lib/isolate_channel.dart
new file mode 100644
index 0000000..5d9f6e1
--- /dev/null
+++ b/pkgs/stream_channel/lib/isolate_channel.dart
@@ -0,0 +1,5 @@
+// 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/isolate_channel.dart' show IsolateChannel;
diff --git a/pkgs/stream_channel/lib/src/close_guarantee_channel.dart b/pkgs/stream_channel/lib/src/close_guarantee_channel.dart
new file mode 100644
index 0000000..13432d1
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/close_guarantee_channel.dart
@@ -0,0 +1,91 @@
+// 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 '../stream_channel.dart';
+
+/// A [StreamChannel] that specifically enforces the stream channel guarantee
+/// that closing the sink causes the stream to close before it emits any more
+/// events
+///
+/// This is exposed via [StreamChannel.withCloseGuarantee].
+class CloseGuaranteeChannel<T> extends StreamChannelMixin<T> {
+ @override
+ Stream<T> get stream => _stream;
+ late final _CloseGuaranteeStream<T> _stream;
+
+ @override
+ StreamSink<T> get sink => _sink;
+ late final _CloseGuaranteeSink<T> _sink;
+
+ /// The subscription to the inner stream.
+ StreamSubscription<T>? _subscription;
+
+ /// Whether the sink has closed, causing the underlying channel to disconnect.
+ bool _disconnected = false;
+
+ CloseGuaranteeChannel(Stream<T> innerStream, StreamSink<T> innerSink) {
+ _sink = _CloseGuaranteeSink<T>(innerSink, this);
+ _stream = _CloseGuaranteeStream<T>(innerStream, this);
+ }
+}
+
+/// The stream for [CloseGuaranteeChannel].
+///
+/// This wraps the inner stream to save the subscription on the channel when
+/// [listen] is called.
+class _CloseGuaranteeStream<T> extends Stream<T> {
+ /// The inner stream this is delegating to.
+ final Stream<T> _inner;
+
+ /// The [CloseGuaranteeChannel] this belongs to.
+ final CloseGuaranteeChannel<T> _channel;
+
+ _CloseGuaranteeStream(this._inner, this._channel);
+
+ @override
+ StreamSubscription<T> listen(void Function(T)? onData,
+ {Function? onError, void Function()? onDone, bool? cancelOnError}) {
+ // If the channel is already disconnected, we shouldn't dispatch anything
+ // but a done event.
+ if (_channel._disconnected) {
+ onData = null;
+ onError = null;
+ }
+
+ var subscription = _inner.listen(onData,
+ onError: onError, onDone: onDone, cancelOnError: cancelOnError);
+ if (!_channel._disconnected) {
+ _channel._subscription = subscription;
+ }
+ return subscription;
+ }
+}
+
+/// The sink for [CloseGuaranteeChannel].
+///
+/// This wraps the inner sink to cancel the stream subscription when the sink is
+/// canceled.
+class _CloseGuaranteeSink<T> extends DelegatingStreamSink<T> {
+ /// The [CloseGuaranteeChannel] this belongs to.
+ final CloseGuaranteeChannel<T> _channel;
+
+ _CloseGuaranteeSink(super.inner, this._channel);
+
+ @override
+ Future<void> close() {
+ var done = super.close();
+ _channel._disconnected = true;
+ var subscription = _channel._subscription;
+ if (subscription != null) {
+ // Don't dispatch anything but a done event.
+ subscription.onData(null);
+ subscription.onError(null);
+ }
+ return done;
+ }
+}
diff --git a/pkgs/stream_channel/lib/src/delegating_stream_channel.dart b/pkgs/stream_channel/lib/src/delegating_stream_channel.dart
new file mode 100644
index 0000000..4484a59
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/delegating_stream_channel.dart
@@ -0,0 +1,23 @@
+// 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 '../stream_channel.dart';
+
+/// A simple delegating wrapper around [StreamChannel].
+///
+/// Subclasses can override individual methods, or use this to expose only
+/// [StreamChannel] methods.
+class DelegatingStreamChannel<T> extends StreamChannelMixin<T> {
+ /// The inner channel to which methods are forwarded.
+ final StreamChannel<T> _inner;
+
+ @override
+ Stream<T> get stream => _inner.stream;
+ @override
+ StreamSink<T> get sink => _inner.sink;
+
+ DelegatingStreamChannel(this._inner);
+}
diff --git a/pkgs/stream_channel/lib/src/disconnector.dart b/pkgs/stream_channel/lib/src/disconnector.dart
new file mode 100644
index 0000000..3414e9c
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/disconnector.dart
@@ -0,0 +1,153 @@
+// 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 '../stream_channel.dart';
+
+/// Allows the caller to force a channel to disconnect.
+///
+/// When [disconnect] is called, the channel (or channels) transformed by this
+/// transformer will act as though the remote end had disconnected—the stream
+/// will emit a done event, and the sink will ignore future inputs. The inner
+/// sink will also be closed to notify the remote end of the disconnection.
+///
+/// If a channel is transformed after the [disconnect] has been called, it will
+/// be disconnected immediately.
+class Disconnector<T> implements StreamChannelTransformer<T, T> {
+ /// Whether [disconnect] has been called.
+ bool get isDisconnected => _disconnectMemo.hasRun;
+
+ /// The sinks for transformed channels.
+ ///
+ /// Note that we assume that transformed channels provide the stream channel
+ /// guarantees. This allows us to only track sinks, because we know closing
+ /// the underlying sink will cause the stream to emit a done event.
+ final _sinks = <_DisconnectorSink<T>>[];
+
+ /// Disconnects all channels that have been transformed.
+ ///
+ /// Returns a future that completes when all inner sinks' [StreamSink.close]
+ /// futures have completed. Note that a [StreamController]'s sink won't close
+ /// until the corresponding stream has a listener.
+ Future<void> disconnect() => _disconnectMemo.runOnce(() {
+ var futures = _sinks.map((sink) => sink._disconnect()).toList();
+ _sinks.clear();
+ return Future.wait(futures, eagerError: true);
+ });
+ final _disconnectMemo = AsyncMemoizer<List<void>>();
+
+ @override
+ StreamChannel<T> bind(StreamChannel<T> channel) {
+ return channel.changeSink((innerSink) {
+ var sink = _DisconnectorSink<T>(innerSink);
+
+ if (isDisconnected) {
+ // Ignore errors here, because otherwise there would be no way for the
+ // user to handle them gracefully.
+ sink._disconnect().catchError((_) {});
+ } else {
+ _sinks.add(sink);
+ }
+
+ return sink;
+ });
+ }
+}
+
+/// A sink wrapper that can force a disconnection.
+class _DisconnectorSink<T> implements StreamSink<T> {
+ /// The inner sink.
+ final StreamSink<T> _inner;
+
+ @override
+ Future<void> get done => _inner.done;
+
+ /// Whether [Disconnector.disconnect] has been called.
+ var _isDisconnected = false;
+
+ /// Whether the user has called [close].
+ var _closed = false;
+
+ /// The subscription to the stream passed to [addStream], if a stream is
+ /// currently being added.
+ StreamSubscription<T>? _addStreamSubscription;
+
+ /// The completer for the future returned by [addStream], if a stream is
+ /// currently being added.
+ Completer? _addStreamCompleter;
+
+ /// Whether we're currently adding a stream with [addStream].
+ bool get _inAddStream => _addStreamSubscription != null;
+
+ _DisconnectorSink(this._inner);
+
+ @override
+ void add(T data) {
+ if (_closed) throw StateError('Cannot add event after closing.');
+ if (_inAddStream) {
+ throw StateError('Cannot add event while adding stream.');
+ }
+ if (_isDisconnected) return;
+
+ _inner.add(data);
+ }
+
+ @override
+ void addError(Object error, [StackTrace? stackTrace]) {
+ if (_closed) throw StateError('Cannot add event after closing.');
+ if (_inAddStream) {
+ throw StateError('Cannot add event while adding stream.');
+ }
+ if (_isDisconnected) return;
+
+ _inner.addError(error, stackTrace);
+ }
+
+ @override
+ Future<void> addStream(Stream<T> stream) {
+ if (_closed) throw StateError('Cannot add stream after closing.');
+ if (_inAddStream) {
+ throw StateError('Cannot add stream while adding stream.');
+ }
+ if (_isDisconnected) return Future.value();
+
+ _addStreamCompleter = Completer.sync();
+ _addStreamSubscription = stream.listen(_inner.add,
+ onError: _inner.addError, onDone: _addStreamCompleter!.complete);
+ return _addStreamCompleter!.future.then((_) {
+ _addStreamCompleter = null;
+ _addStreamSubscription = null;
+ });
+ }
+
+ @override
+ Future<void> close() {
+ if (_inAddStream) {
+ throw StateError('Cannot close sink while adding stream.');
+ }
+
+ _closed = true;
+ return _inner.close();
+ }
+
+ /// Disconnects this sink.
+ ///
+ /// This closes the underlying sink and stops forwarding events. It returns
+ /// the [StreamSink.close] future for the underlying sink.
+ Future<void> _disconnect() {
+ _isDisconnected = true;
+ var future = _inner.close();
+
+ if (_inAddStream) {
+ _addStreamCompleter!.complete(_addStreamSubscription!.cancel());
+ _addStreamCompleter = null;
+ _addStreamSubscription = null;
+ }
+
+ return future;
+ }
+}
diff --git a/pkgs/stream_channel/lib/src/guarantee_channel.dart b/pkgs/stream_channel/lib/src/guarantee_channel.dart
new file mode 100644
index 0000000..30ebe2e
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/guarantee_channel.dart
@@ -0,0 +1,207 @@
+// 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 '../stream_channel.dart';
+
+/// A [StreamChannel] that enforces the stream channel guarantees.
+///
+/// This is exposed via [StreamChannel.withGuarantees].
+class GuaranteeChannel<T> extends StreamChannelMixin<T> {
+ @override
+ Stream<T> get stream => _streamController.stream;
+
+ @override
+ StreamSink<T> get sink => _sink;
+ late final _GuaranteeSink<T> _sink;
+
+ /// The controller for [stream].
+ ///
+ /// This intermediate controller allows us to continue listening for a done
+ /// event even after the user has canceled their subscription, and to send our
+ /// own done event when the sink is closed.
+ late final StreamController<T> _streamController;
+
+ /// The subscription to the inner stream.
+ StreamSubscription<T>? _subscription;
+
+ /// Whether the sink has closed, causing the underlying channel to disconnect.
+ bool _disconnected = false;
+
+ GuaranteeChannel(Stream<T> innerStream, StreamSink<T> innerSink,
+ {bool allowSinkErrors = true}) {
+ _sink = _GuaranteeSink<T>(innerSink, this, allowErrors: allowSinkErrors);
+
+ // Enforce the single-subscription guarantee by changing a broadcast stream
+ // to single-subscription.
+ if (innerStream.isBroadcast) {
+ innerStream =
+ innerStream.transform(SingleSubscriptionTransformer<T, T>());
+ }
+
+ _streamController = StreamController<T>(
+ onListen: () {
+ // If the sink has disconnected, we've already called
+ // [_streamController.close].
+ if (_disconnected) return;
+
+ _subscription = innerStream.listen(_streamController.add,
+ onError: _streamController.addError, onDone: () {
+ _sink._onStreamDisconnected();
+ _streamController.close();
+ });
+ },
+ sync: true);
+ }
+
+ /// Called by [_GuaranteeSink] when the user closes it.
+ ///
+ /// The sink closing indicates that the connection is closed, so the stream
+ /// should stop emitting events.
+ void _onSinkDisconnected() {
+ _disconnected = true;
+ var subscription = _subscription;
+ if (subscription != null) subscription.cancel();
+ _streamController.close();
+ }
+}
+
+/// The sink for [GuaranteeChannel].
+///
+/// This wraps the inner sink to ignore events and cancel any in-progress
+/// [addStream] calls when the underlying channel closes.
+class _GuaranteeSink<T> implements StreamSink<T> {
+ /// The inner sink being wrapped.
+ final StreamSink<T> _inner;
+
+ /// The [GuaranteeChannel] this belongs to.
+ final GuaranteeChannel<T> _channel;
+
+ @override
+ Future<void> get done => _doneCompleter.future;
+ final _doneCompleter = Completer<void>();
+
+ /// Whether connection is disconnected.
+ ///
+ /// This can happen because the stream has emitted a done event, or because
+ /// the user added an error when [_allowErrors] is `false`.
+ bool _disconnected = false;
+
+ /// Whether the user has called [close].
+ bool _closed = false;
+
+ /// The subscription to the stream passed to [addStream], if a stream is
+ /// currently being added.
+ StreamSubscription<T>? _addStreamSubscription;
+
+ /// The completer for the future returned by [addStream], if a stream is
+ /// currently being added.
+ Completer? _addStreamCompleter;
+
+ /// Whether we're currently adding a stream with [addStream].
+ bool get _inAddStream => _addStreamSubscription != null;
+
+ /// Whether errors are passed on to the underlying sink.
+ ///
+ /// If this is `false`, any error passed to the sink is piped to [done] and
+ /// the underlying sink is closed.
+ final bool _allowErrors;
+
+ _GuaranteeSink(this._inner, this._channel, {bool allowErrors = true})
+ : _allowErrors = allowErrors;
+
+ @override
+ void add(T data) {
+ if (_closed) throw StateError('Cannot add event after closing.');
+ if (_inAddStream) {
+ throw StateError('Cannot add event while adding stream.');
+ }
+ if (_disconnected) return;
+
+ _inner.add(data);
+ }
+
+ @override
+ void addError(Object error, [StackTrace? stackTrace]) {
+ if (_closed) throw StateError('Cannot add event after closing.');
+ if (_inAddStream) {
+ throw StateError('Cannot add event while adding stream.');
+ }
+ if (_disconnected) return;
+
+ _addError(error, stackTrace);
+ }
+
+ /// Like [addError], but doesn't check to ensure that an error can be added.
+ ///
+ /// This is called from [addStream], so it shouldn't fail if a stream is being
+ /// added.
+ void _addError(Object error, [StackTrace? stackTrace]) {
+ if (_allowErrors) {
+ _inner.addError(error, stackTrace);
+ return;
+ }
+
+ _doneCompleter.completeError(error, stackTrace);
+
+ // Treat an error like both the stream and sink disconnecting.
+ _onStreamDisconnected();
+ _channel._onSinkDisconnected();
+
+ // Ignore errors from the inner sink. We're already surfacing one error, and
+ // if the user handles it we don't want them to have another top-level.
+ _inner.close().catchError((_) {});
+ }
+
+ @override
+ Future<void> addStream(Stream<T> stream) {
+ if (_closed) throw StateError('Cannot add stream after closing.');
+ if (_inAddStream) {
+ throw StateError('Cannot add stream while adding stream.');
+ }
+ if (_disconnected) return Future.value();
+
+ _addStreamCompleter = Completer.sync();
+ _addStreamSubscription = stream.listen(_inner.add,
+ onError: _addError, onDone: _addStreamCompleter!.complete);
+ return _addStreamCompleter!.future.then((_) {
+ _addStreamCompleter = null;
+ _addStreamSubscription = null;
+ });
+ }
+
+ @override
+ Future<void> close() {
+ if (_inAddStream) {
+ throw StateError('Cannot close sink while adding stream.');
+ }
+
+ if (_closed) return done;
+ _closed = true;
+
+ if (!_disconnected) {
+ _channel._onSinkDisconnected();
+ _doneCompleter.complete(_inner.close());
+ }
+
+ return done;
+ }
+
+ /// Called by [GuaranteeChannel] when the stream emits a done event.
+ ///
+ /// The stream being done indicates that the connection is closed, so the
+ /// sink should stop forwarding events.
+ void _onStreamDisconnected() {
+ _disconnected = true;
+ if (!_doneCompleter.isCompleted) _doneCompleter.complete();
+
+ if (!_inAddStream) return;
+ _addStreamCompleter!.complete(_addStreamSubscription!.cancel());
+ _addStreamCompleter = null;
+ _addStreamSubscription = null;
+ }
+}
diff --git a/pkgs/stream_channel/lib/src/isolate_channel.dart b/pkgs/stream_channel/lib/src/isolate_channel.dart
new file mode 100644
index 0000000..15c68a4
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/isolate_channel.dart
@@ -0,0 +1,115 @@
+// 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 '../stream_channel.dart';
+
+/// A [StreamChannel] that communicates over a [ReceivePort]/[SendPort] pair,
+/// presumably with another isolate.
+///
+/// The remote endpoint doesn't necessarily need to be running an
+/// [IsolateChannel]. This can be used with any two ports, although the
+/// [StreamChannel] semantics mean that this class will treat them as being
+/// paired (for example, closing the [sink] will cause the [stream] to stop
+/// emitting events).
+///
+/// The underlying isolate ports have no notion of closing connections. This
+/// means that [stream] won't close unless [sink] is closed, and that closing
+/// [sink] won't cause the remote endpoint to close. Users should take care to
+/// ensure that they always close the [sink] of every [IsolateChannel] they use
+/// to avoid leaving dangling [ReceivePort]s.
+class IsolateChannel<T> extends StreamChannelMixin<T> {
+ @override
+ final Stream<T> stream;
+ @override
+ final StreamSink<T> sink;
+
+ /// Connects to a remote channel that was created with
+ /// [IsolateChannel.connectSend].
+ ///
+ /// These constructors establish a connection using only a single
+ /// [SendPort]/[ReceivePort] pair, as long as each side uses one of the
+ /// connect constructors.
+ ///
+ /// The connection protocol is guaranteed to remain compatible across versions
+ /// at least until the next major version release. If the protocol is
+ /// violated, the resulting channel will emit a single value on its stream and
+ /// then close.
+ factory IsolateChannel.connectReceive(ReceivePort receivePort) {
+ // We can't use a [StreamChannelCompleter] here because we need the return
+ // value to be an [IsolateChannel].
+ var isCompleted = false;
+ var streamCompleter = StreamCompleter<T>();
+ var sinkCompleter = StreamSinkCompleter<T>();
+
+ var channel = IsolateChannel<T>._(streamCompleter.stream, sinkCompleter.sink
+ .transform(StreamSinkTransformer.fromHandlers(handleDone: (sink) {
+ if (!isCompleted) {
+ receivePort.close();
+ streamCompleter.setSourceStream(const Stream.empty());
+ sinkCompleter.setDestinationSink(NullStreamSink<T>());
+ }
+ sink.close();
+ })));
+
+ // The first message across the ReceivePort should be a SendPort pointing to
+ // the remote end. If it's not, we'll make the stream emit an error
+ // complaining.
+ late StreamSubscription<dynamic> subscription;
+ subscription = receivePort.listen((message) {
+ isCompleted = true;
+ if (message is SendPort) {
+ var controller =
+ StreamChannelController<T>(allowForeignErrors: false, sync: true);
+ SubscriptionStream(subscription).cast<T>().pipe(controller.local.sink);
+ controller.local.stream
+ .listen((data) => message.send(data), onDone: receivePort.close);
+
+ streamCompleter.setSourceStream(controller.foreign.stream);
+ sinkCompleter.setDestinationSink(controller.foreign.sink);
+ return;
+ }
+
+ streamCompleter.setError(
+ StateError('Unexpected Isolate response "$message".'),
+ StackTrace.current);
+ sinkCompleter.setDestinationSink(NullStreamSink<T>());
+ subscription.cancel();
+ });
+
+ return channel;
+ }
+
+ /// Connects to a remote channel that was created with
+ /// [IsolateChannel.connectReceive].
+ ///
+ /// These constructors establish a connection using only a single
+ /// [SendPort]/[ReceivePort] pair, as long as each side uses one of the
+ /// connect constructors.
+ ///
+ /// The connection protocol is guaranteed to remain compatible across versions
+ /// at least until the next major version release.
+ factory IsolateChannel.connectSend(SendPort sendPort) {
+ var receivePort = ReceivePort();
+ sendPort.send(receivePort.sendPort);
+ return IsolateChannel(receivePort, sendPort);
+ }
+
+ /// Creates a stream channel that receives messages from [receivePort] and
+ /// sends them over [sendPort].
+ factory IsolateChannel(ReceivePort receivePort, SendPort sendPort) {
+ var controller =
+ StreamChannelController<T>(allowForeignErrors: false, sync: true);
+ receivePort.cast<T>().pipe(controller.local.sink);
+ controller.local.stream
+ .listen((data) => sendPort.send(data), onDone: receivePort.close);
+ return IsolateChannel._(controller.foreign.stream, controller.foreign.sink);
+ }
+
+ IsolateChannel._(this.stream, this.sink);
+}
diff --git a/pkgs/stream_channel/lib/src/json_document_transformer.dart b/pkgs/stream_channel/lib/src/json_document_transformer.dart
new file mode 100644
index 0000000..3feda43
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/json_document_transformer.dart
@@ -0,0 +1,35 @@
+// 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:convert';
+
+import 'package:async/async.dart';
+
+import '../stream_channel.dart';
+
+/// A [StreamChannelTransformer] that transforms JSON documents—strings that
+/// contain individual objects encoded as JSON—into decoded Dart objects.
+///
+/// This decodes JSON that's emitted by the transformed channel's stream, and
+/// encodes objects so that JSON is passed to the transformed channel's sink.
+///
+/// If the transformed channel emits invalid JSON, this emits a
+/// [FormatException]. If an unencodable object is added to the sink, it
+/// synchronously throws a [JsonUnsupportedObjectError].
+final StreamChannelTransformer<Object?, String> jsonDocument =
+ const _JsonDocument();
+
+class _JsonDocument implements StreamChannelTransformer<Object?, String> {
+ const _JsonDocument();
+
+ @override
+ StreamChannel<Object?> bind(StreamChannel<String> channel) {
+ var stream = channel.stream.map(jsonDecode);
+ var sink = StreamSinkTransformer<Object, String>.fromHandlers(
+ handleData: (data, sink) {
+ sink.add(jsonEncode(data));
+ }).bind(channel.sink);
+ return StreamChannel.withCloseGuarantee(stream, sink);
+ }
+}
diff --git a/pkgs/stream_channel/lib/src/multi_channel.dart b/pkgs/stream_channel/lib/src/multi_channel.dart
new file mode 100644
index 0000000..4894239
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/multi_channel.dart
@@ -0,0 +1,274 @@
+// 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 '../stream_channel.dart';
+
+/// A class that multiplexes multiple virtual channels across a single
+/// underlying transport layer.
+///
+/// This should be connected to another [MultiChannel] on the other end of the
+/// underlying channel. It starts with a single default virtual channel,
+/// accessible via [stream] and [sink]. Additional virtual channels can be
+/// created with [virtualChannel].
+///
+/// When a virtual channel is created by one endpoint, the other must connect to
+/// it before messages may be sent through it. The first endpoint passes its
+/// [VirtualChannel.id] to the second, which then creates a channel from that id
+/// also using [virtualChannel]. For example:
+///
+/// ```dart
+/// // First endpoint
+/// var virtual = multiChannel.virtualChannel();
+/// multiChannel.sink.add({
+/// "channel": virtual.id
+/// });
+///
+/// // Second endpoint
+/// multiChannel.stream.listen((message) {
+/// var virtual = multiChannel.virtualChannel(message["channel"]);
+/// // ...
+/// });
+/// ```
+///
+/// Sending errors across a [MultiChannel] is not supported. Any errors from the
+/// underlying stream will be reported only via the default
+/// [MultiChannel.stream].
+///
+/// Each virtual channel may be closed individually. When all of them are
+/// closed, the underlying [StreamSink] is closed automatically.
+abstract class MultiChannel<T> implements StreamChannel<T> {
+ /// The default input stream.
+ ///
+ /// This connects to the remote [sink].
+ @override
+ Stream<T> get stream;
+
+ /// The default output stream.
+ ///
+ /// This connects to the remote [stream]. If this is closed, the remote
+ /// [stream] will close, but other virtual channels will remain open and new
+ /// virtual channels may be opened.
+ @override
+ StreamSink<T> get sink;
+
+ /// Creates a new [MultiChannel] that sends and receives messages over
+ /// [inner].
+ ///
+ /// The inner channel must take JSON-like objects.
+ factory MultiChannel(StreamChannel<dynamic> inner) => _MultiChannel<T>(inner);
+
+ /// Creates a new virtual channel.
+ ///
+ /// If [id] is not passed, this creates a virtual channel from scratch. Before
+ /// it's used, its [VirtualChannel.id] must be sent to the remote endpoint
+ /// where [virtualChannel] should be called with that id.
+ ///
+ /// If [id] is passed, this creates a virtual channel corresponding to the
+ /// channel with that id on the remote channel.
+ ///
+ /// Throws an [ArgumentError] if a virtual channel already exists for [id].
+ /// Throws a [StateError] if the underlying channel is closed.
+ VirtualChannel<T> virtualChannel([int? id]);
+}
+
+/// The implementation of [MultiChannel].
+///
+/// This is private so that [VirtualChannel] can inherit from [MultiChannel]
+/// without having to implement all the private members.
+class _MultiChannel<T> extends StreamChannelMixin<T>
+ implements MultiChannel<T> {
+ /// The inner channel over which all communication is conducted.
+ ///
+ /// This will be `null` if the underlying communication channel is closed.
+ StreamChannel<dynamic>? _inner;
+
+ /// The subscription to [_inner].stream.
+ StreamSubscription<dynamic>? _innerStreamSubscription;
+
+ @override
+ Stream<T> get stream => _mainController.foreign.stream;
+ @override
+ StreamSink<T> get sink => _mainController.foreign.sink;
+
+ /// The controller for this channel.
+ final _mainController = StreamChannelController<T>(sync: true);
+
+ /// A map from input IDs to [StreamChannelController]s that should be used to
+ /// communicate over those channels.
+ final _controllers = <int, StreamChannelController<T>>{};
+
+ /// Input IDs of controllers in [_controllers] that we've received messages
+ /// for but that have not yet had a local [virtualChannel] created.
+ final _pendingIds = <int>{};
+
+ /// Input IDs of virtual channels that used to exist but have since been
+ /// closed.
+ final _closedIds = <int>{};
+
+ /// The next id to use for a local virtual channel.
+ ///
+ /// Ids are used to identify virtual channels. Each message is tagged with an
+ /// id; the receiving [MultiChannel] uses this id to look up which
+ /// [VirtualChannel] the message should be dispatched to.
+ ///
+ /// The id scheme for virtual channels is somewhat complicated. This is
+ /// necessary to ensure that there are no conflicts even when both endpoints
+ /// have virtual channels with the same id; since both endpoints can send and
+ /// receive messages across each virtual channel, a naïve scheme would make it
+ /// impossible to tell whether a message was from a channel that originated in
+ /// the remote endpoint or a reply on a channel that originated in the local
+ /// endpoint.
+ ///
+ /// The trick is that each endpoint only uses odd ids for its own channels.
+ /// When sending a message over a channel that was created by the remote
+ /// endpoint, the channel's id plus one is used. This way each [MultiChannel]
+ /// knows that if an incoming message has an odd id, it's coming from a
+ /// channel that was originally created remotely, but if it has an even id,
+ /// it's coming from a channel that was originally created locally.
+ var _nextId = 1;
+
+ _MultiChannel(StreamChannel<dynamic> inner) : _inner = inner {
+ // The default connection is a special case which has id 0 on both ends.
+ // This allows it to begin connected without having to send over an id.
+ _controllers[0] = _mainController;
+ _mainController.local.stream.listen(
+ (message) => _inner!.sink.add(<Object?>[0, message]),
+ onDone: () => _closeChannel(0, 0));
+
+ _innerStreamSubscription = _inner!.stream.cast<List>().listen((message) {
+ var id = (message[0] as num).toInt();
+
+ // If the channel was closed before an incoming message was processed,
+ // ignore that message.
+ if (_closedIds.contains(id)) return;
+
+ var controller = _controllers.putIfAbsent(id, () {
+ // If we receive a message for a controller that doesn't have a local
+ // counterpart yet, create a controller for it to buffer incoming
+ // messages for when a local connection is created.
+ _pendingIds.add(id);
+ return StreamChannelController(sync: true);
+ });
+
+ if (message.length > 1) {
+ controller.local.sink.add(message[1] as T);
+ } else {
+ // A message without data indicates that the channel has been closed. We
+ // can just close the sink here without doing any more cleanup, because
+ // the sink closing will cause the stream to emit a done event which
+ // will trigger more cleanup.
+ controller.local.sink.close();
+ }
+ },
+ onDone: _closeInnerChannel,
+ onError: _mainController.local.sink.addError);
+ }
+
+ @override
+ VirtualChannel<T> virtualChannel([int? id]) {
+ int inputId;
+ int outputId;
+ if (id != null) {
+ // Since the user is passing in an id, we're connected to a remote
+ // VirtualChannel. This means messages they send over this channel will
+ // have the original odd id, but our replies will have an even id.
+ inputId = id;
+ outputId = id + 1;
+ } else {
+ // Since we're generating an id, we originated this VirtualChannel. This
+ // means messages we send over this channel will have the original odd id,
+ // but the remote channel's replies will have an even id.
+ inputId = _nextId + 1;
+ outputId = _nextId;
+ _nextId += 2;
+ }
+
+ // If the inner channel has already closed, create new virtual channels in a
+ // closed state.
+ if (_inner == null) {
+ return VirtualChannel._(
+ this, inputId, const Stream.empty(), NullStreamSink());
+ }
+
+ late StreamChannelController<T> controller;
+ if (_pendingIds.remove(inputId)) {
+ // If we've already received messages for this channel, use the controller
+ // where those messages are buffered.
+ controller = _controllers[inputId]!;
+ } else if (_controllers.containsKey(inputId) ||
+ _closedIds.contains(inputId)) {
+ throw ArgumentError('A virtual channel with id $id already exists.');
+ } else {
+ controller = StreamChannelController(sync: true);
+ _controllers[inputId] = controller;
+ }
+
+ controller.local.stream.listen(
+ (message) => _inner!.sink.add(<Object?>[outputId, message]),
+ onDone: () => _closeChannel(inputId, outputId));
+ return VirtualChannel._(
+ this, outputId, controller.foreign.stream, controller.foreign.sink);
+ }
+
+ /// Closes the virtual channel for which incoming messages have [inputId] and
+ /// outgoing messages have [outputId].
+ void _closeChannel(int inputId, int outputId) {
+ _closedIds.add(inputId);
+ var controller = _controllers.remove(inputId)!;
+ controller.local.sink.close();
+
+ if (_inner == null) return;
+
+ // A message without data indicates that the virtual channel has been
+ // closed.
+ _inner!.sink.add([outputId]);
+ if (_controllers.isEmpty) _closeInnerChannel();
+ }
+
+ /// Closes the underlying communication channel.
+ void _closeInnerChannel() {
+ _inner!.sink.close();
+ _innerStreamSubscription!.cancel();
+ _inner = null;
+
+ // Convert this to a list because the close is dispatched synchronously, and
+ // that could conceivably remove a controller from [_controllers].
+ for (var controller in _controllers.values.toList(growable: false)) {
+ controller.local.sink.close();
+ }
+ _controllers.clear();
+ }
+}
+
+/// A virtual channel created by [MultiChannel].
+///
+/// This implements [MultiChannel] for convenience.
+/// [VirtualChannel.virtualChannel] is semantically identical to the parent's
+/// [MultiChannel.virtualChannel].
+class VirtualChannel<T> extends StreamChannelMixin<T>
+ implements MultiChannel<T> {
+ /// The [MultiChannel] that created this.
+ final MultiChannel<T> _parent;
+
+ /// The identifier for this channel.
+ ///
+ /// This can be sent across the [MultiChannel] to provide the remote endpoint
+ /// a means to connect to this channel. Nothing about this is guaranteed
+ /// except that it will be JSON-serializable.
+ final int id;
+
+ @override
+ final Stream<T> stream;
+ @override
+ final StreamSink<T> sink;
+
+ VirtualChannel._(this._parent, this.id, this.stream, this.sink);
+
+ @override
+ VirtualChannel<T> virtualChannel([int? id]) => _parent.virtualChannel(id);
+}
diff --git a/pkgs/stream_channel/lib/src/stream_channel_completer.dart b/pkgs/stream_channel/lib/src/stream_channel_completer.dart
new file mode 100644
index 0000000..9d007eb
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/stream_channel_completer.dart
@@ -0,0 +1,74 @@
+// 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:async/async.dart';
+
+import '../stream_channel.dart';
+
+/// A [channel] where the source and destination are provided later.
+///
+/// The [channel] is a normal channel that can be listened to and that events
+/// can be added to immediately, but until [setChannel] is called it won't emit
+/// any events and all events added to it will be buffered.
+class StreamChannelCompleter<T> {
+ /// The completer for this channel's stream.
+ final _streamCompleter = StreamCompleter<T>();
+
+ /// The completer for this channel's sink.
+ final _sinkCompleter = StreamSinkCompleter<T>();
+
+ /// The channel for this completer.
+ StreamChannel<T> get channel => _channel;
+ late final StreamChannel<T> _channel;
+
+ /// Whether [setChannel] has been called.
+ bool _set = false;
+
+ /// Convert a `Future<StreamChannel>` to a `StreamChannel`.
+ ///
+ /// This creates a channel using a channel completer, and sets the source
+ /// channel to the result of the future when the future completes.
+ ///
+ /// If the future completes with an error, the returned channel's stream will
+ /// instead contain just that error. The sink will silently discard all
+ /// events.
+ static StreamChannel fromFuture(Future<StreamChannel> channelFuture) {
+ var completer = StreamChannelCompleter<void>();
+ channelFuture.then(completer.setChannel, onError: completer.setError);
+ return completer.channel;
+ }
+
+ StreamChannelCompleter() {
+ _channel = StreamChannel<T>(_streamCompleter.stream, _sinkCompleter.sink);
+ }
+
+ /// Set a channel as the source and destination for [channel].
+ ///
+ /// A channel may be set at most once.
+ ///
+ /// Either [setChannel] or [setError] may be called at most once. Trying to
+ /// call either of them again will fail.
+ void setChannel(StreamChannel<T> channel) {
+ if (_set) throw StateError('The channel has already been set.');
+ _set = true;
+
+ _streamCompleter.setSourceStream(channel.stream);
+ _sinkCompleter.setDestinationSink(channel.sink);
+ }
+
+ /// Indicates that there was an error connecting the channel.
+ ///
+ /// This makes the stream emit [error] and close. It makes the sink discard
+ /// all its events.
+ ///
+ /// Either [setChannel] or [setError] may be called at most once. Trying to
+ /// call either of them again will fail.
+ void setError(Object error, [StackTrace? stackTrace]) {
+ if (_set) throw StateError('The channel has already been set.');
+ _set = true;
+
+ _streamCompleter.setError(error, stackTrace);
+ _sinkCompleter.setDestinationSink(NullStreamSink());
+ }
+}
diff --git a/pkgs/stream_channel/lib/src/stream_channel_controller.dart b/pkgs/stream_channel/lib/src/stream_channel_controller.dart
new file mode 100644
index 0000000..25d5239
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/stream_channel_controller.dart
@@ -0,0 +1,67 @@
+// 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.
+
+/// @docImport 'isolate_channel.dart';
+library;
+
+import 'dart:async';
+
+import '../stream_channel.dart';
+
+/// A controller for exposing a new [StreamChannel].
+///
+/// This exposes two connected [StreamChannel]s, [local] and [foreign]. The
+/// user's code should use [local] to emit and receive events. Then [foreign]
+/// can be returned for others to use. For example, here's a simplified version
+/// of the implementation of [IsolateChannel.new]:
+///
+/// ```dart
+/// StreamChannel isolateChannel(ReceivePort receivePort, SendPort sendPort) {
+/// var controller = new StreamChannelController(allowForeignErrors: false);
+///
+/// // Pipe all events from the receive port into the local sink...
+/// receivePort.pipe(controller.local.sink);
+///
+/// // ...and all events from the local stream into the send port.
+/// controller.local.stream.listen(sendPort.send, onDone: receivePort.close);
+///
+/// // Then return the foreign controller for your users to use.
+/// return controller.foreign;
+/// }
+/// ```
+class StreamChannelController<T> {
+ /// The local channel.
+ ///
+ /// This channel should be used directly by the creator of this
+ /// [StreamChannelController] to send and receive events.
+ StreamChannel<T> get local => _local;
+ late final StreamChannel<T> _local;
+
+ /// The foreign channel.
+ ///
+ /// This channel should be returned to external users so they can communicate
+ /// with [local].
+ StreamChannel<T> get foreign => _foreign;
+ late final StreamChannel<T> _foreign;
+
+ /// Creates a [StreamChannelController].
+ ///
+ /// If [sync] is true, events added to either channel's sink are synchronously
+ /// dispatched to the other channel's stream. This should only be done if the
+ /// source of those events is already asynchronous.
+ ///
+ /// If [allowForeignErrors] is `false`, errors are not allowed to be passed to
+ /// the foreign channel's sink. If any are, the connection will close and the
+ /// error will be forwarded to the foreign channel's [StreamSink.done] future.
+ /// This guarantees that the local stream will never emit errors.
+ StreamChannelController({bool allowForeignErrors = true, bool sync = false}) {
+ var localToForeignController = StreamController<T>(sync: sync);
+ var foreignToLocalController = StreamController<T>(sync: sync);
+ _local = StreamChannel<T>.withGuarantees(
+ foreignToLocalController.stream, localToForeignController.sink);
+ _foreign = StreamChannel<T>.withGuarantees(
+ localToForeignController.stream, foreignToLocalController.sink,
+ allowSinkErrors: allowForeignErrors);
+ }
+}
diff --git a/pkgs/stream_channel/lib/src/stream_channel_transformer.dart b/pkgs/stream_channel/lib/src/stream_channel_transformer.dart
new file mode 100644
index 0000000..cf62c76
--- /dev/null
+++ b/pkgs/stream_channel/lib/src/stream_channel_transformer.dart
@@ -0,0 +1,58 @@
+// 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 '../stream_channel.dart';
+
+/// A [StreamChannelTransformer] transforms the events being passed to and
+/// emitted by a [StreamChannel].
+///
+/// This works on the same principle as [StreamTransformer] and
+/// [StreamSinkTransformer]. Each transformer defines a [bind] method that takes
+/// in the original [StreamChannel] and returns the transformed version.
+///
+/// Transformers must be able to have [bind] called multiple times. If a
+/// subclass implements [bind] explicitly, it should be sure that the returned
+/// stream follows the second stream channel guarantee: closing the sink causes
+/// the stream to close before it emits any more events. This guarantee is
+/// invalidated when an asynchronous gap is added between the original stream's
+/// event dispatch and the returned stream's, for example by transforming it
+/// with a [StreamTransformer]. The guarantee can be easily preserved using
+/// [StreamChannel.withCloseGuarantee].
+class StreamChannelTransformer<S, T> {
+ /// The transformer to use on the channel's stream.
+ final StreamTransformer<T, S> _streamTransformer;
+
+ /// The transformer to use on the channel's sink.
+ final StreamSinkTransformer<S, T> _sinkTransformer;
+
+ /// Creates a [StreamChannelTransformer] from existing stream and sink
+ /// transformers.
+ const StreamChannelTransformer(
+ this._streamTransformer, this._sinkTransformer);
+
+ /// Creates a [StreamChannelTransformer] from a codec's encoder and decoder.
+ ///
+ /// All input to the inner channel's sink is encoded using [Codec.encoder],
+ /// and all output from its stream is decoded using [Codec.decoder].
+ StreamChannelTransformer.fromCodec(Codec<S, T> codec)
+ : this(codec.decoder,
+ StreamSinkTransformer.fromStreamTransformer(codec.encoder));
+
+ /// Transforms the events sent to and emitted by [channel].
+ ///
+ /// Creates a new channel. When events are passed to the returned channel's
+ /// sink, the transformer will transform them and pass the transformed
+ /// versions to `channel.sink`. When events are emitted from the
+ /// `channel.straem`, the transformer will transform them and pass the
+ /// transformed versions to the returned channel's stream.
+ StreamChannel<S> bind(StreamChannel<T> channel) =>
+ StreamChannel<S>.withCloseGuarantee(
+ channel.stream.transform(_streamTransformer),
+ _sinkTransformer.bind(channel.sink));
+}
diff --git a/pkgs/stream_channel/lib/stream_channel.dart b/pkgs/stream_channel/lib/stream_channel.dart
new file mode 100644
index 0000000..85f9a97
--- /dev/null
+++ b/pkgs/stream_channel/lib/stream_channel.dart
@@ -0,0 +1,181 @@
+// 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 'src/close_guarantee_channel.dart';
+import 'src/guarantee_channel.dart';
+import 'src/stream_channel_transformer.dart';
+
+export 'src/delegating_stream_channel.dart';
+export 'src/disconnector.dart';
+export 'src/json_document_transformer.dart';
+export 'src/multi_channel.dart';
+export 'src/stream_channel_completer.dart';
+export 'src/stream_channel_controller.dart';
+export 'src/stream_channel_transformer.dart';
+
+/// An abstract class representing a two-way communication channel.
+///
+/// Users should consider the [stream] emitting a "done" event to be the
+/// canonical indicator that the channel has closed. If they wish to close the
+/// channel, they should close the [sink]—canceling the stream subscription is
+/// not sufficient. Protocol errors may be emitted through the stream or through
+/// [sink].done, depending on their underlying cause. Note that the sink may
+/// silently drop events if the channel closes before [sink].close is called.
+///
+/// Implementations are strongly encouraged to mix in or extend
+/// [StreamChannelMixin] to get default implementations of the various instance
+/// methods. Adding new methods to this interface will not be considered a
+/// breaking change if implementations are also added to [StreamChannelMixin].
+///
+/// Implementations must provide the following guarantees:
+///
+/// * The stream is single-subscription, and must follow all the guarantees of
+/// single-subscription streams.
+///
+/// * Closing the sink causes the stream to close before it emits any more
+/// events.
+///
+/// * After the stream closes, the sink is automatically closed. If this
+/// happens, sink methods should silently drop their arguments until
+/// [sink].close is called.
+///
+/// * If the stream closes before it has a listener, the sink should silently
+/// drop events if possible.
+///
+/// * Canceling the stream's subscription has no effect on the sink. The channel
+/// must still be able to respond to the other endpoint closing the channel
+/// even after the subscription has been canceled.
+///
+/// * The sink *either* forwards errors to the other endpoint *or* closes as
+/// soon as an error is added and forwards that error to the [sink].done
+/// future.
+///
+/// These guarantees allow users to interact uniformly with all implementations,
+/// and ensure that either endpoint closing the stream produces consistent
+/// behavior.
+abstract class StreamChannel<T> {
+ /// The single-subscription stream that emits values from the other endpoint.
+ Stream<T> get stream;
+
+ /// The sink for sending values to the other endpoint.
+ StreamSink<T> get sink;
+
+ /// Creates a new [StreamChannel] that communicates over [stream] and [sink].
+ ///
+ /// Note that this stream/sink pair must provide the guarantees listed in the
+ /// [StreamChannel] documentation. If they don't do so natively,
+ /// [StreamChannel.withGuarantees] should be used instead.
+ factory StreamChannel(Stream<T> stream, StreamSink<T> sink) =>
+ _StreamChannel<T>(stream, sink);
+
+ /// Creates a new [StreamChannel] that communicates over [stream] and [sink].
+ ///
+ /// Unlike [StreamChannel.new], this enforces the guarantees listed in the
+ /// [StreamChannel] documentation. This makes it somewhat less efficient than
+ /// just wrapping a stream and a sink directly, so [StreamChannel.new] should
+ /// be used when the guarantees are provided natively.
+ ///
+ /// If [allowSinkErrors] is `false`, errors are not allowed to be passed to
+ /// [sink]. If any are, the connection will close and the error will be
+ /// forwarded to [sink].done.
+ factory StreamChannel.withGuarantees(Stream<T> stream, StreamSink<T> sink,
+ {bool allowSinkErrors = true}) =>
+ GuaranteeChannel(stream, sink, allowSinkErrors: allowSinkErrors);
+
+ /// Creates a new [StreamChannel] that communicates over [stream] and [sink].
+ ///
+ /// This specifically enforces the second guarantee: closing the sink causes
+ /// the stream to close before it emits any more events. This guarantee is
+ /// invalidated when an asynchronous gap is added between the original
+ /// stream's event dispatch and the returned stream's, for example by
+ /// transforming it with a [StreamTransformer]. This is a lighter-weight way
+ /// of preserving that guarantee in particular than
+ /// [StreamChannel.withGuarantees].
+ factory StreamChannel.withCloseGuarantee(
+ Stream<T> stream, StreamSink<T> sink) =>
+ CloseGuaranteeChannel(stream, sink);
+
+ /// Connects this to [other], so that any values emitted by either are sent
+ /// directly to the other.
+ void pipe(StreamChannel<T> other);
+
+ /// Transforms this using [transformer].
+ ///
+ /// This is identical to calling `transformer.bind(channel)`.
+ StreamChannel<S> transform<S>(StreamChannelTransformer<S, T> transformer);
+
+ /// Transforms only the [stream] component of this using [transformer].
+ StreamChannel<T> transformStream(StreamTransformer<T, T> transformer);
+
+ /// Transforms only the [sink] component of this using [transformer].
+ StreamChannel<T> transformSink(StreamSinkTransformer<T, T> transformer);
+
+ /// Returns a copy of this with [stream] replaced by [change]'s return
+ /// value.
+ StreamChannel<T> changeStream(Stream<T> Function(Stream<T>) change);
+
+ /// Returns a copy of this with [sink] replaced by [change]'s return
+ /// value.
+ StreamChannel<T> changeSink(StreamSink<T> Function(StreamSink<T>) change);
+
+ /// Returns a copy of this with the generic type coerced to [S].
+ ///
+ /// If any events emitted by [stream] aren't of type [S], they're converted
+ /// into [TypeError] events (`CastError` on some SDK versions). Similarly, if
+ /// any events are added to [sink] that aren't of type [S], a [TypeError] is
+ /// thrown.
+ StreamChannel<S> cast<S>();
+}
+
+/// An implementation of [StreamChannel] that simply takes a stream and a sink
+/// as parameters.
+///
+/// This is distinct from [StreamChannel] so that it can use
+/// [StreamChannelMixin].
+class _StreamChannel<T> extends StreamChannelMixin<T> {
+ @override
+ final Stream<T> stream;
+ @override
+ final StreamSink<T> sink;
+
+ _StreamChannel(this.stream, this.sink);
+}
+
+/// A mixin that implements the instance methods of [StreamChannel] in terms of
+/// [stream] and [sink].
+abstract class StreamChannelMixin<T> implements StreamChannel<T> {
+ @override
+ void pipe(StreamChannel<T> other) {
+ stream.pipe(other.sink);
+ other.stream.pipe(sink);
+ }
+
+ @override
+ StreamChannel<S> transform<S>(StreamChannelTransformer<S, T> transformer) =>
+ transformer.bind(this);
+
+ @override
+ StreamChannel<T> transformStream(StreamTransformer<T, T> transformer) =>
+ changeStream(transformer.bind);
+
+ @override
+ StreamChannel<T> transformSink(StreamSinkTransformer<T, T> transformer) =>
+ changeSink(transformer.bind);
+
+ @override
+ StreamChannel<T> changeStream(Stream<T> Function(Stream<T>) change) =>
+ StreamChannel.withCloseGuarantee(change(stream), sink);
+
+ @override
+ StreamChannel<T> changeSink(StreamSink<T> Function(StreamSink<T>) change) =>
+ StreamChannel.withCloseGuarantee(stream, change(sink));
+
+ @override
+ StreamChannel<S> cast<S>() => StreamChannel(
+ stream.cast(), StreamController(sync: true)..stream.cast<T>().pipe(sink));
+}
diff --git a/pkgs/stream_channel/pubspec.yaml b/pkgs/stream_channel/pubspec.yaml
new file mode 100644
index 0000000..eec8c1b
--- /dev/null
+++ b/pkgs/stream_channel/pubspec.yaml
@@ -0,0 +1,16 @@
+name: stream_channel
+version: 2.1.3
+description: >-
+ An abstraction for two-way communication channels based on the Dart Stream
+ class.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/stream_channel
+
+environment:
+ sdk: ^3.3.0
+
+dependencies:
+ async: ^2.5.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
+ test: ^1.16.6
diff --git a/pkgs/stream_channel/test/disconnector_test.dart b/pkgs/stream_channel/test/disconnector_test.dart
new file mode 100644
index 0000000..bbba568
--- /dev/null
+++ b/pkgs/stream_channel/test/disconnector_test.dart
@@ -0,0 +1,152 @@
+// 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:test/test.dart';
+
+void main() {
+ late StreamController streamController;
+ late StreamController sinkController;
+ late Disconnector disconnector;
+ late StreamChannel channel;
+ setUp(() {
+ streamController = StreamController<void>();
+ sinkController = StreamController<void>();
+ disconnector = Disconnector();
+ channel = StreamChannel.withGuarantees(
+ streamController.stream, sinkController.sink)
+ .transform(disconnector);
+ });
+
+ group('before disconnection', () {
+ test('forwards events from the sink as normal', () {
+ channel.sink.add(1);
+ channel.sink.add(2);
+ channel.sink.add(3);
+ channel.sink.close();
+
+ expect(sinkController.stream.toList(), completion(equals([1, 2, 3])));
+ });
+
+ test('forwards events to the stream as normal', () {
+ streamController.add(1);
+ streamController.add(2);
+ streamController.add(3);
+ streamController.close();
+
+ expect(channel.stream.toList(), completion(equals([1, 2, 3])));
+ });
+
+ test("events can't be added when the sink is explicitly closed", () {
+ sinkController.stream.listen(null); // Work around sdk#19095.
+
+ expect(channel.sink.close(), completes);
+ expect(() => channel.sink.add(1), throwsStateError);
+ expect(() => channel.sink.addError('oh no'), throwsStateError);
+ expect(() => channel.sink.addStream(Stream.fromIterable([])),
+ throwsStateError);
+ });
+
+ test("events can't be added while a stream is being added", () {
+ var controller = StreamController<void>();
+ channel.sink.addStream(controller.stream);
+
+ expect(() => channel.sink.add(1), throwsStateError);
+ expect(() => channel.sink.addError('oh no'), throwsStateError);
+ expect(() => channel.sink.addStream(Stream.fromIterable([])),
+ throwsStateError);
+ expect(() => channel.sink.close(), throwsStateError);
+
+ controller.close();
+ });
+ });
+
+ test('cancels addStream when disconnected', () async {
+ var canceled = false;
+ var controller = StreamController<void>(onCancel: () {
+ canceled = true;
+ });
+ expect(channel.sink.addStream(controller.stream), completes);
+ unawaited(disconnector.disconnect());
+
+ await pumpEventQueue();
+ expect(canceled, isTrue);
+ });
+
+ test('disconnect() returns the close future from the inner sink', () async {
+ var streamController = StreamController<void>();
+ var sinkController = StreamController<void>();
+ var disconnector = Disconnector<void>();
+ var sink = _CloseCompleterSink(sinkController.sink);
+ StreamChannel.withGuarantees(streamController.stream, sink)
+ .transform(disconnector);
+
+ var disconnectFutureFired = false;
+ expect(
+ disconnector.disconnect().then((_) {
+ disconnectFutureFired = true;
+ }),
+ completes);
+
+ // Give the future time to fire early if it's going to.
+ await pumpEventQueue();
+ expect(disconnectFutureFired, isFalse);
+
+ // When the inner sink's close future completes, so should the
+ // disconnector's.
+ sink.completer.complete();
+ await pumpEventQueue();
+ expect(disconnectFutureFired, isTrue);
+ });
+
+ group('after disconnection', () {
+ setUp(() {
+ disconnector.disconnect();
+ });
+
+ test('closes the inner sink and ignores events to the outer sink', () {
+ channel.sink.add(1);
+ channel.sink.add(2);
+ channel.sink.add(3);
+ channel.sink.close();
+
+ expect(sinkController.stream.toList(), completion(isEmpty));
+ });
+
+ test('closes the stream', () {
+ expect(channel.stream.toList(), completion(isEmpty));
+ });
+
+ test('completes done', () {
+ sinkController.stream.listen(null); // Work around sdk#19095.
+ expect(channel.sink.done, completes);
+ });
+
+ test('still emits state errors after explicit close', () {
+ sinkController.stream.listen(null); // Work around sdk#19095.
+ expect(channel.sink.close(), completes);
+
+ expect(() => channel.sink.add(1), throwsStateError);
+ expect(() => channel.sink.addError('oh no'), throwsStateError);
+ });
+ });
+}
+
+/// A [StreamSink] wrapper that adds the ability to manually complete the Future
+/// returned by [close] using [completer].
+class _CloseCompleterSink extends DelegatingStreamSink {
+ /// The completer for the future returned by [close].
+ final completer = Completer<void>();
+
+ _CloseCompleterSink(super.inner);
+
+ @override
+ Future<void> close() {
+ super.close();
+ return completer.future;
+ }
+}
diff --git a/pkgs/stream_channel/test/isolate_channel_test.dart b/pkgs/stream_channel/test/isolate_channel_test.dart
new file mode 100644
index 0000000..3a8b42e
--- /dev/null
+++ b/pkgs/stream_channel/test/isolate_channel_test.dart
@@ -0,0 +1,174 @@
+// 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:isolate';
+
+import 'package:stream_channel/isolate_channel.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+ late ReceivePort receivePort;
+ late SendPort sendPort;
+ late StreamChannel channel;
+ setUp(() {
+ receivePort = ReceivePort();
+ var receivePortForSend = ReceivePort();
+ sendPort = receivePortForSend.sendPort;
+ channel = IsolateChannel(receivePortForSend, receivePort.sendPort);
+ });
+
+ tearDown(() {
+ receivePort.close();
+ channel.sink.close();
+ });
+
+ test('the channel can send messages', () {
+ channel.sink.add(1);
+ channel.sink.add(2);
+ channel.sink.add(3);
+
+ expect(receivePort.take(3).toList(), completion(equals([1, 2, 3])));
+ });
+
+ test('the channel can receive messages', () {
+ sendPort.send(1);
+ sendPort.send(2);
+ sendPort.send(3);
+
+ expect(channel.stream.take(3).toList(), completion(equals([1, 2, 3])));
+ });
+
+ test("events can't be added to an explicitly-closed sink", () {
+ expect(channel.sink.close(), completes);
+ expect(() => channel.sink.add(1), throwsStateError);
+ expect(() => channel.sink.addError('oh no'), throwsStateError);
+ expect(() => channel.sink.addStream(Stream.fromIterable([])),
+ throwsStateError);
+ });
+
+ test("events can't be added while a stream is being added", () {
+ var controller = StreamController<void>();
+ channel.sink.addStream(controller.stream);
+
+ expect(() => channel.sink.add(1), throwsStateError);
+ expect(() => channel.sink.addError('oh no'), throwsStateError);
+ expect(() => channel.sink.addStream(Stream.fromIterable([])),
+ throwsStateError);
+ expect(() => channel.sink.close(), throwsStateError);
+
+ controller.close();
+ });
+
+ group('stream channel rules', () {
+ test(
+ 'closing the sink causes the stream to close before it emits any more '
+ 'events', () {
+ sendPort.send(1);
+ sendPort.send(2);
+ sendPort.send(3);
+ sendPort.send(4);
+ sendPort.send(5);
+
+ channel.stream.listen(expectAsync1((message) {
+ expect(message, equals(1));
+ channel.sink.close();
+ }, count: 1));
+ });
+
+ test("cancelling the stream's subscription has no effect on the sink",
+ () async {
+ unawaited(channel.stream.listen(null).cancel());
+ await pumpEventQueue();
+
+ channel.sink.add(1);
+ channel.sink.add(2);
+ channel.sink.add(3);
+ expect(receivePort.take(3).toList(), completion(equals([1, 2, 3])));
+ });
+
+ test('the sink closes as soon as an error is added', () async {
+ channel.sink.addError('oh no');
+ channel.sink.add(1);
+ expect(channel.sink.done, throwsA('oh no'));
+
+ // Since the sink is closed, the stream should also be closed.
+ expect(channel.stream.isEmpty, completion(isTrue));
+
+ // The other end shouldn't receive the next event, since the sink was
+ // closed. Pump the event queue to give it a chance to.
+ receivePort.listen(expectAsync1((_) {}, count: 0));
+ await pumpEventQueue();
+ });
+
+ test('the sink closes as soon as an error is added via addStream',
+ () async {
+ var canceled = false;
+ var controller = StreamController<void>(onCancel: () {
+ canceled = true;
+ });
+
+ // This future shouldn't get the error, because it's sent to [Sink.done].
+ expect(channel.sink.addStream(controller.stream), completes);
+
+ controller.addError('oh no');
+ expect(channel.sink.done, throwsA('oh no'));
+ await pumpEventQueue();
+ expect(canceled, isTrue);
+
+ // Even though the sink is closed, this shouldn't throw an error because
+ // the user didn't explicitly close it.
+ channel.sink.add(1);
+ });
+ });
+
+ group('connect constructors', () {
+ late ReceivePort connectPort;
+ setUp(() {
+ connectPort = ReceivePort();
+ });
+
+ tearDown(() {
+ connectPort.close();
+ });
+
+ test('create a connected pair of channels', () async {
+ var channel1 = IsolateChannel<int>.connectReceive(connectPort);
+ var channel2 = IsolateChannel<int>.connectSend(connectPort.sendPort);
+
+ channel1.sink.add(1);
+ channel1.sink.add(2);
+ channel1.sink.add(3);
+ expect(await channel2.stream.take(3).toList(), equals([1, 2, 3]));
+
+ channel2.sink.add(4);
+ channel2.sink.add(5);
+ channel2.sink.add(6);
+ expect(await channel1.stream.take(3).toList(), equals([4, 5, 6]));
+
+ await channel2.sink.close();
+ });
+
+ test('the receiving channel produces an error if it gets the wrong message',
+ () {
+ var connectedChannel = IsolateChannel<int>.connectReceive(connectPort);
+ connectPort.sendPort.send('wrong value');
+
+ expect(connectedChannel.stream.toList(), throwsStateError);
+ expect(connectedChannel.sink.done, completes);
+ });
+
+ test('the receiving channel closes gracefully without a connection',
+ () async {
+ var connectedChannel = IsolateChannel<int>.connectReceive(connectPort);
+ await connectedChannel.sink.close();
+ await expectLater(connectedChannel.stream.toList(), completion(isEmpty));
+ await expectLater(connectedChannel.sink.done, completes);
+ });
+ });
+}
diff --git a/pkgs/stream_channel/test/json_document_transformer_test.dart b/pkgs/stream_channel/test/json_document_transformer_test.dart
new file mode 100644
index 0000000..290c4e2
--- /dev/null
+++ b/pkgs/stream_channel/test/json_document_transformer_test.dart
@@ -0,0 +1,46 @@
+// 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:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+ late StreamController<String> streamController;
+ late StreamController<String> sinkController;
+ late StreamChannel<String> channel;
+ setUp(() {
+ streamController = StreamController<String>();
+ sinkController = StreamController<String>();
+ channel =
+ StreamChannel<String>(streamController.stream, sinkController.sink);
+ });
+
+ test('decodes JSON emitted by the channel', () {
+ var transformed = channel.transform(jsonDocument);
+ streamController.add('{"foo": "bar"}');
+ expect(transformed.stream.first, completion(equals({'foo': 'bar'})));
+ });
+
+ test('encodes objects added to the channel', () {
+ var transformed = channel.transform(jsonDocument);
+ transformed.sink.add({'foo': 'bar'});
+ expect(sinkController.stream.first,
+ completion(equals(jsonEncode({'foo': 'bar'}))));
+ });
+
+ test('emits a stream error when incoming JSON is malformed', () {
+ var transformed = channel.transform(jsonDocument);
+ streamController.add('{invalid');
+ expect(transformed.stream.first, throwsFormatException);
+ });
+
+ test('synchronously throws if an unencodable object is added', () {
+ var transformed = channel.transform(jsonDocument);
+ expect(() => transformed.sink.add(Object()),
+ throwsA(const TypeMatcher<JsonUnsupportedObjectError>()));
+ });
+}
diff --git a/pkgs/stream_channel/test/multi_channel_test.dart b/pkgs/stream_channel/test/multi_channel_test.dart
new file mode 100644
index 0000000..ee6f8d2
--- /dev/null
+++ b/pkgs/stream_channel/test/multi_channel_test.dart
@@ -0,0 +1,478 @@
+// 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:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+ late StreamChannelController controller;
+ late MultiChannel channel1;
+ late MultiChannel channel2;
+ setUp(() {
+ controller = StreamChannelController();
+ channel1 = MultiChannel<int>(controller.local);
+ channel2 = MultiChannel<int>(controller.foreign);
+ });
+
+ group('the default virtual channel', () {
+ test('begins connected', () {
+ var first = true;
+ channel2.stream.listen(expectAsync1((message) {
+ if (first) {
+ expect(message, equals(1));
+ first = false;
+ } else {
+ expect(message, equals(2));
+ }
+ }, count: 2));
+
+ channel1.sink.add(1);
+ channel1.sink.add(2);
+ });
+
+ test('closes the remote virtual channel when it closes', () {
+ expect(channel2.stream.toList(), completion(isEmpty));
+ expect(channel2.sink.done, completes);
+
+ channel1.sink.close();
+ });
+
+ test('closes the local virtual channel when it closes', () {
+ expect(channel1.stream.toList(), completion(isEmpty));
+ expect(channel1.sink.done, completes);
+
+ channel1.sink.close();
+ });
+
+ test(
+ "doesn't closes the local virtual channel when the stream "
+ 'subscription is canceled', () {
+ channel1.sink.done.then(expectAsync1((_) {}, count: 0));
+
+ channel1.stream.listen((_) {}).cancel();
+
+ // Ensure that there's enough time for the channel to close if it's going
+ // to.
+ return pumpEventQueue();
+ });
+
+ test(
+ 'closes the underlying channel when it closes without any other '
+ 'virtual channels', () {
+ expect(controller.local.sink.done, completes);
+ expect(controller.foreign.sink.done, completes);
+
+ channel1.sink.close();
+ });
+
+ test(
+ "doesn't close the underlying channel when it closes with other "
+ 'virtual channels', () {
+ controller.local.sink.done.then(expectAsync1((_) {}, count: 0));
+ controller.foreign.sink.done.then(expectAsync1((_) {}, count: 0));
+
+ // Establish another virtual connection which should keep the underlying
+ // connection open.
+ channel2.virtualChannel(channel1.virtualChannel().id);
+ channel1.sink.close();
+
+ // Ensure that there's enough time for the underlying channel to complete
+ // if it's going to.
+ return pumpEventQueue();
+ });
+ });
+
+ group('a locally-created virtual channel', () {
+ late VirtualChannel virtual1;
+ late VirtualChannel virtual2;
+ setUp(() {
+ virtual1 = channel1.virtualChannel();
+ virtual2 = channel2.virtualChannel(virtual1.id);
+ });
+
+ test('sends messages only to the other virtual channel', () {
+ var first = true;
+ virtual2.stream.listen(expectAsync1((message) {
+ if (first) {
+ expect(message, equals(1));
+ first = false;
+ } else {
+ expect(message, equals(2));
+ }
+ }, count: 2));
+
+ // No other virtual channels should receive the message.
+ for (var i = 0; i < 10; i++) {
+ var virtual = channel2.virtualChannel(channel1.virtualChannel().id);
+ virtual.stream.listen(expectAsync1((_) {}, count: 0));
+ }
+ channel2.stream.listen(expectAsync1((_) {}, count: 0));
+
+ virtual1.sink.add(1);
+ virtual1.sink.add(2);
+ });
+
+ test('closes the remote virtual channel when it closes', () {
+ expect(virtual2.stream.toList(), completion(isEmpty));
+ expect(virtual2.sink.done, completes);
+
+ virtual1.sink.close();
+ });
+
+ test('closes the local virtual channel when it closes', () {
+ expect(virtual1.stream.toList(), completion(isEmpty));
+ expect(virtual1.sink.done, completes);
+
+ virtual1.sink.close();
+ });
+
+ test(
+ "doesn't closes the local virtual channel when the stream "
+ 'subscription is canceled', () {
+ virtual1.sink.done.then(expectAsync1((_) {}, count: 0));
+ virtual1.stream.listen((_) {}).cancel();
+
+ // Ensure that there's enough time for the channel to close if it's going
+ // to.
+ return pumpEventQueue();
+ });
+
+ test(
+ 'closes the underlying channel when it closes without any other '
+ 'virtual channels', () async {
+ // First close the default channel so we can test the new channel as the
+ // last living virtual channel.
+ unawaited(channel1.sink.close());
+
+ await channel2.stream.toList();
+ expect(controller.local.sink.done, completes);
+ expect(controller.foreign.sink.done, completes);
+
+ unawaited(virtual1.sink.close());
+ });
+
+ test(
+ "doesn't close the underlying channel when it closes with other "
+ 'virtual channels', () {
+ controller.local.sink.done.then(expectAsync1((_) {}, count: 0));
+ controller.foreign.sink.done.then(expectAsync1((_) {}, count: 0));
+
+ virtual1.sink.close();
+
+ // Ensure that there's enough time for the underlying channel to complete
+ // if it's going to.
+ return pumpEventQueue();
+ });
+
+ test("doesn't conflict with a remote virtual channel", () {
+ var virtual3 = channel2.virtualChannel();
+ var virtual4 = channel1.virtualChannel(virtual3.id);
+
+ // This is an implementation detail, but we assert it here to make sure
+ // we're properly testing two channels with the same id.
+ expect(virtual1.id, equals(virtual3.id));
+
+ virtual2.stream
+ .listen(expectAsync1((message) => expect(message, equals(1))));
+ virtual4.stream
+ .listen(expectAsync1((message) => expect(message, equals(2))));
+
+ virtual1.sink.add(1);
+ virtual3.sink.add(2);
+ });
+ });
+
+ group('a remotely-created virtual channel', () {
+ late VirtualChannel virtual1;
+ late VirtualChannel virtual2;
+ setUp(() {
+ virtual1 = channel1.virtualChannel();
+ virtual2 = channel2.virtualChannel(virtual1.id);
+ });
+
+ test('sends messages only to the other virtual channel', () {
+ var first = true;
+ virtual1.stream.listen(expectAsync1((message) {
+ if (first) {
+ expect(message, equals(1));
+ first = false;
+ } else {
+ expect(message, equals(2));
+ }
+ }, count: 2));
+
+ // No other virtual channels should receive the message.
+ for (var i = 0; i < 10; i++) {
+ var virtual = channel2.virtualChannel(channel1.virtualChannel().id);
+ virtual.stream.listen(expectAsync1((_) {}, count: 0));
+ }
+ channel1.stream.listen(expectAsync1((_) {}, count: 0));
+
+ virtual2.sink.add(1);
+ virtual2.sink.add(2);
+ });
+
+ test('closes the remote virtual channel when it closes', () {
+ expect(virtual1.stream.toList(), completion(isEmpty));
+ expect(virtual1.sink.done, completes);
+
+ virtual2.sink.close();
+ });
+
+ test('closes the local virtual channel when it closes', () {
+ expect(virtual2.stream.toList(), completion(isEmpty));
+ expect(virtual2.sink.done, completes);
+
+ virtual2.sink.close();
+ });
+
+ test(
+ "doesn't closes the local virtual channel when the stream "
+ 'subscription is canceled', () {
+ virtual2.sink.done.then(expectAsync1((_) {}, count: 0));
+ virtual2.stream.listen((_) {}).cancel();
+
+ // Ensure that there's enough time for the channel to close if it's going
+ // to.
+ return pumpEventQueue();
+ });
+
+ test(
+ 'closes the underlying channel when it closes without any other '
+ 'virtual channels', () async {
+ // First close the default channel so we can test the new channel as the
+ // last living virtual channel.
+ unawaited(channel2.sink.close());
+
+ await channel1.stream.toList();
+ expect(controller.local.sink.done, completes);
+ expect(controller.foreign.sink.done, completes);
+
+ unawaited(virtual2.sink.close());
+ });
+
+ test(
+ "doesn't close the underlying channel when it closes with other "
+ 'virtual channels', () {
+ controller.local.sink.done.then(expectAsync1((_) {}, count: 0));
+ controller.foreign.sink.done.then(expectAsync1((_) {}, count: 0));
+
+ virtual2.sink.close();
+
+ // Ensure that there's enough time for the underlying channel to complete
+ // if it's going to.
+ return pumpEventQueue();
+ });
+
+ test("doesn't allow another virtual channel with the same id", () {
+ expect(() => channel2.virtualChannel(virtual1.id), throwsArgumentError);
+ });
+
+ test('dispatches events received before the virtual channel is created',
+ () async {
+ virtual1 = channel1.virtualChannel();
+
+ virtual1.sink.add(1);
+ await pumpEventQueue();
+
+ virtual1.sink.add(2);
+ await pumpEventQueue();
+
+ expect(channel2.virtualChannel(virtual1.id).stream, emitsInOrder([1, 2]));
+ });
+
+ test(
+ 'dispatches close events received before the virtual channel is '
+ 'created', () async {
+ virtual1 = channel1.virtualChannel();
+
+ unawaited(virtual1.sink.close());
+ await pumpEventQueue();
+
+ expect(channel2.virtualChannel(virtual1.id).stream.toList(),
+ completion(isEmpty));
+ });
+ });
+
+ group('when the underlying stream', () {
+ late VirtualChannel virtual1;
+ late VirtualChannel virtual2;
+ setUp(() {
+ virtual1 = channel1.virtualChannel();
+ virtual2 = channel2.virtualChannel(virtual1.id);
+ });
+
+ test('closes, all virtual channels close', () {
+ expect(channel1.stream.toList(), completion(isEmpty));
+ expect(channel1.sink.done, completes);
+ expect(channel2.stream.toList(), completion(isEmpty));
+ expect(channel2.sink.done, completes);
+ expect(virtual1.stream.toList(), completion(isEmpty));
+ expect(virtual1.sink.done, completes);
+ expect(virtual2.stream.toList(), completion(isEmpty));
+ expect(virtual2.sink.done, completes);
+
+ controller.local.sink.close();
+ });
+
+ test('closes, more virtual channels are created closed', () async {
+ unawaited(channel2.sink.close());
+ unawaited(virtual2.sink.close());
+
+ // Wait for the existing channels to emit done events.
+ await channel1.stream.toList();
+ await virtual1.stream.toList();
+
+ var virtual = channel1.virtualChannel();
+ expect(virtual.stream.toList(), completion(isEmpty));
+ expect(virtual.sink.done, completes);
+
+ virtual = channel1.virtualChannel();
+ expect(virtual.stream.toList(), completion(isEmpty));
+ expect(virtual.sink.done, completes);
+ });
+
+ test('emits an error, the error is sent only to the default channel', () {
+ channel1.stream.listen(expectAsync1((_) {}, count: 0),
+ onError: expectAsync1((error) => expect(error, equals('oh no'))));
+ virtual1.stream.listen(expectAsync1((_) {}, count: 0),
+ onError: expectAsync1((_) {}, count: 0));
+
+ controller.foreign.sink.addError('oh no');
+ });
+ });
+
+ group('stream channel rules', () {
+ group('for the main stream:', () {
+ test(
+ 'closing the sink causes the stream to close before it emits any '
+ 'more events', () {
+ channel1.sink.add(1);
+ channel1.sink.add(2);
+ channel1.sink.add(3);
+
+ channel2.stream.listen(expectAsync1((message) {
+ expect(message, equals(1));
+ channel2.sink.close();
+ }, count: 1));
+ });
+
+ test('after the stream closes, the sink ignores events', () async {
+ unawaited(channel1.sink.close());
+
+ // Wait for the done event to be delivered.
+ await channel2.stream.toList();
+ channel2.sink.add(1);
+ channel2.sink.add(2);
+ channel2.sink.add(3);
+ unawaited(channel2.sink.close());
+
+ // None of our channel.sink additions should make it to the other
+ // endpoint.
+ channel1.stream.listen(expectAsync1((_) {}, count: 0));
+ await pumpEventQueue();
+ });
+
+ test("canceling the stream's subscription has no effect on the sink",
+ () async {
+ unawaited(channel1.stream.listen(null).cancel());
+ await pumpEventQueue();
+
+ channel1.sink.add(1);
+ channel1.sink.add(2);
+ channel1.sink.add(3);
+ unawaited(channel1.sink.close());
+ expect(channel2.stream.toList(), completion(equals([1, 2, 3])));
+ });
+
+ test("canceling the stream's subscription doesn't stop a done event",
+ () async {
+ unawaited(channel1.stream.listen(null).cancel());
+ await pumpEventQueue();
+
+ unawaited(channel2.sink.close());
+ await pumpEventQueue();
+
+ channel1.sink.add(1);
+ channel1.sink.add(2);
+ channel1.sink.add(3);
+ unawaited(channel1.sink.close());
+
+ // The sink should be ignoring events because the channel closed.
+ channel2.stream.listen(expectAsync1((_) {}, count: 0));
+ await pumpEventQueue();
+ });
+ });
+
+ group('for a virtual channel:', () {
+ late VirtualChannel virtual1;
+ late VirtualChannel virtual2;
+ setUp(() {
+ virtual1 = channel1.virtualChannel();
+ virtual2 = channel2.virtualChannel(virtual1.id);
+ });
+
+ test(
+ 'closing the sink causes the stream to close before it emits any '
+ 'more events', () {
+ virtual1.sink.add(1);
+ virtual1.sink.add(2);
+ virtual1.sink.add(3);
+
+ virtual2.stream.listen(expectAsync1((message) {
+ expect(message, equals(1));
+ virtual2.sink.close();
+ }, count: 1));
+ });
+
+ test('after the stream closes, the sink ignores events', () async {
+ unawaited(virtual1.sink.close());
+
+ // Wait for the done event to be delivered.
+ await virtual2.stream.toList();
+ virtual2.sink.add(1);
+ virtual2.sink.add(2);
+ virtual2.sink.add(3);
+ unawaited(virtual2.sink.close());
+
+ // None of our virtual.sink additions should make it to the other
+ // endpoint.
+ virtual1.stream.listen(expectAsync1((_) {}, count: 0));
+ await pumpEventQueue();
+ });
+
+ test("canceling the stream's subscription has no effect on the sink",
+ () async {
+ unawaited(virtual1.stream.listen(null).cancel());
+ await pumpEventQueue();
+
+ virtual1.sink.add(1);
+ virtual1.sink.add(2);
+ virtual1.sink.add(3);
+ unawaited(virtual1.sink.close());
+ expect(virtual2.stream.toList(), completion(equals([1, 2, 3])));
+ });
+
+ test("canceling the stream's subscription doesn't stop a done event",
+ () async {
+ unawaited(virtual1.stream.listen(null).cancel());
+ await pumpEventQueue();
+
+ unawaited(virtual2.sink.close());
+ await pumpEventQueue();
+
+ virtual1.sink.add(1);
+ virtual1.sink.add(2);
+ virtual1.sink.add(3);
+ unawaited(virtual1.sink.close());
+
+ // The sink should be ignoring events because the stream closed.
+ virtual2.stream.listen(expectAsync1((_) {}, count: 0));
+ await pumpEventQueue();
+ });
+ });
+ });
+}
diff --git a/pkgs/stream_channel/test/stream_channel_completer_test.dart b/pkgs/stream_channel/test/stream_channel_completer_test.dart
new file mode 100644
index 0000000..c6fddc0
--- /dev/null
+++ b/pkgs/stream_channel/test/stream_channel_completer_test.dart
@@ -0,0 +1,120 @@
+// 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:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+ late StreamChannelCompleter completer;
+ late StreamController streamController;
+ late StreamController sinkController;
+ late StreamChannel innerChannel;
+ setUp(() {
+ completer = StreamChannelCompleter();
+ streamController = StreamController<void>();
+ sinkController = StreamController<void>();
+ innerChannel = StreamChannel(streamController.stream, sinkController.sink);
+ });
+
+ group('when a channel is set before accessing', () {
+ test('forwards events through the stream', () {
+ completer.setChannel(innerChannel);
+ expect(completer.channel.stream.toList(), completion(equals([1, 2, 3])));
+
+ streamController.add(1);
+ streamController.add(2);
+ streamController.add(3);
+ streamController.close();
+ });
+
+ test('forwards events through the sink', () {
+ completer.setChannel(innerChannel);
+ expect(sinkController.stream.toList(), completion(equals([1, 2, 3])));
+
+ completer.channel.sink.add(1);
+ completer.channel.sink.add(2);
+ completer.channel.sink.add(3);
+ completer.channel.sink.close();
+ });
+
+ test('forwards an error through the stream', () {
+ completer.setError('oh no');
+ expect(completer.channel.stream.first, throwsA('oh no'));
+ });
+
+ test('drops sink events', () {
+ completer.setError('oh no');
+ expect(completer.channel.sink.done, completes);
+ completer.channel.sink.add(1);
+ completer.channel.sink.addError('oh no');
+ });
+ });
+
+ group('when a channel is set after accessing', () {
+ test('forwards events through the stream', () async {
+ expect(completer.channel.stream.toList(), completion(equals([1, 2, 3])));
+ await pumpEventQueue();
+
+ completer.setChannel(innerChannel);
+ streamController.add(1);
+ streamController.add(2);
+ streamController.add(3);
+ unawaited(streamController.close());
+ });
+
+ test('forwards events through the sink', () async {
+ completer.channel.sink.add(1);
+ completer.channel.sink.add(2);
+ completer.channel.sink.add(3);
+ unawaited(completer.channel.sink.close());
+ await pumpEventQueue();
+
+ completer.setChannel(innerChannel);
+ expect(sinkController.stream.toList(), completion(equals([1, 2, 3])));
+ });
+
+ test('forwards an error through the stream', () async {
+ expect(completer.channel.stream.first, throwsA('oh no'));
+ await pumpEventQueue();
+
+ completer.setError('oh no');
+ });
+
+ test('drops sink events', () async {
+ expect(completer.channel.sink.done, completes);
+ completer.channel.sink.add(1);
+ completer.channel.sink.addError('oh no');
+ await pumpEventQueue();
+
+ completer.setError('oh no');
+ });
+ });
+
+ group('forFuture', () {
+ test('forwards a StreamChannel', () {
+ var channel =
+ StreamChannelCompleter.fromFuture(Future.value(innerChannel));
+ channel.sink.add(1);
+ channel.sink.close();
+ streamController.sink.add(2);
+ streamController.sink.close();
+
+ expect(sinkController.stream.toList(), completion(equals([1])));
+ expect(channel.stream.toList(), completion(equals([2])));
+ });
+
+ test('forwards an error', () {
+ var channel = StreamChannelCompleter.fromFuture(Future.error('oh no'));
+ expect(channel.stream.toList(), throwsA('oh no'));
+ });
+ });
+
+ test("doesn't allow the channel to be set multiple times", () {
+ completer.setChannel(innerChannel);
+ expect(() => completer.setChannel(innerChannel), throwsStateError);
+ expect(() => completer.setChannel(innerChannel), throwsStateError);
+ });
+}
diff --git a/pkgs/stream_channel/test/stream_channel_controller_test.dart b/pkgs/stream_channel/test/stream_channel_controller_test.dart
new file mode 100644
index 0000000..3d661e3
--- /dev/null
+++ b/pkgs/stream_channel/test/stream_channel_controller_test.dart
@@ -0,0 +1,104 @@
+// 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/test.dart';
+
+void main() {
+ group('asynchronously', () {
+ late StreamChannelController controller;
+ setUp(() {
+ controller = StreamChannelController();
+ });
+
+ test('forwards events from the local sink to the foreign stream', () {
+ controller.local.sink
+ ..add(1)
+ ..add(2)
+ ..add(3)
+ ..close();
+ expect(controller.foreign.stream.toList(), completion(equals([1, 2, 3])));
+ });
+
+ test('forwards events from the foreign sink to the local stream', () {
+ controller.foreign.sink
+ ..add(1)
+ ..add(2)
+ ..add(3)
+ ..close();
+ expect(controller.local.stream.toList(), completion(equals([1, 2, 3])));
+ });
+
+ test(
+ 'with allowForeignErrors: false, shuts down the connection if an '
+ 'error is added to the foreign channel', () {
+ controller = StreamChannelController(allowForeignErrors: false);
+
+ controller.foreign.sink.addError('oh no');
+ expect(controller.foreign.sink.done, throwsA('oh no'));
+ expect(controller.foreign.stream.toList(), completion(isEmpty));
+ expect(controller.local.sink.done, completes);
+ expect(controller.local.stream.toList(), completion(isEmpty));
+ });
+ });
+
+ group('synchronously', () {
+ late StreamChannelController controller;
+ setUp(() {
+ controller = StreamChannelController(sync: true);
+ });
+
+ test(
+ 'synchronously forwards events from the local sink to the foreign '
+ 'stream', () {
+ var receivedEvent = false;
+ var receivedError = false;
+ var receivedDone = false;
+ controller.foreign.stream.listen(expectAsync1((event) {
+ expect(event, equals(1));
+ receivedEvent = true;
+ }), onError: expectAsync1((error) {
+ expect(error, equals('oh no'));
+ receivedError = true;
+ }), onDone: expectAsync0(() {
+ receivedDone = true;
+ }));
+
+ controller.local.sink.add(1);
+ expect(receivedEvent, isTrue);
+
+ controller.local.sink.addError('oh no');
+ expect(receivedError, isTrue);
+
+ controller.local.sink.close();
+ expect(receivedDone, isTrue);
+ });
+
+ test(
+ 'synchronously forwards events from the foreign sink to the local '
+ 'stream', () {
+ var receivedEvent = false;
+ var receivedError = false;
+ var receivedDone = false;
+ controller.local.stream.listen(expectAsync1((event) {
+ expect(event, equals(1));
+ receivedEvent = true;
+ }), onError: expectAsync1((error) {
+ expect(error, equals('oh no'));
+ receivedError = true;
+ }), onDone: expectAsync0(() {
+ receivedDone = true;
+ }));
+
+ controller.foreign.sink.add(1);
+ expect(receivedEvent, isTrue);
+
+ controller.foreign.sink.addError('oh no');
+ expect(receivedError, isTrue);
+
+ controller.foreign.sink.close();
+ expect(receivedDone, isTrue);
+ });
+ });
+}
diff --git a/pkgs/stream_channel/test/stream_channel_test.dart b/pkgs/stream_channel/test/stream_channel_test.dart
new file mode 100644
index 0000000..c44b6ab
--- /dev/null
+++ b/pkgs/stream_channel/test/stream_channel_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.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:async/async.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test("pipe() pipes data from each channel's stream into the other's sink",
+ () {
+ var otherStreamController = StreamController<int>();
+ var otherSinkController = StreamController<int>();
+ var otherChannel =
+ StreamChannel(otherStreamController.stream, otherSinkController.sink);
+
+ var streamController = StreamController<int>();
+ var sinkController = StreamController<int>();
+ var channel = StreamChannel(streamController.stream, sinkController.sink);
+
+ channel.pipe(otherChannel);
+
+ streamController.add(1);
+ streamController.add(2);
+ streamController.add(3);
+ streamController.close();
+ expect(otherSinkController.stream.toList(), completion(equals([1, 2, 3])));
+
+ otherStreamController.add(4);
+ otherStreamController.add(5);
+ otherStreamController.add(6);
+ otherStreamController.close();
+ expect(sinkController.stream.toList(), completion(equals([4, 5, 6])));
+ });
+
+ test('transform() transforms the channel', () async {
+ var streamController = StreamController<List<int>>();
+ var sinkController = StreamController<List<int>>();
+ var channel = StreamChannel(streamController.stream, sinkController.sink);
+
+ var transformed = channel
+ .cast<List<int>>()
+ .transform(StreamChannelTransformer.fromCodec(utf8));
+
+ streamController.add([102, 111, 111, 98, 97, 114]);
+ unawaited(streamController.close());
+ expect(await transformed.stream.toList(), equals(['foobar']));
+
+ transformed.sink.add('fblthp');
+ unawaited(transformed.sink.close());
+ expect(
+ sinkController.stream.toList(),
+ completion(equals([
+ [102, 98, 108, 116, 104, 112]
+ ])));
+ });
+
+ test('transformStream() transforms only the stream', () async {
+ var streamController = StreamController<String>();
+ var sinkController = StreamController<String>();
+ var channel = StreamChannel(streamController.stream, sinkController.sink);
+
+ var transformed =
+ channel.cast<String>().transformStream(const LineSplitter());
+
+ streamController.add('hello world');
+ streamController.add(' what\nis');
+ streamController.add('\nup');
+ unawaited(streamController.close());
+ expect(await transformed.stream.toList(),
+ equals(['hello world what', 'is', 'up']));
+
+ transformed.sink.add('fbl\nthp');
+ unawaited(transformed.sink.close());
+ expect(sinkController.stream.toList(), completion(equals(['fbl\nthp'])));
+ });
+
+ test('transformSink() transforms only the sink', () async {
+ var streamController = StreamController<String>();
+ var sinkController = StreamController<String>();
+ var channel = StreamChannel(streamController.stream, sinkController.sink);
+
+ var transformed = channel.cast<String>().transformSink(
+ const StreamSinkTransformer.fromStreamTransformer(LineSplitter()));
+
+ streamController.add('fbl\nthp');
+ unawaited(streamController.close());
+ expect(await transformed.stream.toList(), equals(['fbl\nthp']));
+
+ transformed.sink.add('hello world');
+ transformed.sink.add(' what\nis');
+ transformed.sink.add('\nup');
+ unawaited(transformed.sink.close());
+ expect(sinkController.stream.toList(),
+ completion(equals(['hello world what', 'is', 'up'])));
+ });
+
+ test('changeStream() changes the stream', () {
+ var streamController = StreamController<int>();
+ var sinkController = StreamController<int>();
+ var channel = StreamChannel(streamController.stream, sinkController.sink);
+
+ var newController = StreamController<int>();
+ var changed = channel.changeStream((stream) {
+ expect(stream, equals(channel.stream));
+ return newController.stream;
+ });
+
+ newController.add(10);
+ newController.close();
+
+ streamController.add(20);
+ streamController.close();
+
+ expect(changed.stream.toList(), completion(equals([10])));
+ });
+
+ test('changeSink() changes the sink', () {
+ var streamController = StreamController<int>();
+ var sinkController = StreamController<int>();
+ var channel = StreamChannel(streamController.stream, sinkController.sink);
+
+ var newController = StreamController<int>();
+ var changed = channel.changeSink((sink) {
+ expect(sink, equals(channel.sink));
+ return newController.sink;
+ });
+
+ expect(newController.stream.toList(), completion(equals([10])));
+ streamController.stream.listen(expectAsync1((_) {}, count: 0));
+
+ changed.sink.add(10);
+ changed.sink.close();
+ });
+}
diff --git a/pkgs/stream_channel/test/with_close_guarantee_test.dart b/pkgs/stream_channel/test/with_close_guarantee_test.dart
new file mode 100644
index 0000000..9c0b729
--- /dev/null
+++ b/pkgs/stream_channel/test/with_close_guarantee_test.dart
@@ -0,0 +1,69 @@
+// 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:test/test.dart';
+
+final _delayTransformer = StreamTransformer.fromHandlers(
+ handleData: (data, sink) => Future.microtask(() => sink.add(data)),
+ handleDone: (sink) => Future.microtask(() => sink.close()));
+
+final _delaySinkTransformer =
+ StreamSinkTransformer.fromStreamTransformer(_delayTransformer);
+
+void main() {
+ late StreamChannelController controller;
+ late StreamChannel channel;
+ setUp(() {
+ controller = StreamChannelController();
+
+ // Add a bunch of layers of asynchronous dispatch between the channel and
+ // the underlying controllers.
+ var stream = controller.foreign.stream;
+ var sink = controller.foreign.sink;
+ for (var i = 0; i < 10; i++) {
+ stream = stream.transform(_delayTransformer);
+ sink = _delaySinkTransformer.bind(sink);
+ }
+
+ channel = StreamChannel.withCloseGuarantee(stream, sink);
+ });
+
+ test(
+ 'closing the event sink causes the stream to close before it emits any '
+ 'more events', () async {
+ controller.local.sink.add(1);
+ controller.local.sink.add(2);
+ controller.local.sink.add(3);
+
+ expect(
+ channel.stream
+ .listen(expectAsync1((event) {
+ if (event == 2) channel.sink.close();
+ }, count: 2))
+ .asFuture<void>(),
+ completes);
+
+ await pumpEventQueue();
+ });
+
+ test(
+ 'closing the event sink before events are emitted causes the stream to '
+ 'close immediately', () async {
+ unawaited(channel.sink.close());
+ channel.stream.listen(expectAsync1((_) {}, count: 0),
+ onError: expectAsync2((_, __) {}, count: 0),
+ onDone: expectAsync0(() {}));
+
+ controller.local.sink.add(1);
+ controller.local.sink.add(2);
+ controller.local.sink.add(3);
+ unawaited(controller.local.sink.close());
+
+ await pumpEventQueue();
+ });
+}
diff --git a/pkgs/stream_channel/test/with_guarantees_test.dart b/pkgs/stream_channel/test/with_guarantees_test.dart
new file mode 100644
index 0000000..f026079
--- /dev/null
+++ b/pkgs/stream_channel/test/with_guarantees_test.dart
@@ -0,0 +1,200 @@
+// 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:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+void main() {
+ late StreamController streamController;
+ late StreamController sinkController;
+ late StreamChannel channel;
+ setUp(() {
+ streamController = StreamController<void>();
+ sinkController = StreamController<void>();
+ channel = StreamChannel.withGuarantees(
+ streamController.stream, sinkController.sink);
+ });
+
+ group('with a broadcast stream', () {
+ setUp(() {
+ streamController = StreamController.broadcast();
+ channel = StreamChannel.withGuarantees(
+ streamController.stream, sinkController.sink);
+ });
+
+ test('buffers events', () async {
+ streamController.add(1);
+ streamController.add(2);
+ streamController.add(3);
+ await pumpEventQueue();
+
+ expect(channel.stream.toList(), completion(equals([1, 2, 3])));
+ unawaited(streamController.close());
+ });
+
+ test('only allows a single subscription', () {
+ channel.stream.listen(null);
+ expect(() => channel.stream.listen(null), throwsStateError);
+ });
+ });
+
+ test(
+ 'closing the event sink causes the stream to close before it emits any '
+ 'more events', () {
+ streamController.add(1);
+ streamController.add(2);
+ streamController.add(3);
+
+ expect(
+ channel.stream
+ .listen(expectAsync1((event) {
+ if (event == 2) channel.sink.close();
+ }, count: 2))
+ .asFuture<void>(),
+ completes);
+ });
+
+ test('after the stream closes, the sink ignores events', () async {
+ unawaited(streamController.close());
+
+ // Wait for the done event to be delivered.
+ await channel.stream.toList();
+ channel.sink.add(1);
+ channel.sink.add(2);
+ channel.sink.add(3);
+ unawaited(channel.sink.close());
+
+ // None of our channel.sink additions should make it to the other endpoint.
+ sinkController.stream.listen(expectAsync1((_) {}, count: 0),
+ onDone: expectAsync0(() {}, count: 0));
+ await pumpEventQueue();
+ });
+
+ test("canceling the stream's subscription has no effect on the sink",
+ () async {
+ unawaited(channel.stream.listen(null).cancel());
+ await pumpEventQueue();
+
+ channel.sink.add(1);
+ channel.sink.add(2);
+ channel.sink.add(3);
+ unawaited(channel.sink.close());
+ expect(sinkController.stream.toList(), completion(equals([1, 2, 3])));
+ });
+
+ test("canceling the stream's subscription doesn't stop a done event",
+ () async {
+ unawaited(channel.stream.listen(null).cancel());
+ await pumpEventQueue();
+
+ unawaited(streamController.close());
+ await pumpEventQueue();
+
+ channel.sink.add(1);
+ channel.sink.add(2);
+ channel.sink.add(3);
+ unawaited(channel.sink.close());
+
+ // The sink should be ignoring events because the stream closed.
+ sinkController.stream.listen(expectAsync1((_) {}, count: 0),
+ onDone: expectAsync0(() {}, count: 0));
+ await pumpEventQueue();
+ });
+
+ test('forwards errors to the other endpoint', () {
+ channel.sink.addError('error');
+ expect(sinkController.stream.first, throwsA('error'));
+ });
+
+ test('Sink.done completes once the stream is done', () {
+ channel.stream.listen(null);
+ expect(channel.sink.done, completes);
+ streamController.close();
+ });
+
+ test("events can't be added to an explicitly-closed sink", () {
+ sinkController.stream.listen(null); // Work around sdk#19095.
+
+ expect(channel.sink.close(), completes);
+ expect(() => channel.sink.add(1), throwsStateError);
+ expect(() => channel.sink.addError('oh no'), throwsStateError);
+ expect(() => channel.sink.addStream(Stream.fromIterable([])),
+ throwsStateError);
+ });
+
+ test("events can't be added while a stream is being added", () {
+ var controller = StreamController<void>();
+ channel.sink.addStream(controller.stream);
+
+ expect(() => channel.sink.add(1), throwsStateError);
+ expect(() => channel.sink.addError('oh no'), throwsStateError);
+ expect(() => channel.sink.addStream(Stream.fromIterable([])),
+ throwsStateError);
+ expect(() => channel.sink.close(), throwsStateError);
+
+ controller.close();
+ });
+
+ group('with allowSinkErrors: false', () {
+ setUp(() {
+ streamController = StreamController<void>();
+ sinkController = StreamController<void>();
+ channel = StreamChannel.withGuarantees(
+ streamController.stream, sinkController.sink,
+ allowSinkErrors: false);
+ });
+
+ test('forwards errors to Sink.done but not the stream', () {
+ channel.sink.addError('oh no');
+ expect(channel.sink.done, throwsA('oh no'));
+ sinkController.stream
+ .listen(null, onError: expectAsync1((dynamic _) {}, count: 0));
+ });
+
+ test('adding an error causes the stream to emit a done event', () {
+ expect(channel.sink.done, throwsA('oh no'));
+
+ streamController.add(1);
+ streamController.add(2);
+ streamController.add(3);
+
+ expect(
+ channel.stream
+ .listen(expectAsync1((event) {
+ if (event == 2) channel.sink.addError('oh no');
+ }, count: 2))
+ .asFuture<void>(),
+ completes);
+ });
+
+ test('adding an error closes the inner sink', () {
+ channel.sink.addError('oh no');
+ expect(channel.sink.done, throwsA('oh no'));
+ expect(sinkController.stream.toList(), completion(isEmpty));
+ });
+
+ test(
+ 'adding an error via via addStream causes the stream to emit a done '
+ 'event', () async {
+ var canceled = false;
+ var controller = StreamController<void>(onCancel: () {
+ canceled = true;
+ });
+
+ // This future shouldn't get the error, because it's sent to [Sink.done].
+ expect(channel.sink.addStream(controller.stream), completes);
+
+ controller.addError('oh no');
+ expect(channel.sink.done, throwsA('oh no'));
+ await pumpEventQueue();
+ expect(canceled, isTrue);
+
+ // Even though the sink is closed, this shouldn't throw an error because
+ // the user didn't explicitly close it.
+ channel.sink.add(1);
+ });
+ });
+}
diff --git a/pkgs/stream_transform/.gitignore b/pkgs/stream_transform/.gitignore
new file mode 100644
index 0000000..bfffcc6
--- /dev/null
+++ b/pkgs/stream_transform/.gitignore
@@ -0,0 +1,6 @@
+.pub/
+.dart_tool/
+build/
+packages
+pubspec.lock
+.packages
diff --git a/pkgs/stream_transform/CHANGELOG.md b/pkgs/stream_transform/CHANGELOG.md
new file mode 100644
index 0000000..a71b2fb
--- /dev/null
+++ b/pkgs/stream_transform/CHANGELOG.md
@@ -0,0 +1,185 @@
+## 2.1.1
+
+- Require Dart 3.1 or greater
+- Forward errors from the `trigger` future through to the result stream in
+ `takeUntil`. Previously an error would have not closed the stream, and instead
+ raised as an unhandled async error.
+- Move to `dart-lang/tools` monorepo.
+
+## 2.1.0
+
+- Add `whereNotNull`.
+
+## 2.0.1
+
+- Require Dart 2.14 or greater.
+- Wait for the future returned from `StreamSubscription.cancel()` before
+ listening to the subsequent stream in `switchLatest` and `switchMap`.
+
+## 2.0.0
+
+- Migrate to null safety.
+- Improve tests of `switchMap` and improve documentation with links and
+ clarification.
+- Add `trailing` argument to `throttle`.
+
+## 1.2.0
+
+- Add support for emitting the "leading" event in `debounce`.
+
+## 1.1.1
+
+- Fix a bug in `asyncMapSample`, `buffer`, `combineLatest`,
+ `combineLatestAll`, `merge`, and `mergeAll` which would cause an exception
+ when cancelling a subscription after using the transformer if the original
+ stream(s) returned `null` from cancelling their subscriptions.
+
+## 1.1.0
+
+- Add `concurrentAsyncExpand` to interleave events emitted by multiple sub
+ streams created by a callback.
+
+## 1.0.0
+
+- Remove the top level methods and retain the extensions only.
+
+## 0.0.20
+
+- Add extension methods for most transformers. These should be used in place
+ of the current methods. All current implementations are deprecated and will
+ be removed in the next major version bump.
+ - Migrating typical use: Instead of
+ `stream.transform(debounce(Duration(seconds: 1)))` use
+ `stream.debounce(Duration(seconds: 1))`.
+ - To migrate a usage where a `StreamTransformer` instance is stored or
+ passed see "Getting a StreamTransformer instance" on the README.
+- The `map` and `chainTransformers` utilities are no longer useful with the
+ new patterns so they are deprecated without a replacement. If you still have
+ a need for them they can be replicated with `StreamTransformer.fromBind`:
+
+ ```
+ // Replace `map(convert)`
+ StreamTransformer.fromBind((s) => s.map(convert));
+
+ // Replace `chainTransformers(first, second)`
+ StreamTransformer.fromBind((s) => s.transform(first).transform(second));
+ ```
+
+## 0.0.19
+
+- Add `asyncMapSample` transform.
+
+## 0.0.18
+
+- Internal cleanup. Passed "trigger" streams or futures now allow `<void>`
+ generic type rather than an implicit `dynamic>`
+
+## 0.0.17
+
+- Add concrete types to the `onError` callback in `tap`.
+
+## 0.0.16+1
+
+- Remove usage of Set literal which is not available before Dart 2.2.0
+
+## 0.0.16
+
+- Allow a `combine` callback to return a `FutureOr<T>` in `scan`. There are no
+ behavior changes for synchronous callbacks. **Potential breaking change** In
+ the unlikely situation where `scan` was used to produce a `Stream<Future>`
+ inference may now fail and require explicit generic type arguments.
+- Add `combineLatest`.
+- Add `combineLatestAll`.
+
+## 0.0.15
+
+- Add `whereType`.
+
+## 0.0.14+1
+
+- Allow using non-dev Dart 2 SDK.
+
+## 0.0.14
+
+- `asyncWhere` will now forward exceptions thrown by the callback through the
+ result Stream.
+- Added `concurrentAsyncMap`.
+
+## 0.0.13
+
+- `mergeAll` now accepts an `Iterable<Stream>` instead of only `List<Stream>`.
+
+## 0.0.12
+
+- Add `chainTransformers` and `map` for use cases where `StreamTransformer`
+ instances are stored as variables or passed to methods other than `transform`.
+
+## 0.0.11
+
+- Renamed `concat` as `followedBy` to match the naming of `Iterable.followedBy`.
+ `concat` is now deprecated.
+
+## 0.0.10
+
+- Updates to support Dart 2.0 core library changes (wave
+ 2.2). See [issue 31847][sdk#31847] for details.
+
+ [sdk#31847]: https://github.com/dart-lang/sdk/issues/31847
+
+## 0.0.9
+
+- Add `asyncMapBuffer`.
+
+## 0.0.8
+
+- Add `takeUntil`.
+
+## 0.0.7
+
+- Bug Fix: Streams produced with `scan` and `switchMap` now correctly report
+ `isBroadcast`.
+- Add `startWith`, `startWithMany`, and `startWithStream`.
+
+## 0.0.6
+
+- Bug Fix: Some transformers did not correctly add data to all listeners on
+ broadcast streams. Fixed for `throttle`, `debounce`, `asyncWhere` and `audit`.
+- Bug Fix: Only call the `tap` data callback once per event rather than once per
+ listener.
+- Bug Fix: Allow canceling and re-listening to broadcast streams after a
+ `merge` transform.
+- Bug Fix: Broadcast streams which are buffered using a single-subscription
+ trigger can be canceled and re-listened.
+- Bug Fix: Buffer outputs one more value if there is a pending trigger before
+ the trigger closes.
+- Bug Fix: Single-subscription streams concatted after broadcast streams are
+ handled correctly.
+- Use sync `StreamControllers` for forwarding where possible.
+
+## 0.0.5
+
+- Bug Fix: Allow compiling switchLatest with Dart2Js.
+- Add `asyncWhere`: Like `where` but allows an asynchronous predicate.
+
+## 0.0.4
+- Add `scan`: fold which returns intermediate values
+- Add `throttle`: block events for a duration after emitting a value
+- Add `audit`: emits the last event received after a duration
+
+## 0.0.3
+
+- Add `tap`: React to values as they pass without being a subscriber on a stream
+- Add `switchMap` and `switchLatest`: Flatten a Stream of Streams into a Stream
+ which forwards values from the most recent Stream
+
+## 0.0.2
+
+- Add `concat`: Appends streams in series
+- Add `merge` and `mergeAll`: Interleaves streams
+
+## 0.0.1
+
+- Initial release with the following utilities:
+ - `buffer`: Collects events in a `List` until a `trigger` stream fires.
+ - `debounce`, `debounceBuffer`: Collect or drop events which occur closer in
+ time than a given duration.
diff --git a/pkgs/stream_transform/LICENSE b/pkgs/stream_transform/LICENSE
new file mode 100644
index 0000000..03af64a
--- /dev/null
+++ b/pkgs/stream_transform/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2017, 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/stream_transform/README.md b/pkgs/stream_transform/README.md
new file mode 100644
index 0000000..e7049bd
--- /dev/null
+++ b/pkgs/stream_transform/README.md
@@ -0,0 +1,141 @@
+[](https://github.com/dart-lang/tools/actions/workflows/stream_transform.yaml)
+[](https://pub.dev/packages/stream_transform)
+[](https://pub.dev/packages/stream_transform/publisher)
+
+Extension methods on `Stream` adding common transform operators.
+
+## Operators
+
+### asyncMapBuffer, asyncMapSample, concurrentAsyncMap
+
+Alternatives to `asyncMap`. `asyncMapBuffer` prevents the callback from
+overlapping execution and collects events while it is executing.
+`asyncMapSample` prevents overlapping execution and discards events while it is
+executing. `concurrentAsyncMap` allows overlap and removes ordering guarantees
+for higher throughput.
+
+Like `asyncMap` but events are buffered in a List until previous events have
+been processed rather than being called for each element individually.
+
+### asyncWhere
+
+Like `where` but allows an asynchronous predicate.
+
+### audit
+
+Waits for a period of time after receiving a value and then only emits the most
+recent value.
+
+### buffer
+
+Collects values from a source stream until a `trigger` stream fires and the
+collected values are emitted.
+
+### combineLatest, combineLatestAll
+
+Combine the most recent event from multiple streams through a callback or into a
+list.
+
+### debounce, debounceBuffer
+
+Prevents a source stream from emitting too frequently by dropping or collecting
+values that occur within a given duration.
+
+### followedBy
+
+Appends the values of a stream after another stream finishes.
+
+### merge, mergeAll, concurrentAsyncExpand
+
+Interleaves events from multiple streams into a single stream.
+
+### scan
+
+Scan is like fold, but instead of producing a single value it yields each
+intermediate accumulation.
+
+### startWith, startWithMany, startWithStream
+
+Prepend a value, an iterable, or a stream to the beginning of another stream.
+
+### switchMap, switchLatest
+
+Flatten a Stream of Streams into a Stream which forwards values from the most
+recent Stream
+
+### takeUntil
+
+Let values through until a Future fires.
+
+### tap
+
+Taps into a single-subscriber stream to react to values as they pass, without
+being a real subscriber.
+
+### throttle
+
+Blocks events for a duration after an event is successfully emitted.
+
+### whereType
+
+Like `Iterable.whereType` for a stream.
+
+## Comparison to Rx Operators
+
+The semantics and naming in this package have some overlap, and some conflict,
+with the [ReactiveX](https://reactivex.io/) suite of libraries. Some of the
+conflict is intentional - Dart `Stream` predates `Observable` and coherence with
+the Dart ecosystem semantics and naming is a strictly higher priority than
+consistency with ReactiveX.
+
+Rx Operator Category | variation | `stream_transform`
+------------------------- | ------------------------------------------------------ | ------------------
+[`sample`][rx_sample] | `sample/throttleLast(Duration)` | `sample(Stream.periodic(Duration), longPoll: false)`
+​ | `throttleFirst(Duration)` | [`throttle`][throttle]
+​ | `sample(Observable)` | `sample(trigger, longPoll: false)`
+[`debounce`][rx_debounce] | `debounce/throttleWithTimeout(Duration)` | [`debounce`][debounce]
+​ | `debounce(Observable)` | No equivalent
+[`buffer`][rx_buffer] | `buffer(boundary)`, `bufferWithTime`,`bufferWithCount` | No equivalent
+​ | `buffer(boundaryClosingSelector)` | `buffer(trigger, longPoll: false)`
+RxJs extensions | [`audit(callback)`][rxjs_audit] | No equivalent
+​ | [`auditTime(Duration)`][rxjs_auditTime] | [`audit`][audit]
+​ | [`exhaustMap`][rxjs_exhaustMap] | No equivalent
+​ | [`throttleTime(trailing: true)`][rxjs_throttleTime] | `throttle(trailing: true)`
+​ | `throttleTime(leading: false, trailing: true)` | No equivalent
+No equivalent? | | [`asyncMapBuffer`][asyncMapBuffer]
+​ | | [`asyncMapSample`][asyncMapSample]
+​ | | [`buffer`][buffer]
+​ | | [`sample`][sample]
+​ | | [`debounceBuffer`][debounceBuffer]
+​ | | `debounce(leading: true, trailing: false)`
+​ | | `debounce(leading: true, trailing: true)`
+
+[rx_sample]:https://reactivex.io/documentation/operators/sample.html
+[rx_debounce]:https://reactivex.io/documentation/operators/debounce.html
+[rx_buffer]:https://reactivex.io/documentation/operators/buffer.html
+[rxjs_audit]:https://rxjs.dev/api/operators/audit
+[rxjs_auditTime]:https://rxjs.dev/api/operators/auditTime
+[rxjs_throttleTime]:https://rxjs.dev/api/operators/throttleTime
+[rxjs_exhaustMap]:https://rxjs.dev/api/operators/exhaustMap
+[asyncMapBuffer]:https://pub.dev/documentation/stream_transform/latest/stream_transform/AsyncMap/asyncMapBuffer.html
+[asyncMapSample]:https://pub.dev/documentation/stream_transform/latest/stream_transform/AsyncMap/asyncMapSample.html
+[audit]:https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/audit.html
+[buffer]:https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/buffer.html
+[sample]:https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/sample.html
+[debounceBuffer]:https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/debounceBuffer.html
+[debounce]:https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/debounce.html
+[throttle]:https://pub.dev/documentation/stream_transform/latest/stream_transform/RateLimit/throttle.html
+
+## Getting a `StreamTransformer` instance
+
+It may be useful to pass an instance of `StreamTransformer` so that it can be
+used with `stream.transform` calls rather than reference the specific operator
+in place. Any operator on `Stream` that returns a `Stream` can be modeled as a
+`StreamTransformer` using the [`fromBind` constructor][fromBind].
+
+```dart
+final debounce = StreamTransformer.fromBind(
+ (s) => s.debounce(const Duration(milliseconds: 100)));
+```
+
+[fromBind]: https://api.dart.dev/stable/dart-async/StreamTransformer/StreamTransformer.fromBind.html
diff --git a/pkgs/stream_transform/analysis_options.yaml b/pkgs/stream_transform/analysis_options.yaml
new file mode 100644
index 0000000..05f1af1
--- /dev/null
+++ b/pkgs/stream_transform/analysis_options.yaml
@@ -0,0 +1,16 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
+ strict-raw-types: true
+
+linter:
+ rules:
+ - avoid_bool_literals_in_conditional_expressions
+ - avoid_classes_with_only_static_members
+ - avoid_returning_this
+ - avoid_unused_constructor_parameters
+ - cascade_invocations
+ - join_return_with_assignment
+ - no_adjacent_strings_in_list
diff --git a/pkgs/stream_transform/example/index.html b/pkgs/stream_transform/example/index.html
new file mode 100644
index 0000000..aecdc09
--- /dev/null
+++ b/pkgs/stream_transform/example/index.html
@@ -0,0 +1,11 @@
+<html>
+ <head>
+ <script defer src="main.dart.js" type="application/javascript"></script>
+ </head>
+ <body>
+ <input id="first_input"><br>
+ <input id="second_input"><br>
+ <p id="output">
+ </p>
+ </body>
+</html>
diff --git a/pkgs/stream_transform/example/main.dart b/pkgs/stream_transform/example/main.dart
new file mode 100644
index 0000000..70b3e7f
--- /dev/null
+++ b/pkgs/stream_transform/example/main.dart
@@ -0,0 +1,26 @@
+// 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:html';
+
+import 'package:stream_transform/stream_transform.dart';
+
+void main() {
+ var firstInput = document.querySelector('#first_input') as InputElement;
+ var secondInput = document.querySelector('#second_input') as InputElement;
+ var output = document.querySelector('#output')!;
+
+ _inputValues(firstInput)
+ .combineLatest(_inputValues(secondInput),
+ (first, second) => 'First: $first, Second: $second')
+ .tap((v) {
+ print('Saw: $v');
+ }).forEach((v) {
+ output.text = v;
+ });
+}
+
+Stream<String?> _inputValues(InputElement element) => element.onKeyUp
+ .debounce(const Duration(milliseconds: 100))
+ .map((_) => element.value);
diff --git a/pkgs/stream_transform/lib/src/aggregate_sample.dart b/pkgs/stream_transform/lib/src/aggregate_sample.dart
new file mode 100644
index 0000000..f2ff8ed
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/aggregate_sample.dart
@@ -0,0 +1,146 @@
+// 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:async';
+
+import 'common_callbacks.dart';
+
+extension AggregateSample<T> on Stream<T> {
+ /// Computes a value based on sequences of events, then emits that value when
+ /// [trigger] emits an event.
+ ///
+ /// Every time this stream emits an event, an intermediate value is created
+ /// by combining the new event with the previous intermediate value, or with
+ /// `null` if there is no previous value, using the [aggregate] function.
+ ///
+ /// When [trigger] emits value, the returned stream emits the current
+ /// intermediate value and clears it.
+ ///
+ /// If [longPoll] is `false`, if there is no intermediate value when [trigger]
+ /// emits an event, the [onEmpty] function is called with a [Sink] which can
+ /// add events to the returned stream.
+ ///
+ /// If [longPoll] is `true`, and there is no intermediate value when [trigger]
+ /// emits one or more events, then the *next* event from this stream is
+ /// immediately put through [aggregate] and emitted on the returned stream.
+ /// Subsequent events on [trigger] while there have been no events on this
+ /// stream are ignored.
+ /// In that case, [onEmpty] is never used.
+ ///
+ /// The result stream will close as soon as there is a guarantee it will not
+ /// emit any more events. There will not be any more events emitted if:
+ /// - [trigger] is closed and there is no waiting long poll.
+ /// - Or, the source stream is closed and there are no buffered events.
+ ///
+ /// If the source stream is a broadcast stream, the result will be as well.
+ /// Errors from the source stream or the trigger are immediately forwarded to
+ /// the output.
+ Stream<S> aggregateSample<S>(
+ {required Stream<void> trigger,
+ required S Function(T, S?) aggregate,
+ required bool longPoll,
+ required void Function(Sink<S>) onEmpty}) {
+ var controller = isBroadcast
+ ? StreamController<S>.broadcast(sync: true)
+ : StreamController<S>(sync: true);
+
+ S? currentResults;
+ var hasCurrentResults = false;
+ var activeLongPoll = false;
+ var isTriggerDone = false;
+ var isValueDone = false;
+ StreamSubscription<T>? valueSub;
+ StreamSubscription<void>? triggerSub;
+
+ void emit(S results) {
+ currentResults = null;
+ hasCurrentResults = false;
+ controller.add(results);
+ }
+
+ void onValue(T value) {
+ currentResults = aggregate(value, currentResults);
+ hasCurrentResults = true;
+ if (!longPoll) return;
+
+ if (activeLongPoll) {
+ activeLongPoll = false;
+ emit(currentResults as S);
+ }
+
+ if (isTriggerDone) {
+ valueSub!.cancel();
+ controller.close();
+ }
+ }
+
+ void onValuesDone() {
+ isValueDone = true;
+ if (!hasCurrentResults) {
+ triggerSub?.cancel();
+ controller.close();
+ }
+ }
+
+ void onTrigger(_) {
+ if (hasCurrentResults) {
+ emit(currentResults as S);
+ } else if (longPoll) {
+ activeLongPoll = true;
+ } else {
+ onEmpty(controller);
+ }
+
+ if (isValueDone) {
+ triggerSub!.cancel();
+ controller.close();
+ }
+ }
+
+ void onTriggerDone() {
+ isTriggerDone = true;
+ if (!activeLongPoll) {
+ valueSub?.cancel();
+ controller.close();
+ }
+ }
+
+ controller.onListen = () {
+ assert(valueSub == null);
+ valueSub =
+ listen(onValue, onError: controller.addError, onDone: onValuesDone);
+ final priorTriggerSub = triggerSub;
+ if (priorTriggerSub != null) {
+ if (priorTriggerSub.isPaused) priorTriggerSub.resume();
+ } else {
+ triggerSub = trigger.listen(onTrigger,
+ onError: controller.addError, onDone: onTriggerDone);
+ }
+ if (!isBroadcast) {
+ controller
+ ..onPause = () {
+ valueSub?.pause();
+ triggerSub?.pause();
+ }
+ ..onResume = () {
+ valueSub?.resume();
+ triggerSub?.resume();
+ };
+ }
+ controller.onCancel = () {
+ var cancels = <Future<void>>[if (!isValueDone) valueSub!.cancel()];
+ valueSub = null;
+ if (trigger.isBroadcast || !isBroadcast) {
+ if (!isTriggerDone) cancels.add(triggerSub!.cancel());
+ triggerSub = null;
+ } else {
+ triggerSub!.pause();
+ }
+ if (cancels.isEmpty) return null;
+ return cancels.wait.then(ignoreArgument);
+ };
+ };
+ return controller.stream;
+ }
+}
diff --git a/pkgs/stream_transform/lib/src/async_expand.dart b/pkgs/stream_transform/lib/src/async_expand.dart
new file mode 100644
index 0000000..28d2f40
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/async_expand.dart
@@ -0,0 +1,89 @@
+// 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 'common_callbacks.dart';
+import 'switch.dart';
+
+/// Alternatives to [asyncExpand].
+///
+/// The built in [asyncExpand] will not overlap the inner streams and every
+/// event will be sent to the callback individually.
+///
+/// - [concurrentAsyncExpand] allow overlap and merges inner streams without
+/// ordering guarantees.
+extension AsyncExpand<T> on Stream<T> {
+ /// Like [asyncExpand] but the [convert] callback may be called for an element
+ /// before the [Stream] emitted by the previous element has closed.
+ ///
+ /// Events on the result stream will be emitted in the order they are emitted
+ /// by the sub streams, which may not match the order of this stream.
+ ///
+ /// Errors from [convert], the source stream, or any of the sub streams are
+ /// forwarded to the result stream.
+ ///
+ /// The result stream will not close until the source stream closes and all
+ /// sub streams have closed.
+ ///
+ /// If the source stream is a broadcast stream, the result will be as well,
+ /// regardless of the types of streams created by [convert]. In this case,
+ /// some care should be taken:
+ /// - If [convert] returns a single subscription stream it may be listened to
+ /// and never canceled.
+ /// - For any period of time where there are no listeners on the result
+ /// stream, any sub streams from previously emitted events will be ignored,
+ /// regardless of whether they emit further events after a listener is added
+ /// back.
+ ///
+ /// See also:
+ /// - [switchMap], which cancels subscriptions to the previous sub stream
+ /// instead of concurrently emitting events from all sub streams.
+ Stream<S> concurrentAsyncExpand<S>(Stream<S> Function(T) convert) {
+ final controller = isBroadcast
+ ? StreamController<S>.broadcast(sync: true)
+ : StreamController<S>(sync: true);
+
+ controller.onListen = () {
+ final subscriptions = <StreamSubscription<dynamic>>[];
+ final outerSubscription = map(convert).listen((inner) {
+ if (isBroadcast && !inner.isBroadcast) {
+ inner = inner.asBroadcastStream();
+ }
+ final subscription =
+ inner.listen(controller.add, onError: controller.addError);
+ subscription.onDone(() {
+ subscriptions.remove(subscription);
+ if (subscriptions.isEmpty) controller.close();
+ });
+ subscriptions.add(subscription);
+ }, onError: controller.addError);
+ outerSubscription.onDone(() {
+ subscriptions.remove(outerSubscription);
+ if (subscriptions.isEmpty) controller.close();
+ });
+ subscriptions.add(outerSubscription);
+ if (!isBroadcast) {
+ controller
+ ..onPause = () {
+ for (final subscription in subscriptions) {
+ subscription.pause();
+ }
+ }
+ ..onResume = () {
+ for (final subscription in subscriptions) {
+ subscription.resume();
+ }
+ };
+ }
+ controller.onCancel = () {
+ if (subscriptions.isEmpty) return null;
+ return [for (var s in subscriptions) s.cancel()]
+ .wait
+ .then(ignoreArgument);
+ };
+ };
+ return controller.stream;
+ }
+}
diff --git a/pkgs/stream_transform/lib/src/async_map.dart b/pkgs/stream_transform/lib/src/async_map.dart
new file mode 100644
index 0000000..094df9c
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/async_map.dart
@@ -0,0 +1,136 @@
+// 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 'aggregate_sample.dart';
+import 'common_callbacks.dart';
+import 'from_handlers.dart';
+import 'rate_limit.dart';
+
+/// Alternatives to [asyncMap].
+///
+/// The built in [asyncMap] will not overlap execution of the passed callback,
+/// and every event will be sent to the callback individually.
+///
+/// - [asyncMapBuffer] prevents the callback from overlapping execution and
+/// collects events while it is executing to process in batches.
+/// - [asyncMapSample] prevents overlapping execution and discards events while
+/// it is executing.
+/// - [concurrentAsyncMap] allows overlap and removes ordering guarantees.
+extension AsyncMap<T> on Stream<T> {
+ /// Like [asyncMap] but events are buffered until previous events have been
+ /// processed by [convert].
+ ///
+ /// If this stream is a broadcast stream the result will be as well.
+ /// When used with a broadcast stream behavior also differs from [asyncMap] in
+ /// that the [convert] function is only called once per event, rather than
+ /// once per listener per event.
+ ///
+ /// The first event from this stream is always passed to [convert] as a
+ /// list with a single element.
+ /// After that, events are buffered until the previous Future returned from
+ /// [convert] has completed.
+ ///
+ /// Errors from this stream are forwarded directly to the result stream.
+ /// Errors during the conversion are also forwarded to the result stream and
+ /// are considered completing work so the next values are let through.
+ ///
+ /// The result stream will not close until this stream closes and all pending
+ /// conversions have finished.
+ Stream<S> asyncMapBuffer<S>(Future<S> Function(List<T>) convert) {
+ var workFinished = StreamController<void>()
+ // Let the first event through.
+ ..add(null);
+ return buffer(workFinished.stream)._asyncMapThen(convert, workFinished.add);
+ }
+
+ /// Like [asyncMap] but events are discarded while work is happening in
+ /// [convert].
+ ///
+ /// If this stream is a broadcast stream the result will be as well.
+ /// When used with a broadcast stream behavior also differs from [asyncMap] in
+ /// that the [convert] function is only called once per event, rather than
+ /// once per listener per event.
+ ///
+ /// If no work is happening when an event is emitted it will be immediately
+ /// passed to [convert]. If there is ongoing work when an event is emitted it
+ /// will be held until the work is finished. New events emitted will replace a
+ /// pending event.
+ ///
+ /// Errors from this stream are forwarded directly to the result stream.
+ /// Errors during the conversion are also forwarded to the result stream and
+ /// are considered completing work so the next values are let through.
+ ///
+ /// The result stream will not close until this stream closes and all pending
+ /// conversions have finished.
+ Stream<S> asyncMapSample<S>(Future<S> Function(T) convert) {
+ var workFinished = StreamController<void>()
+ // Let the first event through.
+ ..add(null);
+ return aggregateSample(
+ trigger: workFinished.stream,
+ aggregate: _dropPrevious,
+ longPoll: true,
+ onEmpty: ignoreArgument)
+ ._asyncMapThen(convert, workFinished.add);
+ }
+
+ /// Like [asyncMap] but the [convert] callback may be called for an element
+ /// before processing for the previous element is finished.
+ ///
+ /// Events on the result stream will be emitted in the order that [convert]
+ /// completed which may not match the order of this stream.
+ ///
+ /// If this stream is a broadcast stream the result will be as well.
+ /// When used with a broadcast stream behavior also differs from [asyncMap] in
+ /// that the [convert] function is only called once per event, rather than
+ /// once per listener per event. The [convert] callback won't be called for
+ /// events while a broadcast stream has no listener.
+ ///
+ /// Errors from [convert] or this stream are forwarded directly to the
+ /// result stream.
+ ///
+ /// The result stream will not close until this stream closes and all pending
+ /// conversions have finished.
+ Stream<S> concurrentAsyncMap<S>(FutureOr<S> Function(T) convert) {
+ var valuesWaiting = 0;
+ var sourceDone = false;
+ return transformByHandlers(onData: (element, sink) {
+ valuesWaiting++;
+ () async {
+ try {
+ sink.add(await convert(element));
+ } catch (e, st) {
+ sink.addError(e, st);
+ }
+ valuesWaiting--;
+ if (valuesWaiting <= 0 && sourceDone) sink.close();
+ }();
+ }, onDone: (sink) {
+ sourceDone = true;
+ if (valuesWaiting <= 0) sink.close();
+ });
+ }
+
+ /// Like [Stream.asyncMap] but the [convert] is only called once per event,
+ /// rather than once per listener, and [then] is called after completing the
+ /// work.
+ Stream<S> _asyncMapThen<S>(
+ Future<S> Function(T) convert, void Function(void) then) {
+ Future<void>? pendingEvent;
+ return transformByHandlers(onData: (event, sink) {
+ pendingEvent =
+ convert(event).then(sink.add).catchError(sink.addError).then(then);
+ }, onDone: (sink) {
+ if (pendingEvent != null) {
+ pendingEvent!.then((_) => sink.close());
+ } else {
+ sink.close();
+ }
+ });
+ }
+}
+
+T _dropPrevious<T>(T event, _) => event;
diff --git a/pkgs/stream_transform/lib/src/combine_latest.dart b/pkgs/stream_transform/lib/src/combine_latest.dart
new file mode 100644
index 0000000..f02a19e
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/combine_latest.dart
@@ -0,0 +1,240 @@
+// 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:async';
+
+import 'common_callbacks.dart';
+
+/// Utilities to combine events from multiple streams through a callback or into
+/// a list.
+extension CombineLatest<T> on Stream<T> {
+ /// Combines the latest values from this stream with the latest values from
+ /// [other] using [combine].
+ ///
+ /// No event will be emitted until both the source stream and [other] have
+ /// each emitted at least one event. If either the source stream or [other]
+ /// emit multiple events before the other emits the first event, all but the
+ /// last value will be discarded. Once both streams have emitted at least
+ /// once, the result stream will emit any time either input stream emits.
+ ///
+ /// The result stream will not close until both the source stream and [other]
+ /// have closed.
+ ///
+ /// For example:
+ ///
+ /// source.combineLatest(other, (a, b) => a + b);
+ ///
+ /// source: --1--2--------4--|
+ /// other: -------3--|
+ /// result: -------5------7--|
+ ///
+ /// Errors thrown by [combine], along with any errors on the source stream or
+ /// [other], are forwarded to the result stream.
+ ///
+ /// If the source stream is a broadcast stream, the result stream will be as
+ /// well, regardless of [other]'s type. If a single subscription stream is
+ /// combined with a broadcast stream it may never be canceled.
+ Stream<S> combineLatest<T2, S>(
+ Stream<T2> other, FutureOr<S> Function(T, T2) combine) {
+ final controller = isBroadcast
+ ? StreamController<S>.broadcast(sync: true)
+ : StreamController<S>(sync: true);
+
+ other =
+ (isBroadcast && !other.isBroadcast) ? other.asBroadcastStream() : other;
+
+ StreamSubscription<T>? sourceSubscription;
+ StreamSubscription<T2>? otherSubscription;
+
+ var sourceDone = false;
+ var otherDone = false;
+
+ late T latestSource;
+ late T2 latestOther;
+
+ var sourceStarted = false;
+ var otherStarted = false;
+
+ void emitCombined() {
+ if (!sourceStarted || !otherStarted) return;
+ FutureOr<S> result;
+ try {
+ result = combine(latestSource, latestOther);
+ } catch (e, s) {
+ controller.addError(e, s);
+ return;
+ }
+ if (result is Future<S>) {
+ sourceSubscription!.pause();
+ otherSubscription!.pause();
+ result
+ .then(controller.add, onError: controller.addError)
+ .whenComplete(() {
+ sourceSubscription!.resume();
+ otherSubscription!.resume();
+ });
+ } else {
+ controller.add(result);
+ }
+ }
+
+ controller.onListen = () {
+ assert(sourceSubscription == null);
+ sourceSubscription = listen(
+ (s) {
+ sourceStarted = true;
+ latestSource = s;
+ emitCombined();
+ },
+ onError: controller.addError,
+ onDone: () {
+ sourceDone = true;
+ if (otherDone) {
+ controller.close();
+ } else if (!sourceStarted) {
+ // Nothing can ever be emitted
+ otherSubscription!.cancel();
+ controller.close();
+ }
+ });
+ otherSubscription = other.listen(
+ (o) {
+ otherStarted = true;
+ latestOther = o;
+ emitCombined();
+ },
+ onError: controller.addError,
+ onDone: () {
+ otherDone = true;
+ if (sourceDone) {
+ controller.close();
+ } else if (!otherStarted) {
+ // Nothing can ever be emitted
+ sourceSubscription!.cancel();
+ controller.close();
+ }
+ });
+ if (!isBroadcast) {
+ controller
+ ..onPause = () {
+ sourceSubscription!.pause();
+ otherSubscription!.pause();
+ }
+ ..onResume = () {
+ sourceSubscription!.resume();
+ otherSubscription!.resume();
+ };
+ }
+ controller.onCancel = () {
+ var cancels = [
+ sourceSubscription!.cancel(),
+ otherSubscription!.cancel()
+ ];
+ sourceSubscription = null;
+ otherSubscription = null;
+ return cancels.wait.then(ignoreArgument);
+ };
+ };
+ return controller.stream;
+ }
+
+ /// Combine the latest value emitted from the source stream with the latest
+ /// values emitted from [others].
+ ///
+ /// [combineLatestAll] subscribes to the source stream and [others] and when
+ /// any one of the streams emits, the result stream will emit a [List<T>] of
+ /// the latest values emitted from all streams.
+ ///
+ /// No event will be emitted until all source streams emit at least once. If a
+ /// source stream emits multiple values before another starts emitting, all
+ /// but the last value will be discarded. Once all source streams have emitted
+ /// at least once, the result stream will emit any time any source stream
+ /// emits.
+ ///
+ /// The result stream will not close until all source streams have closed.
+ /// When a source stream closes, the result stream will continue to emit the
+ /// last value from the closed stream when the other source streams emit until
+ /// the result stream has closed. If a source stream closes without emitting
+ /// any value, the result stream will close as well.
+ ///
+ /// For example:
+ ///
+ /// final combined = first
+ /// .combineLatestAll([second, third])
+ /// .map((data) => data.join());
+ ///
+ /// first: a----b------------------c--------d---|
+ /// second: --1---------2-----------------|
+ /// third: -------&----------%---|
+ /// combined: -------b1&--b2&---b2%---c2%------d2%-|
+ ///
+ /// Errors thrown by any source stream will be forwarded to the result stream.
+ ///
+ /// If the source stream is a broadcast stream, the result stream will be as
+ /// well, regardless of the types of [others]. If a single subscription stream
+ /// is combined with a broadcast source stream, it may never be canceled.
+ Stream<List<T>> combineLatestAll(Iterable<Stream<T>> others) {
+ final controller = isBroadcast
+ ? StreamController<List<T>>.broadcast(sync: true)
+ : StreamController<List<T>>(sync: true);
+
+ final allStreams = [
+ this,
+ for (final other in others)
+ !isBroadcast || other.isBroadcast ? other : other.asBroadcastStream(),
+ ];
+
+ controller.onListen = () {
+ final subscriptions = <StreamSubscription<T>>[];
+
+ final latestData = List<T?>.filled(allStreams.length, null);
+ final hasEmitted = <int>{};
+ void handleData(int index, T data) {
+ latestData[index] = data;
+ hasEmitted.add(index);
+ if (hasEmitted.length == allStreams.length) {
+ controller.add(List.from(latestData));
+ }
+ }
+
+ var streamId = 0;
+ for (final stream in allStreams) {
+ final index = streamId;
+
+ final subscription = stream.listen((data) => handleData(index, data),
+ onError: controller.addError);
+ subscription.onDone(() {
+ assert(subscriptions.contains(subscription));
+ subscriptions.remove(subscription);
+ if (subscriptions.isEmpty || !hasEmitted.contains(index)) {
+ controller.close();
+ }
+ });
+ subscriptions.add(subscription);
+
+ streamId++;
+ }
+ if (!isBroadcast) {
+ controller
+ ..onPause = () {
+ for (final subscription in subscriptions) {
+ subscription.pause();
+ }
+ }
+ ..onResume = () {
+ for (final subscription in subscriptions) {
+ subscription.resume();
+ }
+ };
+ }
+ controller.onCancel = () {
+ if (subscriptions.isEmpty) return null;
+ return [for (var s in subscriptions) s.cancel()]
+ .wait
+ .then(ignoreArgument);
+ };
+ };
+ return controller.stream;
+ }
+}
diff --git a/pkgs/stream_transform/lib/src/common_callbacks.dart b/pkgs/stream_transform/lib/src/common_callbacks.dart
new file mode 100644
index 0000000..c239220
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/common_callbacks.dart
@@ -0,0 +1,5 @@
+// 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.
+
+void ignoreArgument(_) {}
diff --git a/pkgs/stream_transform/lib/src/concatenate.dart b/pkgs/stream_transform/lib/src/concatenate.dart
new file mode 100644
index 0000000..0330dd7
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/concatenate.dart
@@ -0,0 +1,112 @@
+// 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';
+
+/// Utilities to append or prepend to a stream.
+extension Concatenate<T> on Stream<T> {
+ /// Emits all values and errors from [next] following all values and errors
+ /// from this stream.
+ ///
+ /// If this stream never finishes, the [next] stream will never get a
+ /// listener.
+ ///
+ /// If this stream is a broadcast stream, the result will be as well.
+ /// If a single-subscription follows a broadcast stream it may be listened
+ /// to and never canceled since there may be broadcast listeners added later.
+ ///
+ /// If a broadcast stream follows any other stream it will miss any events or
+ /// errors which occur before this stream is done.
+ /// If a broadcast stream follows a single-subscription stream, pausing the
+ /// stream while it is listening to the second stream will cause events to be
+ /// dropped rather than buffered.
+ Stream<T> followedBy(Stream<T> next) {
+ var controller = isBroadcast
+ ? StreamController<T>.broadcast(sync: true)
+ : StreamController<T>(sync: true);
+
+ next = isBroadcast && !next.isBroadcast ? next.asBroadcastStream() : next;
+
+ StreamSubscription<T>? subscription;
+ var currentStream = this;
+ var thisDone = false;
+ var secondDone = false;
+
+ late void Function() currentDoneHandler;
+
+ void listen() {
+ subscription = currentStream.listen(controller.add,
+ onError: controller.addError, onDone: () => currentDoneHandler());
+ }
+
+ void onSecondDone() {
+ secondDone = true;
+ controller.close();
+ }
+
+ void onThisDone() {
+ thisDone = true;
+ currentStream = next;
+ currentDoneHandler = onSecondDone;
+ listen();
+ }
+
+ currentDoneHandler = onThisDone;
+
+ controller.onListen = () {
+ assert(subscription == null);
+ listen();
+ if (!isBroadcast) {
+ controller
+ ..onPause = () {
+ if (!thisDone || !next.isBroadcast) return subscription!.pause();
+ subscription!.cancel();
+ subscription = null;
+ }
+ ..onResume = () {
+ if (!thisDone || !next.isBroadcast) return subscription!.resume();
+ listen();
+ };
+ }
+ controller.onCancel = () {
+ if (secondDone) return null;
+ var toCancel = subscription!;
+ subscription = null;
+ return toCancel.cancel();
+ };
+ };
+ return controller.stream;
+ }
+
+ /// Emits [initial] before any values or errors from the this stream.
+ ///
+ /// If this stream is a broadcast stream the result will be as well.
+ /// If this stream is a broadcast stream, the returned stream will only
+ /// contain events of this stream that are emitted after the [initial] value
+ /// has been emitted on the returned stream.
+ Stream<T> startWith(T initial) =>
+ startWithStream(Future.value(initial).asStream());
+
+ /// Emits all values in [initial] before any values or errors from this
+ /// stream.
+ ///
+ /// If this stream is a broadcast stream the result will be as well.
+ /// If this stream is a broadcast stream it will miss any events which
+ /// occur before the initial values are all emitted.
+ Stream<T> startWithMany(Iterable<T> initial) =>
+ startWithStream(Stream.fromIterable(initial));
+
+ /// Emits all values and errors in [initial] before any values or errors from
+ /// this stream.
+ ///
+ /// If this stream is a broadcast stream the result will be as well.
+ /// If this stream is a broadcast stream it will miss any events which occur
+ /// before [initial] closes.
+ Stream<T> startWithStream(Stream<T> initial) {
+ if (isBroadcast && !initial.isBroadcast) {
+ initial = initial.asBroadcastStream();
+ }
+ return initial.followedBy(this);
+ }
+}
diff --git a/pkgs/stream_transform/lib/src/from_handlers.dart b/pkgs/stream_transform/lib/src/from_handlers.dart
new file mode 100644
index 0000000..1146a13
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/from_handlers.dart
@@ -0,0 +1,58 @@
+// 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';
+
+extension TransformByHandlers<S> on Stream<S> {
+ /// Transform a stream by callbacks.
+ ///
+ /// This is similar to `transform(StreamTransformer.fromHandler(...))` except
+ /// that the handlers are called once per event rather than called for the
+ /// same event for each listener on a broadcast stream.
+ Stream<T> transformByHandlers<T>(
+ {required void Function(S, EventSink<T>) onData,
+ void Function(Object, StackTrace, EventSink<T>)? onError,
+ void Function(EventSink<T>)? onDone}) {
+ final handleError = onError ?? _defaultHandleError;
+ final handleDone = onDone ?? _defaultHandleDone;
+
+ var controller = isBroadcast
+ ? StreamController<T>.broadcast(sync: true)
+ : StreamController<T>(sync: true);
+
+ StreamSubscription<S>? subscription;
+ controller.onListen = () {
+ assert(subscription == null);
+ var valuesDone = false;
+ subscription = listen((value) => onData(value, controller),
+ onError: (Object error, StackTrace stackTrace) {
+ handleError(error, stackTrace, controller);
+ }, onDone: () {
+ valuesDone = true;
+ handleDone(controller);
+ });
+ if (!isBroadcast) {
+ controller
+ ..onPause = subscription!.pause
+ ..onResume = subscription!.resume;
+ }
+ controller.onCancel = () {
+ var toCancel = subscription;
+ subscription = null;
+ if (!valuesDone) return toCancel!.cancel();
+ return null;
+ };
+ };
+ return controller.stream;
+ }
+
+ static void _defaultHandleError<T>(
+ Object error, StackTrace stackTrace, EventSink<T> sink) {
+ sink.addError(error, stackTrace);
+ }
+
+ static void _defaultHandleDone<T>(EventSink<T> sink) {
+ sink.close();
+ }
+}
diff --git a/pkgs/stream_transform/lib/src/merge.dart b/pkgs/stream_transform/lib/src/merge.dart
new file mode 100644
index 0000000..3bfe06c
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/merge.dart
@@ -0,0 +1,102 @@
+// 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 'common_callbacks.dart';
+
+/// Utilities to interleave events from multiple streams.
+extension Merge<T> on Stream<T> {
+ /// Merges values and errors from this stream and [other] in any order as they
+ /// arrive.
+ ///
+ /// The result stream will not close until both this stream and [other] have
+ /// closed.
+ ///
+ /// For example:
+ ///
+ /// final result = source.merge(other);
+ ///
+ /// source: 1--2-----3--|
+ /// other: ------4-------5--|
+ /// result: 1--2--4--3----5--|
+ ///
+ /// If this stream is a broadcast stream, the result stream will be as
+ /// well, regardless of [other]'s type. If a single subscription stream is
+ /// merged into a broadcast stream it may never be canceled since there may be
+ /// broadcast listeners added later.
+ ///
+ /// If a broadcast stream is merged into a single-subscription stream any
+ /// events emitted by [other] before the result stream has a subscriber will
+ /// be discarded.
+ Stream<T> merge(Stream<T> other) => mergeAll([other]);
+
+ /// Merges values and errors from this stream and any stream in [others] in
+ /// any order as they arrive.
+ ///
+ /// The result stream will not close until this stream and all streams
+ /// in [others] have closed.
+ ///
+ /// For example:
+ ///
+ /// final result = first.mergeAll([second, third]);
+ ///
+ /// first: 1--2--------3--|
+ /// second: ---------4-------5--|
+ /// third: ------6---------------7--|
+ /// result: 1--2--6--4--3----5----7--|
+ ///
+ /// If this stream is a broadcast stream, the result stream will be as
+ /// well, regardless the types of streams in [others]. If a single
+ /// subscription stream is merged into a broadcast stream it may never be
+ /// canceled since there may be broadcast listeners added later.
+ ///
+ /// If a broadcast stream is merged into a single-subscription stream any
+ /// events emitted by that stream before the result stream has a subscriber
+ /// will be discarded.
+ Stream<T> mergeAll(Iterable<Stream<T>> others) {
+ final controller = isBroadcast
+ ? StreamController<T>.broadcast(sync: true)
+ : StreamController<T>(sync: true);
+
+ final allStreams = [
+ this,
+ for (final other in others)
+ !isBroadcast || other.isBroadcast ? other : other.asBroadcastStream(),
+ ];
+
+ controller.onListen = () {
+ final subscriptions = <StreamSubscription<T>>[];
+ for (final stream in allStreams) {
+ final subscription =
+ stream.listen(controller.add, onError: controller.addError);
+ subscription.onDone(() {
+ subscriptions.remove(subscription);
+ if (subscriptions.isEmpty) controller.close();
+ });
+ subscriptions.add(subscription);
+ }
+ if (!isBroadcast) {
+ controller
+ ..onPause = () {
+ for (final subscription in subscriptions) {
+ subscription.pause();
+ }
+ }
+ ..onResume = () {
+ for (final subscription in subscriptions) {
+ subscription.resume();
+ }
+ };
+ }
+ controller.onCancel = () {
+ if (subscriptions.isEmpty) return null;
+ return [for (var s in subscriptions) s.cancel()]
+ .wait
+ .then(ignoreArgument);
+ };
+ };
+ return controller.stream;
+ }
+}
diff --git a/pkgs/stream_transform/lib/src/rate_limit.dart b/pkgs/stream_transform/lib/src/rate_limit.dart
new file mode 100644
index 0000000..299c230
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/rate_limit.dart
@@ -0,0 +1,356 @@
+// 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:async';
+
+import 'aggregate_sample.dart';
+import 'common_callbacks.dart';
+import 'from_handlers.dart';
+
+/// Utilities to rate limit events.
+///
+/// - [debounce] - emit the the _first_ or _last_ event of a series of closely
+/// spaced events.
+/// - [debounceBuffer] - emit _all_ events at the _end_ of a series of closely
+/// spaced events.
+/// - [throttle] - emit the _first_ event at the _beginning_ of the period.
+/// - [audit] - emit the _last_ event at the _end_ of the period.
+/// - [buffer] - emit _all_ events on a _trigger_.
+extension RateLimit<T> on Stream<T> {
+ /// Suppresses events with less inter-event spacing than [duration].
+ ///
+ /// Events which are emitted with less than [duration] elapsed between them
+ /// are considered to be part of the same "series". If [leading] is `true`,
+ /// the first event of this series is emitted immediately. If [trailing] is
+ /// `true` the last event of this series is emitted with a delay of at least
+ /// [duration]. By default only trailing events are emitted, both arguments
+ /// must be specified with `leading: true, trailing: false` to emit only
+ /// leading events.
+ ///
+ /// If this stream is a broadcast stream, the result will be as well.
+ /// Errors are forwarded immediately.
+ ///
+ /// If there is a trailing event waiting during the debounce period when the
+ /// source stream closes the returned stream will wait to emit it following
+ /// the debounce period before closing. If there is no pending debounced event
+ /// when this stream closes the returned stream will close immediately.
+ ///
+ /// For example:
+ ///
+ /// source.debounce(Duration(seconds: 1));
+ ///
+ /// source: 1-2-3---4---5-6-|
+ /// result: ------3---4-----6|
+ ///
+ /// source.debounce(Duration(seconds: 1), leading: true, trailing: false);
+ ///
+ /// source: 1-2-3---4---5-6-|
+ /// result: 1-------4---5---|
+ ///
+ /// source.debounce(Duration(seconds: 1), leading: true);
+ ///
+ /// source: 1-2-3---4---5-6-|
+ /// result: 1-----3-4---5---6|
+ ///
+ /// To collect values emitted during the debounce period see [debounceBuffer].
+ Stream<T> debounce(Duration duration,
+ {bool leading = false, bool trailing = true}) =>
+ _debounceAggregate(duration, _dropPrevious,
+ leading: leading, trailing: trailing);
+
+ /// Buffers values until this stream does not emit for [duration] then emits
+ /// the collected values.
+ ///
+ /// Values will always be delayed by at least [duration], and values which
+ /// come within this time will be aggregated into the same list.
+ ///
+ /// If this stream is a broadcast stream, the result will be as well.
+ /// Errors are forwarded immediately.
+ ///
+ /// If there are events waiting during the debounce period when this stream
+ /// closes the returned stream will wait to emit them following the debounce
+ /// period before closing. If there are no pending debounced events when this
+ /// stream closes the returned stream will close immediately.
+ ///
+ /// To keep only the most recent event during the debounce period see
+ /// [debounce].
+ Stream<List<T>> debounceBuffer(Duration duration) =>
+ _debounceAggregate(duration, _collect, leading: false, trailing: true);
+
+ /// Reduces the rate that events are emitted to at most once per [duration].
+ ///
+ /// No events will ever be emitted within [duration] of another event on the
+ /// result stream.
+ /// If this stream is a broadcast stream, the result will be as well.
+ /// Errors are forwarded immediately.
+ ///
+ /// If [trailing] is `false`, source events emitted during the [duration]
+ /// period following a result event are discarded.
+ /// The result stream will not emit an event until this stream emits an event
+ /// following the throttled period.
+ /// If this stream is consistently emitting events with less than
+ /// [duration] between events, the time between events on the result stream
+ /// may still be more than [duration].
+ /// The result stream will close immediately when this stream closes.
+ ///
+ /// If [trailing] is `true`, the latest source event emitted during the
+ /// [duration] period following an result event is held and emitted following
+ /// the period.
+ /// If this stream is consistently emitting events with less than [duration]
+ /// between events, the time between events on the result stream will be
+ /// [duration].
+ /// If this stream closes the result stream will wait to emit a pending event
+ /// before closing.
+ ///
+ /// For example:
+ ///
+ /// source.throttle(Duration(seconds: 6));
+ ///
+ /// source: 1-2-3---4-5-6---7-8-|
+ /// result: 1-------4-------7---|
+ ///
+ /// source.throttle(Duration(seconds: 6), trailing: true);
+ ///
+ /// source: 1-2-3---4-5----6--|
+ /// result: 1-----3-----5-----6|
+ ///
+ /// source.throttle(Duration(seconds: 6), trailing: true);
+ ///
+ /// source: 1-2-----------3|
+ /// result: 1-----2-------3|
+ ///
+ /// See also:
+ /// - [audit], which emits the most recent event at the end of the period.
+ /// Compared to `audit`, `throttle` will not introduce delay to forwarded
+ /// elements, except for the [trailing] events.
+ /// - [debounce], which uses inter-event spacing instead of a fixed period
+ /// from the first event in a window. Compared to `debouce`, `throttle` cannot
+ /// be starved by having events emitted continuously within [duration].
+ Stream<T> throttle(Duration duration, {bool trailing = false}) =>
+ trailing ? _throttleTrailing(duration) : _throttle(duration);
+
+ Stream<T> _throttle(Duration duration) {
+ Timer? timer;
+
+ return transformByHandlers(onData: (data, sink) {
+ if (timer == null) {
+ sink.add(data);
+ timer = Timer(duration, () {
+ timer = null;
+ });
+ }
+ });
+ }
+
+ Stream<T> _throttleTrailing(Duration duration) {
+ Timer? timer;
+ T? pending;
+ var hasPending = false;
+ var isDone = false;
+
+ return transformByHandlers(onData: (data, sink) {
+ void onTimer() {
+ if (hasPending) {
+ sink.add(pending as T);
+ if (isDone) {
+ sink.close();
+ } else {
+ timer = Timer(duration, onTimer);
+ hasPending = false;
+ pending = null;
+ }
+ } else {
+ timer = null;
+ }
+ }
+
+ if (timer == null) {
+ sink.add(data);
+ timer = Timer(duration, onTimer);
+ } else {
+ hasPending = true;
+ pending = data;
+ }
+ }, onDone: (sink) {
+ isDone = true;
+ if (hasPending) return; // Will be closed by timer.
+ sink.close();
+ timer?.cancel();
+ timer = null;
+ });
+ }
+
+ /// Audit a single event from each [duration] length period where there are
+ /// events on this stream.
+ ///
+ /// No events will ever be emitted within [duration] of another event on the
+ /// result stream.
+ /// If this stream is a broadcast stream, the result will be as well.
+ /// Errors are forwarded immediately.
+ ///
+ /// The first event will begin the audit period. At the end of the audit
+ /// period the most recent event is emitted, and the next event restarts the
+ /// audit period.
+ ///
+ /// If the event that started the period is the one that is emitted it will be
+ /// delayed by [duration]. If a later event comes in within the period it's
+ /// delay will be shorter by the difference in arrival times.
+ ///
+ /// If there is no pending event when this stream closes the output
+ /// stream will close immediately. If there is a pending event the output
+ /// stream will wait to emit it before closing.
+ ///
+ /// For example:
+ ///
+ /// source.audit(Duration(seconds: 5));
+ ///
+ /// source: a------b--c----d--|
+ /// output: -----a------c--------d|
+ ///
+ /// See also:
+ /// - [throttle], which emits the _first_ event during the window, instead of
+ /// the last event in the window. Compared to `throttle`, `audit` will
+ /// introduce delay to forwarded events.
+ /// - [debounce], which only emits after the stream has not emitted for some
+ /// period. Compared to `debouce`, `audit` cannot be starved by having events
+ /// emitted continuously within [duration].
+ Stream<T> audit(Duration duration) {
+ Timer? timer;
+ var shouldClose = false;
+ T recentData;
+
+ return transformByHandlers(onData: (data, sink) {
+ recentData = data;
+ timer ??= Timer(duration, () {
+ sink.add(recentData);
+ timer = null;
+ if (shouldClose) {
+ sink.close();
+ }
+ });
+ }, onDone: (sink) {
+ if (timer != null) {
+ shouldClose = true;
+ } else {
+ sink.close();
+ }
+ });
+ }
+
+ /// Buffers the values emitted on this stream and emits them when [trigger]
+ /// emits an event.
+ ///
+ /// If [longPoll] is `false`, if there are no buffered values when [trigger]
+ /// emits an empty list is immediately emitted.
+ ///
+ /// If [longPoll] is `true`, and there are no buffered values when [trigger]
+ /// emits one or more events, then the *next* value from this stream is
+ /// immediately emitted on the returned stream as a single element list.
+ /// Subsequent events on [trigger] while there have been no events on this
+ /// stream are ignored.
+ ///
+ /// The result stream will close as soon as there is a guarantee it will not
+ /// emit any more events. There will not be any more events emitted if:
+ /// - [trigger] is closed and there is no waiting long poll.
+ /// - Or, this stream is closed and previously buffered events have been
+ /// delivered.
+ ///
+ /// If this stream is a broadcast stream, the result will be as well.
+ /// Errors from this stream or the trigger are immediately forwarded to the
+ /// output.
+ ///
+ /// See also:
+ /// - [sample] which use a [trigger] stream in the same way, but keeps only
+ /// the most recent source event.
+ Stream<List<T>> buffer(Stream<void> trigger, {bool longPoll = true}) =>
+ aggregateSample(
+ trigger: trigger,
+ aggregate: _collect,
+ longPoll: longPoll,
+ onEmpty: _empty);
+
+ /// Emits the most recent new value from this stream when [trigger] emits an
+ /// event.
+ ///
+ /// If [longPoll] is `false`, then an event on [trigger] when there is no
+ /// pending source event will be ignored.
+ /// If [longPoll] is `true` (the default), then an event on [trigger] when
+ /// there is no pending source event will cause the next source event
+ /// to immediately flow to the result stream.
+ ///
+ /// If [longPoll] is `false`, if there is no pending source event when
+ /// [trigger] emits, then the trigger event will be ignored.
+ ///
+ /// If [longPoll] is `true`, and there are no buffered values when [trigger]
+ /// emits one or more events, then the *next* value from this stream is
+ /// immediately emitted on the returned stream as a single element list.
+ /// Subsequent events on [trigger] while there have been no events on this
+ /// stream are ignored.
+ ///
+ /// The result stream will close as soon as there is a guarantee it will not
+ /// emit any more events. There will not be any more events emitted if:
+ /// - [trigger] is closed and there is no waiting long poll.
+ /// - Or, this source stream is closed and any pending source event has been
+ /// delivered.
+ ///
+ /// If this source stream is a broadcast stream, the result will be as well.
+ /// Errors from this source stream or the trigger are immediately forwarded to
+ /// the output.
+ ///
+ /// See also:
+ /// - [buffer] which use [trigger] stream in the same way, but keeps a list of
+ /// pending source events.
+ Stream<T> sample(Stream<void> trigger, {bool longPoll = true}) =>
+ aggregateSample(
+ trigger: trigger,
+ aggregate: _dropPrevious,
+ longPoll: longPoll,
+ onEmpty: ignoreArgument);
+
+ /// Aggregates values until this source stream does not emit for [duration],
+ /// then emits the aggregated values.
+ Stream<S> _debounceAggregate<S>(
+ Duration duration, S Function(T element, S? soFar) collect,
+ {required bool leading, required bool trailing}) {
+ Timer? timer;
+ S? soFar;
+ var hasPending = false;
+ var shouldClose = false;
+ var emittedLatestAsLeading = false;
+
+ return transformByHandlers(onData: (value, sink) {
+ void emit() {
+ sink.add(soFar as S);
+ soFar = null;
+ hasPending = false;
+ }
+
+ timer?.cancel();
+ soFar = collect(value, soFar);
+ hasPending = true;
+ if (timer == null && leading) {
+ emittedLatestAsLeading = true;
+ emit();
+ } else {
+ emittedLatestAsLeading = false;
+ }
+ timer = Timer(duration, () {
+ if (trailing && !emittedLatestAsLeading) emit();
+ if (shouldClose) sink.close();
+ timer = null;
+ });
+ }, onDone: (EventSink<S> sink) {
+ if (hasPending && trailing) {
+ shouldClose = true;
+ } else {
+ timer?.cancel();
+ sink.close();
+ }
+ });
+ }
+}
+
+T _dropPrevious<T>(T element, _) => element;
+List<T> _collect<T>(T event, List<T>? soFar) => (soFar ?? <T>[])..add(event);
+void _empty<T>(Sink<List<T>> sink) => sink.add([]);
diff --git a/pkgs/stream_transform/lib/src/scan.dart b/pkgs/stream_transform/lib/src/scan.dart
new file mode 100644
index 0000000..acd3c76
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/scan.dart
@@ -0,0 +1,31 @@
+// 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';
+
+/// A utility similar to [fold] which emits intermediate accumulations.
+extension Scan<T> on Stream<T> {
+ /// Emits a sequence of the accumulated values from repeatedly applying
+ /// [combine].
+ ///
+ /// Like [fold], but instead of producing a single value it yields each
+ /// intermediate result.
+ ///
+ /// If [combine] returns a future it will not be called again for subsequent
+ /// events from the source until it completes, therefore [combine] is always
+ /// called for elements in order, and the result stream always maintains the
+ /// same order as this stream.
+ Stream<S> scan<S>(
+ S initialValue, FutureOr<S> Function(S soFar, T element) combine) {
+ var accumulated = initialValue;
+ return asyncMap((value) {
+ var result = combine(accumulated, value);
+ if (result is Future<S>) {
+ return result.then((r) => accumulated = r);
+ } else {
+ return accumulated = result;
+ }
+ });
+ }
+}
diff --git a/pkgs/stream_transform/lib/src/switch.dart b/pkgs/stream_transform/lib/src/switch.dart
new file mode 100644
index 0000000..546036e
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/switch.dart
@@ -0,0 +1,135 @@
+// 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 'async_expand.dart';
+import 'common_callbacks.dart';
+
+/// A utility to take events from the most recent sub stream returned by a
+/// callback.
+extension Switch<T> on Stream<T> {
+ /// Maps events to a Stream and emits values from the most recently created
+ /// Stream.
+ ///
+ /// When the source emits a value it will be converted to a [Stream] using
+ /// [convert] and the output will switch to emitting events from that result.
+ /// Like [asyncExpand] but the [Stream] emitted by a previous element
+ /// will be ignored as soon as the source stream emits a new event.
+ ///
+ /// This means that the source stream is not paused until a sub stream
+ /// returned from the [convert] callback is done. Instead, the subscription
+ /// to the sub stream is canceled as soon as the source stream emits a new
+ /// event.
+ ///
+ /// Errors from [convert], the source stream, or any of the sub streams are
+ /// forwarded to the result stream.
+ ///
+ /// The result stream will not close until the source stream closes and
+ /// the current sub stream have closed.
+ ///
+ /// If the source stream is a broadcast stream, the result will be as well,
+ /// regardless of the types of streams created by [convert]. In this case,
+ /// some care should be taken:
+ ///
+ /// * If [convert] returns a single subscription stream it may be listened to
+ /// and never canceled.
+ ///
+ /// See also:
+ /// - [concurrentAsyncExpand], which emits events from all sub streams
+ /// concurrently instead of cancelling subscriptions to previous subs
+ /// streams.
+ Stream<S> switchMap<S>(Stream<S> Function(T) convert) {
+ return map(convert).switchLatest();
+ }
+}
+
+/// A utility to take events from the most recent sub stream.
+extension SwitchLatest<T> on Stream<Stream<T>> {
+ /// Emits values from the most recently emitted Stream.
+ ///
+ /// When the source emits a stream, the output will switch to emitting events
+ /// from that stream.
+ ///
+ /// Whether the source stream is a single-subscription stream or a
+ /// broadcast stream, the result stream will be the same kind of stream,
+ /// regardless of the types of streams emitted.
+ Stream<T> switchLatest() {
+ var controller = isBroadcast
+ ? StreamController<T>.broadcast(sync: true)
+ : StreamController<T>(sync: true);
+
+ controller.onListen = () {
+ StreamSubscription<T>? innerSubscription;
+ var outerStreamDone = false;
+
+ void listenToInnerStream(Stream<T> innerStream) {
+ assert(innerSubscription == null);
+ var subscription = innerStream
+ .listen(controller.add, onError: controller.addError, onDone: () {
+ innerSubscription = null;
+ if (outerStreamDone) controller.close();
+ });
+ // If a pause happens during an innerSubscription.cancel,
+ // we still listen to the next stream when the cancel is done.
+ // Then we immediately pause it again here.
+ if (controller.isPaused) subscription.pause();
+ innerSubscription = subscription;
+ }
+
+ var addError = controller.addError;
+ final outerSubscription = listen(null, onError: addError, onDone: () {
+ outerStreamDone = true;
+ if (innerSubscription == null) controller.close();
+ });
+ outerSubscription.onData((innerStream) async {
+ var currentSubscription = innerSubscription;
+ if (currentSubscription == null) {
+ listenToInnerStream(innerStream);
+ return;
+ }
+ innerSubscription = null;
+ outerSubscription.pause();
+ try {
+ await currentSubscription.cancel();
+ } catch (error, stack) {
+ controller.addError(error, stack);
+ } finally {
+ if (!isBroadcast && !controller.hasListener) {
+ // Result single-subscription stream subscription was cancelled
+ // while waiting for previous innerStream cancel.
+ //
+ // Ensure that the last received stream is also listened to and
+ // cancelled, then do nothing further.
+ innerStream.listen(null).cancel().ignore();
+ } else {
+ outerSubscription.resume();
+ listenToInnerStream(innerStream);
+ }
+ }
+ });
+ if (!isBroadcast) {
+ controller
+ ..onPause = () {
+ innerSubscription?.pause();
+ outerSubscription.pause();
+ }
+ ..onResume = () {
+ innerSubscription?.resume();
+ outerSubscription.resume();
+ };
+ }
+ controller.onCancel = () {
+ var sub = innerSubscription;
+ var cancels = [
+ if (!outerStreamDone) outerSubscription.cancel(),
+ if (sub != null) sub.cancel(),
+ ];
+ if (cancels.isEmpty) return null;
+ return cancels.wait.then(ignoreArgument);
+ };
+ };
+ return controller.stream;
+ }
+}
diff --git a/pkgs/stream_transform/lib/src/take_until.dart b/pkgs/stream_transform/lib/src/take_until.dart
new file mode 100644
index 0000000..e6deaa1
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/take_until.dart
@@ -0,0 +1,64 @@
+// 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';
+
+/// A utility to end a stream based on an external trigger.
+extension TakeUntil<T> on Stream<T> {
+ /// Takes values from this stream which are emitted before [trigger]
+ /// completes.
+ ///
+ /// Completing [trigger] differs from canceling a subscription in that values
+ /// which are emitted before the trigger, but have further asynchronous delays
+ /// in transformations following the takeUtil, will still go through.
+ /// Cancelling a subscription immediately stops values.
+ ///
+ /// If [trigger] completes as an error, the error will be forwarded through
+ /// the result stream before the result stream closes.
+ ///
+ /// If [trigger] completes as a value or as an error after this stream has
+ /// already ended, the completion will be ignored.
+ Stream<T> takeUntil(Future<void> trigger) {
+ var controller = isBroadcast
+ ? StreamController<T>.broadcast(sync: true)
+ : StreamController<T>(sync: true);
+
+ StreamSubscription<T>? subscription;
+ var isDone = false;
+ trigger.then((_) {
+ if (isDone) return;
+ isDone = true;
+ subscription?.cancel();
+ controller.close();
+ }, onError: (Object error, StackTrace stackTrace) {
+ if (isDone) return;
+ isDone = true;
+ controller
+ ..addError(error, stackTrace)
+ ..close();
+ });
+
+ controller.onListen = () {
+ if (isDone) return;
+ subscription =
+ listen(controller.add, onError: controller.addError, onDone: () {
+ if (isDone) return;
+ isDone = true;
+ controller.close();
+ });
+ if (!isBroadcast) {
+ controller
+ ..onPause = subscription!.pause
+ ..onResume = subscription!.resume;
+ }
+ controller.onCancel = () {
+ if (isDone) return null;
+ var toCancel = subscription!;
+ subscription = null;
+ return toCancel.cancel();
+ };
+ };
+ return controller.stream;
+ }
+}
diff --git a/pkgs/stream_transform/lib/src/tap.dart b/pkgs/stream_transform/lib/src/tap.dart
new file mode 100644
index 0000000..4b16ab5
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/tap.dart
@@ -0,0 +1,44 @@
+// 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 'from_handlers.dart';
+
+/// A utility to chain extra behavior on a stream.
+extension Tap<T> on Stream<T> {
+ /// Taps into this stream to allow additional handling on a single-subscriber
+ /// stream without first wrapping as a broadcast stream.
+ ///
+ /// The [onValue] callback will be called with every value from this stream
+ /// before it is forwarded to listeners on the resulting stream.
+ /// May be null if only [onError] or [onDone] callbacks are needed.
+ ///
+ /// The [onError] callback will be called with every error from this stream
+ /// before it is forwarded to listeners on the resulting stream.
+ ///
+ /// The [onDone] callback will be called after this stream closes and before
+ /// the resulting stream is closed.
+ ///
+ /// Errors from any of the callbacks are caught and ignored.
+ ///
+ /// The callbacks may not be called until the tapped stream has a listener,
+ /// and may not be called after the listener has canceled the subscription.
+ Stream<T> tap(void Function(T)? onValue,
+ {void Function(Object, StackTrace)? onError,
+ void Function()? onDone}) =>
+ transformByHandlers(onData: (value, sink) {
+ try {
+ onValue?.call(value);
+ } catch (_) {/*Ignore*/}
+ sink.add(value);
+ }, onError: (error, stackTrace, sink) {
+ try {
+ onError?.call(error, stackTrace);
+ } catch (_) {/*Ignore*/}
+ sink.addError(error, stackTrace);
+ }, onDone: (sink) {
+ try {
+ onDone?.call();
+ } catch (_) {/*Ignore*/}
+ sink.close();
+ });
+}
diff --git a/pkgs/stream_transform/lib/src/where.dart b/pkgs/stream_transform/lib/src/where.dart
new file mode 100644
index 0000000..76aa28a
--- /dev/null
+++ b/pkgs/stream_transform/lib/src/where.dart
@@ -0,0 +1,71 @@
+// 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:async';
+
+import 'from_handlers.dart';
+
+/// Utilities to filter events.
+extension Where<T> on Stream<T> {
+ /// Discards events from this stream that are not of type [S].
+ ///
+ /// If the source stream is a broadcast stream the result will be as well.
+ ///
+ /// Errors from the source stream are forwarded directly to the result stream.
+ ///
+ /// [S] should be a subtype of the stream's generic type, otherwise nothing of
+ /// type [S] could possibly be emitted, however there is no static or runtime
+ /// checking that this is the case.
+ Stream<S> whereType<S>() => transformByHandlers(onData: (event, sink) {
+ if (event is S) sink.add(event);
+ });
+
+ /// Discards events from this stream based on an asynchronous [test] callback.
+ ///
+ /// Like [where] but allows the [test] to return a [Future].
+ ///
+ /// Events on the result stream will be emitted in the order that [test]
+ /// completes which may not match the order of this stream.
+ ///
+ /// If the source stream is a broadcast stream the result will be as well.
+ /// When used with a broadcast stream behavior also differs from [where] in
+ /// that the [test] function is only called once per event, rather than once
+ /// per listener per event.
+ ///
+ /// Errors from the source stream are forwarded directly to the result stream.
+ /// Errors from [test] are also forwarded to the result stream.
+ ///
+ /// The result stream will not close until the source stream closes and all
+ /// pending [test] calls have finished.
+ Stream<T> asyncWhere(FutureOr<bool> Function(T) test) {
+ var valuesWaiting = 0;
+ var sourceDone = false;
+ return transformByHandlers(onData: (element, sink) {
+ valuesWaiting++;
+ () async {
+ try {
+ if (await test(element)) sink.add(element);
+ } catch (e, st) {
+ sink.addError(e, st);
+ }
+ valuesWaiting--;
+ if (valuesWaiting <= 0 && sourceDone) sink.close();
+ }();
+ }, onDone: (sink) {
+ sourceDone = true;
+ if (valuesWaiting <= 0) sink.close();
+ });
+ }
+}
+
+extension WhereNotNull<T extends Object> on Stream<T?> {
+ /// Discards `null` events from this stream.
+ ///
+ /// If the source stream is a broadcast stream the result will be as well.
+ ///
+ /// Errors from the source stream are forwarded directly to the result stream.
+ Stream<T> whereNotNull() => transformByHandlers(onData: (event, sink) {
+ if (event != null) sink.add(event);
+ });
+}
diff --git a/pkgs/stream_transform/lib/stream_transform.dart b/pkgs/stream_transform/lib/stream_transform.dart
new file mode 100644
index 0000000..edf4df9
--- /dev/null
+++ b/pkgs/stream_transform/lib/stream_transform.dart
@@ -0,0 +1,15 @@
+// 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.
+
+export 'src/async_expand.dart';
+export 'src/async_map.dart';
+export 'src/combine_latest.dart';
+export 'src/concatenate.dart';
+export 'src/merge.dart';
+export 'src/rate_limit.dart';
+export 'src/scan.dart';
+export 'src/switch.dart';
+export 'src/take_until.dart';
+export 'src/tap.dart';
+export 'src/where.dart';
diff --git a/pkgs/stream_transform/pubspec.yaml b/pkgs/stream_transform/pubspec.yaml
new file mode 100644
index 0000000..1e2298a
--- /dev/null
+++ b/pkgs/stream_transform/pubspec.yaml
@@ -0,0 +1,13 @@
+name: stream_transform
+version: 2.1.1
+description: A collection of utilities to transform and manipulate streams.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/stream_transform
+
+environment:
+ sdk: ^3.1.0
+
+dev_dependencies:
+ async: ^2.5.0
+ dart_flutter_team_lints: ^2.0.0
+ fake_async: ^1.3.0
+ test: ^1.16.0
diff --git a/pkgs/stream_transform/test/async_expand_test.dart b/pkgs/stream_transform/test/async_expand_test.dart
new file mode 100644
index 0000000..8d84300
--- /dev/null
+++ b/pkgs/stream_transform/test/async_expand_test.dart
@@ -0,0 +1,195 @@
+// 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:async';
+
+import 'package:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ test('forwards errors from the convert callback', () async {
+ var errors = <String>[];
+ var source = Stream.fromIterable([1, 2, 3]);
+ source.concurrentAsyncExpand<void>((i) {
+ // ignore: only_throw_errors
+ throw 'Error: $i';
+ }).listen((_) {}, onError: errors.add);
+ await Future<void>(() {});
+ expect(errors, ['Error: 1', 'Error: 2', 'Error: 3']);
+ });
+
+ for (var outerType in streamTypes) {
+ for (var innerType in streamTypes) {
+ group('concurrentAsyncExpand $outerType to $innerType', () {
+ late StreamController<int> outerController;
+ late bool outerCanceled;
+ late List<StreamController<String>> innerControllers;
+ late List<bool> innerCanceled;
+ late List<String> emittedValues;
+ late bool isDone;
+ late List<String> errors;
+ late Stream<String> transformed;
+ late StreamSubscription<String> subscription;
+
+ setUp(() {
+ outerController = createController(outerType)
+ ..onCancel = () {
+ outerCanceled = true;
+ };
+ outerCanceled = false;
+ innerControllers = [];
+ innerCanceled = [];
+ emittedValues = [];
+ errors = [];
+ isDone = false;
+ transformed = outerController.stream.concurrentAsyncExpand((i) {
+ var index = innerControllers.length;
+ innerCanceled.add(false);
+ innerControllers.add(createController<String>(innerType)
+ ..onCancel = () {
+ innerCanceled[index] = true;
+ });
+ return innerControllers.last.stream;
+ });
+ subscription = transformed
+ .listen(emittedValues.add, onError: errors.add, onDone: () {
+ isDone = true;
+ });
+ });
+
+ test('interleaves events from sub streams', () async {
+ outerController
+ ..add(1)
+ ..add(2);
+ await Future<void>(() {});
+ expect(emittedValues, isEmpty);
+ expect(innerControllers, hasLength(2));
+ innerControllers[0].add('First');
+ innerControllers[1].add('Second');
+ innerControllers[0].add('First again');
+ await Future<void>(() {});
+ expect(emittedValues, ['First', 'Second', 'First again']);
+ });
+
+ test('forwards errors from outer stream', () async {
+ outerController.addError('Error');
+ await Future<void>(() {});
+ expect(errors, ['Error']);
+ });
+
+ test('forwards errors from inner streams', () async {
+ outerController
+ ..add(1)
+ ..add(2);
+ await Future<void>(() {});
+ innerControllers[0].addError('Error 1');
+ innerControllers[1].addError('Error 2');
+ await Future<void>(() {});
+ expect(errors, ['Error 1', 'Error 2']);
+ });
+
+ test('can continue handling events after an error in outer stream',
+ () async {
+ outerController
+ ..addError('Error')
+ ..add(1);
+ await Future<void>(() {});
+ innerControllers[0].add('First');
+ await Future<void>(() {});
+ expect(emittedValues, ['First']);
+ expect(errors, ['Error']);
+ });
+
+ test('cancels outer subscription if output canceled', () async {
+ await subscription.cancel();
+ expect(outerCanceled, true);
+ });
+
+ if (outerType != 'broadcast' || innerType != 'single subscription') {
+ // A single subscription inner stream in a broadcast outer stream is
+ // not canceled.
+ test('cancels inner subscriptions if output canceled', () async {
+ outerController
+ ..add(1)
+ ..add(2);
+ await Future<void>(() {});
+ await subscription.cancel();
+ expect(innerCanceled, [true, true]);
+ });
+ }
+
+ test('stays open if any inner stream is still open', () async {
+ outerController.add(1);
+ await outerController.close();
+ await Future<void>(() {});
+ expect(isDone, false);
+ });
+
+ test('stays open if outer stream is still open', () async {
+ outerController.add(1);
+ await Future<void>(() {});
+ await innerControllers[0].close();
+ await Future<void>(() {});
+ expect(isDone, false);
+ });
+
+ test('closes after all inner streams and outer stream close', () async {
+ outerController.add(1);
+ await Future<void>(() {});
+ await innerControllers[0].close();
+ await outerController.close();
+ await Future<void>(() {});
+ expect(isDone, true);
+ });
+
+ if (outerType == 'broadcast') {
+ test('multiple listerns all get values', () async {
+ var otherValues = <String>[];
+ transformed.listen(otherValues.add);
+ outerController.add(1);
+ await Future<void>(() {});
+ innerControllers[0].add('First');
+ await Future<void>(() {});
+ expect(emittedValues, ['First']);
+ expect(otherValues, ['First']);
+ });
+
+ test('multiple listeners get closed', () async {
+ var otherDone = false;
+ transformed.listen(null, onDone: () => otherDone = true);
+ outerController.add(1);
+ await Future<void>(() {});
+ await innerControllers[0].close();
+ await outerController.close();
+ await Future<void>(() {});
+ expect(isDone, true);
+ expect(otherDone, true);
+ });
+
+ test('can cancel and relisten', () async {
+ outerController
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ innerControllers[0].add('First');
+ innerControllers[1].add('Second');
+ await Future(() {});
+ await subscription.cancel();
+ innerControllers[0].add('Ignored');
+ await Future(() {});
+ subscription = transformed.listen(emittedValues.add);
+ innerControllers[0].add('Also ignored');
+ outerController.add(3);
+ await Future(() {});
+ innerControllers[2].add('More');
+ await Future(() {});
+ expect(emittedValues, ['First', 'Second', 'More']);
+ });
+ }
+ });
+ }
+ }
+}
diff --git a/pkgs/stream_transform/test/async_map_buffer_test.dart b/pkgs/stream_transform/test/async_map_buffer_test.dart
new file mode 100644
index 0000000..2386217
--- /dev/null
+++ b/pkgs/stream_transform/test/async_map_buffer_test.dart
@@ -0,0 +1,204 @@
+// 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ late StreamController<int> values;
+ late List<String> emittedValues;
+ late bool valuesCanceled;
+ late bool isDone;
+ late List<String> errors;
+ late Stream<String> transformed;
+ late StreamSubscription<String> subscription;
+
+ Completer<String>? finishWork;
+ List<int>? workArgument;
+
+ /// Represents the async `convert` function and asserts that is is only called
+ /// after the previous iteration has completed.
+ Future<String> work(List<int> values) {
+ expect(finishWork, isNull,
+ reason: 'See $values befor previous work is complete');
+ workArgument = values;
+ finishWork = Completer()
+ ..future.then((_) {
+ workArgument = null;
+ finishWork = null;
+ }).catchError((_) {
+ workArgument = null;
+ finishWork = null;
+ });
+ return finishWork!.future;
+ }
+
+ for (var streamType in streamTypes) {
+ group('asyncMapBuffer for stream type: [$streamType]', () {
+ setUp(() {
+ valuesCanceled = false;
+ values = createController(streamType)
+ ..onCancel = () {
+ valuesCanceled = true;
+ };
+ emittedValues = [];
+ errors = [];
+ isDone = false;
+ finishWork = null;
+ workArgument = null;
+ transformed = values.stream.asyncMapBuffer(work);
+ subscription = transformed
+ .listen(emittedValues.add, onError: errors.add, onDone: () {
+ isDone = true;
+ });
+ });
+
+ test('does not emit before work finishes', () async {
+ values.add(1);
+ await Future(() {});
+ expect(emittedValues, isEmpty);
+ expect(workArgument, [1]);
+ finishWork!.complete('result');
+ await Future(() {});
+ expect(emittedValues, ['result']);
+ });
+
+ test('buffers values while work is ongoing', () async {
+ values.add(1);
+ await Future(() {});
+ values
+ ..add(2)
+ ..add(3);
+ await Future(() {});
+ finishWork!.complete('');
+ await Future(() {});
+ expect(workArgument, [2, 3]);
+ });
+
+ test('forwards errors without waiting for work', () async {
+ values.add(1);
+ await Future(() {});
+ values.addError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+
+ test('forwards errors which occur during the work', () async {
+ values.add(1);
+ await Future(() {});
+ finishWork!.completeError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+
+ test('can continue handling events after an error', () async {
+ values.add(1);
+ await Future(() {});
+ finishWork!.completeError('error');
+ values.add(2);
+ await Future(() {});
+ expect(workArgument, [2]);
+ finishWork!.completeError('another');
+ await Future(() {});
+ expect(errors, ['error', 'another']);
+ });
+
+ test('does not start next work early due to an error in values',
+ () async {
+ values.add(1);
+ await Future(() {});
+ values
+ ..addError('error')
+ ..add(2);
+ await Future(() {});
+ expect(errors, ['error']);
+ // [work] will assert that the second iteration is not called because
+ // the first has not completed.
+ });
+
+ test('cancels value subscription when output canceled', () async {
+ expect(valuesCanceled, false);
+ await subscription.cancel();
+ expect(valuesCanceled, true);
+ });
+
+ test('closes when values end if no work is pending', () async {
+ expect(isDone, false);
+ await values.close();
+ await Future(() {});
+ expect(isDone, true);
+ });
+
+ test('waits for pending work when values close', () async {
+ values.add(1);
+ await Future(() {});
+ expect(isDone, false);
+ values.add(2);
+ await values.close();
+ expect(isDone, false);
+ finishWork!.complete('');
+ await Future(() {});
+ // Still a pending value
+ expect(isDone, false);
+ finishWork!.complete('');
+ await Future(() {});
+ expect(isDone, true);
+ });
+
+ test('forwards errors from values', () async {
+ values.addError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+
+ if (streamType == 'broadcast') {
+ test('multiple listeners all get values', () async {
+ var otherValues = <String>[];
+ transformed.listen(otherValues.add);
+ values.add(1);
+ await Future(() {});
+ finishWork!.complete('result');
+ await Future(() {});
+ expect(emittedValues, ['result']);
+ expect(otherValues, ['result']);
+ });
+
+ test('multiple listeners get done when values end', () async {
+ var otherDone = false;
+ transformed.listen(null, onDone: () => otherDone = true);
+ values.add(1);
+ await Future(() {});
+ await values.close();
+ expect(isDone, false);
+ expect(otherDone, false);
+ finishWork!.complete('');
+ await Future(() {});
+ expect(isDone, true);
+ expect(otherDone, true);
+ });
+
+ test('can cancel and relisten', () async {
+ values.add(1);
+ await Future(() {});
+ finishWork!.complete('first');
+ await Future(() {});
+ await subscription.cancel();
+ values.add(2);
+ await Future(() {});
+ subscription = transformed.listen(emittedValues.add);
+ values.add(3);
+ await Future(() {});
+ expect(workArgument, [3]);
+ finishWork!.complete('second');
+ await Future(() {});
+ expect(emittedValues, ['first', 'second']);
+ });
+ }
+ });
+ }
+}
diff --git a/pkgs/stream_transform/test/async_map_sample_test.dart b/pkgs/stream_transform/test/async_map_sample_test.dart
new file mode 100644
index 0000000..62b1b92
--- /dev/null
+++ b/pkgs/stream_transform/test/async_map_sample_test.dart
@@ -0,0 +1,209 @@
+// 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:async';
+
+import 'package:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ late StreamController<int> values;
+ late List<String> emittedValues;
+ late bool valuesCanceled;
+ late bool isDone;
+ late List<String> errors;
+ late Stream<String> transformed;
+ late StreamSubscription<String> subscription;
+
+ Completer<String>? finishWork;
+ int? workArgument;
+
+ /// Represents the async `convert` function and asserts that is is only called
+ /// after the previous iteration has completed.
+ Future<String> work(int value) {
+ expect(finishWork, isNull,
+ reason: 'See $values befor previous work is complete');
+ workArgument = value;
+ finishWork = Completer()
+ ..future.then((_) {
+ workArgument = null;
+ finishWork = null;
+ }).catchError((_) {
+ workArgument = null;
+ finishWork = null;
+ });
+ return finishWork!.future;
+ }
+
+ for (var streamType in streamTypes) {
+ group('asyncMapSample for stream type: [$streamType]', () {
+ setUp(() {
+ valuesCanceled = false;
+ values = createController(streamType)
+ ..onCancel = () {
+ valuesCanceled = true;
+ };
+ emittedValues = [];
+ errors = [];
+ isDone = false;
+ finishWork = null;
+ workArgument = null;
+ transformed = values.stream.asyncMapSample(work);
+ subscription = transformed
+ .listen(emittedValues.add, onError: errors.add, onDone: () {
+ isDone = true;
+ });
+ });
+
+ test('does not emit before work finishes', () async {
+ values.add(1);
+ await Future(() {});
+ expect(emittedValues, isEmpty);
+ expect(workArgument, 1);
+ finishWork!.complete('result');
+ await Future(() {});
+ expect(emittedValues, ['result']);
+ });
+
+ test('buffers values while work is ongoing', () async {
+ values.add(1);
+ await Future(() {});
+ values
+ ..add(2)
+ ..add(3);
+ await Future(() {});
+ finishWork!.complete('');
+ await Future(() {});
+ expect(workArgument, 3);
+ });
+
+ test('forwards errors without waiting for work', () async {
+ values.add(1);
+ await Future(() {});
+ values.addError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+
+ test('forwards errors which occur during the work', () async {
+ values.add(1);
+ await Future(() {});
+ finishWork!.completeError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+
+ test('can continue handling events after an error', () async {
+ values.add(1);
+ await Future(() {});
+ finishWork!.completeError('error');
+ values.add(2);
+ await Future(() {});
+ expect(workArgument, 2);
+ finishWork!.completeError('another');
+ await Future(() {});
+ expect(errors, ['error', 'another']);
+ });
+
+ test('does not start next work early due to an error in values',
+ () async {
+ values.add(1);
+ await Future(() {});
+ values
+ ..addError('error')
+ ..add(2);
+ await Future(() {});
+ expect(errors, ['error']);
+ // [work] will assert that the second iteration is not called because
+ // the first has not completed.
+ });
+
+ test('cancels value subscription when output canceled', () async {
+ expect(valuesCanceled, false);
+ await subscription.cancel();
+ expect(valuesCanceled, true);
+ });
+
+ test('closes when values end if no work is pending', () async {
+ expect(isDone, false);
+ await values.close();
+ await Future(() {});
+ expect(isDone, true);
+ });
+
+ test('waits for pending work when values close', () async {
+ values.add(1);
+ await Future(() {});
+ expect(isDone, false);
+ values.add(2);
+ await values.close();
+ expect(isDone, false);
+ finishWork!.complete('');
+ await Future(() {});
+ // Still a pending value
+ expect(isDone, false);
+ finishWork!.complete('');
+ await Future(() {});
+ expect(isDone, true);
+ });
+
+ test('forwards errors from values', () async {
+ values.addError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+
+ if (streamType == 'broadcast') {
+ test('multiple listeners all get values', () async {
+ var otherValues = <String>[];
+ transformed.listen(otherValues.add);
+ values.add(1);
+ await Future(() {});
+ finishWork!.complete('result');
+ await Future(() {});
+ expect(emittedValues, ['result']);
+ expect(otherValues, ['result']);
+ });
+
+ test('multiple listeners get done when values end', () async {
+ var otherDone = false;
+ transformed.listen(null, onDone: () => otherDone = true);
+ values.add(1);
+ await Future(() {});
+ await values.close();
+ expect(isDone, false);
+ expect(otherDone, false);
+ finishWork!.complete('');
+ await Future(() {});
+ expect(isDone, true);
+ expect(otherDone, true);
+ });
+
+ test('can cancel and relisten', () async {
+ values.add(1);
+ await Future(() {});
+ finishWork!.complete('first');
+ await Future(() {});
+ await subscription.cancel();
+ values.add(2);
+ await Future(() {});
+ subscription = transformed.listen(emittedValues.add);
+ values.add(3);
+ await Future(() {});
+ expect(workArgument, 3);
+ finishWork!.complete('second');
+ await Future(() {});
+ expect(emittedValues, ['first', 'second']);
+ });
+ }
+ });
+ }
+
+ test('allows nulls', () async {
+ var stream = Stream<int?>.value(null);
+ await stream.asyncMapSample(expectAsync1((_) async {})).drain<void>();
+ });
+}
diff --git a/pkgs/stream_transform/test/async_where_test.dart b/pkgs/stream_transform/test/async_where_test.dart
new file mode 100644
index 0000000..6ea4e76
--- /dev/null
+++ b/pkgs/stream_transform/test/async_where_test.dart
@@ -0,0 +1,90 @@
+// 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('forwards only events that pass the predicate', () async {
+ var values = Stream.fromIterable([1, 2, 3, 4]);
+ var filtered = values.asyncWhere((e) async => e > 2);
+ expect(await filtered.toList(), [3, 4]);
+ });
+
+ test('allows predicates that go through event loop', () async {
+ var values = Stream.fromIterable([1, 2, 3, 4]);
+ var filtered = values.asyncWhere((e) async {
+ await Future(() {});
+ return e > 2;
+ });
+ expect(await filtered.toList(), [3, 4]);
+ });
+
+ test('allows synchronous predicate', () async {
+ var values = Stream.fromIterable([1, 2, 3, 4]);
+ var filtered = values.asyncWhere((e) => e > 2);
+ expect(await filtered.toList(), [3, 4]);
+ });
+
+ test('can result in empty stream', () async {
+ var values = Stream.fromIterable([1, 2, 3, 4]);
+ var filtered = values.asyncWhere((e) => e > 4);
+ expect(await filtered.isEmpty, true);
+ });
+
+ test('forwards values to multiple listeners', () async {
+ var values = StreamController<int>.broadcast();
+ var filtered = values.stream.asyncWhere((e) async => e > 2);
+ var firstValues = <int>[];
+ var secondValues = <int>[];
+ filtered
+ ..listen(firstValues.add)
+ ..listen(secondValues.add);
+ values
+ ..add(1)
+ ..add(2)
+ ..add(3)
+ ..add(4);
+ await Future(() {});
+ expect(firstValues, [3, 4]);
+ expect(secondValues, [3, 4]);
+ });
+
+ test('closes streams with multiple listeners', () async {
+ var values = StreamController<int>.broadcast();
+ var predicate = Completer<bool>();
+ var filtered = values.stream.asyncWhere((_) => predicate.future);
+ var firstDone = false;
+ var secondDone = false;
+ filtered
+ ..listen(null, onDone: () => firstDone = true)
+ ..listen(null, onDone: () => secondDone = true);
+ values.add(1);
+ await values.close();
+ expect(firstDone, false);
+ expect(secondDone, false);
+
+ predicate.complete(true);
+ await Future(() {});
+ expect(firstDone, true);
+ expect(secondDone, true);
+ });
+
+ test('forwards errors emitted by the test callback', () async {
+ var errors = <Object>[];
+ var emitted = <Object>[];
+ var values = Stream.fromIterable([1, 2, 3, 4]);
+ var filtered = values.asyncWhere((e) async {
+ await Future(() {});
+ if (e.isEven) throw Exception('$e');
+ return true;
+ });
+ var done = Completer<Object?>();
+ filtered.listen(emitted.add, onError: errors.add, onDone: done.complete);
+ await done.future;
+ expect(emitted, [1, 3]);
+ expect(errors.map((e) => '$e'), ['Exception: 2', 'Exception: 4']);
+ });
+}
diff --git a/pkgs/stream_transform/test/audit_test.dart b/pkgs/stream_transform/test/audit_test.dart
new file mode 100644
index 0000000..28537db
--- /dev/null
+++ b/pkgs/stream_transform/test/audit_test.dart
@@ -0,0 +1,140 @@
+// 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:fake_async/fake_async.dart';
+import 'package:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ for (var streamType in streamTypes) {
+ group('Stream type [$streamType]', () {
+ late StreamController<int> values;
+ late List<int> emittedValues;
+ late bool valuesCanceled;
+ late bool isDone;
+ late List<String> errors;
+ late Stream<int> transformed;
+ late StreamSubscription<int> subscription;
+
+ group('audit', () {
+ setUp(() {
+ valuesCanceled = false;
+ values = createController(streamType)
+ ..onCancel = () {
+ valuesCanceled = true;
+ };
+ emittedValues = [];
+ errors = [];
+ isDone = false;
+ transformed = values.stream.audit(const Duration(milliseconds: 6));
+ });
+
+ void listen() {
+ subscription = transformed
+ .listen(emittedValues.add, onError: errors.add, onDone: () {
+ isDone = true;
+ });
+ }
+
+ test('cancels values', () async {
+ listen();
+ await subscription.cancel();
+ expect(valuesCanceled, true);
+ });
+
+ test('swallows values that come faster than duration', () {
+ fakeAsync((async) {
+ listen();
+ values
+ ..add(1)
+ ..add(2)
+ ..close();
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [2]);
+ });
+ });
+
+ test('outputs multiple values spaced further than duration', () {
+ fakeAsync((async) {
+ listen();
+ values.add(1);
+ async.elapse(const Duration(milliseconds: 6));
+ values.add(2);
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [1, 2]);
+ });
+ });
+
+ test('waits for pending value to close', () {
+ fakeAsync((async) {
+ listen();
+ values
+ ..add(1)
+ ..close();
+ expect(isDone, false);
+ async.elapse(const Duration(milliseconds: 6));
+ expect(isDone, true);
+ });
+ });
+
+ test('closes output if there are no pending values', () {
+ fakeAsync((async) {
+ listen();
+ values.add(1);
+ async.elapse(const Duration(milliseconds: 6));
+ values
+ ..add(2)
+ ..close();
+ expect(isDone, false);
+ expect(emittedValues, [1]);
+ async.elapse(const Duration(milliseconds: 6));
+ expect(isDone, true);
+ expect(emittedValues, [1, 2]);
+ });
+ });
+
+ test('does not starve output if many values come closer than duration',
+ () {
+ fakeAsync((async) {
+ listen();
+ values.add(1);
+ async.elapse(const Duration(milliseconds: 3));
+ values.add(2);
+ async.elapse(const Duration(milliseconds: 3));
+ values.add(3);
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [2, 3]);
+ });
+ });
+
+ if (streamType == 'broadcast') {
+ test('multiple listeners all get the values', () {
+ fakeAsync((async) {
+ listen();
+ values.add(1);
+ async.elapse(const Duration(milliseconds: 3));
+ values.add(2);
+ var otherValues = <int>[];
+ transformed.listen(otherValues.add);
+ values.add(3);
+ async.elapse(const Duration(milliseconds: 3));
+ values.add(4);
+ async.elapse(const Duration(milliseconds: 3));
+ values
+ ..add(5)
+ ..close();
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [3, 5]);
+ expect(otherValues, [3, 5]);
+ });
+ });
+ }
+ });
+ });
+ }
+}
diff --git a/pkgs/stream_transform/test/buffer_test.dart b/pkgs/stream_transform/test/buffer_test.dart
new file mode 100644
index 0000000..830f555
--- /dev/null
+++ b/pkgs/stream_transform/test/buffer_test.dart
@@ -0,0 +1,305 @@
+// 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ late StreamController<void> trigger;
+ late StreamController<int> values;
+ late List<List<int>> emittedValues;
+ late bool valuesCanceled;
+ late bool triggerCanceled;
+ late bool triggerPaused;
+ late bool isDone;
+ late List<String> errors;
+ late Stream<List<int>> transformed;
+ late StreamSubscription<List<int>> subscription;
+
+ void setUpForStreamTypes(String triggerType, String valuesType,
+ {required bool longPoll}) {
+ valuesCanceled = false;
+ triggerCanceled = false;
+ triggerPaused = false;
+ trigger = createController(triggerType)
+ ..onCancel = () {
+ triggerCanceled = true;
+ };
+ if (triggerType == 'single subscription') {
+ trigger.onPause = () {
+ triggerPaused = true;
+ };
+ }
+ values = createController(valuesType)
+ ..onCancel = () {
+ valuesCanceled = true;
+ };
+ emittedValues = [];
+ errors = [];
+ isDone = false;
+ transformed = values.stream.buffer(trigger.stream, longPoll: longPoll);
+ subscription =
+ transformed.listen(emittedValues.add, onError: errors.add, onDone: () {
+ isDone = true;
+ });
+ }
+
+ for (var triggerType in streamTypes) {
+ for (var valuesType in streamTypes) {
+ group('Trigger type: [$triggerType], Values type: [$valuesType]', () {
+ group('general behavior', () {
+ setUp(() {
+ setUpForStreamTypes(triggerType, valuesType, longPoll: true);
+ });
+
+ test('does not emit before `trigger`', () async {
+ values.add(1);
+ await Future(() {});
+ expect(emittedValues, isEmpty);
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, [
+ [1]
+ ]);
+ });
+
+ test('groups values between trigger', () async {
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ trigger.add(null);
+ values
+ ..add(3)
+ ..add(4);
+ await Future(() {});
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, [
+ [1, 2],
+ [3, 4]
+ ]);
+ });
+
+ test('cancels value subscription when output canceled', () async {
+ expect(valuesCanceled, false);
+ await subscription.cancel();
+ expect(valuesCanceled, true);
+ });
+
+ test('closes when trigger ends', () async {
+ expect(isDone, false);
+ await trigger.close();
+ await Future(() {});
+ expect(isDone, true);
+ });
+
+ test('closes after outputting final values when source closes',
+ () async {
+ expect(isDone, false);
+ values.add(1);
+ await values.close();
+ expect(isDone, false);
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, [
+ [1]
+ ]);
+ expect(isDone, true);
+ });
+
+ test('closes when source closes and there are no buffered', () async {
+ expect(isDone, false);
+ await values.close();
+ await Future(() {});
+ expect(isDone, true);
+ });
+
+ test('forwards errors from trigger', () async {
+ trigger.addError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+
+ test('forwards errors from values', () async {
+ values.addError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+ });
+
+ group('long polling', () {
+ setUp(() {
+ setUpForStreamTypes(triggerType, valuesType, longPoll: true);
+ });
+
+ test('emits immediately if trigger emits before a value', () async {
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, isEmpty);
+ values.add(1);
+ await Future(() {});
+ expect(emittedValues, [
+ [1]
+ ]);
+ });
+
+ test('two triggers in a row - emit buffere then emit next value',
+ () async {
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ trigger
+ ..add(null)
+ ..add(null);
+ await Future(() {});
+ values.add(3);
+ await Future(() {});
+ expect(emittedValues, [
+ [1, 2],
+ [3]
+ ]);
+ });
+
+ test('pre-emptive trigger then trigger after values', () async {
+ trigger.add(null);
+ await Future(() {});
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, [
+ [1],
+ [2]
+ ]);
+ });
+
+ test('multiple pre-emptive triggers, only emits first value',
+ () async {
+ trigger
+ ..add(null)
+ ..add(null);
+ await Future(() {});
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ expect(emittedValues, [
+ [1]
+ ]);
+ });
+
+ test('closes if there is no waiting long poll when source closes',
+ () async {
+ expect(isDone, false);
+ values.add(1);
+ trigger.add(null);
+ await values.close();
+ await Future(() {});
+ expect(isDone, true);
+ });
+
+ test('waits to emit if there waiting long poll when trigger closes',
+ () async {
+ trigger.add(null);
+ await trigger.close();
+ expect(isDone, false);
+ values.add(1);
+ await Future(() {});
+ expect(emittedValues, [
+ [1]
+ ]);
+ expect(isDone, true);
+ });
+ });
+
+ group('immediate polling', () {
+ setUp(() {
+ setUpForStreamTypes(triggerType, valuesType, longPoll: false);
+ });
+
+ test('emits empty list before values', () async {
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, [<int>[]]);
+ });
+
+ test('emits empty list after emitting values', () async {
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ trigger
+ ..add(null)
+ ..add(null);
+ await Future(() {});
+ expect(emittedValues, [
+ [1, 2],
+ <int>[]
+ ]);
+ });
+ });
+ });
+ }
+ }
+
+ test('always cancels trigger if values is singlesubscription', () async {
+ setUpForStreamTypes('broadcast', 'single subscription', longPoll: true);
+ expect(triggerCanceled, false);
+ await subscription.cancel();
+ expect(triggerCanceled, true);
+
+ setUpForStreamTypes('single subscription', 'single subscription',
+ longPoll: true);
+ expect(triggerCanceled, false);
+ await subscription.cancel();
+ expect(triggerCanceled, true);
+ });
+
+ test('cancels trigger if trigger is broadcast', () async {
+ setUpForStreamTypes('broadcast', 'broadcast', longPoll: true);
+ expect(triggerCanceled, false);
+ await subscription.cancel();
+ expect(triggerCanceled, true);
+ });
+
+ test('pauses single subscription trigger for broadcast values', () async {
+ setUpForStreamTypes('single subscription', 'broadcast', longPoll: true);
+ expect(triggerCanceled, false);
+ expect(triggerPaused, false);
+ await subscription.cancel();
+ expect(triggerCanceled, false);
+ expect(triggerPaused, true);
+ });
+
+ for (var triggerType in streamTypes) {
+ test('cancel and relisten with [$triggerType] trigger', () async {
+ setUpForStreamTypes(triggerType, 'broadcast', longPoll: true);
+ values.add(1);
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, [
+ [1]
+ ]);
+ await subscription.cancel();
+ values.add(2);
+ trigger.add(null);
+ await Future(() {});
+ subscription = transformed.listen(emittedValues.add);
+ values.add(3);
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, [
+ [1],
+ [3]
+ ]);
+ });
+ }
+}
diff --git a/pkgs/stream_transform/test/combine_latest_all_test.dart b/pkgs/stream_transform/test/combine_latest_all_test.dart
new file mode 100644
index 0000000..f4b719c
--- /dev/null
+++ b/pkgs/stream_transform/test/combine_latest_all_test.dart
@@ -0,0 +1,166 @@
+// 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:async';
+
+import 'package:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+Future<void> tick() => Future(() {});
+
+void main() {
+ group('combineLatestAll', () {
+ test('emits latest values', () async {
+ final first = StreamController<String>();
+ final second = StreamController<String>();
+ final third = StreamController<String>();
+ final combined = first.stream.combineLatestAll(
+ [second.stream, third.stream]).map((data) => data.join());
+
+ // first: a----b------------------c--------d---|
+ // second: --1---------2-----------------|
+ // third: -------&----------%---|
+ // combined: -------b1&--b2&---b2%---c2%------d2%-|
+
+ expect(combined,
+ emitsInOrder(['b1&', 'b2&', 'b2%', 'c2%', 'd2%', emitsDone]));
+
+ first.add('a');
+ await tick();
+ second.add('1');
+ await tick();
+ first.add('b');
+ await tick();
+ third.add('&');
+ await tick();
+ second.add('2');
+ await tick();
+ third.add('%');
+ await tick();
+ await third.close();
+ await tick();
+ first.add('c');
+ await tick();
+ await second.close();
+ await tick();
+ first.add('d');
+ await tick();
+ await first.close();
+ });
+
+ test('ends if a Stream closes without ever emitting a value', () async {
+ final first = StreamController<String>();
+ final second = StreamController<String>();
+ final combined = first.stream.combineLatestAll([second.stream]);
+
+ // first: -a------b-------|
+ // second: -----|
+ // combined: -----|
+
+ expect(combined, emits(emitsDone));
+
+ first.add('a');
+ await tick();
+ await second.close();
+ await tick();
+ first.add('b');
+ });
+
+ test('forwards errors', () async {
+ final first = StreamController<String>();
+ final second = StreamController<String>();
+ final combined = first.stream
+ .combineLatestAll([second.stream]).map((data) => data.join());
+
+ // first: -a---------|
+ // second: ----1---#
+ // combined: ----a1--#
+
+ expect(combined, emitsThrough(emitsError('doh')));
+
+ first.add('a');
+ await tick();
+ second.add('1');
+ await tick();
+ second.addError('doh');
+ });
+
+ test('ends after both streams have ended', () async {
+ final first = StreamController<String>();
+ final second = StreamController<String>();
+
+ var done = false;
+ first.stream.combineLatestAll([second.stream]).listen(null,
+ onDone: () => done = true);
+
+ // first: -a---|
+ // second: --------1--|
+ // combined: --------a1-|
+
+ first.add('a');
+ await tick();
+ await first.close();
+ await tick();
+
+ expect(done, isFalse);
+
+ second.add('1');
+ await tick();
+ await second.close();
+ await tick();
+
+ expect(done, isTrue);
+ });
+
+ group('broadcast source', () {
+ test('can cancel and relisten to broadcast stream', () async {
+ final first = StreamController<String>.broadcast();
+ final second = StreamController<String>.broadcast();
+ final combined = first.stream
+ .combineLatestAll([second.stream]).map((data) => data.join());
+
+ // first: a------b----------------c------d----e---|
+ // second: --1---------2---3---4------5-|
+ // combined: --a1---b1---b2--b3--b4-----c5--d5---e5--|
+ // sub1: ^-----------------!
+ // sub2: ----------------------^-----------------|
+
+ expect(combined.take(4), emitsInOrder(['a1', 'b1', 'b2', 'b3']));
+
+ first.add('a');
+ await tick();
+ second.add('1');
+ await tick();
+ first.add('b');
+ await tick();
+ second.add('2');
+ await tick();
+ second.add('3');
+ await tick();
+
+ // First subscription is canceled here by .take(4)
+ expect(first.hasListener, isFalse);
+ expect(second.hasListener, isFalse);
+
+ // This emit is thrown away because there are no subscribers
+ second.add('4');
+ await tick();
+
+ expect(combined, emitsInOrder(['c5', 'd5', 'e5', emitsDone]));
+
+ first.add('c');
+ await tick();
+ second.add('5');
+ await tick();
+ await second.close();
+ await tick();
+ first.add('d');
+ await tick();
+ first.add('e');
+ await tick();
+ await first.close();
+ });
+ });
+ });
+}
diff --git a/pkgs/stream_transform/test/combine_latest_test.dart b/pkgs/stream_transform/test/combine_latest_test.dart
new file mode 100644
index 0000000..1985c75
--- /dev/null
+++ b/pkgs/stream_transform/test/combine_latest_test.dart
@@ -0,0 +1,179 @@
+// 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:async';
+
+import 'package:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('combineLatest', () {
+ test('flows through combine callback', () async {
+ var source = StreamController<int>();
+ var other = StreamController<int>();
+ int sum(int a, int b) => a + b;
+
+ var results = <int>[];
+ unawaited(
+ source.stream.combineLatest(other.stream, sum).forEach(results.add));
+
+ source.add(1);
+ await Future(() {});
+ expect(results, isEmpty);
+
+ other.add(2);
+ await Future(() {});
+ expect(results, [3]);
+
+ source.add(3);
+ await Future(() {});
+ expect(results, [3, 5]);
+
+ source.add(4);
+ await Future(() {});
+ expect(results, [3, 5, 6]);
+
+ other.add(5);
+ await Future(() {});
+ expect(results, [3, 5, 6, 9]);
+ });
+
+ test('can combine different typed streams', () async {
+ var source = StreamController<String>();
+ var other = StreamController<int>();
+ String times(String a, int b) => a * b;
+
+ var results = <String>[];
+ unawaited(source.stream
+ .combineLatest(other.stream, times)
+ .forEach(results.add));
+
+ source
+ ..add('a')
+ ..add('b');
+ await Future(() {});
+ expect(results, isEmpty);
+
+ other.add(2);
+ await Future(() {});
+ expect(results, ['bb']);
+
+ other.add(3);
+ await Future(() {});
+ expect(results, ['bb', 'bbb']);
+
+ source.add('c');
+ await Future(() {});
+ expect(results, ['bb', 'bbb', 'ccc']);
+ });
+
+ test('ends after both streams have ended', () async {
+ var source = StreamController<int>();
+ var other = StreamController<int>();
+ int sum(int a, int b) => a + b;
+
+ var done = false;
+ source.stream
+ .combineLatest(other.stream, sum)
+ .listen(null, onDone: () => done = true);
+
+ source.add(1);
+
+ await source.close();
+ await Future(() {});
+ expect(done, false);
+
+ await other.close();
+ await Future(() {});
+ expect(done, true);
+ });
+
+ test('ends if source stream closes without ever emitting a value',
+ () async {
+ var source = const Stream<int>.empty();
+ var other = StreamController<int>();
+
+ int sum(int a, int b) => a + b;
+
+ var done = false;
+ source
+ .combineLatest(other.stream, sum)
+ .listen(null, onDone: () => done = true);
+
+ await Future(() {});
+ // Nothing can ever be emitted on the result, may as well close.
+ expect(done, true);
+ });
+
+ test('ends if other stream closes without ever emitting a value', () async {
+ var source = StreamController<int>();
+ var other = const Stream<int>.empty();
+
+ int sum(int a, int b) => a + b;
+
+ var done = false;
+ source.stream
+ .combineLatest(other, sum)
+ .listen(null, onDone: () => done = true);
+
+ await Future(() {});
+ // Nothing can ever be emitted on the result, may as well close.
+ expect(done, true);
+ });
+
+ test('forwards errors', () async {
+ var source = StreamController<int>();
+ var other = StreamController<int>();
+ int sum(int a, int b) => throw _NumberedException(3);
+
+ var errors = <Object>[];
+ source.stream
+ .combineLatest(other.stream, sum)
+ .listen(null, onError: errors.add);
+
+ source.addError(_NumberedException(1));
+ other.addError(_NumberedException(2));
+
+ source.add(1);
+ other.add(2);
+
+ await Future(() {});
+
+ expect(errors, [_isException(1), _isException(2), _isException(3)]);
+ });
+
+ group('broadcast source', () {
+ test('can cancel and relisten to broadcast stream', () async {
+ var source = StreamController<int>.broadcast();
+ var other = StreamController<int>();
+ int combine(int a, int b) => a + b;
+
+ var emittedValues = <int>[];
+ var transformed = source.stream.combineLatest(other.stream, combine);
+
+ var subscription = transformed.listen(emittedValues.add);
+
+ source.add(1);
+ other.add(2);
+ await Future(() {});
+ expect(emittedValues, [3]);
+
+ await subscription.cancel();
+
+ subscription = transformed.listen(emittedValues.add);
+ source.add(3);
+ await Future(() {});
+ expect(emittedValues, [3, 5]);
+ });
+ });
+ });
+}
+
+class _NumberedException implements Exception {
+ final int id;
+ _NumberedException(this.id);
+}
+
+Matcher _isException(int id) =>
+ const TypeMatcher<_NumberedException>().having((n) => n.id, 'id', id);
diff --git a/pkgs/stream_transform/test/concurrent_async_map_test.dart b/pkgs/stream_transform/test/concurrent_async_map_test.dart
new file mode 100644
index 0000000..1807f9f
--- /dev/null
+++ b/pkgs/stream_transform/test/concurrent_async_map_test.dart
@@ -0,0 +1,157 @@
+// 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ late StreamController<int> controller;
+ late List<String> emittedValues;
+ late bool valuesCanceled;
+ late bool isDone;
+ late List<String> errors;
+ late Stream<String> transformed;
+ late StreamSubscription<String> subscription;
+
+ late List<Completer<String>> finishWork;
+ late List<dynamic> values;
+
+ Future<String> convert(int value) {
+ values.add(value);
+ var completer = Completer<String>();
+ finishWork.add(completer);
+ return completer.future;
+ }
+
+ for (var streamType in streamTypes) {
+ group('concurrentAsyncMap for stream type: [$streamType]', () {
+ setUp(() {
+ valuesCanceled = false;
+ controller = createController(streamType)
+ ..onCancel = () {
+ valuesCanceled = true;
+ };
+ emittedValues = [];
+ errors = [];
+ isDone = false;
+ finishWork = [];
+ values = [];
+ transformed = controller.stream.concurrentAsyncMap(convert);
+ subscription = transformed
+ .listen(emittedValues.add, onError: errors.add, onDone: () {
+ isDone = true;
+ });
+ });
+
+ test('does not emit before convert finishes', () async {
+ controller.add(1);
+ await Future(() {});
+ expect(emittedValues, isEmpty);
+ expect(values, [1]);
+ finishWork.first.complete('result');
+ await Future(() {});
+ expect(emittedValues, ['result']);
+ });
+
+ test('allows calls to convert before the last one finished', () async {
+ controller
+ ..add(1)
+ ..add(2)
+ ..add(3);
+ await Future(() {});
+ expect(values, [1, 2, 3]);
+ });
+
+ test('forwards errors directly without waiting for previous convert',
+ () async {
+ controller.add(1);
+ await Future(() {});
+ controller.addError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+
+ test('forwards errors which occur during the convert', () async {
+ controller.add(1);
+ await Future(() {});
+ finishWork.first.completeError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+
+ test('can continue handling events after an error', () async {
+ controller.add(1);
+ await Future(() {});
+ finishWork[0].completeError('error');
+ controller.add(2);
+ await Future(() {});
+ expect(values, [1, 2]);
+ finishWork[1].completeError('another');
+ await Future(() {});
+ expect(errors, ['error', 'another']);
+ });
+
+ test('cancels value subscription when output canceled', () async {
+ expect(valuesCanceled, false);
+ await subscription.cancel();
+ expect(valuesCanceled, true);
+ });
+
+ test('closes when values end if no conversion is pending', () async {
+ expect(isDone, false);
+ await controller.close();
+ await Future(() {});
+ expect(isDone, true);
+ });
+
+ if (streamType == 'broadcast') {
+ test('multiple listeners all get values', () async {
+ var otherValues = <String>[];
+ transformed.listen(otherValues.add);
+ controller.add(1);
+ await Future(() {});
+ finishWork.first.complete('result');
+ await Future(() {});
+ expect(emittedValues, ['result']);
+ expect(otherValues, ['result']);
+ });
+
+ test('multiple listeners get done when values end', () async {
+ var otherDone = false;
+ transformed.listen(null, onDone: () => otherDone = true);
+ controller.add(1);
+ await Future(() {});
+ await controller.close();
+ expect(isDone, false);
+ expect(otherDone, false);
+ finishWork.first.complete('');
+ await Future(() {});
+ expect(isDone, true);
+ expect(otherDone, true);
+ });
+
+ test('can cancel and relisten', () async {
+ controller.add(1);
+ await Future(() {});
+ finishWork.first.complete('first');
+ await Future(() {});
+ await subscription.cancel();
+ controller.add(2);
+ await Future(() {});
+ subscription = transformed.listen(emittedValues.add);
+ controller.add(3);
+ await Future(() {});
+ expect(values, [1, 3]);
+ finishWork[1].complete('second');
+ await Future(() {});
+ expect(emittedValues, ['first', 'second']);
+ });
+ }
+ });
+ }
+}
diff --git a/pkgs/stream_transform/test/debounce_test.dart b/pkgs/stream_transform/test/debounce_test.dart
new file mode 100644
index 0000000..19de055
--- /dev/null
+++ b/pkgs/stream_transform/test/debounce_test.dart
@@ -0,0 +1,310 @@
+// 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:fake_async/fake_async.dart';
+import 'package:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ for (var streamType in streamTypes) {
+ group('Stream type [$streamType]', () {
+ group('debounce - trailing', () {
+ late StreamController<int> values;
+ late List<int> emittedValues;
+ late bool valuesCanceled;
+ late bool isDone;
+ late List<String> errors;
+ late StreamSubscription<int> subscription;
+ late Stream<int> transformed;
+
+ setUp(() async {
+ valuesCanceled = false;
+ values = createController(streamType)
+ ..onCancel = () {
+ valuesCanceled = true;
+ };
+ emittedValues = [];
+ errors = [];
+ isDone = false;
+ transformed = values.stream.debounce(const Duration(milliseconds: 5));
+ });
+
+ void listen() {
+ subscription = transformed
+ .listen(emittedValues.add, onError: errors.add, onDone: () {
+ isDone = true;
+ });
+ }
+
+ test('cancels values', () async {
+ listen();
+ await subscription.cancel();
+ expect(valuesCanceled, true);
+ });
+
+ test('swallows values that come faster than duration', () {
+ fakeAsync((async) {
+ listen();
+ values
+ ..add(1)
+ ..add(2)
+ ..close();
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [2]);
+ });
+ });
+
+ test('outputs multiple values spaced further than duration', () {
+ fakeAsync((async) {
+ listen();
+ values.add(1);
+ async.elapse(const Duration(milliseconds: 6));
+ values.add(2);
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [1, 2]);
+ });
+ });
+
+ test('waits for pending value to close', () {
+ fakeAsync((async) {
+ listen();
+ values.add(1);
+ async.elapse(const Duration(milliseconds: 6));
+ values.close();
+ async.flushMicrotasks();
+ expect(isDone, true);
+ });
+ });
+
+ test('closes output if there are no pending values', () {
+ fakeAsync((async) {
+ listen();
+ values.add(1);
+ async.elapse(const Duration(milliseconds: 6));
+ values
+ ..add(2)
+ ..close();
+ async.flushMicrotasks();
+ expect(isDone, false);
+ async.elapse(const Duration(milliseconds: 6));
+ expect(isDone, true);
+ });
+ });
+
+ if (streamType == 'broadcast') {
+ test('multiple listeners all get values', () {
+ fakeAsync((async) {
+ listen();
+ var otherValues = <int>[];
+ transformed.listen(otherValues.add);
+ values
+ ..add(1)
+ ..add(2);
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [2]);
+ expect(otherValues, [2]);
+ });
+ });
+ }
+ });
+
+ group('debounce - leading', () {
+ late StreamController<int> values;
+ late List<int> emittedValues;
+ late Stream<int> transformed;
+ late bool isDone;
+
+ setUp(() async {
+ values = createController(streamType);
+ emittedValues = [];
+ isDone = false;
+ transformed = values.stream.debounce(const Duration(milliseconds: 5),
+ leading: true, trailing: false);
+ });
+
+ void listen() {
+ transformed.listen(emittedValues.add, onDone: () {
+ isDone = true;
+ });
+ }
+
+ test('swallows values that come faster than duration', () async {
+ listen();
+ values
+ ..add(1)
+ ..add(2);
+ await values.close();
+ expect(emittedValues, [1]);
+ });
+
+ test('outputs multiple values spaced further than duration', () {
+ fakeAsync((async) {
+ listen();
+ values.add(1);
+ async.elapse(const Duration(milliseconds: 6));
+ values.add(2);
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [1, 2]);
+ });
+ });
+
+ if (streamType == 'broadcast') {
+ test('multiple listeners all get values', () {
+ fakeAsync((async) {
+ listen();
+ var otherValues = <int>[];
+ transformed.listen(otherValues.add);
+ values
+ ..add(1)
+ ..add(2);
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [1]);
+ expect(otherValues, [1]);
+ });
+ });
+ }
+
+ test('closes output immediately if not waiting for trailing value',
+ () async {
+ listen();
+ values.add(1);
+ await values.close();
+ expect(isDone, true);
+ });
+ });
+
+ group('debounce - leading and trailing', () {
+ late StreamController<int> values;
+ late List<int> emittedValues;
+ late Stream<int> transformed;
+
+ setUp(() async {
+ values = createController(streamType);
+ emittedValues = [];
+ transformed = values.stream.debounce(const Duration(milliseconds: 5),
+ leading: true, trailing: true);
+ });
+ void listen() {
+ transformed.listen(emittedValues.add);
+ }
+
+ test('swallows values that come faster than duration', () {
+ fakeAsync((async) {
+ listen();
+ values
+ ..add(1)
+ ..add(2)
+ ..add(3)
+ ..close();
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [1, 3]);
+ });
+ });
+
+ test('outputs multiple values spaced further than duration', () {
+ fakeAsync((async) {
+ listen();
+ values.add(1);
+ async.elapse(const Duration(milliseconds: 6));
+ values.add(2);
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [1, 2]);
+ });
+ });
+
+ if (streamType == 'broadcast') {
+ test('multiple listeners all get values', () {
+ fakeAsync((async) {
+ listen();
+ var otherValues = <int>[];
+ transformed.listen(otherValues.add);
+ values
+ ..add(1)
+ ..add(2);
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [1, 2]);
+ expect(otherValues, [1, 2]);
+ });
+ });
+ }
+ });
+
+ group('debounceBuffer', () {
+ late StreamController<int> values;
+ late List<List<int>> emittedValues;
+ late List<String> errors;
+ late Stream<List<int>> transformed;
+
+ setUp(() async {
+ values = createController(streamType);
+ emittedValues = [];
+ errors = [];
+ transformed =
+ values.stream.debounceBuffer(const Duration(milliseconds: 5));
+ });
+ void listen() {
+ transformed.listen(emittedValues.add, onError: errors.add);
+ }
+
+ test('Emits all values as a list', () {
+ fakeAsync((async) {
+ listen();
+ values
+ ..add(1)
+ ..add(2)
+ ..close();
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [
+ [1, 2]
+ ]);
+ });
+ });
+
+ test('separate lists for multiple values spaced further than duration',
+ () {
+ fakeAsync((async) {
+ listen();
+ values.add(1);
+ async.elapse(const Duration(milliseconds: 6));
+ values.add(2);
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [
+ [1],
+ [2]
+ ]);
+ });
+ });
+
+ if (streamType == 'broadcast') {
+ test('multiple listeners all get values', () {
+ fakeAsync((async) {
+ listen();
+ var otherValues = <List<int>>[];
+ transformed.listen(otherValues.add);
+ values
+ ..add(1)
+ ..add(2);
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [
+ [1, 2]
+ ]);
+ expect(otherValues, [
+ [1, 2]
+ ]);
+ });
+ });
+ }
+ });
+ });
+ }
+ test('allows nulls', () async {
+ final values = Stream<int?>.fromIterable([null]);
+ final transformed = values.debounce(const Duration(milliseconds: 1));
+ expect(await transformed.toList(), [null]);
+ });
+}
diff --git a/pkgs/stream_transform/test/followd_by_test.dart b/pkgs/stream_transform/test/followd_by_test.dart
new file mode 100644
index 0000000..d600d13
--- /dev/null
+++ b/pkgs/stream_transform/test/followd_by_test.dart
@@ -0,0 +1,159 @@
+// 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ for (var firstType in streamTypes) {
+ for (var secondType in streamTypes) {
+ group('followedBy [$firstType] with [$secondType]', () {
+ late StreamController<int> first;
+ late StreamController<int> second;
+
+ late List<int> emittedValues;
+ late bool firstCanceled;
+ late bool secondCanceled;
+ late bool secondListened;
+ late bool isDone;
+ late List<String> errors;
+ late Stream<int> transformed;
+ late StreamSubscription<int> subscription;
+
+ setUp(() async {
+ firstCanceled = false;
+ secondCanceled = false;
+ secondListened = false;
+ first = createController(firstType)
+ ..onCancel = () {
+ firstCanceled = true;
+ };
+ second = createController(secondType)
+ ..onCancel = () {
+ secondCanceled = true;
+ }
+ ..onListen = () {
+ secondListened = true;
+ };
+ emittedValues = [];
+ errors = [];
+ isDone = false;
+ transformed = first.stream.followedBy(second.stream);
+ subscription = transformed
+ .listen(emittedValues.add, onError: errors.add, onDone: () {
+ isDone = true;
+ });
+ });
+
+ test('adds all values from both streams', () async {
+ first
+ ..add(1)
+ ..add(2);
+ await first.close();
+ await Future(() {});
+ second
+ ..add(3)
+ ..add(4);
+ await Future(() {});
+ expect(emittedValues, [1, 2, 3, 4]);
+ });
+
+ test('Does not listen to second stream before first stream finishes',
+ () async {
+ expect(secondListened, false);
+ await first.close();
+ expect(secondListened, true);
+ });
+
+ test('closes stream after both inputs close', () async {
+ await first.close();
+ await second.close();
+ expect(isDone, true);
+ });
+
+ test('cancels any type of first stream on cancel', () async {
+ await subscription.cancel();
+ expect(firstCanceled, true);
+ });
+
+ if (firstType == 'single subscription') {
+ test(
+ 'cancels any type of second stream on cancel if first is '
+ 'broadcast', () async {
+ await first.close();
+ await subscription.cancel();
+ expect(secondCanceled, true);
+ });
+
+ if (secondType == 'broadcast') {
+ test('can pause and resume during second stream - dropping values',
+ () async {
+ await first.close();
+ subscription.pause();
+ second.add(1);
+ await Future(() {});
+ subscription.resume();
+ second.add(2);
+ await Future(() {});
+ expect(emittedValues, [2]);
+ });
+ } else {
+ test('can pause and resume during second stream - buffering values',
+ () async {
+ await first.close();
+ subscription.pause();
+ second.add(1);
+ await Future(() {});
+ subscription.resume();
+ second.add(2);
+ await Future(() {});
+ expect(emittedValues, [1, 2]);
+ });
+ }
+ }
+
+ if (firstType == 'broadcast') {
+ test('can cancel and relisten during first stream', () async {
+ await subscription.cancel();
+ first.add(1);
+ subscription = transformed.listen(emittedValues.add);
+ first.add(2);
+ await Future(() {});
+ expect(emittedValues, [2]);
+ });
+
+ test('can cancel and relisten during second stream', () async {
+ await first.close();
+ await subscription.cancel();
+ second.add(2);
+ await Future(() {});
+ subscription = transformed.listen(emittedValues.add);
+ second.add(3);
+ await Future(() {});
+ expect(emittedValues, [3]);
+ });
+
+ test('forwards values to multiple listeners', () async {
+ var otherValues = <int>[];
+ transformed.listen(otherValues.add);
+ first.add(1);
+ await first.close();
+ second.add(2);
+ await Future(() {});
+ var thirdValues = <int>[];
+ transformed.listen(thirdValues.add);
+ second.add(3);
+ await Future(() {});
+ expect(emittedValues, [1, 2, 3]);
+ expect(otherValues, [1, 2, 3]);
+ expect(thirdValues, [3]);
+ });
+ }
+ });
+ }
+ }
+}
diff --git a/pkgs/stream_transform/test/from_handlers_test.dart b/pkgs/stream_transform/test/from_handlers_test.dart
new file mode 100644
index 0000000..694199c
--- /dev/null
+++ b/pkgs/stream_transform/test/from_handlers_test.dart
@@ -0,0 +1,183 @@
+// 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:stream_transform/src/from_handlers.dart';
+import 'package:test/test.dart';
+
+void main() {
+ late StreamController<int> values;
+ late List<int> emittedValues;
+ late bool valuesCanceled;
+ late bool isDone;
+ late List<String> errors;
+ late Stream<int> transformed;
+ late StreamSubscription<int> subscription;
+
+ void setUpForController(StreamController<int> controller,
+ Stream<int> Function(Stream<int>) transform) {
+ valuesCanceled = false;
+ values = controller
+ ..onCancel = () {
+ valuesCanceled = true;
+ };
+ emittedValues = [];
+ errors = [];
+ isDone = false;
+ transformed = transform(values.stream);
+ subscription =
+ transformed.listen(emittedValues.add, onError: errors.add, onDone: () {
+ isDone = true;
+ });
+ }
+
+ group('default from_handlers', () {
+ group('Single subscription stream', () {
+ setUp(() {
+ setUpForController(StreamController(),
+ (s) => s.transformByHandlers(onData: (e, sink) => sink.add(e)));
+ });
+
+ test('has correct stream type', () {
+ expect(transformed.isBroadcast, false);
+ });
+
+ test('forwards values', () async {
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ expect(emittedValues, [1, 2]);
+ });
+
+ test('forwards errors', () async {
+ values.addError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+
+ test('forwards done', () async {
+ await values.close();
+ expect(isDone, true);
+ });
+
+ test('forwards cancel', () async {
+ await subscription.cancel();
+ expect(valuesCanceled, true);
+ });
+ });
+
+ group('broadcast stream with muliple listeners', () {
+ late List<int> emittedValues2;
+ late List<String> errors2;
+ late bool isDone2;
+ late StreamSubscription<int> subscription2;
+
+ setUp(() {
+ setUpForController(StreamController.broadcast(),
+ (s) => s.transformByHandlers(onData: (e, sink) => sink.add(e)));
+ emittedValues2 = [];
+ errors2 = [];
+ isDone2 = false;
+ subscription2 = transformed
+ .listen(emittedValues2.add, onError: errors2.add, onDone: () {
+ isDone2 = true;
+ });
+ });
+
+ test('has correct stream type', () {
+ expect(transformed.isBroadcast, true);
+ });
+
+ test('forwards values', () async {
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ expect(emittedValues, [1, 2]);
+ expect(emittedValues2, [1, 2]);
+ });
+
+ test('forwards errors', () async {
+ values.addError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ expect(errors2, ['error']);
+ });
+
+ test('forwards done', () async {
+ await values.close();
+ expect(isDone, true);
+ expect(isDone2, true);
+ });
+
+ test('forwards cancel', () async {
+ await subscription.cancel();
+ expect(valuesCanceled, false);
+ await subscription2.cancel();
+ expect(valuesCanceled, true);
+ });
+ });
+ });
+
+ group('custom handlers', () {
+ group('single subscription', () {
+ setUp(() async {
+ setUpForController(
+ StreamController(),
+ (s) => s.transformByHandlers(onData: (value, sink) {
+ sink.add(value + 1);
+ }));
+ });
+ test('uses transform from handleData', () async {
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ expect(emittedValues, [2, 3]);
+ });
+ });
+
+ group('broadcast stream with multiple listeners', () {
+ late int dataCallCount;
+ late int doneCallCount;
+ late int errorCallCount;
+
+ setUp(() async {
+ dataCallCount = 0;
+ doneCallCount = 0;
+ errorCallCount = 0;
+ setUpForController(
+ StreamController.broadcast(),
+ (s) => s.transformByHandlers(onData: (value, sink) {
+ dataCallCount++;
+ }, onError: (error, stackTrace, sink) {
+ errorCallCount++;
+ sink.addError(error, stackTrace);
+ }, onDone: (sink) {
+ doneCallCount++;
+ }));
+ transformed.listen((_) {}, onError: (_, __) {});
+ });
+
+ test('handles data once', () async {
+ values.add(1);
+ await Future(() {});
+ expect(dataCallCount, 1);
+ });
+
+ test('handles done once', () async {
+ await values.close();
+ expect(doneCallCount, 1);
+ });
+
+ test('handles errors once', () async {
+ values.addError('error');
+ await Future(() {});
+ expect(errorCallCount, 1);
+ });
+ });
+ });
+}
diff --git a/pkgs/stream_transform/test/merge_test.dart b/pkgs/stream_transform/test/merge_test.dart
new file mode 100644
index 0000000..ecbf97f
--- /dev/null
+++ b/pkgs/stream_transform/test/merge_test.dart
@@ -0,0 +1,140 @@
+// 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('merge', () {
+ test('includes all values', () async {
+ var first = Stream.fromIterable([1, 2, 3]);
+ var second = Stream.fromIterable([4, 5, 6]);
+ var allValues = await first.merge(second).toList();
+ expect(allValues, containsAllInOrder([1, 2, 3]));
+ expect(allValues, containsAllInOrder([4, 5, 6]));
+ expect(allValues, hasLength(6));
+ });
+
+ test('cancels both sources', () async {
+ var firstCanceled = false;
+ var first = StreamController<int>()
+ ..onCancel = () {
+ firstCanceled = true;
+ };
+ var secondCanceled = false;
+ var second = StreamController<int>()
+ ..onCancel = () {
+ secondCanceled = true;
+ };
+ var subscription = first.stream.merge(second.stream).listen((_) {});
+ await subscription.cancel();
+ expect(firstCanceled, true);
+ expect(secondCanceled, true);
+ });
+
+ test('completes when both sources complete', () async {
+ var first = StreamController<int>();
+ var second = StreamController<int>();
+ var isDone = false;
+ first.stream.merge(second.stream).listen((_) {}, onDone: () {
+ isDone = true;
+ });
+ await first.close();
+ expect(isDone, false);
+ await second.close();
+ expect(isDone, true);
+ });
+
+ test('can cancel and relisten to broadcast stream', () async {
+ var first = StreamController<int>.broadcast();
+ var second = StreamController<int>();
+ var emittedValues = <int>[];
+ var transformed = first.stream.merge(second.stream);
+ var subscription = transformed.listen(emittedValues.add);
+ first.add(1);
+ second.add(2);
+ await Future(() {});
+ expect(emittedValues, contains(1));
+ expect(emittedValues, contains(2));
+ await subscription.cancel();
+ emittedValues = [];
+ subscription = transformed.listen(emittedValues.add);
+ first.add(3);
+ second.add(4);
+ await Future(() {});
+ expect(emittedValues, contains(3));
+ expect(emittedValues, contains(4));
+ });
+ });
+
+ group('mergeAll', () {
+ test('includes all values', () async {
+ var first = Stream.fromIterable([1, 2, 3]);
+ var second = Stream.fromIterable([4, 5, 6]);
+ var third = Stream.fromIterable([7, 8, 9]);
+ var allValues = await first.mergeAll([second, third]).toList();
+ expect(allValues, containsAllInOrder([1, 2, 3]));
+ expect(allValues, containsAllInOrder([4, 5, 6]));
+ expect(allValues, containsAllInOrder([7, 8, 9]));
+ expect(allValues, hasLength(9));
+ });
+
+ test('handles mix of broadcast and single-subscription', () async {
+ var firstCanceled = false;
+ var first = StreamController<int>.broadcast()
+ ..onCancel = () {
+ firstCanceled = true;
+ };
+ var secondBroadcastCanceled = false;
+ var secondBroadcast = StreamController<int>.broadcast()
+ ..onCancel = () {
+ secondBroadcastCanceled = true;
+ };
+ var secondSingleCanceled = false;
+ var secondSingle = StreamController<int>()
+ ..onCancel = () {
+ secondSingleCanceled = true;
+ };
+
+ var merged =
+ first.stream.mergeAll([secondBroadcast.stream, secondSingle.stream]);
+
+ var firstListenerValues = <int>[];
+ var secondListenerValues = <int>[];
+
+ var firstSubscription = merged.listen(firstListenerValues.add);
+ var secondSubscription = merged.listen(secondListenerValues.add);
+
+ first.add(1);
+ secondBroadcast.add(2);
+ secondSingle.add(3);
+
+ await Future(() {});
+ await firstSubscription.cancel();
+
+ expect(firstCanceled, false);
+ expect(secondBroadcastCanceled, false);
+ expect(secondSingleCanceled, false);
+
+ first.add(4);
+ secondBroadcast.add(5);
+ secondSingle.add(6);
+
+ await Future(() {});
+ await secondSubscription.cancel();
+
+ await Future(() {});
+ expect(firstCanceled, true);
+ expect(secondBroadcastCanceled, true);
+ expect(secondSingleCanceled, false,
+ reason: 'Single subscription streams merged into broadcast streams '
+ 'are not canceled');
+
+ expect(firstListenerValues, [1, 2, 3]);
+ expect(secondListenerValues, [1, 2, 3, 4, 5, 6]);
+ });
+ });
+}
diff --git a/pkgs/stream_transform/test/sample_test.dart b/pkgs/stream_transform/test/sample_test.dart
new file mode 100644
index 0000000..66ca09d
--- /dev/null
+++ b/pkgs/stream_transform/test/sample_test.dart
@@ -0,0 +1,291 @@
+// 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ late StreamController<void> trigger;
+ late StreamController<int> values;
+ late List<int> emittedValues;
+ late bool valuesCanceled;
+ late bool triggerCanceled;
+ late bool triggerPaused;
+ late bool isDone;
+ late List<String> errors;
+ late Stream<int> transformed;
+ late StreamSubscription<int> subscription;
+
+ void setUpForStreamTypes(String triggerType, String valuesType,
+ {required bool longPoll}) {
+ valuesCanceled = false;
+ triggerCanceled = false;
+ triggerPaused = false;
+ trigger = createController(triggerType)
+ ..onCancel = () {
+ triggerCanceled = true;
+ };
+ if (triggerType == 'single subscription') {
+ trigger.onPause = () {
+ triggerPaused = true;
+ };
+ }
+ values = createController(valuesType)
+ ..onCancel = () {
+ valuesCanceled = true;
+ };
+ emittedValues = [];
+ errors = [];
+ isDone = false;
+ transformed = values.stream.sample(trigger.stream, longPoll: longPoll);
+ subscription =
+ transformed.listen(emittedValues.add, onError: errors.add, onDone: () {
+ isDone = true;
+ });
+ }
+
+ for (var triggerType in streamTypes) {
+ for (var valuesType in streamTypes) {
+ group('Trigger type: [$triggerType], Values type: [$valuesType]', () {
+ group('general behavior', () {
+ setUp(() {
+ setUpForStreamTypes(triggerType, valuesType, longPoll: true);
+ });
+
+ test('does not emit before `trigger`', () async {
+ values.add(1);
+ await Future(() {});
+ expect(emittedValues, isEmpty);
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, [1]);
+ });
+
+ test('keeps most recent event between triggers', () async {
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ trigger.add(null);
+ values
+ ..add(3)
+ ..add(4);
+ await Future(() {});
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, [2, 4]);
+ });
+
+ test('cancels value subscription when output canceled', () async {
+ expect(valuesCanceled, false);
+ await subscription.cancel();
+ expect(valuesCanceled, true);
+ });
+
+ test('closes when trigger ends', () async {
+ expect(isDone, false);
+ await trigger.close();
+ await Future(() {});
+ expect(isDone, true);
+ });
+
+ test('closes after outputting final values when source closes',
+ () async {
+ expect(isDone, false);
+ values.add(1);
+ await values.close();
+ expect(isDone, false);
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, [1]);
+ expect(isDone, true);
+ });
+
+ test('closes when source closes and there is no pending', () async {
+ expect(isDone, false);
+ await values.close();
+ await Future(() {});
+ expect(isDone, true);
+ });
+
+ test('forwards errors from trigger', () async {
+ trigger.addError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+
+ test('forwards errors from values', () async {
+ values.addError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+ });
+
+ group('long polling', () {
+ setUp(() {
+ setUpForStreamTypes(triggerType, valuesType, longPoll: true);
+ });
+
+ test('emits immediately if trigger emits before a value', () async {
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, isEmpty);
+ values.add(1);
+ await Future(() {});
+ expect(emittedValues, [1]);
+ });
+
+ test('two triggers in a row - emit buffere then emit next value',
+ () async {
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ trigger
+ ..add(null)
+ ..add(null);
+ await Future(() {});
+ values.add(3);
+ await Future(() {});
+ expect(emittedValues, [2, 3]);
+ });
+
+ test('pre-emptive trigger then trigger after values', () async {
+ trigger.add(null);
+ await Future(() {});
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, [1, 2]);
+ });
+
+ test('multiple pre-emptive triggers, only emits first value',
+ () async {
+ trigger
+ ..add(null)
+ ..add(null);
+ await Future(() {});
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ expect(emittedValues, [1]);
+ });
+
+ test('closes if there is no waiting long poll when source closes',
+ () async {
+ expect(isDone, false);
+ values.add(1);
+ trigger.add(null);
+ await values.close();
+ await Future(() {});
+ expect(isDone, true);
+ });
+
+ test('waits to emit if there waiting long poll when trigger closes',
+ () async {
+ trigger.add(null);
+ await trigger.close();
+ expect(isDone, false);
+ values.add(1);
+ await Future(() {});
+ expect(emittedValues, [1]);
+ expect(isDone, true);
+ });
+ });
+
+ group('immediate polling', () {
+ setUp(() {
+ setUpForStreamTypes(triggerType, valuesType, longPoll: false);
+ });
+
+ test('ignores trigger before values', () async {
+ trigger.add(null);
+ await Future(() {});
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, [2]);
+ });
+
+ test('ignores trigger if no pending values', () async {
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ trigger
+ ..add(null)
+ ..add(null);
+ await Future(() {});
+ values
+ ..add(3)
+ ..add(4);
+ await Future(() {});
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, [2, 4]);
+ });
+ });
+ });
+ }
+ }
+
+ test('always cancels trigger if values is singlesubscription', () async {
+ setUpForStreamTypes('broadcast', 'single subscription', longPoll: true);
+ expect(triggerCanceled, false);
+ await subscription.cancel();
+ expect(triggerCanceled, true);
+
+ setUpForStreamTypes('single subscription', 'single subscription',
+ longPoll: true);
+ expect(triggerCanceled, false);
+ await subscription.cancel();
+ expect(triggerCanceled, true);
+ });
+
+ test('cancels trigger if trigger is broadcast', () async {
+ setUpForStreamTypes('broadcast', 'broadcast', longPoll: true);
+ expect(triggerCanceled, false);
+ await subscription.cancel();
+ expect(triggerCanceled, true);
+ });
+
+ test('pauses single subscription trigger for broadcast values', () async {
+ setUpForStreamTypes('single subscription', 'broadcast', longPoll: true);
+ expect(triggerCanceled, false);
+ expect(triggerPaused, false);
+ await subscription.cancel();
+ expect(triggerCanceled, false);
+ expect(triggerPaused, true);
+ });
+
+ for (var triggerType in streamTypes) {
+ test('cancel and relisten with [$triggerType] trigger', () async {
+ setUpForStreamTypes(triggerType, 'broadcast', longPoll: true);
+ values.add(1);
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, [1]);
+ await subscription.cancel();
+ values.add(2);
+ trigger.add(null);
+ await Future(() {});
+ subscription = transformed.listen(emittedValues.add);
+ values.add(3);
+ trigger.add(null);
+ await Future(() {});
+ expect(emittedValues, [1, 3]);
+ });
+ }
+}
diff --git a/pkgs/stream_transform/test/scan_test.dart b/pkgs/stream_transform/test/scan_test.dart
new file mode 100644
index 0000000..3c749e7
--- /dev/null
+++ b/pkgs/stream_transform/test/scan_test.dart
@@ -0,0 +1,109 @@
+// 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('Scan', () {
+ test('produces intermediate values', () async {
+ var source = Stream.fromIterable([1, 2, 3, 4]);
+ int sum(int x, int y) => x + y;
+ var result = await source.scan(0, sum).toList();
+
+ expect(result, [1, 3, 6, 10]);
+ });
+
+ test('can create a broadcast stream', () {
+ var source = StreamController<int>.broadcast();
+
+ var transformed = source.stream.scan(null, (_, __) {});
+
+ expect(transformed.isBroadcast, true);
+ });
+
+ test('forwards errors from source', () async {
+ var source = StreamController<int>();
+
+ int sum(int x, int y) => x + y;
+
+ var errors = <Object>[];
+
+ source.stream.scan(0, sum).listen(null, onError: errors.add);
+
+ source.addError(StateError('fail'));
+ await Future(() {});
+
+ expect(errors, [isStateError]);
+ });
+
+ group('with async combine', () {
+ test('returns a Stream of non-futures', () async {
+ var source = Stream.fromIterable([1, 2, 3, 4]);
+ Future<int> sum(int x, int y) async => x + y;
+ var result = await source.scan(0, sum).toList();
+
+ expect(result, [1, 3, 6, 10]);
+ });
+
+ test('can return a Stream of futures when specified', () async {
+ var source = Stream.fromIterable([1, 2]);
+ Future<int> sum(Future<int> x, int y) async => (await x) + y;
+ var result =
+ await source.scan<Future<int>>(Future.value(0), sum).toList();
+
+ expect(result, [
+ const TypeMatcher<Future<void>>(),
+ const TypeMatcher<Future<void>>()
+ ]);
+ expect(await result.wait, [1, 3]);
+ });
+
+ test('does not call for subsequent values while waiting', () async {
+ var source = StreamController<int>();
+
+ var calledWith = <int>[];
+ var block = Completer<void>();
+ Future<int> combine(int x, int y) async {
+ calledWith.add(y);
+ await block.future;
+ return x + y;
+ }
+
+ var results = <int>[];
+
+ unawaited(source.stream.scan(0, combine).forEach(results.add));
+
+ source
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ expect(calledWith, [1]);
+ expect(results, isEmpty);
+
+ block.complete();
+ await Future(() {});
+ expect(calledWith, [1, 2]);
+ expect(results, [1, 3]);
+ });
+
+ test('forwards async errors', () async {
+ var source = StreamController<int>();
+
+ Future<int> combine(int x, int y) async => throw StateError('fail');
+
+ var errors = <Object>[];
+
+ source.stream.scan(0, combine).listen(null, onError: errors.add);
+
+ source.add(1);
+ await Future(() {});
+
+ expect(errors, [isStateError]);
+ });
+ });
+ });
+}
diff --git a/pkgs/stream_transform/test/start_with_test.dart b/pkgs/stream_transform/test/start_with_test.dart
new file mode 100644
index 0000000..35f0330
--- /dev/null
+++ b/pkgs/stream_transform/test/start_with_test.dart
@@ -0,0 +1,167 @@
+// 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ late StreamController<int> values;
+ late Stream<int> transformed;
+ late StreamSubscription<int> subscription;
+
+ late List<int> emittedValues;
+ late bool isDone;
+
+ void setupForStreamType(
+ String streamType, Stream<int> Function(Stream<int>) transform) {
+ emittedValues = [];
+ isDone = false;
+ values = createController(streamType);
+ transformed = transform(values.stream);
+ subscription =
+ transformed.listen(emittedValues.add, onDone: () => isDone = true);
+ }
+
+ for (var streamType in streamTypes) {
+ group('startWith then [$streamType]', () {
+ setUp(() => setupForStreamType(streamType, (s) => s.startWith(1)));
+
+ test('outputs all values', () async {
+ values
+ ..add(2)
+ ..add(3);
+ await Future(() {});
+ expect(emittedValues, [1, 2, 3]);
+ });
+
+ test('outputs initial when followed by empty stream', () async {
+ await values.close();
+ expect(emittedValues, [1]);
+ });
+
+ test('closes with values', () async {
+ expect(isDone, false);
+ await values.close();
+ expect(isDone, true);
+ });
+
+ if (streamType == 'broadcast') {
+ test('can cancel and relisten', () async {
+ values.add(2);
+ await Future(() {});
+ await subscription.cancel();
+ subscription = transformed.listen(emittedValues.add);
+ values.add(3);
+ await Future(() {});
+ await Future(() {});
+ expect(emittedValues, [1, 2, 3]);
+ });
+ }
+ });
+
+ group('startWithMany then [$streamType]', () {
+ setUp(() async {
+ setupForStreamType(streamType, (s) => s.startWithMany([1, 2]));
+ // Ensure all initial values go through
+ await Future(() {});
+ });
+
+ test('outputs all values', () async {
+ values
+ ..add(3)
+ ..add(4);
+ await Future(() {});
+ expect(emittedValues, [1, 2, 3, 4]);
+ });
+
+ test('outputs initial when followed by empty stream', () async {
+ await values.close();
+ expect(emittedValues, [1, 2]);
+ });
+
+ test('closes with values', () async {
+ expect(isDone, false);
+ await values.close();
+ expect(isDone, true);
+ });
+
+ if (streamType == 'broadcast') {
+ test('can cancel and relisten', () async {
+ values.add(3);
+ await Future(() {});
+ await subscription.cancel();
+ subscription = transformed.listen(emittedValues.add);
+ values.add(4);
+ await Future(() {});
+ expect(emittedValues, [1, 2, 3, 4]);
+ });
+ }
+ });
+
+ for (var startingStreamType in streamTypes) {
+ group('startWithStream [$startingStreamType] then [$streamType]', () {
+ late StreamController<int> starting;
+ setUp(() async {
+ starting = createController(startingStreamType);
+ setupForStreamType(
+ streamType, (s) => s.startWithStream(starting.stream));
+ });
+
+ test('outputs all values', () async {
+ starting
+ ..add(1)
+ ..add(2);
+ await starting.close();
+ values
+ ..add(3)
+ ..add(4);
+ await Future(() {});
+ expect(emittedValues, [1, 2, 3, 4]);
+ });
+
+ test('closes with values', () async {
+ expect(isDone, false);
+ await starting.close();
+ expect(isDone, false);
+ await values.close();
+ expect(isDone, true);
+ });
+
+ if (streamType == 'broadcast') {
+ test('can cancel and relisten during starting', () async {
+ starting.add(1);
+ await Future(() {});
+ await subscription.cancel();
+ subscription = transformed.listen(emittedValues.add);
+ starting.add(2);
+ await starting.close();
+ values
+ ..add(3)
+ ..add(4);
+ await Future(() {});
+ expect(emittedValues, [1, 2, 3, 4]);
+ });
+
+ test('can cancel and relisten during values', () async {
+ starting
+ ..add(1)
+ ..add(2);
+ await starting.close();
+ values.add(3);
+ await Future(() {});
+ await subscription.cancel();
+ subscription = transformed.listen(emittedValues.add);
+ values.add(4);
+ await Future(() {});
+ expect(emittedValues, [1, 2, 3, 4]);
+ });
+ }
+ });
+ }
+ }
+}
diff --git a/pkgs/stream_transform/test/switch_test.dart b/pkgs/stream_transform/test/switch_test.dart
new file mode 100644
index 0000000..9e70c08
--- /dev/null
+++ b/pkgs/stream_transform/test/switch_test.dart
@@ -0,0 +1,229 @@
+// 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ for (var outerType in streamTypes) {
+ for (var innerType in streamTypes) {
+ group('Outer type: [$outerType], Inner type: [$innerType]', () {
+ late StreamController<int> first;
+ late StreamController<int> second;
+ late StreamController<int> third;
+ late StreamController<Stream<int>> outer;
+
+ late List<int> emittedValues;
+ late bool firstCanceled;
+ late bool outerCanceled;
+ late bool isDone;
+ late List<String> errors;
+ late StreamSubscription<int> subscription;
+
+ setUp(() async {
+ firstCanceled = false;
+ outerCanceled = false;
+ outer = createController(outerType)
+ ..onCancel = () {
+ outerCanceled = true;
+ };
+ first = createController(innerType)
+ ..onCancel = () {
+ firstCanceled = true;
+ };
+ second = createController(innerType);
+ third = createController(innerType);
+ emittedValues = [];
+ errors = [];
+ isDone = false;
+ subscription = outer.stream
+ .switchLatest()
+ .listen(emittedValues.add, onError: errors.add, onDone: () {
+ isDone = true;
+ });
+ });
+
+ test('forwards events', () async {
+ outer.add(first.stream);
+ await Future(() {});
+ first
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+
+ outer.add(second.stream);
+ await Future(() {});
+ second
+ ..add(3)
+ ..add(4);
+ await Future(() {});
+
+ expect(emittedValues, [1, 2, 3, 4]);
+ });
+
+ test('forwards errors from outer Stream', () async {
+ outer.addError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+
+ test('forwards errors from inner Stream', () async {
+ outer.add(first.stream);
+ await Future(() {});
+ first.addError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+
+ test('closes when final stream is done', () async {
+ outer.add(first.stream);
+ await Future(() {});
+
+ outer.add(second.stream);
+ await Future(() {});
+
+ await outer.close();
+ expect(isDone, false);
+
+ await second.close();
+ expect(isDone, true);
+ });
+
+ test(
+ 'closes when outer stream closes if latest inner stream already '
+ 'closed', () async {
+ outer.add(first.stream);
+ await Future(() {});
+ await first.close();
+ expect(isDone, false);
+
+ await outer.close();
+ expect(isDone, true);
+ });
+
+ test('cancels listeners on previous streams', () async {
+ outer.add(first.stream);
+ await Future(() {});
+
+ outer.add(second.stream);
+ await Future(() {});
+ expect(firstCanceled, true);
+ });
+
+ if (innerType != 'broadcast') {
+ test('waits for cancel before listening to subsequent stream',
+ () async {
+ var cancelWork = Completer<void>();
+ first.onCancel = () => cancelWork.future;
+ outer.add(first.stream);
+ await Future(() {});
+
+ var cancelDone = false;
+ second.onListen = expectAsync0(() {
+ expect(cancelDone, true);
+ });
+ outer.add(second.stream);
+ await Future(() {});
+ cancelWork.complete();
+ cancelDone = true;
+ });
+
+ test('all streams are listened to, even while cancelling', () async {
+ var cancelWork = Completer<void>();
+ first.onCancel = () => cancelWork.future;
+ outer.add(first.stream);
+ await Future(() {});
+
+ var cancelDone = false;
+ second.onListen = expectAsync0(() {
+ expect(cancelDone, true);
+ });
+ third.onListen = expectAsync0(() {
+ expect(cancelDone, true);
+ });
+ outer
+ ..add(second.stream)
+ ..add(third.stream);
+ await Future(() {});
+ cancelWork.complete();
+ cancelDone = true;
+ });
+ }
+
+ if (outerType != 'broadcast' && innerType != 'broadcast') {
+ test('pausing while cancelling an inner stream is respected',
+ () async {
+ var cancelWork = Completer<void>();
+ first.onCancel = () => cancelWork.future;
+ outer.add(first.stream);
+ await Future(() {});
+
+ var cancelDone = false;
+ second.onListen = expectAsync0(() {
+ expect(cancelDone, true);
+ });
+ outer.add(second.stream);
+ await Future(() {});
+ subscription.pause();
+ cancelWork.complete();
+ cancelDone = true;
+ await Future(() {});
+ expect(second.isPaused, true);
+ subscription.resume();
+ });
+ }
+
+ test('cancels listener on current and outer stream on cancel',
+ () async {
+ outer.add(first.stream);
+ await Future(() {});
+ await subscription.cancel();
+
+ await Future(() {});
+ expect(outerCanceled, true);
+ expect(firstCanceled, true);
+ });
+ });
+ }
+ }
+
+ group('switchMap', () {
+ test('uses map function', () async {
+ var outer = StreamController<List<int>>();
+
+ var values = <int>[];
+ outer.stream.switchMap(Stream.fromIterable).listen(values.add);
+
+ outer.add([1, 2, 3]);
+ await Future(() {});
+ outer.add([4, 5, 6]);
+ await Future(() {});
+ expect(values, [1, 2, 3, 4, 5, 6]);
+ });
+
+ test('can create a broadcast stream', () async {
+ var outer = StreamController<int>.broadcast();
+
+ var transformed =
+ outer.stream.switchMap((_) => const Stream<int>.empty());
+
+ expect(transformed.isBroadcast, true);
+ });
+
+ test('forwards errors from the convert callback', () async {
+ var errors = <String>[];
+ var source = Stream.fromIterable([1, 2, 3]);
+ source.switchMap<int>((i) {
+ // ignore: only_throw_errors
+ throw 'Error: $i';
+ }).listen((_) {}, onError: errors.add);
+ await Future<void>(() {});
+ expect(errors, ['Error: 1', 'Error: 2', 'Error: 3']);
+ });
+ });
+}
diff --git a/pkgs/stream_transform/test/take_until_test.dart b/pkgs/stream_transform/test/take_until_test.dart
new file mode 100644
index 0000000..982b3da
--- /dev/null
+++ b/pkgs/stream_transform/test/take_until_test.dart
@@ -0,0 +1,135 @@
+// 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ for (var streamType in streamTypes) {
+ group('takeUntil on Stream type [$streamType]', () {
+ late StreamController<int> values;
+ late List<int> emittedValues;
+ late bool valuesCanceled;
+ late bool isDone;
+ late List<String> errors;
+ late Stream<int> transformed;
+ late StreamSubscription<int> subscription;
+ late Completer<void> closeTrigger;
+
+ setUp(() {
+ valuesCanceled = false;
+ values = createController(streamType)
+ ..onCancel = () {
+ valuesCanceled = true;
+ };
+ emittedValues = [];
+ errors = [];
+ isDone = false;
+ closeTrigger = Completer();
+ transformed = values.stream.takeUntil(closeTrigger.future);
+ subscription = transformed
+ .listen(emittedValues.add, onError: errors.add, onDone: () {
+ isDone = true;
+ });
+ });
+
+ test('forwards cancellation', () async {
+ await subscription.cancel();
+ expect(valuesCanceled, true);
+ });
+
+ test('lets values through before trigger', () async {
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ expect(emittedValues, [1, 2]);
+ });
+
+ test('forwards errors', () async {
+ values.addError('error');
+ await Future(() {});
+ expect(errors, ['error']);
+ });
+
+ test('sends done if original strem ends', () async {
+ await values.close();
+ expect(isDone, true);
+ });
+
+ test('sends done when trigger fires', () async {
+ closeTrigger.complete();
+ await Future(() {});
+ expect(isDone, true);
+ });
+
+ test('forwards errors from the close trigger', () async {
+ closeTrigger.completeError('sad');
+ await Future(() {});
+ expect(errors, ['sad']);
+ expect(isDone, true);
+ });
+
+ test('ignores errors from the close trigger after stream closed',
+ () async {
+ await values.close();
+ closeTrigger.completeError('sad');
+ await Future(() {});
+ expect(errors, <Object>[]);
+ });
+
+ test('cancels value subscription when trigger fires', () async {
+ closeTrigger.complete();
+ await Future(() {});
+ expect(valuesCanceled, true);
+ });
+
+ if (streamType == 'broadcast') {
+ test('multiple listeners all get values', () async {
+ var otherValues = <Object>[];
+ transformed.listen(otherValues.add);
+ values
+ ..add(1)
+ ..add(2);
+ await Future(() {});
+ expect(emittedValues, [1, 2]);
+ expect(otherValues, [1, 2]);
+ });
+
+ test('multiple listeners get done when trigger fires', () async {
+ var otherDone = false;
+ transformed.listen(null, onDone: () => otherDone = true);
+ closeTrigger.complete();
+ await Future(() {});
+ expect(otherDone, true);
+ expect(isDone, true);
+ });
+
+ test('multiple listeners get done when values end', () async {
+ var otherDone = false;
+ transformed.listen(null, onDone: () => otherDone = true);
+ await values.close();
+ expect(otherDone, true);
+ expect(isDone, true);
+ });
+
+ test('can cancel and relisten before trigger fires', () async {
+ values.add(1);
+ await Future(() {});
+ await subscription.cancel();
+ values.add(2);
+ await Future(() {});
+ subscription = transformed.listen(emittedValues.add);
+ values.add(3);
+ await Future(() {});
+ expect(emittedValues, [1, 3]);
+ });
+ }
+ });
+ }
+}
diff --git a/pkgs/stream_transform/test/tap_test.dart b/pkgs/stream_transform/test/tap_test.dart
new file mode 100644
index 0000000..f2b4346
--- /dev/null
+++ b/pkgs/stream_transform/test/tap_test.dart
@@ -0,0 +1,116 @@
+// 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('calls function for values', () async {
+ var valuesSeen = <int>[];
+ var stream = Stream.fromIterable([1, 2, 3]);
+ await stream.tap(valuesSeen.add).last;
+ expect(valuesSeen, [1, 2, 3]);
+ });
+
+ test('forwards values', () async {
+ var stream = Stream.fromIterable([1, 2, 3]);
+ var values = await stream.tap((_) {}).toList();
+ expect(values, [1, 2, 3]);
+ });
+
+ test('calls function for errors', () async {
+ dynamic error;
+ var source = StreamController<int>();
+ source.stream.tap((_) {}, onError: (e, st) {
+ error = e;
+ }).listen((_) {}, onError: (_) {});
+ source.addError('error');
+ await Future(() {});
+ expect(error, 'error');
+ });
+
+ test('forwards errors', () async {
+ dynamic error;
+ var source = StreamController<int>();
+ source.stream.tap((_) {}, onError: (e, st) {}).listen((_) {},
+ onError: (Object e) {
+ error = e;
+ });
+ source.addError('error');
+ await Future(() {});
+ expect(error, 'error');
+ });
+
+ test('calls function on done', () async {
+ var doneCalled = false;
+ var source = StreamController<int>();
+ source.stream.tap((_) {}, onDone: () {
+ doneCalled = true;
+ }).listen((_) {});
+ await source.close();
+ expect(doneCalled, true);
+ });
+
+ test('forwards only once with multiple listeners on a broadcast stream',
+ () async {
+ var dataCallCount = 0;
+ var source = StreamController<int>.broadcast();
+ source.stream.tap((_) {
+ dataCallCount++;
+ })
+ ..listen((_) {})
+ ..listen((_) {});
+ source.add(1);
+ await Future(() {});
+ expect(dataCallCount, 1);
+ });
+
+ test(
+ 'forwards errors only once with multiple listeners on a broadcast stream',
+ () async {
+ var errorCallCount = 0;
+ var source = StreamController<int>.broadcast();
+ source.stream.tap((_) {}, onError: (_, __) {
+ errorCallCount++;
+ })
+ ..listen((_) {}, onError: (_, __) {})
+ ..listen((_) {}, onError: (_, __) {});
+ source.addError('error');
+ await Future(() {});
+ expect(errorCallCount, 1);
+ });
+
+ test('calls onDone only once with multiple listeners on a broadcast stream',
+ () async {
+ var doneCallCount = 0;
+ var source = StreamController<int>.broadcast();
+ source.stream.tap((_) {}, onDone: () {
+ doneCallCount++;
+ })
+ ..listen((_) {})
+ ..listen((_) {});
+ await source.close();
+ expect(doneCallCount, 1);
+ });
+
+ test('forwards values to multiple listeners', () async {
+ var source = StreamController<int>.broadcast();
+ var emittedValues1 = <int>[];
+ var emittedValues2 = <int>[];
+ source.stream.tap((_) {})
+ ..listen(emittedValues1.add)
+ ..listen(emittedValues2.add);
+ source.add(1);
+ await Future(() {});
+ expect(emittedValues1, [1]);
+ expect(emittedValues2, [1]);
+ });
+
+ test('allows null callback', () async {
+ var stream = Stream.fromIterable([1, 2, 3]);
+ await stream.tap(null).last;
+ });
+}
diff --git a/pkgs/stream_transform/test/throttle_test.dart b/pkgs/stream_transform/test/throttle_test.dart
new file mode 100644
index 0000000..07f607a
--- /dev/null
+++ b/pkgs/stream_transform/test/throttle_test.dart
@@ -0,0 +1,193 @@
+// 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:fake_async/fake_async.dart';
+import 'package:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ for (var streamType in streamTypes) {
+ group('Stream type [$streamType]', () {
+ late StreamController<int> values;
+ late List<int> emittedValues;
+ late bool valuesCanceled;
+ late bool isDone;
+ late Stream<int> transformed;
+ late StreamSubscription<int> subscription;
+
+ group('throttle - trailing: false', () {
+ setUp(() async {
+ valuesCanceled = false;
+ values = createController(streamType)
+ ..onCancel = () {
+ valuesCanceled = true;
+ };
+ emittedValues = [];
+ isDone = false;
+ transformed = values.stream.throttle(const Duration(milliseconds: 5));
+ });
+
+ void listen() {
+ subscription = transformed.listen(emittedValues.add, onDone: () {
+ isDone = true;
+ });
+ }
+
+ test('cancels values', () async {
+ listen();
+ await subscription.cancel();
+ expect(valuesCanceled, true);
+ });
+
+ test('swallows values that come faster than duration', () {
+ fakeAsync((async) {
+ listen();
+ values
+ ..add(1)
+ ..add(2)
+ ..close();
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [1]);
+ });
+ });
+
+ test('outputs multiple values spaced further than duration', () {
+ fakeAsync((async) {
+ listen();
+ values.add(1);
+ async.elapse(const Duration(milliseconds: 6));
+ values.add(2);
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [1, 2]);
+ async.elapse(const Duration(milliseconds: 6));
+ });
+ });
+
+ test('closes output immediately', () {
+ fakeAsync((async) {
+ listen();
+ values.add(1);
+ async.elapse(const Duration(milliseconds: 6));
+ values
+ ..add(2)
+ ..close();
+ async.flushMicrotasks();
+ expect(isDone, true);
+ });
+ });
+
+ if (streamType == 'broadcast') {
+ test('multiple listeners all get values', () {
+ fakeAsync((async) {
+ listen();
+ var otherValues = <int>[];
+ transformed.listen(otherValues.add);
+ values.add(1);
+ async.flushMicrotasks();
+ expect(emittedValues, [1]);
+ expect(otherValues, [1]);
+ });
+ });
+ }
+ });
+
+ group('throttle - trailing: true', () {
+ setUp(() async {
+ valuesCanceled = false;
+ values = createController(streamType)
+ ..onCancel = () {
+ valuesCanceled = true;
+ };
+ emittedValues = [];
+ isDone = false;
+ transformed = values.stream
+ .throttle(const Duration(milliseconds: 5), trailing: true);
+ });
+ void listen() {
+ subscription = transformed.listen(emittedValues.add, onDone: () {
+ isDone = true;
+ });
+ }
+
+ test('emits both first and last in a period', () {
+ fakeAsync((async) {
+ listen();
+ values
+ ..add(1)
+ ..add(2)
+ ..close();
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [1, 2]);
+ });
+ });
+
+ test('swallows values that are not the latest in a period', () {
+ fakeAsync((async) {
+ listen();
+ values
+ ..add(1)
+ ..add(2)
+ ..add(3)
+ ..close();
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [1, 3]);
+ });
+ });
+
+ test('waits to output the last value even if the stream closes',
+ () async {
+ fakeAsync((async) {
+ listen();
+ values
+ ..add(1)
+ ..add(2)
+ ..close();
+ async.flushMicrotasks();
+ expect(isDone, false);
+ expect(emittedValues, [1],
+ reason: 'Should not be emitted until after duration');
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [1, 2]);
+ expect(isDone, true);
+ async.elapse(const Duration(milliseconds: 6));
+ });
+ });
+
+ test('closes immediately if there is no pending value', () {
+ fakeAsync((async) {
+ listen();
+ values
+ ..add(1)
+ ..close();
+ async.flushMicrotasks();
+ expect(isDone, true);
+ });
+ });
+
+ if (streamType == 'broadcast') {
+ test('multiple listeners all get values', () {
+ fakeAsync((async) {
+ listen();
+ var otherValues = <int>[];
+ transformed.listen(otherValues.add);
+ values
+ ..add(1)
+ ..add(2);
+ async.flushMicrotasks();
+ expect(emittedValues, [1]);
+ expect(otherValues, [1]);
+ async.elapse(const Duration(milliseconds: 6));
+ expect(emittedValues, [1, 2]);
+ expect(otherValues, [1, 2]);
+ });
+ });
+ }
+ });
+ });
+ }
+}
diff --git a/pkgs/stream_transform/test/utils.dart b/pkgs/stream_transform/test/utils.dart
new file mode 100644
index 0000000..42d9613
--- /dev/null
+++ b/pkgs/stream_transform/test/utils.dart
@@ -0,0 +1,19 @@
+// 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';
+
+StreamController<T> createController<T>(String streamType) {
+ switch (streamType) {
+ case 'single subscription':
+ return StreamController<T>();
+ case 'broadcast':
+ return StreamController<T>.broadcast();
+ default:
+ throw ArgumentError.value(
+ streamType, 'streamType', 'Must be one of $streamTypes');
+ }
+}
+
+const streamTypes = ['single subscription', 'broadcast'];
diff --git a/pkgs/stream_transform/test/where_not_null_test.dart b/pkgs/stream_transform/test/where_not_null_test.dart
new file mode 100644
index 0000000..c9af794
--- /dev/null
+++ b/pkgs/stream_transform/test/where_not_null_test.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 'dart:async';
+
+import 'package:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('forwards only events that match the type', () async {
+ var values = Stream.fromIterable([null, 'a', null, 'b']);
+ var filtered = values.whereNotNull();
+ expect(await filtered.toList(), ['a', 'b']);
+ });
+
+ test('can result in empty stream', () async {
+ var values = Stream<Object?>.fromIterable([null, null]);
+ var filtered = values.whereNotNull();
+ expect(await filtered.isEmpty, true);
+ });
+
+ test('forwards values to multiple listeners', () async {
+ var values = StreamController<Object?>.broadcast();
+ var filtered = values.stream.whereNotNull();
+ var firstValues = <Object>[];
+ var secondValues = <Object>[];
+ filtered
+ ..listen(firstValues.add)
+ ..listen(secondValues.add);
+ values
+ ..add(null)
+ ..add('a')
+ ..add(null)
+ ..add('b');
+ await Future(() {});
+ expect(firstValues, ['a', 'b']);
+ expect(secondValues, ['a', 'b']);
+ });
+
+ test('closes streams with multiple listeners', () async {
+ var values = StreamController<Object?>.broadcast();
+ var filtered = values.stream.whereNotNull();
+ var firstDone = false;
+ var secondDone = false;
+ filtered
+ ..listen(null, onDone: () => firstDone = true)
+ ..listen(null, onDone: () => secondDone = true);
+ values
+ ..add(null)
+ ..add('a');
+ await values.close();
+ expect(firstDone, true);
+ expect(secondDone, true);
+ });
+}
diff --git a/pkgs/stream_transform/test/where_type_test.dart b/pkgs/stream_transform/test/where_type_test.dart
new file mode 100644
index 0000000..4cbea37
--- /dev/null
+++ b/pkgs/stream_transform/test/where_type_test.dart
@@ -0,0 +1,56 @@
+// 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:stream_transform/stream_transform.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('forwards only events that match the type', () async {
+ var values = Stream.fromIterable([1, 'a', 2, 'b']);
+ var filtered = values.whereType<String>();
+ expect(await filtered.toList(), ['a', 'b']);
+ });
+
+ test('can result in empty stream', () async {
+ var values = Stream.fromIterable([1, 2, 3, 4]);
+ var filtered = values.whereType<String>();
+ expect(await filtered.isEmpty, true);
+ });
+
+ test('forwards values to multiple listeners', () async {
+ var values = StreamController<Object>.broadcast();
+ var filtered = values.stream.whereType<String>();
+ var firstValues = <Object>[];
+ var secondValues = <Object>[];
+ filtered
+ ..listen(firstValues.add)
+ ..listen(secondValues.add);
+ values
+ ..add(1)
+ ..add('a')
+ ..add(2)
+ ..add('b');
+ await Future(() {});
+ expect(firstValues, ['a', 'b']);
+ expect(secondValues, ['a', 'b']);
+ });
+
+ test('closes streams with multiple listeners', () async {
+ var values = StreamController<Object>.broadcast();
+ var filtered = values.stream.whereType<String>();
+ var firstDone = false;
+ var secondDone = false;
+ filtered
+ ..listen(null, onDone: () => firstDone = true)
+ ..listen(null, onDone: () => secondDone = true);
+ values
+ ..add(1)
+ ..add('a');
+ await values.close();
+ expect(firstDone, true);
+ expect(secondDone, true);
+ });
+}
diff --git a/pkgs/string_scanner/.github/dependabot.yml b/pkgs/string_scanner/.github/dependabot.yml
new file mode 100644
index 0000000..a19a66a
--- /dev/null
+++ b/pkgs/string_scanner/.github/dependabot.yml
@@ -0,0 +1,16 @@
+# Set update schedule for GitHub Actions
+# See https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/keeping-your-actions-up-to-date-with-dependabot
+
+version: 2
+updates:
+
+- package-ecosystem: github-actions
+ directory: /
+ schedule:
+ interval: monthly
+ labels:
+ - autosubmit
+ groups:
+ github-actions:
+ patterns:
+ - "*"
diff --git a/pkgs/string_scanner/.github/workflows/publish.yaml b/pkgs/string_scanner/.github/workflows/publish.yaml
new file mode 100644
index 0000000..27157a0
--- /dev/null
+++ b/pkgs/string_scanner/.github/workflows/publish.yaml
@@ -0,0 +1,17 @@
+# A CI configuration to auto-publish pub packages.
+
+name: Publish
+
+on:
+ pull_request:
+ branches: [ master ]
+ push:
+ tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ]
+
+jobs:
+ publish:
+ if: ${{ github.repository_owner == 'dart-lang' }}
+ uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main
+ permissions:
+ id-token: write # Required for authentication using OIDC
+ pull-requests: write # Required for writing the pull request note
diff --git a/pkgs/string_scanner/.github/workflows/test-package.yml b/pkgs/string_scanner/.github/workflows/test-package.yml
new file mode 100644
index 0000000..c60f710
--- /dev/null
+++ b/pkgs/string_scanner/.github/workflows/test-package.yml
@@ -0,0 +1,64 @@
+name: Dart CI
+
+on:
+ # Run on PRs and pushes to the default branch.
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+ schedule:
+ - cron: "0 0 * * 0"
+
+env:
+ PUB_ENVIRONMENT: bot.github
+
+jobs:
+ # Check code formatting and static analysis on a single OS (linux)
+ # against Dart dev.
+ analyze:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ sdk: [dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Check formatting
+ run: dart format --output=none --set-exit-if-changed .
+ if: always() && steps.install.outcome == 'success'
+ - name: Analyze code
+ run: dart analyze --fatal-infos
+ if: always() && steps.install.outcome == 'success'
+
+ # Run tests on a matrix consisting of two dimensions:
+ # 1. OS: ubuntu-latest, (macos-latest, windows-latest)
+ # 2. release channel: dev
+ test:
+ needs: analyze
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Add macos-latest and/or windows-latest if relevant for this package.
+ os: [ubuntu-latest]
+ sdk: [3.1, dev]
+ steps:
+ - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
+ - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94
+ with:
+ sdk: ${{ matrix.sdk }}
+ - id: install
+ name: Install dependencies
+ run: dart pub get
+ - name: Run VM tests
+ run: dart test --platform vm
+ if: always() && steps.install.outcome == 'success'
+ - name: Run Chrome tests
+ run: dart test --platform chrome
+ if: always() && steps.install.outcome == 'success'
diff --git a/pkgs/string_scanner/.gitignore b/pkgs/string_scanner/.gitignore
new file mode 100644
index 0000000..fb97bde
--- /dev/null
+++ b/pkgs/string_scanner/.gitignore
@@ -0,0 +1,5 @@
+# Don’t commit the following directories created by pub.
+.dart_tool/
+.pub/
+.packages
+pubspec.lock
diff --git a/pkgs/string_scanner/CHANGELOG.md b/pkgs/string_scanner/CHANGELOG.md
new file mode 100644
index 0000000..082e9f2
--- /dev/null
+++ b/pkgs/string_scanner/CHANGELOG.md
@@ -0,0 +1,175 @@
+## 1.4.1
+
+* Move to `dart-lang/tools` monorepo.
+
+## 1.4.0
+
+* Fix `LineScanner`'s handling of `\r\n`'s to preventing errors scanning
+ zero-length matches when between CR and LF. CR is treated as a new line only
+ if not immediately followed by a LF.
+* Fix `LineScanner`'s updating of `column` when setting `position` if the
+ current position is not `0`.
+
+## 1.3.0
+
+* Require Dart 3.1.0
+
+* Add a `SpanScanner.spanFromPosition()` method which takes raw code units
+ rather than `SpanScanner.spanFrom()`'s `LineScannerState`s.
+
+## 1.2.0
+
+* Require Dart 2.18.0
+
+* Add better support for reading code points in the Unicode supplementary plane:
+
+ * Added `StringScanner.readCodePoint()`, which consumes an entire Unicode code
+ point even if it's represented by two UTF-16 code units.
+
+ * Added `StringScanner.peekCodePoint()`, which returns an entire Unicode code
+ point even if it's represented by two UTF-16 code units.
+
+ * `StringScanner.scanChar()` and `StringScanner.expectChar()` will now
+ properly consume two UTF-16 code units if they're passed Unicode code points
+ in the supplementary plane.
+
+## 1.1.1
+
+* Populate the pubspec `repository` field.
+* Switch to `package:lints`.
+* Remove a dependency on `package:charcode`.
+
+## 1.1.0
+
+* Stable release for null safety.
+
+## 1.1.0-nullsafety.3
+
+* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release
+ guidelines.
+
+## 1.1.0-nullsafety.2
+
+* Allow prerelease versions of the 2.12 sdk.
+
+## 1.1.0-nullsafety.1
+
+- Allow 2.10 stable and 2.11.0 dev SDK versions.
+
+## 1.1.0-nullsafety
+
+- Migrate to null safety.
+
+## 1.0.5
+
+- Added an example.
+
+- Update Dart SDK constraint to `>=2.0.0 <3.0.0`.
+
+## 1.0.4
+
+* Add @alwaysThrows annotation to error method.
+
+## 1.0.3
+
+* Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 1.0.2
+
+* `SpanScanner` no longer crashes when creating a span that contains a UTF-16
+ surrogate pair.
+
+## 1.0.1
+
+* Fix the error text emitted by `StringScanner.expectChar()`.
+
+## 1.0.0
+
+* **Breaking change**: `StringScanner.error()`'s `length` argument now defaults
+ to `0` rather than `1` when no match data is available.
+
+* **Breaking change**: `StringScanner.lastMatch` and related methods are now
+ reset when the scanner's position changes without producing a new match.
+
+**Note**: While the changes in `1.0.0` are user-visible, they're unlikely to
+actually break any code in practice. Unless you know that your package is
+incompatible with 0.1.x, consider using 0.1.5 as your lower bound rather
+than 1.0.0. For example, `string_scanner: ">=0.1.5 <2.0.0"`.
+
+## 0.1.5
+
+* Add `new SpanScanner.within()`, which scans within a existing `FileSpan`.
+
+* Add `StringScanner.scanChar()` and `StringScanner.expectChar()`.
+
+## 0.1.4+1
+
+* Remove the dependency on `path`, since we don't actually import it.
+
+## 0.1.4
+
+* Add `new SpanScanner.eager()` for creating a `SpanScanner` that eagerly
+ computes its current line and column numbers.
+
+## 0.1.3+2
+
+* Fix `LineScanner`'s handling of carriage returns to match that of
+ `SpanScanner`.
+
+## 0.1.3+1
+
+* Fixed the homepage URL.
+
+## 0.1.3
+
+* Add an optional `endState` argument to `SpanScanner.spanFrom`.
+
+## 0.1.2
+
+* Add `StringScanner.substring`, which returns a substring of the source string.
+
+## 0.1.1
+
+* Declare `SpanScanner`'s exposed `SourceSpan`s and `SourceLocation`s to be
+ `FileSpan`s and `FileLocation`s. They always were underneath, but callers may
+ now rely on it.
+
+* Add `SpanScanner.location`, which returns the scanner's current
+ `SourceLocation`.
+
+## 0.1.0
+
+* Switch from `source_maps`' `Span` class to `source_span`'s `SourceSpan` class.
+
+* `new StringScanner()`'s `sourceUrl` parameter is now named to make it clear
+ that it can be safely `null`.
+
+* `new StringScannerException()` takes different arguments in a different order
+ to match `SpanFormatException`.
+
+* `StringScannerException.string` has been renamed to
+ `StringScannerException.source` to match the `FormatException` interface.
+
+## 0.0.3
+
+* Make `StringScannerException` inherit from source_map's `SpanFormatException`.
+
+## 0.0.2
+
+* `new StringScanner()` now takes an optional `sourceUrl` argument that provides
+ the URL of the source file. This is used for error reporting.
+
+* Add `StringScanner.readChar()` and `StringScanner.peekChar()` methods for
+ doing character-by-character scanning.
+
+* Scanners now throw `StringScannerException`s which provide more detailed
+ access to information about the errors that were thrown and can provide
+ terminal-colored messages.
+
+* Add a `LineScanner` subclass of `StringScanner` that automatically tracks line
+ and column information of the text being scanned.
+
+* Add a `SpanScanner` subclass of `LineScanner` that exposes matched ranges as
+ [source map][] `Span` objects.
+
+[source_map]: https://pub.dev/packages/source_maps
diff --git a/pkgs/string_scanner/LICENSE b/pkgs/string_scanner/LICENSE
new file mode 100644
index 0000000..000cd7b
--- /dev/null
+++ b/pkgs/string_scanner/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/string_scanner/README.md b/pkgs/string_scanner/README.md
new file mode 100644
index 0000000..e06e325
--- /dev/null
+++ b/pkgs/string_scanner/README.md
@@ -0,0 +1,41 @@
+[](https://github.com/dart-lang/string_scanner/actions/workflows/test-package.yml)
+[](https://pub.dev/packages/string_scanner)
+[](https://pub.dev/packages/string_scanner/publisher)
+
+This package exposes a `StringScanner` type that makes it easy to parse a string
+using a series of `Pattern`s. For example:
+
+```dart
+import 'dart:math' as math;
+
+import 'package:string_scanner/string_scanner.dart';
+
+num parseNumber(String source) {
+ // Scan a number ("1", "1.5", "-3").
+ final scanner = StringScanner(source);
+
+ // [Scanner.scan] tries to consume a [Pattern] and returns whether or not it
+ // succeeded. It will move the scan pointer past the end of the pattern.
+ final negative = scanner.scan('-');
+
+ // [Scanner.expect] consumes a [Pattern] and throws a [FormatError] if it
+ // fails. Like [Scanner.scan], it will move the scan pointer forward.
+ scanner.expect(RegExp(r'\d+'));
+
+ // [Scanner.lastMatch] holds the [MatchData] for the most recent call to
+ // [Scanner.scan], [Scanner.expect], or [Scanner.matches].
+ var number = num.parse(scanner.lastMatch![0]!);
+
+ if (scanner.scan('.')) {
+ scanner.expect(RegExp(r'\d+'));
+ final decimal = scanner.lastMatch![0]!;
+ number += int.parse(decimal) / math.pow(10, decimal.length);
+ }
+
+ // [Scanner.expectDone] will throw a [FormatError] if there's any input that
+ // hasn't yet been consumed.
+ scanner.expectDone();
+
+ return (negative ? -1 : 1) * number;
+}
+```
diff --git a/pkgs/string_scanner/analysis_options.yaml b/pkgs/string_scanner/analysis_options.yaml
new file mode 100644
index 0000000..59f763a
--- /dev/null
+++ b/pkgs/string_scanner/analysis_options.yaml
@@ -0,0 +1,32 @@
+# https://dart.dev/guides/language/analysis-options
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
+ strict-inference: true
+ strict-raw-types: true
+
+linter:
+ rules:
+ - avoid_bool_literals_in_conditional_expressions
+ - avoid_classes_with_only_static_members
+ - avoid_private_typedef_functions
+ - avoid_redundant_argument_values
+ - avoid_returning_this
+ - avoid_unused_constructor_parameters
+ - avoid_void_async
+ - cancel_subscriptions
+ - join_return_with_assignment
+ - literal_only_boolean_expressions
+ - missing_whitespace_between_adjacent_strings
+ - no_adjacent_strings_in_list
+ - no_runtimeType_toString
+ - prefer_const_declarations
+ - prefer_expression_function_bodies
+ - prefer_final_locals
+ - unnecessary_await_in_return
+ - unnecessary_raw_strings
+ - use_if_null_to_convert_nulls_to_bools
+ - use_raw_strings
+ - use_string_buffers
diff --git a/pkgs/string_scanner/example/example.dart b/pkgs/string_scanner/example/example.dart
new file mode 100644
index 0000000..ec9dd76
--- /dev/null
+++ b/pkgs/string_scanner/example/example.dart
@@ -0,0 +1,40 @@
+// 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:math' as math;
+
+import 'package:string_scanner/string_scanner.dart';
+
+void main(List<String> args) {
+ print(parseNumber(args.single));
+}
+
+num parseNumber(String source) {
+ // Scan a number ("1", "1.5", "-3").
+ final scanner = StringScanner(source);
+
+ // [Scanner.scan] tries to consume a [Pattern] and returns whether or not it
+ // succeeded. It will move the scan pointer past the end of the pattern.
+ final negative = scanner.scan('-');
+
+ // [Scanner.expect] consumes a [Pattern] and throws a [FormatError] if it
+ // fails. Like [Scanner.scan], it will move the scan pointer forward.
+ scanner.expect(RegExp(r'\d+'));
+
+ // [Scanner.lastMatch] holds the [MatchData] for the most recent call to
+ // [Scanner.scan], [Scanner.expect], or [Scanner.matches].
+ var number = num.parse(scanner.lastMatch![0]!);
+
+ if (scanner.scan('.')) {
+ scanner.expect(RegExp(r'\d+'));
+ final decimal = scanner.lastMatch![0]!;
+ number += int.parse(decimal) / math.pow(10, decimal.length);
+ }
+
+ // [Scanner.expectDone] will throw a [FormatError] if there's any input that
+ // hasn't yet been consumed.
+ scanner.expectDone();
+
+ return (negative ? -1 : 1) * number;
+}
diff --git a/pkgs/string_scanner/lib/src/charcode.dart b/pkgs/string_scanner/lib/src/charcode.dart
new file mode 100644
index 0000000..d157749
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/charcode.dart
@@ -0,0 +1,24 @@
+// 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.
+
+/// Character '\'.
+const int $backslash = 0x5C;
+
+/// "Carriage return" control character.
+const int $cr = 0x0D;
+
+/// Character '"'.
+const int $doubleQuote = 0x22;
+
+/// Character 'f'.
+const int $f = 0x66;
+
+/// "Line feed" control character.
+const int $lf = 0x0A;
+
+/// Space character.
+const int $space = 0x20;
+
+/// Character 'x'.
+const int $x = 0x78;
diff --git a/pkgs/string_scanner/lib/src/eager_span_scanner.dart b/pkgs/string_scanner/lib/src/eager_span_scanner.dart
new file mode 100644
index 0000000..1ccc746
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/eager_span_scanner.dart
@@ -0,0 +1,133 @@
+// 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 'charcode.dart';
+import 'line_scanner.dart';
+import 'span_scanner.dart';
+import 'utils.dart';
+
+// TODO(nweiz): Currently this duplicates code in line_scanner.dart. Once
+// sdk#23770 is fully complete, we should move the shared code into a mixin.
+
+/// A regular expression matching newlines across platforms.
+final _newlineRegExp = RegExp(r'\r\n?|\n');
+
+/// A [SpanScanner] that tracks the line and column eagerly, like [LineScanner].
+class EagerSpanScanner extends SpanScanner {
+ @override
+ int get line => _line;
+ int _line = 0;
+
+ @override
+ int get column => _column;
+ int _column = 0;
+
+ @override
+ LineScannerState get state =>
+ _EagerSpanScannerState(this, position, line, column);
+
+ bool get _betweenCRLF => peekChar(-1) == $cr && peekChar() == $lf;
+
+ @override
+ set state(LineScannerState state) {
+ if (state is! _EagerSpanScannerState || !identical(state._scanner, this)) {
+ throw ArgumentError('The given LineScannerState was not returned by '
+ 'this LineScanner.');
+ }
+
+ super.position = state.position;
+ _line = state.line;
+ _column = state.column;
+ }
+
+ @override
+ set position(int newPosition) {
+ final oldPosition = position;
+ super.position = newPosition;
+
+ if (newPosition > oldPosition) {
+ final newlines = _newlinesIn(string.substring(oldPosition, newPosition));
+ _line += newlines.length;
+ if (newlines.isEmpty) {
+ _column += newPosition - oldPosition;
+ } else {
+ _column = newPosition - newlines.last.end;
+ }
+ } else {
+ final newlines = _newlinesIn(string.substring(newPosition, oldPosition));
+ if (_betweenCRLF) newlines.removeLast();
+
+ _line -= newlines.length;
+ if (newlines.isEmpty) {
+ _column -= oldPosition - newPosition;
+ } else {
+ _column =
+ newPosition - string.lastIndexOf(_newlineRegExp, newPosition) - 1;
+ }
+ }
+ }
+
+ EagerSpanScanner(super.string, {super.sourceUrl, super.position});
+
+ @override
+ bool scanChar(int character) {
+ if (!super.scanChar(character)) return false;
+ _adjustLineAndColumn(character);
+ return true;
+ }
+
+ @override
+ int readChar() {
+ final character = super.readChar();
+ _adjustLineAndColumn(character);
+ return character;
+ }
+
+ /// Adjusts [_line] and [_column] after having consumed [character].
+ void _adjustLineAndColumn(int character) {
+ if (character == $lf || (character == $cr && peekChar() != $lf)) {
+ _line += 1;
+ _column = 0;
+ } else {
+ _column += inSupplementaryPlane(character) ? 2 : 1;
+ }
+ }
+
+ @override
+ bool scan(Pattern pattern) {
+ if (!super.scan(pattern)) return false;
+ final firstMatch = lastMatch![0]!;
+
+ final newlines = _newlinesIn(firstMatch);
+ _line += newlines.length;
+ if (newlines.isEmpty) {
+ _column += firstMatch.length;
+ } else {
+ _column = firstMatch.length - newlines.last.end;
+ }
+
+ return true;
+ }
+
+ /// Returns a list of [Match]es describing all the newlines in [text], which
+ /// is assumed to end at [position].
+ List<Match> _newlinesIn(String text) {
+ final newlines = _newlineRegExp.allMatches(text).toList();
+ if (_betweenCRLF) newlines.removeLast();
+ return newlines;
+ }
+}
+
+/// A class representing the state of an [EagerSpanScanner].
+class _EagerSpanScannerState implements LineScannerState {
+ final EagerSpanScanner _scanner;
+ @override
+ final int position;
+ @override
+ final int line;
+ @override
+ final int column;
+
+ _EagerSpanScannerState(this._scanner, this.position, this.line, this.column);
+}
diff --git a/pkgs/string_scanner/lib/src/exception.dart b/pkgs/string_scanner/lib/src/exception.dart
new file mode 100644
index 0000000..57af541
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/exception.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.
+
+import 'package:source_span/source_span.dart';
+
+import 'string_scanner.dart';
+
+/// An exception thrown by a [StringScanner] that failed to parse a string.
+class StringScannerException extends SourceSpanFormatException {
+ @override
+ String get source => super.source as String;
+
+ /// The URL of the source file being parsed.
+ ///
+ /// This may be `null`, indicating that the source URL is unknown.
+ Uri? get sourceUrl => span?.sourceUrl;
+
+ StringScannerException(
+ super.message, SourceSpan super.span, String super.source);
+}
diff --git a/pkgs/string_scanner/lib/src/line_scanner.dart b/pkgs/string_scanner/lib/src/line_scanner.dart
new file mode 100644
index 0000000..b18d610
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/line_scanner.dart
@@ -0,0 +1,183 @@
+// 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 'charcode.dart';
+import 'string_scanner.dart';
+import 'utils.dart';
+
+// Note that much of this code is duplicated in eager_span_scanner.dart.
+
+/// A regular expression matching newlines. A newline is either a `\n`, a `\r\n`
+/// or a `\r` that is not immediately followed by a `\n`.
+final _newlineRegExp = RegExp(r'\n|\r\n|\r(?!\n)');
+
+/// A subclass of [StringScanner] that tracks line and column information.
+class LineScanner extends StringScanner {
+ /// The scanner's current (zero-based) line number.
+ int get line => _line;
+ int _line = 0;
+
+ /// The scanner's current (zero-based) column number.
+ int get column => _column;
+ int _column = 0;
+
+ /// The scanner's state, including line and column information.
+ ///
+ /// This can be used to efficiently save and restore the state of the scanner
+ /// when backtracking. A given [LineScannerState] is only valid for the
+ /// [LineScanner] that created it.
+ ///
+ /// This does not include the scanner's match information.
+ LineScannerState get state =>
+ LineScannerState._(this, position, line, column);
+
+ /// Whether the current position is between a CR character and an LF
+ /// charactet.
+ bool get _betweenCRLF => peekChar(-1) == $cr && peekChar() == $lf;
+
+ set state(LineScannerState state) {
+ if (!identical(state._scanner, this)) {
+ throw ArgumentError('The given LineScannerState was not returned by '
+ 'this LineScanner.');
+ }
+
+ super.position = state.position;
+ _line = state.line;
+ _column = state.column;
+ }
+
+ @override
+ set position(int newPosition) {
+ if (newPosition == position) {
+ return;
+ }
+
+ final oldPosition = position;
+ super.position = newPosition;
+
+ if (newPosition == 0) {
+ _line = 0;
+ _column = 0;
+ } else if (newPosition > oldPosition) {
+ final newlines = _newlinesIn(string.substring(oldPosition, newPosition),
+ endPosition: newPosition);
+ _line += newlines.length;
+ if (newlines.isEmpty) {
+ _column += newPosition - oldPosition;
+ } else {
+ // The regex got a substring, so we need to account for where it started
+ // in the string.
+ final offsetOfLastNewline = oldPosition + newlines.last.end;
+ _column = newPosition - offsetOfLastNewline;
+ }
+ } else if (newPosition < oldPosition) {
+ final newlines = _newlinesIn(string.substring(newPosition, oldPosition),
+ endPosition: oldPosition);
+
+ _line -= newlines.length;
+ if (newlines.isEmpty) {
+ _column -= oldPosition - newPosition;
+ } else {
+ // To compute the new column, we need to locate the last newline before
+ // the new position. When searching, we must exclude the CR if we're
+ // between a CRLF because it's not considered a newline.
+ final crOffset = _betweenCRLF ? -1 : 0;
+ // Additionally, if we use newPosition as the end of the search and the
+ // character at that position itself (the next character) is a newline
+ // we should not use it, so also offset to account for that.
+ const currentCharOffset = -1;
+ final lastNewline = string.lastIndexOf(
+ _newlineRegExp, newPosition + currentCharOffset + crOffset);
+
+ // Now we need to know the offset after the newline. This is the index
+ // above plus the length of the newline (eg. if we found `\r\n`) we need
+ // to add two. However if no newline was found, that index is 0.
+ final offsetAfterLastNewline = lastNewline == -1
+ ? 0
+ : string[lastNewline] == '\r' && string[lastNewline + 1] == '\n'
+ ? lastNewline + 2
+ : lastNewline + 1;
+
+ _column = newPosition - offsetAfterLastNewline;
+ }
+ }
+ }
+
+ LineScanner(super.string, {super.sourceUrl, super.position});
+
+ @override
+ bool scanChar(int character) {
+ if (!super.scanChar(character)) return false;
+ _adjustLineAndColumn(character);
+ return true;
+ }
+
+ @override
+ int readChar() {
+ final character = super.readChar();
+ _adjustLineAndColumn(character);
+ return character;
+ }
+
+ /// Adjusts [_line] and [_column] after having consumed [character].
+ void _adjustLineAndColumn(int character) {
+ if (character == $lf || (character == $cr && peekChar() != $lf)) {
+ _line += 1;
+ _column = 0;
+ } else {
+ _column += inSupplementaryPlane(character) ? 2 : 1;
+ }
+ }
+
+ @override
+ bool scan(Pattern pattern) {
+ if (!super.scan(pattern)) return false;
+
+ final newlines = _newlinesIn(lastMatch![0]!, endPosition: position);
+ _line += newlines.length;
+ if (newlines.isEmpty) {
+ _column += lastMatch![0]!.length;
+ } else {
+ _column = lastMatch![0]!.length - newlines.last.end;
+ }
+
+ return true;
+ }
+
+ /// Returns a list of [Match]es describing all the newlines in [text], which
+ /// ends at [endPosition].
+ ///
+ /// If [text] ends with `\r`, it will only be treated as a newline if the next
+ /// character at [position] is not a `\n`.
+ List<Match> _newlinesIn(String text, {required int endPosition}) {
+ final newlines = _newlineRegExp.allMatches(text).toList();
+ // If the last character is a `\r` it will have been treated as a newline,
+ // but this is only valid if the next character is not a `\n`.
+ if (endPosition < string.length &&
+ text.endsWith('\r') &&
+ string[endPosition] == '\n') {
+ // newlines should never be empty here, because if `text` ends with `\r`
+ // it would have matched `\r(?!\n)` in the newline regex.
+ newlines.removeLast();
+ }
+ return newlines;
+ }
+}
+
+/// A class representing the state of a [LineScanner].
+class LineScannerState {
+ /// The [LineScanner] that created this.
+ final LineScanner _scanner;
+
+ /// The position of the scanner in this state.
+ final int position;
+
+ /// The zero-based line number of the scanner in this state.
+ final int line;
+
+ /// The zero-based column number of the scanner in this state.
+ final int column;
+
+ LineScannerState._(this._scanner, this.position, this.line, this.column);
+}
diff --git a/pkgs/string_scanner/lib/src/relative_span_scanner.dart b/pkgs/string_scanner/lib/src/relative_span_scanner.dart
new file mode 100644
index 0000000..cd9af0e
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/relative_span_scanner.dart
@@ -0,0 +1,132 @@
+// 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:source_span/source_span.dart';
+
+import 'exception.dart';
+import 'line_scanner.dart';
+import 'span_scanner.dart';
+import 'string_scanner.dart';
+import 'utils.dart';
+
+/// A [SpanScanner] that scans within an existing [FileSpan].
+///
+/// This re-implements chunks of [SpanScanner] rather than using a dummy span or
+/// inheritance because scanning is often a performance-critical operation, so
+/// it's important to avoid adding extra overhead when relative scanning isn't
+/// needed.
+class RelativeSpanScanner extends StringScanner implements SpanScanner {
+ /// The source of the scanner.
+ ///
+ /// This caches line break information and is used to generate [SourceSpan]s.
+ final SourceFile _sourceFile;
+
+ /// The start location of the span within which this scanner is scanning.
+ ///
+ /// This is used to convert between span-relative and file-relative fields.
+ final FileLocation _startLocation;
+
+ @override
+ int get line =>
+ _sourceFile.getLine(_startLocation.offset + position) -
+ _startLocation.line;
+
+ @override
+ int get column {
+ final line = _sourceFile.getLine(_startLocation.offset + position);
+ final column =
+ _sourceFile.getColumn(_startLocation.offset + position, line: line);
+ return line == _startLocation.line
+ ? column - _startLocation.column
+ : column;
+ }
+
+ @override
+ LineScannerState get state => _SpanScannerState(this, position);
+
+ @override
+ set state(LineScannerState state) {
+ if (state is! _SpanScannerState || !identical(state._scanner, this)) {
+ throw ArgumentError('The given LineScannerState was not returned by '
+ 'this LineScanner.');
+ }
+
+ position = state.position;
+ }
+
+ @override
+ FileSpan? get lastSpan => _lastSpan;
+ FileSpan? _lastSpan;
+
+ @override
+ FileLocation get location =>
+ _sourceFile.location(_startLocation.offset + position);
+
+ @override
+ FileSpan get emptySpan => location.pointSpan();
+
+ RelativeSpanScanner(FileSpan span)
+ : _sourceFile = span.file,
+ _startLocation = span.start,
+ super(span.text, sourceUrl: span.sourceUrl);
+
+ @override
+ FileSpan spanFrom(LineScannerState startState, [LineScannerState? endState]) {
+ final endPosition = endState == null ? position : endState.position;
+ return _sourceFile.span(_startLocation.offset + startState.position,
+ _startLocation.offset + endPosition);
+ }
+
+ @override
+ FileSpan spanFromPosition(int startPosition, [int? endPosition]) {
+ RangeError.checkValidRange(
+ startPosition,
+ endPosition,
+ _sourceFile.length - _startLocation.offset,
+ 'startPosition',
+ 'endPosition');
+ return _sourceFile.span(_startLocation.offset + startPosition,
+ _startLocation.offset + (endPosition ?? position));
+ }
+
+ @override
+ bool matches(Pattern pattern) {
+ if (!super.matches(pattern)) {
+ _lastSpan = null;
+ return false;
+ }
+
+ _lastSpan = _sourceFile.span(_startLocation.offset + position,
+ _startLocation.offset + lastMatch!.end);
+ return true;
+ }
+
+ @override
+ Never error(String message, {Match? match, int? position, int? length}) {
+ validateErrorArgs(string, match, position, length);
+
+ if (match == null && position == null && length == null) match = lastMatch;
+ position ??= match == null ? this.position : match.start;
+ length ??= match == null ? 1 : match.end - match.start;
+
+ final span = _sourceFile.span(_startLocation.offset + position,
+ _startLocation.offset + position + length);
+ throw StringScannerException(message, span, string);
+ }
+}
+
+/// A class representing the state of a [SpanScanner].
+class _SpanScannerState implements LineScannerState {
+ /// The [SpanScanner] that created this.
+ final RelativeSpanScanner _scanner;
+
+ @override
+ final int position;
+ @override
+ int get line => _scanner._sourceFile.getLine(position);
+ @override
+ int get column => _scanner._sourceFile.getColumn(position);
+
+ _SpanScannerState(this._scanner, this.position);
+}
diff --git a/pkgs/string_scanner/lib/src/span_scanner.dart b/pkgs/string_scanner/lib/src/span_scanner.dart
new file mode 100644
index 0000000..509cf60
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/span_scanner.dart
@@ -0,0 +1,142 @@
+// 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 'package:source_span/source_span.dart';
+
+import 'eager_span_scanner.dart';
+import 'exception.dart';
+import 'line_scanner.dart';
+import 'relative_span_scanner.dart';
+import 'string_scanner.dart';
+import 'utils.dart';
+
+/// A subclass of [LineScanner] that exposes matched ranges as source map
+/// [FileSpan]s.
+class SpanScanner extends StringScanner implements LineScanner {
+ /// The source of the scanner.
+ ///
+ /// This caches line break information and is used to generate [FileSpan]s.
+ final SourceFile _sourceFile;
+
+ @override
+ int get line => _sourceFile.getLine(position);
+ @override
+ int get column => _sourceFile.getColumn(position);
+
+ @override
+ LineScannerState get state => _SpanScannerState(this, position);
+
+ @override
+ set state(LineScannerState state) {
+ if (state is! _SpanScannerState || !identical(state._scanner, this)) {
+ throw ArgumentError('The given LineScannerState was not returned by '
+ 'this LineScanner.');
+ }
+
+ position = state.position;
+ }
+
+ /// The [FileSpan] for [lastMatch].
+ ///
+ /// This is the span for the entire match. There's no way to get spans for
+ /// subgroups since [Match] exposes no information about their positions.
+ FileSpan? get lastSpan {
+ if (lastMatch == null) _lastSpan = null;
+ return _lastSpan;
+ }
+
+ FileSpan? _lastSpan;
+
+ /// The current location of the scanner.
+ FileLocation get location => _sourceFile.location(position);
+
+ /// Returns an empty span at the current location.
+ FileSpan get emptySpan => location.pointSpan();
+
+ /// Creates a new [SpanScanner] that starts scanning from [position].
+ ///
+ /// [sourceUrl] is used as [SourceLocation.sourceUrl] for the returned
+ /// [FileSpan]s as well as for error reporting. It can be a [String], a
+ /// [Uri], or `null`.
+ SpanScanner(super.string, {super.sourceUrl, super.position})
+ : _sourceFile = SourceFile.fromString(string, url: sourceUrl);
+
+ /// Creates a new [SpanScanner] that eagerly computes line and column numbers.
+ ///
+ /// In general [SpanScanner.new] will be more efficient, since it avoids extra
+ /// computation on every scan. However, eager scanning can be useful for
+ /// situations where the normal course of parsing frequently involves
+ /// accessing the current line and column numbers.
+ ///
+ /// Note that *only* the `line` and `column` fields on the `SpanScanner`
+ /// itself and its `LineScannerState` are eagerly computed. To limit their
+ /// memory footprint, returned spans and locations will still lazily compute
+ /// their line and column numbers.
+ factory SpanScanner.eager(String string, {sourceUrl, int? position}) =
+ EagerSpanScanner;
+
+ /// Creates a new [SpanScanner] that scans within [span].
+ ///
+ /// This scans through [span]`.text, but emits new spans from [span]`.file` in
+ /// their appropriate relative positions. The [string] field contains only
+ /// [span]`.text`, and [position], [line], and [column] are all relative to
+ /// the span.
+ factory SpanScanner.within(FileSpan span) = RelativeSpanScanner;
+
+ /// Creates a [FileSpan] representing the source range between [startState]
+ /// and the current position.
+ FileSpan spanFrom(LineScannerState startState, [LineScannerState? endState]) {
+ final endPosition = endState == null ? position : endState.position;
+ return _sourceFile.span(startState.position, endPosition);
+ }
+
+ /// Creates a [FileSpan] representing the source range between [startPosition]
+ /// and [endPosition], or the current position if [endPosition] is null.
+ ///
+ /// Each position should be a code unit offset into the string being scanned,
+ /// with the same conventions as [StringScanner.position].
+ ///
+ /// Throws a [RangeError] if [startPosition] or [endPosition] aren't within
+ /// this source file.
+ FileSpan spanFromPosition(int startPosition, [int? endPosition]) =>
+ _sourceFile.span(startPosition, endPosition ?? position);
+
+ @override
+ bool matches(Pattern pattern) {
+ if (!super.matches(pattern)) {
+ _lastSpan = null;
+ return false;
+ }
+
+ _lastSpan = _sourceFile.span(position, lastMatch!.end);
+ return true;
+ }
+
+ @override
+ Never error(String message, {Match? match, int? position, int? length}) {
+ validateErrorArgs(string, match, position, length);
+
+ if (match == null && position == null && length == null) match = lastMatch;
+ position ??= match == null ? this.position : match.start;
+ length ??= match == null ? 0 : match.end - match.start;
+
+ final span = _sourceFile.span(position, position + length);
+ throw StringScannerException(message, span, string);
+ }
+}
+
+/// A class representing the state of a [SpanScanner].
+class _SpanScannerState implements LineScannerState {
+ /// The [SpanScanner] that created this.
+ final SpanScanner _scanner;
+
+ @override
+ final int position;
+ @override
+ int get line => _scanner._sourceFile.getLine(position);
+ @override
+ int get column => _scanner._sourceFile.getColumn(position);
+
+ _SpanScannerState(this._scanner, this.position);
+}
diff --git a/pkgs/string_scanner/lib/src/string_scanner.dart b/pkgs/string_scanner/lib/src/string_scanner.dart
new file mode 100644
index 0000000..1466944
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/string_scanner.dart
@@ -0,0 +1,272 @@
+// 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 'package:source_span/source_span.dart';
+
+import 'charcode.dart';
+import 'exception.dart';
+import 'utils.dart';
+
+/// A class that scans through a string using [Pattern]s.
+class StringScanner {
+ /// The URL of the source of the string being scanned.
+ ///
+ /// This is used for error reporting. It may be `null`, indicating that the
+ /// source URL is unknown or unavailable.
+ final Uri? sourceUrl;
+
+ /// The string being scanned through.
+ final String string;
+
+ /// The current position of the scanner in the string, in characters.
+ int get position => _position;
+ set position(int position) {
+ if (position.isNegative || position > string.length) {
+ throw ArgumentError('Invalid position $position');
+ }
+
+ _position = position;
+ _lastMatch = null;
+ }
+
+ int _position = 0;
+
+ /// The data about the previous match made by the scanner.
+ ///
+ /// If the last match failed, this will be `null`.
+ Match? get lastMatch {
+ // Lazily unset [_lastMatch] so that we avoid extra assignments in
+ // character-by-character methods that are used in core loops.
+ if (_position != _lastMatchPosition) _lastMatch = null;
+ return _lastMatch;
+ }
+
+ Match? _lastMatch;
+ int? _lastMatchPosition;
+
+ /// The portion of the string that hasn't yet been scanned.
+ String get rest => string.substring(position);
+
+ /// Whether the scanner has completely consumed [string].
+ bool get isDone => position == string.length;
+
+ /// Creates a new [StringScanner] that starts scanning from [position].
+ ///
+ /// [position] defaults to 0, the beginning of the string. [sourceUrl] is the
+ /// URL of the source of the string being scanned, if available. It can be
+ /// a [String], a [Uri], or `null`.
+ StringScanner(this.string, {Object? sourceUrl, int? position})
+ : sourceUrl = sourceUrl == null
+ ? null
+ : sourceUrl is String
+ ? Uri.parse(sourceUrl)
+ : sourceUrl as Uri {
+ if (position != null) this.position = position;
+ }
+
+ /// Consumes a single character and returns its character code.
+ ///
+ /// This throws a [FormatException] if the string has been fully consumed. It
+ /// doesn't affect [lastMatch].
+ int readChar() {
+ if (isDone) _fail('more input');
+ return string.codeUnitAt(_position++);
+ }
+
+ /// Returns the character code of the character [offset] away from [position].
+ ///
+ /// [offset] defaults to zero, and may be negative to inspect already-consumed
+ /// characters.
+ ///
+ /// This returns `null` if [offset] points outside the string. It doesn't
+ /// affect [lastMatch].
+ int? peekChar([int? offset]) {
+ offset ??= 0;
+ final index = position + offset;
+ if (index < 0 || index >= string.length) return null;
+ return string.codeUnitAt(index);
+ }
+
+ /// If the next character in the string is [character], consumes it.
+ ///
+ /// If [character] is a Unicode code point in a supplementary plane, this will
+ /// consume two code units. Dart's string representation is UTF-16, which
+ /// represents supplementary-plane code units as two code units.
+ ///
+ /// Returns whether or not [character] was consumed.
+ bool scanChar(int character) {
+ if (inSupplementaryPlane(character)) {
+ if (_position + 1 >= string.length ||
+ string.codeUnitAt(_position) != highSurrogate(character) ||
+ string.codeUnitAt(_position + 1) != lowSurrogate(character)) {
+ return false;
+ } else {
+ _position += 2;
+ return true;
+ }
+ } else {
+ if (isDone) return false;
+ if (string.codeUnitAt(_position) != character) return false;
+ _position++;
+ return true;
+ }
+ }
+
+ /// If the next character in the string is [character], consumes it.
+ ///
+ /// If [character] is a Unicode code point in a supplementary plane, this will
+ /// consume two code units. Dart's string representation is UTF-16, which
+ /// represents supplementary-plane code units as two code units.
+ ///
+ /// If [character] could not be consumed, throws a [FormatException]
+ /// describing the position of the failure. [name] is used in this error as
+ /// the expected name of the character being matched; if it's `null`, the
+ /// character itself is used instead.
+ void expectChar(int character, {String? name}) {
+ if (scanChar(character)) return;
+
+ if (name == null) {
+ if (character == $backslash) {
+ name = r'"\"';
+ } else if (character == $doubleQuote) {
+ name = r'"\""';
+ } else {
+ name = '"${String.fromCharCode(character)}"';
+ }
+ }
+
+ _fail(name);
+ }
+
+ /// Consumes a single Unicode code unit and returns it.
+ ///
+ /// This works like [readChar], except that it automatically handles UTF-16
+ /// surrogate pairs. Specifically, if the next two code units form a surrogate
+ /// pair, consumes them both and returns the corresponding Unicode code point.
+ ///
+ /// If next two characters are not a surrogate pair, the next code unit is
+ /// returned as-is, even if it's an unpaired surrogate.
+ int readCodePoint() {
+ final first = readChar();
+ if (!isHighSurrogate(first)) return first;
+
+ final next = peekChar();
+ if (next == null || !isLowSurrogate(next)) return first;
+
+ readChar();
+ return decodeSurrogatePair(first, next);
+ }
+
+ /// Returns the Unicode code point immediately after [position].
+ ///
+ /// This works like [peekChar], except that it automatically handles UTF-16
+ /// surrogate pairs. Specifically, if the next two code units form a surrogate
+ /// pair, returns the corresponding Unicode code point.
+ ///
+ /// If next two characters are not a surrogate pair, the next code unit is
+ /// returned as-is, even if it's an unpaired surrogate.
+ int? peekCodePoint() {
+ final first = peekChar();
+ if (first == null || !isHighSurrogate(first)) return first;
+
+ final next = peekChar(1);
+ if (next == null || !isLowSurrogate(next)) return first;
+
+ return decodeSurrogatePair(first, next);
+ }
+
+ /// If [pattern] matches at the current position of the string, scans forward
+ /// until the end of the match.
+ ///
+ /// Returns whether or not [pattern] matched.
+ bool scan(Pattern pattern) {
+ final success = matches(pattern);
+ if (success) {
+ _position = _lastMatch!.end;
+ _lastMatchPosition = _position;
+ }
+ return success;
+ }
+
+ /// If [pattern] matches at the current position of the string, scans forward
+ /// until the end of the match.
+ ///
+ /// If [pattern] did not match, throws a [FormatException] describing the
+ /// position of the failure. [name] is used in this error as the expected name
+ /// of the pattern being matched; if it's `null`, the pattern itself is used
+ /// instead.
+ void expect(Pattern pattern, {String? name}) {
+ if (scan(pattern)) return;
+
+ if (name == null) {
+ if (pattern is RegExp) {
+ final source = pattern.pattern;
+ name = '/$source/';
+ } else {
+ name =
+ pattern.toString().replaceAll(r'\', r'\\').replaceAll('"', r'\"');
+ name = '"$name"';
+ }
+ }
+ _fail(name);
+ }
+
+ /// If the string has not been fully consumed, this throws a
+ /// [FormatException].
+ void expectDone() {
+ if (isDone) return;
+ _fail('no more input');
+ }
+
+ /// Returns whether or not [pattern] matches at the current position of the
+ /// string.
+ ///
+ /// This doesn't move the scan pointer forward.
+ bool matches(Pattern pattern) {
+ _lastMatch = pattern.matchAsPrefix(string, position);
+ _lastMatchPosition = _position;
+ return _lastMatch != null;
+ }
+
+ /// Returns the substring of [string] between [start] and [end].
+ ///
+ /// Unlike [String.substring], [end] defaults to [position] rather than the
+ /// end of the string.
+ String substring(int start, [int? end]) {
+ end ??= position;
+ return string.substring(start, end);
+ }
+
+ /// Throws a [FormatException] with [message] as well as a detailed
+ /// description of the location of the error in the string.
+ ///
+ /// [match] is the match information for the span of the string with which the
+ /// error is associated. This should be a match returned by this scanner's
+ /// [lastMatch] property. By default, the error is associated with the last
+ /// match.
+ ///
+ /// If [position] and/or [length] are passed, they are used as the error span
+ /// instead. If only [length] is passed, [position] defaults to the current
+ /// position; if only [position] is passed, [length] defaults to 0.
+ ///
+ /// It's an error to pass [match] at the same time as [position] or [length].
+ Never error(String message, {Match? match, int? position, int? length}) {
+ validateErrorArgs(string, match, position, length);
+
+ if (match == null && position == null && length == null) match = lastMatch;
+ position ??= match == null ? this.position : match.start;
+ length ??= match == null ? 0 : match.end - match.start;
+
+ final sourceFile = SourceFile.fromString(string, url: sourceUrl);
+ final span = sourceFile.span(position, position + length);
+ throw StringScannerException(message, span, string);
+ }
+
+ // TODO(nweiz): Make this handle long lines more gracefully.
+ /// Throws a [FormatException] describing that [name] is expected at the
+ /// current position in the string.
+ Never _fail(String name) {
+ error('expected $name.', position: position, length: 0);
+ }
+}
diff --git a/pkgs/string_scanner/lib/src/utils.dart b/pkgs/string_scanner/lib/src/utils.dart
new file mode 100644
index 0000000..39891a1
--- /dev/null
+++ b/pkgs/string_scanner/lib/src/utils.dart
@@ -0,0 +1,95 @@
+// 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 'string_scanner.dart';
+
+/// Validates the arguments passed to [StringScanner.error].
+void validateErrorArgs(
+ String string, Match? match, int? position, int? length) {
+ if (match != null && (position != null || length != null)) {
+ throw ArgumentError("Can't pass both match and position/length.");
+ }
+
+ if (position != null) {
+ if (position < 0) {
+ throw RangeError('position must be greater than or equal to 0.');
+ } else if (position > string.length) {
+ throw RangeError('position must be less than or equal to the '
+ 'string length.');
+ }
+ }
+
+ if (length != null && length < 0) {
+ throw RangeError('length must be greater than or equal to 0.');
+ }
+
+ if (position != null && length != null && position + length > string.length) {
+ throw RangeError('position plus length must not go beyond the end of '
+ 'the string.');
+ }
+}
+
+// See https://en.wikipedia.org/wiki/UTF-16#Code_points_from_U+010000_to_U+10FFFF
+// for documentation on how UTF-16 encoding works and definitions of various
+// related terms.
+
+/// The inclusive lower bound of Unicode's supplementary plane.
+const _supplementaryPlaneLowerBound = 0x10000;
+
+/// The inclusive upper bound of Unicode's supplementary plane.
+const _supplementaryPlaneUpperBound = 0x10FFFF;
+
+/// The inclusive lower bound of the UTF-16 high surrogate block.
+const _highSurrogateLowerBound = 0xD800;
+
+/// The inclusive lower bound of the UTF-16 low surrogate block.
+const _lowSurrogateLowerBound = 0xDC00;
+
+/// The number of low bits in each code unit of a surrogate pair that goes into
+/// determining which code point it encodes.
+const _surrogateBits = 10;
+
+/// A bit mask that covers the lower [_surrogateBits] of a code point, which can
+/// be used to extract the value of a surrogate or the low surrogate value of a
+/// code unit.
+const _surrogateValueMask = (1 << _surrogateBits) - 1;
+
+/// Returns whether [codePoint] is in the Unicode supplementary plane, and thus
+/// must be represented as a surrogate pair in UTF-16.
+bool inSupplementaryPlane(int codePoint) =>
+ codePoint >= _supplementaryPlaneLowerBound &&
+ codePoint <= _supplementaryPlaneUpperBound;
+
+/// Returns whether [codeUnit] is a UTF-16 high surrogate.
+bool isHighSurrogate(int codeUnit) =>
+ (codeUnit & ~_surrogateValueMask) == _highSurrogateLowerBound;
+
+/// Returns whether [codeUnit] is a UTF-16 low surrogate.
+bool isLowSurrogate(int codeUnit) =>
+ (codeUnit >> _surrogateBits) == (_lowSurrogateLowerBound >> _surrogateBits);
+
+/// Returns the high surrogate needed to encode the supplementary-plane
+/// [codePoint].
+int highSurrogate(int codePoint) {
+ assert(inSupplementaryPlane(codePoint));
+ return ((codePoint - _supplementaryPlaneLowerBound) >> _surrogateBits) +
+ _highSurrogateLowerBound;
+}
+
+/// Returns the low surrogate needed to encode the supplementary-plane
+/// [codePoint].
+int lowSurrogate(int codePoint) {
+ assert(inSupplementaryPlane(codePoint));
+ return ((codePoint - _supplementaryPlaneLowerBound) & _surrogateValueMask) +
+ _lowSurrogateLowerBound;
+}
+
+/// Converts a UTF-16 surrogate pair into the Unicode code unit it represents.
+int decodeSurrogatePair(int highSurrogate, int lowSurrogate) {
+ assert(isHighSurrogate(highSurrogate));
+ assert(isLowSurrogate(lowSurrogate));
+ return _supplementaryPlaneLowerBound +
+ (((highSurrogate & _surrogateValueMask) << _surrogateBits) |
+ (lowSurrogate & _surrogateValueMask));
+}
diff --git a/pkgs/string_scanner/lib/string_scanner.dart b/pkgs/string_scanner/lib/string_scanner.dart
new file mode 100644
index 0000000..e641ae7
--- /dev/null
+++ b/pkgs/string_scanner/lib/string_scanner.dart
@@ -0,0 +1,11 @@
+// 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.
+
+/// A library for parsing strings using a sequence of patterns.
+library;
+
+export 'src/exception.dart';
+export 'src/line_scanner.dart';
+export 'src/span_scanner.dart';
+export 'src/string_scanner.dart';
diff --git a/pkgs/string_scanner/pubspec.yaml b/pkgs/string_scanner/pubspec.yaml
new file mode 100644
index 0000000..9b259cf
--- /dev/null
+++ b/pkgs/string_scanner/pubspec.yaml
@@ -0,0 +1,14 @@
+name: string_scanner
+version: 1.4.1
+description: A class for parsing strings using a sequence of patterns.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/string_scanner
+
+environment:
+ sdk: ^3.1.0
+
+dependencies:
+ source_span: ^1.8.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
+ test: ^1.16.6
diff --git a/pkgs/string_scanner/test/error_test.dart b/pkgs/string_scanner/test/error_test.dart
new file mode 100644
index 0000000..1f98c32
--- /dev/null
+++ b/pkgs/string_scanner/test/error_test.dart
@@ -0,0 +1,143 @@
+// 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 'package:string_scanner/string_scanner.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ test('defaults to the last match', () {
+ final scanner = StringScanner('foo bar baz');
+ scanner.expect('foo ');
+ scanner.expect('bar');
+ expect(() => scanner.error('oh no!'), throwsStringScannerException('bar'));
+ });
+
+ group('with match', () {
+ test('supports an earlier match', () {
+ final scanner = StringScanner('foo bar baz');
+ scanner.expect('foo ');
+ final match = scanner.lastMatch;
+ scanner.expect('bar');
+ expect(() => scanner.error('oh no!', match: match),
+ throwsStringScannerException('foo '));
+ });
+
+ test('supports a match on a previous line', () {
+ final scanner = StringScanner('foo bar baz\ndo re mi\nearth fire water');
+ scanner.expect('foo bar baz\ndo ');
+ scanner.expect('re');
+ final match = scanner.lastMatch;
+ scanner.expect(' mi\nearth ');
+ expect(() => scanner.error('oh no!', match: match),
+ throwsStringScannerException('re'));
+ });
+
+ test('supports a multiline match', () {
+ final scanner = StringScanner('foo bar baz\ndo re mi\nearth fire water');
+ scanner.expect('foo bar ');
+ scanner.expect('baz\ndo');
+ final match = scanner.lastMatch;
+ scanner.expect(' re mi');
+ expect(() => scanner.error('oh no!', match: match),
+ throwsStringScannerException('baz\ndo'));
+ });
+
+ test('supports a match after position', () {
+ final scanner = StringScanner('foo bar baz');
+ scanner.expect('foo ');
+ scanner.expect('bar');
+ final match = scanner.lastMatch;
+ scanner.position = 0;
+ expect(() => scanner.error('oh no!', match: match),
+ throwsStringScannerException('bar'));
+ });
+ });
+
+ group('with position and/or length', () {
+ test('defaults to length 0', () {
+ final scanner = StringScanner('foo bar baz');
+ scanner.expect('foo ');
+ expect(() => scanner.error('oh no!', position: 1),
+ throwsStringScannerException(''));
+ });
+
+ test('defaults to the current position', () {
+ final scanner = StringScanner('foo bar baz');
+ scanner.expect('foo ');
+ expect(() => scanner.error('oh no!', length: 3),
+ throwsStringScannerException('bar'));
+ });
+
+ test('supports an earlier position', () {
+ final scanner = StringScanner('foo bar baz');
+ scanner.expect('foo ');
+ expect(() => scanner.error('oh no!', position: 1, length: 2),
+ throwsStringScannerException('oo'));
+ });
+
+ test('supports a position on a previous line', () {
+ final scanner = StringScanner('foo bar baz\ndo re mi\nearth fire water');
+ scanner.expect('foo bar baz\ndo re mi\nearth');
+ expect(() => scanner.error('oh no!', position: 15, length: 2),
+ throwsStringScannerException('re'));
+ });
+
+ test('supports a multiline length', () {
+ final scanner = StringScanner('foo bar baz\ndo re mi\nearth fire water');
+ scanner.expect('foo bar baz\ndo re mi\nearth');
+ expect(() => scanner.error('oh no!', position: 8, length: 8),
+ throwsStringScannerException('baz\ndo r'));
+ });
+
+ test('supports a position after the current one', () {
+ final scanner = StringScanner('foo bar baz');
+ expect(() => scanner.error('oh no!', position: 4, length: 3),
+ throwsStringScannerException('bar'));
+ });
+
+ test('supports a length of zero', () {
+ final scanner = StringScanner('foo bar baz');
+ expect(() => scanner.error('oh no!', position: 4, length: 0),
+ throwsStringScannerException(''));
+ });
+ });
+
+ group('argument errors', () {
+ late StringScanner scanner;
+ setUp(() {
+ scanner = StringScanner('foo bar baz');
+ scanner.scan('foo');
+ });
+
+ test('if match is passed with position', () {
+ expect(
+ () => scanner.error('oh no!', match: scanner.lastMatch, position: 1),
+ throwsArgumentError);
+ });
+
+ test('if match is passed with length', () {
+ expect(() => scanner.error('oh no!', match: scanner.lastMatch, length: 1),
+ throwsArgumentError);
+ });
+
+ test('if position is negative', () {
+ expect(() => scanner.error('oh no!', position: -1), throwsArgumentError);
+ });
+
+ test('if position is outside the string', () {
+ expect(() => scanner.error('oh no!', position: 100), throwsArgumentError);
+ });
+
+ test('if position + length is outside the string', () {
+ expect(() => scanner.error('oh no!', position: 7, length: 7),
+ throwsArgumentError);
+ });
+
+ test('if length is negative', () {
+ expect(() => scanner.error('oh no!', length: -1), throwsArgumentError);
+ });
+ });
+}
diff --git a/pkgs/string_scanner/test/line_scanner_test.dart b/pkgs/string_scanner/test/line_scanner_test.dart
new file mode 100644
index 0000000..1af5c36
--- /dev/null
+++ b/pkgs/string_scanner/test/line_scanner_test.dart
@@ -0,0 +1,465 @@
+// 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 'package:string_scanner/src/charcode.dart';
+import 'package:string_scanner/string_scanner.dart';
+import 'package:test/test.dart';
+
+void main() {
+ late LineScanner scanner;
+ setUp(() {
+ scanner = LineScanner('foo\nbar\r\nbaz');
+ });
+
+ test('begins with line and column 0', () {
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(0));
+ });
+
+ group('scan()', () {
+ test('consuming no newlines increases the column but not the line', () {
+ scanner.expect('foo');
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(3));
+ });
+
+ test('consuming a LF resets the column and increases the line', () {
+ scanner.expect('foo\nba');
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(2));
+ });
+
+ test('consuming multiple LFs resets the column and increases the line', () {
+ scanner.expect('foo\nbar\r\nb');
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(1));
+ });
+
+ test('consuming a CR LF increases the line only after the LF', () {
+ scanner.expect('foo\nbar\r');
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(4));
+
+ scanner.expect('\nb');
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(1));
+ });
+
+ test('consuming a CR not followed by LF increases the line', () {
+ scanner = LineScanner('foo\nbar\rbaz');
+ scanner.expect('foo\nbar\r');
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(0));
+
+ scanner.expect('b');
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(1));
+ });
+
+ test('consuming a CR at the end increases the line', () {
+ scanner = LineScanner('foo\nbar\r');
+ scanner.expect('foo\nbar\r');
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(0));
+ expect(scanner.isDone, isTrue);
+ });
+
+ test('consuming a mix of CR, LF, CR+LF increases the line', () {
+ scanner = LineScanner('0\n1\r2\r\n3');
+ scanner.expect('0\n1\r2\r\n3');
+ expect(scanner.line, equals(3));
+ expect(scanner.column, equals(1));
+ });
+
+ test('scanning a zero length match between CR LF does not fail', () {
+ scanner.expect('foo\nbar\r');
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(4));
+ scanner.expect(RegExp('(?!x)'));
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(4));
+ });
+ });
+
+ group('readChar()', () {
+ test('on a non-newline character increases the column but not the line',
+ () {
+ scanner.readChar();
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(1));
+ });
+
+ test('consuming a LF resets the column and increases the line', () {
+ scanner.expect('foo');
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(3));
+
+ scanner.readChar();
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(0));
+ });
+
+ test('consuming a CR LF increases the line only after the LF', () {
+ scanner = LineScanner('foo\r\nbar');
+ scanner.expect('foo');
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(3));
+
+ scanner.readChar();
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(4));
+
+ scanner.readChar();
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(0));
+ });
+
+ test('consuming a CR not followed by a LF increases the line', () {
+ scanner = LineScanner('foo\nbar\rbaz');
+ scanner.expect('foo\nbar');
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(3));
+
+ scanner.readChar();
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(0));
+ });
+
+ test('consuming a CR at the end increases the line', () {
+ scanner = LineScanner('foo\nbar\r');
+ scanner.expect('foo\nbar');
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(3));
+
+ scanner.readChar();
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(0));
+ });
+
+ test('consuming a mix of CR, LF, CR+LF increases the line', () {
+ scanner = LineScanner('0\n1\r2\r\n3');
+ for (var i = 0; i < scanner.string.length; i++) {
+ scanner.readChar();
+ }
+
+ expect(scanner.line, equals(3));
+ expect(scanner.column, equals(1));
+ });
+ });
+
+ group('readCodePoint()', () {
+ test('on a non-newline character increases the column but not the line',
+ () {
+ scanner.readCodePoint();
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(1));
+ });
+
+ test('consuming a newline resets the column and increases the line', () {
+ scanner.expect('foo');
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(3));
+
+ scanner.readCodePoint();
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(0));
+ });
+
+ test("consuming halfway through a CR LF doesn't count as a line", () {
+ scanner.expect('foo\nbar');
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(3));
+
+ scanner.readCodePoint();
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(4));
+
+ scanner.readCodePoint();
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(0));
+ });
+ });
+
+ group('scanChar()', () {
+ test('on a non-newline character increases the column but not the line',
+ () {
+ scanner.scanChar($f);
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(1));
+ });
+
+ test('consuming a LF resets the column and increases the line', () {
+ scanner.expect('foo');
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(3));
+
+ scanner.scanChar($lf);
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(0));
+ });
+
+ test('consuming a CR LF increases the line only after the LF', () {
+ scanner.expect('foo\nbar');
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(3));
+
+ scanner.scanChar($cr);
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(4));
+
+ scanner.scanChar($lf);
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(0));
+ });
+
+ test('consuming a CR not followed by LF increases the line', () {
+ scanner = LineScanner('foo\rbar');
+ scanner.expect('foo');
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(3));
+
+ scanner.scanChar($cr);
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(0));
+ });
+
+ test('consuming a CR at the end increases the line', () {
+ scanner = LineScanner('foo\r');
+ scanner.expect('foo');
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(3));
+
+ scanner.scanChar($cr);
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(0));
+ });
+
+ test('consuming a mix of CR, LF, CR+LF increases the line', () {
+ scanner = LineScanner('0\n1\r2\r\n3');
+ for (var i = 0; i < scanner.string.length; i++) {
+ scanner.scanChar(scanner.string[i].codeUnits.single);
+ }
+
+ expect(scanner.line, equals(3));
+ expect(scanner.column, equals(1));
+ });
+ });
+
+ group('before a surrogate pair', () {
+ final codePoint = '\uD83D\uDC6D'.runes.first;
+ const highSurrogate = 0xD83D;
+
+ late LineScanner scanner;
+ setUp(() {
+ scanner = LineScanner('foo: \uD83D\uDC6D');
+ expect(scanner.scan('foo: '), isTrue);
+ });
+
+ test('readChar returns the high surrogate and moves into the pair', () {
+ expect(scanner.readChar(), equals(highSurrogate));
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(6));
+ expect(scanner.position, equals(6));
+ });
+
+ test('readCodePoint returns the code unit and moves past the pair', () {
+ expect(scanner.readCodePoint(), equals(codePoint));
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(7));
+ expect(scanner.position, equals(7));
+ });
+
+ test('scanChar with the high surrogate moves into the pair', () {
+ expect(scanner.scanChar(highSurrogate), isTrue);
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(6));
+ expect(scanner.position, equals(6));
+ });
+
+ test('scanChar with the code point moves past the pair', () {
+ expect(scanner.scanChar(codePoint), isTrue);
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(7));
+ expect(scanner.position, equals(7));
+ });
+
+ test('expectChar with the high surrogate moves into the pair', () {
+ scanner.expectChar(highSurrogate);
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(6));
+ expect(scanner.position, equals(6));
+ });
+
+ test('expectChar with the code point moves past the pair', () {
+ scanner.expectChar(codePoint);
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(7));
+ expect(scanner.position, equals(7));
+ });
+ });
+
+ group('position=', () {
+ test('forward through LFs sets the line and column', () {
+ scanner = LineScanner('foo\nbar\nbaz');
+ scanner.position = 9; // "foo\nbar\nb"
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(1));
+ });
+
+ test('forward from non-zero character through LFs sets the line and column',
+ () {
+ scanner = LineScanner('foo\nbar\nbaz');
+ scanner.expect('fo');
+ scanner.position = 9; // "foo\nbar\nb"
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(1));
+ });
+
+ test('forward through CR LFs sets the line and column', () {
+ scanner = LineScanner('foo\r\nbar\r\nbaz');
+ scanner.position = 11; // "foo\r\nbar\r\nb"
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(1));
+ });
+
+ test('forward through CR not followed by LFs sets the line and column', () {
+ scanner = LineScanner('foo\rbar\rbaz');
+ scanner.position = 9; // "foo\rbar\rb"
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(1));
+ });
+
+ test('forward through CR at end sets the line and column', () {
+ scanner = LineScanner('foo\rbar\r');
+ scanner.position = 8; // "foo\rbar\r"
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(0));
+ });
+
+ test('forward through a mix of CR, LF, CR+LF sets the line and column', () {
+ scanner = LineScanner('0\n1\r2\r\n3');
+ scanner.position = scanner.string.length;
+
+ expect(scanner.line, equals(3));
+ expect(scanner.column, equals(1));
+ });
+
+ test('forward through no newlines sets the column', () {
+ scanner.position = 2; // "fo"
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(2));
+ });
+
+ test('backward through LFs sets the line and column', () {
+ scanner = LineScanner('foo\nbar\nbaz');
+ scanner.expect('foo\nbar\nbaz');
+ scanner.position = 2; // "fo"
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(2));
+ });
+
+ test('backward through CR LFs sets the line and column', () {
+ scanner = LineScanner('foo\r\nbar\r\nbaz');
+ scanner.expect('foo\r\nbar\r\nbaz');
+ scanner.position = 2; // "fo"
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(2));
+ });
+
+ test('backward through CR not followed by LFs sets the line and column',
+ () {
+ scanner = LineScanner('foo\rbar\rbaz');
+ scanner.expect('foo\rbar\rbaz');
+ scanner.position = 2; // "fo"
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(2));
+ });
+
+ test('backward through CR at end sets the line and column', () {
+ scanner = LineScanner('foo\rbar\r');
+ scanner.expect('foo\rbar\r');
+ scanner.position = 2; // "fo"
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(2));
+ });
+
+ test('backward through a mix of CR, LF, CR+LF sets the line and column',
+ () {
+ scanner = LineScanner('0\n1\r2\r\n3');
+ scanner.expect(scanner.string);
+
+ scanner.position = 1;
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(1));
+ });
+
+ test('backward through no newlines sets the column', () {
+ scanner.expect('foo\nbar\r\nbaz');
+ scanner.position = 10; // "foo\nbar\r\nb"
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(1));
+ });
+
+ test("forward halfway through a CR LF doesn't count as a line", () {
+ scanner.position = 8; // "foo\nbar\r"
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(4));
+ });
+
+ test('forward from halfway through a CR LF counts as a line', () {
+ scanner.expect('foo\nbar\r');
+ scanner.position = 11; // "foo\nbar\r\nba"
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(2));
+ });
+
+ test('backward to between CR LF', () {
+ scanner.expect('foo\nbar\r\nbaz');
+ scanner.position = 8; // "foo\nbar\r"
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(4));
+ });
+
+ test('backward from between CR LF', () {
+ scanner.expect('foo\nbar\r');
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(4));
+ scanner.position = 5; // "foo\nb"
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(1));
+ });
+
+ test('backward to after CR LF', () {
+ scanner.expect('foo\nbar\r\nbaz');
+ scanner.position = 9; // "foo\nbar\r\n"
+ expect(scanner.line, equals(2));
+ expect(scanner.column, equals(0));
+ });
+
+ test('backward to before CR LF', () {
+ scanner.expect('foo\nbar\r\nbaz');
+ scanner.position = 7; // "foo\nbar"
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(3));
+ });
+ });
+
+ test('state= restores the line, column, and position', () {
+ scanner.expect('foo\nb');
+ final state = scanner.state;
+
+ scanner.scan('ar\nba');
+ scanner.state = state;
+ expect(scanner.rest, equals('ar\r\nbaz'));
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(1));
+ });
+
+ test('state= rejects a foreign state', () {
+ scanner.scan('foo\nb');
+
+ expect(() => LineScanner(scanner.string).state = scanner.state,
+ throwsArgumentError);
+ });
+}
diff --git a/pkgs/string_scanner/test/span_scanner_test.dart b/pkgs/string_scanner/test/span_scanner_test.dart
new file mode 100644
index 0000000..93d9c47
--- /dev/null
+++ b/pkgs/string_scanner/test/span_scanner_test.dart
@@ -0,0 +1,238 @@
+// 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 'package:source_span/source_span.dart';
+import 'package:string_scanner/string_scanner.dart';
+import 'package:test/test.dart';
+
+import 'utils.dart';
+
+void main() {
+ testForImplementation(
+ 'lazy',
+ ([String? string]) =>
+ SpanScanner(string ?? 'foo\nbar\nbaz', sourceUrl: 'source'));
+
+ testForImplementation(
+ 'eager',
+ ([String? string]) =>
+ SpanScanner.eager(string ?? 'foo\nbar\nbaz', sourceUrl: 'source'));
+
+ group('within', () {
+ const text = 'first\nbefore: foo\nbar\nbaz :after\nlast';
+ final startOffset = text.indexOf('foo');
+
+ late SpanScanner scanner;
+ setUp(() {
+ final file = SourceFile.fromString(text, url: 'source');
+ scanner =
+ SpanScanner.within(file.span(startOffset, text.indexOf(' :after')));
+ });
+
+ test('string only includes the span text', () {
+ expect(scanner.string, equals('foo\nbar\nbaz'));
+ });
+
+ test('line and column are span-relative', () {
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(0));
+
+ scanner.scan('foo');
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(3));
+
+ scanner.scan('\n');
+ expect(scanner.line, equals(1));
+ expect(scanner.column, equals(0));
+ });
+
+ test('tracks the span for the last match', () {
+ scanner.scan('fo');
+ scanner.scan('o\nba');
+
+ final span = scanner.lastSpan!;
+ expect(span.start.offset, equals(startOffset + 2));
+ expect(span.start.line, equals(1));
+ expect(span.start.column, equals(10));
+ expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+ expect(span.end.offset, equals(startOffset + 6));
+ expect(span.end.line, equals(2));
+ expect(span.end.column, equals(2));
+ expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+ expect(span.text, equals('o\nba'));
+ });
+
+ test('.spanFrom() returns a span from a previous state', () {
+ scanner.scan('fo');
+ final state = scanner.state;
+ scanner.scan('o\nba');
+ scanner.scan('r\nba');
+
+ final span = scanner.spanFrom(state);
+ expect(span.text, equals('o\nbar\nba'));
+ });
+
+ test('.spanFromPosition() returns a span from a previous state', () {
+ scanner.scan('fo');
+ final start = scanner.position;
+ scanner.scan('o\nba');
+ scanner.scan('r\nba');
+
+ final span = scanner.spanFromPosition(start + 2, start + 5);
+ expect(span.text, equals('bar'));
+ });
+
+ test('.emptySpan returns an empty span at the current location', () {
+ scanner.scan('foo\nba');
+
+ final span = scanner.emptySpan;
+ expect(span.start.offset, equals(startOffset + 6));
+ expect(span.start.line, equals(2));
+ expect(span.start.column, equals(2));
+ expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+ expect(span.end.offset, equals(startOffset + 6));
+ expect(span.end.line, equals(2));
+ expect(span.end.column, equals(2));
+ expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+ expect(span.text, equals(''));
+ });
+
+ test('.error() uses an absolute span', () {
+ scanner.expect('foo');
+ expect(
+ () => scanner.error('oh no!'), throwsStringScannerException('foo'));
+ });
+
+ test('.isDone returns true at the end of the span', () {
+ scanner.expect('foo\nbar\nbaz');
+ expect(scanner.isDone, isTrue);
+ });
+ });
+}
+
+void testForImplementation(
+ String name, SpanScanner Function([String string]) create) {
+ group('for a $name scanner', () {
+ late SpanScanner scanner;
+ setUp(() => scanner = create());
+
+ test('tracks the span for the last match', () {
+ scanner.scan('fo');
+ scanner.scan('o\nba');
+
+ final span = scanner.lastSpan!;
+ expect(span.start.offset, equals(2));
+ expect(span.start.line, equals(0));
+ expect(span.start.column, equals(2));
+ expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+ expect(span.end.offset, equals(6));
+ expect(span.end.line, equals(1));
+ expect(span.end.column, equals(2));
+ expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+ expect(span.text, equals('o\nba'));
+ });
+
+ test('.spanFrom() returns a span from a previous state', () {
+ scanner.scan('fo');
+ final state = scanner.state;
+ scanner.scan('o\nba');
+ scanner.scan('r\nba');
+
+ final span = scanner.spanFrom(state);
+ expect(span.text, equals('o\nbar\nba'));
+ });
+
+ test('.spanFromPosition() returns a span from a previous state', () {
+ scanner.scan('fo');
+ final start = scanner.position;
+ scanner.scan('o\nba');
+ scanner.scan('r\nba');
+
+ final span = scanner.spanFromPosition(start + 2, start + 5);
+ expect(span.text, equals('bar'));
+ });
+
+ test('.emptySpan returns an empty span at the current location', () {
+ scanner.scan('foo\nba');
+
+ final span = scanner.emptySpan;
+ expect(span.start.offset, equals(6));
+ expect(span.start.line, equals(1));
+ expect(span.start.column, equals(2));
+ expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+ expect(span.end.offset, equals(6));
+ expect(span.end.line, equals(1));
+ expect(span.end.column, equals(2));
+ expect(span.start.sourceUrl, equals(Uri.parse('source')));
+
+ expect(span.text, equals(''));
+ });
+
+ group('before a surrogate pair', () {
+ final codePoint = '\uD83D\uDC6D'.runes.first;
+ const highSurrogate = 0xD83D;
+
+ late SpanScanner scanner;
+ setUp(() {
+ scanner = create('foo: \uD83D\uDC6D bar');
+ expect(scanner.scan('foo: '), isTrue);
+ });
+
+ test('readChar returns the high surrogate and moves into the pair', () {
+ expect(scanner.readChar(), equals(highSurrogate));
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(6));
+ expect(scanner.position, equals(6));
+ });
+
+ test('readCodePoint returns the code unit and moves past the pair', () {
+ expect(scanner.readCodePoint(), equals(codePoint));
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(7));
+ expect(scanner.position, equals(7));
+ });
+
+ test('scanChar with the high surrogate moves into the pair', () {
+ expect(scanner.scanChar(highSurrogate), isTrue);
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(6));
+ expect(scanner.position, equals(6));
+ });
+
+ test('scanChar with the code point moves past the pair', () {
+ expect(scanner.scanChar(codePoint), isTrue);
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(7));
+ expect(scanner.position, equals(7));
+ });
+
+ test('expectChar with the high surrogate moves into the pair', () {
+ scanner.expectChar(highSurrogate);
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(6));
+ expect(scanner.position, equals(6));
+ });
+
+ test('expectChar with the code point moves past the pair', () {
+ scanner.expectChar(codePoint);
+ expect(scanner.line, equals(0));
+ expect(scanner.column, equals(7));
+ expect(scanner.position, equals(7));
+ });
+
+ test('spanFrom covers the surrogate pair', () {
+ final state = scanner.state;
+ scanner.scan('\uD83D\uDC6D b');
+ expect(scanner.spanFrom(state).text, equals('\uD83D\uDC6D b'));
+ });
+ });
+ });
+}
diff --git a/pkgs/string_scanner/test/string_scanner_test.dart b/pkgs/string_scanner/test/string_scanner_test.dart
new file mode 100644
index 0000000..36a737e
--- /dev/null
+++ b/pkgs/string_scanner/test/string_scanner_test.dart
@@ -0,0 +1,564 @@
+// 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 'package:string_scanner/src/charcode.dart';
+import 'package:string_scanner/string_scanner.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('with an empty string', () {
+ late StringScanner scanner;
+ setUp(() {
+ scanner = StringScanner('');
+ });
+
+ test('is done', () {
+ expect(scanner.isDone, isTrue);
+ expect(scanner.expectDone, isNot(throwsFormatException));
+ });
+
+ test('rest is empty', () {
+ expect(scanner.rest, isEmpty);
+ });
+
+ test('lastMatch is null', () {
+ expect(scanner.lastMatch, isNull);
+ });
+
+ test('position is zero', () {
+ expect(scanner.position, equals(0));
+ });
+
+ test("readChar fails and doesn't change the state", () {
+ expect(scanner.readChar, throwsFormatException);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test("readCodePoint fails and doesn't change the state", () {
+ expect(scanner.readCodePoint, throwsFormatException);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test("peekChar returns null and doesn't change the state", () {
+ expect(scanner.peekChar(), isNull);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test("peekCodePoint returns null and doesn't change the state", () {
+ expect(scanner.peekCodePoint(), isNull);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test("scanChar returns false and doesn't change the state", () {
+ expect(scanner.scanChar($f), isFalse);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test("expectChar fails and doesn't change the state", () {
+ expect(() => scanner.expectChar($f), throwsFormatException);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test("scan returns false and doesn't change the state", () {
+ expect(scanner.scan(RegExp('.')), isFalse);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test("expect throws a FormatException and doesn't change the state", () {
+ expect(() => scanner.expect(RegExp('.')), throwsFormatException);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test("matches returns false and doesn't change the state", () {
+ expect(scanner.matches(RegExp('.')), isFalse);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test('substring returns the empty string', () {
+ expect(scanner.substring(0), isEmpty);
+ });
+
+ test('setting position to 1 throws an ArgumentError', () {
+ expect(() {
+ scanner.position = 1;
+ }, throwsArgumentError);
+ });
+
+ test('setting position to -1 throws an ArgumentError', () {
+ expect(() {
+ scanner.position = -1;
+ }, throwsArgumentError);
+ });
+ });
+
+ group('at the beginning of a string', () {
+ late StringScanner scanner;
+ setUp(() {
+ scanner = StringScanner('foo bar');
+ });
+
+ test('is not done', () {
+ expect(scanner.isDone, isFalse);
+ expect(scanner.expectDone, throwsFormatException);
+ });
+
+ test('rest is the whole string', () {
+ expect(scanner.rest, equals('foo bar'));
+ });
+
+ test('lastMatch is null', () {
+ expect(scanner.lastMatch, isNull);
+ });
+
+ test('position is zero', () {
+ expect(scanner.position, equals(0));
+ });
+
+ test('readChar returns the first character and moves forward', () {
+ expect(scanner.readChar(), equals(0x66));
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(1));
+ });
+
+ test('readCodePoint returns the first character and moves forward', () {
+ expect(scanner.readCodePoint(), equals(0x66));
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(1));
+ });
+
+ test('peekChar returns the first character', () {
+ expect(scanner.peekChar(), equals(0x66));
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test('peekChar with an argument returns the nth character', () {
+ expect(scanner.peekChar(4), equals(0x62));
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test('peekCodePoint returns the first character', () {
+ expect(scanner.peekCodePoint(), equals(0x66));
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test('a matching scanChar returns true moves forward', () {
+ expect(scanner.scanChar($f), isTrue);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(1));
+ });
+
+ test('a non-matching scanChar returns false and does nothing', () {
+ expect(scanner.scanChar($x), isFalse);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test('a matching expectChar moves forward', () {
+ scanner.expectChar($f);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(1));
+ });
+
+ test('a non-matching expectChar fails', () {
+ expect(() => scanner.expectChar($x), throwsFormatException);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ });
+
+ test('a matching scan returns true and changes the state', () {
+ expect(scanner.scan(RegExp('f(..)')), isTrue);
+ expect(scanner.lastMatch![1], equals('oo'));
+ expect(scanner.position, equals(3));
+ expect(scanner.rest, equals(' bar'));
+ });
+
+ test('a non-matching scan returns false and sets lastMatch to null', () {
+ expect(scanner.matches(RegExp('f(..)')), isTrue);
+ expect(scanner.lastMatch, isNotNull);
+
+ expect(scanner.scan(RegExp('b(..)')), isFalse);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ expect(scanner.rest, equals('foo bar'));
+ });
+
+ test('a matching expect changes the state', () {
+ scanner.expect(RegExp('f(..)'));
+ expect(scanner.lastMatch![1], equals('oo'));
+ expect(scanner.position, equals(3));
+ expect(scanner.rest, equals(' bar'));
+ });
+
+ test(
+ 'a non-matching expect throws a FormatException and sets lastMatch to '
+ 'null', () {
+ expect(scanner.matches(RegExp('f(..)')), isTrue);
+ expect(scanner.lastMatch, isNotNull);
+
+ expect(() => scanner.expect(RegExp('b(..)')), throwsFormatException);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ expect(scanner.rest, equals('foo bar'));
+ });
+
+ test('a matching matches returns true and only changes lastMatch', () {
+ expect(scanner.matches(RegExp('f(..)')), isTrue);
+ expect(scanner.lastMatch![1], equals('oo'));
+ expect(scanner.position, equals(0));
+ expect(scanner.rest, equals('foo bar'));
+ });
+
+ test("a non-matching matches returns false and doesn't change the state",
+ () {
+ expect(scanner.matches(RegExp('b(..)')), isFalse);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(0));
+ expect(scanner.rest, equals('foo bar'));
+ });
+
+ test('substring from the beginning returns the empty string', () {
+ expect(scanner.substring(0), isEmpty);
+ });
+
+ test('substring with a custom end returns the substring', () {
+ expect(scanner.substring(0, 3), equals('foo'));
+ });
+
+ test('substring with the string length returns the whole string', () {
+ expect(scanner.substring(0, 7), equals('foo bar'));
+ });
+
+ test('setting position to 1 moves the cursor forward', () {
+ scanner.position = 1;
+ expect(scanner.position, equals(1));
+ expect(scanner.rest, equals('oo bar'));
+
+ expect(scanner.scan(RegExp('oo.')), isTrue);
+ expect(scanner.lastMatch![0], equals('oo '));
+ expect(scanner.position, equals(4));
+ expect(scanner.rest, equals('bar'));
+ });
+
+ test('setting position beyond the string throws an ArgumentError', () {
+ expect(() {
+ scanner.position = 8;
+ }, throwsArgumentError);
+ });
+
+ test('setting position to -1 throws an ArgumentError', () {
+ expect(() {
+ scanner.position = -1;
+ }, throwsArgumentError);
+ });
+
+ test('scan accepts any Pattern', () {
+ expect(scanner.scan('foo'), isTrue);
+ expect(scanner.lastMatch![0], equals('foo'));
+ expect(scanner.position, equals(3));
+ expect(scanner.rest, equals(' bar'));
+ });
+
+ test('scans multiple times', () {
+ expect(scanner.scan(RegExp('f(..)')), isTrue);
+ expect(scanner.lastMatch![1], equals('oo'));
+ expect(scanner.position, equals(3));
+ expect(scanner.rest, equals(' bar'));
+
+ expect(scanner.scan(RegExp(' b(..)')), isTrue);
+ expect(scanner.lastMatch![1], equals('ar'));
+ expect(scanner.position, equals(7));
+ expect(scanner.rest, equals(''));
+ expect(scanner.isDone, isTrue);
+ expect(scanner.expectDone, isNot(throwsFormatException));
+ });
+ });
+
+ group('after a scan', () {
+ late StringScanner scanner;
+ setUp(() {
+ scanner = StringScanner('foo bar');
+ expect(scanner.scan('foo'), isTrue);
+ });
+
+ test('readChar returns the first character and unsets the last match', () {
+ expect(scanner.readChar(), equals($space));
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(4));
+ });
+
+ test('readCodePoint returns the first character and unsets the last match',
+ () {
+ expect(scanner.readCodePoint(), equals($space));
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(4));
+ });
+
+ test('a matching scanChar returns true and unsets the last match', () {
+ expect(scanner.scanChar($space), isTrue);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(4));
+ });
+
+ test('a matching expectChar returns true and unsets the last match', () {
+ scanner.expectChar($space);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(4));
+ });
+ });
+
+ group('at the end of a string', () {
+ late StringScanner scanner;
+ setUp(() {
+ scanner = StringScanner('foo bar');
+ expect(scanner.scan('foo bar'), isTrue);
+ });
+
+ test('is done', () {
+ expect(scanner.isDone, isTrue);
+ expect(scanner.expectDone, isNot(throwsFormatException));
+ });
+
+ test('rest is empty', () {
+ expect(scanner.rest, isEmpty);
+ });
+
+ test('position is zero', () {
+ expect(scanner.position, equals(7));
+ });
+
+ test("readChar fails and doesn't change the state", () {
+ expect(scanner.readChar, throwsFormatException);
+ expect(scanner.lastMatch, isNotNull);
+ expect(scanner.position, equals(7));
+ });
+
+ test("readCodePoint fails and doesn't change the state", () {
+ expect(scanner.readCodePoint, throwsFormatException);
+ expect(scanner.lastMatch, isNotNull);
+ expect(scanner.position, equals(7));
+ });
+
+ test("peekChar returns null and doesn't change the state", () {
+ expect(scanner.peekChar(), isNull);
+ expect(scanner.lastMatch, isNotNull);
+ expect(scanner.position, equals(7));
+ });
+
+ test("peekCodePoint returns null and doesn't change the state", () {
+ expect(scanner.peekCodePoint(), isNull);
+ expect(scanner.lastMatch, isNotNull);
+ expect(scanner.position, equals(7));
+ });
+
+ test("scanChar returns false and doesn't change the state", () {
+ expect(scanner.scanChar($f), isFalse);
+ expect(scanner.lastMatch, isNotNull);
+ expect(scanner.position, equals(7));
+ });
+
+ test("expectChar fails and doesn't change the state", () {
+ expect(() => scanner.expectChar($f), throwsFormatException);
+ expect(scanner.lastMatch, isNotNull);
+ expect(scanner.position, equals(7));
+ });
+
+ test('scan returns false and sets lastMatch to null', () {
+ expect(scanner.scan(RegExp('.')), isFalse);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(7));
+ });
+
+ test('expect throws a FormatException and sets lastMatch to null', () {
+ expect(() => scanner.expect(RegExp('.')), throwsFormatException);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(7));
+ });
+
+ test('matches returns false sets lastMatch to null', () {
+ expect(scanner.matches(RegExp('.')), isFalse);
+ expect(scanner.lastMatch, isNull);
+ expect(scanner.position, equals(7));
+ });
+
+ test('substring from the beginning returns the whole string', () {
+ expect(scanner.substring(0), equals('foo bar'));
+ });
+
+ test('substring with a custom start returns a substring from there', () {
+ expect(scanner.substring(4), equals('bar'));
+ });
+
+ test('substring with a custom start and end returns that substring', () {
+ expect(scanner.substring(3, 5), equals(' b'));
+ });
+
+ test('setting position to 1 moves the cursor backward', () {
+ scanner.position = 1;
+ expect(scanner.position, equals(1));
+ expect(scanner.rest, equals('oo bar'));
+
+ expect(scanner.scan(RegExp('oo.')), isTrue);
+ expect(scanner.lastMatch![0], equals('oo '));
+ expect(scanner.position, equals(4));
+ expect(scanner.rest, equals('bar'));
+ });
+
+ test('setting and resetting position clears lastMatch', () {
+ final oldPosition = scanner.position;
+ scanner.position = 1;
+ scanner.position = oldPosition;
+ expect(scanner.lastMatch, isNull);
+ });
+
+ test('setting position beyond the string throws an ArgumentError', () {
+ expect(() {
+ scanner.position = 8;
+ }, throwsArgumentError);
+ });
+
+ test('setting position to -1 throws an ArgumentError', () {
+ expect(() {
+ scanner.position = -1;
+ }, throwsArgumentError);
+ });
+ });
+
+ group('before a surrogate pair', () {
+ final codePoint = '\uD83D\uDC6D'.runes.first;
+ const highSurrogate = 0xD83D;
+
+ late StringScanner scanner;
+ setUp(() {
+ scanner = StringScanner('foo: \uD83D\uDC6D');
+ expect(scanner.scan('foo: '), isTrue);
+ });
+
+ test('readChar returns the high surrogate and moves into the pair', () {
+ expect(scanner.readChar(), equals(highSurrogate));
+ expect(scanner.position, equals(6));
+ });
+
+ test('readCodePoint returns the code unit and moves past the pair', () {
+ expect(scanner.readCodePoint(), equals(codePoint));
+ expect(scanner.position, equals(7));
+ });
+
+ test('peekChar returns the high surrogate', () {
+ expect(scanner.peekChar(), equals(highSurrogate));
+ expect(scanner.position, equals(5));
+ });
+
+ test('peekCodePoint returns the code unit', () {
+ expect(scanner.peekCodePoint(), equals(codePoint));
+ expect(scanner.position, equals(5));
+ });
+
+ test('scanChar with the high surrogate moves into the pair', () {
+ expect(scanner.scanChar(highSurrogate), isTrue);
+ expect(scanner.position, equals(6));
+ });
+
+ test('scanChar with the code point moves past the pair', () {
+ expect(scanner.scanChar(codePoint), isTrue);
+ expect(scanner.position, equals(7));
+ });
+
+ test('expectChar with the high surrogate moves into the pair', () {
+ scanner.expectChar(highSurrogate);
+ expect(scanner.position, equals(6));
+ });
+
+ test('expectChar with the code point moves past the pair', () {
+ scanner.expectChar(codePoint);
+ expect(scanner.position, equals(7));
+ });
+ });
+
+ group('before an invalid surrogate pair', () {
+ // This surrogate pair is invalid because U+E000 is just outside the range
+ // of low surrogates. If it were interpreted as a surrogate pair anyway, the
+ // value would be U+110000, which is outside of the Unicode gamut.
+ const codePoint = 0x110000;
+ const highSurrogate = 0xD800;
+
+ late StringScanner scanner;
+ setUp(() {
+ scanner = StringScanner('foo: \uD800\uE000');
+ expect(scanner.scan('foo: '), isTrue);
+ });
+
+ test('readChar returns the high surrogate and moves into the pair', () {
+ expect(scanner.readChar(), equals(highSurrogate));
+ expect(scanner.position, equals(6));
+ });
+
+ test('readCodePoint returns the high surrogate and moves past the pair',
+ () {
+ expect(scanner.readCodePoint(), equals(highSurrogate));
+ expect(scanner.position, equals(6));
+ });
+
+ test('peekChar returns the high surrogate', () {
+ expect(scanner.peekChar(), equals(highSurrogate));
+ expect(scanner.position, equals(5));
+ });
+
+ test('peekCodePoint returns the high surrogate', () {
+ expect(scanner.peekCodePoint(), equals(highSurrogate));
+ expect(scanner.position, equals(5));
+ });
+
+ test('scanChar with the high surrogate moves into the pair', () {
+ expect(scanner.scanChar(highSurrogate), isTrue);
+ expect(scanner.position, equals(6));
+ });
+
+ test('scanChar with the fake code point returns false', () {
+ expect(scanner.scanChar(codePoint), isFalse);
+ expect(scanner.position, equals(5));
+ });
+
+ test('expectChar with the high surrogate moves into the pair', () {
+ scanner.expectChar(highSurrogate);
+ expect(scanner.position, equals(6));
+ });
+
+ test('expectChar with the fake code point fails', () {
+ expect(() => scanner.expectChar(codePoint), throwsRangeError);
+ });
+ });
+
+ group('a scanner constructed with a custom position', () {
+ test('starts scanning from that position', () {
+ final scanner = StringScanner('foo bar', position: 1);
+ expect(scanner.position, equals(1));
+ expect(scanner.rest, equals('oo bar'));
+
+ expect(scanner.scan(RegExp('oo.')), isTrue);
+ expect(scanner.lastMatch![0], equals('oo '));
+ expect(scanner.position, equals(4));
+ expect(scanner.rest, equals('bar'));
+ });
+
+ test('throws an ArgumentError if the position is -1', () {
+ expect(() => StringScanner('foo bar', position: -1), throwsArgumentError);
+ });
+
+ test('throws an ArgumentError if the position is beyond the string', () {
+ expect(() => StringScanner('foo bar', position: 8), throwsArgumentError);
+ });
+ });
+}
diff --git a/pkgs/string_scanner/test/utils.dart b/pkgs/string_scanner/test/utils.dart
new file mode 100644
index 0000000..ca03c06
--- /dev/null
+++ b/pkgs/string_scanner/test/utils.dart
@@ -0,0 +1,12 @@
+// 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 'package:string_scanner/string_scanner.dart';
+import 'package:test/test.dart';
+
+/// Returns a matcher that asserts that a closure throws a
+/// [StringScannerException] with the given [text].
+Matcher throwsStringScannerException(String text) =>
+ throwsA(const TypeMatcher<StringScannerException>()
+ .having((e) => e.span!.text, 'span.text', text));
diff --git a/pkgs/term_glyph/.gitignore b/pkgs/term_glyph/.gitignore
new file mode 100644
index 0000000..01d42c0
--- /dev/null
+++ b/pkgs/term_glyph/.gitignore
@@ -0,0 +1,4 @@
+.dart_tool/
+.pub/
+.packages
+pubspec.lock
diff --git a/pkgs/term_glyph/AUTHORS b/pkgs/term_glyph/AUTHORS
new file mode 100644
index 0000000..e8063a8
--- /dev/null
+++ b/pkgs/term_glyph/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/term_glyph/CHANGELOG.md b/pkgs/term_glyph/CHANGELOG.md
new file mode 100644
index 0000000..b7359cf
--- /dev/null
+++ b/pkgs/term_glyph/CHANGELOG.md
@@ -0,0 +1,31 @@
+## 1.2.2
+
+* Require Dart 3.1
+* Move to `dart-lang/tools` monorepo.
+
+## 1.2.1
+
+* Migrate to `package:lints`.
+* Populate the pubspec `repository` field.
+
+## 1.2.0
+
+* Stable release for null safety.
+* Update SDK constraints to `>=2.12.0-0 <3.0.0` based on beta release
+ guidelines.
+
+## 1.1.0
+
+* Add a `GlyphSet` class that can be used to easily choose which set of glyphs
+ to use for a particular chunk of code.
+
+* Add `asciiGlyphs`, `unicodeGlyphs`, and `glyphs` getters that provide access
+ to `GlyphSet`s.
+
+## 1.0.1
+
+* Set max SDK version to `<3.0.0`.
+
+## 1.0.0
+
+* Initial version.
diff --git a/pkgs/term_glyph/LICENSE b/pkgs/term_glyph/LICENSE
new file mode 100644
index 0000000..03af64a
--- /dev/null
+++ b/pkgs/term_glyph/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2017, 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/term_glyph/README.md b/pkgs/term_glyph/README.md
new file mode 100644
index 0000000..75039aa
--- /dev/null
+++ b/pkgs/term_glyph/README.md
@@ -0,0 +1,47 @@
+[](https://github.com/dart-lang/tools/actions/workflows/term_glyph.yaml)
+[](https://pub.dev/packages/term_glyph)
+[](https://pub.dev/packages/term_glyph/publisher)
+
+This library contains getters for useful Unicode glyphs as well as plain ASCII
+alternatives. It's intended to be used in command-line applications that may run
+in places where Unicode isn't well-supported and libraries that may be used by
+those applications.
+
+We recommend that you import this library with the prefix "glyph". For example:
+
+```dart
+import 'package:term_glyph/term_glyph.dart' as glyph;
+
+/// Formats [items] into a bulleted list, with one item per line.
+String bulletedList(List<String> items) =>
+ items.map((item) => "${glyph.bullet} $item").join("\n");
+```
+
+## ASCII Mode
+
+Some shells are unable to display Unicode characters, so this package is able to
+transparently switch its glyphs to ASCII alternatives by setting [the `ascii`
+attribute][ascii]. When this attribute is `true`, all glyphs use ASCII
+characters instead. It currently defaults to `false`, although in the future it
+may default to `true` for applications running on the Dart VM on Windows. For
+example:
+
+[ascii]: https://pub.dev/documentation/term_glyph/latest/term_glyph/ascii.html
+
+```dart
+import 'dart:io';
+
+import 'package:term_glyph/term_glyph.dart' as glyph;
+
+void main() {
+ glyph.ascii = Platform.isWindows;
+
+ // Prints "Unicode => ASCII" on Windows, "Unicode ━▶ ASCII" everywhere else.
+ print("Unicode ${glyph.rightArrow} ASCII");
+}
+```
+
+All ASCII glyphs are guaranteed to be the same number of characters as the
+corresponding Unicode glyphs, so that they line up properly when printed on a
+terminal. The specific ASCII text for a given Unicode glyph may change over
+time; this is not considered a breaking change.
diff --git a/pkgs/term_glyph/analysis_options.yaml b/pkgs/term_glyph/analysis_options.yaml
new file mode 100644
index 0000000..6d74ee9
--- /dev/null
+++ b/pkgs/term_glyph/analysis_options.yaml
@@ -0,0 +1,32 @@
+# https://dart.dev/guides/language/analysis-options
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+analyzer:
+ language:
+ strict-casts: true
+ strict-inference: true
+ strict-raw-types: true
+
+linter:
+ rules:
+ - avoid_bool_literals_in_conditional_expressions
+ - avoid_classes_with_only_static_members
+ - avoid_private_typedef_functions
+ - avoid_redundant_argument_values
+ - avoid_returning_this
+ - avoid_unused_constructor_parameters
+ - avoid_void_async
+ - cancel_subscriptions
+ - join_return_with_assignment
+ - literal_only_boolean_expressions
+ - missing_whitespace_between_adjacent_strings
+ - no_adjacent_strings_in_list
+ - no_runtimeType_toString
+ - prefer_const_declarations
+ - prefer_expression_function_bodies
+ - prefer_final_locals
+ - unnecessary_await_in_return
+ - unnecessary_breaks
+ - use_if_null_to_convert_nulls_to_bools
+ - use_raw_strings
+ - use_string_buffers
diff --git a/pkgs/term_glyph/data.csv b/pkgs/term_glyph/data.csv
new file mode 100644
index 0000000..92a72f7
--- /dev/null
+++ b/pkgs/term_glyph/data.csv
@@ -0,0 +1,85 @@
+# Miscellaneous
+bullet,•,*,A bullet point.
+
+# Arrows
+leftArrow,←,<,"A left-pointing arrow.
+
+Note that the Unicode arrow glyphs may overlap with adjacent characters in some
+terminal fonts, and should generally be surrounding by spaces."
+rightArrow,→,>,"A right-pointing arrow.
+
+Note that the Unicode arrow glyphs may overlap with adjacent characters in some
+terminal fonts, and should generally be surrounding by spaces."
+upArrow,↑,^,An upwards-pointing arrow.
+downArrow,↓,v,A downwards-pointing arrow.
+longLeftArrow,◀━,<=,A two-character left-pointing arrow.
+longRightArrow,━▶,=>,A two-character right-pointing arrow.
+
+# Box drawing characters
+
+## Normal
+horizontalLine,─,-,A horizontal line that can be used to draw a box.
+verticalLine,│,|,A vertical line that can be used to draw a box.
+topLeftCorner,┌,",",The upper left-hand corner of a box.
+topRightCorner,┐,",",The upper right-hand corner of a box.
+bottomLeftCorner,└,',The lower left-hand corner of a box.
+bottomRightCorner,┘,',The lower right-hand corner of a box.
+cross,┼,+,An intersection of vertical and horizontal box lines.
+teeUp,┴,+,A horizontal box line with a vertical line going up from the middle.
+teeDown,┬,+,A horizontal box line with a vertical line going down from the middle.
+teeLeft,┤,+,A vertical box line with a horizontal line going left from the middle.
+teeRight,├,+,A vertical box line with a horizontal line going right from the middle.
+upEnd,╵,',The top half of a vertical box line.
+downEnd,╷,",",The bottom half of a vertical box line.
+leftEnd,╴,-,The left half of a horizontal box line.
+rightEnd,╶,-,The right half of a horizontal box line.
+
+## Bold
+horizontalLineBold,━,=,A bold horizontal line that can be used to draw a box.
+verticalLineBold,┃,|,A bold vertical line that can be used to draw a box.
+topLeftCornerBold,┏,",",The bold upper left-hand corner of a box.
+topRightCornerBold,┓,",",The bold upper right-hand corner of a box.
+bottomLeftCornerBold,┗,',The bold lower left-hand corner of a box.
+bottomRightCornerBold,┛,',The bold lower right-hand corner of a box.
+crossBold,╋,+,An intersection of bold vertical and horizontal box lines.
+teeUpBold,┻,+,A bold horizontal box line with a vertical line going up from the middle.
+teeDownBold,┳,+,A bold horizontal box line with a vertical line going down from the middle.
+teeLeftBold,┫,+,A bold vertical box line with a horizontal line going left from the middle.
+teeRightBold,┣,+,A bold vertical box line with a horizontal line going right from the middle.
+upEndBold,╹,',The top half of a bold vertical box line.
+downEndBold,╻,",",The bottom half of a bold vertical box line.
+leftEndBold,╸,-,The left half of a bold horizontal box line.
+rightEndBold,╺,-,The right half of a bold horizontal box line.
+
+## Double
+horizontalLineDouble,═,=,A double horizontal line that can be used to draw a box.
+verticalLineDouble,║,|,A double vertical line that can be used to draw a box.
+topLeftCornerDouble,╔,",",The double upper left-hand corner of a box.
+topRightCornerDouble,╗,",",The double upper right-hand corner of a box.
+bottomLeftCornerDouble,╚,"""",The double lower left-hand corner of a box.
+bottomRightCornerDouble,╝,"""",The double lower right-hand corner of a box.
+crossDouble,╬,+,An intersection of double vertical and horizontal box lines.
+teeUpDouble,╩,+,A double horizontal box line with a vertical line going up from the middle.
+teeDownDouble,╦,+,A double horizontal box line with a vertical line going down from the middle.
+teeLeftDouble,╣,+,A double vertical box line with a horizontal line going left from the middle.
+teeRightDouble,╠,+,A double vertical box line with a horizontal line going right from the middle.
+
+## Dashed
+
+### Double
+horizontalLineDoubleDash,╌,-,A dashed horizontal line that can be used to draw a box.
+horizontalLineDoubleDashBold,╍,-,A bold dashed horizontal line that can be used to draw a box.
+verticalLineDoubleDash,╎,|,A dashed vertical line that can be used to draw a box.
+verticalLineDoubleDashBold,╏,|,A bold dashed vertical line that can be used to draw a box.
+
+### Triple
+horizontalLineTripleDash,┄,-,A dashed horizontal line that can be used to draw a box.
+horizontalLineTripleDashBold,┅,-,A bold dashed horizontal line that can be used to draw a box.
+verticalLineTripleDash,┆,|,A dashed vertical line that can be used to draw a box.
+verticalLineTripleDashBold,┇,|,A bold dashed vertical line that can be used to draw a box.
+
+### Quadruple
+horizontalLineQuadrupleDash,┈,-,A dashed horizontal line that can be used to draw a box.
+horizontalLineQuadrupleDashBold,┉,-,A bold dashed horizontal line that can be used to draw a box.
+verticalLineQuadrupleDash,┊,|,A dashed vertical line that can be used to draw a box.
+verticalLineQuadrupleDashBold,┋,|,A bold dashed vertical line that can be used to draw a box.
diff --git a/pkgs/term_glyph/lib/src/generated/ascii_glyph_set.dart b/pkgs/term_glyph/lib/src/generated/ascii_glyph_set.dart
new file mode 100644
index 0000000..7c97d7f
--- /dev/null
+++ b/pkgs/term_glyph/lib/src/generated/ascii_glyph_set.dart
@@ -0,0 +1,137 @@
+// 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.
+
+// Don't modify this file by hand! It's generated by tool/generate.dart.
+
+import 'glyph_set.dart';
+
+/// A [GlyphSet] that includes only ASCII glyphs.
+class AsciiGlyphSet implements GlyphSet {
+ const AsciiGlyphSet();
+
+ /// Returns [glyph] if `this` supports Unicode glyphs and [alternative]
+ /// otherwise.
+ @override
+ String glyphOrAscii(String glyph, String alternative) => alternative;
+ @override
+ String get bullet => '*';
+ @override
+ String get leftArrow => '<';
+ @override
+ String get rightArrow => '>';
+ @override
+ String get upArrow => '^';
+ @override
+ String get downArrow => 'v';
+ @override
+ String get longLeftArrow => '<=';
+ @override
+ String get longRightArrow => '=>';
+ @override
+ String get horizontalLine => '-';
+ @override
+ String get verticalLine => '|';
+ @override
+ String get topLeftCorner => ',';
+ @override
+ String get topRightCorner => ',';
+ @override
+ String get bottomLeftCorner => "'";
+ @override
+ String get bottomRightCorner => "'";
+ @override
+ String get cross => '+';
+ @override
+ String get teeUp => '+';
+ @override
+ String get teeDown => '+';
+ @override
+ String get teeLeft => '+';
+ @override
+ String get teeRight => '+';
+ @override
+ String get upEnd => "'";
+ @override
+ String get downEnd => ',';
+ @override
+ String get leftEnd => '-';
+ @override
+ String get rightEnd => '-';
+ @override
+ String get horizontalLineBold => '=';
+ @override
+ String get verticalLineBold => '|';
+ @override
+ String get topLeftCornerBold => ',';
+ @override
+ String get topRightCornerBold => ',';
+ @override
+ String get bottomLeftCornerBold => "'";
+ @override
+ String get bottomRightCornerBold => "'";
+ @override
+ String get crossBold => '+';
+ @override
+ String get teeUpBold => '+';
+ @override
+ String get teeDownBold => '+';
+ @override
+ String get teeLeftBold => '+';
+ @override
+ String get teeRightBold => '+';
+ @override
+ String get upEndBold => "'";
+ @override
+ String get downEndBold => ',';
+ @override
+ String get leftEndBold => '-';
+ @override
+ String get rightEndBold => '-';
+ @override
+ String get horizontalLineDouble => '=';
+ @override
+ String get verticalLineDouble => '|';
+ @override
+ String get topLeftCornerDouble => ',';
+ @override
+ String get topRightCornerDouble => ',';
+ @override
+ String get bottomLeftCornerDouble => '"';
+ @override
+ String get bottomRightCornerDouble => '"';
+ @override
+ String get crossDouble => '+';
+ @override
+ String get teeUpDouble => '+';
+ @override
+ String get teeDownDouble => '+';
+ @override
+ String get teeLeftDouble => '+';
+ @override
+ String get teeRightDouble => '+';
+ @override
+ String get horizontalLineDoubleDash => '-';
+ @override
+ String get horizontalLineDoubleDashBold => '-';
+ @override
+ String get verticalLineDoubleDash => '|';
+ @override
+ String get verticalLineDoubleDashBold => '|';
+ @override
+ String get horizontalLineTripleDash => '-';
+ @override
+ String get horizontalLineTripleDashBold => '-';
+ @override
+ String get verticalLineTripleDash => '|';
+ @override
+ String get verticalLineTripleDashBold => '|';
+ @override
+ String get horizontalLineQuadrupleDash => '-';
+ @override
+ String get horizontalLineQuadrupleDashBold => '-';
+ @override
+ String get verticalLineQuadrupleDash => '|';
+ @override
+ String get verticalLineQuadrupleDashBold => '|';
+}
diff --git a/pkgs/term_glyph/lib/src/generated/glyph_set.dart b/pkgs/term_glyph/lib/src/generated/glyph_set.dart
new file mode 100644
index 0000000..be1a668
--- /dev/null
+++ b/pkgs/term_glyph/lib/src/generated/glyph_set.dart
@@ -0,0 +1,227 @@
+// 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.
+
+// Don't modify this file by hand! It's generated by tool/generate.dart.
+
+/// A class that provides access to every configurable glyph.
+///
+/// This is provided as a class so that individual chunks of code can choose
+/// between `ascii` and `unicode` glyphs. For example:
+///
+/// ```dart
+/// import 'package:term_glyph/term_glyph.dart' as glyph;
+///
+/// /// Adds a vertical line to the left of [text].
+/// ///
+/// /// If [unicode] is `true`, this uses Unicode for the line. If it's
+/// /// `false`, this uses plain ASCII characters. If it's `null`, it
+/// /// defaults to [glyph.ascii].
+/// void addVerticalLine(String text, {bool unicode}) {
+/// var glyphs =
+/// (unicode ?? !glyph.ascii) ? glyph.unicodeGlyphs : glyph.asciiGlyphs;
+///
+/// return text
+/// .split('\n')
+/// .map((line) => '${glyphs.verticalLine} $line')
+/// .join('\n');
+/// }
+/// ```
+abstract class GlyphSet {
+ /// Returns [glyph] if `this` supports Unicode glyphs and [alternative]
+ /// otherwise.
+ String glyphOrAscii(String glyph, String alternative);
+
+ /// A bullet point.
+ String get bullet;
+
+ /// A left-pointing arrow.
+ ///
+ /// Note that the Unicode arrow glyphs may overlap with adjacent characters in
+ /// some terminal fonts, and should generally be surrounding by spaces.
+ String get leftArrow;
+
+ /// A right-pointing arrow.
+ ///
+ /// Note that the Unicode arrow glyphs may overlap with adjacent characters in
+ /// some terminal fonts, and should generally be surrounding by spaces.
+ String get rightArrow;
+
+ /// An upwards-pointing arrow.
+ String get upArrow;
+
+ /// A downwards-pointing arrow.
+ String get downArrow;
+
+ /// A two-character left-pointing arrow.
+ String get longLeftArrow;
+
+ /// A two-character right-pointing arrow.
+ String get longRightArrow;
+
+ /// A horizontal line that can be used to draw a box.
+ String get horizontalLine;
+
+ /// A vertical line that can be used to draw a box.
+ String get verticalLine;
+
+ /// The upper left-hand corner of a box.
+ String get topLeftCorner;
+
+ /// The upper right-hand corner of a box.
+ String get topRightCorner;
+
+ /// The lower left-hand corner of a box.
+ String get bottomLeftCorner;
+
+ /// The lower right-hand corner of a box.
+ String get bottomRightCorner;
+
+ /// An intersection of vertical and horizontal box lines.
+ String get cross;
+
+ /// A horizontal box line with a vertical line going up from the middle.
+ String get teeUp;
+
+ /// A horizontal box line with a vertical line going down from the middle.
+ String get teeDown;
+
+ /// A vertical box line with a horizontal line going left from the middle.
+ String get teeLeft;
+
+ /// A vertical box line with a horizontal line going right from the middle.
+ String get teeRight;
+
+ /// The top half of a vertical box line.
+ String get upEnd;
+
+ /// The bottom half of a vertical box line.
+ String get downEnd;
+
+ /// The left half of a horizontal box line.
+ String get leftEnd;
+
+ /// The right half of a horizontal box line.
+ String get rightEnd;
+
+ /// A bold horizontal line that can be used to draw a box.
+ String get horizontalLineBold;
+
+ /// A bold vertical line that can be used to draw a box.
+ String get verticalLineBold;
+
+ /// The bold upper left-hand corner of a box.
+ String get topLeftCornerBold;
+
+ /// The bold upper right-hand corner of a box.
+ String get topRightCornerBold;
+
+ /// The bold lower left-hand corner of a box.
+ String get bottomLeftCornerBold;
+
+ /// The bold lower right-hand corner of a box.
+ String get bottomRightCornerBold;
+
+ /// An intersection of bold vertical and horizontal box lines.
+ String get crossBold;
+
+ /// A bold horizontal box line with a vertical line going up from the middle.
+ String get teeUpBold;
+
+ /// A bold horizontal box line with a vertical line going down from the
+ /// middle.
+ String get teeDownBold;
+
+ /// A bold vertical box line with a horizontal line going left from the
+ /// middle.
+ String get teeLeftBold;
+
+ /// A bold vertical box line with a horizontal line going right from the
+ /// middle.
+ String get teeRightBold;
+
+ /// The top half of a bold vertical box line.
+ String get upEndBold;
+
+ /// The bottom half of a bold vertical box line.
+ String get downEndBold;
+
+ /// The left half of a bold horizontal box line.
+ String get leftEndBold;
+
+ /// The right half of a bold horizontal box line.
+ String get rightEndBold;
+
+ /// A double horizontal line that can be used to draw a box.
+ String get horizontalLineDouble;
+
+ /// A double vertical line that can be used to draw a box.
+ String get verticalLineDouble;
+
+ /// The double upper left-hand corner of a box.
+ String get topLeftCornerDouble;
+
+ /// The double upper right-hand corner of a box.
+ String get topRightCornerDouble;
+
+ /// The double lower left-hand corner of a box.
+ String get bottomLeftCornerDouble;
+
+ /// The double lower right-hand corner of a box.
+ String get bottomRightCornerDouble;
+
+ /// An intersection of double vertical and horizontal box lines.
+ String get crossDouble;
+
+ /// A double horizontal box line with a vertical line going up from the
+ /// middle.
+ String get teeUpDouble;
+
+ /// A double horizontal box line with a vertical line going down from the
+ /// middle.
+ String get teeDownDouble;
+
+ /// A double vertical box line with a horizontal line going left from the
+ /// middle.
+ String get teeLeftDouble;
+
+ /// A double vertical box line with a horizontal line going right from the
+ /// middle.
+ String get teeRightDouble;
+
+ /// A dashed horizontal line that can be used to draw a box.
+ String get horizontalLineDoubleDash;
+
+ /// A bold dashed horizontal line that can be used to draw a box.
+ String get horizontalLineDoubleDashBold;
+
+ /// A dashed vertical line that can be used to draw a box.
+ String get verticalLineDoubleDash;
+
+ /// A bold dashed vertical line that can be used to draw a box.
+ String get verticalLineDoubleDashBold;
+
+ /// A dashed horizontal line that can be used to draw a box.
+ String get horizontalLineTripleDash;
+
+ /// A bold dashed horizontal line that can be used to draw a box.
+ String get horizontalLineTripleDashBold;
+
+ /// A dashed vertical line that can be used to draw a box.
+ String get verticalLineTripleDash;
+
+ /// A bold dashed vertical line that can be used to draw a box.
+ String get verticalLineTripleDashBold;
+
+ /// A dashed horizontal line that can be used to draw a box.
+ String get horizontalLineQuadrupleDash;
+
+ /// A bold dashed horizontal line that can be used to draw a box.
+ String get horizontalLineQuadrupleDashBold;
+
+ /// A dashed vertical line that can be used to draw a box.
+ String get verticalLineQuadrupleDash;
+
+ /// A bold dashed vertical line that can be used to draw a box.
+ String get verticalLineQuadrupleDashBold;
+}
diff --git a/pkgs/term_glyph/lib/src/generated/top_level.dart b/pkgs/term_glyph/lib/src/generated/top_level.dart
new file mode 100644
index 0000000..925903e
--- /dev/null
+++ b/pkgs/term_glyph/lib/src/generated/top_level.dart
@@ -0,0 +1,383 @@
+// 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.
+
+// Don't modify this file by hand! It's generated by tool/generate.dart.
+
+import '../../term_glyph.dart' as glyph;
+
+/// A bullet point.
+///
+/// If [glyph.ascii] is `false`, this is "•". If it's `true`, this is
+/// "*" instead.
+String get bullet => glyph.glyphs.bullet;
+
+/// A left-pointing arrow.
+///
+/// Note that the Unicode arrow glyphs may overlap with adjacent characters in
+/// some terminal fonts, and should generally be surrounding by spaces.
+///
+/// If [glyph.ascii] is `false`, this is "←". If it's `true`, this is
+/// "<" instead.
+String get leftArrow => glyph.glyphs.leftArrow;
+
+/// A right-pointing arrow.
+///
+/// Note that the Unicode arrow glyphs may overlap with adjacent characters in
+/// some terminal fonts, and should generally be surrounding by spaces.
+///
+/// If [glyph.ascii] is `false`, this is "→". If it's `true`, this is
+/// ">" instead.
+String get rightArrow => glyph.glyphs.rightArrow;
+
+/// An upwards-pointing arrow.
+///
+/// If [glyph.ascii] is `false`, this is "↑". If it's `true`, this is
+/// "^" instead.
+String get upArrow => glyph.glyphs.upArrow;
+
+/// A downwards-pointing arrow.
+///
+/// If [glyph.ascii] is `false`, this is "↓". If it's `true`, this is
+/// "v" instead.
+String get downArrow => glyph.glyphs.downArrow;
+
+/// A two-character left-pointing arrow.
+///
+/// If [glyph.ascii] is `false`, this is "◀━". If it's `true`, this is
+/// "<=" instead.
+String get longLeftArrow => glyph.glyphs.longLeftArrow;
+
+/// A two-character right-pointing arrow.
+///
+/// If [glyph.ascii] is `false`, this is "━▶". If it's `true`, this is
+/// "=>" instead.
+String get longRightArrow => glyph.glyphs.longRightArrow;
+
+/// A horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "─". If it's `true`, this is
+/// "-" instead.
+String get horizontalLine => glyph.glyphs.horizontalLine;
+
+/// A vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "│". If it's `true`, this is
+/// "|" instead.
+String get verticalLine => glyph.glyphs.verticalLine;
+
+/// The upper left-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "┌". If it's `true`, this is
+/// "," instead.
+String get topLeftCorner => glyph.glyphs.topLeftCorner;
+
+/// The upper right-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "┐". If it's `true`, this is
+/// "," instead.
+String get topRightCorner => glyph.glyphs.topRightCorner;
+
+/// The lower left-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "└". If it's `true`, this is
+/// "'" instead.
+String get bottomLeftCorner => glyph.glyphs.bottomLeftCorner;
+
+/// The lower right-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "┘". If it's `true`, this is
+/// "'" instead.
+String get bottomRightCorner => glyph.glyphs.bottomRightCorner;
+
+/// An intersection of vertical and horizontal box lines.
+///
+/// If [glyph.ascii] is `false`, this is "┼". If it's `true`, this is
+/// "+" instead.
+String get cross => glyph.glyphs.cross;
+
+/// A horizontal box line with a vertical line going up from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "┴". If it's `true`, this is
+/// "+" instead.
+String get teeUp => glyph.glyphs.teeUp;
+
+/// A horizontal box line with a vertical line going down from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "┬". If it's `true`, this is
+/// "+" instead.
+String get teeDown => glyph.glyphs.teeDown;
+
+/// A vertical box line with a horizontal line going left from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "┤". If it's `true`, this is
+/// "+" instead.
+String get teeLeft => glyph.glyphs.teeLeft;
+
+/// A vertical box line with a horizontal line going right from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "├". If it's `true`, this is
+/// "+" instead.
+String get teeRight => glyph.glyphs.teeRight;
+
+/// The top half of a vertical box line.
+///
+/// If [glyph.ascii] is `false`, this is "╵". If it's `true`, this is
+/// "'" instead.
+String get upEnd => glyph.glyphs.upEnd;
+
+/// The bottom half of a vertical box line.
+///
+/// If [glyph.ascii] is `false`, this is "╷". If it's `true`, this is
+/// "," instead.
+String get downEnd => glyph.glyphs.downEnd;
+
+/// The left half of a horizontal box line.
+///
+/// If [glyph.ascii] is `false`, this is "╴". If it's `true`, this is
+/// "-" instead.
+String get leftEnd => glyph.glyphs.leftEnd;
+
+/// The right half of a horizontal box line.
+///
+/// If [glyph.ascii] is `false`, this is "╶". If it's `true`, this is
+/// "-" instead.
+String get rightEnd => glyph.glyphs.rightEnd;
+
+/// A bold horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "━". If it's `true`, this is
+/// "=" instead.
+String get horizontalLineBold => glyph.glyphs.horizontalLineBold;
+
+/// A bold vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┃". If it's `true`, this is
+/// "|" instead.
+String get verticalLineBold => glyph.glyphs.verticalLineBold;
+
+/// The bold upper left-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "┏". If it's `true`, this is
+/// "," instead.
+String get topLeftCornerBold => glyph.glyphs.topLeftCornerBold;
+
+/// The bold upper right-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "┓". If it's `true`, this is
+/// "," instead.
+String get topRightCornerBold => glyph.glyphs.topRightCornerBold;
+
+/// The bold lower left-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "┗". If it's `true`, this is
+/// "'" instead.
+String get bottomLeftCornerBold => glyph.glyphs.bottomLeftCornerBold;
+
+/// The bold lower right-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "┛". If it's `true`, this is
+/// "'" instead.
+String get bottomRightCornerBold => glyph.glyphs.bottomRightCornerBold;
+
+/// An intersection of bold vertical and horizontal box lines.
+///
+/// If [glyph.ascii] is `false`, this is "╋". If it's `true`, this is
+/// "+" instead.
+String get crossBold => glyph.glyphs.crossBold;
+
+/// A bold horizontal box line with a vertical line going up from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "┻". If it's `true`, this is
+/// "+" instead.
+String get teeUpBold => glyph.glyphs.teeUpBold;
+
+/// A bold horizontal box line with a vertical line going down from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "┳". If it's `true`, this is
+/// "+" instead.
+String get teeDownBold => glyph.glyphs.teeDownBold;
+
+/// A bold vertical box line with a horizontal line going left from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "┫". If it's `true`, this is
+/// "+" instead.
+String get teeLeftBold => glyph.glyphs.teeLeftBold;
+
+/// A bold vertical box line with a horizontal line going right from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "┣". If it's `true`, this is
+/// "+" instead.
+String get teeRightBold => glyph.glyphs.teeRightBold;
+
+/// The top half of a bold vertical box line.
+///
+/// If [glyph.ascii] is `false`, this is "╹". If it's `true`, this is
+/// "'" instead.
+String get upEndBold => glyph.glyphs.upEndBold;
+
+/// The bottom half of a bold vertical box line.
+///
+/// If [glyph.ascii] is `false`, this is "╻". If it's `true`, this is
+/// "," instead.
+String get downEndBold => glyph.glyphs.downEndBold;
+
+/// The left half of a bold horizontal box line.
+///
+/// If [glyph.ascii] is `false`, this is "╸". If it's `true`, this is
+/// "-" instead.
+String get leftEndBold => glyph.glyphs.leftEndBold;
+
+/// The right half of a bold horizontal box line.
+///
+/// If [glyph.ascii] is `false`, this is "╺". If it's `true`, this is
+/// "-" instead.
+String get rightEndBold => glyph.glyphs.rightEndBold;
+
+/// A double horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "═". If it's `true`, this is
+/// "=" instead.
+String get horizontalLineDouble => glyph.glyphs.horizontalLineDouble;
+
+/// A double vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "║". If it's `true`, this is
+/// "|" instead.
+String get verticalLineDouble => glyph.glyphs.verticalLineDouble;
+
+/// The double upper left-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "╔". If it's `true`, this is
+/// "," instead.
+String get topLeftCornerDouble => glyph.glyphs.topLeftCornerDouble;
+
+/// The double upper right-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "╗". If it's `true`, this is
+/// "," instead.
+String get topRightCornerDouble => glyph.glyphs.topRightCornerDouble;
+
+/// The double lower left-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "╚". If it's `true`, this is
+/// """ instead.
+String get bottomLeftCornerDouble => glyph.glyphs.bottomLeftCornerDouble;
+
+/// The double lower right-hand corner of a box.
+///
+/// If [glyph.ascii] is `false`, this is "╝". If it's `true`, this is
+/// """ instead.
+String get bottomRightCornerDouble => glyph.glyphs.bottomRightCornerDouble;
+
+/// An intersection of double vertical and horizontal box lines.
+///
+/// If [glyph.ascii] is `false`, this is "╬". If it's `true`, this is
+/// "+" instead.
+String get crossDouble => glyph.glyphs.crossDouble;
+
+/// A double horizontal box line with a vertical line going up from the middle.
+///
+/// If [glyph.ascii] is `false`, this is "╩". If it's `true`, this is
+/// "+" instead.
+String get teeUpDouble => glyph.glyphs.teeUpDouble;
+
+/// A double horizontal box line with a vertical line going down from the
+/// middle.
+///
+/// If [glyph.ascii] is `false`, this is "╦". If it's `true`, this is
+/// "+" instead.
+String get teeDownDouble => glyph.glyphs.teeDownDouble;
+
+/// A double vertical box line with a horizontal line going left from the
+/// middle.
+///
+/// If [glyph.ascii] is `false`, this is "╣". If it's `true`, this is
+/// "+" instead.
+String get teeLeftDouble => glyph.glyphs.teeLeftDouble;
+
+/// A double vertical box line with a horizontal line going right from the
+/// middle.
+///
+/// If [glyph.ascii] is `false`, this is "╠". If it's `true`, this is
+/// "+" instead.
+String get teeRightDouble => glyph.glyphs.teeRightDouble;
+
+/// A dashed horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "╌". If it's `true`, this is
+/// "-" instead.
+String get horizontalLineDoubleDash => glyph.glyphs.horizontalLineDoubleDash;
+
+/// A bold dashed horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "╍". If it's `true`, this is
+/// "-" instead.
+String get horizontalLineDoubleDashBold =>
+ glyph.glyphs.horizontalLineDoubleDashBold;
+
+/// A dashed vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "╎". If it's `true`, this is
+/// "|" instead.
+String get verticalLineDoubleDash => glyph.glyphs.verticalLineDoubleDash;
+
+/// A bold dashed vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "╏". If it's `true`, this is
+/// "|" instead.
+String get verticalLineDoubleDashBold =>
+ glyph.glyphs.verticalLineDoubleDashBold;
+
+/// A dashed horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┄". If it's `true`, this is
+/// "-" instead.
+String get horizontalLineTripleDash => glyph.glyphs.horizontalLineTripleDash;
+
+/// A bold dashed horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┅". If it's `true`, this is
+/// "-" instead.
+String get horizontalLineTripleDashBold =>
+ glyph.glyphs.horizontalLineTripleDashBold;
+
+/// A dashed vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┆". If it's `true`, this is
+/// "|" instead.
+String get verticalLineTripleDash => glyph.glyphs.verticalLineTripleDash;
+
+/// A bold dashed vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┇". If it's `true`, this is
+/// "|" instead.
+String get verticalLineTripleDashBold =>
+ glyph.glyphs.verticalLineTripleDashBold;
+
+/// A dashed horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┈". If it's `true`, this is
+/// "-" instead.
+String get horizontalLineQuadrupleDash =>
+ glyph.glyphs.horizontalLineQuadrupleDash;
+
+/// A bold dashed horizontal line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┉". If it's `true`, this is
+/// "-" instead.
+String get horizontalLineQuadrupleDashBold =>
+ glyph.glyphs.horizontalLineQuadrupleDashBold;
+
+/// A dashed vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┊". If it's `true`, this is
+/// "|" instead.
+String get verticalLineQuadrupleDash => glyph.glyphs.verticalLineQuadrupleDash;
+
+/// A bold dashed vertical line that can be used to draw a box.
+///
+/// If [glyph.ascii] is `false`, this is "┋". If it's `true`, this is
+/// "|" instead.
+String get verticalLineQuadrupleDashBold =>
+ glyph.glyphs.verticalLineQuadrupleDashBold;
diff --git a/pkgs/term_glyph/lib/src/generated/unicode_glyph_set.dart b/pkgs/term_glyph/lib/src/generated/unicode_glyph_set.dart
new file mode 100644
index 0000000..1ddd165
--- /dev/null
+++ b/pkgs/term_glyph/lib/src/generated/unicode_glyph_set.dart
@@ -0,0 +1,137 @@
+// 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.
+
+// Don't modify this file by hand! It's generated by tool/generate.dart.
+
+import 'glyph_set.dart';
+
+/// A [GlyphSet] that includes only Unicode glyphs.
+class UnicodeGlyphSet implements GlyphSet {
+ const UnicodeGlyphSet();
+
+ /// Returns [glyph] if `this` supports Unicode glyphs and [alternative]
+ /// otherwise.
+ @override
+ String glyphOrAscii(String glyph, String alternative) => glyph;
+ @override
+ String get bullet => '•';
+ @override
+ String get leftArrow => '←';
+ @override
+ String get rightArrow => '→';
+ @override
+ String get upArrow => '↑';
+ @override
+ String get downArrow => '↓';
+ @override
+ String get longLeftArrow => '◀━';
+ @override
+ String get longRightArrow => '━▶';
+ @override
+ String get horizontalLine => '─';
+ @override
+ String get verticalLine => '│';
+ @override
+ String get topLeftCorner => '┌';
+ @override
+ String get topRightCorner => '┐';
+ @override
+ String get bottomLeftCorner => '└';
+ @override
+ String get bottomRightCorner => '┘';
+ @override
+ String get cross => '┼';
+ @override
+ String get teeUp => '┴';
+ @override
+ String get teeDown => '┬';
+ @override
+ String get teeLeft => '┤';
+ @override
+ String get teeRight => '├';
+ @override
+ String get upEnd => '╵';
+ @override
+ String get downEnd => '╷';
+ @override
+ String get leftEnd => '╴';
+ @override
+ String get rightEnd => '╶';
+ @override
+ String get horizontalLineBold => '━';
+ @override
+ String get verticalLineBold => '┃';
+ @override
+ String get topLeftCornerBold => '┏';
+ @override
+ String get topRightCornerBold => '┓';
+ @override
+ String get bottomLeftCornerBold => '┗';
+ @override
+ String get bottomRightCornerBold => '┛';
+ @override
+ String get crossBold => '╋';
+ @override
+ String get teeUpBold => '┻';
+ @override
+ String get teeDownBold => '┳';
+ @override
+ String get teeLeftBold => '┫';
+ @override
+ String get teeRightBold => '┣';
+ @override
+ String get upEndBold => '╹';
+ @override
+ String get downEndBold => '╻';
+ @override
+ String get leftEndBold => '╸';
+ @override
+ String get rightEndBold => '╺';
+ @override
+ String get horizontalLineDouble => '═';
+ @override
+ String get verticalLineDouble => '║';
+ @override
+ String get topLeftCornerDouble => '╔';
+ @override
+ String get topRightCornerDouble => '╗';
+ @override
+ String get bottomLeftCornerDouble => '╚';
+ @override
+ String get bottomRightCornerDouble => '╝';
+ @override
+ String get crossDouble => '╬';
+ @override
+ String get teeUpDouble => '╩';
+ @override
+ String get teeDownDouble => '╦';
+ @override
+ String get teeLeftDouble => '╣';
+ @override
+ String get teeRightDouble => '╠';
+ @override
+ String get horizontalLineDoubleDash => '╌';
+ @override
+ String get horizontalLineDoubleDashBold => '╍';
+ @override
+ String get verticalLineDoubleDash => '╎';
+ @override
+ String get verticalLineDoubleDashBold => '╏';
+ @override
+ String get horizontalLineTripleDash => '┄';
+ @override
+ String get horizontalLineTripleDashBold => '┅';
+ @override
+ String get verticalLineTripleDash => '┆';
+ @override
+ String get verticalLineTripleDashBold => '┇';
+ @override
+ String get horizontalLineQuadrupleDash => '┈';
+ @override
+ String get horizontalLineQuadrupleDashBold => '┉';
+ @override
+ String get verticalLineQuadrupleDash => '┊';
+ @override
+ String get verticalLineQuadrupleDashBold => '┋';
+}
diff --git a/pkgs/term_glyph/lib/term_glyph.dart b/pkgs/term_glyph/lib/term_glyph.dart
new file mode 100644
index 0000000..9f2b422
--- /dev/null
+++ b/pkgs/term_glyph/lib/term_glyph.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 'src/generated/ascii_glyph_set.dart';
+import 'src/generated/glyph_set.dart';
+import 'src/generated/unicode_glyph_set.dart';
+
+export 'src/generated/glyph_set.dart';
+export 'src/generated/top_level.dart';
+
+/// A [GlyphSet] that always returns ASCII glyphs.
+const GlyphSet asciiGlyphs = AsciiGlyphSet();
+
+/// A [GlyphSet] that always returns Unicode glyphs.
+const GlyphSet unicodeGlyphs = UnicodeGlyphSet();
+
+/// Returns [asciiGlyphs] if [ascii] is `true` or [unicodeGlyphs] otherwise.
+///
+/// Returns [unicodeGlyphs] by default.
+GlyphSet get glyphs => _glyphs;
+GlyphSet _glyphs = unicodeGlyphs;
+
+/// Whether the glyph getters return plain ASCII, as opposed to Unicode
+/// characters or sequences.
+///
+/// Defaults to `false`.
+bool get ascii => glyphs == asciiGlyphs;
+
+set ascii(bool value) {
+ _glyphs = value ? asciiGlyphs : unicodeGlyphs;
+}
+
+/// Returns [glyph] if Unicode glyph are allowed, and [alternative] if they
+/// aren't.
+String glyphOrAscii(String glyph, String alternative) =>
+ glyphs.glyphOrAscii(glyph, alternative);
diff --git a/pkgs/term_glyph/pubspec.yaml b/pkgs/term_glyph/pubspec.yaml
new file mode 100644
index 0000000..c429307
--- /dev/null
+++ b/pkgs/term_glyph/pubspec.yaml
@@ -0,0 +1,13 @@
+name: term_glyph
+version: 1.2.2
+description: Useful Unicode glyphs and ASCII substitutes.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/term_glyph
+
+environment:
+ sdk: ^3.1.0
+
+dev_dependencies:
+ csv: ^6.0.0
+ dart_flutter_team_lints: ^3.0.0
+ dart_style: ^2.0.0
+ test: ^1.16.6
diff --git a/pkgs/term_glyph/test/symbol_test.dart b/pkgs/term_glyph/test/symbol_test.dart
new file mode 100644
index 0000000..b3b4d09
--- /dev/null
+++ b/pkgs/term_glyph/test/symbol_test.dart
@@ -0,0 +1,60 @@
+// 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';
+
+void main() {
+ group('with ascii = false', () {
+ setUpAll(() {
+ glyph.ascii = false;
+ });
+
+ test('glyph getters return Unicode versions', () {
+ expect(glyph.topLeftCorner, equals('┌'));
+ expect(glyph.teeUpBold, equals('┻'));
+ expect(glyph.longLeftArrow, equals('◀━'));
+ });
+
+ test('glyphOrAscii returns the first argument', () {
+ expect(glyph.glyphOrAscii('A', 'B'), equals('A'));
+ });
+
+ test('glyphs returns unicodeGlyphs', () {
+ expect(glyph.glyphs, equals(glyph.unicodeGlyphs));
+ });
+
+ test('asciiGlyphs still returns ASCII characters', () {
+ expect(glyph.asciiGlyphs.topLeftCorner, equals(','));
+ expect(glyph.asciiGlyphs.teeUpBold, equals('+'));
+ expect(glyph.asciiGlyphs.longLeftArrow, equals('<='));
+ });
+ });
+
+ group('with ascii = true', () {
+ setUpAll(() {
+ glyph.ascii = true;
+ });
+
+ test('glyphs return ASCII versions', () {
+ expect(glyph.topLeftCorner, equals(','));
+ expect(glyph.teeUpBold, equals('+'));
+ expect(glyph.longLeftArrow, equals('<='));
+ });
+
+ test('glyphOrAscii returns the second argument', () {
+ expect(glyph.glyphOrAscii('A', 'B'), equals('B'));
+ });
+
+ test('glyphs returns asciiGlyphs', () {
+ expect(glyph.glyphs, equals(glyph.asciiGlyphs));
+ });
+
+ test('unicodeGlyphs still returns Unicode characters', () {
+ expect(glyph.unicodeGlyphs.topLeftCorner, equals('┌'));
+ expect(glyph.unicodeGlyphs.teeUpBold, equals('┻'));
+ expect(glyph.unicodeGlyphs.longLeftArrow, equals('◀━'));
+ });
+ });
+}
diff --git a/pkgs/term_glyph/tool/generate.dart b/pkgs/term_glyph/tool/generate.dart
new file mode 100644
index 0000000..f5cdade
--- /dev/null
+++ b/pkgs/term_glyph/tool/generate.dart
@@ -0,0 +1,153 @@
+// 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:csv/csv.dart';
+
+void main() {
+ final csv = CsvCodec(eol: '\n');
+ final data = csv.decoder.convert(File('data.csv').readAsStringSync());
+
+ // Remove comments and empty lines.
+ data.removeWhere((row) => row.length < 3);
+
+ Directory('lib/src/generated').createSync(recursive: true);
+
+ _writeGlyphSetInterface(data);
+ _writeGlyphSet(data, ascii: false);
+ _writeGlyphSet(data, ascii: true);
+ _writeTopLevel(data);
+
+ final result = Process.runSync(
+ 'pub', ['run', 'dart_style:format', '-w', 'lib/src/generated']);
+ print(result.stderr);
+ exit(result.exitCode);
+}
+
+/// Writes `lib/src/generated/glyph_set.dart`.
+void _writeGlyphSetInterface(List<List<dynamic>> data) {
+ final file =
+ File('lib/src/generated/glyph_set.dart').openSync(mode: FileMode.write);
+ file.writeStringSync(r'''
+ // 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.
+
+ // Don't modify this file by hand! It's generated by tool/generate.dart.
+
+ /// A class that provides access to every configurable glyph.
+ ///
+ /// This is provided as a class so that individual chunks of code can choose
+ /// between `ascii` and `unicode` glyphs. For example:
+ ///
+ /// ```dart
+ /// import 'package:term_glyph/term_glyph.dart' as glyph;
+ ///
+ /// /// Adds a vertical line to the left of [text].
+ /// ///
+ /// /// If [unicode] is `true`, this uses Unicode for the line. If it's
+ /// /// `false`, this uses plain ASCII characters. If it's `null`, it
+ /// /// defaults to [glyph.ascii].
+ /// void addVerticalLine(String text, {bool unicode}) {
+ /// var glyphs =
+ /// (unicode ?? !glyph.ascii) ? glyph.unicodeGlyphs : glyph.asciiGlyphs;
+ ///
+ /// return text
+ /// .split('\n')
+ /// .map((line) => '${glyphs.verticalLine} $line')
+ /// .join('\n');
+ /// }
+ /// ```
+ abstract class GlyphSet {
+ /// Returns [glyph] if `this` supports Unicode glyphs and [alternative]
+ /// otherwise.
+ String glyphOrAscii(String glyph, String alternative);
+ ''');
+
+ for (var glyph in data) {
+ for (var line in (glyph[3] as String).split('\n')) {
+ file.writeStringSync('/// $line\n');
+ }
+
+ file.writeStringSync('String get ${glyph[0]};');
+ }
+
+ file.writeStringSync('}');
+ file.closeSync();
+}
+
+/// Writes `lib/src/generated/${prefix.toLowerCase()}_glyph_set.dart`.
+///
+/// If [ascii] is `true`, this writes the ASCII glyph set. Otherwise it writes
+/// the Unicode glyph set.
+void _writeGlyphSet(List<List<dynamic>> data, {required bool ascii}) {
+ final file =
+ File('lib/src/generated/${ascii ? "ascii" : "unicode"}_glyph_set.dart')
+ .openSync(mode: FileMode.write);
+
+ final className = '${ascii ? "Ascii" : "Unicode"}GlyphSet';
+ file.writeStringSync('''
+ // 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.
+
+ // Don't modify this file by hand! It's generated by tool/generate.dart.
+
+ import 'glyph_set.dart';
+
+ /// A [GlyphSet] that includes only ${ascii ? "ASCII" : "Unicode"} glyphs.
+ class $className implements GlyphSet {
+ const $className();
+ /// Returns [glyph] if `this` supports Unicode glyphs and [alternative]
+ /// otherwise.
+ @override
+ String glyphOrAscii(String glyph, String alternative) =>
+ ${ascii ? "alternative" : "glyph"};
+ ''');
+
+ final index = ascii ? 2 : 1;
+ for (var glyph in data) {
+ file.writeStringSync('''
+ @override
+ String get ${glyph[0]} => ${_quote(glyph[index] as String)};
+ ''');
+ }
+
+ file.writeStringSync('}');
+ file.closeSync();
+}
+
+/// Writes `lib/src/generated/top_level.dart`.
+void _writeTopLevel(List<List<dynamic>> data) {
+ final file =
+ File('lib/src/generated/top_level.dart').openSync(mode: FileMode.write);
+
+ file.writeStringSync('''
+ // 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.
+
+ // Don't modify this file by hand! It's generated by tool/generate.dart.
+
+ import '../../term_glyph.dart' as glyph;
+ ''');
+
+ for (var glyph in data) {
+ for (var line in (glyph[3] as String).split('\n')) {
+ file.writeStringSync('/// $line\n');
+ }
+
+ file.writeStringSync('''
+ ///
+ /// If [glyph.ascii] is `false`, this is "${glyph[1]}". If it's `true`, this is
+ /// "${glyph[2]}" instead.
+ String get ${glyph[0]} => glyph.glyphs.${glyph[0]};
+ ''');
+ }
+
+ file.closeSync();
+}
+
+String _quote(String input) => input.contains("'") ? '"$input"' : "'$input'";
diff --git a/pkgs/test_reflective_loader/.gitignore b/pkgs/test_reflective_loader/.gitignore
new file mode 100644
index 0000000..2a2c261
--- /dev/null
+++ b/pkgs/test_reflective_loader/.gitignore
@@ -0,0 +1,11 @@
+.buildlog
+.DS_Store
+.idea
+.dart_tool/
+.pub/
+.project
+.settings/
+build/
+packages
+.packages
+pubspec.lock
diff --git a/pkgs/test_reflective_loader/AUTHORS b/pkgs/test_reflective_loader/AUTHORS
new file mode 100644
index 0000000..e8063a8
--- /dev/null
+++ b/pkgs/test_reflective_loader/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/test_reflective_loader/CHANGELOG.md b/pkgs/test_reflective_loader/CHANGELOG.md
new file mode 100644
index 0000000..803eb0e
--- /dev/null
+++ b/pkgs/test_reflective_loader/CHANGELOG.md
@@ -0,0 +1,72 @@
+## 0.2.3
+
+- Require Dart `^3.1.0`.
+- Move to `dart-lang/tools` monorepo.
+
+## 0.2.2
+
+- Update to package:lints 2.0.0 and move it to a dev dependency.
+
+## 0.2.1
+
+- Use package:lints for analysis.
+- Populate the pubspec `repository` field.
+
+## 0.2.0
+
+- Stable null safety release.
+
+## 0.2.0-nullsafety.0
+
+- Migrate to the null safety language feature.
+
+## 0.1.9
+
+- Add `@SkippedTest` annotation and `skip_test` prefix.
+
+## 0.1.8
+
+- Update `FailingTest` to add named parameters `issue` and `reason`.
+
+## 0.1.7
+
+- Update documentation comments.
+- Remove `@MirrorsUsed` annotation on `dart:mirrors`.
+
+## 0.1.6
+
+- Make `FailingTest` public, with the URI of the issue that causes
+ the test to break.
+
+## 0.1.5
+
+- Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 0.1.3
+
+- Fix `@failingTest` to fail when the test passes.
+
+## 0.1.2
+
+- Update the pubspec `dependencies` section to include `package:test`
+
+## 0.1.1
+
+- For `@failingTest` tests, properly handle when the test fails by throwing an
+ exception in a timer task
+- Analyze this package in strong mode
+
+## 0.1.0
+
+- Switched from 'package:unittest' to 'package:test'.
+- Since 'package:test' does not define 'solo_test', in order to keep this
+ functionality, `defineReflectiveSuite` must be used to wrap all
+ `defineReflectiveTests` invocations.
+
+## 0.0.4
+
+- Added @failingTest, @assertFailingTest and @soloTest annotations.
+
+## 0.0.1
+
+- Initial version
diff --git a/pkgs/test_reflective_loader/LICENSE b/pkgs/test_reflective_loader/LICENSE
new file mode 100644
index 0000000..633672a
--- /dev/null
+++ b/pkgs/test_reflective_loader/LICENSE
@@ -0,0 +1,27 @@
+Copyright 2015, 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_reflective_loader/README.md b/pkgs/test_reflective_loader/README.md
new file mode 100644
index 0000000..9b5a83d
--- /dev/null
+++ b/pkgs/test_reflective_loader/README.md
@@ -0,0 +1,28 @@
+[](https://github.com/dart-lang/tools/actions/workflows/test_reflective_loader.yaml)
+[](https://pub.dev/packages/test_reflective_loader)
+[](https://pub.dev/packages/test_reflective_loader/publisher)
+
+Support for discovering tests and test suites using reflection.
+
+This package follows the xUnit style where each class is a test suite, and each
+method with the name prefix `test_` is a single test.
+
+Methods with names starting with `test_` are run using the `test()` function with
+the corresponding name. If the class defines methods `setUp()` or `tearDown()`,
+they are executed before / after each test correspondingly, even if the test fails.
+
+Methods with names starting with `solo_test_` are run using the `solo_test()` function.
+
+Methods with names starting with `fail_` are expected to fail.
+
+Methods with names starting with `solo_fail_` are run using the `solo_test()` function
+and expected to fail.
+
+Method returning `Future` class instances are asynchronous, so `tearDown()` is
+executed after the returned `Future` completes.
+
+## Features and bugs
+
+Please file feature requests and bugs at the [issue tracker][tracker].
+
+[tracker]: https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Atest_reflective_loader
diff --git a/pkgs/test_reflective_loader/analysis_options.yaml b/pkgs/test_reflective_loader/analysis_options.yaml
new file mode 100644
index 0000000..ea61158
--- /dev/null
+++ b/pkgs/test_reflective_loader/analysis_options.yaml
@@ -0,0 +1,5 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
+
+linter:
+ rules:
+ - public_member_api_docs
diff --git a/pkgs/test_reflective_loader/lib/test_reflective_loader.dart b/pkgs/test_reflective_loader/lib/test_reflective_loader.dart
new file mode 100644
index 0000000..cb69bf3
--- /dev/null
+++ b/pkgs/test_reflective_loader/lib/test_reflective_loader.dart
@@ -0,0 +1,354 @@
+// 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:mirrors';
+
+import 'package:test/test.dart' as test_package;
+
+/// A marker annotation used to annotate test methods which are expected to fail
+/// when asserts are enabled.
+const Object assertFailingTest = _AssertFailingTest();
+
+/// A marker annotation used to annotate test methods which are expected to
+/// fail.
+const Object failingTest = FailingTest();
+
+/// A marker annotation used to instruct dart2js to keep reflection information
+/// for the annotated classes.
+const Object reflectiveTest = _ReflectiveTest();
+
+/// A marker annotation used to annotate test methods that should be skipped.
+const Object skippedTest = SkippedTest();
+
+/// A marker annotation used to annotate "solo" groups and tests.
+const Object soloTest = _SoloTest();
+
+final List<_Group> _currentGroups = <_Group>[];
+int _currentSuiteLevel = 0;
+String _currentSuiteName = '';
+
+/// Is `true` the application is running in the checked mode.
+final bool _isCheckedMode = () {
+ try {
+ assert(false);
+ return false;
+ } catch (_) {
+ return true;
+ }
+}();
+
+/// Run the [define] function parameter that calls [defineReflectiveTests] to
+/// add normal and "solo" tests, and also calls [defineReflectiveSuite] to
+/// create embedded suites. If the current suite is the top-level one, perform
+/// check for "solo" groups and tests, and run all or only "solo" items.
+void defineReflectiveSuite(void Function() define, {String name = ''}) {
+ var groupName = _currentSuiteName;
+ _currentSuiteLevel++;
+ try {
+ _currentSuiteName = _combineNames(_currentSuiteName, name);
+ define();
+ } finally {
+ _currentSuiteName = groupName;
+ _currentSuiteLevel--;
+ }
+ _addTestsIfTopLevelSuite();
+}
+
+/// Runs test methods existing in the given [type].
+///
+/// If there is a "solo" test method in the top-level suite, only "solo" methods
+/// are run.
+///
+/// If there is a "solo" test type, only its test methods are run.
+///
+/// Otherwise all tests methods of all test types are run.
+///
+/// Each method is run with a new instance of [type].
+/// So, [type] should have a default constructor.
+///
+/// If [type] declares method `setUp`, it methods will be invoked before any
+/// test method invocation.
+///
+/// If [type] declares method `tearDown`, it will be invoked after any test
+/// method invocation. If method returns [Future] to test some asynchronous
+/// behavior, then `tearDown` will be invoked in `Future.complete`.
+void defineReflectiveTests(Type type) {
+ var classMirror = reflectClass(type);
+ if (!classMirror.metadata.any((InstanceMirror annotation) =>
+ annotation.type.reflectedType == _ReflectiveTest)) {
+ var name = MirrorSystem.getName(classMirror.qualifiedName);
+ throw Exception('Class $name must have annotation "@reflectiveTest" '
+ 'in order to be run by runReflectiveTests.');
+ }
+
+ _Group group;
+ {
+ var isSolo = _hasAnnotationInstance(classMirror, soloTest);
+ var className = MirrorSystem.getName(classMirror.simpleName);
+ group = _Group(isSolo, _combineNames(_currentSuiteName, className));
+ _currentGroups.add(group);
+ }
+
+ classMirror.instanceMembers
+ .forEach((Symbol symbol, MethodMirror memberMirror) {
+ // we need only methods
+ if (!memberMirror.isRegularMethod) {
+ return;
+ }
+ // prepare information about the method
+ var memberName = MirrorSystem.getName(symbol);
+ var isSolo = memberName.startsWith('solo_') ||
+ _hasAnnotationInstance(memberMirror, soloTest);
+ // test_
+ if (memberName.startsWith('test_')) {
+ if (_hasSkippedTestAnnotation(memberMirror)) {
+ group.addSkippedTest(memberName);
+ } else {
+ group.addTest(isSolo, memberName, memberMirror, () {
+ if (_hasFailingTestAnnotation(memberMirror) ||
+ _isCheckedMode && _hasAssertFailingTestAnnotation(memberMirror)) {
+ return _runFailingTest(classMirror, symbol);
+ } else {
+ return _runTest(classMirror, symbol);
+ }
+ });
+ }
+ return;
+ }
+ // solo_test_
+ if (memberName.startsWith('solo_test_')) {
+ group.addTest(true, memberName, memberMirror, () {
+ return _runTest(classMirror, symbol);
+ });
+ }
+ // fail_test_
+ if (memberName.startsWith('fail_')) {
+ group.addTest(isSolo, memberName, memberMirror, () {
+ return _runFailingTest(classMirror, symbol);
+ });
+ }
+ // solo_fail_test_
+ if (memberName.startsWith('solo_fail_')) {
+ group.addTest(true, memberName, memberMirror, () {
+ return _runFailingTest(classMirror, symbol);
+ });
+ }
+ // skip_test_
+ if (memberName.startsWith('skip_test_')) {
+ group.addSkippedTest(memberName);
+ }
+ });
+
+ // Support for the case of missing enclosing [defineReflectiveSuite].
+ _addTestsIfTopLevelSuite();
+}
+
+/// If the current suite is the top-level one, add tests to the `test` package.
+void _addTestsIfTopLevelSuite() {
+ if (_currentSuiteLevel == 0) {
+ void runTests({required bool allGroups, required bool allTests}) {
+ for (var group in _currentGroups) {
+ if (allGroups || group.isSolo) {
+ for (var test in group.tests) {
+ if (allTests || test.isSolo) {
+ test_package.test(test.name, test.function,
+ timeout: test.timeout, skip: test.isSkipped);
+ }
+ }
+ }
+ }
+ }
+
+ if (_currentGroups.any((g) => g.hasSoloTest)) {
+ runTests(allGroups: true, allTests: false);
+ } else if (_currentGroups.any((g) => g.isSolo)) {
+ runTests(allGroups: false, allTests: true);
+ } else {
+ runTests(allGroups: true, allTests: true);
+ }
+ _currentGroups.clear();
+ }
+}
+
+/// Return the combination of the [base] and [addition] names.
+/// If any other two is `null`, then the other one is returned.
+String _combineNames(String base, String addition) {
+ if (base.isEmpty) {
+ return addition;
+ } else if (addition.isEmpty) {
+ return base;
+ } else {
+ return '$base | $addition';
+ }
+}
+
+Object? _getAnnotationInstance(DeclarationMirror declaration, Type type) {
+ for (var annotation in declaration.metadata) {
+ if ((annotation.reflectee as Object).runtimeType == type) {
+ return annotation.reflectee;
+ }
+ }
+ return null;
+}
+
+bool _hasAnnotationInstance(DeclarationMirror declaration, Object instance) =>
+ declaration.metadata.any((InstanceMirror annotation) =>
+ identical(annotation.reflectee, instance));
+
+bool _hasAssertFailingTestAnnotation(MethodMirror method) =>
+ _hasAnnotationInstance(method, assertFailingTest);
+
+bool _hasFailingTestAnnotation(MethodMirror method) =>
+ _hasAnnotationInstance(method, failingTest);
+
+bool _hasSkippedTestAnnotation(MethodMirror method) =>
+ _hasAnnotationInstance(method, skippedTest);
+
+Future<Object?> _invokeSymbolIfExists(
+ InstanceMirror instanceMirror, Symbol symbol) {
+ Object? invocationResult;
+ InstanceMirror? closure;
+ try {
+ closure = instanceMirror.getField(symbol);
+ // ignore: avoid_catching_errors
+ } on NoSuchMethodError {
+ // ignore
+ }
+
+ if (closure is ClosureMirror) {
+ invocationResult = closure.apply([]).reflectee;
+ }
+ return Future.value(invocationResult);
+}
+
+/// Run a test that is expected to fail, and confirm that it fails.
+///
+/// This properly handles the following cases:
+/// - The test fails by throwing an exception
+/// - The test returns a future which completes with an error.
+/// - An exception is thrown to the zone handler from a timer task.
+Future<Object?>? _runFailingTest(ClassMirror classMirror, Symbol symbol) {
+ var passed = false;
+ return runZonedGuarded(() {
+ // ignore: void_checks
+ return Future.sync(() => _runTest(classMirror, symbol)).then<void>((_) {
+ passed = true;
+ test_package.fail('Test passed - expected to fail.');
+ }).catchError((Object e) {
+ // if passed, and we call fail(), rethrow this exception
+ if (passed) {
+ // ignore: only_throw_errors
+ throw e;
+ }
+ // otherwise, an exception is not a failure for _runFailingTest
+ });
+ }, (e, st) {
+ // if passed, and we call fail(), rethrow this exception
+ if (passed) {
+ // ignore: only_throw_errors
+ throw e;
+ }
+ // otherwise, an exception is not a failure for _runFailingTest
+ });
+}
+
+Future<void> _runTest(ClassMirror classMirror, Symbol symbol) async {
+ var instanceMirror = classMirror.newInstance(const Symbol(''), []);
+ try {
+ await _invokeSymbolIfExists(instanceMirror, #setUp);
+ await instanceMirror.invoke(symbol, []).reflectee;
+ } finally {
+ await _invokeSymbolIfExists(instanceMirror, #tearDown);
+ }
+}
+
+typedef _TestFunction = dynamic Function();
+
+/// A marker annotation used to annotate test methods which are expected to
+/// fail.
+class FailingTest {
+ /// Initialize this annotation with the given arguments.
+ ///
+ /// [issue] is a full URI describing the failure and used for tracking.
+ /// [reason] is a free form textual description.
+ const FailingTest({String? issue, String? reason});
+}
+
+/// A marker annotation used to annotate test methods which are skipped.
+class SkippedTest {
+ /// Initialize this annotation with the given arguments.
+ ///
+ /// [issue] is a full URI describing the failure and used for tracking.
+ /// [reason] is a free form textual description.
+ const SkippedTest({String? issue, String? reason});
+}
+
+/// A marker annotation used to annotate test methods with additional timeout
+/// information.
+class TestTimeout {
+ final test_package.Timeout _timeout;
+
+ /// Initialize this annotation with the given timeout.
+ const TestTimeout(test_package.Timeout timeout) : _timeout = timeout;
+}
+
+/// A marker annotation used to annotate test methods which are expected to fail
+/// when asserts are enabled.
+class _AssertFailingTest {
+ const _AssertFailingTest();
+}
+
+/// Information about a type based test group.
+class _Group {
+ final bool isSolo;
+ final String name;
+ final List<_Test> tests = <_Test>[];
+
+ _Group(this.isSolo, this.name);
+
+ bool get hasSoloTest => tests.any((test) => test.isSolo);
+
+ void addSkippedTest(String name) {
+ var fullName = _combineNames(this.name, name);
+ tests.add(_Test.skipped(isSolo, fullName));
+ }
+
+ void addTest(bool isSolo, String name, MethodMirror memberMirror,
+ _TestFunction function) {
+ var fullName = _combineNames(this.name, name);
+ var timeout =
+ _getAnnotationInstance(memberMirror, TestTimeout) as TestTimeout?;
+ tests.add(_Test(isSolo, fullName, function, timeout?._timeout));
+ }
+}
+
+/// A marker annotation used to instruct dart2js to keep reflection information
+/// for the annotated classes.
+class _ReflectiveTest {
+ const _ReflectiveTest();
+}
+
+/// A marker annotation used to annotate "solo" groups and tests.
+class _SoloTest {
+ const _SoloTest();
+}
+
+/// Information about a test.
+class _Test {
+ final bool isSolo;
+ final String name;
+ final _TestFunction function;
+ final test_package.Timeout? timeout;
+
+ final bool isSkipped;
+
+ _Test(this.isSolo, this.name, this.function, this.timeout)
+ : isSkipped = false;
+
+ _Test.skipped(this.isSolo, this.name)
+ : isSkipped = true,
+ function = (() {}),
+ timeout = null;
+}
diff --git a/pkgs/test_reflective_loader/pubspec.yaml b/pkgs/test_reflective_loader/pubspec.yaml
new file mode 100644
index 0000000..569933f
--- /dev/null
+++ b/pkgs/test_reflective_loader/pubspec.yaml
@@ -0,0 +1,13 @@
+name: test_reflective_loader
+version: 0.2.3
+description: Support for discovering tests and test suites using reflection.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/test_reflective_loader
+
+environment:
+ sdk: ^3.1.0
+
+dependencies:
+ test: ^1.16.0
+
+dev_dependencies:
+ dart_flutter_team_lints: ^3.0.0
diff --git a/pkgs/test_reflective_loader/test/test_reflective_loader_test.dart b/pkgs/test_reflective_loader/test/test_reflective_loader_test.dart
new file mode 100644
index 0000000..fad98a5
--- /dev/null
+++ b/pkgs/test_reflective_loader/test/test_reflective_loader_test.dart
@@ -0,0 +1,48 @@
+// 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: non_constant_identifier_names
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+
+void main() {
+ defineReflectiveSuite(() {
+ defineReflectiveTests(TestReflectiveLoaderTest);
+ });
+}
+
+@reflectiveTest
+class TestReflectiveLoaderTest {
+ void test_passes() {
+ expect(true, true);
+ }
+
+ @failingTest
+ void test_fails() {
+ expect(false, true);
+ }
+
+ @failingTest
+ void test_fails_throws_sync() {
+ throw StateError('foo');
+ }
+
+ @failingTest
+ Future test_fails_throws_async() {
+ return Future.error('foo');
+ }
+
+ @skippedTest
+ void test_fails_but_skipped() {
+ throw StateError('foo');
+ }
+
+ @skippedTest
+ void test_times_out_but_skipped() {
+ while (true) {}
+ }
+}
diff --git a/pkgs/timing/.gitignore b/pkgs/timing/.gitignore
new file mode 100644
index 0000000..1ddf798
--- /dev/null
+++ b/pkgs/timing/.gitignore
@@ -0,0 +1,7 @@
+.packages
+/build/
+pubspec.lock
+
+# Files generated by dart tools
+.dart_tool
+doc/
diff --git a/pkgs/timing/CHANGELOG.md b/pkgs/timing/CHANGELOG.md
new file mode 100644
index 0000000..8cdb8ea
--- /dev/null
+++ b/pkgs/timing/CHANGELOG.md
@@ -0,0 +1,34 @@
+## 1.0.2
+
+- Require Dart `3.4`.
+- Move to `dart-lang/tools` monorepo.
+
+## 1.0.1
+
+- Require Dart `2.14`.
+
+## 1.0.0
+
+- Enable null safety.
+- Require Dart `2.12`.
+
+## 0.1.1+3
+
+- Allow `package:json_annotation` `'>=1.0.0 <5.0.0'`.
+
+## 0.1.1+2
+
+- Support the latest version of `package:json_annotation`.
+- Require Dart 2.2 or later.
+
+## 0.1.1+1
+
+- Support the latest version of `package:json_annotation`.
+
+## 0.1.1
+
+- Add JSON serialization
+
+## 0.1.0
+
+- Initial release
diff --git a/pkgs/timing/LICENSE b/pkgs/timing/LICENSE
new file mode 100644
index 0000000..9972f6e
--- /dev/null
+++ b/pkgs/timing/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/timing/README.md b/pkgs/timing/README.md
new file mode 100644
index 0000000..9dab7cc
--- /dev/null
+++ b/pkgs/timing/README.md
@@ -0,0 +1,30 @@
+[](https://github.com/dart-lang/tools/actions/workflows/timing.yaml)
+[](https://pub.dev/packages/timing)
+[](https://pub.dev/packages/timing/publisher)
+
+Timing is a simple package for tracking performance of both async and sync actions
+
+## Usage
+
+```dart
+var tracker = AsyncTimeTracker();
+await tracker.track(() async {
+ // some async code here
+});
+
+// Use results
+print('${tracker.duration} ${tracker.innerDuration} ${tracker.slices}');
+```
+
+## Building
+
+Use the following command to re-generate `lib/src/timing.g.dart` file:
+
+```bash
+dart pub run build_runner build
+```
+
+## Publishing automation
+
+For information about our publishing automation and release process, see
+https://github.com/dart-lang/ecosystem/wiki/Publishing-automation.
diff --git a/pkgs/timing/analysis_options.yaml b/pkgs/timing/analysis_options.yaml
new file mode 100644
index 0000000..396236d
--- /dev/null
+++ b/pkgs/timing/analysis_options.yaml
@@ -0,0 +1,2 @@
+# https://dart.dev/tools/analysis#the-analysis-options-file
+include: package:dart_flutter_team_lints/analysis_options.yaml
diff --git a/pkgs/timing/lib/src/clock.dart b/pkgs/timing/lib/src/clock.dart
new file mode 100644
index 0000000..6a9d295
--- /dev/null
+++ b/pkgs/timing/lib/src/clock.dart
@@ -0,0 +1,20 @@
+// 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';
+
+/// A function that returns the current [DateTime].
+typedef _Clock = DateTime Function();
+DateTime _defaultClock() => DateTime.now();
+
+const _zoneKey = #timing_Clock;
+
+/// Returns the current [DateTime].
+///
+/// May be overridden for tests using [scopeClock].
+DateTime now() => (Zone.current[_zoneKey] as _Clock? ?? _defaultClock)();
+
+/// Runs [f], with [clock] scoped whenever [now] is called.
+T scopeClock<T>(DateTime Function() clock, T Function() f) =>
+ runZoned(f, zoneValues: {_zoneKey: clock});
diff --git a/pkgs/timing/lib/src/timing.dart b/pkgs/timing/lib/src/timing.dart
new file mode 100644
index 0000000..049ba81
--- /dev/null
+++ b/pkgs/timing/lib/src/timing.dart
@@ -0,0 +1,338 @@
+// 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:json_annotation/json_annotation.dart';
+
+import 'clock.dart';
+
+part 'timing.g.dart';
+
+/// The timings of an operation, including its [startTime], [stopTime], and
+/// [duration].
+@JsonSerializable()
+class TimeSlice {
+ /// The total duration of this operation, equivalent to taking the difference
+ /// between [stopTime] and [startTime].
+ Duration get duration => stopTime.difference(startTime);
+
+ final DateTime startTime;
+
+ final DateTime stopTime;
+
+ TimeSlice(this.startTime, this.stopTime);
+
+ factory TimeSlice.fromJson(Map<String, dynamic> json) =>
+ _$TimeSliceFromJson(json);
+
+ Map<String, dynamic> toJson() => _$TimeSliceToJson(this);
+
+ @override
+ String toString() => '($startTime + $duration)';
+}
+
+/// The timings of an async operation, consist of several sync [slices] and
+/// includes total [startTime], [stopTime], and [duration].
+@JsonSerializable()
+class TimeSliceGroup implements TimeSlice {
+ final List<TimeSlice> slices;
+
+ @override
+ DateTime get startTime => slices.first.startTime;
+
+ @override
+ DateTime get stopTime => slices.last.stopTime;
+
+ /// The total duration of this operation, equivalent to taking the difference
+ /// between [stopTime] and [startTime].
+ @override
+ Duration get duration => stopTime.difference(startTime);
+
+ /// Sum of [duration]s of all [slices].
+ ///
+ /// If some of slices implements [TimeSliceGroup] [innerDuration] will be used
+ /// to compute sum.
+ Duration get innerDuration => slices.fold(
+ Duration.zero,
+ (duration, slice) =>
+ duration +
+ (slice is TimeSliceGroup ? slice.innerDuration : slice.duration));
+
+ TimeSliceGroup(this.slices);
+
+ /// Constructs TimeSliceGroup from JSON representation
+ factory TimeSliceGroup.fromJson(Map<String, dynamic> json) =>
+ _$TimeSliceGroupFromJson(json);
+
+ @override
+ Map<String, dynamic> toJson() => _$TimeSliceGroupToJson(this);
+
+ @override
+ String toString() => slices.toString();
+}
+
+abstract class TimeTracker implements TimeSlice {
+ /// Whether tracking is active.
+ ///
+ /// Tracking is only active after `isStarted` and before `isFinished`.
+ bool get isTracking;
+
+ /// Whether tracking is finished.
+ ///
+ /// Tracker can't be used as [TimeSlice] before it is finished
+ bool get isFinished;
+
+ /// Whether tracking was started.
+ ///
+ /// Equivalent of `isTracking || isFinished`
+ bool get isStarted;
+
+ T track<T>(T Function() action);
+}
+
+/// Tracks only sync actions
+class SyncTimeTracker implements TimeTracker {
+ /// When this operation started, call [_start] to set this.
+ @override
+ DateTime get startTime => _startTime!;
+ DateTime? _startTime;
+
+ /// When this operation stopped, call [_stop] to set this.
+ @override
+ DateTime get stopTime => _stopTime!;
+ DateTime? _stopTime;
+
+ /// Start tracking this operation, must only be called once, before [_stop].
+ void _start() {
+ assert(_startTime == null && _stopTime == null);
+ _startTime = now();
+ }
+
+ /// Stop tracking this operation, must only be called once, after [_start].
+ void _stop() {
+ assert(_startTime != null && _stopTime == null);
+ _stopTime = now();
+ }
+
+ /// Splits tracker into two slices.
+ ///
+ /// Returns new [TimeSlice] started on [startTime] and ended now. Modifies
+ /// [startTime] of tracker to current time point
+ ///
+ /// Don't change state of tracker. Can be called only while [isTracking], and
+ /// tracker will sill be tracking after call.
+ TimeSlice _split() {
+ if (!isTracking) {
+ throw StateError('Can be only called while tracking');
+ }
+ final splitPoint = now();
+ final prevSlice = TimeSlice(_startTime!, splitPoint);
+ _startTime = splitPoint;
+ return prevSlice;
+ }
+
+ @override
+ T track<T>(T Function() action) {
+ if (isStarted) {
+ throw StateError('Can not be tracked twice');
+ }
+ _start();
+ try {
+ return action();
+ } finally {
+ _stop();
+ }
+ }
+
+ @override
+ bool get isStarted => _startTime != null;
+
+ @override
+ bool get isTracking => _startTime != null && _stopTime == null;
+
+ @override
+ bool get isFinished => _startTime != null && _stopTime != null;
+
+ @override
+ Duration get duration => _stopTime!.difference(_startTime!);
+
+ /// Converts to JSON representation
+ ///
+ /// Can't be used before [isFinished]
+ @override
+ Map<String, dynamic> toJson() => _$TimeSliceToJson(this);
+}
+
+/// Async actions returning [Future] will be tracked as single sync time span
+/// from the beginning of execution till completion of future
+class SimpleAsyncTimeTracker extends SyncTimeTracker {
+ @override
+ T track<T>(T Function() action) {
+ if (isStarted) {
+ throw StateError('Can not be tracked twice');
+ }
+ T result;
+ _start();
+ try {
+ result = action();
+ } catch (_) {
+ _stop();
+ rethrow;
+ }
+ if (result is Future) {
+ return result.whenComplete(_stop) as T;
+ } else {
+ _stop();
+ return result;
+ }
+ }
+}
+
+/// No-op implementation of [SyncTimeTracker] that does nothing.
+class NoOpTimeTracker implements TimeTracker {
+ static final sharedInstance = NoOpTimeTracker();
+
+ @override
+ Duration get duration =>
+ throw UnsupportedError('Unsupported in no-op implementation');
+
+ @override
+ DateTime get startTime =>
+ throw UnsupportedError('Unsupported in no-op implementation');
+
+ @override
+ DateTime get stopTime =>
+ throw UnsupportedError('Unsupported in no-op implementation');
+
+ @override
+ bool get isStarted =>
+ throw UnsupportedError('Unsupported in no-op implementation');
+
+ @override
+ bool get isTracking =>
+ throw UnsupportedError('Unsupported in no-op implementation');
+
+ @override
+ bool get isFinished =>
+ throw UnsupportedError('Unsupported in no-op implementation');
+
+ @override
+ T track<T>(T Function() action) => action();
+
+ @override
+ Map<String, dynamic> toJson() =>
+ throw UnsupportedError('Unsupported in no-op implementation');
+}
+
+/// Track all async execution as disjoint time [slices] in ascending order.
+///
+/// Can [track] both async and sync actions.
+/// Can exclude time of tested trackers.
+///
+/// If tracked action spawns some dangled async executions behavior is't
+/// defined. Tracked might or might not track time of such executions
+class AsyncTimeTracker extends TimeSliceGroup implements TimeTracker {
+ final bool trackNested;
+
+ static const _zoneKey = #timing_AsyncTimeTracker;
+
+ AsyncTimeTracker({this.trackNested = true}) : super([]);
+
+ T _trackSyncSlice<T>(ZoneDelegate parent, Zone zone, T Function() action) {
+ // Ignore dangling runs after tracker completes
+ if (isFinished) {
+ return action();
+ }
+
+ final isNestedRun = slices.isNotEmpty &&
+ slices.last is SyncTimeTracker &&
+ (slices.last as SyncTimeTracker).isTracking;
+ final isExcludedNestedTrack = !trackNested && zone[_zoneKey] != this;
+
+ // Exclude nested sync tracks
+ if (isNestedRun && isExcludedNestedTrack) {
+ final timer = slices.last as SyncTimeTracker;
+ // Split already tracked time into new slice.
+ // Replace tracker in slices.last with splitted slice, to indicate for
+ // recursive calls that we not tracking.
+ slices.last = parent.run(zone, timer._split);
+ try {
+ return action();
+ } finally {
+ // Split tracker again and discard slice from nested tracker
+ parent.run(zone, timer._split);
+ // Add tracker back to list of slices and continue tracking
+ slices.add(timer);
+ }
+ }
+
+ // Exclude nested async tracks
+ if (isExcludedNestedTrack) {
+ return action();
+ }
+
+ // Split time slices in nested sync runs
+ if (isNestedRun) {
+ return action();
+ }
+
+ final timer = SyncTimeTracker();
+ slices.add(timer);
+
+ // Pass to parent zone, in case of overwritten clock
+ return parent.runUnary(zone, timer.track, action);
+ }
+
+ static final asyncTimeTrackerZoneSpecification = ZoneSpecification(
+ run: <R>(Zone self, ZoneDelegate parent, Zone zone, R Function() f) {
+ final tracker = self[_zoneKey] as AsyncTimeTracker;
+ return tracker._trackSyncSlice(parent, zone, () => parent.run(zone, f));
+ },
+ runUnary: <R, T>(Zone self, ZoneDelegate parent, Zone zone, R Function(T) f,
+ T arg) {
+ final tracker = self[_zoneKey] as AsyncTimeTracker;
+ return tracker._trackSyncSlice(
+ parent, zone, () => parent.runUnary(zone, f, arg));
+ },
+ runBinary: <R, T1, T2>(Zone self, ZoneDelegate parent, Zone zone,
+ R Function(T1, T2) f, T1 arg1, T2 arg2) {
+ final tracker = self[_zoneKey] as AsyncTimeTracker;
+ return tracker._trackSyncSlice(
+ parent, zone, () => parent.runBinary(zone, f, arg1, arg2));
+ },
+ );
+
+ @override
+ T track<T>(T Function() action) {
+ if (isStarted) {
+ throw StateError('Can not be tracked twice');
+ }
+ _tracking = true;
+ final result = runZoned(action,
+ zoneSpecification: asyncTimeTrackerZoneSpecification,
+ zoneValues: {_zoneKey: this});
+ if (result is Future) {
+ return result
+ // Break possible sync processing of future completion, so slice
+ // trackers can be finished
+ .whenComplete(Future.value)
+ .whenComplete(() => _tracking = false) as T;
+ } else {
+ _tracking = false;
+ return result;
+ }
+ }
+
+ bool? _tracking;
+
+ @override
+ bool get isStarted => _tracking != null;
+
+ @override
+ bool get isFinished => _tracking == false;
+
+ @override
+ bool get isTracking => _tracking == true;
+}
diff --git a/pkgs/timing/lib/src/timing.g.dart b/pkgs/timing/lib/src/timing.g.dart
new file mode 100644
index 0000000..679c082
--- /dev/null
+++ b/pkgs/timing/lib/src/timing.g.dart
@@ -0,0 +1,29 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'timing.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+TimeSlice _$TimeSliceFromJson(Map<String, dynamic> json) => TimeSlice(
+ DateTime.parse(json['startTime'] as String),
+ DateTime.parse(json['stopTime'] as String),
+ );
+
+Map<String, dynamic> _$TimeSliceToJson(TimeSlice instance) => <String, dynamic>{
+ 'startTime': instance.startTime.toIso8601String(),
+ 'stopTime': instance.stopTime.toIso8601String(),
+ };
+
+TimeSliceGroup _$TimeSliceGroupFromJson(Map<String, dynamic> json) =>
+ TimeSliceGroup(
+ (json['slices'] as List<dynamic>)
+ .map((e) => TimeSlice.fromJson(e as Map<String, dynamic>))
+ .toList(),
+ );
+
+Map<String, dynamic> _$TimeSliceGroupToJson(TimeSliceGroup instance) =>
+ <String, dynamic>{
+ 'slices': instance.slices,
+ };
diff --git a/pkgs/timing/lib/timing.dart b/pkgs/timing/lib/timing.dart
new file mode 100644
index 0000000..5cb16d4
--- /dev/null
+++ b/pkgs/timing/lib/timing.dart
@@ -0,0 +1,13 @@
+// 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/timing.dart'
+ show
+ AsyncTimeTracker,
+ NoOpTimeTracker,
+ SimpleAsyncTimeTracker,
+ SyncTimeTracker,
+ TimeSlice,
+ TimeSliceGroup,
+ TimeTracker;
diff --git a/pkgs/timing/pubspec.yaml b/pkgs/timing/pubspec.yaml
new file mode 100644
index 0000000..891a8af
--- /dev/null
+++ b/pkgs/timing/pubspec.yaml
@@ -0,0 +1,18 @@
+name: timing
+version: 1.0.2
+description: >-
+ A simple package for tracking the performance of synchronous and asynchronous
+ actions.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/timing
+
+environment:
+ sdk: ^3.4.0
+
+dependencies:
+ json_annotation: ^4.9.0
+
+dev_dependencies:
+ build_runner: ^2.0.6
+ dart_flutter_team_lints: ^3.0.0
+ json_serializable: ^6.0.0
+ test: ^1.17.10
diff --git a/pkgs/timing/test/timing_test.dart b/pkgs/timing/test/timing_test.dart
new file mode 100644
index 0000000..b5836d9
--- /dev/null
+++ b/pkgs/timing/test/timing_test.dart
@@ -0,0 +1,416 @@
+// 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: only_throw_errors, inference_failure_on_instance_creation
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:timing/src/clock.dart';
+import 'package:timing/src/timing.dart';
+
+void _noop() {}
+
+void main() {
+ late DateTime time;
+ final startTime = DateTime(2017);
+ DateTime fakeClock() => time;
+
+ late TimeTracker tracker;
+ late TimeTracker nestedTracker;
+
+ T scopedTrack<T>(T Function() f) =>
+ scopeClock(fakeClock, () => tracker.track(f));
+
+ setUp(() {
+ time = startTime;
+ });
+
+ void canHandleSync([void Function() additionalExpects = _noop]) {
+ test('Can track sync code', () {
+ expect(tracker.isStarted, false);
+ expect(tracker.isTracking, false);
+ expect(tracker.isFinished, false);
+ scopedTrack(() {
+ expect(tracker.isStarted, true);
+ expect(tracker.isTracking, true);
+ expect(tracker.isFinished, false);
+ time = time.add(const Duration(seconds: 5));
+ });
+ expect(tracker.isStarted, true);
+ expect(tracker.isTracking, false);
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 5));
+ additionalExpects();
+ });
+
+ test('Can track handled sync exceptions', () async {
+ scopedTrack(() {
+ try {
+ time = time.add(const Duration(seconds: 4));
+ throw 'error';
+ } on String {
+ time = time.add(const Duration(seconds: 1));
+ }
+ });
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 5));
+ additionalExpects();
+ });
+
+ test('Can track in case of unhandled sync exceptions', () async {
+ expect(
+ () => scopedTrack(() {
+ time = time.add(const Duration(seconds: 5));
+ throw 'error';
+ }),
+ throwsA(const TypeMatcher<String>()));
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 5));
+ additionalExpects();
+ });
+
+ test('Can be nested sync', () {
+ scopedTrack(() {
+ time = time.add(const Duration(seconds: 1));
+ nestedTracker.track(() {
+ time = time.add(const Duration(seconds: 2));
+ });
+ time = time.add(const Duration(seconds: 4));
+ });
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 7));
+ expect(nestedTracker.startTime.isAfter(startTime), true);
+ expect(nestedTracker.stopTime.isBefore(time), true);
+ expect(nestedTracker.duration, const Duration(seconds: 2));
+ additionalExpects();
+ });
+ }
+
+ void canHandleAsync([void Function() additionalExpects = _noop]) {
+ test('Can track async code', () async {
+ expect(tracker.isStarted, false);
+ expect(tracker.isTracking, false);
+ expect(tracker.isFinished, false);
+ await scopedTrack(() => Future(() {
+ expect(tracker.isStarted, true);
+ expect(tracker.isTracking, true);
+ expect(tracker.isFinished, false);
+ time = time.add(const Duration(seconds: 5));
+ }));
+ expect(tracker.isStarted, true);
+ expect(tracker.isTracking, false);
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 5));
+ additionalExpects();
+ });
+
+ test('Can track handled async exceptions', () async {
+ await scopedTrack(() {
+ time = time.add(const Duration(seconds: 1));
+ return Future(() {
+ time = time.add(const Duration(seconds: 2));
+ throw 'error';
+ }).then((_) {
+ time = time.add(const Duration(seconds: 4));
+ }).catchError((error, stack) {
+ time = time.add(const Duration(seconds: 8));
+ });
+ });
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 11));
+ additionalExpects();
+ });
+
+ test('Can track in case of unhandled async exceptions', () async {
+ final future = scopedTrack(() {
+ time = time.add(const Duration(seconds: 1));
+ return Future(() {
+ time = time.add(const Duration(seconds: 2));
+ throw 'error';
+ }).then((_) {
+ time = time.add(const Duration(seconds: 4));
+ });
+ });
+ await expectLater(future, throwsA(const TypeMatcher<String>()));
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 3));
+ additionalExpects();
+ });
+
+ test('Can be nested async', () async {
+ await scopedTrack(() async {
+ time = time.add(const Duration(milliseconds: 1));
+ await Future.value();
+ time = time.add(const Duration(milliseconds: 2));
+ await nestedTracker.track(() async {
+ time = time.add(const Duration(milliseconds: 4));
+ await Future.value();
+ time = time.add(const Duration(milliseconds: 8));
+ await Future.value();
+ time = time.add(const Duration(milliseconds: 16));
+ });
+ time = time.add(const Duration(milliseconds: 32));
+ await Future.value();
+ time = time.add(const Duration(milliseconds: 64));
+ });
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(milliseconds: 127));
+ expect(nestedTracker.startTime.isAfter(startTime), true);
+ expect(nestedTracker.stopTime.isBefore(time), true);
+ expect(nestedTracker.duration, const Duration(milliseconds: 28));
+ additionalExpects();
+ });
+ }
+
+ group('SyncTimeTracker', () {
+ setUp(() {
+ tracker = SyncTimeTracker();
+ nestedTracker = SyncTimeTracker();
+ });
+
+ canHandleSync();
+
+ test('Can not track async code', () async {
+ await scopedTrack(() => Future(() {
+ time = time.add(const Duration(seconds: 5));
+ }));
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, startTime);
+ expect(tracker.duration, const Duration(seconds: 0));
+ });
+ });
+
+ group('AsyncTimeTracker.simple', () {
+ setUp(() {
+ tracker = SimpleAsyncTimeTracker();
+ nestedTracker = SimpleAsyncTimeTracker();
+ });
+
+ canHandleSync();
+
+ canHandleAsync();
+
+ test('Can not distinguish own async code', () async {
+ final future = scopedTrack(() => Future(() {
+ time = time.add(const Duration(seconds: 5));
+ }));
+ time = time.add(const Duration(seconds: 10));
+ await future;
+ expect(tracker.isFinished, true);
+ expect(tracker.startTime, startTime);
+ expect(tracker.stopTime, time);
+ expect(tracker.duration, const Duration(seconds: 15));
+ });
+ });
+
+ group('AsyncTimeTracker', () {
+ late AsyncTimeTracker asyncTracker;
+ late AsyncTimeTracker nestedAsyncTracker;
+ setUp(() {
+ tracker = asyncTracker = AsyncTimeTracker();
+ nestedTracker = nestedAsyncTracker = AsyncTimeTracker();
+ });
+
+ canHandleSync(() {
+ expect(asyncTracker.innerDuration, asyncTracker.duration);
+ expect(asyncTracker.slices.length, 1);
+ });
+
+ canHandleAsync(() {
+ expect(asyncTracker.innerDuration, asyncTracker.duration);
+ expect(asyncTracker.slices.length, greaterThan(1));
+ });
+
+ test('Can track complex async innerDuration', () async {
+ final completer = Completer();
+ final future = scopedTrack(() async {
+ time = time.add(const Duration(seconds: 1)); // Tracked sync
+ await Future.value();
+ time = time.add(const Duration(seconds: 2)); // Tracked async
+ await completer.future;
+ time = time.add(const Duration(seconds: 4)); // Tracked async, delayed
+ }).then((_) {
+ time = time.add(const Duration(seconds: 8)); // Async, after tracking
+ });
+ time = time.add(const Duration(seconds: 16)); // Sync, between slices
+
+ await Future(() {
+ // Async, between slices
+ time = time.add(const Duration(seconds: 32));
+ completer.complete();
+ });
+ await future;
+ expect(asyncTracker.isFinished, true);
+ expect(asyncTracker.startTime, startTime);
+ expect(asyncTracker.stopTime.isBefore(time), true);
+ expect(asyncTracker.duration, const Duration(seconds: 55));
+ expect(asyncTracker.innerDuration, const Duration(seconds: 7));
+ expect(asyncTracker.slices.length, greaterThan(1));
+ });
+
+ test('Can exclude nested sync', () {
+ tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
+ scopedTrack(() {
+ time = time.add(const Duration(seconds: 1));
+ nestedAsyncTracker.track(() {
+ time = time.add(const Duration(seconds: 2));
+ });
+ time = time.add(const Duration(seconds: 4));
+ });
+ expect(asyncTracker.isFinished, true);
+ expect(asyncTracker.startTime, startTime);
+ expect(asyncTracker.stopTime, time);
+ expect(asyncTracker.duration, const Duration(seconds: 7));
+ expect(asyncTracker.innerDuration, const Duration(seconds: 5));
+ expect(asyncTracker.slices.length, greaterThan(1));
+ expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
+ expect(nestedAsyncTracker.stopTime.isBefore(time), true);
+ expect(nestedAsyncTracker.duration, const Duration(seconds: 2));
+ expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 2));
+ expect(nestedAsyncTracker.slices.length, 1);
+ });
+
+ test('Can exclude complex nested sync', () {
+ tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
+ nestedAsyncTracker = AsyncTimeTracker(trackNested: false);
+ final nestedAsyncTracker2 = AsyncTimeTracker(trackNested: false);
+ scopedTrack(() {
+ time = time.add(const Duration(seconds: 1));
+ nestedAsyncTracker.track(() {
+ time = time.add(const Duration(seconds: 2));
+ nestedAsyncTracker2.track(() {
+ time = time.add(const Duration(seconds: 4));
+ });
+ time = time.add(const Duration(seconds: 8));
+ });
+ time = time.add(const Duration(seconds: 16));
+ });
+ expect(asyncTracker.isFinished, true);
+ expect(asyncTracker.startTime, startTime);
+ expect(asyncTracker.stopTime, time);
+ expect(asyncTracker.duration, const Duration(seconds: 31));
+ expect(asyncTracker.innerDuration, const Duration(seconds: 17));
+ expect(asyncTracker.slices.length, greaterThan(1));
+ expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
+ expect(nestedAsyncTracker.stopTime.isBefore(time), true);
+ expect(nestedAsyncTracker.duration, const Duration(seconds: 14));
+ expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 10));
+ expect(nestedAsyncTracker.slices.length, greaterThan(1));
+ expect(nestedAsyncTracker2.startTime.isAfter(startTime), true);
+ expect(nestedAsyncTracker2.stopTime.isBefore(time), true);
+ expect(nestedAsyncTracker2.duration, const Duration(seconds: 4));
+ expect(nestedAsyncTracker2.innerDuration, const Duration(seconds: 4));
+ expect(nestedAsyncTracker2.slices.length, 1);
+ });
+
+ test(
+ 'Can track all on grand-parent level and '
+ 'exclude grand-childrens from parent', () {
+ tracker = asyncTracker = AsyncTimeTracker(trackNested: true);
+ nestedAsyncTracker = AsyncTimeTracker(trackNested: false);
+ final nestedAsyncTracker2 = AsyncTimeTracker();
+ scopedTrack(() {
+ time = time.add(const Duration(seconds: 1));
+ nestedAsyncTracker.track(() {
+ time = time.add(const Duration(seconds: 2));
+ nestedAsyncTracker2.track(() {
+ time = time.add(const Duration(seconds: 4));
+ });
+ time = time.add(const Duration(seconds: 8));
+ });
+ time = time.add(const Duration(seconds: 16));
+ });
+ expect(asyncTracker.isFinished, true);
+ expect(asyncTracker.startTime, startTime);
+ expect(asyncTracker.stopTime, time);
+ expect(asyncTracker.duration, const Duration(seconds: 31));
+ expect(asyncTracker.innerDuration, const Duration(seconds: 31));
+ expect(asyncTracker.slices.length, 1);
+ expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
+ expect(nestedAsyncTracker.stopTime.isBefore(time), true);
+ expect(nestedAsyncTracker.duration, const Duration(seconds: 14));
+ expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 10));
+ expect(nestedAsyncTracker.slices.length, greaterThan(1));
+ expect(nestedAsyncTracker2.startTime.isAfter(startTime), true);
+ expect(nestedAsyncTracker2.stopTime.isBefore(time), true);
+ expect(nestedAsyncTracker2.duration, const Duration(seconds: 4));
+ expect(nestedAsyncTracker2.innerDuration, const Duration(seconds: 4));
+ expect(nestedAsyncTracker2.slices.length, 1);
+ });
+
+ test('Can exclude nested async', () async {
+ tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
+ await scopedTrack(() async {
+ time = time.add(const Duration(seconds: 1));
+ await nestedAsyncTracker.track(() async {
+ time = time.add(const Duration(seconds: 2));
+ await Future.value();
+ time = time.add(const Duration(seconds: 4));
+ await Future.value();
+ time = time.add(const Duration(seconds: 8));
+ });
+ time = time.add(const Duration(seconds: 16));
+ });
+ expect(asyncTracker.isFinished, true);
+ expect(asyncTracker.startTime, startTime);
+ expect(asyncTracker.stopTime, time);
+ expect(asyncTracker.duration, const Duration(seconds: 31));
+ expect(asyncTracker.innerDuration, const Duration(seconds: 17));
+ expect(asyncTracker.slices.length, greaterThan(1));
+ expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
+ expect(nestedAsyncTracker.stopTime.isBefore(time), true);
+ expect(nestedAsyncTracker.duration, const Duration(seconds: 14));
+ expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 14));
+ expect(nestedAsyncTracker.slices.length, greaterThan(1));
+ });
+
+ test('Can handle callbacks in excluded nested async', () async {
+ tracker = asyncTracker = AsyncTimeTracker(trackNested: false);
+ await scopedTrack(() async {
+ time = time.add(const Duration(seconds: 1));
+ final completer = Completer();
+ final future = completer.future.then((_) {
+ time = time.add(const Duration(seconds: 2));
+ });
+ await nestedAsyncTracker.track(() async {
+ time = time.add(const Duration(seconds: 4));
+ await Future.value();
+ time = time.add(const Duration(seconds: 8));
+ completer.complete();
+ await future;
+ time = time.add(const Duration(seconds: 16));
+ });
+ time = time.add(const Duration(seconds: 32));
+ });
+ expect(asyncTracker.isFinished, true);
+ expect(asyncTracker.startTime, startTime);
+ expect(asyncTracker.stopTime, time);
+ expect(asyncTracker.duration, const Duration(seconds: 63));
+ expect(asyncTracker.innerDuration, const Duration(seconds: 35));
+ expect(asyncTracker.slices.length, greaterThan(1));
+ expect(nestedAsyncTracker.startTime.isAfter(startTime), true);
+ expect(nestedAsyncTracker.stopTime.isBefore(time), true);
+ expect(nestedAsyncTracker.duration, const Duration(seconds: 30));
+ expect(nestedAsyncTracker.innerDuration, const Duration(seconds: 28));
+ expect(nestedAsyncTracker.slices.length, greaterThan(1));
+ });
+ });
+}
diff --git a/pkgs/watcher/.gitignore b/pkgs/watcher/.gitignore
new file mode 100644
index 0000000..ac98e87
--- /dev/null
+++ b/pkgs/watcher/.gitignore
@@ -0,0 +1,4 @@
+# Don’t commit the following directories created by pub.
+.dart_tool
+.packages
+pubspec.lock
diff --git a/pkgs/watcher/.test_config b/pkgs/watcher/.test_config
new file mode 100644
index 0000000..531426a
--- /dev/null
+++ b/pkgs/watcher/.test_config
@@ -0,0 +1,5 @@
+{
+ "test_package": {
+ "platforms": ["vm"]
+ }
+}
\ No newline at end of file
diff --git a/pkgs/watcher/CHANGELOG.md b/pkgs/watcher/CHANGELOG.md
new file mode 100644
index 0000000..ef3a7e2
--- /dev/null
+++ b/pkgs/watcher/CHANGELOG.md
@@ -0,0 +1,130 @@
+## 1.1.1
+
+- Ensure `PollingFileWatcher.ready` completes for files that do not exist.
+- Require Dart SDK `^3.1.0`
+- Move to `dart-lang/tools` monorepo.
+
+## 1.1.0
+
+- Require Dart SDK >= 3.0.0
+- Remove usage of redundant ConstructableFileSystemEvent classes.
+
+## 1.0.3-dev
+
+- Require Dart SDK >= 2.19
+
+## 1.0.2
+
+- Require Dart SDK >= 2.14
+- Ensure `DirectoryWatcher.ready` completes even when errors occur that close the watcher.
+- Add markdown badges to the readme.
+
+## 1.0.1
+
+* Drop package:pedantic and use package:lints instead.
+
+## 1.0.0
+
+* Require Dart SDK >= 2.12
+* Add the ability to create custom Watcher types for specific file paths.
+
+## 0.9.7+15
+
+* Fix a bug on Mac where modifying a directory with a path exactly matching a
+ prefix of a modified file would suppress change events for that file.
+
+## 0.9.7+14
+
+* Prepare for breaking change in SDK where modified times for not found files
+ becomes meaningless instead of null.
+
+## 0.9.7+13
+
+* Catch & forward `FileSystemException` from unexpectedly closed file watchers
+ on windows; the watcher will also be automatically restarted when this occurs.
+
+## 0.9.7+12
+
+* Catch `FileSystemException` during `existsSync()` on Windows.
+* Internal cleanup.
+
+## 0.9.7+11
+
+* Fix an analysis hint.
+
+## 0.9.7+10
+
+* Set max SDK version to `<3.0.0`, and adjust other dependencies.
+
+## 0.9.7+9
+
+* Internal changes only.
+
+## 0.9.7+8
+
+* Fix Dart 2.0 type issues on Mac and Windows.
+
+## 0.9.7+7
+
+* Updates to support Dart 2.0 core library changes (wave 2.2).
+ See [issue 31847][sdk#31847] for details.
+
+ [sdk#31847]: https://github.com/dart-lang/sdk/issues/31847
+
+
+## 0.9.7+6
+
+* Internal changes only, namely removing dep on scheduled test.
+
+## 0.9.7+5
+
+* Fix an analysis warning.
+
+## 0.9.7+4
+
+* Declare support for `async` 2.0.0.
+
+## 0.9.7+3
+
+* Fix a crashing bug on Linux.
+
+## 0.9.7+2
+
+* Narrow the constraint on `async` to reflect the APIs this package is actually
+ using.
+
+## 0.9.7+1
+
+* Fix all strong-mode warnings.
+
+## 0.9.7
+
+* Fix a bug in `FileWatcher` where events could be added after watchers were
+ closed.
+
+## 0.9.6
+
+* Add a `Watcher` interface that encompasses watching both files and
+ directories.
+
+* Add `FileWatcher` and `PollingFileWatcher` classes for watching changes to
+ individual files.
+
+* Deprecate `DirectoryWatcher.directory`. Use `DirectoryWatcher.path` instead.
+
+## 0.9.5
+
+* Fix bugs where events could be added after watchers were closed.
+
+## 0.9.4
+
+* Treat add events for known files as modifications instead of discarding them
+ on Mac OS.
+
+## 0.9.3
+
+* Improved support for Windows via `WindowsDirectoryWatcher`.
+
+* Simplified `PollingDirectoryWatcher`.
+
+* Fixed bugs in `MacOSDirectoryWatcher`
diff --git a/pkgs/watcher/LICENSE b/pkgs/watcher/LICENSE
new file mode 100644
index 0000000..000cd7b
--- /dev/null
+++ b/pkgs/watcher/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/watcher/README.md b/pkgs/watcher/README.md
new file mode 100644
index 0000000..83a0324
--- /dev/null
+++ b/pkgs/watcher/README.md
@@ -0,0 +1,10 @@
+[](https://github.com/dart-lang/tools/actions/workflows/watcher.yaml)
+[](https://pub.dev/packages/watcher)
+[](https://pub.dev/packages/watcher/publisher)
+
+A file system watcher.
+
+## What's this?
+
+`package:watcher` monitors changes to contents of directories and sends
+notifications when files have been added, removed, or modified.
diff --git a/pkgs/watcher/analysis_options.yaml b/pkgs/watcher/analysis_options.yaml
new file mode 100644
index 0000000..d978f81
--- /dev/null
+++ b/pkgs/watcher/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:dart_flutter_team_lints/analysis_options.yaml
diff --git a/pkgs/watcher/benchmark/path_set.dart b/pkgs/watcher/benchmark/path_set.dart
new file mode 100644
index 0000000..e7929d8
--- /dev/null
+++ b/pkgs/watcher/benchmark/path_set.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.
+
+/// Benchmarks for the PathSet class.
+library;
+
+import 'dart:io';
+import 'dart:math' as math;
+
+import 'package:benchmark_harness/benchmark_harness.dart';
+import 'package:path/path.dart' as p;
+import 'package:watcher/src/path_set.dart';
+
+final String root = Platform.isWindows ? r'C:\root' : '/root';
+
+/// Base class for benchmarks on [PathSet].
+abstract class PathSetBenchmark extends BenchmarkBase {
+ PathSetBenchmark(String method) : super('PathSet.$method');
+
+ final PathSet pathSet = PathSet(root);
+
+ /// Use a fixed [math.Random] with a constant seed to ensure the tests are
+ /// deterministic.
+ final math.Random random = math.Random(1234);
+
+ /// Walks over a virtual directory [depth] levels deep invoking [callback]
+ /// for each "file".
+ ///
+ /// Each virtual directory contains ten entries: either subdirectories or
+ /// files.
+ void walkTree(int depth, void Function(String) callback) {
+ void recurse(String path, int remainingDepth) {
+ for (var i = 0; i < 10; i++) {
+ var padded = i.toString().padLeft(2, '0');
+ if (remainingDepth == 0) {
+ callback(p.join(path, 'file_$padded.txt'));
+ } else {
+ var subdir = p.join(path, 'subdirectory_$padded');
+ recurse(subdir, remainingDepth - 1);
+ }
+ }
+ }
+
+ recurse(root, depth);
+ }
+}
+
+class AddBenchmark extends PathSetBenchmark {
+ AddBenchmark() : super('add()');
+
+ final List<String> paths = [];
+
+ @override
+ void setup() {
+ // Make a bunch of paths in about the same order we expect to get them from
+ // Directory.list().
+ walkTree(3, paths.add);
+ }
+
+ @override
+ void run() {
+ for (var path in paths) {
+ pathSet.add(path);
+ }
+ }
+}
+
+class ContainsBenchmark extends PathSetBenchmark {
+ ContainsBenchmark() : super('contains()');
+
+ final List<String> paths = [];
+
+ @override
+ void setup() {
+ // Add a bunch of paths to the set.
+ walkTree(3, (path) {
+ pathSet.add(path);
+ paths.add(path);
+ });
+
+ // Add some non-existent paths to test the false case.
+ for (var i = 0; i < 100; i++) {
+ paths.addAll([
+ '/nope',
+ '/root/nope',
+ '/root/subdirectory_04/nope',
+ '/root/subdirectory_04/subdirectory_04/nope',
+ '/root/subdirectory_04/subdirectory_04/subdirectory_04/nope',
+ '/root/subdirectory_04/subdirectory_04/subdirectory_04/nope/file_04.txt',
+ ]);
+ }
+ }
+
+ @override
+ void run() {
+ var contained = 0;
+ for (var path in paths) {
+ if (pathSet.contains(path)) contained++;
+ }
+
+ if (contained != 10000) throw StateError('Wrong result: $contained');
+ }
+}
+
+class PathsBenchmark extends PathSetBenchmark {
+ PathsBenchmark() : super('toSet()');
+
+ @override
+ void setup() {
+ walkTree(3, pathSet.add);
+ }
+
+ @override
+ void run() {
+ var count = 0;
+ for (var _ in pathSet.paths) {
+ count++;
+ }
+
+ if (count != 10000) throw StateError('Wrong result: $count');
+ }
+}
+
+class RemoveBenchmark extends PathSetBenchmark {
+ RemoveBenchmark() : super('remove()');
+
+ final List<String> paths = [];
+
+ @override
+ void setup() {
+ // Make a bunch of paths. Do this here so that we don't spend benchmarked
+ // time synthesizing paths.
+ walkTree(3, (path) {
+ pathSet.add(path);
+ paths.add(path);
+ });
+
+ // Shuffle the paths so that we delete them in a random order that
+ // hopefully mimics real-world file system usage. Do the shuffling here so
+ // that we don't spend benchmarked time shuffling.
+ paths.shuffle(random);
+ }
+
+ @override
+ void run() {
+ for (var path in paths) {
+ pathSet.remove(path);
+ }
+ }
+}
+
+void main() {
+ AddBenchmark().report();
+ ContainsBenchmark().report();
+ PathsBenchmark().report();
+ RemoveBenchmark().report();
+}
diff --git a/pkgs/watcher/example/watch.dart b/pkgs/watcher/example/watch.dart
new file mode 100644
index 0000000..37931d3
--- /dev/null
+++ b/pkgs/watcher/example/watch.dart
@@ -0,0 +1,19 @@
+// 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.
+
+/// Watches the given directory and prints each modification to it.
+library;
+
+import 'package:path/path.dart' as p;
+import 'package:watcher/watcher.dart';
+
+void main(List<String> arguments) {
+ if (arguments.length != 1) {
+ print('Usage: watch <directory path>');
+ return;
+ }
+
+ var watcher = DirectoryWatcher(p.absolute(arguments[0]));
+ watcher.events.listen(print);
+}
diff --git a/pkgs/watcher/lib/src/async_queue.dart b/pkgs/watcher/lib/src/async_queue.dart
new file mode 100644
index 0000000..f6c76a9
--- /dev/null
+++ b/pkgs/watcher/lib/src/async_queue.dart
@@ -0,0 +1,70 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:collection';
+
+typedef ItemProcessor<T> = Future<void> Function(T item);
+
+/// A queue of items that are sequentially, asynchronously processed.
+///
+/// Unlike [Stream.map] or [Stream.forEach], the callback used to process each
+/// item returns a [Future], and it will not advance to the next item until the
+/// current item is finished processing.
+///
+/// Items can be added at any point in time and processing will be started as
+/// needed. When all items are processed, it stops processing until more items
+/// are added.
+class AsyncQueue<T> {
+ final _items = Queue<T>();
+
+ /// Whether or not the queue is currently waiting on a processing future to
+ /// complete.
+ bool _isProcessing = false;
+
+ /// The callback to invoke on each queued item.
+ ///
+ /// The next item in the queue will not be processed until the [Future]
+ /// returned by this completes.
+ final ItemProcessor<T> _processor;
+
+ /// The handler for errors thrown during processing.
+ ///
+ /// Used to avoid top-leveling asynchronous errors.
+ final void Function(Object, StackTrace) _errorHandler;
+
+ AsyncQueue(this._processor,
+ {required void Function(Object, StackTrace) onError})
+ : _errorHandler = onError;
+
+ /// Enqueues [item] to be processed and starts asynchronously processing it
+ /// if a process isn't already running.
+ void add(T item) {
+ _items.add(item);
+
+ // Start up the asynchronous processing if not already running.
+ if (_isProcessing) return;
+ _isProcessing = true;
+
+ _processNextItem().catchError(_errorHandler);
+ }
+
+ /// Removes all remaining items to be processed.
+ void clear() {
+ _items.clear();
+ }
+
+ /// Pulls the next item off [_items] and processes it.
+ ///
+ /// When complete, recursively calls itself to continue processing unless
+ /// the process was cancelled.
+ Future<void> _processNextItem() async {
+ var item = _items.removeFirst();
+ await _processor(item);
+ if (_items.isNotEmpty) return _processNextItem();
+
+ // We have drained the queue, stop processing and wait until something
+ // has been enqueued.
+ _isProcessing = false;
+ }
+}
diff --git a/pkgs/watcher/lib/src/custom_watcher_factory.dart b/pkgs/watcher/lib/src/custom_watcher_factory.dart
new file mode 100644
index 0000000..fc4e3fb
--- /dev/null
+++ b/pkgs/watcher/lib/src/custom_watcher_factory.dart
@@ -0,0 +1,88 @@
+// 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 '../watcher.dart';
+
+/// A factory to produce custom watchers for specific file paths.
+class _CustomWatcherFactory {
+ final String id;
+ final DirectoryWatcher? Function(String path, {Duration? pollingDelay})
+ createDirectoryWatcher;
+ final FileWatcher? Function(String path, {Duration? pollingDelay})
+ createFileWatcher;
+
+ _CustomWatcherFactory(
+ this.id, this.createDirectoryWatcher, this.createFileWatcher);
+}
+
+/// Registers a custom watcher.
+///
+/// Each custom watcher must have a unique [id] and the same watcher may not be
+/// registered more than once.
+/// [createDirectoryWatcher] and [createFileWatcher] should return watchers for
+/// the file paths they are able to handle. If the custom watcher is not able to
+/// handle the path it should return null.
+/// The paths handled by each custom watch may not overlap, at most one custom
+/// matcher may return a non-null watcher for a given path.
+///
+/// When a file or directory watcher is created the path is checked against each
+/// registered custom watcher, and if exactly one custom watcher is available it
+/// will be used instead of the default.
+void registerCustomWatcher(
+ String id,
+ DirectoryWatcher? Function(String path, {Duration? pollingDelay})?
+ createDirectoryWatcher,
+ FileWatcher? Function(String path, {Duration? pollingDelay})?
+ createFileWatcher,
+) {
+ if (_customWatcherFactories.containsKey(id)) {
+ throw ArgumentError('A custom watcher with id `$id` '
+ 'has already been registered');
+ }
+ _customWatcherFactories[id] = _CustomWatcherFactory(
+ id,
+ createDirectoryWatcher ?? (_, {pollingDelay}) => null,
+ createFileWatcher ?? (_, {pollingDelay}) => null);
+}
+
+/// Tries to create a custom [DirectoryWatcher] and returns it.
+///
+/// Returns `null` if no custom watcher was applicable and throws a [StateError]
+/// if more than one was.
+DirectoryWatcher? createCustomDirectoryWatcher(String path,
+ {Duration? pollingDelay}) {
+ DirectoryWatcher? customWatcher;
+ String? customFactoryId;
+ for (var watcherFactory in _customWatcherFactories.values) {
+ if (customWatcher != null) {
+ throw StateError('Two `CustomWatcherFactory`s applicable: '
+ '`$customFactoryId` and `${watcherFactory.id}` for `$path`');
+ }
+ customWatcher =
+ watcherFactory.createDirectoryWatcher(path, pollingDelay: pollingDelay);
+ customFactoryId = watcherFactory.id;
+ }
+ return customWatcher;
+}
+
+/// Tries to create a custom [FileWatcher] and returns it.
+///
+/// Returns `null` if no custom watcher was applicable and throws a [StateError]
+/// if more than one was.
+FileWatcher? createCustomFileWatcher(String path, {Duration? pollingDelay}) {
+ FileWatcher? customWatcher;
+ String? customFactoryId;
+ for (var watcherFactory in _customWatcherFactories.values) {
+ if (customWatcher != null) {
+ throw StateError('Two `CustomWatcherFactory`s applicable: '
+ '`$customFactoryId` and `${watcherFactory.id}` for `$path`');
+ }
+ customWatcher =
+ watcherFactory.createFileWatcher(path, pollingDelay: pollingDelay);
+ customFactoryId = watcherFactory.id;
+ }
+ return customWatcher;
+}
+
+final _customWatcherFactories = <String, _CustomWatcherFactory>{};
diff --git a/pkgs/watcher/lib/src/directory_watcher.dart b/pkgs/watcher/lib/src/directory_watcher.dart
new file mode 100644
index 0000000..158b86b
--- /dev/null
+++ b/pkgs/watcher/lib/src/directory_watcher.dart
@@ -0,0 +1,41 @@
+// 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:io';
+
+import '../watcher.dart';
+import 'custom_watcher_factory.dart';
+import 'directory_watcher/linux.dart';
+import 'directory_watcher/mac_os.dart';
+import 'directory_watcher/windows.dart';
+
+/// Watches the contents of a directory and emits [WatchEvent]s when something
+/// in the directory has changed.
+abstract class DirectoryWatcher implements Watcher {
+ /// The directory whose contents are being monitored.
+ @Deprecated('Expires in 1.0.0. Use DirectoryWatcher.path instead.')
+ String get directory;
+
+ /// Creates a new [DirectoryWatcher] monitoring [directory].
+ ///
+ /// If a native directory watcher is available for this platform, this will
+ /// use it. Otherwise, it will fall back to a [PollingDirectoryWatcher].
+ ///
+ /// If [pollingDelay] is passed, it specifies the amount of time the watcher
+ /// will pause between successive polls of the directory contents. Making this
+ /// shorter will give more immediate feedback at the expense of doing more IO
+ /// and higher CPU usage. Defaults to one second. Ignored for non-polling
+ /// watchers.
+ factory DirectoryWatcher(String directory, {Duration? pollingDelay}) {
+ if (FileSystemEntity.isWatchSupported) {
+ var customWatcher =
+ createCustomDirectoryWatcher(directory, pollingDelay: pollingDelay);
+ if (customWatcher != null) return customWatcher;
+ if (Platform.isLinux) return LinuxDirectoryWatcher(directory);
+ if (Platform.isMacOS) return MacOSDirectoryWatcher(directory);
+ if (Platform.isWindows) return WindowsDirectoryWatcher(directory);
+ }
+ return PollingDirectoryWatcher(directory, pollingDelay: pollingDelay);
+ }
+}
diff --git a/pkgs/watcher/lib/src/directory_watcher/linux.dart b/pkgs/watcher/lib/src/directory_watcher/linux.dart
new file mode 100644
index 0000000..cb1d077
--- /dev/null
+++ b/pkgs/watcher/lib/src/directory_watcher/linux.dart
@@ -0,0 +1,294 @@
+// 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:async';
+import 'dart:io';
+
+import 'package:async/async.dart';
+
+import '../directory_watcher.dart';
+import '../path_set.dart';
+import '../resubscribable.dart';
+import '../utils.dart';
+import '../watch_event.dart';
+
+/// Uses the inotify subsystem to watch for filesystem events.
+///
+/// Inotify doesn't suport recursively watching subdirectories, nor does
+/// [Directory.watch] polyfill that functionality. This class polyfills it
+/// instead.
+///
+/// This class also compensates for the non-inotify-specific issues of
+/// [Directory.watch] producing multiple events for a single logical action
+/// (issue 14372) and providing insufficient information about move events
+/// (issue 14424).
+class LinuxDirectoryWatcher extends ResubscribableWatcher
+ implements DirectoryWatcher {
+ @override
+ String get directory => path;
+
+ LinuxDirectoryWatcher(String directory)
+ : super(directory, () => _LinuxDirectoryWatcher(directory));
+}
+
+class _LinuxDirectoryWatcher
+ implements DirectoryWatcher, ManuallyClosedWatcher {
+ @override
+ String get directory => _files.root;
+ @override
+ String get path => _files.root;
+
+ @override
+ Stream<WatchEvent> get events => _eventsController.stream;
+ final _eventsController = StreamController<WatchEvent>.broadcast();
+
+ @override
+ bool get isReady => _readyCompleter.isCompleted;
+
+ @override
+ Future<void> get ready => _readyCompleter.future;
+ final _readyCompleter = Completer<void>();
+
+ /// A stream group for the [Directory.watch] events of [path] and all its
+ /// subdirectories.
+ final _nativeEvents = StreamGroup<FileSystemEvent>();
+
+ /// All known files recursively within [path].
+ final PathSet _files;
+
+ /// [Directory.watch] streams for [path]'s subdirectories, indexed by name.
+ ///
+ /// A stream is in this map if and only if it's also in [_nativeEvents].
+ final _subdirStreams = <String, Stream<FileSystemEvent>>{};
+
+ /// A set of all subscriptions that this watcher subscribes to.
+ ///
+ /// These are gathered together so that they may all be canceled when the
+ /// watcher is closed.
+ final _subscriptions = <StreamSubscription>{};
+
+ _LinuxDirectoryWatcher(String path) : _files = PathSet(path) {
+ _nativeEvents.add(Directory(path)
+ .watch()
+ .transform(StreamTransformer.fromHandlers(handleDone: (sink) {
+ // Handle the done event here rather than in the call to [_listen] because
+ // [innerStream] won't close until we close the [StreamGroup]. However, if
+ // we close the [StreamGroup] here, we run the risk of new-directory
+ // events being fired after the group is closed, since batching delays
+ // those events. See b/30768513.
+ _onDone();
+ })));
+
+ // Batch the inotify changes together so that we can dedup events.
+ var innerStream = _nativeEvents.stream.batchEvents();
+ _listen(innerStream, _onBatch,
+ onError: (Object error, StackTrace stackTrace) {
+ // Guarantee that ready always completes.
+ if (!isReady) {
+ _readyCompleter.complete();
+ }
+ _eventsController.addError(error, stackTrace);
+ });
+
+ _listen(
+ Directory(path).list(recursive: true),
+ (FileSystemEntity entity) {
+ if (entity is Directory) {
+ _watchSubdir(entity.path);
+ } else {
+ _files.add(entity.path);
+ }
+ },
+ onError: _emitError,
+ onDone: () {
+ if (!isReady) {
+ _readyCompleter.complete();
+ }
+ },
+ cancelOnError: true,
+ );
+ }
+
+ @override
+ void close() {
+ for (var subscription in _subscriptions) {
+ subscription.cancel();
+ }
+
+ _subscriptions.clear();
+ _subdirStreams.clear();
+ _files.clear();
+ _nativeEvents.close();
+ _eventsController.close();
+ }
+
+ /// Watch a subdirectory of [directory] for changes.
+ void _watchSubdir(String path) {
+ // TODO(nweiz): Right now it's possible for the watcher to emit an event for
+ // a file before the directory list is complete. This could lead to the user
+ // seeing a MODIFY or REMOVE event for a file before they see an ADD event,
+ // which is bad. We should handle that.
+ //
+ // One possibility is to provide a general means (e.g.
+ // `DirectoryWatcher.eventsAndExistingFiles`) to tell a watcher to emit
+ // events for all the files that already exist. This would be useful for
+ // top-level clients such as barback as well, and could be implemented with
+ // a wrapper similar to how listening/canceling works now.
+
+ // TODO(nweiz): Catch any errors here that indicate that the directory in
+ // question doesn't exist and silently stop watching it instead of
+ // propagating the errors.
+ var stream = Directory(path).watch();
+ _subdirStreams[path] = stream;
+ _nativeEvents.add(stream);
+ }
+
+ /// The callback that's run when a batch of changes comes in.
+ void _onBatch(List<FileSystemEvent> batch) {
+ var files = <String>{};
+ var dirs = <String>{};
+ var changed = <String>{};
+
+ // inotify event batches are ordered by occurrence, so we treat them as a
+ // log of what happened to a file. We only emit events based on the
+ // difference between the state before the batch and the state after it, not
+ // the intermediate state.
+ for (var event in batch) {
+ // If the watched directory is deleted or moved, we'll get a deletion
+ // event for it. Ignore it; we handle closing [this] when the underlying
+ // stream is closed.
+ if (event.path == path) continue;
+
+ changed.add(event.path);
+
+ if (event is FileSystemMoveEvent) {
+ files.remove(event.path);
+ dirs.remove(event.path);
+
+ var destination = event.destination;
+ if (destination == null) continue;
+
+ changed.add(destination);
+ if (event.isDirectory) {
+ files.remove(destination);
+ dirs.add(destination);
+ } else {
+ files.add(destination);
+ dirs.remove(destination);
+ }
+ } else if (event is FileSystemDeleteEvent) {
+ files.remove(event.path);
+ dirs.remove(event.path);
+ } else if (event.isDirectory) {
+ files.remove(event.path);
+ dirs.add(event.path);
+ } else {
+ files.add(event.path);
+ dirs.remove(event.path);
+ }
+ }
+
+ _applyChanges(files, dirs, changed);
+ }
+
+ /// Applies the net changes computed for a batch.
+ ///
+ /// The [files] and [dirs] sets contain the files and directories that now
+ /// exist, respectively. The [changed] set contains all files and directories
+ /// that have changed (including being removed), and so is a superset of
+ /// [files] and [dirs].
+ void _applyChanges(Set<String> files, Set<String> dirs, Set<String> changed) {
+ for (var path in changed) {
+ var stream = _subdirStreams.remove(path);
+ if (stream != null) _nativeEvents.add(stream);
+
+ // Unless [path] was a file and still is, emit REMOVE events for it or its
+ // contents,
+ if (files.contains(path) && _files.contains(path)) continue;
+ for (var file in _files.remove(path)) {
+ _emitEvent(ChangeType.REMOVE, file);
+ }
+ }
+
+ for (var file in files) {
+ if (_files.contains(file)) {
+ _emitEvent(ChangeType.MODIFY, file);
+ } else {
+ _emitEvent(ChangeType.ADD, file);
+ _files.add(file);
+ }
+ }
+
+ for (var dir in dirs) {
+ _watchSubdir(dir);
+ _addSubdir(dir);
+ }
+ }
+
+ /// Emits [ChangeType.ADD] events for the recursive contents of [path].
+ void _addSubdir(String path) {
+ _listen(Directory(path).list(recursive: true), (FileSystemEntity entity) {
+ if (entity is Directory) {
+ _watchSubdir(entity.path);
+ } else {
+ _files.add(entity.path);
+ _emitEvent(ChangeType.ADD, entity.path);
+ }
+ }, onError: (Object error, StackTrace stackTrace) {
+ // Ignore an exception caused by the dir not existing. It's fine if it
+ // was added and then quickly removed.
+ if (error is FileSystemException) return;
+
+ _emitError(error, stackTrace);
+ }, cancelOnError: true);
+ }
+
+ /// Handles the underlying event stream closing, indicating that the directory
+ /// being watched was removed.
+ void _onDone() {
+ // Most of the time when a directory is removed, its contents will get
+ // individual REMOVE events before the watch stream is closed -- in that
+ // case, [_files] will be empty here. However, if the directory's removal is
+ // caused by a MOVE, we need to manually emit events.
+ if (isReady) {
+ for (var file in _files.paths) {
+ _emitEvent(ChangeType.REMOVE, file);
+ }
+ }
+
+ close();
+ }
+
+ /// Emits a [WatchEvent] with [type] and [path] if this watcher is in a state
+ /// to emit events.
+ void _emitEvent(ChangeType type, String path) {
+ if (!isReady) return;
+ if (_eventsController.isClosed) return;
+ _eventsController.add(WatchEvent(type, path));
+ }
+
+ /// Emit an error, then close the watcher.
+ void _emitError(Object error, StackTrace stackTrace) {
+ // Guarantee that ready always completes.
+ if (!isReady) {
+ _readyCompleter.complete();
+ }
+ _eventsController.addError(error, stackTrace);
+ close();
+ }
+
+ /// Like [Stream.listen], but automatically adds the subscription to
+ /// [_subscriptions] so that it can be canceled when [close] is called.
+ void _listen<T>(Stream<T> stream, void Function(T) onData,
+ {Function? onError,
+ void Function()? onDone,
+ bool cancelOnError = false}) {
+ late StreamSubscription<T> subscription;
+ subscription = stream.listen(onData, onError: onError, onDone: () {
+ _subscriptions.remove(subscription);
+ onDone?.call();
+ }, cancelOnError: cancelOnError);
+ _subscriptions.add(subscription);
+ }
+}
diff --git a/pkgs/watcher/lib/src/directory_watcher/mac_os.dart b/pkgs/watcher/lib/src/directory_watcher/mac_os.dart
new file mode 100644
index 0000000..b461383
--- /dev/null
+++ b/pkgs/watcher/lib/src/directory_watcher/mac_os.dart
@@ -0,0 +1,410 @@
+// 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:async';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+
+import '../directory_watcher.dart';
+import '../path_set.dart';
+import '../resubscribable.dart';
+import '../utils.dart';
+import '../watch_event.dart';
+
+/// Uses the FSEvents subsystem to watch for filesystem events.
+///
+/// FSEvents has two main idiosyncrasies that this class works around. First, it
+/// will occasionally report events that occurred before the filesystem watch
+/// was initiated. Second, if multiple events happen to the same file in close
+/// succession, it won't report them in the order they occurred. See issue
+/// 14373.
+///
+/// This also works around issues 16003 and 14849 in the implementation of
+/// [Directory.watch].
+class MacOSDirectoryWatcher extends ResubscribableWatcher
+ implements DirectoryWatcher {
+ @override
+ String get directory => path;
+
+ MacOSDirectoryWatcher(String directory)
+ : super(directory, () => _MacOSDirectoryWatcher(directory));
+}
+
+class _MacOSDirectoryWatcher
+ implements DirectoryWatcher, ManuallyClosedWatcher {
+ @override
+ String get directory => path;
+ @override
+ final String path;
+
+ @override
+ Stream<WatchEvent> get events => _eventsController.stream;
+ final _eventsController = StreamController<WatchEvent>.broadcast();
+
+ @override
+ bool get isReady => _readyCompleter.isCompleted;
+
+ @override
+ Future<void> get ready => _readyCompleter.future;
+ final _readyCompleter = Completer<void>();
+
+ /// The set of files that are known to exist recursively within the watched
+ /// directory.
+ ///
+ /// The state of files on the filesystem is compared against this to determine
+ /// the real change that occurred when working around issue 14373. This is
+ /// also used to emit REMOVE events when subdirectories are moved out of the
+ /// watched directory.
+ final PathSet _files;
+
+ /// The subscription to the stream returned by [Directory.watch].
+ ///
+ /// This is separate from [_listSubscriptions] because this stream
+ /// occasionally needs to be resubscribed in order to work around issue 14849.
+ StreamSubscription<List<FileSystemEvent>>? _watchSubscription;
+
+ /// The subscription to the [Directory.list] call for the initial listing of
+ /// the directory to determine its initial state.
+ StreamSubscription<FileSystemEntity>? _initialListSubscription;
+
+ /// The subscriptions to [Directory.list] calls for listing the contents of a
+ /// subdirectory that was moved into the watched directory.
+ final _listSubscriptions = <StreamSubscription<FileSystemEntity>>{};
+
+ /// The timer for tracking how long we wait for an initial batch of bogus
+ /// events (see issue 14373).
+ late Timer _bogusEventTimer;
+
+ _MacOSDirectoryWatcher(this.path) : _files = PathSet(path) {
+ _startWatch();
+
+ // Before we're ready to emit events, wait for [_listDir] to complete and
+ // for enough time to elapse that if bogus events (issue 14373) would be
+ // emitted, they will be.
+ //
+ // If we do receive a batch of events, [_onBatch] will ensure that these
+ // futures don't fire and that the directory is re-listed.
+ Future.wait([_listDir(), _waitForBogusEvents()]).then((_) {
+ if (!isReady) {
+ _readyCompleter.complete();
+ }
+ });
+ }
+
+ @override
+ void close() {
+ _watchSubscription?.cancel();
+ _initialListSubscription?.cancel();
+ _watchSubscription = null;
+ _initialListSubscription = null;
+
+ for (var subscription in _listSubscriptions) {
+ subscription.cancel();
+ }
+ _listSubscriptions.clear();
+
+ _eventsController.close();
+ }
+
+ /// The callback that's run when [Directory.watch] emits a batch of events.
+ void _onBatch(List<FileSystemEvent> batch) {
+ // If we get a batch of events before we're ready to begin emitting events,
+ // it's probable that it's a batch of pre-watcher events (see issue 14373).
+ // Ignore those events and re-list the directory.
+ if (!isReady) {
+ // Cancel the timer because bogus events only occur in the first batch, so
+ // we can fire [ready] as soon as we're done listing the directory.
+ _bogusEventTimer.cancel();
+ _listDir().then((_) {
+ if (!isReady) {
+ _readyCompleter.complete();
+ }
+ });
+ return;
+ }
+
+ _sortEvents(batch).forEach((path, eventSet) {
+ var canonicalEvent = _canonicalEvent(eventSet);
+ var events = canonicalEvent == null
+ ? _eventsBasedOnFileSystem(path)
+ : [canonicalEvent];
+
+ for (var event in events) {
+ if (event is FileSystemCreateEvent) {
+ if (!event.isDirectory) {
+ // If we already know about the file, treat it like a modification.
+ // This can happen if a file is copied on top of an existing one.
+ // We'll see an ADD event for the latter file when from the user's
+ // perspective, the file's contents just changed.
+ var type =
+ _files.contains(path) ? ChangeType.MODIFY : ChangeType.ADD;
+
+ _emitEvent(type, path);
+ _files.add(path);
+ continue;
+ }
+
+ if (_files.containsDir(path)) continue;
+
+ var stream = Directory(path).list(recursive: true);
+ var subscription = stream.listen((entity) {
+ if (entity is Directory) return;
+ if (_files.contains(path)) return;
+
+ _emitEvent(ChangeType.ADD, entity.path);
+ _files.add(entity.path);
+ }, cancelOnError: true);
+ subscription.onDone(() {
+ _listSubscriptions.remove(subscription);
+ });
+ subscription.onError(_emitError);
+ _listSubscriptions.add(subscription);
+ } else if (event is FileSystemModifyEvent) {
+ assert(!event.isDirectory);
+ _emitEvent(ChangeType.MODIFY, path);
+ } else {
+ assert(event is FileSystemDeleteEvent);
+ for (var removedPath in _files.remove(path)) {
+ _emitEvent(ChangeType.REMOVE, removedPath);
+ }
+ }
+ }
+ });
+ }
+
+ /// Sort all the events in a batch into sets based on their path.
+ ///
+ /// A single input event may result in multiple events in the returned map;
+ /// for example, a MOVE event becomes a DELETE event for the source and a
+ /// CREATE event for the destination.
+ ///
+ /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
+ /// contain any events relating to [path].
+ Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
+ var eventsForPaths = <String, Set<FileSystemEvent>>{};
+
+ // FSEvents can report past events, including events on the root directory
+ // such as it being created. We want to ignore these. If the directory is
+ // really deleted, that's handled by [_onDone].
+ batch = batch.where((event) => event.path != path).toList();
+
+ // Events within directories that already have events are superfluous; the
+ // directory's full contents will be examined anyway, so we ignore such
+ // events. Emitting them could cause useless or out-of-order events.
+ var directories = unionAll(batch.map((event) {
+ if (!event.isDirectory) return <String>{};
+ if (event is FileSystemMoveEvent) {
+ var destination = event.destination;
+ if (destination != null) {
+ return {event.path, destination};
+ }
+ }
+ return {event.path};
+ }));
+
+ bool isInModifiedDirectory(String path) =>
+ directories.any((dir) => path != dir && p.isWithin(dir, path));
+
+ void addEvent(String path, FileSystemEvent event) {
+ if (isInModifiedDirectory(path)) return;
+ eventsForPaths.putIfAbsent(path, () => <FileSystemEvent>{}).add(event);
+ }
+
+ for (var event in batch) {
+ // The Mac OS watcher doesn't emit move events. See issue 14806.
+ assert(event is! FileSystemMoveEvent);
+ addEvent(event.path, event);
+ }
+
+ return eventsForPaths;
+ }
+
+ /// Returns the canonical event from a batch of events on the same path, if
+ /// one exists.
+ ///
+ /// If [batch] doesn't contain any contradictory events (e.g. DELETE and
+ /// CREATE, or events with different values for `isDirectory`), this returns a
+ /// single event that describes what happened to the path in question.
+ ///
+ /// If [batch] does contain contradictory events, this returns `null` to
+ /// indicate that the state of the path on the filesystem should be checked to
+ /// determine what occurred.
+ FileSystemEvent? _canonicalEvent(Set<FileSystemEvent> batch) {
+ // An empty batch indicates that we've learned earlier that the batch is
+ // contradictory (e.g. because of a move).
+ if (batch.isEmpty) return null;
+
+ var type = batch.first.type;
+ var isDir = batch.first.isDirectory;
+ var hadModifyEvent = false;
+
+ for (var event in batch.skip(1)) {
+ // If one event reports that the file is a directory and another event
+ // doesn't, that's a contradiction.
+ if (isDir != event.isDirectory) return null;
+
+ // Modify events don't contradict either CREATE or REMOVE events. We can
+ // safely assume the file was modified after a CREATE or before the
+ // REMOVE; otherwise there will also be a REMOVE or CREATE event
+ // (respectively) that will be contradictory.
+ if (event is FileSystemModifyEvent) {
+ hadModifyEvent = true;
+ continue;
+ }
+ assert(event is FileSystemCreateEvent || event is FileSystemDeleteEvent);
+
+ // If we previously thought this was a MODIFY, we now consider it to be a
+ // CREATE or REMOVE event. This is safe for the same reason as above.
+ if (type == FileSystemEvent.modify) {
+ type = event.type;
+ continue;
+ }
+
+ // A CREATE event contradicts a REMOVE event and vice versa.
+ assert(type == FileSystemEvent.create || type == FileSystemEvent.delete);
+ if (type != event.type) return null;
+ }
+
+ // If we got a CREATE event for a file we already knew about, that comes
+ // from FSEvents reporting an add that happened prior to the watch
+ // beginning. If we also received a MODIFY event, we want to report that,
+ // but not the CREATE.
+ if (type == FileSystemEvent.create &&
+ hadModifyEvent &&
+ _files.contains(batch.first.path)) {
+ type = FileSystemEvent.modify;
+ }
+
+ switch (type) {
+ case FileSystemEvent.create:
+ // Issue 16003 means that a CREATE event for a directory can indicate
+ // that the directory was moved and then re-created.
+ // [_eventsBasedOnFileSystem] will handle this correctly by producing a
+ // DELETE event followed by a CREATE event if the directory exists.
+ if (isDir) return null;
+ return FileSystemCreateEvent(batch.first.path, false);
+ case FileSystemEvent.delete:
+ return FileSystemDeleteEvent(batch.first.path, isDir);
+ case FileSystemEvent.modify:
+ return FileSystemModifyEvent(batch.first.path, isDir, false);
+ default:
+ throw StateError('unreachable');
+ }
+ }
+
+ /// Returns one or more events that describe the change between the last known
+ /// state of [path] and its current state on the filesystem.
+ ///
+ /// This returns a list whose order should be reflected in the events emitted
+ /// to the user, unlike the batched events from [Directory.watch]. The
+ /// returned list may be empty, indicating that no changes occurred to [path]
+ /// (probably indicating that it was created and then immediately deleted).
+ List<FileSystemEvent> _eventsBasedOnFileSystem(String path) {
+ var fileExisted = _files.contains(path);
+ var dirExisted = _files.containsDir(path);
+ var fileExists = File(path).existsSync();
+ var dirExists = Directory(path).existsSync();
+
+ var events = <FileSystemEvent>[];
+ if (fileExisted) {
+ if (fileExists) {
+ events.add(FileSystemModifyEvent(path, false, false));
+ } else {
+ events.add(FileSystemDeleteEvent(path, false));
+ }
+ } else if (dirExisted) {
+ if (dirExists) {
+ // If we got contradictory events for a directory that used to exist and
+ // still exists, we need to rescan the whole thing in case it was
+ // replaced with a different directory.
+ events.add(FileSystemDeleteEvent(path, true));
+ events.add(FileSystemCreateEvent(path, true));
+ } else {
+ events.add(FileSystemDeleteEvent(path, true));
+ }
+ }
+
+ if (!fileExisted && fileExists) {
+ events.add(FileSystemCreateEvent(path, false));
+ } else if (!dirExisted && dirExists) {
+ events.add(FileSystemCreateEvent(path, true));
+ }
+
+ return events;
+ }
+
+ /// The callback that's run when the [Directory.watch] stream is closed.
+ void _onDone() {
+ _watchSubscription = null;
+
+ // If the directory still exists and we're still expecting bogus events,
+ // this is probably issue 14849 rather than a real close event. We should
+ // just restart the watcher.
+ if (!isReady && Directory(path).existsSync()) {
+ _startWatch();
+ return;
+ }
+
+ // FSEvents can fail to report the contents of the directory being removed
+ // when the directory itself is removed, so we need to manually mark the
+ // files as removed.
+ for (var file in _files.paths) {
+ _emitEvent(ChangeType.REMOVE, file);
+ }
+ _files.clear();
+ close();
+ }
+
+ /// Start or restart the underlying [Directory.watch] stream.
+ void _startWatch() {
+ // Batch the FSEvent changes together so that we can dedup events.
+ var innerStream = Directory(path).watch(recursive: true).batchEvents();
+ _watchSubscription = innerStream.listen(_onBatch,
+ onError: _eventsController.addError, onDone: _onDone);
+ }
+
+ /// Starts or restarts listing the watched directory to get an initial picture
+ /// of its state.
+ Future<void> _listDir() {
+ assert(!isReady);
+ _initialListSubscription?.cancel();
+
+ _files.clear();
+ var completer = Completer<void>();
+ var stream = Directory(path).list(recursive: true);
+ _initialListSubscription = stream.listen((entity) {
+ if (entity is! Directory) _files.add(entity.path);
+ }, onError: _emitError, onDone: completer.complete, cancelOnError: true);
+ return completer.future;
+ }
+
+ /// Wait 200ms for a batch of bogus events (issue 14373) to come in.
+ ///
+ /// 200ms is short in terms of human interaction, but longer than any Mac OS
+ /// watcher tests take on the bots, so it should be safe to assume that any
+ /// bogus events will be signaled in that time frame.
+ Future<void> _waitForBogusEvents() {
+ var completer = Completer<void>();
+ _bogusEventTimer =
+ Timer(const Duration(milliseconds: 200), completer.complete);
+ return completer.future;
+ }
+
+ /// Emit an event with the given [type] and [path].
+ void _emitEvent(ChangeType type, String path) {
+ if (!isReady) return;
+ _eventsController.add(WatchEvent(type, path));
+ }
+
+ /// Emit an error, then close the watcher.
+ void _emitError(Object error, StackTrace stackTrace) {
+ // Guarantee that ready always completes.
+ if (!isReady) {
+ _readyCompleter.complete();
+ }
+ _eventsController.addError(error, stackTrace);
+ close();
+ }
+}
diff --git a/pkgs/watcher/lib/src/directory_watcher/polling.dart b/pkgs/watcher/lib/src/directory_watcher/polling.dart
new file mode 100644
index 0000000..207679b
--- /dev/null
+++ b/pkgs/watcher/lib/src/directory_watcher/polling.dart
@@ -0,0 +1,191 @@
+// 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:async';
+import 'dart:io';
+
+import '../async_queue.dart';
+import '../directory_watcher.dart';
+import '../resubscribable.dart';
+import '../stat.dart';
+import '../utils.dart';
+import '../watch_event.dart';
+
+/// Periodically polls a directory for changes.
+class PollingDirectoryWatcher extends ResubscribableWatcher
+ implements DirectoryWatcher {
+ @override
+ String get directory => path;
+
+ /// Creates a new polling watcher monitoring [directory].
+ ///
+ /// If [pollingDelay] is passed, it specifies the amount of time the watcher
+ /// will pause between successive polls of the directory contents. Making this
+ /// shorter will give more immediate feedback at the expense of doing more IO
+ /// and higher CPU usage. Defaults to one second.
+ PollingDirectoryWatcher(String directory, {Duration? pollingDelay})
+ : super(directory, () {
+ return _PollingDirectoryWatcher(
+ directory, pollingDelay ?? const Duration(seconds: 1));
+ });
+}
+
+class _PollingDirectoryWatcher
+ implements DirectoryWatcher, ManuallyClosedWatcher {
+ @override
+ String get directory => path;
+ @override
+ final String path;
+
+ @override
+ Stream<WatchEvent> get events => _events.stream;
+ final _events = StreamController<WatchEvent>.broadcast();
+
+ @override
+ bool get isReady => _readyCompleter.isCompleted;
+
+ @override
+ Future<void> get ready => _readyCompleter.future;
+ final _readyCompleter = Completer<void>();
+
+ /// The amount of time the watcher pauses between successive polls of the
+ /// directory contents.
+ final Duration _pollingDelay;
+
+ /// The previous modification times of the files in the directory.
+ ///
+ /// Used to tell which files have been modified.
+ final _lastModifieds = <String, DateTime?>{};
+
+ /// The subscription used while [directory] is being listed.
+ ///
+ /// Will be `null` if a list is not currently happening.
+ StreamSubscription<FileSystemEntity>? _listSubscription;
+
+ /// The queue of files waiting to be processed to see if they have been
+ /// modified.
+ ///
+ /// Processing a file is asynchronous, as is listing the directory, so the
+ /// queue exists to let each of those proceed at their own rate. The lister
+ /// will enqueue files as quickly as it can. Meanwhile, files are dequeued
+ /// and processed sequentially.
+ late final AsyncQueue<String?> _filesToProcess =
+ AsyncQueue<String?>(_processFile, onError: (error, stackTrace) {
+ if (!_events.isClosed) _events.addError(error, stackTrace);
+ });
+
+ /// The set of files that have been seen in the current directory listing.
+ ///
+ /// Used to tell which files have been removed: files that are in
+ /// [_lastModifieds] but not in here when a poll completes have been removed.
+ final _polledFiles = <String>{};
+
+ _PollingDirectoryWatcher(this.path, this._pollingDelay) {
+ _poll();
+ }
+
+ @override
+ void close() {
+ _events.close();
+
+ // If we're in the middle of listing the directory, stop.
+ _listSubscription?.cancel();
+
+ // Don't process any remaining files.
+ _filesToProcess.clear();
+ _polledFiles.clear();
+ _lastModifieds.clear();
+ }
+
+ /// Scans the contents of the directory once to see which files have been
+ /// added, removed, and modified.
+ void _poll() {
+ _filesToProcess.clear();
+ _polledFiles.clear();
+
+ void endListing() {
+ assert(!_events.isClosed);
+ _listSubscription = null;
+
+ // Null tells the queue consumer that we're done listing.
+ _filesToProcess.add(null);
+ }
+
+ var stream = Directory(path).list(recursive: true);
+ _listSubscription = stream.listen((entity) {
+ assert(!_events.isClosed);
+
+ if (entity is! File) return;
+ _filesToProcess.add(entity.path);
+ }, onError: (Object error, StackTrace stackTrace) {
+ // Guarantee that ready always completes.
+ if (!isReady) {
+ _readyCompleter.complete();
+ }
+ if (!isDirectoryNotFoundException(error)) {
+ // It's some unknown error. Pipe it over to the event stream so the
+ // user can see it.
+ _events.addError(error, stackTrace);
+ }
+
+ // When an error occurs, we end the listing normally, which has the
+ // desired effect of marking all files that were in the directory as
+ // being removed.
+ endListing();
+ }, onDone: endListing, cancelOnError: true);
+ }
+
+ /// Processes [file] to determine if it has been modified since the last
+ /// time it was scanned.
+ Future<void> _processFile(String? file) async {
+ // `null` is the sentinel which means the directory listing is complete.
+ if (file == null) {
+ await _completePoll();
+ return;
+ }
+
+ final modified = await modificationTime(file);
+
+ if (_events.isClosed) return;
+
+ var lastModified = _lastModifieds[file];
+
+ // If its modification time hasn't changed, assume the file is unchanged.
+ if (lastModified != null && lastModified == modified) {
+ // The file is still here.
+ _polledFiles.add(file);
+ return;
+ }
+
+ if (_events.isClosed) return;
+
+ _lastModifieds[file] = modified;
+ _polledFiles.add(file);
+
+ // Only notify if we're ready to emit events.
+ if (!isReady) return;
+
+ var type = lastModified == null ? ChangeType.ADD : ChangeType.MODIFY;
+ _events.add(WatchEvent(type, file));
+ }
+
+ /// After the directory listing is complete, this determines which files were
+ /// removed and then restarts the next poll.
+ Future<void> _completePoll() async {
+ // Any files that were not seen in the last poll but that we have a
+ // status for must have been removed.
+ var removedFiles = _lastModifieds.keys.toSet().difference(_polledFiles);
+ for (var removed in removedFiles) {
+ if (isReady) _events.add(WatchEvent(ChangeType.REMOVE, removed));
+ _lastModifieds.remove(removed);
+ }
+
+ if (!isReady) _readyCompleter.complete();
+
+ // Wait and then poll again.
+ await Future<void>.delayed(_pollingDelay);
+ if (_events.isClosed) return;
+ _poll();
+ }
+}
diff --git a/pkgs/watcher/lib/src/directory_watcher/windows.dart b/pkgs/watcher/lib/src/directory_watcher/windows.dart
new file mode 100644
index 0000000..d1c98be
--- /dev/null
+++ b/pkgs/watcher/lib/src/directory_watcher/windows.dart
@@ -0,0 +1,437 @@
+// 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.
+// TODO(rnystrom): Merge with mac_os version.
+
+import 'dart:async';
+import 'dart:collection';
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+
+import '../directory_watcher.dart';
+import '../path_set.dart';
+import '../resubscribable.dart';
+import '../utils.dart';
+import '../watch_event.dart';
+
+class WindowsDirectoryWatcher extends ResubscribableWatcher
+ implements DirectoryWatcher {
+ @override
+ String get directory => path;
+
+ WindowsDirectoryWatcher(String directory)
+ : super(directory, () => _WindowsDirectoryWatcher(directory));
+}
+
+class _EventBatcher {
+ static const Duration _batchDelay = Duration(milliseconds: 100);
+ final List<FileSystemEvent> events = [];
+ Timer? timer;
+
+ void addEvent(FileSystemEvent event, void Function() callback) {
+ events.add(event);
+ timer?.cancel();
+ timer = Timer(_batchDelay, callback);
+ }
+
+ void cancelTimer() {
+ timer?.cancel();
+ }
+}
+
+class _WindowsDirectoryWatcher
+ implements DirectoryWatcher, ManuallyClosedWatcher {
+ @override
+ String get directory => path;
+ @override
+ final String path;
+
+ @override
+ Stream<WatchEvent> get events => _eventsController.stream;
+ final _eventsController = StreamController<WatchEvent>.broadcast();
+
+ @override
+ bool get isReady => _readyCompleter.isCompleted;
+
+ @override
+ Future<void> get ready => _readyCompleter.future;
+ final _readyCompleter = Completer<void>();
+
+ final Map<String, _EventBatcher> _eventBatchers =
+ HashMap<String, _EventBatcher>();
+
+ /// The set of files that are known to exist recursively within the watched
+ /// directory.
+ ///
+ /// The state of files on the filesystem is compared against this to determine
+ /// the real change that occurred. This is also used to emit REMOVE events
+ /// when subdirectories are moved out of the watched directory.
+ final PathSet _files;
+
+ /// The subscription to the stream returned by [Directory.watch].
+ StreamSubscription<FileSystemEvent>? _watchSubscription;
+
+ /// The subscription to the stream returned by [Directory.watch] of the
+ /// parent directory to [directory]. This is needed to detect changes to
+ /// [directory], as they are not included on Windows.
+ StreamSubscription<FileSystemEvent>? _parentWatchSubscription;
+
+ /// The subscription to the [Directory.list] call for the initial listing of
+ /// the directory to determine its initial state.
+ StreamSubscription<FileSystemEntity>? _initialListSubscription;
+
+ /// The subscriptions to the [Directory.list] calls for listing the contents
+ /// of subdirectories that were moved into the watched directory.
+ final Set<StreamSubscription<FileSystemEntity>> _listSubscriptions =
+ HashSet<StreamSubscription<FileSystemEntity>>();
+
+ _WindowsDirectoryWatcher(this.path) : _files = PathSet(path) {
+ // Before we're ready to emit events, wait for [_listDir] to complete.
+ _listDir().then((_) {
+ _startWatch();
+ _startParentWatcher();
+ if (!isReady) {
+ _readyCompleter.complete();
+ }
+ });
+ }
+
+ @override
+ void close() {
+ _watchSubscription?.cancel();
+ _parentWatchSubscription?.cancel();
+ _initialListSubscription?.cancel();
+ for (var sub in _listSubscriptions) {
+ sub.cancel();
+ }
+ _listSubscriptions.clear();
+ for (var batcher in _eventBatchers.values) {
+ batcher.cancelTimer();
+ }
+ _eventBatchers.clear();
+ _watchSubscription = null;
+ _parentWatchSubscription = null;
+ _initialListSubscription = null;
+ _eventsController.close();
+ }
+
+ /// On Windows, if [directory] is deleted, we will not receive any event.
+ ///
+ /// Instead, we add a watcher on the parent folder (if any), that can notify
+ /// us about [path]. This also includes events such as moves.
+ void _startParentWatcher() {
+ var absoluteDir = p.absolute(path);
+ var parent = p.dirname(absoluteDir);
+ // Check if [path] is already the root directory.
+ if (FileSystemEntity.identicalSync(parent, path)) return;
+ var parentStream = Directory(parent).watch(recursive: false);
+ _parentWatchSubscription = parentStream.listen((event) {
+ // Only look at events for 'directory'.
+ if (p.basename(event.path) != p.basename(absoluteDir)) return;
+ // Test if the directory is removed. FileSystemEntity.typeSync will
+ // return NOT_FOUND if it's unable to decide upon the type, including
+ // access denied issues, which may happen when the directory is deleted.
+ // FileSystemMoveEvent and FileSystemDeleteEvent events will always mean
+ // the directory is now gone.
+ if (event is FileSystemMoveEvent ||
+ event is FileSystemDeleteEvent ||
+ (FileSystemEntity.typeSync(path) == FileSystemEntityType.notFound)) {
+ for (var path in _files.paths) {
+ _emitEvent(ChangeType.REMOVE, path);
+ }
+ _files.clear();
+ close();
+ }
+ }, onError: (error) {
+ // Ignore errors, simply close the stream. The user listens on
+ // [directory], and while it can fail to listen on the parent, we may
+ // still be able to listen on the path requested.
+ _parentWatchSubscription?.cancel();
+ _parentWatchSubscription = null;
+ });
+ }
+
+ void _onEvent(FileSystemEvent event) {
+ assert(isReady);
+ final batcher = _eventBatchers.putIfAbsent(event.path, _EventBatcher.new);
+ batcher.addEvent(event, () {
+ _eventBatchers.remove(event.path);
+ _onBatch(batcher.events);
+ });
+ }
+
+ /// The callback that's run when [Directory.watch] emits a batch of events.
+ void _onBatch(List<FileSystemEvent> batch) {
+ _sortEvents(batch).forEach((path, eventSet) {
+ var canonicalEvent = _canonicalEvent(eventSet);
+ var events = canonicalEvent == null
+ ? _eventsBasedOnFileSystem(path)
+ : [canonicalEvent];
+
+ for (var event in events) {
+ if (event is FileSystemCreateEvent) {
+ if (!event.isDirectory) {
+ if (_files.contains(path)) continue;
+
+ _emitEvent(ChangeType.ADD, path);
+ _files.add(path);
+ continue;
+ }
+
+ if (_files.containsDir(path)) continue;
+
+ var stream = Directory(path).list(recursive: true);
+ var subscription = stream.listen((entity) {
+ if (entity is Directory) return;
+ if (_files.contains(path)) return;
+
+ _emitEvent(ChangeType.ADD, entity.path);
+ _files.add(entity.path);
+ }, cancelOnError: true);
+ subscription.onDone(() {
+ _listSubscriptions.remove(subscription);
+ });
+ subscription.onError((Object e, StackTrace stackTrace) {
+ _listSubscriptions.remove(subscription);
+ _emitError(e, stackTrace);
+ });
+ _listSubscriptions.add(subscription);
+ } else if (event is FileSystemModifyEvent) {
+ if (!event.isDirectory) {
+ _emitEvent(ChangeType.MODIFY, path);
+ }
+ } else {
+ assert(event is FileSystemDeleteEvent);
+ for (var removedPath in _files.remove(path)) {
+ _emitEvent(ChangeType.REMOVE, removedPath);
+ }
+ }
+ }
+ });
+ }
+
+ /// Sort all the events in a batch into sets based on their path.
+ ///
+ /// A single input event may result in multiple events in the returned map;
+ /// for example, a MOVE event becomes a DELETE event for the source and a
+ /// CREATE event for the destination.
+ ///
+ /// The returned events won't contain any [FileSystemMoveEvent]s, nor will it
+ /// contain any events relating to [path].
+ Map<String, Set<FileSystemEvent>> _sortEvents(List<FileSystemEvent> batch) {
+ var eventsForPaths = <String, Set<FileSystemEvent>>{};
+
+ // Events within directories that already have events are superfluous; the
+ // directory's full contents will be examined anyway, so we ignore such
+ // events. Emitting them could cause useless or out-of-order events.
+ var directories = unionAll(batch.map((event) {
+ if (!event.isDirectory) return <String>{};
+ if (event is FileSystemMoveEvent) {
+ var destination = event.destination;
+ if (destination != null) {
+ return {event.path, destination};
+ }
+ }
+ return {event.path};
+ }));
+
+ bool isInModifiedDirectory(String path) =>
+ directories.any((dir) => path != dir && p.isWithin(dir, path));
+
+ void addEvent(String path, FileSystemEvent event) {
+ if (isInModifiedDirectory(path)) return;
+ eventsForPaths.putIfAbsent(path, () => <FileSystemEvent>{}).add(event);
+ }
+
+ for (var event in batch) {
+ if (event is FileSystemMoveEvent) {
+ var destination = event.destination;
+ if (destination != null) {
+ addEvent(destination, event);
+ }
+ }
+ addEvent(event.path, event);
+ }
+
+ return eventsForPaths;
+ }
+
+ /// Returns the canonical event from a batch of events on the same path, if
+ /// one exists.
+ ///
+ /// If [batch] doesn't contain any contradictory events (e.g. DELETE and
+ /// CREATE, or events with different values for `isDirectory`), this returns a
+ /// single event that describes what happened to the path in question.
+ ///
+ /// If [batch] does contain contradictory events, this returns `null` to
+ /// indicate that the state of the path on the filesystem should be checked to
+ /// determine what occurred.
+ FileSystemEvent? _canonicalEvent(Set<FileSystemEvent> batch) {
+ // An empty batch indicates that we've learned earlier that the batch is
+ // contradictory (e.g. because of a move).
+ if (batch.isEmpty) return null;
+
+ var type = batch.first.type;
+ var isDir = batch.first.isDirectory;
+
+ for (var event in batch.skip(1)) {
+ // If one event reports that the file is a directory and another event
+ // doesn't, that's a contradiction.
+ if (isDir != event.isDirectory) return null;
+
+ // Modify events don't contradict either CREATE or REMOVE events. We can
+ // safely assume the file was modified after a CREATE or before the
+ // REMOVE; otherwise there will also be a REMOVE or CREATE event
+ // (respectively) that will be contradictory.
+ if (event is FileSystemModifyEvent) continue;
+ assert(event is FileSystemCreateEvent ||
+ event is FileSystemDeleteEvent ||
+ event is FileSystemMoveEvent);
+
+ // If we previously thought this was a MODIFY, we now consider it to be a
+ // CREATE or REMOVE event. This is safe for the same reason as above.
+ if (type == FileSystemEvent.modify) {
+ type = event.type;
+ continue;
+ }
+
+ // A CREATE event contradicts a REMOVE event and vice versa.
+ assert(type == FileSystemEvent.create ||
+ type == FileSystemEvent.delete ||
+ type == FileSystemEvent.move);
+ if (type != event.type) return null;
+ }
+
+ switch (type) {
+ case FileSystemEvent.create:
+ return FileSystemCreateEvent(batch.first.path, isDir);
+ case FileSystemEvent.delete:
+ return FileSystemDeleteEvent(batch.first.path, isDir);
+ case FileSystemEvent.modify:
+ return FileSystemModifyEvent(batch.first.path, isDir, false);
+ case FileSystemEvent.move:
+ return null;
+ default:
+ throw StateError('unreachable');
+ }
+ }
+
+ /// Returns zero or more events that describe the change between the last
+ /// known state of [path] and its current state on the filesystem.
+ ///
+ /// This returns a list whose order should be reflected in the events emitted
+ /// to the user, unlike the batched events from [Directory.watch]. The
+ /// returned list may be empty, indicating that no changes occurred to [path]
+ /// (probably indicating that it was created and then immediately deleted).
+ List<FileSystemEvent> _eventsBasedOnFileSystem(String path) {
+ var fileExisted = _files.contains(path);
+ var dirExisted = _files.containsDir(path);
+
+ bool fileExists;
+ bool dirExists;
+ try {
+ fileExists = File(path).existsSync();
+ dirExists = Directory(path).existsSync();
+ } on FileSystemException {
+ return const <FileSystemEvent>[];
+ }
+
+ var events = <FileSystemEvent>[];
+ if (fileExisted) {
+ if (fileExists) {
+ events.add(FileSystemModifyEvent(path, false, false));
+ } else {
+ events.add(FileSystemDeleteEvent(path, false));
+ }
+ } else if (dirExisted) {
+ if (dirExists) {
+ // If we got contradictory events for a directory that used to exist and
+ // still exists, we need to rescan the whole thing in case it was
+ // replaced with a different directory.
+ events.add(FileSystemDeleteEvent(path, true));
+ events.add(FileSystemCreateEvent(path, true));
+ } else {
+ events.add(FileSystemDeleteEvent(path, true));
+ }
+ }
+
+ if (!fileExisted && fileExists) {
+ events.add(FileSystemCreateEvent(path, false));
+ } else if (!dirExisted && dirExists) {
+ events.add(FileSystemCreateEvent(path, true));
+ }
+
+ return events;
+ }
+
+ /// The callback that's run when the [Directory.watch] stream is closed.
+ /// Note that this is unlikely to happen on Windows, unless the system itself
+ /// closes the handle.
+ void _onDone() {
+ _watchSubscription = null;
+
+ // Emit remove events for any remaining files.
+ for (var file in _files.paths) {
+ _emitEvent(ChangeType.REMOVE, file);
+ }
+ _files.clear();
+ close();
+ }
+
+ /// Start or restart the underlying [Directory.watch] stream.
+ void _startWatch() {
+ // Note: "watcher closed" exceptions do not get sent over the stream
+ // returned by watch, and must be caught via a zone handler.
+ runZonedGuarded(() {
+ var innerStream = Directory(path).watch(recursive: true);
+ _watchSubscription = innerStream.listen(_onEvent,
+ onError: _eventsController.addError, onDone: _onDone);
+ }, (error, stackTrace) {
+ if (error is FileSystemException &&
+ error.message.startsWith('Directory watcher closed unexpectedly')) {
+ _watchSubscription?.cancel();
+ _eventsController.addError(error, stackTrace);
+ _startWatch();
+ } else {
+ // ignore: only_throw_errors
+ throw error;
+ }
+ });
+ }
+
+ /// Starts or restarts listing the watched directory to get an initial picture
+ /// of its state.
+ Future<void> _listDir() {
+ assert(!isReady);
+ _initialListSubscription?.cancel();
+
+ _files.clear();
+ var completer = Completer<void>();
+ var stream = Directory(path).list(recursive: true);
+ void handleEntity(FileSystemEntity entity) {
+ if (entity is! Directory) _files.add(entity.path);
+ }
+
+ _initialListSubscription = stream.listen(handleEntity,
+ onError: _emitError, onDone: completer.complete, cancelOnError: true);
+ return completer.future;
+ }
+
+ /// Emit an event with the given [type] and [path].
+ void _emitEvent(ChangeType type, String path) {
+ if (!isReady) return;
+
+ _eventsController.add(WatchEvent(type, path));
+ }
+
+ /// Emit an error, then close the watcher.
+ void _emitError(Object error, StackTrace stackTrace) {
+ // Guarantee that ready always completes.
+ if (!isReady) {
+ _readyCompleter.complete();
+ }
+ _eventsController.addError(error, stackTrace);
+ close();
+ }
+}
diff --git a/pkgs/watcher/lib/src/file_watcher.dart b/pkgs/watcher/lib/src/file_watcher.dart
new file mode 100644
index 0000000..143aa31
--- /dev/null
+++ b/pkgs/watcher/lib/src/file_watcher.dart
@@ -0,0 +1,44 @@
+// 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 '../watcher.dart';
+import 'custom_watcher_factory.dart';
+import 'file_watcher/native.dart';
+
+/// Watches a file and emits [WatchEvent]s when the file has changed.
+///
+/// Note that since each watcher only watches a single file, it will only emit
+/// [ChangeType.MODIFY] events, except when the file is deleted at which point
+/// it will emit a single [ChangeType.REMOVE] event and then close the stream.
+///
+/// If the file is deleted and quickly replaced (when a new file is moved in its
+/// place, for example) this will emit a [ChangeType.MODIFY] event.
+abstract class FileWatcher implements Watcher {
+ /// Creates a new [FileWatcher] monitoring [file].
+ ///
+ /// If a native file watcher is available for this platform, this will use it.
+ /// Otherwise, it will fall back to a [PollingFileWatcher]. Notably, native
+ /// file watching is *not* supported on Windows.
+ ///
+ /// If [pollingDelay] is passed, it specifies the amount of time the watcher
+ /// will pause between successive polls of the directory contents. Making this
+ /// shorter will give more immediate feedback at the expense of doing more IO
+ /// and higher CPU usage. Defaults to one second. Ignored for non-polling
+ /// watchers.
+ factory FileWatcher(String file, {Duration? pollingDelay}) {
+ var customWatcher =
+ createCustomFileWatcher(file, pollingDelay: pollingDelay);
+ if (customWatcher != null) return customWatcher;
+
+ // [File.watch] doesn't work on Windows, but
+ // [FileSystemEntity.isWatchSupported] is still true because directory
+ // watching does work.
+ if (FileSystemEntity.isWatchSupported && !Platform.isWindows) {
+ return NativeFileWatcher(file);
+ }
+ return PollingFileWatcher(file, pollingDelay: pollingDelay);
+ }
+}
diff --git a/pkgs/watcher/lib/src/file_watcher/native.dart b/pkgs/watcher/lib/src/file_watcher/native.dart
new file mode 100644
index 0000000..502aa10
--- /dev/null
+++ b/pkgs/watcher/lib/src/file_watcher/native.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 'dart:async';
+import 'dart:io';
+
+import '../file_watcher.dart';
+import '../resubscribable.dart';
+import '../utils.dart';
+import '../watch_event.dart';
+
+/// Uses the native file system notifications to watch for filesystem events.
+///
+/// Single-file notifications are much simpler than those for multiple files, so
+/// this doesn't need to be split out into multiple OS-specific classes.
+class NativeFileWatcher extends ResubscribableWatcher implements FileWatcher {
+ NativeFileWatcher(String path) : super(path, () => _NativeFileWatcher(path));
+}
+
+class _NativeFileWatcher implements FileWatcher, ManuallyClosedWatcher {
+ @override
+ final String path;
+
+ @override
+ Stream<WatchEvent> get events => _eventsController.stream;
+ final _eventsController = StreamController<WatchEvent>.broadcast();
+
+ @override
+ bool get isReady => _readyCompleter.isCompleted;
+
+ @override
+ Future<void> get ready => _readyCompleter.future;
+ final _readyCompleter = Completer<void>();
+
+ StreamSubscription<List<FileSystemEvent>>? _subscription;
+
+ _NativeFileWatcher(this.path) {
+ _listen();
+
+ // We don't need to do any initial set-up, so we're ready immediately after
+ // being listened to.
+ _readyCompleter.complete();
+ }
+
+ void _listen() {
+ // Batch the events together so that we can dedup them.
+ _subscription = File(path)
+ .watch()
+ .batchEvents()
+ .listen(_onBatch, onError: _eventsController.addError, onDone: _onDone);
+ }
+
+ void _onBatch(List<FileSystemEvent> batch) {
+ if (batch.any((event) => event.type == FileSystemEvent.delete)) {
+ // If the file is deleted, the underlying stream will close. We handle
+ // emitting our own REMOVE event in [_onDone].
+ return;
+ }
+
+ _eventsController.add(WatchEvent(ChangeType.MODIFY, path));
+ }
+
+ void _onDone() async {
+ var fileExists = await File(path).exists();
+
+ // Check for this after checking whether the file exists because it's
+ // possible that [close] was called between [File.exists] being called and
+ // it completing.
+ if (_eventsController.isClosed) return;
+
+ if (fileExists) {
+ // If the file exists now, it was probably removed and quickly replaced;
+ // this can happen for example when another file is moved on top of it.
+ // Re-subscribe and report a modify event.
+ _eventsController.add(WatchEvent(ChangeType.MODIFY, path));
+ _listen();
+ } else {
+ _eventsController.add(WatchEvent(ChangeType.REMOVE, path));
+ close();
+ }
+ }
+
+ @override
+ void close() {
+ _subscription?.cancel();
+ _subscription = null;
+ _eventsController.close();
+ }
+}
diff --git a/pkgs/watcher/lib/src/file_watcher/polling.dart b/pkgs/watcher/lib/src/file_watcher/polling.dart
new file mode 100644
index 0000000..15ff9ab
--- /dev/null
+++ b/pkgs/watcher/lib/src/file_watcher/polling.dart
@@ -0,0 +1,106 @@
+// 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 '../file_watcher.dart';
+import '../resubscribable.dart';
+import '../stat.dart';
+import '../watch_event.dart';
+
+/// Periodically polls a file for changes.
+class PollingFileWatcher extends ResubscribableWatcher implements FileWatcher {
+ PollingFileWatcher(String path, {Duration? pollingDelay})
+ : super(path, () {
+ return _PollingFileWatcher(
+ path, pollingDelay ?? const Duration(seconds: 1));
+ });
+}
+
+class _PollingFileWatcher implements FileWatcher, ManuallyClosedWatcher {
+ @override
+ final String path;
+
+ @override
+ Stream<WatchEvent> get events => _eventsController.stream;
+ final _eventsController = StreamController<WatchEvent>.broadcast();
+
+ @override
+ bool get isReady => _readyCompleter.isCompleted;
+
+ @override
+ Future<void> get ready => _readyCompleter.future;
+ final _readyCompleter = Completer<void>();
+
+ /// The timer that controls polling.
+ late final Timer _timer;
+
+ /// The previous modification time of the file.
+ ///
+ /// `null` indicates the file does not (or did not on the last poll) exist.
+ DateTime? _lastModified;
+
+ _PollingFileWatcher(this.path, Duration pollingDelay) {
+ _timer = Timer.periodic(pollingDelay, (_) => _poll());
+ _poll();
+ }
+
+ /// Checks the mtime of the file and whether it's been removed.
+ Future<void> _poll() async {
+ // We don't mark the file as removed if this is the first poll. Instead,
+ // below we forward the dart:io error that comes from trying to read the
+ // mtime below.
+ var pathExists = await File(path).exists();
+ if (_eventsController.isClosed) return;
+
+ if (_lastModified != null && !pathExists) {
+ _flagReady();
+ _eventsController.add(WatchEvent(ChangeType.REMOVE, path));
+ unawaited(close());
+ return;
+ }
+
+ DateTime? modified;
+ try {
+ modified = await modificationTime(path);
+ } on FileSystemException catch (error, stackTrace) {
+ if (!_eventsController.isClosed) {
+ _flagReady();
+ _eventsController.addError(error, stackTrace);
+ await close();
+ }
+ }
+ if (_eventsController.isClosed) {
+ _flagReady();
+ return;
+ }
+
+ if (!isReady) {
+ // If this is the first poll, don't emit an event, just set the last mtime
+ // and complete the completer.
+ _lastModified = modified;
+ _flagReady();
+ return;
+ }
+
+ if (_lastModified == modified) return;
+
+ _lastModified = modified;
+ _eventsController.add(WatchEvent(ChangeType.MODIFY, path));
+ }
+
+ /// Flags this watcher as ready if it has not already been done.
+ void _flagReady() {
+ if (!isReady) {
+ _readyCompleter.complete();
+ }
+ }
+
+ @override
+ Future<void> close() async {
+ _timer.cancel();
+ await _eventsController.close();
+ }
+}
diff --git a/pkgs/watcher/lib/src/path_set.dart b/pkgs/watcher/lib/src/path_set.dart
new file mode 100644
index 0000000..4f41cf9
--- /dev/null
+++ b/pkgs/watcher/lib/src/path_set.dart
@@ -0,0 +1,190 @@
+// 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:path/path.dart' as p;
+
+/// A set of paths, organized into a directory hierarchy.
+///
+/// When a path is [add]ed, it creates an implicit directory structure above
+/// that path. Directories can be inspected using [containsDir] and removed
+/// using [remove]. If they're removed, their contents are removed as well.
+///
+/// The paths in the set are normalized so that they all begin with [root].
+class PathSet {
+ /// The root path, which all paths in the set must be under.
+ final String root;
+
+ /// The path set's directory hierarchy.
+ ///
+ /// Each entry represents a directory or file. It may be a file or directory
+ /// that was explicitly added, or a parent directory that was implicitly
+ /// added in order to add a child.
+ final _Entry _entries = _Entry();
+
+ PathSet(this.root);
+
+ /// Adds [path] to the set.
+ void add(String path) {
+ path = _normalize(path);
+
+ var parts = p.split(path);
+ var entry = _entries;
+ for (var part in parts) {
+ entry = entry.contents.putIfAbsent(part, _Entry.new);
+ }
+
+ entry.isExplicit = true;
+ }
+
+ /// Removes [path] and any paths beneath it from the set and returns the
+ /// removed paths.
+ ///
+ /// Even if [path] itself isn't in the set, if it's a directory containing
+ /// paths that are in the set those paths will be removed and returned.
+ ///
+ /// If neither [path] nor any paths beneath it are in the set, returns an
+ /// empty set.
+ Set<String> remove(String path) {
+ path = _normalize(path);
+ var parts = Queue.of(p.split(path));
+
+ // Remove the children of [dir], as well as [dir] itself if necessary.
+ //
+ // [partialPath] is the path to [dir], and a prefix of [path]; the remaining
+ // components of [path] are in [parts].
+ Set<String> recurse(_Entry dir, String partialPath) {
+ if (parts.length > 1) {
+ // If there's more than one component left in [path], recurse down to
+ // the next level.
+ var part = parts.removeFirst();
+ var entry = dir.contents[part];
+ if (entry == null || entry.contents.isEmpty) return <String>{};
+
+ partialPath = p.join(partialPath, part);
+ var paths = recurse(entry, partialPath);
+ // After removing this entry's children, if it has no more children and
+ // it's not in the set in its own right, remove it as well.
+ if (entry.contents.isEmpty && !entry.isExplicit) {
+ dir.contents.remove(part);
+ }
+ return paths;
+ }
+
+ // If there's only one component left in [path], we should remove it.
+ var entry = dir.contents.remove(parts.first);
+ if (entry == null) return <String>{};
+
+ if (entry.contents.isEmpty) {
+ return {p.join(root, path)};
+ }
+
+ var set = _explicitPathsWithin(entry, path);
+ if (entry.isExplicit) {
+ set.add(p.join(root, path));
+ }
+
+ return set;
+ }
+
+ return recurse(_entries, root);
+ }
+
+ /// Recursively lists all of the explicit paths within [dir].
+ ///
+ /// [dirPath] should be the path to [dir].
+ Set<String> _explicitPathsWithin(_Entry dir, String dirPath) {
+ var paths = <String>{};
+ void recurse(_Entry dir, String path) {
+ dir.contents.forEach((name, entry) {
+ var entryPath = p.join(path, name);
+ if (entry.isExplicit) paths.add(p.join(root, entryPath));
+
+ recurse(entry, entryPath);
+ });
+ }
+
+ recurse(dir, dirPath);
+ return paths;
+ }
+
+ /// Returns whether this set contains [path].
+ ///
+ /// This only returns true for paths explicitly added to this set.
+ /// Implicitly-added directories can be inspected using [containsDir].
+ bool contains(String path) {
+ path = _normalize(path);
+ var entry = _entries;
+
+ for (var part in p.split(path)) {
+ var child = entry.contents[part];
+ if (child == null) return false;
+ entry = child;
+ }
+
+ return entry.isExplicit;
+ }
+
+ /// Returns whether this set contains paths beneath [path].
+ bool containsDir(String path) {
+ path = _normalize(path);
+ var entry = _entries;
+
+ for (var part in p.split(path)) {
+ var child = entry.contents[part];
+ if (child == null) return false;
+ entry = child;
+ }
+
+ return entry.contents.isNotEmpty;
+ }
+
+ /// All of the paths explicitly added to this set.
+ List<String> get paths {
+ var result = <String>[];
+
+ void recurse(_Entry dir, String path) {
+ for (var mapEntry in dir.contents.entries) {
+ var entry = mapEntry.value;
+ var entryPath = p.join(path, mapEntry.key);
+ if (entry.isExplicit) result.add(entryPath);
+ recurse(entry, entryPath);
+ }
+ }
+
+ recurse(_entries, root);
+ return result;
+ }
+
+ /// Removes all paths from this set.
+ void clear() {
+ _entries.contents.clear();
+ }
+
+ /// Returns a normalized version of [path].
+ ///
+ /// This removes any extra ".." or "."s and ensure that the returned path
+ /// begins with [root]. It's an error if [path] isn't within [root].
+ String _normalize(String path) {
+ assert(p.isWithin(root, path));
+
+ return p.relative(p.normalize(path), from: root);
+ }
+}
+
+/// A virtual file system entity tracked by the [PathSet].
+///
+/// It may have child entries in [contents], which implies it's a directory.
+class _Entry {
+ /// The child entries contained in this directory.
+ final Map<String, _Entry> contents = {};
+
+ /// If this entry was explicitly added as a leaf file system entity, this
+ /// will be true.
+ ///
+ /// Otherwise, it represents a parent directory that was implicitly added
+ /// when added some child of it.
+ bool isExplicit = false;
+}
diff --git a/pkgs/watcher/lib/src/resubscribable.dart b/pkgs/watcher/lib/src/resubscribable.dart
new file mode 100644
index 0000000..b99e9d7
--- /dev/null
+++ b/pkgs/watcher/lib/src/resubscribable.dart
@@ -0,0 +1,79 @@
+// 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:async';
+
+import '../watcher.dart';
+
+/// A wrapper for [ManuallyClosedWatcher] that encapsulates support for closing
+/// the watcher when it has no subscribers and re-opening it when it's
+/// re-subscribed.
+///
+/// It's simpler to implement watchers without worrying about this behavior.
+/// This class wraps a watcher class which can be written with the simplifying
+/// assumption that it can continue emitting events until an explicit `close`
+/// method is called, at which point it will cease emitting events entirely. The
+/// [ManuallyClosedWatcher] interface is used for these watchers.
+///
+/// This would be more cleanly implemented as a function that takes a class and
+/// emits a new class, but Dart doesn't support that sort of thing. Instead it
+/// takes a factory function that produces instances of the inner class.
+abstract class ResubscribableWatcher implements Watcher {
+ /// The factory function that produces instances of the inner class.
+ final ManuallyClosedWatcher Function() _factory;
+
+ @override
+ final String path;
+
+ @override
+ Stream<WatchEvent> get events => _eventsController.stream;
+ late StreamController<WatchEvent> _eventsController;
+
+ @override
+ bool get isReady => _readyCompleter.isCompleted;
+
+ @override
+ Future<void> get ready => _readyCompleter.future;
+ var _readyCompleter = Completer<void>();
+
+ /// Creates a new [ResubscribableWatcher] wrapping the watchers
+ /// emitted by [_factory].
+ ResubscribableWatcher(this.path, this._factory) {
+ late ManuallyClosedWatcher watcher;
+ late StreamSubscription<WatchEvent> subscription;
+
+ _eventsController = StreamController<WatchEvent>.broadcast(
+ onListen: () async {
+ watcher = _factory();
+ subscription = watcher.events.listen(_eventsController.add,
+ onError: _eventsController.addError,
+ onDone: _eventsController.close);
+
+ // It's important that we complete the value of [_readyCompleter] at
+ // the time [onListen] is called, as opposed to the value when
+ // [watcher.ready] fires. A new completer may be created by that time.
+ await watcher.ready;
+ _readyCompleter.complete();
+ },
+ onCancel: () {
+ // Cancel the subscription before closing the watcher so that the
+ // watcher's `onDone` event doesn't close [events].
+ subscription.cancel();
+ watcher.close();
+ _readyCompleter = Completer();
+ },
+ sync: true);
+ }
+}
+
+/// An interface for watchers with an explicit, manual [close] method.
+///
+/// See [ResubscribableWatcher].
+abstract class ManuallyClosedWatcher implements Watcher {
+ /// Closes the watcher.
+ ///
+ /// Subclasses should close their [events] stream and release any internal
+ /// resources.
+ void close();
+}
diff --git a/pkgs/watcher/lib/src/stat.dart b/pkgs/watcher/lib/src/stat.dart
new file mode 100644
index 0000000..fe0f155
--- /dev/null
+++ b/pkgs/watcher/lib/src/stat.dart
@@ -0,0 +1,34 @@
+// 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:io';
+
+/// A function that takes a file path and returns the last modified time for
+/// the file at that path.
+typedef MockTimeCallback = DateTime? Function(String path);
+
+MockTimeCallback? _mockTimeCallback;
+
+/// Overrides the default behavior for accessing a file's modification time
+/// with [callback].
+///
+/// The OS file modification time has pretty rough granularity (like a few
+/// seconds) which can make for slow tests that rely on modtime. This lets you
+/// replace it with something you control.
+void mockGetModificationTime(MockTimeCallback callback) {
+ _mockTimeCallback = callback;
+}
+
+/// Gets the modification time for the file at [path].
+/// Completes with `null` if the file does not exist.
+Future<DateTime?> modificationTime(String path) async {
+ var mockTimeCallback = _mockTimeCallback;
+ if (mockTimeCallback != null) {
+ return mockTimeCallback(path);
+ }
+
+ final stat = await FileStat.stat(path);
+ if (stat.type == FileSystemEntityType.notFound) return null;
+ return stat.modified;
+}
diff --git a/pkgs/watcher/lib/src/utils.dart b/pkgs/watcher/lib/src/utils.dart
new file mode 100644
index 0000000..c2e71b3
--- /dev/null
+++ b/pkgs/watcher/lib/src/utils.dart
@@ -0,0 +1,52 @@
+// 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:async';
+import 'dart:collection';
+import 'dart:io';
+
+/// Returns `true` if [error] is a [FileSystemException] for a missing
+/// directory.
+bool isDirectoryNotFoundException(Object error) {
+ if (error is! FileSystemException) return false;
+
+ // See dartbug.com/12461 and tests/standalone/io/directory_error_test.dart.
+ var notFoundCode = Platform.operatingSystem == 'windows' ? 3 : 2;
+ return error.osError?.errorCode == notFoundCode;
+}
+
+/// Returns the union of all elements in each set in [sets].
+Set<T> unionAll<T>(Iterable<Set<T>> sets) =>
+ sets.fold(<T>{}, (union, set) => union.union(set));
+
+extension BatchEvents<T> on Stream<T> {
+ /// Batches all events that are sent at the same time.
+ ///
+ /// When multiple events are synchronously added to a stream controller, the
+ /// [StreamController] implementation uses [scheduleMicrotask] to schedule the
+ /// asynchronous firing of each event. In order to recreate the synchronous
+ /// batches, this collates all the events that are received in "nearby"
+ /// microtasks.
+ Stream<List<T>> batchEvents() {
+ var batch = Queue<T>();
+ return StreamTransformer<T, List<T>>.fromHandlers(
+ handleData: (event, sink) {
+ batch.add(event);
+
+ // [Timer.run] schedules an event that runs after any microtasks that have
+ // been scheduled.
+ Timer.run(() {
+ if (batch.isEmpty) return;
+ sink.add(batch.toList());
+ batch.clear();
+ });
+ }, handleDone: (sink) {
+ if (batch.isNotEmpty) {
+ sink.add(batch.toList());
+ batch.clear();
+ }
+ sink.close();
+ }).bind(this);
+ }
+}
diff --git a/pkgs/watcher/lib/src/watch_event.dart b/pkgs/watcher/lib/src/watch_event.dart
new file mode 100644
index 0000000..b65afc2
--- /dev/null
+++ b/pkgs/watcher/lib/src/watch_event.dart
@@ -0,0 +1,38 @@
+// 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.
+
+/// An event describing a single change to the file system.
+class WatchEvent {
+ /// The manner in which the file at [path] has changed.
+ final ChangeType type;
+
+ /// The path of the file that changed.
+ final String path;
+
+ WatchEvent(this.type, this.path);
+
+ @override
+ String toString() => '$type $path';
+}
+
+/// Enum for what kind of change has happened to a file.
+class ChangeType {
+ /// A new file has been added.
+ // ignore: constant_identifier_names
+ static const ADD = ChangeType('add');
+
+ /// A file has been removed.
+ // ignore: constant_identifier_names
+ static const REMOVE = ChangeType('remove');
+
+ /// The contents of a file have changed.
+ // ignore: constant_identifier_names
+ static const MODIFY = ChangeType('modify');
+
+ final String _name;
+ const ChangeType(this._name);
+
+ @override
+ String toString() => _name;
+}
diff --git a/pkgs/watcher/lib/watcher.dart b/pkgs/watcher/lib/watcher.dart
new file mode 100644
index 0000000..12a5369
--- /dev/null
+++ b/pkgs/watcher/lib/watcher.dart
@@ -0,0 +1,70 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'src/directory_watcher.dart';
+import 'src/file_watcher.dart';
+import 'src/watch_event.dart';
+
+export 'src/custom_watcher_factory.dart' show registerCustomWatcher;
+export 'src/directory_watcher.dart';
+export 'src/directory_watcher/polling.dart';
+export 'src/file_watcher.dart';
+export 'src/file_watcher/polling.dart';
+export 'src/watch_event.dart';
+
+abstract class Watcher {
+ /// The path to the file or directory whose contents are being monitored.
+ String get path;
+
+ /// The broadcast [Stream] of events that have occurred to the watched file or
+ /// files in the watched directory.
+ ///
+ /// Changes will only be monitored while this stream has subscribers. Any
+ /// changes that occur during periods when there are no subscribers will not
+ /// be reported the next time a subscriber is added.
+ Stream<WatchEvent> get events;
+
+ /// Whether the watcher is initialized and watching for changes.
+ ///
+ /// This is true if and only if [ready] is complete.
+ bool get isReady;
+
+ /// A [Future] that completes when the watcher is initialized and watching for
+ /// changes.
+ ///
+ /// If the watcher is not currently monitoring the file or directory (because
+ /// there are no subscribers to [events]), this returns a future that isn't
+ /// complete yet. It will complete when a subscriber starts listening and the
+ /// watcher finishes any initialization work it needs to do.
+ ///
+ /// If the watcher is already monitoring, this returns an already complete
+ /// future.
+ ///
+ /// This future always completes successfully as errors are provided through
+ /// the [events] stream.
+ Future get ready;
+
+ /// Creates a new [DirectoryWatcher] or [FileWatcher] monitoring [path],
+ /// depending on whether it's a file or directory.
+ ///
+ /// If a native watcher is available for this platform, this will use it.
+ /// Otherwise, it will fall back to a polling watcher. Notably, watching
+ /// individual files is not natively supported on Windows, although watching
+ /// directories is.
+ ///
+ /// If [pollingDelay] is passed, it specifies the amount of time the watcher
+ /// will pause between successive polls of the contents of [path]. Making this
+ /// shorter will give more immediate feedback at the expense of doing more IO
+ /// and higher CPU usage. Defaults to one second. Ignored for non-polling
+ /// watchers.
+ factory Watcher(String path, {Duration? pollingDelay}) {
+ if (File(path).existsSync()) {
+ return FileWatcher(path, pollingDelay: pollingDelay);
+ } else {
+ return DirectoryWatcher(path, pollingDelay: pollingDelay);
+ }
+ }
+}
diff --git a/pkgs/watcher/pubspec.yaml b/pkgs/watcher/pubspec.yaml
new file mode 100644
index 0000000..7781bd4
--- /dev/null
+++ b/pkgs/watcher/pubspec.yaml
@@ -0,0 +1,19 @@
+name: watcher
+version: 1.1.1
+description: >-
+ A file system watcher. It monitors changes to contents of directories and
+ sends notifications when files have been added, removed, or modified.
+repository: https://github.com/dart-lang/tools/tree/main/pkgs/watcher
+
+environment:
+ sdk: ^3.1.0
+
+dependencies:
+ async: ^2.5.0
+ path: ^1.8.0
+
+dev_dependencies:
+ benchmark_harness: ^2.0.0
+ dart_flutter_team_lints: ^3.0.0
+ test: ^1.16.6
+ test_descriptor: ^2.0.0
diff --git a/pkgs/watcher/test/custom_watcher_factory_test.dart b/pkgs/watcher/test/custom_watcher_factory_test.dart
new file mode 100644
index 0000000..e9d65bb
--- /dev/null
+++ b/pkgs/watcher/test/custom_watcher_factory_test.dart
@@ -0,0 +1,142 @@
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:watcher/watcher.dart';
+
+void main() {
+ late _MemFs memFs;
+ final memFsFactoryId = 'MemFs';
+ final noOpFactoryId = 'NoOp';
+
+ setUpAll(() {
+ memFs = _MemFs();
+ var memFsWatcherFactory = _MemFsWatcherFactory(memFs);
+ var noOpWatcherFactory = _NoOpWatcherFactory();
+ registerCustomWatcher(
+ noOpFactoryId,
+ noOpWatcherFactory.createDirectoryWatcher,
+ noOpWatcherFactory.createFileWatcher);
+ registerCustomWatcher(
+ memFsFactoryId,
+ memFsWatcherFactory.createDirectoryWatcher,
+ memFsWatcherFactory.createFileWatcher);
+ });
+
+ test('notifies for files', () async {
+ var watcher = FileWatcher('file.txt');
+
+ var completer = Completer<WatchEvent>();
+ watcher.events.listen((event) => completer.complete(event));
+ await watcher.ready;
+ memFs.add('file.txt');
+ var event = await completer.future;
+
+ expect(event.type, ChangeType.ADD);
+ expect(event.path, 'file.txt');
+ });
+
+ test('notifies for directories', () async {
+ var watcher = DirectoryWatcher('dir');
+
+ var completer = Completer<WatchEvent>();
+ watcher.events.listen((event) => completer.complete(event));
+ await watcher.ready;
+ memFs.add('dir');
+ var event = await completer.future;
+
+ expect(event.type, ChangeType.ADD);
+ expect(event.path, 'dir');
+ });
+
+ test('registering twice throws', () async {
+ expect(
+ () => registerCustomWatcher(
+ memFsFactoryId,
+ (_, {pollingDelay}) => throw UnimplementedError(),
+ (_, {pollingDelay}) => throw UnimplementedError()),
+ throwsA(isA<ArgumentError>()),
+ );
+ });
+
+ test('finding two applicable factories throws', () async {
+ // Note that _MemFsWatcherFactory always returns a watcher, so having two
+ // will always produce a conflict.
+ var watcherFactory = _MemFsWatcherFactory(memFs);
+ registerCustomWatcher('Different id', watcherFactory.createDirectoryWatcher,
+ watcherFactory.createFileWatcher);
+ expect(() => FileWatcher('file.txt'), throwsA(isA<StateError>()));
+ expect(() => DirectoryWatcher('dir'), throwsA(isA<StateError>()));
+ });
+}
+
+class _MemFs {
+ final _streams = <String, Set<StreamController<WatchEvent>>>{};
+
+ StreamController<WatchEvent> watchStream(String path) {
+ var controller = StreamController<WatchEvent>();
+ _streams
+ .putIfAbsent(path, () => <StreamController<WatchEvent>>{})
+ .add(controller);
+ return controller;
+ }
+
+ void add(String path) {
+ var controllers = _streams[path];
+ if (controllers != null) {
+ for (var controller in controllers) {
+ controller.add(WatchEvent(ChangeType.ADD, path));
+ }
+ }
+ }
+
+ void remove(String path) {
+ var controllers = _streams[path];
+ if (controllers != null) {
+ for (var controller in controllers) {
+ controller.add(WatchEvent(ChangeType.REMOVE, path));
+ }
+ }
+ }
+}
+
+class _MemFsWatcher implements FileWatcher, DirectoryWatcher, Watcher {
+ final String _path;
+ final StreamController<WatchEvent> _controller;
+
+ _MemFsWatcher(this._path, this._controller);
+
+ @override
+ String get path => _path;
+
+ @override
+ String get directory => throw UnsupportedError('directory is not supported');
+
+ @override
+ Stream<WatchEvent> get events => _controller.stream;
+
+ @override
+ bool get isReady => true;
+
+ @override
+ Future<void> get ready async {}
+}
+
+class _MemFsWatcherFactory {
+ final _MemFs _memFs;
+ _MemFsWatcherFactory(this._memFs);
+
+ DirectoryWatcher? createDirectoryWatcher(String path,
+ {Duration? pollingDelay}) =>
+ _MemFsWatcher(path, _memFs.watchStream(path));
+
+ FileWatcher? createFileWatcher(String path, {Duration? pollingDelay}) =>
+ _MemFsWatcher(path, _memFs.watchStream(path));
+}
+
+class _NoOpWatcherFactory {
+ DirectoryWatcher? createDirectoryWatcher(String path,
+ {Duration? pollingDelay}) =>
+ null;
+
+ FileWatcher? createFileWatcher(String path, {Duration? pollingDelay}) => null;
+}
diff --git a/pkgs/watcher/test/directory_watcher/linux_test.dart b/pkgs/watcher/test/directory_watcher/linux_test.dart
new file mode 100644
index 0000000..a10a72c
--- /dev/null
+++ b/pkgs/watcher/test/directory_watcher/linux_test.dart
@@ -0,0 +1,44 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('linux')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/linux.dart';
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+ watcherFactory = LinuxDirectoryWatcher.new;
+
+ sharedTests();
+
+ test('DirectoryWatcher creates a LinuxDirectoryWatcher on Linux', () {
+ expect(DirectoryWatcher('.'), const TypeMatcher<LinuxDirectoryWatcher>());
+ });
+
+ test('emits events for many nested files moved out then immediately back in',
+ () async {
+ withPermutations(
+ (i, j, k) => writeFile('dir/sub/sub-$i/sub-$j/file-$k.txt'));
+ await startWatcher(path: 'dir');
+
+ renameDir('dir/sub', 'sub');
+ renameDir('sub', 'dir/sub');
+
+ await allowEither(() {
+ inAnyOrder(withPermutations(
+ (i, j, k) => isRemoveEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+
+ inAnyOrder(withPermutations(
+ (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+ }, () {
+ inAnyOrder(withPermutations(
+ (i, j, k) => isModifyEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+ });
+ });
+}
diff --git a/pkgs/watcher/test/directory_watcher/mac_os_test.dart b/pkgs/watcher/test/directory_watcher/mac_os_test.dart
new file mode 100644
index 0000000..3376626
--- /dev/null
+++ b/pkgs/watcher/test/directory_watcher/mac_os_test.dart
@@ -0,0 +1,69 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('mac-os')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/mac_os.dart';
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+ watcherFactory = MacOSDirectoryWatcher.new;
+
+ sharedTests();
+
+ test('DirectoryWatcher creates a MacOSDirectoryWatcher on Mac OS', () {
+ expect(DirectoryWatcher('.'), const TypeMatcher<MacOSDirectoryWatcher>());
+ });
+
+ test(
+ 'does not notify about the watched directory being deleted and '
+ 'recreated immediately before watching', () async {
+ createDir('dir');
+ writeFile('dir/old.txt');
+ deleteDir('dir');
+ createDir('dir');
+
+ await startWatcher(path: 'dir');
+ writeFile('dir/newer.txt');
+ await expectAddEvent('dir/newer.txt');
+ });
+
+ test('emits events for many nested files moved out then immediately back in',
+ () async {
+ withPermutations(
+ (i, j, k) => writeFile('dir/sub/sub-$i/sub-$j/file-$k.txt'));
+
+ await startWatcher(path: 'dir');
+
+ renameDir('dir/sub', 'sub');
+ renameDir('sub', 'dir/sub');
+
+ await allowEither(() {
+ inAnyOrder(withPermutations(
+ (i, j, k) => isRemoveEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+
+ inAnyOrder(withPermutations(
+ (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+ }, () {
+ inAnyOrder(withPermutations(
+ (i, j, k) => isModifyEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+ });
+ });
+ test('does not suppress files with the same prefix as a directory', () async {
+ // Regression test for https://github.com/dart-lang/watcher/issues/83
+ writeFile('some_name.txt');
+
+ await startWatcher();
+
+ writeFile('some_name/some_name.txt');
+ deleteFile('some_name.txt');
+
+ await expectRemoveEvent('some_name.txt');
+ });
+}
diff --git a/pkgs/watcher/test/directory_watcher/polling_test.dart b/pkgs/watcher/test/directory_watcher/polling_test.dart
new file mode 100644
index 0000000..f4ec8f4
--- /dev/null
+++ b/pkgs/watcher/test/directory_watcher/polling_test.dart
@@ -0,0 +1,26 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:test/test.dart';
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+ // Use a short delay to make the tests run quickly.
+ watcherFactory = (dir) => PollingDirectoryWatcher(dir,
+ pollingDelay: const Duration(milliseconds: 100));
+
+ sharedTests();
+
+ test('does not notify if the modification time did not change', () async {
+ writeFile('a.txt', contents: 'before');
+ writeFile('b.txt', contents: 'before');
+ await startWatcher();
+ writeFile('a.txt', contents: 'after', updateModified: false);
+ writeFile('b.txt', contents: 'after');
+ await expectModifyEvent('b.txt');
+ });
+}
diff --git a/pkgs/watcher/test/directory_watcher/shared.dart b/pkgs/watcher/test/directory_watcher/shared.dart
new file mode 100644
index 0000000..1ebc78d
--- /dev/null
+++ b/pkgs/watcher/test/directory_watcher/shared.dart
@@ -0,0 +1,344 @@
+// 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 'package:watcher/src/utils.dart';
+
+import '../utils.dart';
+
+void sharedTests() {
+ test('does not notify for files that already exist when started', () async {
+ // Make some pre-existing files.
+ writeFile('a.txt');
+ writeFile('b.txt');
+
+ await startWatcher();
+
+ // Change one after the watcher is running.
+ writeFile('b.txt', contents: 'modified');
+
+ // We should get a modify event for the changed file, but no add events
+ // for them before this.
+ await expectModifyEvent('b.txt');
+ });
+
+ test('notifies when a file is added', () async {
+ await startWatcher();
+ writeFile('file.txt');
+ await expectAddEvent('file.txt');
+ });
+
+ test('notifies when a file is modified', () async {
+ writeFile('file.txt');
+ await startWatcher();
+ writeFile('file.txt', contents: 'modified');
+ await expectModifyEvent('file.txt');
+ });
+
+ test('notifies when a file is removed', () async {
+ writeFile('file.txt');
+ await startWatcher();
+ deleteFile('file.txt');
+ await expectRemoveEvent('file.txt');
+ });
+
+ test('notifies when a file is modified multiple times', () async {
+ writeFile('file.txt');
+ await startWatcher();
+ writeFile('file.txt', contents: 'modified');
+ await expectModifyEvent('file.txt');
+ writeFile('file.txt', contents: 'modified again');
+ await expectModifyEvent('file.txt');
+ });
+
+ test('notifies even if the file contents are unchanged', () async {
+ writeFile('a.txt', contents: 'same');
+ writeFile('b.txt', contents: 'before');
+ await startWatcher();
+
+ writeFile('a.txt', contents: 'same');
+ writeFile('b.txt', contents: 'after');
+ await inAnyOrder([isModifyEvent('a.txt'), isModifyEvent('b.txt')]);
+ });
+
+ test('when the watched directory is deleted, removes all files', () async {
+ writeFile('dir/a.txt');
+ writeFile('dir/b.txt');
+
+ await startWatcher(path: 'dir');
+
+ deleteDir('dir');
+ await inAnyOrder([isRemoveEvent('dir/a.txt'), isRemoveEvent('dir/b.txt')]);
+ });
+
+ test('when the watched directory is moved, removes all files', () async {
+ writeFile('dir/a.txt');
+ writeFile('dir/b.txt');
+
+ await startWatcher(path: 'dir');
+
+ renameDir('dir', 'moved_dir');
+ createDir('dir');
+ await inAnyOrder([isRemoveEvent('dir/a.txt'), isRemoveEvent('dir/b.txt')]);
+ });
+
+ // Regression test for b/30768513.
+ test(
+ "doesn't crash when the directory is moved immediately after a subdir "
+ 'is added', () async {
+ writeFile('dir/a.txt');
+ writeFile('dir/b.txt');
+
+ await startWatcher(path: 'dir');
+
+ createDir('dir/subdir');
+ renameDir('dir', 'moved_dir');
+ createDir('dir');
+ await inAnyOrder([isRemoveEvent('dir/a.txt'), isRemoveEvent('dir/b.txt')]);
+ });
+
+ group('moves', () {
+ test('notifies when a file is moved within the watched directory',
+ () async {
+ writeFile('old.txt');
+ await startWatcher();
+ renameFile('old.txt', 'new.txt');
+
+ await inAnyOrder([isAddEvent('new.txt'), isRemoveEvent('old.txt')]);
+ });
+
+ test('notifies when a file is moved from outside the watched directory',
+ () async {
+ writeFile('old.txt');
+ createDir('dir');
+ await startWatcher(path: 'dir');
+
+ renameFile('old.txt', 'dir/new.txt');
+ await expectAddEvent('dir/new.txt');
+ });
+
+ test('notifies when a file is moved outside the watched directory',
+ () async {
+ writeFile('dir/old.txt');
+ await startWatcher(path: 'dir');
+
+ renameFile('dir/old.txt', 'new.txt');
+ await expectRemoveEvent('dir/old.txt');
+ });
+
+ test('notifies when a file is moved onto an existing one', () async {
+ writeFile('from.txt');
+ writeFile('to.txt');
+ await startWatcher();
+
+ renameFile('from.txt', 'to.txt');
+ await inAnyOrder([isRemoveEvent('from.txt'), isModifyEvent('to.txt')]);
+ }, onPlatform: {
+ 'windows': const Skip('https://github.com/dart-lang/watcher/issues/125')
+ });
+ });
+
+ // Most of the time, when multiple filesystem actions happen in sequence,
+ // they'll be batched together and the watcher will see them all at once.
+ // These tests verify that the watcher normalizes and combine these events
+ // properly. However, very occasionally the events will be reported in
+ // separate batches, and the watcher will report them as though they occurred
+ // far apart in time, so each of these tests has a "backup case" to allow for
+ // that as well.
+ group('clustered changes', () {
+ test("doesn't notify when a file is created and then immediately removed",
+ () async {
+ writeFile('test.txt');
+ await startWatcher();
+ writeFile('file.txt');
+ deleteFile('file.txt');
+
+ // Backup case.
+ startClosingEventStream();
+ await allowEvents(() {
+ expectAddEvent('file.txt');
+ expectRemoveEvent('file.txt');
+ });
+ });
+
+ test(
+ 'reports a modification when a file is deleted and then immediately '
+ 'recreated', () async {
+ writeFile('file.txt');
+ await startWatcher();
+
+ deleteFile('file.txt');
+ writeFile('file.txt', contents: 're-created');
+
+ await allowEither(() {
+ expectModifyEvent('file.txt');
+ }, () {
+ // Backup case.
+ expectRemoveEvent('file.txt');
+ expectAddEvent('file.txt');
+ });
+ });
+
+ test(
+ 'reports a modification when a file is moved and then immediately '
+ 'recreated', () async {
+ writeFile('old.txt');
+ await startWatcher();
+
+ renameFile('old.txt', 'new.txt');
+ writeFile('old.txt', contents: 're-created');
+
+ await allowEither(() {
+ inAnyOrder([isModifyEvent('old.txt'), isAddEvent('new.txt')]);
+ }, () {
+ // Backup case.
+ expectRemoveEvent('old.txt');
+ expectAddEvent('new.txt');
+ expectAddEvent('old.txt');
+ });
+ });
+
+ test(
+ 'reports a removal when a file is modified and then immediately '
+ 'removed', () async {
+ writeFile('file.txt');
+ await startWatcher();
+
+ writeFile('file.txt', contents: 'modified');
+ deleteFile('file.txt');
+
+ // Backup case.
+ await allowModifyEvent('file.txt');
+
+ await expectRemoveEvent('file.txt');
+ });
+
+ test('reports an add when a file is added and then immediately modified',
+ () async {
+ await startWatcher();
+
+ writeFile('file.txt');
+ writeFile('file.txt', contents: 'modified');
+
+ await expectAddEvent('file.txt');
+
+ // Backup case.
+ startClosingEventStream();
+ await allowModifyEvent('file.txt');
+ });
+ });
+
+ group('subdirectories', () {
+ test('watches files in subdirectories', () async {
+ await startWatcher();
+ writeFile('a/b/c/d/file.txt');
+ await expectAddEvent('a/b/c/d/file.txt');
+ });
+
+ test(
+ 'notifies when a subdirectory is moved within the watched directory '
+ 'and then its contents are modified', () async {
+ writeFile('old/file.txt');
+ await startWatcher();
+
+ renameDir('old', 'new');
+ await inAnyOrder(
+ [isRemoveEvent('old/file.txt'), isAddEvent('new/file.txt')]);
+
+ writeFile('new/file.txt', contents: 'modified');
+ await expectModifyEvent('new/file.txt');
+ });
+
+ test('notifies when a file is replaced by a subdirectory', () async {
+ writeFile('new');
+ writeFile('old/file.txt');
+ await startWatcher();
+
+ deleteFile('new');
+ renameDir('old', 'new');
+ await inAnyOrder([
+ isRemoveEvent('new'),
+ isRemoveEvent('old/file.txt'),
+ isAddEvent('new/file.txt')
+ ]);
+ });
+
+ test('notifies when a subdirectory is replaced by a file', () async {
+ writeFile('old');
+ writeFile('new/file.txt');
+ await startWatcher();
+
+ renameDir('new', 'newer');
+ renameFile('old', 'new');
+ await inAnyOrder([
+ isRemoveEvent('new/file.txt'),
+ isAddEvent('newer/file.txt'),
+ isRemoveEvent('old'),
+ isAddEvent('new')
+ ]);
+ }, onPlatform: {
+ 'windows': const Skip('https://github.com/dart-lang/watcher/issues/21')
+ });
+
+ test('emits events for many nested files added at once', () async {
+ withPermutations((i, j, k) => writeFile('sub/sub-$i/sub-$j/file-$k.txt'));
+
+ createDir('dir');
+ await startWatcher(path: 'dir');
+ renameDir('sub', 'dir/sub');
+
+ await inAnyOrder(withPermutations(
+ (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+ });
+
+ test('emits events for many nested files removed at once', () async {
+ withPermutations(
+ (i, j, k) => writeFile('dir/sub/sub-$i/sub-$j/file-$k.txt'));
+
+ createDir('dir');
+ await startWatcher(path: 'dir');
+
+ // Rename the directory rather than deleting it because native watchers
+ // report a rename as a single DELETE event for the directory, whereas
+ // they report recursive deletion with DELETE events for every file in the
+ // directory.
+ renameDir('dir/sub', 'sub');
+
+ await inAnyOrder(withPermutations(
+ (i, j, k) => isRemoveEvent('dir/sub/sub-$i/sub-$j/file-$k.txt')));
+ });
+
+ test('emits events for many nested files moved at once', () async {
+ withPermutations(
+ (i, j, k) => writeFile('dir/old/sub-$i/sub-$j/file-$k.txt'));
+
+ createDir('dir');
+ await startWatcher(path: 'dir');
+ renameDir('dir/old', 'dir/new');
+
+ await inAnyOrder(unionAll(withPermutations((i, j, k) {
+ return {
+ isRemoveEvent('dir/old/sub-$i/sub-$j/file-$k.txt'),
+ isAddEvent('dir/new/sub-$i/sub-$j/file-$k.txt')
+ };
+ })));
+ });
+
+ test(
+ 'emits events for many files added at once in a subdirectory with the '
+ 'same name as a removed file', () async {
+ writeFile('dir/sub');
+ withPermutations((i, j, k) => writeFile('old/sub-$i/sub-$j/file-$k.txt'));
+ await startWatcher(path: 'dir');
+
+ deleteFile('dir/sub');
+ renameDir('old', 'dir/sub');
+
+ var events = withPermutations(
+ (i, j, k) => isAddEvent('dir/sub/sub-$i/sub-$j/file-$k.txt'));
+ events.add(isRemoveEvent('dir/sub'));
+ await inAnyOrder(events);
+ });
+ });
+}
diff --git a/pkgs/watcher/test/directory_watcher/windows_test.dart b/pkgs/watcher/test/directory_watcher/windows_test.dart
new file mode 100644
index 0000000..499e7fb
--- /dev/null
+++ b/pkgs/watcher/test/directory_watcher/windows_test.dart
@@ -0,0 +1,23 @@
+// 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.
+
+@TestOn('windows')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/windows.dart';
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+ watcherFactory = WindowsDirectoryWatcher.new;
+
+ group('Shared Tests:', sharedTests);
+
+ test('DirectoryWatcher creates a WindowsDirectoryWatcher on Windows', () {
+ expect(DirectoryWatcher('.'), const TypeMatcher<WindowsDirectoryWatcher>());
+ });
+}
diff --git a/pkgs/watcher/test/file_watcher/native_test.dart b/pkgs/watcher/test/file_watcher/native_test.dart
new file mode 100644
index 0000000..0d4ad63
--- /dev/null
+++ b/pkgs/watcher/test/file_watcher/native_test.dart
@@ -0,0 +1,22 @@
+// 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('linux || mac-os')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/file_watcher/native.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+ watcherFactory = NativeFileWatcher.new;
+
+ setUp(() {
+ writeFile('file.txt');
+ });
+
+ sharedTests();
+}
diff --git a/pkgs/watcher/test/file_watcher/polling_test.dart b/pkgs/watcher/test/file_watcher/polling_test.dart
new file mode 100644
index 0000000..861fcb2
--- /dev/null
+++ b/pkgs/watcher/test/file_watcher/polling_test.dart
@@ -0,0 +1,20 @@
+// 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/test.dart';
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+ watcherFactory = (file) =>
+ PollingFileWatcher(file, pollingDelay: const Duration(milliseconds: 100));
+
+ setUp(() {
+ writeFile('file.txt');
+ });
+
+ sharedTests();
+}
diff --git a/pkgs/watcher/test/file_watcher/shared.dart b/pkgs/watcher/test/file_watcher/shared.dart
new file mode 100644
index 0000000..081b92e
--- /dev/null
+++ b/pkgs/watcher/test/file_watcher/shared.dart
@@ -0,0 +1,73 @@
+// 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/test.dart';
+
+import '../utils.dart';
+
+void sharedTests() {
+ test("doesn't notify if the file isn't modified", () async {
+ await startWatcher(path: 'file.txt');
+ await pumpEventQueue();
+ deleteFile('file.txt');
+ await expectRemoveEvent('file.txt');
+ });
+
+ test('notifies when a file is modified', () async {
+ await startWatcher(path: 'file.txt');
+ writeFile('file.txt', contents: 'modified');
+ await expectModifyEvent('file.txt');
+ });
+
+ test('notifies when a file is removed', () async {
+ await startWatcher(path: 'file.txt');
+ deleteFile('file.txt');
+ await expectRemoveEvent('file.txt');
+ });
+
+ test('notifies when a file is modified multiple times', () async {
+ await startWatcher(path: 'file.txt');
+ writeFile('file.txt', contents: 'modified');
+ await expectModifyEvent('file.txt');
+ writeFile('file.txt', contents: 'modified again');
+ await expectModifyEvent('file.txt');
+ });
+
+ test('notifies even if the file contents are unchanged', () async {
+ await startWatcher(path: 'file.txt');
+ writeFile('file.txt');
+ await expectModifyEvent('file.txt');
+ });
+
+ test('emits a remove event when the watched file is moved away', () async {
+ await startWatcher(path: 'file.txt');
+ renameFile('file.txt', 'new.txt');
+ await expectRemoveEvent('file.txt');
+ });
+
+ test(
+ 'emits a modify event when another file is moved on top of the watched '
+ 'file', () async {
+ writeFile('old.txt');
+ await startWatcher(path: 'file.txt');
+ renameFile('old.txt', 'file.txt');
+ await expectModifyEvent('file.txt');
+ });
+
+ // Regression test for a race condition.
+ test('closes the watcher immediately after deleting the file', () async {
+ writeFile('old.txt');
+ var watcher = createWatcher(path: 'file.txt');
+ var sub = watcher.events.listen(null);
+
+ deleteFile('file.txt');
+ await Future<void>.delayed(const Duration(milliseconds: 10));
+ await sub.cancel();
+ });
+
+ test('ready completes even if file does not exist', () async {
+ // startWatcher awaits 'ready'
+ await startWatcher(path: 'foo/bar/baz');
+ });
+}
diff --git a/pkgs/watcher/test/no_subscription/linux_test.dart b/pkgs/watcher/test/no_subscription/linux_test.dart
new file mode 100644
index 0000000..aac0810
--- /dev/null
+++ b/pkgs/watcher/test/no_subscription/linux_test.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('linux')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/linux.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+ watcherFactory = LinuxDirectoryWatcher.new;
+
+ sharedTests();
+}
diff --git a/pkgs/watcher/test/no_subscription/mac_os_test.dart b/pkgs/watcher/test/no_subscription/mac_os_test.dart
new file mode 100644
index 0000000..55a8308
--- /dev/null
+++ b/pkgs/watcher/test/no_subscription/mac_os_test.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('mac-os')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/mac_os.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+ watcherFactory = MacOSDirectoryWatcher.new;
+
+ sharedTests();
+}
diff --git a/pkgs/watcher/test/no_subscription/polling_test.dart b/pkgs/watcher/test/no_subscription/polling_test.dart
new file mode 100644
index 0000000..bfd2958
--- /dev/null
+++ b/pkgs/watcher/test/no_subscription/polling_test.dart
@@ -0,0 +1,14 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+ watcherFactory = PollingDirectoryWatcher.new;
+
+ sharedTests();
+}
diff --git a/pkgs/watcher/test/no_subscription/shared.dart b/pkgs/watcher/test/no_subscription/shared.dart
new file mode 100644
index 0000000..e7a6144
--- /dev/null
+++ b/pkgs/watcher/test/no_subscription/shared.dart
@@ -0,0 +1,54 @@
+// 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 'dart:async';
+
+import 'package:async/async.dart';
+import 'package:test/test.dart';
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+
+void sharedTests() {
+ test('does not notify for changes when there are no subscribers', () async {
+ // Note that this test doesn't rely as heavily on the test functions in
+ // utils.dart because it needs to be very explicit about when the event
+ // stream is and is not subscribed.
+ var watcher = createWatcher();
+ var queue = StreamQueue(watcher.events);
+ unawaited(queue.hasNext);
+
+ var future =
+ expectLater(queue, emits(isWatchEvent(ChangeType.ADD, 'file.txt')));
+ expect(queue, neverEmits(anything));
+
+ await watcher.ready;
+
+ writeFile('file.txt');
+
+ await future;
+
+ // Unsubscribe.
+ await queue.cancel(immediate: true);
+
+ // Now write a file while we aren't listening.
+ writeFile('unwatched.txt');
+
+ queue = StreamQueue(watcher.events);
+ future =
+ expectLater(queue, emits(isWatchEvent(ChangeType.ADD, 'added.txt')));
+ expect(queue, neverEmits(isWatchEvent(ChangeType.ADD, 'unwatched.txt')));
+
+ // Wait until the watcher is ready to dispatch events again.
+ await watcher.ready;
+
+ // And add a third file.
+ writeFile('added.txt');
+
+ // Wait until we get an event for the third file.
+ await future;
+
+ await queue.cancel(immediate: true);
+ });
+}
diff --git a/pkgs/watcher/test/no_subscription/windows_test.dart b/pkgs/watcher/test/no_subscription/windows_test.dart
new file mode 100644
index 0000000..9f9e5a9
--- /dev/null
+++ b/pkgs/watcher/test/no_subscription/windows_test.dart
@@ -0,0 +1,18 @@
+// 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('windows')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/windows.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+ watcherFactory = WindowsDirectoryWatcher.new;
+
+ sharedTests();
+}
diff --git a/pkgs/watcher/test/path_set_test.dart b/pkgs/watcher/test/path_set_test.dart
new file mode 100644
index 0000000..61ab2cd
--- /dev/null
+++ b/pkgs/watcher/test/path_set_test.dart
@@ -0,0 +1,228 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:watcher/src/path_set.dart';
+
+Matcher containsPath(String path) => predicate(
+ (paths) => paths is PathSet && paths.contains(path),
+ 'set contains "$path"');
+
+Matcher containsDir(String path) => predicate(
+ (paths) => paths is PathSet && paths.containsDir(path),
+ 'set contains directory "$path"');
+
+void main() {
+ late PathSet paths;
+ setUp(() => paths = PathSet('root'));
+
+ group('adding a path', () {
+ test('stores the path in the set', () {
+ paths.add('root/path/to/file');
+ expect(paths, containsPath('root/path/to/file'));
+ });
+
+ test("that's a subdir of another path keeps both in the set", () {
+ paths.add('root/path');
+ paths.add('root/path/to/file');
+ expect(paths, containsPath('root/path'));
+ expect(paths, containsPath('root/path/to/file'));
+ });
+
+ test("that's not normalized normalizes the path before storing it", () {
+ paths.add('root/../root/path/to/../to/././file');
+ expect(paths, containsPath('root/path/to/file'));
+ });
+
+ test("that's absolute normalizes the path before storing it", () {
+ paths.add(p.absolute('root/path/to/file'));
+ expect(paths, containsPath('root/path/to/file'));
+ });
+ });
+
+ group('removing a path', () {
+ test("that's in the set removes and returns that path", () {
+ paths.add('root/path/to/file');
+ expect(paths.remove('root/path/to/file'),
+ unorderedEquals([p.normalize('root/path/to/file')]));
+ expect(paths, isNot(containsPath('root/path/to/file')));
+ });
+
+ test("that's not in the set returns an empty set", () {
+ paths.add('root/path/to/file');
+ expect(paths.remove('root/path/to/nothing'), isEmpty);
+ });
+
+ test("that's a directory removes and returns all files beneath it", () {
+ paths.add('root/outside');
+ paths.add('root/path/to/one');
+ paths.add('root/path/to/two');
+ paths.add('root/path/to/sub/three');
+
+ expect(
+ paths.remove('root/path'),
+ unorderedEquals([
+ 'root/path/to/one',
+ 'root/path/to/two',
+ 'root/path/to/sub/three'
+ ].map(p.normalize)));
+
+ expect(paths, containsPath('root/outside'));
+ expect(paths, isNot(containsPath('root/path/to/one')));
+ expect(paths, isNot(containsPath('root/path/to/two')));
+ expect(paths, isNot(containsPath('root/path/to/sub/three')));
+ });
+
+ test(
+ "that's a directory in the set removes and returns it and all files "
+ 'beneath it', () {
+ paths.add('root/path');
+ paths.add('root/path/to/one');
+ paths.add('root/path/to/two');
+ paths.add('root/path/to/sub/three');
+
+ expect(
+ paths.remove('root/path'),
+ unorderedEquals([
+ 'root/path',
+ 'root/path/to/one',
+ 'root/path/to/two',
+ 'root/path/to/sub/three'
+ ].map(p.normalize)));
+
+ expect(paths, isNot(containsPath('root/path')));
+ expect(paths, isNot(containsPath('root/path/to/one')));
+ expect(paths, isNot(containsPath('root/path/to/two')));
+ expect(paths, isNot(containsPath('root/path/to/sub/three')));
+ });
+
+ test("that's not normalized removes and returns the normalized path", () {
+ paths.add('root/path/to/file');
+ expect(paths.remove('root/../root/path/to/../to/./file'),
+ unorderedEquals([p.normalize('root/path/to/file')]));
+ });
+
+ test("that's absolute removes and returns the normalized path", () {
+ paths.add('root/path/to/file');
+ expect(paths.remove(p.absolute('root/path/to/file')),
+ unorderedEquals([p.normalize('root/path/to/file')]));
+ });
+ });
+
+ group('containsPath()', () {
+ test('returns false for a non-existent path', () {
+ paths.add('root/path/to/file');
+ expect(paths, isNot(containsPath('root/path/to/nothing')));
+ });
+
+ test("returns false for a directory that wasn't added explicitly", () {
+ paths.add('root/path/to/file');
+ expect(paths, isNot(containsPath('root/path')));
+ });
+
+ test('returns true for a directory that was added explicitly', () {
+ paths.add('root/path');
+ paths.add('root/path/to/file');
+ expect(paths, containsPath('root/path'));
+ });
+
+ test('with a non-normalized path normalizes the path before looking it up',
+ () {
+ paths.add('root/path/to/file');
+ expect(paths, containsPath('root/../root/path/to/../to/././file'));
+ });
+
+ test('with an absolute path normalizes the path before looking it up', () {
+ paths.add('root/path/to/file');
+ expect(paths, containsPath(p.absolute('root/path/to/file')));
+ });
+ });
+
+ group('containsDir()', () {
+ test('returns true for a directory that was added implicitly', () {
+ paths.add('root/path/to/file');
+ expect(paths, containsDir('root/path'));
+ expect(paths, containsDir('root/path/to'));
+ });
+
+ test('returns true for a directory that was added explicitly', () {
+ paths.add('root/path');
+ paths.add('root/path/to/file');
+ expect(paths, containsDir('root/path'));
+ });
+
+ test("returns false for a directory that wasn't added", () {
+ expect(paths, isNot(containsDir('root/nothing')));
+ });
+
+ test('returns false for a non-directory path that was added', () {
+ paths.add('root/path/to/file');
+ expect(paths, isNot(containsDir('root/path/to/file')));
+ });
+
+ test(
+ 'returns false for a directory that was added implicitly and then '
+ 'removed implicitly', () {
+ paths.add('root/path/to/file');
+ paths.remove('root/path/to/file');
+ expect(paths, isNot(containsDir('root/path')));
+ });
+
+ test(
+ 'returns false for a directory that was added explicitly whose '
+ 'children were then removed', () {
+ paths.add('root/path');
+ paths.add('root/path/to/file');
+ paths.remove('root/path/to/file');
+ expect(paths, isNot(containsDir('root/path')));
+ });
+
+ test('with a non-normalized path normalizes the path before looking it up',
+ () {
+ paths.add('root/path/to/file');
+ expect(paths, containsDir('root/../root/path/to/../to/.'));
+ });
+
+ test('with an absolute path normalizes the path before looking it up', () {
+ paths.add('root/path/to/file');
+ expect(paths, containsDir(p.absolute('root/path')));
+ });
+ });
+
+ group('paths', () {
+ test('returns paths added to the set', () {
+ paths.add('root/path');
+ paths.add('root/path/to/one');
+ paths.add('root/path/to/two');
+
+ expect(
+ paths.paths,
+ unorderedEquals([
+ 'root/path',
+ 'root/path/to/one',
+ 'root/path/to/two',
+ ].map(p.normalize)));
+ });
+
+ test("doesn't return paths removed from the set", () {
+ paths.add('root/path/to/one');
+ paths.add('root/path/to/two');
+ paths.remove('root/path/to/two');
+
+ expect(paths.paths, unorderedEquals([p.normalize('root/path/to/one')]));
+ });
+ });
+
+ group('clear', () {
+ test('removes all paths from the set', () {
+ paths.add('root/path');
+ paths.add('root/path/to/one');
+ paths.add('root/path/to/two');
+
+ paths.clear();
+ expect(paths.paths, isEmpty);
+ });
+ });
+}
diff --git a/pkgs/watcher/test/ready/linux_test.dart b/pkgs/watcher/test/ready/linux_test.dart
new file mode 100644
index 0000000..aac0810
--- /dev/null
+++ b/pkgs/watcher/test/ready/linux_test.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('linux')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/linux.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+ watcherFactory = LinuxDirectoryWatcher.new;
+
+ sharedTests();
+}
diff --git a/pkgs/watcher/test/ready/mac_os_test.dart b/pkgs/watcher/test/ready/mac_os_test.dart
new file mode 100644
index 0000000..55a8308
--- /dev/null
+++ b/pkgs/watcher/test/ready/mac_os_test.dart
@@ -0,0 +1,18 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+@TestOn('mac-os')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/mac_os.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+ watcherFactory = MacOSDirectoryWatcher.new;
+
+ sharedTests();
+}
diff --git a/pkgs/watcher/test/ready/polling_test.dart b/pkgs/watcher/test/ready/polling_test.dart
new file mode 100644
index 0000000..bfd2958
--- /dev/null
+++ b/pkgs/watcher/test/ready/polling_test.dart
@@ -0,0 +1,14 @@
+// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:watcher/watcher.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+ watcherFactory = PollingDirectoryWatcher.new;
+
+ sharedTests();
+}
diff --git a/pkgs/watcher/test/ready/shared.dart b/pkgs/watcher/test/ready/shared.dart
new file mode 100644
index 0000000..ab2c3e1
--- /dev/null
+++ b/pkgs/watcher/test/ready/shared.dart
@@ -0,0 +1,84 @@
+// 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 'dart:async';
+
+import 'package:test/test.dart';
+
+import '../utils.dart';
+
+void sharedTests() {
+ test('ready does not complete until after subscription', () async {
+ var watcher = createWatcher();
+
+ var ready = false;
+ unawaited(watcher.ready.then((_) {
+ ready = true;
+ }));
+ await pumpEventQueue();
+
+ expect(ready, isFalse);
+
+ // Subscribe to the events.
+ var subscription = watcher.events.listen((event) {});
+
+ await watcher.ready;
+
+ // Should eventually be ready.
+ expect(watcher.isReady, isTrue);
+
+ await subscription.cancel();
+ });
+
+ test('ready completes immediately when already ready', () async {
+ var watcher = createWatcher();
+
+ // Subscribe to the events.
+ var subscription = watcher.events.listen((event) {});
+
+ // Allow watcher to become ready
+ await watcher.ready;
+
+ // Ensure ready completes immediately
+ expect(
+ watcher.ready.timeout(
+ const Duration(milliseconds: 0),
+ onTimeout: () => throw StateError('Does not complete immediately'),
+ ),
+ completes,
+ );
+
+ await subscription.cancel();
+ });
+
+ test('ready returns a future that does not complete after unsubscribing',
+ () async {
+ var watcher = createWatcher();
+
+ // Subscribe to the events.
+ var subscription = watcher.events.listen((event) {});
+
+ // Wait until ready.
+ await watcher.ready;
+
+ // Now unsubscribe.
+ await subscription.cancel();
+
+ // Should be back to not ready.
+ expect(watcher.ready, doesNotComplete);
+ });
+
+ test('ready completes even if directory does not exist', () async {
+ var watcher = createWatcher(path: 'does/not/exist');
+
+ // Subscribe to the events (else ready will never fire).
+ var subscription = watcher.events.listen((event) {}, onError: (error) {});
+
+ // Expect ready still completes.
+ await watcher.ready;
+
+ // Now unsubscribe.
+ await subscription.cancel();
+ });
+}
diff --git a/pkgs/watcher/test/ready/windows_test.dart b/pkgs/watcher/test/ready/windows_test.dart
new file mode 100644
index 0000000..9f9e5a9
--- /dev/null
+++ b/pkgs/watcher/test/ready/windows_test.dart
@@ -0,0 +1,18 @@
+// 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('windows')
+library;
+
+import 'package:test/test.dart';
+import 'package:watcher/src/directory_watcher/windows.dart';
+
+import '../utils.dart';
+import 'shared.dart';
+
+void main() {
+ watcherFactory = WindowsDirectoryWatcher.new;
+
+ sharedTests();
+}
diff --git a/pkgs/watcher/test/utils.dart b/pkgs/watcher/test/utils.dart
new file mode 100644
index 0000000..7867b9f
--- /dev/null
+++ b/pkgs/watcher/test/utils.dart
@@ -0,0 +1,288 @@
+// 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 'dart:async';
+import 'dart:io';
+
+import 'package:async/async.dart';
+import 'package:path/path.dart' as p;
+import 'package:test/test.dart';
+import 'package:test_descriptor/test_descriptor.dart' as d;
+import 'package:watcher/src/stat.dart';
+import 'package:watcher/watcher.dart';
+
+typedef WatcherFactory = Watcher Function(String directory);
+
+/// Sets the function used to create the watcher.
+set watcherFactory(WatcherFactory factory) {
+ _watcherFactory = factory;
+}
+
+/// The mock modification times (in milliseconds since epoch) for each file.
+///
+/// The actual file system has pretty coarse granularity for file modification
+/// times. This means using the real file system requires us to put delays in
+/// the tests to ensure we wait long enough between operations for the mod time
+/// to be different.
+///
+/// Instead, we'll just mock that out. Each time a file is written, we manually
+/// increment the mod time for that file instantly.
+final _mockFileModificationTimes = <String, int>{};
+
+late WatcherFactory _watcherFactory;
+
+/// Creates a new [Watcher] that watches a temporary file or directory.
+///
+/// If [path] is provided, watches a subdirectory in the sandbox with that name.
+Watcher createWatcher({String? path}) {
+ if (path == null) {
+ path = d.sandbox;
+ } else {
+ path = p.join(d.sandbox, path);
+ }
+
+ return _watcherFactory(path);
+}
+
+/// The stream of events from the watcher started with [startWatcher].
+late StreamQueue<WatchEvent> _watcherEvents;
+
+/// Whether the event stream has been closed.
+///
+/// If this is not done by a test (by calling [startClosingEventStream]) it will
+/// be done automatically via [addTearDown] in [startWatcher].
+var _hasClosedStream = true;
+
+/// Creates a new [Watcher] that watches a temporary file or directory and
+/// starts monitoring it for events.
+///
+/// If [path] is provided, watches a path in the sandbox with that name.
+Future<void> startWatcher({String? path}) async {
+ mockGetModificationTime((path) {
+ final normalized = p.normalize(p.relative(path, from: d.sandbox));
+
+ // Make sure we got a path in the sandbox.
+ assert(p.isRelative(normalized) && !normalized.startsWith('..'),
+ 'Path is not in the sandbox: $path not in ${d.sandbox}');
+
+ var mtime = _mockFileModificationTimes[normalized];
+ return mtime != null ? DateTime.fromMillisecondsSinceEpoch(mtime) : null;
+ });
+
+ // We want to wait until we're ready *after* we subscribe to the watcher's
+ // events.
+ var watcher = createWatcher(path: path);
+ _watcherEvents = StreamQueue(watcher.events);
+ // Forces a subscription to the underlying stream.
+ unawaited(_watcherEvents.hasNext);
+
+ _hasClosedStream = false;
+ addTearDown(startClosingEventStream);
+
+ await watcher.ready;
+}
+
+/// Schedule closing the watcher stream after the event queue has been pumped.
+///
+/// This is necessary when events are allowed to occur, but don't have to occur,
+/// at the end of a test. Otherwise, if they don't occur, the test will wait
+/// indefinitely because they might in the future and because the watcher is
+/// normally only closed after the test completes.
+void startClosingEventStream() async {
+ if (_hasClosedStream) return;
+ _hasClosedStream = true;
+ await pumpEventQueue();
+ await _watcherEvents.cancel(immediate: true);
+}
+
+/// A list of [StreamMatcher]s that have been collected using
+/// [_collectStreamMatcher].
+List<StreamMatcher>? _collectedStreamMatchers;
+
+/// Collects all stream matchers that are registered within [block] into a
+/// single stream matcher.
+///
+/// The returned matcher will match each of the collected matchers in order.
+StreamMatcher _collectStreamMatcher(void Function() block) {
+ var oldStreamMatchers = _collectedStreamMatchers;
+ var collectedStreamMatchers = _collectedStreamMatchers = <StreamMatcher>[];
+ try {
+ block();
+ return emitsInOrder(collectedStreamMatchers);
+ } finally {
+ _collectedStreamMatchers = oldStreamMatchers;
+ }
+}
+
+/// Either add [streamMatcher] as an expectation to [_watcherEvents], or collect
+/// it with [_collectStreamMatcher].
+///
+/// [streamMatcher] can be a [StreamMatcher], a [Matcher], or a value.
+Future _expectOrCollect(Matcher streamMatcher) {
+ var collectedStreamMatchers = _collectedStreamMatchers;
+ if (collectedStreamMatchers != null) {
+ collectedStreamMatchers.add(emits(streamMatcher));
+ return Future.sync(() {});
+ } else {
+ return expectLater(_watcherEvents, emits(streamMatcher));
+ }
+}
+
+/// Expects that [matchers] will match emitted events in any order.
+///
+/// [matchers] may be [Matcher]s or values, but not [StreamMatcher]s.
+Future inAnyOrder(Iterable matchers) {
+ matchers = matchers.toSet();
+ return _expectOrCollect(emitsInAnyOrder(matchers));
+}
+
+/// Expects that the expectations established in either [block1] or [block2]
+/// will match the emitted events.
+///
+/// If both blocks match, the one that consumed more events will be used.
+Future allowEither(void Function() block1, void Function() block2) =>
+ _expectOrCollect(emitsAnyOf(
+ [_collectStreamMatcher(block1), _collectStreamMatcher(block2)]));
+
+/// Allows the expectations established in [block] to match the emitted events.
+///
+/// If the expectations in [block] don't match, no error will be raised and no
+/// events will be consumed. If this is used at the end of a test,
+/// [startClosingEventStream] should be called before it.
+Future allowEvents(void Function() block) =>
+ _expectOrCollect(mayEmit(_collectStreamMatcher(block)));
+
+/// Returns a StreamMatcher that matches a [WatchEvent] with the given [type]
+/// and [path].
+Matcher isWatchEvent(ChangeType type, String path) {
+ return predicate((e) {
+ return e is WatchEvent &&
+ e.type == type &&
+ e.path == p.join(d.sandbox, p.normalize(path));
+ }, 'is $type $path');
+}
+
+/// Returns a [Matcher] that matches a [WatchEvent] for an add event for [path].
+Matcher isAddEvent(String path) => isWatchEvent(ChangeType.ADD, path);
+
+/// Returns a [Matcher] that matches a [WatchEvent] for a modification event for
+/// [path].
+Matcher isModifyEvent(String path) => isWatchEvent(ChangeType.MODIFY, path);
+
+/// Returns a [Matcher] that matches a [WatchEvent] for a removal event for
+/// [path].
+Matcher isRemoveEvent(String path) => isWatchEvent(ChangeType.REMOVE, path);
+
+/// Expects that the next event emitted will be for an add event for [path].
+Future expectAddEvent(String path) =>
+ _expectOrCollect(isWatchEvent(ChangeType.ADD, path));
+
+/// Expects that the next event emitted will be for a modification event for
+/// [path].
+Future expectModifyEvent(String path) =>
+ _expectOrCollect(isWatchEvent(ChangeType.MODIFY, path));
+
+/// Expects that the next event emitted will be for a removal event for [path].
+Future expectRemoveEvent(String path) =>
+ _expectOrCollect(isWatchEvent(ChangeType.REMOVE, path));
+
+/// Consumes a modification event for [path] if one is emitted at this point in
+/// the schedule, but doesn't throw an error if it isn't.
+///
+/// If this is used at the end of a test, [startClosingEventStream] should be
+/// called before it.
+Future allowModifyEvent(String path) =>
+ _expectOrCollect(mayEmit(isWatchEvent(ChangeType.MODIFY, path)));
+
+/// Track a fake timestamp to be used when writing files. This always increases
+/// so that files that are deleted and re-created do not have their timestamp
+/// set back to a previously used value.
+int _nextTimestamp = 1;
+
+/// Schedules writing a file in the sandbox at [path] with [contents].
+///
+/// If [contents] is omitted, creates an empty file. If [updateModified] is
+/// `false`, the mock file modification time is not changed.
+void writeFile(String path, {String? contents, bool? updateModified}) {
+ contents ??= '';
+ updateModified ??= true;
+
+ var fullPath = p.join(d.sandbox, path);
+
+ // Create any needed subdirectories.
+ var dir = Directory(p.dirname(fullPath));
+ if (!dir.existsSync()) {
+ dir.createSync(recursive: true);
+ }
+
+ File(fullPath).writeAsStringSync(contents);
+
+ if (updateModified) {
+ path = p.normalize(path);
+
+ _mockFileModificationTimes[path] = _nextTimestamp++;
+ }
+}
+
+/// Schedules deleting a file in the sandbox at [path].
+void deleteFile(String path) {
+ File(p.join(d.sandbox, path)).deleteSync();
+
+ _mockFileModificationTimes.remove(path);
+}
+
+/// Schedules renaming a file in the sandbox from [from] to [to].
+void renameFile(String from, String to) {
+ File(p.join(d.sandbox, from)).renameSync(p.join(d.sandbox, to));
+
+ // Make sure we always use the same separator on Windows.
+ to = p.normalize(to);
+
+ _mockFileModificationTimes.update(to, (value) => value + 1,
+ ifAbsent: () => 1);
+}
+
+/// Schedules creating a directory in the sandbox at [path].
+void createDir(String path) {
+ Directory(p.join(d.sandbox, path)).createSync();
+}
+
+/// Schedules renaming a directory in the sandbox from [from] to [to].
+void renameDir(String from, String to) {
+ Directory(p.join(d.sandbox, from)).renameSync(p.join(d.sandbox, to));
+
+ // Migrate timestamps for any files in this folder.
+ final knownFilePaths = _mockFileModificationTimes.keys.toList();
+ for (final filePath in knownFilePaths) {
+ if (p.isWithin(from, filePath)) {
+ _mockFileModificationTimes[filePath.replaceAll(from, to)] =
+ _mockFileModificationTimes[filePath]!;
+ _mockFileModificationTimes.remove(filePath);
+ }
+ }
+}
+
+/// Schedules deleting a directory in the sandbox at [path].
+void deleteDir(String path) {
+ Directory(p.join(d.sandbox, path)).deleteSync(recursive: true);
+}
+
+/// Runs [callback] with every permutation of non-negative numbers for each
+/// argument less than [limit].
+///
+/// Returns a set of all values returns by [callback].
+///
+/// [limit] defaults to 3.
+Set<S> withPermutations<S>(S Function(int, int, int) callback, {int? limit}) {
+ limit ??= 3;
+ var results = <S>{};
+ for (var i = 0; i < limit; i++) {
+ for (var j = 0; j < limit; j++) {
+ for (var k = 0; k < limit; k++) {
+ results.add(callback(i, j, k));
+ }
+ }
+ }
+ return results;
+}