├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.yaml │ ├── build.yaml │ ├── chore.yaml │ ├── ci.yaml │ ├── config.yml │ ├── documentation.yaml │ ├── feature_request.yaml │ ├── performance.yaml │ ├── refactor.yaml │ ├── revert.yaml │ ├── style.yaml │ └── test.yaml ├── PULL_REQUEST_TEMPLATE.md ├── cspell.json ├── dependabot.yaml └── workflows │ ├── pana_score.yaml │ ├── pub_publish.yaml │ ├── semantic_pull_request.yaml │ ├── spell_check.yaml │ ├── sync_labels.yaml │ └── very_good_test_runner.yaml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── build.yaml ├── coverage_badge.svg ├── example └── main.dart ├── lib ├── src │ ├── models │ │ ├── models.dart │ │ ├── test_event.dart │ │ └── test_event.g.dart │ └── very_good_test_runner.dart └── very_good_test_runner.dart ├── pubspec.yaml ├── test └── src │ ├── models │ └── test_event_test.dart │ └── very_good_test_runner_test.dart └── tool └── release_ready.sh /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Every request must be reviewed and accepted by: 2 | 3 | * @VeryGoodOpenSource/codeowners 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help us improve 3 | title: "fix: " 4 | labels: [bug] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: A clear and concise description of what the bug is. 11 | placeholder: "Describe the bug." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: setps-to-reproduce 16 | attributes: 17 | label: Steps To Reproduce 18 | description: A set of instructions, step by step, explaining how to reproduce the bug. 19 | placeholder: | 20 | 1. Go to '...' 21 | 2. Click on '....' 22 | 3. Scroll down to '....' 23 | 4. See error 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: expected-behavior 28 | attributes: 29 | label: Expected Behavior 30 | description: A clear and concise description of what you expected to happen. 31 | placeholder: "Describe what you expected to happen." 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: additional-context 36 | attributes: 37 | label: Additional Context 38 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 39 | placeholder: "Provide context here." 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build System 2 | description: Changes that affect the build system or external dependencies 3 | title: "build: " 4 | labels: [build] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Describe what changes need to be done to the build system and why 11 | placeholder: "Describe the build system change." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] All CI/CD checks are passing. 21 | - [ ] There is no drop in the test coverage percentage. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: additional-context 26 | attributes: 27 | label: Additional Context 28 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 29 | placeholder: "Provide context here." 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/chore.yaml: -------------------------------------------------------------------------------- 1 | name: Chore 2 | description: Other changes that don't modify source or test files 3 | title: "chore: " 4 | labels: [chore] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Clearly describe what change is needed and why. If this changes code then please use another issue type. 11 | placeholder: "Provide a description of the chore." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] No functional changes to the code. 21 | - [ ] All CI/CD checks are passing. 22 | - [ ] There is no drop in the test coverage percentage. 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: additional-context 27 | attributes: 28 | label: Additional Context 29 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 30 | placeholder: "Provide context here." 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | description: Changes to the CI configuration files and scripts 3 | title: "ci: " 4 | labels: [ci] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Describe what changes need to be done to the CI/CD system and why. 11 | placeholder: "Provide a description of the changes that need to be done to the CI/CD system." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] All CI/CD checks are passing. 21 | - [ ] There is no drop in the test coverage percentage. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: additional-context 26 | attributes: 27 | label: Additional Context 28 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 29 | placeholder: "Provide context here." 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yaml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | description: Improve the documentation so all collaborators have a common understanding 3 | title: "docs: " 4 | labels: [documentation] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Clearly describe what documentation you are looking to add or improve. 11 | placeholder: "Provide a description of the documentation changes." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] No functional changes to the code. 21 | - [ ] All CI/CD checks are passing. 22 | - [ ] There is no drop in the test coverage percentage. 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: additional-context 27 | attributes: 28 | label: Additional Context 29 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 30 | placeholder: "Provide context here." 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: A new feature to be added to the project 3 | title: "feat: " 4 | labels: [feature] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Clearly describe what you are looking to add. The more business/user context the better. 11 | placeholder: "Provide a description of the feature." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] All CI/CD checks are passing. 21 | - [ ] There is no drop in the test coverage percentage. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: additional-context 26 | attributes: 27 | label: Additional Context 28 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 29 | placeholder: "Provide context here." 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/performance.yaml: -------------------------------------------------------------------------------- 1 | name: Performance Update 2 | description: A code change that improves performance 3 | title: "perf: " 4 | labels: [performance] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Clearly describe what code needs to be changed and what the performance impact is going to be. Bonus point's if you can tie this directly to user experience. 11 | placeholder: " Provide a description of the performance update." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] All CI/CD checks are passing. 21 | - [ ] There is no drop in the test coverage percentage. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: additional-context 26 | attributes: 27 | label: Additional Context 28 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 29 | placeholder: "Provide context here." 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/refactor.yaml: -------------------------------------------------------------------------------- 1 | name: Refactor 2 | description: A code change that neither fixes a bug nor adds a feature 3 | title: "refactor: " 4 | labels: [refactor] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize. 11 | placeholder: "Provide a description of the refactor." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] All CI/CD checks are passing. 21 | - [ ] There is no drop in the test coverage percentage. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: additional-context 26 | attributes: 27 | label: Additional Context 28 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 29 | placeholder: "Provide context here." 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/revert.yaml: -------------------------------------------------------------------------------- 1 | name: Revert 2 | description: Revert a previous commit 3 | title: "revert: " 4 | labels: [revert] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Provide a link to a PR/Commit that you are looking to revert and why. 11 | placeholder: "Provide a description of and link to the commit that needs to be reverted." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] Change has been reverted. 21 | - [ ] No change in unit/widget test coverage has happened. 22 | - [ ] A new ticket is created for any follow on work that needs to happen. 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: additional-context 27 | attributes: 28 | label: Additional Context 29 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 30 | placeholder: "Provide context here." 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/style.yaml: -------------------------------------------------------------------------------- 1 | name: Style 2 | description: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 3 | title: "style: " 4 | labels: [style] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: Clearly describe what you are looking to change and why. 11 | placeholder: "Provide a description of the style changes." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] All CI/CD checks are passing. 21 | - [ ] There is no drop in the unit or widget test coverage percentage. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: additional-context 26 | attributes: 27 | label: Additional Context 28 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 29 | placeholder: "Provide context here." 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | description: Adding missing tests or correcting existing tests 3 | title: "test: " 4 | labels: [test] 5 | body: 6 | - type: textarea 7 | id: description 8 | attributes: 9 | label: Description 10 | description: List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past. 11 | placeholder: "Provide a description of the tests that need to be added or changed." 12 | validations: 13 | required: true 14 | - type: textarea 15 | id: requirements 16 | attributes: 17 | label: Requirements 18 | description: The list of requirements that need to be met in order to consider the ticket to be completed. Please be as explicit as possible. 19 | value: | 20 | - [ ] All CI/CD checks are passing. 21 | - [ ] There is no drop in the test coverage percentage. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: additional-context 26 | attributes: 27 | label: Additional Context 28 | description: Add any other context, including links/screenshots/video recordings/etc about the problem here. 29 | placeholder: "Provide context here." 30 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## Status 10 | 11 | **READY/IN DEVELOPMENT/HOLD** 12 | 13 | ## Description 14 | 15 | 16 | 17 | ## Type of Change 18 | 19 | 20 | 21 | - [ ] ✨ New feature (non-breaking change which adds functionality) 22 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) 23 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) 24 | - [ ] 🧹 Code refactor 25 | - [ ] ✅ Build configuration change 26 | - [ ] 📝 Documentation 27 | - [ ] 🗑️ Chore 28 | -------------------------------------------------------------------------------- /.github/cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2", 3 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", 4 | "dictionaries": ["vgv_allowed", "vgv_forbidden"], 5 | "dictionaryDefinitions": [ 6 | { 7 | "name": "vgv_allowed", 8 | "path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/allowed.txt", 9 | "description": "Allowed VGV Spellings" 10 | }, 11 | { 12 | "name": "vgv_forbidden", 13 | "path": "https://raw.githubusercontent.com/verygoodopensource/very_good_dictionaries/main/forbidden.txt", 14 | "description": "Forbidden VGV Spellings" 15 | } 16 | ], 17 | "useGitignore": true 18 | } 19 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "pub" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/pana_score.yaml: -------------------------------------------------------------------------------- 1 | name: pana_score 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | pull_request: 12 | branches: 13 | - main 14 | 15 | jobs: 16 | pana_score: 17 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/pana.yml@v1 18 | -------------------------------------------------------------------------------- /.github/workflows/pub_publish.yaml: -------------------------------------------------------------------------------- 1 | name: pub_publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+*" 7 | 8 | jobs: 9 | publish: 10 | permissions: 11 | id-token: write # Required for authentication using OIDC 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: 📚 Git Checkout 15 | uses: actions/checkout@v4 16 | - name: 🎯 Setup Dart 17 | uses: dart-lang/setup-dart@v1 18 | - name: 🐦 Setup Flutter 19 | uses: subosito/flutter-action@v2 20 | - name: 📦 Install Dependencies 21 | run: flutter pub get 22 | - name: 🌵 Dry Run 23 | run: dart pub publish --dry-run 24 | - name: 📢 Publish 25 | run: dart pub publish --force 26 | -------------------------------------------------------------------------------- /.github/workflows/semantic_pull_request.yaml: -------------------------------------------------------------------------------- 1 | name: semantic_pull_request 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | pull_request: 12 | branches: 13 | - main 14 | 15 | jobs: 16 | semantic-pull-request: 17 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/semantic_pull_request.yml@v1 18 | -------------------------------------------------------------------------------- /.github/workflows/spell_check.yaml: -------------------------------------------------------------------------------- 1 | name: spell_check 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | pull_request: 12 | branches: 13 | - main 14 | 15 | jobs: 16 | spell-check: 17 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/spell_check.yml@v1 18 | with: 19 | includes: "**/*.md" 20 | modified_files_only: false 21 | -------------------------------------------------------------------------------- /.github/workflows/sync_labels.yaml: -------------------------------------------------------------------------------- 1 | name: ♻️ Sync Labels 2 | 3 | on: 4 | push: 5 | paths: 6 | - .github/labels.yml 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | jobs: 12 | labels: 13 | name: ♻️ Sync labels 14 | runs-on: ubuntu-20.04 15 | steps: 16 | - name: ⤵️ Check out code from GitHub 17 | uses: actions/checkout@v4 18 | 19 | - name: 🚀 Run Label Sync 20 | uses: srealmoreno/label-sync-action@v2 21 | with: 22 | config-file: https://raw.githubusercontent.com/VeryGoodOpenSource/.github/main/.github/labels.yml 23 | -------------------------------------------------------------------------------- /.github/workflows/very_good_test_runner.yaml: -------------------------------------------------------------------------------- 1 | name: very_good_test_runner 2 | concurrency: 3 | group: ${{ github.workflow }}-${{ github.ref }} 4 | cancel-in-progress: true 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | jobs: 15 | build: 16 | strategy: 17 | matrix: 18 | flutter_version: 19 | - "3.24.0" # The Flutter version with a Dart SDK that matches the minimum Dart SDK version supported by the package. 20 | - "3.x" # The Flutter version with a Dart SDK that matches the maximum Dart SDK version supported by the package. 21 | 22 | uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/flutter_package.yml@v1 23 | with: 24 | flutter_version: ${{ matrix.flutter_version }} 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://www.dartlang.org/guides/libraries/private-files 2 | 3 | # Files and directories created by pub 4 | .dart_tool/ 5 | .packages 6 | build/ 7 | pubspec.lock 8 | 9 | # Android Studio and IntelliJ 10 | .idea 11 | 12 | # Files related to tests 13 | coverage/ 14 | .test_coverage.dart -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 0.3.0 2 | 3 | - chore: tighten dependencies ([#29](https://github.com/VeryGoodOpenSource/very_good_test_runner/pull/29)) 4 | 5 | # 0.2.0 6 | 7 | - ci: use semantic-pull-request workflow ([#8](https://github.com/VeryGoodOpenSource/very_good_test_runner/pull/8)) 8 | - ci: add dependabot ([#9](https://github.com/VeryGoodOpenSource/very_good_test_runner/pull/9)) 9 | - feat!: update workflows, add spellcheck, update very_good_analysis ([#16](https://github.com/VeryGoodOpenSource/very_good_test_runner/pull/16)) 10 | - feat!: Update Dart to 3.0.0 ([#20](https://github.com/VeryGoodOpenSource/very_good_test_runner/pull/20)) 11 | 12 | # [0.1.2](https://github.com/VeryGoodOpenSource/very_good_test_runner/compare/v0.1.1...v0.1.2) (2022-05-05) 13 | 14 | ### Features 15 | 16 | - add ExitTestEvent to indicate the process has exited ([#6](https://github.com/VeryGoodOpenSource/very_good_test_runner/issues/6)) ([8f37fbd](https://github.com/VeryGoodOpenSource/very_good_test_runner/commit/8f37fbde9dafaef5702b75c8aad06e4fdca0d015)) 17 | 18 | ## [0.1.1](https://github.com/VeryGoodOpenSource/very_good_test_runner/compare/v0.1.0...v0.1.1) (2022-03-09) 19 | 20 | ### Bug Fixes 21 | 22 | - README logo order ([aaba957](https://github.com/VeryGoodOpenSource/very_good_test_runner/commit/aaba957d3bc6739e7d7314cdb8a0dd1fae5e696d)) 23 | 24 | # [0.1.0](https://github.com/VeryGoodOpenSource/very_good_test_runner/compare/3001cef12ee5fb5c52a1652ff24209037a225ece...v0.1.0) (2022-03-09) 25 | 26 | ### Features 27 | 28 | - add dartTest ([#3](https://github.com/VeryGoodOpenSource/very_good_test_runner/issues/3)) ([10336d2](https://github.com/VeryGoodOpenSource/very_good_test_runner/commit/10336d2bce2fbb0e19d85d636737dcc7b92b8e77)) 29 | - add flutterTest ([#2](https://github.com/VeryGoodOpenSource/very_good_test_runner/issues/2)) ([4db0c6b](https://github.com/VeryGoodOpenSource/very_good_test_runner/commit/4db0c6b0da2d99acad0582a33b8e5885bdc6bf36)) 30 | - export TestEvents ([#1](https://github.com/VeryGoodOpenSource/very_good_test_runner/issues/1)) ([1de845a](https://github.com/VeryGoodOpenSource/very_good_test_runner/commit/1de845a0f6ce3fcbdfc77d10f76fe38f4c77a858)) 31 | - generate package via very_good_cli 🦄 ([3001cef](https://github.com/VeryGoodOpenSource/very_good_test_runner/commit/3001cef12ee5fb5c52a1652ff24209037a225ece)) 32 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at hello@verygood.ventures. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faqs 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Very Good Test Runner 2 | 3 | First off, thanks for taking the time to contribute! 🎉👍 4 | 5 | This project is opinionated and follows patterns and practices used by the team at [Very Good Ventures][very_good_ventures_link]. **At this time, we welcome bug tickets but will not be accepting feature requests because the roadmap and scope of this project is still being defined.** 6 | 7 | ## Creating a Bug Report 8 | 9 | We highly recommend [creating an issue][bug_report_link] if you have found a bug rather than immediately opening a pull request. This lets us reach an agreement on a fix before you put significant effort into a pull request. Please use the built-in [Bug Report][bug_report_link] template and provide as much information as possible including detailed reproduction steps. Once one of the package maintainers has reviewed the issue and an agreement is reached regarding the fix, a pull request can be created. 10 | 11 | ## Creating a Pull Request 12 | 13 | Before creating a pull request please: 14 | 15 | 1. Fork the repository and create your branch from `main`. 16 | 1. Install all dependencies (`flutter packages get` or `pub get`). 17 | 1. Squash your commits and ensure you have a meaningful, [semantic][conventional_commits_link] commit message. 18 | 1. Add tests! Pull Requests without 100% test coverage will not be approved. 19 | 1. Ensure the existing test suite passes locally. 20 | 1. Format your code (`dart format .`). 21 | 1. Analyze your code (`dart analyze --fatal-infos --fatal-warnings .`). 22 | 1. Create the Pull Request. 23 | 1. Verify that all status checks are passing. 24 | 25 | While the prerequisites above must be satisfied prior to having your 26 | pull request reviewed, the reviewer(s) may ask you to complete additional 27 | work, tests, or other changes before your pull request can be ultimately 28 | accepted. 29 | 30 | [conventional_commits_link]: https://www.conventionalcommits.org/en/v1.0.0 31 | [bug_report_link]: https://github.com/VeryGoodOpenSource/very_good_test_runner/issues/new?assignees=&labels=bug&template=bug_report.md&title=fix%3A+ 32 | [very_good_ventures_link]: https://verygood.ventures 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Very Good Ventures 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Very Good Test Runner 2 | 3 | [![Very Good Ventures][logo_black]][very_good_ventures_link_light] 4 | [![Very Good Ventures][logo_white]][very_good_ventures_link_dark] 5 | 6 | Developed with 💙 by [Very Good Ventures][very_good_ventures_link] 🦄 7 | 8 | [![ci][ci_badge]][ci_link] 9 | [![coverage][coverage_badge]][ci_link] 10 | [![pub package][pub_badge]][pub_link] 11 | [![style: very good analysis][very_good_analysis_badge]][very_good_analysis_link] 12 | [![License: MIT][license_badge]][license_link] 13 | 14 | This package is a test runner for Flutter and Dart created by Very Good Ventures. It is intended to be used when writing custom tooling that runs Flutter or Dart tests and exposes a stream of `TestEvent` instances. For more information about the various `TestEvent` types, refer to the [JSON Reporter Test Protocol][json_reporter_test_protocol_link]. 15 | 16 | ## Usage 17 | 18 | ```dart 19 | import 'package:very_good_test_runner/very_good_test_runner.dart'; 20 | 21 | void main() { 22 | const arguments = ['--coverage']; 23 | const workingDirectory = 'path/to/project'; 24 | 25 | // Run `dart test` process. 26 | dartTest( 27 | arguments: arguments, 28 | workingDirectory: workingDirectory, 29 | ).listen((TestEvent event) { 30 | // React to `TestEvent` instances. 31 | print(event); 32 | }); 33 | 34 | // Run `flutter test` process. 35 | flutterTest( 36 | arguments: arguments, 37 | workingDirectory: workingDirectory, 38 | ).listen((TestEvent event) { 39 | // React to `TestEvent` instances. 40 | print(event); 41 | }); 42 | } 43 | ``` 44 | 45 | [ci_badge]: https://github.com/VeryGoodOpenSource/very_good_test_runner/workflows/very_good_test_runner/badge.svg 46 | [ci_link]: https://github.com/VeryGoodOpenSource/very_good_test_runner/actions 47 | [coverage_badge]: https://raw.githubusercontent.com/VeryGoodOpenSource/very_good_test_runner/main/coverage_badge.svg 48 | [json_reporter_test_protocol_link]: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/json_reporter.md 49 | [license_badge]: https://img.shields.io/badge/license-MIT-blue.svg 50 | [license_link]: https://opensource.org/licenses/MIT 51 | [logo_black]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_black.png#gh-light-mode-only 52 | [logo_white]: https://raw.githubusercontent.com/VGVentures/very_good_brand/main/styles/README/vgv_logo_white.png#gh-dark-mode-only 53 | [pub_badge]: https://img.shields.io/pub/v/very_good_test_runner.svg 54 | [pub_link]: https://pub.dartlang.org/packages/very_good_test_runner 55 | [very_good_analysis_badge]: https://img.shields.io/badge/style-very_good_analysis-B22C89.svg 56 | [very_good_analysis_link]: https://pub.dev/packages/very_good_analysis 57 | [very_good_ventures_link]: https://verygood.ventures/?utm_source=github 58 | [very_good_ventures_link_dark]: https://verygood.ventures/?utm_source=github#gh-dark-mode-only 59 | [very_good_ventures_link_light]: https://verygood.ventures/?utm_source=github#gh-light-mode-only 60 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:very_good_analysis/analysis_options.7.0.0.yaml 2 | -------------------------------------------------------------------------------- /build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | source_gen|combining_builder: 5 | options: 6 | ignore_for_file: 7 | - document_ignores 8 | - implicit_dynamic_parameter 9 | - cast_nullable_to_non_nullable 10 | - require_trailing_commas 11 | - lines_longer_than_80_chars 12 | json_serializable: 13 | options: 14 | create_to_json: false 15 | checked: true 16 | -------------------------------------------------------------------------------- /coverage_badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | coverage 16 | coverage 17 | 100% 18 | 100% 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/main.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_print 2 | 3 | import 'package:very_good_test_runner/very_good_test_runner.dart'; 4 | 5 | void main() { 6 | // React to `TestEvent` instances. 7 | flutterTest().listen(print); 8 | } 9 | -------------------------------------------------------------------------------- /lib/src/models/models.dart: -------------------------------------------------------------------------------- 1 | export 'test_event.dart'; 2 | -------------------------------------------------------------------------------- /lib/src/models/test_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:json_annotation/json_annotation.dart'; 2 | 3 | part 'test_event.g.dart'; 4 | 5 | /// {@template test_event} 6 | /// This is the root class of the protocol. 7 | /// All root-level objects emitted by the JSON reporter 8 | /// will be subclasses of [TestEvent]. 9 | /// https://github.com/dart-lang/test/blob/master/pkgs/test/doc/json_reporter.md 10 | /// {@endtemplate} 11 | abstract class TestEvent { 12 | /// {@macro test_event} 13 | const TestEvent({required this.type, required this.time}); 14 | 15 | /// Converts [json] into a [TestEvent]. 16 | static TestEvent fromJson(Map json) { 17 | final type = json['type'] as String?; 18 | switch (type) { 19 | case 'start': 20 | return StartTestEvent.fromJson(json); 21 | case 'allSuites': 22 | return AllSuitesTestEvent.fromJson(json); 23 | case 'suite': 24 | return SuiteTestEvent.fromJson(json); 25 | case 'debug': 26 | return DebugTestEvent.fromJson(json); 27 | case 'group': 28 | return GroupTestEvent.fromJson(json); 29 | case 'testStart': 30 | return TestStartEvent.fromJson(json); 31 | case 'print': 32 | return MessageTestEvent.fromJson(json); 33 | case 'error': 34 | return ErrorTestEvent.fromJson(json); 35 | case 'testDone': 36 | return TestDoneEvent.fromJson(json); 37 | case 'done': 38 | return DoneTestEvent.fromJson(json); 39 | case 'exit': 40 | return ExitTestEvent.fromJson(json); 41 | default: 42 | throw UnsupportedError('Unsupported type: $type'); 43 | } 44 | } 45 | 46 | /// The type of the event. 47 | /// 48 | /// This is always one of the subclass types listed below. 49 | final String type; 50 | 51 | /// The time (in milliseconds) that has elapsed since the test runner started. 52 | final int time; 53 | } 54 | 55 | /// {@template start_test_event} 56 | /// A single start event is emitted before any other events. 57 | /// It indicates that the test runner has started running. 58 | /// {@endtemplate} 59 | @JsonSerializable() 60 | class StartTestEvent extends TestEvent { 61 | /// {@macro start_test_event} 62 | const StartTestEvent({ 63 | required this.protocolVersion, 64 | required this.runnerVersion, 65 | required this.pid, 66 | required super.time, 67 | }) : super(type: 'start'); 68 | 69 | /// {@macro start_test_event} 70 | factory StartTestEvent.fromJson(Map json) => 71 | _$StartTestEventFromJson(json); 72 | 73 | /// The version of the JSON reporter protocol being used. 74 | /// 75 | /// This is a semantic version, but it reflects only the version of the 76 | /// protocol—it's not identical to the version of the test runner itself. 77 | final String protocolVersion; 78 | 79 | /// The version of the test runner being used. 80 | /// 81 | /// This is null if for some reason the version couldn't be loaded. 82 | final String? runnerVersion; 83 | 84 | /// The pid of the VM process running the tests. 85 | final int pid; 86 | } 87 | 88 | /// {@template all_suites_test_event} 89 | /// A single suite count event is emitted once the test runner knows the total 90 | /// number of suites that will be loaded over the course of the test run. 91 | /// Because this is determined asynchronously, its position relative to other 92 | /// events (except [StartTestEvent]) is not guaranteed. 93 | /// {@endtemplate} 94 | @JsonSerializable() 95 | class AllSuitesTestEvent extends TestEvent { 96 | /// {@macro all_suites_test_event} 97 | const AllSuitesTestEvent({ 98 | required this.count, 99 | required super.time, 100 | }) : super(type: 'allSuites'); 101 | 102 | /// {@macro all_suites_test_event} 103 | factory AllSuitesTestEvent.fromJson(Map json) => 104 | _$AllSuitesTestEventFromJson(json); 105 | 106 | /// The total number of suites that will be loaded. 107 | final int count; 108 | } 109 | 110 | /// {@template suite_test_event} 111 | /// A suite event is emitted before any GroupEvents for groups 112 | /// in a given test suite. 113 | /// This is the only event that contains the full metadata about a suite; 114 | /// future events will refer to the suite by its opaque ID. 115 | /// {@endtemplate} 116 | @JsonSerializable() 117 | class SuiteTestEvent extends TestEvent { 118 | /// {@macro suite_test_event} 119 | const SuiteTestEvent({ 120 | required this.suite, 121 | required super.time, 122 | }) : super(type: 'suite'); 123 | 124 | /// {@macro suite_test_event} 125 | factory SuiteTestEvent.fromJson(Map json) => 126 | _$SuiteTestEventFromJson(json); 127 | 128 | /// Metadata about the suite. 129 | final TestSuite suite; 130 | } 131 | 132 | /// {@template debug_test_event} 133 | /// A debug event is emitted after (although not necessarily directly after) 134 | /// a [SuiteTestEvent], and includes information about how to debug that suite. 135 | /// It's only emitted if the --debug flag is passed to the test runner. 136 | /// {@endtemplate} 137 | @JsonSerializable() 138 | class DebugTestEvent extends TestEvent { 139 | /// {@macro debug_test_event} 140 | const DebugTestEvent({ 141 | required this.suiteID, 142 | required this.observatory, 143 | required this.remoteDebugger, 144 | required super.time, 145 | }) : super(type: 'debug'); 146 | 147 | /// {@macro debug_test_event} 148 | factory DebugTestEvent.fromJson(Map json) => 149 | _$DebugTestEventFromJson(json); 150 | 151 | /// The suite for which debug information is reported. 152 | final int suiteID; 153 | 154 | /// The HTTP URL for the Dart Observatory, or `null` if the Observatory isn't 155 | /// available for this suite. 156 | final String? observatory; 157 | 158 | /// The HTTP URL for the remote debugger for this suite's host page, or `null` 159 | /// if no remote debugger is available for this suite. 160 | final String? remoteDebugger; 161 | } 162 | 163 | /// {@template group_test_event} 164 | /// A group event is emitted before any 165 | /// [TestStartEvent] for tests in a given group. 166 | /// This is the only event that contains the full metadata about a group; 167 | /// future events will refer to the group by its opaque ID. 168 | /// {@endtemplate} 169 | @JsonSerializable() 170 | class GroupTestEvent extends TestEvent { 171 | /// {@macro group_test_event} 172 | const GroupTestEvent({ 173 | required this.group, 174 | required super.time, 175 | }) : super(type: 'group'); 176 | 177 | /// {@macro group_test_event} 178 | factory GroupTestEvent.fromJson(Map json) => 179 | _$GroupTestEventFromJson(json); 180 | 181 | /// Metadata about the group. 182 | final TestGroup group; 183 | } 184 | 185 | /// {@template test_start_event} 186 | /// An event emitted when a test begins running. 187 | /// This is the only event that contains the full metadata about a test; 188 | /// future events will refer to the test by its opaque ID. 189 | /// {@endtemplate} 190 | @JsonSerializable() 191 | class TestStartEvent extends TestEvent { 192 | /// {@macro test_start_event} 193 | const TestStartEvent({ 194 | required this.test, 195 | required super.time, 196 | }) : super(type: 'testStart'); 197 | 198 | /// {@macro test_start_event} 199 | factory TestStartEvent.fromJson(Map json) => 200 | _$TestStartEventFromJson(json); 201 | 202 | /// Metadata about the test that started. 203 | final Test test; 204 | } 205 | 206 | /// {@template message_test_event} 207 | /// A MessageEvent indicates that a test emitted a message that 208 | /// should be displayed to the user. 209 | /// The [messageType] field indicates the precise type of this message. 210 | /// Different message types should be visually distinguishable. 211 | /// {@endtemplate} 212 | @JsonSerializable() 213 | class MessageTestEvent extends TestEvent { 214 | /// {@macro message_test_event} 215 | const MessageTestEvent({ 216 | required this.testID, 217 | required this.messageType, 218 | required this.message, 219 | required super.time, 220 | }) : super(type: 'print'); 221 | 222 | /// {@macro message_test_event} 223 | factory MessageTestEvent.fromJson(Map json) => 224 | _$MessageTestEventFromJson(json); 225 | 226 | /// The ID of the test that printed a message. 227 | final int testID; 228 | 229 | /// The type of message being printed. 230 | final String messageType; 231 | 232 | /// The message that was printed. 233 | final String message; 234 | } 235 | 236 | /// {@template error_test_event} 237 | /// An [ErrorTestEvent] indicates that a test encountered an uncaught error. 238 | /// Note that this may happen even after the test has completed, 239 | /// in which case it should be considered to have failed. 240 | /// {@endtemplate} 241 | @JsonSerializable() 242 | class ErrorTestEvent extends TestEvent { 243 | /// {@macro error_test_event} 244 | const ErrorTestEvent({ 245 | required this.testID, 246 | required this.error, 247 | required this.stackTrace, 248 | required this.isFailure, 249 | required super.time, 250 | }) : super(type: 'error'); 251 | 252 | /// {@macro error_test_event} 253 | factory ErrorTestEvent.fromJson(Map json) => 254 | _$ErrorTestEventFromJson(json); 255 | 256 | /// The ID of the test that experienced the error. 257 | final int testID; 258 | 259 | /// The result of calling toString() on the error object. 260 | final String error; 261 | 262 | /// The error's stack trace, in the stack_trace package format. 263 | final String stackTrace; 264 | 265 | /// Whether the error was a TestFailure. 266 | final bool isFailure; 267 | } 268 | 269 | /// The result of a test. 270 | enum TestResult { 271 | /// the test had no errors 272 | success, 273 | 274 | /// the test had a `TestFailure` but no other errors. 275 | failure, 276 | 277 | /// the test had an error other than `TestFailure` 278 | error 279 | } 280 | 281 | /// {@template test_done_event} 282 | /// An event emitted when a test completes. 283 | /// The result attribute indicates the result of the test. 284 | /// {@endtemplate} 285 | @JsonSerializable() 286 | class TestDoneEvent extends TestEvent { 287 | /// {@macro test_done_event} 288 | const TestDoneEvent({ 289 | required this.testID, 290 | required this.result, 291 | required this.hidden, 292 | required this.skipped, 293 | required super.time, 294 | }) : super(type: 'testDone'); 295 | 296 | /// {@macro test_done_event} 297 | factory TestDoneEvent.fromJson(Map json) => 298 | _$TestDoneEventFromJson(json); 299 | 300 | /// The ID of the test that completed. 301 | final int testID; 302 | 303 | /// The result of the test. 304 | final TestResult result; 305 | 306 | /// Whether the test's result should be hidden. 307 | final bool hidden; 308 | 309 | /// Whether the test (or some part of it) was skipped. 310 | final bool skipped; 311 | } 312 | 313 | /// {@template done_test_event} 314 | /// An event indicating the result of the entire test run. 315 | /// This will be the final event emitted by the reporter. 316 | /// {@endtemplate} 317 | @JsonSerializable() 318 | class DoneTestEvent extends TestEvent { 319 | /// {@macro done_test_event} 320 | const DoneTestEvent({ 321 | required this.success, 322 | required super.time, 323 | }) : super(type: 'done'); 324 | 325 | /// {@macro done_test_event} 326 | factory DoneTestEvent.fromJson(Map json) => 327 | _$DoneTestEventFromJson(json); 328 | 329 | /// Whether all tests succeeded (or were skipped). 330 | /// 331 | /// Will be `null` if the test runner was close before all tests completed 332 | /// running. 333 | final bool? success; 334 | } 335 | 336 | /// {@template test_exit_event} 337 | /// An event emitted when a test completes. 338 | /// The [exitCode] attribute indicates the result of the test process. 339 | /// {@endtemplate} 340 | @JsonSerializable() 341 | class ExitTestEvent extends TestEvent { 342 | /// {@macro test_exit_event} 343 | const ExitTestEvent({required super.time, required this.exitCode}) 344 | : super(type: 'exit'); 345 | 346 | /// {@macro test_exit_event} 347 | factory ExitTestEvent.fromJson(Map json) => 348 | _$ExitTestEventFromJson(json); 349 | 350 | /// The exit code associated with the test process. 351 | final int exitCode; 352 | } 353 | 354 | /// {@template test_suite} 355 | /// A test suite corresponding to a loaded test file. 356 | /// The suite's ID is unique in the context of this test run. 357 | /// It's used elsewhere in the protocol to refer to this suite 358 | /// without including its full representation. 359 | 360 | /// A suite's platform is one of the platforms that can be passed to the 361 | /// --platform option, or null if there is no platform 362 | /// (for example if the file doesn't exist at all). 363 | /// Its path is either absolute or relative to the root of the current package. 364 | /// {@endtemplate} 365 | @JsonSerializable() 366 | class TestSuite { 367 | /// {@macro test_suite} 368 | const TestSuite({ 369 | required this.id, 370 | required this.platform, 371 | this.path, 372 | }); 373 | 374 | /// {@macro test_suite} 375 | factory TestSuite.fromJson(Map json) => 376 | _$TestSuiteFromJson(json); 377 | 378 | /// An opaque ID for the group. 379 | final int id; 380 | 381 | /// The platform on which the suite is running. 382 | final String platform; 383 | 384 | /// The path to the suite's file, or `null` if that path is unknown. 385 | final String? path; 386 | } 387 | 388 | /// {@template test_group} 389 | /// A group containing test cases. 390 | /// The group's ID is unique in the context of this test run. 391 | /// It's used elsewhere in the protocol to refer to this group 392 | /// without including its full representation. 393 | /// {@endtemplate} 394 | @JsonSerializable() 395 | class TestGroup { 396 | /// {@macro test_group} 397 | const TestGroup({ 398 | required this.id, 399 | required this.name, 400 | required this.suiteID, 401 | required this.testCount, 402 | required this.metadata, 403 | this.parentID, 404 | this.line, 405 | this.column, 406 | this.url, 407 | }); 408 | 409 | /// {@macro test_group} 410 | factory TestGroup.fromJson(Map json) => 411 | _$TestGroupFromJson(json); 412 | 413 | /// An opaque ID for the group. 414 | final int id; 415 | 416 | /// The name of the group, including prefixes from any containing groups. 417 | final String name; 418 | 419 | /// The ID of the suite containing this group. 420 | final int suiteID; 421 | 422 | /// The ID of the group's parent group, unless it's the root group. 423 | final int? parentID; 424 | 425 | /// The number of tests (recursively) within this group. 426 | final int testCount; 427 | 428 | /// The (1-based) line on which the group was defined, or `null`. 429 | final int? line; 430 | 431 | /// The (1-based) column on which the group was defined, or `null`. 432 | final int? column; 433 | 434 | /// The URL for the file in which the group was defined, or `null`. 435 | final String? url; 436 | 437 | /// This field is deprecated and should not be used. 438 | final TestMetadata metadata; 439 | } 440 | 441 | /// {@template test_metadata} 442 | /// Test metadata regarding whether the test was skipped and the reason. 443 | /// {@endtemplate} 444 | @JsonSerializable() 445 | class TestMetadata { 446 | /// {@macro test_metadata} 447 | TestMetadata({required this.skip, this.skipReason}); 448 | 449 | /// {@macro test_metadata} 450 | factory TestMetadata.fromJson(Map json) => 451 | _$TestMetadataFromJson(json); 452 | 453 | /// Whether the test was skipped. 454 | final bool skip; 455 | 456 | /// The reason the tests was skipped, or `null` if it wasn't skipped. 457 | final String? skipReason; 458 | } 459 | 460 | /// {@template test} 461 | /// A single test case. The test's ID is unique in the context of this test run. 462 | /// It's used elsewhere in the protocol to refer to this test 463 | /// without including its full representation. 464 | /// {@endtemplate} 465 | @JsonSerializable() 466 | class Test { 467 | /// {@macro test} 468 | Test({ 469 | required this.id, 470 | required this.name, 471 | required this.suiteID, 472 | required this.groupIDs, 473 | required this.metadata, 474 | this.line, 475 | this.column, 476 | this.url, 477 | this.rootLine, 478 | this.rootColumn, 479 | this.rootUrl, 480 | }); 481 | 482 | /// {@macro test} 483 | factory Test.fromJson(Map json) => _$TestFromJson(json); 484 | 485 | /// An opaque ID for the test. 486 | final int id; 487 | 488 | /// The name of the test, including prefixes from any containing groups. 489 | final String name; 490 | 491 | /// The ID of the suite containing this test. 492 | final int suiteID; 493 | 494 | /// The IDs of groups containing this test, in order from outermost to 495 | /// innermost. 496 | final List groupIDs; 497 | 498 | /// The (1-based) line on which the test was defined, or `null`. 499 | final int? line; 500 | 501 | /// The (1-based) column on which the test was defined, or `null`. 502 | final int? column; 503 | 504 | /// The URL for the file in which the test was defined, or `null`. 505 | final String? url; 506 | 507 | /// The (1-based) line in the original test suite from which the test 508 | /// originated. 509 | /// 510 | /// Will only be present if `root_url` is different from `url`. 511 | final int? rootLine; 512 | 513 | /// The (1-based) line on in the original test suite from which the test 514 | /// originated. 515 | /// 516 | /// Will only be present if `root_url` is different from `url`. 517 | final int? rootColumn; 518 | 519 | /// The URL for the original test suite in which the test was defined. 520 | /// 521 | /// Will only be present if different from `url`. 522 | final String? rootUrl; 523 | 524 | /// This field is deprecated and should not be used. 525 | final TestMetadata metadata; 526 | } 527 | -------------------------------------------------------------------------------- /lib/src/models/test_event.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ignore_for_file: document_ignores, implicit_dynamic_parameter, cast_nullable_to_non_nullable, require_trailing_commas, lines_longer_than_80_chars 4 | 5 | part of 'test_event.dart'; 6 | 7 | // ************************************************************************** 8 | // JsonSerializableGenerator 9 | // ************************************************************************** 10 | 11 | StartTestEvent _$StartTestEventFromJson(Map json) => 12 | $checkedCreate( 13 | 'StartTestEvent', 14 | json, 15 | ($checkedConvert) { 16 | final val = StartTestEvent( 17 | protocolVersion: 18 | $checkedConvert('protocolVersion', (v) => v as String), 19 | runnerVersion: $checkedConvert('runnerVersion', (v) => v as String?), 20 | pid: $checkedConvert('pid', (v) => (v as num).toInt()), 21 | time: $checkedConvert('time', (v) => (v as num).toInt()), 22 | ); 23 | return val; 24 | }, 25 | ); 26 | 27 | AllSuitesTestEvent _$AllSuitesTestEventFromJson(Map json) => 28 | $checkedCreate( 29 | 'AllSuitesTestEvent', 30 | json, 31 | ($checkedConvert) { 32 | final val = AllSuitesTestEvent( 33 | count: $checkedConvert('count', (v) => (v as num).toInt()), 34 | time: $checkedConvert('time', (v) => (v as num).toInt()), 35 | ); 36 | return val; 37 | }, 38 | ); 39 | 40 | SuiteTestEvent _$SuiteTestEventFromJson(Map json) => 41 | $checkedCreate( 42 | 'SuiteTestEvent', 43 | json, 44 | ($checkedConvert) { 45 | final val = SuiteTestEvent( 46 | suite: $checkedConvert( 47 | 'suite', (v) => TestSuite.fromJson(v as Map)), 48 | time: $checkedConvert('time', (v) => (v as num).toInt()), 49 | ); 50 | return val; 51 | }, 52 | ); 53 | 54 | DebugTestEvent _$DebugTestEventFromJson(Map json) => 55 | $checkedCreate( 56 | 'DebugTestEvent', 57 | json, 58 | ($checkedConvert) { 59 | final val = DebugTestEvent( 60 | suiteID: $checkedConvert('suiteID', (v) => (v as num).toInt()), 61 | observatory: $checkedConvert('observatory', (v) => v as String?), 62 | remoteDebugger: 63 | $checkedConvert('remoteDebugger', (v) => v as String?), 64 | time: $checkedConvert('time', (v) => (v as num).toInt()), 65 | ); 66 | return val; 67 | }, 68 | ); 69 | 70 | GroupTestEvent _$GroupTestEventFromJson(Map json) => 71 | $checkedCreate( 72 | 'GroupTestEvent', 73 | json, 74 | ($checkedConvert) { 75 | final val = GroupTestEvent( 76 | group: $checkedConvert( 77 | 'group', (v) => TestGroup.fromJson(v as Map)), 78 | time: $checkedConvert('time', (v) => (v as num).toInt()), 79 | ); 80 | return val; 81 | }, 82 | ); 83 | 84 | TestStartEvent _$TestStartEventFromJson(Map json) => 85 | $checkedCreate( 86 | 'TestStartEvent', 87 | json, 88 | ($checkedConvert) { 89 | final val = TestStartEvent( 90 | test: $checkedConvert( 91 | 'test', (v) => Test.fromJson(v as Map)), 92 | time: $checkedConvert('time', (v) => (v as num).toInt()), 93 | ); 94 | return val; 95 | }, 96 | ); 97 | 98 | MessageTestEvent _$MessageTestEventFromJson(Map json) => 99 | $checkedCreate( 100 | 'MessageTestEvent', 101 | json, 102 | ($checkedConvert) { 103 | final val = MessageTestEvent( 104 | testID: $checkedConvert('testID', (v) => (v as num).toInt()), 105 | messageType: $checkedConvert('messageType', (v) => v as String), 106 | message: $checkedConvert('message', (v) => v as String), 107 | time: $checkedConvert('time', (v) => (v as num).toInt()), 108 | ); 109 | return val; 110 | }, 111 | ); 112 | 113 | ErrorTestEvent _$ErrorTestEventFromJson(Map json) => 114 | $checkedCreate( 115 | 'ErrorTestEvent', 116 | json, 117 | ($checkedConvert) { 118 | final val = ErrorTestEvent( 119 | testID: $checkedConvert('testID', (v) => (v as num).toInt()), 120 | error: $checkedConvert('error', (v) => v as String), 121 | stackTrace: $checkedConvert('stackTrace', (v) => v as String), 122 | isFailure: $checkedConvert('isFailure', (v) => v as bool), 123 | time: $checkedConvert('time', (v) => (v as num).toInt()), 124 | ); 125 | return val; 126 | }, 127 | ); 128 | 129 | TestDoneEvent _$TestDoneEventFromJson(Map json) => 130 | $checkedCreate( 131 | 'TestDoneEvent', 132 | json, 133 | ($checkedConvert) { 134 | final val = TestDoneEvent( 135 | testID: $checkedConvert('testID', (v) => (v as num).toInt()), 136 | result: $checkedConvert( 137 | 'result', (v) => $enumDecode(_$TestResultEnumMap, v)), 138 | hidden: $checkedConvert('hidden', (v) => v as bool), 139 | skipped: $checkedConvert('skipped', (v) => v as bool), 140 | time: $checkedConvert('time', (v) => (v as num).toInt()), 141 | ); 142 | return val; 143 | }, 144 | ); 145 | 146 | const _$TestResultEnumMap = { 147 | TestResult.success: 'success', 148 | TestResult.failure: 'failure', 149 | TestResult.error: 'error', 150 | }; 151 | 152 | DoneTestEvent _$DoneTestEventFromJson(Map json) => 153 | $checkedCreate( 154 | 'DoneTestEvent', 155 | json, 156 | ($checkedConvert) { 157 | final val = DoneTestEvent( 158 | success: $checkedConvert('success', (v) => v as bool?), 159 | time: $checkedConvert('time', (v) => (v as num).toInt()), 160 | ); 161 | return val; 162 | }, 163 | ); 164 | 165 | ExitTestEvent _$ExitTestEventFromJson(Map json) => 166 | $checkedCreate( 167 | 'ExitTestEvent', 168 | json, 169 | ($checkedConvert) { 170 | final val = ExitTestEvent( 171 | time: $checkedConvert('time', (v) => (v as num).toInt()), 172 | exitCode: $checkedConvert('exitCode', (v) => (v as num).toInt()), 173 | ); 174 | return val; 175 | }, 176 | ); 177 | 178 | TestSuite _$TestSuiteFromJson(Map json) => $checkedCreate( 179 | 'TestSuite', 180 | json, 181 | ($checkedConvert) { 182 | final val = TestSuite( 183 | id: $checkedConvert('id', (v) => (v as num).toInt()), 184 | platform: $checkedConvert('platform', (v) => v as String), 185 | path: $checkedConvert('path', (v) => v as String?), 186 | ); 187 | return val; 188 | }, 189 | ); 190 | 191 | TestGroup _$TestGroupFromJson(Map json) => $checkedCreate( 192 | 'TestGroup', 193 | json, 194 | ($checkedConvert) { 195 | final val = TestGroup( 196 | id: $checkedConvert('id', (v) => (v as num).toInt()), 197 | name: $checkedConvert('name', (v) => v as String), 198 | suiteID: $checkedConvert('suiteID', (v) => (v as num).toInt()), 199 | testCount: $checkedConvert('testCount', (v) => (v as num).toInt()), 200 | metadata: $checkedConvert('metadata', 201 | (v) => TestMetadata.fromJson(v as Map)), 202 | parentID: $checkedConvert('parentID', (v) => (v as num?)?.toInt()), 203 | line: $checkedConvert('line', (v) => (v as num?)?.toInt()), 204 | column: $checkedConvert('column', (v) => (v as num?)?.toInt()), 205 | url: $checkedConvert('url', (v) => v as String?), 206 | ); 207 | return val; 208 | }, 209 | ); 210 | 211 | TestMetadata _$TestMetadataFromJson(Map json) => 212 | $checkedCreate( 213 | 'TestMetadata', 214 | json, 215 | ($checkedConvert) { 216 | final val = TestMetadata( 217 | skip: $checkedConvert('skip', (v) => v as bool), 218 | skipReason: $checkedConvert('skipReason', (v) => v as String?), 219 | ); 220 | return val; 221 | }, 222 | ); 223 | 224 | Test _$TestFromJson(Map json) => $checkedCreate( 225 | 'Test', 226 | json, 227 | ($checkedConvert) { 228 | final val = Test( 229 | id: $checkedConvert('id', (v) => (v as num).toInt()), 230 | name: $checkedConvert('name', (v) => v as String), 231 | suiteID: $checkedConvert('suiteID', (v) => (v as num).toInt()), 232 | groupIDs: $checkedConvert( 233 | 'groupIDs', 234 | (v) => 235 | (v as List).map((e) => (e as num).toInt()).toList()), 236 | metadata: $checkedConvert('metadata', 237 | (v) => TestMetadata.fromJson(v as Map)), 238 | line: $checkedConvert('line', (v) => (v as num?)?.toInt()), 239 | column: $checkedConvert('column', (v) => (v as num?)?.toInt()), 240 | url: $checkedConvert('url', (v) => v as String?), 241 | rootLine: $checkedConvert('rootLine', (v) => (v as num?)?.toInt()), 242 | rootColumn: 243 | $checkedConvert('rootColumn', (v) => (v as num?)?.toInt()), 244 | rootUrl: $checkedConvert('rootUrl', (v) => v as String?), 245 | ); 246 | return val; 247 | }, 248 | ); 249 | -------------------------------------------------------------------------------- /lib/src/very_good_test_runner.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:universal_io/io.dart'; 5 | import 'package:very_good_test_runner/very_good_test_runner.dart'; 6 | 7 | /// Signature for `Process.start`. 8 | typedef StartProcess = Future Function( 9 | String executable, 10 | List arguments, { 11 | String? workingDirectory, 12 | Map? environment, 13 | bool includeParentEnvironment, 14 | bool runInShell, 15 | ProcessStartMode mode, 16 | }); 17 | 18 | /// Runs `dart test` and returns a stream of [TestEvent] 19 | /// reported by the process. 20 | /// 21 | /// ```dart 22 | /// void main() { 23 | /// // React to `TestEvent` instances. 24 | /// dartTest().listen(print); 25 | /// } 26 | /// ``` 27 | Stream dartTest({ 28 | List? arguments, 29 | String? workingDirectory, 30 | Map? environment, 31 | bool runInShell = false, 32 | StartProcess startProcess = Process.start, 33 | }) { 34 | return _runTestProcess( 35 | () => startProcess( 36 | 'dart', 37 | ['test', ...?arguments, '--reporter=json'], 38 | environment: environment, 39 | workingDirectory: workingDirectory, 40 | runInShell: runInShell, 41 | ), 42 | ); 43 | } 44 | 45 | /// Runs `flutter test` and returns a stream of [TestEvent] 46 | /// reported by the process. 47 | /// 48 | /// ```dart 49 | /// void main() { 50 | /// // React to `TestEvent` instances. 51 | /// flutterTest().listen(print); 52 | /// } 53 | /// ``` 54 | Stream flutterTest({ 55 | List? arguments, 56 | String? workingDirectory, 57 | Map? environment, 58 | bool runInShell = false, 59 | StartProcess startProcess = Process.start, 60 | }) { 61 | return _runTestProcess( 62 | () => startProcess( 63 | 'flutter', 64 | ['test', ...?arguments, '--reporter=json'], 65 | environment: environment, 66 | workingDirectory: workingDirectory, 67 | runInShell: runInShell, 68 | ), 69 | ); 70 | } 71 | 72 | Stream _runTestProcess( 73 | Future Function() processRunner, 74 | ) { 75 | final controller = StreamController(); 76 | late StreamSubscription testEventSubscription; 77 | late StreamSubscription errorSubscription; 78 | late Future processFuture; 79 | 80 | Future onListen() async { 81 | final stopwatch = Stopwatch()..start(); 82 | processFuture = processRunner(); 83 | final process = await processFuture; 84 | final errors = process.stderr.map((e) => utf8.decode(e).trim()); 85 | final testEvents = process.stdout.mapToTestEvents(); 86 | errorSubscription = errors.listen(controller.addError); 87 | testEventSubscription = testEvents.listen( 88 | controller.add, 89 | onError: controller.addError, 90 | ); 91 | 92 | final exitCode = await process.exitCode; 93 | stopwatch.stop(); 94 | await Future.wait([ 95 | errorSubscription.cancel(), 96 | testEventSubscription.cancel(), 97 | ]); 98 | if (controller.isClosed) return; 99 | controller.add( 100 | ExitTestEvent( 101 | time: stopwatch.elapsedMilliseconds, 102 | exitCode: exitCode, 103 | ), 104 | ); 105 | await controller.close(); 106 | } 107 | 108 | Future onCancel() async { 109 | await controller.close(); 110 | (await processFuture).kill(); 111 | await errorSubscription.cancel(); 112 | await testEventSubscription.cancel(); 113 | } 114 | 115 | controller 116 | ..onListen = onListen 117 | ..onCancel = onCancel; 118 | 119 | return controller.stream; 120 | } 121 | 122 | extension on Stream> { 123 | Stream mapToTestEvents() { 124 | return map(utf8.decode) 125 | .expand(_splitLines) 126 | .map>(_tryDecode) 127 | .where((value) => value.isNotEmpty) 128 | .map(TestEvent.fromJson); 129 | } 130 | 131 | Iterable _splitLines(String content) sync* { 132 | for (final line in content.split('\n')) { 133 | yield line.trim(); 134 | } 135 | } 136 | 137 | Map _tryDecode(String value) { 138 | try { 139 | if (value.isEmpty) return const {}; 140 | return json.decode(value) as Map; 141 | } on FormatException { 142 | return const {}; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /lib/very_good_test_runner.dart: -------------------------------------------------------------------------------- 1 | /// A test runner for Flutter and Dart created by Very Good Ventures. 2 | library; 3 | 4 | export 'src/models/models.dart'; 5 | export 'src/very_good_test_runner.dart' show dartTest, flutterTest; 6 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: very_good_test_runner 2 | description: A test runner for Flutter and Dart created by Very Good Ventures 3 | homepage: https://github.com/VeryGoodOpenSource/very_good_test_runner 4 | version: 0.3.0 5 | 6 | environment: 7 | sdk: ^3.5.0 8 | 9 | dependencies: 10 | json_annotation: ^4.9.0 11 | universal_io: ^2.2.2 12 | 13 | dev_dependencies: 14 | build_runner: ^2.4.12 15 | build_verify: ^3.1.0 16 | json_serializable: ^6.8.0 17 | mocktail: ^1.0.4 18 | test: ^1.25.8 19 | very_good_analysis: ^7.0.0 20 | -------------------------------------------------------------------------------- /test/src/models/test_event_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:test/test.dart'; 2 | import 'package:very_good_test_runner/very_good_test_runner.dart'; 3 | 4 | void main() { 5 | group('TestEvent', () { 6 | group('fromJson', () { 7 | test( 8 | 'throws UnsupportedError ' 9 | 'when object is not a supported test event', () { 10 | expect( 11 | () => TestEvent.fromJson({}), 12 | throwsUnsupportedError, 13 | ); 14 | expect( 15 | () => TestEvent.fromJson({'type': 'invalid'}), 16 | throwsUnsupportedError, 17 | ); 18 | }); 19 | 20 | test('returns StartTestEvent when type is start', () { 21 | final json = { 22 | 'protocolVersion': '0.1.1', 23 | 'runnerVersion': '1.19.5', 24 | 'pid': 67090, 25 | 'type': 'start', 26 | 'time': 0, 27 | }; 28 | expect(TestEvent.fromJson(json), isA()); 29 | }); 30 | 31 | test('returns SuiteTestEvent when type is suite', () { 32 | final json = { 33 | 'suite': { 34 | 'id': 0, 35 | 'platform': 'vm', 36 | 'path': '/example/test/app/view/app_test.dart', 37 | }, 38 | 'type': 'suite', 39 | 'time': 0, 40 | }; 41 | expect(TestEvent.fromJson(json), isA()); 42 | }); 43 | 44 | test('returns GroupTestEvent when type is group', () { 45 | final json = { 46 | 'group': { 47 | 'id': 6, 48 | 'suiteID': 0, 49 | 'parentID': null, 50 | 'name': '', 51 | 'metadata': {'skip': false, 'skipReason': null}, 52 | 'testCount': 1, 53 | 'line': null, 54 | 'column': null, 55 | 'url': null, 56 | }, 57 | 'type': 'group', 58 | 'time': 2599, 59 | }; 60 | 61 | expect(TestEvent.fromJson(json), isA()); 62 | }); 63 | 64 | test('returns TestStartEvent when type is testStart', () { 65 | final json = { 66 | 'test': { 67 | 'id': 1, 68 | 'name': 'loading /example/test/app/view/app_test.dart', 69 | 'suiteID': 0, 70 | 'groupIDs': [], 71 | 'metadata': {'skip': false, 'skipReason': null}, 72 | 'line': null, 73 | 'column': null, 74 | 'url': null, 75 | }, 76 | 'type': 'testStart', 77 | 'time': 1, 78 | }; 79 | 80 | expect(TestEvent.fromJson(json), isA()); 81 | }); 82 | 83 | test('returns AllSuitesTestEvent when type is allSuites', () { 84 | final json = {'count': 3, 'time': 10, 'type': 'allSuites'}; 85 | 86 | expect(TestEvent.fromJson(json), isA()); 87 | }); 88 | 89 | test('returns ErrorTestEvent when type is error', () { 90 | final json = { 91 | 'testID': 8, 92 | 'error': 93 | '''Test failed. See exception logs above.\nThe test description was: renders CounterPage''', 94 | 'stackTrace': '', 95 | 'isFailure': false, 96 | 'type': 'error', 97 | 'time': 3288, 98 | }; 99 | 100 | expect(TestEvent.fromJson(json), isA()); 101 | }); 102 | 103 | test('returns MessageTestEvent when type is print', () { 104 | final json = { 105 | 'testID': 8, 106 | 'messageType': 'print', 107 | 'message': 108 | '''══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════\nThe following TestFailure was thrown running a test:\nExpected: no matching nodes in the widget tree\n Actual: _WidgetTypeFinder:\n Which: means one was found but none were expected\n\nWhen the exception was thrown, this was the stack:\n#4 main.. (file:///example/test/app/view/app_test.dart:16:7)\n\n\n(elided one frame from package:stack_trace)\n\nThis was caught by the test expectation on the following line:\n file:///example/test/app/view/app_test.dart line 16\nThe test description was:\n renders CounterPage\n════════════════════════════════════════════════════════════════════════════════════════════════════''', 109 | 'type': 'print', 110 | 'time': 3284, 111 | }; 112 | 113 | expect(TestEvent.fromJson(json), isA()); 114 | }); 115 | 116 | test('returns DebugTestEvent when type is debug', () { 117 | final json = { 118 | 'suiteID': 0, 119 | 'type': 'debug', 120 | 'observatory': null, 121 | 'remoteDebugger': null, 122 | 'time': 3288, 123 | }; 124 | 125 | expect(TestEvent.fromJson(json), isA()); 126 | }); 127 | 128 | test('returns TestDoneEvent when type is testDone', () { 129 | final json = { 130 | 'testID': 1, 131 | 'result': 'success', 132 | 'skipped': false, 133 | 'hidden': true, 134 | 'type': 'testDone', 135 | 'time': 2593, 136 | }; 137 | 138 | expect(TestEvent.fromJson(json), isA()); 139 | }); 140 | 141 | test('returns DoneTestEvent when type is done', () { 142 | final json = {'success': true, 'type': 'done', 'time': 4034}; 143 | 144 | expect(TestEvent.fromJson(json), isA()); 145 | }); 146 | 147 | test('returns ExitTestEvent when type is exit', () { 148 | final json = {'exitCode': 0, 'type': 'exit', 'time': 4034}; 149 | 150 | expect(TestEvent.fromJson(json), isA()); 151 | }); 152 | }); 153 | }); 154 | } 155 | -------------------------------------------------------------------------------- /test/src/very_good_test_runner_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'package:mocktail/mocktail.dart'; 5 | import 'package:test/test.dart'; 6 | import 'package:universal_io/io.dart'; 7 | import 'package:very_good_test_runner/very_good_test_runner.dart'; 8 | 9 | // Needed for test setup 10 | // ignore: one_member_abstracts 11 | abstract class TestProcess { 12 | Future start( 13 | String executable, 14 | List arguments, { 15 | String? workingDirectory, 16 | Map? environment, 17 | bool includeParentEnvironment = false, 18 | bool runInShell = false, 19 | ProcessStartMode mode = ProcessStartMode.normal, 20 | }); 21 | } 22 | 23 | class MockTestProcess extends Mock implements TestProcess {} 24 | 25 | class MockProcess extends Mock implements Process {} 26 | 27 | void main() { 28 | group('dartTest', () { 29 | late StreamController> stdoutController; 30 | late StreamController> stderrController; 31 | late Process process; 32 | late TestProcess testProcess; 33 | 34 | setUp(() { 35 | stdoutController = StreamController(); 36 | stderrController = StreamController(); 37 | process = MockProcess(); 38 | testProcess = MockTestProcess(); 39 | when( 40 | () => testProcess.start( 41 | any(), 42 | any(), 43 | workingDirectory: any(named: 'workingDirectory'), 44 | environment: any(named: 'environment'), 45 | includeParentEnvironment: any(named: 'includeParentEnvironment'), 46 | runInShell: any(named: 'runInShell'), 47 | ), 48 | ).thenAnswer((_) async => process); 49 | when(() => process.stdout).thenAnswer((_) => stdoutController.stream); 50 | when(() => process.stderr).thenAnswer((_) => stderrController.stream); 51 | when(() => process.exitCode).thenAnswer((_) async => 0); 52 | when(process.kill).thenReturn(true); 53 | }); 54 | 55 | test('passes correct parameters to Process.start', () async { 56 | final events = []; 57 | const arguments = ['--no-pub']; 58 | const environment = {'foo': 'bar'}; 59 | const workingDirectory = './path/to/tests'; 60 | const runInShell = true; 61 | final testEvents = dartTest( 62 | arguments: arguments, 63 | environment: environment, 64 | workingDirectory: workingDirectory, 65 | startProcess: testProcess.start, 66 | runInShell: runInShell, 67 | ); 68 | final subscription = testEvents.listen(events.add); 69 | await stdoutController.close(); 70 | expect(events, isEmpty); 71 | verify( 72 | () => testProcess.start( 73 | 'dart', 74 | ['test', ...arguments, '--reporter=json'], 75 | workingDirectory: workingDirectory, 76 | environment: environment, 77 | runInShell: runInShell, 78 | ), 79 | ).called(1); 80 | unawaited(subscription.cancel()); 81 | }); 82 | 83 | test('emits error from stderr', () async { 84 | const expectedError = 'oops'; 85 | final events = []; 86 | final errors = []; 87 | final testEvents = dartTest(startProcess: testProcess.start); 88 | final subscription = testEvents.listen(events.add, onError: errors.add); 89 | stderrController.add(utf8.encode(expectedError)); 90 | await stdoutController.close(); 91 | expect(events, isEmpty); 92 | expect(errors, equals([expectedError])); 93 | unawaited(subscription.cancel()); 94 | }); 95 | 96 | test('kills process when subscription is canceled', () async { 97 | final events = []; 98 | final testEvents = dartTest(startProcess: testProcess.start); 99 | final subscription = testEvents.listen(events.add); 100 | await subscription.cancel(); 101 | verify(process.kill).called(1); 102 | }); 103 | 104 | test('emits correctly (e2e)', () async { 105 | final tempDirectory = Directory.systemTemp.createTempSync(); 106 | File('${tempDirectory.path}/pubspec.yaml').writeAsStringSync( 107 | ''' 108 | name: example 109 | version: 0.1.0+1 110 | 111 | environment: 112 | sdk: ">=2.12.0 <3.0.0" 113 | 114 | dev_dependencies: 115 | test: any 116 | ''', 117 | ); 118 | final testDirectory = Directory('${tempDirectory.path}/test') 119 | ..createSync(); 120 | File('${testDirectory.path}/example_test.dart').writeAsStringSync( 121 | ''' 122 | import 'package:test/test.dart'; 123 | 124 | void main() { 125 | test('example', () { 126 | expect(true, isTrue); 127 | }); 128 | } 129 | ''', 130 | ); 131 | expect( 132 | dartTest(workingDirectory: tempDirectory.path) 133 | .where((e) => e is DoneTestEvent) 134 | .first, 135 | completes, 136 | ); 137 | }); 138 | }); 139 | 140 | group('flutterTest', () { 141 | late StreamController> stdoutController; 142 | late StreamController> stderrController; 143 | late Process process; 144 | late TestProcess testProcess; 145 | 146 | setUp(() { 147 | stdoutController = StreamController(); 148 | stderrController = StreamController(); 149 | process = MockProcess(); 150 | testProcess = MockTestProcess(); 151 | when( 152 | () => testProcess.start( 153 | any(), 154 | any(), 155 | workingDirectory: any(named: 'workingDirectory'), 156 | environment: any(named: 'environment'), 157 | includeParentEnvironment: any(named: 'includeParentEnvironment'), 158 | runInShell: any(named: 'runInShell'), 159 | ), 160 | ).thenAnswer((_) async => process); 161 | when(() => process.stdout).thenAnswer((_) => stdoutController.stream); 162 | when(() => process.stderr).thenAnswer((_) => stderrController.stream); 163 | when(() => process.exitCode).thenAnswer((_) async => 0); 164 | when(process.kill).thenReturn(true); 165 | }); 166 | 167 | test('passes correct parameters to Process.start', () async { 168 | final events = []; 169 | const arguments = ['--no-pub']; 170 | const environment = {'foo': 'bar'}; 171 | const workingDirectory = './path/to/tests'; 172 | const runInShell = true; 173 | final testEvents = flutterTest( 174 | arguments: arguments, 175 | environment: environment, 176 | workingDirectory: workingDirectory, 177 | startProcess: testProcess.start, 178 | runInShell: runInShell, 179 | ); 180 | final subscription = testEvents.listen(events.add); 181 | await stdoutController.close(); 182 | expect(events, isEmpty); 183 | verify( 184 | () => testProcess.start( 185 | 'flutter', 186 | ['test', ...arguments, '--reporter=json'], 187 | workingDirectory: workingDirectory, 188 | environment: environment, 189 | runInShell: runInShell, 190 | ), 191 | ).called(1); 192 | unawaited(subscription.cancel()); 193 | }); 194 | 195 | test('emits error from stderr', () async { 196 | const expectedError = 'oops'; 197 | final events = []; 198 | final errors = []; 199 | final testEvents = flutterTest(startProcess: testProcess.start); 200 | final subscription = testEvents.listen(events.add, onError: errors.add); 201 | stderrController.add(utf8.encode(expectedError)); 202 | await stdoutController.close(); 203 | expect(events, isEmpty); 204 | expect(errors, equals([expectedError])); 205 | unawaited(subscription.cancel()); 206 | }); 207 | 208 | test('kills process when subscription is canceled', () async { 209 | final events = []; 210 | final testEvents = flutterTest(startProcess: testProcess.start); 211 | final subscription = testEvents.listen(events.add); 212 | await subscription.cancel(); 213 | verify(process.kill).called(1); 214 | }); 215 | 216 | test('emits correctly (e2e)', () async { 217 | final tempDirectory = Directory.systemTemp.createTempSync(); 218 | File('${tempDirectory.path}/pubspec.yaml').writeAsStringSync( 219 | ''' 220 | name: example 221 | version: 0.1.0+1 222 | 223 | environment: 224 | sdk: ">=2.12.0 <3.0.0" 225 | 226 | dev_dependencies: 227 | test: any 228 | ''', 229 | ); 230 | final testDirectory = Directory('${tempDirectory.path}/test') 231 | ..createSync(); 232 | File('${testDirectory.path}/example_a[1]_test.dart').writeAsStringSync( 233 | ''' 234 | import 'package:test/test.dart'; 235 | 236 | void main() { 237 | test('example', () { 238 | expect(true, isTrue); 239 | }); 240 | } 241 | ''', 242 | ); 243 | expect( 244 | flutterTest(workingDirectory: tempDirectory.path) 245 | .where((e) => e is DoneTestEvent) 246 | .first, 247 | completes, 248 | ); 249 | }); 250 | 251 | test('emits correct stream of TestEvents', () async { 252 | final completer = Completer(); 253 | when(() => process.exitCode).thenAnswer((_) => completer.future); 254 | final rawEvents = [ 255 | { 256 | 'protocolVersion': '0.1.1', 257 | 'runnerVersion': '1.20.1', 258 | 'pid': 80881, 259 | 'type': 'start', 260 | 'time': 0, 261 | }, 262 | { 263 | 'suite': { 264 | 'id': 0, 265 | 'platform': 'vm', 266 | 'path': 267 | 'very_good_test_runner/test/src/models/test_event_test.dart', 268 | }, 269 | 'type': 'suite', 270 | 'time': 0, 271 | }, 272 | { 273 | 'test': { 274 | 'id': 1, 275 | 'name': 276 | 'loading very_good_test_runner/test/src/models/test_event_test.dart', 277 | 'suiteID': 0, 278 | 'groupIDs': [], 279 | 'metadata': {'skip': false, 'skipReason': null}, 280 | 'line': null, 281 | 'column': null, 282 | 'url': null, 283 | }, 284 | 'type': 'testStart', 285 | 'time': 1, 286 | }, 287 | { 288 | 'suite': { 289 | 'id': 2, 290 | 'platform': 'vm', 291 | 'path': 292 | 'very_good_test_runner/test/src/flutter_test_runner_test.dart', 293 | }, 294 | 'type': 'suite', 295 | 'time': 10, 296 | }, 297 | { 298 | 'test': { 299 | 'id': 3, 300 | 'name': 301 | 'loading very_good_test_runner/test/src/flutter_test_runner_test.dart', 302 | 'suiteID': 2, 303 | 'groupIDs': [], 304 | 'metadata': {'skip': false, 'skipReason': null}, 305 | 'line': null, 306 | 'column': null, 307 | 'url': null, 308 | }, 309 | 'type': 'testStart', 310 | 'time': 11, 311 | }, 312 | {'count': 2, 'time': 11, 'type': 'allSuites'}, 313 | { 314 | 'testID': 1, 315 | 'result': 'success', 316 | 'skipped': false, 317 | 'hidden': true, 318 | 'type': 'testDone', 319 | 'time': 1058, 320 | }, 321 | { 322 | 'group': { 323 | 'id': 4, 324 | 'suiteID': 0, 325 | 'parentID': null, 326 | 'name': '', 327 | 'metadata': {'skip': false, 'skipReason': null}, 328 | 'testCount': 11, 329 | 'line': null, 330 | 'column': null, 331 | 'url': null, 332 | }, 333 | 'type': 'group', 334 | 'time': 1064, 335 | }, 336 | { 337 | 'group': { 338 | 'id': 5, 339 | 'suiteID': 0, 340 | 'parentID': 4, 341 | 'name': 'TestEvent', 342 | 'metadata': {'skip': false, 'skipReason': null}, 343 | 'testCount': 11, 344 | 'line': 5, 345 | 'column': 3, 346 | 'url': 347 | 'file://very_good_test_runner/test/src/models/test_event_test.dart', 348 | }, 349 | 'type': 'group', 350 | 'time': 1066, 351 | }, 352 | { 353 | 'group': { 354 | 'id': 6, 355 | 'suiteID': 0, 356 | 'parentID': 5, 357 | 'name': 'TestEvent fromJson', 358 | 'metadata': {'skip': false, 'skipReason': null}, 359 | 'testCount': 11, 360 | 'line': 6, 361 | 'column': 5, 362 | 'url': 363 | 'file://very_good_test_runner/test/src/models/test_event_test.dart', 364 | }, 365 | 'type': 'group', 366 | 'time': 1067, 367 | }, 368 | { 369 | 'test': { 370 | 'id': 7, 371 | 'name': 372 | '''TestEvent fromJson throws UnsupportedError when object is not a supported test event''', 373 | 'suiteID': 0, 374 | 'groupIDs': [4, 5, 6], 375 | 'metadata': {'skip': false, 'skipReason': null}, 376 | 'line': 7, 377 | 'column': 7, 378 | 'url': 379 | 'file://very_good_test_runner/test/src/models/test_event_test.dart', 380 | }, 381 | 'type': 'testStart', 382 | 'time': 1067, 383 | }, 384 | { 385 | 'testID': 3, 386 | 'result': 'success', 387 | 'skipped': false, 388 | 'hidden': true, 389 | 'type': 'testDone', 390 | 'time': 1074, 391 | }, 392 | { 393 | 'group': { 394 | 'id': 8, 395 | 'suiteID': 2, 396 | 'parentID': null, 397 | 'name': '', 398 | 'metadata': {'skip': false, 'skipReason': null}, 399 | 'testCount': 3, 400 | 'line': null, 401 | 'column': null, 402 | 'url': null, 403 | }, 404 | 'type': 'group', 405 | 'time': 1074, 406 | }, 407 | { 408 | 'group': { 409 | 'id': 9, 410 | 'suiteID': 2, 411 | 'parentID': 8, 412 | 'name': 'flutterTest', 413 | 'metadata': {'skip': false, 'skipReason': null}, 414 | 'testCount': 3, 415 | 'line': 27, 416 | 'column': 3, 417 | 'url': 418 | 'file://very_good_test_runner/test/src/flutter_test_runner_test.dart', 419 | }, 420 | 'type': 'group', 421 | 'time': 1074, 422 | }, 423 | { 424 | 'test': { 425 | 'id': 10, 426 | 'name': 'flutterTest passes correct parameters to Process.start', 427 | 'suiteID': 2, 428 | 'groupIDs': [8, 9], 429 | 'metadata': {'skip': false, 'skipReason': null}, 430 | 'line': 53, 431 | 'column': 5, 432 | 'url': 433 | 'file://very_good_test_runner/test/src/flutter_test_runner_test.dart', 434 | }, 435 | 'type': 'testStart', 436 | 'time': 1075, 437 | }, 438 | { 439 | 'testID': 7, 440 | 'result': 'success', 441 | 'skipped': false, 442 | 'hidden': false, 443 | 'type': 'testDone', 444 | 'time': 1125, 445 | }, 446 | { 447 | 'test': { 448 | 'id': 11, 449 | 'name': 450 | 'TestEvent fromJson returns StartTestEvent when type is start', 451 | 'suiteID': 0, 452 | 'groupIDs': [4, 5, 6], 453 | 'metadata': {'skip': false, 'skipReason': null}, 454 | 'line': 20, 455 | 'column': 7, 456 | 'url': 457 | 'file://very_good_test_runner/test/src/models/test_event_test.dart', 458 | }, 459 | 'type': 'testStart', 460 | 'time': 1125, 461 | }, 462 | { 463 | 'testID': 11, 464 | 'result': 'success', 465 | 'skipped': false, 466 | 'hidden': false, 467 | 'type': 'testDone', 468 | 'time': 1132, 469 | }, 470 | { 471 | 'test': { 472 | 'id': 12, 473 | 'name': 474 | 'TestEvent fromJson returns SuiteTestEvent when type is suite', 475 | 'suiteID': 0, 476 | 'groupIDs': [4, 5, 6], 477 | 'metadata': {'skip': false, 'skipReason': null}, 478 | 'line': 31, 479 | 'column': 7, 480 | 'url': 481 | 'file://very_good_test_runner/test/src/models/test_event_test.dart', 482 | }, 483 | 'type': 'testStart', 484 | 'time': 1133, 485 | }, 486 | { 487 | 'testID': 12, 488 | 'result': 'success', 489 | 'skipped': false, 490 | 'hidden': false, 491 | 'type': 'testDone', 492 | 'time': 1138, 493 | }, 494 | { 495 | 'test': { 496 | 'id': 13, 497 | 'name': 498 | 'TestEvent fromJson returns GroupTestEvent when type is group', 499 | 'suiteID': 0, 500 | 'groupIDs': [4, 5, 6], 501 | 'metadata': {'skip': false, 'skipReason': null}, 502 | 'line': 44, 503 | 'column': 7, 504 | 'url': 505 | 'file://very_good_test_runner/test/src/models/test_event_test.dart', 506 | }, 507 | 'type': 'testStart', 508 | 'time': 1139, 509 | }, 510 | { 511 | 'testID': 13, 512 | 'result': 'success', 513 | 'skipped': false, 514 | 'hidden': false, 515 | 'type': 'testDone', 516 | 'time': 1148, 517 | }, 518 | { 519 | 'test': { 520 | 'id': 14, 521 | 'name': 522 | '''TestEvent fromJson returns TestStartEvent when type is testStart''', 523 | 'suiteID': 0, 524 | 'groupIDs': [4, 5, 6], 525 | 'metadata': {'skip': false, 'skipReason': null}, 526 | 'line': 64, 527 | 'column': 7, 528 | 'url': 529 | 'file://very_good_test_runner/test/src/models/test_event_test.dart', 530 | }, 531 | 'type': 'testStart', 532 | 'time': 1148, 533 | }, 534 | { 535 | 'testID': 14, 536 | 'result': 'success', 537 | 'skipped': false, 538 | 'hidden': false, 539 | 'type': 'testDone', 540 | 'time': 1155, 541 | }, 542 | { 543 | 'test': { 544 | 'id': 15, 545 | 'name': 546 | '''TestEvent fromJson returns AllSuitesTestEvent when type is allSuites''', 547 | 'suiteID': 0, 548 | 'groupIDs': [4, 5, 6], 549 | 'metadata': {'skip': false, 'skipReason': null}, 550 | 'line': 83, 551 | 'column': 7, 552 | 'url': 553 | 'file://very_good_test_runner/test/src/models/test_event_test.dart', 554 | }, 555 | 'type': 'testStart', 556 | 'time': 1155, 557 | }, 558 | { 559 | 'testID': 15, 560 | 'result': 'success', 561 | 'skipped': false, 562 | 'hidden': false, 563 | 'type': 'testDone', 564 | 'time': 1160, 565 | }, 566 | { 567 | 'test': { 568 | 'id': 16, 569 | 'name': 570 | 'TestEvent fromJson returns ErrorTestEvent when type is error', 571 | 'suiteID': 0, 572 | 'groupIDs': [4, 5, 6], 573 | 'metadata': {'skip': false, 'skipReason': null}, 574 | 'line': 89, 575 | 'column': 7, 576 | 'url': 577 | 'file://very_good_test_runner/test/src/models/test_event_test.dart', 578 | }, 579 | 'type': 'testStart', 580 | 'time': 1160, 581 | }, 582 | { 583 | 'testID': 16, 584 | 'result': 'success', 585 | 'skipped': false, 586 | 'hidden': false, 587 | 'type': 'testDone', 588 | 'time': 1167, 589 | }, 590 | { 591 | 'test': { 592 | 'id': 17, 593 | 'name': 594 | '''TestEvent fromJson returns MessageTestEvent when type is print''', 595 | 'suiteID': 0, 596 | 'groupIDs': [4, 5, 6], 597 | 'metadata': {'skip': false, 'skipReason': null}, 598 | 'line': 103, 599 | 'column': 7, 600 | 'url': 601 | 'file://very_good_test_runner/test/src/models/test_event_test.dart', 602 | }, 603 | 'type': 'testStart', 604 | 'time': 1168, 605 | }, 606 | { 607 | 'testID': 17, 608 | 'result': 'success', 609 | 'skipped': false, 610 | 'hidden': false, 611 | 'type': 'testDone', 612 | 'time': 1176, 613 | }, 614 | { 615 | 'test': { 616 | 'id': 18, 617 | 'name': 618 | 'TestEvent fromJson returns DebugTestEvent when type is debug', 619 | 'suiteID': 0, 620 | 'groupIDs': [4, 5, 6], 621 | 'metadata': {'skip': false, 'skipReason': null}, 622 | 'line': 116, 623 | 'column': 7, 624 | 'url': 625 | 'file://very_good_test_runner/test/src/models/test_event_test.dart', 626 | }, 627 | 'type': 'testStart', 628 | 'time': 1176, 629 | }, 630 | { 631 | 'testID': 10, 632 | 'result': 'success', 633 | 'skipped': false, 634 | 'hidden': false, 635 | 'type': 'testDone', 636 | 'time': 1179, 637 | }, 638 | { 639 | 'test': { 640 | 'id': 19, 641 | 'name': 'flutterTest emits error from stderr', 642 | 'suiteID': 2, 643 | 'groupIDs': [8, 9], 644 | 'metadata': {'skip': false, 'skipReason': null}, 645 | 'line': 81, 646 | 'column': 5, 647 | 'url': 648 | 'file://very_good_test_runner/test/src/flutter_test_runner_test.dart', 649 | }, 650 | 'type': 'testStart', 651 | 'time': 1179, 652 | }, 653 | { 654 | 'testID': 18, 655 | 'result': 'success', 656 | 'skipped': false, 657 | 'hidden': false, 658 | 'type': 'testDone', 659 | 'time': 1183, 660 | }, 661 | { 662 | 'test': { 663 | 'id': 20, 664 | 'name': 665 | '''TestEvent fromJson returns TestDoneEvent when type is testDone''', 666 | 'suiteID': 0, 667 | 'groupIDs': [4, 5, 6], 668 | 'metadata': {'skip': false, 'skipReason': null}, 669 | 'line': 128, 670 | 'column': 7, 671 | 'url': 672 | 'file://very_good_test_runner/test/src/models/test_event_test.dart', 673 | }, 674 | 'type': 'testStart', 675 | 'time': 1184, 676 | }, 677 | { 678 | 'testID': 19, 679 | 'result': 'success', 680 | 'skipped': false, 681 | 'hidden': false, 682 | 'type': 'testDone', 683 | 'time': 1192, 684 | }, 685 | { 686 | 'test': { 687 | 'id': 21, 688 | 'name': 'flutterTest kills process when subscription is canceled', 689 | 'suiteID': 2, 690 | 'groupIDs': [8, 9], 691 | 'metadata': {'skip': false, 'skipReason': null}, 692 | 'line': 94, 693 | 'column': 5, 694 | 'url': 695 | 'file://very_good_test_runner/test/src/flutter_test_runner_test.dart', 696 | }, 697 | 'type': 'testStart', 698 | 'time': 1192, 699 | }, 700 | { 701 | 'testID': 20, 702 | 'result': 'success', 703 | 'skipped': false, 704 | 'hidden': false, 705 | 'type': 'testDone', 706 | 'time': 1193, 707 | }, 708 | { 709 | 'test': { 710 | 'id': 22, 711 | 'name': 712 | 'TestEvent fromJson returns DoneTestEvent when type is done', 713 | 'suiteID': 0, 714 | 'groupIDs': [4, 5, 6], 715 | 'metadata': {'skip': false, 'skipReason': null}, 716 | 'line': 141, 717 | 'column': 7, 718 | 'url': 719 | 'file://very_good_test_runner/test/src/models/test_event_test.dart', 720 | }, 721 | 'type': 'testStart', 722 | 'time': 1194, 723 | }, 724 | { 725 | 'testID': 22, 726 | 'result': 'success', 727 | 'skipped': false, 728 | 'hidden': false, 729 | 'type': 'testDone', 730 | 'time': 1199, 731 | }, 732 | { 733 | 'testID': 21, 734 | 'result': 'success', 735 | 'skipped': false, 736 | 'hidden': false, 737 | 'type': 'testDone', 738 | 'time': 1199, 739 | }, 740 | {'success': true, 'type': 'done', 'time': 1229}, 741 | ]; 742 | final events = []; 743 | final testEvents = flutterTest(startProcess: testProcess.start); 744 | final subscription = testEvents.listen(events.add); 745 | final encodedEvents = rawEvents.map( 746 | (element) => utf8.encode(json.encode(element)), 747 | ); 748 | await stdoutController.addStream(Stream.fromIterable(encodedEvents)); 749 | expect( 750 | events, 751 | equals([ 752 | isA(), 753 | isA(), 754 | isA(), 755 | isA(), 756 | isA(), 757 | isA(), 758 | isA(), 759 | isA(), 760 | isA(), 761 | isA(), 762 | isA(), 763 | isA(), 764 | isA(), 765 | isA(), 766 | isA(), 767 | isA(), 768 | isA(), 769 | isA(), 770 | isA(), 771 | isA(), 772 | isA(), 773 | isA(), 774 | isA(), 775 | isA(), 776 | isA(), 777 | isA(), 778 | isA(), 779 | isA(), 780 | isA(), 781 | isA(), 782 | isA(), 783 | isA(), 784 | isA(), 785 | isA(), 786 | isA(), 787 | isA(), 788 | isA(), 789 | isA(), 790 | isA(), 791 | isA(), 792 | isA(), 793 | isA(), 794 | ]), 795 | ); 796 | completer.complete(0); 797 | await Future.delayed(Duration.zero); 798 | expect(events.last, isA()); 799 | unawaited(subscription.cancel()); 800 | }); 801 | }); 802 | } 803 | -------------------------------------------------------------------------------- /tool/release_ready.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Ensures that the package is ready for a release. 4 | # 5 | # Will update the version.dart file and update the CHANGELOG.md. 6 | # 7 | # Set it up for a new version: 8 | # `./release_ready.sh 9 | 10 | # Check if current directory is usable for this script, if so we assume it is correctly set up. 11 | if [ ! -f "pubspec.yaml" ]; then 12 | echo "$(pwd) is not a valid (dart/npm) package or brick." 13 | exit 1 14 | fi 15 | 16 | currentBranch=$(git symbolic-ref --short -q HEAD) 17 | if [[ ! $currentBranch == "main" ]]; then 18 | echo "Releasing is only supported on the main branch." 19 | exit 1 20 | fi 21 | 22 | # Get information 23 | old_version=$(dart pub deps --json | pcregrep -o1 -i '"version": "(.*?)"' | head -1) 24 | 25 | if [ -z "$old_version" ]; then 26 | echo "Current version was not resolved." 27 | exit 1 28 | fi 29 | 30 | # Get new version 31 | new_version="$1"; 32 | 33 | if [[ "$new_version" == "" ]]; then 34 | echo "No new version supplied, please provide one" 35 | exit 1 36 | fi 37 | 38 | if [[ "$new_version" == "$old_version" ]]; then 39 | echo "Current version is $old_version, can't update." 40 | exit 1 41 | fi 42 | 43 | # Retrieving all the commits in the current directory since the last tag. 44 | previousTag="v${old_version}" 45 | raw_commits="$(git log --pretty=format:"%s" --no-merges --reverse $previousTag..HEAD -- .)" 46 | markdown_commits=$(echo "$raw_commits" | sed -En "s/\(#([0-9]+)\)/([#\1](https:\/\/github.com\/VeryGoodOpenSource\/very_good_test_runner\/pull\/\1))/p") 47 | 48 | if [[ "$markdown_commits" == "" ]]; then 49 | echo "No commits since last tag, can't update." 50 | exit 0 51 | fi 52 | commits=$(echo "$markdown_commits" | sed -En "s/^/- /p") 53 | 54 | echo "Updating version to $new_version" 55 | sed -i '' "s/version: $old_version/version: $new_version/g" pubspec.yaml 56 | 57 | # Update dart file with new version. 58 | dart run build_runner build --delete-conflicting-outputs > /dev/null 59 | 60 | if grep -q v$new_version "CHANGELOG.md"; then 61 | echo "CHANGELOG already contains version $new_version." 62 | exit 1 63 | fi 64 | 65 | # Add a new version entry with the found commits to the CHANGELOG.md. 66 | echo "# ${new_version} \n\n ${commits}\n\n$(cat CHANGELOG.md)" > CHANGELOG.md 67 | echo "CHANGELOG generated, validate entries here: $(pwd)/CHANGELOG.md" 68 | 69 | echo "Creating git branch for ver_good_cli@$new_version" 70 | git checkout -b "chore/$new_version" > /dev/null 71 | 72 | git add pubspec.yaml CHANGELOG.md 73 | if [ -f lib/src/version.dart ]; then 74 | git add lib/src/version.dart 75 | fi 76 | 77 | echo "" 78 | echo "Run the following command if you wish to commit the changes:" 79 | echo "git commit -m \"chore: v$new_version\"" --------------------------------------------------------------------------------