Go’s Testable Examples under the hood

Hidden introduction to ast and parser packages

Michał Łowicki
golangspec
Published in
5 min readOct 10, 2016

--

Golang’s toolchain implements feature called Testable Examples. If name doesn’t tell you much I strongly recommend to read first “Testable Examples in Go” as a gentle introduction. Throughput this post we’ll see what underpins the whole solution and how to build its simplified version.

Let’s see how Testable Examples work:

upper_test.go:

Examples just like test functions are placed in xxx_test.go files but are prefixed with Example instead of Test. Command go test uses comments in special format (Output: something) and compare them against captured data, normally written to stdout. The same comments are used by other tools like godoc to enrich automatically generated documentation.

The question is how go test or godoc are able to extract data from dedicated comments? Is there any secret mechanism in the language making it possible? Or maybe everything can be achieved with well-known constructions?

It turns out that standard library ships elements (spread across few packages) related to parsing source code in Go itself. These tools produce abstract syntax trees and provide access i.e. to comments left by programmer.

Abstract syntax tree (AST)

It’s a representation of elements found in the source code while parsing. Let’s consider a simple expression:

AST can be generated with snippet:

which outputs:

Output can be simplified using diagram where actual tree is more visible:

Two standard packages are crucial while working with ASTs:

  • parser supplies machinery for parsing source code written in Go
  • ast implements primitives for working with ASTs for code in Go

Normally during lexical analysis comments are removed. There is a special flag to preserve comments and put them into AST — parser.ParseComments:

3rd parameter to parser.ParseFile is an optional source code passed f.ex. as string or io.Reader. Since I’ve used file from disk it’s set to nil.

t.go:

Output:

Comment group

It’s a sequence of comments with no elements in between. In above example comments “a” and “b” belong to the same group.

Pos & Position

Position of elements within source code are recorded using Pos type (its more verbose counterpart is Position). It’s a single integer value which encodes information like line or column but Position struct keeps them in separate fields. By adding in outer loop line:

program additionally outputs:

Fileset

Positions are calculated relatively to set of parsed files. Every file has assigned disjoint range and each position sits in one of these ranges. In our case we’ve only one but the whole set is required to decode Pos:

Tree traversal

Package ast provides a convenient function for traversing AST in depth-first order:

Since we know how to extract all comments, now it’s time to find all top-level ExampleXXX functions.

doc.Examples

Package doc provides function which does exactly what we need:

e.go:

Output:

doc.Examples doesn’t have any magical skills. It relies on what we already seen so mainly building and traversing abstract syntax tree. Let’s build something similar:

Output:

Comments aren’t regular nodes of AST tree. They’re accessible through Comments field of ast.File (which is returned by f.ex. parser.ParseFile). Order of comments on this list is the same as they appear in the source code. To find comments inside certain block we need to compare positions like in findExampleOutput above:

Condition inside if statement checks if comment group falls into block’s range.

As we see higher up the standard library gives great support in parsing. Utilities there made the whole work really pleasant and crafted code is compact.

If you like the post and want to get updates about new ones please follow me. Help others discover this material by clicking ❤ below.

Resources

--

--

Michał Łowicki
golangspec

Software engineer at Datadog, previously at Facebook and Opera, never satisfied.