github.com/go-maxhub/gremlins@v1.0.1-0.20231227222204-b03a6a1e3e09/core/coverage/coverage.go (about)

     1  /*
     2   * Copyright 2022 The Gremlins Authors
     3   *
     4   *    Licensed under the Apache License, Version 2.0 (the "License");
     5   *    you may not use this file except in compliance with the License.
     6   *    You may obtain a copy of the License at
     7   *
     8   *        http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   *    Unless required by applicable law or agreed to in writing, software
    11   *    distributed under the License is distributed on an "AS IS" BASIS,
    12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   *    See the License for the specific language governing permissions and
    14   *    limitations under the License.
    15   */
    16  
    17  package coverage
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"os"
    23  	"os/exec"
    24  	"path/filepath"
    25  	"strings"
    26  	"time"
    27  
    28  	"golang.org/x/tools/cover"
    29  
    30  	"github.com/go-maxhub/gremlins/core/log"
    31  
    32  	"github.com/go-maxhub/gremlins/core/configuration"
    33  	"github.com/go-maxhub/gremlins/core/gomodule"
    34  )
    35  
    36  // Result contains the Profile generated by the coverage and the time
    37  // it took to generate the coverage report.
    38  type Result struct {
    39  	Profile Profile
    40  	Elapsed time.Duration
    41  }
    42  
    43  // Coverage is responsible for executing a Go test with coverage via the Run() method,
    44  // then parsing the result coverage report file.
    45  type Coverage struct {
    46  	cmdContext execContext
    47  	workDir    string
    48  	path       string
    49  	fileName   string
    50  	mod        gomodule.GoModule
    51  
    52  	buildTags       string
    53  	coverPkg        string
    54  	integrationMode bool
    55  }
    56  
    57  // Option for the Coverage initialization.
    58  type Option func(c *Coverage) *Coverage
    59  
    60  type execContext = func(name string, args ...string) *exec.Cmd
    61  
    62  // New instantiates a Coverage element using exec.Command as execContext,
    63  // actually running the command on the OS.
    64  func New(workdir string, mod gomodule.GoModule, opts ...Option) *Coverage {
    65  	return NewWithCmd(exec.Command, workdir, mod, opts...)
    66  }
    67  
    68  // NewWithCmd instantiates a Coverage element given a custom execContext.
    69  func NewWithCmd(cmdContext execContext, workdir string, mod gomodule.GoModule, opts ...Option) *Coverage {
    70  	buildTags := configuration.Get[string](configuration.UnleashTagsKey)
    71  	coverPkg := configuration.Get[string](configuration.UnleashCoverPkgKey)
    72  	integrationMode := configuration.Get[bool](configuration.UnleashIntegrationMode)
    73  
    74  	c := &Coverage{
    75  		cmdContext:      cmdContext,
    76  		workDir:         workdir,
    77  		path:            "./...",
    78  		fileName:        "coverage",
    79  		mod:             mod,
    80  		buildTags:       buildTags,
    81  		coverPkg:        coverPkg,
    82  		integrationMode: integrationMode,
    83  	}
    84  	for _, opt := range opts {
    85  		c = opt(c)
    86  	}
    87  
    88  	return c
    89  }
    90  
    91  // Run executes the coverage command and parses the results, returning a *Profile
    92  // object.
    93  // Before executing the coverage, it downloads the go modules in a separate step.
    94  // This is done to avoid that the download phase impacts the execution time which
    95  // is later used as timeout for the mutant testing execution.
    96  func (c *Coverage) Run() (Result, error) {
    97  	log.Infof("Gathering coverage... ")
    98  	_ = os.Chdir(c.mod.Root)
    99  	//if err := c.downloadModules(); err != nil {
   100  	//	return Result{}, fmt.Errorf("impossible to download modules: %w", err)
   101  	//}
   102  	elapsed, err := c.executeCoverage()
   103  	if err != nil {
   104  		return Result{}, fmt.Errorf("impossible to executeCoverage coverage: %w", err)
   105  	}
   106  	log.Infof("done in %s\n", elapsed)
   107  	profile, err := c.profile()
   108  	if err != nil {
   109  		return Result{}, fmt.Errorf("an error occurred while generating coverage profile: %w", err)
   110  	}
   111  
   112  	return Result{Profile: profile, Elapsed: elapsed}, nil
   113  }
   114  
   115  func (c *Coverage) profile() (Profile, error) {
   116  	cf, err := os.Open(c.filePath())
   117  	defer func(cf *os.File) {
   118  		_ = cf.Close()
   119  	}(cf)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  	profile, err := c.parse(cf)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	return profile, nil
   129  }
   130  
   131  func (c *Coverage) filePath() string {
   132  	return fmt.Sprintf("%v/%v", c.workDir, c.fileName)
   133  }
   134  
   135  func (c *Coverage) downloadModules() error {
   136  	cmd := c.cmdContext("go", "mod", "download")
   137  	cmd.Stdout = os.Stdout
   138  	cmd.Stderr = os.Stderr
   139  
   140  	return cmd.Run()
   141  }
   142  
   143  func (c *Coverage) executeCoverage() (time.Duration, error) {
   144  	args := []string{"test"}
   145  	if c.buildTags != "" {
   146  		args = append(args, "-tags", c.buildTags)
   147  	}
   148  	if c.coverPkg != "" {
   149  		args = append(args, "-coverpkg", c.coverPkg)
   150  	}
   151  
   152  	args = append(args, "-cover", "-coverprofile", c.filePath(), c.scanPath())
   153  	cmd := c.cmdContext("go", args...)
   154  
   155  	start := time.Now()
   156  	if out, err := cmd.CombinedOutput(); err != nil {
   157  		log.Infof("\n%s\n", string(out))
   158  
   159  		return 0, err
   160  	}
   161  
   162  	return time.Since(start), nil
   163  }
   164  
   165  func (c *Coverage) scanPath() string {
   166  	path := "./..."
   167  	if !c.integrationMode {
   168  		if c.mod.CallingDir != "." {
   169  			path = fmt.Sprintf("./%s/...", c.mod.CallingDir)
   170  		}
   171  	}
   172  
   173  	return path
   174  }
   175  
   176  func (c *Coverage) parse(data io.Reader) (Profile, error) {
   177  	profiles, err := cover.ParseProfilesFromReader(data)
   178  	if err != nil {
   179  		return nil, err
   180  	}
   181  	status := make(Profile)
   182  	for _, p := range profiles {
   183  		for _, b := range p.Blocks {
   184  			if b.Count == 0 {
   185  				continue
   186  			}
   187  			block := Block{
   188  				StartLine: b.StartLine,
   189  				StartCol:  b.StartCol,
   190  				EndLine:   b.EndLine,
   191  				EndCol:    b.EndCol,
   192  			}
   193  			fn := c.removeModuleFromPath(p)
   194  			status[fn] = append(status[fn], block)
   195  		}
   196  	}
   197  
   198  	return status, nil
   199  }
   200  
   201  func (c *Coverage) removeModuleFromPath(p *cover.Profile) string {
   202  	path := strings.ReplaceAll(p.FileName, c.mod.Name+"/", "")
   203  	path, _ = filepath.Rel(c.mod.CallingDir, path)
   204  
   205  	return path
   206  }