Source file
src/crypto/tls/bogo_shim_test.go
1
2
3
4
5 package tls
6
7 import (
8 "bytes"
9 "crypto/internal/cryptotest"
10 "crypto/x509"
11 "encoding/base64"
12 "encoding/json"
13 "encoding/pem"
14 "errors"
15 "flag"
16 "fmt"
17 "html/template"
18 "internal/byteorder"
19 "internal/testenv"
20 "io"
21 "log"
22 "net"
23 "os"
24 "path/filepath"
25 "runtime"
26 "slices"
27 "strconv"
28 "strings"
29 "testing"
30 "time"
31
32 "golang.org/x/crypto/cryptobyte"
33 )
34
35 const boringsslModVer = "v0.0.0-20250620172916-f51d8b099832"
36
37 var (
38 port = flag.String("port", "", "")
39 server = flag.Bool("server", false, "")
40
41 isHandshakerSupported = flag.Bool("is-handshaker-supported", false, "")
42
43 keyfile = flag.String("key-file", "", "")
44 certfile = flag.String("cert-file", "", "")
45 ocspResponse = flagBase64("ocsp-response", "")
46 signingPrefs = flagIntSlice("signing-prefs", "")
47
48 trustCert = flag.String("trust-cert", "", "")
49
50 minVersion = flag.Int("min-version", VersionSSL30, "")
51 maxVersion = flag.Int("max-version", VersionTLS13, "")
52 expectVersion = flag.Int("expect-version", 0, "")
53
54 noTLS1 = flag.Bool("no-tls1", false, "")
55 noTLS11 = flag.Bool("no-tls11", false, "")
56 noTLS12 = flag.Bool("no-tls12", false, "")
57 noTLS13 = flag.Bool("no-tls13", false, "")
58
59 requireAnyClientCertificate = flag.Bool("require-any-client-certificate", false, "")
60
61 shimWritesFirst = flag.Bool("shim-writes-first", false, "")
62
63 resumeCount = flag.Int("resume-count", 0, "")
64
65 curves = flagIntSlice("curves", "")
66 expectedCurve = flag.String("expect-curve-id", "", "")
67
68 verifyPrefs = flagIntSlice("verify-prefs", "")
69 expectedSigAlg = flag.String("expect-peer-signature-algorithm", "", "")
70 expectedPeerSigAlg = flagIntSlice("expect-peer-verify-pref", "")
71
72 shimID = flag.Uint64("shim-id", 0, "")
73 _ = flag.Bool("ipv6", false, "")
74
75 echConfigList = flagBase64("ech-config-list", "")
76 expectECHAccepted = flag.Bool("expect-ech-accept", false, "")
77 expectHRR = flag.Bool("expect-hrr", false, "")
78 expectNoHRR = flag.Bool("expect-no-hrr", false, "")
79 expectedECHRetryConfigs = flag.String("expect-ech-retry-configs", "", "")
80 expectNoECHRetryConfigs = flag.Bool("expect-no-ech-retry-configs", false, "")
81 onInitialExpectECHAccepted = flag.Bool("on-initial-expect-ech-accept", false, "")
82 _ = flag.Bool("expect-no-ech-name-override", false, "")
83 _ = flag.String("expect-ech-name-override", "", "")
84 _ = flag.Bool("reverify-on-resume", false, "")
85 onResumeECHConfigList = flagBase64("on-resume-ech-config-list", "")
86 _ = flag.Bool("on-resume-expect-reject-early-data", false, "")
87 onResumeExpectECHAccepted = flag.Bool("on-resume-expect-ech-accept", false, "")
88 _ = flag.Bool("on-resume-expect-no-ech-name-override", false, "")
89 expectedServerName = flag.String("expect-server-name", "", "")
90 echServerConfig = flagStringSlice("ech-server-config", "")
91 echServerKey = flagStringSlice("ech-server-key", "")
92 echServerRetryConfig = flagStringSlice("ech-is-retry-config", "")
93
94 expectSessionMiss = flag.Bool("expect-session-miss", false, "")
95
96 _ = flag.Bool("enable-early-data", false, "")
97 _ = flag.Bool("on-resume-expect-accept-early-data", false, "")
98 _ = flag.Bool("expect-ticket-supports-early-data", false, "")
99 _ = flag.Bool("on-resume-shim-writes-first", false, "")
100
101 advertiseALPN = flag.String("advertise-alpn", "", "")
102 expectALPN = flag.String("expect-alpn", "", "")
103 rejectALPN = flag.Bool("reject-alpn", false, "")
104 declineALPN = flag.Bool("decline-alpn", false, "")
105 expectAdvertisedALPN = flag.String("expect-advertised-alpn", "", "")
106 selectALPN = flag.String("select-alpn", "", "")
107
108 hostName = flag.String("host-name", "", "")
109
110 verifyPeer = flag.Bool("verify-peer", false, "")
111 _ = flag.Bool("use-custom-verify-callback", false, "")
112
113 waitForDebugger = flag.Bool("wait-for-debugger", false, "")
114 )
115
116 type stringSlice []string
117
118 func flagStringSlice(name, usage string) *stringSlice {
119 f := new(stringSlice)
120 flag.Var(f, name, usage)
121 return f
122 }
123
124 func (saf *stringSlice) String() string {
125 return strings.Join(*saf, ",")
126 }
127
128 func (saf *stringSlice) Set(s string) error {
129 *saf = append(*saf, s)
130 return nil
131 }
132
133 type intSlice []int64
134
135 func flagIntSlice(name, usage string) *intSlice {
136 f := new(intSlice)
137 flag.Var(f, name, usage)
138 return f
139 }
140
141 func (sf *intSlice) String() string {
142 return strings.Join(strings.Split(fmt.Sprint(*sf), " "), ",")
143 }
144
145 func (sf *intSlice) Set(s string) error {
146 i, err := strconv.ParseInt(s, 10, 64)
147 if err != nil {
148 return err
149 }
150 *sf = append(*sf, i)
151 return nil
152 }
153
154 type base64Flag []byte
155
156 func flagBase64(name, usage string) *base64Flag {
157 f := new(base64Flag)
158 flag.Var(f, name, usage)
159 return f
160 }
161
162 func (f *base64Flag) String() string {
163 return base64.StdEncoding.EncodeToString(*f)
164 }
165
166 func (f *base64Flag) Set(s string) error {
167 if *f != nil {
168 return fmt.Errorf("multiple base64 values not supported")
169 }
170 b, err := base64.StdEncoding.DecodeString(s)
171 if err != nil {
172 return err
173 }
174 *f = b
175 return nil
176 }
177
178 func bogoShim() {
179 if *isHandshakerSupported {
180 fmt.Println("No")
181 return
182 }
183
184 fmt.Printf("BoGo shim flags: %q", os.Args[1:])
185
186
187 var ciphersuites []uint16
188 for _, s := range append(CipherSuites(), InsecureCipherSuites()...) {
189 ciphersuites = append(ciphersuites, s.ID)
190 }
191
192 cfg := &Config{
193 ServerName: "test",
194
195 MinVersion: uint16(*minVersion),
196 MaxVersion: uint16(*maxVersion),
197
198 ClientSessionCache: NewLRUClientSessionCache(0),
199
200 CipherSuites: ciphersuites,
201
202 GetConfigForClient: func(chi *ClientHelloInfo) (*Config, error) {
203
204 if *expectAdvertisedALPN != "" {
205
206 s := cryptobyte.String(*expectAdvertisedALPN)
207
208 var expectedALPNs []string
209
210 for !s.Empty() {
211 var alpn cryptobyte.String
212 if !s.ReadUint8LengthPrefixed(&alpn) {
213 return nil, fmt.Errorf("unexpected error while parsing arguments for -expect-advertised-alpn")
214 }
215 expectedALPNs = append(expectedALPNs, string(alpn))
216 }
217
218 if !slices.Equal(chi.SupportedProtos, expectedALPNs) {
219 return nil, fmt.Errorf("unexpected ALPN: got %q, want %q", chi.SupportedProtos, expectedALPNs)
220 }
221 }
222 return nil, nil
223 },
224 }
225
226 if *noTLS1 {
227 cfg.MinVersion = VersionTLS11
228 if *noTLS11 {
229 cfg.MinVersion = VersionTLS12
230 if *noTLS12 {
231 cfg.MinVersion = VersionTLS13
232 if *noTLS13 {
233 log.Fatalf("no supported versions enabled")
234 }
235 }
236 }
237 } else if *noTLS13 {
238 cfg.MaxVersion = VersionTLS12
239 if *noTLS12 {
240 cfg.MaxVersion = VersionTLS11
241 if *noTLS11 {
242 cfg.MaxVersion = VersionTLS10
243 if *noTLS1 {
244 log.Fatalf("no supported versions enabled")
245 }
246 }
247 }
248 }
249
250 if *advertiseALPN != "" {
251 alpns := *advertiseALPN
252 for len(alpns) > 0 {
253 alpnLen := int(alpns[0])
254 cfg.NextProtos = append(cfg.NextProtos, alpns[1:1+alpnLen])
255 alpns = alpns[alpnLen+1:]
256 }
257 }
258
259 if *rejectALPN {
260 cfg.NextProtos = []string{"unnegotiableprotocol"}
261 }
262
263 if *declineALPN {
264 cfg.NextProtos = []string{}
265 }
266 if *selectALPN != "" {
267 cfg.NextProtos = []string{*selectALPN}
268 }
269
270 if *hostName != "" {
271 cfg.ServerName = *hostName
272 }
273
274 if *keyfile != "" || *certfile != "" {
275 pair, err := LoadX509KeyPair(*certfile, *keyfile)
276 if err != nil {
277 log.Fatalf("load key-file err: %s", err)
278 }
279 for _, id := range *signingPrefs {
280 pair.SupportedSignatureAlgorithms = append(pair.SupportedSignatureAlgorithms, SignatureScheme(id))
281 }
282 pair.OCSPStaple = *ocspResponse
283
284
285
286 cfg.GetCertificate = func(chi *ClientHelloInfo) (*Certificate, error) {
287 if *expectedPeerSigAlg != nil {
288 if len(chi.SignatureSchemes) != len(*expectedPeerSigAlg) {
289 return nil, fmt.Errorf("unexpected signature algorithms: got %s, want %v", chi.SignatureSchemes, *expectedPeerSigAlg)
290 }
291 for i := range *expectedPeerSigAlg {
292 if chi.SignatureSchemes[i] != SignatureScheme((*expectedPeerSigAlg)[i]) {
293 return nil, fmt.Errorf("unexpected signature algorithms: got %s, want %v", chi.SignatureSchemes, *expectedPeerSigAlg)
294 }
295 }
296 }
297 return &pair, nil
298 }
299 cfg.GetClientCertificate = func(cri *CertificateRequestInfo) (*Certificate, error) {
300 if *expectedPeerSigAlg != nil {
301 if len(cri.SignatureSchemes) != len(*expectedPeerSigAlg) {
302 return nil, fmt.Errorf("unexpected signature algorithms: got %s, want %v", cri.SignatureSchemes, *expectedPeerSigAlg)
303 }
304 for i := range *expectedPeerSigAlg {
305 if cri.SignatureSchemes[i] != SignatureScheme((*expectedPeerSigAlg)[i]) {
306 return nil, fmt.Errorf("unexpected signature algorithms: got %s, want %v", cri.SignatureSchemes, *expectedPeerSigAlg)
307 }
308 }
309 }
310 return &pair, nil
311 }
312 }
313 if *trustCert != "" {
314 pool := x509.NewCertPool()
315 certFile, err := os.ReadFile(*trustCert)
316 if err != nil {
317 log.Fatalf("load trust-cert err: %s", err)
318 }
319 block, _ := pem.Decode(certFile)
320 cert, err := x509.ParseCertificate(block.Bytes)
321 if err != nil {
322 log.Fatalf("parse trust-cert err: %s", err)
323 }
324 pool.AddCert(cert)
325 cfg.RootCAs = pool
326 }
327
328 if *requireAnyClientCertificate {
329 cfg.ClientAuth = RequireAnyClientCert
330 }
331 if *verifyPeer {
332 cfg.ClientAuth = VerifyClientCertIfGiven
333 }
334
335 if *echConfigList != nil {
336 cfg.EncryptedClientHelloConfigList = *echConfigList
337 cfg.MinVersion = VersionTLS13
338 }
339
340 if *curves != nil {
341 for _, id := range *curves {
342 cfg.CurvePreferences = append(cfg.CurvePreferences, CurveID(id))
343 }
344 }
345
346 if *verifyPrefs != nil {
347 for _, id := range *verifyPrefs {
348 testingOnlySupportedSignatureAlgorithms = append(testingOnlySupportedSignatureAlgorithms, SignatureScheme(id))
349 }
350 }
351
352 if *echServerConfig != nil {
353 if len(*echServerConfig) != len(*echServerKey) || len(*echServerConfig) != len(*echServerRetryConfig) {
354 log.Fatal("-ech-server-config, -ech-server-key, and -ech-is-retry-config mismatch")
355 }
356
357 for i, c := range *echServerConfig {
358 configBytes, err := base64.StdEncoding.DecodeString(c)
359 if err != nil {
360 log.Fatalf("parse ech-server-config err: %s", err)
361 }
362 privBytes, err := base64.StdEncoding.DecodeString((*echServerKey)[i])
363 if err != nil {
364 log.Fatalf("parse ech-server-key err: %s", err)
365 }
366
367 cfg.EncryptedClientHelloKeys = append(cfg.EncryptedClientHelloKeys, EncryptedClientHelloKey{
368 Config: configBytes,
369 PrivateKey: privBytes,
370 SendAsRetry: (*echServerRetryConfig)[i] == "1",
371 })
372 }
373 }
374
375 for i := 0; i < *resumeCount+1; i++ {
376 if i > 0 && *onResumeECHConfigList != nil {
377 cfg.EncryptedClientHelloConfigList = *onResumeECHConfigList
378 }
379
380 conn, err := net.Dial("tcp", net.JoinHostPort("localhost", *port))
381 if err != nil {
382 log.Fatalf("dial err: %s", err)
383 }
384 defer conn.Close()
385
386
387 shimIDBytes := make([]byte, 8)
388 byteorder.LEPutUint64(shimIDBytes, *shimID)
389 if _, err := conn.Write(shimIDBytes); err != nil {
390 log.Fatalf("failed to write shim id: %s", err)
391 }
392
393 var tlsConn *Conn
394 if *server {
395 tlsConn = Server(conn, cfg)
396 } else {
397 tlsConn = Client(conn, cfg)
398 }
399
400 if i == 0 && *shimWritesFirst {
401 if _, err := tlsConn.Write([]byte("hello")); err != nil {
402 log.Fatalf("write err: %s", err)
403 }
404 }
405
406
407
408 if *waitForDebugger {
409 pauseProcess()
410 }
411
412 for {
413 buf := make([]byte, 500)
414 var n int
415 n, err = tlsConn.Read(buf)
416 if err != nil {
417 break
418 }
419 buf = buf[:n]
420 for i := range buf {
421 buf[i] ^= 0xff
422 }
423 if _, err = tlsConn.Write(buf); err != nil {
424 break
425 }
426 }
427 if err != io.EOF {
428
429
430
431
432 orderlyShutdown(tlsConn)
433
434 retryErr, ok := err.(*ECHRejectionError)
435 if !ok {
436 log.Fatal(err)
437 }
438 if *expectNoECHRetryConfigs && len(retryErr.RetryConfigList) > 0 {
439 log.Fatalf("expected no ECH retry configs, got some")
440 }
441 if *expectedECHRetryConfigs != "" {
442 expectedRetryConfigs, err := base64.StdEncoding.DecodeString(*expectedECHRetryConfigs)
443 if err != nil {
444 log.Fatalf("failed to decode expected retry configs: %s", err)
445 }
446 if !bytes.Equal(retryErr.RetryConfigList, expectedRetryConfigs) {
447 log.Fatalf("unexpected retry list returned: got %x, want %x", retryErr.RetryConfigList, expectedRetryConfigs)
448 }
449 }
450 log.Fatalf("conn error: %s", err)
451 }
452
453 cs := tlsConn.ConnectionState()
454 if cs.HandshakeComplete {
455 if *expectALPN != "" && cs.NegotiatedProtocol != *expectALPN {
456 log.Fatalf("unexpected protocol negotiated: want %q, got %q", *expectALPN, cs.NegotiatedProtocol)
457 }
458
459 if *selectALPN != "" && cs.NegotiatedProtocol != *selectALPN {
460 log.Fatalf("unexpected protocol negotiated: want %q, got %q", *selectALPN, cs.NegotiatedProtocol)
461 }
462
463 if *expectVersion != 0 && cs.Version != uint16(*expectVersion) {
464 log.Fatalf("expected ssl version %q, got %q", uint16(*expectVersion), cs.Version)
465 }
466 if *declineALPN && cs.NegotiatedProtocol != "" {
467 log.Fatal("unexpected ALPN protocol")
468 }
469 if *expectECHAccepted && !cs.ECHAccepted {
470 log.Fatal("expected ECH to be accepted, but connection state shows it was not")
471 } else if i == 0 && *onInitialExpectECHAccepted && !cs.ECHAccepted {
472 log.Fatal("expected ECH to be accepted, but connection state shows it was not")
473 } else if i > 0 && *onResumeExpectECHAccepted && !cs.ECHAccepted {
474 log.Fatal("expected ECH to be accepted on resumption, but connection state shows it was not")
475 } else if i == 0 && !*expectECHAccepted && cs.ECHAccepted {
476 log.Fatal("did not expect ECH, but it was accepted")
477 }
478
479 if *expectHRR && !cs.testingOnlyDidHRR {
480 log.Fatal("expected HRR but did not do it")
481 }
482
483 if *expectNoHRR && cs.testingOnlyDidHRR {
484 log.Fatal("expected no HRR but did do it")
485 }
486
487 if *expectSessionMiss && cs.DidResume {
488 log.Fatal("unexpected session resumption")
489 }
490
491 if *expectedServerName != "" && cs.ServerName != *expectedServerName {
492 log.Fatalf("unexpected server name: got %q, want %q", cs.ServerName, *expectedServerName)
493 }
494 }
495
496 if *expectedCurve != "" {
497 expectedCurveID, err := strconv.Atoi(*expectedCurve)
498 if err != nil {
499 log.Fatalf("failed to parse -expect-curve-id: %s", err)
500 }
501 if cs.CurveID != CurveID(expectedCurveID) {
502 log.Fatalf("unexpected curve id: want %d, got %d", expectedCurveID, tlsConn.curveID)
503 }
504 }
505
506
507 if *expectedSigAlg != "" && !cs.DidResume {
508 expectedSigAlgID, err := strconv.Atoi(*expectedSigAlg)
509 if err != nil {
510 log.Fatalf("failed to parse -expect-peer-signature-algorithm: %s", err)
511 }
512 if cs.testingOnlyPeerSignatureAlgorithm != SignatureScheme(expectedSigAlgID) {
513 log.Fatalf("unexpected peer signature algorithm: want %s, got %s", SignatureScheme(expectedSigAlgID), cs.testingOnlyPeerSignatureAlgorithm)
514 }
515 }
516 }
517 }
518
519
520
521
522
523
524 func orderlyShutdown(tlsConn *Conn) {
525
526 tlsConn.flush()
527
528 netConn := tlsConn.NetConn()
529 tcpConn := netConn.(*net.TCPConn)
530 tcpConn.CloseWrite()
531
532
533 buf := make([]byte, maxPlaintext)
534 for {
535 n, err := tcpConn.Read(buf)
536 if n == 0 || err != nil {
537 break
538 }
539 }
540
541 tcpConn.CloseRead()
542 }
543
544 func TestBogoSuite(t *testing.T) {
545 testenv.MustHaveGoBuild(t)
546 if testing.Short() {
547 t.Skip("skipping in short mode")
548 }
549 if testenv.Builder() != "" && runtime.GOOS == "windows" {
550 t.Skip("#66913: windows network connections are flakey on builders")
551 }
552 skipFIPS(t)
553
554
555
556
557
558 if _, err := os.Stat("bogo_config.json"); err != nil {
559 t.Fatal(err)
560 }
561
562 var bogoDir string
563 if *bogoLocalDir != "" {
564 ensureLocalBogo(t, *bogoLocalDir)
565 bogoDir = *bogoLocalDir
566 } else {
567 bogoDir = cryptotest.FetchModule(t, "boringssl.googlesource.com/boringssl.git", boringsslModVer)
568 }
569
570 cwd, err := os.Getwd()
571 if err != nil {
572 t.Fatal(err)
573 }
574
575 resultsFile := filepath.Join(t.TempDir(), "results.json")
576
577 args := []string{
578 "test",
579 ".",
580 fmt.Sprintf("-shim-config=%s", filepath.Join(cwd, "bogo_config.json")),
581 fmt.Sprintf("-shim-path=%s", os.Args[0]),
582 "-shim-extra-flags=-bogo-mode",
583 "-allow-unimplemented",
584 "-loose-errors",
585 fmt.Sprintf("-json-output=%s", resultsFile),
586 }
587 if *bogoFilter != "" {
588 args = append(args, fmt.Sprintf("-test=%s", *bogoFilter))
589 }
590
591 cmd := testenv.Command(t, testenv.GoToolPath(t), args...)
592 out := &strings.Builder{}
593 cmd.Stderr = out
594 cmd.Dir = filepath.Join(bogoDir, "ssl/test/runner")
595 err = cmd.Run()
596
597
598
599
600
601
602 resultsJSON, jsonErr := os.ReadFile(resultsFile)
603 if jsonErr != nil {
604 if err != nil {
605 t.Fatalf("bogo failed: %s\n%s", err, out)
606 }
607 t.Fatalf("failed to read results JSON file: %s", jsonErr)
608 }
609
610 var results bogoResults
611 if err := json.Unmarshal(resultsJSON, &results); err != nil {
612 t.Fatalf("failed to parse results JSON: %s", err)
613 }
614
615 if *bogoReport != "" {
616 if err := generateReport(results, *bogoReport); err != nil {
617 t.Fatalf("failed to generate report: %v", err)
618 }
619 }
620
621
622
623
624 assertResults := map[string]string{
625 "CurveTest-Client-MLKEM-TLS13": "PASS",
626 "CurveTest-Server-MLKEM-TLS13": "PASS",
627
628
629
630 "ClientAuth-Enforced": "PASS",
631 "ServerAuth-Enforced": "PASS",
632 "ClientAuth-Enforced-TLS13": "PASS",
633 "ServerAuth-Enforced-TLS13": "PASS",
634 "VerifyPreferences-Advertised": "PASS",
635 "VerifyPreferences-Enforced": "PASS",
636 "Client-TLS12-NoSign-RSA_PKCS1_MD5_SHA1": "PASS",
637 "Server-TLS12-NoSign-RSA_PKCS1_MD5_SHA1": "PASS",
638 "Client-TLS13-NoSign-RSA_PKCS1_MD5_SHA1": "PASS",
639 "Server-TLS13-NoSign-RSA_PKCS1_MD5_SHA1": "PASS",
640 }
641
642 for name, result := range results.Tests {
643
644 t.Run(name, func(t *testing.T) {
645 if result.Actual == "FAIL" && result.IsUnexpected {
646 t.Fail()
647 }
648 if result.Error != "" {
649 t.Log(result.Error)
650 }
651 if exp, ok := assertResults[name]; ok && exp != result.Actual {
652 t.Errorf("unexpected result: got %s, want %s", result.Actual, exp)
653 }
654 delete(assertResults, name)
655 if result.Actual == "SKIP" {
656 t.SkipNow()
657 }
658 })
659 }
660 if *bogoFilter == "" {
661
662 for name, expectedResult := range assertResults {
663 t.Run(name, func(t *testing.T) {
664 t.Fatalf("expected test to run with result %s, but it was not present in the test results", expectedResult)
665 })
666 }
667 }
668 }
669
670
671
672
673
674
675 func ensureLocalBogo(t *testing.T, localBogoDir string) {
676 t.Helper()
677
678 if stat, err := os.Stat(localBogoDir); err == nil {
679 if !stat.IsDir() {
680 t.Fatalf("local bogo dir (%q) exists but is not a directory", localBogoDir)
681 }
682
683 t.Logf("using local bogo checkout from %q", localBogoDir)
684 return
685 } else if !errors.Is(err, os.ErrNotExist) {
686 t.Fatalf("failed to stat local bogo dir (%q): %v", localBogoDir, err)
687 }
688
689 testenv.MustHaveExecPath(t, "git")
690
691 idx := strings.LastIndex(boringsslModVer, "-")
692 if idx == -1 || idx == len(boringsslModVer)-1 {
693 t.Fatalf("invalid boringsslModVer format: %q", boringsslModVer)
694 }
695 commitSHA := boringsslModVer[idx+1:]
696
697 t.Logf("cloning boringssl@%s to %q", commitSHA, localBogoDir)
698 cloneCmd := testenv.Command(t, "git", "clone", "--no-checkout", "https://boringssl.googlesource.com/boringssl", localBogoDir)
699 if err := cloneCmd.Run(); err != nil {
700 t.Fatalf("git clone failed: %v", err)
701 }
702
703 checkoutCmd := testenv.Command(t, "git", "checkout", commitSHA)
704 checkoutCmd.Dir = localBogoDir
705 if err := checkoutCmd.Run(); err != nil {
706 t.Fatalf("git checkout failed: %v", err)
707 }
708
709 t.Logf("using fresh local bogo checkout from %q", localBogoDir)
710 return
711 }
712
713 func generateReport(results bogoResults, outPath string) error {
714 data := reportData{
715 Results: results,
716 Timestamp: time.Unix(int64(results.SecondsSinceEpoch), 0).Format("2006-01-02 15:04:05"),
717 Revision: boringsslModVer,
718 }
719
720 tmpl := template.Must(template.New("report").Parse(reportTemplate))
721 file, err := os.Create(outPath)
722 if err != nil {
723 return err
724 }
725 defer file.Close()
726
727 return tmpl.Execute(file, data)
728 }
729
730
731 type bogoResults struct {
732 Version int `json:"version"`
733 Interrupted bool `json:"interrupted"`
734 PathDelimiter string `json:"path_delimiter"`
735 SecondsSinceEpoch float64 `json:"seconds_since_epoch"`
736 NumFailuresByType map[string]int `json:"num_failures_by_type"`
737 Tests map[string]struct {
738 Actual string `json:"actual"`
739 Expected string `json:"expected"`
740 IsUnexpected bool `json:"is_unexpected"`
741 Error string `json:"error,omitempty"`
742 } `json:"tests"`
743 }
744
745 type reportData struct {
746 Results bogoResults
747 SkipReasons map[string]string
748 Timestamp string
749 Revision string
750 }
751
752 const reportTemplate = `
753 <!DOCTYPE html>
754 <html>
755 <head>
756 <title>BoGo Results Report</title>
757 <style>
758 body { font-family: monospace; margin: 20px; }
759 .summary { background: #f5f5f5; padding: 10px; margin-bottom: 20px; }
760 .controls { margin-bottom: 10px; }
761 .controls input, select { margin-right: 10px; }
762 table { width: 100%; border-collapse: collapse; table-layout: fixed; }
763 th, td { border: 1px solid #ddd; padding: 8px; text-align: left; vertical-align: top; }
764 th { background-color: #f2f2f2; cursor: pointer; }
765 .name-col { width: 30%; }
766 .status-col { width: 8%; }
767 .actual-col { width: 8%; }
768 .expected-col { width: 8%; }
769 .error-col { width: 26%; }
770 .PASS { background-color: #d4edda; }
771 .FAIL { background-color: #f8d7da; }
772 .SKIP { background-color: #fff3cd; }
773 .error {
774 font-family: monospace;
775 font-size: 0.9em;
776 color: #721c24;
777 white-space: pre-wrap;
778 word-break: break-word;
779 }
780 </style>
781 </head>
782 <body>
783 <h1>BoGo Results Report</h1>
784
785 <div class="summary">
786 <strong>Generated:</strong> {{.Timestamp}} | <strong>BoGo Revision:</strong> {{.Revision}}<br>
787 {{range $status, $count := .Results.NumFailuresByType}}
788 <strong>{{$status}}:</strong> {{$count}} |
789 {{end}}
790 </div>
791
792 <div class="controls">
793 <input type="text" id="search" placeholder="Search tests..." onkeyup="filterTests()">
794 <select id="statusFilter" onchange="filterTests()">
795 <option value="">All</option>
796 <option value="FAIL">Failed</option>
797 <option value="PASS">Passed</option>
798 <option value="SKIP">Skipped</option>
799 </select>
800 </div>
801
802 <table id="resultsTable">
803 <thead>
804 <tr>
805 <th class="name-col" onclick="sortBy('name')">Test Name</th>
806 <th class="status-col" onclick="sortBy('status')">Status</th>
807 <th class="actual-col" onclick="sortBy('actual')">Actual</th>
808 <th class="expected-col" onclick="sortBy('expected')">Expected</th>
809 <th class="error-col">Error</th>
810 </tr>
811 </thead>
812 <tbody>
813 {{range $name, $test := .Results.Tests}}
814 <tr class="{{$test.Actual}}" data-name="{{$name}}" data-status="{{$test.Actual}}">
815 <td>{{$name}}</td>
816 <td>{{$test.Actual}}</td>
817 <td>{{$test.Actual}}</td>
818 <td>{{$test.Expected}}</td>
819 <td class="error">{{$test.Error}}</td>
820 </tr>
821 {{end}}
822 </tbody>
823 </table>
824
825 <script>
826 function filterTests() {
827 const search = document.getElementById('search').value.toLowerCase();
828 const status = document.getElementById('statusFilter').value;
829 const rows = document.querySelectorAll('#resultsTable tbody tr');
830
831 rows.forEach(row => {
832 const name = row.dataset.name.toLowerCase();
833 const rowStatus = row.dataset.status;
834 const matchesSearch = name.includes(search);
835 const matchesStatus = !status || rowStatus === status;
836
837 row.style.display = matchesSearch && matchesStatus ? '' : 'none';
838 });
839 }
840
841 function sortBy(column) {
842 const tbody = document.querySelector('#resultsTable tbody');
843 const rows = Array.from(tbody.querySelectorAll('tr'));
844
845 rows.sort((a, b) => {
846 if (column === 'status') {
847 const statusOrder = {'FAIL': 0, 'PASS': 1, 'SKIP': 2};
848 const aStatus = a.dataset.status;
849 const bStatus = b.dataset.status;
850 if (aStatus !== bStatus) {
851 return statusOrder[aStatus] - statusOrder[bStatus];
852 }
853 return a.dataset.name.localeCompare(b.dataset.name);
854 } else {
855 return a.dataset.name.localeCompare(b.dataset.name);
856 }
857 });
858
859 rows.forEach(row => tbody.appendChild(row));
860 filterTests();
861 }
862
863 sortBy("status");
864 </script>
865 </body>
866 </html>
867 `
868
View as plain text