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

thetic / mutiny / 24546306344

17 Apr 2026 03:34AM UTC coverage: 98.629% (-0.01%) from 98.642%
24546306344

Pull #58

github

web-flow
Merge 8cd914c6b into 405a5edbc
Pull Request #58: Fix JUnit XML conformance and output structure

65 of 67 new or added lines in 2 files covered. (97.01%)

1 existing line in 1 file now uncovered.

5109 of 5180 relevant lines covered (98.63%)

4000.65 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;
61✔
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;
46✔
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()
46✔
80
  : impl_(new JUnitTestOutputImpl)
46✔
81
{
82
}
46✔
83

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

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

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

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

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

134
void JUnitOutput::print_tests_ended(const Result& /*result*/)
42✔
135
{
136
  Output::File file = fopen_(create_file_name().c_str(), "w");
42✔
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),
42✔
142
      static_cast<int>(impl_->total_failure_count),
42✔
143
      static_cast<int>(impl_->total_error_count),
42✔
144
      static_cast<int>(impl_->total_skip_count),
42✔
145
      static_cast<int>(impl_->total_exec_time / 1000),
42✔
146
      static_cast<int>(impl_->total_exec_time % 1000),
42✔
147
      impl_->start_timestamp.c_str()
42✔
148
  );
42✔
149
  fputs_(header.c_str(), file);
42✔
150
  fputs_(impl_->accumulated_xml.c_str(), file);
42✔
151
  fputs_("</testsuites>\n", file);
42✔
152
  fclose_(file);
42✔
153
}
42✔
154

155
void JUnitOutput::print_current_group_ended(const Result& result)
48✔
156
{
157
  if (impl_->results.test_count == 0) {
48✔
NEW
158
    reset_test_group_result();
×
NEW
159
    return;
×
160
  }
161
  impl_->results.group_exec_time =
96✔
162
      result.get_current_group_total_execution_time();
48✔
163
  impl_->total_test_count += impl_->results.test_count;
48✔
164
  impl_->total_failure_count += impl_->results.failure_count;
48✔
165
  impl_->total_error_count += impl_->results.error_count;
48✔
166
  impl_->total_skip_count += impl_->results.skip_count;
48✔
167
  impl_->total_exec_time += impl_->results.group_exec_time;
48✔
168
  write_test_group_to_file();
48✔
169
  reset_test_group_result();
48✔
170
}
171

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

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

194
String JUnitOutput::create_file_name()
46✔
195
{
196
  if (!impl_->package.empty())
46✔
197
    return encode_file_name(impl_->package) + ".xml";
22✔
198
  return "mutiny.xml";
35✔
199
}
200

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

206
  String result = file_name;
11✔
207
  for (const char* sym = forbidden_characters; *sym; ++sym) {
121✔
208
    string_replace(result, *sym, '_');
110✔
209
  }
210
  return result;
11✔
211
}
×
212

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

220
void JUnitOutput::write_test_suite_summary()
48✔
221
{
222
  size_t total_assertions = 0;
48✔
223
  for (JUnitTestCaseResultNode* n = impl_->results.head; n; n = n->next)
109✔
224
    total_assertions = n->check_count;
61✔
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),
48✔
231
      static_cast<int>(impl_->results.failure_count),
48✔
232
      static_cast<int>(impl_->results.skip_count),
48✔
233
      static_cast<int>(total_assertions),
234
      impl_->results.group.c_str(),
48✔
235
      static_cast<int>(impl_->results.test_count),
48✔
236
      static_cast<int>(impl_->results.group_exec_time / 1000),
48✔
237
      static_cast<int>(impl_->results.group_exec_time % 1000),
48✔
238
      get_time_string()
239
  );
48✔
240
  write_to_file(buf.c_str());
48✔
241
}
48✔
242

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

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

259
  while (cur) {
109✔
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(),
61✔
264
        impl_->package.empty() ? "" : ".",
122✔
265
        impl_->results.group.c_str(),
61✔
266
        cur->name.c_str(),
267
        static_cast<int>(cur->check_count - impl_->results.total_check_count),
61✔
268
        static_cast<int>(cur->exec_time / 1000),
61✔
269
        static_cast<int>(cur->exec_time % 1000),
61✔
270
        cur->file.c_str(),
271
        static_cast<int>(cur->line_number)
61✔
272
    );
61✔
273
    write_to_file(buf.c_str());
61✔
274

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

277
    if (cur->properties) {
61✔
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) {
61✔
291
      if (cur->failure_is_error)
18✔
292
        write_error(cur);
4✔
293
      else
294
        write_failure(cur);
14✔
295
    } else if (cur->ignored) {
43✔
296
      if (cur->skip_message.empty()) {
4✔
UNCOV
297
        write_to_file("<skipped />\n");
×
298
      } else {
299
        write_to_file(string_from_format(
4✔
300
                          "<skipped message=\"%s\" />\n",
301
                          encode_xml_text(cur->skip_message).c_str()
8✔
302
        )
303
                          .c_str());
304
      }
305
    }
306

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

312
void JUnitOutput::write_failure(JUnitTestCaseResultNode* node)
14✔
313
{
314
  String file = encode_xml_text(node->failure->get_file_name());
14✔
315
  String msg = encode_xml_text(node->failure->get_message());
14✔
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()),
28✔
321
      msg.c_str(),
322
      file.c_str(),
323
      static_cast<int>(node->failure->get_failure_line_number()),
28✔
324
      msg.c_str()
325
  );
14✔
326
  write_to_file(buf.c_str());
14✔
327
  write_to_file("</failure>\n");
14✔
328
}
14✔
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()
48✔
344
{
345
  write_to_file("</testsuite>\n");
48✔
346
}
48✔
347

348
void JUnitOutput::write_test_group_to_file()
48✔
349
{
350
  open_file_for_write();
48✔
351
  write_test_suite_summary();
48✔
352
  write_test_cases();
48✔
353
  write_file_ending();
48✔
354
  close_file();
48✔
355
}
48✔
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)
3✔
376
{
377
  if (impl_->results.tail == nullptr)
3✔
378
    return;
×
379
  impl_->results.tail->ignored = true;
3✔
380
  impl_->results.tail->skip_message = message;
3✔
381
  impl_->results.skip_count++;
3✔
382
}
383

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

397
void JUnitOutput::open_file_for_write()
48✔
398
{
399
  impl_->current_group_xml.clear();
48✔
400
}
48✔
401

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

407
void JUnitOutput::close_file()
48✔
408
{
409
  impl_->accumulated_xml += impl_->current_group_xml;
48✔
410
}
48✔
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