github.com/thediveo/gons@v0.9.9/reexec/testing/profile.go (about)

     1  // Copyright 2020 Harald Albrecht.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //    http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package testing
    16  
    17  import (
    18  	"bufio"
    19  	"fmt"
    20  	"os"
    21  	"regexp"
    22  	"sort"
    23  	"strconv"
    24  )
    25  
    26  // coverageProfile represents coverage profile data for a specific coverage
    27  // profile data file.
    28  type coverageProfile struct {
    29  	// Mode of coverage profile: "atomic", "count", or "set".
    30  	Mode string
    31  	// Sources with block coverage data, indexed by source file name.
    32  	Sources map[string]*coverageProfileSource
    33  }
    34  
    35  // newCoverageProfile returns a new and correctly initialized coverageProfile.
    36  func newCoverageProfile() *coverageProfile {
    37  	return &coverageProfile{
    38  		Sources: map[string]*coverageProfileSource{},
    39  	}
    40  }
    41  
    42  // coverageProfileSource represents the coverage blocks of a single source
    43  // file.
    44  type coverageProfileSource struct {
    45  	Blocks []coverageProfileBlock // coverage blocks per source file.
    46  }
    47  
    48  // coverageProfileBlockByStart is a type alias for sorting slices of
    49  // coverageProfileBlocks.
    50  type coverageProfileBlockByStart []coverageProfileBlock
    51  
    52  func (b coverageProfileBlockByStart) Len() int      { return len(b) }
    53  func (b coverageProfileBlockByStart) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
    54  func (b coverageProfileBlockByStart) Less(i, j int) bool {
    55  	bi, bj := b[i], b[j]
    56  	return bi.StartLine < bj.StartLine ||
    57  		(bi.StartLine == bj.StartLine && bi.StartCol < bj.StartCol)
    58  }
    59  
    60  // coverageProfileBlock represents a single block of coverage profiling data.
    61  type coverageProfileBlock struct {
    62  	StartLine uint32 // line number for block start.
    63  	StartCol  uint16 // column number for block start.
    64  	EndLine   uint32 // line number for block end.
    65  	EndCol    uint16 // column number for block end.
    66  	NumStmts  uint16 // number of statements included in this block.
    67  	Counts    uint32 // number of times this block was executed.
    68  }
    69  
    70  // modeRe specifies the format of the first "mode:" text line of a coverage
    71  // profile data file.
    72  var modeRe = regexp.MustCompile(`^mode: ([[:alpha:]]+)$`)
    73  
    74  // lineRe specifies the format of the block text lines in coverage profile
    75  // data files.
    76  var lineRe = regexp.MustCompile(
    77  	`^(.+):([0-9]+).([0-9]+),([0-9]+).([0-9]+) ([0-9]+) ([0-9]+)$`)
    78  
    79  // mergeCoverageFile reads coverage profile data from the file specified in
    80  // the path parameter and merges it with the summary coverage profile in
    81  // sumcp.
    82  func mergeCoverageFile(path string, sumcp *coverageProfile) {
    83  	// Phase I: read in the specified coverage profile data file, before we
    84  	// can attempt to merge it.
    85  	cp := readcovfile(path)
    86  	if cp == nil {
    87  		return
    88  	}
    89  	// Phase II: check for the proper coverage profile mode; if not set yet
    90  	// for the results, then accept the one from the coverage profile just
    91  	// read. Normally, this will be the "main" coverage profile file created
    92  	// by the process under test, as we'll read in the other profiles from
    93  	// re-executed children only later.
    94  	if sumcp.Mode == "" {
    95  		sumcp.Mode = cp.Mode
    96  	} else if cp.Mode != sumcp.Mode {
    97  		panic(fmt.Sprintf("expected mode %q, got mode %q", sumcp.Mode, cp.Mode))
    98  	}
    99  	// Phase III: for each source, append the new blocks to the existing ones,
   100  	// sort them, and then finally merge blocks.
   101  	setmode := sumcp.Mode == "set"
   102  	for srcname, source := range cp.Sources {
   103  		// Look up the corresponding source in the summary coverage profile,
   104  		// or create a new one, if not already present.
   105  		var sumsource *coverageProfileSource
   106  		var ok bool
   107  		if sumsource, ok = sumcp.Sources[srcname]; !ok {
   108  			sumsource = &coverageProfileSource{Blocks: source.Blocks}
   109  			sumcp.Sources[srcname] = sumsource
   110  		} else {
   111  			sumsource.Blocks = append(sumsource.Blocks, source.Blocks...)
   112  		}
   113  		// We might not merge, but we do sort anyway. While not strictly
   114  		// necessary, this helps our tests to have a well-defined result
   115  		// order.
   116  		mergecovblocks(sumsource, setmode)
   117  	}
   118  }
   119  
   120  // readcovfile reads a coverage profile data file and returns it as a
   121  // coverageProfile. Returns nil if no such coverage profile file exists or is
   122  // empty. If the file turns out to be unparseable for some other reason, it
   123  // simply panics.
   124  func readcovfile(path string) *coverageProfile {
   125  	cpf, err := os.Open(toOutputDir(path))
   126  	if err != nil {
   127  		if os.IsNotExist(err) {
   128  			// Silently skip the situation when a re-execution did not create
   129  			// a coverage profile data file.
   130  			return nil
   131  		}
   132  		panic(fmt.Sprintf(
   133  			"unable to merge coverage profile data file %q: %s",
   134  			toOutputDir(path), err.Error()))
   135  	}
   136  	defer cpf.Close()
   137  	scan := bufio.NewScanner(cpf)
   138  	if !scan.Scan() {
   139  		return nil
   140  	}
   141  	// Phase I: read in the specified coverage profile data file, before we
   142  	// can attempt to merge it.
   143  	cp := newCoverageProfile()
   144  	// The first line of a coverage profile data file is the mode how
   145  	// coverage data was gathered; either "atomic", "count", or "set".
   146  	line := scan.Text()
   147  	m := modeRe.FindStringSubmatch(line)
   148  	if m == nil {
   149  		panic(fmt.Sprintf(
   150  			"line %q doesn't match expected mode: line format", line))
   151  	}
   152  	cp.Mode = m[1]
   153  	// The remaining lines contain coverage profile block data. We optimize
   154  	// here on the basis that Go's testing/coverage.go writes coverage profile
   155  	// data files where the coverage block data for the same source file is
   156  	// continuous (instead of being scattered around). However, the code
   157  	// blocks are not sorted.
   158  	var srcname string                // caches most recent source filename.
   159  	var source *coverageProfileSource // caches most recent source data.
   160  	for scan.Scan() {
   161  		line = scan.Text()
   162  		m := lineRe.FindStringSubmatch(line)
   163  		if m == nil {
   164  			panic(fmt.Sprintf(
   165  				"line %q doesn't match expected block line format", line))
   166  		}
   167  		if m[1] != srcname {
   168  			// If we haven't seen this source filename yet, allocate a
   169  			// coverage data source element and put into the map of known
   170  			// sources.
   171  			srcname = m[1]
   172  			source = &coverageProfileSource{}
   173  			cp.Sources[srcname] = source
   174  		}
   175  		// Append the block data from the coverage profile data file line, the
   176  		// sequence of blocks is yet unsorted.
   177  		source.Blocks = append(source.Blocks, coverageProfileBlock{
   178  			StartLine: toUint32(m[2]),
   179  			StartCol:  toUint16(m[3]),
   180  			EndLine:   toUint32(m[4]),
   181  			EndCol:    toUint16(m[5]),
   182  			NumStmts:  toUint16(m[6]),
   183  			Counts:    toUint32(m[7]),
   184  		})
   185  	}
   186  	return cp
   187  }
   188  
   189  // toUint32 converts a textual int value into its binary uint32
   190  // representation. If the specified text doesn't represent a valid uint32
   191  // value, toUint32 panics.
   192  func toUint32(s string) uint32 {
   193  	if v, err := strconv.ParseUint(s, 10, 32); err != nil {
   194  		panic(err.Error())
   195  	} else {
   196  		return uint32(v)
   197  	}
   198  }
   199  
   200  // toUint16 converts a textual int value into its binary uint16
   201  // representation. If the specified text doesn't represent a valid uint16
   202  // value, toUint16 panics.
   203  func toUint16(s string) uint16 {
   204  	if v, err := strconv.ParseUint(s, 10, 16); err != nil {
   205  		panic(err.Error())
   206  	} else {
   207  		return uint16(v)
   208  	}
   209  }
   210  
   211  // mergecovblocks merges coverage blocks for the same code blocks.
   212  func mergecovblocks(sumsource *coverageProfileSource, setmode bool) {
   213  	// First sort, so that multiple coverages for the same block location will
   214  	// be adjacent.
   215  	sort.Sort(coverageProfileBlockByStart(sumsource.Blocks))
   216  	mergeidx := 0
   217  	for idx := mergeidx + 1; idx < len(sumsource.Blocks); idx++ {
   218  		mergeblock := &sumsource.Blocks[mergeidx]
   219  		block := &sumsource.Blocks[idx]
   220  		if mergeblock.StartLine == block.StartLine &&
   221  			mergeblock.StartCol == block.StartCol &&
   222  			mergeblock.EndLine == block.EndLine &&
   223  			mergeblock.EndCol == block.EndCol {
   224  			// We've found a(nother) matching code block, so update the
   225  			// first's coverage data.
   226  			if setmode {
   227  				mergeblock.Counts |= block.Counts
   228  			} else {
   229  				mergeblock.Counts += block.Counts
   230  			}
   231  			continue
   232  		}
   233  		// We've reached a different code location after a set of mergeable
   234  		// locations, so move this new location block downwards to the end of
   235  		// already merged blocks.
   236  		mergeidx++
   237  		if mergeidx != idx {
   238  			sumsource.Blocks[mergeidx] = *block
   239  		}
   240  	}
   241  	// Shorten the code block locations slice to only, erm, "cover" the unique
   242  	// (and probably merged) blocks.
   243  	sumsource.Blocks = sumsource.Blocks[:mergeidx+1]
   244  }