diff --git a/ginkgo/main.go b/ginkgo/main.go index f60c48a72..ac725bf40 100644 --- a/ginkgo/main.go +++ b/ginkgo/main.go @@ -111,6 +111,11 @@ will output an executable file named `package.test`. This can be run directly o ginkgo + +To print an outline of Ginkgo specs and containers in a file: + + gingko outline + To print out Ginkgo's version: ginkgo version @@ -172,6 +177,7 @@ func init() { Commands = append(Commands, BuildUnfocusCommand()) Commands = append(Commands, BuildVersionCommand()) Commands = append(Commands, BuildHelpCommand()) + Commands = append(Commands, BuildOutlineCommand()) } func main() { diff --git a/ginkgo/outline/_testdata/focused_test.go b/ginkgo/outline/_testdata/focused_test.go new file mode 100644 index 000000000..3e56b581f --- /dev/null +++ b/ginkgo/outline/_testdata/focused_test.go @@ -0,0 +1,48 @@ +package example_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" +) + +var _ = Describe("FocusedFixture", func() { + FDescribe("focused", func() { + It("focused", func() { + + }) + }) + + FContext("focused", func() { + It("focused", func() { + + }) + }) + + FWhen("focused", func() { + It("focused", func() { + + }) + }) + + FIt("focused", func() { + + }) + + FSpecify("focused", func() { + + }) + + FMeasure("focused", func(b Benchmarker) { + + }, 2) + + FDescribeTable("focused", + func() {}, + Entry("focused"), + ) + + DescribeTable("focused", + func() {}, + FEntry("focused"), + ) +}) diff --git a/ginkgo/outline/_testdata/focused_test_outline.csv b/ginkgo/outline/_testdata/focused_test_outline.csv new file mode 100644 index 000000000..49bddba88 --- /dev/null +++ b/ginkgo/outline/_testdata/focused_test_outline.csv @@ -0,0 +1,11 @@ +Name,Text,Start,End,Spec,Focused,Pending +Describe,FocusedFixture,116,596,false,false,false +FDescribe,focused,153,217,false,true,false +It,focused,185,213,true,false,false +FContext,focused,220,283,false,true,false +It,focused,251,279,true,false,false +FWhen,focused,286,346,false,true,false +It,focused,314,342,true,false,false +FIt,focused,349,377,true,true,false +FSpecify,focused,380,413,true,true,false +FMeasure,focused,416,465,true,true,false diff --git a/ginkgo/outline/_testdata/focused_test_outline.json b/ginkgo/outline/_testdata/focused_test_outline.json new file mode 100644 index 000000000..f265139b5 --- /dev/null +++ b/ginkgo/outline/_testdata/focused_test_outline.json @@ -0,0 +1,100 @@ +[ + { + "name": "Describe", + "text": "FocusedFixture", + "start": 116, + "end": 596, + "spec": false, + "focused": false, + "pending": false, + "nodes": [ + { + "name": "FDescribe", + "text": "focused", + "start": 153, + "end": 217, + "spec": false, + "focused": true, + "pending": false, + "nodes": [ + { + "name": "It", + "text": "focused", + "start": 185, + "end": 213, + "spec": true, + "focused": false, + "pending": false + } + ] + }, + { + "name": "FContext", + "text": "focused", + "start": 220, + "end": 283, + "spec": false, + "focused": true, + "pending": false, + "nodes": [ + { + "name": "It", + "text": "focused", + "start": 251, + "end": 279, + "spec": true, + "focused": false, + "pending": false + } + ] + }, + { + "name": "FWhen", + "text": "focused", + "start": 286, + "end": 346, + "spec": false, + "focused": true, + "pending": false, + "nodes": [ + { + "name": "It", + "text": "focused", + "start": 314, + "end": 342, + "spec": true, + "focused": false, + "pending": false + } + ] + }, + { + "name": "FIt", + "text": "focused", + "start": 349, + "end": 377, + "spec": true, + "focused": true, + "pending": false + }, + { + "name": "FSpecify", + "text": "focused", + "start": 380, + "end": 413, + "spec": true, + "focused": true, + "pending": false + }, + { + "name": "FMeasure", + "text": "focused", + "start": 416, + "end": 465, + "spec": true, + "focused": true, + "pending": false + } + ] + } +] diff --git a/ginkgo/outline/_testdata/normal_test.go b/ginkgo/outline/_testdata/normal_test.go new file mode 100644 index 000000000..39cdd67e0 --- /dev/null +++ b/ginkgo/outline/_testdata/normal_test.go @@ -0,0 +1,48 @@ +package example_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" +) + +var _ = Describe("NormalFixture", func() { + Describe("normal", func() { + It("normal", func() { + + }) + }) + + Context("normal", func() { + It("normal", func() { + + }) + }) + + When("normal", func() { + It("normal", func() { + + }) + }) + + It("normal", func() { + + }) + + Specify("normal", func() { + + }) + + Measure("normal", func(b Benchmarker) { + + }, 2) + + DescribeTable("normal", + func() {}, + Entry("normal"), + ) + + DescribeTable("normal", + func() {}, + Entry("normal"), + ) +}) diff --git a/ginkgo/outline/_testdata/normal_test_outline.csv b/ginkgo/outline/_testdata/normal_test_outline.csv new file mode 100644 index 000000000..7938056b5 --- /dev/null +++ b/ginkgo/outline/_testdata/normal_test_outline.csv @@ -0,0 +1,11 @@ +Name,Text,Start,End,Spec,Focused,Pending +Describe,NormalFixture,116,574,false,false,false +Describe,normal,152,213,false,false,false +It,normal,182,209,true,false,false +Context,normal,216,276,false,false,false +It,normal,245,272,true,false,false +When,normal,279,336,false,false,false +It,normal,305,332,true,false,false +It,normal,339,365,true,false,false +Specify,normal,368,399,true,false,false +Measure,normal,402,449,true,false,false diff --git a/ginkgo/outline/_testdata/normal_test_outline.json b/ginkgo/outline/_testdata/normal_test_outline.json new file mode 100644 index 000000000..e81637f24 --- /dev/null +++ b/ginkgo/outline/_testdata/normal_test_outline.json @@ -0,0 +1,100 @@ +[ + { + "name": "Describe", + "text": "NormalFixture", + "start": 116, + "end": 574, + "spec": false, + "focused": false, + "pending": false, + "nodes": [ + { + "name": "Describe", + "text": "normal", + "start": 152, + "end": 213, + "spec": false, + "focused": false, + "pending": false, + "nodes": [ + { + "name": "It", + "text": "normal", + "start": 182, + "end": 209, + "spec": true, + "focused": false, + "pending": false + } + ] + }, + { + "name": "Context", + "text": "normal", + "start": 216, + "end": 276, + "spec": false, + "focused": false, + "pending": false, + "nodes": [ + { + "name": "It", + "text": "normal", + "start": 245, + "end": 272, + "spec": true, + "focused": false, + "pending": false + } + ] + }, + { + "name": "When", + "text": "normal", + "start": 279, + "end": 336, + "spec": false, + "focused": false, + "pending": false, + "nodes": [ + { + "name": "It", + "text": "normal", + "start": 305, + "end": 332, + "spec": true, + "focused": false, + "pending": false + } + ] + }, + { + "name": "It", + "text": "normal", + "start": 339, + "end": 365, + "spec": true, + "focused": false, + "pending": false + }, + { + "name": "Specify", + "text": "normal", + "start": 368, + "end": 399, + "spec": true, + "focused": false, + "pending": false + }, + { + "name": "Measure", + "text": "normal", + "start": 402, + "end": 449, + "spec": true, + "focused": false, + "pending": false + } + ] + } +] diff --git a/ginkgo/outline/_testdata/pending_test.go b/ginkgo/outline/_testdata/pending_test.go new file mode 100644 index 000000000..babd0078c --- /dev/null +++ b/ginkgo/outline/_testdata/pending_test.go @@ -0,0 +1,48 @@ +package example_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" +) + +var _ = Describe("PendingFixture", func() { + PDescribe("pending", func() { + It("pending", func() { + + }) + }) + + PContext("pending", func() { + It("pending", func() { + + }) + }) + + PWhen("pending", func() { + It("pending", func() { + + }) + }) + + PIt("pending", func() { + + }) + + PSpecify("pending", func() { + + }) + + PMeasure("pending", func(b Benchmarker) { + + }, 2) + + PDescribeTable("pending", + func() {}, + Entry("pending"), + ) + + DescribeTable("pending", + func() {}, + PEntry("pending"), + ) +}) diff --git a/ginkgo/outline/_testdata/pending_test_outline.csv b/ginkgo/outline/_testdata/pending_test_outline.csv new file mode 100644 index 000000000..f88cdbd09 --- /dev/null +++ b/ginkgo/outline/_testdata/pending_test_outline.csv @@ -0,0 +1,11 @@ +Name,Text,Start,End,Spec,Focused,Pending +Describe,PendingFixture,116,596,false,false,false +PDescribe,pending,153,217,false,false,true +It,pending,185,213,true,false,true +PContext,pending,220,283,false,false,true +It,pending,251,279,true,false,true +PWhen,pending,286,346,false,false,true +It,pending,314,342,true,false,true +PIt,pending,349,377,true,false,true +PSpecify,pending,380,413,true,false,true +PMeasure,pending,416,465,true,false,true diff --git a/ginkgo/outline/_testdata/pending_test_outline.json b/ginkgo/outline/_testdata/pending_test_outline.json new file mode 100644 index 000000000..719b1c96d --- /dev/null +++ b/ginkgo/outline/_testdata/pending_test_outline.json @@ -0,0 +1,100 @@ +[ + { + "name": "Describe", + "text": "PendingFixture", + "start": 116, + "end": 596, + "spec": false, + "focused": false, + "pending": false, + "nodes": [ + { + "name": "PDescribe", + "text": "pending", + "start": 153, + "end": 217, + "spec": false, + "focused": false, + "pending": true, + "nodes": [ + { + "name": "It", + "text": "pending", + "start": 185, + "end": 213, + "spec": true, + "focused": false, + "pending": true + } + ] + }, + { + "name": "PContext", + "text": "pending", + "start": 220, + "end": 283, + "spec": false, + "focused": false, + "pending": true, + "nodes": [ + { + "name": "It", + "text": "pending", + "start": 251, + "end": 279, + "spec": true, + "focused": false, + "pending": true + } + ] + }, + { + "name": "PWhen", + "text": "pending", + "start": 286, + "end": 346, + "spec": false, + "focused": false, + "pending": true, + "nodes": [ + { + "name": "It", + "text": "pending", + "start": 314, + "end": 342, + "spec": true, + "focused": false, + "pending": true + } + ] + }, + { + "name": "PIt", + "text": "pending", + "start": 349, + "end": 377, + "spec": true, + "focused": false, + "pending": true + }, + { + "name": "PSpecify", + "text": "pending", + "start": 380, + "end": 413, + "spec": true, + "focused": false, + "pending": true + }, + { + "name": "PMeasure", + "text": "pending", + "start": 416, + "end": 465, + "spec": true, + "focused": false, + "pending": true + } + ] + } +] diff --git a/ginkgo/outline/_testdata/suite_test.go b/ginkgo/outline/_testdata/suite_test.go new file mode 100644 index 000000000..e28786a5a --- /dev/null +++ b/ginkgo/outline/_testdata/suite_test.go @@ -0,0 +1,13 @@ +package example_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestExample(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Example Suite") +} diff --git a/ginkgo/outline/_testdata/suite_test_outline.json b/ginkgo/outline/_testdata/suite_test_outline.json new file mode 100644 index 000000000..0637a088a --- /dev/null +++ b/ginkgo/outline/_testdata/suite_test_outline.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/ginkgo/outline/outline.go b/ginkgo/outline/outline.go new file mode 100644 index 000000000..8f82ee853 --- /dev/null +++ b/ginkgo/outline/outline.go @@ -0,0 +1,198 @@ +package outline + +import ( + "encoding/json" + "fmt" + "go/ast" + "go/token" + "strconv" + "strings" + + "golang.org/x/tools/go/ast/inspector" +) + +const ( + // undefinedTextAlt is used if the spec/container text cannot be derived + undefinedTextAlt = "undefined" +) + +// ginkgoMetadata holds useful bits of information for every entry in the outline +type ginkgoMetadata struct { + // Name is the spec or container function name, e.g. `Describe` or `It` + Name string `json:"name"` + + // Text is the `text` argument passed to specs, and some containers + Text string `json:"text"` + + // Start is the position of first character of the spec or container block + Start token.Pos `json:"start"` + + // End is the position of first character immediately after the spec or container block + End token.Pos `json:"end"` + + Spec bool `json:"spec"` + Focused bool `json:"focused"` + Pending bool `json:"pending"` +} + +// ginkgoNode is used to construct the outline as a tree +type ginkgoNode struct { + ginkgoMetadata + Nodes []*ginkgoNode `json:"nodes,omitempty"` +} + +type walkFunc func(n *ginkgoNode) + +func (n *ginkgoNode) Walk(f walkFunc) { + f(n) + for _, m := range n.Nodes { + m.Walk(f) + } +} + +// ginkgoNodeFromCallExpr derives an outline entry from a go AST subtree +// corresponding to a Ginkgo container or spec. +func ginkgoNodeFromCallExpr(ce *ast.CallExpr) (*ginkgoNode, bool) { + id, ok := ce.Fun.(*ast.Ident) + if !ok { + return nil, false + } + + n := ginkgoNode{} + n.Name = id.Name + n.Start = ce.Pos() + n.End = ce.End() + // TODO: Handle nodot and alias imports of the ginkgo package. + // The below assumes dot imports . + switch id.Name { + case "It", "Measure", "Specify": + n.Spec = true + n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt) + case "FIt", "FMeasure", "FSpecify": + n.Spec = true + n.Focused = true + n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt) + case "PIt", "PMeasure", "PSpecify", "XIt", "XMeasure", "XSpecify": + n.Spec = true + n.Pending = true + n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt) + case "Context", "Describe", "When": + n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt) + case "FContext", "FDescribe", "FWhen": + n.Focused = true + n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt) + case "PContext", "PDescribe", "PWhen", "XContext", "XDescribe", "XWhen": + n.Pending = true + n.Text = textOrAltFromCallExpr(ce, undefinedTextAlt) + case "By": + case "AfterEach", "BeforeEach": + case "JustAfterEach", "JustBeforeEach": + case "AfterSuite", "BeforeSuite": + case "SynchronizedAfterSuite", "SynchronizedBeforeSuite": + default: + return nil, false + } + return &n, true +} + +// textOrAltFromCallExpr tries to derive the "text" of a Ginkgo spec or +// container. If it cannot derive it, it returns the alt text. +func textOrAltFromCallExpr(ce *ast.CallExpr, alt string) string { + text, defined := textFromCallExpr(ce) + if !defined { + return alt + } + return text +} + +// textFromCallExpr tries to derive the "text" of a Ginkgo spec or container. If +// it cannot derive it, it returns false. +func textFromCallExpr(ce *ast.CallExpr) (string, bool) { + if len(ce.Args) < 1 { + return "", false + } + text, ok := ce.Args[0].(*ast.BasicLit) + if !ok { + return "", false + } + switch text.Kind { + case token.CHAR, token.STRING: + // For token.CHAR and token.STRING, Value is quoted + unquoted, err := strconv.Unquote(text.Value) + if err != nil { + // If unquoting fails, just use the raw Value + return text.Value, true + } + return unquoted, true + default: + return text.Value, true + } +} + +// FromASTFile returns an outline for a Ginkgo test source file +func FromASTFile(src *ast.File) (*outline, error) { + root := ginkgoNode{ + Nodes: []*ginkgoNode{}, + } + stack := []*ginkgoNode{&root} + + ispr := inspector.New([]*ast.File{src}) + ispr.Nodes([]ast.Node{(*ast.CallExpr)(nil)}, func(node ast.Node, push bool) bool { + ce, ok := node.(*ast.CallExpr) + if !ok { + // Because `Nodes` calls this function only when the node is an + // ast.CallExpr, this should never happen + panic(fmt.Errorf("node starting at %d, ending at %d is not an *ast.CallExpr", node.Pos(), node.End())) + } + gn, ok := ginkgoNodeFromCallExpr(ce) + if !ok { + // Not a Ginkgo call, continue + return true + } + + // Visiting this node on the way down + if push { + parent := stack[len(stack)-1] + if parent.Pending { + gn.Pending = true + } + // TODO: Update focused based on ginkgo behavior: + // > Nested programmatically focused specs follow a simple rule: if + // > a leaf-node is marked focused, any of its ancestor nodes that + // > are marked focus will be unfocused. + parent.Nodes = append(parent.Nodes, gn) + + stack = append(stack, gn) + return true + } + // Visiting node on the way up + stack = stack[0 : len(stack)-1] + return true + }) + + return &outline{ + outerNodes: root.Nodes, + }, nil +} + +type outline struct { + outerNodes []*ginkgoNode +} + +func (o *outline) MarshalJSON() ([]byte, error) { + return json.Marshal(o.outerNodes) +} + +// String returns a CSV-formatted outline. Spec or container are output in +// depth-first order. +func (o *outline) String() string { + var b strings.Builder + b.WriteString("Name,Text,Start,End,Spec,Focused,Pending\n") + f := func(n *ginkgoNode) { + b.WriteString(fmt.Sprintf("%s,%s,%d,%d,%t,%t,%t\n", n.Name, n.Text, n.Start, n.End, n.Spec, n.Focused, n.Pending)) + } + for _, n := range o.outerNodes { + n.Walk(f) + } + return b.String() +} diff --git a/ginkgo/outline/outline_suite_test.go b/ginkgo/outline/outline_suite_test.go new file mode 100644 index 000000000..4cceae9f6 --- /dev/null +++ b/ginkgo/outline/outline_suite_test.go @@ -0,0 +1,13 @@ +package outline_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestOutline(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Outline Suite") +} diff --git a/ginkgo/outline/outline_test.go b/ginkgo/outline/outline_test.go new file mode 100644 index 000000000..4d6b62d46 --- /dev/null +++ b/ginkgo/outline/outline_test.go @@ -0,0 +1,46 @@ +package outline + +import ( + "encoding/json" + "go/parser" + "go/token" + "io/ioutil" + "log" + "path/filepath" + + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = DescribeTable("Validate outline from file with", + func(srcFilename, jsonOutlineFilename, csvOutlineFilename string) { + fset := token.NewFileSet() + astFile, err := parser.ParseFile(fset, filepath.Join("_testdata", srcFilename), nil, 0) + Expect(err).To(BeNil(), "error parsing source: %s", err) + + if err != nil { + log.Fatalf("error parsing source: %s", err) + } + + o, err := FromASTFile(astFile) + Expect(err).To(BeNil(), "error creating outline: %s", err) + + gotJSON, err := json.MarshalIndent(o, "", " ") + Expect(err).To(BeNil(), "error marshalling outline to json: %s", err) + + wantJSON, err := ioutil.ReadFile(filepath.Join("_testdata", jsonOutlineFilename)) + Expect(err).To(BeNil(), "error reading JSON outline fixture: %s", err) + + Expect(gotJSON).To(MatchJSON(wantJSON)) + + gotCSV := o.String() + + wantCSV, err := ioutil.ReadFile(filepath.Join("_testdata", csvOutlineFilename)) + Expect(err).To(BeNil(), "error reading CSV outline fixture: %s", err) + + Expect(gotCSV).To(Equal(string(wantCSV))) + }, + Entry("normal containers and specs", "normal_test.go", "normal_test_outline.json", "normal_test_outline.csv"), + Entry("focused containers and specs", "focused_test.go", "focused_test_outline.json", "focused_test_outline.csv"), + Entry("pending containers and specs", "pending_test.go", "pending_test_outline.json", "pending_test_outline.csv"), +) diff --git a/ginkgo/outline_command.go b/ginkgo/outline_command.go new file mode 100644 index 000000000..bc51514c4 --- /dev/null +++ b/ginkgo/outline_command.go @@ -0,0 +1,70 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "go/parser" + "go/token" + "os" + + "github.com/onsi/ginkgo/ginkgo/outline" +) + +func BuildOutlineCommand() *Command { + const defaultFormat = "csv" + var format string + flagSet := flag.NewFlagSet("outline", flag.ExitOnError) + flagSet.StringVar(&format, "format", defaultFormat, "Format of outline. Accepted: 'csv', 'json'") + return &Command{ + Name: "outline", + FlagSet: flagSet, + UsageCommand: "ginkgo outline ", + Usage: []string{ + "Outline of Ginkgo symbols for the file", + }, + Command: func(args []string, additionalArgs []string) { + outlineFile(args, format) + }, + } +} + +func outlineFile(args []string, format string) { + if len(args) != 1 { + println("usage: ginkgo outline ") + os.Exit(1) + } + + filename := args[0] + fset := token.NewFileSet() + + src, err := parser.ParseFile(fset, filename, nil, 0) + if err != nil { + println(fmt.Sprintf("error parsing source: %s", err)) + os.Exit(1) + } + + o, err := outline.FromASTFile(src) + if err != nil { + println(fmt.Sprintf("error creating outline: %s", err)) + os.Exit(1) + } + + var oerr error + switch format { + case "csv": + _, oerr = fmt.Print(o) + case "json": + b, err := json.Marshal(o) + if err != nil { + println(fmt.Sprintf("error marshalling to json: %s", err)) + } + _, oerr = fmt.Println(string(b)) + default: + complainAndQuit(fmt.Sprintf("format %s not accepted", format)) + } + if oerr != nil { + println(fmt.Sprintf("error writing outline: %s", oerr)) + os.Exit(1) + } +}