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.conv;
21 import std.exception;
22 import std.regex;
23 import std..string;
24 import std.typecons;
25 static import std.compiler;
26 
27 /**
28 	A version in the format "major.update.bugfix-pre-release+build-metadata" or
29 	"~master", to identify trunk, or "~branch_name" to identify a branch. Both 
30 	Version types starting with "~"	refer to the head revision of the 
31 	corresponding branch.
32 	
33 	Except for the "~branch" Version format, this follows the Semantic Versioning
34 	Specification (SemVer) 2.0.0-rc.2.
35 */
36 struct Version {
37 	private { 
38 		enum MASTER_VERS = cast(size_t)(-1);
39 		enum MAX_VERS = "99999.0.0";
40 		string m_version;
41 	}
42 
43 	static @property RELEASE() { return Version("0.0.0"); }
44 	static @property HEAD() { return Version(MAX_VERS); }
45 	static @property INVALID() { return Version(""); }
46 	static @property MASTER() { return Version(MASTER_STRING); }
47 	static @property MASTER_STRING() { return "~master"; }
48 	static @property BRANCH_IDENT() { return '~'; }
49 	
50 	this(string vers)
51 	{
52 		enforce(vers.length > 1, "Version strings must not be empty.");
53 		enforce(vers[0] == BRANCH_IDENT || vers.isValidVersion(), "Invalid SemVer format: "~vers);
54 		m_version = vers;
55 	}
56 
57 	bool opEquals(ref const Version oth) const { return m_version == oth.m_version; }
58 	bool opEquals(const Version oth) const { return m_version == oth.m_version; }
59 	
60 	/// Returns true, if this version indicates a branch, which is not the trunk.
61 	@property bool isBranch() const { return m_version[0] == BRANCH_IDENT && m_version != MASTER_STRING; }
62 	@property bool isMaster() const { return m_version == MASTER_STRING; }
63 	@property bool isPreRelease() const {
64 		if (isBranch || isMaster) return true;
65 		return isPreReleaseVersion(m_version);
66 	}
67 
68 	/** 
69 		Comparing Versions is generally possible, but comparing Versions 
70 		identifying branches other than master will fail. Only equality
71 		can be tested for these.
72 	*/
73 	int opCmp(ref const Version other)
74 	const {
75 		if(isBranch || other.isBranch) {
76 			if(m_version == other.m_version) return 0;
77 			else throw new Exception("Can't compare branch versions! (this: %s, other: %s)".format(this, other));
78 		}
79 
80 		return compareVersions(isMaster ? MAX_VERS : m_version, other.isMaster ? MAX_VERS : other.m_version);
81 	}
82 	int opCmp(in Version other) const { return opCmp(other); }
83 	
84 	string toString() const { return m_version; }
85 }
86 
87 unittest {
88 	Version a, b;
89 
90 	assertNotThrown(a = Version("1.0.0"), "Constructing Version('1.0.0') failed");
91 	assert(!a.isBranch, "Error: '1.0.0' treated as branch");
92 	assert(a == a, "a == a failed");
93 
94 	assertNotThrown(a = Version(Version.MASTER_STRING), "Constructing Version("~Version.MASTER_STRING~"') failed");
95 	assert(!a.isBranch, "Error: '"~Version.MASTER_STRING~"' treated as branch");
96 	assert(a == Version.MASTER, "Constructed master version != default master version.");
97 
98 	assertNotThrown(a = Version("~BRANCH"), "Construction of branch Version failed.");
99 	assert(a.isBranch, "Error: '~BRANCH' not treated as branch'");
100 	assert(a == a, "a == a with branch failed");
101 
102 	// opCmp
103 	a = Version("1.0.0");
104 	b = Version("1.0.0");
105 	assert(a == b, "a == b with a:'1.0.0', b:'1.0.0' failed");
106 	b = Version("2.0.0");
107 	assert(a != b, "a != b with a:'1.0.0', b:'2.0.0' failed");
108 	a = Version(Version.MASTER_STRING);
109 	b = Version("~BRANCH");
110 	assert(a != b, "a != b with a:MASTER, b:'~branch' failed");
111 	
112 	// SemVer 2.0.0-rc.2
113 	a = Version("2.0.0-rc.2");
114 	b = Version("2.0.0-rc.3");
115 	assert(a < b, "Failed: 2.0.0-rc.2 < 2.0.0-rc.3");
116 	
117 	a = Version("2.0.0-rc.2+build-metadata");
118 	b = Version("2.0.0+build-metadata");
119 	assert(a < b, "Failed: "~to!string(a)~"<"~to!string(b));
120 	
121 	// 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
122 	Version[] versions;
123 	versions ~= Version("1.0.0-alpha");
124 	versions ~= Version("1.0.0-alpha.1");
125 	versions ~= Version("1.0.0-beta.2");
126 	versions ~= Version("1.0.0-beta.11");
127 	versions ~= Version("1.0.0-rc.1");
128 	versions ~= Version("1.0.0");
129 	for(int i=1; i<versions.length; ++i)
130 		for(int j=i-1; j>=0; --j)
131 			assert(versions[j] < versions[i], "Failed: " ~ to!string(versions[j]) ~ "<" ~ to!string(versions[i]));
132 }
133 
134 /// Representing a dependency, which is basically a version string and a 
135 /// compare methode, e.g. '>=1.0.0 <2.0.0' (i.e. a space separates the two
136 /// version numbers)
137 struct Dependency {
138 	private {
139 		string m_cmpA;
140 		Version m_versA;
141 		string m_cmpB;
142 		Version m_versB;
143 		Path m_path;
144 		string m_configuration = "library";
145 		bool m_optional = false;
146 	}
147 
148 	this(string ves)
149 	{
150 		enforce(ves.length > 0);
151 		string orig = ves;
152 		if (ves[0] == Version.BRANCH_IDENT) {
153 			m_cmpA = ">=";
154 			m_cmpB = "<=";
155 			m_versA = m_versB = Version(ves);
156 		} else {
157 			m_cmpA = skipComp(ves);
158 			size_t idx2 = std..string.indexOf(ves, " ");
159 			if (idx2 == -1) {
160 				if (m_cmpA == "<=" || m_cmpA == "<") {
161 					m_versA = Version.RELEASE;
162 					m_cmpB = m_cmpA;
163 					m_cmpA = ">=";
164 					m_versB = Version(ves);
165 				} else if (m_cmpA == ">=" || m_cmpA == ">") {
166 					m_versA = Version(ves);
167 					m_versB = Version.HEAD;
168 					m_cmpB = "<=";
169 				} else {
170 					// Converts "==" to ">=a&&<=a", which makes merging easier
171 					m_versA = m_versB = Version(ves);
172 					m_cmpA = ">=";
173 					m_cmpB = "<=";
174 				}
175 			} else {
176 				assert(ves[idx2] == ' ');
177 				m_versA = Version(ves[0..idx2]);
178 				string v2 = ves[idx2+1..$];
179 				m_cmpB = skipComp(v2);
180 				m_versB = Version(v2);
181 
182 				enforce(!m_versA.isBranch, "Partly a branch (A): %s", ves);
183 				enforce(!m_versB.isBranch, "Partly a branch (B): %s", ves);
184 
185 				if (m_versB < m_versA) {
186 					swap(m_versA, m_versB);
187 					swap(m_cmpA, m_cmpB);
188 				}
189 				enforce( m_cmpA != "==" && m_cmpB != "==", "For equality, please specify a single version.");
190 			}
191 		}
192 	}
193 
194 	this(in Version ver)
195 	{
196 		m_cmpA = ">=";
197 		m_cmpB = "<=";
198 		m_versA = ver;
199 		m_versB = ver;
200 	}
201 
202 	@property void path(Path value) { m_path = value; }
203 	@property Path path() const { return m_path; }
204 	@property bool optional() const { return m_optional; }
205 	@property void optional(bool optional) { m_optional = optional; }
206 
207 	@property Version version_() const { assert(m_versA == m_versB); return m_versA; }
208 	
209 	string toString()
210 	const {
211 		string r;
212 		// Special "==" case
213 		if( m_versA == m_versB && m_cmpA == ">=" && m_cmpB == "<=" ){
214 			if( m_versA == Version.MASTER ) r = "~master";
215 			else r = "==" ~ to!string(m_versA);
216 		} else {
217 			if( m_versA != Version.RELEASE ) r = m_cmpA ~ to!string(m_versA);
218 			if( m_versB != Version.HEAD ) r ~= (r.length==0?"" : " ") ~ m_cmpB ~ to!string(m_versB);
219 			if( m_versA == Version.RELEASE && m_versB == Version.HEAD ) r = ">=0.0.0";
220 		}
221 		// TODO(mdondorff): add information to path and optionality.
222 		return r;
223 	}
224 
225 	bool opEquals(in Dependency o)
226 	const {
227 		// TODO(mdondorff): Check if not comparing the path is correct for all clients.
228 		return o.m_cmpA == m_cmpA && o.m_cmpB == m_cmpB 
229 			&& o.m_versA == m_versA && o.m_versB == m_versB 
230 			&& o.m_configuration == m_configuration
231 			&& o.m_optional == m_optional;
232 	}
233 	
234 	bool valid() const {
235 		return m_versA == m_versB // compare not important
236 			|| (m_versA < m_versB && doCmp(m_cmpA, m_versB, m_versA) && doCmp(m_cmpB, m_versA, m_versB));
237 	}
238 	
239 	bool matches(string vers) const { return matches(Version(vers)); }
240 	bool matches(const(Version) v) const { return matches(v); }
241 	bool matches(ref const(Version) v) const {
242 		//logDebug(" try match: %s with: %s", v, this);
243 		// Master only matches master
244 		if(m_versA == Version.MASTER || m_versA.isBranch) {
245 			enforce(m_versA == m_versB);
246 			return m_versA == v;
247 		}
248 		if(v.isBranch)
249 			return m_versA == v;
250 		if(m_versA == Version.MASTER || v == Version.MASTER)
251 			return m_versA == v;
252 		if( !doCmp(m_cmpA, v, m_versA) )
253 			return false;
254 		if( !doCmp(m_cmpB, v, m_versB) )
255 			return false;
256 		return true;
257 	}
258 	
259 	/// Merges to versions
260 	Dependency merge(ref const(Dependency) o) const {
261 		if (!valid()) return this;
262 		if (!o.valid()) return o;
263 		if (m_configuration != o.m_configuration)
264 			return Dependency(">=1.0.0 <=0.0.0");
265 		
266 		Version a = m_versA > o.m_versA? m_versA : o.m_versA;
267 		Version b = m_versB < o.m_versB? m_versB : o.m_versB;
268 	
269 		Dependency d = this;
270 		d.m_cmpA = !doCmp(m_cmpA, a,a)? m_cmpA : o.m_cmpA;
271 		d.m_versA = a;
272 		d.m_cmpB = !doCmp(m_cmpB, b,b)? m_cmpB : o.m_cmpB;
273 		d.m_versB = b;
274 		d.m_optional = m_optional && o.m_optional;
275 		
276 		return d;
277 	}
278 	
279 	private static bool isDigit(char ch) { return ch >= '0' && ch <= '9'; }
280 	private static string skipComp(ref string c) {
281 		size_t idx = 0;
282 		while( idx < c.length && !isDigit(c[idx]) ) idx++;
283 		enforce(idx < c.length, "Expected version number in version spec: "~c);
284 		string cmp = idx==c.length-1||idx==0? ">=" : c[0..idx];
285 		c = c[idx..$];
286 		switch(cmp) {
287 			default: enforce(false, "No/Unknown comparision specified: '"~cmp~"'"); return ">=";
288 			case ">=": goto case; case ">": goto case;
289 			case "<=": goto case; case "<": goto case;
290 			case "==": return cmp;
291 		}
292 	}
293 	
294 	private static bool doCmp(string mthd, ref const Version a, ref const Version b) {
295 		//logDebug("Calling %s%s%s", a, mthd, b);
296 		switch(mthd) {
297 			default: throw new Exception("Unknown comparison operator: "~mthd);
298 			case ">": return a>b;
299 			case ">=": return a>=b;
300 			case "==": return a==b;
301 			case "<=": return a<=b;
302 			case "<": return a<b;
303 		}
304 	}
305 }
306 
307 unittest {
308 	Dependency a = Dependency(">=1.1.0"), b = Dependency(">=1.3.0");
309 	assert( a.merge(b).valid() && to!string(a.merge(b)) == ">=1.3.0", to!string(a.merge(b)) );
310 	
311 	a = Dependency("<=1.0.0 >=2.0.0");
312 	assert( !a.valid(), to!string(a) );
313 	
314 	a = Dependency(">=1.0.0 <=5.0.0"), b = Dependency(">=2.0.0");
315 	assert( a.merge(b).valid() && to!string(a.merge(b)) == ">=2.0.0 <=5.0.0", to!string(a.merge(b)) );
316 	
317 	assertThrown(a = Dependency(">1.0.0 ==5.0.0"), "Construction is invalid");
318 	
319 	a = Dependency(">1.0.0"), b = Dependency("<2.0.0");
320 	assert( a.merge(b).valid(), to!string(a.merge(b)));
321 	assert( to!string(a.merge(b)) == ">1.0.0 <2.0.0", to!string(a.merge(b)) );
322 	
323 	a = Dependency(">2.0.0"), b = Dependency("<1.0.0");
324 	assert( !(a.merge(b)).valid(), to!string(a.merge(b)));
325 	
326 	a = Dependency(">=2.0.0"), b = Dependency("<=1.0.0");
327 	assert( !(a.merge(b)).valid(), to!string(a.merge(b)));
328 	
329 	a = Dependency("==2.0.0"), b = Dependency("==1.0.0");
330 	assert( !(a.merge(b)).valid(), to!string(a.merge(b)));
331 	
332 	a = Dependency("<=2.0.0"), b = Dependency("==1.0.0");
333 	Dependency m = a.merge(b);
334 	assert( m.valid(), to!string(m));
335 	assert( m.matches( Version("1.0.0") ) );
336 	assert( !m.matches( Version("1.1.0") ) );
337 	assert( !m.matches( Version("0.0.1") ) );
338 
339 
340 	// branches / head revisions
341 	a = Dependency(Version.MASTER_STRING);
342 	assert(a.valid());
343 	assert(a.matches(Version.MASTER));
344 	b = Dependency(Version.MASTER_STRING);
345 	m = a.merge(b);
346 	assert(m.matches(Version.MASTER));
347 
348 	//assertThrown(a = Dependency(Version.MASTER_STRING ~ " <=1.0.0"), "Construction invalid");
349 	assertThrown(a = Dependency(">=1.0.0 " ~ Version.MASTER_STRING), "Construction invalid");
350 
351 	a = Dependency(">=1.0.0");
352 	b = Dependency(Version.MASTER_STRING);
353 
354 	//// support crazy stuff like this?
355 	//m = a.merge(b);
356 	//assert(m.valid());
357 	//assert(m.matches(Version.MASTER));
358 
359 	//b = Dependency("~not_the_master");
360 	//m = a.merge(b);
361 //	assert(!m.valid());
362 
363 	immutable string branch1 = Version.BRANCH_IDENT ~ "Branch1";
364 	immutable string branch2 = Version.BRANCH_IDENT ~ "Branch2";
365 
366 	//assertThrown(a = Dependency(branch1 ~ " " ~ branch2), "Error: '" ~ branch1 ~ " " ~ branch2 ~ "' succeeded");
367 	//assertThrown(a = Dependency(Version.MASTER_STRING ~ " " ~ branch1), "Error: '" ~ Version.MASTER_STRING ~ " " ~ branch1 ~ "' succeeded");
368 
369 	a = Dependency(branch1);
370 	b = Dependency(branch2);
371 	assertThrown(a.merge(b), "Shouldn't be able to merge to different branches");
372 	assertNotThrown(b = a.merge(a), "Should be able to merge the same branches. (?)");
373 	assert(a == b);
374 
375 	a = Dependency(branch1);
376 	assert(a.matches(branch1), "Dependency(branch1) does not match 'branch1'");
377 	assert(a.matches(Version(branch1)), "Dependency(branch1) does not match Version('branch1')");
378 	assert(!a.matches(Version.MASTER), "Dependency(branch1) matches Version.MASTER");
379 	assert(!a.matches(branch2), "Dependency(branch1) matches 'branch2'");
380 	assert(!a.matches(Version("1.0.0")), "Dependency(branch1) matches '1.0.0'");
381 	a = Dependency(">=1.0.0");
382 	assert(!a.matches(Version(branch1)), "Dependency(1.0.0) matches 'branch1'");
383 
384 	// Testing optional dependencies.
385 	a = Dependency(">=1.0.0");
386 	assert(!a.optional, "Default is not optional.");
387 	b = a;
388 	assert(!a.merge(b).optional, "Merging two not optional dependencies wrong.");
389 	a.optional = true;
390 	assert(!a.merge(b).optional, "Merging optional with not optional wrong.");
391 	b.optional = true;
392 	assert(a.merge(b).optional, "Merging two optional dependencies wrong.");
393 
394 	logDebug("Dependency Unittest sucess.");
395 }
396 
397 struct RequestedDependency {
398 	this( string pkg, Dependency de) {
399 		dependency = de;
400 		packages[pkg] = de;
401 	}
402 	Dependency dependency;
403 	Dependency[string] packages;
404 }
405 
406 class DependencyGraph {	
407 	this(const Package root) {
408 		m_root = root;
409 		m_packages[m_root.name] = root;
410 	}
411 	
412 	void insert(const Package p) {
413 		enforce(p.name != m_root.name, format("Dependency with the same name as the root package (%s) detected.", p.name));
414 		m_packages[p.name] = p;
415 	}
416 	
417 	void remove(const Package p) {
418 		enforce(p.name != m_root.name);
419 		Rebindable!(const Package)* pkg = p.name in m_packages;
420 		if( pkg ) m_packages.remove(p.name);
421 	}
422 	
423 	private
424 	{
425 		alias Rebindable!(const Package) PkgType;
426 	}
427 	
428 	void clearUnused() {
429 		Rebindable!(const Package)[string] unused = m_packages.dup;
430 		unused.remove(m_root.name);
431 		forAllDependencies( (const PkgType* avail, string s, Dependency d, const Package issuer) {
432 			if(avail && d.matches(avail.vers))
433 				unused.remove(avail.name);
434 		});
435 		foreach(string unusedPkg, d; unused) {
436 			logDebug("Removed unused package: "~unusedPkg);
437 			m_packages.remove(unusedPkg);
438 		}
439 	}
440 	
441 	RequestedDependency[string] conflicted() const {
442 		RequestedDependency[string] deps = needed();
443 		RequestedDependency[string] conflicts;
444 		foreach(string pkg, d; deps)
445 			if(!d.dependency.valid())
446 				conflicts[pkg] = d;
447 		return conflicts;
448 	}
449 	
450 	RequestedDependency[string] missing() const {
451 		RequestedDependency[string] deps;
452 		forAllDependencies( (const PkgType* avail, string pkgId, Dependency d, const Package issuer) {
453 			if(!d.optional && (!avail || !d.matches(avail.vers)))
454 				addDependency(deps, pkgId, d, issuer);
455 		});
456 		return deps;
457 	}
458 	
459 	RequestedDependency[string] needed() const {
460 		RequestedDependency[string] deps;
461 		forAllDependencies( (const PkgType* avail, string pkgId, Dependency d, const Package issuer) {
462 			if(!d.optional)
463 				addDependency(deps, pkgId, d, issuer);
464 		});
465 		return deps;
466 	}
467 
468 	RequestedDependency[string] optional() const {
469 		RequestedDependency[string] allDeps;
470 		forAllDependencies( (const PkgType* avail, string pkgId, Dependency d, const Package issuer) {
471 			addDependency(allDeps, pkgId, d, issuer);
472 		});
473 		RequestedDependency[string] optionalDeps;
474 		foreach(id, req; allDeps)
475 			if(req.dependency.optional) optionalDeps[id] = req;
476 		return optionalDeps;
477 	}
478 	
479 	private void forAllDependencies(void delegate (const PkgType* avail, string pkgId, Dependency d, const Package issuer) dg) const {
480 		foreach(string issuerPackag, issuer; m_packages) {
481 			foreach(string depPkg, dependency; issuer.dependencies) {
482 				auto availPkg = depPkg in m_packages;
483 				dg(availPkg, depPkg, dependency, issuer);
484 			}
485 		}
486 	}
487 	
488 	private static void addDependency(ref RequestedDependency[string] deps, string packageId, Dependency d, const Package issuer) {
489 		auto d2 = packageId in deps;
490 		if(!d2) {
491 			deps[packageId] = RequestedDependency(issuer.name, d);
492 		}
493 		else {
494 			d2.dependency = d2.dependency.merge(d);
495 			d2.packages[issuer.name] = d;
496 		}
497 	}
498 	
499 	private {
500 		const Package m_root;
501 		PkgType[string] m_packages;
502 	}
503 
504 	unittest {
505 		/*
506 			R (master) -> A (master)
507 		*/
508 		auto R_json = parseJsonString(`
509 		{
510 			"name": "R",
511 			"dependencies": {
512 				"A": "~master",
513 				"B": "1.0.0"
514 			},
515 			"version": "~master"
516 		}
517 			`);
518 		Package r_master = new Package(R_json);
519 		auto graph = new DependencyGraph(r_master);
520 
521 		assert(graph.conflicted.length == 0, "There are conflicting packages");
522 
523 		void expectA(RequestedDependency[string] requested, string name) {
524 			assert("A" in requested, "Package A is not the "~name~" package");
525 			assert(requested["A"].dependency == Dependency("~master"), "Package A is not "~name~" as ~master version.");
526 			assert("R" in requested["A"].packages, "Package R is not the issuer of "~name~" Package A(~master).");
527 			assert(requested["A"].packages["R"] == Dependency("~master"), "Package R is not the issuer of "~name~" Package A(~master).");
528 		}
529 		void expectB(RequestedDependency[string] requested, string name) {
530 			assert("B" in requested, "Package B is not the "~name~" package");
531 			assert(requested["B"].dependency == Dependency("1.0.0"), "Package B is not "~name~" as 1.0.0 version.");
532 			assert("R" in requested["B"].packages, "Package R is not the issuer of "~name~" Package B(1.0.0).");
533 			assert(requested["B"].packages["R"] == Dependency("1.0.0"), "Package R is not the issuer of "~name~" Package B(1.0.0).");
534 		}
535 		auto missing = graph.missing();
536 		assert(missing.length == 2, "Invalid count of missing items");
537 		expectA(missing, "missing");
538 		expectB(missing, "missing");
539 
540 		auto needed = graph.needed();
541 		assert(needed.length == 2, "Invalid count of needed packages.");		
542 		expectA(needed, "needed");
543 		expectB(needed, "needed");
544 
545 		assert(graph.optional.length == 0, "There are optional packages reported");
546 
547 		auto A_json = parseJsonString(`
548 		{
549 			"name": "A",
550 			"dependencies": {
551 			},
552 			"version": "~master"
553 		}
554 			`);
555 		Package a_master = new Package(A_json);
556 		graph.insert(a_master);
557 
558 		assert(graph.conflicted.length == 0, "There are conflicting packages");
559 
560 		auto missing2 = graph.missing;
561 		assert(missing2.length == 1, "Missing list does not contain an package.");
562 		expectB(missing2, "missing2");
563 
564 		needed = graph.needed;
565 		assert(needed.length == 2, "Invalid count of needed packages.");		
566 		expectA(needed, "needed");
567 		expectB(needed, "needed");
568 
569 		assert(graph.optional.length == 0, "There are optional packages reported");
570 	}
571 
572 	unittest {
573 		/*
574 			R -> R:sub
575 		*/
576 		auto R_json = parseJsonString(`
577 		{
578 			"name": "R",
579 			"dependencies": {
580 				"R:sub": "~master"
581 			},
582 			"version": "~master",
583 			"subPackages": [
584 				{
585 					"name": "sub"
586 				}
587 			]
588 		}
589 			`);
590 
591 		Package r_master = new Package(R_json);
592 		auto graph = new DependencyGraph(r_master);
593 		assert(graph.missing().length == 1);
594 		// Subpackages need to be explicitly added.
595 		graph.insert(r_master.subPackages[0]);
596 		assert(graph.missing().length == 0);
597 	}
598 
599 	unittest {
600 		/*
601 			R -> S:sub
602 		*/
603 		auto R_json = parseJsonString(`
604 		{
605 			"name": "R",
606 			"dependencies": {
607 				"S:sub": "~master"
608 			},
609 			"version": "~master"
610 		}
611 			`);
612 		auto S_w_sub_json = parseJsonString(`
613 		{
614 			"name": "S",
615 			"version": "~master",
616 			"subPackages": [
617 				{
618 					"name": "sub"
619 				}
620 			]
621 		}
622 			`);
623 		auto S_wout_sub_json = parseJsonString(`
624 		{
625 			"name": "S",
626 			"version": "~master"
627 		}
628 			`);
629 		auto sub_json = parseJsonString(`
630 		{
631 			"name": "sub",
632 			"version": "~master"
633 		}
634 			`);
635 
636 		Package r_master = new Package(R_json);
637 		auto graph = new DependencyGraph(r_master);
638 		assert(graph.missing().length == 1);
639 		Package s_master = new Package(S_w_sub_json);
640 		graph.insert(s_master);
641 		assert(graph.missing().length == 1);
642 		graph.insert(s_master.subPackages[0]);
643 		assert(graph.missing().length == 0);
644 
645 		graph = new DependencyGraph(r_master);
646 		assert(graph.missing().length == 1);
647 		s_master = new Package(S_wout_sub_json);
648 		graph.insert(s_master);
649 		assert(graph.missing().length == 1);
650 
651 		graph = new DependencyGraph(r_master);
652 		assert(graph.missing().length == 1);
653 		s_master = new Package(sub_json);
654 		graph.insert(s_master);
655 		assert(graph.missing().length == 1);
656 	}
657 }