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