1 /** 2 ... 3 4 Copyright: © 2012 Matthias Dondorff 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Matthias Dondorff 7 */ 8 module dub.internal.utils; 9 10 import dub.internal.vibecompat.core.file; 11 import dub.internal.vibecompat.data.json; 12 import dub.internal.vibecompat.inet.url; 13 import dub.compilers.buildsettings : BuildSettings; 14 import dub.version_; 15 import dub.internal.logging; 16 17 import core.time : Duration; 18 import std.algorithm : canFind, startsWith; 19 import std.array : appender, array; 20 import std.conv : to; 21 import std.exception : enforce; 22 import std.file; 23 import std.format; 24 import std.string : format; 25 import std.process; 26 import std.traits : isIntegral; 27 version(DubUseCurl) 28 { 29 import std.net.curl; 30 public import std.net.curl : HTTPStatusException; 31 } 32 33 34 private NativePath[] temporary_files; 35 36 NativePath getTempDir() 37 { 38 return NativePath(std.file.tempDir()); 39 } 40 41 NativePath getTempFile(string prefix, string extension = null) 42 { 43 import std.uuid : randomUUID; 44 import std.array: replace; 45 46 string fileName = prefix ~ "-" ~ randomUUID.toString() ~ extension; 47 48 if (extension !is null && extension == ".d") 49 fileName = fileName.replace("-", "_"); 50 51 auto path = getTempDir() ~ fileName; 52 temporary_files ~= path; 53 return path; 54 } 55 56 /** 57 * Obtain a lock for a file at the given path. 58 * 59 * If the file cannot be locked within the given duration, 60 * an exception is thrown. The file will be created if it does not yet exist. 61 * Deleting the file is not safe as another process could create a new file 62 * with the same name. 63 * The returned lock will get unlocked upon destruction. 64 * 65 * Params: 66 * path = path to file that gets locked 67 * timeout = duration after which locking failed 68 * 69 * Returns: 70 * The locked file or an Exception on timeout. 71 */ 72 auto lockFile(string path, Duration timeout) 73 { 74 import core.thread : Thread; 75 import std.datetime, std.stdio : File; 76 import std.algorithm : move; 77 78 // Just a wrapper to hide (and destruct) the locked File. 79 static struct LockFile 80 { 81 // The Lock can't be unlinked as someone could try to lock an already 82 // opened fd while a new file with the same name gets created. 83 // Exclusive filesystem locks (O_EXCL, mkdir) could be deleted but 84 // aren't automatically freed when a process terminates, see #1149. 85 private File f; 86 } 87 88 auto file = File(path, "w"); 89 auto t0 = Clock.currTime(); 90 auto dur = 1.msecs; 91 while (true) 92 { 93 if (file.tryLock()) 94 return LockFile(move(file)); 95 enforce(Clock.currTime() - t0 < timeout, "Failed to lock '"~path~"'."); 96 if (dur < 1024.msecs) // exponentially increase sleep time 97 dur *= 2; 98 Thread.sleep(dur); 99 } 100 } 101 102 static ~this() 103 { 104 foreach (path; temporary_files) 105 { 106 auto spath = path.toNativeString(); 107 if (spath.exists) 108 std.file.remove(spath); 109 } 110 } 111 112 bool isWritableDir(NativePath p, bool create_if_missing = false) 113 { 114 import std.random; 115 auto fname = p ~ format("__dub_write_test_%08X", uniform(0, uint.max)); 116 if (create_if_missing && !exists(p.toNativeString())) mkdirRecurse(p.toNativeString()); 117 try openFile(fname, FileMode.createTrunc).close(); 118 catch (Exception) return false; 119 remove(fname.toNativeString()); 120 return true; 121 } 122 123 Json jsonFromFile(NativePath file, bool silent_fail = false) { 124 if( silent_fail && !existsFile(file) ) return Json.emptyObject; 125 auto f = openFile(file.toNativeString(), FileMode.read); 126 scope(exit) f.close(); 127 auto text = stripUTF8Bom(cast(string)f.readAll()); 128 return parseJsonString(text, file.toNativeString()); 129 } 130 131 /** 132 Read package info file content from archive. 133 File needs to be in root folder or in first 134 sub folder. 135 136 Params: 137 zip = path to archive file 138 fileName = Package file name 139 Returns: 140 package file content. 141 */ 142 string packageInfoFileFromZip(NativePath zip, out string fileName) { 143 import std.zip : ZipArchive, ArchiveMember; 144 import dub.package_ : packageInfoFiles; 145 146 auto f = openFile(zip, FileMode.read); 147 ubyte[] b = new ubyte[cast(size_t)f.size]; 148 f.rawRead(b); 149 f.close(); 150 auto archive = new ZipArchive(b); 151 alias PSegment = typeof (NativePath.init.head); 152 foreach (ArchiveMember am; archive.directory) { 153 auto path = NativePath(am.name).bySegment.array; 154 foreach (fil; packageInfoFiles) { 155 if ((path.length == 1 && path[0] == fil.filename) || (path.length == 2 && path[$-1].name == fil.filename)) { 156 fileName = fil.filename; 157 return stripUTF8Bom(cast(string) archive.expand(archive.directory[am.name])); 158 } 159 } 160 } 161 throw new Exception("No package descriptor found"); 162 } 163 164 void writeJsonFile(NativePath path, Json json) 165 { 166 auto f = openFile(path, FileMode.createTrunc); 167 scope(exit) f.close(); 168 f.writePrettyJsonString(json); 169 } 170 171 /// Performs a write->delete->rename sequence to atomically "overwrite" the destination file 172 void atomicWriteJsonFile(NativePath path, Json json) 173 { 174 import std.random : uniform; 175 auto tmppath = path.parentPath ~ format("%s.%s.tmp", path.head, uniform(0, int.max)); 176 auto f = openFile(tmppath, FileMode.createTrunc); 177 scope (failure) { 178 f.close(); 179 removeFile(tmppath); 180 } 181 f.writePrettyJsonString(json); 182 f.close(); 183 if (existsFile(path)) removeFile(path); 184 moveFile(tmppath, path); 185 } 186 187 bool existsDirectory(NativePath path) { 188 if( !existsFile(path) ) return false; 189 auto fi = getFileInfo(path); 190 return fi.isDirectory; 191 } 192 193 void runCommand(string command, string[string] env = null, string workDir = null) 194 { 195 runCommands((&command)[0 .. 1], env, workDir); 196 } 197 198 void runCommands(in string[] commands, string[string] env = null, string workDir = null) 199 { 200 import std.stdio : stdin, stdout, stderr, File; 201 202 version(Windows) enum nullFile = "NUL"; 203 else version(Posix) enum nullFile = "/dev/null"; 204 else static assert(0); 205 206 auto childStdout = stdout; 207 auto childStderr = stderr; 208 auto config = Config.retainStdout | Config.retainStderr; 209 210 // Disable child's stdout/stderr depending on LogLevel 211 auto logLevel = getLogLevel(); 212 if(logLevel >= LogLevel.warn) 213 childStdout = File(nullFile, "w"); 214 if(logLevel >= LogLevel.none) 215 childStderr = File(nullFile, "w"); 216 217 foreach(cmd; commands){ 218 logDiagnostic("Running %s", cmd); 219 Pid pid; 220 pid = spawnShell(cmd, stdin, childStdout, childStderr, env, config, workDir); 221 auto exitcode = pid.wait(); 222 enforce(exitcode == 0, "Command failed with exit code " 223 ~ to!string(exitcode) ~ ": " ~ cmd); 224 } 225 } 226 227 version (Have_vibe_d_http) 228 public import vibe.http.common : HTTPStatusException; 229 230 /** 231 Downloads a file from the specified URL. 232 233 Any redirects will be followed until the actual file resource is reached or if the redirection 234 limit of 10 is reached. Note that only HTTP(S) is currently supported. 235 236 The download times out if a connection cannot be established within 237 `timeout` ms, or if the average transfer rate drops below 10 bytes / s for 238 more than `timeout` seconds. Pass `0` as `timeout` to disable both timeout 239 mechanisms. 240 241 Note: Timeouts are only implemented when curl is used (DubUseCurl). 242 */ 243 void download(string url, string filename, uint timeout = 8) 244 { 245 version(DubUseCurl) { 246 auto conn = HTTP(); 247 setupHTTPClient(conn, timeout); 248 logDebug("Storing %s...", url); 249 std.net.curl.download(url, filename, conn); 250 // workaround https://issues.dlang.org/show_bug.cgi?id=18318 251 auto sl = conn.statusLine; 252 logDebug("Download %s %s", url, sl); 253 if (sl.code / 100 != 2) 254 throw new HTTPStatusException(sl.code, 255 "Downloading %s failed with %d (%s).".format(url, sl.code, sl.reason)); 256 } else version (Have_vibe_d_http) { 257 import vibe.inet.urltransfer; 258 vibe.inet.urltransfer.download(url, filename); 259 } else assert(false); 260 } 261 /// ditto 262 void download(URL url, NativePath filename, uint timeout = 8) 263 { 264 download(url.toString(), filename.toNativeString(), timeout); 265 } 266 /// ditto 267 ubyte[] download(string url, uint timeout = 8) 268 { 269 version(DubUseCurl) { 270 auto conn = HTTP(); 271 setupHTTPClient(conn, timeout); 272 logDebug("Getting %s...", url); 273 return cast(ubyte[])get(url, conn); 274 } else version (Have_vibe_d_http) { 275 import vibe.inet.urltransfer; 276 import vibe.stream.operations; 277 ubyte[] ret; 278 vibe.inet.urltransfer.download(url, (scope input) { ret = input.readAll(); }); 279 return ret; 280 } else assert(false); 281 } 282 /// ditto 283 ubyte[] download(URL url, uint timeout = 8) 284 { 285 return download(url.toString(), timeout); 286 } 287 288 /** 289 Downloads a file from the specified URL with retry logic. 290 291 Downloads a file from the specified URL with up to n tries on failure 292 Throws: `Exception` if the download failed or `HTTPStatusException` after the nth retry or 293 on "unrecoverable failures" such as 404 not found 294 Otherwise might throw anything else that `download` throws. 295 See_Also: download 296 297 The download times out if a connection cannot be established within 298 `timeout` ms, or if the average transfer rate drops below 10 bytes / s for 299 more than `timeout` seconds. Pass `0` as `timeout` to disable both timeout 300 mechanisms. 301 302 Note: Timeouts are only implemented when curl is used (DubUseCurl). 303 **/ 304 void retryDownload(URL url, NativePath filename, size_t retryCount = 3, uint timeout = 8) 305 { 306 foreach(i; 0..retryCount) { 307 version(DubUseCurl) { 308 try { 309 download(url, filename, timeout); 310 return; 311 } 312 catch(HTTPStatusException e) { 313 if (e.status == 404) throw e; 314 else { 315 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 316 if (i == retryCount - 1) throw e; 317 else continue; 318 } 319 } 320 catch(CurlException e) { 321 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 322 continue; 323 } 324 } 325 else 326 { 327 try { 328 download(url, filename); 329 return; 330 } 331 catch(HTTPStatusException e) { 332 if (e.status == 404) throw e; 333 else { 334 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 335 if (i == retryCount - 1) throw e; 336 else continue; 337 } 338 } 339 } 340 } 341 throw new Exception("Failed to download %s".format(url)); 342 } 343 344 ///ditto 345 ubyte[] retryDownload(URL url, size_t retryCount = 3, uint timeout = 8) 346 { 347 foreach(i; 0..retryCount) { 348 version(DubUseCurl) { 349 try { 350 return download(url, timeout); 351 } 352 catch(HTTPStatusException e) { 353 if (e.status == 404) throw e; 354 else { 355 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 356 if (i == retryCount - 1) throw e; 357 else continue; 358 } 359 } 360 catch(CurlException e) { 361 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 362 continue; 363 } 364 } 365 else 366 { 367 try { 368 return download(url); 369 } 370 catch(HTTPStatusException e) { 371 if (e.status == 404) throw e; 372 else { 373 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 374 if (i == retryCount - 1) throw e; 375 else continue; 376 } 377 } 378 } 379 } 380 throw new Exception("Failed to download %s".format(url)); 381 } 382 383 /// Returns the current DUB version in semantic version format 384 string getDUBVersion() 385 { 386 import dub.version_; 387 import std.array : split, join; 388 // convert version string to valid SemVer format 389 auto verstr = dubVersion; 390 if (verstr.startsWith("v")) verstr = verstr[1 .. $]; 391 auto parts = verstr.split("-"); 392 if (parts.length >= 3) { 393 // detect GIT commit suffix 394 if (parts[$-1].length == 8 && parts[$-1][1 .. $].isHexNumber() && parts[$-2].isNumber()) 395 verstr = parts[0 .. $-2].join("-") ~ "+" ~ parts[$-2 .. $].join("-"); 396 } 397 return verstr; 398 } 399 400 401 /** 402 Get current executable's path if running as DUB executable, 403 or find a DUB executable if DUB is used as a library. 404 For the latter, the following locations are checked in order: 405 $(UL 406 $(LI current working directory) 407 $(LI same directory as `compilerBinary` (if supplied)) 408 $(LI all components of the `$PATH` variable) 409 ) 410 Params: 411 compilerBinary = optional path to a D compiler executable, used to locate DUB executable 412 Returns: 413 The path to a valid DUB executable 414 Throws: 415 an Exception if no valid DUB executable is found 416 */ 417 public string getDUBExePath(in string compilerBinary=null) 418 { 419 version(DubApplication) { 420 import std.file : thisExePath; 421 return thisExePath(); 422 } 423 else { 424 // this must be dub as a library 425 import std.algorithm : filter, map, splitter; 426 import std.array : array; 427 import std.file : exists, getcwd; 428 import std.path : chainPath, dirName; 429 import std.range : chain, only, take; 430 import std.process : environment; 431 432 version(Windows) { 433 enum exeName = "dub.exe"; 434 enum pathSep = ';'; 435 } 436 else { 437 enum exeName = "dub"; 438 enum pathSep = ':'; 439 } 440 441 auto dubLocs = only( 442 getcwd().chainPath(exeName), 443 compilerBinary.dirName.chainPath(exeName), 444 ) 445 .take(compilerBinary.length ? 2 : 1) 446 .chain( 447 environment.get("PATH", "") 448 .splitter(pathSep) 449 .map!(p => p.chainPath(exeName)) 450 ) 451 .filter!exists; 452 453 enforce(!dubLocs.empty, "Could not find DUB executable"); 454 return dubLocs.front.array; 455 } 456 } 457 458 459 version(DubUseCurl) { 460 void setupHTTPClient(ref HTTP conn, uint timeout) 461 { 462 static if( is(typeof(&conn.verifyPeer)) ) 463 conn.verifyPeer = false; 464 465 auto proxy = environment.get("http_proxy", null); 466 if (proxy.length) conn.proxy = proxy; 467 468 auto noProxy = environment.get("no_proxy", null); 469 if (noProxy.length) conn.handle.set(CurlOption.noproxy, noProxy); 470 471 conn.handle.set(CurlOption.encoding, ""); 472 if (timeout) { 473 // connection (TLS+TCP) times out after 8s 474 conn.handle.set(CurlOption.connecttimeout, timeout); 475 // transfers time out after 8s below 10 byte/s 476 conn.handle.set(CurlOption.low_speed_limit, 10); 477 conn.handle.set(CurlOption.low_speed_time, timeout); 478 } 479 480 conn.addRequestHeader("User-Agent", "dub/"~getDUBVersion()~" (std.net.curl; +https://github.com/rejectedsoftware/dub)"); 481 482 enum CURL_NETRC_OPTIONAL = 1; 483 conn.handle.set(CurlOption.netrc, CURL_NETRC_OPTIONAL); 484 } 485 } 486 487 string stripUTF8Bom(string str) 488 { 489 if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] ) 490 return str[3 ..$]; 491 return str; 492 } 493 494 private bool isNumber(string str) { 495 foreach (ch; str) 496 switch (ch) { 497 case '0': .. case '9': break; 498 default: return false; 499 } 500 return true; 501 } 502 503 private bool isHexNumber(string str) { 504 foreach (ch; str) 505 switch (ch) { 506 case '0': .. case '9': break; 507 case 'a': .. case 'f': break; 508 case 'A': .. case 'F': break; 509 default: return false; 510 } 511 return true; 512 } 513 514 /** 515 Get the closest match of $(D input) in the $(D array), where $(D distance) 516 is the maximum levenshtein distance allowed between the compared strings. 517 Returns $(D null) if no closest match is found. 518 */ 519 string getClosestMatch(string[] array, string input, size_t distance) 520 { 521 import std.algorithm : countUntil, map, levenshteinDistance; 522 import std.uni : toUpper; 523 524 auto distMap = array.map!(elem => 525 levenshteinDistance!((a, b) => toUpper(a) == toUpper(b))(elem, input)); 526 auto idx = distMap.countUntil!(a => a <= distance); 527 return (idx == -1) ? null : array[idx]; 528 } 529 530 /** 531 Searches for close matches to input in range. R must be a range of strings 532 Note: Sorts the strings range. Use std.range.indexed to avoid this... 533 */ 534 auto fuzzySearch(R)(R strings, string input){ 535 import std.algorithm : levenshteinDistance, schwartzSort, partition3; 536 import std.traits : isSomeString; 537 import std.range : ElementType; 538 539 static assert(isSomeString!(ElementType!R), "Cannot call fuzzy search on non string rang"); 540 immutable threshold = input.length / 4; 541 return strings.partition3!((a, b) => a.length + threshold < b.length)(input)[1] 542 .schwartzSort!(p => levenshteinDistance(input.toUpper, p.toUpper)); 543 } 544 545 /** 546 If T is a bitfield-style enum, this function returns a string range 547 listing the names of all members included in the given value. 548 549 Example: 550 --------- 551 enum Bits { 552 none = 0, 553 a = 1<<0, 554 b = 1<<1, 555 c = 1<<2, 556 a_c = a | c, 557 } 558 559 assert( bitFieldNames(Bits.none).equals(["none"]) ); 560 assert( bitFieldNames(Bits.a).equals(["a"]) ); 561 assert( bitFieldNames(Bits.a_c).equals(["a", "c", "a_c"]) ); 562 --------- 563 */ 564 auto bitFieldNames(T)(T value) if(is(T==enum) && isIntegral!T) 565 { 566 import std.algorithm : filter, map; 567 import std.conv : to; 568 import std.traits : EnumMembers; 569 570 return [ EnumMembers!(T) ] 571 .filter!(member => member==0? value==0 : (value & member) == member) 572 .map!(member => to!string(member)); 573 } 574 575 576 bool isIdentChar(dchar ch) 577 { 578 import std.ascii : isAlphaNum; 579 return isAlphaNum(ch) || ch == '_'; 580 } 581 582 string stripDlangSpecialChars(string s) 583 { 584 import std.array : appender; 585 auto ret = appender!string(); 586 foreach(ch; s) 587 ret.put(isIdentChar(ch) ? ch : '_'); 588 return ret.data; 589 } 590 591 string determineModuleName(BuildSettings settings, NativePath file, NativePath base_path) 592 { 593 import std.algorithm : map; 594 import std.array : array; 595 import std.range : walkLength; 596 597 assert(base_path.absolute); 598 if (!file.absolute) file = base_path ~ file; 599 600 size_t path_skip = 0; 601 foreach (ipath; settings.importPaths.map!(p => NativePath(p))) { 602 if (!ipath.absolute) ipath = base_path ~ ipath; 603 assert(!ipath.empty); 604 if (file.startsWith(ipath) && ipath.bySegment.walkLength > path_skip) 605 path_skip = ipath.bySegment.walkLength; 606 } 607 608 auto mpath = file.bySegment.array[path_skip .. $]; 609 auto ret = appender!string; 610 611 //search for module keyword in file 612 string moduleName = getModuleNameFromFile(file.to!string); 613 614 if(moduleName.length) { 615 assert(moduleName.length > 0, "Wasn't this module name already checked? what"); 616 return moduleName; 617 } 618 619 //create module name from path 620 if (path_skip == 0) 621 { 622 import std.path; 623 ret ~= mpath[$-1].name.baseName(".d"); 624 } 625 else 626 { 627 foreach (i; 0 .. mpath.length) { 628 import std.path; 629 auto p = mpath[i].name; 630 if (p == "package.d") break ; 631 if (ret.data.length > 0) ret ~= "."; 632 if (i+1 < mpath.length) ret ~= p; 633 else ret ~= p.baseName(".d"); 634 } 635 } 636 637 assert(ret.data.length > 0, "A module name was expected to be computed, and none was."); 638 return ret.data; 639 } 640 641 /** 642 * Search for module keyword in D Code 643 */ 644 string getModuleNameFromContent(string content) { 645 import std.regex; 646 import std.string; 647 648 content = content.strip; 649 if (!content.length) return null; 650 651 static bool regex_initialized = false; 652 static Regex!char comments_pattern, module_pattern; 653 654 if (!regex_initialized) { 655 comments_pattern = regex(`//[^\r\n]*\r?\n?|/\*.*?\*/|/\+.*\+/`, "gs"); 656 module_pattern = regex(`module\s+([\w\.]+)\s*;`, "g"); 657 regex_initialized = true; 658 } 659 660 content = replaceAll(content, comments_pattern, " "); 661 auto result = matchFirst(content, module_pattern); 662 663 if (!result.empty) return result[1]; 664 665 return null; 666 } 667 668 unittest { 669 assert(getModuleNameFromContent("") == ""); 670 assert(getModuleNameFromContent("module myPackage.myModule;") == "myPackage.myModule"); 671 assert(getModuleNameFromContent("module \t\n myPackage.myModule \t\r\n;") == "myPackage.myModule"); 672 assert(getModuleNameFromContent("// foo\nmodule bar;") == "bar"); 673 assert(getModuleNameFromContent("/*\nfoo\n*/\nmodule bar;") == "bar"); 674 assert(getModuleNameFromContent("/+\nfoo\n+/\nmodule bar;") == "bar"); 675 assert(getModuleNameFromContent("/***\nfoo\n***/\nmodule bar;") == "bar"); 676 assert(getModuleNameFromContent("/+++\nfoo\n+++/\nmodule bar;") == "bar"); 677 assert(getModuleNameFromContent("// module foo;\nmodule bar;") == "bar"); 678 assert(getModuleNameFromContent("/* module foo; */\nmodule bar;") == "bar"); 679 assert(getModuleNameFromContent("/+ module foo; +/\nmodule bar;") == "bar"); 680 assert(getModuleNameFromContent("/+ /+ module foo; +/ +/\nmodule bar;") == "bar"); 681 assert(getModuleNameFromContent("// module foo;\nmodule bar; // module foo;") == "bar"); 682 assert(getModuleNameFromContent("// module foo;\nmodule// module foo;\nbar//module foo;\n;// module foo;") == "bar"); 683 assert(getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;") == "bar", getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;")); 684 assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar;") == "bar"); 685 //assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar/++/;") == "bar"); // nested comments require a context-free parser! 686 assert(getModuleNameFromContent("/*\nmodule sometest;\n*/\n\nmodule fakemath;\n") == "fakemath"); 687 } 688 689 /** 690 * Search for module keyword in file 691 */ 692 string getModuleNameFromFile(string filePath) { 693 if (!filePath.exists) 694 { 695 return null; 696 } 697 string fileContent = filePath.readText; 698 699 logDiagnostic("Get module name from path: %s", filePath); 700 return getModuleNameFromContent(fileContent); 701 } 702 703 /** 704 * Compare two instances of the same type for equality, 705 * providing a rich error message on failure. 706 * 707 * This function will recurse into composite types (struct, AA, arrays) 708 * and compare element / member wise, taking opEquals into account, 709 * to provide the most accurate reason why comparison failed. 710 */ 711 void deepCompare (T) ( 712 in T result, in T expected, string file = __FILE__, size_t line = __LINE__) 713 { 714 deepCompareImpl!T(result, expected, T.stringof, file, line); 715 } 716 717 void deepCompareImpl (T) ( 718 in T result, in T expected, string path, string file, size_t line) 719 { 720 static if (is(T == struct) && !is(typeof(T.init.opEquals(T.init)) : bool)) 721 { 722 static foreach (idx; 0 .. T.tupleof.length) 723 deepCompareImpl(result.tupleof[idx], expected.tupleof[idx], 724 format("%s.%s", path, __traits(identifier, T.tupleof[idx])), 725 file, line); 726 } 727 else static if (is(T : KeyT[ValueT], KeyT, ValueT)) 728 { 729 if (result.length != expected.length) 730 throw new Exception( 731 format("%s: AA has different number of entries (%s != %s): %s != %s", 732 path, result.length, expected.length, result, expected), 733 file, line); 734 foreach (key, value; expected) 735 { 736 if (auto ptr = key in result) 737 deepCompareImpl(*ptr, value, format("%s[%s]", path, key), file, line); 738 else 739 throw new Exception( 740 format("Expected key %s[%s] not present in result. %s != %s", 741 path, key, result, expected), file, line); 742 } 743 } 744 else if (result != expected) { 745 static if (is(T == struct) && is(typeof(T.init.opEquals(T.init)) : bool)) 746 path ~= ".opEquals"; 747 throw new Exception( 748 format("%s: result != expected: %s != %s", path, result, expected), 749 file, line); 750 } 751 }