github.com/aexvir/courtney@v0.3.0/tester/tester.go (about)

     1  package tester
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strings"
    11  
    12  	"crypto/md5"
    13  
    14  	"github.com/dave/courtney/shared"
    15  	"github.com/dave/courtney/tester/logger"
    16  	"github.com/dave/courtney/tester/merge"
    17  	"github.com/pkg/errors"
    18  	"golang.org/x/tools/cover"
    19  )
    20  
    21  // New creates a new Tester with the provided setup
    22  func New(setup *shared.Setup) *Tester {
    23  	t := &Tester{
    24  		setup: setup,
    25  	}
    26  	return t
    27  }
    28  
    29  // Tester runs tests and merges coverage files
    30  type Tester struct {
    31  	setup   *shared.Setup
    32  	cover   string
    33  	Results []*cover.Profile
    34  }
    35  
    36  // Load loads pre-prepared coverage files instead of running 'go test'
    37  func (t *Tester) Load() error {
    38  	files, err := filepath.Glob(t.setup.Load)
    39  	if err != nil {
    40  		return errors.Wrap(err, "Error loading coverage files")
    41  	}
    42  	for _, fpath := range files {
    43  		if err := t.processCoverageFile(fpath); err != nil {
    44  			return err
    45  		}
    46  	}
    47  	return nil
    48  }
    49  
    50  // Test initiates the tests and merges the coverage files
    51  func (t *Tester) Test() error {
    52  
    53  	var err error
    54  	if t.cover, err = ioutil.TempDir("", "coverage"); err != nil {
    55  		return errors.Wrap(err, "Error creating temporary coverage dir")
    56  	}
    57  	defer os.RemoveAll(t.cover)
    58  
    59  	for _, spec := range t.setup.Packages {
    60  		if err := t.processDir(spec.Dir); err != nil {
    61  			return err
    62  		}
    63  	}
    64  
    65  	return nil
    66  }
    67  
    68  // Save saves the coverage file
    69  func (t *Tester) Save() error {
    70  	if len(t.Results) == 0 {
    71  		fmt.Fprintln(t.setup.Env.Stdout(), "No results")
    72  		return nil
    73  	}
    74  	currentDir, err := t.setup.Env.Getwd()
    75  	if err != nil {
    76  		return errors.Wrap(err, "Error getting working dir")
    77  	}
    78  	out := filepath.Join(currentDir, "coverage.out")
    79  	if t.setup.Output != "" {
    80  		out = t.setup.Output
    81  	}
    82  	f, err := os.Create(out)
    83  	if err != nil {
    84  		return errors.Wrapf(err, "Error creating output coverage file %s", out)
    85  	}
    86  	defer f.Close()
    87  	merge.DumpProfiles(t.Results, f)
    88  	return nil
    89  }
    90  
    91  // Enforce returns an error if code is untested if the -e command line option
    92  // is set
    93  func (t *Tester) Enforce() error {
    94  	if !t.setup.Enforce {
    95  		return nil
    96  	}
    97  	untested := make(map[string][]cover.ProfileBlock)
    98  	for _, r := range t.Results {
    99  		for _, b := range r.Blocks {
   100  			if b.Count == 0 {
   101  				if len(untested[r.FileName]) > 0 {
   102  					// check if the new block is directly after the last one
   103  					last := untested[r.FileName][len(untested[r.FileName])-1]
   104  					if b.StartLine <= last.EndLine+1 {
   105  						last.EndLine = b.EndLine
   106  						last.EndCol = b.EndCol
   107  						untested[r.FileName][len(untested[r.FileName])-1] = last
   108  						continue
   109  					}
   110  				}
   111  				untested[r.FileName] = append(untested[r.FileName], b)
   112  			}
   113  		}
   114  	}
   115  
   116  	if len(untested) == 0 {
   117  		return nil
   118  	}
   119  
   120  	var s string
   121  	for name, blocks := range untested {
   122  		fpath, err := t.setup.Paths.FilePath(name)
   123  		if err != nil {
   124  			return err
   125  		}
   126  		by, err := ioutil.ReadFile(fpath)
   127  		if err != nil {
   128  			return errors.Wrapf(err, "Error reading source file %s", fpath)
   129  		}
   130  		lines := strings.Split(string(by), "\n")
   131  		for _, b := range blocks {
   132  			s += fmt.Sprintf("%s:%d-%d:\n", name, b.StartLine, b.EndLine)
   133  			undented := undent(lines[b.StartLine-1 : b.EndLine])
   134  			s += strings.Join(undented, "\n")
   135  		}
   136  	}
   137  	return errors.Errorf("Error - untested code:\n%s", s)
   138  
   139  }
   140  
   141  // ProcessExcludes uses the output from the scanner package and removes blocks
   142  // from the merged coverage file.
   143  func (t *Tester) ProcessExcludes(excludes map[string]map[int]bool) error {
   144  	var processed []*cover.Profile
   145  
   146  	for _, p := range t.Results {
   147  
   148  		// Filenames in t.Results are in go package form. We need to convert to
   149  		// filepaths before use
   150  		fpath, err := t.setup.Paths.FilePath(p.FileName)
   151  		if err != nil {
   152  			return err
   153  		}
   154  
   155  		f, ok := excludes[fpath]
   156  		if !ok {
   157  			// no excludes in this file - add the profile unchanged
   158  			processed = append(processed, p)
   159  			continue
   160  		}
   161  		var blocks []cover.ProfileBlock
   162  		for _, b := range p.Blocks {
   163  			excluded := false
   164  			for line := b.StartLine; line <= b.EndLine; line++ {
   165  				if ex, ok := f[line]; ok && ex {
   166  					excluded = true
   167  					break
   168  				}
   169  			}
   170  			if !excluded || b.Count > 0 {
   171  				// include blocks that are not excluded
   172  				// also include any blocks that have coverage
   173  				blocks = append(blocks, b)
   174  			}
   175  		}
   176  		profile := &cover.Profile{
   177  			FileName: p.FileName,
   178  			Mode:     p.Mode,
   179  			Blocks:   blocks,
   180  		}
   181  		processed = append(processed, profile)
   182  	}
   183  	t.Results = processed
   184  	return nil
   185  }
   186  
   187  func (t *Tester) processDir(dir string) error {
   188  
   189  	coverfile := filepath.Join(
   190  		t.cover,
   191  		fmt.Sprintf("%x", md5.Sum([]byte(dir)))+".out",
   192  	)
   193  
   194  	files, err := ioutil.ReadDir(dir)
   195  	if err != nil {
   196  		return errors.Wrapf(err, "Error reading files from %s", dir)
   197  	}
   198  
   199  	foundTest := false
   200  	for _, f := range files {
   201  		if strings.HasSuffix(f.Name(), "_test.go") {
   202  			foundTest = true
   203  		}
   204  	}
   205  	if !foundTest {
   206  		// notest
   207  		return nil
   208  	}
   209  
   210  	combined, stdout, stderr := logger.Log(
   211  		t.setup.Verbose,
   212  		t.setup.Env.Stdout(),
   213  		t.setup.Env.Stderr(),
   214  	)
   215  
   216  	var args []string
   217  	var pkgs []string
   218  	for _, s := range t.setup.Packages {
   219  		pkgs = append(pkgs, s.Path)
   220  	}
   221  	args = append(args, "test")
   222  	if t.setup.Short {
   223  		// notest
   224  		// TODO: add test
   225  		args = append(args, "-short")
   226  	}
   227  	if t.setup.Timeout != "" {
   228  		// notest
   229  		// TODO: add test
   230  		args = append(args, "-timeout", t.setup.Timeout)
   231  	}
   232  	args = append(args, fmt.Sprintf("-coverpkg=%s", strings.Join(pkgs, ",")))
   233  	args = append(args, fmt.Sprintf("-coverprofile=%s", coverfile))
   234  	if t.setup.Verbose {
   235  		args = append(args, "-v")
   236  	}
   237  	if len(t.setup.TestArgs) > 0 {
   238  		// notest
   239  		args = append(args, t.setup.TestArgs...)
   240  	}
   241  	if t.setup.Verbose {
   242  		fmt.Fprintf(
   243  			t.setup.Env.Stdout(),
   244  			"Running test: %s\n",
   245  			strings.Join(append([]string{"go"}, args...), " "),
   246  		)
   247  	}
   248  	exe := exec.Command("go", args...)
   249  	exe.Dir = dir
   250  	exe.Env = t.setup.Env.Environ()
   251  	exe.Stdout = stdout
   252  	exe.Stderr = stderr
   253  	err = exe.Run()
   254  	if strings.Contains(combined.String(), "no buildable Go source files in") {
   255  		// notest
   256  		return nil
   257  	}
   258  	if err != nil {
   259  		// TODO: Remove when https://github.com/dave/courtney/issues/4 is fixed
   260  		// notest
   261  		if t.setup.Verbose {
   262  			// They will already have seen the output
   263  			return errors.Wrap(err, "Error executing test")
   264  		}
   265  		return errors.Wrapf(err, "Error executing test \nOutput:[\n%s]\n", combined.String())
   266  	}
   267  	return t.processCoverageFile(coverfile)
   268  }
   269  
   270  func (t *Tester) processCoverageFile(filename string) error {
   271  	profiles, err := cover.ParseProfiles(filename)
   272  	if err != nil {
   273  		return err
   274  	}
   275  	for _, p := range profiles {
   276  		if t.Results, err = merge.AddProfile(t.Results, p); err != nil {
   277  			return err
   278  		}
   279  	}
   280  	return nil
   281  }
   282  
   283  func undent(lines []string) []string {
   284  
   285  	indentRegex := regexp.MustCompile("[^\t]")
   286  	mindent := -1
   287  
   288  	for _, line := range lines {
   289  		loc := indentRegex.FindStringIndex(line)
   290  		if len(loc) == 0 {
   291  			// notest
   292  			// string is empty?
   293  			continue
   294  		}
   295  		if mindent == -1 || loc[0] < mindent {
   296  			mindent = loc[0]
   297  		}
   298  	}
   299  
   300  	var out []string
   301  	for _, line := range lines {
   302  		if line == "" {
   303  			// notest
   304  			out = append(out, "")
   305  		} else {
   306  			out = append(out, "\t"+line[mindent:])
   307  		}
   308  	}
   309  	return out
   310  }