Browse Source

feat: Pretty printer for GraphQL

Matthias Ladkau 2 năm trước cách đây
mục cha
commit
7a75f34299
2 tập tin đã thay đổi với 911 bổ sung0 xóa
  1. 335 0
      lang/graphql/parser/prettyprinter.go
  2. 576 0
      lang/graphql/parser/prettyprinter_test.go

+ 335 - 0
lang/graphql/parser/prettyprinter.go

@@ -0,0 +1,335 @@
+/*
+ * Public Domain Software
+ *
+ * I (Matthias Ladkau) am the author of the source code in this file.
+ * I have placed the source code in this file in the public domain.
+ *
+ * For further information see: http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+package parser
+
+import (
+	"bytes"
+	"fmt"
+	"regexp"
+	"strconv"
+	"strings"
+	"text/template"
+	"unicode"
+
+	"devt.de/krotik/common/errorutil"
+	"devt.de/krotik/common/stringutil"
+)
+
+/*
+IndentationLevel is the level of indentation which the pretty printer should use
+*/
+const IndentationLevel = 2
+
+/*
+Map of pretty printer templates for AST nodes
+
+There is special treatment for NodeVALUE.
+*/
+var prettyPrinterMap = map[string]*template.Template{
+	NodeArgument + "_2": template.Must(template.New(NodeArgument).Parse("{{.c1}}: {{.c2}}")),
+
+	NodeOperationDefinition + "_1": template.Must(template.New(NodeArgument).Parse("{{.c1}}")),
+	NodeOperationDefinition + "_2": template.Must(template.New(NodeArgument).Parse("{{.c1}} {{.c2}}")),
+	NodeOperationDefinition + "_3": template.Must(template.New(NodeArgument).Parse("{{.c1}} {{.c2}} {{.c3}}")),
+	NodeOperationDefinition + "_4": template.Must(template.New(NodeArgument).Parse("{{.c1}} {{.c2}} {{.c3}} {{.c4}}")),
+	NodeOperationDefinition + "_5": template.Must(template.New(NodeArgument).Parse("{{.c1}} {{.c2}} {{.c3}} {{.c4}} {{.c5}}")),
+
+	NodeFragmentDefinition + "_3": template.Must(template.New(NodeArgument).Parse("fragment {{.c1}} {{.c2}} {{.c3}}")),
+	NodeFragmentDefinition + "_4": template.Must(template.New(NodeArgument).Parse("fragment {{.c1}} {{.c2}} {{.c3}} {{.c4}}")),
+
+	NodeInlineFragment + "_1": template.Must(template.New(NodeArgument).Parse("... {{.c1}}\n")),
+	NodeInlineFragment + "_2": template.Must(template.New(NodeArgument).Parse("... {{.c1}} {{.c2}}\n")),
+	NodeInlineFragment + "_3": template.Must(template.New(NodeArgument).Parse("... {{.c1}} {{.c2}} {{.c3}}\n")),
+
+	NodeExecutableDefinition + "_1": template.Must(template.New(NodeArgument).Parse("{{.c1}}")),
+
+	NodeVariableDefinition + "_2": template.Must(template.New(NodeArgument).Parse("{{.c1}}: {{.c2}}")),
+	NodeVariableDefinition + "_3": template.Must(template.New(NodeArgument).Parse("{{.c1}}: {{.c2}}{{.c3}}")),
+
+	NodeDirective + "_1": template.Must(template.New(NodeArgument).Parse("@{{.c1}}")),
+	NodeDirective + "_2": template.Must(template.New(NodeArgument).Parse("@{{.c1}}{{.c2}}")),
+}
+
+/*
+PrettyPrint produces a pretty printed EQL query from a given AST.
+*/
+func PrettyPrint(ast *ASTNode) (string, error) {
+	var visit func(ast *ASTNode, path []*ASTNode) (string, error)
+
+	quoteValue := func(val string, allowNonQuotation bool) string {
+
+		if val == "" {
+			return `""`
+		}
+
+		isNumber, _ := regexp.MatchString("^[0-9][0-9\\.e-+]*$", val)
+		isInlineString, _ := regexp.MatchString("^[a-zA-Z0-9_:.]*$", val)
+
+		if allowNonQuotation && (isNumber || isInlineString) {
+			return val
+		} else if strings.ContainsRune(val, '"') {
+			val = strings.Replace(val, "\"", "\\\"", -1)
+		}
+		if strings.Contains(val, "\n") {
+			return fmt.Sprintf("\"\"\"%v\"\"\"", val)
+		}
+		return fmt.Sprintf("\"%v\"", val)
+	}
+
+	visit = func(ast *ASTNode, path []*ASTNode) (string, error) {
+
+		// Handle special cases which don't have children but values
+
+		if ast.Name == NodeValue {
+			v := ast.Token.Val
+
+			_, err := strconv.ParseFloat(v, 32)
+			isNum := err == nil
+
+			isConst := stringutil.IndexOf(v, []string{
+				"true", "false", "null",
+			}) != -1
+
+			return quoteValue(ast.Token.Val, isConst || isNum), nil
+
+		} else if ast.Name == NodeVariable {
+			return fmt.Sprintf("$%v", ast.Token.Val), nil
+		} else if ast.Name == NodeAlias {
+			return fmt.Sprintf("%v :", ast.Token.Val), nil
+		} else if ast.Name == NodeFragmentSpread {
+			return ppPostProcessing(ast, path, fmt.Sprintf("...%v\n", ast.Token.Val)), nil
+		} else if ast.Name == NodeTypeCondition {
+			return fmt.Sprintf("on %v", ast.Token.Val), nil
+		} else if ast.Name == NodeDefaultValue {
+			return fmt.Sprintf("=%v", ast.Token.Val), nil
+		}
+
+		var children map[string]string
+		var tempKey = ast.Name
+		var buf bytes.Buffer
+
+		// First pretty print children
+
+		if len(ast.Children) > 0 {
+			children = make(map[string]string)
+			for i, child := range ast.Children {
+				res, err := visit(child, append(path, child))
+				if err != nil {
+					return "", err
+				}
+
+				children[fmt.Sprint("c", i+1)] = res
+			}
+
+			tempKey += fmt.Sprint("_", len(children))
+		}
+
+		// Handle special cases requiring children
+
+		if ast.Name == NodeDocument {
+			if children != nil {
+				i := 1
+				for ; i < len(children); i++ {
+					buf.WriteString(children[fmt.Sprint("c", i)])
+
+					if ast.Children[i].Name != NodeArguments {
+						buf.WriteString("\n\n")
+					}
+				}
+				buf.WriteString(children[fmt.Sprint("c", i)])
+			}
+
+			return ppPostProcessing(ast, path, buf.String()), nil
+
+		} else if ast.Name == NodeOperationType || ast.Name == NodeName ||
+			ast.Name == NodeFragmentName || ast.Name == NodeType || ast.Name == NodeEnumValue {
+
+			return ast.Token.Val, nil
+
+		} else if ast.Name == NodeArguments {
+
+			buf.WriteString("(")
+
+			if children != nil {
+				i := 1
+				for ; i < len(children); i++ {
+					buf.WriteString(children[fmt.Sprint("c", i)])
+					buf.WriteString(", ")
+				}
+				buf.WriteString(children[fmt.Sprint("c", i)])
+			}
+			buf.WriteString(")")
+
+			return ppPostProcessing(ast, path, buf.String()), nil
+
+		} else if ast.Name == NodeListValue {
+			buf.WriteString("[")
+			if children != nil {
+				i := 1
+				for ; i < len(children); i++ {
+					buf.WriteString(children[fmt.Sprint("c", i)])
+					buf.WriteString(", ")
+				}
+				buf.WriteString(children[fmt.Sprint("c", i)])
+			}
+			buf.WriteString("]")
+
+			return ppPostProcessing(ast, path, buf.String()), nil
+
+		} else if ast.Name == NodeVariableDefinitions {
+			buf.WriteString("(")
+			if children != nil {
+				i := 1
+				for ; i < len(children); i++ {
+					buf.WriteString(children[fmt.Sprint("c", i)])
+					buf.WriteString(", ")
+				}
+				buf.WriteString(children[fmt.Sprint("c", i)])
+			}
+			buf.WriteString(")")
+
+			return ppPostProcessing(ast, path, buf.String()), nil
+
+		} else if ast.Name == NodeSelectionSet {
+			buf.WriteString("{\n")
+			if children != nil {
+				i := 1
+				for ; i < len(children); i++ {
+					buf.WriteString(children[fmt.Sprint("c", i)])
+				}
+				buf.WriteString(children[fmt.Sprint("c", i)])
+			}
+			buf.WriteString("}")
+
+			return ppPostProcessing(ast, path, buf.String()), nil
+
+		} else if ast.Name == NodeObjectValue {
+
+			buf.WriteString("{")
+
+			if children != nil {
+				i := 1
+				for ; i < len(children); i++ {
+					buf.WriteString(children[fmt.Sprint("c", i)])
+					buf.WriteString(", ")
+				}
+				buf.WriteString(children[fmt.Sprint("c", i)])
+			}
+			buf.WriteString("}")
+
+			return ppPostProcessing(ast, path, buf.String()), nil
+
+		} else if ast.Name == NodeObjectField {
+
+			buf.WriteString(ast.Token.Val)
+			buf.WriteString(" : ")
+			buf.WriteString(children["c1"])
+
+			return buf.String(), nil
+
+		} else if ast.Name == NodeField {
+
+			if children != nil {
+				i := 1
+				for ; i < len(children); i++ {
+					buf.WriteString(children[fmt.Sprint("c", i)])
+
+					if ast.Children[i].Name != NodeArguments {
+						buf.WriteString(" ")
+					}
+				}
+				buf.WriteString(children[fmt.Sprint("c", i)])
+				buf.WriteString("\n")
+			}
+
+			return ppPostProcessing(ast, path, buf.String()), nil
+		} else if ast.Name == NodeDirectives {
+
+			if children != nil {
+				i := 1
+				for ; i < len(children); i++ {
+					buf.WriteString(children[fmt.Sprint("c", i)])
+
+					if ast.Children[i].Name != NodeArguments {
+						buf.WriteString(" ")
+					}
+				}
+				buf.WriteString(children[fmt.Sprint("c", i)])
+			}
+
+			return ppPostProcessing(ast, path, buf.String()), nil
+		}
+
+		// Retrieve the template
+
+		temp, ok := prettyPrinterMap[tempKey]
+		if !ok {
+			return "", fmt.Errorf("Could not find template for %v (tempkey: %v)",
+				ast.Name, tempKey)
+		}
+
+		// Use the children as parameters for template
+
+		errorutil.AssertOk(temp.Execute(&buf, children))
+
+		return ppPostProcessing(ast, path, buf.String()), nil
+	}
+
+	res, err := visit(ast, []*ASTNode{ast})
+
+	return strings.TrimSpace(res), err
+}
+
+/*
+ppPostProcessing applies post processing rules.
+*/
+func ppPostProcessing(ast *ASTNode, path []*ASTNode, ppString string) string {
+	ret := ppString
+
+	// Apply indentation
+
+	if len(path) > 1 {
+		if stringutil.IndexOf(ast.Name, []string{
+			NodeField,
+			NodeFragmentSpread,
+			NodeInlineFragment,
+		}) != -1 {
+
+			parent := path[len(path)-3]
+
+			indentSpaces := stringutil.GenerateRollingString(" ", IndentationLevel)
+			ret = strings.ReplaceAll(ret, "\n", "\n"+indentSpaces)
+			ret = fmt.Sprintf("%v%v", indentSpaces, ret)
+
+			// Remove indentation from last line unless we have a special case
+
+			if stringutil.IndexOf(parent.Name, []string{
+				NodeField,
+				NodeOperationDefinition,
+			}) == -1 {
+
+				if idx := strings.LastIndex(ret, "\n"); idx != -1 {
+					ret = ret[:idx+1] + ret[idx+IndentationLevel+1:]
+				}
+			}
+
+		}
+	}
+
+	// Remove all trailing spaces
+
+	newlineSplit := strings.Split(ret, "\n")
+
+	for i, s := range newlineSplit {
+		newlineSplit[i] = strings.TrimRightFunc(s, unicode.IsSpace)
+	}
+
+	return strings.Join(newlineSplit, "\n")
+}

