86 / 88 lines covered (97.7%)
1use BDD::Behave::Formatter;2use BDD::Behave::Failures;3use BDD::Behave::Failure;45unit class BDD::Behave::Formatter::JsonEvents does BDD::Behave::Formatter;67has Int $!failure-watermark = 0;89method name(--> Str) { 'json-events' }1011sub emit(%event --> Nil) {12 $*OUT.put(to-json-line(%event));13}1415sub to-json-line(%h --> Str) {16 json-value(%h);17}1819sub json-value($v --> Str) {20 given $v {21 when Nil { 'null' }22 when Bool { $v ?? 'true' !! 'false' }23 when Int { $v.Str }24 when Real { my $s = $v.Str; $s eq 'NaN' || $s eq 'Inf' || $s eq '-Inf' ?? 'null' !! $s }25 when Str { json-string($v) }26 when Positional { '[' ~ $v.list.map(&json-value).join(',') ~ ']' }27 when Associative {28 # Sorting the keys rather than the rendered pairs keeps the order an29 # event is emitted in stable without comparing whole payloads.30 '{' ~ $v.keys.sort.map({ json-string(.Str) ~ ':' ~ json-value($v{$_}) }).join(',') ~ '}';31 }32 default { json-string($v.gist) }33 }34}3536sub json-string(Str $s --> Str) {37 my $out = $s;38 $out = $out.subst('\\', '\\\\', :g);39 $out = $out.subst('"', '\\"', :g);40 $out = $out.subst("\n", '\\n', :g);41 $out = $out.subst("\r", '\\r', :g);42 $out = $out.subst("\t", '\\t', :g);43 # A control character other than the three with their own escape is rare, so44 # the pass that rewrites one runs only when the string holds one.45 $out = $out.subst(/<:Cc>/, { sprintf('\\u%04x', $/.Str.ord) }, :g) if $out ~~ /<:Cc>/;46 '"' ~ $out ~ '"';47}4849method node-id($node --> Str) {50 $node.file.absolute ~ ':' ~ $node.line;51}5253method suite-loading(Str :$file) {54 emit %( :type<suite-loading>, :$file );55}5657method suite-start($suite, Bool :$multi-file = False) {58 emit %(59 :type<suite-start>,60 :id(self.node-id($suite)),61 :description($suite.description),62 :file($suite.file.absolute),63 :line($suite.line),64 :$multi-file,65 );66}6768method suite-end($suite) {69 emit %(70 :type<suite-end>,71 :id(self.node-id($suite)),72 );73}7475method group-start($group) {76 emit %(77 :type<group-start>,78 :id(self.node-id($group)),79 :description($group.description),80 :file($group.file.absolute),81 :line($group.line),82 );83}8485method group-end($group) {86 emit %(87 :type<group-end>,88 :id(self.node-id($group)),89 );90}9192method group-around-skipped($group) {93 emit %(94 :type<group-around-skipped>,95 :id(self.node-id($group)),96 );97}9899method example-start($example, Bool :$auto = False) {100 emit %(101 :type<example-start>,102 :id(self.node-id($example)),103 :description($example.description),104 :file($example.file.absolute),105 :line($example.line),106 :$auto,107 );108}109110method example-auto-description($example, Str :$description) {111 emit %(112 :type<example-auto-description>,113 :id(self.node-id($example)),114 :$description,115 );116}117118method example-pass($example) {119 emit %(120 :type<example-pass>,121 :id(self.node-id($example)),122 :description($example.description),123 :file($example.file.absolute),124 :line($example.line),125 :duration($example.duration.defined ?? $example.duration.Real !! 0.0),126 );127}128129method example-fail($example, :$failure-info) {130 my %payload = (131 :type<example-fail>,132 :id(self.node-id($example)),133 :description($example.description),134 :file($example.file.absolute),135 :line($example.line),136 :duration($example.duration.defined ?? $example.duration.Real !! 0.0),137 );138 with $failure-info {139 %payload<failure-description> = ($failure-info<description> // '').Str;140 %payload<failure-file> = ($failure-info<file> // '').Str;141 %payload<failure-line> = ($failure-info<line> // 0).Int;142 with $failure-info<exception> {143 %payload<exception-message> = .message;144 %payload<exception-backtrace> = .backtrace.full.Str;145 }146 }147 my $total = Failures.list.elems;148 my @new = $total > $!failure-watermark149 ?? Failures.list[$!failure-watermark ..^ $total].grep(!*.from-runner-exception).List150 !! ().List;151 $!failure-watermark = $total;152 %payload<failures> = @new.map({153 my $given = ($_.given // '').gist;154 my $expected = ($_.expected // '').gist;155 my $negated = ?$_.negated;156157 %(158 :file($_.file // ''),159 :line($_.line // 0),160 :$given,161 :$expected,162 :message(($_.?message // Str).defined163 ?? $_.message164 !! compose-failure-message($given, $expected, $negated)),165 :aggregation-label(($_.?aggregation-label // Str).defined ?? $_.aggregation-label !! ''),166 :$negated,167 )168 }).List;169 emit %payload;170}171172method example-pending($example) {173 emit %(174 :type<example-pending>,175 :id(self.node-id($example)),176 :description($example.description),177 :file($example.file.absolute),178 :line($example.line),179 );180}181182method example-skipped($example) {183 emit %(184 :type<example-skipped>,185 :id(self.node-id($example)),186 :description($example.description),187 :file($example.file.absolute),188 :line($example.line),189 );190}191192method example-around-skipped($example) {193 emit %(194 :type<example-around-skipped>,195 :id(self.node-id($example)),196 :description($example.description),197 :file($example.file.absolute),198 :line($example.line),199 );200}201202method example-retry($example, Int :$attempt, Int :$max-attempts) {203 emit %(204 :type<example-retry>,205 :id(self.node-id($example)),206 :description($example.description),207 :file($example.file.absolute),208 :line($example.line),209 :attempt($attempt.Int),210 :max-attempts($max-attempts.Int),211 );212}213214method example-slow($example, Real :$threshold) {215 emit %(216 :type<example-slow>,217 :id(self.node-id($example)),218 :threshold($threshold.Real),219 :duration($example.duration.defined ?? $example.duration.Real !! 0.0),220 );221}222223method retry-summary(@records) {224 for @records -> $rec {225 emit %(226 :type<retry-record>,227 :description($rec.description.Str),228 :location($rec.location.Str),229 :attempts($rec.attempts.Int),230 :max-attempts($rec.max-attempts.Int),231 :outcome($rec.outcome.Str),232 );233 }234}235236method profile-summary(@records, Int :$limit) {237 return unless $limit > 0;238 for @records -> $rec {239 my $ex = $rec<example>;240 emit %(241 :type<profile-record>,242 :id($ex.defined ?? self.node-id($ex) !! ''),243 :description(($rec<description> // '').Str),244 :duration(($rec<duration> // 0).Real),245 :file($ex.defined ?? $ex.file.absolute !! ''),246 :line($ex.defined ?? $ex.line.Int !! 0),247 );248 }249}250251method benchmark-summary-section(252 @summaries, @regressions,253 Real :$threshold,254 Str :$format,255 IO::Path :$output,256 :$runner,257) {258 for @summaries -> %s {259 my $ex = %s<example>;260 emit %(261 :type<benchmark-record>,262 :id($ex.defined ?? self.node-id($ex) !! ''),263 :description((%s<description> // '').Str),264 :key((%s<key> // '').Str),265 :label(%s<label>.defined ?? %s<label>.Str !! ''),266 :position((%s<position> // 0).Int),267 :runs((%s<runs> // 0).Int),268 :iterations((%s<iterations> // 0).Int),269 :timings(%s<timings>.list.map(*.Real).List),270 :min((%s<min> // 0).Real),271 :max((%s<max> // 0).Real),272 :mean((%s<mean> // 0).Real),273 :median((%s<median> // 0).Real),274 :total((%s<total> // 0).Real),275 :file($ex.defined ?? $ex.file.absolute !! ''),276 :line($ex.defined ?? $ex.line.Int !! 0),277 );278 }279}280281method run-summary(282 $result,283 Bool :$aborted = False,284 Int :$fail-fast = 0,285 Str :$order = 'defined',286 Int :$seed,287) {288 emit %(289 :type<run-summary>,290 :total($result.total.Int),291 :passed($result.passed.Int),292 :failed($result.failed.Int),293 :pending($result.pending.Int),294 :skipped($result.skipped.Int),295 :aborted(?$aborted),296 :fail-fast($fail-fast.Int),297 :$order,298 :seed($seed.defined ?? $seed.Int !! 0),299 );300}301302method load-errors(@errors) {303 for @errors -> $err {304 emit %(305 :type<load-error>,306 :file(($err<file> // '').Str),307 :message(($err<message> // '').Str),308 );309 }310}