github.com/weaveworks/common@v0.0.0-20230728070032-dd9e68f319d5/tools/runner/runner.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"net/http"
     8  	"net/url"
     9  	"os"
    10  	"os/exec"
    11  	"sort"
    12  	"strconv"
    13  	"strings"
    14  	"sync"
    15  	"time"
    16  
    17  	"github.com/mgutz/ansi"
    18  	"github.com/weaveworks/common/mflag"
    19  )
    20  
    21  const (
    22  	defaultSchedulerHost = "positive-cocoa-90213.appspot.com"
    23  	jsonContentType      = "application/json"
    24  )
    25  
    26  var (
    27  	start = ansi.ColorCode("black+ub")
    28  	fail  = ansi.ColorCode("red+b")
    29  	succ  = ansi.ColorCode("green+b")
    30  	reset = ansi.ColorCode("reset")
    31  
    32  	schedulerHost = defaultSchedulerHost
    33  	useScheduler  = false
    34  	runParallel   = false
    35  	verbose       = false
    36  	timeout       = 180 // In seconds. Three minutes ought to be enough for any test
    37  
    38  	consoleLock = sync.Mutex{}
    39  )
    40  
    41  type test struct {
    42  	name  string
    43  	hosts int
    44  }
    45  
    46  type schedule struct {
    47  	Tests []string `json:"tests"`
    48  }
    49  
    50  type result struct {
    51  	test
    52  	errored bool
    53  	hosts   []string
    54  }
    55  
    56  type tests []test
    57  
    58  func (ts tests) Len() int      { return len(ts) }
    59  func (ts tests) Swap(i, j int) { ts[i], ts[j] = ts[j], ts[i] }
    60  func (ts tests) Less(i, j int) bool {
    61  	if ts[i].hosts != ts[j].hosts {
    62  		return ts[i].hosts < ts[j].hosts
    63  	}
    64  	return ts[i].name < ts[j].name
    65  }
    66  
    67  func (ts *tests) pick(available int) (test, bool) {
    68  	// pick the first test that fits in the available hosts
    69  	for i, test := range *ts {
    70  		if test.hosts <= available {
    71  			*ts = append((*ts)[:i], (*ts)[i+1:]...)
    72  			return test, true
    73  		}
    74  	}
    75  
    76  	return test{}, false
    77  }
    78  
    79  func (t test) run(hosts []string) bool {
    80  	consoleLock.Lock()
    81  	fmt.Printf("%s>>> Running %s on %s%s\n", start, t.name, hosts, reset)
    82  	consoleLock.Unlock()
    83  
    84  	var out bytes.Buffer
    85  
    86  	cmd := exec.Command(t.name)
    87  	cmd.Env = os.Environ()
    88  	cmd.Stdout = &out
    89  	cmd.Stderr = &out
    90  
    91  	// replace HOSTS in env
    92  	for i, env := range cmd.Env {
    93  		if strings.HasPrefix(env, "HOSTS") {
    94  			cmd.Env[i] = fmt.Sprintf("HOSTS=%s", strings.Join(hosts, " "))
    95  			break
    96  		}
    97  	}
    98  
    99  	start := time.Now()
   100  	var err error
   101  
   102  	c := make(chan error, 1)
   103  	go func() { c <- cmd.Run() }()
   104  	select {
   105  	case err = <-c:
   106  	case <-time.After(time.Duration(timeout) * time.Second):
   107  		err = fmt.Errorf("timed out")
   108  	}
   109  
   110  	duration := float64(time.Since(start)) / float64(time.Second)
   111  
   112  	consoleLock.Lock()
   113  	if err != nil {
   114  		fmt.Printf("%s>>> Test %s finished after %0.1f secs with error: %v%s\n", fail, t.name, duration, err, reset)
   115  	} else {
   116  		fmt.Printf("%s>>> Test %s finished with success after %0.1f secs%s\n", succ, t.name, duration, reset)
   117  	}
   118  	if err != nil || verbose {
   119  		fmt.Print(out.String())
   120  		fmt.Println()
   121  	}
   122  	consoleLock.Unlock()
   123  
   124  	if err != nil && useScheduler {
   125  		updateScheduler(t.name, duration)
   126  	}
   127  
   128  	return err != nil
   129  }
   130  
   131  func updateScheduler(test string, duration float64) {
   132  	req := &http.Request{
   133  		Method: "POST",
   134  		Host:   schedulerHost,
   135  		URL: &url.URL{
   136  			Opaque: fmt.Sprintf("/record/%s/%0.2f", url.QueryEscape(test), duration),
   137  			Scheme: "http",
   138  			Host:   schedulerHost,
   139  		},
   140  		Close: true,
   141  	}
   142  	if resp, err := http.DefaultClient.Do(req); err != nil {
   143  		fmt.Printf("Error updating scheduler: %v\n", err)
   144  	} else {
   145  		resp.Body.Close()
   146  	}
   147  }
   148  
   149  func getSchedule(tests []string) ([]string, error) {
   150  	var (
   151  		userName    = os.Getenv("CIRCLE_PROJECT_USERNAME")
   152  		project     = os.Getenv("CIRCLE_PROJECT_REPONAME")
   153  		buildNum    = os.Getenv("CIRCLE_BUILD_NUM")
   154  		testRun     = userName + "-" + project + "-integration-" + buildNum
   155  		shardCount  = os.Getenv("CIRCLE_NODE_TOTAL")
   156  		shardID     = os.Getenv("CIRCLE_NODE_INDEX")
   157  		requestBody = &bytes.Buffer{}
   158  	)
   159  	if err := json.NewEncoder(requestBody).Encode(schedule{tests}); err != nil {
   160  		return []string{}, err
   161  	}
   162  	url := fmt.Sprintf("http://%s/schedule/%s/%s/%s", schedulerHost, testRun, shardCount, shardID)
   163  	resp, err := http.Post(url, jsonContentType, requestBody)
   164  	if err != nil {
   165  		return []string{}, err
   166  	}
   167  	var sched schedule
   168  	if err := json.NewDecoder(resp.Body).Decode(&sched); err != nil {
   169  		return []string{}, err
   170  	}
   171  	return sched.Tests, nil
   172  }
   173  
   174  func getTests(testNames []string) (tests, error) {
   175  	var err error
   176  	if useScheduler {
   177  		testNames, err = getSchedule(testNames)
   178  		if err != nil {
   179  			return tests{}, err
   180  		}
   181  	}
   182  	tests := tests{}
   183  	for _, name := range testNames {
   184  		parts := strings.Split(strings.TrimSuffix(name, "_test.sh"), "_")
   185  		numHosts, err := strconv.Atoi(parts[len(parts)-1])
   186  		if err != nil {
   187  			numHosts = 1
   188  		}
   189  		tests = append(tests, test{name, numHosts})
   190  		fmt.Printf("Test %s needs %d hosts\n", name, numHosts)
   191  	}
   192  	return tests, nil
   193  }
   194  
   195  func summary(tests, failed tests) {
   196  	if len(failed) > 0 {
   197  		fmt.Printf("%s>>> Ran %d tests, %d failed%s\n", fail, len(tests), len(failed), reset)
   198  		for _, test := range failed {
   199  			fmt.Printf("%s>>> Fail %s%s\n", fail, test.name, reset)
   200  		}
   201  	} else {
   202  		fmt.Printf("%s>>> Ran %d tests, all succeeded%s\n", succ, len(tests), reset)
   203  	}
   204  }
   205  
   206  func parallel(ts tests, hosts []string) bool {
   207  	testsCopy := ts
   208  	sort.Sort(sort.Reverse(ts))
   209  	resultsChan := make(chan result)
   210  	outstanding := 0
   211  	failed := tests{}
   212  	for len(ts) > 0 || outstanding > 0 {
   213  		// While we have some free hosts, try and schedule
   214  		// a test on them
   215  		for len(hosts) > 0 {
   216  			test, ok := ts.pick(len(hosts))
   217  			if !ok {
   218  				break
   219  			}
   220  			testHosts := hosts[:test.hosts]
   221  			hosts = hosts[test.hosts:]
   222  
   223  			go func() {
   224  				errored := test.run(testHosts)
   225  				resultsChan <- result{test, errored, testHosts}
   226  			}()
   227  			outstanding++
   228  		}
   229  
   230  		// Otherwise, wait for the test to finish and return
   231  		// the hosts to the pool
   232  		result := <-resultsChan
   233  		hosts = append(hosts, result.hosts...)
   234  		outstanding--
   235  		if result.errored {
   236  			failed = append(failed, result.test)
   237  		}
   238  	}
   239  	summary(testsCopy, failed)
   240  	return len(failed) > 0
   241  }
   242  
   243  func sequential(ts tests, hosts []string) bool {
   244  	failed := tests{}
   245  	for _, test := range ts {
   246  		if test.run(hosts) {
   247  			failed = append(failed, test)
   248  		}
   249  	}
   250  	summary(ts, failed)
   251  	return len(failed) > 0
   252  }
   253  
   254  func main() {
   255  	mflag.BoolVar(&useScheduler, []string{"scheduler"}, false, "Use scheduler to distribute tests across shards")
   256  	mflag.BoolVar(&runParallel, []string{"parallel"}, false, "Run tests in parallel on hosts where possible")
   257  	mflag.BoolVar(&verbose, []string{"v"}, false, "Print output from all tests (Also enabled via DEBUG=1)")
   258  	mflag.StringVar(&schedulerHost, []string{"scheduler-host"}, defaultSchedulerHost, "Hostname of scheduler.")
   259  	mflag.IntVar(&timeout, []string{"timeout"}, 180, "Max time to run one test for, in seconds")
   260  	mflag.Parse()
   261  
   262  	if len(os.Getenv("DEBUG")) > 0 {
   263  		verbose = true
   264  	}
   265  
   266  	testArgs := mflag.Args()
   267  	tests, err := getTests(testArgs)
   268  	if err != nil {
   269  		fmt.Printf("Error parsing tests: %v (%v)\n", err, testArgs)
   270  		os.Exit(1)
   271  	}
   272  
   273  	hosts := strings.Fields(os.Getenv("HOSTS"))
   274  	maxHosts := len(hosts)
   275  	if maxHosts == 0 {
   276  		fmt.Print("No HOSTS specified.\n")
   277  		os.Exit(1)
   278  	}
   279  
   280  	var errored bool
   281  	if runParallel {
   282  		errored = parallel(tests, hosts)
   283  	} else {
   284  		errored = sequential(tests, hosts)
   285  	}
   286  
   287  	if errored {
   288  		os.Exit(1)
   289  	}
   290  }