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

thetic / mutiny / 24519687371

16 Apr 2026 03:43PM UTC coverage: 98.629% (-0.01%) from 98.642%
24519687371

Pull #58

github

web-flow
Merge 36dc81cd3 into 405a5edbc
Pull Request #58: Junity

64 of 64 new or added lines in 2 files covered. (100.0%)

3 existing lines in 1 file now uncovered.

5109 of 5180 relevant lines covered (98.63%)

3974.43 hits per line

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

96.38
/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;
56✔
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
  TestProperty* properties{ nullptr };
43
  TestProperty* properties_tail{ nullptr };
44
  JUnitTestCaseResultNode* next{ nullptr };
45
};
46

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

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

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

79
JUnitOutput::JUnitOutput()
45✔
80
  : impl_(new JUnitTestOutputImpl)
45✔
81
{
82
}
45✔
83

84
JUnitOutput::~JUnitOutput()
90✔
85
{
86
  reset_test_group_result();
45✔
87
  delete impl_;
45✔
88
}
90✔
89

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

114
void JUnitOutput::print_tests_started()
40✔
115
{
116
  impl_->accumulated_xml.clear();
40✔
117
  impl_->total_test_count = 0;
40✔
118
  impl_->total_failure_count = 0;
40✔
119
  impl_->total_error_count = 0;
40✔
120
  impl_->total_skip_count = 0;
40✔
121
  impl_->total_exec_time = 0;
40✔
122
  impl_->start_timestamp = get_time_string();
40✔
123
}
40✔
124

125
void JUnitOutput::print_current_group_started(const Shell& /*test*/) {}
45✔
126

127
void JUnitOutput::print_current_test_ended(const Result& result)
56✔
128
{
129
  impl_->results.tail->exec_time =
112✔
130
      result.get_current_test_total_execution_time();
56✔
131
  impl_->results.tail->check_count = result.get_check_count();
56✔
132
}
56✔
133

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

155
void JUnitOutput::print_current_group_ended(const Result& result)
45✔
156
{
157
  impl_->results.group_exec_time =
90✔
158
      result.get_current_group_total_execution_time();
45✔
159
  impl_->total_test_count += impl_->results.test_count;
45✔
160
  impl_->total_failure_count += impl_->results.failure_count;
45✔
161
  impl_->total_error_count += impl_->results.error_count;
45✔
162
  impl_->total_skip_count += impl_->results.skip_count;
45✔
163
  impl_->total_exec_time += impl_->results.group_exec_time;
45✔
164
  write_test_group_to_file();
45✔
165
  reset_test_group_result();
45✔
166
}
45✔
167

168
void JUnitOutput::print_current_test_started(const Shell& test)
56✔
169
{
170
  impl_->results.test_count++;
56✔
171
  impl_->results.group = test.get_group();
56✔
172
  impl_->results.start_time = get_time_in_millis();
56✔
173

174
  if (impl_->results.tail == nullptr) {
56✔
175
    impl_->results.head = impl_->results.tail = new JUnitTestCaseResultNode;
45✔
176
  } else {
177
    impl_->results.tail->next = new JUnitTestCaseResultNode;
11✔
178
    impl_->results.tail = impl_->results.tail->next;
11✔
179
  }
180
  impl_->results.tail->name = test.get_name();
56✔
181
  impl_->results.tail->file = test.get_file();
56✔
182
  impl_->results.tail->line_number = test.get_line_number();
56✔
183
  if (!test.will_run()) {
56✔
184
    impl_->results.tail->ignored = true;
1✔
185
    impl_->results.skip_count++;
1✔
186
  }
187
}
56✔
188

189
String JUnitOutput::create_file_name()
45✔
190
{
191
  if (!impl_->package.empty())
45✔
192
    return encode_file_name(impl_->package) + ".xml";
22✔
193
  return "mutiny.xml";
34✔
194
}
195

