Harvest from pub. Mostly a straight copy, except:
- Reorganized into smaller libraries.
- Added tests for prioritize() and antiprioritize().
- Filled in a few doc comments.
R=nweiz@google.com
Review URL: https://chromiumcodereview.appspot.com//607663002
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7178642
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+packages
+pubspec.lock
+
+# IntelliJ project.
+.idea
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..bf0e1f9
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+# 1.0.0
+
+* Initial release.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1cb1527
--- /dev/null
+++ b/README.md
@@ -0,0 +1,77 @@
+Handles version numbers and version constraints in the same way that [pub][]
+does. The semantics here very closely follow the [Semantic Versioning][semver]
+spec. 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 to not allow pre-release versions of the
+ maximum unless the max is itself a pre-release. 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`.
+
+ * **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.
+
+[pub]: http://pub.dartlang.org/
+[semver]: http://semver.org/
diff --git a/lib/pub_semver.dart b/lib/pub_semver.dart
new file mode 100644
index 0000000..436e226
--- /dev/null
+++ b/lib/pub_semver.dart
@@ -0,0 +1,9 @@
+// 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.
+
+library pub_semver;
+
+export 'src/version.dart';
+export 'src/version_constraint.dart';
+export 'src/version_range.dart';
diff --git a/lib/src/patterns.dart b/lib/src/patterns.dart
new file mode 100644
index 0000000..4a22618
--- /dev/null
+++ b/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.
+
+library pub_semver.src.patterns;
+
+/// Regex that matches a version number at the beginning of a string.
+final START_VERSION = new 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 [START_VERSION] but matches the entire string.
+final COMPLETE_VERSION = new RegExp(START_VERSION.pattern + r'$');
+
+/// Parses a comparison operator ("<", ">", "<=", or ">=") at the beginning of
+/// a string.
+final START_COMPARISON = new RegExp(r"^[<>]=?");
diff --git a/lib/src/version.dart b/lib/src/version.dart
new file mode 100644
index 0000000..866d89c
--- /dev/null
+++ b/lib/src/version.dart
@@ -0,0 +1,295 @@
+// 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.
+
+library pub_semver.src.version;
+
+import 'dart:math';
+
+import 'package:collection/equality.dart';
+
+import 'patterns.dart';
+import 'version_constraint.dart';
+import 'version_range.dart';
+
+/// The equality operator to use for comparing version components.
+final _equality = const IterableEquality();
+
+/// A parsed semantic version number.
+class Version implements Comparable<Version>, VersionConstraint {
+ /// No released version: i.e. "0.0.0".
+ static Version get none => new 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 [proiritize], 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;
+
+ 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 new ArgumentError(
+ 'Major version must be non-negative.');
+ if (minor < 0) throw new ArgumentError(
+ 'Minor version must be non-negative.');
+ if (patch < 0) throw new 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 new Version._(major, minor, patch, pre, build, text);
+ }
+
+ /// Creates a new [Version] by parsing [text].
+ factory Version.parse(String text) {
+ final match = COMPLETE_VERSION.firstMatch(text);
+ if (match == null) {
+ throw new FormatException('Could not parse "$text".');
+ }
+
+ try {
+ int major = int.parse(match[1]);
+ int minor = int.parse(match[2]);
+ int patch = int.parse(match[3]);
+
+ String preRelease = match[5];
+ String build = match[8];
+
+ return new Version._(major, minor, patch, preRelease, build, text);
+ } on FormatException catch (ex) {
+ throw new FormatException('Could not parse "$text".');
+ }
+ }
+
+ /// Returns the primary version out of a list of candidates.
+ ///
+ /// This is the highest-numbered stable (non-prerelease) version. If there
+ /// are no stable versions, it's just the highest-numbered version.
+ static Version primary(List<Version> versions) {
+ var primary;
+ for (var version in versions) {
+ if (primary == null || (!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) {
+ return text.split('.').map((part) {
+ try {
+ return int.parse(part);
+ } on FormatException catch (ex) {
+ // Not a number.
+ return part;
+ }
+ }).toList();
+ }
+
+ bool operator ==(other) {
+ if (other is! Version) return false;
+ return major == other.major && minor == other.minor &&
+ patch == other.patch &&
+ _equality.equals(preRelease, other.preRelease) &&
+ _equality.equals(build, other.build);
+ }
+
+ 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;
+
+ bool get isAny => false;
+ 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 new Version(major, minor, patch);
+ }
+
+ return new Version(major + 1, 0, 0);
+ }
+
+ /// 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 new Version(major, minor, patch);
+ }
+
+ return new Version(major, minor + 1, 0);
+ }
+
+ /// 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 new Version(major, minor, patch);
+ }
+
+ return new Version(major, minor, patch + 1);
+ }
+
+ /// Tests if [other] matches this version exactly.
+ bool allows(Version other) => this == other;
+
+ VersionConstraint intersect(VersionConstraint other) {
+ if (other.isEmpty) return other;
+
+ // Intersect a version and a range.
+ if (other is VersionRange) return other.intersect(this);
+
+ // Intersecting two versions only works if they are the same.
+ if (other is Version) {
+ return this == other ? this : VersionConstraint.empty;
+ }
+
+ throw new ArgumentError(
+ 'Unknown VersionConstraint type $other.');
+ }
+
+ int compareTo(Version other) {
+ 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);
+ }
+
+ String toString() => _text;
+
+ /// 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 < 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.compareTo(bPart);
+ }
+ }
+ }
+
+ // The lists are entirely equal.
+ return 0;
+ }
+}
diff --git a/lib/src/version_constraint.dart b/lib/src/version_constraint.dart
new file mode 100644
index 0000000..499892e
--- /dev/null
+++ b/lib/src/version_constraint.dart
@@ -0,0 +1,151 @@
+// 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.
+
+library pub_semver.src.version_constraint;
+
+import 'patterns.dart';
+import 'version.dart';
+import 'version_range.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 = new VersionRange();
+
+ /// A [VersionConstraint] that allows no versions -- the empty set.
+ static VersionConstraint empty = const _EmptyVersion();
+
+ /// Parses a version constraint.
+ ///
+ /// This string is either "any" or 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
+ /// 1.2.3-alpha
+ /// <=5.1.4
+ /// >2.0.4 <= 2.4.6
+ factory VersionConstraint.parse(String text) {
+ // Handle the "any" constraint.
+ if (text.trim() == "any") return new VersionRange();
+
+ var originalText = text;
+ var constraints = [];
+
+ skipWhitespace() {
+ text = text.trim();
+ }
+
+ // Try to parse and consume a version number.
+ matchVersion() {
+ var version = START_VERSION.firstMatch(text);
+ if (version == null) return null;
+
+ text = text.substring(version.end);
+ return new Version.parse(version[0]);
+ }
+
+ // Try to parse and consume a comparison operator followed by a version.
+ matchComparison() {
+ var comparison = START_COMPARISON.firstMatch(text);
+ if (comparison == null) return null;
+
+ var op = comparison[0];
+ text = text.substring(comparison.end);
+ skipWhitespace();
+
+ var version = matchVersion();
+ if (version == null) {
+ throw new FormatException('Expected version number after "$op" in '
+ '"$originalText", got "$text".');
+ }
+
+ switch (op) {
+ case '<=': return new VersionRange(max: version, includeMax: true);
+ case '<': return new VersionRange(max: version, includeMax: false);
+ case '>=': return new VersionRange(min: version, includeMin: true);
+ case '>': return new VersionRange(min: version, includeMin: false);
+ }
+ throw "Unreachable.";
+ }
+
+ while (true) {
+ skipWhitespace();
+ if (text.isEmpty) break;
+
+ var version = matchVersion();
+ if (version != null) {
+ constraints.add(version);
+ continue;
+ }
+
+ var comparison = matchComparison();
+ if (comparison != null) {
+ constraints.add(comparison);
+ continue;
+ }
+
+ // If we got here, we couldn't parse the remaining string.
+ throw new FormatException('Could not parse version "$originalText". '
+ 'Unknown text at "$text".');
+ }
+
+ if (constraints.isEmpty) {
+ throw new FormatException('Cannot parse an empty string.');
+ }
+
+ return new VersionConstraint.intersection(constraints);
+ }
+
+ /// 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<VersionConstraint> constraints) {
+ var constraint = new VersionRange();
+ for (var other in constraints) {
+ constraint = constraint.intersect(other);
+ }
+ return constraint;
+ }
+
+ /// 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);
+
+ /// Creates a new [VersionConstraint] that only allows [Version]s allowed by
+ /// both this and [other].
+ VersionConstraint intersect(VersionConstraint other);
+}
+
+class _EmptyVersion implements VersionConstraint {
+ const _EmptyVersion();
+
+ bool get isEmpty => true;
+ bool get isAny => false;
+ bool allows(Version other) => false;
+ VersionConstraint intersect(VersionConstraint other) => this;
+ String toString() => '<empty>';
+}
diff --git a/lib/src/version_range.dart b/lib/src/version_range.dart
new file mode 100644
index 0000000..7020077
--- /dev/null
+++ b/lib/src/version_range.dart
@@ -0,0 +1,179 @@
+// 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.
+
+library pub_semver.src.version_range;
+
+import 'version.dart';
+import 'version_constraint.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`.
+class VersionRange implements 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.
+ VersionRange({this.min, this.max,
+ this.includeMin: false, this.includeMax: false}) {
+ if (min != null && max != null && min > max) {
+ throw new ArgumentError(
+ 'Minimum version ("$min") must be less than maximum ("$max").');
+ }
+ }
+
+ bool operator ==(other) {
+ if (other is! VersionRange) return false;
+
+ return min == other.min &&
+ max == other.max &&
+ includeMin == other.includeMin &&
+ includeMax == other.includeMax;
+ }
+
+ bool get isEmpty => false;
+
+ bool get isAny => min == null && max == null;
+
+ /// Tests if [other] falls within this version range.
+ 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;
+
+ // If the max isn't itself a pre-release, don't allow any pre-release
+ // versions of the max.
+ //
+ // See: https://www.npmjs.org/doc/misc/semver.html
+ if (!includeMax &&
+ !max.isPreRelease && other.isPreRelease &&
+ other.major == max.major && other.minor == max.minor &&
+ other.patch == max.patch) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ VersionConstraint intersect(VersionConstraint other) {
+ if (other.isEmpty) return other;
+
+ // 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.
+ var intersectMin = min;
+ var intersectIncludeMin = includeMin;
+ var intersectMax = max;
+ var intersectIncludeMax = includeMax;
+
+ if (other.min == null) {
+ // Do nothing.
+ } else if (intersectMin == null || intersectMin < other.min) {
+ intersectMin = other.min;
+ intersectIncludeMin = other.includeMin;
+ } else if (intersectMin == other.min && !other.includeMin) {
+ // The edges are the same, but one is exclusive, make it exclusive.
+ intersectIncludeMin = false;
+ }
+
+ if (other.max == null) {
+ // Do nothing.
+ } else if (intersectMax == null || intersectMax > other.max) {
+ intersectMax = other.max;
+ intersectIncludeMax = other.includeMax;
+ } else if (intersectMax == other.max && !other.includeMax) {
+ // The edges are the same, but one is exclusive, make it exclusive.
+ intersectIncludeMax = false;
+ }
+
+ if (intersectMin == null && intersectMax == null) {
+ // Open range.
+ return new VersionRange();
+ }
+
+ // If the range is just a single version.
+ if (intersectMin == intersectMax) {
+ // If both ends are inclusive, allow that version.
+ if (intersectIncludeMin && intersectIncludeMax) return intersectMin;
+
+ // Otherwise, no versions.
+ return VersionConstraint.empty;
+ }
+
+ if (intersectMin != null && intersectMax != null &&
+ intersectMin > intersectMax) {
+ // Non-overlapping ranges, so empty.
+ return VersionConstraint.empty;
+ }
+
+ // If we got here, there is an actual range.
+ return new VersionRange(min: intersectMin, max: intersectMax,
+ includeMin: intersectIncludeMin, includeMax: intersectIncludeMax);
+ }
+
+ throw new ArgumentError('Unknown VersionConstraint type $other.');
+ }
+
+ String toString() {
+ var buffer = new StringBuffer();
+
+ if (min != null) {
+ buffer.write(includeMin ? '>=' : '>');
+ buffer.write(min);
+ }
+
+ if (max != null) {
+ if (min != null) buffer.write(' ');
+ buffer.write(includeMax ? '<=' : '<');
+ buffer.write(max);
+ }
+
+ if (min == null && max == null) buffer.write('any');
+ return buffer.toString();
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..63b09e1
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,13 @@
+name: pub_semver
+version: 1.0.0
+author: Dart Team <misc@dartlang.org>
+description: >
+ Versions and version constraints implementing pub's versioning policy. This
+ is very similar to vanilla semver, with a few corner cases.
+homepage: http://github.com/dart-lang/pub_semver
+dependencies:
+ collection: ">=0.9.0 <2.0.0"
+dev_dependencies:
+ unittest: ">=0.9.0 <0.12.0"
+environment:
+ sdk: ">=1.0.0 <2.0.0"
diff --git a/test/test_all.dart b/test/test_all.dart
new file mode 100644
index 0000000..533dde8
--- /dev/null
+++ b/test/test_all.dart
@@ -0,0 +1,17 @@
+// 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.
+
+library pub_semver.test.test_all;
+
+import 'package:unittest/unittest.dart';
+
+import 'version_constraint_test.dart' as version_constraint_test;
+import 'version_range_test.dart' as version_range_test;
+import 'version_test.dart' as version_test;
+
+main() {
+ group('Version', version_test.main);
+ group('VersionConstraint', version_constraint_test.main);
+ group('VersionRange', version_range_test.main);
+}
diff --git a/test/utils.dart b/test/utils.dart
new file mode 100644
index 0000000..da89683
--- /dev/null
+++ b/test/utils.dart
@@ -0,0 +1,96 @@
+// 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.
+
+library pub_semver.test.utils;
+
+import 'package:unittest/unittest.dart';
+
+import 'package:pub_semver/pub_semver.dart';
+
+/// Some stock example versions to use in tests.
+final v114 = new Version.parse('1.1.4');
+final v123 = new Version.parse('1.2.3');
+final v124 = new Version.parse('1.2.4');
+final v130 = new Version.parse('1.3.0');
+final v140 = new Version.parse('1.4.0');
+final v200 = new Version.parse('2.0.0');
+final v201 = new Version.parse('2.0.1');
+final v234 = new Version.parse('2.3.4');
+final v250 = new Version.parse('2.5.0');
+final v300 = new Version.parse('3.0.0');
+
+/// A [Matcher] that tests if a [VersionConstraint] allows or does not allow a
+/// given list of [Version]s.
+class _VersionConstraintMatcher implements Matcher {
+ final List<Version> _expected;
+ final bool _allow;
+
+ _VersionConstraintMatcher(this._expected, this._allow);
+
+ bool matches(item, Map matchState) => (item is VersionConstraint) &&
+ _expected.every((version) => item.allows(version) == _allow);
+
+ Description describe(Description description) {
+ description.addAll(' ${_allow ? "allows" : "does not allow"} versions ',
+ ', ', '', _expected);
+ return description;
+ }
+
+ Description describeMismatch(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 new _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 new _VersionConstraintMatcher(versions, false);
+}
+
+List<Version> _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;
+}
\ No newline at end of file
diff --git a/test/version_constraint_test.dart b/test/version_constraint_test.dart
new file mode 100644
index 0000000..df8c099
--- /dev/null
+++ b/test/version_constraint_test.dart
@@ -0,0 +1,143 @@
+// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+library pub_semver.test.version_constraint_test;
+
+import 'package:unittest/unittest.dart';
+
+import 'package:pub_semver/pub_semver.dart';
+
+import 'utils.dart';
+
+main() {
+ test('any', () {
+ expect(VersionConstraint.any.isAny, isTrue);
+ expect(VersionConstraint.any, allows(
+ new Version.parse('0.0.0-blah'),
+ new Version.parse('1.2.3'),
+ new Version.parse('12345.678.90')));
+ });
+
+ test('empty', () {
+ expect(VersionConstraint.empty.isEmpty, isTrue);
+ expect(VersionConstraint.empty.isAny, isFalse);
+ expect(VersionConstraint.empty, doesNotAllow(
+ new Version.parse('0.0.0-blah'),
+ new Version.parse('1.2.3'),
+ new Version.parse('12345.678.90')));
+ });
+
+ group('parse()', () {
+ test('parses an exact version', () {
+ var constraint = new VersionConstraint.parse('1.2.3-alpha');
+
+ expect(constraint is Version, isTrue);
+ expect(constraint, equals(new Version(1, 2, 3, pre: 'alpha')));
+ });
+
+ test('parses "any"', () {
+ var constraint = new VersionConstraint.parse('any');
+
+ expect(constraint is VersionConstraint, isTrue);
+ expect(constraint, allows(
+ new Version.parse('0.0.0'),
+ new Version.parse('1.2.3'),
+ new Version.parse('12345.678.90')));
+ });
+
+ test('parses a ">" minimum version', () {
+ var constraint = new VersionConstraint.parse('>1.2.3');
+
+ expect(constraint, allows(
+ new Version.parse('1.2.3+foo'),
+ new Version.parse('1.2.4')));
+ expect(constraint, doesNotAllow(
+ new Version.parse('1.2.1'),
+ new Version.parse('1.2.3-build'),
+ new Version.parse('1.2.3')));
+ });
+
+ test('parses a ">=" minimum version', () {
+ var constraint = new VersionConstraint.parse('>=1.2.3');
+
+ expect(constraint, allows(
+ new Version.parse('1.2.3'),
+ new Version.parse('1.2.3+foo'),
+ new Version.parse('1.2.4')));
+ expect(constraint, doesNotAllow(
+ new Version.parse('1.2.1'),
+ new Version.parse('1.2.3-build')));
+ });
+
+ test('parses a "<" maximum version', () {
+ var constraint = new VersionConstraint.parse('<1.2.3');
+
+ expect(constraint, allows(
+ new Version.parse('1.2.1'),
+ new Version.parse('1.2.2+foo')));
+ expect(constraint, doesNotAllow(
+ new Version.parse('1.2.3'),
+ new Version.parse('1.2.3+foo'),
+ new Version.parse('1.2.4')));
+ });
+
+ test('parses a "<=" maximum version', () {
+ var constraint = new VersionConstraint.parse('<=1.2.3');
+
+ expect(constraint, allows(
+ new Version.parse('1.2.1'),
+ new Version.parse('1.2.3-build'),
+ new Version.parse('1.2.3')));
+ expect(constraint, doesNotAllow(
+ new Version.parse('1.2.3+foo'),
+ new Version.parse('1.2.4')));
+ });
+
+ test('parses a series of space-separated constraints', () {
+ var constraint = new VersionConstraint.parse('>1.0.0 >=1.2.3 <1.3.0');
+
+ expect(constraint, allows(
+ new Version.parse('1.2.3'),
+ new Version.parse('1.2.5')));
+ expect(constraint, doesNotAllow(
+ new Version.parse('1.2.3-pre'),
+ new Version.parse('1.3.0'),
+ new Version.parse('3.4.5')));
+ });
+
+ test('ignores whitespace around operators', () {
+ var constraint = new VersionConstraint.parse(' >1.0.0>=1.2.3 < 1.3.0');
+
+ expect(constraint, allows(
+ new Version.parse('1.2.3'),
+ new Version.parse('1.2.5')));
+ expect(constraint, doesNotAllow(
+ new Version.parse('1.2.3-pre'),
+ new Version.parse('1.3.0'),
+ new Version.parse('3.4.5')));
+ });
+
+ test('does not allow "any" to be mixed with other constraints', () {
+ expect(() => new VersionConstraint.parse('any 1.0.0'),
+ throwsFormatException);
+ });
+
+ test('throws FormatException on a bad string', () {
+ var bad = [
+ "", " ", // Empty string.
+ "foo", // Bad text.
+ ">foo", // Bad text after operator.
+ "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(() => new VersionConstraint.parse(text),
+ throwsFormatException);
+ }
+ });
+ });
+}
diff --git a/test/version_range_test.dart b/test/version_range_test.dart
new file mode 100644
index 0000000..388917f
--- /dev/null
+++ b/test/version_range_test.dart
@@ -0,0 +1,202 @@
+// 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.
+
+library pub_semver.test.version_range_test;
+
+import 'package:unittest/unittest.dart';
+
+import 'package:pub_semver/pub_semver.dart';
+
+import 'utils.dart';
+
+main() {
+ group('constructor', () {
+ test('takes a min and max', () {
+ var range = new VersionRange(min: v123, max: v124);
+ expect(range.isAny, isFalse);
+ expect(range.min, equals(v123));
+ expect(range.max, equals(v124));
+ });
+
+ test('allows omitting max', () {
+ var range = new VersionRange(min: v123);
+ expect(range.isAny, isFalse);
+ expect(range.min, equals(v123));
+ expect(range.max, isNull);
+ });
+
+ test('allows omitting min and max', () {
+ var range = new VersionRange();
+ expect(range.isAny, isTrue);
+ expect(range.min, isNull);
+ expect(range.max, isNull);
+ });
+
+ test('takes includeMin', () {
+ var range = new VersionRange(min: v123, includeMin: true);
+ expect(range.includeMin, isTrue);
+ });
+
+ test('includeMin defaults to false if omitted', () {
+ var range = new VersionRange(min: v123);
+ expect(range.includeMin, isFalse);
+ });
+
+ test('takes includeMax', () {
+ var range = new VersionRange(max: v123, includeMax: true);
+ expect(range.includeMax, isTrue);
+ });
+
+ test('includeMax defaults to false if omitted', () {
+ var range = new VersionRange(max: v123);
+ expect(range.includeMax, isFalse);
+ });
+
+ test('throws if min > max', () {
+ expect(() => new VersionRange(min: v124, max: v123), throwsArgumentError);
+ });
+ });
+
+ group('allows()', () {
+ test('version must be greater than min', () {
+ var range = new VersionRange(min: v123);
+
+ expect(range, allows(
+ new Version.parse('1.3.3'),
+ new Version.parse('2.3.3')));
+ expect(range, doesNotAllow(
+ new Version.parse('1.2.2'),
+ new Version.parse('1.2.3')));
+ });
+
+ test('version must be min or greater if includeMin', () {
+ var range = new VersionRange(min: v123, includeMin: true);
+
+ expect(range, allows(
+ new Version.parse('1.2.3'),
+ new Version.parse('1.3.3'),
+ new Version.parse('2.3.3')));
+ expect(range, doesNotAllow(new Version.parse('1.2.2')));
+ });
+
+ test('pre-release versions of inclusive min are excluded', () {
+ var range = new VersionRange(min: v123, includeMin: true);
+
+ expect(range, allows(new Version.parse('1.2.4-dev')));
+ expect(range, doesNotAllow(new Version.parse('1.2.3-dev')));
+ });
+
+ test('version must be less than max', () {
+ var range = new VersionRange(max: v234);
+
+ expect(range, allows(new Version.parse('2.3.3')));
+ expect(range, doesNotAllow(
+ new Version.parse('2.3.4'),
+ new Version.parse('2.4.3')));
+ });
+
+ test('pre-release versions of non-pre-release max are excluded', () {
+ var range = new VersionRange(max: v234);
+
+ expect(range, allows(new Version.parse('2.3.3')));
+ expect(range, doesNotAllow(
+ new Version.parse('2.3.4-dev'),
+ new Version.parse('2.3.4')));
+ });
+
+ test('pre-release versions of pre-release max are included', () {
+ var range = new VersionRange(max: new Version.parse('2.3.4-dev.2'));
+
+ expect(range, allows(
+ new Version.parse('2.3.4-dev.1')));
+ expect(range, doesNotAllow(
+ new Version.parse('2.3.4-dev.2'),
+ new Version.parse('2.3.4-dev.3')));
+ });
+
+ test('version must be max or less if includeMax', () {
+ var range = new VersionRange(min: v123, max: v234, includeMax: true);
+
+ expect(range, allows(
+ new Version.parse('2.3.3'),
+ new Version.parse('2.3.4'),
+ // Pre-releases of the max are allowed.
+ new Version.parse('2.3.4-dev')));
+ expect(range, doesNotAllow(new Version.parse('2.4.3')));
+ });
+
+ test('has no min if one was not set', () {
+ var range = new VersionRange(max: v123);
+
+ expect(range, allows(new Version.parse('0.0.0')));
+ expect(range, doesNotAllow(new Version.parse('1.2.3')));
+ });
+
+ test('has no max if one was not set', () {
+ var range = new VersionRange(min: v123);
+
+ expect(range, allows(
+ new Version.parse('1.3.3'),
+ new Version.parse('999.3.3')));
+ expect(range, doesNotAllow(new Version.parse('1.2.3')));
+ });
+
+ test('allows any version if there is no min or max', () {
+ var range = new VersionRange();
+
+ expect(range, allows(
+ new Version.parse('0.0.0'),
+ new Version.parse('999.99.9')));
+ });
+ });
+
+ group('intersect()', () {
+ test('two overlapping ranges', () {
+ var a = new VersionRange(min: v123, max: v250);
+ var b = new VersionRange(min: v200, max: v300);
+ var intersect = a.intersect(b);
+ expect(intersect.min, equals(v200));
+ expect(intersect.max, equals(v250));
+ expect(intersect.includeMin, isFalse);
+ expect(intersect.includeMax, isFalse);
+ });
+
+ test('a non-overlapping range allows no versions', () {
+ var a = new VersionRange(min: v114, max: v124);
+ var b = new VersionRange(min: v200, max: v250);
+ expect(a.intersect(b).isEmpty, isTrue);
+ });
+
+ test('adjacent ranges allow no versions if exclusive', () {
+ var a = new VersionRange(min: v114, max: v124, includeMax: false);
+ var b = new VersionRange(min: v124, max: v200, includeMin: true);
+ expect(a.intersect(b).isEmpty, isTrue);
+ });
+
+ test('adjacent ranges allow version if inclusive', () {
+ var a = new VersionRange(min: v114, max: v124, includeMax: true);
+ var b = new VersionRange(min: v124, max: v200, includeMin: true);
+ expect(a.intersect(b), equals(v124));
+ });
+
+ test('with an open range', () {
+ var open = new VersionRange();
+ var a = new 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(new VersionRange(min: v114, max: v124).intersect(v123),
+ equals(v123));
+ expect(new VersionRange(min: v123, max: v124).intersect(v114).isEmpty,
+ isTrue);
+ });
+ });
+
+ test('isEmpty', () {
+ expect(new VersionRange().isEmpty, isFalse);
+ expect(new VersionRange(min: v123, max: v124).isEmpty, isFalse);
+ });
+}
diff --git a/test/version_test.dart b/test/version_test.dart
new file mode 100644
index 0000000..b271ecc
--- /dev/null
+++ b/test/version_test.dart
@@ -0,0 +1,248 @@
+// 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.
+
+library pub_semver.test.version_test;
+
+import 'package:unittest/unittest.dart';
+
+import 'package:pub_semver/pub_semver.dart';
+
+import 'utils.dart';
+
+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 = new Version.parse(versions[i]);
+ var b = new 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 = new Version.parse(versions[i]);
+ var b = new Version.parse(versions[j]);
+ expect(Version.antiprioritize(a, b), equals(i.compareTo(j)));
+ }
+ }
+ });
+
+ group('constructor', () {
+ test('throws on negative numbers', () {
+ expect(() => new Version(-1, 1, 1), throwsArgumentError);
+ expect(() => new Version(1, -1, 1), throwsArgumentError);
+ expect(() => new 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 = new Version.parse(versions[i]);
+ var b = new 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 = new Version.parse(versions[i]);
+ var b = new 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(new Version.parse('01.2.3'), equals(new Version.parse('1.2.3')));
+ expect(new Version.parse('1.02.3'), equals(new Version.parse('1.2.3')));
+ expect(new Version.parse('1.2.03'), equals(new Version.parse('1.2.3')));
+ expect(new Version.parse('1.2.3-01'),
+ equals(new Version.parse('1.2.3-1')));
+ expect(new Version.parse('1.2.3+01'),
+ equals(new Version.parse('1.2.3+1')));
+ });
+ });
+
+ test('allows()', () {
+ expect(v123, allows(v123));
+ expect(v123, doesNotAllow(
+ new Version.parse('2.2.3'),
+ new Version.parse('1.3.3'),
+ new Version.parse('1.2.4'),
+ new Version.parse('1.2.3-dev'),
+ new Version.parse('1.2.3+build')));
+ });
+
+ 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(new VersionRange(min: v114, max: v124)),
+ equals(v123));
+
+ // Intersecting a range allows no versions if the range doesn't allow it.
+ expect(v114.intersect(new VersionRange(min: v123, max: v124)).isEmpty,
+ isTrue);
+ });
+
+ 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(new Version.parse('1.2.3-dev').nextMajor, equals(v200));
+
+ // Just removes it if on a major version.
+ expect(new Version.parse('2.0.0-dev').nextMajor, equals(v200));
+
+ // Strips build suffix.
+ expect(new 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(new Version.parse('1.2.3-dev').nextMinor, equals(v130));
+
+ // Just removes it if on a minor version.
+ expect(new Version.parse('1.3.0-dev').nextMinor, equals(v130));
+
+ // Strips build suffix.
+ expect(new 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(new Version.parse('1.2.4-dev').nextPatch, equals(v124));
+
+ // Strips build suffix.
+ expect(new Version.parse('1.2.3+patch').nextPatch, equals(v124));
+ });
+
+ test('parse()', () {
+ expect(new Version.parse('0.0.0'), equals(new Version(0, 0, 0)));
+ expect(new Version.parse('12.34.56'), equals(new Version(12, 34, 56)));
+
+ expect(new Version.parse('1.2.3-alpha.1'),
+ equals(new Version(1, 2, 3, pre: 'alpha.1')));
+ expect(new Version.parse('1.2.3-x.7.z-92'),
+ equals(new Version(1, 2, 3, pre: 'x.7.z-92')));
+
+ expect(new Version.parse('1.2.3+build.1'),
+ equals(new Version(1, 2, 3, build: 'build.1')));
+ expect(new Version.parse('1.2.3+x.7.z-92'),
+ equals(new Version(1, 2, 3, build: 'x.7.z-92')));
+
+ expect(new Version.parse('1.0.0-rc-1+build-1'),
+ equals(new Version(1, 0, 0, pre: 'rc-1', build: 'build-1')));
+
+ expect(() => new Version.parse('1.0'), throwsFormatException);
+ expect(() => new Version.parse('1.2.3.4'), throwsFormatException);
+ expect(() => new Version.parse('1234'), throwsFormatException);
+ expect(() => new Version.parse('-2.3.4'), throwsFormatException);
+ expect(() => new Version.parse('1.3-pre'), throwsFormatException);
+ expect(() => new Version.parse('1.3+build'), throwsFormatException);
+ expect(() => new Version.parse('1.3+bu?!3ild'), throwsFormatException);
+ });
+
+ group('toString()', () {
+ test('returns the version string', () {
+ expect(new Version(0, 0, 0).toString(), equals('0.0.0'));
+ expect(new Version(12, 34, 56).toString(), equals('12.34.56'));
+
+ expect(new Version(1, 2, 3, pre: 'alpha.1').toString(),
+ equals('1.2.3-alpha.1'));
+ expect(new Version(1, 2, 3, pre: 'x.7.z-92').toString(),
+ equals('1.2.3-x.7.z-92'));
+
+ expect(new Version(1, 2, 3, build: 'build.1').toString(),
+ equals('1.2.3+build.1'));
+ expect(new Version(1, 2, 3, pre: 'pre', build: 'bui').toString(),
+ equals('1.2.3-pre+bui'));
+ });
+
+ test('preserves leading zeroes', () {
+ expect(new Version.parse('001.02.0003-01.dev+pre.002').toString(),
+ equals('001.02.0003-01.dev+pre.002'));
+ });
+ });
+}