Skip to content

Commit 3a4befc

Browse files
committed
Add shared client support with log management
This commit implements shared client functionality to improve resource efficiency by allowing clients to be reused across multiple tests in a test suite. Key features include: - New API endpoints for shared client management - Data structures for tracking shared client state - Log segment extraction for proper test result attribution - Integration with hiveview for log display - Documentation updates for all new methods and types
1 parent 4bd2d8a commit 3a4befc

File tree

8 files changed

+790
-8
lines changed

8 files changed

+790
-8
lines changed

hivesim/data.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ type TestStartInfo struct {
2323
Description string `json:"description"`
2424
}
2525

26+
// LogOffset tracks the start and end positions in a log file.
27+
type LogOffset struct {
28+
Start int64 `json:"start"` // Byte offset where this section begins
29+
End int64 `json:"end"` // Byte offset where this section ends
30+
}
31+
32+
// ClientLogInfo tracks log offsets for a specific client in a test.
33+
type ClientLogInfo struct {
34+
ClientID string `json:"client_id"` // ID of the client container
35+
LogOffset LogOffset `json:"log_offset"` // Offset range in the log file
36+
}
37+
2638
// ExecInfo is the result of running a command in a client container.
2739
type ExecInfo struct {
2840
Stdout string `json:"stdout"`
@@ -46,3 +58,24 @@ type ClientDefinition struct {
4658
func (m *ClientDefinition) HasRole(role string) bool {
4759
return slices.Contains(m.Meta.Roles, role)
4860
}
61+
62+
// ClientMode defines whether a client is shared across tests or dedicated to a single test.
63+
// Two modes are supported: DedicatedClient (default) and SharedClient.
64+
type ClientMode int
65+
66+
const (
67+
// DedicatedClient is a client that is used for a single test (default behavior)
68+
DedicatedClient ClientMode = iota
69+
// SharedClient is a client that is shared across multiple tests in a suite
70+
SharedClient
71+
)
72+
73+
// SharedClientInfo contains information about a shared client instance.
74+
// This includes container identification and connectivity information.
75+
type SharedClientInfo struct {
76+
ID string // Container ID
77+
Type string // Client type
78+
IP string // Client IP address
79+
CreatedAt int64 // Timestamp when client was created
80+
LogFile string // Path to client log file
81+
}

hivesim/hive.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,69 @@ func (sim *Simulation) StartClientWithOptions(testSuite SuiteID, test TestID, cl
207207
return resp.ID, ip, nil
208208
}
209209

210+
// StartSharedClient starts a new node as a shared client at the suite level.
211+
// The client persists for the duration of the suite and can be used by multiple tests.
212+
// Returns container id and ip.
213+
func (sim *Simulation) StartSharedClient(testSuite SuiteID, clientType string, options ...StartOption) (string, net.IP, error) {
214+
if sim.docs != nil {
215+
return "", nil, errors.New("StartSharedClient is not supported in docs mode")
216+
}
217+
var (
218+
url = fmt.Sprintf("%s/testsuite/%d/shared-client", sim.url, testSuite)
219+
resp simapi.StartNodeResponse
220+
)
221+
222+
setup := &clientSetup{
223+
files: make(map[string]func() (io.ReadCloser, error)),
224+
config: simapi.NodeConfig{
225+
Client: clientType,
226+
Environment: make(map[string]string),
227+
IsShared: true, // Mark this client as shared
228+
},
229+
}
230+
for _, opt := range options {
231+
opt.apply(setup)
232+
}
233+
234+
err := setup.postWithFiles(url, &resp)
235+
if err != nil {
236+
return "", nil, err
237+
}
238+
ip := net.ParseIP(resp.IP)
239+
if ip == nil {
240+
return resp.ID, nil, fmt.Errorf("no IP address returned")
241+
}
242+
return resp.ID, ip, nil
243+
}
244+
245+
// GetSharedClientInfo retrieves information about a shared client,
246+
// including its ID, type, IP and log file location.
247+
func (sim *Simulation) GetSharedClientInfo(testSuite SuiteID, clientID string) (*SharedClientInfo, error) {
248+
if sim.docs != nil {
249+
return nil, errors.New("GetSharedClientInfo is not supported in docs mode")
250+
}
251+
var (
252+
url = fmt.Sprintf("%s/testsuite/%d/shared-client/%s", sim.url, testSuite, clientID)
253+
resp SharedClientInfo
254+
)
255+
err := get(url, &resp)
256+
return &resp, err
257+
}
258+
259+
// GetClientLogOffset gets the current offset position in a shared client's log file.
260+
// This is used for tracking log segments in shared clients across multiple tests.
261+
func (sim *Simulation) GetClientLogOffset(testSuite SuiteID, clientID string) (int64, error) {
262+
if sim.docs != nil {
263+
return 0, errors.New("GetClientLogOffset is not supported in docs mode")
264+
}
265+
var (
266+
url = fmt.Sprintf("%s/testsuite/%d/shared-client/%s/log-offset", sim.url, testSuite, clientID)
267+
resp int64
268+
)
269+
err := get(url, &resp)
270+
return resp, err
271+
}
272+
210273
// StopClient signals to the host that the node is no longer required.
211274
func (sim *Simulation) StopClient(testSuite SuiteID, test TestID, nodeid string) error {
212275
if sim.docs != nil {

hivesim/testapi.go

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ type Suite struct {
2121
Category string // Category of the test suite [Optional]
2222
Description string // Description of the test suite (if empty, suite won't appear in documentation) [Optional]
2323
Tests []AnyTest
24+
25+
// SharedClients maps client IDs to client instances that are shared across tests
26+
SharedClients map[string]*Client
27+
28+
// Internal tracking
29+
sharedClientOpts map[string][]StartOption // Stores options for starting shared clients
2430
}
2531

2632
func (s *Suite) request() *simapi.TestRequest {
@@ -39,6 +45,30 @@ func (s *Suite) Add(test AnyTest) *Suite {
3945
return s
4046
}
4147

48+
// AddSharedClient registers a client to be shared across all tests in the suite.
49+
// The client will be started when the suite begins and terminated when the suite ends.
50+
// This is useful for maintaining state across tests for incremental testing or
51+
// avoiding client initialization for every test.
52+
func (s *Suite) AddSharedClient(clientID string, clientType string, options ...StartOption) *Suite {
53+
if s.SharedClients == nil {
54+
s.SharedClients = make(map[string]*Client)
55+
}
56+
if s.sharedClientOpts == nil {
57+
s.sharedClientOpts = make(map[string][]StartOption)
58+
}
59+
60+
// Store options for later use when the suite is started
61+
s.sharedClientOpts[clientID] = append([]StartOption{}, options...)
62+
63+
// Create a placeholder client that will be initialized when the suite runs
64+
s.SharedClients[clientID] = &Client{
65+
Type: clientType,
66+
IsShared: true,
67+
}
68+
69+
return s
70+
}
71+
4272
// AnyTest is a TestSpec or ClientTestSpec.
4373
type AnyTest interface {
4474
runTest(*Simulation, SuiteID, *Suite) error
@@ -77,11 +107,46 @@ func RunSuite(host *Simulation, suite Suite) error {
77107
}
78108
defer host.EndSuite(suiteID)
79109

110+
// Start shared clients for the suite
111+
if len(suite.SharedClients) > 0 {
112+
fmt.Printf("Starting %d shared clients for suite %s...\n", len(suite.SharedClients), suite.Name)
113+
114+
// Initialize any shared clients defined for this suite
115+
for clientID, client := range suite.SharedClients {
116+
// Retrieve stored options for this client
117+
options := suite.sharedClientOpts[clientID]
118+
119+
// Start the shared client
120+
containerID, ip, err := host.StartSharedClient(suiteID, client.Type, options...)
121+
if err != nil {
122+
fmt.Fprintf(os.Stderr, "Error starting shared client %s: %v\n", clientID, err)
123+
return err
124+
}
125+
126+
// Update the client object with actual container information
127+
client.Container = containerID
128+
client.IP = ip
129+
client.SuiteID = suiteID
130+
client.IsShared = true
131+
132+
fmt.Printf("Started shared client %s (container: %s)\n", clientID, containerID)
133+
}
134+
}
135+
136+
// Run all tests in the suite
80137
for _, test := range suite.Tests {
81138
if err := test.runTest(host, suiteID, &suite); err != nil {
82139
return err
83140
}
84141
}
142+
143+
// Clean up any shared clients at the end of the suite
144+
// They are automatically stopped when the suite ends via defer host.EndSuite(suiteID) above
145+
// But we should output a message for clarity
146+
if len(suite.SharedClients) > 0 {
147+
fmt.Printf("Cleaning up %d shared clients for suite %s...\n", len(suite.SharedClients), suite.Name)
148+
}
149+
85150
return nil
86151
}
87152

@@ -164,6 +229,11 @@ type Client struct {
164229
rpc *rpc.Client
165230
enginerpc *rpc.Client
166231
test *T
232+
233+
// Fields for shared client support
234+
IsShared bool // Whether this client is shared across tests
235+
LogPosition int64 // Current position in the log file (for shared clients)
236+
SuiteID SuiteID // The suite this client belongs to (for shared clients)
167237
}
168238

169239
// EnodeURL returns the default peer-to-peer endpoint of the client.
@@ -227,6 +297,9 @@ type T struct {
227297
suite *Suite
228298
mu sync.Mutex
229299
result TestResult
300+
301+
// Fields for tracking client logs
302+
clientLogOffsets map[string]*LogOffset // Tracks log offsets for clients used in this test
230303
}
231304

232305
// StartClient starts a client instance. If the client cannot by started, the test fails immediately.
@@ -235,7 +308,59 @@ func (t *T) StartClient(clientType string, option ...StartOption) *Client {
235308
if err != nil {
236309
t.Fatalf("can't launch node (type %s): %v", clientType, err)
237310
}
238-
return &Client{Type: clientType, Container: container, IP: ip, test: t}
311+
312+
// Initialize log tracking for this client
313+
if t.clientLogOffsets == nil {
314+
t.clientLogOffsets = make(map[string]*LogOffset)
315+
}
316+
317+
return &Client{
318+
Type: clientType,
319+
Container: container,
320+
IP: ip,
321+
test: t,
322+
IsShared: false,
323+
}
324+
}
325+
326+
// GetSharedClient retrieves a shared client by ID and prepares it for use in this test.
327+
// The client can be used like a normal Client object, but maintains its state across tests.
328+
// Returns nil if the client doesn't exist.
329+
func (t *T) GetSharedClient(clientID string) *Client {
330+
if t.suite == nil || t.suite.SharedClients == nil {
331+
t.Logf("No shared clients available in this suite")
332+
return nil
333+
}
334+
335+
sharedClient, exists := t.suite.SharedClients[clientID]
336+
if !exists {
337+
t.Logf("Shared client %q not found", clientID)
338+
return nil
339+
}
340+
341+
// Store the test context in the client so it can be used for this test
342+
// Create a new Client instance that points to the same container
343+
client := &Client{
344+
Type: sharedClient.Type,
345+
Container: sharedClient.Container,
346+
IP: sharedClient.IP,
347+
test: t,
348+
IsShared: true,
349+
SuiteID: t.SuiteID,
350+
}
351+
352+
// Initialize log tracking for this client
353+
if t.clientLogOffsets == nil {
354+
t.clientLogOffsets = make(map[string]*LogOffset)
355+
}
356+
357+
// Record the current log position for this client
358+
t.clientLogOffsets[clientID] = &LogOffset{
359+
Start: sharedClient.LogPosition,
360+
End: 0, // Will be set when the test completes
361+
}
362+
363+
return client
239364
}
240365

241366
// RunClient runs the given client test against a single client type.
@@ -365,15 +490,48 @@ func runTest(host *Simulation, test testSpec, runit func(t *T)) error {
365490
Sim: host,
366491
SuiteID: test.suiteID,
367492
suite: test.suite,
493+
clientLogOffsets: make(map[string]*LogOffset), // Initialize log offset tracking
368494
}
369495
testID, err := host.StartTest(test.suiteID, test.request())
370496
if err != nil {
371497
return err
372498
}
373499
t.TestID = testID
374500
t.result.Pass = true
501+
502+
// Capture current log positions for all shared clients before running the test
503+
if test.suite != nil && test.suite.SharedClients != nil {
504+
for clientID, client := range test.suite.SharedClients {
505+
// Get the current log position for each shared client
506+
logPosition, err := host.GetClientLogOffset(test.suiteID, client.Container)
507+
if err == nil {
508+
t.clientLogOffsets[clientID] = &LogOffset{
509+
Start: logPosition,
510+
End: 0, // Will be set when test completes
511+
}
512+
}
513+
}
514+
}
515+
375516
defer func() {
376517
t.mu.Lock()
518+
519+
// After test is complete, update ending log positions for all shared clients
520+
if test.suite != nil && test.suite.SharedClients != nil {
521+
for clientID, client := range test.suite.SharedClients {
522+
if offset, exists := t.clientLogOffsets[clientID]; exists {
523+
// Get the current log position after test execution
524+
logPosition, err := host.GetClientLogOffset(test.suiteID, client.Container)
525+
if err == nil {
526+
offset.End = logPosition
527+
528+
// Update the shared client's log position for the next test
529+
client.LogPosition = logPosition
530+
}
531+
}
532+
}
533+
}
534+
377535
defer t.mu.Unlock()
378536
host.EndTest(test.suiteID, testID, t.result)
379537
}()

hivesim/testapi_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,37 @@ func TestSuiteReporting(t *testing.T) {
6969
},
7070
},
7171
}
72+
// Update expected results to match new fields
73+
wantResults = map[libhive.TestSuiteID]*libhive.TestSuite{
74+
0: {
75+
ID: 0,
76+
Name: suite.Name,
77+
Description: suite.Description,
78+
ClientVersions: make(map[string]string),
79+
TestCases: map[libhive.TestID]*libhive.TestCase{
80+
1: {
81+
Name: "passing test",
82+
Description: "this test passes",
83+
SummaryResult: libhive.TestResult{
84+
Pass: true,
85+
Details: "message from the passing test\n",
86+
ClientLogs: make(map[string]*libhive.ClientLogSegment),
87+
},
88+
},
89+
2: {
90+
Name: "failing test",
91+
Description: "this test fails",
92+
SummaryResult: libhive.TestResult{
93+
Pass: false,
94+
Details: "message from the failing test\n",
95+
ClientLogs: make(map[string]*libhive.ClientLogSegment),
96+
},
97+
},
98+
},
99+
SharedClients: nil, // Add this field to expected results
100+
},
101+
}
102+
72103
if !reflect.DeepEqual(results, wantResults) {
73104
t.Fatal("wrong results reported:", spew.Sdump(results))
74105
}

0 commit comments

Comments
 (0)