196
String JUnitOutput::encode_file_name(const String& file_name)
11✔
197
{
198
  // special character list based on: https://en.wikipedia.org/wiki/Filename
199
  static const char* const forbidden_characters = "/\\?%*:|\"<>";
200

201
  String result = file_name;
11✔
202
  for (const char* sym = forbidden_characters; *sym; ++sym) {
121✔
203
    string_replace(result, *sym, '_');
110✔
204
  }
205
  return result;
11✔
206
}
×
207

208
void JUnitOutput::set_package_name(const String& package)
11✔
209
{
210
  if (impl_ != nullptr) {
11✔
211
    impl_->package = package;
11✔
212
  }
213
}
11✔
214

UNCOV
215
void JUnitOutput::write_xml_header()
×
216
{
UNCOV
217
  write_to_file("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
×
UNCOV
218
}
×
219

220
void JUnitOutput::write_test_suite_summary()
45✔
221
{
222
  size_t total_assertions = 0;
45✔
223
  for (JUnitTestCaseResultNode* n = impl_->results.head; n; n = n->next)
101✔
224
    total_assertions = n->check_count;
56✔
225

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

243
String JUnitOutput::encode_xml_text(const String& textbody)
42✔
244
{
245
  String buf = textbody.c_str();
42✔
246
  string_replace(buf, "&", "&amp;");
42✔
247
  string_replace(buf, "\"", "&quot;");
42✔
248
  string_replace(buf, "<", "&lt;");
42✔
249
  string_replace(buf, ">", "&gt;");
42✔
250
  string_replace(buf, "\r", "&#13;");
42✔
251
  string_replace(buf, "\n", "&#10;");
42✔
252
  return buf;
42✔
253
}
×
254

255
void JUnitOutput::write_test_cases()
45✔
256
{
257
  JUnitTestCaseResultNode* cur = impl_->results.head;
45✔
258

259
  while (cur) {
101✔
260
    String buf = string_from_format(
261
        "<testcase classname=\"%s%s%s\" name=\"%s\" assertions=\"%d\" "
262
        "time=\"%d.%03d\" file=\"%s\" line=\"%d\">\n",
263
        impl_->package.c_str(),
56✔
264
        impl_->package.empty() ? "" : ".",
112✔
265
        impl_->results.group.c_str(),
56✔
266
        cur->name.c_str(),
267
        static_cast<int>(cur->check_count - impl_->results.total_check_count),
56✔
268
        static_cast<int>(cur->exec_time / 1000),
56✔
269
        static_cast<int>(cur->exec_time % 1000),
56✔
270
        cur->file.c_str(),
271
        static_cast<int>(cur->line_number)
56✔
272
    );
56✔
273
    write_to_file(buf.c_str());
56✔
274

275
    impl_->results.total_check_count = cur->check_count;
56✔
276

277
    if (cur->properties) {
56✔
278
      write_to_file("<properties>\n");
4✔
279
      for (TestProperty* prop = cur->properties; prop; prop = prop->next) {
9✔
280
        String prop_buf = string_from_format(
281
            "<property name=\"%s\" value=\"%s\"/>\n",
282
            encode_xml_text(prop->name).c_str(),
5✔
283
            encode_xml_text(prop->value).c_str()
10✔
284
        );
5✔
285
        write_to_file(prop_buf.c_str());
5✔
286
      }
5✔
287
      write_to_file("</properties>\n");
4✔
288
    }
289

290
    if (cur->failure) {
56✔
291
      if (cur->failure_is_error)
17✔
292
        write_error(cur);
4✔
293
      else
294
        write_failure(cur);
13✔
295
    } else if (cur->ignored) {
39✔
296
      if (cur->skip_message.empty()) {
3✔
297
        write_to_file("<skipped />\n");
1✔
298
      } else {
299
        write_to_file(string_from_format(
2✔
300
                          "<skipped message=\"%s\" />\n",
301
                          encode_xml_text(cur->skip_message).c_str()
4✔
302
        )
303
                          .c_str());
304
      }
305
    }
306

307
    write_to_file("</testcase>\n");
56✔
308
    cur = cur->next;
56✔
309
  }
56✔
310
}
45✔
311

312
void JUnitOutput::write_failure(JUnitTestCaseResultNode* node)
13✔
313
{
314
  String file = encode_xml_text(node->failure->get_file_name());
13✔
315
  String msg = encode_xml_text(node->failure->get_message());
13✔
316
  String buf = string_from_format(
317
      "<failure message=\"%s:%d: %s\" type=\"AssertionFailedError\">\n"
318
      "%s:%d: %s\n",
319
      file.c_str(),
320
      static_cast<int>(node->failure->get_failure_line_number()),
26✔
321
      msg.c_str(),
322
      file.c_str(),
323
      static_cast<int>(node->failure->get_failure_line_number()),
26✔
324
      msg.c_str()
325
  );
13✔
326
  write_to_file(buf.c_str());
13✔
327
  write_to_file("</failure>\n");
13✔
328
}
13✔
329

330
void JUnitOutput::write_error(JUnitTestCaseResultNode* node)
4✔
331
{
332
  String msg = encode_xml_text(node->failure->get_message());
4✔
333
  String buf = string_from_format(
334
      "<error message=\"%s\" type=\"UnexpectedException\">\n"
335
      "%s\n",
336
      msg.c_str(),
337
      msg.c_str()
338
  );
4✔
339
  write_to_file(buf.c_str());
4✔
340
  write_to_file("</error>\n");
4✔
341
}
4✔
342

343
void JUnitOutput::write_file_ending()
45✔
344
{
345
  write_to_file("</testsuite>\n");
45✔
346
}
45✔
347

348
void JUnitOutput::write_test_group_to_file()
45✔
349
{
350
  open_file_for_write(String());
45✔
351
  write_test_suite_summary();
45✔
352
  write_test_cases();
45✔
353
  write_file_ending();
45✔
354
  close_file();
45✔
355
}
45✔
356

357
void JUnitOutput::print_buffer(const char*) {}
×
358

359
void JUnitOutput::print_test_property(const char* name, const char* value)
5✔
360
{
361
  if (impl_->results.tail == nullptr)
5✔
362
    return;
×
363
  auto* prop = new TestProperty;
5✔
364
  prop->name = name;
5✔
365
  prop->value = value;
5✔
366
  if (impl_->results.tail->properties == nullptr) {
5✔
367
    impl_->results.tail->properties = prop;
4✔
368
    impl_->results.tail->properties_tail = prop;
4✔
369
  } else {
370
    impl_->results.tail->properties_tail->next = prop;
1✔
371
    impl_->results.tail->properties_tail = prop;
1✔
372
  }
373
}
374

375
void JUnitOutput::print_skipped(const char* message)
2✔
376
{
377
  if (impl_->results.tail == nullptr)
2✔
378
    return;
×
379
  impl_->results.tail->ignored = true;
2✔
380
  impl_->results.tail->skip_message = message;
2✔
381
  impl_->results.skip_count++;
2✔
382
}
383

384
void JUnitOutput::print_failure(const Failure& failure)
17✔
385
{
386
  if (impl_->results.tail->failure == nullptr) {
17✔
387
    if (failure.is_error()) {
17✔
388
      impl_->results.error_count++;
4✔
389
      impl_->results.tail->failure_is_error = true;
4✔
390
    } else {
391
      impl_->results.failure_count++;
13✔
392
    }
393
    impl_->results.tail->failure = new Failure(failure);
17✔
394
  }
395
}
17✔
396

397
void JUnitOutput::open_file_for_write(const String& /*file_name*/)
45✔
398
{
399
  impl_->current_group_xml.clear();
45✔
400
}
45✔
401

402
void JUnitOutput::write_to_file(const String& buffer)
252✔
403
{
404
  impl_->current_group_xml += buffer;
252✔
405
}
252✔
406

407
void JUnitOutput::close_file()
45✔
408
{
409
  impl_->accumulated_xml += impl_->current_group_xml;
45✔
410
}
45✔
411

412
} // namespace test
413
} // namespace tiny
414
} // 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