1
2
3
4
5
6 package dev.metaschema.cli;
7
8 import static org.assertj.core.api.Assertions.assertThat;
9 import static org.junit.jupiter.api.Assertions.assertAll;
10 import static org.junit.jupiter.api.Assertions.assertEquals;
11 import static org.junit.jupiter.api.Assertions.assertNull;
12
13 import org.junit.jupiter.api.Test;
14 import org.junit.jupiter.api.parallel.Execution;
15 import org.junit.jupiter.api.parallel.ExecutionMode;
16 import org.junit.jupiter.params.ParameterizedTest;
17 import org.junit.jupiter.params.provider.Arguments;
18 import org.junit.jupiter.params.provider.MethodSource;
19
20 import java.io.OutputStream;
21 import java.io.PrintStream;
22 import java.util.LinkedList;
23 import java.util.List;
24 import java.util.stream.Stream;
25
26 import dev.metaschema.cli.processor.ExitCode;
27 import dev.metaschema.cli.processor.ExitStatus;
28 import edu.umd.cs.findbugs.annotations.NonNull;
29 import nl.altindag.log.LogCaptor;
30
31
32
33
34 @Execution(value = ExecutionMode.SAME_THREAD, reason = "Log capturing needs to be single threaded")
35 public class CLITest {
36 private static final ExitCode NO_EXCEPTION_CLASS = null;
37
38
39
40
41
42 @SuppressWarnings("resource")
43 private static final PrintStream NULL_STREAM = new PrintStream(new OutputStream() {
44 @Override
45 public void write(int b) {
46
47 }
48
49 @Override
50 public void write(byte[] b, int off, int len) {
51
52 }
53 });
54
55 void evaluateResult(@NonNull ExitStatus status, @NonNull ExitCode expectedCode, @NonNull String[] args) {
56 status.generateMessage(true);
57 Throwable thrown = status.getThrowable();
58 assertAll(
59 () -> assertEquals(expectedCode, status.getExitCode(),
60 () -> buildExitCodeMismatchMessage(status, expectedCode, thrown, args)),
61 () -> assertNull(thrown,
62 () -> buildUnexpectedThrowableMessage(thrown, args)));
63 }
64
65 void evaluateResult(@NonNull ExitStatus status, @NonNull ExitCode expectedCode,
66 @NonNull Class<? extends Throwable> thrownClass, @NonNull String[] args) {
67 Throwable thrown = status.getThrowable();
68 assertAll(
69 () -> assertEquals(expectedCode, status.getExitCode(),
70 () -> buildExitCodeMismatchMessage(status, expectedCode, thrown, args)),
71 () -> assertEquals(thrownClass, thrown == null ? null : thrown.getClass(),
72 () -> buildThrowableMismatchMessage(thrownClass, thrown, args)));
73 }
74
75 private static String buildExitCodeMismatchMessage(@NonNull ExitStatus status, @NonNull ExitCode expectedCode,
76 Throwable thrown, @NonNull String[] args) {
77 StringBuilder sb = new StringBuilder();
78 sb.append("exit code mismatch: expected <").append(expectedCode).append("> but was <")
79 .append(status.getExitCode()).append(">");
80 sb.append("\nCommand args: ").append(String.join(" ", args));
81 if (status.getMessage() != null) {
82 sb.append("\nStatus message: ").append(status.getMessage());
83 }
84 if (thrown != null) {
85 sb.append("\nThrowable: ").append(thrown.getClass().getName()).append(": ").append(thrown.getMessage());
86 sb.append("\nStack trace:\n").append(getStackTraceAsString(thrown));
87 }
88 return sb.toString();
89 }
90
91 private static String buildUnexpectedThrowableMessage(Throwable thrown, @NonNull String[] args) {
92 if (thrown == null) {
93 return "expected null Throwable";
94 }
95 StringBuilder sb = new StringBuilder();
96 sb.append("expected null Throwable but got: ").append(thrown.getClass().getName())
97 .append(": ").append(thrown.getMessage());
98 sb.append("\nCommand args: ").append(String.join(" ", args));
99 sb.append("\nStack trace:\n").append(getStackTraceAsString(thrown));
100 return sb.toString();
101 }
102
103 private static String buildThrowableMismatchMessage(Class<? extends Throwable> expectedClass, Throwable thrown,
104 @NonNull String[] args) {
105 StringBuilder sb = new StringBuilder();
106 sb.append("expected Throwable mismatch: expected <")
107 .append(expectedClass == null ? "null" : expectedClass.getName())
108 .append("> but was <")
109 .append(thrown == null ? "null" : thrown.getClass().getName())
110 .append(">");
111 sb.append("\nCommand args: ").append(String.join(" ", args));
112 if (thrown != null) {
113 sb.append("\nMessage: ").append(thrown.getMessage());
114 sb.append("\nStack trace:\n").append(getStackTraceAsString(thrown));
115 }
116 return sb.toString();
117 }
118
119 private static String getStackTraceAsString(Throwable throwable) {
120 java.io.StringWriter sw = new java.io.StringWriter();
121 throwable.printStackTrace(new java.io.PrintWriter(sw));
122 return sw.toString();
123 }
124
125 private static Stream<Arguments> providesValues() {
126 List<Arguments> values = new LinkedList<>() {
127 {
128 add(Arguments.of(new String[] {}, ExitCode.INVALID_COMMAND,
129 NO_EXCEPTION_CLASS));
130 add(Arguments.of(new String[] { "-h" }, ExitCode.OK, NO_EXCEPTION_CLASS));
131 add(Arguments.of(new String[] { "generate-schema", "--help" }, ExitCode.OK,
132 NO_EXCEPTION_CLASS));
133 add(Arguments.of(new String[] { "generate-diagram", "--help" }, ExitCode.OK,
134 NO_EXCEPTION_CLASS));
135 add(Arguments.of(new String[] { "validate", "--help" }, ExitCode.OK,
136 NO_EXCEPTION_CLASS));
137 add(Arguments.of(new String[] { "validate-content", "--help" }, ExitCode.OK,
138 NO_EXCEPTION_CLASS));
139 add(Arguments.of(new String[] { "convert", "--help" }, ExitCode.OK,
140 NO_EXCEPTION_CLASS));
141 add(Arguments.of(new String[] { "metapath", "list-functions", "--help" }, ExitCode.OK,
142 NO_EXCEPTION_CLASS));
143 add(Arguments.of(new String[] { "metapath", "eval", "--help" }, ExitCode.OK,
144 NO_EXCEPTION_CLASS));
145 add(Arguments.of(
146 new String[] { "validate",
147 "../databind/src/test/resources/metaschema/fields_with_flags/metaschema.xml"
148 },
149 ExitCode.OK, NO_EXCEPTION_CLASS));
150 add(Arguments.of(
151 new String[] { "generate-schema", "--overwrite", "--as",
152 "JSON",
153 "../databind/src/test/resources/metaschema/fields_with_flags/metaschema.xml",
154 "target/schema-test.json" },
155 ExitCode.OK, NO_EXCEPTION_CLASS));
156 add(Arguments.of(
157 new String[] { "validate-content", "--as=xml",
158 "-m=../databind/src/test/resources/metaschema/bad_index-has-key/metaschema.xml",
159 "../databind/src/test/resources/metaschema/bad_index-has-key/example.xml",
160 "--show-stack-trace" },
161 ExitCode.FAIL, NO_EXCEPTION_CLASS));
162 add(Arguments.of(
163 new String[] { "validate-content", "--as=json",
164 "-m=../databind/src/test/resources/metaschema/bad_index-has-key/metaschema.xml",
165 "../databind/src/test/resources/metaschema/bad_index-has-key/example.json", "--show-stack-trace" },
166 ExitCode.FAIL, NO_EXCEPTION_CLASS));
167 add(Arguments.of(
168 new String[] { "validate",
169 "../databind/src/test/resources/metaschema/simple/metaschema.xml",
170 "--show-stack-trace" },
171 ExitCode.OK, NO_EXCEPTION_CLASS));
172 add(Arguments.of(
173 new String[] { "generate-schema",
174 "../databind/src/test/resources/metaschema/simple/metaschema.xml",
175 "--as", "xml",
176 },
177 ExitCode.OK, NO_EXCEPTION_CLASS));
178 add(Arguments.of(
179 new String[] { "generate-schema",
180 "../databind/src/test/resources/metaschema/simple/metaschema.xml",
181 "--as", "json",
182 },
183 ExitCode.OK, NO_EXCEPTION_CLASS));
184 add(Arguments.of(
185 new String[] { "generate-diagram",
186 "../databind/src/test/resources/metaschema/simple/metaschema.xml"
187 },
188 ExitCode.OK, NO_EXCEPTION_CLASS));
189 add(Arguments.of(
190 new String[] { "validate-content",
191 "-m",
192 "../databind/src/test/resources/metaschema/simple/metaschema.xml",
193 "../databind/src/test/resources/metaschema/simple/example.json",
194 "--as=json"
195 },
196 ExitCode.OK, NO_EXCEPTION_CLASS));
197 add(Arguments.of(
198 new String[] { "validate-content",
199 "-m",
200 "../databind/src/test/resources/metaschema/simple/metaschema.xml",
201 "../databind/src/test/resources/metaschema/simple/example.xml",
202 "--as=xml"
203 },
204 ExitCode.OK, NO_EXCEPTION_CLASS));
205 add(Arguments.of(
206 new String[] { "validate-content",
207 "-m",
208 "../databind/src/test/resources/metaschema/simple/metaschema.xml",
209 "https://bad.domain.example.net/example.xml",
210 "--as=xml"
211 },
212 ExitCode.IO_ERROR, java.net.UnknownHostException.class));
213 add(Arguments.of(
214 new String[] { "validate-content",
215 "-m",
216 "../databind/src/test/resources/metaschema/simple/metaschema.xml",
217 "https://github.com/no-example.xml",
218 "--as=xml"
219 },
220 ExitCode.IO_ERROR, java.io.FileNotFoundException.class));
221 add(Arguments.of(
222 new String[] { "validate-content",
223 "-m",
224 "src/test/resources/content/schema-validation-module.xml",
225 "src/test/resources/content/schema-validation-module-missing-required.xml",
226 "--as=xml"
227 },
228
229 ExitCode.FAIL, NO_EXCEPTION_CLASS));
230 add(Arguments.of(
231 new String[] { "validate-content",
232 "-m",
233 "src/test/resources/content/schema-validation-module.xml",
234 "src/test/resources/content/schema-validation-module-missing-required.xml",
235 "--as=xml",
236 "--disable-schema-validation"
237 },
238
239 ExitCode.FAIL, NO_EXCEPTION_CLASS));
240 add(Arguments.of(
241 new String[] { "validate-content",
242 "-m",
243 "src/test/resources/content/schema-validation-module.xml",
244 "src/test/resources/content/schema-validation-module-missing-required.xml",
245 "--as=xml",
246 "--disable-schema-validation",
247 "--disable-constraint-validation"
248 },
249 ExitCode.OK, NO_EXCEPTION_CLASS));
250 add(Arguments.of(
251 new String[] { "metapath", "list-functions" },
252 ExitCode.OK, NO_EXCEPTION_CLASS));
253 add(Arguments.of(
254 new String[] { "convert",
255 "-m",
256 "../core/metaschema/schema/metaschema/metaschema-module-metaschema.xml",
257 "--to=yaml",
258 "../core/metaschema/schema/metaschema/metaschema-module-metaschema.xml",
259 },
260 ExitCode.OK, NO_EXCEPTION_CLASS));
261
262 add(Arguments.of(
263 new String[] { "validate-content",
264 "-m",
265 "src/test/resources/content/test-markup-line-module.yaml",
266 "src/test/resources/content/test-markup-line-content.json",
267 "--as=json"
268 },
269 ExitCode.OK, NO_EXCEPTION_CLASS));
270 }
271 };
272 return values.stream();
273 }
274
275 @ParameterizedTest
276 @MethodSource("providesValues")
277 void testAllCommands(@NonNull String[] args, @NonNull ExitCode expectedExitCode,
278 Class<? extends Throwable> expectedThrownClass) {
279 String[] defaultArgs = { "--show-stack-trace" };
280 String[] fullArgs = Stream.of(args, defaultArgs).flatMap(Stream::of)
281 .toArray(String[]::new);
282 if (expectedThrownClass == null) {
283 evaluateResult(CLI.runCli(NULL_STREAM, fullArgs), expectedExitCode, fullArgs);
284 } else {
285 evaluateResult(CLI.runCli(NULL_STREAM, fullArgs), expectedExitCode, expectedThrownClass, fullArgs);
286 }
287 }
288
289 @Test
290 void testValidateContent() {
291 try (LogCaptor captor = LogCaptor.forRoot()) {
292 String[] cliArgs = { "validate-content",
293 "-m",
294 "src/test/resources/content/215-module.xml",
295 "src/test/resources/content/215.xml",
296 "--disable-schema-validation"
297 };
298 CLI.runCli(NULL_STREAM, cliArgs);
299 assertThat(captor.getErrorLogs().toString())
300 .contains("expect-default-non-zero: Expect constraint '. > 0' did not match the data",
301 "expect-custom-non-zero: No default message, custom error message for expect-custom-non-zero constraint.",
302 "matches-default-regex-letters-only: Value '1' did not match the pattern",
303 "matches-custom-regex-letters-only: No default message, custom error message for" +
304 " matches-custom-regex-letters-only constraint.",
305 "cardinality-default-two-minimum: The cardinality '1' is below the required minimum '2' for items" +
306 " matching",
307 "index-items-default: Index 'index-items-default' has duplicate key for items",
308 "index-items-custom: No default message, custom error message for index-item-custom.",
309 "is-unique-default: Unique constraint violation at paths",
310 "is-unique-custom: No default message, custom error message for is-unique-custom.",
311 "index-has-key-default: Key reference [2] not found in index 'index-items-default' for item",
312 "index-has-key-custom: No default message, custom error message for index-has-key-custom.");
313 }
314 }
315
316 @Test
317 void testValidateConstraints() {
318 try (LogCaptor captor = LogCaptor.forRoot()) {
319 String[] cliArgs = { "validate",
320 "src/test/resources/content/constraint-example.xml",
321 "-c",
322 "src/test/resources/content/constraint-constraints.xml",
323 "--disable-schema-validation",
324 };
325 CLI.runCli(NULL_STREAM, cliArgs);
326 assertThat(captor.getErrorLogs().toString())
327 .contains("This constraint SHOULD be violated if test passes.");
328 }
329 }
330 }