blob: 964105a6129ab1390b1ac95ab2f76944a7991cf8 [file] [log] [blame]
// Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors
// Licensed under the MIT License:
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// This is a fuzz test which randomly generates a schema for a struct one change at a time.
// Each modification is known a priori to be compatible or incompatible. The type is compiled
// before and after the change and both versions are loaded into a SchemaLoader with the
// expectation that this will succeed if they are compatible and fail if they are not. If
// the types are expected to be compatible, the test also constructs an instance of the old
// type and reads it as the new type, and vice versa.
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#include <capnp/compiler/grammar.capnp.h>
#include <capnp/schema-loader.h>
#include <capnp/message.h>
#include <capnp/pretty-print.h>
#include "compiler.h"
#include <kj/function.h>
#include <kj/debug.h>
#include <stdlib.h>
#include <time.h>
#include <kj/main.h>
#include <kj/io.h>
#include <kj/miniposix.h>
namespace capnp {
namespace compiler {
namespace {
static const kj::StringPtr RFC3092[] = {"foo", "bar", "baz", "qux"};
template <typename T, size_t size>
T& chooseFrom(T (&arr)[size]) {
return arr[rand() % size];
}
template <typename T>
auto chooseFrom(T arr) -> decltype(arr[0]) {
return arr[rand() % arr.size()];
}
static Declaration::Builder addNested(Declaration::Builder parent) {
auto oldNestedOrphan = parent.disownNestedDecls();
auto oldNested = oldNestedOrphan.get();
auto newNested = parent.initNestedDecls(oldNested.size() + 1);
uint index = rand() % (oldNested.size() + 1);
for (uint i = 0; i < index; i++) {
newNested.setWithCaveats(i, oldNested[i]);
}
for (uint i = index + 1; i < newNested.size(); i++) {
newNested.setWithCaveats(i, oldNested[i - 1]);
}
return newNested[index];
}
struct TypeOption {
kj::StringPtr name;
kj::ConstFunction<void(Expression::Builder)> makeValue;
};
static const TypeOption TYPE_OPTIONS[] = {
{ "Int32",
[](Expression::Builder builder) {
builder.setPositiveInt(rand() % (1 << 24));
}},
{ "Float64",
[](Expression::Builder builder) {
builder.setPositiveInt(rand());
}},
{ "Int8",
[](Expression::Builder builder) {
builder.setPositiveInt(rand() % 128);
}},
{ "UInt16",
[](Expression::Builder builder) {
builder.setPositiveInt(rand() % (1 << 16));
}},
{ "Bool",
[](Expression::Builder builder) {
builder.initRelativeName().setValue("true");
}},
{ "Text",
[](Expression::Builder builder) {
builder.setString(chooseFrom(RFC3092));
}},
{ "StructType",
[](Expression::Builder builder) {
auto assignment = builder.initTuple(1)[0];
assignment.initNamed().setValue("i");
assignment.initValue().setPositiveInt(rand() % (1 << 24));
}},
{ "EnumType",
[](Expression::Builder builder) {
builder.initRelativeName().setValue(chooseFrom(RFC3092));
}},
};
void setDeclName(Expression::Builder decl, kj::StringPtr name) {
decl.initRelativeName().setValue(name);
}
static kj::ConstFunction<void(Expression::Builder)> randomizeType(Expression::Builder type) {
auto option = &chooseFrom(TYPE_OPTIONS);
if (rand() % 4 == 0) {
auto app = type.initApplication();
setDeclName(app.initFunction(), "List");
setDeclName(app.initParams(1)[0].initValue(), option->name);
return [option](Expression::Builder builder) {
for (auto element: builder.initList(rand() % 4 + 1)) {
option->makeValue(element);
}
};
} else {
setDeclName(type, option->name);
return option->makeValue.reference();
}
}
enum ChangeKind {
NO_CHANGE,
COMPATIBLE,
INCOMPATIBLE,
SUBTLY_COMPATIBLE
// The change is technically compatible on the wire, but SchemaLoader will complain.
};
struct ChangeInfo {
ChangeKind kind;
kj::String description;
ChangeInfo(): kind(NO_CHANGE) {}
ChangeInfo(ChangeKind kind, kj::StringPtr description)
: kind(kind), description(kj::str(description)) {}
ChangeInfo(ChangeKind kind, kj::String&& description)
: kind(kind), description(kj::mv(description)) {}
};
extern kj::ArrayPtr<kj::ConstFunction<ChangeInfo(Declaration::Builder, uint&, bool)>> STRUCT_MODS;
extern kj::ArrayPtr<kj::ConstFunction<ChangeInfo(Declaration::Builder, uint&, bool)>> FIELD_MODS;
// ================================================================================
static ChangeInfo declChangeName(Declaration::Builder decl, uint& nextOrdinal,
bool scopeHasUnion) {
auto name = decl.getName();
if (name.getValue().size() == 0) {
// Naming an unnamed union.
name.setValue(kj::str("unUnnamed", nextOrdinal));
return { SUBTLY_COMPATIBLE, "Assign name to unnamed union." };
} else {
name.setValue(kj::str(name.getValue(), "Xx"));
return { COMPATIBLE, "Rename declaration." };
}
}
static ChangeInfo structAddField(Declaration::Builder decl, uint& nextOrdinal, bool scopeHasUnion) {
auto fieldDecl = addNested(decl);
uint ordinal = nextOrdinal++;
fieldDecl.initName().setValue(kj::str("f", ordinal));
fieldDecl.getId().initOrdinal().setValue(ordinal);
auto field = fieldDecl.initField();
auto makeValue = randomizeType(field.initType());
if (rand() % 4 == 0) {
makeValue(field.getDefaultValue().initValue());
} else {
field.getDefaultValue().setNone();
}
return { COMPATIBLE, "Add field." };
}
static ChangeInfo structModifyField(Declaration::Builder decl, uint& nextOrdinal,
bool scopeHasUnion) {
auto nested = decl.getNestedDecls();
if (nested.size() == 0) {
return { NO_CHANGE, "Modify field, but there were none to modify." };
}
auto field = chooseFrom(nested);
bool hasUnion = false;
if (decl.isUnion()) {
hasUnion = true;
} else {
for (auto n: nested) {
if (n.isUnion() && n.getName().getValue().size() == 0) {
hasUnion = true;
break;
}
}
}
if (field.isGroup() || field.isUnion()) {
return chooseFrom(STRUCT_MODS)(field, nextOrdinal, hasUnion);
} else {
return chooseFrom(FIELD_MODS)(field, nextOrdinal, hasUnion);
}
}
static ChangeInfo structGroupifyFields(
Declaration::Builder decl, uint& nextOrdinal, bool scopeHasUnion) {
// Place a random subset of the fields into a group.
if (decl.isUnion()) {
return { NO_CHANGE,
"Randomly make a group out of some fields, but I can't do this to a union." };
}
kj::Vector<Orphan<Declaration>> groupified;
kj::Vector<Orphan<Declaration>> notGroupified;
auto orphanage = Orphanage::getForMessageContaining(decl);
for (auto nested: decl.getNestedDecls()) {
if (rand() % 2) {
groupified.add(orphanage.newOrphanCopy(nested.asReader()));
} else {
notGroupified.add(orphanage.newOrphanCopy(nested.asReader()));
}
}
if (groupified.size() == 0) {
return { NO_CHANGE,
"Randomly make a group out of some fields, but I ended up choosing none of them." };
}
auto newNested = decl.initNestedDecls(notGroupified.size() + 1);
uint index = rand() % (notGroupified.size() + 1);
for (uint i = 0; i < index; i++) {
newNested.adoptWithCaveats(i, kj::mv(notGroupified[i]));
}
for (uint i = index; i < notGroupified.size(); i++) {
newNested.adoptWithCaveats(i + 1, kj::mv(notGroupified[i]));
}
auto newGroup = newNested[index];
auto groupNested = newGroup.initNestedDecls(groupified.size());
for (uint i = 0; i < groupified.size(); i++) {
groupNested.adoptWithCaveats(i, kj::mv(groupified[i]));
}
newGroup.initName().setValue(kj::str("g", nextOrdinal, "x", groupNested[0].getName().getValue()));
newGroup.getId().setUnspecified();
newGroup.setGroup();
return { SUBTLY_COMPATIBLE, "Randomly group some set of existing fields." };
}
static ChangeInfo structPermuteFields(
Declaration::Builder decl, uint& nextOrdinal, bool scopeHasUnion) {
if (decl.getNestedDecls().size() == 0) {
return { NO_CHANGE, "Permute field code order, but there were none." };
}
auto oldOrphan = decl.disownNestedDecls();
auto old = oldOrphan.get();
KJ_STACK_ARRAY(uint, mapping, old.size(), 16, 64);
for (uint i = 0; i < mapping.size(); i++) {
mapping[i] = i;
}
for (uint i = mapping.size() - 1; i > 0; i--) {
uint j = rand() % i;
uint temp = mapping[j];
mapping[j] = mapping[i];
mapping[i] = temp;
}
auto newNested = decl.initNestedDecls(old.size());
for (uint i = 0; i < old.size(); i++) {
newNested.setWithCaveats(i, old[mapping[i]]);
}
return { COMPATIBLE, "Permute field code order." };
}
kj::ConstFunction<ChangeInfo(Declaration::Builder, uint&, bool)> STRUCT_MODS_[] = {
structAddField,
structAddField,
structAddField,
structModifyField,
structModifyField,
structModifyField,
structPermuteFields,
declChangeName,
structGroupifyFields // do more rarely because it creates slowness
};
kj::ArrayPtr<kj::ConstFunction<ChangeInfo(Declaration::Builder, uint&, bool)>>
STRUCT_MODS = STRUCT_MODS_;
// ================================================================================
static ChangeInfo fieldUpgradeList(Declaration::Builder decl, uint& nextOrdinal,
bool scopeHasUnion) {
// Upgrades a non-struct list to a struct list.
auto field = decl.getField();
if (field.getDefaultValue().isValue()) {
return { NO_CHANGE, "Upgrade primitive list to struct list, but it had a default value." };
}
auto type = field.getType();
if (!type.isApplication()) {
return { NO_CHANGE, "Upgrade primitive list to struct list, but it wasn't a list." };
}
auto typeParams = type.getApplication().getParams();
auto elementType = typeParams[0].getValue();
auto relativeName = elementType.getRelativeName();
auto nameText = relativeName.asReader().getValue();
if (nameText == "StructType" || nameText.endsWith("Struct")) {
return { NO_CHANGE, "Upgrade primitive list to struct list, but it was already a struct list."};
}
if (nameText == "Bool") {
return { NO_CHANGE, "Upgrade primitive list to struct list, but bool lists can't be upgraded."};
}
relativeName.setValue(kj::str(nameText, "Struct"));
return { COMPATIBLE, "Upgrade primitive list to struct list" };
}
static ChangeInfo fieldExpandGroup(Declaration::Builder decl, uint& nextOrdinal,
bool scopeHasUnion) {
Declaration::Builder newDecl = decl.initNestedDecls(1)[0];
newDecl.adoptName(decl.disownName());
newDecl.getId().adoptOrdinal(decl.getId().disownOrdinal());
auto field = decl.getField();
auto newField = newDecl.initField();
newField.adoptType(field.disownType());
if (field.getDefaultValue().isValue()) {
newField.getDefaultValue().adoptValue(field.getDefaultValue().disownValue());
} else {
newField.getDefaultValue().setNone();
}
decl.initName().setValue(kj::str("g", newDecl.getName().getValue()));
decl.getId().setUnspecified();
if (rand() % 2 == 0) {
decl.setGroup();
} else {
decl.setUnion();
if (!scopeHasUnion && rand() % 2 == 0) {
// Make it an unnamed union.
decl.getName().setValue("");
}
structAddField(decl, nextOrdinal, scopeHasUnion); // union must have two members
}
return { COMPATIBLE, "Wrap a field in a singleton group." };
}
static ChangeInfo fieldChangeType(Declaration::Builder decl, uint& nextOrdinal,
bool scopeHasUnion) {
auto field = decl.getField();
if (field.getDefaultValue().isNone()) {
// Change the type.
auto type = field.getType();
while (type.isApplication()) {
// Either change the list parameter, or revert to a non-list.
if (rand() % 2) {
type = type.getApplication().getParams()[0].getValue();
} else {
type.initRelativeName();
}
}
auto typeName = type.getRelativeName();
if (typeName.asReader().getValue().startsWith("Text")) {
typeName.setValue("Int32");
} else {
typeName.setValue("Text");
}
return { INCOMPATIBLE, "Change the type of a field." };
} else {
// Change the default value.
auto dval = field.getDefaultValue().getValue();
switch (dval.which()) {
case Expression::UNKNOWN: KJ_FAIL_ASSERT("unknown value expression?");
case Expression::POSITIVE_INT: dval.setPositiveInt(dval.getPositiveInt() ^ 1); break;
case Expression::NEGATIVE_INT: dval.setNegativeInt(dval.getNegativeInt() ^ 1); break;
case Expression::FLOAT: dval.setFloat(-dval.getFloat()); break;
case Expression::RELATIVE_NAME: {
auto name = dval.getRelativeName();
auto nameText = name.asReader().getValue();
if (nameText == "true") {
name.setValue("false");
} else if (nameText == "false") {
name.setValue("true");
} else if (nameText == "foo") {
name.setValue("bar");
} else {
name.setValue("foo");
}
break;
}
case Expression::STRING:
case Expression::BINARY:
case Expression::LIST:
case Expression::TUPLE:
return { NO_CHANGE, "Change the default value of a field, but it's a pointer field." };
case Expression::ABSOLUTE_NAME:
case Expression::IMPORT:
case Expression::EMBED:
case Expression::APPLICATION:
case Expression::MEMBER:
KJ_FAIL_ASSERT("Unexpected expression type.");
}
return { INCOMPATIBLE, "Change the default value of a pritimive field." };
}
}
kj::ConstFunction<ChangeInfo(Declaration::Builder, uint&, bool)> FIELD_MODS_[] = {
fieldUpgradeList,
fieldExpandGroup,
fieldChangeType,
declChangeName
};
kj::ArrayPtr<kj::ConstFunction<ChangeInfo(Declaration::Builder, uint&, bool)>>
FIELD_MODS = FIELD_MODS_;
// ================================================================================
uint getOrdinal(StructSchema::Field field) {
auto proto = field.getProto();
if (proto.getOrdinal().isExplicit()) {
return proto.getOrdinal().getExplicit();
}
KJ_ASSERT(proto.isGroup());
auto group = field.getType().asStruct();
return getOrdinal(group.getFields()[0]);
}
Orphan<DynamicStruct> makeExampleStruct(
Orphanage orphanage, StructSchema schema, uint sharedOrdinalCount);
void checkExampleStruct(DynamicStruct::Reader reader, uint sharedOrdinalCount);
Orphan<DynamicValue> makeExampleValue(
Orphanage orphanage, uint ordinal, Type type, uint sharedOrdinalCount) {
switch (type.which()) {
case schema::Type::INT32: return ordinal * 47327;
case schema::Type::FLOAT64: return ordinal * 313.25;
case schema::Type::INT8: return int(ordinal % 256) - 128;
case schema::Type::UINT16: return ordinal * 13;
case schema::Type::BOOL: return ordinal % 2 == 0;
case schema::Type::TEXT: return orphanage.newOrphanCopy(Text::Reader(kj::str(ordinal)));
case schema::Type::STRUCT: {
auto structType = type.asStruct();
auto result = orphanage.newOrphan(structType);
auto builder = result.get();
KJ_IF_MAYBE(fieldI, structType.findFieldByName("i")) {
// Type is "StructType"
builder.set(*fieldI, ordinal);
} else {
// Type is "Int32Struct" or the like.
auto field = structType.getFieldByName("f0");
builder.adopt(field, makeExampleValue(
orphanage, ordinal, field.getType(), sharedOrdinalCount));
}
return kj::mv(result);
}
case schema::Type::ENUM: {
auto enumerants = type.asEnum().getEnumerants();
return DynamicEnum(enumerants[ordinal %enumerants.size()]);
}
case schema::Type::LIST: {
auto listType = type.asList();
auto elementType = listType.getElementType();
auto result = orphanage.newOrphan(listType, 1);
result.get().adopt(0, makeExampleValue(
orphanage, ordinal, elementType, sharedOrdinalCount));
return kj::mv(result);
}
default:
KJ_FAIL_ASSERT("You added a new possible field type!");
}
}
void checkExampleValue(DynamicValue::Reader value, uint ordinal, schema::Type::Reader type,
uint sharedOrdinalCount) {
switch (type.which()) {
case schema::Type::INT32: KJ_ASSERT(value.as<int32_t>() == ordinal * 47327); break;
case schema::Type::FLOAT64: KJ_ASSERT(value.as<double>() == ordinal * 313.25); break;
case schema::Type::INT8: KJ_ASSERT(value.as<int8_t>() == int(ordinal % 256) - 128); break;
case schema::Type::UINT16: KJ_ASSERT(value.as<uint16_t>() == ordinal * 13); break;
case schema::Type::BOOL: KJ_ASSERT(value.as<bool>() == (ordinal % 2 == 0)); break;
case schema::Type::TEXT: KJ_ASSERT(value.as<Text>() == kj::str(ordinal)); break;
case schema::Type::STRUCT: {
auto structValue = value.as<DynamicStruct>();
auto structType = structValue.getSchema();
KJ_IF_MAYBE(fieldI, structType.findFieldByName("i")) {
// Type is "StructType"
KJ_ASSERT(structValue.get(*fieldI).as<uint32_t>() == ordinal);
} else {
// Type is "Int32Struct" or the like.
auto field = structType.getFieldByName("f0");
checkExampleValue(structValue.get(field), ordinal,
field.getProto().getSlot().getType(), sharedOrdinalCount);
}
break;
}
case schema::Type::ENUM: {
auto enumerant = KJ_ASSERT_NONNULL(value.as<DynamicEnum>().getEnumerant());
KJ_ASSERT(enumerant.getIndex() ==
ordinal % enumerant.getContainingEnum().getEnumerants().size());
break;
}
case schema::Type::LIST:
checkExampleValue(value.as<DynamicList>()[0], ordinal, type.getList().getElementType(),
sharedOrdinalCount);
break;
default:
KJ_FAIL_ASSERT("You added a new possible field type!");
}
}
void setExampleField(DynamicStruct::Builder builder, StructSchema::Field field,
uint sharedOrdinalCount) {
auto fieldProto = field.getProto();
switch (fieldProto.which()) {
case schema::Field::SLOT:
builder.adopt(field, makeExampleValue(
Orphanage::getForMessageContaining(builder),
getOrdinal(field), field.getType(), sharedOrdinalCount));
break;
case schema::Field::GROUP:
builder.adopt(field, makeExampleStruct(
Orphanage::getForMessageContaining(builder),
field.getType().asStruct(), sharedOrdinalCount));
break;
}
}
void checkExampleField(DynamicStruct::Reader reader, StructSchema::Field field,
uint sharedOrdinalCount) {
auto fieldProto = field.getProto();
switch (fieldProto.which()) {
case schema::Field::SLOT: {
uint ordinal = getOrdinal(field);
if (ordinal < sharedOrdinalCount) {
checkExampleValue(reader.get(field), ordinal,
fieldProto.getSlot().getType(), sharedOrdinalCount);
}
break;
}
case schema::Field::GROUP:
checkExampleStruct(reader.get(field).as<DynamicStruct>(), sharedOrdinalCount);
break;
}
}
Orphan<DynamicStruct> makeExampleStruct(
Orphanage orphanage, StructSchema schema, uint sharedOrdinalCount) {
// Initialize all fields of the struct via reflection, such that they can be verified using
// a different version of the struct. sharedOrdinalCount is the number of ordinals shared by
// the two versions. This is used mainly to avoid setting union members that the other version
// doesn't have.
Orphan<DynamicStruct> result = orphanage.newOrphan(schema);
auto builder = result.get();
for (auto field: schema.getNonUnionFields()) {
setExampleField(builder, field, sharedOrdinalCount);
}
auto unionFields = schema.getUnionFields();
// Pretend the union doesn't have any fields that aren't in the shared ordinal range.
uint range = unionFields.size();
while (range > 0 && getOrdinal(unionFields[range - 1]) >= sharedOrdinalCount) {
--range;
}
if (range > 0) {
auto field = unionFields[getOrdinal(unionFields[0]) % range];
setExampleField(builder, field, sharedOrdinalCount);
}
return kj::mv(result);
}
void checkExampleStruct(DynamicStruct::Reader reader, uint sharedOrdinalCount) {
auto schema = reader.getSchema();
for (auto field: schema.getNonUnionFields()) {
checkExampleField(reader, field, sharedOrdinalCount);
}
auto unionFields = schema.getUnionFields();
// Pretend the union doesn't have any fields that aren't in the shared ordinal range.
uint range = unionFields.size();
while (range > 0 && getOrdinal(unionFields[range - 1]) >= sharedOrdinalCount) {
--range;
}
if (range > 0) {
auto field = unionFields[getOrdinal(unionFields[0]) % range];
checkExampleField(reader, field, sharedOrdinalCount);
}
}
// ================================================================================
class ModuleImpl final: public Module {
public:
explicit ModuleImpl(ParsedFile::Reader content): content(content) {}
kj::StringPtr getSourceName() override { return "evolving-schema.capnp"; }
Orphan<ParsedFile> loadContent(Orphanage orphanage) override {
return orphanage.newOrphanCopy(content);
}
kj::Maybe<Module&> importRelative(kj::StringPtr importPath) override {
return nullptr;
}
kj::Maybe<kj::Array<const byte>> embedRelative(kj::StringPtr embedPath) override {
return nullptr;
}
void addError(uint32_t startByte, uint32_t endByte, kj::StringPtr message) override {
KJ_FAIL_ASSERT("Unexpected parse error.", startByte, endByte, message);
}
bool hadErrors() override {
return false;
}
private:
ParsedFile::Reader content;
};
static void loadStructAndGroups(const SchemaLoader& src, SchemaLoader& dst, uint64_t id) {
auto proto = src.get(id).getProto();
dst.load(proto);
for (auto field: proto.getStruct().getFields()) {
if (field.isGroup()) {
loadStructAndGroups(src, dst, field.getGroup().getTypeId());
}
}
}
static kj::Maybe<kj::Exception> loadFile(
ParsedFile::Reader file, SchemaLoader& loader, bool allNodes,
kj::Maybe<kj::Own<MallocMessageBuilder>>& messageBuilder,
uint sharedOrdinalCount) {
Compiler compiler;
ModuleImpl module(file);
KJ_ASSERT(compiler.add(module).getId() == 0x8123456789abcdefllu);
if (allNodes) {
// Eagerly compile and load the whole thing.
compiler.eagerlyCompile(0x8123456789abcdefllu, Compiler::ALL_RELATED_NODES);
KJ_IF_MAYBE(m, messageBuilder) {
// Build an example struct using the compiled schema.
m->get()->adoptRoot(makeExampleStruct(
m->get()->getOrphanage(), compiler.getLoader().get(0x823456789abcdef1llu).asStruct(),
sharedOrdinalCount));
}
for (auto schema: compiler.getLoader().getAllLoaded()) {
loader.load(schema.getProto());
}
return nullptr;
} else {
// Compile the file root so that the children are findable, then load the specific child
// we want.
compiler.eagerlyCompile(0x8123456789abcdefllu, Compiler::NODE);
KJ_IF_MAYBE(m, messageBuilder) {
// Check that the example struct matches the compiled schema.
auto root = m->get()->getRoot<DynamicStruct>(
compiler.getLoader().get(0x823456789abcdef1llu).asStruct()).asReader();
KJ_CONTEXT(root);
checkExampleStruct(root, sharedOrdinalCount);
}
return kj::runCatchingExceptions([&]() {
loadStructAndGroups(compiler.getLoader(), loader, 0x823456789abcdef1llu);
});
}
}
bool checkChange(ParsedFile::Reader file1, ParsedFile::Reader file2, ChangeKind changeKind,
uint sharedOrdinalCount) {
// Try loading file1 followed by file2 into the same SchemaLoader, expecting it to behave
// according to changeKind. Returns true if the files are both expected to be compatible and
// actually are -- the main loop uses this to decide which version to keep
kj::Maybe<kj::Own<MallocMessageBuilder>> exampleBuilder;
if (changeKind != INCOMPATIBLE) {
// For COMPATIBLE and SUBTLY_COMPATIBLE changes, build an example message with one schema
// and check it with the other.
exampleBuilder = kj::heap<MallocMessageBuilder>();
}
SchemaLoader loader;
loadFile(file1, loader, true, exampleBuilder, sharedOrdinalCount);
auto exception = loadFile(file2, loader, false, exampleBuilder, sharedOrdinalCount);
if (changeKind == COMPATIBLE) {
KJ_IF_MAYBE(e, exception) {
kj::getExceptionCallback().onFatalException(kj::mv(*e));
return false;
} else {
return true;
}
} else if (changeKind == INCOMPATIBLE) {
KJ_ASSERT(exception != nullptr, file1, file2);
return false;
} else {
KJ_ASSERT(changeKind == SUBTLY_COMPATIBLE);
// SchemaLoader is allowed to throw an exception in this case, but we ignore it.
return true;
}
}
void doTest() {
auto builder = kj::heap<MallocMessageBuilder>();
{
// Set up the basic file decl.
auto parsedFile = builder->initRoot<ParsedFile>();
auto file = parsedFile.initRoot();
file.setFile();
file.initId().initUid().setValue(0x8123456789abcdefllu);
auto decls = file.initNestedDecls(3 + kj::size(TYPE_OPTIONS));
{
auto decl = decls[0];
decl.initName().setValue("EvolvingStruct");
decl.initId().initUid().setValue(0x823456789abcdef1llu);
decl.setStruct();
}
{
auto decl = decls[1];
decl.initName().setValue("StructType");
decl.setStruct();
auto fieldDecl = decl.initNestedDecls(1)[0];
fieldDecl.initName().setValue("i");
fieldDecl.getId().initOrdinal().setValue(0);
auto field = fieldDecl.initField();
setDeclName(field.initType(), "UInt32");
}
{
auto decl = decls[2];
decl.initName().setValue("EnumType");
decl.setEnum();
auto enumerants = decl.initNestedDecls(4);
for (uint i = 0; i < kj::size(RFC3092); i++) {
auto enumerantDecl = enumerants[i];
enumerantDecl.initName().setValue(RFC3092[i]);
enumerantDecl.getId().initOrdinal().setValue(i);
enumerantDecl.setEnumerant();
}
}
// For each of TYPE_OPTIONS, declare a struct type that contains that type as its @0 field.
for (uint i = 0; i < kj::size(TYPE_OPTIONS); i++) {
auto decl = decls[3 + i];
auto& option = TYPE_OPTIONS[i];
decl.initName().setValue(kj::str(option.name, "Struct"));
decl.setStruct();
auto fieldDecl = decl.initNestedDecls(1)[0];
fieldDecl.initName().setValue("f0");
fieldDecl.getId().initOrdinal().setValue(0);
auto field = fieldDecl.initField();
setDeclName(field.initType(), option.name);
uint ordinal = 1;
for (auto j: kj::range(0, rand() % 4)) {
(void)j;
structAddField(decl, ordinal, false);
}
}
}
uint nextOrdinal = 0;
for (uint i = 0; i < 96; i++) {
uint oldOrdinalCount = nextOrdinal;
auto newBuilder = kj::heap<MallocMessageBuilder>();
newBuilder->setRoot(builder->getRoot<ParsedFile>().asReader());
auto parsedFile = newBuilder->getRoot<ParsedFile>();
Declaration::Builder decl = parsedFile.getRoot().getNestedDecls()[0];
// Apply a random modification.
ChangeInfo changeInfo;
while (changeInfo.kind == NO_CHANGE) {
auto& mod = chooseFrom(STRUCT_MODS);
changeInfo = mod(decl, nextOrdinal, false);
}
KJ_CONTEXT(changeInfo.description);
if (checkChange(builder->getRoot<ParsedFile>(), parsedFile, changeInfo.kind, oldOrdinalCount) &&
checkChange(parsedFile, builder->getRoot<ParsedFile>(), changeInfo.kind, oldOrdinalCount)) {
builder = kj::mv(newBuilder);
}
}
}
class EvolutionTestMain {
public:
explicit EvolutionTestMain(kj::ProcessContext& context)
: context(context) {}
kj::MainFunc getMain() {
return kj::MainBuilder(context, "(unknown version)",
"Integration test / fuzzer which randomly modifies schemas is backwards-compatible ways "
"and verifies that they do actually remain compatible.")
.addOptionWithArg({"seed"}, KJ_BIND_METHOD(*this, setSeed), "<num>",
"Set random number seed to <num>. By default, time() is used.")
.callAfterParsing(KJ_BIND_METHOD(*this, run))
.build();
}
kj::MainBuilder::Validity setSeed(kj::StringPtr value) {
char* end;
seed = strtol(value.cStr(), &end, 0);
if (value.size() == 0 || *end != '\0') {
return "not an integer";
} else {
return true;
}
}
kj::MainBuilder::Validity run() {
// https://github.com/capnproto/capnproto/issues/344 describes an obscure bug in the layout
// algorithm, the fix for which breaks backwards-compatibility for any schema triggering the
// bug. In order to avoid silently breaking protocols, we are temporarily throwing an exception
// in cases where this bug would have occurred, so that people can decide what to do.
// However, the evolution test can occasionally trigger the bug (depending on the random path
// it takes). Rather than try to avoid it, we disable the exception-throwing, because the bug
// is actually fixed, and the exception is only there to raise awareness of the compatibility
// concerns.
//
// On Linux, seed 1467142714 (for example) will trigger the exception (without this env var).
#if defined(__MINGW32__) || defined(_MSC_VER)
putenv("CAPNP_IGNORE_ISSUE_344=1");
#else
setenv("CAPNP_IGNORE_ISSUE_344", "1", true);
#endif
srand(seed);
{
kj::String text = kj::str(
"Randomly testing backwards-compatibility scenarios with seed: ", seed, "\n");
kj::FdOutputStream(STDOUT_FILENO).write(text.begin(), text.size());
}
KJ_CONTEXT(seed, "PLEASE REPORT THIS FAILURE AND INCLUDE THE SEED");
doTest();
return true;
}
private:
kj::ProcessContext& context;
uint seed = time(nullptr);
};
} // namespace
} // namespace compiler
} // namespace capnp
KJ_MAIN(capnp::compiler::EvolutionTestMain);