| // Copyright 2026 The BoringSSL Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package runner |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "os" |
| "path/filepath" |
| "slices" |
| "strconv" |
| "testing" |
| |
| "golang.org/x/crypto/cryptobyte" |
| ) |
| |
| // writeConnectionTrace writes a file in the format the shim writes, so that |
| // the recorder can read it back. |
| func writeConnectionTrace(t *testing.T, recorder *hintTraceRecorder, testBaseName string, connIndex int, clientHello, hint []byte) { |
| t.Helper() |
| if _, err := recorder.prepare(testBaseName); err != nil { |
| t.Fatalf("failed to create hint trace directory: %v", err) |
| } |
| trace := cryptobyte.NewBuilder(nil) |
| addUint24LengthPrefixedBytes(trace, clientHello) |
| addUint24LengthPrefixedBytes(trace, hint) |
| path := recorder.filePrefix(testBaseName) + strconv.Itoa(connIndex) |
| if err := os.WriteFile(path, trace.BytesOrPanic(), 0644); err != nil { |
| t.Fatalf("failed to write %s: %v", path, err) |
| } |
| } |
| |
| func TestReadConnections(t *testing.T) { |
| recorder := newHintTraceRecorder(t.TempDir()) |
| |
| // Simulate the files written by the C++ shim to transmit the hints and |
| // ClientHellos. |
| clientHellos := [][]byte{ |
| {0x03, 0x03, 0x01, 0x02, 0x03}, |
| {0x03, 0x03, 0x04, 0x05, 0x06}, |
| } |
| hints := [][]byte{ |
| {0xaa, 0xbb, 0xcc}, |
| {0xdd, 0xee, 0xff}, |
| } |
| for i := range clientHellos { |
| writeConnectionTrace(t, recorder, "TestA", i, clientHellos[i], hints[i]) |
| } |
| |
| connections, err := recorder.readConnections("TestA", len(clientHellos)) |
| if err != nil { |
| t.Fatalf("readConnections failed: %v", err) |
| } |
| if len(connections) != len(clientHellos) { |
| t.Fatalf("connections length mismatch: got %d, want %d", len(connections), len(clientHellos)) |
| } |
| for i, conn := range connections { |
| if !bytes.Equal(conn.ClientHello, clientHellos[i]) { |
| t.Errorf("connection %d ClientHello mismatch: got %x, want %x", i, conn.ClientHello, clientHellos[i]) |
| } |
| if !bytes.Equal(conn.HandshakeHint, hints[i]) { |
| t.Errorf("connection %d hint mismatch: got %x, want %x", i, conn.HandshakeHint, hints[i]) |
| } |
| } |
| } |
| |
| // TestReadConnectionsMissing tests that connections the shim did not write are |
| // skipped. |
| func TestReadConnectionsMissing(t *testing.T) { |
| recorder := newHintTraceRecorder(t.TempDir()) |
| writeConnectionTrace(t, recorder, "TestA", 0, []byte{1, 2, 3}, []byte{4, 5, 6}) |
| |
| connections, err := recorder.readConnections("TestA", 2) |
| if err != nil { |
| t.Fatalf("readConnections failed: %v", err) |
| } |
| if len(connections) != 1 { |
| t.Fatalf("connections length mismatch: got %d, want 1", len(connections)) |
| } |
| } |
| |
| func TestCleanup(t *testing.T) { |
| recorder := newHintTraceRecorder(t.TempDir()) |
| writeConnectionTrace(t, recorder, "TestA", 0, []byte{1, 2, 3}, []byte{4, 5, 6}) |
| |
| recorder.cleanup("TestA", 1) |
| |
| path := recorder.filePrefix("TestA") + "0" |
| if _, err := os.Stat(path); !os.IsNotExist(err) { |
| t.Errorf("%s was not removed: %v", path, err) |
| } |
| } |
| |
| func TestWriteTraces(t *testing.T) { |
| testDir := t.TempDir() |
| recorder := newHintTraceRecorder(testDir) |
| |
| // Record some traces. |
| recorder.record("TestA", []string{"-flag1", "val1"}, []connectionTrace{ |
| { |
| ClientHello: []byte{1, 2, 3}, |
| HandshakeHint: []byte{4, 5, 6}, |
| }, |
| }) |
| recorder.record("TestB", []string{"-flag2"}, []connectionTrace{ |
| { |
| ClientHello: []byte{7, 8}, |
| HandshakeHint: []byte{9, 10}, |
| }, |
| { |
| ClientHello: []byte{11, 12}, |
| HandshakeHint: []byte{13, 14}, |
| }, |
| }) |
| |
| if got := recorder.numTraces(); got != 2 { |
| t.Errorf("numTraces mismatch: got %d, want 2", got) |
| } |
| |
| // Write them. |
| if err := recorder.writeTraces(); err != nil { |
| t.Fatalf("writeTraces failed: %v", err) |
| } |
| |
| // Load them back. |
| path := filepath.Join(testDir, hintTracesSubdir, hintTracesFile) |
| data, err := os.ReadFile(path) |
| if err != nil { |
| t.Errorf("failed to read hint traces from %s: %v", path, err) |
| } |
| var traces map[string]*hintTrace |
| if err := json.Unmarshal(data, &traces); err != nil { |
| t.Errorf("failed to parse hint traces from %s: %v", path, err) |
| } |
| |
| if len(traces) != 2 { |
| t.Fatalf("expected 2 traces, got %d", len(traces)) |
| } |
| |
| // Verify TestA. |
| traceA, ok := traces["TestA"] |
| if !ok { |
| t.Fatal("missing trace for TestA") |
| } |
| if len(traceA.Connections) != 1 { |
| t.Fatalf("TestA Connections length mismatch: got %d, want 1", len(traceA.Connections)) |
| } |
| if !bytes.Equal(traceA.Connections[0].ClientHello, []byte{1, 2, 3}) { |
| t.Errorf("TestA ClientHello mismatch") |
| } |
| if !bytes.Equal(traceA.Connections[0].HandshakeHint, []byte{4, 5, 6}) { |
| t.Errorf("TestA HandshakeHint mismatch") |
| } |
| if !slices.Equal(traceA.ConfigFlags, []string{"-flag1", "val1"}) { |
| t.Errorf("TestA ConfigFlags mismatch: %v", traceA.ConfigFlags) |
| } |
| if traceA.Date == "" { |
| t.Error("TestA Date is empty") |
| } |
| |
| // Verify TestB (multi-connection / resumption). |
| traceB, ok := traces["TestB"] |
| if !ok { |
| t.Fatal("missing trace for TestB") |
| } |
| if len(traceB.Connections) != 2 { |
| t.Fatalf("TestB Connections length mismatch: got %d, want 2", len(traceB.Connections)) |
| } |
| if !bytes.Equal(traceB.Connections[0].ClientHello, []byte{7, 8}) { |
| t.Errorf("TestB Connection 0 ClientHello mismatch") |
| } |
| if !bytes.Equal(traceB.Connections[1].HandshakeHint, []byte{13, 14}) { |
| t.Errorf("TestB Connection 1 HandshakeHint mismatch") |
| } |
| } |