tap: import some changes
[argpar.git] / tests / test_argpar.c
... / ...
CommitLineData
1/*
2 * Copyright (c) 2019-2021 Philippe Proulx <pproulx@efficios.com>
3 * Copyright (c) 2020-2021 Simon Marchi <simon.marchi@efficios.com>
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; under version 2 of the License.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17 */
18
19#include <assert.h>
20#include <stdlib.h>
21#include <stdio.h>
22#include <string.h>
23#include <stdbool.h>
24#include <glib.h>
25
26#include "tap/tap.h"
27#include "argpar/argpar.h"
28
29/*
30 * Formats `item` and appends the resulting string to `res_str` to
31 * incrementally build an expected command line string.
32 *
33 * This function:
34 *
35 * * Prefers the `--long-opt=arg` style over the `-s arg` style.
36 *
37 * * Uses the `arg<A,B>` form for non-option arguments, where `A` is the
38 * original argument index and `B` is the non-option argument index.
39 */
40static
41void append_to_res_str(GString * const res_str,
42 const struct argpar_item * const item)
43{
44 if (res_str->len > 0) {
45 g_string_append_c(res_str, ' ');
46 }
47
48 switch (argpar_item_type(item)) {
49 case ARGPAR_ITEM_TYPE_OPT:
50 {
51 const struct argpar_opt_descr * const descr =
52 argpar_item_opt_descr(item);
53 const char * const arg = argpar_item_opt_arg(item);
54
55 if (descr->long_name) {
56 g_string_append_printf(res_str, "--%s",
57 descr->long_name);
58
59 if (arg) {
60 g_string_append_printf(res_str, "=%s", arg);
61 }
62 } else if (descr->short_name) {
63 g_string_append_printf(res_str, "-%c",
64 descr->short_name);
65
66 if (arg) {
67 g_string_append_printf(res_str, " %s", arg);
68 }
69 }
70
71 break;
72 }
73 case ARGPAR_ITEM_TYPE_NON_OPT:
74 {
75 const char * const arg = argpar_item_non_opt_arg(item);
76 const unsigned int orig_index =
77 argpar_item_non_opt_orig_index(item);
78 const unsigned int non_opt_index =
79 argpar_item_non_opt_non_opt_index(item);
80
81 g_string_append_printf(res_str, "%s<%u,%u>", arg, orig_index,
82 non_opt_index);
83 break;
84 }
85 default:
86 abort();
87 }
88}
89
90/*
91 * Parses `cmdline` with the argpar API using the option descriptors
92 * `descrs`, and ensures that the resulting effective command line is
93 * `expected_cmd_line` and that the number of ingested original
94 * arguments is `expected_ingested_orig_args`.
95 *
96 * This function splits `cmdline` on spaces to create an original
97 * argument array.
98 *
99 * This function builds the resulting command line from parsing items
100 * by space-separating each formatted item (see append_to_res_str()).
101 */
102static
103void test_succeed(const char * const cmdline,
104 const char * const expected_cmd_line,
105 const struct argpar_opt_descr * const descrs,
106 const unsigned int expected_ingested_orig_args)
107{
108 struct argpar_iter *iter = NULL;
109 const struct argpar_item *item = NULL;
110 const struct argpar_error *error = NULL;
111 GString * const res_str = g_string_new(NULL);
112 gchar ** const argv = g_strsplit(cmdline, " ", 0);
113 unsigned int i, actual_ingested_orig_args;
114
115 assert(argv);
116 assert(res_str);
117 iter = argpar_iter_create(g_strv_length(argv),
118 (const char * const *) argv, descrs);
119 assert(iter);
120
121 for (i = 0; ; i++) {
122 enum argpar_iter_next_status status;
123
124 ARGPAR_ITEM_DESTROY_AND_RESET(item);
125 status = argpar_iter_next(iter, &item, &error);
126
127 ok(status == ARGPAR_ITER_NEXT_STATUS_OK ||
128 status == ARGPAR_ITER_NEXT_STATUS_END,
129 "argpar_iter_next() returns the expected status "
130 "(%d) for command line `%s` (call %u)",
131 status, cmdline, i + 1);
132 ok(!error,
133 "argpar_iter_next() doesn't set an error for "
134 "command line `%s` (call %u)",
135 cmdline, i + 1);
136
137 if (status == ARGPAR_ITER_NEXT_STATUS_END) {
138 ok(!item,
139 "argpar_iter_next() doesn't set an item "
140 "for status `ARGPAR_ITER_NEXT_STATUS_END` "
141 "and command line `%s` (call %u)",
142 cmdline, i + 1);
143 break;
144 }
145
146 append_to_res_str(res_str, item);
147 }
148
149 actual_ingested_orig_args = argpar_iter_ingested_orig_args(iter);
150 ok(actual_ingested_orig_args == expected_ingested_orig_args,
151 "argpar_iter_ingested_orig_args() returns the expected "
152 "number of ingested original arguments for command line `%s`",
153 cmdline);
154
155 if (actual_ingested_orig_args != expected_ingested_orig_args) {
156 diag("Expected: %u Got: %u", expected_ingested_orig_args,
157 actual_ingested_orig_args);
158 }
159
160 ok(strcmp(expected_cmd_line, res_str->str) == 0,
161 "argpar_iter_next() returns the expected parsing items "
162 "for command line `%s`", cmdline);
163
164 if (strcmp(expected_cmd_line, res_str->str) != 0) {
165 diag("Expected: `%s`", expected_cmd_line);
166 diag("Got: `%s`", res_str->str);
167 }
168
169 argpar_item_destroy(item);
170 argpar_iter_destroy(iter);
171 assert(!error);
172 g_string_free(res_str, TRUE);
173 g_strfreev(argv);
174}
175
176static
177void succeed_tests(void)
178{
179 /* No arguments */
180 {
181 const struct argpar_opt_descr descrs[] = {
182 ARGPAR_OPT_DESCR_SENTINEL
183 };
184
185 test_succeed(
186 "",
187 "",
188 descrs, 0);
189 }
190
191 /* Single long option */
192 {
193 const struct argpar_opt_descr descrs[] = {
194 { 0, '\0', "salut", false },
195 ARGPAR_OPT_DESCR_SENTINEL
196 };
197
198 test_succeed(
199 "--salut",
200 "--salut",
201 descrs, 1);
202 }
203
204 /* Single short option */
205 {
206 const struct argpar_opt_descr descrs[] = {
207 { 0, 'f', NULL, false },
208 ARGPAR_OPT_DESCR_SENTINEL
209 };
210
211 test_succeed(
212 "-f",
213 "-f",
214 descrs, 1);
215 }
216
217 /* Short and long option (aliases) */
218 {
219 const struct argpar_opt_descr descrs[] = {
220 { 0, 'f', "flaw", false },
221 ARGPAR_OPT_DESCR_SENTINEL
222 };
223
224 test_succeed(
225 "-f --flaw",
226 "--flaw --flaw",
227 descrs, 2);
228 }
229
230 /* Long option with argument (space form) */
231 {
232 const struct argpar_opt_descr descrs[] = {
233 { 0, '\0', "tooth", true },
234 ARGPAR_OPT_DESCR_SENTINEL
235 };
236
237 test_succeed(
238 "--tooth 67",
239 "--tooth=67",
240 descrs, 2);
241 }
242
243 /* Long option with argument (equal form) */
244 {
245 const struct argpar_opt_descr descrs[] = {
246 { 0, '\0', "polish", true },
247 ARGPAR_OPT_DESCR_SENTINEL
248 };
249
250 test_succeed(
251 "--polish=brick",
252 "--polish=brick",
253 descrs, 1);
254 }
255
256 /* Short option with argument (space form) */
257 {
258 const struct argpar_opt_descr descrs[] = {
259 { 0, 'c', NULL, true },
260 ARGPAR_OPT_DESCR_SENTINEL
261 };
262
263 test_succeed(
264 "-c chilly",
265 "-c chilly",
266 descrs, 2);
267 }
268
269 /* Short option with argument (glued form) */
270 {
271 const struct argpar_opt_descr descrs[] = {
272 { 0, 'c', NULL, true },
273 ARGPAR_OPT_DESCR_SENTINEL
274 };
275
276 test_succeed(
277 "-cchilly",
278 "-c chilly",
279 descrs, 1);
280 }
281
282 /* Short and long option (aliases) with argument (all forms) */
283 {
284 const struct argpar_opt_descr descrs[] = {
285 { 0, 'd', "dry", true },
286 ARGPAR_OPT_DESCR_SENTINEL
287 };
288
289 test_succeed(
290 "--dry=rate -dthing --dry street --dry=shape",
291 "--dry=rate --dry=thing --dry=street --dry=shape",
292 descrs, 5);
293 }
294
295 /* Many short options, last one with argument (glued form) */
296 {
297 const struct argpar_opt_descr descrs[] = {
298 { 0, 'd', NULL, false },
299 { 0, 'e', NULL, false },
300 { 0, 'f', NULL, true },
301 ARGPAR_OPT_DESCR_SENTINEL
302 };
303
304 test_succeed(
305 "-defmeow",
306 "-d -e -f meow",
307 descrs, 1);
308 }
309
310 /* Many options */
311 {
312 const struct argpar_opt_descr descrs[] = {
313 { 0, 'd', NULL, false },
314 { 0, 'e', "east", true },
315 { 0, '\0', "mind", false },
316 ARGPAR_OPT_DESCR_SENTINEL
317 };
318
319 test_succeed(
320 "-d --mind -destart --mind --east cough -d --east=itch",
321 "-d --mind -d --east=start --mind --east=cough -d --east=itch",
322 descrs, 8);
323 }
324
325 /* Single non-option argument */
326 {
327 const struct argpar_opt_descr descrs[] = {
328 ARGPAR_OPT_DESCR_SENTINEL
329 };
330
331 test_succeed(
332 "kilojoule",
333 "kilojoule<0,0>",
334 descrs, 1);
335 }
336
337 /* Two non-option arguments */
338 {
339 const struct argpar_opt_descr descrs[] = {
340 ARGPAR_OPT_DESCR_SENTINEL
341 };
342
343 test_succeed(
344 "kilojoule mitaine",
345 "kilojoule<0,0> mitaine<1,1>",
346 descrs, 2);
347 }
348
349 /* Single non-option argument mixed with options */
350 {
351 const struct argpar_opt_descr descrs[] = {
352 { 0, 'd', NULL, false },
353 { 0, '\0', "squeeze", true },
354 ARGPAR_OPT_DESCR_SENTINEL
355 };
356
357 test_succeed(
358 "-d sprout yes --squeeze little bag -d",
359 "-d sprout<1,0> yes<2,1> --squeeze=little bag<5,2> -d",
360 descrs, 7);
361 }
362
363 /* Valid `---opt` */
364 {
365 const struct argpar_opt_descr descrs[] = {
366 { 0, '\0', "-fuel", true },
367 ARGPAR_OPT_DESCR_SENTINEL
368 };
369
370 test_succeed(
371 "---fuel=three",
372 "---fuel=three",
373 descrs, 1);
374 }
375
376 /* Long option containing `=` in argument (equal form) */
377 {
378 const struct argpar_opt_descr descrs[] = {
379 { 0, '\0', "zebra", true },
380 ARGPAR_OPT_DESCR_SENTINEL
381 };
382
383 test_succeed(
384 "--zebra=three=yes",
385 "--zebra=three=yes",
386 descrs, 1);
387 }
388
389 /* Short option's argument starting with `-` (glued form) */
390 {
391 const struct argpar_opt_descr descrs[] = {
392 { 0, 'z', NULL, true },
393 ARGPAR_OPT_DESCR_SENTINEL
394 };
395
396 test_succeed(
397 "-z-will",
398 "-z -will",
399 descrs, 1);
400 }
401
402 /* Short option's argument starting with `-` (space form) */
403 {
404 const struct argpar_opt_descr descrs[] = {
405 { 0, 'z', NULL, true },
406 ARGPAR_OPT_DESCR_SENTINEL
407 };
408
409 test_succeed(
410 "-z -will",
411 "-z -will",
412 descrs, 2);
413 }
414
415 /* Long option's argument starting with `-` (space form) */
416 {
417 const struct argpar_opt_descr descrs[] = {
418 { 0, '\0', "janine", true },
419 ARGPAR_OPT_DESCR_SENTINEL
420 };
421
422 test_succeed(
423 "--janine -sutto",
424 "--janine=-sutto",
425 descrs, 2);
426 }
427
428 /* Long option's argument starting with `-` (equal form) */
429 {
430 const struct argpar_opt_descr descrs[] = {
431 { 0, '\0', "janine", true },
432 ARGPAR_OPT_DESCR_SENTINEL
433 };
434
435 test_succeed(
436 "--janine=-sutto",
437 "--janine=-sutto",
438 descrs, 1);
439 }
440
441 /* Long option's empty argument (equal form) */
442 {
443 const struct argpar_opt_descr descrs[] = {
444 { 0, 'f', NULL, false },
445 { 0, '\0', "yeah", true },
446 ARGPAR_OPT_DESCR_SENTINEL
447 };
448
449 test_succeed(
450 "-f --yeah= -f",
451 "-f --yeah= -f",
452 descrs, 3);
453 }
454
455 /* `-` non-option argument */
456 {
457 const struct argpar_opt_descr descrs[] = {
458 { 0, 'f', NULL, false },
459 ARGPAR_OPT_DESCR_SENTINEL
460 };
461
462 test_succeed(
463 "-f - -f",
464 "-f -<1,0> -f",
465 descrs, 3);
466 }
467
468 /* `--` non-option argument */
469 {
470 const struct argpar_opt_descr descrs[] = {
471 { 0, 'f', NULL, false },
472 ARGPAR_OPT_DESCR_SENTINEL
473 };
474
475 test_succeed(
476 "-f -- -f",
477 "-f --<1,0> -f",
478 descrs, 3);
479 }
480
481 /* Very long name of long option */
482 {
483 const char opt_name[] =
484 "kale-chips-waistcoat-yr-bicycle-rights-gochujang-"
485 "woke-tumeric-flexitarian-biodiesel-chillwave-cliche-"
486 "ethical-cardigan-listicle-pok-pok-sustainable-live-"
487 "edge-jianbing-gochujang-butcher-disrupt-tattooed-"
488 "tumeric-prism-photo-booth-vape-kogi-jean-shorts-"
489 "blog-williamsburg-fingerstache-palo-santo-artisan-"
490 "affogato-occupy-skateboard-adaptogen-neutra-celiac-"
491 "put-a-bird-on-it-kombucha-everyday-carry-hot-chicken-"
492 "craft-beer-subway-tile-tote-bag-disrupt-selvage-"
493 "raclette-art-party-readymade-paleo-heirloom-trust-"
494 "fund-small-batch-kinfolk-woke-cardigan-prism-"
495 "chambray-la-croix-hashtag-unicorn-edison-bulb-tbh-"
496 "cornhole-cliche-tattooed-green-juice-adaptogen-"
497 "kitsch-lo-fi-vexillologist-migas-gentrify-"
498 "viral-raw-denim";
499 const struct argpar_opt_descr descrs[] = {
500 { 0, '\0', opt_name, true },
501 ARGPAR_OPT_DESCR_SENTINEL
502 };
503 char cmdline[1024];
504
505 sprintf(cmdline, "--%s=23", opt_name);
506 test_succeed(cmdline, cmdline, descrs, 1);
507 }
508}
509
510/*
511 * Parses `cmdline` with the argpar API using the option descriptors
512 * `descrs`, and ensures that argpar_iter_next() fails with status
513 * `expected_status` and that it sets an error having:
514 *
515 * * The original argument index `expected_orig_index`.
516 *
517 * * If applicable:
518 *
519 * * The unknown option name `expected_unknown_opt_name`.
520 *
521 * * The option descriptor at index `expected_opt_descr_index` of
522 * `descrs`.
523 *
524 * * The option type `expected_is_short`.
525 *
526 * This function splits `cmdline` on spaces to create an original
527 * argument array.
528 */
529static
530void test_fail(const char * const cmdline,
531 const enum argpar_error_type expected_error_type,
532 const unsigned int expected_orig_index,
533 const char * const expected_unknown_opt_name,
534 const unsigned int expected_opt_descr_index,
535 const bool expected_is_short,
536 const struct argpar_opt_descr * const descrs)
537{
538 struct argpar_iter *iter = NULL;
539 const struct argpar_item *item = NULL;
540 gchar ** const argv = g_strsplit(cmdline, " ", 0);
541 unsigned int i;
542 const struct argpar_error *error = NULL;
543
544 iter = argpar_iter_create(g_strv_length(argv),
545 (const char * const *) argv, descrs);
546 assert(iter);
547
548 for (i = 0; ; i++) {
549 enum argpar_iter_next_status status;
550
551 ARGPAR_ITEM_DESTROY_AND_RESET(item);
552 status = argpar_iter_next(iter, &item, &error);
553 ok(status == ARGPAR_ITER_NEXT_STATUS_OK ||
554 (status == ARGPAR_ITER_NEXT_STATUS_ERROR &&
555 argpar_error_type(error) == expected_error_type),
556 "argpar_iter_next() returns the expected status "
557 "and error type (%d) for command line `%s` (call %u)",
558 expected_error_type, cmdline, i + 1);
559
560 if (status != ARGPAR_ITER_NEXT_STATUS_OK) {
561 ok(!item,
562 "argpar_iter_next() doesn't set an item "
563 "for other status than "
564 "`ARGPAR_ITER_NEXT_STATUS_OK` "
565 "and command line `%s` (call %u)",
566 cmdline, i + 1);
567 ok(error,
568 "argpar_iter_next() sets an error for "
569 "other status than "
570 " `ARGPAR_ITER_NEXT_STATUS_OK` "
571 "and command line `%s` (call %u)",
572 cmdline, i + 1);
573 ok(argpar_error_orig_index(error) ==
574 expected_orig_index,
575 "argpar_iter_next() sets an error with "
576 "the expected original argument index "
577 "for command line `%s` (call %u)",
578 cmdline, i + 1);
579
580 if (argpar_error_type(error) == ARGPAR_ERROR_TYPE_UNKNOWN_OPT) {
581 ok(strcmp(argpar_error_unknown_opt_name(error),
582 expected_unknown_opt_name) == 0,
583 "argpar_iter_next() sets an error with "
584 "the expected unknown option name "
585 "for command line `%s` (call %u)",
586 cmdline, i + 1);
587 } else {
588 bool is_short;
589
590 ok(argpar_error_opt_descr(error, &is_short) ==
591 &descrs[expected_opt_descr_index],
592 "argpar_iter_next() sets an error with "
593 "the expected option descriptor "
594 "for command line `%s` (call %u)",
595 cmdline, i + 1);
596 ok(is_short == expected_is_short,
597 "argpar_iter_next() sets an error with "
598 "the expected option type "
599 "for command line `%s` (call %u)",
600 cmdline, i + 1);
601 }
602 break;
603 }
604
605 ok(item,
606 "argpar_iter_next() sets an item for status "
607 "`ARGPAR_ITER_NEXT_STATUS_OK` "
608 "and command line `%s` (call %u)",
609 cmdline, i + 1);
610 ok(!error,
611 "argpar_iter_next() doesn't set an error for status "
612 "`ARGPAR_ITER_NEXT_STATUS_OK` "
613 "and command line `%s` (call %u)",
614 cmdline, i + 1);
615 }
616
617 /*
618 ok(strcmp(expected_error, error) == 0,
619 "argpar_iter_next() sets the expected error string "
620 "for command line `%s`", cmdline);
621
622 if (strcmp(expected_error, error) != 0) {
623 diag("Expected: `%s`", expected_error);
624 diag("Got: `%s`", error);
625 }
626 */
627
628 argpar_item_destroy(item);
629 argpar_iter_destroy(iter);
630 argpar_error_destroy(error);
631 g_strfreev(argv);
632}
633
634static
635void fail_tests(void)
636{
637
638 /* Unknown short option (space form) */
639 {
640 const struct argpar_opt_descr descrs[] = {
641 { 0, 'd', NULL, true },
642 ARGPAR_OPT_DESCR_SENTINEL
643 };
644
645 test_fail(
646 "-d salut -e -d meow",
647 ARGPAR_ERROR_TYPE_UNKNOWN_OPT,
648 2, "-e", 0, false,
649 descrs);
650 }
651
652 /* Unknown short option (glued form) */
653 {
654 const struct argpar_opt_descr descrs[] = {
655 { 0, 'd', 0, true },
656 ARGPAR_OPT_DESCR_SENTINEL
657 };
658
659 test_fail(
660 "-dsalut -e -d meow",
661 ARGPAR_ERROR_TYPE_UNKNOWN_OPT,
662 1, "-e", 0, false,
663 descrs);
664 }
665
666 /* Unknown long option (space form) */
667 {
668 const struct argpar_opt_descr descrs[] = {
669 { 0, '\0', "sink", true },
670 ARGPAR_OPT_DESCR_SENTINEL
671 };
672
673 test_fail(
674 "--sink party --food --sink impulse",
675 ARGPAR_ERROR_TYPE_UNKNOWN_OPT,
676 2, "--food", 0, false,
677 descrs);
678 }
679
680 /* Unknown long option (equal form) */
681 {
682 const struct argpar_opt_descr descrs[] = {
683 { 0, '\0', "sink", true },
684 ARGPAR_OPT_DESCR_SENTINEL
685 };
686
687 test_fail(
688 "--sink=party --food --sink=impulse",
689 ARGPAR_ERROR_TYPE_UNKNOWN_OPT,
690 1, "--food", 0, false,
691 descrs);
692 }
693
694 /* Unknown option before non-option argument */
695 {
696 const struct argpar_opt_descr descrs[] = {
697 { 0, '\0', "thumb", true },
698 ARGPAR_OPT_DESCR_SENTINEL
699 };
700
701 test_fail(
702 "--thumb=party --food=18 bateau --thumb waves",
703 ARGPAR_ERROR_TYPE_UNKNOWN_OPT,
704 1, "--food", 0, false,
705 descrs);
706 }
707
708 /* Unknown option after non-option argument */
709 {
710 const struct argpar_opt_descr descrs[] = {
711 { 0, '\0', "thumb", true },
712 ARGPAR_OPT_DESCR_SENTINEL
713 };
714
715 test_fail(
716 "--thumb=party wound --food --thumb waves",
717 ARGPAR_ERROR_TYPE_UNKNOWN_OPT,
718 2, "--food", 0, false,
719 descrs);
720 }
721
722 /* Missing long option argument */
723 {
724 const struct argpar_opt_descr descrs[] = {
725 { 0, '\0', "thumb", true },
726 ARGPAR_OPT_DESCR_SENTINEL
727 };
728
729 test_fail(
730 "allo --thumb",
731 ARGPAR_ERROR_TYPE_MISSING_OPT_ARG,
732 1, NULL, 0, false,
733 descrs);
734 }
735
736 /* Missing short option argument */
737 {
738 const struct argpar_opt_descr descrs[] = {
739 { 0, 'k', NULL, true },
740 ARGPAR_OPT_DESCR_SENTINEL
741 };
742
743 test_fail(
744 "zoom heille -k",
745 ARGPAR_ERROR_TYPE_MISSING_OPT_ARG,
746 2, NULL, 0, true,
747 descrs);
748 }
749
750 /* Missing short option argument (multiple glued) */
751 {
752 const struct argpar_opt_descr descrs[] = {
753 { 0, 'a', NULL, false },
754 { 0, 'b', NULL, false },
755 { 0, 'c', NULL, true },
756 ARGPAR_OPT_DESCR_SENTINEL
757 };
758
759 test_fail(
760 "-abc",
761 ARGPAR_ERROR_TYPE_MISSING_OPT_ARG,
762 0, NULL, 2, true,
763 descrs);
764 }
765
766 /* Unexpected long option argument */
767 {
768 const struct argpar_opt_descr descrs[] = {
769 { 0, 'c', "chevre", false },
770 ARGPAR_OPT_DESCR_SENTINEL
771 };
772
773 test_fail(
774 "ambulance --chevre=fromage tar -cjv",
775 ARGPAR_ERROR_TYPE_UNEXPECTED_OPT_ARG,
776 1, NULL, 0, false,
777 descrs);
778 }
779}
780
781int main(void)
782{
783 plan_tests(309);
784 succeed_tests();
785 fail_tests();
786 return exit_status();
787}
This page took 0.023839 seconds and 4 git commands to generate.