Added all of the existing code
This commit is contained in:
528
tools/stress_test.go
Normal file
528
tools/stress_test.go
Normal file
@@ -0,0 +1,528 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StressConfig defines the configuration for stress testing
|
||||
type StressConfig struct {
|
||||
Server struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
} `json:"server"`
|
||||
|
||||
Test struct {
|
||||
MaxClients int `json:"max_clients"`
|
||||
ConnectDelay int `json:"connect_delay_ms"`
|
||||
ActionInterval int `json:"action_interval_ms"`
|
||||
TestDuration int `json:"test_duration_seconds"`
|
||||
RandomSeed int `json:"random_seed"`
|
||||
} `json:"test"`
|
||||
|
||||
Behavior struct {
|
||||
JoinChannels bool `json:"join_channels"`
|
||||
SendMessages bool `json:"send_messages"`
|
||||
ChangeNicks bool `json:"change_nicks"`
|
||||
UseNewCommands bool `json:"use_new_commands"`
|
||||
RandomQuit bool `json:"random_quit"`
|
||||
|
||||
MessageRate float64 `json:"message_rate"`
|
||||
ChannelJoinRate float64 `json:"channel_join_rate"`
|
||||
CommandRate float64 `json:"command_rate"`
|
||||
} `json:"behavior"`
|
||||
|
||||
Channels []string `json:"channels"`
|
||||
|
||||
Messages []string `json:"messages"`
|
||||
|
||||
Commands []string `json:"commands"`
|
||||
}
|
||||
|
||||
// IRCClient represents a single IRC client connection
|
||||
type IRCClient struct {
|
||||
ID int
|
||||
Nick string
|
||||
Conn net.Conn
|
||||
Reader *bufio.Reader
|
||||
Writer *bufio.Writer
|
||||
Channels []string
|
||||
Active bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// StressTest manages the entire stress testing operation
|
||||
type StressTest struct {
|
||||
Config *StressConfig
|
||||
Clients []*IRCClient
|
||||
Stats *TestStats
|
||||
wg sync.WaitGroup
|
||||
quit chan bool
|
||||
}
|
||||
|
||||
// TestStats tracks testing statistics
|
||||
type TestStats struct {
|
||||
ConnectedClients int
|
||||
MessagesSent int
|
||||
CommandsSent int
|
||||
ChannelsJoined int
|
||||
Errors int
|
||||
StartTime time.Time
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// LoadConfig loads configuration from JSON file
|
||||
func LoadConfig(filename string) (*StressConfig, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error opening config file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
config := &StressConfig{}
|
||||
decoder := json.NewDecoder(file)
|
||||
if err := decoder.Decode(config); err != nil {
|
||||
return nil, fmt.Errorf("error parsing config: %v", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// CreateDefaultConfig creates a default configuration file
|
||||
func CreateDefaultConfig(filename string) error {
|
||||
config := &StressConfig{
|
||||
Server: struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
}{
|
||||
Host: "localhost",
|
||||
Port: 6667,
|
||||
},
|
||||
Test: struct {
|
||||
MaxClients int `json:"max_clients"`
|
||||
ConnectDelay int `json:"connect_delay_ms"`
|
||||
ActionInterval int `json:"action_interval_ms"`
|
||||
TestDuration int `json:"test_duration_seconds"`
|
||||
RandomSeed int `json:"random_seed"`
|
||||
}{
|
||||
MaxClients: 500,
|
||||
ConnectDelay: 50,
|
||||
ActionInterval: 1000,
|
||||
TestDuration: 300,
|
||||
RandomSeed: 42,
|
||||
},
|
||||
Behavior: struct {
|
||||
JoinChannels bool `json:"join_channels"`
|
||||
SendMessages bool `json:"send_messages"`
|
||||
ChangeNicks bool `json:"change_nicks"`
|
||||
UseNewCommands bool `json:"use_new_commands"`
|
||||
RandomQuit bool `json:"random_quit"`
|
||||
MessageRate float64 `json:"message_rate"`
|
||||
ChannelJoinRate float64 `json:"channel_join_rate"`
|
||||
CommandRate float64 `json:"command_rate"`
|
||||
}{
|
||||
JoinChannels: true,
|
||||
SendMessages: true,
|
||||
ChangeNicks: true,
|
||||
UseNewCommands: true,
|
||||
RandomQuit: false,
|
||||
MessageRate: 0.3,
|
||||
ChannelJoinRate: 0.2,
|
||||
CommandRate: 0.1,
|
||||
},
|
||||
Channels: []string{
|
||||
"#test", "#stress", "#general", "#random", "#chaos",
|
||||
"#lobby", "#gaming", "#tech", "#chat", "#help",
|
||||
},
|
||||
Messages: []string{
|
||||
"Hello everyone!",
|
||||
"This is a stress test message",
|
||||
"How is everyone doing?",
|
||||
"Testing the server stability",
|
||||
"Random message from client",
|
||||
"IRC is awesome!",
|
||||
"TechIRCd rocks!",
|
||||
"Can you see this message?",
|
||||
"Stress testing in progress",
|
||||
"Everything working fine here",
|
||||
},
|
||||
Commands: []string{
|
||||
"MOTD", "RULES", "MAP", "TIME", "VERSION", "LUSERS",
|
||||
"WHO #test", "WHOIS testuser", "LIST",
|
||||
},
|
||||
}
|
||||
|
||||
file, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating config file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
encoder := json.NewEncoder(file)
|
||||
encoder.SetIndent("", " ")
|
||||
return encoder.Encode(config)
|
||||
}
|
||||
|
||||
// NewStressTest creates a new stress test instance
|
||||
func NewStressTest(config *StressConfig) *StressTest {
|
||||
return &StressTest{
|
||||
Config: config,
|
||||
Clients: make([]*IRCClient, 0, config.Test.MaxClients),
|
||||
Stats: &TestStats{
|
||||
StartTime: time.Now(),
|
||||
},
|
||||
quit: make(chan bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes connection to IRC server
|
||||
func (c *IRCClient) Connect(host string, port int) error {
|
||||
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", host, port))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect: %v", err)
|
||||
}
|
||||
|
||||
c.Conn = conn
|
||||
c.Reader = bufio.NewReader(conn)
|
||||
c.Writer = bufio.NewWriter(conn)
|
||||
c.Active = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register performs IRC client registration
|
||||
func (c *IRCClient) Register() error {
|
||||
commands := []string{
|
||||
fmt.Sprintf("NICK %s", c.Nick),
|
||||
fmt.Sprintf("USER %s 0 * :Stress Test Client %d", c.Nick, c.ID),
|
||||
}
|
||||
|
||||
for _, cmd := range commands {
|
||||
if err := c.SendCommand(cmd); err != nil {
|
||||
return fmt.Errorf("registration failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendCommand sends a command to the IRC server
|
||||
func (c *IRCClient) SendCommand(command string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if !c.Active || c.Writer == nil {
|
||||
return fmt.Errorf("client not active")
|
||||
}
|
||||
|
||||
_, err := c.Writer.WriteString(command + "\r\n")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Writer.Flush()
|
||||
}
|
||||
|
||||
// ReadMessages continuously reads messages from server
|
||||
func (c *IRCClient) ReadMessages(stats *TestStats) {
|
||||
defer func() {
|
||||
c.Active = false
|
||||
if c.Conn != nil {
|
||||
c.Conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
for c.Active {
|
||||
if c.Reader == nil {
|
||||
break
|
||||
}
|
||||
|
||||
line, err := c.Reader.ReadString('\n')
|
||||
if err != nil {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
break
|
||||
}
|
||||
|
||||
// Handle PING responses
|
||||
if len(line) > 4 && line[:4] == "PING" {
|
||||
pong := "PONG" + line[4:]
|
||||
c.SendCommand(pong[:len(pong)-2]) // Remove \r\n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PerformRandomAction performs a random IRC action
|
||||
func (c *IRCClient) PerformRandomAction(config *StressConfig, stats *TestStats) {
|
||||
if !c.Active {
|
||||
return
|
||||
}
|
||||
|
||||
action := rand.Float64()
|
||||
|
||||
switch {
|
||||
case action < config.Behavior.MessageRate && config.Behavior.SendMessages:
|
||||
c.SendRandomMessage(config, stats)
|
||||
case action < config.Behavior.MessageRate+config.Behavior.ChannelJoinRate && config.Behavior.JoinChannels:
|
||||
c.JoinRandomChannel(config, stats)
|
||||
case action < config.Behavior.MessageRate+config.Behavior.ChannelJoinRate+config.Behavior.CommandRate && config.Behavior.UseNewCommands:
|
||||
c.SendRandomCommand(config, stats)
|
||||
case config.Behavior.ChangeNicks && rand.Float64() < 0.05:
|
||||
c.ChangeNick(stats)
|
||||
}
|
||||
}
|
||||
|
||||
// SendRandomMessage sends a random message to a random channel
|
||||
func (c *IRCClient) SendRandomMessage(config *StressConfig, stats *TestStats) {
|
||||
if len(c.Channels) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
channel := c.Channels[rand.Intn(len(c.Channels))]
|
||||
message := config.Messages[rand.Intn(len(config.Messages))]
|
||||
|
||||
cmd := fmt.Sprintf("PRIVMSG %s :%s", channel, message)
|
||||
if err := c.SendCommand(cmd); err == nil {
|
||||
stats.mu.Lock()
|
||||
stats.MessagesSent++
|
||||
stats.mu.Unlock()
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// JoinRandomChannel joins a random channel
|
||||
func (c *IRCClient) JoinRandomChannel(config *StressConfig, stats *TestStats) {
|
||||
channel := config.Channels[rand.Intn(len(config.Channels))]
|
||||
|
||||
// Check if already in channel
|
||||
for _, ch := range c.Channels {
|
||||
if ch == channel {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf("JOIN %s", channel)
|
||||
if err := c.SendCommand(cmd); err == nil {
|
||||
c.Channels = append(c.Channels, channel)
|
||||
stats.mu.Lock()
|
||||
stats.ChannelsJoined++
|
||||
stats.mu.Unlock()
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// SendRandomCommand sends a random IRC command
|
||||
func (c *IRCClient) SendRandomCommand(config *StressConfig, stats *TestStats) {
|
||||
command := config.Commands[rand.Intn(len(config.Commands))]
|
||||
|
||||
if err := c.SendCommand(command); err == nil {
|
||||
stats.mu.Lock()
|
||||
stats.CommandsSent++
|
||||
stats.mu.Unlock()
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// ChangeNick changes the client's nickname
|
||||
func (c *IRCClient) ChangeNick(stats *TestStats) {
|
||||
newNick := fmt.Sprintf("User%d_%d", c.ID, rand.Intn(1000))
|
||||
cmd := fmt.Sprintf("NICK %s", newNick)
|
||||
|
||||
if err := c.SendCommand(cmd); err == nil {
|
||||
c.Nick = newNick
|
||||
} else {
|
||||
stats.mu.Lock()
|
||||
stats.Errors++
|
||||
stats.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// CreateClient creates and connects a new IRC client
|
||||
func (st *StressTest) CreateClient(id int) error {
|
||||
client := &IRCClient{
|
||||
ID: id,
|
||||
Nick: fmt.Sprintf("StressUser%d", id),
|
||||
Channels: make([]string, 0),
|
||||
}
|
||||
|
||||
// Connect to server
|
||||
if err := client.Connect(st.Config.Server.Host, st.Config.Server.Port); err != nil {
|
||||
return fmt.Errorf("client %d connection failed: %v", id, err)
|
||||
}
|
||||
|
||||
// Register with IRC server
|
||||
if err := client.Register(); err != nil {
|
||||
client.Conn.Close()
|
||||
return fmt.Errorf("client %d registration failed: %v", id, err)
|
||||
}
|
||||
|
||||
st.Clients = append(st.Clients, client)
|
||||
|
||||
st.Stats.mu.Lock()
|
||||
st.Stats.ConnectedClients++
|
||||
st.Stats.mu.Unlock()
|
||||
|
||||
// Start message reader goroutine
|
||||
go client.ReadMessages(st.Stats)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RunStressTest executes the complete stress test
|
||||
func (st *StressTest) RunStressTest() error {
|
||||
log.Printf("Starting stress test with %d clients", st.Config.Test.MaxClients)
|
||||
|
||||
// Set random seed
|
||||
rand.Seed(int64(st.Config.Test.RandomSeed))
|
||||
|
||||
// Connect clients gradually
|
||||
connectDelay := time.Duration(st.Config.Test.ConnectDelay) * time.Millisecond
|
||||
for i := 0; i < st.Config.Test.MaxClients; i++ {
|
||||
if err := st.CreateClient(i); err != nil {
|
||||
log.Printf("Failed to create client %d: %v", i, err)
|
||||
st.Stats.mu.Lock()
|
||||
st.Stats.Errors++
|
||||
st.Stats.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
if i%10 == 0 {
|
||||
log.Printf("Connected %d/%d clients", i+1, st.Config.Test.MaxClients)
|
||||
}
|
||||
|
||||
time.Sleep(connectDelay)
|
||||
}
|
||||
|
||||
log.Printf("All clients connected. Starting activity simulation...")
|
||||
|
||||
// Start activity simulation
|
||||
actionInterval := time.Duration(st.Config.Test.ActionInterval) * time.Millisecond
|
||||
testDuration := time.Duration(st.Config.Test.TestDuration) * time.Second
|
||||
|
||||
actionTicker := time.NewTicker(actionInterval)
|
||||
defer actionTicker.Stop()
|
||||
|
||||
statsTicker := time.NewTicker(10 * time.Second)
|
||||
defer statsTicker.Stop()
|
||||
|
||||
testTimer := time.NewTimer(testDuration)
|
||||
defer testTimer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-actionTicker.C:
|
||||
// Perform random actions for random clients
|
||||
numActions := rand.Intn(len(st.Clients)/4) + 1
|
||||
for i := 0; i < numActions; i++ {
|
||||
if len(st.Clients) > 0 {
|
||||
clientIndex := rand.Intn(len(st.Clients))
|
||||
go st.Clients[clientIndex].PerformRandomAction(st.Config, st.Stats)
|
||||
}
|
||||
}
|
||||
|
||||
case <-statsTicker.C:
|
||||
st.PrintStats()
|
||||
|
||||
case <-testTimer.C:
|
||||
log.Println("Test duration completed. Shutting down...")
|
||||
st.Shutdown()
|
||||
return nil
|
||||
|
||||
case <-st.quit:
|
||||
log.Println("Test interrupted. Shutting down...")
|
||||
st.Shutdown()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintStats prints current test statistics
|
||||
func (st *StressTest) PrintStats() {
|
||||
st.Stats.mu.Lock()
|
||||
defer st.Stats.mu.Unlock()
|
||||
|
||||
elapsed := time.Since(st.Stats.StartTime)
|
||||
|
||||
log.Printf("=== STRESS TEST STATS ===")
|
||||
log.Printf("Runtime: %v", elapsed.Round(time.Second))
|
||||
log.Printf("Connected Clients: %d", st.Stats.ConnectedClients)
|
||||
log.Printf("Messages Sent: %d", st.Stats.MessagesSent)
|
||||
log.Printf("Commands Sent: %d", st.Stats.CommandsSent)
|
||||
log.Printf("Channels Joined: %d", st.Stats.ChannelsJoined)
|
||||
log.Printf("Errors: %d", st.Stats.Errors)
|
||||
|
||||
if elapsed.Seconds() > 0 {
|
||||
log.Printf("Messages/sec: %.2f", float64(st.Stats.MessagesSent)/elapsed.Seconds())
|
||||
log.Printf("Commands/sec: %.2f", float64(st.Stats.CommandsSent)/elapsed.Seconds())
|
||||
}
|
||||
log.Printf("========================")
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down all clients
|
||||
func (st *StressTest) Shutdown() {
|
||||
log.Println("Shutting down all clients...")
|
||||
|
||||
for i, client := range st.Clients {
|
||||
if client.Active {
|
||||
client.SendCommand("QUIT :Stress test completed")
|
||||
client.Active = false
|
||||
if client.Conn != nil {
|
||||
client.Conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if i%50 == 0 {
|
||||
log.Printf("Disconnected %d/%d clients", i+1, len(st.Clients))
|
||||
}
|
||||
}
|
||||
|
||||
st.PrintStats()
|
||||
log.Println("Stress test completed!")
|
||||
}
|
||||
|
||||
func main() {
|
||||
configFile := "stress_test_config.json"
|
||||
|
||||
// Check if config file exists, create default if not
|
||||
if _, err := os.Stat(configFile); os.IsNotExist(err) {
|
||||
log.Printf("Config file %s not found. Creating default...", configFile)
|
||||
if err := CreateDefaultConfig(configFile); err != nil {
|
||||
log.Fatalf("Failed to create default config: %v", err)
|
||||
}
|
||||
log.Printf("Default config created at %s. Please review and modify as needed.", configFile)
|
||||
log.Println("Run the command again to start the stress test.")
|
||||
return
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
config, err := LoadConfig(configFile)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("Loaded config: %d clients, %d second test", config.Test.MaxClients, config.Test.TestDuration)
|
||||
|
||||
// Create and run stress test
|
||||
stressTest := NewStressTest(config)
|
||||
|
||||
if err := stressTest.RunStressTest(); err != nil {
|
||||
log.Fatalf("Stress test failed: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user