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 }