1 /++
2   A module containing custom exceptions for display convenience
3 
4   Copyright: © 2017 Szabo Bogdan
5   License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6   Authors: Szabo Bogdan
7 +/
8 module trial.stackresult;
9 
10 import std.conv;
11 import std.regex;
12 import std.exception;
13 import std.stdio;
14 import std..string;
15 import std.algorithm;
16 
17 import core.demangle;
18 
19 version (Have_fluent_asserts) { } else {
20   auto toTestException(Throwable t)
21   {
22     return t;
23   }
24 }
25 
26 version (Have_fluent_asserts) {
27 
28   import fluentasserts.core.base;
29   import fluentasserts.core.results;
30 
31   ///
32   class TestExceptionWrapper : TestException {
33     private {
34       TestException exception;
35       IResult[] results;
36     }
37 
38     ///
39     this(TestException exception, IResult[] results, string fileName, size_t line, Throwable next = null) {
40       this.exception = exception;
41       this.results = results;
42 
43       super(results, fileName, line, next);
44 
45       this.msg = exception.msg ~ "\n" ~ this.msg;
46     }
47 
48     ///
49     override void print(ResultPrinter printer) {
50       exception.print(printer);
51 
52       results.each!(a => a.print(printer));
53     }
54 
55     ///
56     override string toString() {
57       return exception.toString ~ results.map!(a => a.toString).join("\n").to!string;
58     }
59   }
60 
61   /// The message of a wrapped exception should contain the original exception
62   unittest {
63     auto exception = new TestException([ new MessageResult("first message") ], "", 0);
64     auto wrappedException = new TestExceptionWrapper(exception, [ new MessageResult("second message") ], "", 0);
65 
66     wrappedException.msg.should.equal("first message\n\nsecond message\n");
67   }
68 
69   /// Converts a Throwable to a TestException which improves the failure verbosity
70   TestException toTestException(Throwable t)
71   {
72     auto exception = cast(TestException) t;
73 
74     if (exception is null)
75     {
76       IResult[] results = [cast(IResult) new MessageResult(t.classinfo.name ~ ": " ~ t.msg)];
77 
78       if (t.file.indexOf("../") == -1)
79       {
80         results ~= cast(IResult) new SourceResult(t.file, t.line);
81       }
82 
83       if (t !is null && t.info !is null)
84       {
85         results ~= cast(IResult) new StackResult(t.info);
86       }
87 
88       exception = new TestException(results, t.file, t.line, t);
89     } else {
90       exception = new TestExceptionWrapper(exception, [ cast(IResult) new StackResult(t.info) ], t.file, t.line, t);
91     }
92 
93     return exception;
94   }
95 
96   @("toTestException should convert an Exception from the current project to a TestException with 2 reporters")
97   unittest
98   {
99     auto exception = new Exception("random text");
100     auto testException = exception.toTestException;
101 
102     (testException !is null).should.equal(true);
103     testException.toString.should.contain("random text");
104     testException.toString.should.contain("lifecycle/trial/runner.d");
105   }
106 
107   @("toTestException should convert an Exception from other project to a TestException with 1 reporter")
108   unittest
109   {
110     auto exception = new Exception("random text", "../file.d");
111     auto testException = exception.toTestException;
112 
113     (testException !is null).should.equal(true);
114     testException.toString.should.contain("random text");
115     testException.toString.should.not.contain("lifecycle/trial/runner.d");
116   }
117 
118   /// A structure that allows you to detect which modules are relevant to display
119   struct ExternalValidator
120   {
121 
122     /// The list of external modules like the standard library or dub dependencies
123     string[] externalModules;
124 
125     /// Check if the provided name comes from an external dependency
126     bool isExternal(const string name) @safe
127     {
128       auto reversed = name.dup;
129       reverse(reversed);
130 
131       string substring = name;
132       int sum = 0;
133       int index = 0;
134       foreach (ch; reversed)
135       {
136         if (ch == ')')
137         {
138           sum++;
139         }
140 
141         if (ch == '(')
142         {
143           sum--;
144         }
145 
146         if (sum == 0)
147         {
148           break;
149         }
150         index++;
151       }
152 
153       auto tmpSubstring = reversed[index .. $];
154       reverse(tmpSubstring);
155       substring = tmpSubstring.to!string;
156 
157       auto wordEnd = substring.lastIndexOf(' ') + 1;
158       auto chainEnd = substring.lastIndexOf(").") + 1;
159 
160       if (chainEnd > wordEnd)
161       {
162         return isExternal(name[0 .. chainEnd]);
163       }
164 
165       auto functionName = substring[wordEnd .. $];
166 
167       return !externalModules.filter!(a => functionName.indexOf(a) == 0).empty;
168     }
169   }
170 
171   @("It should detect external functions")
172   unittest
173   {
174     auto validator = ExternalValidator(["selenium.api", "selenium.session"]);
175 
176     validator.isExternal("selenium.api.SeleniumApiConnector selenium.api.SeleniumApiConnector.__ctor()")
177       .should.equal(true);
178 
179     validator.isExternal("void selenium.api.SeleniumApiConnector.__ctor()").should.equal(true);
180 
181     validator.isExternal(
182         "pure @safe bool selenium.api.enforce!(Exception, bool).enforce(bool, lazy const(char)[], immutable(char)[], ulong)")
183       .should.equal(true);
184 
185     validator.isExternal("immutable(immutable(selenium.session.SeleniumSession) function(immutable(char)[], selenium.api.Capabilities, selenium.api.Capabilities, selenium.api.Capabilities)) selenium.session.SeleniumSession.__ctor")
186       .should.equal(true);
187   }
188 
189   /// Used to display the stack
190   class StackResult : IResult
191   {
192     static
193     {
194       ///
195       string[] externalModules;
196     }
197 
198     ///
199     Frame[] frames;
200 
201     ///
202     this(Throwable.TraceInfo t)
203     {
204       foreach (line; t)
205       {
206         auto frame = line.to!string.toFrame;
207         frame.name = demangle(frame.name).to!string;
208         frames ~= frame;
209       }
210     }
211 
212     private
213     {
214       auto getFrames()
215       {
216         return frames.until!(a => a.name.indexOf("generated") != -1)
217           .until!(a => a.name.indexOf("D5trial") != -1);
218       }
219     }
220 
221     override
222     {
223       /// Converts the result to a string
224       string toString() @safe
225       {
226         string result = "Stack trace:\n-------------------\n...\n";
227 
228         foreach (frame; getFrames)
229         {
230           result ~= frame.toString ~ "\n";
231         }
232 
233         return result ~ "...";
234       }
235 
236       /// Prints the stack using the default writer
237       void print(ResultPrinter printer)
238       {
239         int colorIndex = 0;
240         printer.primary("Stack trace:\n-------------------\n...\n");
241 
242         auto validator = ExternalValidator(externalModules);
243 
244         foreach (frame; getFrames)
245         {
246           if (validator.isExternal(frame.name))
247           {
248             printer.primary(frame.toString);
249           }
250           else
251           {
252             frame.print(printer);
253           }
254 
255           printer.primary("\n");
256         }
257 
258         printer.primary("...");
259       }
260     }
261   }
262 
263   @("The stack result should display the stack in a readable form")
264   unittest
265   {
266     Throwable exception;
267 
268     try
269     {
270       assert(false, "random message");
271     }
272     catch (Throwable t)
273     {
274       exception = t;
275     }
276 
277     auto result = new StackResult(exception.info).toString;
278 
279     result.should.startWith("Stack trace:\n-------------------\n...");
280     result.should.endWith("\n...");
281   }
282 } else {
283 
284   /// Used to display the stack
285   class StackResult
286   {
287     static
288     {
289       ///
290       string[] externalModules;
291     }
292 
293     ///
294     Frame[] frames;
295 
296     ///
297     this(Throwable.TraceInfo t)
298     {
299       foreach (line; t)
300       {
301         auto frame = line.to!string.toFrame;
302         frame.name = demangle(frame.name).to!string;
303         frames ~= frame;
304       }
305     }
306 
307     private
308     {
309       auto getFrames()
310       {
311         return frames.until!(a => a.name.indexOf("generated") != -1)
312           .until!(a => a.name.indexOf("D5trial") != -1);
313       }
314     }
315 
316     override
317     {
318       /// Converts the result to a string
319       string toString() @safe
320       {
321         string result = "Stack trace:\n-------------------\n...\n";
322 
323         foreach (frame; getFrames)
324         {
325           result ~= frame.toString ~ "\n";
326         }
327 
328         return result ~ "...";
329       }
330     }
331   }
332 }
333 
334 /// Represents a stack frame
335 struct Frame
336 {
337   ///
338   int index = -1;
339 
340   ///
341   string moduleName;
342 
343   ///
344   string address;
345 
346   ///
347   string name;
348 
349   ///
350   string offset;
351 
352   ///
353   string file;
354 
355   ///
356   int line = -1;
357 
358   ///
359   bool invalid = true;
360 
361   ///
362   string raw;
363 
364   string toString() const @safe {
365     if(raw != "") {
366       return raw;
367     }
368 
369     string result;
370 
371     if(index >= 0) {
372       result ~= leftJustifier(index.to!string, 4).to!string;
373     }
374 
375     result ~= address ~ " ";
376     result ~= name == "" ? "????" : name;
377 
378     if(moduleName != "") {
379       result ~= " at " ~ moduleName;
380     }
381 
382     if(offset != "") {
383       result ~= " + " ~ offset;
384     }
385 
386     if(file != "") {
387       result ~= " (" ~ file;
388 
389       if(line > 0) {
390         result ~= ":" ~ line.to!string;
391       }
392 
393       result ~= ")";
394     }
395 
396     return result;
397   }
398 
399   version(Have_fluent_asserts) {
400     void print(ResultPrinter printer) @safe
401     {
402       if(raw != "") {
403         printer.primary(raw);
404 
405         return;
406       }
407 
408       if(index >= 0) {
409         printer.info(leftJustifier(index.to!string, 4).to!string);
410       }
411 
412       printer.primary(address ~ " ");
413       printer.info(name == "" ? "????" : name);
414 
415       if(moduleName != "") {
416         printer.primary(" at ");
417         printer.info(moduleName);
418       }
419 
420       if(offset != "") {
421         printer.primary(" + ");
422         printer.info(offset);
423       }
424 
425       if(file != "") {
426         printer.primary(" (");
427         printer.info(file);
428 
429         if(line > 0) {
430           printer.primary(":");
431           printer.info(line.to!string);
432         }
433 
434         printer.primary(")");
435       }
436     }
437   }
438 }
439 
440 /// The frame should convert a frame to string
441 unittest
442 {
443   Frame(10, "some.module", "0xffffff", "name", "offset", "file.d", 120).toString.should.equal(
444     `10  0xffffff name at some.module + offset (file.d:120)`
445   );
446 }
447 
448 /// The frame should not output an index < 0 or a line < 0
449 unittest
450 {
451   Frame(-1, "some.module", "0xffffff", "name", "offset", "file.d", -1).toString.should.equal(
452     `0xffffff name at some.module + offset (file.d)`
453   );
454 }
455 
456 /// The frame should not output the file if it is missing from the stack
457 unittest
458 {
459   Frame(-1, "some.module", "0xffffff", "name", "offset", "", 10).toString.should.equal(
460     `0xffffff name at some.module + offset`
461   );
462 }
463 
464 /// The frame should not output the module if it is missing from the stack
465 unittest
466 {
467   Frame(-1, "", "0xffffff", "name", "offset", "", 10).toString.should.equal(
468     `0xffffff name + offset`
469   );
470 }
471 
472 /// The frame should not output the offset if it is missing from the stack
473 unittest
474 {
475   Frame(-1, "", "0xffffff", "name", "", "", 10).toString.should.equal(
476     `0xffffff name`
477   );
478 }
479 
480 /// The frame should display `????` when the name is missing
481 unittest
482 {
483   Frame(-1, "", "0xffffff", "", "", "", 10).toString.should.equal(
484     `0xffffff ????`
485   );
486 }
487 
488 version(unittest) {
489   version(Have_fluent_asserts): 
490   class MockPrinter : ResultPrinter {
491     string buffer;
492 
493     void primary(string val) {
494       buffer ~= val;
495     }
496 
497     void info(string val) {
498       buffer ~= "[info:" ~ val ~ "]";
499     }
500 
501     void danger(string val) {
502       buffer ~= "[danger:" ~ val ~ "]";
503     }
504 
505     void success(string val) {
506       buffer ~= "[success:" ~ val ~ "]";
507     }
508 
509     void dangerReverse(string val) {
510       buffer ~= "[dangerReverse:" ~ val ~ "]";
511     }
512 
513     void successReverse(string val) {
514       buffer ~= "[successReverse:" ~ val ~ "]";
515     }
516   }
517 }
518 
519 /// The frame should print all fields
520 unittest
521 {
522   auto printer = new MockPrinter;
523   Frame(10, "some.module", "0xffffff", "name", "offset", "file.d", 120).print(printer);
524 
525   printer.buffer.should.equal(
526     `[info:10  ]0xffffff [info:name] at [info:some.module] + [info:offset] ([info:file.d]:[info:120])`
527   );
528 }
529 
530 /// The frame should not print an index < 0 or a line < 0
531 unittest
532 {
533   auto printer = new MockPrinter;
534   Frame(-1, "some.module", "0xffffff", "name", "offset", "file.d", -1).print(printer);
535 
536   printer.buffer.should.equal(
537     `0xffffff [info:name] at [info:some.module] + [info:offset] ([info:file.d])`
538   );
539 }
540 
541 /// The frame should not print the file if it's missing
542 unittest
543 {
544   auto printer = new MockPrinter;
545   Frame(-1, "some.module", "0xffffff", "name", "offset", "", 10).print(printer);
546 
547   printer.buffer.should.equal(
548     `0xffffff [info:name] at [info:some.module] + [info:offset]`
549   );
550 }
551 
552 /// The frame should not print the module if it's missing
553 unittest
554 {
555   auto printer = new MockPrinter;
556   Frame(-1, "", "0xffffff", "name", "offset", "", 10).print(printer);
557 
558   printer.buffer.should.equal(
559     `0xffffff [info:name] + [info:offset]`
560   );
561 }
562 
563 /// The frame should not print the offset if it's missing
564 unittest
565 {
566   auto printer = new MockPrinter;
567   Frame(-1, "", "0xffffff", "name", "", "", 10).print(printer);
568 
569   printer.buffer.should.equal(
570     `0xffffff [info:name]`
571   );
572 }
573 
574 /// The frame should print ???? when the name is missing
575 unittest
576 {
577   auto printer = new MockPrinter;
578   Frame(-1, "", "0xffffff", "", "", "", 10).print(printer);
579 
580   printer.buffer.should.equal(
581     `0xffffff [info:????]`
582   );
583 }
584 
585 immutable static
586 {
587   string index = `(?P<index>[0-9]+)`;
588   string moduleName = `(?P<module>\S+)`;
589   string address = `(?P<address>0x[0-9a-fA-F]+)`;
590   string name = `(?P<name>.+)`;
591   string offset = `(?P<offset>(0x[0-9A-Za-z]+)|([0-9]+))`;
592   string file = `(?P<file>.+)`;
593   string linePattern = `(?P<line>[0-9]+)`;
594 }
595 
596 /// Parse a MacOS string frame
597 Frame toDarwinFrame(string line)
598 {
599   Frame frame;
600 
601   auto darwinPattern = index ~ `(\s+)` ~ moduleName ~ `(\s+)` ~ address ~ `(\s+)`
602     ~ name ~ `\s\+\s` ~ offset;
603 
604   auto matched = matchFirst(line, darwinPattern);
605 
606   if(matched.length < 5) {
607     return frame;
608   }
609 
610   frame.invalid = false;
611   frame.index = matched["index"].to!int;
612   frame.moduleName = matched["module"];
613   frame.address = matched["address"];
614   frame.name = matched["name"];
615   frame.offset = matched["offset"];
616 
617   return frame;
618 }
619 
620 /// Parse a Windows string frame
621 Frame toWindows1Frame(string line)
622 {
623   Frame frame;
624 
625   auto matched = matchFirst(line,
626       address ~ `(\s+)in(\s+)` ~ name ~ `(\s+)at(\s+)` ~ file ~ `\(` ~ linePattern ~ `\)`); // ~ );
627 
628   if(matched.length < 4) {
629     return frame;
630   }
631  
632   frame.address = matched["address"];
633   frame.name = matched["name"];
634   frame.file = matched["file"];
635   frame.line = matched["line"].to!int;
636   
637   frame.invalid = frame.address == "" || frame.name == "" || frame.file == "";
638 
639   return frame;
640 }
641 
642 /// ditto
643 Frame toWindows2Frame(string line)
644 {
645   Frame frame;
646 
647   auto matched = matchFirst(line, address ~ `(\s+)in(\s+)` ~ name);
648 
649   if(matched.length < 2) {
650     return frame;
651   }
652 
653   frame.address = matched["address"];
654   frame.name = matched["name"];
655 
656   frame.invalid = frame.address == "" || frame.name == "";
657 
658   return frame;
659 }
660 
661 /// Parse a GLibC string frame
662 Frame toGLibCFrame(string line)
663 {
664   Frame frame;
665 
666   auto matched = matchFirst(line, moduleName ~ `\(` ~ name ~ `\)\s+\[` ~ address ~ `\]`);
667 
668   if(matched.length < 3) {
669     return frame;
670   }
671 
672   frame.address = matched["address"];
673   frame.name = matched["name"];
674   frame.moduleName = matched["module"];
675 
676   auto plusSign = frame.name.indexOf("+");
677 
678   if (plusSign != -1)
679   {
680     frame.offset = frame.name[plusSign + 1 .. $];
681     frame.name = frame.name[0 .. plusSign];
682   }
683 
684   frame.invalid = frame.address == "" || frame.name == "" || frame.moduleName == "" ||
685     frame.name.indexOf("(") >= 0;
686 
687   return frame;
688 }
689 
690 /// Parse a NetBsd string frame
691 Frame toNetBsdFrame(string line)
692 {
693   Frame frame;
694 
695   auto matched = matchFirst(line, address ~ `\s+<` ~ name ~ `\+` ~ offset ~ `>\s+at\s+` ~ moduleName);
696 
697   if(matched.length < 4) {
698     return frame;
699   }
700 
701   frame.address = matched["address"];
702   frame.name = matched["name"];
703   frame.moduleName = matched["module"];
704   frame.offset = matched["offset"];
705 
706   frame.invalid = frame.address == "" || frame.name == "" || frame.moduleName == "" || frame.offset == "";
707 
708   return frame;
709 }
710 
711 /// Parse a Linux frame
712 Frame toLinuxFrame(string line) {
713   Frame frame;
714 
715   auto matched = matchFirst(line, file ~ `:` ~ linePattern ~ `\s+` ~ name ~ `\s+\[` ~ address ~ `\]`);
716 
717   if(matched.length < 4) {
718     return frame;
719   }
720 
721   frame.file = matched["file"];
722   frame.name = matched["name"];
723   frame.address = matched["address"];
724   frame.line = matched["line"].to!int;
725 
726   frame.invalid = frame.address == "" || frame.name == "" || frame.file == "" || frame.line == 0;
727 
728   return frame;
729 }
730 
731 /// Parse a Linux frame
732 Frame toMissingInfoLinuxFrame(string line) {
733   Frame frame;
734 
735   auto matched = matchFirst(line, `\?\?:\?\s+` ~ name ~ `\s+\[` ~ address ~ `\]`);
736 
737   if(matched.length < 2) {
738     return frame;
739   }
740 
741   frame.name = matched["name"];
742   frame.address = matched["address"];
743 
744   frame.invalid = frame.address == "" || frame.name == "";
745 
746   return frame;
747 }
748 
749 /// Converts a stack trace line to a Frame structure
750 Frame toFrame(string line)
751 {
752   Frame frame;
753   frame.raw = line;
754   frame.invalid = false;
755 
756   auto frames = [
757     line.toDarwinFrame,
758     line.toWindows1Frame,
759     line.toWindows2Frame,
760     line.toGLibCFrame,
761     line.toNetBsdFrame,
762     line.toLinuxFrame,
763     line.toMissingInfoLinuxFrame,
764     frame
765   ];
766 
767   return frames.filter!(a => !a.invalid).front;
768 }
769 
770 @("Get frame info from Darwin platform format")
771 unittest
772 {
773   auto line = "1  ???fluent-asserts    0x00abcdef000000 D6module4funcAFZv + 0";
774 
775   auto frame = line.toFrame;
776   frame.invalid.should.equal(false);
777   frame.index.should.equal(1);
778   frame.moduleName.should.equal("???fluent-asserts");
779   frame.address.should.equal("0x00abcdef000000");
780   frame.name.should.equal("D6module4funcAFZv");
781   frame.offset.should.equal("0");
782 }
783 
784 @("Get frame info from windows platform format without path")
785 unittest
786 {
787   auto line = "0x779CAB5A in RtlInitializeExceptionChain";
788 
789   auto frame = line.toFrame;
790   frame.invalid.should.equal(false);
791   frame.index.should.equal(-1);
792   frame.moduleName.should.equal("");
793   frame.address.should.equal("0x779CAB5A");
794   frame.name.should.equal("RtlInitializeExceptionChain");
795   frame.offset.should.equal("");
796 }
797 
798 @("Get frame info from windows platform format with path")
799 unittest
800 {
801   auto line = `0x00402669 in void app.__unitestL82_8() at D:\tidynumbers\source\app.d(84)`;
802 
803   auto frame = line.toFrame;
804   frame.invalid.should.equal(false);
805   frame.index.should.equal(-1);
806   frame.moduleName.should.equal("");
807   frame.address.should.equal("0x00402669");
808   frame.name.should.equal("void app.__unitestL82_8()");
809   frame.file.should.equal(`D:\tidynumbers\source\app.d`);
810   frame.line.should.equal(84);
811   frame.offset.should.equal("");
812 }
813 
814 @("Get frame info from CRuntime_Glibc format without offset")
815 unittest
816 {
817   auto line = `module(_D6module4funcAFZv) [0x00000000]`;
818 
819   auto frame = line.toFrame;
820 
821   frame.invalid.should.equal(false);
822   frame.moduleName.should.equal("module");
823   frame.name.should.equal("_D6module4funcAFZv");
824   frame.address.should.equal("0x00000000");
825   frame.index.should.equal(-1);
826   frame.offset.should.equal("");
827 }
828 
829 @("Get frame info from CRuntime_Glibc format with offset")
830 unittest
831 {
832   auto line = `module(_D6module4funcAFZv+0x78) [0x00000000]`;
833 
834   auto frame = line.toFrame;
835 
836   frame.invalid.should.equal(false);
837   frame.moduleName.should.equal("module");
838   frame.name.should.equal("_D6module4funcAFZv");
839   frame.address.should.equal("0x00000000");
840   frame.index.should.equal(-1);
841   frame.offset.should.equal("0x78");
842 }
843 
844 @("Get frame info from NetBSD format")
845 unittest
846 {
847   auto line = `0x00000000 <_D6module4funcAFZv+0x78> at module`;
848 
849   auto frame = line.toFrame;
850 
851   frame.invalid.should.equal(false);
852   frame.moduleName.should.equal("module");
853   frame.name.should.equal("_D6module4funcAFZv");
854   frame.address.should.equal("0x00000000");
855   frame.index.should.equal(-1);
856   frame.offset.should.equal("0x78");
857 }
858 
859 /// Get the main frame info from linux format
860 unittest {
861   auto line = `generated.d:45 _Dmain [0x8e80c4]`;
862 
863   auto frame = line.toFrame;
864 
865   frame.invalid.should.equal(false);
866   frame.moduleName.should.equal("");
867   frame.file.should.equal("generated.d");
868   frame.line.should.equal(45);
869   frame.name.should.equal("_Dmain");
870   frame.address.should.equal("0x8e80c4");
871   frame.index.should.equal(-1);
872   frame.offset.should.equal("");
873 }
874 
875 /// Get a function frame info from linux format
876 unittest {
877   auto line = `lifecycle/trial/runner.d:106 trial.interfaces.SuiteResult[] trial.runner.runTests(const(trial.interfaces.TestCase)[], immutable(char)[]) [0x8b0ec1]`;
878   auto frame = line.toFrame;
879 
880   frame.invalid.should.equal(false);
881   frame.moduleName.should.equal("");
882   frame.file.should.equal("lifecycle/trial/runner.d");
883   frame.line.should.equal(106);
884   frame.name.should.equal("trial.interfaces.SuiteResult[] trial.runner.runTests(const(trial.interfaces.TestCase)[], immutable(char)[])");
885   frame.address.should.equal("0x8b0ec1");
886   frame.index.should.equal(-1);
887   frame.offset.should.equal("");
888 }
889 
890 /// Get an external function frame info from linux format
891 unittest {
892   auto line = `../../.dub/packages/fluent-asserts-0.6.6/fluent-asserts/core/fluentasserts/core/base.d:39 void fluentasserts.core.base.Result.perform() [0x8f4b47]`;
893   auto frame = line.toFrame;
894 
895   frame.invalid.should.equal(false);
896   frame.moduleName.should.equal("");
897   frame.file.should.equal("../../.dub/packages/fluent-asserts-0.6.6/fluent-asserts/core/fluentasserts/core/base.d");
898   frame.line.should.equal(39);
899   frame.name.should.equal("void fluentasserts.core.base.Result.perform()");
900   frame.address.should.equal("0x8f4b47");
901   frame.index.should.equal(-1);
902   frame.offset.should.equal("");
903 }
904 
905 /// Get an external function frame info from linux format
906 unittest {
907   auto line = `lifecycle/trial/discovery/unit.d:268 _D5trial9discovery4unit17UnitTestDiscovery231__T12addTestCasesVAyaa62_2f686f6d652f626f737a2f776f726b73706163652f64746573742f6c6966656379636c652f747269616c2f6578656375746f722f706172616c6c656c2e64VAyaa23_747269616c2e6578656375746f722e706172616c6c656cS245trial8executor8parallelZ12addTestCasesMFZ9__lambda4FZv [0x872000]`;
908   auto frame = line.toFrame;
909 
910   frame.invalid.should.equal(false);
911   frame.moduleName.should.equal("");
912   frame.file.should.equal("lifecycle/trial/discovery/unit.d");
913   frame.line.should.equal(268);
914   frame.name.should.equal("_D5trial9discovery4unit17UnitTestDiscovery231__T12addTestCasesVAyaa62_2f686f6d652f626f737a2f776f726b73706163652f64746573742f6c6966656379636c652f747269616c2f6578656375746f722f706172616c6c656c2e64VAyaa23_747269616c2e6578656375746f722e706172616c6c656cS245trial8executor8parallelZ12addTestCasesMFZ9__lambda4FZv");
915   frame.address.should.equal("0x872000");
916   frame.index.should.equal(-1);
917   frame.offset.should.equal("");
918 }
919 
920 /// Get an missing info function frame info from linux format
921 unittest {
922   auto line = `??:? __libc_start_main [0x174bbf44]`;
923   auto frame = line.toFrame;
924 
925   frame.invalid.should.equal(false);
926   frame.moduleName.should.equal("");
927   frame.file.should.equal("");
928   frame.line.should.equal(-1);
929   frame.name.should.equal("__libc_start_main");
930   frame.address.should.equal("0x174bbf44");
931   frame.index.should.equal(-1);
932   frame.offset.should.equal("");
933 }