diff --git a/.github/ISSUE_TEMPLATE/pub_semver.md b/.github/ISSUE_TEMPLATE/pub_semver.md new file mode 100644 index 000000000..c7db9b5c1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/pub_semver.md @@ -0,0 +1,5 @@ +--- +name: "package:pub_semver" +about: "Create a bug or file a feature request against package:pub_semver." +labels: "package:pub_semver" +--- \ No newline at end of file diff --git a/.github/labeler.yml b/.github/labeler.yml index c4d658fba..09e5425d0 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -88,6 +88,10 @@ - changed-files: - any-glob-to-any-file: 'pkgs/pool/**' +'package:pub_semver': + - changed-files: + - any-glob-to-any-file: 'pkgs/pub_semver/**' + 'package:source_map_stack_trace': - changed-files: - any-glob-to-any-file: 'pkgs/source_map_stack_trace/**' diff --git a/.github/workflows/pub_semver.yaml b/.github/workflows/pub_semver.yaml new file mode 100644 index 000000000..ba0db18a3 --- /dev/null +++ b/.github/workflows/pub_semver.yaml @@ -0,0 +1,75 @@ +name: package:pub_semver + +on: + # Run on PRs and pushes to the default branch. + push: + branches: [ main ] + paths: + - '.github/workflows/pub_semver.yaml' + - 'pkgs/pub_semver/**' + pull_request: + branches: [ main ] + paths: + - '.github/workflows/pub_semver.yaml' + - 'pkgs/pub_semver/**' + schedule: + - cron: "0 0 * * 0" + +env: + PUB_ENVIRONMENT: bot.github + + +defaults: + run: + working-directory: pkgs/pub_semver/ + +jobs: + # Check code formatting and static analysis on a single OS (linux) + # against Dart dev. + analyze: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + sdk: [dev] + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Check formatting + run: dart format --output=none --set-exit-if-changed . + if: always() && steps.install.outcome == 'success' + - name: Analyze code + run: dart analyze --fatal-infos + if: always() && steps.install.outcome == 'success' + + # Run tests on a matrix consisting of two dimensions: + # 1. OS: ubuntu-latest, (macos-latest, windows-latest) + # 2. release channel: dev + test: + needs: analyze + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # Add macos-latest and/or windows-latest if relevant for this package. + os: [ubuntu-latest] + sdk: [3.4, dev] + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + - uses: dart-lang/setup-dart@e630b99d28a3b71860378cafdc2a067c71107f94 + with: + sdk: ${{ matrix.sdk }} + - id: install + name: Install dependencies + run: dart pub get + - name: Run VM tests + run: dart test --platform vm + if: always() && steps.install.outcome == 'success' + - name: Run Chrome tests + run: dart test --platform chrome --compiler dart2js,dart2wasm + if: always() && steps.install.outcome == 'success' diff --git a/README.md b/README.md index 0b97b210f..7846ba911 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ don't naturally belong to other topic monorepos (like | [oauth2](pkgs/oauth2/) | A client library for authenticating with a remote service via OAuth2 on behalf of a user, and making authorized HTTP requests with the user's OAuth2 credentials. | [![package issues](https://img.shields.io/badge/package:oauth2-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aoauth2) | [![pub package](https://img.shields.io/pub/v/oauth2.svg)](https://pub.dev/packages/oauth2) | | [package_config](pkgs/package_config/) | Support for reading and writing Dart Package Configuration files. | [![package issues](https://img.shields.io/badge/package:package_config-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apackage_config) | [![pub package](https://img.shields.io/pub/v/package_config.svg)](https://pub.dev/packages/package_config) | | [pool](pkgs/pool/) | Manage a finite pool of resources. Useful for controlling concurrent file system or network requests. | [![package issues](https://img.shields.io/badge/package:pool-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apool) | [![pub package](https://img.shields.io/pub/v/pool.svg)](https://pub.dev/packages/pool) | +| [pub_semver](pkgs/pub_semver/) | Versions and version constraints implementing pub's versioning policy. This is very similar to vanilla semver, with a few corner cases. | [![package issues](https://img.shields.io/badge/package:pub_semver-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Apub_semver) | [![pub package](https://img.shields.io/pub/v/pub_semver.svg)](https://pub.dev/packages/pub_semver) | | [source_map_stack_trace](pkgs/source_map_stack_trace/) | A package for applying source maps to stack traces. | [![package issues](https://img.shields.io/badge/package:source_map_stack_trace-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Asource_map_stack_trace) | [![pub package](https://img.shields.io/pub/v/source_map_stack_trace.svg)](https://pub.dev/packages/source_map_stack_trace) | | [unified_analytics](pkgs/unified_analytics/) | A package for logging analytics for all Dart and Flutter related tooling to Google Analytics. | [![package issues](https://img.shields.io/badge/package:unified_analytics-4774bc)](https://github.com/dart-lang/tools/issues?q=is%3Aissue+is%3Aopen+label%3Apackage%3Aunified_analytics) | [![pub package](https://img.shields.io/pub/v/unified_analytics.svg)](https://pub.dev/packages/unified_analytics) | diff --git a/pkgs/pub_semver/.gitignore b/pkgs/pub_semver/.gitignore new file mode 100644 index 000000000..49ce72d76 --- /dev/null +++ b/pkgs/pub_semver/.gitignore @@ -0,0 +1,3 @@ +.dart_tool/ +.packages +pubspec.lock diff --git a/pkgs/pub_semver/CHANGELOG.md b/pkgs/pub_semver/CHANGELOG.md new file mode 100644 index 000000000..a31fbb243 --- /dev/null +++ b/pkgs/pub_semver/CHANGELOG.md @@ -0,0 +1,177 @@ +## 2.1.5 + +- Require Dart `3.4.0`. +- Move to `dart-lang/tools` monorepo. + +## 2.1.4 + +- Added topics to `pubspec.yaml`. + +## 2.1.3 + +- Add type parameters to the signatures of the `Version.preRelease` and + `Version.build` fields (`List` ==> `List`). + [#74](https://github.com/dart-lang/pub_semver/pull/74). +- Require Dart 2.17. + +## 2.1.2 + +- Add markdown badges to the readme. + +## 2.1.1 + +- Fixed the version parsing pattern to only accept dots between version + components. + +## 2.1.0 + +- Added `Version.canonicalizedVersion` to help scrub leading zeros and highlight + that `Version.toString()` preserves leading zeros. +- Annotated `Version` with `@sealed` to discourage users from implementing the + interface. + +## 2.0.0 + +- Stable null safety release. +- `Version.primary` now throws `StateError` if the `versions` argument is empty. + +## 1.4.4 + +- Fix a bug of `VersionRange.union` where ranges bounded at infinity would get + combined wrongly. + +# 1.4.3 + +- Update Dart SDK constraint to `>=2.0.0 <3.0.0`. +- Update `package:collection` constraint to `^1.0.0`. + +## 1.4.2 + +* Set max SDK version to `<3.0.0`. + +## 1.4.1 + +* Fix a bug where there upper bound of a version range with a build identifier + could accidentally be rewritten. + +## 1.4.0 + +* Add a `Version.firstPreRelease` getter that returns the first possible + pre-release of a version. + +* Add a `Version.isFirstPreRelease` getter that returns whether a version is the + first possible pre-release. + +* `new VersionRange()` with an exclusive maximum now replaces the maximum with + its first pre-release version. This matches the existing semantics, where an + exclusive maximum would exclude pre-release versions of that maximum. + + Explicitly representing this by changing the maximum version ensures that all + operations behave correctly with respect to the special pre-release semantics. + In particular, it fixes bugs where, for example, + `(>=1.0.0 <2.0.0-dev).union(>=2.0.0-dev <2.0.0)` and + `(>=1.0.0 <3.0.0).difference(^1.0.0)` wouldn't include `2.0.0-dev`. + +* Add an `alwaysIncludeMaxPreRelease` parameter to `new VersionRange()`, which + disables the replacement described above and allows users to create ranges + that do include the pre-release versions of an exclusive max version. + +## 1.3.7 + +* Fix more bugs with `VersionRange.intersect()`, `VersionRange.difference()`, + and `VersionRange.union()` involving version ranges with pre-release maximums. + +## 1.3.6 + +* Fix a bug where constraints that only allowed pre-release versions would be + parsed as empty constraints. + +## 1.3.5 + +* Fix a bug where `VersionRange.intersect()` would return incorrect results for + pre-release versions with the same base version number as release versions. + +## 1.3.4 + +* Fix a bug where `VersionRange.allowsAll()`, `VersionRange.allowsAny()`, and + `VersionRange.difference()` would return incorrect results for pre-release + versions with the same base version number as release versions. + +## 1.3.3 + +* Fix a bug where `VersionRange.difference()` with a union constraint that + covered the entire range would crash. + +## 1.3.2 + +* Fix a checked-mode error in `VersionRange.difference()`. + +## 1.3.1 + +* Fix a new strong mode error. + +## 1.3.0 + +* Make the `VersionUnion` class public. This was previously used internally to + implement `new VersionConstraint.unionOf()` and `VersionConstraint.union()`. + Now it's public so you can use it too. + +* Added `VersionConstraint.difference()`. This returns a constraint matching all + versions matched by one constraint but not another. + +* Make `VersionRange` implement `Comparable`. Ranges are ordered + first by lower bound, then by upper bound. + +## 1.2.4 + +* Fix all remaining strong mode warnings. + +## 1.2.3 + +* Addressed three strong mode warnings. + +## 1.2.2 + +* Make the package analyze under strong mode and compile with the DDC (Dart Dev + Compiler). Fix two issues with a private subclass of `VersionConstraint` + having different types for overridden methods. + +## 1.2.1 + +* Allow version ranges like `>=1.2.3-dev.1 <1.2.3` to match pre-release versions + of `1.2.3`. Previously, these didn't match, since the pre-release versions had + the same major, minor, and patch numbers as the max; now an exception has been + added if they also have the same major, minor, and patch numbers as the min + *and* the min is also a pre-release version. + +## 1.2.0 + +* Add a `VersionConstraint.union()` method and a `new + VersionConstraint.unionOf()` constructor. These each return a constraint that + matches multiple existing constraints. + +* Add a `VersionConstraint.allowsAll()` method, which returns whether one + constraint is a superset of another. + +* Add a `VersionConstraint.allowsAny()` method, which returns whether one + constraint overlaps another. + +* `Version` now implements `VersionRange`. + +## 1.1.0 + +* Add support for the `^` operator for compatible versions according to pub's + notion of compatibility. `^1.2.3` is equivalent to `>=1.2.3 <2.0.0`; `^0.1.2` + is equivalent to `>=0.1.2 <0.2.0`. + +* Add `Version.nextBreaking`, which returns the next version that introduces + breaking changes after a given version. + +* Add `new VersionConstraint.compatibleWith()`, which returns a range covering + all versions compatible with a given version. + +* Add a custom `VersionRange.hashCode` to make it properly hashable. + +## 1.0.0 + +* Initial release. diff --git a/pkgs/pub_semver/LICENSE b/pkgs/pub_semver/LICENSE new file mode 100644 index 000000000..000cd7bec --- /dev/null +++ b/pkgs/pub_semver/LICENSE @@ -0,0 +1,27 @@ +Copyright 2014, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkgs/pub_semver/README.md b/pkgs/pub_semver/README.md new file mode 100644 index 000000000..03c92a3c5 --- /dev/null +++ b/pkgs/pub_semver/README.md @@ -0,0 +1,107 @@ +[![Build Status](https://github.com/dart-lang/tools/actions/workflows/pub_semver.yaml/badge.svg)](https://github.com/dart-lang/tools/actions/workflows/pub_semver.yaml) +[![pub package](https://img.shields.io/pub/v/pub_semver.svg)](https://pub.dev/packages/pub_semver) +[![package publisher](https://img.shields.io/pub/publisher/pub_semver.svg)](https://pub.dev/packages/pub_semver/publisher) + +Handles version numbers and version constraints in the same way that [pub][] +does. + +## Semantics + +The semantics here very closely follow the +[Semantic Versioning spec version 2.0.0-rc.1][semver]. It differs from semver +in a few corner cases: + + * **Version ordering does take build suffixes into account.** This is unlike + semver 2.0.0 but like earlier versions of semver. Version `1.2.3+1` is + considered a lower number than `1.2.3+2`. + + Since a package may have published multiple versions that differ only by + build suffix, pub still has to pick one of them *somehow*. Semver leaves + that issue unresolved, so we just say that build numbers are sorted like + pre-release suffixes. + + * **Pre-release versions are excluded from most max ranges.** Let's say a + user is depending on "foo" with constraint `>=1.0.0 <2.0.0` and that "foo" + has published these versions: + + * `1.0.0` + * `1.1.0` + * `1.2.0` + * `2.0.0-alpha` + * `2.0.0-beta` + * `2.0.0` + * `2.1.0` + + Versions `2.0.0` and `2.1.0` are excluded by the constraint since neither + matches `<2.0.0`. However, since semver specifies that pre-release versions + are lower than the non-prerelease version (i.e. `2.0.0-beta < 2.0.0`, then + the `<2.0.0` constraint does technically allow those. + + But that's almost never what the user wants. If their package doesn't work + with foo `2.0.0`, it's certainly not likely to work with experimental, + unstable versions of `2.0.0`'s API, which is what pre-release versions + represent. + + To handle that, `<` version ranges don't allow pre-release versions of the + maximum unless the max is itself a pre-release, or the min is a pre-release + of the same version. In other words, a `<2.0.0` constraint will prohibit not + just `2.0.0` but any pre-release of `2.0.0`. However, `<2.0.0-beta` will + exclude `2.0.0-beta` but allow `2.0.0-alpha`. Likewise, `>2.0.0-alpha + <2.0.0` will exclude `2.0.0-alpha` but allow `2.0.0-beta`. + + * **Pre-release versions are avoided when possible.** The above case + handles pre-release versions at the top of the range, but what about in + the middle? What if "foo" has these versions: + + * `1.0.0` + * `1.2.0-alpha` + * `1.2.0` + * `1.3.0-experimental` + + When a number of versions are valid, pub chooses the best one where "best" + usually means "highest numbered". That follows the user's intuition that, + all else being equal, they want the latest and greatest. Here, that would + mean `1.3.0-experimental`. However, most users don't want to use unstable + versions of their dependencies. + + We want pre-releases to be explicitly opt-in so that package consumers + don't get unpleasant surprises and so that package maintainers are free to + put out pre-releases and get feedback without dragging all of their users + onto the bleeding edge. + + To accommodate that, when pub is choosing a version, it uses *priority* + order which is different from strict comparison ordering. Any stable + version is considered higher priority than any unstable version. The above + versions, in priority order, are: + + * `1.2.0-alpha` + * `1.3.0-experimental` + * `1.0.0` + * `1.2.0` + + This ensures that users only end up with an unstable version when there are + no alternatives. Usually this means they've picked a constraint that + specifically selects that unstable version -- they've deliberately opted + into it. + + * **There is a notion of compatibility between pre-1.0.0 versions.** Semver + deems all pre-1.0.0 versions to be incompatible. This means that the only + way to ensure compatibility when depending on a pre-1.0.0 package is to + pin the dependency to an exact version. Pinned version constraints prevent + automatic patch and pre-release updates. To avoid this situation, pub + defines the "next breaking" version as the version which increments the + major version if it's greater than zero, and the minor version otherwise, + resets subsequent digits to zero, and strips any pre-release or build + suffix. For example, here are some versions along with their next breaking + ones: + + `0.0.3` -> `0.1.0` + `0.7.2-alpha` -> `0.8.0` + `1.2.3` -> `2.0.0` + + To make use of this, pub defines a "^" operator which yields a version + constraint greater than or equal to a given version, but less than its next + breaking one. + +[pub]: https://pub.dev +[semver]: https://semver.org/spec/v2.0.0-rc.1.html diff --git a/pkgs/pub_semver/analysis_options.yaml b/pkgs/pub_semver/analysis_options.yaml new file mode 100644 index 000000000..76380a006 --- /dev/null +++ b/pkgs/pub_semver/analysis_options.yaml @@ -0,0 +1,31 @@ +# https://dart.dev/guides/language/analysis-options +include: package:dart_flutter_team_lints/analysis_options.yaml + +analyzer: + language: + strict-casts: true + strict-inference: true + strict-raw-types: true + +linter: + rules: + - avoid_bool_literals_in_conditional_expressions + - avoid_classes_with_only_static_members + - avoid_private_typedef_functions + - avoid_redundant_argument_values + - avoid_returning_this + - avoid_unused_constructor_parameters + - avoid_void_async + - cancel_subscriptions + - cascade_invocations + - join_return_with_assignment + - literal_only_boolean_expressions + - missing_whitespace_between_adjacent_strings + - no_adjacent_strings_in_list + - no_runtimeType_toString + - prefer_const_declarations + - prefer_expression_function_bodies + - unnecessary_await_in_return + - use_if_null_to_convert_nulls_to_bools + - use_raw_strings + - use_string_buffers diff --git a/pkgs/pub_semver/example/example.dart b/pkgs/pub_semver/example/example.dart new file mode 100644 index 000000000..890343c93 --- /dev/null +++ b/pkgs/pub_semver/example/example.dart @@ -0,0 +1,17 @@ +// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:pub_semver/pub_semver.dart'; + +void main() { + final range = VersionConstraint.parse('^2.0.0'); + + for (var version in [ + Version.parse('1.2.3-pre'), + Version.parse('2.0.0+123'), + Version.parse('3.0.0-dev'), + ]) { + print('$version ${version.isPreRelease} ${range.allows(version)}'); + } +} diff --git a/pkgs/pub_semver/lib/pub_semver.dart b/pkgs/pub_semver/lib/pub_semver.dart new file mode 100644 index 000000000..4b6487c9e --- /dev/null +++ b/pkgs/pub_semver/lib/pub_semver.dart @@ -0,0 +1,8 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +export 'src/version.dart'; +export 'src/version_constraint.dart'; +export 'src/version_range.dart' hide CompatibleWithVersionRange; +export 'src/version_union.dart'; diff --git a/pkgs/pub_semver/lib/src/patterns.dart b/pkgs/pub_semver/lib/src/patterns.dart new file mode 100644 index 000000000..03119acf9 --- /dev/null +++ b/pkgs/pub_semver/lib/src/patterns.dart @@ -0,0 +1,19 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +/// Regex that matches a version number at the beginning of a string. +final startVersion = RegExp(r'^' // Start at beginning. + r'(\d+)\.(\d+)\.(\d+)' // Version number. + r'(-([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?' // Pre-release. + r'(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?'); // Build. + +/// Like [startVersion] but matches the entire string. +final completeVersion = RegExp('${startVersion.pattern}\$'); + +/// Parses a comparison operator ("<", ">", "<=", or ">=") at the beginning of +/// a string. +final startComparison = RegExp(r'^[<>]=?'); + +/// The "compatible with" operator. +const compatibleWithChar = '^'; diff --git a/pkgs/pub_semver/lib/src/utils.dart b/pkgs/pub_semver/lib/src/utils.dart new file mode 100644 index 000000000..a9f714f02 --- /dev/null +++ b/pkgs/pub_semver/lib/src/utils.dart @@ -0,0 +1,58 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'version.dart'; +import 'version_range.dart'; + +/// Returns whether [range1] is immediately next to, but not overlapping, +/// [range2]. +bool areAdjacent(VersionRange range1, VersionRange range2) { + if (range1.max != range2.min) return false; + + return (range1.includeMax && !range2.includeMin) || + (!range1.includeMax && range2.includeMin); +} + +/// Returns whether [range1] allows lower versions than [range2]. +bool allowsLower(VersionRange range1, VersionRange range2) { + if (range1.min == null) return range2.min != null; + if (range2.min == null) return false; + + var comparison = range1.min!.compareTo(range2.min!); + if (comparison == -1) return true; + if (comparison == 1) return false; + return range1.includeMin && !range2.includeMin; +} + +/// Returns whether [range1] allows higher versions than [range2]. +bool allowsHigher(VersionRange range1, VersionRange range2) { + if (range1.max == null) return range2.max != null; + if (range2.max == null) return false; + + var comparison = range1.max!.compareTo(range2.max!); + if (comparison == 1) return true; + if (comparison == -1) return false; + return range1.includeMax && !range2.includeMax; +} + +/// Returns whether [range1] allows only versions lower than those allowed by +/// [range2]. +bool strictlyLower(VersionRange range1, VersionRange range2) { + if (range1.max == null || range2.min == null) return false; + + var comparison = range1.max!.compareTo(range2.min!); + if (comparison == -1) return true; + if (comparison == 1) return false; + return !range1.includeMax || !range2.includeMin; +} + +/// Returns whether [range1] allows only versions higher than those allowed by +/// [range2]. +bool strictlyHigher(VersionRange range1, VersionRange range2) => + strictlyLower(range2, range1); + +bool equalsWithoutPreRelease(Version version1, Version version2) => + version1.major == version2.major && + version1.minor == version2.minor && + version1.patch == version2.patch; diff --git a/pkgs/pub_semver/lib/src/version.dart b/pkgs/pub_semver/lib/src/version.dart new file mode 100644 index 000000000..90f3d535f --- /dev/null +++ b/pkgs/pub_semver/lib/src/version.dart @@ -0,0 +1,391 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:math' as math; + +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart' show sealed; + +import 'patterns.dart'; +import 'version_constraint.dart'; +import 'version_range.dart'; + +/// The equality operator to use for comparing version components. +const _equality = IterableEquality(); + +/// A parsed semantic version number. +@sealed +class Version implements VersionConstraint, VersionRange { + /// No released version: i.e. "0.0.0". + static Version get none => Version(0, 0, 0); + + /// Compares [a] and [b] to see which takes priority over the other. + /// + /// Returns `1` if [a] takes priority over [b] and `-1` if vice versa. If + /// [a] and [b] are equivalent, returns `0`. + /// + /// Unlike [compareTo], which *orders* versions, this determines which + /// version a user is likely to prefer. In particular, it prioritizes + /// pre-release versions lower than stable versions, regardless of their + /// version numbers. Pub uses this when determining which version to prefer + /// when a number of versions are allowed. In that case, it will always + /// choose a stable version when possible. + /// + /// When used to sort a list, orders in ascending priority so that the + /// highest priority version is *last* in the result. + static int prioritize(Version a, Version b) { + // Sort all prerelease versions after all normal versions. This way + // the solver will prefer stable packages over unstable ones. + if (a.isPreRelease && !b.isPreRelease) return -1; + if (!a.isPreRelease && b.isPreRelease) return 1; + + return a.compareTo(b); + } + + /// Like [prioritize], but lower version numbers are considered greater than + /// higher version numbers. + /// + /// This still considers prerelease versions to be lower than non-prerelease + /// versions. Pub uses this when downgrading -- it chooses the lowest version + /// but still excludes pre-release versions when possible. + static int antiprioritize(Version a, Version b) { + if (a.isPreRelease && !b.isPreRelease) return -1; + if (!a.isPreRelease && b.isPreRelease) return 1; + + return b.compareTo(a); + } + + /// The major version number: "1" in "1.2.3". + final int major; + + /// The minor version number: "2" in "1.2.3". + final int minor; + + /// The patch version number: "3" in "1.2.3". + final int patch; + + /// The pre-release identifier: "foo" in "1.2.3-foo". + /// + /// This is split into a list of components, each of which may be either a + /// string or a non-negative integer. It may also be empty, indicating that + /// this version has no pre-release identifier. + final List preRelease; + + /// The build identifier: "foo" in "1.2.3+foo". + /// + /// This is split into a list of components, each of which may be either a + /// string or a non-negative integer. It may also be empty, indicating that + /// this version has no build identifier. + final List build; + + /// The original string representation of the version number. + /// + /// This preserves textual artifacts like leading zeros that may be left out + /// of the parsed version. + final String _text; + + @override + Version get min => this; + @override + Version get max => this; + @override + bool get includeMin => true; + @override + bool get includeMax => true; + + Version._(this.major, this.minor, this.patch, String? preRelease, + String? build, this._text) + : preRelease = preRelease == null ? [] : _splitParts(preRelease), + build = build == null ? [] : _splitParts(build) { + if (major < 0) throw ArgumentError('Major version must be non-negative.'); + if (minor < 0) throw ArgumentError('Minor version must be non-negative.'); + if (patch < 0) throw ArgumentError('Patch version must be non-negative.'); + } + + /// Creates a new [Version] object. + factory Version(int major, int minor, int patch, + {String? pre, String? build}) { + var text = '$major.$minor.$patch'; + if (pre != null) text += '-$pre'; + if (build != null) text += '+$build'; + + return Version._(major, minor, patch, pre, build, text); + } + + /// Creates a new [Version] by parsing [text]. + factory Version.parse(String text) { + final match = completeVersion.firstMatch(text); + if (match == null) { + throw FormatException('Could not parse "$text".'); + } + + try { + var major = int.parse(match[1]!); + var minor = int.parse(match[2]!); + var patch = int.parse(match[3]!); + + var preRelease = match[5]; + var build = match[8]; + + return Version._(major, minor, patch, preRelease, build, text); + } on FormatException { + throw FormatException('Could not parse "$text".'); + } + } + + /// Returns the primary version out of [versions]. + /// + /// This is the highest-numbered stable (non-prerelease) version. If there + /// are no stable versions, it's just the highest-numbered version. + /// + /// If [versions] is empty, throws a [StateError]. + static Version primary(List versions) { + var primary = versions.first; + for (var version in versions.skip(1)) { + if ((!version.isPreRelease && primary.isPreRelease) || + (version.isPreRelease == primary.isPreRelease && version > primary)) { + primary = version; + } + } + return primary; + } + + /// Splits a string of dot-delimited identifiers into their component parts. + /// + /// Identifiers that are numeric are converted to numbers. + static List _splitParts(String text) => text + .split('.') + .map((part) => + // Return an integer part if possible, otherwise return the string + // as-is + int.tryParse(part) ?? part) + .toList(); + + @override + bool operator ==(Object other) => + other is Version && + major == other.major && + minor == other.minor && + patch == other.patch && + _equality.equals(preRelease, other.preRelease) && + _equality.equals(build, other.build); + + @override + int get hashCode => + major ^ + minor ^ + patch ^ + _equality.hash(preRelease) ^ + _equality.hash(build); + + bool operator <(Version other) => compareTo(other) < 0; + bool operator >(Version other) => compareTo(other) > 0; + bool operator <=(Version other) => compareTo(other) <= 0; + bool operator >=(Version other) => compareTo(other) >= 0; + + @override + bool get isAny => false; + @override + bool get isEmpty => false; + + /// Whether or not this is a pre-release version. + bool get isPreRelease => preRelease.isNotEmpty; + + /// Gets the next major version number that follows this one. + /// + /// If this version is a pre-release of a major version release (i.e. the + /// minor and patch versions are zero), then it just strips the pre-release + /// suffix. Otherwise, it increments the major version and resets the minor + /// and patch. + Version get nextMajor { + if (isPreRelease && minor == 0 && patch == 0) { + return Version(major, minor, patch); + } + + return _incrementMajor(); + } + + /// Gets the next minor version number that follows this one. + /// + /// If this version is a pre-release of a minor version release (i.e. the + /// patch version is zero), then it just strips the pre-release suffix. + /// Otherwise, it increments the minor version and resets the patch. + Version get nextMinor { + if (isPreRelease && patch == 0) { + return Version(major, minor, patch); + } + + return _incrementMinor(); + } + + /// Gets the next patch version number that follows this one. + /// + /// If this version is a pre-release, then it just strips the pre-release + /// suffix. Otherwise, it increments the patch version. + Version get nextPatch { + if (isPreRelease) { + return Version(major, minor, patch); + } + + return _incrementPatch(); + } + + /// Gets the next breaking version number that follows this one. + /// + /// Increments [major] if it's greater than zero, otherwise [minor], resets + /// subsequent digits to zero, and strips any [preRelease] or [build] + /// suffix. + Version get nextBreaking { + if (major == 0) { + return _incrementMinor(); + } + + return _incrementMajor(); + } + + /// Returns the first possible pre-release of this version. + Version get firstPreRelease => Version(major, minor, patch, pre: '0'); + + /// Returns whether this is the first possible pre-release of its version. + bool get isFirstPreRelease => preRelease.length == 1 && preRelease.first == 0; + + Version _incrementMajor() => Version(major + 1, 0, 0); + Version _incrementMinor() => Version(major, minor + 1, 0); + Version _incrementPatch() => Version(major, minor, patch + 1); + + /// Tests if [other] matches this version exactly. + @override + bool allows(Version other) => this == other; + + @override + bool allowsAll(VersionConstraint other) => other.isEmpty || other == this; + + @override + bool allowsAny(VersionConstraint other) => other.allows(this); + + @override + VersionConstraint intersect(VersionConstraint other) => + other.allows(this) ? this : VersionConstraint.empty; + + @override + VersionConstraint union(VersionConstraint other) { + if (other.allows(this)) return other; + + if (other is VersionRange) { + if (other.min == this) { + return VersionRange( + min: other.min, + max: other.max, + includeMin: true, + includeMax: other.includeMax, + alwaysIncludeMaxPreRelease: true); + } + + if (other.max == this) { + return VersionRange( + min: other.min, + max: other.max, + includeMin: other.includeMin, + includeMax: true, + alwaysIncludeMaxPreRelease: true); + } + } + + return VersionConstraint.unionOf([this, other]); + } + + @override + VersionConstraint difference(VersionConstraint other) => + other.allows(this) ? VersionConstraint.empty : this; + + @override + int compareTo(VersionRange other) { + if (other is Version) { + if (major != other.major) return major.compareTo(other.major); + if (minor != other.minor) return minor.compareTo(other.minor); + if (patch != other.patch) return patch.compareTo(other.patch); + + // Pre-releases always come before no pre-release string. + if (!isPreRelease && other.isPreRelease) return 1; + if (!other.isPreRelease && isPreRelease) return -1; + + var comparison = _compareLists(preRelease, other.preRelease); + if (comparison != 0) return comparison; + + // Builds always come after no build string. + if (build.isEmpty && other.build.isNotEmpty) return -1; + if (other.build.isEmpty && build.isNotEmpty) return 1; + return _compareLists(build, other.build); + } else { + return -other.compareTo(this); + } + } + + /// Get non-canonical string representation of this [Version]. + /// + /// If created with [Version.parse], the string from which the version was + /// parsed is returned. Unlike the [canonicalizedVersion] this preserves + /// artifacts such as leading zeros. + @override + String toString() => _text; + + /// Get a canonicalized string representation of this [Version]. + /// + /// Unlike [Version.toString()] this always returns a canonical string + /// representation of this [Version]. + /// + /// **Example** + /// ```dart + /// final v = Version.parse('01.02.03-01.dev+pre.02'); + /// + /// assert(v.toString() == '01.02.03-01.dev+pre.02'); + /// assert(v.canonicalizedVersion == '1.2.3-1.dev+pre.2'); + /// assert(Version.parse(v.canonicalizedVersion) == v); + /// ``` + String get canonicalizedVersion => Version( + major, + minor, + patch, + pre: preRelease.isNotEmpty ? preRelease.join('.') : null, + build: build.isNotEmpty ? build.join('.') : null, + ).toString(); + + /// Compares a dot-separated component of two versions. + /// + /// This is used for the pre-release and build version parts. This follows + /// Rule 12 of the Semantic Versioning spec (v2.0.0-rc.1). + int _compareLists(List a, List b) { + for (var i = 0; i < math.max(a.length, b.length); i++) { + var aPart = (i < a.length) ? a[i] : null; + var bPart = (i < b.length) ? b[i] : null; + + if (aPart == bPart) continue; + + // Missing parts come before present ones. + if (aPart == null) return -1; + if (bPart == null) return 1; + + if (aPart is num) { + if (bPart is num) { + // Compare two numbers. + return aPart.compareTo(bPart); + } else { + // Numbers come before strings. + return -1; + } + } else { + if (bPart is num) { + // Strings come after numbers. + return 1; + } else { + // Compare two strings. + return (aPart as String).compareTo(bPart as String); + } + } + } + + // The lists are entirely equal. + return 0; + } +} diff --git a/pkgs/pub_semver/lib/src/version_constraint.dart b/pkgs/pub_semver/lib/src/version_constraint.dart new file mode 100644 index 000000000..948118ef3 --- /dev/null +++ b/pkgs/pub_semver/lib/src/version_constraint.dart @@ -0,0 +1,287 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'patterns.dart'; +import 'utils.dart'; +import 'version.dart'; +import 'version_range.dart'; +import 'version_union.dart'; + +/// A [VersionConstraint] is a predicate that can determine whether a given +/// version is valid or not. +/// +/// For example, a ">= 2.0.0" constraint allows any version that is "2.0.0" or +/// greater. Version objects themselves implement this to match a specific +/// version. +abstract class VersionConstraint { + /// A [VersionConstraint] that allows all versions. + static VersionConstraint any = VersionRange(); + + /// A [VersionConstraint] that allows no versions -- the empty set. + static VersionConstraint empty = const _EmptyVersion(); + + /// Parses a version constraint. + /// + /// This string is one of: + /// + /// * "any". [any] version. + /// * "^" followed by a version string. Versions compatible with + /// ([VersionConstraint.compatibleWith]) the version. + /// * a series of version parts. Each part can be one of: + /// * A version string like `1.2.3`. In other words, anything that can be + /// parsed by [Version.parse()]. + /// * A comparison operator (`<`, `>`, `<=`, or `>=`) followed by a + /// version string. + /// + /// Whitespace is ignored. + /// + /// Examples: + /// + /// any + /// ^0.7.2 + /// ^1.0.0-alpha + /// 1.2.3-alpha + /// <=5.1.4 + /// >2.0.4 <= 2.4.6 + factory VersionConstraint.parse(String text) { + var originalText = text; + + void skipWhitespace() { + text = text.trim(); + } + + skipWhitespace(); + + // Handle the "any" constraint. + if (text == 'any') return any; + + // Try to parse and consume a version number. + Version? matchVersion() { + var version = startVersion.firstMatch(text); + if (version == null) return null; + + text = text.substring(version.end); + return Version.parse(version[0]!); + } + + // Try to parse and consume a comparison operator followed by a version. + VersionRange? matchComparison() { + var comparison = startComparison.firstMatch(text); + if (comparison == null) return null; + + var op = comparison[0]!; + text = text.substring(comparison.end); + skipWhitespace(); + + var version = matchVersion(); + if (version == null) { + throw FormatException('Expected version number after "$op" in ' + '"$originalText", got "$text".'); + } + + return switch (op) { + '<=' => VersionRange(max: version, includeMax: true), + '<' => VersionRange(max: version, alwaysIncludeMaxPreRelease: true), + '>=' => VersionRange(min: version, includeMin: true), + '>' => VersionRange(min: version), + _ => throw UnsupportedError(op), + }; + } + + // Try to parse the "^" operator followed by a version. + VersionConstraint? matchCompatibleWith() { + if (!text.startsWith(compatibleWithChar)) return null; + + text = text.substring(compatibleWithChar.length); + skipWhitespace(); + + var version = matchVersion(); + if (version == null) { + throw FormatException('Expected version number after ' + '"$compatibleWithChar" in "$originalText", got "$text".'); + } + + if (text.isNotEmpty) { + throw FormatException('Cannot include other constraints with ' + '"$compatibleWithChar" constraint in "$originalText".'); + } + + return VersionConstraint.compatibleWith(version); + } + + var compatibleWith = matchCompatibleWith(); + if (compatibleWith != null) return compatibleWith; + + Version? min; + var includeMin = false; + Version? max; + var includeMax = false; + + for (;;) { + skipWhitespace(); + + if (text.isEmpty) break; + + var newRange = matchVersion() ?? matchComparison(); + if (newRange == null) { + throw FormatException('Could not parse version "$originalText". ' + 'Unknown text at "$text".'); + } + + if (newRange.min != null) { + if (min == null || newRange.min! > min) { + min = newRange.min; + includeMin = newRange.includeMin; + } else if (newRange.min == min && !newRange.includeMin) { + includeMin = false; + } + } + + if (newRange.max != null) { + if (max == null || newRange.max! < max) { + max = newRange.max; + includeMax = newRange.includeMax; + } else if (newRange.max == max && !newRange.includeMax) { + includeMax = false; + } + } + } + + if (min == null && max == null) { + throw const FormatException('Cannot parse an empty string.'); + } + + if (min != null && max != null) { + if (min > max) return VersionConstraint.empty; + if (min == max) { + if (includeMin && includeMax) return min; + return VersionConstraint.empty; + } + } + + return VersionRange( + min: min, includeMin: includeMin, max: max, includeMax: includeMax); + } + + /// Creates a version constraint which allows all versions that are + /// backward compatible with [version]. + /// + /// Versions are considered backward compatible with [version] if they + /// are greater than or equal to [version], but less than the next breaking + /// version ([Version.nextBreaking]) of [version]. + factory VersionConstraint.compatibleWith(Version version) => + CompatibleWithVersionRange(version); + + /// Creates a new version constraint that is the intersection of + /// [constraints]. + /// + /// It only allows versions that all of those constraints allow. If + /// constraints is empty, then it returns a VersionConstraint that allows + /// all versions. + factory VersionConstraint.intersection( + Iterable constraints) { + var constraint = VersionRange(); + for (var other in constraints) { + constraint = constraint.intersect(other) as VersionRange; + } + return constraint; + } + + /// Creates a new version constraint that is the union of [constraints]. + /// + /// It allows any versions that any of those constraints allows. If + /// [constraints] is empty, this returns a constraint that allows no versions. + factory VersionConstraint.unionOf(Iterable constraints) { + var flattened = constraints.expand((constraint) { + if (constraint.isEmpty) return []; + if (constraint is VersionUnion) return constraint.ranges; + if (constraint is VersionRange) return [constraint]; + throw ArgumentError('Unknown VersionConstraint type $constraint.'); + }).toList(); + + if (flattened.isEmpty) return VersionConstraint.empty; + + if (flattened.any((constraint) => constraint.isAny)) { + return VersionConstraint.any; + } + + flattened.sort(); + + var merged = []; + for (var constraint in flattened) { + // Merge this constraint with the previous one, but only if they touch. + if (merged.isEmpty || + (!merged.last.allowsAny(constraint) && + !areAdjacent(merged.last, constraint))) { + merged.add(constraint); + } else { + merged[merged.length - 1] = + merged.last.union(constraint) as VersionRange; + } + } + + if (merged.length == 1) return merged.single; + return VersionUnion.fromRanges(merged); + } + + /// Returns `true` if this constraint allows no versions. + bool get isEmpty; + + /// Returns `true` if this constraint allows all versions. + bool get isAny; + + /// Returns `true` if this constraint allows [version]. + bool allows(Version version); + + /// Returns `true` if this constraint allows all the versions that [other] + /// allows. + bool allowsAll(VersionConstraint other); + + /// Returns `true` if this constraint allows any of the versions that [other] + /// allows. + bool allowsAny(VersionConstraint other); + + /// Returns a [VersionConstraint] that only allows [Version]s allowed by both + /// this and [other]. + VersionConstraint intersect(VersionConstraint other); + + /// Returns a [VersionConstraint] that allows [Version]s allowed by either + /// this or [other]. + VersionConstraint union(VersionConstraint other); + + /// Returns a [VersionConstraint] that allows [Version]s allowed by this but + /// not [other]. + VersionConstraint difference(VersionConstraint other); +} + +class _EmptyVersion implements VersionConstraint { + const _EmptyVersion(); + + @override + bool get isEmpty => true; + + @override + bool get isAny => false; + + @override + bool allows(Version other) => false; + + @override + bool allowsAll(VersionConstraint other) => other.isEmpty; + + @override + bool allowsAny(VersionConstraint other) => false; + + @override + VersionConstraint intersect(VersionConstraint other) => this; + + @override + VersionConstraint union(VersionConstraint other) => other; + + @override + VersionConstraint difference(VersionConstraint other) => this; + + @override + String toString() => ''; +} diff --git a/pkgs/pub_semver/lib/src/version_range.dart b/pkgs/pub_semver/lib/src/version_range.dart new file mode 100644 index 000000000..6f2ed54b0 --- /dev/null +++ b/pkgs/pub_semver/lib/src/version_range.dart @@ -0,0 +1,476 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'utils.dart'; +import 'version.dart'; +import 'version_constraint.dart'; +import 'version_union.dart'; + +/// Constrains versions to a fall within a given range. +/// +/// If there is a minimum, then this only allows versions that are at that +/// minimum or greater. If there is a maximum, then only versions less than +/// that are allowed. In other words, this allows `>= min, < max`. +/// +/// Version ranges are ordered first by their lower bounds, then by their upper +/// bounds. For example, `>=1.0.0 <2.0.0` is before `>=1.5.0 <2.0.0` is before +/// `>=1.5.0 <3.0.0`. +class VersionRange implements Comparable, VersionConstraint { + /// The minimum end of the range. + /// + /// If [includeMin] is `true`, this will be the minimum allowed version. + /// Otherwise, it will be the highest version below the range that is not + /// allowed. + /// + /// This may be `null` in which case the range has no minimum end and allows + /// any version less than the maximum. + final Version? min; + + /// The maximum end of the range. + /// + /// If [includeMax] is `true`, this will be the maximum allowed version. + /// Otherwise, it will be the lowest version above the range that is not + /// allowed. + /// + /// This may be `null` in which case the range has no maximum end and allows + /// any version greater than the minimum. + final Version? max; + + /// If `true` then [min] is allowed by the range. + final bool includeMin; + + /// If `true`, then [max] is allowed by the range. + final bool includeMax; + + /// Creates a new version range from [min] to [max], either inclusive or + /// exclusive. + /// + /// If it is an error if [min] is greater than [max]. + /// + /// Either [max] or [min] may be omitted to not clamp the range at that end. + /// If both are omitted, the range allows all versions. + /// + /// If [includeMin] is `true`, then the minimum end of the range is inclusive. + /// Likewise, passing [includeMax] as `true` makes the upper end inclusive. + /// + /// If [alwaysIncludeMaxPreRelease] is `true`, this will always include + /// pre-release versions of an exclusive [max]. Otherwise, it will use the + /// default behavior for pre-release versions of [max]. + factory VersionRange( + {Version? min, + Version? max, + bool includeMin = false, + bool includeMax = false, + bool alwaysIncludeMaxPreRelease = false}) { + if (min != null && max != null && min > max) { + throw ArgumentError( + 'Minimum version ("$min") must be less than maximum ("$max").'); + } + + if (!alwaysIncludeMaxPreRelease && + !includeMax && + max != null && + !max.isPreRelease && + max.build.isEmpty && + (min == null || + !min.isPreRelease || + !equalsWithoutPreRelease(min, max))) { + max = max.firstPreRelease; + } + + return VersionRange._(min, max, includeMin, includeMax); + } + + VersionRange._(this.min, this.max, this.includeMin, this.includeMax); + + @override + bool operator ==(Object other) { + if (other is! VersionRange) return false; + + return min == other.min && + max == other.max && + includeMin == other.includeMin && + includeMax == other.includeMax; + } + + @override + int get hashCode => + min.hashCode ^ + (max.hashCode * 3) ^ + (includeMin.hashCode * 5) ^ + (includeMax.hashCode * 7); + + @override + bool get isEmpty => false; + + @override + bool get isAny => min == null && max == null; + + /// Tests if [other] falls within this version range. + @override + bool allows(Version other) { + if (min != null) { + if (other < min!) return false; + if (!includeMin && other == min) return false; + } + + if (max != null) { + if (other > max!) return false; + if (!includeMax && other == max) return false; + } + + return true; + } + + @override + bool allowsAll(VersionConstraint other) { + if (other.isEmpty) return true; + if (other is Version) return allows(other); + + if (other is VersionUnion) { + return other.ranges.every(allowsAll); + } + + if (other is VersionRange) { + return !allowsLower(other, this) && !allowsHigher(other, this); + } + + throw ArgumentError('Unknown VersionConstraint type $other.'); + } + + @override + bool allowsAny(VersionConstraint other) { + if (other.isEmpty) return false; + if (other is Version) return allows(other); + + if (other is VersionUnion) { + return other.ranges.any(allowsAny); + } + + if (other is VersionRange) { + return !strictlyLower(other, this) && !strictlyHigher(other, this); + } + + throw ArgumentError('Unknown VersionConstraint type $other.'); + } + + @override + VersionConstraint intersect(VersionConstraint other) { + if (other.isEmpty) return other; + if (other is VersionUnion) return other.intersect(this); + + // A range and a Version just yields the version if it's in the range. + if (other is Version) { + return allows(other) ? other : VersionConstraint.empty; + } + + if (other is VersionRange) { + // Intersect the two ranges. + Version? intersectMin; + bool intersectIncludeMin; + if (allowsLower(this, other)) { + if (strictlyLower(this, other)) return VersionConstraint.empty; + intersectMin = other.min; + intersectIncludeMin = other.includeMin; + } else { + if (strictlyLower(other, this)) return VersionConstraint.empty; + intersectMin = min; + intersectIncludeMin = includeMin; + } + + Version? intersectMax; + bool intersectIncludeMax; + if (allowsHigher(this, other)) { + intersectMax = other.max; + intersectIncludeMax = other.includeMax; + } else { + intersectMax = max; + intersectIncludeMax = includeMax; + } + + if (intersectMin == null && intersectMax == null) { + // Open range. + return VersionRange(); + } + + // If the range is just a single version. + if (intersectMin == intersectMax) { + // Because we already verified that the lower range isn't strictly + // lower, there must be some overlap. + assert(intersectIncludeMin && intersectIncludeMax); + return intersectMin!; + } + + // If we got here, there is an actual range. + return VersionRange( + min: intersectMin, + max: intersectMax, + includeMin: intersectIncludeMin, + includeMax: intersectIncludeMax, + alwaysIncludeMaxPreRelease: true); + } + + throw ArgumentError('Unknown VersionConstraint type $other.'); + } + + @override + VersionConstraint union(VersionConstraint other) { + if (other is Version) { + if (allows(other)) return this; + + if (other == min) { + return VersionRange( + min: min, + max: max, + includeMin: true, + includeMax: includeMax, + alwaysIncludeMaxPreRelease: true); + } + + if (other == max) { + return VersionRange( + min: min, + max: max, + includeMin: includeMin, + includeMax: true, + alwaysIncludeMaxPreRelease: true); + } + + return VersionConstraint.unionOf([this, other]); + } + + if (other is VersionRange) { + // If the two ranges don't overlap, we won't be able to create a single + // VersionRange for both of them. + var edgesTouch = (max != null && + max == other.min && + (includeMax || other.includeMin)) || + (min != null && min == other.max && (includeMin || other.includeMax)); + if (!edgesTouch && !allowsAny(other)) { + return VersionConstraint.unionOf([this, other]); + } + + Version? unionMin; + bool unionIncludeMin; + if (allowsLower(this, other)) { + unionMin = min; + unionIncludeMin = includeMin; + } else { + unionMin = other.min; + unionIncludeMin = other.includeMin; + } + + Version? unionMax; + bool unionIncludeMax; + if (allowsHigher(this, other)) { + unionMax = max; + unionIncludeMax = includeMax; + } else { + unionMax = other.max; + unionIncludeMax = other.includeMax; + } + + return VersionRange( + min: unionMin, + max: unionMax, + includeMin: unionIncludeMin, + includeMax: unionIncludeMax, + alwaysIncludeMaxPreRelease: true); + } + + return VersionConstraint.unionOf([this, other]); + } + + @override + VersionConstraint difference(VersionConstraint other) { + if (other.isEmpty) return this; + + if (other is Version) { + if (!allows(other)) return this; + + if (other == min) { + if (!includeMin) return this; + return VersionRange( + min: min, + max: max, + includeMax: includeMax, + alwaysIncludeMaxPreRelease: true); + } + + if (other == max) { + if (!includeMax) return this; + return VersionRange( + min: min, + max: max, + includeMin: includeMin, + alwaysIncludeMaxPreRelease: true); + } + + return VersionUnion.fromRanges([ + VersionRange( + min: min, + max: other, + includeMin: includeMin, + alwaysIncludeMaxPreRelease: true), + VersionRange( + min: other, + max: max, + includeMax: includeMax, + alwaysIncludeMaxPreRelease: true) + ]); + } else if (other is VersionRange) { + if (!allowsAny(other)) return this; + + VersionRange? before; + if (!allowsLower(this, other)) { + before = null; + } else if (min == other.min) { + assert(includeMin && !other.includeMin); + assert(min != null); + before = min; + } else { + before = VersionRange( + min: min, + max: other.min, + includeMin: includeMin, + includeMax: !other.includeMin, + alwaysIncludeMaxPreRelease: true); + } + + VersionRange? after; + if (!allowsHigher(this, other)) { + after = null; + } else if (max == other.max) { + assert(includeMax && !other.includeMax); + assert(max != null); + after = max; + } else { + after = VersionRange( + min: other.max, + max: max, + includeMin: !other.includeMax, + includeMax: includeMax, + alwaysIncludeMaxPreRelease: true); + } + + if (before == null && after == null) return VersionConstraint.empty; + if (before == null) return after!; + if (after == null) return before; + return VersionUnion.fromRanges([before, after]); + } else if (other is VersionUnion) { + var ranges = []; + var current = this; + + for (var range in other.ranges) { + // Skip any ranges that are strictly lower than [current]. + if (strictlyLower(range, current)) continue; + + // If we reach a range strictly higher than [current], no more ranges + // will be relevant so we can bail early. + if (strictlyHigher(range, current)) break; + + var difference = current.difference(range); + if (difference.isEmpty) { + return VersionConstraint.empty; + } else if (difference is VersionUnion) { + // If [range] split [current] in half, we only need to continue + // checking future ranges against the latter half. + assert(difference.ranges.length == 2); + ranges.add(difference.ranges.first); + current = difference.ranges.last; + } else { + current = difference as VersionRange; + } + } + + if (ranges.isEmpty) return current; + return VersionUnion.fromRanges(ranges..add(current)); + } + + throw ArgumentError('Unknown VersionConstraint type $other.'); + } + + @override + int compareTo(VersionRange other) { + if (min == null) { + if (other.min == null) return _compareMax(other); + return -1; + } else if (other.min == null) { + return 1; + } + + var result = min!.compareTo(other.min!); + if (result != 0) return result; + if (includeMin != other.includeMin) return includeMin ? -1 : 1; + + return _compareMax(other); + } + + /// Compares the maximum values of `this` and [other]. + int _compareMax(VersionRange other) { + if (max == null) { + if (other.max == null) return 0; + return 1; + } else if (other.max == null) { + return -1; + } + + var result = max!.compareTo(other.max!); + if (result != 0) return result; + if (includeMax != other.includeMax) return includeMax ? 1 : -1; + return 0; + } + + @override + String toString() { + var buffer = StringBuffer(); + + final min = this.min; + if (min != null) { + buffer + ..write(includeMin ? '>=' : '>') + ..write(min); + } + + final max = this.max; + + if (max != null) { + if (min != null) buffer.write(' '); + if (includeMax) { + buffer + ..write('<=') + ..write(max); + } else { + buffer.write('<'); + if (max.isFirstPreRelease) { + // Since `"<$max"` would parse the same as `"<$max-0"`, we just emit + // `<$max` to avoid confusing "-0" suffixes. + buffer.write('${max.major}.${max.minor}.${max.patch}'); + } else { + buffer.write(max); + + // If `">=$min <$max"` would parse as `">=$min <$max-0"`, add `-*` to + // indicate that actually does allow pre-release versions. + var minIsPreReleaseOfMax = min != null && + min.isPreRelease && + equalsWithoutPreRelease(min, max); + if (!max.isPreRelease && max.build.isEmpty && !minIsPreReleaseOfMax) { + buffer.write('-∞'); + } + } + } + } + + if (min == null && max == null) buffer.write('any'); + return buffer.toString(); + } +} + +class CompatibleWithVersionRange extends VersionRange { + CompatibleWithVersionRange(Version version) + : super._(version, version.nextBreaking.firstPreRelease, true, false); + + @override + String toString() => '^$min'; +} diff --git a/pkgs/pub_semver/lib/src/version_union.dart b/pkgs/pub_semver/lib/src/version_union.dart new file mode 100644 index 000000000..844d3b8ef --- /dev/null +++ b/pkgs/pub_semver/lib/src/version_union.dart @@ -0,0 +1,224 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:collection/collection.dart'; + +import 'utils.dart'; +import 'version.dart'; +import 'version_constraint.dart'; +import 'version_range.dart'; + +/// A version constraint representing a union of multiple disjoint version +/// ranges. +/// +/// An instance of this will only be created if the version can't be represented +/// as a non-compound value. +class VersionUnion implements VersionConstraint { + /// The constraints that compose this union. + /// + /// This list has two invariants: + /// + /// * Its contents are sorted using the standard ordering of [VersionRange]s. + /// * Its contents are disjoint and non-adjacent. In other words, for any two + /// constraints next to each other in the list, there's some version between + /// those constraints that they don't match. + final List ranges; + + @override + bool get isEmpty => false; + + @override + bool get isAny => false; + + /// Creates a union from a list of ranges with no pre-processing. + /// + /// It's up to the caller to ensure that the invariants described in [ranges] + /// are maintained. They are not verified by this constructor. To + /// automatically ensure that they're maintained, use + /// [VersionConstraint.unionOf] instead. + VersionUnion.fromRanges(this.ranges); + + @override + bool allows(Version version) => + ranges.any((constraint) => constraint.allows(version)); + + @override + bool allowsAll(VersionConstraint other) { + var ourRanges = ranges.iterator; + var theirRanges = _rangesFor(other).iterator; + + // Because both lists of ranges are ordered by minimum version, we can + // safely move through them linearly here. + var ourRangesMoved = ourRanges.moveNext(); + var theirRangesMoved = theirRanges.moveNext(); + while (ourRangesMoved && theirRangesMoved) { + if (ourRanges.current.allowsAll(theirRanges.current)) { + theirRangesMoved = theirRanges.moveNext(); + } else { + ourRangesMoved = ourRanges.moveNext(); + } + } + + // If our ranges have allowed all of their ranges, we'll have consumed all + // of them. + return !theirRangesMoved; + } + + @override + bool allowsAny(VersionConstraint other) { + var ourRanges = ranges.iterator; + var theirRanges = _rangesFor(other).iterator; + + // Because both lists of ranges are ordered by minimum version, we can + // safely move through them linearly here. + var ourRangesMoved = ourRanges.moveNext(); + var theirRangesMoved = theirRanges.moveNext(); + while (ourRangesMoved && theirRangesMoved) { + if (ourRanges.current.allowsAny(theirRanges.current)) { + return true; + } + + // Move the constraint with the lower max value forward. This ensures that + // we keep both lists in sync as much as possible. + if (allowsHigher(theirRanges.current, ourRanges.current)) { + ourRangesMoved = ourRanges.moveNext(); + } else { + theirRangesMoved = theirRanges.moveNext(); + } + } + + return false; + } + + @override + VersionConstraint intersect(VersionConstraint other) { + var ourRanges = ranges.iterator; + var theirRanges = _rangesFor(other).iterator; + + // Because both lists of ranges are ordered by minimum version, we can + // safely move through them linearly here. + var newRanges = []; + var ourRangesMoved = ourRanges.moveNext(); + var theirRangesMoved = theirRanges.moveNext(); + while (ourRangesMoved && theirRangesMoved) { + var intersection = ourRanges.current.intersect(theirRanges.current); + + if (!intersection.isEmpty) newRanges.add(intersection as VersionRange); + + // Move the constraint with the lower max value forward. This ensures that + // we keep both lists in sync as much as possible, and that large ranges + // have a chance to match multiple small ranges that they contain. + if (allowsHigher(theirRanges.current, ourRanges.current)) { + ourRangesMoved = ourRanges.moveNext(); + } else { + theirRangesMoved = theirRanges.moveNext(); + } + } + + if (newRanges.isEmpty) return VersionConstraint.empty; + if (newRanges.length == 1) return newRanges.single; + + return VersionUnion.fromRanges(newRanges); + } + + @override + VersionConstraint difference(VersionConstraint other) { + var ourRanges = ranges.iterator; + var theirRanges = _rangesFor(other).iterator; + + var newRanges = []; + ourRanges.moveNext(); + theirRanges.moveNext(); + var current = ourRanges.current; + + bool theirNextRange() { + if (theirRanges.moveNext()) return true; + + // If there are no more of their ranges, none of the rest of our ranges + // need to be subtracted so we can add them as-is. + newRanges.add(current); + while (ourRanges.moveNext()) { + newRanges.add(ourRanges.current); + } + return false; + } + + bool ourNextRange({bool includeCurrent = true}) { + if (includeCurrent) newRanges.add(current); + if (!ourRanges.moveNext()) return false; + current = ourRanges.current; + return true; + } + + for (;;) { + // If the current ranges are disjoint, move the lowest one forward. + if (strictlyLower(theirRanges.current, current)) { + if (!theirNextRange()) break; + continue; + } + + if (strictlyHigher(theirRanges.current, current)) { + if (!ourNextRange()) break; + continue; + } + + // If we're here, we know [theirRanges.current] overlaps [current]. + var difference = current.difference(theirRanges.current); + if (difference is VersionUnion) { + // If their range split [current] in half, we only need to continue + // checking future ranges against the latter half. + assert(difference.ranges.length == 2); + newRanges.add(difference.ranges.first); + current = difference.ranges.last; + + // Since their range split [current], it definitely doesn't allow higher + // versions, so we should move their ranges forward. + if (!theirNextRange()) break; + } else if (difference.isEmpty) { + if (!ourNextRange(includeCurrent: false)) break; + } else { + current = difference as VersionRange; + + // Move the constraint with the lower max value forward. This ensures + // that we keep both lists in sync as much as possible, and that large + // ranges have a chance to subtract or be subtracted by multiple small + // ranges that they contain. + if (allowsHigher(current, theirRanges.current)) { + if (!theirNextRange()) break; + } else { + if (!ourNextRange()) break; + } + } + } + + if (newRanges.isEmpty) return VersionConstraint.empty; + if (newRanges.length == 1) return newRanges.single; + return VersionUnion.fromRanges(newRanges); + } + + /// Returns [constraint] as a list of ranges. + /// + /// This is used to normalize ranges of various types. + List _rangesFor(VersionConstraint constraint) { + if (constraint.isEmpty) return []; + if (constraint is VersionUnion) return constraint.ranges; + if (constraint is VersionRange) return [constraint]; + throw ArgumentError('Unknown VersionConstraint type $constraint.'); + } + + @override + VersionConstraint union(VersionConstraint other) => + VersionConstraint.unionOf([this, other]); + + @override + bool operator ==(Object other) => + other is VersionUnion && + const ListEquality().equals(ranges, other.ranges); + + @override + int get hashCode => const ListEquality().hash(ranges); + + @override + String toString() => ranges.join(' or '); +} diff --git a/pkgs/pub_semver/pubspec.yaml b/pkgs/pub_semver/pubspec.yaml new file mode 100644 index 000000000..290fb9254 --- /dev/null +++ b/pkgs/pub_semver/pubspec.yaml @@ -0,0 +1,20 @@ +name: pub_semver +version: 2.1.5 +description: >- + Versions and version constraints implementing pub's versioning policy. This + is very similar to vanilla semver, with a few corner cases. +repository: https://github.com/dart-lang/tools/tree/main/pkgs/pub_semver +topics: + - dart-pub + - semver + +environment: + sdk: ^3.4.0 + +dependencies: + collection: ^1.15.0 + meta: ^1.3.0 + +dev_dependencies: + dart_flutter_team_lints: ^3.0.0 + test: ^1.16.0 diff --git a/pkgs/pub_semver/test/utils.dart b/pkgs/pub_semver/test/utils.dart new file mode 100644 index 000000000..bd7aa8f8f --- /dev/null +++ b/pkgs/pub_semver/test/utils.dart @@ -0,0 +1,123 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +/// Some stock example versions to use in tests. +final v003 = Version.parse('0.0.3'); +final v010 = Version.parse('0.1.0'); +final v072 = Version.parse('0.7.2'); +final v080 = Version.parse('0.8.0'); +final v114 = Version.parse('1.1.4'); +final v123 = Version.parse('1.2.3'); +final v124 = Version.parse('1.2.4'); +final v130 = Version.parse('1.3.0'); +final v140 = Version.parse('1.4.0'); +final v200 = Version.parse('2.0.0'); +final v201 = Version.parse('2.0.1'); +final v234 = Version.parse('2.3.4'); +final v250 = Version.parse('2.5.0'); +final v300 = Version.parse('3.0.0'); + +/// A range that allows pre-release versions of its max version. +final includeMaxPreReleaseRange = + VersionRange(max: v200, alwaysIncludeMaxPreRelease: true); + +/// A [Matcher] that tests if a [VersionConstraint] allows or does not allow a +/// given list of [Version]s. +class _VersionConstraintMatcher implements Matcher { + final List _expected; + final bool _allow; + + _VersionConstraintMatcher(this._expected, this._allow); + + @override + bool matches(dynamic item, Map matchState) => + (item is VersionConstraint) && + _expected.every((version) => item.allows(version) == _allow); + + @override + Description describe(Description description) { + description.addAll(' ${_allow ? "allows" : "does not allow"} versions ', + ', ', '', _expected); + return description; + } + + @override + Description describeMismatch(dynamic item, Description mismatchDescription, + Map matchState, bool verbose) { + if (item is! VersionConstraint) { + mismatchDescription.add('was not a VersionConstraint'); + return mismatchDescription; + } + + var first = true; + for (var version in _expected) { + if (item.allows(version) != _allow) { + if (first) { + if (_allow) { + mismatchDescription.addDescriptionOf(item).add(' did not allow '); + } else { + mismatchDescription.addDescriptionOf(item).add(' allowed '); + } + } else { + mismatchDescription.add(' and '); + } + first = false; + + mismatchDescription.add(version.toString()); + } + } + + return mismatchDescription; + } +} + +/// Gets a [Matcher] that validates that a [VersionConstraint] allows all +/// given versions. +Matcher allows(Version v1, + [Version? v2, + Version? v3, + Version? v4, + Version? v5, + Version? v6, + Version? v7, + Version? v8]) { + var versions = _makeVersionList(v1, v2, v3, v4, v5, v6, v7, v8); + return _VersionConstraintMatcher(versions, true); +} + +/// Gets a [Matcher] that validates that a [VersionConstraint] allows none of +/// the given versions. +Matcher doesNotAllow(Version v1, + [Version? v2, + Version? v3, + Version? v4, + Version? v5, + Version? v6, + Version? v7, + Version? v8]) { + var versions = _makeVersionList(v1, v2, v3, v4, v5, v6, v7, v8); + return _VersionConstraintMatcher(versions, false); +} + +List _makeVersionList(Version v1, + [Version? v2, + Version? v3, + Version? v4, + Version? v5, + Version? v6, + Version? v7, + Version? v8]) { + var versions = [v1]; + if (v2 != null) versions.add(v2); + if (v3 != null) versions.add(v3); + if (v4 != null) versions.add(v4); + if (v5 != null) versions.add(v5); + if (v6 != null) versions.add(v6); + if (v7 != null) versions.add(v7); + if (v8 != null) versions.add(v8); + return versions; +} diff --git a/pkgs/pub_semver/test/version_constraint_test.dart b/pkgs/pub_semver/test/version_constraint_test.dart new file mode 100644 index 000000000..4fbcbe0cb --- /dev/null +++ b/pkgs/pub_semver/test/version_constraint_test.dart @@ -0,0 +1,185 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + test('any', () { + expect(VersionConstraint.any.isAny, isTrue); + expect( + VersionConstraint.any, + allows(Version.parse('0.0.0-blah'), Version.parse('1.2.3'), + Version.parse('12345.678.90'))); + }); + + test('empty', () { + expect(VersionConstraint.empty.isEmpty, isTrue); + expect(VersionConstraint.empty.isAny, isFalse); + expect( + VersionConstraint.empty, + doesNotAllow(Version.parse('0.0.0-blah'), Version.parse('1.2.3'), + Version.parse('12345.678.90'))); + }); + + group('parse()', () { + test('parses an exact version', () { + var constraint = VersionConstraint.parse('1.2.3-alpha'); + + expect(constraint is Version, isTrue); + expect(constraint, equals(Version(1, 2, 3, pre: 'alpha'))); + }); + + test('parses "any"', () { + var constraint = VersionConstraint.parse('any'); + + expect( + constraint, + allows(Version.parse('0.0.0'), Version.parse('1.2.3'), + Version.parse('12345.678.90'))); + }); + + test('parses a ">" minimum version', () { + var constraint = VersionConstraint.parse('>1.2.3'); + + expect(constraint, + allows(Version.parse('1.2.3+foo'), Version.parse('1.2.4'))); + expect( + constraint, + doesNotAllow(Version.parse('1.2.1'), Version.parse('1.2.3-build'), + Version.parse('1.2.3'))); + }); + + test('parses a ">=" minimum version', () { + var constraint = VersionConstraint.parse('>=1.2.3'); + + expect( + constraint, + allows(Version.parse('1.2.3'), Version.parse('1.2.3+foo'), + Version.parse('1.2.4'))); + expect(constraint, + doesNotAllow(Version.parse('1.2.1'), Version.parse('1.2.3-build'))); + }); + + test('parses a "<" maximum version', () { + var constraint = VersionConstraint.parse('<1.2.3'); + + expect(constraint, + allows(Version.parse('1.2.1'), Version.parse('1.2.2+foo'))); + expect( + constraint, + doesNotAllow(Version.parse('1.2.3'), Version.parse('1.2.3+foo'), + Version.parse('1.2.4'))); + }); + + test('parses a "<=" maximum version', () { + var constraint = VersionConstraint.parse('<=1.2.3'); + + expect( + constraint, + allows(Version.parse('1.2.1'), Version.parse('1.2.3-build'), + Version.parse('1.2.3'))); + expect(constraint, + doesNotAllow(Version.parse('1.2.3+foo'), Version.parse('1.2.4'))); + }); + + test('parses a series of space-separated constraints', () { + var constraint = VersionConstraint.parse('>1.0.0 >=1.2.3 <1.3.0'); + + expect( + constraint, allows(Version.parse('1.2.3'), Version.parse('1.2.5'))); + expect( + constraint, + doesNotAllow(Version.parse('1.2.3-pre'), Version.parse('1.3.0'), + Version.parse('3.4.5'))); + }); + + test('parses a pre-release-only constraint', () { + var constraint = VersionConstraint.parse('>=1.0.0-dev.2 <1.0.0'); + expect(constraint, + allows(Version.parse('1.0.0-dev.2'), Version.parse('1.0.0-dev.3'))); + expect(constraint, + doesNotAllow(Version.parse('1.0.0-dev.1'), Version.parse('1.0.0'))); + }); + + test('ignores whitespace around comparison operators', () { + var constraint = VersionConstraint.parse(' >1.0.0>=1.2.3 < 1.3.0'); + + expect( + constraint, allows(Version.parse('1.2.3'), Version.parse('1.2.5'))); + expect( + constraint, + doesNotAllow(Version.parse('1.2.3-pre'), Version.parse('1.3.0'), + Version.parse('3.4.5'))); + }); + + test('does not allow "any" to be mixed with other constraints', () { + expect(() => VersionConstraint.parse('any 1.0.0'), throwsFormatException); + }); + + test('parses a "^" version', () { + expect(VersionConstraint.parse('^0.0.3'), + equals(VersionConstraint.compatibleWith(v003))); + + expect(VersionConstraint.parse('^0.7.2'), + equals(VersionConstraint.compatibleWith(v072))); + + expect(VersionConstraint.parse('^1.2.3'), + equals(VersionConstraint.compatibleWith(v123))); + + var min = Version.parse('0.7.2-pre+1'); + expect(VersionConstraint.parse('^0.7.2-pre+1'), + equals(VersionConstraint.compatibleWith(min))); + }); + + test('does not allow "^" to be mixed with other constraints', () { + expect(() => VersionConstraint.parse('>=1.2.3 ^1.0.0'), + throwsFormatException); + expect(() => VersionConstraint.parse('^1.0.0 <1.2.3'), + throwsFormatException); + }); + + test('ignores whitespace around "^"', () { + var constraint = VersionConstraint.parse(' ^ 1.2.3 '); + + expect(constraint, equals(VersionConstraint.compatibleWith(v123))); + }); + + test('throws FormatException on a bad string', () { + var bad = [ + '', ' ', // Empty string. + 'foo', // Bad text. + '>foo', // Bad text after operator. + '^foo', // Bad text after "^". + '1.0.0 foo', '1.0.0foo', // Bad text after version. + 'anything', // Bad text after "any". + '<>1.0.0', // Multiple operators. + '1.0.0<' // Trailing operator. + ]; + + for (var text in bad) { + expect(() => VersionConstraint.parse(text), throwsFormatException); + } + }); + }); + + group('compatibleWith()', () { + test('returns the range of compatible versions', () { + var constraint = VersionConstraint.compatibleWith(v072); + + expect( + constraint, + equals(VersionRange( + min: v072, includeMin: true, max: v072.nextBreaking))); + }); + + test('toString() uses "^"', () { + var constraint = VersionConstraint.compatibleWith(v072); + + expect(constraint.toString(), equals('^0.7.2')); + }); + }); +} diff --git a/pkgs/pub_semver/test/version_range_test.dart b/pkgs/pub_semver/test/version_range_test.dart new file mode 100644 index 000000000..5978df0c7 --- /dev/null +++ b/pkgs/pub_semver/test/version_range_test.dart @@ -0,0 +1,998 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + group('constructor', () { + test('takes a min and max', () { + var range = VersionRange(min: v123, max: v124); + expect(range.isAny, isFalse); + expect(range.min, equals(v123)); + expect(range.max, equals(v124.firstPreRelease)); + }); + + group("doesn't make the max a pre-release if", () { + test("it's already a pre-release", () { + expect(VersionRange(max: Version.parse('1.2.4-pre')).max, + equals(Version.parse('1.2.4-pre'))); + }); + + test('includeMax is true', () { + expect(VersionRange(max: v124, includeMax: true).max, equals(v124)); + }); + + test('min is a prerelease of max', () { + expect(VersionRange(min: Version.parse('1.2.4-pre'), max: v124).max, + equals(v124)); + }); + + test('max has a build identifier', () { + expect(VersionRange(max: Version.parse('1.2.4+1')).max, + equals(Version.parse('1.2.4+1'))); + }); + }); + + test('allows omitting max', () { + var range = VersionRange(min: v123); + expect(range.isAny, isFalse); + expect(range.min, equals(v123)); + expect(range.max, isNull); + }); + + test('allows omitting min and max', () { + var range = VersionRange(); + expect(range.isAny, isTrue); + expect(range.min, isNull); + expect(range.max, isNull); + }); + + test('takes includeMin', () { + var range = VersionRange(min: v123, includeMin: true); + expect(range.includeMin, isTrue); + }); + + test('includeMin defaults to false if omitted', () { + var range = VersionRange(min: v123); + expect(range.includeMin, isFalse); + }); + + test('takes includeMax', () { + var range = VersionRange(max: v123, includeMax: true); + expect(range.includeMax, isTrue); + }); + + test('includeMax defaults to false if omitted', () { + var range = VersionRange(max: v123); + expect(range.includeMax, isFalse); + }); + + test('throws if min > max', () { + expect(() => VersionRange(min: v124, max: v123), throwsArgumentError); + }); + }); + + group('allows()', () { + test('version must be greater than min', () { + var range = VersionRange(min: v123); + + expect(range, allows(Version.parse('1.3.3'), Version.parse('2.3.3'))); + expect( + range, doesNotAllow(Version.parse('1.2.2'), Version.parse('1.2.3'))); + }); + + test('version must be min or greater if includeMin', () { + var range = VersionRange(min: v123, includeMin: true); + + expect( + range, + allows(Version.parse('1.2.3'), Version.parse('1.3.3'), + Version.parse('2.3.3'))); + expect(range, doesNotAllow(Version.parse('1.2.2'))); + }); + + test('pre-release versions of inclusive min are excluded', () { + var range = VersionRange(min: v123, includeMin: true); + + expect(range, allows(Version.parse('1.2.4-dev'))); + expect(range, doesNotAllow(Version.parse('1.2.3-dev'))); + }); + + test('version must be less than max', () { + var range = VersionRange(max: v234); + + expect(range, allows(Version.parse('2.3.3'))); + expect( + range, doesNotAllow(Version.parse('2.3.4'), Version.parse('2.4.3'))); + }); + + test('pre-release versions of non-pre-release max are excluded', () { + var range = VersionRange(max: v234); + + expect(range, allows(Version.parse('2.3.3'))); + expect(range, + doesNotAllow(Version.parse('2.3.4-dev'), Version.parse('2.3.4'))); + }); + + test( + 'pre-release versions of non-pre-release max are included if min is a ' + 'pre-release of the same version', () { + var range = VersionRange(min: Version.parse('2.3.4-dev.0'), max: v234); + + expect(range, allows(Version.parse('2.3.4-dev.1'))); + expect( + range, + doesNotAllow(Version.parse('2.3.3'), Version.parse('2.3.4-dev'), + Version.parse('2.3.4'))); + }); + + test('pre-release versions of pre-release max are included', () { + var range = VersionRange(max: Version.parse('2.3.4-dev.2')); + + expect(range, allows(Version.parse('2.3.4-dev.1'))); + expect( + range, + doesNotAllow( + Version.parse('2.3.4-dev.2'), Version.parse('2.3.4-dev.3'))); + }); + + test('version must be max or less if includeMax', () { + var range = VersionRange(min: v123, max: v234, includeMax: true); + + expect( + range, + allows( + Version.parse('2.3.3'), + Version.parse('2.3.4'), + // Pre-releases of the max are allowed. + Version.parse('2.3.4-dev'))); + expect(range, doesNotAllow(Version.parse('2.4.3'))); + }); + + test('has no min if one was not set', () { + var range = VersionRange(max: v123); + + expect(range, allows(Version.parse('0.0.0'))); + expect(range, doesNotAllow(Version.parse('1.2.3'))); + }); + + test('has no max if one was not set', () { + var range = VersionRange(min: v123); + + expect(range, allows(Version.parse('1.3.3'), Version.parse('999.3.3'))); + expect(range, doesNotAllow(Version.parse('1.2.3'))); + }); + + test('allows any version if there is no min or max', () { + var range = VersionRange(); + + expect(range, allows(Version.parse('0.0.0'), Version.parse('999.99.9'))); + }); + + test('allows pre-releases of the max with includeMaxPreRelease', () { + expect(includeMaxPreReleaseRange, allows(Version.parse('2.0.0-dev'))); + }); + }); + + group('allowsAll()', () { + test('allows an empty constraint', () { + expect( + VersionRange(min: v123, max: v250).allowsAll(VersionConstraint.empty), + isTrue); + }); + + test('allows allowed versions', () { + var range = VersionRange(min: v123, max: v250, includeMax: true); + expect(range.allowsAll(v123), isFalse); + expect(range.allowsAll(v124), isTrue); + expect(range.allowsAll(v250), isTrue); + expect(range.allowsAll(v300), isFalse); + }); + + test('with no min', () { + var range = VersionRange(max: v250); + expect(range.allowsAll(VersionRange(min: v080, max: v140)), isTrue); + expect(range.allowsAll(VersionRange(min: v080, max: v300)), isFalse); + expect(range.allowsAll(VersionRange(max: v140)), isTrue); + expect(range.allowsAll(VersionRange(max: v300)), isFalse); + expect(range.allowsAll(range), isTrue); + expect(range.allowsAll(VersionConstraint.any), isFalse); + }); + + test('with no max', () { + var range = VersionRange(min: v010); + expect(range.allowsAll(VersionRange(min: v080, max: v140)), isTrue); + expect(range.allowsAll(VersionRange(min: v003, max: v140)), isFalse); + expect(range.allowsAll(VersionRange(min: v080)), isTrue); + expect(range.allowsAll(VersionRange(min: v003)), isFalse); + expect(range.allowsAll(range), isTrue); + expect(range.allowsAll(VersionConstraint.any), isFalse); + }); + + test('with a min and max', () { + var range = VersionRange(min: v010, max: v250); + expect(range.allowsAll(VersionRange(min: v080, max: v140)), isTrue); + expect(range.allowsAll(VersionRange(min: v080, max: v300)), isFalse); + expect(range.allowsAll(VersionRange(min: v003, max: v140)), isFalse); + expect(range.allowsAll(VersionRange(min: v080)), isFalse); + expect(range.allowsAll(VersionRange(max: v140)), isFalse); + expect(range.allowsAll(range), isTrue); + }); + + test("allows a bordering range that's not more inclusive", () { + var exclusive = VersionRange(min: v010, max: v250); + var inclusive = VersionRange( + min: v010, includeMin: true, max: v250, includeMax: true); + expect(inclusive.allowsAll(exclusive), isTrue); + expect(inclusive.allowsAll(inclusive), isTrue); + expect(exclusive.allowsAll(inclusive), isFalse); + expect(exclusive.allowsAll(exclusive), isTrue); + }); + + test('allows unions that are completely contained', () { + var range = VersionRange(min: v114, max: v200); + expect(range.allowsAll(VersionRange(min: v123, max: v124).union(v140)), + isTrue); + expect(range.allowsAll(VersionRange(min: v010, max: v124).union(v140)), + isFalse); + expect(range.allowsAll(VersionRange(min: v123, max: v234).union(v140)), + isFalse); + }); + + group('pre-release versions', () { + test('of inclusive min are excluded', () { + var range = VersionRange(min: v123, includeMin: true); + + expect(range.allowsAll(VersionConstraint.parse('>1.2.4-dev')), isTrue); + expect(range.allowsAll(VersionConstraint.parse('>1.2.3-dev')), isFalse); + }); + + test('of non-pre-release max are excluded', () { + var range = VersionRange(max: v234); + + expect(range.allowsAll(VersionConstraint.parse('<2.3.3')), isTrue); + expect(range.allowsAll(VersionConstraint.parse('<2.3.4-dev')), isFalse); + }); + + test('of non-pre-release max are included with includeMaxPreRelease', () { + expect( + includeMaxPreReleaseRange + .allowsAll(VersionConstraint.parse('<2.0.0-dev')), + isTrue); + }); + + test( + 'of non-pre-release max are included if min is a pre-release of the ' + 'same version', () { + var range = VersionRange(min: Version.parse('2.3.4-dev.0'), max: v234); + + expect( + range.allowsAll( + VersionConstraint.parse('>2.3.4-dev.0 <2.3.4-dev.1')), + isTrue); + }); + + test('of pre-release max are included', () { + var range = VersionRange(max: Version.parse('2.3.4-dev.2')); + + expect( + range.allowsAll(VersionConstraint.parse('<2.3.4-dev.1')), isTrue); + expect( + range.allowsAll(VersionConstraint.parse('<2.3.4-dev.2')), isTrue); + expect( + range.allowsAll(VersionConstraint.parse('<=2.3.4-dev.2')), isFalse); + expect( + range.allowsAll(VersionConstraint.parse('<2.3.4-dev.3')), isFalse); + }); + }); + }); + + group('allowsAny()', () { + test('disallows an empty constraint', () { + expect( + VersionRange(min: v123, max: v250).allowsAny(VersionConstraint.empty), + isFalse); + }); + + test('allows allowed versions', () { + var range = VersionRange(min: v123, max: v250, includeMax: true); + expect(range.allowsAny(v123), isFalse); + expect(range.allowsAny(v124), isTrue); + expect(range.allowsAny(v250), isTrue); + expect(range.allowsAny(v300), isFalse); + }); + + test('with no min', () { + var range = VersionRange(max: v200); + expect(range.allowsAny(VersionRange(min: v140, max: v300)), isTrue); + expect(range.allowsAny(VersionRange(min: v234, max: v300)), isFalse); + expect(range.allowsAny(VersionRange(min: v140)), isTrue); + expect(range.allowsAny(VersionRange(min: v234)), isFalse); + expect(range.allowsAny(range), isTrue); + }); + + test('with no max', () { + var range = VersionRange(min: v072); + expect(range.allowsAny(VersionRange(min: v003, max: v140)), isTrue); + expect(range.allowsAny(VersionRange(min: v003, max: v010)), isFalse); + expect(range.allowsAny(VersionRange(max: v080)), isTrue); + expect(range.allowsAny(VersionRange(max: v003)), isFalse); + expect(range.allowsAny(range), isTrue); + }); + + test('with a min and max', () { + var range = VersionRange(min: v072, max: v200); + expect(range.allowsAny(VersionRange(min: v003, max: v140)), isTrue); + expect(range.allowsAny(VersionRange(min: v140, max: v300)), isTrue); + expect(range.allowsAny(VersionRange(min: v003, max: v010)), isFalse); + expect(range.allowsAny(VersionRange(min: v234, max: v300)), isFalse); + expect(range.allowsAny(VersionRange(max: v010)), isFalse); + expect(range.allowsAny(VersionRange(min: v234)), isFalse); + expect(range.allowsAny(range), isTrue); + }); + + test('allows a bordering range when both are inclusive', () { + expect( + VersionRange(max: v250).allowsAny(VersionRange(min: v250)), isFalse); + + expect( + VersionRange(max: v250, includeMax: true) + .allowsAny(VersionRange(min: v250)), + isFalse); + + expect( + VersionRange(max: v250) + .allowsAny(VersionRange(min: v250, includeMin: true)), + isFalse); + + expect( + VersionRange(max: v250, includeMax: true) + .allowsAny(VersionRange(min: v250, includeMin: true)), + isTrue); + + expect( + VersionRange(min: v250).allowsAny(VersionRange(max: v250)), isFalse); + + expect( + VersionRange(min: v250, includeMin: true) + .allowsAny(VersionRange(max: v250)), + isFalse); + + expect( + VersionRange(min: v250) + .allowsAny(VersionRange(max: v250, includeMax: true)), + isFalse); + + expect( + VersionRange(min: v250, includeMin: true) + .allowsAny(VersionRange(max: v250, includeMax: true)), + isTrue); + }); + + test('allows unions that are partially contained', () { + var range = VersionRange(min: v114, max: v200); + expect(range.allowsAny(VersionRange(min: v010, max: v080).union(v140)), + isTrue); + expect(range.allowsAny(VersionRange(min: v123, max: v234).union(v300)), + isTrue); + expect(range.allowsAny(VersionRange(min: v234, max: v300).union(v010)), + isFalse); + }); + + group('pre-release versions', () { + test('of inclusive min are excluded', () { + var range = VersionRange(min: v123, includeMin: true); + + expect(range.allowsAny(VersionConstraint.parse('<1.2.4-dev')), isTrue); + expect(range.allowsAny(VersionConstraint.parse('<1.2.3-dev')), isFalse); + }); + + test('of non-pre-release max are excluded', () { + var range = VersionRange(max: v234); + + expect(range.allowsAny(VersionConstraint.parse('>2.3.3')), isTrue); + expect(range.allowsAny(VersionConstraint.parse('>2.3.4-dev')), isFalse); + }); + + test('of non-pre-release max are included with includeMaxPreRelease', () { + expect( + includeMaxPreReleaseRange + .allowsAny(VersionConstraint.parse('>2.0.0-dev')), + isTrue); + }); + + test( + 'of non-pre-release max are included if min is a pre-release of the ' + 'same version', () { + var range = VersionRange(min: Version.parse('2.3.4-dev.0'), max: v234); + + expect( + range.allowsAny(VersionConstraint.parse('>2.3.4-dev.1')), isTrue); + expect(range.allowsAny(VersionConstraint.parse('>2.3.4')), isFalse); + + expect( + range.allowsAny(VersionConstraint.parse('<2.3.4-dev.1')), isTrue); + expect(range.allowsAny(VersionConstraint.parse('<2.3.4-dev')), isFalse); + }); + + test('of pre-release max are included', () { + var range = VersionConstraint.parse('<2.3.4-dev.2'); + + expect( + range.allowsAny(VersionConstraint.parse('>2.3.4-dev.1')), isTrue); + expect( + range.allowsAny(VersionConstraint.parse('>2.3.4-dev.2')), isFalse); + expect( + range.allowsAny(VersionConstraint.parse('>2.3.4-dev.3')), isFalse); + }); + }); + }); + + group('intersect()', () { + test('two overlapping ranges', () { + expect( + VersionRange(min: v123, max: v250) + .intersect(VersionRange(min: v200, max: v300)), + equals(VersionRange(min: v200, max: v250))); + }); + + test('a non-overlapping range allows no versions', () { + var a = VersionRange(min: v114, max: v124); + var b = VersionRange(min: v200, max: v250); + expect(a.intersect(b).isEmpty, isTrue); + }); + + test('adjacent ranges allow no versions if exclusive', () { + var a = VersionRange(min: v114, max: v124); + var b = VersionRange(min: v124, max: v200); + expect(a.intersect(b).isEmpty, isTrue); + }); + + test('adjacent ranges allow version if inclusive', () { + var a = VersionRange(min: v114, max: v124, includeMax: true); + var b = VersionRange(min: v124, max: v200, includeMin: true); + expect(a.intersect(b), equals(v124)); + }); + + test('with an open range', () { + var open = VersionRange(); + var a = VersionRange(min: v114, max: v124); + expect(open.intersect(open), equals(open)); + expect(a.intersect(open), equals(a)); + }); + + test('returns the version if the range allows it', () { + expect(VersionRange(min: v114, max: v124).intersect(v123), equals(v123)); + expect( + VersionRange(min: v123, max: v124).intersect(v114).isEmpty, isTrue); + }); + + test('with a range with a pre-release min, returns an empty constraint', + () { + expect( + VersionRange(max: v200) + .intersect(VersionConstraint.parse('>=2.0.0-dev')), + equals(VersionConstraint.empty)); + }); + + test('with a range with a pre-release max, returns the original', () { + expect( + VersionRange(max: v200) + .intersect(VersionConstraint.parse('<2.0.0-dev')), + equals(VersionRange(max: v200))); + }); + + group('with includeMaxPreRelease', () { + test('preserves includeMaxPreRelease if the max version is included', () { + expect( + includeMaxPreReleaseRange + .intersect(VersionConstraint.parse('<1.0.0')), + equals(VersionConstraint.parse('<1.0.0'))); + expect( + includeMaxPreReleaseRange + .intersect(VersionConstraint.parse('<2.0.0')), + equals(VersionConstraint.parse('<2.0.0'))); + expect(includeMaxPreReleaseRange.intersect(includeMaxPreReleaseRange), + equals(includeMaxPreReleaseRange)); + expect( + includeMaxPreReleaseRange + .intersect(VersionConstraint.parse('<3.0.0')), + equals(includeMaxPreReleaseRange)); + expect( + includeMaxPreReleaseRange + .intersect(VersionConstraint.parse('>1.1.4')), + equals(VersionRange( + min: v114, max: v200, alwaysIncludeMaxPreRelease: true))); + }); + + test( + 'and a range with a pre-release min, returns ' + 'an intersection', () { + expect( + includeMaxPreReleaseRange + .intersect(VersionConstraint.parse('>=2.0.0-dev')), + equals(VersionConstraint.parse('>=2.0.0-dev <2.0.0'))); + }); + + test( + 'and a range with a pre-release max, returns ' + 'the narrower constraint', () { + expect( + includeMaxPreReleaseRange + .intersect(VersionConstraint.parse('<2.0.0-dev')), + equals(VersionConstraint.parse('<2.0.0-dev'))); + }); + }); + }); + + group('union()', () { + test('with a version returns the range if it contains the version', () { + var range = VersionRange(min: v114, max: v124); + expect(range.union(v123), equals(range)); + }); + + test('with a version on the edge of the range, expands the range', () { + expect( + VersionRange(min: v114, max: v124, alwaysIncludeMaxPreRelease: true) + .union(v124), + equals(VersionRange(min: v114, max: v124, includeMax: true))); + expect(VersionRange(min: v114, max: v124).union(v114), + equals(VersionRange(min: v114, max: v124, includeMin: true))); + }); + + test( + 'with a version allows both the range and the version if the range ' + "doesn't contain the version", () { + var result = VersionRange(min: v003, max: v114).union(v124); + expect(result, allows(v010)); + expect(result, doesNotAllow(v123)); + expect(result, allows(v124)); + }); + + test('returns a VersionUnion for a disjoint range', () { + var result = VersionRange(min: v003, max: v114) + .union(VersionRange(min: v130, max: v200)); + expect(result, allows(v080)); + expect(result, doesNotAllow(v123)); + expect(result, allows(v140)); + }); + + test('returns a VersionUnion for a disjoint range with infinite end', () { + void isVersionUnion(VersionConstraint constraint) { + expect(constraint, allows(v080)); + expect(constraint, doesNotAllow(v123)); + expect(constraint, allows(v140)); + } + + for (final includeAMin in [true, false]) { + for (final includeAMax in [true, false]) { + for (final includeBMin in [true, false]) { + for (final includeBMax in [true, false]) { + final a = VersionRange( + min: v130, includeMin: includeAMin, includeMax: includeAMax); + final b = VersionRange( + max: v114, includeMin: includeBMin, includeMax: includeBMax); + isVersionUnion(a.union(b)); + isVersionUnion(b.union(a)); + } + } + } + } + }); + + test('considers open ranges disjoint', () { + var result = VersionRange(min: v003, max: v114) + .union(VersionRange(min: v114, max: v200)); + expect(result, allows(v080)); + expect(result, doesNotAllow(v114)); + expect(result, allows(v140)); + + result = VersionRange(min: v114, max: v200) + .union(VersionRange(min: v003, max: v114)); + expect(result, allows(v080)); + expect(result, doesNotAllow(v114)); + expect(result, allows(v140)); + }); + + test('returns a merged range for an overlapping range', () { + var result = VersionRange(min: v003, max: v114) + .union(VersionRange(min: v080, max: v200)); + expect(result, equals(VersionRange(min: v003, max: v200))); + }); + + test('considers closed ranges overlapping', () { + var result = VersionRange(min: v003, max: v114, includeMax: true) + .union(VersionRange(min: v114, max: v200)); + expect(result, equals(VersionRange(min: v003, max: v200))); + + result = + VersionRange(min: v003, max: v114, alwaysIncludeMaxPreRelease: true) + .union(VersionRange(min: v114, max: v200, includeMin: true)); + expect(result, equals(VersionRange(min: v003, max: v200))); + + result = VersionRange(min: v114, max: v200) + .union(VersionRange(min: v003, max: v114, includeMax: true)); + expect(result, equals(VersionRange(min: v003, max: v200))); + + result = VersionRange(min: v114, max: v200, includeMin: true).union( + VersionRange(min: v003, max: v114, alwaysIncludeMaxPreRelease: true)); + expect(result, equals(VersionRange(min: v003, max: v200))); + }); + + test('includes edges if either range does', () { + var result = VersionRange(min: v003, max: v114, includeMin: true) + .union(VersionRange(min: v003, max: v114, includeMax: true)); + expect( + result, + equals(VersionRange( + min: v003, max: v114, includeMin: true, includeMax: true))); + }); + + test('with a range with a pre-release min, returns a constraint with a gap', + () { + var result = + VersionRange(max: v200).union(VersionConstraint.parse('>=2.0.0-dev')); + expect(result, allows(v140)); + expect(result, doesNotAllow(Version.parse('2.0.0-alpha'))); + expect(result, allows(Version.parse('2.0.0-dev'))); + expect(result, allows(Version.parse('2.0.0-dev.1'))); + expect(result, allows(Version.parse('2.0.0'))); + }); + + test('with a range with a pre-release max, returns the larger constraint', + () { + expect( + VersionRange(max: v200).union(VersionConstraint.parse('<2.0.0-dev')), + equals(VersionConstraint.parse('<2.0.0-dev'))); + }); + + group('with includeMaxPreRelease', () { + test('adds includeMaxPreRelease if the max version is included', () { + expect( + includeMaxPreReleaseRange.union(VersionConstraint.parse('<1.0.0')), + equals(includeMaxPreReleaseRange)); + expect(includeMaxPreReleaseRange.union(includeMaxPreReleaseRange), + equals(includeMaxPreReleaseRange)); + expect( + includeMaxPreReleaseRange.union(VersionConstraint.parse('<2.0.0')), + equals(includeMaxPreReleaseRange)); + expect( + includeMaxPreReleaseRange.union(VersionConstraint.parse('<3.0.0')), + equals(VersionConstraint.parse('<3.0.0'))); + }); + + test('and a range with a pre-release min, returns any', () { + expect( + includeMaxPreReleaseRange + .union(VersionConstraint.parse('>=2.0.0-dev')), + equals(VersionConstraint.any)); + }); + + test('and a range with a pre-release max, returns the original', () { + expect( + includeMaxPreReleaseRange + .union(VersionConstraint.parse('<2.0.0-dev')), + equals(includeMaxPreReleaseRange)); + }); + }); + }); + + group('difference()', () { + test('with an empty range returns the original range', () { + expect( + VersionRange(min: v003, max: v114) + .difference(VersionConstraint.empty), + equals(VersionRange(min: v003, max: v114))); + }); + + test('with a version outside the range returns the original range', () { + expect(VersionRange(min: v003, max: v114).difference(v200), + equals(VersionRange(min: v003, max: v114))); + }); + + test('with a version in the range splits the range', () { + expect( + VersionRange(min: v003, max: v114).difference(v072), + equals(VersionConstraint.unionOf([ + VersionRange( + min: v003, max: v072, alwaysIncludeMaxPreRelease: true), + VersionRange(min: v072, max: v114) + ]))); + }); + + test('with the max version makes the max exclusive', () { + expect( + VersionRange(min: v003, max: v114, includeMax: true).difference(v114), + equals(VersionRange( + min: v003, max: v114, alwaysIncludeMaxPreRelease: true))); + }); + + test('with the min version makes the min exclusive', () { + expect( + VersionRange(min: v003, max: v114, includeMin: true).difference(v003), + equals(VersionRange(min: v003, max: v114))); + }); + + test('with a disjoint range returns the original', () { + expect( + VersionRange(min: v003, max: v114) + .difference(VersionRange(min: v123, max: v140)), + equals(VersionRange(min: v003, max: v114))); + }); + + test('with an adjacent range returns the original', () { + expect( + VersionRange(min: v003, max: v114, includeMax: true) + .difference(VersionRange(min: v114, max: v140)), + equals(VersionRange(min: v003, max: v114, includeMax: true))); + }); + + test('with a range at the beginning cuts off the beginning of the range', + () { + expect( + VersionRange(min: v080, max: v130) + .difference(VersionRange(min: v010, max: v114)), + equals(VersionConstraint.parse('>=1.1.4-0 <1.3.0'))); + expect( + VersionRange(min: v080, max: v130) + .difference(VersionRange(max: v114)), + equals(VersionConstraint.parse('>=1.1.4-0 <1.3.0'))); + expect( + VersionRange(min: v080, max: v130) + .difference(VersionRange(min: v010, max: v114, includeMax: true)), + equals(VersionRange(min: v114, max: v130))); + expect( + VersionRange(min: v080, max: v130, includeMin: true) + .difference(VersionRange(min: v010, max: v080, includeMax: true)), + equals(VersionRange(min: v080, max: v130))); + expect( + VersionRange(min: v080, max: v130, includeMax: true) + .difference(VersionRange(min: v080, max: v130)), + equals(VersionConstraint.parse('>=1.3.0-0 <=1.3.0'))); + }); + + test('with a range at the end cuts off the end of the range', () { + expect( + VersionRange(min: v080, max: v130) + .difference(VersionRange(min: v114, max: v140)), + equals(VersionRange(min: v080, max: v114, includeMax: true))); + expect( + VersionRange(min: v080, max: v130) + .difference(VersionRange(min: v114)), + equals(VersionRange(min: v080, max: v114, includeMax: true))); + expect( + VersionRange(min: v080, max: v130) + .difference(VersionRange(min: v114, max: v140, includeMin: true)), + equals(VersionRange( + min: v080, max: v114, alwaysIncludeMaxPreRelease: true))); + expect( + VersionRange(min: v080, max: v130, includeMax: true) + .difference(VersionRange(min: v130, max: v140, includeMin: true)), + equals(VersionRange( + min: v080, max: v130, alwaysIncludeMaxPreRelease: true))); + expect( + VersionRange(min: v080, max: v130, includeMin: true) + .difference(VersionRange(min: v080, max: v130)), + equals(v080)); + }); + + test('with a range in the middle cuts the range in half', () { + expect( + VersionRange(min: v003, max: v130) + .difference(VersionRange(min: v072, max: v114)), + equals(VersionConstraint.unionOf([ + VersionRange(min: v003, max: v072, includeMax: true), + VersionConstraint.parse('>=1.1.4-0 <1.3.0') + ]))); + }); + + test('with a totally covering range returns empty', () { + expect( + VersionRange(min: v114, max: v200) + .difference(VersionRange(min: v072, max: v300)), + isEmpty); + expect( + VersionRange(min: v003, max: v114) + .difference(VersionRange(min: v003, max: v114)), + isEmpty); + expect( + VersionRange(min: v003, max: v114, includeMin: true, includeMax: true) + .difference(VersionRange( + min: v003, max: v114, includeMin: true, includeMax: true)), + isEmpty); + }); + + test( + "with a version union that doesn't cover the range, returns the " + 'original', () { + expect( + VersionRange(min: v114, max: v140) + .difference(VersionConstraint.unionOf([v010, v200])), + equals(VersionRange(min: v114, max: v140))); + }); + + test('with a version union that intersects the ends, chops them off', () { + expect( + VersionRange(min: v114, max: v140).difference( + VersionConstraint.unionOf([ + VersionRange(min: v080, max: v123), + VersionRange(min: v130, max: v200) + ])), + equals(VersionConstraint.parse('>=1.2.3-0 <=1.3.0'))); + }); + + test('with a version union that intersects the middle, chops it up', () { + expect( + VersionRange(min: v114, max: v140) + .difference(VersionConstraint.unionOf([v123, v124, v130])), + equals(VersionConstraint.unionOf([ + VersionRange( + min: v114, max: v123, alwaysIncludeMaxPreRelease: true), + VersionRange( + min: v123, max: v124, alwaysIncludeMaxPreRelease: true), + VersionRange( + min: v124, max: v130, alwaysIncludeMaxPreRelease: true), + VersionRange(min: v130, max: v140) + ]))); + }); + + test('with a version union that covers the whole range, returns empty', () { + expect( + VersionRange(min: v114, max: v140).difference( + VersionConstraint.unionOf([v003, VersionRange(min: v010)])), + equals(VersionConstraint.empty)); + }); + + test('with a range with a pre-release min, returns the original', () { + expect( + VersionRange(max: v200) + .difference(VersionConstraint.parse('>=2.0.0-dev')), + equals(VersionRange(max: v200))); + }); + + test('with a range with a pre-release max, returns null', () { + expect( + VersionRange(max: v200) + .difference(VersionConstraint.parse('<2.0.0-dev')), + equals(VersionConstraint.empty)); + }); + + group('with includeMaxPreRelease', () { + group('for the minuend', () { + test('preserves includeMaxPreRelease if the max version is included', + () { + expect( + includeMaxPreReleaseRange + .difference(VersionConstraint.parse('<1.0.0')), + equals(VersionRange( + min: Version.parse('1.0.0-0'), + max: v200, + includeMin: true, + alwaysIncludeMaxPreRelease: true))); + expect( + includeMaxPreReleaseRange + .difference(VersionConstraint.parse('<2.0.0')), + equals(VersionRange( + min: v200.firstPreRelease, + max: v200, + includeMin: true, + alwaysIncludeMaxPreRelease: true))); + expect( + includeMaxPreReleaseRange.difference(includeMaxPreReleaseRange), + equals(VersionConstraint.empty)); + expect( + includeMaxPreReleaseRange + .difference(VersionConstraint.parse('<3.0.0')), + equals(VersionConstraint.empty)); + }); + + test('with a range with a pre-release min, adjusts the max', () { + expect( + includeMaxPreReleaseRange + .difference(VersionConstraint.parse('>=2.0.0-dev')), + equals(VersionConstraint.parse('<2.0.0-dev'))); + }); + + test('with a range with a pre-release max, adjusts the min', () { + expect( + includeMaxPreReleaseRange + .difference(VersionConstraint.parse('<2.0.0-dev')), + equals(VersionConstraint.parse('>=2.0.0-dev <2.0.0'))); + }); + }); + + group('for the subtrahend', () { + group("doesn't create a pre-release minimum", () { + test('when cutting off the bottom', () { + expect( + VersionConstraint.parse('<3.0.0') + .difference(includeMaxPreReleaseRange), + equals(VersionRange(min: v200, max: v300, includeMin: true))); + }); + + test('with splitting down the middle', () { + expect( + VersionConstraint.parse('<4.0.0').difference(VersionRange( + min: v200, + max: v300, + includeMin: true, + alwaysIncludeMaxPreRelease: true)), + equals(VersionConstraint.unionOf([ + VersionRange(max: v200, alwaysIncludeMaxPreRelease: true), + VersionConstraint.parse('>=3.0.0 <4.0.0') + ]))); + }); + + test('can leave a single version', () { + expect( + VersionConstraint.parse('<=2.0.0') + .difference(includeMaxPreReleaseRange), + equals(v200)); + }); + }); + }); + }); + }); + + test('isEmpty', () { + expect(VersionRange().isEmpty, isFalse); + expect(VersionRange(min: v123, max: v124).isEmpty, isFalse); + }); + + group('compareTo()', () { + test('orders by minimum first', () { + _expectComparesSmaller(VersionRange(min: v003, max: v080), + VersionRange(min: v010, max: v072)); + _expectComparesSmaller(VersionRange(min: v003, max: v080), + VersionRange(min: v010, max: v080)); + _expectComparesSmaller(VersionRange(min: v003, max: v080), + VersionRange(min: v010, max: v114)); + }); + + test('orders by maximum second', () { + _expectComparesSmaller(VersionRange(min: v003, max: v010), + VersionRange(min: v003, max: v072)); + }); + + test('includeMin comes before !includeMin', () { + _expectComparesSmaller( + VersionRange(min: v003, max: v080, includeMin: true), + VersionRange(min: v003, max: v080)); + }); + + test('includeMax comes after !includeMax', () { + _expectComparesSmaller(VersionRange(min: v003, max: v080), + VersionRange(min: v003, max: v080, includeMax: true)); + }); + + test('includeMaxPreRelease comes after !includeMaxPreRelease', () { + _expectComparesSmaller( + VersionRange(max: v200), includeMaxPreReleaseRange); + }); + + test('no minimum comes before small minimum', () { + _expectComparesSmaller( + VersionRange(max: v010), VersionRange(min: v003, max: v010)); + _expectComparesSmaller(VersionRange(max: v010, includeMin: true), + VersionRange(min: v003, max: v010)); + }); + + test('no maximium comes after large maximum', () { + _expectComparesSmaller( + VersionRange(min: v003, max: v300), VersionRange(min: v003)); + _expectComparesSmaller(VersionRange(min: v003, max: v300), + VersionRange(min: v003, includeMax: true)); + }); + }); +} + +void _expectComparesSmaller(VersionRange smaller, VersionRange larger) { + expect(smaller.compareTo(larger), lessThan(0), + reason: 'expected $smaller to sort below $larger'); + expect(larger.compareTo(smaller), greaterThan(0), + reason: 'expected $larger to sort above $smaller'); +} diff --git a/pkgs/pub_semver/test/version_test.dart b/pkgs/pub_semver/test/version_test.dart new file mode 100644 index 000000000..d7f1197c8 --- /dev/null +++ b/pkgs/pub_semver/test/version_test.dart @@ -0,0 +1,411 @@ +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + test('none', () { + expect(Version.none.toString(), equals('0.0.0')); + }); + + test('prioritize()', () { + // A correctly sorted list of versions in order of increasing priority. + var versions = [ + '1.0.0-alpha', + '2.0.0-alpha', + '1.0.0', + '1.0.0+build', + '1.0.1', + '1.1.0', + '2.0.0' + ]; + + // Ensure that every pair of versions is prioritized in the order that it + // appears in the list. + for (var i = 0; i < versions.length; i++) { + for (var j = 0; j < versions.length; j++) { + var a = Version.parse(versions[i]); + var b = Version.parse(versions[j]); + expect(Version.prioritize(a, b), equals(i.compareTo(j))); + } + } + }); + + test('antiprioritize()', () { + // A correctly sorted list of versions in order of increasing antipriority. + var versions = [ + '2.0.0-alpha', + '1.0.0-alpha', + '2.0.0', + '1.1.0', + '1.0.1', + '1.0.0+build', + '1.0.0' + ]; + + // Ensure that every pair of versions is prioritized in the order that it + // appears in the list. + for (var i = 0; i < versions.length; i++) { + for (var j = 0; j < versions.length; j++) { + var a = Version.parse(versions[i]); + var b = Version.parse(versions[j]); + expect(Version.antiprioritize(a, b), equals(i.compareTo(j))); + } + } + }); + + group('constructor', () { + test('throws on negative numbers', () { + expect(() => Version(-1, 1, 1), throwsArgumentError); + expect(() => Version(1, -1, 1), throwsArgumentError); + expect(() => Version(1, 1, -1), throwsArgumentError); + }); + }); + + group('comparison', () { + // A correctly sorted list of versions. + var versions = [ + '1.0.0-alpha', + '1.0.0-alpha.1', + '1.0.0-beta.2', + '1.0.0-beta.11', + '1.0.0-rc.1', + '1.0.0-rc.1+build.1', + '1.0.0', + '1.0.0+0.3.7', + '1.3.7+build', + '1.3.7+build.2.b8f12d7', + '1.3.7+build.11.e0f985a', + '2.0.0', + '2.1.0', + '2.2.0', + '2.11.0', + '2.11.1' + ]; + + test('compareTo()', () { + // Ensure that every pair of versions compares in the order that it + // appears in the list. + for (var i = 0; i < versions.length; i++) { + for (var j = 0; j < versions.length; j++) { + var a = Version.parse(versions[i]); + var b = Version.parse(versions[j]); + expect(a.compareTo(b), equals(i.compareTo(j))); + } + } + }); + + test('operators', () { + for (var i = 0; i < versions.length; i++) { + for (var j = 0; j < versions.length; j++) { + var a = Version.parse(versions[i]); + var b = Version.parse(versions[j]); + expect(a < b, equals(i < j)); + expect(a > b, equals(i > j)); + expect(a <= b, equals(i <= j)); + expect(a >= b, equals(i >= j)); + expect(a == b, equals(i == j)); + expect(a != b, equals(i != j)); + } + } + }); + + test('equality', () { + expect(Version.parse('01.2.3'), equals(Version.parse('1.2.3'))); + expect(Version.parse('1.02.3'), equals(Version.parse('1.2.3'))); + expect(Version.parse('1.2.03'), equals(Version.parse('1.2.3'))); + expect(Version.parse('1.2.3-01'), equals(Version.parse('1.2.3-1'))); + expect(Version.parse('1.2.3+01'), equals(Version.parse('1.2.3+1'))); + }); + }); + + test('allows()', () { + expect(v123, allows(v123)); + expect( + v123, + doesNotAllow( + Version.parse('2.2.3'), + Version.parse('1.3.3'), + Version.parse('1.2.4'), + Version.parse('1.2.3-dev'), + Version.parse('1.2.3+build'))); + }); + + test('allowsAll()', () { + expect(v123.allowsAll(v123), isTrue); + expect(v123.allowsAll(v003), isFalse); + expect(v123.allowsAll(VersionRange(min: v114, max: v124)), isFalse); + expect(v123.allowsAll(VersionConstraint.any), isFalse); + expect(v123.allowsAll(VersionConstraint.empty), isTrue); + }); + + test('allowsAny()', () { + expect(v123.allowsAny(v123), isTrue); + expect(v123.allowsAny(v003), isFalse); + expect(v123.allowsAny(VersionRange(min: v114, max: v124)), isTrue); + expect(v123.allowsAny(VersionConstraint.any), isTrue); + expect(v123.allowsAny(VersionConstraint.empty), isFalse); + }); + + test('intersect()', () { + // Intersecting the same version returns the version. + expect(v123.intersect(v123), equals(v123)); + + // Intersecting a different version allows no versions. + expect(v123.intersect(v114).isEmpty, isTrue); + + // Intersecting a range returns the version if the range allows it. + expect(v123.intersect(VersionRange(min: v114, max: v124)), equals(v123)); + + // Intersecting a range allows no versions if the range doesn't allow it. + expect(v114.intersect(VersionRange(min: v123, max: v124)).isEmpty, isTrue); + }); + + group('union()', () { + test('with the same version returns the version', () { + expect(v123.union(v123), equals(v123)); + }); + + test('with a different version returns a version that matches both', () { + var result = v123.union(v080); + expect(result, allows(v123)); + expect(result, allows(v080)); + + // Nothing in between should match. + expect(result, doesNotAllow(v114)); + }); + + test('with a range returns the range if it contains the version', () { + var range = VersionRange(min: v114, max: v124); + expect(v123.union(range), equals(range)); + }); + + test('with a range with the version on the edge, expands the range', () { + expect( + v124.union(VersionRange( + min: v114, max: v124, alwaysIncludeMaxPreRelease: true)), + equals(VersionRange(min: v114, max: v124, includeMax: true))); + expect( + v124.firstPreRelease.union(VersionRange(min: v114, max: v124)), + equals(VersionRange( + min: v114, max: v124.firstPreRelease, includeMax: true))); + expect(v114.union(VersionRange(min: v114, max: v124)), + equals(VersionRange(min: v114, max: v124, includeMin: true))); + }); + + test( + 'with a range allows both the range and the version if the range ' + "doesn't contain the version", () { + var result = v123.union(VersionRange(min: v003, max: v114)); + expect(result, allows(v123)); + expect(result, allows(v010)); + }); + }); + + group('difference()', () { + test('with the same version returns an empty constraint', () { + expect(v123.difference(v123), isEmpty); + }); + + test('with a different version returns the original version', () { + expect(v123.difference(v080), equals(v123)); + }); + + test('returns an empty constraint with a range that contains the version', + () { + expect(v123.difference(VersionRange(min: v114, max: v124)), isEmpty); + }); + + test("returns the version constraint with a range that doesn't contain it", + () { + expect(v123.difference(VersionRange(min: v140, max: v300)), equals(v123)); + }); + }); + + test('isEmpty', () { + expect(v123.isEmpty, isFalse); + }); + + test('nextMajor', () { + expect(v123.nextMajor, equals(v200)); + expect(v114.nextMajor, equals(v200)); + expect(v200.nextMajor, equals(v300)); + + // Ignores pre-release if not on a major version. + expect(Version.parse('1.2.3-dev').nextMajor, equals(v200)); + + // Just removes it if on a major version. + expect(Version.parse('2.0.0-dev').nextMajor, equals(v200)); + + // Strips build suffix. + expect(Version.parse('1.2.3+patch').nextMajor, equals(v200)); + }); + + test('nextMinor', () { + expect(v123.nextMinor, equals(v130)); + expect(v130.nextMinor, equals(v140)); + + // Ignores pre-release if not on a minor version. + expect(Version.parse('1.2.3-dev').nextMinor, equals(v130)); + + // Just removes it if on a minor version. + expect(Version.parse('1.3.0-dev').nextMinor, equals(v130)); + + // Strips build suffix. + expect(Version.parse('1.2.3+patch').nextMinor, equals(v130)); + }); + + test('nextPatch', () { + expect(v123.nextPatch, equals(v124)); + expect(v200.nextPatch, equals(v201)); + + // Just removes pre-release version if present. + expect(Version.parse('1.2.4-dev').nextPatch, equals(v124)); + + // Strips build suffix. + expect(Version.parse('1.2.3+patch').nextPatch, equals(v124)); + }); + + test('nextBreaking', () { + expect(v123.nextBreaking, equals(v200)); + expect(v072.nextBreaking, equals(v080)); + expect(v003.nextBreaking, equals(v010)); + + // Removes pre-release version if present. + expect(Version.parse('1.2.3-dev').nextBreaking, equals(v200)); + + // Strips build suffix. + expect(Version.parse('1.2.3+patch').nextBreaking, equals(v200)); + }); + + test('parse()', () { + expect(Version.parse('0.0.0'), equals(Version(0, 0, 0))); + expect(Version.parse('12.34.56'), equals(Version(12, 34, 56))); + + expect(Version.parse('1.2.3-alpha.1'), + equals(Version(1, 2, 3, pre: 'alpha.1'))); + expect(Version.parse('1.2.3-x.7.z-92'), + equals(Version(1, 2, 3, pre: 'x.7.z-92'))); + + expect(Version.parse('1.2.3+build.1'), + equals(Version(1, 2, 3, build: 'build.1'))); + expect(Version.parse('1.2.3+x.7.z-92'), + equals(Version(1, 2, 3, build: 'x.7.z-92'))); + + expect(Version.parse('1.0.0-rc-1+build-1'), + equals(Version(1, 0, 0, pre: 'rc-1', build: 'build-1'))); + + expect(() => Version.parse('1.0'), throwsFormatException); + expect(() => Version.parse('1a2b3'), throwsFormatException); + expect(() => Version.parse('1.2.3.4'), throwsFormatException); + expect(() => Version.parse('1234'), throwsFormatException); + expect(() => Version.parse('-2.3.4'), throwsFormatException); + expect(() => Version.parse('1.3-pre'), throwsFormatException); + expect(() => Version.parse('1.3+build'), throwsFormatException); + expect(() => Version.parse('1.3+bu?!3ild'), throwsFormatException); + }); + + group('toString()', () { + test('returns the version string', () { + expect(Version(0, 0, 0).toString(), equals('0.0.0')); + expect(Version(12, 34, 56).toString(), equals('12.34.56')); + + expect( + Version(1, 2, 3, pre: 'alpha.1').toString(), equals('1.2.3-alpha.1')); + expect(Version(1, 2, 3, pre: 'x.7.z-92').toString(), + equals('1.2.3-x.7.z-92')); + + expect(Version(1, 2, 3, build: 'build.1').toString(), + equals('1.2.3+build.1')); + expect(Version(1, 2, 3, pre: 'pre', build: 'bui').toString(), + equals('1.2.3-pre+bui')); + }); + + test('preserves leading zeroes', () { + expect(Version.parse('001.02.0003-01.dev+pre.002').toString(), + equals('001.02.0003-01.dev+pre.002')); + }); + }); + + group('canonicalizedVersion', () { + test('returns version string', () { + expect(Version(0, 0, 0).canonicalizedVersion, equals('0.0.0')); + expect(Version(12, 34, 56).canonicalizedVersion, equals('12.34.56')); + + expect(Version(1, 2, 3, pre: 'alpha.1').canonicalizedVersion, + equals('1.2.3-alpha.1')); + expect(Version(1, 2, 3, pre: 'x.7.z-92').canonicalizedVersion, + equals('1.2.3-x.7.z-92')); + + expect(Version(1, 2, 3, build: 'build.1').canonicalizedVersion, + equals('1.2.3+build.1')); + expect(Version(1, 2, 3, pre: 'pre', build: 'bui').canonicalizedVersion, + equals('1.2.3-pre+bui')); + }); + + test('discards leading zeroes', () { + expect(Version.parse('001.02.0003-01.dev+pre.002').canonicalizedVersion, + equals('1.2.3-1.dev+pre.2')); + }); + + test('example from documentation', () { + final v = Version.parse('01.02.03-01.dev+pre.02'); + + assert(v.toString() == '01.02.03-01.dev+pre.02'); + assert(v.canonicalizedVersion == '1.2.3-1.dev+pre.2'); + assert(Version.parse(v.canonicalizedVersion) == v); + }); + }); + + group('primary', () { + test('single', () { + expect( + _primary([ + '1.2.3', + ]).toString(), + '1.2.3', + ); + }); + + test('normal', () { + expect( + _primary([ + '1.2.3', + '1.2.2', + ]).toString(), + '1.2.3', + ); + }); + + test('all prerelease', () { + expect( + _primary([ + '1.2.2-dev.1', + '1.2.2-dev.2', + ]).toString(), + '1.2.2-dev.2', + ); + }); + + test('later prerelease', () { + expect( + _primary([ + '1.2.3', + '1.2.3-dev', + ]).toString(), + '1.2.3', + ); + }); + + test('empty', () { + expect(() => Version.primary([]), throwsStateError); + }); + }); +} + +Version _primary(List input) => + Version.primary(input.map(Version.parse).toList()); diff --git a/pkgs/pub_semver/test/version_union_test.dart b/pkgs/pub_semver/test/version_union_test.dart new file mode 100644 index 000000000..857f10e87 --- /dev/null +++ b/pkgs/pub_semver/test/version_union_test.dart @@ -0,0 +1,482 @@ +// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +import 'utils.dart'; + +void main() { + group('factory', () { + test('ignores empty constraints', () { + expect( + VersionConstraint.unionOf([ + VersionConstraint.empty, + VersionConstraint.empty, + v123, + VersionConstraint.empty + ]), + equals(v123)); + + expect( + VersionConstraint.unionOf( + [VersionConstraint.empty, VersionConstraint.empty]), + isEmpty); + }); + + test('returns an empty constraint for an empty list', () { + expect(VersionConstraint.unionOf([]), isEmpty); + }); + + test('any constraints override everything', () { + expect( + VersionConstraint.unionOf([ + v123, + VersionConstraint.any, + v200, + VersionRange(min: v234, max: v250) + ]), + equals(VersionConstraint.any)); + }); + + test('flattens other unions', () { + expect( + VersionConstraint.unionOf([ + v072, + VersionConstraint.unionOf([v123, v124]), + v250 + ]), + equals(VersionConstraint.unionOf([v072, v123, v124, v250]))); + }); + + test('returns a single merged range as-is', () { + expect( + VersionConstraint.unionOf([ + VersionRange(min: v080, max: v140), + VersionRange(min: v123, max: v200) + ]), + equals(VersionRange(min: v080, max: v200))); + }); + }); + + group('equality', () { + test("doesn't depend on original order", () { + expect( + VersionConstraint.unionOf([ + v250, + VersionRange(min: v201, max: v234), + v124, + v072, + VersionRange(min: v080, max: v114), + v123 + ]), + equals(VersionConstraint.unionOf([ + v072, + VersionRange(min: v080, max: v114), + v123, + v124, + VersionRange(min: v201, max: v234), + v250 + ]))); + }); + + test('merges overlapping ranges', () { + expect( + VersionConstraint.unionOf([ + VersionRange(min: v003, max: v072), + VersionRange(min: v010, max: v080), + VersionRange(min: v114, max: v124), + VersionRange(min: v123, max: v130) + ]), + equals(VersionConstraint.unionOf([ + VersionRange(min: v003, max: v080), + VersionRange(min: v114, max: v130) + ]))); + }); + + test('merges adjacent ranges', () { + expect( + VersionConstraint.unionOf([ + VersionRange(min: v003, max: v072, includeMax: true), + VersionRange(min: v072, max: v080), + VersionRange( + min: v114, max: v124, alwaysIncludeMaxPreRelease: true), + VersionRange(min: v124, max: v130, includeMin: true), + VersionRange(min: v130.firstPreRelease, max: v200, includeMin: true) + ]), + equals(VersionConstraint.unionOf([ + VersionRange(min: v003, max: v080), + VersionRange(min: v114, max: v200) + ]))); + }); + + test("doesn't merge not-quite-adjacent ranges", () { + expect( + VersionConstraint.unionOf([ + VersionRange(min: v114, max: v124), + VersionRange(min: v124, max: v130, includeMin: true) + ]), + isNot(equals(VersionRange(min: v114, max: v130)))); + + expect( + VersionConstraint.unionOf([ + VersionRange(min: v003, max: v072), + VersionRange(min: v072, max: v080) + ]), + isNot(equals(VersionRange(min: v003, max: v080)))); + }); + + test('merges version numbers into ranges', () { + expect( + VersionConstraint.unionOf([ + VersionRange(min: v003, max: v072), + v010, + VersionRange(min: v114, max: v124), + v123 + ]), + equals(VersionConstraint.unionOf([ + VersionRange(min: v003, max: v072), + VersionRange(min: v114, max: v124) + ]))); + }); + + test('merges adjacent version numbers into ranges', () { + expect( + VersionConstraint.unionOf([ + VersionRange( + min: v003, max: v072, alwaysIncludeMaxPreRelease: true), + v072, + v114, + VersionRange(min: v114, max: v124), + v124.firstPreRelease + ]), + equals(VersionConstraint.unionOf([ + VersionRange(min: v003, max: v072, includeMax: true), + VersionRange( + min: v114, + max: v124.firstPreRelease, + includeMin: true, + includeMax: true) + ]))); + }); + + test("doesn't merge not-quite-adjacent version numbers into ranges", () { + expect( + VersionConstraint.unionOf([VersionRange(min: v003, max: v072), v072]), + isNot(equals(VersionRange(min: v003, max: v072, includeMax: true)))); + }); + }); + + test('isEmpty returns false', () { + expect( + VersionConstraint.unionOf([ + VersionRange(min: v003, max: v080), + VersionRange(min: v123, max: v130), + ]), + isNot(isEmpty)); + }); + + test('isAny returns false', () { + expect( + VersionConstraint.unionOf([ + VersionRange(min: v003, max: v080), + VersionRange(min: v123, max: v130), + ]).isAny, + isFalse); + }); + + test('allows() allows anything the components allow', () { + var union = VersionConstraint.unionOf([ + VersionRange(min: v003, max: v080), + VersionRange(min: v123, max: v130), + v200 + ]); + + expect(union, allows(v010)); + expect(union, doesNotAllow(v080)); + expect(union, allows(v124)); + expect(union, doesNotAllow(v140)); + expect(union, allows(v200)); + }); + + group('allowsAll()', () { + test('for a version, returns true if any component allows the version', () { + var union = VersionConstraint.unionOf([ + VersionRange(min: v003, max: v080), + VersionRange(min: v123, max: v130), + v200 + ]); + + expect(union.allowsAll(v010), isTrue); + expect(union.allowsAll(v080), isFalse); + expect(union.allowsAll(v124), isTrue); + expect(union.allowsAll(v140), isFalse); + expect(union.allowsAll(v200), isTrue); + }); + + test( + 'for a version range, returns true if any component allows the whole ' + 'range', () { + var union = VersionConstraint.unionOf([ + VersionRange(min: v003, max: v080), + VersionRange(min: v123, max: v130) + ]); + + expect(union.allowsAll(VersionRange(min: v003, max: v080)), isTrue); + expect(union.allowsAll(VersionRange(min: v010, max: v072)), isTrue); + expect(union.allowsAll(VersionRange(min: v010, max: v124)), isFalse); + }); + + group('for a union,', () { + var union = VersionConstraint.unionOf([ + VersionRange(min: v003, max: v080), + VersionRange(min: v123, max: v130) + ]); + + test('returns true if every constraint matches a different constraint', + () { + expect( + union.allowsAll(VersionConstraint.unionOf([ + VersionRange(min: v010, max: v072), + VersionRange(min: v124, max: v130) + ])), + isTrue); + }); + + test('returns true if every constraint matches the same constraint', () { + expect( + union.allowsAll(VersionConstraint.unionOf([ + VersionRange(min: v003, max: v010), + VersionRange(min: v072, max: v080) + ])), + isTrue); + }); + + test("returns false if there's an unmatched constraint", () { + expect( + union.allowsAll(VersionConstraint.unionOf([ + VersionRange(min: v010, max: v072), + VersionRange(min: v124, max: v130), + VersionRange(min: v140, max: v200) + ])), + isFalse); + }); + + test("returns false if a constraint isn't fully matched", () { + expect( + union.allowsAll(VersionConstraint.unionOf([ + VersionRange(min: v010, max: v114), + VersionRange(min: v124, max: v130) + ])), + isFalse); + }); + }); + }); + + group('allowsAny()', () { + test('for a version, returns true if any component allows the version', () { + var union = VersionConstraint.unionOf([ + VersionRange(min: v003, max: v080), + VersionRange(min: v123, max: v130), + v200 + ]); + + expect(union.allowsAny(v010), isTrue); + expect(union.allowsAny(v080), isFalse); + expect(union.allowsAny(v124), isTrue); + expect(union.allowsAny(v140), isFalse); + expect(union.allowsAny(v200), isTrue); + }); + + test( + 'for a version range, returns true if any component allows part of ' + 'the range', () { + var union = + VersionConstraint.unionOf([VersionRange(min: v003, max: v080), v123]); + + expect(union.allowsAny(VersionRange(min: v010, max: v114)), isTrue); + expect(union.allowsAny(VersionRange(min: v114, max: v124)), isTrue); + expect(union.allowsAny(VersionRange(min: v124, max: v130)), isFalse); + }); + + group('for a union,', () { + var union = VersionConstraint.unionOf([ + VersionRange(min: v010, max: v080), + VersionRange(min: v123, max: v130) + ]); + + test('returns true if any constraint matches', () { + expect( + union.allowsAny(VersionConstraint.unionOf( + [v072, VersionRange(min: v200, max: v300)])), + isTrue); + + expect( + union.allowsAny(VersionConstraint.unionOf( + [v003, VersionRange(min: v124, max: v300)])), + isTrue); + }); + + test('returns false if no constraint matches', () { + expect( + union.allowsAny(VersionConstraint.unionOf([ + v003, + VersionRange(min: v130, max: v140), + VersionRange(min: v140, max: v200) + ])), + isFalse); + }); + }); + }); + + group('intersect()', () { + test('with an overlapping version, returns that version', () { + expect( + VersionConstraint.unionOf([ + VersionRange(min: v010, max: v080), + VersionRange(min: v123, max: v140) + ]).intersect(v072), + equals(v072)); + }); + + test('with a non-overlapping version, returns an empty constraint', () { + expect( + VersionConstraint.unionOf([ + VersionRange(min: v010, max: v080), + VersionRange(min: v123, max: v140) + ]).intersect(v300), + isEmpty); + }); + + test('with an overlapping range, returns that range', () { + var range = VersionRange(min: v072, max: v080); + expect( + VersionConstraint.unionOf([ + VersionRange(min: v010, max: v080), + VersionRange(min: v123, max: v140) + ]).intersect(range), + equals(range)); + }); + + test('with a non-overlapping range, returns an empty constraint', () { + expect( + VersionConstraint.unionOf([ + VersionRange(min: v010, max: v080), + VersionRange(min: v123, max: v140) + ]).intersect(VersionRange(min: v080, max: v123)), + isEmpty); + }); + + test('with a parially-overlapping range, returns the overlapping parts', + () { + expect( + VersionConstraint.unionOf([ + VersionRange(min: v010, max: v080), + VersionRange(min: v123, max: v140) + ]).intersect(VersionRange(min: v072, max: v130)), + equals(VersionConstraint.unionOf([ + VersionRange(min: v072, max: v080), + VersionRange(min: v123, max: v130) + ]))); + }); + + group('for a union,', () { + var union = VersionConstraint.unionOf([ + VersionRange(min: v003, max: v080), + VersionRange(min: v123, max: v130) + ]); + + test('returns the overlapping parts', () { + expect( + union.intersect(VersionConstraint.unionOf([ + v010, + VersionRange(min: v072, max: v124), + VersionRange(min: v124, max: v130) + ])), + equals(VersionConstraint.unionOf([ + v010, + VersionRange(min: v072, max: v080), + VersionRange(min: v123, max: v124), + VersionRange(min: v124, max: v130) + ]))); + }); + + test("drops parts that don't match", () { + expect( + union.intersect(VersionConstraint.unionOf([ + v003, + VersionRange(min: v072, max: v080), + VersionRange(min: v080, max: v123) + ])), + equals(VersionRange(min: v072, max: v080))); + }); + }); + }); + + group('difference()', () { + test("ignores ranges that don't intersect", () { + expect( + VersionConstraint.unionOf([ + VersionRange(min: v072, max: v080), + VersionRange(min: v123, max: v130) + ]).difference(VersionConstraint.unionOf([ + VersionRange(min: v003, max: v010), + VersionRange(min: v080, max: v123), + VersionRange(min: v140) + ])), + equals(VersionConstraint.unionOf([ + VersionRange(min: v072, max: v080), + VersionRange(min: v123, max: v130) + ]))); + }); + + test('removes overlapping portions', () { + expect( + VersionConstraint.unionOf([ + VersionRange(min: v010, max: v080), + VersionRange(min: v123, max: v130) + ]).difference(VersionConstraint.unionOf( + [VersionRange(min: v003, max: v072), VersionRange(min: v124)])), + equals(VersionConstraint.unionOf([ + VersionRange( + min: v072.firstPreRelease, max: v080, includeMin: true), + VersionRange(min: v123, max: v124, includeMax: true) + ]))); + }); + + test('removes multiple portions from the same range', () { + expect( + VersionConstraint.unionOf([ + VersionRange(min: v010, max: v114), + VersionRange(min: v130, max: v200) + ]).difference(VersionConstraint.unionOf([v072, v080])), + equals(VersionConstraint.unionOf([ + VersionRange( + min: v010, max: v072, alwaysIncludeMaxPreRelease: true), + VersionRange( + min: v072, max: v080, alwaysIncludeMaxPreRelease: true), + VersionRange(min: v080, max: v114), + VersionRange(min: v130, max: v200) + ]))); + }); + + test('removes the same range from multiple ranges', () { + expect( + VersionConstraint.unionOf([ + VersionRange(min: v010, max: v072), + VersionRange(min: v080, max: v123), + VersionRange(min: v124, max: v130), + VersionRange(min: v200, max: v234), + VersionRange(min: v250, max: v300) + ]).difference(VersionRange(min: v114, max: v201)), + equals(VersionConstraint.unionOf([ + VersionRange(min: v010, max: v072), + VersionRange(min: v080, max: v114, includeMax: true), + VersionRange( + min: v201.firstPreRelease, max: v234, includeMin: true), + VersionRange(min: v250, max: v300) + ]))); + }); + }); +}