OLD | NEW |
| (Empty) |
1 // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 #library('args_test'); | |
6 | |
7 #import('../../../pkg/unittest/unittest.dart'); | |
8 #import('../../../lib/args/args.dart'); | |
9 | |
10 main() { | |
11 group('ArgParser.addFlag()', () { | |
12 test('throws IllegalArgumentException if the flag already exists', () { | |
13 var parser = new ArgParser(); | |
14 parser.addFlag('foo'); | |
15 throwsIllegalArg(() => parser.addFlag('foo')); | |
16 }); | |
17 | |
18 test('throws IllegalArgumentException if the option already exists', () { | |
19 var parser = new ArgParser(); | |
20 parser.addOption('foo'); | |
21 throwsIllegalArg(() => parser.addFlag('foo')); | |
22 }); | |
23 | |
24 test('throws IllegalArgumentException if the abbreviation exists', () { | |
25 var parser = new ArgParser(); | |
26 parser.addFlag('foo', abbr: 'f'); | |
27 throwsIllegalArg(() => parser.addFlag('flummox', abbr: 'f')); | |
28 }); | |
29 | |
30 test('throws IllegalArgumentException if the abbreviation is longer ' | |
31 'than one character', () { | |
32 var parser = new ArgParser(); | |
33 throwsIllegalArg(() => parser.addFlag('flummox', abbr: 'flu')); | |
34 }); | |
35 }); | |
36 | |
37 group('ArgParser.addOption()', () { | |
38 test('throws IllegalArgumentException if the flag already exists', () { | |
39 var parser = new ArgParser(); | |
40 parser.addFlag('foo'); | |
41 throwsIllegalArg(() => parser.addOption('foo')); | |
42 }); | |
43 | |
44 test('throws IllegalArgumentException if the option already exists', () { | |
45 var parser = new ArgParser(); | |
46 parser.addOption('foo'); | |
47 throwsIllegalArg(() => parser.addOption('foo')); | |
48 }); | |
49 | |
50 test('throws IllegalArgumentException if the abbreviation exists', () { | |
51 var parser = new ArgParser(); | |
52 parser.addFlag('foo', abbr: 'f'); | |
53 throwsIllegalArg(() => parser.addOption('flummox', abbr: 'f')); | |
54 }); | |
55 | |
56 test('throws IllegalArgumentException if the abbreviation is longer ' | |
57 'than one character', () { | |
58 var parser = new ArgParser(); | |
59 throwsIllegalArg(() => parser.addOption('flummox', abbr: 'flu')); | |
60 }); | |
61 }); | |
62 | |
63 group('ArgParser.parse()', () { | |
64 group('flags', () { | |
65 test('are true if present', () { | |
66 var parser = new ArgParser(); | |
67 parser.addFlag('verbose'); | |
68 | |
69 var args = parser.parse(['--verbose']); | |
70 expect(args['verbose'], isTrue); | |
71 }); | |
72 | |
73 test('default if missing', () { | |
74 var parser = new ArgParser(); | |
75 parser.addFlag('a', defaultsTo: true); | |
76 parser.addFlag('b', defaultsTo: false); | |
77 | |
78 var args = parser.parse([]); | |
79 expect(args['a'], isTrue); | |
80 expect(args['b'], isFalse); | |
81 }); | |
82 | |
83 test('are false if missing with no default', () { | |
84 var parser = new ArgParser(); | |
85 parser.addFlag('verbose'); | |
86 | |
87 var args = parser.parse([]); | |
88 expect(args['verbose'], isFalse); | |
89 }); | |
90 | |
91 test('throws if given a value', () { | |
92 var parser = new ArgParser(); | |
93 parser.addFlag('verbose'); | |
94 | |
95 throwsFormat(parser, ['--verbose=true']); | |
96 }); | |
97 }); | |
98 | |
99 group('flags negated with "no-"', () { | |
100 test('set the flag to false', () { | |
101 var parser = new ArgParser(); | |
102 parser.addFlag('verbose'); | |
103 | |
104 var args = parser.parse(['--no-verbose']); | |
105 expect(args['verbose'], isFalse); | |
106 }); | |
107 | |
108 test('set the flag to true if the flag actually starts with "no-"', () { | |
109 var parser = new ArgParser(); | |
110 parser.addFlag('no-body'); | |
111 | |
112 var args = parser.parse(['--no-body']); | |
113 expect(args['no-body'], isTrue); | |
114 }); | |
115 | |
116 test('are not preferred over a colliding one without', () { | |
117 var parser = new ArgParser(); | |
118 parser.addFlag('no-strum'); | |
119 parser.addFlag('strum'); | |
120 | |
121 var args = parser.parse(['--no-strum']); | |
122 expect(args['no-strum'], isTrue); | |
123 expect(args['strum'], isFalse); | |
124 }); | |
125 | |
126 test('fail for non-negatable flags', () { | |
127 var parser = new ArgParser(); | |
128 parser.addFlag('strum', negatable: false); | |
129 | |
130 throwsFormat(parser, ['--no-strum']); | |
131 }); | |
132 }); | |
133 | |
134 group('callbacks', () { | |
135 test('for present flags are invoked with the value', () { | |
136 var a; | |
137 var parser = new ArgParser(); | |
138 parser.addFlag('a', callback: (value) => a = value); | |
139 | |
140 var args = parser.parse(['--a']); | |
141 expect(a, isTrue); | |
142 }); | |
143 | |
144 test('for absent flags are invoked with the default value', () { | |
145 var a; | |
146 var parser = new ArgParser(); | |
147 parser.addFlag('a', defaultsTo: false, | |
148 callback: (value) => a = value); | |
149 | |
150 var args = parser.parse([]); | |
151 expect(a, isFalse); | |
152 }); | |
153 | |
154 test('are invoked even if the flag is not present', () { | |
155 var a = 'not called'; | |
156 var parser = new ArgParser(); | |
157 parser.addFlag('a', callback: (value) => a = value); | |
158 | |
159 var args = parser.parse([]); | |
160 expect(a, isFalse); | |
161 }); | |
162 | |
163 test('for present options are invoked with the value', () { | |
164 var a; | |
165 var parser = new ArgParser(); | |
166 parser.addOption('a', callback: (value) => a = value); | |
167 | |
168 var args = parser.parse(['--a=v']); | |
169 expect(a, equals('v')); | |
170 }); | |
171 | |
172 test('for absent options are invoked with the default value', () { | |
173 var a; | |
174 var parser = new ArgParser(); | |
175 parser.addOption('a', defaultsTo: 'v', | |
176 callback: (value) => a = value); | |
177 | |
178 var args = parser.parse([]); | |
179 expect(a, equals('v')); | |
180 }); | |
181 | |
182 test('are invoked even if the option is not present', () { | |
183 var a = 'not called'; | |
184 var parser = new ArgParser(); | |
185 parser.addOption('a', callback: (value) => a = value); | |
186 | |
187 var args = parser.parse([]); | |
188 expect(a, isNull); | |
189 }); | |
190 }); | |
191 | |
192 group('abbreviations', () { | |
193 test('are parsed with a preceding "-"', () { | |
194 var parser = new ArgParser(); | |
195 parser.addFlag('arg', abbr: 'a'); | |
196 | |
197 var args = parser.parse(['-a']); | |
198 expect(args['arg'], isTrue); | |
199 }); | |
200 | |
201 test('can use multiple after a single "-"', () { | |
202 var parser = new ArgParser(); | |
203 parser.addFlag('first', abbr: 'f'); | |
204 parser.addFlag('second', abbr: 's'); | |
205 parser.addFlag('third', abbr: 't'); | |
206 | |
207 var args = parser.parse(['-tf']); | |
208 expect(args['first'], isTrue); | |
209 expect(args['second'], isFalse); | |
210 expect(args['third'], isTrue); | |
211 }); | |
212 | |
213 test('can have multiple "-" args', () { | |
214 var parser = new ArgParser(); | |
215 parser.addFlag('first', abbr: 'f'); | |
216 parser.addFlag('second', abbr: 's'); | |
217 parser.addFlag('third', abbr: 't'); | |
218 | |
219 var args = parser.parse(['-s', '-tf']); | |
220 expect(args['first'], isTrue); | |
221 expect(args['second'], isTrue); | |
222 expect(args['third'], isTrue); | |
223 }); | |
224 | |
225 test('can take arguments without a space separating', () { | |
226 var parser = new ArgParser(); | |
227 parser.addOption('file', abbr: 'f'); | |
228 | |
229 var args = parser.parse(['-flip']); | |
230 expect(args['file'], equals('lip')); | |
231 }); | |
232 | |
233 test('can take arguments with a space separating', () { | |
234 var parser = new ArgParser(); | |
235 parser.addOption('file', abbr: 'f'); | |
236 | |
237 var args = parser.parse(['-f', 'name']); | |
238 expect(args['file'], equals('name')); | |
239 }); | |
240 | |
241 test('allow non-option characters in the value', () { | |
242 var parser = new ArgParser(); | |
243 parser.addOption('apple', abbr: 'a'); | |
244 | |
245 var args = parser.parse(['-ab?!c']); | |
246 expect(args['apple'], equals('b?!c')); | |
247 }); | |
248 | |
249 test('throw if unknown', () { | |
250 var parser = new ArgParser(); | |
251 throwsFormat(parser, ['-f']); | |
252 }); | |
253 | |
254 test('throw if the value is missing', () { | |
255 var parser = new ArgParser(); | |
256 parser.addOption('file', abbr: 'f'); | |
257 | |
258 throwsFormat(parser, ['-f']); | |
259 }); | |
260 | |
261 test('throw if the value looks like an option', () { | |
262 var parser = new ArgParser(); | |
263 parser.addOption('file', abbr: 'f'); | |
264 parser.addOption('other'); | |
265 | |
266 throwsFormat(parser, ['-f', '--other']); | |
267 throwsFormat(parser, ['-f', '--unknown']); | |
268 throwsFormat(parser, ['-f', '-abbr']); | |
269 }); | |
270 | |
271 test('throw if the value is not allowed', () { | |
272 var parser = new ArgParser(); | |
273 parser.addOption('mode', abbr: 'm', allowed: ['debug', 'release']); | |
274 | |
275 throwsFormat(parser, ['-mprofile']); | |
276 }); | |
277 | |
278 test('throw if any but the first is not a flag', () { | |
279 var parser = new ArgParser(); | |
280 parser.addFlag('apple', abbr: 'a'); | |
281 parser.addOption('banana', abbr: 'b'); // Takes an argument. | |
282 parser.addFlag('cherry', abbr: 'c'); | |
283 | |
284 throwsFormat(parser, ['-abc']); | |
285 }); | |
286 | |
287 test('throw if it has a value but the option is a flag', () { | |
288 var parser = new ArgParser(); | |
289 parser.addFlag('apple', abbr: 'a'); | |
290 parser.addFlag('banana', abbr: 'b'); | |
291 | |
292 // The '?!' means this can only be understood as '--apple b?!c'. | |
293 throwsFormat(parser, ['-ab?!c']); | |
294 }); | |
295 }); | |
296 | |
297 group('options', () { | |
298 test('are parsed if present', () { | |
299 var parser = new ArgParser(); | |
300 parser.addOption('mode'); | |
301 var args = parser.parse(['--mode=release']); | |
302 expect(args['mode'], equals('release')); | |
303 }); | |
304 | |
305 test('are null if not present', () { | |
306 var parser = new ArgParser(); | |
307 parser.addOption('mode'); | |
308 var args = parser.parse([]); | |
309 expect(args['mode'], isNull); | |
310 }); | |
311 | |
312 test('default if missing', () { | |
313 var parser = new ArgParser(); | |
314 parser.addOption('mode', defaultsTo: 'debug'); | |
315 var args = parser.parse([]); | |
316 expect(args['mode'], equals('debug')); | |
317 }); | |
318 | |
319 test('allow the value to be separated by whitespace', () { | |
320 var parser = new ArgParser(); | |
321 parser.addOption('mode'); | |
322 var args = parser.parse(['--mode', 'release']); | |
323 expect(args['mode'], equals('release')); | |
324 }); | |
325 | |
326 test('throw if unknown', () { | |
327 var parser = new ArgParser(); | |
328 throwsFormat(parser, ['--unknown']); | |
329 throwsFormat(parser, ['--nobody']); // Starts with "no". | |
330 }); | |
331 | |
332 test('throw if the arg does not include a value', () { | |
333 var parser = new ArgParser(); | |
334 parser.addOption('mode'); | |
335 throwsFormat(parser, ['--mode']); | |
336 }); | |
337 | |
338 test('throw if the value looks like an option', () { | |
339 var parser = new ArgParser(); | |
340 parser.addOption('mode'); | |
341 parser.addOption('other'); | |
342 | |
343 throwsFormat(parser, ['--mode', '--other']); | |
344 throwsFormat(parser, ['--mode', '--unknown']); | |
345 throwsFormat(parser, ['--mode', '-abbr']); | |
346 }); | |
347 | |
348 test('do not throw if the value is in the allowed set', () { | |
349 var parser = new ArgParser(); | |
350 parser.addOption('mode', allowed: ['debug', 'release']); | |
351 var args = parser.parse(['--mode=debug']); | |
352 expect(args['mode'], equals('debug')); | |
353 }); | |
354 | |
355 test('throw if the value is not in the allowed set', () { | |
356 var parser = new ArgParser(); | |
357 parser.addOption('mode', allowed: ['debug', 'release']); | |
358 throwsFormat(parser, ['--mode=profile']); | |
359 }); | |
360 | |
361 test('returns last provided value', () { | |
362 var parser = new ArgParser(); | |
363 parser.addOption('define'); | |
364 var args = parser.parse(['--define=1', '--define=2']); | |
365 expect(args['define'], equals('2')); | |
366 }); | |
367 | |
368 test('returns a List if multi-valued', () { | |
369 var parser = new ArgParser(); | |
370 parser.addOption('define', allowMultiple: true); | |
371 var args = parser.parse(['--define=1']); | |
372 expect(args['define'], equals(['1'])); | |
373 args = parser.parse(['--define=1', '--define=2']); | |
374 expect(args['define'], equals(['1','2'])); | |
375 }); | |
376 | |
377 test('returns the default value for multi-valued arguments ' | |
378 'if not explicitly set', () { | |
379 var parser = new ArgParser(); | |
380 parser.addOption('define', defaultsTo: '0', allowMultiple: true); | |
381 var args = parser.parse(['']); | |
382 expect(args['define'], equals(['0'])); | |
383 }); | |
384 }); | |
385 | |
386 group('query default values', () { | |
387 test('queries the default value', () { | |
388 var parser = new ArgParser(); | |
389 parser.addOption('define', defaultsTo: '0'); | |
390 expect(()=>parser.getDefault('undefine'), | |
391 throwsIllegalArgumentException); | |
392 }); | |
393 | |
394 test('queries the default value for an unknown option', () { | |
395 var parser = new ArgParser(); | |
396 parser.addOption('define', defaultsTo: '0'); | |
397 expect(()=>parser.getDefault('undefine'), | |
398 throwsIllegalArgumentException); | |
399 }); | |
400 }); | |
401 | |
402 group('gets the option names from an ArgsResult', () { | |
403 test('queries the set options', () { | |
404 var parser = new ArgParser(); | |
405 parser.addFlag('woof', defaultsTo: false); | |
406 parser.addOption('meow', defaultsTo: 'kitty'); | |
407 var args = parser.parse([]); | |
408 expect(args.options, hasLength(2)); | |
409 expect(args.options.some((o) => o == 'woof'), isTrue); | |
410 expect(args.options.some((o) => o == 'meow'), isTrue); | |
411 }); | |
412 }); | |
413 | |
414 group('remaining args', () { | |
415 test('stops parsing args when a non-option-like arg is encountered', () { | |
416 var parser = new ArgParser(); | |
417 parser.addFlag('woof'); | |
418 parser.addOption('meow'); | |
419 parser.addOption('tweet', defaultsTo: 'bird'); | |
420 | |
421 var results = parser.parse(['--woof', '--meow', 'v', 'not', 'option']); | |
422 expect(results['woof'], isTrue); | |
423 expect(results['meow'], equals('v')); | |
424 expect(results['tweet'], equals('bird')); | |
425 expect(results.rest, orderedEquals(['not', 'option'])); | |
426 }); | |
427 | |
428 test('stops parsing at "--"', () { | |
429 var parser = new ArgParser(); | |
430 parser.addFlag('woof', defaultsTo: false); | |
431 parser.addOption('meow', defaultsTo: 'kitty'); | |
432 | |
433 var results = parser.parse(['--woof', '--', '--meow']); | |
434 expect(results['woof'], isTrue); | |
435 expect(results['meow'], equals('kitty')); | |
436 expect(results.rest, orderedEquals(['--meow'])); | |
437 }); | |
438 | |
439 test('handles options with case-sensitivity', () { | |
440 var parser = new ArgParser(); | |
441 parser.addFlag('recurse', defaultsTo: false, abbr:'R'); | |
442 var results = parser.parse(['-R']); | |
443 expect(results['recurse'], isTrue); | |
444 expect(results.rest, [ ]); | |
445 throwsFormat(parser, ['-r']); | |
446 }); | |
447 }); | |
448 }); | |
449 | |
450 group('ArgParser.getUsage()', () { | |
451 test('negatable flags show "no-" in title', () { | |
452 var parser = new ArgParser(); | |
453 parser.addFlag('mode', help: 'The mode'); | |
454 | |
455 validateUsage(parser, | |
456 ''' | |
457 --[no-]mode The mode | |
458 '''); | |
459 }); | |
460 | |
461 test('non-negatable flags don\'t show "no-" in title', () { | |
462 var parser = new ArgParser(); | |
463 parser.addFlag('mode', negatable: false, help: 'The mode'); | |
464 | |
465 validateUsage(parser, | |
466 ''' | |
467 --mode The mode | |
468 '''); | |
469 }); | |
470 | |
471 test('if there are no abbreviations, there is no column for them', () { | |
472 var parser = new ArgParser(); | |
473 parser.addFlag('mode', help: 'The mode'); | |
474 | |
475 validateUsage(parser, | |
476 ''' | |
477 --[no-]mode The mode | |
478 '''); | |
479 }); | |
480 | |
481 test('options are lined up past abbreviations', () { | |
482 var parser = new ArgParser(); | |
483 parser.addFlag('mode', abbr: 'm', help: 'The mode'); | |
484 parser.addOption('long', help: 'Lacks an abbreviation'); | |
485 | |
486 validateUsage(parser, | |
487 ''' | |
488 -m, --[no-]mode The mode | |
489 --long Lacks an abbreviation | |
490 '''); | |
491 }); | |
492 | |
493 test('help text is lined up past the longest option', () { | |
494 var parser = new ArgParser(); | |
495 parser.addFlag('mode', abbr: 'm', help: 'Lined up with below'); | |
496 parser.addOption('a-really-long-name', help: 'Its help text'); | |
497 | |
498 validateUsage(parser, | |
499 ''' | |
500 -m, --[no-]mode Lined up with below | |
501 --a-really-long-name Its help text | |
502 '''); | |
503 }); | |
504 | |
505 test('leading empty lines are ignored in help text', () { | |
506 var parser = new ArgParser(); | |
507 parser.addFlag('mode', help: '\n\n\n\nAfter newlines'); | |
508 | |
509 validateUsage(parser, | |
510 ''' | |
511 --[no-]mode After newlines | |
512 '''); | |
513 }); | |
514 | |
515 test('trailing empty lines are ignored in help text', () { | |
516 var parser = new ArgParser(); | |
517 parser.addFlag('mode', help: 'Before newlines\n\n\n\n'); | |
518 | |
519 validateUsage(parser, | |
520 ''' | |
521 --[no-]mode Before newlines | |
522 '''); | |
523 }); | |
524 | |
525 test('options are documented in the order they were added', () { | |
526 var parser = new ArgParser(); | |
527 parser.addFlag('zebra', help: 'First'); | |
528 parser.addFlag('monkey', help: 'Second'); | |
529 parser.addFlag('wombat', help: 'Third'); | |
530 | |
531 validateUsage(parser, | |
532 ''' | |
533 --[no-]zebra First | |
534 --[no-]monkey Second | |
535 --[no-]wombat Third | |
536 '''); | |
537 }); | |
538 | |
539 test('the default value for a flag is shown if on', () { | |
540 var parser = new ArgParser(); | |
541 parser.addFlag('affirm', help: 'Should be on', defaultsTo: true); | |
542 parser.addFlag('negate', help: 'Should be off', defaultsTo: false); | |
543 | |
544 validateUsage(parser, | |
545 ''' | |
546 --[no-]affirm Should be on | |
547 (defaults to on) | |
548 | |
549 --[no-]negate Should be off | |
550 '''); | |
551 }); | |
552 | |
553 test('the default value for an option with no allowed list is shown', () { | |
554 var parser = new ArgParser(); | |
555 parser.addOption('any', help: 'Can be anything', defaultsTo: 'whatevs'); | |
556 | |
557 validateUsage(parser, | |
558 ''' | |
559 --any Can be anything | |
560 (defaults to "whatevs") | |
561 '''); | |
562 }); | |
563 | |
564 test('the allowed list is shown', () { | |
565 var parser = new ArgParser(); | |
566 parser.addOption('suit', help: 'Like in cards', | |
567 allowed: ['spades', 'clubs', 'hearts', 'diamonds']); | |
568 | |
569 validateUsage(parser, | |
570 ''' | |
571 --suit Like in cards | |
572 [spades, clubs, hearts, diamonds] | |
573 '''); | |
574 }); | |
575 | |
576 test('the default is highlighted in the allowed list', () { | |
577 var parser = new ArgParser(); | |
578 parser.addOption('suit', help: 'Like in cards', defaultsTo: 'clubs', | |
579 allowed: ['spades', 'clubs', 'hearts', 'diamonds']); | |
580 | |
581 validateUsage(parser, | |
582 ''' | |
583 --suit Like in cards | |
584 [spades, clubs (default), hearts, diamonds] | |
585 '''); | |
586 }); | |
587 | |
588 test('the allowed help is shown', () { | |
589 var parser = new ArgParser(); | |
590 parser.addOption('suit', help: 'Like in cards', defaultsTo: 'clubs', | |
591 allowed: ['spades', 'clubs', 'diamonds', 'hearts'], | |
592 allowedHelp: { | |
593 'spades': 'Swords of a soldier', | |
594 'clubs': 'Weapons of war', | |
595 'diamonds': 'Money for this art', | |
596 'hearts': 'The shape of my heart' | |
597 }); | |
598 | |
599 validateUsage(parser, | |
600 ''' | |
601 --suit Like in cards | |
602 | |
603 [clubs] Weapons of war | |
604 [diamonds] Money for this art | |
605 [hearts] The shape of my heart | |
606 [spades] Swords of a soldier | |
607 '''); | |
608 }); | |
609 }); | |
610 | |
611 group('ArgResults[]', () { | |
612 test('throws if the name is not an option', () { | |
613 var parser = new ArgParser(); | |
614 var results = parser.parse([]); | |
615 throwsIllegalArg(() => results['unknown']); | |
616 }); | |
617 }); | |
618 } | |
619 | |
620 throwsIllegalArg(function) { | |
621 expect(function, throwsIllegalArgumentException); | |
622 } | |
623 | |
624 throwsFormat(ArgParser parser, List<String> args) { | |
625 expect(() => parser.parse(args), throwsFormatException); | |
626 } | |
627 | |
628 validateUsage(ArgParser parser, String expected) { | |
629 expected = unindentString(expected); | |
630 expect(parser.getUsage(), equals(expected)); | |
631 } | |
632 | |
633 // TODO(rnystrom): Replace one in test_utils. | |
634 String unindentString(String text) { | |
635 var lines = text.split('\n'); | |
636 | |
637 // Count the indentation of the last line. | |
638 var whitespace = const RegExp('^ *'); | |
639 var indent = whitespace.firstMatch(lines[lines.length - 1])[0].length; | |
640 | |
641 // Drop the last line. It only exists for specifying indentation. | |
642 lines.removeLast(); | |
643 | |
644 // Strip indentation from the remaining lines. | |
645 for (var i = 0; i < lines.length; i++) { | |
646 var line = lines[i]; | |
647 if (line.length <= indent) { | |
648 // It's short, so it must be nothing but whitespace. | |
649 if (line.trim() != '') { | |
650 throw new IllegalArgumentException( | |
651 'Line "$line" does not have enough indentation.'); | |
652 } | |
653 | |
654 lines[i] = ''; | |
655 } else { | |
656 if (line.substring(0, indent).trim() != '') { | |
657 throw new IllegalArgumentException( | |
658 'Line "$line" does not have enough indentation.'); | |
659 } | |
660 | |
661 lines[i] = line.substring(indent); | |
662 } | |
663 } | |
664 | |
665 return Strings.join(lines, '\n'); | |
666 } | |
OLD | NEW |