Skip to content

Commit d02b16d

Browse files
committed
test(argv): add ordered option and immutable request RED contracts
Cover repeatable option occurrences, explicit valued false, positive/negative flags, nonrepeatable and legacy collisions, malformed schemas, and captured mutable inputs. Use public API reflection so absent API is an assertion failure, not compile failure. The existing 33 real-child contracts remain enabled.
1 parent 6f830b4 commit d02b16d

1 file changed

Lines changed: 180 additions & 0 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package io.github.easy4j.opencli.contract;
2+
3+
import io.github.easy4j.opencli.OpenCliProperties;
4+
import io.github.easy4j.opencli.core.OpenCliAdapterChannel;
5+
import io.github.easy4j.opencli.core.OpenCliAdapterCommandRequest;
6+
import io.github.easy4j.opencli.core.OpenCliExecutor;
7+
import io.github.easy4j.opencli.core.OpenCliResult;
8+
import java.io.File;
9+
import java.lang.reflect.InvocationTargetException;
10+
import java.lang.reflect.Method;
11+
import java.nio.charset.StandardCharsets;
12+
import java.util.ArrayList;
13+
import java.util.Arrays;
14+
import java.util.Base64;
15+
import java.util.Collections;
16+
import java.util.LinkedHashMap;
17+
import java.util.List;
18+
import java.util.Map;
19+
import org.junit.jupiter.api.Test;
20+
import static org.junit.jupiter.api.Assertions.*;
21+
22+
/** Public binary/API contract: missing methods fail assertions rather than preventing the RED build. */
23+
class OpenCliStructuredArgvContractTest {
24+
private static Class<?> type(String simpleName) {
25+
return assertDoesNotThrow(() -> Class.forName("io.github.easy4j.opencli.core." + simpleName),
26+
"required ordered-option API is not implemented");
27+
}
28+
29+
private static Object call(Class<?> owner, Object receiver, String name, Class<?>[] parameterTypes, Object... args) {
30+
Method method = assertDoesNotThrow(() -> owner.getMethod(name, parameterTypes),
31+
"required public method is not implemented: " + name);
32+
try {
33+
return method.invoke(receiver, args);
34+
} catch (InvocationTargetException ex) {
35+
Throwable cause = ex.getCause();
36+
if (cause instanceof RuntimeException) {
37+
throw (RuntimeException) cause;
38+
}
39+
throw new AssertionError("unexpected checked failure", cause);
40+
} catch (ReflectiveOperationException ex) {
41+
throw new AssertionError("public API is inaccessible", ex);
42+
}
43+
}
44+
45+
private static Object schema(String form, String name, boolean repeatable) {
46+
Class<?> owner = type("OpenCliOptionSchema");
47+
return "value".equals(form)
48+
? call(owner, null, form, new Class<?>[]{String.class, boolean.class}, name, repeatable)
49+
: call(owner, null, form, new Class<?>[]{String.class}, name);
50+
}
51+
52+
private static Object value(Object definition, Object value) {
53+
return call(type("OpenCliOption"), null, "value",
54+
new Class<?>[]{type("OpenCliOptionSchema"), Object.class}, definition, value);
55+
}
56+
57+
private static Object flag(String form, Object definition) {
58+
return call(type("OpenCliOption"), null, form,
59+
new Class<?>[]{type("OpenCliOptionSchema")}, definition);
60+
}
61+
62+
private static void add(Object builder, Object occurrence) {
63+
call(builder.getClass(), builder, "option", new Class<?>[]{type("OpenCliOption")}, occurrence);
64+
}
65+
66+
private static List<String> run(OpenCliAdapterCommandRequest request) {
67+
OpenCliProperties p = new OpenCliProperties();
68+
String exe = System.getProperty("os.name").startsWith("Windows") ? "java.exe" : "java";
69+
p.setExecutable(new File(new File(System.getProperty("java.home"), "bin"), exe).getAbsolutePath());
70+
p.setLeadingArguments(new ArrayList<>(Arrays.asList("-cp",
71+
System.getProperty("surefire.test.class.path", System.getProperty("java.class.path")),
72+
ContractProbe.class.getName())));
73+
p.setCommandTimeoutMillis(10000L);
74+
OpenCliResult result = new OpenCliAdapterChannel(new OpenCliExecutor(p), "demo").invoke(request);
75+
assertTrue(result.isSuccess());
76+
String[] lines = result.getStdout().split("\r?\n");
77+
List<String> actual = new ArrayList<>();
78+
for (int i = 1; i < lines.length; i++) {
79+
assertTrue(lines[i].startsWith("arg:"));
80+
actual.add(new String(Base64.getDecoder().decode(lines[i].substring(4)), StandardCharsets.UTF_8));
81+
}
82+
assertEquals("argc:" + actual.size(), lines[0]);
83+
return actual;
84+
}
85+
86+
@Test
87+
void orderedRepeatedValuesAndExplicitFalseReachChild() {
88+
Object tags = schema("value", "--tag", true);
89+
Object enabled = schema("value", "--enabled", false);
90+
OpenCliAdapterCommandRequest.OpenCliAdapterCommandRequestBuilder b = OpenCliAdapterCommandRequest.builder().subcommand("echo");
91+
add(b, value(tags, " A "));
92+
add(b, value(enabled, false));
93+
add(b, value(tags, ""));
94+
assertEquals(Arrays.asList("demo", "echo", "--tag", " A ", "--enabled", "false", "--tag", ""), run(b.build()));
95+
}
96+
97+
@Test
98+
void explicitNegationIsDifferentFromAbsenceAndPresence() {
99+
Object cache = schema("negatableFlag", "--cache", false);
100+
OpenCliAdapterCommandRequest.OpenCliAdapterCommandRequestBuilder b = OpenCliAdapterCommandRequest.builder().subcommand("echo");
101+
add(b, flag("negated", cache));
102+
assertEquals(Arrays.asList("demo", "echo", "--no-cache"), run(b.build()));
103+
OpenCliAdapterCommandRequest.OpenCliAdapterCommandRequestBuilder present = OpenCliAdapterCommandRequest.builder().subcommand("echo");
104+
add(present, flag("present", cache));
105+
assertEquals(Arrays.asList("demo", "echo", "--cache"), run(present.build()));
106+
assertEquals(Arrays.asList("demo", "echo"), run(OpenCliAdapterCommandRequest.builder().subcommand("echo").build()));
107+
}
108+
109+
@Test
110+
void flagSchemaCannotSilentlyConsumeAValue() {
111+
Object verbose = schema("flag", "--verbose", false);
112+
assertThrows(IllegalArgumentException.class, () -> value(verbose, false));
113+
assertThrows(IllegalArgumentException.class, () -> flag("negated", verbose));
114+
}
115+
116+
@Test
117+
void nonrepeatableOptionCannotAppearTwice() {
118+
Object once = schema("value", "--limit", false);
119+
OpenCliAdapterCommandRequest.OpenCliAdapterCommandRequestBuilder b = OpenCliAdapterCommandRequest.builder().subcommand("echo");
120+
add(b, value(once, 1));
121+
add(b, value(once, 2));
122+
assertThrows(IllegalArgumentException.class, () -> b.build().toSubcommandAndArgs());
123+
}
124+
125+
@Test
126+
void legacyAndOrderedOptionsCannotCollide() {
127+
Object limit = schema("value", "--limit", true);
128+
OpenCliAdapterCommandRequest.OpenCliAdapterCommandRequestBuilder b = OpenCliAdapterCommandRequest.builder().subcommand("echo")
129+
.options(Collections.<String, Object>singletonMap("limit", "1"));
130+
add(b, value(limit, "2"));
131+
assertThrows(IllegalArgumentException.class, () -> b.build().toSubcommandAndArgs());
132+
}
133+
134+
@Test
135+
void mutableOccurrenceValueIsCapturedWhenCreated() {
136+
StringBuilder text = new StringBuilder(" before ");
137+
Object occurrence = value(schema("value", "--text", false), text);
138+
text.append("after");
139+
OpenCliAdapterCommandRequest.OpenCliAdapterCommandRequestBuilder b = OpenCliAdapterCommandRequest.builder().subcommand("echo");
140+
add(b, occurrence);
141+
assertEquals(Arrays.asList("demo", "echo", "--text", " before "), run(b.build()));
142+
}
143+
144+
@Test
145+
void malformedSchemaIdentifiersAreRejected() {
146+
Class<?> owner = type("OpenCliOptionSchema");
147+
assertThrows(IllegalArgumentException.class,
148+
() -> call(owner, null, "flag", new Class<?>[]{String.class}, "--x y"));
149+
assertThrows(IllegalArgumentException.class,
150+
() -> call(owner, null, "flag", new Class<?>[]{String.class}, ""));
151+
}
152+
153+
@Test
154+
void legacyMapIsCapturedRatherThanAliased() {
155+
Map<String, Object> options = new LinkedHashMap<>();
156+
options.put("text", " before ");
157+
OpenCliAdapterCommandRequest request = OpenCliAdapterCommandRequest.builder().subcommand("echo").options(options).build();
158+
options.put("text", "after");
159+
assertEquals(Arrays.asList("demo", "echo", "--text", " before "), run(request));
160+
}
161+
162+
@Test
163+
void mutableLegacyValueCannotChangeAnExistingRequest() {
164+
StringBuilder text = new StringBuilder(" before ");
165+
OpenCliAdapterCommandRequest request = OpenCliAdapterCommandRequest.builder().subcommand("echo")
166+
.options(Collections.<String, Object>singletonMap("text", text)).build();
167+
text.append("after");
168+
assertEquals(Arrays.asList("demo", "echo", "--text", " before "), run(request));
169+
}
170+
171+
@Test
172+
void reusingBuilderDoesNotMutatePreviousRequest() {
173+
Object tags = schema("value", "--tag", true);
174+
OpenCliAdapterCommandRequest.OpenCliAdapterCommandRequestBuilder b = OpenCliAdapterCommandRequest.builder().subcommand("echo");
175+
add(b, value(tags, "A"));
176+
OpenCliAdapterCommandRequest first = b.build();
177+
add(b, value(tags, "B"));
178+
assertEquals(Arrays.asList("demo", "echo", "--tag", "A"), run(first));
179+
}
180+
}

0 commit comments

Comments
 (0)