github.com/zmap/zlint@v1.1.0/integration/corpus_test.go (about)

     1  // +build integration
     2  
     3  package integration
     4  
     5  import (
     6  	"fmt"
     7  	"log"
     8  	"sort"
     9  	"strings"
    10  	"sync"
    11  	"testing"
    12  
    13  	"github.com/zmap/zlint"
    14  	"github.com/zmap/zlint/lints"
    15  )
    16  
    17  // lintCertificate lints the provided work item's certificate to produce
    18  // a certResult that can be used to determine which lint results the certificate
    19  // had without maintaining the full ResultSet. If lintFilter is not nil only
    20  // lints with names matching the filter will be run.
    21  func lintCertificate(work workItem) certResult {
    22  	// Lint the certiifcate to produce a full result set
    23  	cr := certResult{
    24  		Fingerprint: work.Fingerprint,
    25  		LintSummary: make(map[string]lints.LintStatus),
    26  	}
    27  	resultSet := zlint.LintCertificateFiltered(work.Certificate, lintFilter)
    28  	for lintName, r := range resultSet.Results {
    29  		cr.LintSummary[lintName] = r.Status
    30  		cr.Result.Inc(r.Status)
    31  	}
    32  	return cr
    33  }
    34  
    35  // keyedCounts are a map from a string key (hex encoded cert fingerprint, lint name)
    36  // to a resultCount for that key.
    37  type keyedCounts map[string]resultCount
    38  
    39  // String returns a sorted table of keys and their resultCount that is formatted
    40  // for printing. Keys should be less than 65 characters long to preserve the
    41  // table format.
    42  func (counts keyedCounts) String() string {
    43  	var keys []string
    44  	for k := range counts {
    45  		keys = append(keys, k)
    46  	}
    47  	sort.Strings(keys)
    48  
    49  	var buf strings.Builder
    50  	for _, k := range keys {
    51  		buf.WriteString(fmt.Sprintf("%-65s\t%s\n", k, counts[k]))
    52  	}
    53  	return buf.String()
    54  }
    55  
    56  // TestCorpus concurrently reads certificates from each of the global conf's CSV
    57  // data files while in parallel linting the certificates and counting how many
    58  // of each lint result are produced across all data files. The lint result
    59  // totals are enforced against the expected values from the global conf.
    60  func TestCorpus(t *testing.T) {
    61  	// Create a work channel with enough capacity to let each loader write
    62  	// 1 work item without blocking.
    63  	workChannel := make(chan workItem, len(conf.Files))
    64  
    65  	// Start loading certificates from the config CSV files. This is done in
    66  	// a separate Go routine because loadCSV will block until completion. We want
    67  	// to let the test continue to run so certificates can be linted as they
    68  	// arrive.
    69  	go func() {
    70  		loadCSV(workChannel, conf.CacheDir)
    71  	}()
    72  
    73  	log.Printf(
    74  		"Linting certificates using %d Go routines. "+
    75  			"Printing one '.' per %d certificates",
    76  		*parallelism, *outputTick)
    77  
    78  	// Create *parallelism separate Go routines for reading certificates from
    79  	// the work channel, linting them, and writing the result to a results
    80  	// channel.
    81  	results := make(chan certResult, *parallelism)
    82  	var wg sync.WaitGroup
    83  	for i := 0; i < *parallelism; i++ {
    84  		wg.Add(1)
    85  		go func() {
    86  			// Read work until the channel is closed
    87  			for c := range workChannel {
    88  				results <- lintCertificate(c)
    89  			}
    90  			// Once the workChannel has closed this routine is done.
    91  			wg.Done()
    92  		}()
    93  	}
    94  
    95  	// Also start a Go routine to read from the results channel, aggregating the
    96  	// results into the results map
    97  	var total int
    98  	var fatalResults int
    99  	resultsByFP := make(keyedCounts)
   100  	resultsByLint := make(keyedCounts)
   101  	doneChan := make(chan bool, 1)
   102  	go func() {
   103  		// Read results as they arrive on the channel until it is closed.
   104  		for r := range results {
   105  			// Count fatal results separately since this should always be 0
   106  			fatalResults += int(r.Result.FatalCount)
   107  			// if the result had some error/warn/info findings, track the fingerprint
   108  			// in the resultsByFP map and update the resultsByLint count for each
   109  			// of the lints that didn't pass.
   110  			if !r.Result.fullPass() {
   111  				resultsByFP[r.Fingerprint] = r.Result
   112  				for lintName, status := range r.LintSummary {
   113  					cur := resultsByLint[lintName]
   114  					cur.Inc(status)
   115  					resultsByLint[lintName] = cur
   116  				}
   117  			}
   118  
   119  			// Every *outputTick certificate results print a '.' to keep CI from thinking this
   120  			// long running job is dead in the water.
   121  			total++
   122  			if total%*outputTick == 0 {
   123  				fmt.Printf(".")
   124  			}
   125  		}
   126  		// Once the results channel is closed and we're done tabulating in this
   127  		// routine write to the doneChan so the test can complete.
   128  		doneChan <- true
   129  	}()
   130  
   131  	// Wait for the work channel to be drained by all of the workers.
   132  	wg.Wait()
   133  	// Close the results channel
   134  	close(results)
   135  	// Wait for the results tabulation routine to complete.
   136  	<-doneChan
   137  
   138  	// Verify results match the conf's expected totals.
   139  	t.Logf("linted %d certificates", total)
   140  	// There should never be any fatal results.
   141  	if fatalResults != 0 {
   142  		t.Errorf("expected 0 fatal results, found %d\n", fatalResults)
   143  	}
   144  
   145  	if *fpSummarize {
   146  		fmt.Println("\nsummary of result type by certificate fingerprint:")
   147  		fmt.Println(resultsByFP)
   148  	}
   149  
   150  	if *lintSummarize {
   151  		fmt.Println("\nsummary of result type by lint name:")
   152  		fmt.Println(resultsByLint)
   153  	}
   154  
   155  	// No expected to confirm against, save a new expected
   156  	if len(conf.Expected) == 0 {
   157  		t.Logf("config file %q had no expected map to enforce results against",
   158  			*configFile)
   159  	} else {
   160  		// Otherwise enforce the maps match
   161  		for k, v := range resultsByLint {
   162  			if conf.Expected[k] != v {
   163  				t.Errorf("expected lint %q to have result %s got %s\n",
   164  					k, conf.Expected[k], v)
   165  			}
   166  		}
   167  	}
   168  
   169  	// If *overwriteExpected is true overwrite the expected map with the results
   170  	// from this run and save the updated configuration to disk. If there were
   171  	// t.Errorf's in this run then they will pass next run because the
   172  	// expectations will match reality. This should primarily be used to bootstrap
   173  	// an initial expectedMap or to update the expectedMap with vetted changes to
   174  	// the corpus that result from new lints, bugfixes, etc.
   175  	if *overwriteExpected {
   176  		t.Logf("overwriting expected map in config file %q",
   177  			*configFile)
   178  		conf.Expected = resultsByLint
   179  		if err := conf.Save(*configFile); err != nil {
   180  			t.Errorf("failed to save expected map to config file %q: %v", *configFile, err)
   181  		}
   182  	}
   183  }