github.com/mmirolim/gtr@v0.3.0/testrunner.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"log"
     7  	"os"
     8  	"runtime"
     9  	"sort"
    10  	"strconv"
    11  	"strings"
    12  )
    13  
    14  var _ Task = (*GoTestRunner)(nil)
    15  
    16  // Strategy interface defines provider of tests for the testrunner
    17  type Strategy interface {
    18  	CoverageEnabled() bool
    19  	TestsToRun(context.Context) (runAll bool, tests, subTests []string, err error)
    20  }
    21  
    22  // GoTestRunner runs go tests
    23  type GoTestRunner struct {
    24  	strategy Strategy
    25  	cmd      CommandCreator
    26  	args     string
    27  	log      *log.Logger
    28  }
    29  
    30  // NewGoTestRunner creates test runner
    31  // strategy to use
    32  // cmd creator
    33  // args to runner
    34  // logger for runner
    35  func NewGoTestRunner(
    36  	strategy Strategy,
    37  	cmd CommandCreator,
    38  	args string,
    39  	logger *log.Logger,
    40  ) *GoTestRunner {
    41  	return &GoTestRunner{
    42  		strategy: strategy,
    43  		cmd:      cmd,
    44  		args:     args,
    45  		log:      logger,
    46  	}
    47  }
    48  
    49  // ID returns Task ID
    50  func (tr *GoTestRunner) ID() string {
    51  	return "GoTestRunner"
    52  }
    53  
    54  // Run method implements Task interface
    55  // runs go tests
    56  func (tr *GoTestRunner) Run(ctx context.Context) (string, error) {
    57  	runAll, tests, subTests, err := tr.strategy.TestsToRun(ctx)
    58  	if err != nil {
    59  		if err == ErrBuildFailed {
    60  			return "Build Failed", nil
    61  		}
    62  		return "", fmt.Errorf("strategy error %v", err)
    63  	}
    64  	if len(tests) == 0 && len(subTests) == 0 {
    65  		return "No test found to run", nil
    66  	}
    67  
    68  	var listArg []string
    69  	pkgPaths := map[string][]string{}
    70  	for _, tname := range tests {
    71  		id := strings.LastIndexByte(tname, '.')
    72  		pkgPath := tname[:id]
    73  		if pkgPath == "" {
    74  			pkgPath = "."
    75  		}
    76  		pkgPaths[pkgPath] = append(pkgPaths[pkgPath], tname[id+1:])
    77  	}
    78  
    79  	// run tests
    80  	// do not wait process to finish
    81  	// in case of console blocking programs
    82  	// -vet=off to improve speed
    83  	// TODO handle run all
    84  	msg := ""
    85  	testParams := []string{"test", "-v", "-vet", "off", "-failfast",
    86  		"-cpu", strconv.Itoa(runtime.GOMAXPROCS(0))}
    87  
    88  	logStrList(tr.log, "Tests to run", tests, true)
    89  	if len(subTests) > 0 {
    90  		logStrList(tr.log, "Subtests to run", subTests, true)
    91  	}
    92  	var pkgList, testNames []string
    93  	for k, pkgtests := range pkgPaths {
    94  		pkgList = append(pkgList, k)
    95  		testNames = append(testNames, pkgtests...)
    96  	}
    97  	testsFormated := tr.joinTestAndSubtest(testNames, subTests)
    98  	var cmd CommandExecutor
    99  	// TODO refactor
   100  	if runAll {
   101  		if tr.strategy.CoverageEnabled() {
   102  			testParams = append(testParams, "-coverprofile")
   103  			testParams = append(testParams, "coverage_profile")
   104  		}
   105  		testParams = append(testParams, "-run")
   106  		testParams = append(testParams, testsFormated)
   107  		testParams = append(testParams, listArg...)
   108  		if len(tr.args) > 0 {
   109  			testParams = append(testParams, "-args")
   110  			testParams = append(testParams, tr.args)
   111  		}
   112  		cmd = tr.cmd(ctx, "go", testParams...)
   113  		tr.log.Println(">>", strings.Join(cmd.GetArgs(), " "))
   114  
   115  		cmd.SetStdout(os.Stdout)
   116  		cmd.SetStderr(os.Stderr)
   117  		cmd.SetEnv(os.Environ())
   118  		cmd.Run()
   119  	} else {
   120  		// run cmd for each test and skip subtests to have separation between tests
   121  	OUTER:
   122  		for pkg, pkgtests := range pkgPaths {
   123  			for _, tname := range pkgtests {
   124  				// run all tests
   125  				testParams := []string{"test", "-v", "-vet", "off",
   126  					"-cpu", strconv.Itoa(runtime.GOMAXPROCS(0))}
   127  
   128  				if tr.strategy.CoverageEnabled() {
   129  					testParams = append(testParams, "-coverprofile")
   130  					testParams = append(testParams, fmt.Sprintf(".gtr/%s.%s",
   131  						strings.ReplaceAll(pkg, "/", "_"),
   132  						tname))
   133  				}
   134  
   135  				testParams = append(testParams, "-run")
   136  				testParams = append(testParams, tname+"$") // test
   137  				if tr.strategy.CoverageEnabled() {
   138  					// for all packages
   139  					testParams = append(testParams, "./...")
   140  				} else {
   141  					// package
   142  					testParams = append(testParams, pkg)
   143  				}
   144  				if len(tr.args) > 0 {
   145  					testParams = append(testParams, "-args")
   146  					// test binary args
   147  					testParams = append(testParams, tr.args)
   148  				}
   149  				cmd = tr.cmd(ctx, "go", testParams...)
   150  				tr.log.Println(">>", strings.Join(cmd.GetArgs(), " "))
   151  
   152  				cmd.SetStdout(os.Stdout)
   153  				cmd.SetStderr(os.Stderr)
   154  				cmd.SetEnv(os.Environ())
   155  				cmd.Run()
   156  				if !cmd.Success() {
   157  					// stop on failed test
   158  					break OUTER
   159  				}
   160  			}
   161  		}
   162  	}
   163  
   164  	if cmd.Success() {
   165  		msg = "Tests PASS: " + testsFormated
   166  		tr.log.Println("\033[32mTests PASS\033[39m")
   167  	} else {
   168  		msg = "Tests FAIL: " + testsFormated
   169  		tr.log.Println("\033[31mTests FAIL\033[39m")
   170  	}
   171  	return msg, nil
   172  }
   173  
   174  // joinTestAndSubtest joins and format tests according to go test -run arg format
   175  func (tr *GoTestRunner) joinTestAndSubtest(tests, subTests []string) string {
   176  	sort.Strings(tests)
   177  	sort.Strings(subTests)
   178  	out := strings.Join(tests, "$|")
   179  	if len(out) > 0 {
   180  		out += "$"
   181  	}
   182  	for i := range subTests {
   183  		subTests[i] = strings.ReplaceAll(subTests[i], " ", "_")
   184  	}
   185  	if len(subTests) != 0 {
   186  		out += "/(" + strings.Join(subTests, "|") + ")"
   187  	}
   188  	return out
   189  }
   190  
   191  func logStrList(log *log.Logger, title string, tests []string, toSort bool) {
   192  	var out []string
   193  	if toSort {
   194  		out = make([]string, len(tests))
   195  		copy(out[0:], tests)
   196  		sort.Strings(out)
   197  	} else {
   198  		out = tests
   199  	}
   200  
   201  	log.Println("=============") // output for debug
   202  	log.Println(title)
   203  	for i := range out {
   204  		log.Printf("-> %+v\n", out[i]) // output for debug
   205  	}
   206  	log.Println("=============")
   207  }