瀏覽代碼

Merge pull request #2596 from Seldaek/spaceyAND

SemVer 2.0 and npm semver support
Jordi Boggiano 10 年之前
父節點
當前提交
72a35263ec

+ 16 - 8
doc/01-basic-usage.md

@@ -72,17 +72,19 @@ means any version in the `1.0` development branch. It would match `1.0.0`,
 
 
 Version constraints can be specified in a few different ways.
 Version constraints can be specified in a few different ways.
 
 
-Name           | Example                                                            | Description
--------------- | ------------------------------------------------------------------ | -----------
-Exact version  | `1.0.2`                                                            | You can specify the exact version of a package.
-Range          | `>=1.0` `>=1.0,<2.0` <code>&gt;=1.0,&lt;1.1 &#124; &gt;=1.2</code> | By using comparison operators you can specify ranges of valid versions. Valid operators are `>`, `>=`, `<`, `<=`, `!=`. <br />You can define multiple ranges. Ranges separated by a comma (`,`) will be treated as a **logical AND**. A pipe (<code>&#124;</code>) will be treated as a **logical OR**. AND has higher precedence than OR.
-Wildcard       | `1.0.*`                                                            | You can specify a pattern with a `*` wildcard. `1.0.*` is the equivalent of `>=1.0,<1.1`.
-Tilde Operator | `~1.2`                                                             | Very useful for projects that follow semantic versioning. `~1.2` is equivalent to `>=1.2,<2.0`. For more details, read the next section below.
+Name           | Example                                                                  | Description
+-------------- | ------------------------------------------------------------------------ | -----------
+Exact version  | `1.0.2`                                                                  | You can specify the exact version of a package.
+Range          | `>=1.0` `>=1.0 <2.0` <code>&gt;=1.0 &lt;1.1 &#124;&#124; &gt;=1.2</code> | By using comparison operators you can specify ranges of valid versions. Valid operators are `>`, `>=`, `<`, `<=`, `!=`. <br />You can define multiple ranges. Ranges separated by a space (` `) or comma (`,`) will be treated as a **logical AND**. A double pipe (<code>&#124;&#124;</code>) will be treated as a **logical OR**. AND has higher precedence than OR.
+Hyphen Range   | `1.0 - 2.0`                                                              | Inclusive set of versions. Partial versions on the right include are completed with a wildcard. For example `1.0 - 2.0` is equivalent to `>=1.0.0 <2.1` as the `2.0` becomes `2.0.*`. On the other hand `1.0.0 - 2.1.0` is equivalent to `>=1.0.0 <=2.1.0`.
+Wildcard       | `1.0.*`                                                                  | You can specify a pattern with a `*` wildcard. `1.0.*` is the equivalent of `>=1.0 <1.1`.
+Tilde Operator | `~1.2`                                                                   | Very useful for projects that follow semantic versioning. `~1.2` is equivalent to `>=1.2 <2.0`. For more details, read the next section below.
+Caret Operator | `^1.2.3`                                                                 | Very useful for projects that follow semantic versioning. `^1.2.3` is equivalent to `>=1.2.3 <2.0`. For more details, read the next section below.
 
 
-### Next Significant Release (Tilde Operator)
+### Next Significant Release (Tilde and Caret Operators)
 
 
 The `~` operator is best explained by example: `~1.2` is equivalent to
 The `~` operator is best explained by example: `~1.2` is equivalent to
