Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions pkg/monitoring/dynamic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package monitoring

import (
"testing"
"time"

"github.com/thebuidl-grid/starknode-kit/pkg/types"
"github.com/thebuidl-grid/starknode-kit/pkg/utils"
)

// TestDynamicLayout tests that the layout rebuilds correctly based on running clients
func TestDynamicLayout(t *testing.T) {
app := NewMonitorApp()

// Test 1: Initial layout should have all panels created
if app.ExecutionLogBox == nil {
t.Error("ExecutionLogBox should be created")
}
if app.ConsensusLogBox == nil {
t.Error("ConsensusLogBox should be created")
}
if app.JunoLogBox == nil {
t.Error("JunoLogBox should be created")
}
if app.ValidatorLogBox == nil {
t.Error("ValidatorLogBox should be created")
}
if app.NoClientsBox == nil {
t.Error("NoClientsBox should be created")
}
}

// TestValidatorLogChannel tests that the validator log channel is created and working
func TestValidatorLogChannel(t *testing.T) {
app := NewMonitorApp()

// Test that channel is created
if app.ValidatorLogChan == nil {
t.Fatal("ValidatorLogChan should be created")
}

// Test that we can send to the channel
testMessage := "Test validator log message"
select {
case app.ValidatorLogChan <- testMessage:
// Successfully sent
case <-time.After(time.Second):
t.Error("Failed to send to ValidatorLogChan")
}

// Test that we can receive from the channel
select {
case msg := <-app.ValidatorLogChan:
if msg != testMessage {
t.Errorf("Expected '%s', got '%s'", testMessage, msg)
}
case <-time.After(time.Second):
t.Error("Failed to receive from ValidatorLogChan")
}
}

// TestRebuildDynamicLayoutNoClients tests layout when no clients are running
func TestRebuildDynamicLayoutNoClients(t *testing.T) {
app := NewMonitorApp()

// Mock no running clients
// Note: This test assumes GetRunningClients returns empty array when no clients running
app.rebuildDynamicLayout()

// The grid should be rebuilt but we can't easily test the internal state
// We just verify it doesn't panic
t.Log("Dynamic layout rebuild completed without panics")
}

// TestValidatorClientDetection tests that validator is properly detected
func TestValidatorClientDetection(t *testing.T) {
// Get running clients
clients := utils.GetRunningClients()

// Check if Validator is in the supported client types
hasValidator := false
for _, client := range clients {
if client.Name == "Validator" || client.Name == "StarknetValidator" {
hasValidator = true
t.Logf("Validator client detected: %s (PID: %d)", client.Name, client.PID)
break
}
}

if !hasValidator {
t.Log("No validator client currently running (expected if not started)")
}
}

// TestValidatorClientType tests the ClientStarkValidator constant
func TestValidatorClientType(t *testing.T) {
if types.ClientStarkValidator != "starknet-staking-v2" {
t.Errorf("Expected ClientStarkValidator to be 'starknet-staking-v2', got '%s'", types.ClientStarkValidator)
}
}

// TestAllLogChannelsCreated tests that all log channels are properly initialized
func TestAllLogChannelsCreated(t *testing.T) {
app := NewMonitorApp()

channels := map[string]chan string{
"ExecutionLogChan": app.ExecutionLogChan,
"ConsensusLogChan": app.ConsensusLogChan,
"JunoLogChan": app.JunoLogChan,
"ValidatorLogChan": app.ValidatorLogChan,
"StatusChan": app.StatusChan,
"NetworkChan": app.NetworkChan,
"JunoStatusChan": app.JunoStatusChan,
"ChainInfoChan": app.ChainInfoChan,
"SystemStatsChan": app.SystemStatsChan,
"RPCInfoChan": app.RPCInfoChan,
}

for name, ch := range channels {
if ch == nil {
t.Errorf("Channel %s should be initialized", name)
}
}
}

// TestNoClientsMessage tests the "No Clients Running" message box
func TestNoClientsMessage(t *testing.T) {
app := NewMonitorApp()

if app.NoClientsBox == nil {
t.Fatal("NoClientsBox should be created")
}

text := app.NoClientsBox.GetText(false)
if text == "" {
t.Error("NoClientsBox should have a message")
}

// Check that it contains the expected warning message
if !contains(text, "NO CLIENTS RUNNING") {
t.Error("NoClientsBox should contain 'NO CLIENTS RUNNING' message")
}
}

// TestLogFormatting tests that log lines are properly formatted
func TestLogFormatting(t *testing.T) {
testCases := []struct {
input string
contains []string
}{
{
input: "INFO [12-07|15:32:22.145] Test message",
contains: []string{"INFO", "Test message"},
},
{
input: "WARN something happened",
contains: []string{"WARN", "something happened"},
},
{
input: "ERROR critical failure",
contains: []string{"ERROR", "critical failure"},
},
}

for _, tc := range testCases {
formatted := formatLogLines(tc.input)
for _, expected := range tc.contains {
if !contains(formatted, expected) {
t.Errorf("Formatted log should contain '%s'\nInput: %s\nOutput: %s", expected, tc.input, formatted)
}
}
}
}

