github.com/lixvbnet/courtney@v0.0.0-20221025031132-0dcb02231211/tester/tester.go (about)

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