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