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 }