github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/coverage/pods/pods.go (about) 1 // Copyright 2022 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package pods 6 7 import ( 8 "fmt" 9 "os" 10 "path/filepath" 11 "regexp" 12 "sort" 13 "strconv" 14 15 "github.com/go-asm/go/coverage" 16 ) 17 18 // Pod encapsulates a set of files emitted during the executions of a 19 // coverage-instrumented binary. Each pod contains a single meta-data 20 // file, and then 0 or more counter data files that refer to that 21 // meta-data file. Pods are intended to simplify processing of 22 // coverage output files in the case where we have several coverage 23 // output directories containing output files derived from more 24 // than one instrumented executable. In the case where the files that 25 // make up a pod are spread out across multiple directories, each 26 // element of the "Origins" field below will be populated with the 27 // index of the originating directory for the corresponding counter 28 // data file (within the slice of input dirs handed to CollectPods). 29 // The ProcessIDs field will be populated with the process ID of each 30 // data file in the CounterDataFiles slice. 31 type Pod struct { 32 MetaFile string 33 CounterDataFiles []string 34 Origins []int 35 ProcessIDs []int 36 } 37 38 // CollectPods visits the files contained within the directories in 39 // the list 'dirs', collects any coverage-related files, partitions 40 // them into pods, and returns a list of the pods to the caller, along 41 // with an error if something went wrong during directory/file 42 // reading. 43 // 44 // CollectPods skips over any file that is not related to coverage 45 // (e.g. avoids looking at things that are not meta-data files or 46 // counter-data files). CollectPods also skips over 'orphaned' counter 47 // data files (e.g. counter data files for which we can't find the 48 // corresponding meta-data file). If "warn" is true, CollectPods will 49 // issue warnings to stderr when it encounters non-fatal problems (for 50 // orphans or a directory with no meta-data files). 51 func CollectPods(dirs []string, warn bool) ([]Pod, error) { 52 files := []string{} 53 dirIndices := []int{} 54 for k, dir := range dirs { 55 dents, err := os.ReadDir(dir) 56 if err != nil { 57 return nil, err 58 } 59 for _, e := range dents { 60 if e.IsDir() { 61 continue 62 } 63 files = append(files, filepath.Join(dir, e.Name())) 64 dirIndices = append(dirIndices, k) 65 } 66 } 67 return collectPodsImpl(files, dirIndices, warn), nil 68 } 69 70 // CollectPodsFromFiles functions the same as "CollectPods" but 71 // operates on an explicit list of files instead of a directory. 72 func CollectPodsFromFiles(files []string, warn bool) []Pod { 73 return collectPodsImpl(files, nil, warn) 74 } 75 76 type fileWithAnnotations struct { 77 file string 78 origin int 79 pid int 80 } 81 82 type protoPod struct { 83 mf string 84 elements []fileWithAnnotations 85 } 86 87 // collectPodsImpl examines the specified list of files and picks out 88 // subsets that correspond to coverage pods. The first stage in this 89 // process is collecting a set { M1, M2, ... MN } where each M_k is a 90 // distinct coverage meta-data file. We then create a single pod for 91 // each meta-data file M_k, then find all of the counter data files 92 // that refer to that meta-data file (recall that the counter data 93 // file name incorporates the meta-data hash), and add the counter 94 // data file to the appropriate pod. 95 // 96 // This process is complicated by the fact that we need to keep track 97 // of directory indices for counter data files. Here is an example to 98 // motivate: 99 // 100 // directory 1: 101 // 102 // M1 covmeta.9bbf1777f47b3fcacb05c38b035512d6 103 // C1 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677673.1662138360208416486 104 // C2 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677637.1662138359974441782 105 // 106 // directory 2: 107 // 108 // M2 covmeta.9bbf1777f47b3fcacb05c38b035512d6 109 // C3 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677445.1662138360208416480 110 // C4 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677677.1662138359974441781 111 // M3 covmeta.a723844208cea2ae80c63482c78b2245 112 // C5 covcounters.a723844208cea2ae80c63482c78b2245.3677445.1662138360208416480 113 // C6 covcounters.a723844208cea2ae80c63482c78b2245.1877677.1662138359974441781 114 // 115 // In these two directories we have three meta-data files, but only 116 // two are distinct, meaning that we'll wind up with two pods. The 117 // first pod (with meta-file M1) will have four counter data files 118 // (C1, C2, C3, C4) and the second pod will have two counter data files 119 // (C5, C6). 120 func collectPodsImpl(files []string, dirIndices []int, warn bool) []Pod { 121 metaRE := regexp.MustCompile(fmt.Sprintf(`^%s\.(\S+)$`, coverage.MetaFilePref)) 122 mm := make(map[string]protoPod) 123 for _, f := range files { 124 base := filepath.Base(f) 125 if m := metaRE.FindStringSubmatch(base); m != nil { 126 tag := m[1] 127 // We need to allow for the possibility of duplicate 128 // meta-data files. If we hit this case, use the 129 // first encountered as the canonical version. 130 if _, ok := mm[tag]; !ok { 131 mm[tag] = protoPod{mf: f} 132 } 133 // FIXME: should probably check file length and hash here for 134 // the duplicate. 135 } 136 } 137 counterRE := regexp.MustCompile(fmt.Sprintf(coverage.CounterFileRegexp, coverage.CounterFilePref)) 138 for k, f := range files { 139 base := filepath.Base(f) 140 if m := counterRE.FindStringSubmatch(base); m != nil { 141 tag := m[1] // meta hash 142 pid, err := strconv.Atoi(m[2]) 143 if err != nil { 144 continue 145 } 146 if v, ok := mm[tag]; ok { 147 idx := -1 148 if dirIndices != nil { 149 idx = dirIndices[k] 150 } 151 fo := fileWithAnnotations{file: f, origin: idx, pid: pid} 152 v.elements = append(v.elements, fo) 153 mm[tag] = v 154 } else { 155 if warn { 156 warning("skipping orphaned counter file: %s", f) 157 } 158 } 159 } 160 } 161 if len(mm) == 0 { 162 if warn { 163 warning("no coverage data files found") 164 } 165 return nil 166 } 167 pods := make([]Pod, 0, len(mm)) 168 for _, p := range mm { 169 sort.Slice(p.elements, func(i, j int) bool { 170 if p.elements[i].origin != p.elements[j].origin { 171 return p.elements[i].origin < p.elements[j].origin 172 } 173 return p.elements[i].file < p.elements[j].file 174 }) 175 pod := Pod{ 176 MetaFile: p.mf, 177 CounterDataFiles: make([]string, 0, len(p.elements)), 178 Origins: make([]int, 0, len(p.elements)), 179 ProcessIDs: make([]int, 0, len(p.elements)), 180 } 181 for _, e := range p.elements { 182 pod.CounterDataFiles = append(pod.CounterDataFiles, e.file) 183 pod.Origins = append(pod.Origins, e.origin) 184 pod.ProcessIDs = append(pod.ProcessIDs, e.pid) 185 } 186 pods = append(pods, pod) 187 } 188 sort.Slice(pods, func(i, j int) bool { 189 return pods[i].MetaFile < pods[j].MetaFile 190 }) 191 return pods 192 } 193 194 func warning(s string, a ...interface{}) { 195 fmt.Fprintf(os.Stderr, "warning: ") 196 fmt.Fprintf(os.Stderr, s, a...) 197 fmt.Fprintf(os.Stderr, "\n") 198 }