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