1 /** 2 Stuff with dependencies. 3 4 Copyright: © 2012-2013 Matthias Dondorff 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Matthias Dondorff, Sönke Ludwig 7 */ 8 module dub.dependency; 9 10 import dub.internal.utils; 11 import dub.internal.vibecompat.core.log; 12 import dub.internal.vibecompat.core.file; 13 import dub.internal.vibecompat.data.json; 14 import dub.internal.vibecompat.inet.url; 15 import dub.package_; 16 import dub.semver; 17 18 import std.algorithm; 19 import std.array; 20 import std.exception; 21 import std.regex; 22 import std..string; 23 import std.typecons; 24 static import std.compiler; 25 26 27 /** 28 Representing a dependency, which is basically a version string and a 29 compare methode, e.g. '>=1.0.0 <2.0.0' (i.e. a space separates the two 30 version numbers) 31 */ 32 struct Dependency { 33 private { 34 // Shortcut to create >=0.0.0 35 enum ANY_IDENT = "*"; 36 bool m_inclusiveA = true; // A comparison > (true) or >= (false) 37 Version m_versA; 38 bool m_inclusiveB = true; // B comparison < (true) or <= (false) 39 Version m_versB; 40 Path m_path; 41 bool m_optional = false; 42 } 43 44 // A Dependency, which matches every valid version. 45 static @property any() { return Dependency(ANY_IDENT); } 46 static @property invalid() { Dependency ret; ret.m_versA = Version.HEAD; ret.m_versB = Version.RELEASE; return ret; } 47 48 alias ANY = any; 49 alias INVALID = invalid; 50 51 this(string ves) 52 { 53 enforce(ves.length > 0); 54 string orig = ves; 55 56 if (ves == ANY_IDENT) { 57 // Any version is good. 58 ves = ">=0.0.0"; 59 } 60 61 if (ves.startsWith("~>")) { 62 // Shortcut: "~>x.y.z" variant. Last non-zero number will indicate 63 // the base for this so something like this: ">=x.y.z <x.(y+1).z" 64 m_inclusiveA = true; 65 m_inclusiveB = false; 66 ves = ves[2..$]; 67 m_versA = Version(expandVersion(ves)); 68 m_versB = Version(bumpVersion(ves)); 69 } else if (ves[0] == Version.BRANCH_IDENT) { 70 m_inclusiveA = true; 71 m_inclusiveB = true; 72 m_versA = m_versB = Version(ves); 73 } else if (std..string.indexOf("><=", ves[0]) == -1) { 74 m_inclusiveA = true; 75 m_inclusiveB = true; 76 m_versA = m_versB = Version(ves); 77 } else { 78 auto cmpa = skipComp(ves); 79 size_t idx2 = std..string.indexOf(ves, " "); 80 if (idx2 == -1) { 81 if (cmpa == "<=" || cmpa == "<") { 82 m_versA = Version.RELEASE; 83 m_inclusiveA = true; 84 m_versB = Version(ves); 85 m_inclusiveB = cmpa == "<="; 86 } else if (cmpa == ">=" || cmpa == ">") { 87 m_versA = Version(ves); 88 m_inclusiveA = cmpa == ">="; 89 m_versB = Version.HEAD; 90 m_inclusiveB = true; 91 } else { 92 // Converts "==" to ">=a&&<=a", which makes merging easier 93 m_versA = m_versB = Version(ves); 94 m_inclusiveA = m_inclusiveB = true; 95 } 96 } else { 97 enforce(cmpa == ">" || cmpa == ">=", "First comparison operator expected to be either > or >=, not "~cmpa); 98 assert(ves[idx2] == ' '); 99 m_versA = Version(ves[0..idx2]); 100 m_inclusiveA = cmpa == ">="; 101 string v2 = ves[idx2+1..$]; 102 auto cmpb = skipComp(v2); 103 enforce(cmpb == "<" || cmpb == "<=", "Second comparison operator expected to be either < or <=, not "~cmpb); 104 m_versB = Version(v2); 105 m_inclusiveB = cmpb == "<="; 106 107 enforce(!m_versA.isBranch && !m_versB.isBranch, format("Cannot compare branches: %s", ves)); 108 enforce(m_versA <= m_versB, "First version must not be greater than the second one."); 109 } 110 } 111 } 112 113 this(in Version ver) 114 { 115 m_inclusiveA = m_inclusiveB = true; 116 m_versA = ver; 117 m_versB = ver; 118 } 119 120 this(Path path) 121 { 122 this(ANY_IDENT); 123 m_path = path; 124 } 125 126 @property void path(Path value) { m_path = value; } 127 @property Path path() const { return m_path; } 128 @property bool optional() const { return m_optional; } 129 @property void optional(bool optional) { m_optional = optional; } 130 @property bool isExactVersion() const { return m_versA == m_versB; } 131 132 @property Version version_() const { 133 enforce(m_versA == m_versB, "Dependency "~versionString~" is no exact version."); 134 return m_versA; 135 } 136 137 @property string versionString() 138 const { 139 string r; 140 141 if (this == invalid) return "invalid"; 142 143 if (m_versA == m_versB && m_inclusiveA && m_inclusiveB) { 144 // Special "==" case 145 if (m_versA == Version.MASTER ) r = "~master"; 146 else r = m_versA.toString(); 147 } else { 148 if (m_versA != Version.RELEASE) r = (m_inclusiveA ? ">=" : ">") ~ m_versA.toString(); 149 if (m_versB != Version.HEAD) r ~= (r.length==0 ? "" : " ") ~ (m_inclusiveB ? "<=" : "<") ~ m_versB.toString(); 150 if (m_versA == Version.RELEASE && m_versB == Version.HEAD) r = ">=0.0.0"; 151 } 152 return r; 153 } 154 155 Dependency mapToPath(Path path) 156 const { 157 if (m_path.empty || m_path.absolute) return this; 158 else { 159 Dependency ret = this; 160 ret.path = path ~ ret.path; 161 return ret; 162 } 163 } 164 165 string toString()() 166 const { 167 auto ret = versionString; 168 if (optional) ret ~= " (optional)"; 169 if (!path.empty) ret ~= " @"~path.toNativeString(); 170 return ret; 171 } 172 173 Json toJson() const { 174 Json json; 175 if( path.empty && !optional ){ 176 json = Json(this.versionString); 177 } else { 178 json = Json.emptyObject; 179 json["version"] = this.versionString; 180 if (!path.empty) json["path"] = path.toString(); 181 if (optional) json["optional"] = true; 182 } 183 return json; 184 } 185 186 unittest { 187 Dependency d = Dependency("==1.0.0"); 188 assert(d.toJson() == Json("1.0.0"), "Failed: " ~ d.toJson().toPrettyString()); 189 d = fromJson((fromJson(d.toJson())).toJson()); 190 assert(d == Dependency("1.0.0")); 191 assert(d.toJson() == Json("1.0.0"), "Failed: " ~ d.toJson().toPrettyString()); 192 } 193 194 static Dependency fromJson(Json verspec) { 195 Dependency dep; 196 if( verspec.type == Json.Type.object ){ 197 if( auto pp = "path" in verspec ) { 198 if (auto pv = "version" in verspec) 199 logDiagnostic("Ignoring version specification (%s) for path based dependency %s", pv.get!string, pp.get!string); 200 201 dep = Dependency.ANY; 202 dep.path = Path(verspec.path.get!string); 203 } else { 204 enforce("version" in verspec, "No version field specified!"); 205 auto ver = verspec["version"].get!string; 206 // Using the string to be able to specifiy a range of versions. 207 dep = Dependency(ver); 208 } 209 if( auto po = "optional" in verspec ) { 210 dep.optional = verspec.optional.get!bool; 211 } 212 } else { 213 // canonical "package-id": "version" 214 dep = Dependency(verspec.get!string); 215 } 216 return dep; 217 } 218 219 unittest { 220 assert(fromJson(parseJsonString("\">=1.0.0 <2.0.0\"")) == Dependency(">=1.0.0 <2.0.0")); 221 Dependency parsed = fromJson(parseJsonString(` 222 { 223 "version": "2.0.0", 224 "optional": true, 225 "path": "path/to/package" 226 } 227 `)); 228 Dependency d = Dependency.ANY; // supposed to ignore the version spec 229 d.optional = true; 230 d.path = Path("path/to/package"); 231 assert(d == parsed); 232 // optional and path not checked by opEquals. 233 assert(d.optional == parsed.optional); 234 assert(d.path == parsed.path); 235 } 236 237 bool opEquals(in Dependency o) 238 const { 239 // TODO(mdondorff): Check if not comparing the path is correct for all clients. 240 return o.m_inclusiveA == m_inclusiveA && o.m_inclusiveB == m_inclusiveB 241 && o.m_versA == m_versA && o.m_versB == m_versB 242 && o.m_optional == m_optional; 243 } 244 245 int opCmp(in Dependency o) 246 const { 247 if (m_inclusiveA != o.m_inclusiveA) return m_inclusiveA < o.m_inclusiveA ? -1 : 1; 248 if (m_inclusiveB != o.m_inclusiveB) return m_inclusiveB < o.m_inclusiveB ? -1 : 1; 249 if (m_versA != o.m_versA) return m_versA < o.m_versA ? -1 : 1; 250 if (m_versB != o.m_versB) return m_versB < o.m_versB ? -1 : 1; 251 if (m_optional != o.m_optional) return m_optional ? -1 : 1; 252 return 0; 253 } 254 255 hash_t toHash() const nothrow @trusted { 256 try { 257 auto strhash = &typeid(string).getHash; 258 auto str = this.toString(); 259 return strhash(&str); 260 } catch (Exception) assert(false); 261 } 262 263 bool valid() const { 264 return m_versA <= m_versB && doCmp(m_inclusiveA && m_inclusiveB, m_versA, m_versB); 265 } 266 267 bool matches(string vers) const { return matches(Version(vers)); } 268 bool matches(const(Version) v) const { return matches(v); } 269 bool matches(ref const(Version) v) const { 270 if (this == ANY) return true; 271 //logDebug(" try match: %s with: %s", v, this); 272 // Master only matches master 273 if(m_versA.isBranch) { 274 enforce(m_versA == m_versB); 275 return m_versA == v; 276 } 277 if(v.isBranch || m_versA.isBranch) 278 return m_versA == v; 279 if( !doCmp(m_inclusiveA, m_versA, v) ) 280 return false; 281 if( !doCmp(m_inclusiveB, v, m_versB) ) 282 return false; 283 return true; 284 } 285 286 /// Merges to versions 287 Dependency merge(ref const(Dependency) o) 288 const { 289 if (this == ANY) return o; 290 if (o == ANY) return this; 291 if (!this.valid || !o.valid) return INVALID; 292 if (m_versA.isBranch != o.m_versA.isBranch) return INVALID; 293 if (m_versB.isBranch != o.m_versB.isBranch) return INVALID; 294 if (m_versA.isBranch) return m_versA == o.m_versA ? this : INVALID; 295 if (this.path != o.path) return INVALID; 296 297 Version a = m_versA > o.m_versA ? m_versA : o.m_versA; 298 Version b = m_versB < o.m_versB ? m_versB : o.m_versB; 299 300 Dependency d = this; 301 d.m_inclusiveA = !m_inclusiveA && m_versA >= o.m_versA ? false : o.m_inclusiveA; 302 d.m_versA = a; 303 d.m_inclusiveB = !m_inclusiveB && m_versB <= o.m_versB ? false : o.m_inclusiveB; 304 d.m_versB = b; 305 d.m_optional = m_optional && o.m_optional; 306 if (!d.valid) return INVALID; 307 308 return d; 309 } 310 311 private static bool isDigit(char ch) { return ch >= '0' && ch <= '9'; } 312 private static string skipComp(ref string c) { 313 size_t idx = 0; 314 while (idx < c.length && !isDigit(c[idx]) && c[idx] != Version.BRANCH_IDENT) idx++; 315 enforce(idx < c.length, "Expected version number in version spec: "~c); 316 string cmp = idx==c.length-1||idx==0? ">=" : c[0..idx]; 317 c = c[idx..$]; 318 switch(cmp) { 319 default: enforce(false, "No/Unknown comparision specified: '"~cmp~"'"); return ">="; 320 case ">=": goto case; case ">": goto case; 321 case "<=": goto case; case "<": goto case; 322 case "==": return cmp; 323 } 324 } 325 326 private static bool doCmp(bool inclusive, ref const Version a, ref const Version b) { 327 return inclusive ? a <= b : a < b; 328 } 329 } 330 331 unittest { 332 Dependency a = Dependency(">=1.1.0"), b = Dependency(">=1.3.0"); 333 assert (a.merge(b).valid() && a.merge(b).versionString == ">=1.3.0", a.merge(b).toString()); 334 335 assertThrown(Dependency("<=2.0.0 >=1.0.0")); 336 assertThrown(Dependency(">=2.0.0 <=1.0.0")); 337 338 a = Dependency(">=1.0.0 <=5.0.0"); b = Dependency(">=2.0.0"); 339 assert (a.merge(b).valid() && a.merge(b).versionString == ">=2.0.0 <=5.0.0", a.merge(b).toString()); 340 341 assertThrown(a = Dependency(">1.0.0 ==5.0.0"), "Construction is invalid"); 342 343 a = Dependency(">1.0.0"); b = Dependency("<2.0.0"); 344 assert (a.merge(b).valid(), a.merge(b).toString()); 345 assert (a.merge(b).versionString == ">1.0.0 <2.0.0", a.merge(b).toString()); 346 347 a = Dependency(">2.0.0"); b = Dependency("<1.0.0"); 348 assert (!(a.merge(b)).valid(), a.merge(b).toString()); 349 350 a = Dependency(">=2.0.0"); b = Dependency("<=1.0.0"); 351 assert (!(a.merge(b)).valid(), a.merge(b).toString()); 352 353 a = Dependency("==2.0.0"); b = Dependency("==1.0.0"); 354 assert (!(a.merge(b)).valid(), a.merge(b).toString()); 355 356 a = Dependency("1.0.0"); b = Dependency("==1.0.0"); 357 assert (a == b); 358 359 a = Dependency("<=2.0.0"); b = Dependency("==1.0.0"); 360 Dependency m = a.merge(b); 361 assert (m.valid(), m.toString()); 362 assert (m.matches(Version("1.0.0"))); 363 assert (!m.matches(Version("1.1.0"))); 364 assert (!m.matches(Version("0.0.1"))); 365 366 367 // branches / head revisions 368 a = Dependency(Version.MASTER_STRING); 369 assert(a.valid()); 370 assert(a.matches(Version.MASTER)); 371 b = Dependency(Version.MASTER_STRING); 372 m = a.merge(b); 373 assert(m.matches(Version.MASTER)); 374 375 //assertThrown(a = Dependency(Version.MASTER_STRING ~ " <=1.0.0"), "Construction invalid"); 376 assertThrown(a = Dependency(">=1.0.0 " ~ Version.MASTER_STRING), "Construction invalid"); 377 378 immutable string branch1 = Version.BRANCH_IDENT ~ "Branch1"; 379 immutable string branch2 = Version.BRANCH_IDENT ~ "Branch2"; 380 381 //assertThrown(a = Dependency(branch1 ~ " " ~ branch2), "Error: '" ~ branch1 ~ " " ~ branch2 ~ "' succeeded"); 382 //assertThrown(a = Dependency(Version.MASTER_STRING ~ " " ~ branch1), "Error: '" ~ Version.MASTER_STRING ~ " " ~ branch1 ~ "' succeeded"); 383 384 a = Dependency(branch1); 385 b = Dependency(branch2); 386 assert(!a.merge(b).valid, "Shouldn't be able to merge to different branches"); 387 b = a.merge(a); 388 assert(b.valid, "Should be able to merge the same branches. (?)"); 389 assert(a == b); 390 391 a = Dependency(branch1); 392 assert(a.matches(branch1), "Dependency(branch1) does not match 'branch1'"); 393 assert(a.matches(Version(branch1)), "Dependency(branch1) does not match Version('branch1')"); 394 assert(!a.matches(Version.MASTER), "Dependency(branch1) matches Version.MASTER"); 395 assert(!a.matches(branch2), "Dependency(branch1) matches 'branch2'"); 396 assert(!a.matches(Version("1.0.0")), "Dependency(branch1) matches '1.0.0'"); 397 a = Dependency(">=1.0.0"); 398 assert(!a.matches(Version(branch1)), "Dependency(1.0.0) matches 'branch1'"); 399 400 // Testing optional dependencies. 401 a = Dependency(">=1.0.0"); 402 assert(!a.optional, "Default is not optional."); 403 b = a; 404 assert(!a.merge(b).optional, "Merging two not optional dependencies wrong."); 405 a.optional = true; 406 assert(!a.merge(b).optional, "Merging optional with not optional wrong."); 407 b.optional = true; 408 assert(a.merge(b).optional, "Merging two optional dependencies wrong."); 409 410 // SemVer's sub identifiers. 411 a = Dependency(">=1.0.0-beta"); 412 assert(!a.matches(Version("1.0.0-alpha")), "Failed: match 1.0.0-alpha with >=1.0.0-beta"); 413 assert(a.matches(Version("1.0.0-beta")), "Failed: match 1.0.0-beta with >=1.0.0-beta"); 414 assert(a.matches(Version("1.0.0")), "Failed: match 1.0.0 with >=1.0.0-beta"); 415 assert(a.matches(Version("1.0.0-rc")), "Failed: match 1.0.0-rc with >=1.0.0-beta"); 416 417 // Approximate versions. 418 a = Dependency("~>3.0"); 419 b = Dependency(">=3.0.0 <4.0.0"); 420 assert(a == b, "Testing failed: " ~ a.toString()); 421 assert(a.matches(Version("3.1.146")), "Failed: Match 3.1.146 with ~>0.1.2"); 422 assert(!a.matches(Version("0.2.0")), "Failed: Match 0.2.0 with ~>0.1.2"); 423 a = Dependency("~>3.0.0"); 424 assert(a == Dependency(">=3.0.0 <3.1.0"), "Testing failed: " ~ a.toString()); 425 a = Dependency("~>3.5"); 426 assert(a == Dependency(">=3.5.0 <4.0.0"), "Testing failed: " ~ a.toString()); 427 a = Dependency("~>3.5.0"); 428 assert(a == Dependency(">=3.5.0 <3.6.0"), "Testing failed: " ~ a.toString()); 429 430 a = Dependency("~>0.1.1"); 431 b = Dependency("==0.1.0"); 432 assert(!a.merge(b).valid); 433 b = Dependency("==0.1.9999"); 434 assert(a.merge(b).valid); 435 b = Dependency("==0.2.0"); 436 assert(!a.merge(b).valid); 437 438 a = Dependency("~>1.0.1-beta"); 439 b = Dependency(">=1.0.1-beta <1.1.0"); 440 assert(a == b, "Testing failed: " ~ a.toString()); 441 assert(a.matches(Version("1.0.1-beta"))); 442 assert(a.matches(Version("1.0.1-beta.6"))); 443 444 a = Dependency("~d2test"); 445 assert(!a.optional); 446 assert(a.valid); 447 assert(a.version_ == Version("~d2test")); 448 449 a = Dependency("==~d2test"); 450 assert(!a.optional); 451 assert(a.valid); 452 assert(a.version_ == Version("~d2test")); 453 454 a = Dependency.ANY; 455 assert(!a.optional); 456 assert(a.valid); 457 assertThrown(a.version_); 458 b = Dependency(">=1.0.1"); 459 assert(b == a.merge(b)); 460 assert(b == b.merge(a)); 461 462 logDebug("Dependency Unittest sucess."); 463 } 464 465 466 /** 467 A version in the format "major.update.bugfix-prerelease+buildmetadata" 468 according to Semantic Versioning Specification v2.0.0. 469 470 (deprecated): 471 This also supports a format like "~master", to identify trunk, or 472 "~branch_name" to identify a branch. Both Version types starting with "~" 473 refer to the head revision of the corresponding branch. 474 This is subject to be removed soon. 475 */ 476 struct Version { 477 private { 478 enum MAX_VERS = "99999.0.0"; 479 enum UNKNOWN_VERS = "unknown"; 480 string m_version; 481 } 482 483 static @property RELEASE() { return Version("0.0.0"); } 484 static @property HEAD() { return Version(MAX_VERS); } 485 static @property MASTER() { return Version(MASTER_STRING); } 486 static @property UNKNOWN() { return Version(UNKNOWN_VERS); } 487 static @property MASTER_STRING() { return "~master"; } 488 static @property BRANCH_IDENT() { return '~'; } 489 490 this(string vers) 491 { 492 enforce(vers.length > 1, "Version strings must not be empty."); 493 if (vers[0] != BRANCH_IDENT && vers != UNKNOWN_VERS) 494 enforce(vers.isValidVersion(), "Invalid SemVer format: " ~ vers); 495 m_version = vers; 496 } 497 498 static Version fromString(string vers) { return Version(vers); } 499 500 bool opEquals(const Version oth) const { 501 if (isUnknown || oth.isUnknown) { 502 throw new Exception("Can't compare unknown versions! (this: %s, other: %s)".format(this, oth)); 503 } 504 return opCmp(oth) == 0; 505 } 506 507 /// Returns true, if this version indicates a branch, which is not the trunk. 508 @property bool isBranch() const { return !m_version.empty && m_version[0] == BRANCH_IDENT; } 509 @property bool isMaster() const { return m_version == MASTER_STRING; } 510 @property bool isPreRelease() const { 511 if (isBranch) return true; 512 return isPreReleaseVersion(m_version); 513 } 514 @property bool isUnknown() const { return m_version == UNKNOWN_VERS; } 515 516 /** 517 Comparing Versions is generally possible, but comparing Versions 518 identifying branches other than master will fail. Only equality 519 can be tested for these. 520 */ 521 int opCmp(ref const Version other) 522 const { 523 if (isUnknown || other.isUnknown) { 524 throw new Exception("Can't compare unknown versions! (this: %s, other: %s)".format(this, other)); 525 } 526 if (isBranch || other.isBranch) { 527 if(m_version == other.m_version) return 0; 528 if (!isBranch) return 1; 529 else if (!other.isBranch) return -1; 530 if (isMaster) return 1; 531 else if (other.isMaster) return -1; 532 return this.m_version < other.m_version ? -1 : 1; 533 } 534 535 return compareVersions(isMaster ? MAX_VERS : m_version, other.isMaster ? MAX_VERS : other.m_version); 536 } 537 int opCmp(in Version other) const { return opCmp(other); } 538 539 string toString() const { return m_version; } 540 } 541 542 unittest { 543 Version a, b; 544 545 assertNotThrown(a = Version("1.0.0"), "Constructing Version('1.0.0') failed"); 546 assert(!a.isBranch, "Error: '1.0.0' treated as branch"); 547 assert(a == a, "a == a failed"); 548 549 assertNotThrown(a = Version(Version.MASTER_STRING), "Constructing Version("~Version.MASTER_STRING~"') failed"); 550 assert(a.isBranch, "Error: '"~Version.MASTER_STRING~"' treated as branch"); 551 assert(a.isMaster); 552 assert(a == Version.MASTER, "Constructed master version != default master version."); 553 554 assertNotThrown(a = Version("~BRANCH"), "Construction of branch Version failed."); 555 assert(a.isBranch, "Error: '~BRANCH' not treated as branch'"); 556 assert(!a.isMaster); 557 assert(a == a, "a == a with branch failed"); 558 559 // opCmp 560 a = Version("1.0.0"); 561 b = Version("1.0.0"); 562 assert(a == b, "a == b with a:'1.0.0', b:'1.0.0' failed"); 563 b = Version("2.0.0"); 564 assert(a != b, "a != b with a:'1.0.0', b:'2.0.0' failed"); 565 a = Version(Version.MASTER_STRING); 566 b = Version("~BRANCH"); 567 assert(a != b, "a != b with a:MASTER, b:'~branch' failed"); 568 assert(a > b); 569 assert(a < Version("0.0.0")); 570 assert(b < Version("0.0.0")); 571 assert(a > Version("~Z")); 572 assert(b < Version("~Z")); 573 574 // SemVer 2.0.0-rc.2 575 a = Version("2.0.0-rc.2"); 576 b = Version("2.0.0-rc.3"); 577 assert(a < b, "Failed: 2.0.0-rc.2 < 2.0.0-rc.3"); 578 579 a = Version("2.0.0-rc.2+build-metadata"); 580 b = Version("2.0.0+build-metadata"); 581 assert(a < b, "Failed: "~a.toString()~"<"~b.toString()); 582 583 // 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 584 Version[] versions; 585 versions ~= Version("1.0.0-alpha"); 586 versions ~= Version("1.0.0-alpha.1"); 587 versions ~= Version("1.0.0-beta.2"); 588 versions ~= Version("1.0.0-beta.11"); 589 versions ~= Version("1.0.0-rc.1"); 590 versions ~= Version("1.0.0"); 591 for(int i=1; i<versions.length; ++i) 592 for(int j=i-1; j>=0; --j) 593 assert(versions[j] < versions[i], "Failed: " ~ versions[j].toString() ~ "<" ~ versions[i].toString()); 594 595 a = Version.UNKNOWN; 596 b = Version.RELEASE; 597 assertThrown(a == b, "Failed: compared " ~ a.toString() ~ " with " ~ b.toString() ~ ""); 598 599 a = Version.UNKNOWN; 600 b = Version.UNKNOWN; 601 assertThrown(a == b, "Failed: UNKNOWN == UNKNOWN"); 602 603 assert(Version("1.0.0+a") == Version("1.0.0+b")); 604 }