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 }