Skip to content

Commit

Permalink
Add selector compliance tests
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewFossAWS committed Mar 2, 2023
1 parent 5e2b310 commit 0a2b3f0
Show file tree
Hide file tree
Showing 13 changed files with 868 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package software.amazon.smithy.model.selector;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;

public class SelectorRunnerTest {

private static final String SMITHY_NAMESPACE = "smithy.api";

@ParameterizedTest(name = "{0}")
@MethodSource("source")
public void selectorTest(Path filename) {
Model model = Model.assembler().addImport(filename).assemble().unwrap();
List<ObjectNode> tests = findTestCases(model);

for (ObjectNode test : tests) {
Selector selector = Selector.parse(test.expectStringMember("selector").getValue());
boolean skipPrelude = test.getBooleanMemberOrDefault("skipPreludeShapes", false);

Set<ShapeId> expectedMatches = new HashSet<>(new HashSet<>(test.expectArrayMember("matches")
.getElementsAs(n -> n.expectStringNode("Each element of matches must be an ID").expectShapeId())))
.stream()
.filter(shapeId -> {
String namespace = shapeId.getNamespace();
return !namespace.contains(SMITHY_NAMESPACE) || (!skipPrelude && namespace.contains(SMITHY_NAMESPACE));
})
.collect(Collectors.toSet());

Set<ShapeId> actualMatches = selector.shapes(model)
.map(Shape::getId)
.filter(shapeId -> {
String namespace = shapeId.getNamespace();
return !namespace.contains(SMITHY_NAMESPACE) || (!skipPrelude && namespace.contains(SMITHY_NAMESPACE));
})
.collect(Collectors.toSet());

if (!expectedMatches.equals(actualMatches)) {
failTest(filename, test, expectedMatches, actualMatches);
}
}
}

private List<ObjectNode> findTestCases(Model model) {
return model.getMetadataProperty("selectorTests")
.orElseThrow(() -> new IllegalArgumentException("Missing selectorTests metadata key"))
.expectArrayNode("selectorTests must be an array")
.getElementsAs(ObjectNode.class);
}

private void failTest(Path filename, ObjectNode test, Set<ShapeId> expectedMatches, Set<ShapeId> actualMatches) {
String selector = test.expectStringMember("selector").getValue();
Set<ShapeId> missing = new TreeSet<>(expectedMatches);
missing.removeAll(actualMatches);

Set<ShapeId> extra = new TreeSet<>(actualMatches);
extra.removeAll(expectedMatches);

StringBuilder error = new StringBuilder("Selector ")
.append(selector)
.append(" test case failed.\n");

if (!missing.isEmpty()) {
error.append("The following shapes were not matched: ").append(missing).append(".\n");
}

if (!extra.isEmpty()) {
error.append("The following shapes were matched unexpectedly: ").append(extra).append(".\n");
}

test.getStringMember("documentation")
.ifPresent(docs -> error.append('(').append(docs.getValue()).append(")"));

Assertions.fail(error.toString());
}

public static List<Path> source() throws Exception {
List<Path> paths = new ArrayList<>();
try (Stream<Path> files = Files.walk(Paths.get(SelectorRunnerTest.class.getResource("cases").toURI()))) {
files
.filter(Files::isRegularFile)
.filter(file -> {
String filename = file.toString();
return filename.endsWith(".smithy") || filename.endsWith(".json");
})
.forEach(paths::add);
} catch (IOException e) {
throw new RuntimeException("Error loading models for selector runner", e);
}

return paths;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
$version: "2.0"

metadata selectorTests = [
{
selector: "[trait|enum]"
matches: [
smithy.example#SimpleEnum
smithy.example#EnumWithTags
]
},
{
selector: "[trait|enum|(values)|tags|(values)]"
matches: [
smithy.example#EnumWithTags
]
}
]

namespace smithy.example

@deprecated
string NoMatch

@enum([
{name: "foo", value: "foo"}
{name: "baz", value: "baz"}
])
string SimpleEnum

@enum([
{name: "foo", value: "foo", tags: ["a"]}
{name: "baz", value: "baz"}
{name: "spam", value: "spam", tags: []}
])
string EnumWithTags
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
$version: "2.0"

metadata selectorTests = [
{
selector: "list:test(> member > string)"
skipPreludeShapes: true
matches: [
smithy.example#StringList
]
}
{
selector: ":is(string, number)"
skipPreludeShapes: true
matches: [
smithy.example#SimpleString
smithy.example#SimpleInteger
]
}
{
selector: "member > :is(string, number)"
skipPreludeShapes: true
matches: [
smithy.example#SimpleInteger,
smithy.example#SimpleString
]
}
{
selector: ":is(list > member > string, map > member > number)"
skipPreludeShapes: true
matches: [
smithy.example#SimpleString
smithy.example#SimpleInteger
]
}
{
selector: ":not(string) :not(number) :not(structure) :not(service) :not(operation) :not(resource)"
skipPreludeShapes: true
matches: [
smithy.example#IntegerList
smithy.example#IntegerList$member
smithy.example#SimpleMap
smithy.example#SimpleMap$key
smithy.example#SimpleMap$value
smithy.example#StringList
smithy.example#StringList$member
]
}
{
selector: "list :not(> member > string)"
skipPreludeShapes: true
matches: [
smithy.example#IntegerList
]
}
{
selector: ":topdown([trait|smithy.example#dataPlane])"
matches: [
smithy.example#OperationA
smithy.example#Resource
smithy.example#ServiceA
]
}
{
selector: ":topdown([trait|smithy.example#dataPlane], [trait|smithy.example#controlPlane])"
matches: [
smithy.example#OperationA
smithy.example#ServiceA
]
}
]

namespace smithy.example

string SimpleString

integer SimpleInteger

list StringList {
member: SimpleString
}

list IntegerList {
member: SimpleInteger
}

map SimpleMap {
key: String,
value: SimpleInteger
}

@trait(selector: ":test(service, resource, operation)")
structure dataPlane {}

@trait(selector: ":test(service, resource, operation)")
structure controlPlane {}

@dataPlane
service ServiceA {
version: "2019-06-17",
operations: [OperationA]
resources: [Resource]
}

@controlPlane
service ServiceB {
version: "2019-06-17",
operations: [OperationB]
resources: [Resource]
}

@dataPlane
operation OperationA {}

@controlPlane
operation OperationB {}

@controlPlane
resource Resource {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
$version: "2.0"

metadata selectorTests = [
{
selector: "[id = smithy.example#Exception]"
matches: [
smithy.example#Exception
]
}
{
selector: "[id|namespace = 'smithy.example']"
matches: [
smithy.example#Exception
smithy.example#Exception$message
]
}
{
selector: "[id|(length) <= 24]"
skipPreludeShapes: true
matches: [
smithy.example#Exception
]
}
{
selector: "[id|(length) > 24]"
skipPreludeShapes: true
matches: [
smithy.example#Exception$message
]
}
]

namespace smithy.example

structure Exception {
message: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
$version: "2.0"

metadata selectorTests = [
{
selector: "service ~> operation"
matches: [
smithy.example#OperationA
smithy.example#OperationB
]
}
{
selector: "service[trait|title] ~> operation:not([trait|http])"
matches: [
smithy.example#OperationB
]
}
{
selector: "string :test(< member < list)"
skipPreludeShapes: true
matches: [
smithy.example#String
]
}
{
selector: ":not([trait|trait]) :not(< *)"
skipPreludeShapes: true
matches: [
smithy.example#List
smithy.example#Structure
]
}
{
selector: "[trait|streaming]
:test(<)
:not(< member < structure <-[input, output]- operation)"
matches: [
smithy.example#StreamBlob
]
}
{
selector: "[trait|trait] :not(<-[trait]-)"
skipPreludeShapes: true
matches: [
smithy.example#Regex
]
}
]

namespace smithy.example

@title("Service")
service Service {
version: "2019-06-17",
operations: [OperationA, OperationB]
}

// Inherits the authorizer of ServiceA
@http(method: "GET", uri: "/operationA")
operation OperationA {}

operation OperationB {
input: Input
}

string String

list List {
member: String
}

structure Input {
@required
content: StreamFile
}

structure Structure {
@required
content: StreamBlob
}

@streaming
blob StreamBlob

@streaming
blob StreamFile

@trait
string Regex
Loading

0 comments on commit 0a2b3f0

Please sign in to comment.