+ 576 - 0
lang/graphql/parser/prettyprinter_test.go

@@ -0,0 +1,576 @@
+/*
+ * Public Domain Software
+ *
+ * I (Matthias Ladkau) am the author of the source code in this file.
+ * I have placed the source code in this file in the public domain.
+ *
+ * For further information see: http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+package parser
+
+import (
+	"fmt"
+	"os"
+	"testing"
+)
+
+func TestSimpleExpressionPrinting(t *testing.T) {
+
+	input := `query {
+  likeStory(storyID: 12345) {
+    story {
+      likeCount
+    }
+  }
+}`
+
+	expectedOutput := `
+Document
+  ExecutableDefinition
+    OperationDefinition
+      OperationType: query
+      SelectionSet
+        Field
+          Name: likeStory
+          Arguments
+            Argument
+              Name: storyID
+              Value: 12345
+          SelectionSet
+            Field
+              Name: story
+              SelectionSet
+                Field
+                  Name: likeCount
+`[1:]
+
+	if err := testPrettyPrinting(input, expectedOutput,
+		input); err != nil {
+		t.Error(err)
+		return
+	}
+
+	/*
+	   From the spec 2.9.4 Strings
+
+	   Since block strings represent freeform text often used in indented positions,
+	   the string value semantics of a block string excludes uniform indentation and
+	   blank initial and trailing lines via BlockStringValue().
+
+	   For example, the following operation containing a block string:
+
+	   mutation {
+	     sendEmail(message: """
+	       Hello,
+	         World!
+
+	       Yours,
+	         GraphQL.
+	     """)
+	   }
+
+	   Is identical to the standard quoted string:
+
+	   mutation {
+	     sendEmail(message: "Hello,\n  World!\n\nYours,\n  GraphQL.")
+	   }
+	*/
+
+	input = `{
+  foo(bar: """
+    Hello,
+      World!
+
+    Yours,
+      GraphQL.
+  """)                      # Block string value
+}
+`
+	expectedOutput = `
+Document
+  ExecutableDefinition
+    OperationDefinition
+      SelectionSet
+        Field
+          Name: foo
+          Arguments
+            Argument
+              Name: bar
+              Value: Hello,
+  World!
+
+Yours,
+  GraphQL.
+`[1:]
+
+	astres, err := ParseWithRuntime("mytest", input, &TestRuntimeProvider{})
+	if err != nil || fmt.Sprint(astres) != expectedOutput {
+		t.Error(fmt.Sprintf("Unexpected parser output:\n%v expected was:\n%v Error: %v", astres, expectedOutput, err))
+		return
+	}
+
+	ppOutput := `{
+  foo(bar: """Hello,
+    World!
+
+  Yours,
+    GraphQL.""")
+}`
+
+	ppres, err := PrettyPrint(astres)
+	if err != nil || ppres != ppOutput {
+		fmt.Fprintf(os.Stderr, "#\n%v#", ppres)
+		t.Error(fmt.Sprintf("Unexpected result:\n%v\nError: %v", ppres, err))
+		return
+	}
+
+	val := astres.Children[0].Children[0].Children[0].Children[0].Children[1].Children[0].Children[1].Token.Val
+	if val != "Hello,\n  World!\n\nYours,\n  GraphQL." {
+		t.Error("Unexpected result:", val)
+	}
+
+	input = `{
+  foo(bar: $Hello)        # Variable value
+  foo(bar: 1)             # Int value
+  foo(bar: 1.1)           # Float value
+  foo(bar: "Hello")       # String value
+  foo(bar: false)         # Boolean value
+  foo(bar: null)          # Null value
+  foo(bar: MOBILE_WEB)    # Enum value
+  foo(bar: [1,2,[A,"B"]]) # List value
+  foo(bar: {foo:"bar"
+    foo2 : [12],
+    foo3 : { X:Y }
+    })         # Object value
+}
+`
+
+	expectedOutput = `
+Document
+  ExecutableDefinition
+    OperationDefinition
+      SelectionSet
+        Field
+          Name: foo
+          Arguments
+            Argument
+              Name: bar
+              Variable: Hello
+        Field
+          Name: foo
+          Arguments
+            Argument
+              Name: bar
+              Value: 1
+        Field
+          Name: foo
+          Arguments
+            Argument
+              Name: bar
+              Value: 1.1
+        Field
+          Name: foo
+          Arguments
+            Argument
+              Name: bar
+              Value: Hello
+        Field
+          Name: foo
+          Arguments
+            Argument
+              Name: bar
+              Value: false
+        Field
+          Name: foo
+          Arguments
+            Argument
+              Name: bar
+              Value: null
+        Field
+          Name: foo
+          Arguments
+            Argument
+              Name: bar
+              EnumValue: MOBILE_WEB
+        Field
+          Name: foo
+          Arguments
+            Argument
+              Name: bar
+              ListValue
+                Value: 1
+                Value: 2
+                ListValue
+                  EnumValue: A
+                  Value: B
+        Field
+          Name: foo
+          Arguments
+            Argument
+              Name: bar
+              ObjectValue
+                ObjectField: foo
+                  Value: bar
+                ObjectField: foo2
+                  ListValue
+                    Value: 12
+                ObjectField: foo3
+                  ObjectValue
+                    ObjectField: X
+                      EnumValue: Y
+`[1:]
+
+	expectedPPResult := `{
+  foo(bar: $Hello)
+  foo(bar: 1)
+  foo(bar: 1.1)
+  foo(bar: "Hello")
+  foo(bar: false)
+  foo(bar: null)
+  foo(bar: MOBILE_WEB)
+  foo(bar: [1, 2, [A, "B"]])
+  foo(bar: {foo : "bar", foo2 : [12], foo3 : {X : Y}})
+}`
+
+	if err := testPrettyPrinting(input, expectedOutput,
+		expectedPPResult); err != nil {
+		t.Error(err)
+		return
+	}
+
+	input = `{
+  my : field
+}`
+
+	expectedOutput = `
+Document
+  ExecutableDefinition
+    OperationDefinition
+      SelectionSet
+        Field
+          Alias: my
+          Name: field
+`[1:]
+
+	if err := testPrettyPrinting(input, expectedOutput,
+		input); err != nil {
+		t.Error(err)
+		return
+	}
+
+	input = `query getBozoProfile ($devicePicSize: Int, $foo: bar=123) {
+  user(id: 4) {
+    id
+    name
+    profilePic(size: $devicePicSize)
+  }
+}`
+
+	expectedOutput = `
+Document
+  ExecutableDefinition
+    OperationDefinition
+      OperationType: query
+      Name: getBozoProfile
+      VariableDefinitions
+        VariableDefinition
+          Variable: devicePicSize
+          Type: Int
+        VariableDefinition
+          Variable: foo
+          Type: bar
+          DefaultValue: 123
+      SelectionSet
+        Field
+          Name: user
+          Arguments
+            Argument
+              Name: id
+              Value: 4
+          SelectionSet
+            Field
+              Name: id
+            Field
+              Name: name
+            Field
+              Name: profilePic
+              Arguments
+                Argument
+                  Name: size
+                  Variable: devicePicSize
+`[1:]
+
+	if err := testPrettyPrinting(input, expectedOutput,
+		input); err != nil {
+		t.Error(err)
+		return
+	}
+
+	input = `
+query withNestedFragments {
+  user(id: 4) {
+    friends(first: 10) {
+      ...friendFields
+    }
+    mutualFriends(first: 10) {
+      ...friendFields
+    }
+  }
+}
+
+fragment friendFields on User {
+  id
+  name
+  ...standardProfilePic
+}
+
+fragment standardProfilePic on User {
+  profilePic(size: 50)
+}`[1:]
+
+	expectedOutput = `
+Document
+  ExecutableDefinition
+    OperationDefinition
+      OperationType: query
+      Name: withNestedFragments
+      SelectionSet
+        Field
+          Name: user
+          Arguments
+            Argument
+              Name: id
+              Value: 4
+          SelectionSet
+            Field
+              Name: friends
+              Arguments
+                Argument
+                  Name: first
+                  Value: 10
+              SelectionSet
+                FragmentSpread: friendFields
+            Field
+              Name: mutualFriends
+              Arguments
+                Argument
+                  Name: first
+                  Value: 10
+              SelectionSet
+                FragmentSpread: friendFields
+  ExecutableDefinition
+    FragmentDefinition
+      FragmentName: friendFields
+      TypeCondition: User
+      SelectionSet
+        Field
+          Name: id
+        Field
+          Name: name
+        FragmentSpread: standardProfilePic
+  ExecutableDefinition
+    FragmentDefinition
+      FragmentName: standardProfilePic
+      TypeCondition: User
+      SelectionSet
+        Field
+          Name: profilePic
+          Arguments
+            Argument
+              Name: size
+              Value: 50
+`[1:]
+
+	if err := testPrettyPrinting(input, expectedOutput,
+		input); err != nil {
+		t.Error(err)
+		return
+	}
+
+	input = `
+query inlineFragmentTyping {
+  profiles(handles: ["zuck", "cocacola"]) {
+    handle
+    ... on User {
+      friends {
+        count
+      }
+    }
+    ... on Page {
+      likers {
+        count
+      }
+    }
+  }
+}`[1:]
+
+	expectedOutput = `
+Document
+  ExecutableDefinition
+    OperationDefinition
+      OperationType: query
+      Name: inlineFragmentTyping
+      SelectionSet
+        Field
+          Name: profiles
+          Arguments
+            Argument
+              Name: handles
+              ListValue
+                Value: zuck
+                Value: cocacola
+          SelectionSet
+            Field
+              Name: handle
+            InlineFragment
+              TypeCondition: User
+              SelectionSet
+                Field
+                  Name: friends
+                  SelectionSet
+                    Field
+                      Name: count
+            InlineFragment
+              TypeCondition: Page
+              SelectionSet
+                Field
+                  Name: likers
+                  SelectionSet
+                    Field
+                      Name: count
+`[1:]
+
+	if err := testPrettyPrinting(input, expectedOutput,
+		input); err != nil {
+		t.Error(err)
+		return
+	}
+
+	input = `
+{
+  my : field(size: 4) @include(if: true) @id() @foo(x: 1, y: "z")
+}`[1:]
+
+	expectedOutput = `
+Document
+  ExecutableDefinition
+    OperationDefinition
+      SelectionSet
+        Field
+          Alias: my
+          Name: field
+          Arguments
+            Argument
+              Name: size
+              Value: 4
+          Directives
+            Directive
+              Name: include
+              Arguments
+                Argument
+                  Name: if
+                  Value: true
+            Directive
+              Name: id
+              Arguments
+            Directive
+              Name: foo
+              Arguments
+                Argument
+                  Name: x
+                  Value: 1
+                Argument
+                  Name: y
+                  Value: z
+`[1:]
+
+	if err := testPrettyPrinting(input, expectedOutput,
+		input); err != nil {
+		t.Error(err)
+		return
+	}
+}
+
+func TestErrorCases(t *testing.T) {
+
+	astres, _ := ParseWithRuntime("mytest", `{ a }`, &TestRuntimeProvider{})
+	astres.Children[0].Name = "foo"
+	_, err := PrettyPrint(astres)
+
+	if err == nil || err.Error() != "Could not find template for foo (tempkey: foo_1)" {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	astres, err = ParseWithRuntime("mytest", `{ a(b:"""a"a""" x:""){ d} }`, &TestRuntimeProvider{})
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	pp, _ := PrettyPrint(astres)
+
+	astres, err = ParseWithRuntime("mytest", pp, &TestRuntimeProvider{})
+	if err != nil {
+		t.Error(err)
+		return
+	}
+
+	if astres.String() != `
+Document
+  ExecutableDefinition
+    OperationDefinition
+      SelectionSet
+        Field
+          Name: a
+          Arguments
+            Argument
+              Name: b
+              Value: a"a
+            Argument
+              Name: x
+              Value: 
+          SelectionSet
+            Field
+              Name: d
+`[1:] {
+		t.Error("Unexpected result:", astres)
+		return
+	}
+}
+
+func testPPOut(input string) (string, error) {
+	var ppres string
+
+	astres, err := ParseWithRuntime("mytest", input, &TestRuntimeProvider{})
+
+	if err == nil {
+		ppres, err = PrettyPrint(astres)
+	}
+
+	return ppres, err
+}
+
+func testPrettyPrinting(input, astOutput, ppOutput string) error {
+
+	astres, err := ParseWithRuntime("mytest", input, &TestRuntimeProvider{})
+	if err != nil || fmt.Sprint(astres) != astOutput {
+		return fmt.Errorf("Unexpected parser output:\n%v expected was:\n%v Error: %v", astres, astOutput, err)
+	}
+
+	ppres, err := PrettyPrint(astres)
+	if err != nil || ppres != ppOutput {
+		fmt.Fprintf(os.Stderr, "#\n%v#", ppres)
+		return fmt.Errorf("Unexpected result:\n%v\nError: %v", ppres, err)
+	}
+
+	// Make sure the pretty printed result is valid and gets the same parse tree
+
+	astres2, err := ParseWithRuntime("mytest", ppres, &TestRuntimeProvider{})
+	if err != nil || fmt.Sprint(astres2) != astOutput {
+		return fmt.Errorf("Unexpected parser output from pretty print string:\n%v expected was:\n%v Error: %v", astres2, astOutput, err)
+	}
+
+	return nil
+}