Skip to content

Commit

Permalink
First cut of qa auto
Browse files Browse the repository at this point in the history
  • Loading branch information
ajbouh committed Jul 24, 2016
1 parent 978f98f commit 497e4eb
Show file tree
Hide file tree
Showing 3 changed files with 290 additions and 0 deletions.
1 change: 1 addition & 0 deletions Gimmefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"vendor/go-bindata:go-bindata",

// For testing.
"vendor/watchman:watchman",
"vendor/ruby:ruby",
"vendor/minitest:minitest",
"vendor/test-unit:test-unit",
Expand Down
140 changes: 140 additions & 0 deletions src/qa/cmd/auto/auto.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package auto

import (
"flag"
"fmt"
"os"
"os/signal"
"path/filepath"
"qa/cmd"
"qa/cmd/run"
"qa/fileevents"
"qa/runner"
"syscall"
)

// When watchman emits an event, execute a qa run for that test
// Expect:
// {
// "version": "1.6",
// "subscribe": "mysubscriptionname"
// }
type eventFileLister fileevents.Event

func (s eventFileLister) Dir() string {
return s.Root
}

func (s eventFileLister) Patterns() []string {
var names []string
for _, file := range s.Files {
names = append(names, file.Name)
}
return names
}

func (s eventFileLister) ListFiles() ([]string, error) {
return s.Patterns(), nil
}

func pruneRunEnvForFiles(
runEnv *run.Env,
runnerConfig runner.Config,
event *fileevents.Event) *run.Env {

pruned := *runEnv
runnerConfig.FileLister = eventFileLister(*event)
pruned.RunnerConfigs = []runner.Config{
runnerConfig,
}

return &pruned
}

func subscribeToRunnerConfigFiles(watcher fileevents.Watcher, runnerConfig runner.Config) (*fileevents.Subscription, error) {
dir, err := filepath.Abs(runnerConfig.FileLister.Dir())
if err != nil {
return nil, err
}

dir, err = filepath.EvalSymlinks(dir)
if err != nil {
return nil, err
}

expr := map[string](interface{}){
"expression": []string{"match", runnerConfig.FileLister.Patterns()[0], "wholename"},
"fields": []string{"name", "new", "exists"},
"defer_vcs": true,
}

return watcher.Subscribe(dir, "tests", expr)
}

func Main(env *cmd.Env, args []string) error {
watcher, err := fileevents.StartWatchman("/tmp/watchman")
if err != nil {
return err
}
defer watcher.Close()

flags := flag.NewFlagSet("auto", flag.ContinueOnError)

runFlags := run.DefineFlags(flags)
err = flags.Parse(args)
if err != nil {
return err
}

runEnv, err := runFlags.NewEnv(env, flags.Args())
if err != nil {
return err
}

srv := runEnv.Server
defer srv.Close()
go srv.Run()

runEnvChan := make(chan *run.Env)

// Handle common process-killing signals so we can gracefully shut down:
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, os.Interrupt, os.Kill, syscall.SIGTERM)
go func(c chan os.Signal) {
// Wait for signal
sig, ok := <-c
if ok {
fmt.Fprintln(env.Stderr, "Got signal:", sig)
srv.Close()
watcher.Close()
close(runEnvChan)
}
}(sigc)
defer signal.Stop(sigc)
defer close(sigc)

for _, runnerConfig := range runEnv.RunnerConfigs {
sub, err := subscribeToRunnerConfigFiles(watcher, runnerConfig)
if err != nil {
return nil
}
defer sub.Close()

go func(sub *fileevents.Subscription, runnerConfig runner.Config) {
for event := range sub.Events {
runEnvChan <- pruneRunEnvForFiles(runEnv, runnerConfig, event)
}
}(sub, runnerConfig)
}

// Figure out which file changed. If file that changed matches the existing glob,
// start running that test.
for runEnv := range runEnvChan {
_, err := run.Run(runEnv)
if err != nil {
return err
}
}

return nil
}
149 changes: 149 additions & 0 deletions src/qa/fileevents/watchman.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package fileevents

import (
"encoding/json"
"fmt"
"io"
"net"
"os"
"os/exec"
"syscall"
"time"
)

type Event struct {
Version string `json:"version"`
Clock string `json:"clock"`
Files []File `json:"files"`
Root string `json:"root"`
Subscription string `json:"subscription"`
}

type File struct {
Name string `json:"name"`
New bool `json:"new"`
Exists bool `json:"exists"`
}

type Subscription struct {
Events chan *Event
closer io.Closer
}

func (s *Subscription) Close() error {
return s.closer.Close()
}

type Watcher interface {
Subscribe(root string, name string, expr interface{}) (*Subscription, error)
Close() error
}

func tolerantDial(sockname string) (net.Conn, error) {
for {
conn, err := net.Dial("unix", sockname)
if err == nil {
return conn, err
}

opErr, ok := err.(*net.OpError)
if !ok {
return conn, err
}

syscallError, ok := opErr.Err.(*os.SyscallError)
if !ok {
return conn, err
}

switch syscallError.Err {
case syscall.ECONNREFUSED:
time.Sleep(50 * time.Millisecond)
continue
case syscall.ENOENT:
time.Sleep(50 * time.Millisecond)
continue
case syscall.ENODATA:
time.Sleep(50 * time.Millisecond)
continue
}

fmt.Fprintf(os.Stderr, "Saw error %#v\n", syscallError)
return conn, err
}
}

func StartWatchman(sockname string) (Watcher, error) {
cmd := exec.Command(
"watchman",
"--no-save-state",
"--foreground",
"--sockname", sockname,
"--logfile", "/dev/stdout")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
err := cmd.Start()
if err != nil {
return nil, err
}

return &WatchmanService{process: cmd.Process, sockname: sockname}, nil
}

type WatchmanService struct {
process *os.Process
sockname string
}

func (w *WatchmanService) Close() error {
err := w.process.Kill()
if err != nil {
_, err = w.process.Wait()
}

return err
}

func (w *WatchmanService) Subscribe(root string, name string, expr interface{}) (*Subscription, error) {
// Open unix socket and keep it open for the future
conn, err := tolerantDial(w.sockname)
if err != nil {
return nil, err
}

encoder := json.NewEncoder(conn)
message := []interface{}{"subscribe", root, name, expr}
err = encoder.Encode(message)
if err != nil {
return nil, err
}

decoder := json.NewDecoder(conn)
// Expect response:
// {
// "version": "1.6",
// "subscribe": "mysubscriptionname"
// }
var response interface{}
err = decoder.Decode(&response)
if err != nil {
return nil, err
}

c := make(chan *Event, 1)
go func() {
defer close(c)
defer conn.Close()

for {
var event Event
err := decoder.Decode(&event)
if err != nil {
break
}
c <- &event
}
}()

return &Subscription{Events: c, closer: conn}, nil
}

0 comments on commit 497e4eb

Please sign in to comment.