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 }