// Copyright 2022 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package pods import ( "cmp" "fmt" "internal/coverage" "os" "path/filepath" "regexp" "slices" "strconv" "strings" ) // Pod encapsulates a set of files emitted during the executions of a // coverage-instrumented binary. Each pod contains a single meta-data // file, and then 0 or more counter data files that refer to that // meta-data file. Pods are intended to simplify processing of // coverage output files in the case where we have several coverage // output directories containing output files derived from more // than one instrumented executable. In the case where the files that // make up a pod are spread out across multiple directories, each // element of the "Origins" field below will be populated with the // index of the originating directory for the corresponding counter // data file (within the slice of input dirs handed to CollectPods). // The ProcessIDs field will be populated with the process ID of each // data file in the CounterDataFiles slice. type Pod struct { MetaFile string CounterDataFiles []string Origins []int ProcessIDs []int } // CollectPods visits the files contained within the directories in // the list 'dirs', collects any coverage-related files, partitions // them into pods, and returns a list of the pods to the caller, along // with an error if something went wrong during directory/file // reading. // // CollectPods skips over any file that is not related to coverage // (e.g. avoids looking at things that are not meta-data files or // counter-data files). CollectPods also skips over 'orphaned' counter // data files (e.g. counter data files for which we can't find the // corresponding meta-data file). If "warn" is true, CollectPods will // issue warnings to stderr when it encounters non-fatal problems (for // orphans or a directory with no meta-data files). func CollectPods(dirs []string, warn bool) ([]Pod, error) { files := []string{} dirIndices := []int{} for k, dir := range dirs { dents, err := os.ReadDir(dir) if err != nil { return nil, err } for _, e := range dents { if e.IsDir() { continue } files = append(files, filepath.Join(dir, e.Name())) dirIndices = append(dirIndices, k) } } return collectPodsImpl(files, dirIndices, warn), nil } // CollectPodsFromFiles functions the same as "CollectPods" but // operates on an explicit list of files instead of a directory. func CollectPodsFromFiles(files []string, warn bool) []Pod { return collectPodsImpl(files, nil, warn) } type fileWithAnnotations struct { file string origin int pid int } type protoPod struct { mf string elements []fileWithAnnotations } // collectPodsImpl examines the specified list of files and picks out // subsets that correspond to coverage pods. The first stage in this // process is collecting a set { M1, M2, ... MN } where each M_k is a // distinct coverage meta-data file. We then create a single pod for // each meta-data file M_k, then find all of the counter data files // that refer to that meta-data file (recall that the counter data // file name incorporates the meta-data hash), and add the counter // data file to the appropriate pod. // // This process is complicated by the fact that we need to keep track // of directory indices for counter data files. Here is an example to // motivate: // // directory 1: // // M1 covmeta.9bbf1777f47b3fcacb05c38b035512d6 // C1 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677673.1662138360208416486 // C2 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677637.1662138359974441782 // // directory 2: // // M2 covmeta.9bbf1777f47b3fcacb05c38b035512d6 // C3 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677445.1662138360208416480 // C4 covcounters.9bbf1777f47b3fcacb05c38b035512d6.1677677.1662138359974441781 // M3 covmeta.a723844208cea2ae80c63482c78b2245 // C5 covcounters.a723844208cea2ae80c63482c78b2245.3677445.1662138360208416480 // C6 covcounters.a723844208cea2ae80c63482c78b2245.1877677.1662138359974441781 // // In these two directories we have three meta-data files, but only // two are distinct, meaning that we'll wind up with two pods. The // first pod (with meta-file M1) will have four counter data files // (C1, C2, C3, C4) and the second pod will have two counter data files // (C5, C6). func collectPodsImpl(files []string, dirIndices []int, warn bool) []Pod { metaRE := regexp.MustCompile(fmt.Sprintf(`^%s\.(\S+)$`, coverage.MetaFilePref)) mm := make(map[string]protoPod) for _, f := range files { base := filepath.Base(f) if m := metaRE.FindStringSubmatch(base); m != nil { tag := m[1] // We need to allow for the possibility of duplicate // meta-data files. If we hit this case, use the // first encountered as the canonical version. if _, ok := mm[tag]; !ok { mm[tag] = protoPod{mf: f} } // FIXME: should probably check file length and hash here for // the duplicate. } } counterRE := regexp.MustCompile(fmt.Sprintf(coverage.CounterFileRegexp, coverage.CounterFilePref)) for k, f := range files { base := filepath.Base(f) if m := counterRE.FindStringSubmatch(base); m != nil { tag := m[1] // meta hash pid, err := strconv.Atoi(m[2]) if err != nil { continue } if v, ok := mm[tag]; ok { idx := -1 if dirIndices != nil { idx = dirIndices[k] } fo := fileWithAnnotations{file: f, origin: idx, pid: pid} v.elements = append(v.elements, fo) mm[tag] = v } else { if warn { warning("skipping orphaned counter file: %s", f) } } } } if len(mm) == 0 { if warn { warning("no coverage data files found") } return nil } pods := make([]Pod, 0, len(mm)) for _, p := range mm { slices.SortFunc(p.elements, func(a, b fileWithAnnotations) int { if r := cmp.Compare(a.origin, b.origin); r != 0 { return r } return strings.Compare(a.file, b.file) }) pod := Pod{ MetaFile: p.mf, CounterDataFiles: make([]string, 0, len(p.elements)), Origins: make([]int, 0, len(p.elements)), ProcessIDs: make([]int, 0, len(p.elements)), } for _, e := range p.elements { pod.CounterDataFiles = append(pod.CounterDataFiles, e.file) pod.Origins = append(pod.Origins, e.origin) pod.ProcessIDs = append(pod.ProcessIDs, e.pid) } pods = append(pods, pod) } slices.SortFunc(pods, func(a, b Pod) int { return strings.Compare(a.MetaFile, b.MetaFile) }) return pods } func warning(s string, a ...interface{}) { fmt.Fprintf(os.Stderr, "warning: ") fmt.Fprintf(os.Stderr, s, a...) fmt.Fprintf(os.Stderr, "\n") }