-`>=1.2,<2.0`, while `~1.2.3` is equivalent to `>=1.2.3,<1.3`. As you can see
+`>=1.2 <2.0.0`, while `~1.2.3` is equivalent to `>=1.2.3 <1.3.0`. As you can see
 it is mostly useful for projects respecting [semantic
 it is mostly useful for projects respecting [semantic
 versioning](http://semver.org/). A common usage would be to mark the minimum
 versioning](http://semver.org/). A common usage would be to mark the minimum
 minor version you depend on, like `~1.2` (which allows anything up to, but not
 minor version you depend on, like `~1.2` (which allows anything up to, but not
@@ -90,6 +92,12 @@ including, 2.0). Since in theory there should be no backwards compatibility
 breaks until 2.0, that works well. Another way of looking at it is that using
 breaks until 2.0, that works well. Another way of looking at it is that using
 `~` specifies a minimum version, but allows the last digit specified to go up.
 `~` specifies a minimum version, but allows the last digit specified to go up.
 
 
+The `^` operator behaves very similarly but it sticks closer to semantic
+versioning, and will always allow non-breaking updates. For example `^1.2.3`
+is equivalent to `>=1.2.3 <2.0.0` as none of the releases until 2.0 should
+break backwards compatibility. For pre-1.0 versions it also acts with safety
+in mind and treats `^0.3` as `>=0.3.0 <0.4.0`
+
 > **Note:** Though `2.0-beta.1` is strictly before `2.0`, a version constraint
 > **Note:** Though `2.0-beta.1` is strictly before `2.0`, a version constraint
 > like `~1.2` would not install it. As said above `~1.2` only means the `.2`
 > like `~1.2` would not install it. As said above `~1.2` only means the `.2`
 > can change but the `1.` part is fixed.
 > can change but the `1.` part is fixed.

+ 2 - 2
doc/04-schema.md

@@ -345,10 +345,10 @@ dependencies from being installed.
 Lists packages that conflict with this version of this package. They
 Lists packages that conflict with this version of this package. They
 will not be allowed to be installed together with your package.
 will not be allowed to be installed together with your package.
 
 
-Note that when specifying ranges like `<1.0, >= 1.1` in a `conflict` link,
+Note that when specifying ranges like `<1.0 >=1.1` in a `conflict` link,
 this will state a conflict with all versions that are less than 1.0 *and* equal
 this will state a conflict with all versions that are less than 1.0 *and* equal
 or newer than 1.1 at the same time, which is probably not what you want. You
 or newer than 1.1 at the same time, which is probably not what you want. You
-probably want to go for `<1.0 | >= 1.1` in this case.
+probably want to go for `<1.0 | >=1.1` in this case.
 
 
 #### replace
 #### replace
 
 

+ 72 - 8
src/Composer/Package/Version/VersionParser.php

@@ -103,6 +103,11 @@ class VersionParser
             $version = $match[1];
             $version = $match[1];
         }
         }
 
 
+        // ignore build metadata
+        if (preg_match('{^([^,\s+]+)\+[^\s]+$}', $version, $match)) {
+            $version = $match[1];
+        }
+
         // match master-like branches
         // match master-like branches
         if (preg_match('{^(?:dev-)?(?:master|trunk|default)$}i', $version)) {
         if (preg_match('{^(?:dev-)?(?:master|trunk|default)$}i', $version)) {
             return '9999999-dev';
             return '9999999-dev';
@@ -178,10 +183,10 @@ class VersionParser
             return $this->normalize($name);
             return $this->normalize($name);
         }
         }
 
 
-        if (preg_match('#^v?(\d+)(\.(?:\d+|[x*]))?(\.(?:\d+|[x*]))?(\.(?:\d+|[x*]))?$#i', $name, $matches)) {
+        if (preg_match('#^v?(\d+)(\.(?:\d+|[xX*]))?(\.(?:\d+|[xX*]))?(\.(?:\d+|[xX*]))?$#i', $name, $matches)) {
             $version = '';
             $version = '';
             for ($i = 1; $i < 5; $i++) {
             for ($i = 1; $i < 5; $i++) {
-                $version .= isset($matches[$i]) ? str_replace('*', 'x', $matches[$i]) : '.x';
+                $version .= isset($matches[$i]) ? str_replace(array('*', 'X'), 'x', $matches[$i]) : '.x';
             }
             }
 
 
             return str_replace('x', '9999999', $version).'-dev';
             return str_replace('x', '9999999', $version).'-dev';
@@ -230,11 +235,10 @@ class VersionParser
             $constraints = $match[1];
             $constraints = $match[1];
         }
         }
 
 
-        $orConstraints = preg_split('{\s*\|\s*}', trim($constraints));
+        $orConstraints = preg_split('{\s*\|\|?\s*}', trim($constraints));
         $orGroups = array();
         $orGroups = array();
         foreach ($orConstraints as $constraints) {
         foreach ($orConstraints as $constraints) {
-            $andConstraints = preg_split('{\s*,\s*}', $constraints);
-
+            $andConstraints = preg_split('{(?<!^|as|[=>< ,]) *(?<!-)[, ](?!-) *(?!,|as|$)}', $constraints);
             if (count($andConstraints) > 1) {
             if (count($andConstraints) > 1) {
                 $constraintObjects = array();
                 $constraintObjects = array();
                 foreach ($andConstraints as $constraint) {
                 foreach ($andConstraints as $constraint) {
@@ -273,16 +277,18 @@ class VersionParser
             }
             }
         }
         }
 
 
-        if (preg_match('{^[x*](\.[x*])*$}i', $constraint)) {
+        if (preg_match('{^[xX*](\.[xX*])*$}i', $constraint)) {
             return array(new EmptyConstraint);
             return array(new EmptyConstraint);
         }
         }
 
 
+        $versionRegex = '(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?'.self::$modifierRegex;
+
         // match tilde constraints
         // match tilde constraints
         // like wildcard constraints, unsuffixed tilde constraints say that they must be greater than the previous
         // like wildcard constraints, unsuffixed tilde constraints say that they must be greater than the previous
         // version, to ensure that unstable instances of the current version are allowed.
         // version, to ensure that unstable instances of the current version are allowed.
         // however, if a stability suffix is added to the constraint, then a >= match on the current version is
         // however, if a stability suffix is added to the constraint, then a >= match on the current version is
         // used instead
         // used instead
-        if (preg_match('{^~>?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?'.self::$modifierRegex.'?$}i', $constraint, $matches)) {
+        if (preg_match('{^~>?'.$versionRegex.'$}i', $constraint, $matches)) {
             if (substr($constraint, 0, 2) === '~>') {
             if (substr($constraint, 0, 2) === '~>') {
                 throw new \UnexpectedValueException(
                 throw new \UnexpectedValueException(
                     'Could not parse version constraint '.$constraint.': '.
                     'Could not parse version constraint '.$constraint.': '.
@@ -329,8 +335,39 @@ class VersionParser
             );
             );
         }
         }
 
 
+        // match caret constraints
+        if (preg_match('{^\^'.$versionRegex.'($)}i', $constraint, $matches)) {
+            // Work out which position in the version we are operating at
+            if ('0' !== $matches[1] || '' === $matches[2]) {
+                $position = 1;
+            } elseif ('0' !== $matches[2] || '' === $matches[3]) {
+                $position = 2;
+            } else {
+                $position = 3;
+            }
+
+            // Calculate the stability suffix
+            $stabilitySuffix = '';
+            if (empty($matches[5]) && empty($matches[7])) {
+                $stabilitySuffix .= '-dev';
+            }
+
+            $lowVersion = $this->normalize(substr($constraint . $stabilitySuffix, 1));
+            $lowerBound = new VersionConstraint('>=', $lowVersion);
+
+            // For upper bound, we increment the position of one more significance,
+            // but highPosition = 0 would be illegal
+            $highVersion = $this->manipulateVersionString($matches, $position, 1) . '-dev';
+            $upperBound = new VersionConstraint('<', $highVersion);
+
+            return array(
+                $lowerBound,
+                $upperBound
+            );
+        }
+
         // match wildcard constraints
         // match wildcard constraints
-        if (preg_match('{^(\d+)(?:\.(\d+))?(?:\.(\d+))?\.[x*]$}', $constraint, $matches)) {
+        if (preg_match('{^(\d+)(?:\.(\d+))?(?:\.(\d+))?\.[xX*]$}', $constraint, $matches)) {
             if (isset($matches[3]) && '' !== $matches[3]) {
             if (isset($matches[3]) && '' !== $matches[3]) {
                 $position = 3;
                 $position = 3;
             } elseif (isset($matches[2]) && '' !== $matches[2]) {
             } elseif (isset($matches[2]) && '' !== $matches[2]) {
@@ -352,6 +389,33 @@ class VersionParser
             );
             );
         }
         }
 
 
+        // match hyphen constraints
+        if (preg_match('{^(?P<from>'.$versionRegex.') +- +(?P<to>'.$versionRegex.')($)}i', $constraint, $matches)) {
+            // Calculate the stability suffix
+            $lowStabilitySuffix = '';
+            if (empty($matches[6]) && empty($matches[8])) {
+                $lowStabilitySuffix = '-dev';
+            }
+
+            $lowVersion = $this->normalize($matches['from']);
+            $lowerBound = new VersionConstraint('>=', $lowVersion . $lowStabilitySuffix);
+
+            $highVersion = $matches[10];
+            if ((!empty($matches[11]) && !empty($matches[12])) || !empty($matches[14]) || !empty($matches[16])) {
+                $highVersion = $this->normalize($matches['to']);
+                $upperBound = new VersionConstraint('<=', $highVersion);
+            } else {
+                $highMatch = array('', $matches[10], $matches[11], $matches[12], $matches[13]);
+                $highVersion = $this->manipulateVersionString($highMatch, empty($matches[11]) ? 1 : 2, 1) . '-dev';
+                $upperBound = new VersionConstraint('<', $highVersion);
+            }
+
+            return array(
+                $lowerBound,
+                $upperBound
+            );
+        }
+
         // match operators constraints
         // match operators constraints
         if (preg_match('{^(<>|!=|>=?|<=?|==?)?\s*(.*)}', $constraint, $matches)) {
         if (preg_match('{^(<>|!=|>=?|<=?|==?)?\s*(.*)}', $constraint, $matches)) {
             try {
             try {

+ 108 - 6
tests/Composer/Test/Package/Version/VersionParserTest.php

@@ -90,6 +90,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
             'forces w.x.y.z/2'   => array('0',                   '0.0.0.0'),
             'forces w.x.y.z/2'   => array('0',                   '0.0.0.0'),
             'parses long'        => array('10.4.13-beta',        '10.4.13.0-beta'),
             'parses long'        => array('10.4.13-beta',        '10.4.13.0-beta'),
             'parses long/2'      => array('10.4.13beta2',        '10.4.13.0-beta2'),
             'parses long/2'      => array('10.4.13beta2',        '10.4.13.0-beta2'),
+            'parses long/semver' => array('10.4.13beta.2',       '10.4.13.0-beta2'),
             'expand shorthand'   => array('10.4.13-b',           '10.4.13.0-beta'),
             'expand shorthand'   => array('10.4.13-b',           '10.4.13.0-beta'),
             'expand shorthand2'  => array('10.4.13-b5',          '10.4.13.0-beta5'),
             'expand shorthand2'  => array('10.4.13-b5',          '10.4.13.0-beta5'),
             'strips leading v'   => array('v1.0.0',              '1.0.0.0'),
             'strips leading v'   => array('v1.0.0',              '1.0.0.0'),
@@ -109,6 +110,10 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
             'parses arbitrary2'  => array('DEV-FOOBAR',          'dev-FOOBAR'),
             'parses arbitrary2'  => array('DEV-FOOBAR',          'dev-FOOBAR'),
             'parses arbitrary3'  => array('dev-feature/foo',     'dev-feature/foo'),
             'parses arbitrary3'  => array('dev-feature/foo',     'dev-feature/foo'),
             'ignores aliases'    => array('dev-master as 1.0.0', '9999999-dev'),
             'ignores aliases'    => array('dev-master as 1.0.0', '9999999-dev'),
+            'semver metadata'    => array('dev-master+foo.bar',  '9999999-dev'),
+            'semver metadata/2'  => array('1.0.0-beta.5+foo',    '1.0.0.0-beta5'),
+            'semver metadata/3'  => array('1.0.0+foo',           '1.0.0.0'),
+            'metadata w/ alias'  => array('1.0.0+foo as 2.0',    '1.0.0.0'),
         );
         );
     }
     }
 
 
@@ -130,6 +135,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
             'invalid type'      => array('1.0.0-meh'),
             'invalid type'      => array('1.0.0-meh'),
             'too many bits'     => array('1.0.0.0.0'),
             'too many bits'     => array('1.0.0.0.0'),
             'non-dev arbitrary' => array('feature-foo'),
             'non-dev arbitrary' => array('feature-foo'),
+            'metadata w/ space' => array('1.0.0+foo bar'),
         );
         );
     }
     }
 
 
@@ -208,7 +214,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
             'match any'         => array('*',           new EmptyConstraint()),
             'match any'         => array('*',           new EmptyConstraint()),
             'match any/2'       => array('*.*',         new EmptyConstraint()),
             'match any/2'       => array('*.*',         new EmptyConstraint()),
             'match any/3'       => array('*.x.*',       new EmptyConstraint()),
             'match any/3'       => array('*.x.*',       new EmptyConstraint()),
-            'match any/4'       => array('x.x.x.*',     new EmptyConstraint()),
+            'match any/4'       => array('x.X.x.*',     new EmptyConstraint()),
             'not equal'         => array('<>1.0.0',     new VersionConstraint('<>', '1.0.0.0')),
             'not equal'         => array('<>1.0.0',     new VersionConstraint('<>', '1.0.0.0')),
             'not equal/2'       => array('!=1.0.0',     new VersionConstraint('!=', '1.0.0.0')),
             'not equal/2'       => array('!=1.0.0',     new VersionConstraint('!=', '1.0.0.0')),
             'greater than'      => array('>1.0.0',      new VersionConstraint('>', '1.0.0.0')),
             'greater than'      => array('>1.0.0',      new VersionConstraint('>', '1.0.0.0')),
@@ -221,6 +227,8 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
             'completes version' => array('=1.0',        new VersionConstraint('=', '1.0.0.0')),
             'completes version' => array('=1.0',        new VersionConstraint('=', '1.0.0.0')),
             'shorthand beta'    => array('1.2.3b5',     new VersionConstraint('=', '1.2.3.0-beta5')),
             'shorthand beta'    => array('1.2.3b5',     new VersionConstraint('=', '1.2.3.0-beta5')),
             'accepts spaces'    => array('>= 1.2.3',    new VersionConstraint('>=', '1.2.3.0')),
             'accepts spaces'    => array('>= 1.2.3',    new VersionConstraint('>=', '1.2.3.0')),
+            'accepts spaces/2'  => array('< 1.2.3',     new VersionConstraint('<', '1.2.3.0-dev')),
+            'accepts spaces/3'  => array('> 1.2.3',     new VersionConstraint('>', '1.2.3.0')),
             'accepts master'    => array('>=dev-master',    new VersionConstraint('>=', '9999999-dev')),
             'accepts master'    => array('>=dev-master',    new VersionConstraint('>=', '9999999-dev')),
             'accepts master/2'  => array('dev-master',      new VersionConstraint('=', '9999999-dev')),
             'accepts master/2'  => array('dev-master',      new VersionConstraint('=', '9999999-dev')),
             'accepts arbitrary' => array('dev-feature-a',   new VersionConstraint('=', 'dev-feature-a')),
             'accepts arbitrary' => array('dev-feature-a',   new VersionConstraint('=', 'dev-feature-a')),
@@ -253,7 +261,7 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
             array('20.*',    new VersionConstraint('>=', '20.0.0.0-dev'), new VersionConstraint('<', '21.0.0.0-dev')),
             array('20.*',    new VersionConstraint('>=', '20.0.0.0-dev'), new VersionConstraint('<', '21.0.0.0-dev')),
             array('2.0.*',   new VersionConstraint('>=', '2.0.0.0-dev'), new VersionConstraint('<', '2.1.0.0-dev')),
             array('2.0.*',   new VersionConstraint('>=', '2.0.0.0-dev'), new VersionConstraint('<', '2.1.0.0-dev')),
             array('2.2.x',   new VersionConstraint('>=', '2.2.0.0-dev'), new VersionConstraint('<', '2.3.0.0-dev')),
             array('2.2.x',   new VersionConstraint('>=', '2.2.0.0-dev'), new VersionConstraint('<', '2.3.0.0-dev')),
-            array('2.10.x',  new VersionConstraint('>=', '2.10.0.0-dev'), new VersionConstraint('<', '2.11.0.0-dev')),
+            array('2.10.X',  new VersionConstraint('>=', '2.10.0.0-dev'), new VersionConstraint('<', '2.11.0.0-dev')),
             array('2.1.3.*', new VersionConstraint('>=', '2.1.3.0-dev'), new VersionConstraint('<', '2.1.4.0-dev')),
             array('2.1.3.*', new VersionConstraint('>=', '2.1.3.0-dev'), new VersionConstraint('<', '2.1.4.0-dev')),
             array('0.*',     null, new VersionConstraint('<', '1.0.0.0-dev')),
             array('0.*',     null, new VersionConstraint('<', '1.0.0.0-dev')),
         );
         );
