1 package io.jawk;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 import static org.junit.Assert.assertArrayEquals;
26 import static org.junit.Assert.assertEquals;
27 import static org.junit.Assume.assumeNoException;
28 import static org.junit.Assume.assumeTrue;
29
30 import java.io.BufferedReader;
31 import java.io.BufferedWriter;
32 import java.io.ByteArrayInputStream;
33 import java.io.ByteArrayOutputStream;
34 import java.io.File;
35 import java.io.IOException;
36 import java.io.InputStream;
37 import java.io.InputStreamReader;
38 import java.io.PrintStream;
39 import java.io.Reader;
40 import java.io.StringReader;
41 import java.io.UncheckedIOException;
42 import java.nio.charset.StandardCharsets;
43 import java.nio.file.Files;
44 import java.nio.file.Path;
45 import java.util.ArrayList;
46 import java.util.Arrays;
47 import java.util.Collection;
48 import java.util.Collections;
49 import java.util.LinkedHashMap;
50 import java.util.List;
51 import java.util.Locale;
52 import java.util.Map;
53 import java.util.function.Function;
54 import java.util.stream.Collectors;
55 import java.util.stream.Stream;
56 import io.jawk.ext.JawkExtension;
57 import io.jawk.jrt.InputSource;
58
59
60
61
62
63
64
65
66
67
68 public final class AwkTestSupport {
69
70 private static final boolean IS_POSIX = !System
71 .getProperty("os.name", "")
72 .toLowerCase(Locale.ROOT)
73 .contains("win");
74
75 private static final Path SHARED_TEMP_DIR;
76
77 static {
78 try {
79 SHARED_TEMP_DIR = Files.createTempDirectory("jawk-shared");
80 SHARED_TEMP_DIR.toFile().deleteOnExit();
81 } catch (IOException ex) {
82 throw new ExceptionInInitializerError(ex);
83 }
84 }
85
86 private AwkTestSupport() {}
87
88
89
90
91
92
93
94
95
96 public static AwkTestBuilder awkTest(String description) {
97 return new AwkTestBuilder(description);
98 }
99
100
101
102
103
104
105
106
107
108 public static CliTestBuilder cliTest(String description) {
109 return new CliTestBuilder(description);
110 }
111
112
113
114
115
116
117
118
119 public static Path sharedTempDirectory() {
120 return SHARED_TEMP_DIR;
121 }
122
123
124
125
126
127
128 public interface ConfiguredTest {
129
130
131
132
133
134
135 String description();
136
137
138
139
140
141 void assumeSupported();
142
143
144
145
146
147
148
149
150 TestResult run() throws Exception;
151
152
153
154
155
156
157
158 default void runAndAssert() throws Exception {
159 assumeSupported();
160 run().assertExpected();
161 }
162 }
163
164
165
166
167
168
169 public static final class TestResult {
170 private final String description;
171 private final String output;
172 private final String errorOutput;
173 private final int exitCode;
174 private final String expectedOutput;
175 private final List<String> expectedLines;
176 private final Integer expectedExitCode;
177 private final Class<? extends Throwable> expectedException;
178 private final Throwable thrownException;
179
180 TestResult(
181 String description,
182 String output,
183 String errorOutput,
184 int exitCode,
185 String expectedOutput,
186 List<String> expectedLines,
187 Integer expectedExitCode,
188 Class<? extends Throwable> expectedException,
189 Throwable thrownException) {
190 this.description = description;
191 this.output = output;
192 this.errorOutput = errorOutput;
193 this.exitCode = exitCode;
194 this.expectedOutput = expectedOutput;
195 this.expectedLines = expectedLines != null ? Collections.unmodifiableList(new ArrayList<>(expectedLines)) : null;
196 this.expectedExitCode = expectedExitCode;
197 this.expectedException = expectedException;
198 this.thrownException = thrownException;
199 }
200
201
202
203
204
205
206 public String description() {
207 return description;
208 }
209
210
211
212
213
214
215 public String output() {
216 return output;
217 }
218
219
220
221
222
223
224 public String errorOutput() {
225 return errorOutput;
226 }
227
228
229
230
231
232
233 public int exitCode() {
234 return exitCode;
235 }
236
237
238
239
240
241
242
243
244
245 public String[] lines() {
246 List<String> split = readOutputLines(output);
247 return split.toArray(new String[0]);
248 }
249
250
251
252
253
254 public void assertExpected() {
255 if (expectedException != null) {
256 if (thrownException == null) {
257 throw new AssertionError(
258 "Expected exception "
259 + expectedException.getName()
260 + " for "
261 + description
262 + " but execution completed successfully");
263 }
264 if (!expectedException.isInstance(thrownException)) {
265 throw new AssertionError(
266 "Expected exception "
267 + expectedException.getName()
268 + " for "
269 + description
270 + " but got "
271 + thrownException.getClass().getName());
272 }
273 return;
274 }
275 if (expectedLines != null) {
276 List<String> actualLines = readOutputLines(output);
277 assertArrayEquals(
278 "Unexpected output for " + description,
279 expectedLines.toArray(new String[0]),
280 actualLines.toArray(new String[0]));
281 } else if (expectedOutput != null) {
282 assertEquals("Unexpected output for " + description, expectedOutput, output);
283 }
284 if (expectedExitCode != null) {
285 assertEquals("Unexpected exit code for " + description, expectedExitCode.intValue(), exitCode);
286 } else {
287 assertEquals("Unexpected exit code for " + description, 0, exitCode);
288 }
289 }
290
291 private static List<String> readOutputLines(String output) {
292 if (output.isEmpty()) {
293 return Collections.emptyList();
294 }
295 List<String> lines = new ArrayList<>();
296 try (BufferedReader reader = new BufferedReader(new StringReader(output))) {
297 String line;
298 while ((line = reader.readLine()) != null) {
299 lines.add(line);
300 }
301 } catch (IOException ex) {
302 throw new UncheckedIOException("Failed to split captured output", ex);
303 }
304 return Collections.unmodifiableList(lines);
305 }
306
307 public String expectedOutput() {
308 return expectedOutput;
309 }
310
311
312
313
314
315
316
317 public Integer expectedExitCode() {
318 return expectedExitCode;
319 }
320
321
322
323
324
325
326
327 public Throwable thrownException() {
328 return thrownException;
329 }
330
331
332
333
334
335
336
337 public Class<? extends Throwable> expectedException() {
338 return expectedException;
339 }
340 }
341
342
343
344
345
346
347 public static final class AwkTestBuilder extends BaseTestBuilder<AwkTestBuilder> {
348 private final Map<String, Object> preAssignments = new LinkedHashMap<>();
349 private Awk customAwk;
350 private final List<JawkExtension> extensions = new ArrayList<>();
351 private InputSource inputSource;
352 private Reader scriptReader;
353 private Path scriptPath;
354
355 private AwkTestBuilder(String description) {
356 super(description);
357 }
358
359
360
361
362
363
364
365
366 @Override
367 public AwkTestBuilder script(String script) {
368 scriptReader = null;
369 scriptPath = null;
370 return super.script(script);
371 }
372
373
374
375
376
377
378
379
380
381 public AwkTestBuilder script(Reader reader) {
382 if (reader == null) {
383 throw new IllegalArgumentException("reader must not be null");
384 }
385 script = null;
386 scriptReader = reader;
387 scriptPath = null;
388 return this;
389 }
390
391
392
393
394
395
396
397
398
399
400
401 public AwkTestBuilder script(InputStream scriptStream) {
402 if (scriptStream == null) {
403 throw new IllegalArgumentException("scriptStream must not be null");
404 }
405 return script(new InputStreamReader(scriptStream, StandardCharsets.UTF_8));
406 }
407
408
409
410
411
412
413
414
415 public AwkTestBuilder script(Path path) {
416 if (path == null) {
417 throw new IllegalArgumentException("path must not be null");
418 }
419 script = null;
420 scriptReader = null;
421 scriptPath = path;
422 return this;
423 }
424
425
426
427
428
429
430
431
432
433 public AwkTestBuilder preassign(String name, Object value) {
434 preAssignments.put(name, value);
435 return this;
436 }
437
438
439
440
441
442
443
444
445 public AwkTestBuilder withAwk(Awk awkEngine) {
446 if (awkEngine == null) {
447 throw new IllegalArgumentException("Awk instance must not be null");
448 }
449 this.customAwk = awkEngine;
450 return this;
451 }
452
453
454
455
456
457
458
459
460
461 public AwkTestBuilder withExtensions(JawkExtension... extensionsParam) {
462 if (extensionsParam != null) {
463 extensions.addAll(Arrays.asList(extensionsParam));
464 }
465 return this;
466 }
467
468
469
470
471
472
473
474
475
476 public AwkTestBuilder withExtensions(Collection<? extends JawkExtension> extensionsParam) {
477 if (extensionsParam != null) {
478 extensions.addAll(extensionsParam);
479 }
480 return this;
481 }
482
483
484
485
486
487
488
489
490
491
492 public AwkTestBuilder withInputSource(InputSource inputSourceParam) {
493 if (inputSourceParam == null) {
494 throw new IllegalArgumentException("InputSource must not be null");
495 }
496 this.inputSource = inputSourceParam;
497 return this;
498 }
499
500 @Override
501 protected AwkTestCase buildTestCase(
502 TestLayout layout,
503 Map<String, String> files,
504 Map<String, String> symlinks,
505 List<String> operands,
506 List<String> placeholders) {
507 if (useTempDir && !preAssignments.containsKey("TEMPDIR")) {
508 preAssignments.put("TEMPDIR", SHARED_TEMP_DIR.toString());
509 }
510 return new AwkTestCase(
511 layout,
512 files,
513 symlinks,
514 operands,
515 placeholders,
516 requiresPosix,
517 preAssignments,
518 customAwk,
519 extensions,
520 inputSource,
521 scriptReader,
522 scriptPath);
523 }
524 }
525
526
527
528
529
530
531 public static final class CliTestBuilder extends BaseTestBuilder<CliTestBuilder> {
532 private final List<String> argumentSpecs = new ArrayList<>();
533 private final Map<String, Object> assignments = new LinkedHashMap<>();
534 private final Map<String, String> environment = new LinkedHashMap<>();
535 private boolean redirectErrorStream;
536 private InputStream stdinStream;
537
538 private CliTestBuilder(String description) {
539 super(description);
540 }
541
542
543
544
545
546
547
548
549
550
551 public CliTestBuilder stdin(InputStream stream) {
552 this.stdinStream = stream;
553 return this;
554 }
555
556
557
558
559
560
561
562
563
564 public CliTestBuilder redirectErrorStream() {
565 redirectErrorStream = true;
566 return this;
567 }
568
569
570
571
572
573
574
575
576 public CliTestBuilder argument(String... args) {
577 argumentSpecs.addAll(Arrays.asList(args));
578 return this;
579 }
580
581
582
583
584
585
586
587
588
589 public CliTestBuilder preassign(String name, Object value) {
590 assignments.put(name, value);
591 return this;
592 }
593
594
595
596
597
598
599
600
601
602 public CliTestBuilder env(String name, String value) {
603 environment.put(name, value);
604 return this;
605 }
606
607
608
609
610
611
612
613
614
615 public CliTestBuilder env(Map<String, String> values) {
616 if (values != null) {
617 environment.putAll(values);
618 }
619 return this;
620 }
621
622 @Override
623 protected CliTestCase buildTestCase(
624 TestLayout layout,
625 Map<String, String> files,
626 Map<String, String> symlinks,
627 List<String> operands,
628 List<String> placeholders) {
629 if (useTempDir && !assignments.containsKey("TEMPDIR")) {
630 assignments.put("TEMPDIR", SHARED_TEMP_DIR.toString());
631 }
632 return new CliTestCase(
633 layout,
634 files,
635 symlinks,
636 operands,
637 placeholders,
638 requiresPosix,
639 argumentSpecs,
640 assignments,
641 environment,
642 redirectErrorStream,
643 stdinStream);
644 }
645 }
646
647
648
649
650
651
652
653
654 private abstract static class BaseTestBuilder<B extends BaseTestBuilder<B>> {
655 protected final String description;
656 protected String script;
657 protected String stdin;
658 protected final Map<String, String> fileContents = new LinkedHashMap<>();
659 protected final Map<String, String> symbolicLinks = new LinkedHashMap<>();
660 protected final List<String> operandSpecs = new ArrayList<>();
661 protected final List<String> pathPlaceholders = new ArrayList<>();
662 protected String expectedOutput;
663 protected List<String> expectedLines;
664 protected Integer expectedExitCode;
665 protected Class<? extends Throwable> expectedException;
666 protected boolean requiresPosix;
667 protected boolean useTempDir;
668 protected List<Function<String, String>> postProcessors = new ArrayList<>();
669
670 BaseTestBuilder(String description) {
671 this.description = description;
672 }
673
674
675
676
677
678
679
680
681 @SuppressWarnings("unchecked")
682 public B script(String script) {
683 this.script = script;
684 return (B) this;
685 }
686
687
688
689
690
691
692
693
694 @SuppressWarnings("unchecked")
695 public B stdin(String stdin) {
696 this.stdin = stdin;
697 return (B) this;
698 }
699
700
701
702
703
704
705
706
707
708
709 @SuppressWarnings("unchecked")
710 public B file(String name, String contents) {
711 fileContents.put(name, contents);
712 return (B) this;
713 }
714
715
716
717
718
719
720
721
722
723
724
725 @SuppressWarnings("unchecked")
726 public B symlink(String name, String target) {
727 symbolicLinks.put(name, target);
728 return (B) this;
729 }
730
731
732
733
734
735
736
737
738 @SuppressWarnings("unchecked")
739 public B operand(String... operands) {
740 operandSpecs.addAll(Arrays.asList(operands));
741 return (B) this;
742 }
743
744
745
746
747
748
749
750
751
752 @SuppressWarnings("unchecked")
753 public B path(String placeholder) {
754 pathPlaceholders.add(placeholder);
755 return (B) this;
756 }
757
758
759
760
761
762
763
764
765 @SuppressWarnings("unchecked")
766 public B postProcessWith(Function<String, String> processor) {
767 if (processor != null) {
768 postProcessors.add(processor);
769 }
770 return (B) this;
771 }
772
773
774
775
776
777
778
779 @SuppressWarnings("unchecked")
780 public B expect(String expected) {
781 this.expectedOutput = expected;
782 this.expectedLines = null;
783 return (B) this;
784 }
785
786
787
788
789
790
791
792
793 public B expectLines(String... lines) {
794 return expectLines(Arrays.asList(Arrays.copyOf(lines, lines.length)));
795 }
796
797
798
799
800
801
802
803
804 @SuppressWarnings("unchecked")
805 public B expectLines(List<String> lines) {
806 this.expectedLines = new ArrayList<>(lines);
807 this.expectedOutput = null;
808 return (B) this;
809 }
810
811
812
813
814
815
816
817
818
819 public B expectLines(File expectedResultFile) throws IOException {
820 return expectLines(expectedResultFile.toPath());
821 }
822
823
824
825
826
827
828
829
830
831 public B expectLines(Path expectedResultPath) throws IOException {
832 return expectLines(Files.readAllLines(expectedResultPath, StandardCharsets.UTF_8));
833 }
834
835
836
837
838
839
840
841 @SuppressWarnings("unchecked")
842 public B expectExit(int code) {
843 this.expectedExitCode = code;
844 return (B) this;
845 }
846
847
848
849
850
851
852
853 @SuppressWarnings("unchecked")
854 public B expectThrow(Class<? extends Throwable> exceptionClass) {
855 this.expectedException = exceptionClass;
856 return (B) this;
857 }
858
859
860
861
862
863
864
865 @SuppressWarnings("unchecked")
866 public B posixOnly() {
867 this.requiresPosix = true;
868 return (B) this;
869 }
870
871
872
873
874
875
876
877
878 @SuppressWarnings("unchecked")
879 public B withTempDir() {
880 this.useTempDir = true;
881 return (B) this;
882 }
883
884
885
886
887
888
889
890 public ConfiguredTest build() {
891 TestLayout layout = new TestLayout(
892 description,
893 script,
894 stdin,
895 postProcessors,
896 expectedOutput,
897 expectedLines,
898 expectedExitCode,
899 expectedException);
900 Map<String, String> files = new LinkedHashMap<>(fileContents);
901 Map<String, String> symlinks = new LinkedHashMap<>(symbolicLinks);
902 List<String> operands = new ArrayList<>(operandSpecs);
903 List<String> placeholders = new ArrayList<>(pathPlaceholders);
904 return buildTestCase(layout, files, symlinks, operands, placeholders);
905 }
906
907
908
909
910
911
912
913
914 public TestResult run() throws Exception {
915 return build().run();
916 }
917
918
919
920
921
922
923
924 public void runAndAssert() throws Exception {
925 build().runAndAssert();
926 }
927
928 protected abstract BaseTestCase buildTestCase(
929 TestLayout layout,
930 Map<String, String> fileContents,
931 Map<String, String> symbolicLinks,
932 List<String> operandSpecs,
933 List<String> pathPlaceholders);
934 }
935
936 private abstract static class BaseTestCase implements ConfiguredTest {
937 private final TestLayout layout;
938 private final Map<String, String> fileContents;
939 private final Map<String, String> symbolicLinks;
940 private final List<String> operandSpecs;
941 private final List<String> pathPlaceholders;
942 private final boolean requiresPosix;
943
944 BaseTestCase(
945 TestLayout layout,
946 Map<String, String> fileContents,
947 Map<String, String> symbolicLinks,
948 List<String> operandSpecs,
949 List<String> pathPlaceholders,
950 boolean requiresPosix) {
951 this.layout = layout;
952 this.fileContents = fileContents;
953 this.symbolicLinks = symbolicLinks;
954 this.operandSpecs = operandSpecs;
955 this.pathPlaceholders = pathPlaceholders;
956 this.requiresPosix = requiresPosix;
957 }
958
959 @Override
960 public String description() {
961 return layout.description;
962 }
963
964 @Override
965 public void assumeSupported() {
966 if (requiresPosix) {
967 assumeTrue("POSIX-like environment required for " + layout.description, IS_POSIX);
968 }
969 }
970
971 @Override
972 public final TestResult run() throws Exception {
973 assumeSupported();
974 ExecutionEnvironment env = prepareEnvironment();
975 try {
976 return executeAndCapture(env);
977 } finally {
978 deleteRecursively(env.tempDir);
979 }
980 }
981
982 private TestResult executeAndCapture(ExecutionEnvironment env) throws Exception {
983 try {
984
985 ActualResult result = execute(env);
986 String actualOutput = result.output;
987
988
989 if (layout.postProcessors != null) {
990 for (Function<String, String> processor : layout.postProcessors) {
991 actualOutput = processor.apply(actualOutput);
992 }
993 }
994
995
996 String expected = layout.expectedOutput != null ? env.resolve(layout.expectedOutput) : null;
997 List<String> expectedLines = null;
998 if (layout.expectedLines != null) {
999 expectedLines = new ArrayList<>(layout.expectedLines.size());
1000 for (String line : layout.expectedLines) {
1001 expectedLines.add(env.resolve(line));
1002 }
1003 }
1004
1005 return new TestResult(
1006 layout.description,
1007 actualOutput,
1008 result.errorOutput,
1009 result.exitCode,
1010 expected,
1011 expectedLines,
1012 layout.expectedExitCode,
1013 layout.expectedException,
1014 null);
1015 } catch (Throwable ex) {
1016 if (layout.expectedException != null && layout.expectedException.isInstance(ex)) {
1017 return new TestResult(
1018 layout.description,
1019 "",
1020 "",
1021 0,
1022 null,
1023 null,
1024 layout.expectedExitCode,
1025 layout.expectedException,
1026 ex);
1027 }
1028 if (ex instanceof Exception) {
1029 throw (Exception) ex;
1030 }
1031 throw (Error) ex;
1032 }
1033 }
1034
1035 protected abstract ActualResult execute(ExecutionEnvironment env) throws Exception;
1036
1037 protected ExecutionEnvironment prepareEnvironment() throws IOException {
1038 Path tempDir = Files.createTempDirectory("jawk-test");
1039 Map<String, Path> placeholders = new LinkedHashMap<>();
1040 for (Map.Entry<String, String> entry : fileContents.entrySet()) {
1041 Path path = tempDir.resolve(entry.getKey());
1042 Path parent = path.getParent();
1043 if (parent != null) {
1044 Files.createDirectories(parent);
1045 }
1046
1047 if (entry.getValue() != null) {
1048 try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
1049 writer.write(entry.getValue());
1050 }
1051 }
1052 placeholders.put(entry.getKey(), path);
1053 }
1054 for (Map.Entry<String, String> entry : symbolicLinks.entrySet()) {
1055 Path link = tempDir.resolve(entry.getKey());
1056 Path parent = link.getParent();
1057 if (parent != null) {
1058 Files.createDirectories(parent);
1059 }
1060 try {
1061 Files.createSymbolicLink(link, tempDir.resolve(entry.getValue()));
1062 } catch (IOException | UnsupportedOperationException | SecurityException ex) {
1063 deleteRecursively(tempDir);
1064 assumeNoException("Symbolic links are unavailable for " + layout.description, ex);
1065 }
1066 placeholders.put(entry.getKey(), link);
1067 }
1068 for (String placeholder : pathPlaceholders) {
1069 Path path = tempDir.resolve(placeholder);
1070 Path parent = path.getParent();
1071 if (parent != null) {
1072 Files.createDirectories(parent);
1073 }
1074 placeholders.put(placeholder, path);
1075 }
1076 return new ExecutionEnvironment(tempDir, placeholders);
1077 }
1078
1079 protected List<String> resolvedOperands(ExecutionEnvironment env) {
1080 return operandSpecs
1081 .stream()
1082 .map(env::resolve)
1083 .collect(Collectors.toList());
1084 }
1085
1086 protected String resolvedScript(ExecutionEnvironment env) {
1087 return layout.script != null ? env.resolveScript(layout.script) : null;
1088 }
1089
1090 protected String resolvedStdin(ExecutionEnvironment env) {
1091 return layout.stdin != null ? env.resolve(layout.stdin) : null;
1092 }
1093 }
1094
1095 private static final class AwkTestCase extends BaseTestCase {
1096 private final Map<String, Object> preAssignments;
1097 private final Awk customAwk;
1098 private final List<JawkExtension> extensions;
1099 private final InputSource inputSource;
1100 private final Reader scriptReader;
1101 private final Path scriptPath;
1102
1103 AwkTestCase(
1104 TestLayout layout,
1105 Map<String, String> fileContents,
1106 Map<String, String> symbolicLinks,
1107 List<String> operandSpecs,
1108 List<String> pathPlaceholders,
1109 boolean requiresPosix,
1110 Map<String, Object> preAssignments,
1111 Awk customAwk,
1112 List<JawkExtension> extensions,
1113 InputSource inputSource,
1114 Reader scriptReader,
1115 Path scriptPath) {
1116 super(layout, fileContents, symbolicLinks, operandSpecs, pathPlaceholders, requiresPosix);
1117 this.preAssignments = new LinkedHashMap<>(preAssignments);
1118 this.customAwk = customAwk;
1119 this.extensions = new ArrayList<>(extensions);
1120 this.inputSource = inputSource;
1121 this.scriptReader = scriptReader;
1122 this.scriptPath = scriptPath;
1123 }
1124
1125 @Override
1126 protected ActualResult execute(ExecutionEnvironment env) throws Exception {
1127
1128
1129 Awk awk;
1130 if (customAwk != null) {
1131 awk = customAwk;
1132 } else if (extensions.isEmpty()) {
1133 awk = new Awk();
1134 } else {
1135 awk = new Awk(extensions);
1136 }
1137 StringBuilder out = new StringBuilder();
1138 AwkProgram program;
1139 if (scriptPath != null) {
1140 try (BufferedReader reader = Files.newBufferedReader(scriptPath, StandardCharsets.UTF_8)) {
1141 program = awk.compile(reader);
1142 }
1143 } else if (scriptReader != null) {
1144 try (Reader reader = scriptReader) {
1145 program = awk.compile(reader);
1146 }
1147 } else {
1148 program = awk.compile(resolvedScript(env));
1149 }
1150 Awk.AwkRunBuilder builder = awk
1151 .script(program)
1152 .arguments(resolvedOperands(env))
1153 .variables(preAssignments);
1154 if (inputSource != null) {
1155 builder.input(inputSource);
1156 } else {
1157 String stdin = resolvedStdin(env);
1158 if (stdin != null) {
1159 builder.input(stdin);
1160 }
1161 }
1162 int exitCode = 0;
1163 try {
1164 builder.execute(out);
1165 } catch (ExitException ex) {
1166 exitCode = ex.getCode();
1167 }
1168 return new ActualResult(out.toString(), "", exitCode);
1169 }
1170 }
1171
1172 private static final class CliTestCase extends BaseTestCase {
1173 private final List<String> argumentSpecs;
1174 private final Map<String, Object> assignments;
1175 private final Map<String, String> environment;
1176 private final boolean redirectErrorStream;
1177 private final InputStream stdinStream;
1178
1179 CliTestCase(
1180 TestLayout layout,
1181 Map<String, String> fileContents,
1182 Map<String, String> symbolicLinks,
1183 List<String> operandSpecs,
1184 List<String> pathPlaceholders,
1185 boolean requiresPosix,
1186 List<String> argumentSpecs,
1187 Map<String, Object> assignments,
1188 Map<String, String> environment,
1189 boolean redirectErrorStream,
1190 InputStream stdinStream) {
1191 super(layout, fileContents, symbolicLinks, operandSpecs, pathPlaceholders, requiresPosix);
1192 this.argumentSpecs = new ArrayList<>(argumentSpecs);
1193 this.assignments = new LinkedHashMap<>(assignments);
1194 this.environment = new LinkedHashMap<>(environment);
1195 this.redirectErrorStream = redirectErrorStream;
1196 this.stdinStream = stdinStream;
1197 }
1198
1199 @Override
1200 protected ActualResult execute(ExecutionEnvironment env) throws Exception {
1201 String stdin = resolvedStdin(env);
1202 InputStream in;
1203 if (stdinStream != null) {
1204 in = stdinStream;
1205 } else {
1206 in = stdin != null ?
1207 new ByteArrayInputStream(stdin.getBytes(StandardCharsets.UTF_8)) :
1208 new ByteArrayInputStream(new byte[0]);
1209 }
1210 ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
1211 ByteArrayOutputStream errBytes = new ByteArrayOutputStream();
1212 Map<String, String> resolvedEnvironment = new LinkedHashMap<String, String>();
1213 for (Map.Entry<String, String> entry : environment.entrySet()) {
1214 resolvedEnvironment.put(entry.getKey(), env.resolve(entry.getValue()));
1215 }
1216 PrintStream outStream = new PrintStream(outBytes, true, StandardCharsets.UTF_8.name());
1217 PrintStream errStream = redirectErrorStream ?
1218 outStream :
1219 new PrintStream(errBytes, true, StandardCharsets.UTF_8.name());
1220 Cli cli = new Cli(
1221 in,
1222 outStream,
1223 errStream,
1224 resolvedEnvironment);
1225
1226 List<String> args = new ArrayList<>();
1227 for (Map.Entry<String, Object> entry : assignments.entrySet()) {
1228 args.add("-v");
1229 args.add(entry.getKey() + "=" + String.valueOf(entry.getValue()));
1230 }
1231 for (String spec : argumentSpecs) {
1232 args.add(env.resolve(spec));
1233 }
1234 String resolvedScript = resolvedScript(env);
1235 if (resolvedScript != null) {
1236 args.add(resolvedScript);
1237 }
1238 args.addAll(resolvedOperands(env));
1239
1240 int exitCode = 0;
1241 try {
1242 cli.parse(args.toArray(new String[0]));
1243 cli.run();
1244 } catch (ExitException ex) {
1245 exitCode = ex.getCode();
1246 }
1247 return new ActualResult(
1248 outBytes.toString(StandardCharsets.UTF_8.name()),
1249 errBytes.toString(StandardCharsets.UTF_8.name()),
1250 exitCode);
1251 }
1252 }
1253
1254 private static final class ExecutionEnvironment {
1255 private final Path tempDir;
1256 private final Map<String, Path> placeholders;
1257
1258 ExecutionEnvironment(Path tempDir, Map<String, Path> placeholders) {
1259 this.tempDir = tempDir;
1260 this.placeholders = placeholders;
1261 }
1262
1263 String resolve(String value) {
1264 if (value == null) {
1265 return null;
1266 }
1267 return replacePlaceholders(value, false);
1268 }
1269
1270 String resolveScript(String value) {
1271 if (value == null) {
1272 return null;
1273 }
1274 return replacePlaceholders(value, true);
1275 }
1276
1277 private String replacePlaceholders(String value, boolean escapeForScript) {
1278 String result = value;
1279 for (Map.Entry<String, Path> entry : placeholders.entrySet()) {
1280 String replacement = entry.getValue().toString();
1281 if (escapeForScript) {
1282 replacement = escapeForAwkString(replacement);
1283 }
1284 result = result.replace("{{" + entry.getKey() + "}}", replacement);
1285 }
1286 return result;
1287 }
1288 }
1289
1290 private static final class ActualResult {
1291 final String output;
1292 final String errorOutput;
1293 final int exitCode;
1294
1295 ActualResult(String output, String errorOutput, int exitCode) {
1296 this.output = output;
1297 this.errorOutput = errorOutput;
1298 this.exitCode = exitCode;
1299 }
1300 }
1301
1302 private static final class TestLayout {
1303 final String description;
1304 final String script;
1305 final String stdin;
1306 final List<Function<String, String>> postProcessors;
1307 final String expectedOutput;
1308 final List<String> expectedLines;
1309 final Integer expectedExitCode;
1310 final Class<? extends Throwable> expectedException;
1311
1312 TestLayout(
1313 String description,
1314 String script,
1315 String stdin,
1316 List<Function<String, String>> postProcessors,
1317 String expectedOutput,
1318 List<String> expectedLines,
1319 Integer expectedExitCode,
1320 Class<? extends Throwable> expectedException) {
1321 this.description = description;
1322 this.script = script;
1323 this.stdin = stdin;
1324 this.postProcessors = postProcessors != null ?
1325 Collections.unmodifiableList(new ArrayList<>(postProcessors)) : null;
1326 this.expectedOutput = expectedOutput;
1327 this.expectedLines = expectedLines != null ? Collections.unmodifiableList(new ArrayList<>(expectedLines)) : null;
1328 this.expectedExitCode = expectedExitCode;
1329 this.expectedException = expectedException;
1330 }
1331 }
1332
1333 private static void deleteRecursively(Path root) throws IOException {
1334 if (root == null || !Files.exists(root)) {
1335 return;
1336 }
1337 try (Stream<Path> walk = Files.walk(root)) {
1338 walk.sorted((a, b) -> b.compareTo(a)).forEach(path -> {
1339 try {
1340 Files.deleteIfExists(path);
1341 } catch (IOException ignored) {
1342
1343 }
1344 });
1345 }
1346 }
1347
1348 private static String escapeForAwkString(String value) {
1349 StringBuilder builder = new StringBuilder(value.length() * 2);
1350 for (int i = 0; i < value.length(); i++) {
1351 char ch = value.charAt(i);
1352 if (ch == '\\' || ch == '"') {
1353 builder.append('\\');
1354 }
1355 builder.append(ch);
1356 }
1357 return builder.toString();
1358 }
1359 }