• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

thetic / mutiny / 24496111858

16 Apr 2026 06:42AM UTC coverage: 98.499% (-0.1%) from 98.642%
24496111858

Pull #58

github

web-flow
Merge 4b35d227b into 405a5edbc
Pull Request #58: Junity

68 of 73 new or added lines in 2 files covered. (93.15%)

3 existing lines in 1 file now uncovered.

5119 of 5197 relevant lines covered (98.5%)

4069.35 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.8
/src/test/JUnitOutput.cpp
1
#include "mutiny/test/JUnitOutput.hpp"
2

3
#include "mutiny/test/Failure.hpp"
4
#include "mutiny/test/Output.hpp"
5
#include "mutiny/test/Result.hpp"
6
#include "mutiny/test/Shell.hpp"
7

8
#include "mutiny/time.hpp"
9

10
#include <stdint.h>
11

12
namespace mu {
13
namespace tiny {
14
namespace test {
15

16
namespace {
17

18
class TestProperty
19
{
20
public:
21
  String name;
22
  String value;
23
  TestProperty* next{ nullptr };
24
};
25

26
} // namespace
27

28
class JUnitTestCaseResultNode
29
{
30
public:
31
  JUnitTestCaseResultNode() = default;
62✔
32

33
  String name;
34
  uint_least64_t exec_time{ 0 };
35
  Failure* failure{ nullptr };
36
  bool failure_is_error{ false };
37
  bool ignored{ false };
38
  String skip_message;
39
  String file;
40
  size_t line_number{ 0 };
41
  size_t check_count{ 0 };
42
  String output;
43
  TestProperty* properties{ nullptr };
44
  TestProperty* properties_tail{ nullptr };
45
  JUnitTestCaseResultNode* next{ nullptr };
46
};
47

48
class JUnitTestGroupResult
49
{
50
public:
51
  JUnitTestGroupResult() = default;
45✔
52

53
  size_t test_count{ 0 };
54
  size_t failure_count{ 0 };
55
  size_t error_count{ 0 };
56
  size_t skip_count{ 0 };
57
  size_t total_check_count{ 0 };
58
  bool in_test{ false };
59
  uint_least64_t start_time{ 0 };
60
  uint_least64_t group_exec_time{ 0 };
61
  String group;
62
  JUnitTestCaseResultNode* head{ nullptr };
63
  JUnitTestCaseResultNode* tail{ nullptr };
64
};
65

66
class JUnitTestOutputImpl
67
{
68
public:
69
  JUnitTestGroupResult results;
70
  String current_group_xml;
71
  String accumulated_xml;
72
  String package;
73
  String std_output;
74
  size_t total_test_count{ 0 };
75
  size_t total_failure_count{ 0 };
76
  size_t total_error_count{ 0 };
77
  size_t total_skip_count{ 0 };
78
  uint_least64_t total_exec_time{ 0 };
79
  String start_timestamp;
80
};
81

82
JUnitOutput::JUnitOutput()
45✔
83
  : impl_(new JUnitTestOutputImpl)
45✔
84
{
85
}
45✔
86

87
JUnitOutput::~JUnitOutput()
90✔
88
{
89
  reset_test_group_result();
45✔
90
  delete impl_;
45✔
91
}
90✔
92

93
void JUnitOutput::reset_test_group_result()
94✔
94
{
95
  impl_->results.test_count = 0;
94✔
96
  impl_->results.failure_count = 0;
94✔
97
  impl_->results.error_count = 0;
94✔
98
  impl_->results.skip_count = 0;
94✔
99
  impl_->results.group = "";
94✔
100
  JUnitTestCaseResultNode* cur = impl_->results.head;
94✔
101
  while (cur) {
156✔
102
    JUnitTestCaseResultNode* tmp = cur->next;
62✔
103
    delete cur->failure;
62✔
104
    TestProperty* prop = cur->properties;
62✔
105
    while (prop) {
67✔
106
      TestProperty* prop_tmp = prop->next;
5✔
107
      delete prop;
5✔
108
      prop = prop_tmp;
5✔
109
    }
110
    delete cur;
62✔
111
    cur = tmp;
62✔
112
  }
113
  impl_->results.head = nullptr;
94✔
114
  impl_->results.tail = nullptr;
94✔
115
  impl_->std_output.clear();
94✔
116
}
94✔
117

118
void JUnitOutput::print_tests_started()
44✔
119
{
120
  impl_->accumulated_xml.clear();
44✔
121
  impl_->total_test_count = 0;
44✔
122
  impl_->total_failure_count = 0;
44✔
123
  impl_->total_error_count = 0;
44✔
124
  impl_->total_skip_count = 0;
44✔
125
  impl_->total_exec_time = 0;
44✔
126
  impl_->start_timestamp = get_time_string();
44✔
127
}
44✔
128

129
void JUnitOutput::print_current_group_started(const Shell& /*test*/) {}
49✔
130

131
void JUnitOutput::print_current_test_ended(const Result& result)
62✔
132
{
133
  impl_->results.tail->exec_time =
124✔
134
      result.get_current_test_total_execution_time();
62✔
135
  impl_->results.tail->check_count = result.get_check_count();
62✔
136
  impl_->results.in_test = false;
62✔
137
}
62✔
138

139
void JUnitOutput::print_tests_ended(const Result& /*result*/)
44✔
140
{
141
  Output::File file = fopen_(create_file_name().c_str(), "w");
44✔
142
  String header = string_from_format(
143
      "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
144
      "<testsuites tests=\"%d\" failures=\"%d\" errors=\"%d\" "
145
      "skipped=\"%d\" time=\"%d.%03d\" timestamp=\"%s\">\n",
146
      static_cast<int>(impl_->total_test_count),
44✔
147
      static_cast<int>(impl_->total_failure_count),
44✔
148
      static_cast<int>(impl_->total_error_count),
44✔
149
      static_cast<int>(impl_->total_skip_count),
44✔
150
      static_cast<int>(impl_->total_exec_time / 1000),
44✔
151
      static_cast<int>(impl_->total_exec_time % 1000),
44✔
152
      impl_->start_timestamp.c_str()
44✔
153
  );
44✔
154
  fputs_(header.c_str(), file);
44✔
155
  fputs_(impl_->accumulated_xml.c_str(), file);
44✔
156
  fputs_("</testsuites>\n", file);
44✔
157
  fclose_(file);
44✔
158
}
44✔
159

160
void JUnitOutput::print_current_group_ended(const Result& result)
49✔
161
{
162
  impl_->results.group_exec_time =
98✔
163
      result.get_current_group_total_execution_time();
49✔
164
  impl_->total_test_count += impl_->results.test_count;
49✔
165
  impl_->total_failure_count += impl_->results.failure_count;
49✔
166
  impl_->total_error_count += impl_->results.error_count;
49✔
167
  impl_->total_skip_count += impl_->results.skip_count;
49✔
168
  impl_->total_exec_time += impl_->results.group_exec_time;
49✔
169
  write_test_group_to_file();
49✔
170
  reset_test_group_result();
49✔
171
}
49✔
172

173
void JUnitOutput::print_current_test_started(const Shell& test)
62✔
174
{
175
  impl_->results.test_count++;
62✔
176
  impl_->results.group = test.get_group();
62✔
177
  impl_->results.start_time = get_time_in_millis();
62✔
178

179
  if (impl_->results.tail == nullptr) {
62✔
180
    impl_->results.head = impl_->results.tail = new JUnitTestCaseResultNode;
49✔
181
  } else {
182
    impl_->results.tail->next = new JUnitTestCaseResultNode;
13✔
183
    impl_->results.tail = impl_->results.tail->next;
13✔
184
  }
185
  impl_->results.tail->name = test.get_name();
62✔
186
  impl_->results.tail->file = test.get_file();
62✔
187
  impl_->results.tail->line_number = test.get_line_number();
62✔
188
  impl_->results.in_test = true;
62✔
189
  if (!test.will_run()) {
62✔
190
    impl_->results.tail->ignored = true;
1✔
191
    impl_->results.skip_count++;
1✔
192
  }
193
}
62✔
194

195
String JUnitOutput::create_file_name()
45✔
196
{
197
  if (!impl_->package.empty())
45✔
198
    return encode_file_name(impl_->package) + ".xml";
16✔
199
  return "mutiny.xml";
37✔
200
}
201

202
String JUnitOutput::encode_file_name(const String& file_name)
8✔
203
{
204
  // special character list based on: https://en.wikipedia.org/wiki/Filename
205
  static const char* const forbidden_characters = "/\\?%*:|\"<>";
206

207
  String result = file_name;
8✔
208
  for (const char* sym = forbidden_characters; *sym; ++sym) {
88✔
209
    string_replace(result, *sym, '_');
80✔
210
  }
211
  return result;
8✔
212
}
×
213

214
void JUnitOutput::set_package_name(const String& package)
8✔
215
{
216
  if (impl_ != nullptr) {
8✔
217
    impl_->package = package;
8✔
218
  }
219
}
8✔
220

UNCOV
221
void JUnitOutput::write_xml_header()
×
222
{
UNCOV
223
  write_to_file("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
×
UNCOV
224
}
×
225

226
void JUnitOutput::write_test_suite_summary()
49✔
227
{
228
  size_t total_assertions = 0;
49✔
229
  for (JUnitTestCaseResultNode* n = impl_->results.head; n; n = n->next)
111✔
230
    total_assertions = n->check_count;
62✔
231

232
  String buf = string_from_format(
233
      "<testsuite errors=\"%d\" failures=\"%d\" skipped=\"%d\" "
234
      "assertions=\"%d\" name=\"%s\" tests=\"%d\" "
235
      "time=\"%d.%03d\" timestamp=\"%s\">\n",
236
      static_cast<int>(impl_->results.error_count),
49✔
237
      static_cast<int>(impl_->results.failure_count),
49✔
238
      static_cast<int>(impl_->results.skip_count),
49✔
239
      static_cast<int>(total_assertions),
240
      impl_->results.group.c_str(),
49✔
241
      static_cast<int>(impl_->results.test_count),
49✔
242
      static_cast<int>(impl_->results.group_exec_time / 1000),
49✔
243
      static_cast<int>(impl_->results.group_exec_time % 1000),
49✔
244
      get_time_string()
245
  );
49✔
246
  write_to_file(buf.c_str());
49✔
247
}
49✔
248

249
String JUnitOutput::encode_xml_text(const String& textbody)
92✔
250
{
251
  String buf = textbody.c_str();
92✔
252
  string_replace(buf, "&", "&amp;");
92✔
253
  string_replace(buf, "\"", "&quot;");
92✔
254
  string_replace(buf, "<", "&lt;");
92✔
255
  string_replace(buf, ">", "&gt;");
92✔
256
  string_replace(buf, "\r", "&#13;");
92✔
257
  string_replace(buf, "\n", "&#10;");
92✔
258
  return buf;
92✔
259
}
×
260

261
void JUnitOutput::write_test_cases()
49✔
262
{
263
  JUnitTestCaseResultNode* cur = impl_->results.head;
49✔
264

265
  while (cur) {
111✔
266
    String buf = string_from_format(
267
        "<testcase classname=\"%s%s%s\" name=\"%s\" assertions=\"%d\" "
268
        "time=\"%d.%03d\" file=\"%s\" line=\"%d\">\n",
269
        impl_->package.c_str(),
62✔
270
        impl_->package.empty() ? "" : ".",
124✔
271
        impl_->results.group.c_str(),
62✔
272
        cur->name.c_str(),
273
        static_cast<int>(cur->check_count - impl_->results.total_check_count),
62✔
274
        static_cast<int>(cur->exec_time / 1000),
62✔
275
        static_cast<int>(cur->exec_time % 1000),
62✔
276
        cur->file.c_str(),
277
        static_cast<int>(cur->line_number)
62✔
278
    );
62✔
279
    write_to_file(buf.c_str());
62✔
280

281
    impl_->results.total_check_count = cur->check_count;
62✔
282

283
    if (cur->properties) {
62✔
284
      write_to_file("<properties>\n");
4✔
285
      for (TestProperty* prop = cur->properties; prop; prop = prop->next) {
9✔
286
        String prop_buf = string_from_format(
287
            "<property name=\"%s\" value=\"%s\"/>\n",
288
            encode_xml_text(prop->name).c_str(),
5✔
289
            encode_xml_text(prop->value).c_str()
10✔
290
        );
5✔
291
        write_to_file(prop_buf.c_str());
5✔
292
      }
5✔
293
      write_to_file("</properties>\n");
4✔
294
    }
295

296
    if (cur->failure) {
62✔
297
      if (cur->failure_is_error)
17✔
298
        write_error(cur);
4✔
299
      else
300
        write_failure(cur);
13✔
301
    } else if (cur->ignored) {
45✔
302
      if (cur->skip_message.empty()) {
3✔
303
        write_to_file("<skipped />\n");
1✔
304
      } else {
305
        write_to_file(string_from_format(
2✔
306
                          "<skipped message=\"%s\" />\n",
307
                          encode_xml_text(cur->skip_message).c_str()
4✔
308
        )
309
                          .c_str());
310
      }
311
    }
312

313
    if (!cur->output.empty()) {
62✔
314
      write_to_file("<system-out>");
1✔
315
      write_to_file(encode_xml_text(cur->output));
1✔
316
      write_to_file("</system-out>\n");
1✔
317
    }
318

319
    write_to_file("</testcase>\n");
62✔
320
    cur = cur->next;
62✔
321
  }
62✔
322
}
49✔
323

324
void JUnitOutput::write_failure(JUnitTestCaseResultNode* node)
13✔
325
{
326
  String file = encode_xml_text(node->failure->get_file_name());
13✔
327
  String msg = encode_xml_text(node->failure->get_message());
13✔
328
  String buf = string_from_format(
329
      "<failure message=\"%s:%d: %s\" type=\"AssertionFailedError\">\n"
330
      "%s:%d: %s\n",
331
      file.c_str(),
332
      static_cast<int>(node->failure->get_failure_line_number()),
26✔
333
      msg.c_str(),
334
      file.c_str(),
335
      static_cast<int>(node->failure->get_failure_line_number()),
26✔
336
      msg.c_str()
337
  );
13✔
338
  write_to_file(buf.c_str());
13✔
339
  write_to_file("</failure>\n");
13✔
340
}
13✔
341

342
void JUnitOutput::write_error(JUnitTestCaseResultNode* node)
4✔
343
{
344
  String msg = encode_xml_text(node->failure->get_message());
4✔
345
  String buf = string_from_format(
346
      "<error message=\"%s\" type=\"UnexpectedException\">\n"
347
      "%s\n",
348
      msg.c_str(),
349
      msg.c_str()
350
  );
4✔
351
  write_to_file(buf.c_str());
4✔
352
  write_to_file("</error>\n");
4✔
353
}
4✔
354

355
void JUnitOutput::write_file_ending()
49✔
356
{
357
  write_to_file("<system-out>");
49✔
358
  write_to_file(encode_xml_text(impl_->std_output));
49✔
359
  write_to_file("</system-out>\n");
49✔
360
  write_to_file("</testsuite>\n");
49✔
361
}
49✔
362

363
void JUnitOutput::write_test_group_to_file()
49✔
364
{
365
  open_file_for_write(String());
49✔
366
  write_test_suite_summary();
49✔
367
  write_test_cases();
49✔
368
  write_file_ending();
49✔
369
  close_file();
49✔
370
}
49✔
371

372
void JUnitOutput::print_buffer(const char*) {}
×
373

374
void JUnitOutput::print(const char* output)
3✔
375
{
376
  if (impl_->results.in_test)
3✔
377
    impl_->results.tail->output += output;
1✔
378
  else
379
    impl_->std_output += output;
2✔
380
}
3✔
381

382
void JUnitOutput::print(long) {}
×
383

384
void JUnitOutput::print(size_t) {}
×
385

386
void JUnitOutput::print_test_property(const char* name, const char* value)
5✔
387
{
388
  if (impl_->results.tail == nullptr)
5✔
389
    return;
×
390
  auto* prop = new TestProperty;
5✔
391
  prop->name = name;
5✔
392
  prop->value = value;
5✔
393
  if (impl_->results.tail->properties == nullptr) {
5✔
394
    impl_->results.tail->properties = prop;
4✔
395
    impl_->results.tail->properties_tail = prop;
4✔
396
  } else {
397
    impl_->results.tail->properties_tail->next = prop;
1✔
398
    impl_->results.tail->properties_tail = prop;
1✔
399
  }
400
}
401

402
void JUnitOutput::print_skipped(const char* message)
2✔
403
{
404
  if (impl_->results.tail == nullptr)
2✔
405
    return;
×
406
  impl_->results.tail->ignored = true;
2✔
407
  impl_->results.tail->skip_message = message;
2✔
408
  impl_->results.skip_count++;
2✔
409
}
410

411
void JUnitOutput::print_failure(const Failure& failure)
17✔
412
{
413
  if (impl_->results.tail->failure == nullptr) {
17✔
414
    if (failure.is_error()) {
17✔
415
      impl_->results.error_count++;
4✔
416
      impl_->results.tail->failure_is_error = true;
4✔
417
    } else {
418
      impl_->results.failure_count++;
13✔
419
    }
420
    impl_->results.tail->failure = new Failure(failure);
17✔
421
  }
422
}
17✔
423

424
void JUnitOutput::open_file_for_write(const String& /*file_name*/)
49✔
425
{
426
  impl_->current_group_xml.clear();
49✔
427
}
49✔
428

429
void JUnitOutput::write_to_file(const String& buffer)
422✔
430
{
431
  impl_->current_group_xml += buffer;
422✔
432
}
422✔
433

434
void JUnitOutput::close_file()
49✔
435
{
436
  impl_->accumulated_xml += impl_->current_group_xml;
49✔
437
}
49✔
438

439
} // namespace test
440
} // namespace tiny
441
} // namespace mu
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc