Skip to content

Commit

Permalink
Make unfocus ("blur") much faster (#674)
Browse files Browse the repository at this point in the history
- Existing implementation used `gofmt` with a rule (`-r`) to
sequentially remove each type of focus
- Existing implementation would ignore `vendor` directories at the top
level, but not subdirectories
- New implementation parses all Go files and looks for calls to focus
functions. If it finds them, it records their position and removes the
`F` byte from each function call to remove the focus
- This approach was found to be 30 times faster on a test repo
  • Loading branch information
blgm authored May 15, 2020
1 parent 7fdcbe8 commit 8b18061
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 34 deletions.
173 changes: 146 additions & 27 deletions ginkgo/unfocus_command.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package main

import (
"bytes"
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"io"
"io/ioutil"
"os/exec"
"os"
"path/filepath"
"strings"
"sync"
)

func BuildUnfocusCommand() *Command {
Expand All @@ -22,40 +29,152 @@ func BuildUnfocusCommand() *Command {
}

func unfocusSpecs([]string, []string) {
unfocus("Describe")
unfocus("Context")
unfocus("It")
unfocus("Measure")
unfocus("DescribeTable")
unfocus("Entry")
unfocus("Specify")
unfocus("When")
}

func unfocus(component string) {
fmt.Printf("Removing F%s...\n", component)
files, err := ioutil.ReadDir(".")
fmt.Println("Scanning for focus...")

goFiles := make(chan string)
go func() {
unfocusDir(goFiles, ".")
close(goFiles)
}()

const workers = 10
wg := sync.WaitGroup{}
wg.Add(workers)

for i := 0; i < workers; i++ {
go func() {
for path := range goFiles {
unfocusFile(path)
}
wg.Done()
}()
}

wg.Wait()
}

func unfocusDir(goFiles chan string, path string) {
files, err := ioutil.ReadDir(path)
if err != nil {
fmt.Println(err.Error())
return
}

for _, f := range files {
// Exclude "vendor" directory
if f.IsDir() && f.Name() == "vendor" {
continue
switch {
case f.IsDir() && shouldProcessDir(f.Name()):
unfocusDir(goFiles, filepath.Join(path, f.Name()))
case !f.IsDir() && shouldProcessFile(f.Name()):
goFiles <- filepath.Join(path, f.Name())
}
// Exclude non-go files in the current directory
if !f.IsDir() && !strings.HasSuffix(f.Name(), ".go") {
continue
}
}

func shouldProcessDir(basename string) bool {
return basename != "vendor" && !strings.HasPrefix(basename, ".")
}

func shouldProcessFile(basename string) bool {
return strings.HasSuffix(basename, ".go")
}

func unfocusFile(path string) {
data, err := ioutil.ReadFile(path)
if err != nil {
fmt.Printf("error reading file '%s': %s\n", path, err.Error())
return
}

ast, err := parser.ParseFile(token.NewFileSet(), path, bytes.NewReader(data), 0)
if err != nil {
fmt.Printf("error parsing file '%s': %s\n", path, err.Error())
return
}

eliminations := scanForFocus(ast)
if len(eliminations) == 0 {
return
}

fmt.Printf("...updating %s\n", path)
backup, err := writeBackup(path, data)
if err != nil {
fmt.Printf("error creating backup file: %s\n", err.Error())
return
}

if err := updateFile(path, data, eliminations); err != nil {
fmt.Printf("error writing file '%s': %s\n", path, err.Error())
return
}

os.Remove(backup)
}

func writeBackup(path string, data []byte) (string, error) {
t, err := ioutil.TempFile(filepath.Dir(path), filepath.Base(path))

if err != nil {
return "", fmt.Errorf("error creating temporary file: %w", err)
}
defer t.Close()

if _, err := io.Copy(t, bytes.NewReader(data)); err != nil {
return "", fmt.Errorf("error writing to temporary file: %w", err)
}

return t.Name(), nil
}

func updateFile(path string, data []byte, eliminations []int64) error {
to, err := os.Create(path)
if err != nil {
return fmt.Errorf("error opening file for writing '%s': %w\n", path, err)
}
defer to.Close()

from := bytes.NewReader(data)
var cursor int64
for _, byteToEliminate := range eliminations {
if _, err := io.CopyN(to, from, byteToEliminate-cursor); err != nil {
return fmt.Errorf("error copying data: %w", err)
}
// Recursively run `gofmt` otherwise
cmd := exec.Command("gofmt", fmt.Sprintf("-r=F%s -> %s", component, component), "-w", f.Name())
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Println(err.Error())

cursor = byteToEliminate + 1

if _, err := from.Seek(1, io.SeekCurrent); err != nil {
return fmt.Errorf("error seeking to position in buffer: %w", err)
}
if string(out) != "" {
fmt.Println(string(out))
}

if _, err := io.Copy(to, from); err != nil {
return fmt.Errorf("error copying end data: %w", err)
}

return nil
}

func scanForFocus(file *ast.File) (eliminations []int64) {
ast.Inspect(file, func(n ast.Node) bool {
if c, ok := n.(*ast.CallExpr); ok {
if i, ok := c.Fun.(*ast.Ident); ok {
if isFocus(i.Name) {
eliminations = append(eliminations, int64(i.Pos()-file.Pos()))
}
}
}

return true
})

return eliminations
}

func isFocus(name string) bool {
switch name {
case "FDescribe", "FContext", "FIt", "FMeasure", "FDescribeTable", "FEntry", "FSpecify", "FWhen":
return true
default:
return false
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package focused_fixture_test

import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"

"testing"
)

func TestFocused_fixture(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Focused_fixture Suite")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package focused_fixture_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"),
)

Describe("not focused", func() {
It("not focused", func() {

})
})

Context("not focused", func() {
It("not focused", func() {

})
})

It("not focused", func() {

})

Measure("not focused", func(b Benchmarker) {

}, 2)

DescribeTable("not focused",
func() {},
Entry("not focused"),
)
})
14 changes: 7 additions & 7 deletions integration/subcommand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,25 +404,25 @@ var _ = Describe("Subcommand", func() {
It("should unfocus tests", func() {
pathToTest := tmpPath("focused")
fixture := fixturePath("focused_fixture")
copyIn(fixture, pathToTest, false)
copyIn(fixture, pathToTest, true)

session := startGinkgo(pathToTest, "--noColor")
session := startGinkgo(pathToTest, "--noColor", "-r")
Eventually(session).Should(gexec.Exit(types.GINKGO_FOCUS_EXIT_CODE))
output := session.Out.Contents()

Ω(string(output)).Should(ContainSubstring("8 Passed"))
Ω(string(output)).Should(ContainSubstring("5 Skipped"))
Ω(string(output)).Should(ContainSubstring("Detected Programmatic Focus"))

session = startGinkgo(pathToTest, "blur")
Eventually(session).Should(gexec.Exit(0))
output = session.Out.Contents()
Ω(string(output)).ShouldNot(ContainSubstring("expected 'package'"))

session = startGinkgo(pathToTest, "--noColor")
session = startGinkgo(pathToTest, "--noColor", "-r")
Eventually(session).Should(gexec.Exit(0))
output = session.Out.Contents()
Ω(string(output)).Should(ContainSubstring("13 Passed"))
Ω(string(output)).Should(ContainSubstring("0 Skipped"))
Ω(string(output)).Should(ContainSubstring("Ginkgo ran 2 suites"))
Ω(string(output)).Should(ContainSubstring("Test Suite Passed"))
Ω(string(output)).ShouldNot(ContainSubstring("Detected Programmatic Focus"))

Expect(sameFile(filepath.Join(pathToTest, "README.md"), filepath.Join(fixture, "README.md"))).To(BeTrue())
})
Expand Down

0 comments on commit 8b18061

Please sign in to comment.