@@ -291,13 +299,92 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
         );
         );
     }
     }
 
 
-    public function testParseConstraintsMulti()
+    /**
+     * @dataProvider caretConstraints
+     */
+    public function testParseCaretWildcard($input, $min, $max)
+    {
+        $parser = new VersionParser;
+        if ($min) {
+            $expected = new MultiConstraint(array($min, $max));
+        } else {
+            $expected = $max;
+        }
+
+        $this->assertSame((string) $expected, (string) $parser->parseConstraints($input));
+    }
+
+    public function caretConstraints()
+    {
+        return array(
+            array('^1',            new VersionConstraint('>=', '1.0.0.0-dev'), new VersionConstraint('<', '2.0.0.0-dev')),
+            array('^0',            new VersionConstraint('>=', '0.0.0.0-dev'), new VersionConstraint('<', '1.0.0.0-dev')),
+            array('^0.0',          new VersionConstraint('>=', '0.0.0.0-dev'), new VersionConstraint('<', '0.1.0.0-dev')),
+            array('^1.2',          new VersionConstraint('>=', '1.2.0.0-dev'), new VersionConstraint('<', '2.0.0.0-dev')),
+            array('^1.2.3-beta.2', new VersionConstraint('>=', '1.2.3.0-beta2'), new VersionConstraint('<', '2.0.0.0-dev')),
+            array('^1.2.3.4',      new VersionConstraint('>=', '1.2.3.4-dev'), new VersionConstraint('<', '2.0.0.0-dev')),
+            array('^1.2.3',        new VersionConstraint('>=', '1.2.3.0-dev'), new VersionConstraint('<', '2.0.0.0-dev')),
+            array('^0.2.3',        new VersionConstraint('>=', '0.2.3.0-dev'), new VersionConstraint('<', '0.3.0.0-dev')),
+            array('^0.2',          new VersionConstraint('>=', '0.2.0.0-dev'), new VersionConstraint('<', '0.3.0.0-dev')),
+            array('^0.0.3',        new VersionConstraint('>=', '0.0.3.0-dev'), new VersionConstraint('<', '0.0.4.0-dev')),
+            array('^0.0.3-alpha',  new VersionConstraint('>=', '0.0.3.0-alpha'), new VersionConstraint('<', '0.0.4.0-dev')),
+            array('^0.0.3-dev',    new VersionConstraint('>=', '0.0.3.0-dev'), new VersionConstraint('<', '0.0.4.0-dev')),
+        );
+    }
+
+    /**
+     * @dataProvider hyphenConstraints
+     */
+    public function testParseHyphen($input, $min, $max)
+    {
+        $parser = new VersionParser;
+        if ($min) {
+            $expected = new MultiConstraint(array($min, $max));
+        } else {
+            $expected = $max;
+        }
+
+        $this->assertSame((string) $expected, (string) $parser->parseConstraints($input));
+    }
+
+    public function hyphenConstraints()
+    {
+        return array(
+            array('1 - 2',                  new VersionConstraint('>=', '1.0.0.0-dev'),   new VersionConstraint('<',  '3.0.0.0-dev')),
+            array('1.2.3 - 2.3.4.5',        new VersionConstraint('>=', '1.2.3.0-dev'),   new VersionConstraint('<=', '2.3.4.5')),
+            array('1.2-beta - 2.3',         new VersionConstraint('>=', '1.2.0.0-beta'),  new VersionConstraint('<',  '2.4.0.0-dev')),
+            array('1.2-beta - 2.3-dev',     new VersionConstraint('>=', '1.2.0.0-beta'),  new VersionConstraint('<=', '2.3.0.0-dev')),
+            array('1.2-RC - 2.3.1',         new VersionConstraint('>=', '1.2.0.0-RC'),    new VersionConstraint('<=', '2.3.1.0')),
+            array('1.2.3-alpha - 2.3-RC',   new VersionConstraint('>=', '1.2.3.0-alpha'), new VersionConstraint('<=', '2.3.0.0-RC')),
+        );
+    }
+
+    /**
+     * @dataProvider multiConstraintProvider
+     */
+    public function testParseConstraintsMulti($constraint)
     {
     {
         $parser = new VersionParser;
         $parser = new VersionParser;
         $first = new VersionConstraint('>', '2.0.0.0');
         $first = new VersionConstraint('>', '2.0.0.0');
         $second = new VersionConstraint('<=', '3.0.0.0');
         $second = new VersionConstraint('<=', '3.0.0.0');
         $multi = new MultiConstraint(array($first, $second));
         $multi = new MultiConstraint(array($first, $second));
-        $this->assertSame((string) $multi, (string) $parser->parseConstraints('>2.0,<=3.0'));
+        $this->assertSame((string) $multi, (string) $parser->parseConstraints($constraint));
+    }
+
+    public function multiConstraintProvider()
+    {
+        return array(
+            array('>2.0,<=3.0'),
+            array('>2.0 <=3.0'),
+            array('>2.0  <=3.0'),
+            array('>2.0, <=3.0'),
+            array('>2.0 ,<=3.0'),
+            array('>2.0 , <=3.0'),
+            array('>2.0   , <=3.0'),
+            array('> 2.0   <=  3.0'),
+            array('> 2.0  ,  <=  3.0'),
+            array('  > 2.0  ,  <=  3.0 '),
+        );
     }
     }
 
 
     public function testParseConstraintsMultiWithStabilitySuffix()
     public function testParseConstraintsMultiWithStabilitySuffix()
