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 }