GCC Code Coverage Report | |||||||||||||||||||||
|
|||||||||||||||||||||
Line | Branch | Exec | Source |
1 |
#ifndef SRC_NODE_OPTIONS_INL_H_ |
||
2 |
#define SRC_NODE_OPTIONS_INL_H_ |
||
3 |
|||
4 |
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS |
||
5 |
|||
6 |
#include <cstdlib> |
||
7 |
#include "node_options.h" |
||
8 |
#include "util.h" |
||
9 |
|||
10 |
namespace node { |
||
11 |
|||
12 |
588539 |
PerIsolateOptions* PerProcessOptions::get_per_isolate_options() { |
|
13 |
588539 |
return per_isolate.get(); |
|
14 |
} |
||
15 |
|||
16 |
549174 |
EnvironmentOptions* PerIsolateOptions::get_per_env_options() { |
|
17 |
549174 |
return per_env.get(); |
|
18 |
} |
||
19 |
|||
20 |
namespace options_parser { |
||
21 |
|||
22 |
template <typename Options> |
||
23 |
843960 |
void OptionsParser<Options>::AddOption(const char* name, |
|
24 |
const char* help_text, |
||
25 |
bool Options::* field, |
||
26 |
OptionEnvvarSettings env_setting, |
||
27 |
bool default_is_true) { |
||
28 |
843960 |
options_.emplace(name, |
|
29 |
OptionInfo{kBoolean, |
||
30 |
std::make_shared<SimpleOptionField<bool>>(field), |
||
31 |
env_setting, |
||
32 |
help_text, |
||
33 |
default_is_true}); |
||
34 |
843960 |
} |
|
35 |
|||
36 |
template <typename Options> |
||
37 |
16230 |
void OptionsParser<Options>::AddOption(const char* name, |
|
38 |
const char* help_text, |
||
39 |
uint64_t Options::* field, |
||
40 |
OptionEnvvarSettings env_setting) { |
||
41 |
16230 |
options_.emplace( |
|
42 |
name, |
||
43 |
OptionInfo{kUInteger, |
||
44 |
std::make_shared<SimpleOptionField<uint64_t>>(field), |
||
45 |
env_setting, |
||
46 |
help_text}); |
||
47 |
16230 |
} |
|
48 |
|||
49 |
template <typename Options> |
||
50 |
43280 |
void OptionsParser<Options>::AddOption(const char* name, |
|
51 |
const char* help_text, |
||
52 |
int64_t Options::* field, |
||
53 |
OptionEnvvarSettings env_setting) { |
||
54 |
43280 |
options_.emplace( |
|
55 |
name, |
||
56 |
OptionInfo{kInteger, |
||
57 |
std::make_shared<SimpleOptionField<int64_t>>(field), |
||
58 |
env_setting, |
||
59 |
help_text}); |
||
60 |
43280 |
} |
|
61 |
|||
62 |
template <typename Options> |
||
63 |
302960 |
void OptionsParser<Options>::AddOption(const char* name, |
|
64 |
const char* help_text, |
||
65 |
std::string Options::* field, |
||
66 |
OptionEnvvarSettings env_setting) { |
||
67 |
302960 |
options_.emplace( |
|
68 |
name, |
||
69 |
OptionInfo{kString, |
||
70 |
std::make_shared<SimpleOptionField<std::string>>(field), |
||
71 |
env_setting, |
||
72 |
help_text}); |
||
73 |
302960 |
} |
|
74 |
|||
75 |
template <typename Options> |
||
76 |
54100 |
void OptionsParser<Options>::AddOption( |
|
77 |
const char* name, |
||
78 |
const char* help_text, |
||
79 |
std::vector<std::string> Options::* field, |
||
80 |
OptionEnvvarSettings env_setting) { |
||
81 |
54100 |
options_.emplace(name, OptionInfo { |
|
82 |
kStringList, |
||
83 |
std::make_shared<SimpleOptionField<std::vector<std::string>>>(field), |
||
84 |
env_setting, |
||
85 |
help_text |
||
86 |
}); |
||
87 |
54100 |
} |
|
88 |
|||
89 |
template <typename Options> |
||
90 |
5410 |
void OptionsParser<Options>::AddOption(const char* name, |
|
91 |
const char* help_text, |
||
92 |
HostPort Options::* field, |
||
93 |
OptionEnvvarSettings env_setting) { |
||
94 |
5410 |
options_.emplace( |
|
95 |
name, |
||
96 |
OptionInfo{kHostPort, |
||
97 |
std::make_shared<SimpleOptionField<HostPort>>(field), |
||
98 |
env_setting, |
||
99 |
help_text}); |
||
100 |
5410 |
} |
|
101 |
|||
102 |
template <typename Options> |
||
103 |
97380 |
void OptionsParser<Options>::AddOption(const char* name, |
|
104 |
const char* help_text, |
||
105 |
NoOp no_op_tag, |
||
106 |
OptionEnvvarSettings env_setting) { |
||
107 |
97380 |
options_.emplace(name, OptionInfo{kNoOp, nullptr, env_setting, help_text}); |
|
108 |
97380 |
} |
|
109 |
|||
110 |
template <typename Options> |
||
111 |
140660 |
void OptionsParser<Options>::AddOption(const char* name, |
|
112 |
const char* help_text, |
||
113 |
V8Option v8_option_tag, |
||
114 |
OptionEnvvarSettings env_setting) { |
||
115 |
140660 |
options_.emplace(name, |
|
116 |
OptionInfo{kV8Option, nullptr, env_setting, help_text}); |
||
117 |
140660 |
} |
|
118 |
|||
119 |
template <typename Options> |
||
120 |
173120 |
void OptionsParser<Options>::AddAlias(const char* from, |
|
121 |
const char* to) { |
||
122 |
✓✓ | 346240 |
aliases_[from] = { to }; |
123 |
173120 |
} |
|
124 |
|||
125 |
template <typename Options> |
||
126 |
64920 |
void OptionsParser<Options>::AddAlias(const char* from, |
|
127 |
const std::vector<std::string>& to) { |
||
128 |
64920 |
aliases_[from] = to; |
|
129 |
64920 |
} |
|
130 |
|||
131 |
template <typename Options> |
||
132 |
64920 |
void OptionsParser<Options>::AddAlias( |
|
133 |
const char* from, |
||
134 |
const std::initializer_list<std::string>& to) { |
||
135 |
64920 |
AddAlias(from, std::vector<std::string>(to)); |
|
136 |
64920 |
} |
|
137 |
|||
138 |
template <typename Options> |
||
139 |
108200 |
void OptionsParser<Options>::Implies(const char* from, |
|
140 |
const char* to) { |
||
141 |
108200 |
auto it = options_.find(to); |
|
142 |
✗✓ | 108200 |
CHECK_NE(it, options_.end()); |
143 |
✓✓✗✓ ✗✓ |
108200 |
CHECK(it->second.type == kBoolean || it->second.type == kV8Option); |
144 |
216400 |
implications_.emplace( |
|
145 |
216400 |
from, Implication{it->second.type, to, it->second.field, true}); |
|
146 |
108200 |
} |
|
147 |
|||
148 |
template <typename Options> |
||
149 |
21640 |
void OptionsParser<Options>::ImpliesNot(const char* from, |
|
150 |
const char* to) { |
||
151 |
21640 |
auto it = options_.find(to); |
|
152 |
✗✓ | 21640 |
CHECK_NE(it, options_.end()); |
153 |
✗✓ | 21640 |
CHECK_EQ(it->second.type, kBoolean); |
154 |
43280 |
implications_.emplace( |
|
155 |
43280 |
from, Implication{it->second.type, to, it->second.field, false}); |
|
156 |
21640 |
} |
|
157 |
|||
158 |
template <typename Options> |
||
159 |
template <typename OriginalField, typename ChildOptions> |
||
160 |
2347940 |
auto OptionsParser<Options>::Convert( |
|
161 |
std::shared_ptr<OriginalField> original, |
||
162 |
ChildOptions* (Options::* get_child)()) { |
||
163 |
// If we have a field on ChildOptions, and we want to access it from an |
||
164 |
// Options instance, we call get_child() on the original Options and then |
||
165 |
// access it, i.e. this class implements a kind of function chaining. |
||
166 |
struct AdaptedField : BaseOptionField { |
||
167 |
1166241 |
void* LookupImpl(Options* options) const override { |
|
168 |
1166241 |
return original->LookupImpl((options->*get_child)()); |
|
169 |
} |
||
170 |
|||
171 |
1173970 |
AdaptedField( |
|
172 |
std::shared_ptr<OriginalField> original, |
||
173 |
ChildOptions* (Options::* get_child)()) |
||
174 |
1173970 |
: original(original), get_child(get_child) {} |
|
175 |
|||
176 |
std::shared_ptr<OriginalField> original; |
||
177 |
ChildOptions* (Options::* get_child)(); |
||
178 |
}; |
||
179 |
|||
180 |
return std::shared_ptr<BaseOptionField>( |
||
181 |
2347940 |
new AdaptedField(original, get_child)); |
|
182 |
} |
||
183 |
template <typename Options> |
||
184 |
template <typename ChildOptions> |
||
185 |
2196460 |
auto OptionsParser<Options>::Convert( |
|
186 |
typename OptionsParser<ChildOptions>::OptionInfo original, |
||
187 |
ChildOptions* (Options::* get_child)()) { |
||
188 |
2196460 |
return OptionInfo{original.type, |
|
189 |
2196460 |
Convert(original.field, get_child), |
|
190 |
2196460 |
original.env_setting, |
|
191 |
2196460 |
original.help_text, |
|
192 |
2196460 |
original.default_is_true}; |
|
193 |
} |
||
194 |
|||
195 |
template <typename Options> |
||
196 |
template <typename ChildOptions> |
||
197 |
151480 |
auto OptionsParser<Options>::Convert( |
|
198 |
typename OptionsParser<ChildOptions>::Implication original, |
||
199 |
ChildOptions* (Options::* get_child)()) { |
||
200 |
return Implication{ |
||
201 |
151480 |
original.type, |
|
202 |
151480 |
original.name, |
|
203 |
151480 |
Convert(original.target_field, get_child), |
|
204 |
151480 |
original.target_value, |
|
205 |
151480 |
}; |
|
206 |
} |
||
207 |
|||
208 |
template <typename Options> |
||
209 |
template <typename ChildOptions> |
||
210 |
32460 |
void OptionsParser<Options>::Insert( |
|
211 |
const OptionsParser<ChildOptions>& child_options_parser, |
||
212 |
ChildOptions* (Options::* get_child)()) { |
||
213 |
32460 |
aliases_.insert(std::begin(child_options_parser.aliases_), |
|
214 |
32460 |
std::end(child_options_parser.aliases_)); |
|
215 |
|||
216 |
✓✓ | 2228920 |
for (const auto& pair : child_options_parser.options_) |
217 |
2196460 |
options_.emplace(pair.first, Convert(pair.second, get_child)); |
|
218 |
|||
219 |
✓✓ | 183940 |
for (const auto& pair : child_options_parser.implications_) |
220 |
151480 |
implications_.emplace(pair.first, Convert(pair.second, get_child)); |
|
221 |
32460 |
} |
|
222 |
|||
223 |
18 |
inline std::string NotAllowedInEnvErr(const std::string& arg) { |
|
224 |
18 |
return arg + " is not allowed in NODE_OPTIONS"; |
|
225 |
} |
||
226 |
|||
227 |
7 |
inline std::string RequiresArgumentErr(const std::string& arg) { |
|
228 |
7 |
return arg + " requires an argument"; |
|
229 |
} |
||
230 |
|||
231 |
1 |
inline std::string NegationImpliesBooleanError(const std::string& arg) { |
|
232 |
1 |
return arg + " is an invalid negation because it is not a boolean option"; |
|
233 |
} |
||
234 |
|||
235 |
// We store some of the basic information around a single Parse call inside |
||
236 |
// this struct, to separate storage of command line arguments and their |
||
237 |
// handling. In particular, this makes it easier to introduce 'synthetic' |
||
238 |
// arguments that get inserted by expanding option aliases. |
||
239 |
struct ArgsInfo { |
||
240 |
// Generally, the idea here is that the first entry in `*underlying` stores |
||
241 |
// the "0th" argument (the program name), then `synthetic_args` are inserted, |
||
242 |
// followed by the remainder of `*underlying`. |
||
243 |
std::vector<std::string>* underlying; |
||
244 |
std::vector<std::string> synthetic_args; |
||
245 |
|||
246 |
std::vector<std::string>* exec_args; |
||
247 |
|||
248 |
11260 |
ArgsInfo(std::vector<std::string>* args, |
|
249 |
std::vector<std::string>* exec_args) |
||
250 |
11260 |
: underlying(args), exec_args(exec_args) {} |
|
251 |
|||
252 |
17011 |
size_t remaining() const { |
|
253 |
// -1 to account for the program name. |
||
254 |
17011 |
return underlying->size() - 1 + synthetic_args.size(); |
|
255 |
} |
||
256 |
|||
257 |
17011 |
bool empty() const { return remaining() == 0; } |
|
258 |
11260 |
const std::string& program_name() const { return underlying->at(0); } |
|
259 |
|||
260 |
22356 |
std::string& first() { |
|
261 |
✓✓ | 22356 |
return synthetic_args.empty() ? underlying->at(1) : synthetic_args.front(); |
262 |
} |
||
263 |
|||
264 |
3277 |
std::string pop_first() { |
|
265 |
3277 |
std::string ret = std::move(first()); |
|
266 |
✓✓ | 3277 |
if (synthetic_args.empty()) { |
267 |
// Only push arguments to `exec_args` that were also originally passed |
||
268 |
// on the command line (i.e. not generated through alias expansion). |
||
269 |
// '--' is a special case here since its purpose is to end `exec_argv`, |
||
270 |
// which is why we do not include it. |
||
271 |
✓✓✓✓ ✓✓ |
3074 |
if (exec_args != nullptr && ret != "--") |
272 |
2952 |
exec_args->push_back(ret); |
|
273 |
3074 |
underlying->erase(underlying->begin() + 1); |
|
274 |
} else { |
||
275 |
203 |
synthetic_args.erase(synthetic_args.begin()); |
|
276 |
} |
||
277 |
3277 |
return ret; |
|
278 |
} |
||
279 |
}; |
||
280 |
|||
281 |
template <typename Options> |
||
282 |
22520 |
void OptionsParser<Options>::Parse( |
|
283 |
std::vector<std::string>* const orig_args, |
||
284 |
std::vector<std::string>* const exec_args, |
||
285 |
std::vector<std::string>* const v8_args, |
||
286 |
Options* const options, |
||
287 |
OptionEnvvarSettings required_env_settings, |
||
288 |
std::vector<std::string>* const errors) const { |
||
289 |
45040 |
ArgsInfo args(orig_args, exec_args); |
|
290 |
|||
291 |
// The first entry is the process name. Make sure it ends up in the V8 argv, |
||
292 |
// since V8::SetFlagsFromCommandLine() expects that to hold true for that |
||
293 |
// array as well. |
||
294 |
✓✗ | 22520 |
if (v8_args->empty()) |
295 |
22520 |
v8_args->push_back(args.program_name()); |
|
296 |
|||
297 |
✓✓✓✗ ✓✓ |
27334 |
while (!args.empty() && errors->empty()) { |
298 |
✓✓✓✓ ✓✓ |
14652 |
if (args.first().size() <= 1 || args.first()[0] != '-') break; |
299 |
|||
300 |
// We know that we're either going to consume this |
||
301 |
// argument or fail completely. |
||
302 |
4896 |
const std::string arg = args.pop_first(); |
|
303 |
|||
304 |
✓✓ | 4896 |
if (arg == "--") { |
305 |
✓✓ | 32 |
if (required_env_settings == kAllowedInEnvironment) |
306 |
2 |
errors->push_back(NotAllowedInEnvErr("--")); |
|
307 |
32 |
break; |
|
308 |
} |
||
309 |
|||
310 |
// Only allow --foo=bar notation for options starting with double dashes. |
||
311 |
// (E.g. -e=a is not allowed as shorthand for --eval=a, which would |
||
312 |
// otherwise be the result of alias expansion.) |
||
313 |
4864 |
const std::string::size_type equals_index = |
|
314 |
✓✗✓✓ |
4864 |
arg[0] == '-' && arg[1] == '-' ? arg.find('=') : std::string::npos; |
315 |
✓✓ | 4864 |
std::string name = |
316 |
equals_index == std::string::npos ? arg : arg.substr(0, equals_index); |
||
317 |
|||
318 |
// Store the 'original name' of the argument. This name differs from |
||
319 |
// 'name' in that it contains a possible '=' sign and is not affected |
||
320 |
// by alias expansion. |
||
321 |
4864 |
std::string original_name = name; |
|
322 |
✓✓ | 4864 |
if (equals_index != std::string::npos) |
323 |
470 |
original_name += '='; |
|
324 |
|||
325 |
4885 |
auto missing_argument = [&]() { |
|
326 |
7 |
errors->push_back(RequiresArgumentErr(original_name)); |
|
327 |
}; |
||
328 |
|||
329 |
// Normalize by replacing `_` with `-` in options. |
||
330 |
✓✓ | 58732 |
for (std::string::size_type i = 2; i < name.size(); ++i) { |
331 |
✓✓ | 53868 |
if (name[i] == '_') |
332 |
78 |
name[i] = '-'; |
|
333 |
} |
||
334 |
|||
335 |
// Convert --no-foo to --foo and keep in mind that we're negating. |
||
336 |
4864 |
bool is_negation = false; |
|
337 |
✓✓ | 4864 |
if (name.find("--no-") == 0) { |
338 |
336 |
name.erase(2, 3); // remove no- |
|
339 |
336 |
is_negation = true; |
|
340 |
} |
||
341 |
|||
342 |
{ |
||
343 |
4864 |
auto it = aliases_.end(); |
|
344 |
// Expand aliases: |
||
345 |
// - If `name` can be found in `aliases_`. |
||
346 |
// - If `name` + '=' can be found in `aliases_`. |
||
347 |
// - If `name` + " <arg>" can be found in `aliases_`, and we have |
||
348 |
// a subsequent argument that does not start with '-' itself. |
||
349 |
✓✓ | 13364 |
while ((it = aliases_.find(name)) != aliases_.end() || |
350 |
✓✓ | 596 |
(equals_index != std::string::npos && |
351 |
✓✓✓✓ |
25364 |
(it = aliases_.find(name + '=')) != aliases_.end()) || |
352 |
✓✓ | 5020 |
(!args.empty() && |
353 |
✓✓ | 4514 |
!args.first().empty() && |
354 |
✓✓✓✓ |
7830 |
args.first()[0] != '-' && |
355 |
✓✓✓✓ |
9860 |
(it = aliases_.find(name + " <arg>")) != aliases_.end())) { |
356 |
1680 |
const std::string prev_name = std::move(name); |
|
357 |
1680 |
const std::vector<std::string>& expansion = it->second; |
|
358 |
|||
359 |
// Use the first entry in the expansion as the new 'name'. |
||
360 |
1680 |
name = expansion.front(); |
|
361 |
|||
362 |
✓✓ | 1680 |
if (expansion.size() > 1) { |
363 |
// The other arguments, if any, are going to be handled later. |
||
364 |
1224 |
args.synthetic_args.insert( |
|
365 |
408 |
args.synthetic_args.begin(), |
|
366 |
816 |
expansion.begin() + 1, |
|
367 |
expansion.end()); |
||
368 |
} |
||
369 |
|||
370 |
✓✓ | 1680 |
if (name == prev_name) break; |
371 |
} |
||
372 |
} |
||
373 |
|||
374 |
4864 |
auto it = options_.find(name); |
|
375 |
|||
376 |
4864 |
if ((it == options_.end() || |
|
377 |
✓✓✓✓ ✓✓✓✓ |
4864 |
it->second.env_setting == kDisallowedInEnvironment) && |
378 |
required_env_settings == kAllowedInEnvironment) { |
||
379 |
34 |
errors->push_back(NotAllowedInEnvErr(original_name)); |
|
380 |
34 |
break; |
|
381 |
} |
||
382 |
|||
383 |
{ |
||
384 |
9660 |
std::string implied_name = name; |
|
385 |
✓✓ | 4830 |
if (is_negation) { |
386 |
// Implications for negated options are defined with "--no-". |
||
387 |
336 |
implied_name.insert(2, "no-"); |
|
388 |
} |
||
389 |
4830 |
auto implications = implications_.equal_range(implied_name); |
|
390 |
✓✓ | 5864 |
for (auto it = implications.first; it != implications.second; ++it) { |
391 |
✓✓ | 1034 |
if (it->second.type == kV8Option) { |
392 |
2 |
v8_args->push_back(it->second.name); |
|
393 |
} else { |
||
394 |
1032 |
*it->second.target_field->template Lookup<bool>(options) = |
|
395 |
1032 |
it->second.target_value; |
|
396 |
} |
||
397 |
} |
||
398 |
} |
||
399 |
|||
400 |
✓✓ | 4830 |
if (it == options_.end()) { |
401 |
212 |
v8_args->push_back(arg); |
|
402 |
212 |
continue; |
|
403 |
} |
||
404 |
|||
405 |
4618 |
const OptionInfo& info = it->second; |
|
406 |
|||
407 |
// Some V8 options can be negated and they are validated by V8 later. |
||
408 |
✓✓✓✓ ✓✗ |
4618 |
if (is_negation && info.type != kBoolean && info.type != kV8Option) { |
409 |
2 |
errors->push_back(NegationImpliesBooleanError(arg)); |
|
410 |
2 |
break; |
|
411 |
} |
||
412 |
|||
413 |
4616 |
std::string value; |
|
414 |
✓✓✓✓ ✓✓ |
4616 |
if (info.type != kBoolean && info.type != kNoOp && info.type != kV8Option) { |
415 |
✓✓ | 2110 |
if (equals_index != std::string::npos) { |
416 |
442 |
value = arg.substr(equals_index + 1); |
|
417 |
✓✓ | 442 |
if (value.empty()) { |
418 |
4 |
missing_argument(); |
|
419 |
4 |
break; |
|
420 |
} |
||
421 |
} else { |
||
422 |
✓✓ | 1668 |
if (args.empty()) { |
423 |
10 |
missing_argument(); |
|
424 |
10 |
break; |
|
425 |
} |
||
426 |
|||
427 |
1658 |
value = args.pop_first(); |
|
428 |
|||
429 |
✓✓✗✓ ✗✓ |
1658 |
if (!value.empty() && value[0] == '-') { |
430 |
missing_argument(); |
||
431 |
break; |
||
432 |
} else { |
||
433 |
✓✓✓✓ ✓✗✓✓ |
1658 |
if (!value.empty() && value[0] == '\\' && value[1] == '-') |
434 |
2 |
value = value.substr(1); // Treat \- as escaping an -. |
|
435 |
} |
||
436 |
} |
||
437 |
} |
||
438 |
|||
439 |
✓✓✓✓ ✓✓✓✓ ✗ |
4602 |
switch (info.type) { |
440 |
2414 |
case kBoolean: |
|
441 |
2414 |
*Lookup<bool>(info.field, options) = !is_negation; |
|
442 |
2414 |
break; |
|
443 |
12 |
case kInteger: |
|
444 |
12 |
*Lookup<int64_t>(info.field, options) = std::atoll(value.c_str()); |
|
445 |
12 |
break; |
|
446 |
62 |
case kUInteger: |
|
447 |
62 |
*Lookup<uint64_t>(info.field, options) = std::stoull(value); |
|
448 |
62 |
break; |
|
449 |
1496 |
case kString: |
|
450 |
1496 |
*Lookup<std::string>(info.field, options) = value; |
|
451 |
1496 |
break; |
|
452 |
326 |
case kStringList: |
|
453 |
652 |
Lookup<std::vector<std::string>>(info.field, options) |
|
454 |
326 |
->emplace_back(std::move(value)); |
|
455 |
326 |
break; |
|
456 |
200 |
case kHostPort: |
|
457 |
200 |
Lookup<HostPort>(info.field, options) |
|
458 |
->Update(SplitHostPort(value, errors)); |
||
459 |
200 |
break; |
|
460 |
2 |
case kNoOp: |
|
461 |
2 |
break; |
|
462 |
90 |
case kV8Option: |
|
463 |
90 |
v8_args->push_back(arg); |
|
464 |
90 |
break; |
|
465 |
default: |
||
466 |
UNREACHABLE(); |
||
467 |
} |
||
468 |
} |
||
469 |
22520 |
options->CheckOptions(errors); |
|
470 |
} |
||
471 |
|||
472 |
} // namespace options_parser |
||
473 |
} // namespace node |
||
474 |
|||
475 |
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS |
||
476 |
|||
477 |
#endif // SRC_NODE_OPTIONS_INL_H_ |
Generated by: GCOVR (Version 4.2) |