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/pkgs/pub_semver/.gitignore b/pkgs/pub_semver/.gitignore new file mode 100644 index 0000000..7178642 --- /dev/null +++ b/pkgs/pub_semver/.gitignore
@@ -0,0 +1,5 @@ +packages +pubspec.lock + +# IntelliJ project. +.idea
diff --git a/pkgs/pub_semver/CHANGELOG.md b/pkgs/pub_semver/CHANGELOG.md new file mode 100644 index 0000000..bf0e1f9 --- /dev/null +++ b/pkgs/pub_semver/CHANGELOG.md
@@ -0,0 +1,3 @@ +# 1.0.0 + +* Initial release.
diff --git a/pkgs/pub_semver/README.md b/pkgs/pub_semver/README.md new file mode 100644 index 0000000..1cb1527 --- /dev/null +++ b/pkgs/pub_semver/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/pkgs/pub_semver/lib/pub_semver.dart b/pkgs/pub_semver/lib/pub_semver.dart new file mode 100644 index 0000000..436e226 --- /dev/null +++ b/pkgs/pub_semver/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/pkgs/pub_semver/lib/src/patterns.dart b/pkgs/pub_semver/lib/src/patterns.dart new file mode 100644 index 0000000..4a22618 --- /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. + +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/pkgs/pub_semver/lib/src/version.dart b/pkgs/pub_semver/lib/src/version.dart new file mode 100644 index 0000000..866d89c --- /dev/null +++ b/pkgs/pub_semver/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/pkgs/pub_semver/lib/src/version_constraint.dart b/pkgs/pub_semver/lib/src/version_constraint.dart new file mode 100644 index 0000000..499892e --- /dev/null +++ b/pkgs/pub_semver/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/pkgs/pub_semver/lib/src/version_range.dart b/pkgs/pub_semver/lib/src/version_range.dart new file mode 100644 index 0000000..7020077 --- /dev/null +++ b/pkgs/pub_semver/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/pkgs/pub_semver/pubspec.yaml b/pkgs/pub_semver/pubspec.yaml new file mode 100644 index 0000000..63b09e1 --- /dev/null +++ b/pkgs/pub_semver/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/pkgs/pub_semver/test/test_all.dart b/pkgs/pub_semver/test/test_all.dart new file mode 100644 index 0000000..533dde8 --- /dev/null +++ b/pkgs/pub_semver/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/pkgs/pub_semver/test/utils.dart b/pkgs/pub_semver/test/utils.dart new file mode 100644 index 0000000..da89683 --- /dev/null +++ b/pkgs/pub_semver/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/pkgs/pub_semver/test/version_constraint_test.dart b/pkgs/pub_semver/test/version_constraint_test.dart new file mode 100644 index 0000000..df8c099 --- /dev/null +++ b/pkgs/pub_semver/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/pkgs/pub_semver/test/version_range_test.dart b/pkgs/pub_semver/test/version_range_test.dart new file mode 100644 index 0000000..388917f --- /dev/null +++ b/pkgs/pub_semver/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/pkgs/pub_semver/test/version_test.dart b/pkgs/pub_semver/test/version_test.dart new file mode 100644 index 0000000..b271ecc --- /dev/null +++ b/pkgs/pub_semver/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')); + }); + }); +}