Mojom frontend: Detect Ill-founded Types
This patch adds the functionality to detect user-defined types that are unserializable because every instance of the type would contain a cycle.
We use the ill-founded node detection algorithm from https://codereview.chromium.org/1908273003/ by having UserDefinedTypeBase implement the |Node| interface.
- In mojom_descriptor.go we add the method DetectIllFoundedTypes()
- We Invoke DetectIllFoundedTypes()in parse_driver.go after ComputeFinalData()
- We add the method NonAvoidableUserDefinedType() to the TypeRef interface to capture the notion that a type reference unavoidably entails a non-null pointer to a struct or union.
-We fixed up some ill-founded types that appeared in serialization_test.go because now we would be detecting them and failing before serialization.
- We enhanced the ill-founded graph detection library (in wellfounded_graphs.go) by introducing the ability for the client to add labels to the edges. The labels are opaque to the algorithm but we use them to maintain enough information to generate a useful error message. For example we use them to capture the name of the struct or union field corresponding to a graph edge.
- See illfounded_types_test.go to see the format of the error messages we will emit when an ill-founded type is detected.
BUG= fixes #694
R=azani@chromium.org
Review URL: https://codereview.chromium.org/1915413002 .
diff --git a/mojom/mojom_tool/integration_tests/illfounded_types_test.go b/mojom/mojom_tool/integration_tests/illfounded_types_test.go
new file mode 100644
index 0000000..859bc55
--- /dev/null
+++ b/mojom/mojom_tool/integration_tests/illfounded_types_test.go
@@ -0,0 +1,474 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+package parser
+
+import (
+ "fmt"
+ "mojom/mojom_tool/mojom"
+ "mojom/mojom_tool/parser"
+ "strings"
+ "testing"
+)
+
+// TestIllegalPatterns tests cases where DetectIllFoundedTypes() should return an error.
+func TestIllegalPatterns(t *testing.T) {
+ test := singleFileTest{}
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Struct field is non-nullable pointer to struct itself
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ struct Foo{
+ Foo x;
+ };`
+ test.addTestCase(contents, []string{
+ "The type Foo is unserializable: Every instance of this type would include a cycle.",
+ "Example cycle: Foo.x -> Foo",
+ "One way to break this cycle is to make the field \"x\" nullable.",
+ })
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Struct field is non-nullable, fixed-length array of non-nullable pointer to struct itself
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ struct Foo{
+ array<Foo, 1> x;
+ };`
+ test.addTestCase(contents, []string{
+ "The type Foo is unserializable: Every instance of this type would include a cycle.",
+ "Example cycle: Foo.x -> Foo",
+ "One way to break this cycle is to make the field \"x\" nullable.",
+ })
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Struct field is non-nullable, fixed-length array of
+ // non-nullable fixed-length array of non-nullable pointer to struct itself
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ struct Foo{
+ array<array<Foo, 1>, 1> x;
+ };`
+ test.addTestCase(contents, []string{
+ "The type Foo is unserializable: Every instance of this type would include a cycle.",
+ "Example cycle: Foo.x -> Foo",
+ "One way to break this cycle is to make the field \"x\" nullable.",
+ })
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Union field is non-nullable pointer to union itself
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ union Foo{
+ Foo x;
+ };`
+ test.addTestCase(contents, []string{
+ "The type Foo is unserializable: Every instance of this type would include a cycle.",
+ "Example cycle: Foo.x -> Foo",
+ "One way to break this cycle is to make the field \"x\" nullable.",
+ })
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Union field is non-nullable, fixed-length array of non-nullable pointer to union itself
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ union Foo{
+ array<Foo, 1> x;
+ };`
+ test.addTestCase(contents, []string{
+ "The type Foo is unserializable: Every instance of this type would include a cycle.",
+ "Example cycle: Foo.x -> Foo",
+ "One way to break this cycle is to make the field \"x\" nullable.",
+ })
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: A cycle of length 3.
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ struct MyStruct1 {
+ MyStruct2 x;
+ };
+
+ struct MyStruct2 {
+ MyStruct3 x;
+ };
+
+ struct MyStruct3 {
+ MyStruct1 x;
+ };`
+ test.addTestCase(contents, []string{
+ "The type MyStruct1 is unserializable: Every instance of this type would include a cycle.",
+ "Example cycle: MyStruct1.x -> MyStruct2.x -> MyStruct3.x -> MyStruct1",
+ "One way to break this cycle is to make the field \"x\" nullable.",
+ })
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Two structs and a union
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ struct MyStruct1 {
+ MyStruct2 x;
+ int32 y;
+ };
+
+ struct MyStruct2 {
+ MyUnion x;
+ int32 y;
+ };
+
+ union MyUnion {
+ MyStruct1 x;
+ MyStruct2 y;
+ };`
+ test.addTestCase(contents, []string{
+ "The type MyStruct1 is unserializable: Every instance of this type would include a cycle.",
+ "Example cycle: MyStruct1.x -> MyStruct2.x -> MyUnion.x -> MyStruct1",
+ "One way to break this cycle is to make the field \"x\" nullable.",
+ })
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Ill-foundedness not found until second pass of algorithm.
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ struct AEmptyStruct {
+ };
+
+ struct BMyUnion {
+ AEmptyStruct x;
+ CBadStruct y;
+ };
+
+ struct CBadStruct {
+ CBadStruct x;
+ };`
+
+ test.addTestCase(contents, []string{
+ "The type CBadStruct is unserializable: Every instance of this type would include a cycle.",
+ "Example cycle: CBadStruct.x -> CBadStruct",
+ "One way to break this cycle is to make the field \"x\" nullable.",
+ })
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Execute all of the test cases.
+ ////////////////////////////////////////////////////////////
+ for i, c := range test.cases {
+ // Parse anresolve the mojom input.
+ descriptor := mojom.NewMojomDescriptor()
+ specifiedName := ""
+ if c.importedFrom == nil {
+ specifiedName = c.fileName
+ }
+ parser := parser.MakeParser(c.fileName, specifiedName, c.mojomContents, descriptor, c.importedFrom)
+ parser.Parse()
+ if !parser.OK() {
+ t.Errorf("Parsing error for %s: %s", c.fileName, parser.GetError().Error())
+ continue
+ }
+ err := descriptor.Resolve()
+ if err != nil {
+ t.Errorf("Resolution error for %s: %s", c.fileName, err)
+ continue
+ }
+ if err = descriptor.ComputeFinalData(); err != nil {
+ t.Errorf("ComputeFinalData error for %s: %s", c.fileName, err)
+ continue
+ }
+
+ descriptor.SetTestingMode(true)
+ err = descriptor.DetectIllFoundedTypes()
+
+ if err == nil {
+ t.Errorf("No cycles were detected for test case %d.", i)
+ continue
+ }
+
+ got := err.Error()
+ for _, expected := range c.expectedErrors {
+ if !strings.Contains(got, expected) {
+ t.Errorf("%s:\n*****expected to contain:\n%s\n****actual\n%s", c.fileName, expected, got)
+ }
+ }
+
+ }
+}
+
+// TestLegalPatterns tests cases where DetectIllFoundedTypes() should not return an error.
+func TestLegalPatterns(t *testing.T) {
+ test := singleFileSuccessTest{}
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Struct field is nullable, pointer to struct itself
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ struct Foo{
+ Foo? x;
+ };`
+
+ testFunc := func(descriptor *mojom.MojomDescriptor) error {
+ return nil
+ }
+ test.addTestCase("", contents, testFunc)
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Struct field is non-nullable, variable-length array of non-nullable pointer to struct itself
+ ////////////////////////////////////////////////////////////
+
+ {
+ contents := `
+ struct Foo{
+ array<Foo> x;
+ };`
+
+ testFunc := func(descriptor *mojom.MojomDescriptor) error {
+ return nil
+ }
+ test.addTestCase("", contents, testFunc)
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Struct field is nullable, fixed-length array of non-nullable pointer to struct itself
+ ////////////////////////////////////////////////////////////
+
+ {
+ contents := `
+ struct Foo{
+ array<Foo, 1>? x;
+ };`
+
+ test.addTestCase("", contents, nil)
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Struct field is non-nullable, fixed-length array of nullable pointer to struct itself
+ ////////////////////////////////////////////////////////////
+
+ {
+ contents := `
+ struct Foo{
+ array<Foo?, 1> x;
+ };`
+
+ test.addTestCase("", contents, nil)
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Struct field is non-nullable, variable-length array of
+ // non-nullable fixed-length array of non-nullable pointer to struct itself
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ struct Foo{
+ array<array<Foo, 1>> x;
+ };`
+ test.addTestCase("", contents, nil)
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Struct field is non-nullable, fixed-length array of
+ // non-nullable variable-length array of non-nullable pointer to struct itself
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ struct Foo{
+ array<array<Foo>, 1> x;
+ };`
+ test.addTestCase("", contents, nil)
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Struct field is non-nullable, fixed-length array of
+ // non-nullable fixed-length array of nullable pointer to struct itself
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ struct Foo{
+ array<array<Foo?, 1>, 1> x;
+ };`
+ test.addTestCase("", contents, nil)
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Struct field is nullable, fixed-length array of
+ // non-nullable fixed-length array of non-nullable pointer to struct itself
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ struct Foo{
+ array<array<Foo, 1>, 1>? x;
+ };`
+ test.addTestCase("", contents, nil)
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Field of struct Foo is a map from a string to a Foo
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ struct Foo{
+ map<string, Foo> x;
+ };`
+
+ testFunc := func(descriptor *mojom.MojomDescriptor) error {
+ return nil
+ }
+ test.addTestCase("", contents, testFunc)
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: union field is nullable, pointer to union itself
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ union Foo{
+ Foo? x;
+ };`
+
+ testFunc := func(descriptor *mojom.MojomDescriptor) error {
+ return nil
+ }
+ test.addTestCase("", contents, testFunc)
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: union field is non-nullable, variable-length array of non-nullable pointer to union itself
+ ////////////////////////////////////////////////////////////
+
+ {
+ contents := `
+ union Foo{
+ array<Foo> x;
+ };`
+
+ testFunc := func(descriptor *mojom.MojomDescriptor) error {
+ return nil
+ }
+ test.addTestCase("", contents, testFunc)
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: union field is nullable, fixed-length array of non-nullable pointer to union itself
+ ////////////////////////////////////////////////////////////
+
+ {
+ contents := `
+ union Foo{
+ array<Foo, 1>? x;
+ };`
+
+ test.addTestCase("", contents, nil)
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: union field is non-nullable, fixed-length array of nullable pointer to union itself
+ ////////////////////////////////////////////////////////////
+
+ {
+ contents := `
+ union Foo{
+ array<Foo?, 1> x;
+ };`
+
+ test.addTestCase("", contents, nil)
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: A cycle of length 3 with 1 nullable.
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ struct MyStruct1 {
+ MyStruct2 x;
+ };
+
+ struct MyStruct2 {
+ MyStruct3? x;
+ };
+
+ struct MyStruct3 {
+ MyStruct1 x;
+ };`
+ test.addTestCase("", contents, nil)
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Test Case: Well-founded because of CStruct2
+ ////////////////////////////////////////////////////////////
+ {
+ contents := `
+ union AUnion0 {
+ BStruct1 x;
+ CStruct2 y;
+ };
+
+ struct BStruct1 {
+ int32 x;
+ DUnion3 y;
+ };
+
+ struct CStruct2 {
+ string x;
+ };
+
+ union DUnion3 {
+ AUnion0 x;
+ BStruct1 y;
+ };`
+ test.addTestCase("", contents, nil)
+ }
+
+ ////////////////////////////////////////////////////////////
+ // Execute all of the test cases.
+ ////////////////////////////////////////////////////////////
+ for i, c := range test.cases {
+ // Parse and resolve the mojom input.
+ descriptor := mojom.NewMojomDescriptor()
+ fileName := fmt.Sprintf("file%d", i)
+ parser := parser.MakeParser(fileName, fileName, c.mojomContents, descriptor, nil)
+ parser.Parse()
+ if !parser.OK() {
+ t.Errorf("Parsing error for %s: %s", fileName, parser.GetError().Error())
+ continue
+ }
+ err := descriptor.Resolve()
+ if err != nil {
+ t.Errorf("Resolution failed for test case %d: %s", i, err.Error())
+ continue
+ }
+
+ if err := descriptor.ComputeFinalData(); err != nil {
+ t.Errorf("ComputeFinalData error for test case %d: %s", i, err.Error())
+ continue
+ }
+
+ if err := descriptor.DetectIllFoundedTypes(); err != nil {
+ t.Errorf("DetectIllFoundedTypes error for test case %d: %s", i, err.Error())
+ continue
+ }
+
+ if c.testFunc != nil {
+ if err := c.testFunc(descriptor); err != nil {
+ t.Errorf("%s:\n%s", fileName, err.Error())
+ continue
+ }
+ }
+ }
+}
diff --git a/mojom/mojom_tool/mojom/mojom_descriptor.go b/mojom/mojom_tool/mojom/mojom_descriptor.go
index cedad88..923b98f 100644
--- a/mojom/mojom_tool/mojom/mojom_descriptor.go
+++ b/mojom/mojom_tool/mojom/mojom_descriptor.go
@@ -8,6 +8,7 @@
"bytes"
"fmt"
"mojom/mojom_tool/lexer"
+ "sort"
"strings"
)
@@ -306,6 +307,9 @@
// there are unresolved references in the .mojom files.
unresolvedTypeReferences []*UserTypeRef
unresolvedValueReferences []*UserValueRef
+
+ // In testing mode we iterate through some maps in deterministic order.
+ testingMode bool
}
func NewMojomDescriptor() *MojomDescriptor {
@@ -324,6 +328,13 @@
return descriptor
}
+// SetTestingMode is used to enable or disable testing mode. It is disabled by
+// default. In testing mode some operations will iterate through maps in a
+// deterministic order. This is less performant but enables deterministic testing.
+func (d *MojomDescriptor) SetTestingMode(testingMode bool) {
+ d.testingMode = testingMode
+}
+
func (d *MojomDescriptor) getAbstractModuleScope(fullyQualifiedName string) *Scope {
if scope, ok := d.abstractScopesByName[fullyQualifiedName]; ok {
return scope
@@ -361,6 +372,41 @@
return ok
}
+// DetectIllFoundedTypes() should be invoked after Resolve() has completed
+// successfully. It performs an analysis of the type graph in order to detect
+// ill-founded types. An ill-founded type is one for which it is impossible
+// to create an instance that would be legally serializable using Mojo
+// serialization. This method returns a non-nil error just in case an
+// ill-founded type is detected. The contained error message is appropriate
+// for display to an end-user.
+//
+// An example of an ill-founded type is a struct |Foo| with a field whose type
+// is a non-nullable Foo. Thus ill-foundedness may be due to a cycle in the
+// type graph. But not all cycles cause ill-foundedness. Firstly nullable
+// fields do not lead to ill-foundedness as the potential cycle can be broken
+// by setting the field to null. Also the situation with unions is more complicated:
+// Type graphs involving unions are only ill-founded if every possible way of
+// chosing a value for all of the unions still leads to an unbreakable cycle.
+func (d *MojomDescriptor) DetectIllFoundedTypes() error {
+ // We capture the keys of the map TypesByKey in a list and iterate over that list
+ // rather than iterating over the map. This is so that in testing mode
+ // we can first sort the list so as to iterate in a deterministic order.
+ keyList := make([]string, 0, len(d.TypesByKey))
+ for key, _ := range d.TypesByKey {
+ keyList = append(keyList, key)
+ }
+ if d.testingMode {
+ sort.Strings(keyList)
+ }
+ for _, key := range keyList {
+ udt := d.TypesByKey[key]
+ if err := udt.CheckWellFounded(); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
/////////////////////////////////////////
/// Type and Value Resolution
////////////////////////////////////////
diff --git a/mojom/mojom_tool/mojom/types.go b/mojom/mojom_tool/mojom/types.go
index 3796978..d1bbc8c 100644
--- a/mojom/mojom_tool/mojom/types.go
+++ b/mojom/mojom_tool/mojom/types.go
@@ -131,6 +131,33 @@
// It returns the number of bytes on which an instance of the type to which this
// type reference resolves must be aligned during Mojo serialization.
SerializationAlignment() uint32
+
+ // NonAvoidableUserType is invoked after the resolution and validation phases.
+ // It returns a non-nil UserDefinedType just in case this type
+ // reference resolves to a type the serialization of which will
+ // include a non-nullable pointer to a struct or union. In that case a
+ // non-nil UserDefinedType representing that struct or union is returnd.
+ //
+ // This method is used during the analysis of ill-founded types. The goal of
+ // that analysis is to find user-defined types that are unserializable because
+ // every instance of the type would involve a cycle.
+ //
+ // For example, suppose there is a struct Foo that contains a field |x|
+ // and the type of x is Foo. An instance of Foo could not be serialized because
+ // the cycle would cause an infinite recursion.
+ //
+ // However a cycle in the type graph is allowed if it is avoidable.
+ // For example if the type of x were instead Foo? then an instance
+ // of Foo could be serialized by setting x to null at some level
+ // of the recursion.
+ //
+ // Similarly if the type of x were array<Foo, 1> then an instance of
+ // Foo would not be serializable. However it allowed for the type of
+ // x to be any of the following:
+ // - array<Foo>
+ // - array<Foo?, 1>
+ // - array<Foo, 1>?
+ NonAvoidableUserType() UserDefinedType
}
/////////////////////////////////////////////////////////////
@@ -361,6 +388,10 @@
return t.SerializationSize()
}
+func (SimpleType) NonAvoidableUserType() UserDefinedType {
+ return nil
+}
+
func (t SimpleType) String() string {
switch t {
case SimpleTypeBool:
@@ -421,6 +452,10 @@
return 8
}
+func (StringType) NonAvoidableUserType() UserDefinedType {
+ return nil
+}
+
// StringLiteralType is a global singleton representing the unique LiteralType string.
var StringLiteralType LiteralType = StringType{}
@@ -518,6 +553,10 @@
return 4
}
+func (HandleTypeRef) NonAvoidableUserType() UserDefinedType {
+ return nil
+}
+
func (h HandleTypeRef) HandleKind() HandleKind {
return h.kind
}
@@ -663,6 +702,13 @@
return a.elementType
}
+func (a ArrayTypeRef) NonAvoidableUserType() UserDefinedType {
+ if a.nullable || a.fixedLength <= 0 {
+ return nil
+ }
+ return a.elementType.NonAvoidableUserType()
+}
+
// An ArrayTypeRef is a TypeRef:
func (ArrayTypeRef) TypeRefKind() TypeKind {
@@ -747,6 +793,10 @@
return 8
}
+func (MapTypeRef) NonAvoidableUserType() UserDefinedType {
+ return nil
+}
+
func (m MapTypeRef) KeyType() TypeRef {
return m.keyType
}
@@ -885,6 +935,20 @@
return t.resolvedType.SerializationAlignment()
}
+func (t *UserTypeRef) NonAvoidableUserType() UserDefinedType {
+ if t.nullable || t.interfaceRequest {
+ return nil
+ }
+ if t.resolvedType == nil {
+ panic("This method should only be invoked after successful resolution.")
+ }
+ switch udt := t.resolvedType.(type) {
+ case *MojomStruct, *MojomUnion:
+ return udt
+ }
+ return nil
+}
+
func (t *UserTypeRef) IsInterfaceRequest() bool {
return t.interfaceRequest
}
diff --git a/mojom/mojom_tool/mojom/user_defined_types.go b/mojom/mojom_tool/mojom/user_defined_types.go
index 0234bfe..ac8c05c 100644
--- a/mojom/mojom_tool/mojom/user_defined_types.go
+++ b/mojom/mojom_tool/mojom/user_defined_types.go
@@ -5,6 +5,7 @@
package mojom
import (
+ "bytes"
"errors"
"fmt"
"math"
@@ -83,6 +84,30 @@
// See computed_data.go.
ComputeFinalData() error
+ // CheckWellFounded() is invoked on each user-defined type in a MojomDescriptor
+ // after the resolution and type validation phases have completed successfully.
+ // The method performs an analysis of the type graph below a given type in order
+ // to detect ill-founded types. An ill-founded type is one for which it is impossible
+ // to create an instance that would be legally serializable using Mojo
+ // serialization. This method returns a non-nil error just in case an
+ // ill-founded type is detected. The contained error message is appropriate
+ // for display to an end-user.
+ //
+ // An example of an ill-founded type is a struct |Foo| with a field whose type
+ // is a non-nullable Foo. Thus ill-foundedness may be due to a cycle in the
+ // type graph. But not all cycles cause ill-foundedness. Firstly nullable
+ // fields do not lead to ill-foundedness as the potential cycle can be broken
+ // by setting the field to null. Also the situation with unions is more complicated:
+ // Type graphs involving unions are only ill-founded if every possible way of
+ // chosing a value for all of the unions still leads to an unbreakable cycle.
+ //
+ // We model this using the notion of a well-founded two-sorted graph. See
+ // utils/well-founded_graphs.go. Using the terminology from that file, we
+ // model structs as circle nodes and unions as square nodes. In this context
+ // the type graph only includes edges for non-nullable fields and arrays of
+ // fixed positive length.
+ CheckWellFounded() error
+
// SerializationSize() returns the number of bytes necessary to serialize an
// instance of this type in Mojo serialization.
SerializationSize() uint32
@@ -92,12 +117,19 @@
SerializationAlignment() uint32
}
+/////////////////////////////////////////////////////////////
+// type UserDefinedTypeBase
+/////////////////////////////////////////////////////////////
+
// This struct is embedded in each of MojomStruct, MojomInterface
// MojomEnum and MojomUnion
type UserDefinedTypeBase struct {
DeclarationData
thisType UserDefinedType
typeKey string
+
+ // Cache the fact that |SetKnownWellFounded| has been invoked.
+ knownWellFounded bool
}
// This method is invoked from the constructors for the containing types:
@@ -154,6 +186,101 @@
return b.scope
}
+func (b *UserDefinedTypeBase) CheckWellFounded() error {
+ cycleDescription := utils.CheckWellFounded(b)
+ if cycleDescription != nil {
+ firstIllFoundedType := nodeToUserDefinedType(cycleDescription.First)
+ var buffer bytes.Buffer
+ fmt.Fprintf(&buffer, "The type %s is unserializable: Every instance of this type would include a cycle.",
+ firstIllFoundedType.FullyQualifiedName())
+ fmt.Fprintf(&buffer, "\nExample cycle: %s", firstIllFoundedType.SimpleName())
+ for _, edge := range cycleDescription.Path {
+ fmt.Fprintf(&buffer, ".%s -> %s", edge.Label.(lexer.Token).Text, nodeToUserDefinedType(edge.Target).SimpleName())
+ }
+ fmt.Fprintf(&buffer, "\n")
+ fmt.Fprintf(&buffer, "One way to break this cycle is to make the field %q nullable.", cycleDescription.Path[0].Label.(lexer.Token).Text)
+ return fmt.Errorf(UserErrorMessage(
+ firstIllFoundedType.Scope().file, cycleDescription.Path[0].Label.(lexer.Token), buffer.String()))
+ }
+ return nil
+}
+
+// *UserDefinedTypeBase implements utils.Node for the sake of |CheckWellFounded|.
+func (b *UserDefinedTypeBase) KnownWellFounded() bool {
+ return b.knownWellFounded
+}
+
+// *UserDefinedTypeBase implements utils.Node for the sake of |CheckWellFounded|.
+func (b *UserDefinedTypeBase) SetKnownWellFounded() {
+ b.knownWellFounded = true
+}
+
+// *UserDefinedTypeBase implements utils.Node for the sake of |CheckWellFounded|.
+func (b *UserDefinedTypeBase) Name() string {
+ return b.FullyQualifiedName()
+}
+
+// *UserDefinedTypeBase implements utils.Node for the sake of |CheckWellFounded|.
+func (b *UserDefinedTypeBase) IsSquare() bool {
+ // A node in the type graph is square if and only if the type is a union.
+ return b.thisType.Kind() == UserDefinedTypeKindUnion
+}
+
+// *UserDefinedTypeBase implements utils.Node for the sake of |CheckWellFounded|.
+func (b *UserDefinedTypeBase) OutEdges() []utils.OutEdge {
+ // The only types we need to model as nodes are structs and unions.
+ // We model some of their fields as children: fields whose type is a non-nullable
+ // pointer or a fixed-length array to another struct or union.
+ outEdges := make([]utils.OutEdge, 0)
+ switch udt := b.thisType.(type) {
+ case *MojomEnum, *MojomInterface:
+ return nil
+ case *MojomStruct:
+ for _, field := range udt.FieldsInLexicalOrder {
+ childType := field.FieldType.NonAvoidableUserType()
+ if childType != nil {
+ outEdges = append(outEdges, utils.OutEdge{field.nameToken, getTypeBase(childType)})
+ }
+ }
+ case *MojomUnion:
+ for _, field := range udt.FieldsInLexicalOrder {
+ childType := field.FieldType.NonAvoidableUserType()
+ if childType != nil {
+ outEdges = append(outEdges, utils.OutEdge{field.nameToken, getTypeBase(childType)})
+ }
+ }
+ default:
+ panic(fmt.Sprintf("unexpected kind %T", udt))
+ }
+ return outEdges
+}
+
+// getTypeBase returns the *UserDefinedTypeBase contained in the given UserDefinedType
+func getTypeBase(udt UserDefinedType) *UserDefinedTypeBase {
+ switch udt := udt.(type) {
+ case *MojomStruct:
+ return &udt.UserDefinedTypeBase
+ case *MojomUnion:
+ return &udt.UserDefinedTypeBase
+ case *MojomEnum:
+ return &udt.UserDefinedTypeBase
+ case *MojomInterface:
+ return &udt.UserDefinedTypeBase
+ default:
+ panic(fmt.Sprintf("Unrecognized user defined type %T", udt))
+ }
+}
+
+// nodeToUserDefinedType assumes that the type of |node| is *UserDefinedTypeBase
+// and returns the associated UserDefinedType
+func nodeToUserDefinedType(node utils.Node) UserDefinedType {
+ return node.(*UserDefinedTypeBase).thisType
+}
+
+/////////////////////////////////////////////////////////////
+// type DeclaredObjectsContainerBase
+/////////////////////////////////////////////////////////////
+
// DeclaredObjectsContainerBase holds a list of DeclaredObjects in order of
// occurrence in the source.
// It includes all declared objects (for example methods and fields) not just
@@ -181,6 +308,10 @@
GetDeclaredObjects() []DeclaredObject
}
+/////////////////////////////////////////////////////////////
+// type NestedDeclarations
+/////////////////////////////////////////////////////////////
+
// Some user-defined types, namely interfaces and structs, may act as
// namespaced scopes for declarations of constants and enums.
type NestedDeclarations struct {
diff --git a/mojom/mojom_tool/parser/parse_driver.go b/mojom/mojom_tool/parser/parse_driver.go
index d058a99..f7b71bb 100644
--- a/mojom/mojom_tool/parser/parse_driver.go
+++ b/mojom/mojom_tool/parser/parse_driver.go
@@ -170,7 +170,12 @@
}
// Compute data for generators.
- err = descriptor.ComputeFinalData()
+ if err = descriptor.ComputeFinalData(); err != nil {
+ return
+ }
+
+ // Check for ill-founded types.
+ err = descriptor.DetectIllFoundedTypes()
return
}
diff --git a/mojom/mojom_tool/serialization/serialization_test.go b/mojom/mojom_tool/serialization/serialization_test.go
index cce8a1d..545609c 100644
--- a/mojom/mojom_tool/serialization/serialization_test.go
+++ b/mojom/mojom_tool/serialization/serialization_test.go
@@ -1148,6 +1148,10 @@
t.Errorf("ComputeFinalData error for %s: %s", c.fileName, err.Error())
continue
}
+ if err := descriptor.DetectIllFoundedTypes(); err != nil {
+ t.Errorf("DetectIllFoundedTypes error for %s: %s", c.fileName, err.Error())
+ continue
+ }
// Simulate setting the canonical file name for the imported files. In real operation
// this step is done in parser_driver.go when each of the imported files are parsed.
@@ -2082,7 +2086,7 @@
struct MyStruct1 {
int8 x;
MyUnion1 my_union;
- MyStruct1 my_struct;
+ MyStruct1? my_struct;
MyInterface1& my_interface_request;
int32 y;
MyInterface1 my_interface;
@@ -2117,7 +2121,7 @@
{
DeclData: test.newShortDeclDataO(2, -1, "my_struct"),
Type: &mojom_types.TypeTypeReference{mojom_types.TypeReference{
- false, false, stringPointer("MyStruct1"), stringPointer("TYPE_KEY:MyStruct1")}},
+ true, false, stringPointer("MyStruct1"), stringPointer("TYPE_KEY:MyStruct1")}},
Offset: 24,
},
// field my_interface_request
@@ -2181,7 +2185,7 @@
struct MyStruct1 {
int8 x;
MyUnion1 my_union;
- MyStruct1 my_struct;
+ MyStruct1? my_struct;
[MinVersion=1]
MyInterface1&? my_interface_request;
@@ -2222,7 +2226,7 @@
{
DeclData: test.newShortDeclDataO(2, -1, "my_struct"),
Type: &mojom_types.TypeTypeReference{mojom_types.TypeReference{
- false, false, stringPointer("MyStruct1"), stringPointer("TYPE_KEY:MyStruct1")}},
+ true, false, stringPointer("MyStruct1"), stringPointer("TYPE_KEY:MyStruct1")}},
Offset: 24,
},
// field my_interface_request
@@ -2303,6 +2307,10 @@
t.Errorf("ComputeFinalData error for %s: %s", c.fileName, err.Error())
continue
}
+ if err := descriptor.DetectIllFoundedTypes(); err != nil {
+ t.Errorf("DetectIllFoundedTypes error for %s: %s", c.fileName, err.Error())
+ continue
+ }
// Simulate setting the canonical file name for the imported files. In real operation
// this step is done in parser_driver.go when each of the imported files are parsed.
@@ -2412,6 +2420,10 @@
t.Errorf("ComputeFinalData error for %s: %s", c.fileName, err.Error())
continue
}
+ if err := descriptor.DetectIllFoundedTypes(); err != nil {
+ t.Errorf("DetectIllFoundedTypes error for %s: %s", c.fileName, err.Error())
+ continue
+ }
// Serialize
bytes, _, err := serialize(descriptor, false, false, c.lineAndcolumnNumbers, false)
@@ -2687,6 +2699,10 @@
t.Errorf("ComputeFinalData error for case %d: %s", i, err.Error())
continue
}
+ if err := descriptor.DetectIllFoundedTypes(); err != nil {
+ t.Errorf("DetectIllFoundedTypes error for case %d: %s", i, err.Error())
+ continue
+ }
// Serialize
bytes, _, err := serialize(descriptor, false, false, c.lineAndcolumnNumbers, false)
@@ -3296,6 +3312,10 @@
t.Errorf("ComputeFinalData error for case %d: %s", i, err.Error())
continue
}
+ if err := descriptor.DetectIllFoundedTypes(); err != nil {
+ t.Errorf("DetectIllFoundedTypes error for case %d: %s", i, err.Error())
+ continue
+ }
// Serialize
bytes, _, err := serialize(descriptor, false, false, false, true)
diff --git a/mojom/mojom_tool/utils/wellfounded_graphs.go b/mojom/mojom_tool/utils/wellfounded_graphs.go
index 6603c0f..1266a96 100644
--- a/mojom/mojom_tool/utils/wellfounded_graphs.go
+++ b/mojom/mojom_tool/utils/wellfounded_graphs.go
@@ -28,12 +28,15 @@
///////////////////////////////////////////////////
// Node type
//
-// A |Node| is a node in a directed graph.
+// A |Node| is a node in a directed graph. A user of this library is responsible
+// for implementing this interface. This library will use the Go equality operator
+// to compare instances of |Node| and it is the responsibility of the user
+// to ensure that x == y iff x and y represent the same logical node.
///////////////////////////////////////////////////
type Node interface {
// Returns the list of children of this node.
- OutEdges() []Node
+ OutEdges() []OutEdge
// Returns whether or not this node is a square node.
IsSquare() bool
@@ -53,8 +56,18 @@
// this library.
KnownWellFounded() bool
- // Returns the name of this node, appropriate for debugging.
- DebugName() string
+ // Returns a human-readable name for this node. Not used by the algorithm
+ // but used for generating debugging and error messages.
+ Name() string
+}
+
+type OutEdge struct {
+ // The Label on an edge is opaque data that may be used by the client however it wishes.
+ // The algorithm never inspects this data.
+ Label interface{}
+
+ // The target node of the out edge.
+ Target Node
}
///////////////////////////////////////////////////
@@ -134,13 +147,14 @@
///////////////////////////////////////////////////
type cycleFinder struct {
- // Maps each node seen by the computation to its holder.
+ // Maps each node seen by the computation to its holder. Note that this assumes
+ // that the implementation of Node uses values that are comparable and
+ // obey identity semantics.
nodeToHolder map[Node]*nodeHolder
foundationSet, pendingSet NodeSet
visitationIndex int
- currentPath nodeStack
debugDataRequest *debugData
}
@@ -155,7 +169,6 @@
finder.nodeToHolder = make(map[Node]*nodeHolder)
finder.foundationSet = MakeNodeSet()
finder.pendingSet = MakeNodeSet()
- finder.currentPath = nodeStack{}
return finder
}
@@ -206,7 +219,7 @@
// build a canonical cycle.
freshCycleFinder := makeCycleFinder()
holder := freshCycleFinder.holderForNode(minIllfoundedNode)
- return freshCycleFinder.findKnownIllFoundedCycle(holder)
+ return freshCycleFinder.findKnownIllFoundedCycle(holder, nil)
}
// checkWellFoundedPhase1 is a recursive helper function that does a depth-first traversal
@@ -246,8 +259,8 @@
// Next we examine each of the children and recurse into the unvisited ones.
sawUnverifiedChild := false
- for _, child := range nodeHolder.node.OutEdges() {
- childHolder := finder.holderForNode(child)
+ for _, edge := range nodeHolder.node.OutEdges() {
+ childHolder := finder.holderForNode(edge.Target)
if childHolder.state == vsUnvisited {
// Recursively visit this child.
finder.checkWellFoundedPhase1(childHolder)
@@ -323,7 +336,8 @@
if finder.pendingSet.Contains(p) {
knownWellFounded := true
if !p.node.IsSquare() {
- for _, child := range p.node.OutEdges() {
+ for _, edge := range p.node.OutEdges() {
+ child := edge.Target
if child != p.node && !child.KnownWellFounded() {
knownWellFounded = false
break
@@ -340,6 +354,14 @@
}
}
+// A pathElement is a node and one of its out-edges.
+type pathElement struct {
+ node Node
+ edge OutEdge
+}
+
+type nodePath []pathElement
+
// findKnownIllFoundedCycle finds and returns a |CycleDescription| starting from a node that is known
// to be ill-founded. This proceeds by following edges from an ill-founded node to
// an ill-founded child node until a cycle is formed. We return a *canonical* cycle,
@@ -348,18 +370,28 @@
// We are not concerned with optimizing the performance of phase 3 because in the intended application
// phase 3 can occur at most once in the lifetime of a program: Once an ill-founded node is detected the
// program exits with a cycle description allowing the user to fix the ill-foundedness.
-func (finder *cycleFinder) findKnownIllFoundedCycle(nodeHolder *nodeHolder) *CycleDescription {
+//
+// This method is recursive. To initiate the recursion |nodeHolder| should be set to the starting node and
+// |path| should be nil. In subsequent recursive calls |nodeHolder| is the current node of the walk and
+// |path| is the path from the starting node to the current node.
+func (finder *cycleFinder) findKnownIllFoundedCycle(nodeHolder *nodeHolder, path nodePath) *CycleDescription {
+ // If this is the start of the recursion initialize the path.
+ if path == nil {
+ path = make(nodePath, 0)
+ }
+
// Mark the current node as started
nodeHolder.state = vsVisitStarted
- finder.currentPath.Push(nodeHolder)
- for _, child := range nodeHolder.node.OutEdges() {
- childHolder := finder.holderForNode(child)
+ for _, edge := range nodeHolder.node.OutEdges() {
+ childHolder := finder.holderForNode(edge.Target)
if childHolder.state == vsVisitStarted {
// If the child has been started but not finished then we have found a cycle
// from the child to the current node back to the child.
- return newCycleDescription(finder.currentPath.elements, childHolder, nodeHolder)
+ path = append(path, pathElement{nodeHolder.node, edge})
+ return newCycleDescription(path, childHolder.node, nodeHolder.node)
} else if !childHolder.node.KnownWellFounded() {
- return finder.findKnownIllFoundedCycle(childHolder)
+ path = append(path, pathElement{nodeHolder.node, edge})
+ return finder.findKnownIllFoundedCycle(childHolder, path)
}
}
panic("Program logic error: Could not find a known ill-founded cycle.")
@@ -413,49 +445,6 @@
return nodeHolder
}
-//////////////////////////////////////////////
-/// nodeStack type
-//////////////////////////////////////////////
-
-// A nodeStack is a stack of *nodeHolders
-type nodeStack struct {
- elements []*nodeHolder
-}
-
-func (stack *nodeStack) Push(n *nodeHolder) {
- stack.elements = append(stack.elements, n)
-}
-
-func (stack *nodeStack) Size() int {
- return len(stack.elements)
-}
-
-func (stack *nodeStack) Pop() (n *nodeHolder) {
- lastIndex := stack.Size() - 1
- n = stack.elements[lastIndex]
- stack.elements = stack.elements[:lastIndex]
- return
-}
-
-func (stack *nodeStack) Peek() (n *nodeHolder) {
- return stack.elements[stack.Size()-1]
-}
-
-func (stack *nodeStack) String() string {
- var buffer bytes.Buffer
- fmt.Fprintf(&buffer, "[")
- first := true
- for _, e := range stack.elements {
- if !first {
- fmt.Fprintf(&buffer, ", ")
- }
- fmt.Fprintf(&buffer, "%s", e.node.DebugName())
- first = false
- }
- fmt.Fprintln(&buffer, "]")
- return buffer.String()
-}
-
///////////////////////////////////////////////////
/// NodeSet type
///////////////////////////////////////////////////
@@ -542,12 +531,12 @@
func compareNodeSets(expected, actual *NodeSet) error {
for n, _ := range expected.elements {
if !actual.Contains(n) {
- return fmt.Errorf("%s is in expected but not actual", n.node.DebugName())
+ return fmt.Errorf("%s is in expected but not actual", n.node.Name())
}
}
for n, _ := range actual.elements {
if !expected.Contains(n) {
- return fmt.Errorf("%s is in actual but not expected", n.node.DebugName())
+ return fmt.Errorf("%s is in actual but not expected", n.node.Name())
}
}
return nil
@@ -562,7 +551,7 @@
if !first {
fmt.Fprintf(&buffer, ", ")
}
- fmt.Fprintf(&buffer, "%s", e.node.DebugName())
+ fmt.Fprintf(&buffer, "%s", e.node.Name())
first = false
}
fmt.Fprintln(&buffer, "}")
@@ -576,39 +565,57 @@
///////////////////////////////////////////////////
type CycleDescription struct {
- first, last Node
- path []Node
+ First, Last Node
+ Path []OutEdge
}
func (c *CycleDescription) String() string {
var buffer bytes.Buffer
- fmt.Fprintf(&buffer, "first:%s", c.first.DebugName())
- fmt.Fprintf(&buffer, ", last:%s", c.last.DebugName())
+ fmt.Fprintf(&buffer, "first:%s", c.First.Name())
+ fmt.Fprintf(&buffer, ", last:%s", c.Last.Name())
fmt.Fprintf(&buffer, ", path:{")
- first := true
- for _, n := range c.path {
- if !first {
+ fmt.Fprintf(&buffer, "%s", c.First.Name())
+ for _, edge := range c.Path {
+ if edge.Target != c.First {
fmt.Fprintf(&buffer, ", ")
+ fmt.Fprintf(&buffer, "%s", edge.Target.Name())
}
- fmt.Fprintf(&buffer, "%s", n.DebugName())
- first = false
}
fmt.Fprintln(&buffer, "}")
return buffer.String()
}
-func newCycleDescription(path []*nodeHolder, first, last *nodeHolder) *CycleDescription {
+// newCycleDescription builds a CycleDescription based on the given data.
+// |path| should be a nodePath that ends with a cycle. That is it should look like
+// {x1, ->x2}, {x2, ->x3}, {x3, ->x4}, {x4, ->x5}, {x5, ->x2}
+//
+// |cycleStart| should be the first node of |path| included in the cycle. In
+// the above example it would be x2.
+//
+// |end| should be the last node of the path. In the above example it would be x5.
+//
+// The return value using our example would be:
+// {First: x2, Last: x5, Path:[{x2, x2->x3}, {x3, x3->x4}, {x4, x4->x5}, {x5, x5->x2}]}
+func newCycleDescription(path nodePath, cycleStart, end Node) *CycleDescription {
+ lastNode := path[len(path)-1].node
+ if lastNode != end {
+ panic(fmt.Sprintf("%s != %s", lastNode.Name(), end.Name()))
+ }
+ lastTarget := path[len(path)-1].edge.Target
+ if lastTarget != cycleStart {
+ panic(fmt.Sprintf("(%T)%s != (%T)%s", lastTarget, lastTarget.Name(), cycleStart, cycleStart.Name()))
+ }
description := CycleDescription{}
- description.first = first.node
- description.last = last.node
- description.path = make([]Node, 0, len(path))
- for _, n := range path {
- if len(description.path) > 0 || n.node == first.node {
- description.path = append(description.path, n.node)
+ description.First = cycleStart
+ description.Last = end
+ description.Path = make([]OutEdge, 0, len(path))
+ for _, element := range path {
+ if len(description.Path) > 0 || element.node == cycleStart {
+ description.Path = append(description.Path, element.edge)
}
}
- if description.path[len(description.path)-1] != last.node {
- panic(fmt.Sprintf("%s != %s", description.path[len(description.path)-1], last.node))
+ if description.Path[len(description.Path)-1].Target != cycleStart {
+ panic(fmt.Sprintf("%s != %s", description.Path[len(description.Path)-1].Target.Name(), cycleStart.Name()))
}
return &description
}
diff --git a/mojom/mojom_tool/utils/wellfounded_graphs_test.go b/mojom/mojom_tool/utils/wellfounded_graphs_test.go
index 958fa9a..5eb0898 100644
--- a/mojom/mojom_tool/utils/wellfounded_graphs_test.go
+++ b/mojom/mojom_tool/utils/wellfounded_graphs_test.go
@@ -20,18 +20,18 @@
// Is this node a square node?
isSquare bool
- // The list of out-edges. The elements will be *SimpleNodes.
- edges []Node
+ // The list of out-edges.
+ edges []OutEdge
// Cache the fact that |SetKnownWellFounded| has been invoked.
knownWellFounded bool
}
-func (node *SimpleNode) DebugName() string {
+func (node *SimpleNode) Name() string {
return fmt.Sprintf("Node%d", node.value)
}
-func (node *SimpleNode) OutEdges() []Node {
+func (node *SimpleNode) OutEdges() []OutEdge {
return node.edges
}
@@ -45,7 +45,7 @@
func (node *SimpleNode) SetKnownWellFounded() {
if node.knownWellFounded {
- panic(fmt.Sprintf("SetKnownWellFounded invoked twice on %s", node.DebugName()))
+ panic(fmt.Sprintf("SetKnownWellFounded invoked twice on %s", node.Name()))
}
node.knownWellFounded = true
}
@@ -54,7 +54,7 @@
func NewNode(value, numEdges int, isSquare bool) *SimpleNode {
node := new(SimpleNode)
node.value = value
- node.edges = make([]Node, numEdges)
+ node.edges = make([]OutEdge, numEdges)
node.isSquare = isSquare
return node
}
@@ -89,7 +89,7 @@
}
for index, connectionList := range connectionMatrix {
for i, target := range connectionList {
- nodes[index].edges[i] = nodes[target]
+ nodes[index].edges[i] = OutEdge{"", nodes[target]}
}
}
return
@@ -120,7 +120,7 @@
expectedMap[n] = true
}
for _, n := range actual {
- actualMap[n.DebugName()] = true
+ actualMap[n.Name()] = true
}
for _, n := range expected {
if _, ok := actualMap[n]; !ok {
@@ -128,8 +128,8 @@
}
}
for _, n := range actual {
- if _, ok := expectedMap[n.DebugName()]; !ok {
- return fmt.Errorf("%s: unexpected: %s, expected: %v, actual: %s", name, n.DebugName(), expected, NodeSliceToString(actual))
+ if _, ok := expectedMap[n.Name()]; !ok {
+ return fmt.Errorf("%s: unexpected: %s, expected: %v, actual: %s", name, n.Name(), expected, NodeSliceToString(actual))
}
}
return nil
@@ -143,7 +143,7 @@
if !first {
fmt.Fprintf(&buffer, ", ")
}
- fmt.Fprintf(&buffer, "%s", n.DebugName())
+ fmt.Fprintf(&buffer, "%s", n.Name())
first = false
}
fmt.Fprintln(&buffer, "]")
@@ -458,13 +458,13 @@
t.Fatalf("Case %d: Expected a cycle.", i)
}
if !strings.Contains(cycleDescription.String(), fmt.Sprintf("first:%s", c.expectedFirstA)) {
- t.Fatalf("Case %d: got=%s expectedFirst=%q", i, cycleDescription.String(), c.expectedFirstA)
+ t.Fatalf("Case %d: got=%s expectedFirstA=%q", i, cycleDescription.String(), c.expectedFirstA)
}
if !strings.Contains(cycleDescription.String(), fmt.Sprintf("last:%s", c.expectedLastA)) {
- t.Fatalf("Case %d: got=%s expectedLast=%q", i, cycleDescription.String(), c.expectedLastA)
+ t.Fatalf("Case %d: got=%s expectedLastA=%q", i, cycleDescription.String(), c.expectedLastA)
}
if !strings.Contains(cycleDescription.String(), fmt.Sprintf("{%s}", c.expectedPathA)) {
- t.Fatalf("Case %d: got=%s expectedPath=%q", i, cycleDescription.String(), c.expectedPathA)
+ t.Fatalf("Case %d: got=%s expectedPathA=%q", i, cycleDescription.String(), c.expectedPathA)
}
}
@@ -484,13 +484,13 @@
t.Fatalf("Case %dB: Expected a cycle.", i)
}
if !strings.Contains(cycleDescription.String(), fmt.Sprintf("first:%s", c.expectedFirstB)) {
- t.Fatalf("Case %dB: got=%s expectedFirst=%q", i, cycleDescription.String(), c.expectedFirstB)
+ t.Fatalf("Case %dB: got=%s expectedFirstB=%q", i, cycleDescription.String(), c.expectedFirstB)
}
if !strings.Contains(cycleDescription.String(), fmt.Sprintf("last:%s", c.expectedLastB)) {
- t.Fatalf("Case %dB: got=%s expectedLast=%q", i, cycleDescription.String(), c.expectedLastB)
+ t.Fatalf("Case %dB: got=%s expectedLastB=%q", i, cycleDescription.String(), c.expectedLastB)
}
if !strings.Contains(cycleDescription.String(), fmt.Sprintf("{%s}", c.expectedPathB)) {
- t.Fatalf("Case %dB: got=%s expectedPath=%q", i, cycleDescription.String(), c.expectedPathB)
+ t.Fatalf("Case %dB: got=%s expectedPathB=%q", i, cycleDescription.String(), c.expectedPathB)
}
}
}