1020304050607080901001101201301401501601701801902002102202302402502602702802903003103203303403503603703803904004104204304404504604704804905005105205305405505605705805906006106206306406506606706806907007107207307407507607707807908008108208308408508608708808909009109209309409509609709809901000101010201030104010501060107010801090110011101120113011401150116011701180119012001210122012301240125012601270128012901300131013201330134013501360137013801390140014101420143014401450146014701480149015001510152015301540155015601570158015901600161016201630164016501660167016801690170017101720173017401750176017701780179018001810182018301840185018601870188018901900191019201930194019501960197019801990200020102020203020402050206020702080209021002110212021302140215021602170218021902200221022202230224022502260227022802290230023102320233023402350236023702380239024002410242024302440245024602470248024902500251025202530254025502560257191258191259026002610262997263997264997265997266997267026802690270027102720273027402750276027702780279028002810282028302849972850286997287997288997289997290997291029299729302940295029602970298029903000301030203030304030503060307030803090310031103120313031403150316031703181319132013211322032303240325032603270328032903300331176332176333176334176335033603370338033903400341034203430344034503460347034803490350035103520353035403550356035703580359036003610362036303640365036603670368036903700371037203730374037503760377037803790380038103820383038403859973869973879973880389039003910392039303940395039603970398039904000401040204030404040504060407040804090410041104120413041404150416041704180419042004210422042304240425042604270428042904300431043204330434043504360437043804390440044104420443044404450446044799744804499974509974510452045304540455045604570458045904600461046204630464046504660467046804690470047104720473047404750476047704780479048004810482048304840485048604870488048904900491049204930494049504960497049804990500050105020503050405050506050705080509051005110512051305140515051605170518051905200521052205230524052505260527052805290530053105320533053405350536053705380539054005410542054305440545054605470548054905500551055205530554055535560557055805590560056165621656305640565056665672156805690570057165721857305740575057665771957805790580058165821583058405850586058765881358905900591059265931459405950596059705980599060006010602060306041995605060606070608060956100611061206130614561506160617061806195286200621062206230624528625062606270628062999763049856310632063306340635997636498563706380639064006410642064306440645064606470648064906500651065206539976549976559976560657065806590660166116620663066406650666166706680 module trial.interfaces; import std.datetime; import std.algorithm; import std.array; import std.functional; import std.conv; import std.file; import std.path; import std.uuid; import std.exception; import std.json; import std.algorithm; alias TestCaseDelegate = void delegate() @system; alias TestCaseFunction = void function() @system; interface ILifecycleListener { void begin(ulong testCount); void update(); void end(SuiteResult[]); } string toJsonString(Throwable throwable) { if(throwable is null) { return "{}"; } string fields; fields ~= `"file":"` ~ throwable.file.escapeJson ~ `",`; fields ~= `"line":"` ~ throwable.line.to!string.escapeJson ~ `",`; fields ~= `"msg":"` ~ throwable.msg.escapeJson ~ `",`; fields ~= `"info":"` ~ throwable.info.to!string.escapeJson ~ `",`; fields ~= `"raw":"` ~ throwable.toString.escapeJson ~ `"`; return "{" ~ fields ~ "}"; } interface ITestDiscovery { TestCase[] getTestCases(); } interface ITestDescribe { TestCase[] discoverTestCases(string file); } interface ITestExecutor { SuiteResult[] beginExecution(ref const(TestCase)[]); SuiteResult[] execute(ref const(TestCase)); SuiteResult[] endExecution(); } interface ISuiteLifecycleListener { void begin(ref SuiteResult); void end(ref SuiteResult); } interface IAttachmentListener { void attach(ref const Attachment); } interface ITestCaseLifecycleListener { void begin(string suite, ref TestResult); void end(string suite, ref TestResult); } interface IStepLifecycleListener { void begin(string suite, string test, ref StepResult); void end(string suite, string test, ref StepResult); } struct Label { string name; string value; string toString() inout { return `{ "name": "` ~ name.escapeJson ~ `", "value": "` ~ value.escapeJson ~ `" }`; } static Label[] fromJsonArray(string value) { return parseJSON(value).array.map!(a => Label(a["name"].str, a["value"].str)).array; } static Label fromJson(string value) { auto parsedValue = parseJSON(value); return Label(parsedValue["name"].str, parsedValue["value"].str); } } struct Attachment { string name; string file; string mime; static string destination; static Attachment fromFile(const string name, const string path, const string mime) { auto fileDestination = buildPath(destination, randomUUID.toString ~ "." ~ path.baseName); copy(path, fileDestination); auto a = const Attachment(name, fileDestination, mime); if(LifeCycleListeners.instance !is null) { LifeCycleListeners.instance.attach(a); } return a; } static Attachment fromString(const string content) { ulong index; string fileDestination = buildPath(destination, randomUUID.toString ~ ".txt"); fileDestination.write(content); auto a = const Attachment("unknown", fileDestination, "text/plain"); if(LifeCycleListeners.instance !is null) { LifeCycleListeners.instance.attach(a); } return a; } string toString() inout { string fields; fields ~= `"name":"` ~ name ~ `",`; fields ~= `"file":"` ~ file ~ `",`; fields ~= `"mime":"` ~ mime ~ `"`; return "{" ~ fields ~ "}"; } } struct SourceLocation { string fileName; size_t line; string toString() inout { return `{ "fileName": "` ~ fileName.escapeJson ~ `", "line": ` ~ line.to!string ~ ` }`; } } private string escapeJson(string value) { return value.replace(`"`, `\"`).replace("\r", `\r`).replace("\n", `\n`).replace("\t", `\t`); } struct TestCase { string suiteName; string name; TestCaseDelegate func; Label[] labels; SourceLocation location; this(const TestCase testCase) { suiteName = testCase.suiteName.dup; name = testCase.name.dup; func = testCase.func; location = testCase.location; labels.length = testCase.labels.length; foreach(key, val; testCase.labels) { labels[key] = val; } } this(T)(string suiteName, string name, T func, Label[] labels, SourceLocation location) { this(suiteName, name, func.toDelegate, labels); this.location = location; } this(string suiteName, string name, TestCaseFunction func, Label[] labels = []) { this(suiteName, name, func.toDelegate, labels); } this(string suiteName, string name, TestCaseDelegate func, Label[] labels = []) { this.suiteName = suiteName; this.name = name; this.func = func; this.labels = labels; } string toString() const { string jsonRepresentation = "{ "; jsonRepresentation ~= `"suiteName": "` ~ suiteName.escapeJson ~ `", `; jsonRepresentation ~= `"name": "` ~ name.escapeJson ~ `", `; jsonRepresentation ~= `"labels": [ ` ~ labels.map!(a => a.toString).join(", ") ~ ` ], `; jsonRepresentation ~= `"location": ` ~ location.toString; return jsonRepresentation ~ " }"; } } TestResult toTestResult(const TestCase testCase) { auto testResult = new TestResult(testCase.name.dup); testResult.begin = Clock.currTime; testResult.end = testResult.begin; testResult.labels = testCase.labels.dup; testResult.fileName = testCase.location.fileName; testResult.line = testCase.location.line; return testResult; } struct SuiteResult { string name; SysTime begin; SysTime end; TestResult[] tests; Attachment[] attachments; @disable this(); this(string name) { this.name = name; begin = SysTime.fromUnixTime(0); end = SysTime.fromUnixTime(0); } this(string name, SysTime begin) { this.name = name; this.begin = begin; } this(string name, SysTime begin, SysTime end) { this.name = name; this.begin = begin; this.end = end; } this(string name, SysTime begin, SysTime end, TestResult[] tests) { this.name = name; this.begin = begin; this.end = end; this.tests = tests; } this(string name, SysTime begin, SysTime end, TestResult[] tests, Attachment[] attachments) { this.name = name; this.begin = begin; this.end = end; this.tests = tests; this.attachments = attachments; } string toString() { string fields; fields ~= `"name":"` ~ name.escapeJson ~ `",`; fields ~= `"begin":"` ~ begin.toISOExtString ~ `",`; fields ~= `"end":"` ~ end.toISOExtString ~ `",`; fields ~= `"tests":[` ~ tests.map!(a => a.toString).join(",") ~ `],`; fields ~= `"attachments":[` ~ attachments.map!(a => a.toString).join(",") ~ `]`; return "{" ~ fields ~ "}"; } } class StepResult { string name; SysTime begin; SysTime end; StepResult[] steps; Attachment[] attachments; this() { begin = SysTime.min; end = SysTime.min; } protected string fields() { string result; result ~= `"name":"` ~ name.escapeJson ~ `",`; result ~= `"begin":"` ~ begin.toISOExtString ~ `",`; result ~= `"end":"` ~ end.toISOExtString ~ `",`; result ~= `"steps":[` ~ steps.map!(a => a.toString).join(",") ~ `],`; result ~= `"attachments":[` ~ attachments.map!(a => a.toString).join(",") ~ `]`; return result; } override string toString() { return "{" ~ fields ~ "}"; } } class TestResult : StepResult { enum Status { created, failure, skip, started, success, pending, unknown } string fileName; size_t line; Status status = Status.created; Label[] labels; Throwable throwable; this(string name) { this.name = name; super(); } override string toString() { string result = fields ~ ","; result ~= `"fileName":"` ~ fileName.escapeJson ~ `",`; result ~= `"line":"` ~ line.to!string ~ `",`; result ~= `"status":"` ~ status.to!string ~ `",`; result ~= `"labels":[` ~ labels.map!(a => a.toString).join(",") ~ `],`; result ~= `"throwable":` ~ throwable.toJsonString; return "{" ~ result ~ "}"; } } struct Flaky { static Label[] labels() { return [Label("status_details", "flaky")]; } } struct Issue { private string name; Label[] labels() { return [ Label("issue", name) ]; } } struct Feature { private string name; Label[] labels() { return [ Label("feature", name) ]; } } struct Story { private string name; Label[] labels() { return [ Label("story", name) ]; } } class PendingTestException : Exception { this(string file = __FILE__, size_t line = __LINE__, Throwable next = null) { super("You cannot run pending tests", file, line, next); } } class LifeCycleListeners { static LifeCycleListeners instance; private { ISuiteLifecycleListener[] suiteListeners; ITestCaseLifecycleListener[] testListeners; IStepLifecycleListener[] stepListeners; ILifecycleListener[] lifecycleListeners; ITestDiscovery[] testDiscoveryListeners; IAttachmentListener[] attachmentListeners; ITestExecutor executor; string currentTest; bool started; } @property { string runningTest() const nothrow { return currentTest; } bool isRunning() { return started; } } TestCase[] getTestCases() { return testDiscoveryListeners.map!(a => a.getTestCases).join; } void add(T)(T listener) { static if(!is(CommonType!(ISuiteLifecycleListener, T) == void)) { suiteListeners ~= cast(ISuiteLifecycleListener) listener; suiteListeners = suiteListeners.filter!(a => a !is null).array; } static if(!is(CommonType!(ITestCaseLifecycleListener, T) == void)) { testListeners ~= cast(ITestCaseLifecycleListener) listener; testListeners = testListeners.filter!(a => a !is null).array; } static if(!is(CommonType!(IStepLifecycleListener, T) == void)) { stepListeners ~= cast(IStepLifecycleListener) listener; stepListeners = stepListeners.filter!(a => a !is null).array; } static if(!is(CommonType!(ILifecycleListener, T) == void)) { lifecycleListeners ~= cast(ILifecycleListener) listener; lifecycleListeners = lifecycleListeners.filter!(a => a !is null).array; } static if(!is(CommonType!(ITestExecutor, T) == void)) { if(cast(ITestExecutor) listener !is null) { executor = cast(ITestExecutor) listener; } } static if(!is(CommonType!(ITestDiscovery, T) == void)) { testDiscoveryListeners ~= cast(ITestDiscovery) listener; testDiscoveryListeners = testDiscoveryListeners.filter!(a => a !is null).array; } static if(!is(CommonType!(IAttachmentListener, T) == void)) { attachmentListeners ~= cast(IAttachmentListener) listener; attachmentListeners = attachmentListeners.filter!(a => a !is null).array; } } void attach(ref const Attachment attachment) { attachmentListeners.each!(a => a.attach(attachment)); } void update() { lifecycleListeners.each!"a.update"; } void begin(ulong testCount) { lifecycleListeners.each!(a => a.begin(testCount)); } void end(SuiteResult[] result) { lifecycleListeners.each!(a => a.end(result)); } void begin(ref SuiteResult suite) { suiteListeners.each!(a => a.begin(suite)); } void end(ref SuiteResult suite) { suiteListeners.each!(a => a.end(suite)); } void begin(string suite, ref TestResult test) { currentTest = suite ~ "." ~ test.name; testListeners.each!(a => a.begin(suite, test)); } void end(string suite, ref TestResult test) { currentTest = ""; testListeners.each!(a => a.end(suite, test)); } void begin(string suite, string test, ref StepResult step) { currentTest = suite ~ "." ~ test ~ "." ~ step.name; stepListeners.each!(a => a.begin(suite, test, step)); } void end(string suite, string test, ref StepResult step) { currentTest = ""; stepListeners.each!(a => a.end(suite, test, step)); } SuiteResult[] execute(ref const(TestCase) func) { started = true; scope(exit) started = false; return executor.execute(func); } SuiteResult[] beginExecution(ref const(TestCase)[] tests) { enforce(executor !is null, "The test executor was not set."); return executor.beginExecution(tests); } SuiteResult[] endExecution() { return executor.endExecution(); } }
module trial.interfaces; import std.datetime; import std.algorithm; import std.array; import std.functional; import std.conv; import std.file; import std.path; import std.uuid; import std.exception; import std.json; import std.algorithm; alias TestCaseDelegate = void delegate() @system; alias TestCaseFunction = void function() @system; interface ILifecycleListener { void begin(ulong testCount); void update(); void end(SuiteResult[]); } string toJsonString(Throwable throwable) { if(throwable is null) { return "{}"; } string fields; fields ~= `"file":"` ~ throwable.file.escapeJson ~ `",`; fields ~= `"line":"` ~ throwable.line.to!string.escapeJson ~ `",`; fields ~= `"msg":"` ~ throwable.msg.escapeJson ~ `",`; fields ~= `"info":"` ~ throwable.info.to!string.escapeJson ~ `",`; fields ~= `"raw":"` ~ throwable.toString.escapeJson ~ `"`; return "{" ~ fields ~ "}"; } interface ITestDiscovery { TestCase[] getTestCases(); } interface ITestDescribe { TestCase[] discoverTestCases(string file); } interface ITestExecutor { SuiteResult[] beginExecution(ref const(TestCase)[]); SuiteResult[] execute(ref const(TestCase)); SuiteResult[] endExecution(); } interface ISuiteLifecycleListener { void begin(ref SuiteResult); void end(ref SuiteResult); } interface IAttachmentListener { void attach(ref const Attachment); } interface ITestCaseLifecycleListener { void begin(string suite, ref TestResult); void end(string suite, ref TestResult); } interface IStepLifecycleListener { void begin(string suite, string test, ref StepResult); void end(string suite, string test, ref StepResult); } struct Label { string name; string value; string toString() inout { return `{ "name": "` ~ name.escapeJson ~ `", "value": "` ~ value.escapeJson ~ `" }`; } static Label[] fromJsonArray(string value) { return parseJSON(value).array.map!(a => Label(a["name"].str, a["value"].str)).array; } static Label fromJson(string value) { auto parsedValue = parseJSON(value); return Label(parsedValue["name"].str, parsedValue["value"].str); } } struct Attachment { string name; string file; string mime; static string destination; static Attachment fromFile(const string name, const string path, const string mime) { auto fileDestination = buildPath(destination, randomUUID.toString ~ "." ~ path.baseName); copy(path, fileDestination); auto a = const Attachment(name, fileDestination, mime); if(LifeCycleListeners.instance !is null) { LifeCycleListeners.instance.attach(a); } return a; } static Attachment fromString(const string content) { ulong index; string fileDestination = buildPath(destination, randomUUID.toString ~ ".txt"); fileDestination.write(content); auto a = const Attachment("unknown", fileDestination, "text/plain"); if(LifeCycleListeners.instance !is null) { LifeCycleListeners.instance.attach(a); } return a; } string toString() inout { string fields; fields ~= `"name":"` ~ name ~ `",`; fields ~= `"file":"` ~ file ~ `",`; fields ~= `"mime":"` ~ mime ~ `"`; return "{" ~ fields ~ "}"; } } struct SourceLocation { string fileName; size_t line; string toString() inout { return `{ "fileName": "` ~ fileName.escapeJson ~ `", "line": ` ~ line.to!string ~ ` }`; } } private string escapeJson(string value) { return value.replace(`"`, `\"`).replace("\r", `\r`).replace("\n", `\n`).replace("\t", `\t`); } struct TestCase { string suiteName; string name; TestCaseDelegate func; Label[] labels; SourceLocation location; this(const TestCase testCase) { suiteName = testCase.suiteName.dup; name = testCase.name.dup; func = testCase.func; location = testCase.location; labels.length = testCase.labels.length; foreach(key, val; testCase.labels) { labels[key] = val; } } this(T)(string suiteName, string name, T func, Label[] labels, SourceLocation location) { this(suiteName, name, func.toDelegate, labels); this.location = location; } this(string suiteName, string name, TestCaseFunction func, Label[] labels = []) { this(suiteName, name, func.toDelegate, labels); } this(string suiteName, string name, TestCaseDelegate func, Label[] labels = []) { this.suiteName = suiteName; this.name = name; this.func = func; this.labels = labels; } string toString() const { string jsonRepresentation = "{ "; jsonRepresentation ~= `"suiteName": "` ~ suiteName.escapeJson ~ `", `; jsonRepresentation ~= `"name": "` ~ name.escapeJson ~ `", `; jsonRepresentation ~= `"labels": [ ` ~ labels.map!(a => a.toString).join(", ") ~ ` ], `; jsonRepresentation ~= `"location": ` ~ location.toString; return jsonRepresentation ~ " }"; } } TestResult toTestResult(const TestCase testCase) { auto testResult = new TestResult(testCase.name.dup); testResult.begin = Clock.currTime; testResult.end = testResult.begin; testResult.labels = testCase.labels.dup; testResult.fileName = testCase.location.fileName; testResult.line = testCase.location.line; return testResult; } struct SuiteResult { string name; SysTime begin; SysTime end; TestResult[] tests; Attachment[] attachments; @disable this(); this(string name) { this.name = name; begin = SysTime.fromUnixTime(0); end = SysTime.fromUnixTime(0); } this(string name, SysTime begin) { this.name = name; this.begin = begin; } this(string name, SysTime begin, SysTime end) { this.name = name; this.begin = begin; this.end = end; } this(string name, SysTime begin, SysTime end, TestResult[] tests) { this.name = name; this.begin = begin; this.end = end; this.tests = tests; } this(string name, SysTime begin, SysTime end, TestResult[] tests, Attachment[] attachments) { this.name = name; this.begin = begin; this.end = end; this.tests = tests; this.attachments = attachments; } string toString() { string fields; fields ~= `"name":"` ~ name.escapeJson ~ `",`; fields ~= `"begin":"` ~ begin.toISOExtString ~ `",`; fields ~= `"end":"` ~ end.toISOExtString ~ `",`; fields ~= `"tests":[` ~ tests.map!(a => a.toString).join(",") ~ `],`; fields ~= `"attachments":[` ~ attachments.map!(a => a.toString).join(",") ~ `]`; return "{" ~ fields ~ "}"; } } class StepResult { string name; SysTime begin; SysTime end; StepResult[] steps; Attachment[] attachments; this() { begin = SysTime.min; end = SysTime.min; } protected string fields() { string result; result ~= `"name":"` ~ name.escapeJson ~ `",`; result ~= `"begin":"` ~ begin.toISOExtString ~ `",`; result ~= `"end":"` ~ end.toISOExtString ~ `",`; result ~= `"steps":[` ~ steps.map!(a => a.toString).join(",") ~ `],`; result ~= `"attachments":[` ~ attachments.map!(a => a.toString).join(",") ~ `]`; return result; } override string toString() { return "{" ~ fields ~ "}"; } } class TestResult : StepResult { enum Status { created, failure, skip, started, success, pending, unknown } string fileName; size_t line; Status status = Status.created; Label[] labels; Throwable throwable; this(string name) { this.name = name; super(); } override string toString() { string result = fields ~ ","; result ~= `"fileName":"` ~ fileName.escapeJson ~ `",`; result ~= `"line":"` ~ line.to!string ~ `",`; result ~= `"status":"` ~ status.to!string ~ `",`; result ~= `"labels":[` ~ labels.map!(a => a.toString).join(",") ~ `],`; result ~= `"throwable":` ~ throwable.toJsonString; return "{" ~ result ~ "}"; } } struct Flaky { static Label[] labels() { return [Label("status_details", "flaky")]; } } struct Issue { private string name; Label[] labels() { return [ Label("issue", name) ]; } } struct Feature { private string name; Label[] labels() { return [ Label("feature", name) ]; } } struct Story { private string name; Label[] labels() { return [ Label("story", name) ]; } } class PendingTestException : Exception { this(string file = __FILE__, size_t line = __LINE__, Throwable next = null) { super("You cannot run pending tests", file, line, next); } } class LifeCycleListeners { static LifeCycleListeners instance; private { ISuiteLifecycleListener[] suiteListeners; ITestCaseLifecycleListener[] testListeners; IStepLifecycleListener[] stepListeners; ILifecycleListener[] lifecycleListeners; ITestDiscovery[] testDiscoveryListeners; IAttachmentListener[] attachmentListeners; ITestExecutor executor; string currentTest; bool started; } @property { string runningTest() const nothrow { return currentTest; } bool isRunning() { return started; } } TestCase[] getTestCases() { return testDiscoveryListeners.map!(a => a.getTestCases).join; } void add(T)(T listener) { static if(!is(CommonType!(ISuiteLifecycleListener, T) == void)) { suiteListeners ~= cast(ISuiteLifecycleListener) listener; suiteListeners = suiteListeners.filter!(a => a !is null).array; } static if(!is(CommonType!(ITestCaseLifecycleListener, T) == void)) { testListeners ~= cast(ITestCaseLifecycleListener) listener; testListeners = testListeners.filter!(a => a !is null).array; } static if(!is(CommonType!(IStepLifecycleListener, T) == void)) { stepListeners ~= cast(IStepLifecycleListener) listener; stepListeners = stepListeners.filter!(a => a !is null).array; } static if(!is(CommonType!(ILifecycleListener, T) == void)) { lifecycleListeners ~= cast(ILifecycleListener) listener; lifecycleListeners = lifecycleListeners.filter!(a => a !is null).array; } static if(!is(CommonType!(ITestExecutor, T) == void)) { if(cast(ITestExecutor) listener !is null) { executor = cast(ITestExecutor) listener; } } static if(!is(CommonType!(ITestDiscovery, T) == void)) { testDiscoveryListeners ~= cast(ITestDiscovery) listener; testDiscoveryListeners = testDiscoveryListeners.filter!(a => a !is null).array; } static if(!is(CommonType!(IAttachmentListener, T) == void)) { attachmentListeners ~= cast(IAttachmentListener) listener; attachmentListeners = attachmentListeners.filter!(a => a !is null).array; } } void attach(ref const Attachment attachment) { attachmentListeners.each!(a => a.attach(attachment)); } void update() { lifecycleListeners.each!"a.update"; } void begin(ulong testCount) { lifecycleListeners.each!(a => a.begin(testCount)); } void end(SuiteResult[] result) { lifecycleListeners.each!(a => a.end(result)); } void begin(ref SuiteResult suite) { suiteListeners.each!(a => a.begin(suite)); } void end(ref SuiteResult suite) { suiteListeners.each!(a => a.end(suite)); } void begin(string suite, ref TestResult test) { currentTest = suite ~ "." ~ test.name; testListeners.each!(a => a.begin(suite, test)); } void end(string suite, ref TestResult test) { currentTest = ""; testListeners.each!(a => a.end(suite, test)); } void begin(string suite, string test, ref StepResult step) { currentTest = suite ~ "." ~ test ~ "." ~ step.name; stepListeners.each!(a => a.begin(suite, test, step)); } void end(string suite, string test, ref StepResult step) { currentTest = ""; stepListeners.each!(a => a.end(suite, test, step)); } SuiteResult[] execute(ref const(TestCase) func) { started = true; scope(exit) started = false; return executor.execute(func); } SuiteResult[] beginExecution(ref const(TestCase)[] tests) { enforce(executor !is null, "The test executor was not set."); return executor.beginExecution(tests); } SuiteResult[] endExecution() { return executor.endExecution(); } }