@@ -314,7 +401,10 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
         $this->assertSame((string) $multi, (string) $parser->parseConstraints('>=1.1.0-alpha4,<1.2-beta2'));
         $this->assertSame((string) $multi, (string) $parser->parseConstraints('>=1.1.0-alpha4,<1.2-beta2'));
     }
     }
 
 
-    public function testParseConstraintsMultiDisjunctiveHasPrioOverConjuctive()
+    /**
+     * @dataProvider multiConstraintProvider2
+     */
+    public function testParseConstraintsMultiDisjunctiveHasPrioOverConjuctive($constraint)
     {
     {
         $parser = new VersionParser;
         $parser = new VersionParser;
         $first = new VersionConstraint('>', '2.0.0.0');
         $first = new VersionConstraint('>', '2.0.0.0');
@@ -322,7 +412,16 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
         $third = new VersionConstraint('>', '2.0.6.0');
         $third = new VersionConstraint('>', '2.0.6.0');
         $multi1 = new MultiConstraint(array($first, $second));
         $multi1 = new MultiConstraint(array($first, $second));
         $multi2 = new MultiConstraint(array($multi1, $third), false);
         $multi2 = new MultiConstraint(array($multi1, $third), false);
-        $this->assertSame((string) $multi2, (string) $parser->parseConstraints('>2.0,<2.0.5 | >2.0.6'));
+        $this->assertSame((string) $multi2, (string) $parser->parseConstraints($constraint));
+    }
+
+    public function multiConstraintProvider2()
+    {
+        return array(
+            array('>2.0,<2.0.5 | >2.0.6'),
+            array('>2.0,<2.0.5 || >2.0.6'),
+            array('> 2.0 , <2.0.5 | >  2.0.6'),
+        );
     }
     }
 
 
     public function testParseConstraintsMultiWithStabilities()
     public function testParseConstraintsMultiWithStabilities()
@@ -349,6 +448,9 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
         return array(
         return array(
             'empty '            => array(''),
             'empty '            => array(''),
             'invalid version'   => array('1.0.0-meh'),
             'invalid version'   => array('1.0.0-meh'),
+            'operator abuse'    => array('>2.0,,<=3.0'),
+            'operator abuse/2'  => array('>2.0 ,, <=3.0'),
+            'operator abuse/3'  => array('>2.0 ||| <=3.0'),
         );
         );
     }
     }