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 }