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  }