MultiConstraintTest.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. <?php
  2. /*
  3. * This file is part of composer/semver.
  4. *
  5. * (c) Composer <https://github.com/composer>
  6. *
  7. * For the full copyright and license information, please view
  8. * the LICENSE file that was distributed with this source code.
  9. */
  10. namespace Composer\Semver\Constraint;
  11. use Composer\Semver\VersionParser;
  12. use PHPUnit\Framework\TestCase;
  13. use Composer\Semver\Intervals;
  14. class MultiConstraintTest extends TestCase
  15. {
  16. /**
  17. * @var Constraint
  18. */
  19. protected $versionRequireStart;
  20. /**
  21. * @var Constraint
  22. */
  23. protected $versionRequireEnd;
  24. protected function setUp()
  25. {
  26. $this->versionRequireStart = new Constraint('>', '1.0');
  27. $this->versionRequireEnd = new Constraint('<', '1.2');
  28. }
  29. public function testIsConjunctive()
  30. {
  31. $multiConstraint = new MultiConstraint(array($this->versionRequireStart, $this->versionRequireEnd), true);
  32. $this->assertTrue($multiConstraint->isConjunctive());
  33. $this->assertFalse($multiConstraint->isDisjunctive());
  34. }
  35. public function testIsDisjunctive()
  36. {
  37. $multiConstraint = new MultiConstraint(array($this->versionRequireStart, $this->versionRequireEnd), false);
  38. $this->assertFalse($multiConstraint->isConjunctive());
  39. $this->assertTrue($multiConstraint->isDisjunctive());
  40. }
  41. public function testMultiVersionMatchSucceeds()
  42. {
  43. $versionProvide = new Constraint('==', '1.1');
  44. $multiRequire = new MultiConstraint(array($this->versionRequireStart, $this->versionRequireEnd));
  45. $this->assertTrue($multiRequire->matches($versionProvide));
  46. $this->assertTrue($versionProvide->matches($multiRequire));
  47. $this->assertTrue($this->matchCompiled($multiRequire, '==', '1.1'));
  48. $this->assertTrue(Intervals::haveIntersections($multiRequire, $versionProvide));
  49. $this->assertTrue(Intervals::compactConstraint($multiRequire)->matches(Intervals::compactConstraint($versionProvide)));
  50. $this->assertTrue(Intervals::compactConstraint($versionProvide)->matches(Intervals::compactConstraint($multiRequire)));
  51. }
  52. public function testMultiVersionProvidedMatchSucceeds()
  53. {
  54. $versionProvideStart = new Constraint('>=', '1.1');
  55. $versionProvideEnd = new Constraint('<', '2.0');
  56. $multiRequire = new MultiConstraint(array($this->versionRequireStart, $this->versionRequireEnd));
  57. $multiProvide = new MultiConstraint(array($versionProvideStart, $versionProvideEnd));
  58. $this->assertTrue($multiRequire->matches($multiProvide));
  59. $this->assertTrue($multiProvide->matches($multiRequire));
  60. $this->assertTrue(Intervals::haveIntersections($multiRequire, $multiProvide));
  61. $this->assertTrue(Intervals::compactConstraint($multiRequire)->matches(Intervals::compactConstraint($multiProvide)));
  62. $this->assertTrue(Intervals::compactConstraint($multiProvide)->matches(Intervals::compactConstraint($multiRequire)));
  63. }
  64. public function testMultiVersionMatchSucceedsInsideForeachLoop()
  65. {
  66. $versionProvideStart = new Constraint('>', '1.0');
  67. $versionProvideEnd = new Constraint('<', '1.2');
  68. $multiRequire = new MultiConstraint(array($this->versionRequireStart, $this->versionRequireEnd), false);
  69. $multiProvide = new MultiConstraint(array($versionProvideStart, $versionProvideEnd), false);
  70. $this->assertTrue($multiRequire->matches($multiProvide));
  71. $this->assertTrue($multiProvide->matches($multiRequire));
  72. $this->assertTrue(Intervals::haveIntersections($multiRequire, $multiProvide));
  73. $this->assertTrue(Intervals::compactConstraint($multiRequire)->matches(Intervals::compactConstraint($multiProvide)));
  74. $this->assertTrue(Intervals::compactConstraint($multiProvide)->matches(Intervals::compactConstraint($multiRequire)));
  75. }
  76. public function testConjunctiveMatchesDisjunctiveFalse()
  77. {
  78. $versionProvideStart = new Constraint('<', '1.0');
  79. $versionProvideEnd = new Constraint('>', '2.0');
  80. $multiRequire = new MultiConstraint(array($this->versionRequireStart, $this->versionRequireEnd), true);
  81. $multiProvide = new MultiConstraint(array($versionProvideStart, $versionProvideEnd), false);
  82. $this->assertFalse($multiRequire->matches($multiProvide));
  83. $this->assertFalse($multiProvide->matches($multiRequire));
  84. $this->assertFalse(Intervals::haveIntersections($multiRequire, $multiProvide));
  85. $this->assertFalse(Intervals::compactConstraint($multiRequire)->matches(Intervals::compactConstraint($multiProvide)));
  86. $this->assertFalse(Intervals::compactConstraint($multiProvide)->matches(Intervals::compactConstraint($multiRequire)));
  87. }
  88. public function testMultiVersionMatchFails()
  89. {
  90. $versionProvide = new Constraint('==', '1.2');
  91. $multiRequire = new MultiConstraint(array($this->versionRequireStart, $this->versionRequireEnd));
  92. $this->assertFalse($multiRequire->matches($versionProvide));
  93. $this->assertFalse($versionProvide->matches($multiRequire));
  94. $this->assertFalse($this->matchCompiled($multiRequire, '==', '1.2'));
  95. $this->assertFalse(Intervals::haveIntersections($multiRequire, $versionProvide));
  96. $this->assertFalse(Intervals::compactConstraint($multiRequire)->matches(Intervals::compactConstraint($versionProvide)));
  97. $this->assertFalse(Intervals::compactConstraint($versionProvide)->matches(Intervals::compactConstraint($multiRequire)));
  98. }
  99. public function testGetPrettyString()
  100. {
  101. $multiConstraint = new MultiConstraint(array($this->versionRequireStart, $this->versionRequireEnd));
  102. $expectedString = 'pretty-string';
  103. $multiConstraint->setPrettyString($expectedString);
  104. $result = $multiConstraint->getPrettyString();
  105. $this->assertSame($expectedString, $result);
  106. $expectedString = '[> 1.0 < 1.2]';
  107. $multiConstraint->setPrettyString(null);
  108. $result = $multiConstraint->getPrettyString();
  109. $this->assertSame($expectedString, $result);
  110. }
  111. /**
  112. * @dataProvider bounds
  113. *
  114. * @param array<ConstraintInterface> $constraints
  115. * @param bool $conjunctive
  116. * @param Bound $expectedLower
  117. * @param Bound $expectedUpper
  118. */
  119. public function testBounds(array $constraints, $conjunctive, Bound $expectedLower, Bound $expectedUpper)
  120. {
  121. $constraint = new MultiConstraint($constraints, $conjunctive);
  122. $this->assertEquals($expectedLower, $constraint->getLowerBound(), 'Expected lower bound does not match');
  123. $this->assertEquals($expectedUpper, $constraint->getUpperBound(), 'Expected upper bound does not match');
  124. }
  125. /**
  126. * @return array<mixed>
  127. */
  128. public function bounds()
  129. {
  130. return array(
  131. 'all equal' => array(
  132. array(
  133. new Constraint('==', '1.0.0.0'),
  134. new Constraint('==', '1.0.0.0'),
  135. ),
  136. true,
  137. new Bound('1.0.0.0', true),
  138. new Bound('1.0.0.0', true),
  139. ),
  140. '">" should take precedence ">=" for lower bound when conjunctive' => array(
  141. array(
  142. new Constraint('>', '1.0.0.0'),
  143. new Constraint('>=', '1.0.0.0'),
  144. new Constraint('>', '1.0.0.0'),
  145. ),
  146. true,
  147. new Bound('1.0.0.0', false),
  148. Bound::positiveInfinity(),
  149. ),
  150. '">=" should take precedence ">" for lower bound when disjunctive' => array(
  151. array(
  152. new Constraint('>', '1.0.0.0'),
  153. new Constraint('>=', '1.0.0.0'),
  154. new Constraint('>', '1.0.0.0'),
  155. ),
  156. false,
  157. new Bound('1.0.0.0', true),
  158. Bound::positiveInfinity(),
  159. ),
  160. 'Bounds should be limited when conjunctive' => array(
  161. array(
  162. new Constraint('>=', '7.0.0.0'),
  163. new Constraint('<', '8.0.0.0'),
  164. ),
  165. true,
  166. new Bound('7.0.0.0', true),
  167. new Bound('8.0.0.0', false),
  168. ),
  169. 'Bounds should be unlimited when disjunctive' => array(
  170. array(
  171. new Constraint('>=', '7.0.0.0'),
  172. new Constraint('<', '8.0.0.0'),
  173. ),
  174. false,
  175. Bound::zero(),
  176. Bound::positiveInfinity(),
  177. ),
  178. );
  179. }
  180. /**
  181. * @dataProvider boundsIntegration
  182. *
  183. * @param string $constraints
  184. * @param Bound $expectedLower
  185. * @param Bound $expectedUpper
  186. */
  187. public function testBoundsIntegrationWithVersionParser($constraints, Bound $expectedLower, Bound $expectedUpper)
  188. {
  189. $versionParser = new VersionParser();
  190. $constraint = $versionParser->parseConstraints($constraints);
  191. $this->assertEquals($expectedLower, $constraint->getLowerBound(), 'Expected lower bound does not match');
  192. $this->assertEquals($expectedUpper, $constraint->getUpperBound(), 'Expected upper bound does not match');
  193. }
  194. /**
  195. * @return array<mixed>
  196. */
  197. public function boundsIntegration()
  198. {
  199. return array(
  200. '^7.0' => array(
  201. '^7.0',
  202. new Bound('7.0.0.0-dev', true),
  203. new Bound('8.0.0.0-dev', false),
  204. ),
  205. '^7.2' => array(
  206. '^7.2',
  207. new Bound('7.2.0.0-dev', true),
  208. new Bound('8.0.0.0-dev', false),
  209. ),
  210. '7.4.*' => array(
  211. '7.4.*',
  212. new Bound('7.4.0.0-dev', true),
  213. new Bound('7.5.0.0-dev', false),
  214. ),
  215. '7.2.* || 7.4.*' => array(
  216. '7.2.* || 7.4.*',
  217. new Bound('7.2.0.0-dev', true),
  218. new Bound('7.5.0.0-dev', false),
  219. ),
  220. );
  221. }
  222. public function testMultipleMultiConstraintsMerging()
  223. {
  224. $versionParser = new VersionParser();
  225. $strConstraints = array(
  226. '^7.0',
  227. '^7.2',
  228. '7.4.*',
  229. '7.2.* || 7.4.*',
  230. );
  231. $constraints = array();
  232. foreach ($strConstraints as $str) {
  233. $constraints[] = $versionParser->parseConstraints($str);
  234. }
  235. $constraint = new MultiConstraint($constraints);
  236. $this->assertEquals(new Bound('7.4.0.0-dev', true), $constraint->getLowerBound(), 'Expected lower bound does not match');
  237. $this->assertEquals(new Bound('7.5.0.0-dev', false), $constraint->getUpperBound(), 'Expected upper bound does not match');
  238. }
  239. public function testMultipleMultiConstraintsMergingWithGaps()
  240. {
  241. $versionParser = new VersionParser();
  242. $constraint = new MultiConstraint(array(
  243. $versionParser->parseConstraints('^7.1.15 || ^7.2.3'),
  244. $versionParser->parseConstraints('^7.2.2'),
  245. ));
  246. $this->assertEquals(new Bound('7.2.2.0-dev', true), $constraint->getLowerBound(), 'Expected lower bound does not match');
  247. $this->assertEquals(new Bound('8.0.0.0-dev', false), $constraint->getUpperBound(), 'Expected upper bound does not match');
  248. }
  249. public function testCreatesMatchAllConstraintIfNoneGiven()
  250. {
  251. $this->assertInstanceOf('Composer\Semver\Constraint\MatchAllConstraint', MultiConstraint::create(array()));
  252. }
  253. public function testMatchAllConstraintWithinConjunctiveMultiConstraint()
  254. {
  255. $this->assertSame('[>= 2.5.0.0-dev <= 3.0.0.0-dev *]', (string) MultiConstraint::create(
  256. array(new Constraint('>=', '2.5.0.0-dev'), new Constraint('<=', '3.0.0.0-dev'), new MatchAllConstraint())
  257. ));
  258. }
  259. public function testMatchAllConstraintWithinDisjunctiveMultiConstraint()
  260. {
  261. $this->assertSame('[>= 2.5.0.0-dev || *]', (string) MultiConstraint::create(
  262. array(new Constraint('>=', '2.5.0.0-dev'), new MatchAllConstraint()), false
  263. ));
  264. }
  265. /**
  266. * @dataProvider multiConstraintOptimizations
  267. *
  268. * @param string $constraints
  269. */
  270. public function testMultiConstraintOptimizations($constraints, ConstraintInterface $expectedConstraint)
  271. {
  272. // We're using the version parser here because that uses MultiConstraint::create() internally and
  273. // thus tests our optimizations. It's just easier to write complex multi constraint instances
  274. // using the string notation.
  275. $parser = new VersionParser();
  276. $this->assertSame((string) $expectedConstraint, (string) $parser->parseConstraints($constraints));
  277. }
  278. /**
  279. * @return array<mixed>
  280. */
  281. public function multiConstraintOptimizations()
  282. {
  283. return array(
  284. 'Test collapses contiguous' => array(
  285. '^2.5 || ^3.0',
  286. new MultiConstraint(
  287. array(
  288. new Constraint('>=', '2.5.0.0-dev'),
  289. new Constraint('<', '4.0.0.0-dev'),
  290. ),
  291. true // conjunctive
  292. ),
  293. ),
  294. 'Test collapses multiple contiguous' => array(
  295. '^2.5 || ^3.0 || ^4.0',
  296. new MultiConstraint(
  297. array(
  298. new Constraint('>=', '2.5.0.0-dev'),
  299. new Constraint('<', '5.0.0.0-dev'),
  300. ),
  301. true // conjunctive
  302. ),
  303. ),
  304. 'Test does not collapse when one side is more complex' => array(
  305. '~2.5.9 || ~2.6, >=2.6.2',
  306. new MultiConstraint(
  307. array(
  308. new MultiConstraint(
  309. array(
  310. new Constraint('>=', '2.5.9.0-dev'),
  311. new Constraint('<', '2.6.0.0-dev'),
  312. ),
  313. true // conjunctive
  314. ),
  315. new MultiConstraint(
  316. array(
  317. new Constraint('>=', '2.6.0.0-dev'),
  318. new Constraint('<', '3.0.0.0-dev'),
  319. new Constraint('>=', '2.6.2.0-dev'),
  320. ),
  321. true // conjunctive
  322. ),
  323. ),
  324. false
  325. )
  326. ),
  327. 'Test does not collapse multiple contiguous with other constraint but collapses the end' => array(
  328. '^1.0 || ^2.0 !=2.0.1 || ^3.0 || ^4.0',
  329. new MultiConstraint(
  330. array(
  331. new MultiConstraint(
  332. array(
  333. new Constraint('>=', '1.0.0.0-dev'),
  334. new Constraint('<', '2.0.0.0-dev'),
  335. ),
  336. true // conjunctive
  337. ),
  338. new MultiConstraint(
  339. array(
  340. new Constraint('>=', '2.0.0.0-dev'),
  341. new Constraint('<', '3.0.0.0-dev'),
  342. new Constraint('!=', '2.0.1.0'),
  343. ),
  344. true // conjunctive
  345. ),
  346. new MultiConstraint(
  347. array(
  348. new Constraint('>=', '3.0.0.0-dev'),
  349. new Constraint('<', '5.0.0.0-dev'),
  350. ),
  351. true // conjunctive
  352. ),
  353. ),
  354. false
  355. )
  356. ),
  357. 'Test does not collapse multiple contiguous with multiple other constraint' => array(
  358. '^1.0 != 1.0.1 || ^2.0 !=2.0.1 || ^3.0 || ^4.0 != 4.0.1',
  359. new MultiConstraint(
  360. array(
  361. new MultiConstraint(
  362. array(
  363. new Constraint('>=', '1.0.0.0-dev'),
  364. new Constraint('<', '2.0.0.0-dev'),
  365. new Constraint('!=', '1.0.1.0'),
  366. ),
  367. true // conjunctive
  368. ),
  369. new MultiConstraint(
  370. array(
  371. new Constraint('>=', '2.0.0.0-dev'),
  372. new Constraint('<', '3.0.0.0-dev'),
  373. new Constraint('!=', '2.0.1.0'),
  374. ),
  375. true // conjunctive
  376. ),
  377. new MultiConstraint(
  378. array(
  379. new Constraint('>=', '3.0.0.0-dev'),
  380. new Constraint('<', '4.0.0.0-dev'),
  381. ),
  382. true // conjunctive
  383. ),
  384. new MultiConstraint(
  385. array(
  386. new Constraint('>=', '4.0.0.0-dev'),
  387. new Constraint('<', '5.0.0.0-dev'),
  388. new Constraint('!=', '4.0.1.0'),
  389. ),
  390. true // conjunctive
  391. ),
  392. ),
  393. false
  394. )
  395. ),
  396. 'Test does not collapse if contiguous range and other constraints also apply' => array(
  397. '~0.1 || ~1.0 !=1.0.1',
  398. new MultiConstraint(
  399. array(
  400. new MultiConstraint(
  401. array(
  402. new Constraint('>=', '0.1.0.0-dev'),
  403. new Constraint('<', '1.0.0.0-dev'),
  404. ),
  405. true // conjunctive
  406. ),
  407. new MultiConstraint(
  408. array(
  409. new Constraint('>=', '1.0.0.0-dev'),
  410. new Constraint('<', '2.0.0.0-dev'),
  411. new Constraint('!=', '1.0.1.0'),
  412. ),
  413. true // conjunctive
  414. ),
  415. ),
  416. false
  417. )
  418. ),
  419. 'Parse caret constraints must not collapse if non contiguous range' => array(
  420. '^0.2 || ^1.0',
  421. new MultiConstraint(
  422. array(
  423. new MultiConstraint(
  424. array(
  425. new Constraint('>=', '0.2.0.0-dev'),
  426. new Constraint('<', '0.3.0.0-dev'),
  427. )
  428. ),
  429. new MultiConstraint(
  430. array(
  431. new Constraint('>=', '1.0.0.0-dev'),
  432. new Constraint('<', '2.0.0.0-dev'),
  433. )
  434. ),
  435. ),
  436. false // disjunctive
  437. ),
  438. ),
  439. 'Must not collapse if not contiguous range but collapse following constraints' => array(
  440. '^0.1 || ^1.0 || ^2.0',
  441. new MultiConstraint(
  442. array(
  443. new MultiConstraint(
  444. array(
  445. new Constraint('>=', '0.1.0.0-dev'),
  446. new Constraint('<', '0.2.0.0-dev'),
  447. )
  448. ),
  449. new MultiConstraint(
  450. array(
  451. new Constraint('>=', '1.0.0.0-dev'),
  452. new Constraint('<', '3.0.0.0-dev'),
  453. )
  454. ),
  455. ),
  456. false // disjunctive
  457. ),
  458. ),
  459. 'Must not collapse other constraint not in range' => array(
  460. '^1.0 || 2.1 || ^3.0',
  461. new MultiConstraint(
  462. array(
  463. new MultiConstraint(
  464. array(
  465. new Constraint('>=', '1.0.0.0-dev'),
  466. new Constraint('<', '2.0.0.0-dev'),
  467. )
  468. ),
  469. new Constraint('=', '2.1.0.0'),
  470. new MultiConstraint(
  471. array(
  472. new Constraint('>=', '3.0.0.0-dev'),
  473. new Constraint('<', '4.0.0.0-dev'),
  474. )
  475. ),
  476. ),
  477. false // disjunctive
  478. ),
  479. ),
  480. );
  481. }
  482. public function testMultiConstraintNotconjunctiveFillWithFalse()
  483. {
  484. $versionProvide = new Constraint('==', '1.1');
  485. $multiRequire = new MultiConstraint(array(
  486. new Constraint('>', 'dev-foo'), // always false
  487. new Constraint('>', 'dev-bar'), // always false
  488. ), false);
  489. $this->assertFalse($multiRequire->matches($versionProvide));
  490. $this->assertFalse($versionProvide->matches($multiRequire));
  491. $this->assertFalse($this->matchCompiled($multiRequire, '==', '1.1'));
  492. $this->assertFalse(Intervals::haveIntersections($multiRequire, $versionProvide));
  493. }
  494. public function testMultiConstraintConjunctiveFillWithTrue()
  495. {
  496. $versionProvide = new Constraint('!=', '1.1');
  497. $multiRequire = new MultiConstraint(array(
  498. new Constraint('!=', 'dev-foo'), // always true
  499. new Constraint('!=', 'dev-bar'), // always true
  500. ), true);
  501. $this->assertTrue($multiRequire->matches($versionProvide));
  502. $this->assertTrue($versionProvide->matches($multiRequire));
  503. $this->assertTrue($this->matchCompiled($multiRequire, '!=', '1.1'));
  504. $this->assertTrue(Intervals::haveIntersections($multiRequire, $versionProvide));
  505. }
  506. /**
  507. * @param Constraint::STR_OP_* $operator
  508. * @param string $version
  509. * @return bool
  510. */
  511. private function matchCompiled(ConstraintInterface $constraint, $operator, $version)
  512. {
  513. $map = array(
  514. '=' => Constraint::OP_EQ,
  515. '==' => Constraint::OP_EQ,
  516. '<' => Constraint::OP_LT,
  517. '<=' => Constraint::OP_LE,
  518. '>' => Constraint::OP_GT,
  519. '>=' => Constraint::OP_GE,
  520. '<>' => Constraint::OP_NE,
  521. '!=' => Constraint::OP_NE,
  522. );
  523. $code = $constraint->compile($map[$operator]);
  524. $v = $version;
  525. $b = 'dev-' === substr($v, 0, 4);
  526. return eval("return $code;");
  527. }
  528. }