|
| 1 | +package com.ibm.cldk.schema; |
| 2 | + |
| 3 | +import static org.junit.jupiter.api.Assertions.assertFalse; |
| 4 | +import static org.junit.jupiter.api.Assertions.assertNotNull; |
| 5 | +import static org.junit.jupiter.api.Assertions.assertTrue; |
| 6 | + |
| 7 | +import com.fasterxml.jackson.databind.ObjectMapper; |
| 8 | +import com.networknt.schema.JsonSchema; |
| 9 | +import com.networknt.schema.JsonSchemaFactory; |
| 10 | +import com.networknt.schema.SpecVersion; |
| 11 | +import com.networknt.schema.ValidationMessage; |
| 12 | +import java.io.IOException; |
| 13 | +import java.io.InputStream; |
| 14 | +import java.util.Set; |
| 15 | +import java.util.stream.Collectors; |
| 16 | +import org.junit.jupiter.api.Test; |
| 17 | + |
| 18 | +/** |
| 19 | + * The oracle for the body-node ids L4 introduces, tested against hand-authored payloads rather than |
| 20 | + * through the producer — the sibling of {@link L3SchemaOracleTest}, one level up. |
| 21 | + * |
| 22 | + * <p>{@code SdgVertices} keys a call site's actuals off the site's own local id: |
| 23 | + * {@code <line:col>/actual_in:<i>} and {@code <line:col>/actual_out}. The formals it mints are |
| 24 | + * {@code @}-tagged and were already admitted; the actuals were not, so an {@code -a 4} payload |
| 25 | + * failed this repository's own conformance gate (#207). The pattern enumerates those two forms |
| 26 | + * instead of loosening its character class, so the rejections below are the point of the fix: a |
| 27 | + * pattern widened to accept anything with a slash in it would gate nothing. |
| 28 | + */ |
| 29 | +class L4SchemaOracleTest { |
| 30 | + |
| 31 | + private static final ObjectMapper MAPPER = new ObjectMapper(); |
| 32 | + |
| 33 | + /** A level-4 payload whose single callable carries one body node under the given local id. */ |
| 34 | + private static String payload(String localId, String kind) { |
| 35 | + String callable = "{" |
| 36 | + + "\"id\":\"can://java/myapp/Foo.java/Foo/m()\"," |
| 37 | + + "\"kind\":\"method\"," |
| 38 | + + "\"signature\":\"m()\"," |
| 39 | + + "\"body\":{\"" + localId + "\":{\"kind\":\"" + kind + "\"}}" |
| 40 | + + "}"; |
| 41 | + String type = "{" |
| 42 | + + "\"id\":\"can://java/myapp/Foo.java/Foo\"," |
| 43 | + + "\"kind\":\"class\"," |
| 44 | + + "\"callables\":{\"m()\":" + callable + "}}"; |
| 45 | + String module = "{" |
| 46 | + + "\"id\":\"can://java/myapp/Foo.java\"," |
| 47 | + + "\"kind\":\"module\"," |
| 48 | + + "\"source\":\"\"," |
| 49 | + + "\"types\":{\"Foo\":" + type + "}}"; |
| 50 | + return "{" |
| 51 | + + "\"schema_version\":\"2.0.0\"," |
| 52 | + + "\"language\":\"java\"," |
| 53 | + + "\"max_level\":4," |
| 54 | + + "\"application\":{" |
| 55 | + + "\"id\":\"can://java/myapp\"," |
| 56 | + + "\"kind\":\"application\"," |
| 57 | + + "\"symbol_table\":{\"Foo.java\":" + module + "}}}"; |
| 58 | + } |
| 59 | + |
| 60 | + private static Set<ValidationMessage> validate(String json) throws IOException { |
| 61 | + try (InputStream in = L4SchemaOracleTest.class.getResourceAsStream("/schema/analysis.v2.schema.json")) { |
| 62 | + assertNotNull(in, "the canonical v2 schema must be on the test classpath"); |
| 63 | + JsonSchema schema = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012).getSchema(in); |
| 64 | + return schema.validate(MAPPER.readTree(json)); |
| 65 | + } |
| 66 | + } |
| 67 | + |
| 68 | + private static void assertAccepted(String localId, String kind) throws IOException { |
| 69 | + Set<ValidationMessage> problems = validate(payload(localId, kind)); |
| 70 | + assertTrue(problems.isEmpty(), "expected the oracle to accept the local id " + localId + ", but got:\n " |
| 71 | + + problems.stream().map(ValidationMessage::getMessage).collect(Collectors.joining("\n "))); |
| 72 | + } |
| 73 | + |
| 74 | + private static void assertRejected(String localId, String because) throws IOException { |
| 75 | + assertFalse(validate(payload(localId, "statement")).isEmpty(), |
| 76 | + "the oracle must reject the local id " + localId + ": " + because); |
| 77 | + } |
| 78 | + |
| 79 | + @Test |
| 80 | + void anActualInVertexKeyedOffItsCallSiteValidates() throws IOException { |
| 81 | + assertAccepted("3:39/actual_in:0", "actual_in"); |
| 82 | + } |
| 83 | + |
| 84 | + @Test |
| 85 | + void anActualOutVertexKeyedOffItsCallSiteValidates() throws IOException { |
| 86 | + assertAccepted("3:39/actual_out", "actual_out"); |
| 87 | + } |
| 88 | + |
| 89 | + @Test |
| 90 | + void theFormalVerticesStillValidate() throws IOException { |
| 91 | + assertAccepted("@formal_in:0", "formal_in"); |
| 92 | + assertAccepted("@formal_out", "formal_out"); |
| 93 | + } |
| 94 | + |
| 95 | + @Test |
| 96 | + void aPlainPositionStillValidates() throws IOException { |
| 97 | + assertAccepted("3:39", "call"); |
| 98 | + } |
| 99 | + |
| 100 | + @Test |
| 101 | + void anActualInWithoutItsIndexIsRejected() throws IOException { |
| 102 | + // SdgVertices always numbers an actual_in by the parameter it feeds. |
| 103 | + assertRejected("3:39/actual_in", "actual_in carries the argument's ordinal"); |
| 104 | + } |
| 105 | + |
| 106 | + @Test |
| 107 | + void anActualOutCarryingAnIndexIsRejected() throws IOException { |
| 108 | + // A call site returns at most one value, so there is no ordinal to carry. |
| 109 | + assertRejected("3:39/actual_out:0", "actual_out is unindexed"); |
| 110 | + } |
| 111 | + |
| 112 | + @Test |
| 113 | + void anUnknownSuffixIsRejected() throws IOException { |
| 114 | + assertRejected("3:39/actual_side", "only the two forms SdgVertices mints are admitted"); |
| 115 | + } |
| 116 | + |
| 117 | + @Test |
| 118 | + void aCompoundIdWhoseBaseIsNotAPositionIsRejected() throws IOException { |
| 119 | + assertRejected("entry/actual_out", "the base of a compound id is the call site's line:col"); |
| 120 | + } |
| 121 | + |
| 122 | + @Test |
| 123 | + void aBareSlashSuffixIsRejected() throws IOException { |
| 124 | + assertRejected("3:39/", "a trailing slash names no vertex"); |
| 125 | + } |
| 126 | +} |
0 commit comments