// Helper function to check if a string contains a substring
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) &&
(s[:len(substr)] == substr || s[len(s)-len(substr):] == substr ||
indexInString(s, substr) >= 0))
}

func indexInString(s, substr string) int {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return i
}
}
return -1
}
156 changes: 156 additions & 0 deletions pkg/monitoring/layout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package monitoring

import (
"context"
"time"

"github.com/rivo/tview"
"github.com/thebuidl-grid/starknode-kit/pkg/utils"
)

// rebuildDynamicLayout rebuilds the entire grid layout based on running clients
func (m *MonitorApp) rebuildDynamicLayout() {
runningClients := utils.GetRunningClients()

// Determine which clients are running
hasExecution := false
hasConsensus := false
hasJuno := false
hasValidator := false

for _, client := range runningClients {
switch client.Name {
case "Geth", "Reth":
hasExecution = true
case "Lighthouse", "Prysm":
hasConsensus = true
case "Juno":
hasJuno = true
case "Validator":
hasValidator = true
}
}

// Count active log panels
activeLogPanels := 0
if hasExecution {
activeLogPanels++
}
if hasConsensus {
activeLogPanels++
}
if hasJuno {
activeLogPanels++
}
if hasValidator {
activeLogPanels++
}

// Clear the grid
m.Grid.Clear()

// If no clients are running, show "No Clients Running" message
if activeLogPanels == 0 {
m.Grid.SetRows(-1).
SetColumns(-1).
SetBorders(false)
m.Grid.AddItem(m.NoClientsBox, 0, 0, 1, 1, 0, 0, false)
return
}

// Create dynamic row configuration based on number of active clients
rows := make([]int, activeLogPanels)
for i := range rows {
rows[i] = -1 // Equal height for all rows
}

m.Grid.SetRows(rows...).
SetColumns(-3, -2). // LEFT(60%), RIGHT(40%)
SetBorders(false).
SetGap(0, 0)

// Add active log panels to the left side
currentRow := 0
if hasExecution {
m.Grid.AddItem(m.ExecutionLogBox, currentRow, 0, 1, 1, 0, 0, false)
currentRow++
}
if hasConsensus {
m.Grid.AddItem(m.ConsensusLogBox, currentRow, 0, 1, 1, 0, 0, false)
currentRow++
}
if hasJuno {
m.Grid.AddItem(m.JunoLogBox, currentRow, 0, 1, 1, 0, 0, false)
currentRow++
}
if hasValidator {
m.Grid.AddItem(m.ValidatorLogBox, currentRow, 0, 1, 1, 0, 0, false)
currentRow++
}

// RIGHT SIDE - Create sub-grid for info panels (5 rows total)
rightGrid := tview.NewGrid().
SetRows(-1, -1, -1, -1, -1). // 5 equal rows
SetColumns(-1). // Single column
SetBorders(false).
SetGap(0, 0)

// Create status grid for ETH and Starknet status side by side
statusGrid := tview.NewGrid().
SetRows(-1). // Single row
SetColumns(-1, -1). // 2 equal columns for ETH and Starknet
SetBorders(false).
SetGap(1, 0) // Small gap between status panels

// Add ETH and Starknet status to the status grid
statusGrid.AddItem(m.StatusBox, 0, 0, 1, 1, 0, 0, false) // ETH Status (left)
statusGrid.AddItem(m.StarknetStatusBox, 0, 1, 1, 1, 0, 0, false) // Starknet Status (right)

// Add all panels to the right side sub-grid
rightGrid.AddItem(m.NetworkBox, 0, 0, 1, 1, 0, 0, false) // Row 0: Network
rightGrid.AddItem(statusGrid, 1, 0, 1, 1, 0, 0, false) // Row 1: Status grid (ETH + Starknet)
rightGrid.AddItem(m.ChainInfoBox, 2, 0, 1, 1, 0, 0, false) // Row 2: Chain Info
rightGrid.AddItem(m.RPCInfoBox, 3, 0, 1, 1, 0, 0, false) // Row 3: RPC Info
rightGrid.AddItem(m.SystemStatsBox, 4, 0, 1, 1, 0, 0, false) // Row 4: System Stats

// Add the right side sub-grid to main grid (spans all rows on right)
m.Grid.AddItem(rightGrid, 0, 1, activeLogPanels, 1, 0, 0, false)
}

// updateLayoutDynamically periodically checks for running clients and updates the layout
func (m *MonitorApp) updateLayoutDynamically(ctx context.Context) {
ticker := time.NewTicker(5 * time.Second) // Check every 5 seconds
defer ticker.Stop()

previousState := ""

for {
select {
case <-ctx.Done():
return
case <-m.StopChan:
return
case <-ticker.C:
if m.paused {
continue
}

// Get current running clients
runningClients := utils.GetRunningClients()

// Create a state signature based on running clients
currentState := ""
for _, client := range runningClients {
currentState += client.Name + ","
}

// Only rebuild layout if the state has changed
if currentState != previousState {
m.App.QueueUpdateDraw(func() {
m.rebuildDynamicLayout()
})
previousState = currentState
}
}
}
}
Loading
Loading