1 /++ 2 A module containing the StatsReporter 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.reporters.stats; 9 10 import std.algorithm; 11 import std..string; 12 import std.conv; 13 import std.exception; 14 import std.array; 15 import std.datetime; 16 import std.stdio; 17 import std.file; 18 import std.path; 19 20 import trial.runner; 21 import trial.interfaces; 22 23 /// 24 struct Stat 25 { 26 /// 27 string name; 28 /// 29 SysTime begin; 30 /// 31 SysTime end; 32 /// 33 TestResult.Status status = TestResult.Status.unknown; 34 /// 35 SourceLocation location; 36 } 37 38 /// 39 class StatStorage 40 { 41 /// 42 Stat[] values; 43 } 44 45 Stat find(StatStorage storage, const(string) name) 46 { 47 auto res = storage.values.filter!(a => a.name == name); 48 49 if (res.empty) 50 { 51 return Stat("", SysTime.min, SysTime.min); 52 } 53 54 return res.front; 55 } 56 57 /// The stats reporter creates a csv file with the duration and the result of all your steps and tests. 58 /// It's usefull to use it with other reporters, like spec progress. 59 class StatsReporter : ILifecycleListener, ITestCaseLifecycleListener, 60 ISuiteLifecycleListener, IStepLifecycleListener 61 { 62 private 63 { 64 immutable string destination; 65 StatStorage storage; 66 string[][string] path; 67 } 68 69 this(StatStorage storage, string destination) 70 { 71 this.storage = storage; 72 this.destination = destination; 73 } 74 75 this(string destination) 76 { 77 this(new StatStorage, destination); 78 } 79 80 private 81 { 82 auto lastItem(string key) 83 { 84 enforce(path[key].length > 0, "There is no defined path"); 85 return path[key][path.length - 1]; 86 } 87 } 88 89 void update() 90 { 91 } 92 93 void begin(ref SuiteResult suite) 94 { 95 } 96 97 void end(ref SuiteResult suite) 98 { 99 storage.values ~= Stat(suite.name, suite.begin, Clock.currTime); 100 } 101 102 void begin(string suite, ref TestResult test) 103 { 104 } 105 106 void end(string suite, ref TestResult test) 107 { 108 storage.values ~= Stat(suite ~ "." ~ test.name, test.begin, Clock.currTime, test.status, SourceLocation(test.fileName, test.line)); 109 } 110 111 void begin(string suite, string test, ref StepResult step) 112 { 113 string key = suite ~ "." ~ test; 114 path[key] ~= step.name; 115 } 116 117 void end(string suite, string test, ref StepResult step) 118 { 119 string key = suite ~ "." ~ test; 120 121 enforce(lastItem(key) == step.name, "Invalid step name"); 122 storage.values ~= Stat(key ~ "." ~ path[key].join('.'), step.begin, Clock.currTime); 123 path[key] = path[key][0 .. $ - 1]; 124 } 125 126 void begin(ulong) 127 { 128 } 129 130 void end(SuiteResult[]) 131 { 132 auto parent = buildPath(pathSplitter(destination).array[0..$-1]); 133 134 if(parent != "" && !parent.exists) { 135 mkdirRecurse(parent); 136 } 137 138 std.file.write(destination, storage.toCsv); 139 140 auto attachment = const Attachment("stats", destination, "text/csv"); 141 142 if(LifeCycleListeners.instance !is null) { 143 LifeCycleListeners.instance.attach(attachment); 144 } 145 } 146 } 147 148 version (unittest) 149 { 150 version(Have_fluent_asserts) { 151 import fluent.asserts; 152 import std.datetime; 153 import std.stdio; 154 } 155 } 156 157 /// It should write the stats to the expected path 158 unittest { 159 scope(exit) { 160 if(exists("destination.csv")) { 161 std.file.remove("destination.csv"); 162 } 163 } 164 165 auto stats = new StatsReporter("destination.csv"); 166 stats.end([]); 167 168 "destination.csv".exists.should.equal(true); 169 } 170 171 @("it should add suite to the storage") 172 unittest 173 { 174 auto storage = new StatStorage; 175 auto stats = new StatsReporter(storage, "trial-stats.csv"); 176 177 SuiteResult suite = SuiteResult("suite1"); 178 179 stats.begin(suite); 180 stats.end(suite); 181 182 storage.values.length.should.equal(1); 183 184 suite.name = "suite2"; 185 stats.begin(suite); 186 stats.end(suite); 187 188 storage.values.length.should.equal(2); 189 storage.values.map!(a => a.name).array.should.equal(["suite1", "suite2"]); 190 storage.values.map!(a => a.status) 191 .array.should.equal([TestResult.Status.unknown, TestResult.Status.unknown]); 192 storage.values.map!(a => a.begin).array.should.equal([suite.begin, suite.begin]); 193 storage.values.map!(a => a.end > a.begin).array.should.equal([true, true]); 194 } 195 196 @("it should add tests to the storage") 197 unittest 198 { 199 auto storage = new StatStorage; 200 auto stats = new StatsReporter(storage, "trial-stats.csv"); 201 202 SuiteResult suite = SuiteResult("suite"); 203 204 auto test = new TestResult("test1"); 205 test.fileName = "file1.d"; 206 test.line = 11; 207 test.status = TestResult.Status.success; 208 209 stats.begin(suite); 210 stats.begin("suite", test); 211 stats.end("suite", test); 212 213 storage.values.length.should.equal(1); 214 215 test.name = "test2"; 216 test.status = TestResult.Status.failure; 217 test.fileName = "file2.d"; 218 test.line = 22; 219 220 stats.begin("suite", test); 221 stats.end("suite", test); 222 223 storage.values.length.should.equal(2); 224 storage.values.map!(a => a.name).array.should.equal(["suite.test1", "suite.test2"]); 225 storage.values.map!(a => a.status).array.should.equal([TestResult.Status.success, TestResult.Status.failure]); 226 storage.values.map!(a => a.begin).array.should.equal([test.begin, test.begin]); 227 storage.values.map!(a => a.end > a.begin).array.should.equal([true, true]); 228 storage.values.map!(a => a.location.fileName).array.should.equal(["file1.d", "file2.d"]); 229 storage.values.map!(a => a.location.line).array.should.equal([11, 22].to!(size_t[])); 230 } 231 232 @("it should add steps to the storage") 233 unittest 234 { 235 auto storage = new StatStorage; 236 auto stats = new StatsReporter(storage, "trial-stats.csv"); 237 238 SuiteResult suite = SuiteResult("suite"); 239 240 auto test = new TestResult("test"); 241 auto step = new StepResult; 242 step.name = "step1"; 243 step.begin = Clock.currTime; 244 245 stats.begin(suite); 246 stats.begin("suite", test); 247 stats.begin("suite", "test", step); 248 stats.end("suite", "test", step); 249 250 storage.values.length.should.equal(1); 251 252 step.name = "step2"; 253 stats.begin("suite", "test", step); 254 stats.end("suite", "test", step); 255 256 storage.values.length.should.equal(2); 257 storage.values.map!(a => a.name).array.should.equal(["suite.test.step1", "suite.test.step2"]); 258 storage.values.map!(a => a.status) 259 .array.should.equal([TestResult.Status.unknown, TestResult.Status.unknown]); 260 storage.values.map!(a => a.begin).array.should.equal([step.begin, step.begin]); 261 storage.values.map!(a => a.end > a.begin).array.should.equal([true, true]); 262 } 263 264 string toCsv(const(StatStorage) storage) 265 { 266 return storage.values.map!(a => [a.name, a.begin.toISOExtString, 267 a.end.toISOExtString, a.status.to!string, a.location.fileName, a.location.line.to!string]).map!(a => a.join(',')).join('\n'); 268 } 269 270 @("it should convert stat storage to csv") 271 unittest 272 { 273 auto stats = new StatStorage; 274 stats.values = [Stat("1", SysTime.min, SysTime.max), Stat("2", SysTime.min, SysTime.max, TestResult.Status.success, SourceLocation("file.d", 2))]; 275 276 stats.toCsv.should.equal("1,-29227-04-19T21:11:54.5224192Z,+29228-09-14T02:48:05.4775807Z,unknown,,0\n" 277 ~ "2,-29227-04-19T21:11:54.5224192Z,+29228-09-14T02:48:05.4775807Z,success,file.d,2"); 278 } 279 280 StatStorage toStatStorage(const(string) data) 281 { 282 auto stat = new StatStorage; 283 284 stat.values = data 285 .split('\n') 286 .map!(a => a.split(',')) 287 .filter!(a => a.length == 6) 288 .map!(a => 289 Stat(a[0], 290 SysTime.fromISOExtString(a[1]), 291 SysTime.fromISOExtString(a[2]), 292 a[3].to!(TestResult.Status), 293 SourceLocation(a[4], a[5].to!size_t))) 294 .array; 295 296 return stat; 297 } 298 299 @("it should create stat storage from csv") 300 unittest 301 { 302 auto storage = ("1,-29227-04-19T21:11:54.5224192Z,+29228-09-14T02:48:05.4775807Z,success,,0\n" 303 ~ "2,-29227-04-19T21:11:54.5224192Z,+29228-09-14T02:48:05.4775807Z,unknown,file.d,12").toStatStorage; 304 305 storage.values.length.should.equal(2); 306 storage.values[0].name.should.equal("1"); 307 storage.values[0].begin.should.equal(SysTime.min); 308 storage.values[0].end.should.equal(SysTime.max); 309 storage.values[0].status.should.equal(TestResult.Status.success); 310 storage.values[0].location.fileName.should.equal(""); 311 storage.values[0].location.line.should.equal(0); 312 313 storage.values[1].name.should.equal("2"); 314 storage.values[1].begin.should.equal(SysTime.min); 315 storage.values[1].end.should.equal(SysTime.max); 316 storage.values[1].status.should.equal(TestResult.Status.unknown); 317 storage.values[1].location.fileName.should.equal("file.d"); 318 storage.values[1].location.line.should.equal(12); 319 } 320 321 StatStorage statsFromFile(string fileName) 322 { 323 if (!fileName.exists) 324 { 325 return new StatStorage(); 326 } 327 328 return fileName.readText.toStatStorage; 329 }