Added all of the existing code
This commit is contained in:
272
tools/build.go
Normal file
272
tools/build.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
binaryName = "techircd"
|
||||
version = "1.0.0"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
buildFlag = flag.Bool("build", false, "Build the binary")
|
||||
runFlag = flag.Bool("run", false, "Build and run the server")
|
||||
testFlag = flag.Bool("test", false, "Run all tests")
|
||||
cleanFlag = flag.Bool("clean", false, "Clean build artifacts")
|
||||
fmtFlag = flag.Bool("fmt", false, "Format Go code")
|
||||
lintFlag = flag.Bool("lint", false, "Run linters")
|
||||
buildAllFlag = flag.Bool("build-all", false, "Build for multiple platforms")
|
||||
releaseFlag = flag.Bool("release", false, "Create optimized release build")
|
||||
helpFlag = flag.Bool("help", false, "Show help message")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
if *helpFlag || flag.NFlag() == 0 {
|
||||
showHelp()
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case *buildFlag:
|
||||
build()
|
||||
case *runFlag:
|
||||
build()
|
||||
run()
|
||||
case *testFlag:
|
||||
test()
|
||||
case *cleanFlag:
|
||||
clean()
|
||||
case *fmtFlag:
|
||||
format()
|
||||
case *lintFlag:
|
||||
lint()
|
||||
case *buildAllFlag:
|
||||
buildAll()
|
||||
case *releaseFlag:
|
||||
release()
|
||||
}
|
||||
}
|
||||
|
||||
func showHelp() {
|
||||
fmt.Println("TechIRCd Build Tool")
|
||||
fmt.Println("")
|
||||
fmt.Println("Usage:")
|
||||
fmt.Println(" go run build.go [options]")
|
||||
fmt.Println("")
|
||||
fmt.Println("Options:")
|
||||
fmt.Println(" -build Build the binary")
|
||||
fmt.Println(" -run Build and run the server")
|
||||
fmt.Println(" -test Run all tests")
|
||||
fmt.Println(" -clean Clean build artifacts")
|
||||
fmt.Println(" -fmt Format Go code")
|
||||
fmt.Println(" -lint Run linters")
|
||||
fmt.Println(" -build-all Build for multiple platforms")
|
||||
fmt.Println(" -release Create optimized release build")
|
||||
fmt.Println(" -help Show this help message")
|
||||
}
|
||||
|
||||
func build() {
|
||||
fmt.Println("Building TechIRCd...")
|
||||
|
||||
gitVersion, err := exec.Command("git", "describe", "--tags", "--always", "--dirty").Output()
|
||||
var versionStr string
|
||||
if err != nil {
|
||||
versionStr = version
|
||||
} else {
|
||||
versionStr = strings.TrimSpace(string(gitVersion))
|
||||
}
|
||||
|
||||
ldflags := fmt.Sprintf("-ldflags=-X main.version=%s", versionStr)
|
||||
|
||||
cmd := exec.Command("go", "build", ldflags, "-o", binaryName, ".")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Build failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("Build completed successfully!")
|
||||
}
|
||||
|
||||
func run() {
|
||||
fmt.Println("Starting TechIRCd...")
|
||||
|
||||
cmd := exec.Command("./" + binaryName)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Run failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func test() {
|
||||
fmt.Println("Running tests...")
|
||||
|
||||
cmd := exec.Command("go", "test", "-v", "-race", "./...")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Tests failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("All tests passed!")
|
||||
}
|
||||
|
||||
func clean() {
|
||||
fmt.Println("Cleaning build artifacts...")
|
||||
|
||||
// Remove binary files
|
||||
patterns := []string{
|
||||
binaryName + "*",
|
||||
"coverage.out",
|
||||
"coverage.html",
|
||||
}
|
||||
|
||||
for _, pattern := range patterns {
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, match := range matches {
|
||||
if err := os.Remove(match); err != nil {
|
||||
fmt.Printf("Failed to remove %s: %v\n", match, err)
|
||||
} else {
|
||||
fmt.Printf("Removed %s\n", match)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Clean completed!")
|
||||
}
|
||||
|
||||
func format() {
|
||||
fmt.Println("Formatting Go code...")
|
||||
|
||||
cmd := exec.Command("go", "fmt", "./...")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Format failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Try to run goimports if available
|
||||
if _, err := exec.LookPath("goimports"); err == nil {
|
||||
fmt.Println("Running goimports...")
|
||||
cmd := exec.Command("goimports", "-w", "-local", "github.com/ComputerTech312/TechIRCd", ".")
|
||||
cmd.Run() // Don't fail if this doesn't work
|
||||
}
|
||||
|
||||
fmt.Println("Format completed!")
|
||||
}
|
||||
|
||||
func lint() {
|
||||
fmt.Println("Running linters...")
|
||||
|
||||
if _, err := exec.LookPath("golangci-lint"); err != nil {
|
||||
fmt.Println("golangci-lint not found, skipping...")
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.Command("golangci-lint", "run")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Linting found issues: %v\n", err)
|
||||
// Don't exit on lint errors, just report them
|
||||
} else {
|
||||
fmt.Println("No linting issues found!")
|
||||
}
|
||||
}
|
||||
|
||||
func buildAll() {
|
||||
fmt.Println("Building for multiple platforms...")
|
||||
|
||||
platforms := []struct {
|
||||
goos string
|
||||
goarch string
|
||||
ext string
|
||||
}{
|
||||
{"linux", "amd64", ""},
|
||||
{"windows", "amd64", ".exe"},
|
||||
{"darwin", "amd64", ""},
|
||||
{"darwin", "arm64", ""},
|
||||
}
|
||||
|
||||
gitVersion, err := exec.Command("git", "describe", "--tags", "--always", "--dirty").Output()
|
||||
var versionStr string
|
||||
if err != nil {
|
||||
versionStr = version
|
||||
} else {
|
||||
versionStr = strings.TrimSpace(string(gitVersion))
|
||||
}
|
||||
|
||||
for _, platform := range platforms {
|
||||
outputName := fmt.Sprintf("%s-%s-%s%s", binaryName, platform.goos, platform.goarch, platform.ext)
|
||||
fmt.Printf("Building %s...\n", outputName)
|
||||
|
||||
ldflags := fmt.Sprintf("-ldflags=-X main.version=%s", versionStr)
|
||||
|
||||
cmd := exec.Command("go", "build", ldflags, "-o", outputName, ".")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GOOS="+platform.goos,
|
||||
"GOARCH="+platform.goarch,
|
||||
)
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Failed to build %s: %v\n", outputName, err)
|
||||
} else {
|
||||
fmt.Printf("Built %s successfully!\n", outputName)
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Println("Cross-platform build completed!")
|
||||
}
|
||||
|
||||
func release() {
|
||||
fmt.Println("Creating optimized release build...")
|
||||
|
||||
gitVersion, err := exec.Command("git", "describe", "--tags", "--always", "--dirty").Output()
|
||||
var versionStr string
|
||||
if err != nil {
|
||||
versionStr = version
|
||||
} else {
|
||||
versionStr = strings.TrimSpace(string(gitVersion))
|
||||
}
|
||||
|
||||
ldflags := fmt.Sprintf("-ldflags=-X main.version=%s", versionStr)
|
||||
|
||||
cmd := exec.Command("go", "build", ldflags, "-a", "-installsuffix", "cgo", "-o", binaryName, ".")
|
||||
cmd.Env = append(os.Environ(), "CGO_ENABLED=0")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Release build failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Get file info to show size
|
||||
if info, err := os.Stat(binaryName); err == nil {
|
||||
fmt.Printf("Release build completed! Binary size: %.2f MB\n", float64(info.Size())/1024/1024)
|
||||
} else {
|
||||
fmt.Println("Release build completed!")
|
||||
}
|
||||
}
|
||||
525
tools/go_stress_test.go
Normal file
525
tools/go_stress_test.go
Normal file
@@ -0,0 +1,525 @@
|
||||
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
19
tools/run_stress_test.sh
Normal file
19
tools/run_stress_test.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Build and run the Go IRC Stress Tester
|
||||
|
||||
echo "Building Go IRC Stress Tester..."
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Build the stress tester
|
||||
go build -o irc_stress_test go_stress_test.go
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "Build successful!"
|
||||
echo "Running stress test..."
|
||||
echo "========================"
|
||||
./irc_stress_test
|
||||
else
|
||||
echo "Build failed!"
|
||||
exit 1
|
||||
fi
|
||||
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)
|
||||
}
|
||||
}
|
||||
464
tools/stress_test.py
Normal file
464
tools/stress_test.py
Normal file
@@ -0,0 +1,464 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TechIRCd Advanced Stress Tester
|
||||
===============================
|
||||
Comprehensive IRC server stress testing tool with configurable behavior.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import time
|
||||
import logging
|
||||
from typing import List, Dict, Optional
|
||||
import json
|
||||
|
||||
# =============================================================================
|
||||
# CONFIGURATION - Edit these values to customize your stress test
|
||||
# =============================================================================
|
||||
|
||||
CONFIG = {
|
||||
# Server connection settings
|
||||
"SERVER": {
|
||||
"host": "localhost",
|
||||
"port": 6667,
|
||||
},
|
||||
|
||||
# Test parameters
|
||||
"TEST": {
|
||||
"max_clients": 300, # Number of concurrent clients
|
||||
"connect_delay": 0.05, # Seconds between connections
|
||||
"test_duration": 300, # Test duration in seconds
|
||||
"action_interval": 1.0, # Seconds between random actions
|
||||
"stats_interval": 10, # Seconds between stats reports
|
||||
},
|
||||
|
||||
# Client behavior probabilities (0.0 = never, 1.0 = always)
|
||||
"BEHAVIOR": {
|
||||
"join_channels": True,
|
||||
"send_messages": True,
|
||||
"use_new_commands": True,
|
||||
"change_nicks": True,
|
||||
"random_quit": False,
|
||||
|
||||
# Action rates (probability per action cycle)
|
||||
"message_rate": 0.4, # Chance to send a message
|
||||
"channel_join_rate": 0.2, # Chance to join a channel
|
||||
"command_rate": 0.15, # Chance to send a command
|
||||
"nick_change_rate": 0.05, # Chance to change nick
|
||||
"quit_rate": 0.01, # Chance to quit and reconnect
|
||||
},
|
||||
|
||||
# IRC channels to use
|
||||
"CHANNELS": [
|
||||
"#test", "#stress", "#general", "#random", "#chaos",
|
||||
"#lobby", "#gaming", "#tech", "#chat", "#help",
|
||||
"#dev", "#admin", "#support", "#lounge", "#public"
|
||||
],
|
||||
|
||||
# Random messages to send
|
||||
"MESSAGES": [
|
||||
"Hello everyone!",
|
||||
"This is a stress test message",
|
||||
"How is everyone doing today?",
|
||||
"Testing server stability under load",
|
||||
"Random message from stress test client",
|
||||
"IRC is still the best chat protocol!",
|
||||
"TechIRCd is handling this load well",
|
||||
"Can you see this message?",
|
||||
"Stress testing in progress...",
|
||||
"Everything working fine here",
|
||||
"Anyone else here for the stress test?",
|
||||
"Server performance looking good!",
|
||||
"Testing new IRC commands",
|
||||
"This channel is quite active",
|
||||
"Load testing is important for stability"
|
||||
],
|
||||
|
||||
# IRC commands to test (including new ones!)
|
||||
"COMMANDS": [
|
||||
"MOTD",
|
||||
"RULES",
|
||||
"MAP",
|
||||
"TIME",
|
||||
"VERSION",
|
||||
"LUSERS",
|
||||
"WHO #test",
|
||||
"WHOIS testuser",
|
||||
"LIST",
|
||||
"ADMIN",
|
||||
"INFO",
|
||||
"KNOCK #test",
|
||||
"SETNAME :New real name from stress test"
|
||||
],
|
||||
|
||||
# Logging configuration
|
||||
"LOGGING": {
|
||||
"level": "INFO", # DEBUG, INFO, WARNING, ERROR
|
||||
"show_irc_traffic": False, # Set to True to see all IRC messages
|
||||
}
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# STRESS TESTING CODE - Don't modify unless you know what you're doing
|
||||
# =============================================================================
|
||||
|
||||
class StressTestStats:
|
||||
"""Tracks statistics during stress testing"""
|
||||
|
||||
def __init__(self):
|
||||
self.start_time = time.time()
|
||||
self.connected_clients = 0
|
||||
self.messages_sent = 0
|
||||
self.commands_sent = 0
|
||||
self.channels_joined = 0
|
||||
self.nick_changes = 0
|
||||
self.errors = 0
|
||||
self.reconnections = 0
|
||||
|
||||
def runtime(self) -> float:
|
||||
return time.time() - self.start_time
|
||||
|
||||
def print_stats(self):
|
||||
runtime = self.runtime()
|
||||
print("\n" + "="*50)
|
||||
print("STRESS TEST STATISTICS")
|
||||
print("="*50)
|
||||
print(f"Runtime: {runtime:.1f}s")
|
||||
print(f"Connected Clients: {self.connected_clients}")
|
||||
print(f"Messages Sent: {self.messages_sent}")
|
||||
print(f"Commands Sent: {self.commands_sent}")
|
||||
print(f"Channels Joined: {self.channels_joined}")
|
||||
print(f"Nick Changes: {self.nick_changes}")
|
||||
print(f"Reconnections: {self.reconnections}")
|
||||
print(f"Errors: {self.errors}")
|
||||
|
||||
if runtime > 0:
|
||||
print(f"Messages/sec: {self.messages_sent/runtime:.2f}")
|
||||
print(f"Commands/sec: {self.commands_sent/runtime:.2f}")
|
||||
print(f"Actions/sec: {(self.messages_sent + self.commands_sent)/runtime:.2f}")
|
||||
|
||||
print("="*50)
|
||||
|
||||
class IRCStressClient:
|
||||
"""Individual IRC client for stress testing"""
|
||||
|
||||
def __init__(self, client_id: int, stats: StressTestStats):
|
||||
self.client_id = client_id
|
||||
self.nick = f"StressUser{client_id}"
|
||||
self.user = f"stress{client_id}"
|
||||
self.realname = f"Stress Test Client {client_id}"
|
||||
self.channels = []
|
||||
self.stats = stats
|
||||
self.reader = None
|
||||
self.writer = None
|
||||
self.connected = False
|
||||
self.registered = False
|
||||
self.running = True
|
||||
|
||||
async def connect(self, host: str, port: int) -> bool:
|
||||
"""Connect to IRC server"""
|
||||
try:
|
||||
self.reader, self.writer = await asyncio.open_connection(host, port)
|
||||
self.connected = True
|
||||
logging.debug(f"Client {self.client_id} connected to {host}:{port}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"Client {self.client_id} connection failed: {e}")
|
||||
self.stats.errors += 1
|
||||
return False
|
||||
|
||||
async def register(self) -> bool:
|
||||
"""Register with IRC server"""
|
||||
try:
|
||||
await self.send_command(f"NICK {self.nick}")
|
||||
await self.send_command(f"USER {self.user} 0 * :{self.realname}")
|
||||
self.registered = True
|
||||
self.stats.connected_clients += 1
|
||||
logging.debug(f"Client {self.client_id} registered as {self.nick}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.error(f"Client {self.client_id} registration failed: {e}")
|
||||
self.stats.errors += 1
|
||||
return False
|
||||
|
||||
async def send_command(self, command: str):
|
||||
"""Send IRC command to server"""
|
||||
if not self.connected or not self.writer:
|
||||
return
|
||||
|
||||
try:
|
||||
message = f"{command}\r\n"
|
||||
self.writer.write(message.encode())
|
||||
await self.writer.drain()
|
||||
|
||||
if CONFIG["LOGGING"]["show_irc_traffic"]:
|
||||
logging.debug(f"Client {self.client_id} >>> {command}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Client {self.client_id} send error: {e}")
|
||||
self.stats.errors += 1
|
||||
|
||||
async def read_messages(self):
|
||||
"""Read and handle messages from server"""
|
||||
while self.running and self.connected:
|
||||
try:
|
||||
if not self.reader:
|
||||
break
|
||||
|
||||
line = await self.reader.readline()
|
||||
if not line:
|
||||
break
|
||||
|
||||
message = line.decode().strip()
|
||||
if not message:
|
||||
continue
|
||||
|
||||
if CONFIG["LOGGING"]["show_irc_traffic"]:
|
||||
logging.debug(f"Client {self.client_id} <<< {message}")
|
||||
|
||||
# Handle PING
|
||||
if message.startswith("PING"):
|
||||
pong = message.replace("PING", "PONG", 1)
|
||||
await self.send_command(pong)
|
||||
|
||||
# Handle other server messages if needed
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Client {self.client_id} read error: {e}")
|
||||
self.stats.errors += 1
|
||||
break
|
||||
|
||||
async def join_random_channel(self):
|
||||
"""Join a random channel"""
|
||||
if not CONFIG["BEHAVIOR"]["join_channels"]:
|
||||
return
|
||||
|
||||
channel = random.choice(CONFIG["CHANNELS"])
|
||||
if channel not in self.channels:
|
||||
await self.send_command(f"JOIN {channel}")
|
||||
self.channels.append(channel)
|
||||
self.stats.channels_joined += 1
|
||||
logging.debug(f"Client {self.client_id} joined {channel}")
|
||||
|
||||
async def send_random_message(self):
|
||||
"""Send a random message to a random channel"""
|
||||
if not CONFIG["BEHAVIOR"]["send_messages"] or not self.channels:
|
||||
return
|
||||
|
||||
channel = random.choice(self.channels)
|
||||
message = random.choice(CONFIG["MESSAGES"])
|
||||
await self.send_command(f"PRIVMSG {channel} :{message}")
|
||||
self.stats.messages_sent += 1
|
||||
logging.debug(f"Client {self.client_id} sent message to {channel}")
|
||||
|
||||
async def send_random_command(self):
|
||||
"""Send a random IRC command"""
|
||||
if not CONFIG["BEHAVIOR"]["use_new_commands"]:
|
||||
return
|
||||
|
||||
command = random.choice(CONFIG["COMMANDS"])
|
||||
await self.send_command(command)
|
||||
self.stats.commands_sent += 1
|
||||
logging.debug(f"Client {self.client_id} sent command: {command}")
|
||||
|
||||
async def change_nick(self):
|
||||
"""Change nickname"""
|
||||
if not CONFIG["BEHAVIOR"]["change_nicks"]:
|
||||
return
|
||||
|
||||
new_nick = f"User{self.client_id}_{random.randint(1, 9999)}"
|
||||
await self.send_command(f"NICK {new_nick}")
|
||||
self.nick = new_nick
|
||||
self.stats.nick_changes += 1
|
||||
logging.debug(f"Client {self.client_id} changed nick to {new_nick}")
|
||||
|
||||
async def random_quit_reconnect(self):
|
||||
"""Randomly quit and reconnect"""
|
||||
if not CONFIG["BEHAVIOR"]["random_quit"]:
|
||||
return
|
||||
|
||||
await self.send_command("QUIT :Reconnecting...")
|
||||
await self.disconnect()
|
||||
|
||||
# Wait a moment then reconnect
|
||||
await asyncio.sleep(random.uniform(1, 3))
|
||||
|
||||
if await self.connect(CONFIG["SERVER"]["host"], CONFIG["SERVER"]["port"]):
|
||||
await self.register()
|
||||
self.stats.reconnections += 1
|
||||
logging.debug(f"Client {self.client_id} reconnected")
|
||||
|
||||
async def perform_random_action(self):
|
||||
"""Perform a random IRC action"""
|
||||
if not self.registered:
|
||||
return
|
||||
|
||||
action = random.random()
|
||||
|
||||
if action < CONFIG["BEHAVIOR"]["message_rate"]:
|
||||
await self.send_random_message()
|
||||
elif action < CONFIG["BEHAVIOR"]["message_rate"] + CONFIG["BEHAVIOR"]["channel_join_rate"]:
|
||||
await self.join_random_channel()
|
||||
elif action < CONFIG["BEHAVIOR"]["message_rate"] + CONFIG["BEHAVIOR"]["channel_join_rate"] + CONFIG["BEHAVIOR"]["command_rate"]:
|
||||
await self.send_random_command()
|
||||
elif action < CONFIG["BEHAVIOR"]["message_rate"] + CONFIG["BEHAVIOR"]["channel_join_rate"] + CONFIG["BEHAVIOR"]["command_rate"] + CONFIG["BEHAVIOR"]["nick_change_rate"]:
|
||||
await self.change_nick()
|
||||
elif action < CONFIG["BEHAVIOR"]["message_rate"] + CONFIG["BEHAVIOR"]["channel_join_rate"] + CONFIG["BEHAVIOR"]["command_rate"] + CONFIG["BEHAVIOR"]["nick_change_rate"] + CONFIG["BEHAVIOR"]["quit_rate"]:
|
||||
await self.random_quit_reconnect()
|
||||
|
||||
async def disconnect(self):
|
||||
"""Disconnect from server"""
|
||||
self.running = False
|
||||
self.connected = False
|
||||
|
||||
if self.writer:
|
||||
try:
|
||||
await self.send_command("QUIT :Stress test completed")
|
||||
self.writer.close()
|
||||
await self.writer.wait_closed()
|
||||
except:
|
||||
pass
|
||||
|
||||
if self.registered:
|
||||
self.stats.connected_clients -= 1
|
||||
self.registered = False
|
||||
|
||||
class IRCStressTester:
|
||||
"""Main stress testing coordinator"""
|
||||
|
||||
def __init__(self):
|
||||
self.clients: List[IRCStressClient] = []
|
||||
self.stats = StressTestStats()
|
||||
self.running = False
|
||||
|
||||
# Setup logging
|
||||
log_level = getattr(logging, CONFIG["LOGGING"]["level"])
|
||||
logging.basicConfig(
|
||||
level=log_level,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
datefmt='%H:%M:%S'
|
||||
)
|
||||
|
||||
async def create_client(self, client_id: int) -> Optional[IRCStressClient]:
|
||||
"""Create and connect a new client"""
|
||||
client = IRCStressClient(client_id, self.stats)
|
||||
|
||||
if await client.connect(CONFIG["SERVER"]["host"], CONFIG["SERVER"]["port"]):
|
||||
if await client.register():
|
||||
# Start message reader
|
||||
asyncio.create_task(client.read_messages())
|
||||
return client
|
||||
|
||||
return None
|
||||
|
||||
async def connect_all_clients(self):
|
||||
"""Connect all clients with delay"""
|
||||
print(f"Connecting {CONFIG['TEST']['max_clients']} clients...")
|
||||
|
||||
for i in range(CONFIG["TEST"]["max_clients"]):
|
||||
client = await self.create_client(i)
|
||||
if client:
|
||||
self.clients.append(client)
|
||||
|
||||
# Join initial channel
|
||||
if CONFIG["BEHAVIOR"]["join_channels"]:
|
||||
await client.join_random_channel()
|
||||
|
||||
# Progress reporting
|
||||
if (i + 1) % 10 == 0:
|
||||
print(f"Connected {i + 1}/{CONFIG['TEST']['max_clients']} clients")
|
||||
|
||||
# Delay between connections
|
||||
if CONFIG["TEST"]["connect_delay"] > 0:
|
||||
await asyncio.sleep(CONFIG["TEST"]["connect_delay"])
|
||||
|
||||
print(f"All {len(self.clients)} clients connected!")
|
||||
|
||||
async def run_activity_simulation(self):
|
||||
"""Run the main activity simulation"""
|
||||
print("Starting activity simulation...")
|
||||
|
||||
start_time = time.time()
|
||||
last_stats = time.time()
|
||||
|
||||
while self.running and (time.time() - start_time) < CONFIG["TEST"]["test_duration"]:
|
||||
# Perform random actions
|
||||
active_clients = [c for c in self.clients if c.registered]
|
||||
if active_clients:
|
||||
# Select random clients to perform actions
|
||||
num_actions = random.randint(1, len(active_clients) // 4 + 1)
|
||||
selected_clients = random.sample(active_clients, min(num_actions, len(active_clients)))
|
||||
|
||||
# Perform actions concurrently
|
||||
tasks = [client.perform_random_action() for client in selected_clients]
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# Print stats periodically
|
||||
if time.time() - last_stats >= CONFIG["TEST"]["stats_interval"]:
|
||||
self.stats.print_stats()
|
||||
last_stats = time.time()
|
||||
|
||||
# Wait before next action cycle
|
||||
await asyncio.sleep(CONFIG["TEST"]["action_interval"])
|
||||
|
||||
async def shutdown_all_clients(self):
|
||||
"""Gracefully disconnect all clients"""
|
||||
print("Shutting down all clients...")
|
||||
|
||||
disconnect_tasks = []
|
||||
for client in self.clients:
|
||||
disconnect_tasks.append(client.disconnect())
|
||||
|
||||
# Wait for all disconnections
|
||||
await asyncio.gather(*disconnect_tasks, return_exceptions=True)
|
||||
|
||||
print("All clients disconnected.")
|
||||
|
||||
async def run_stress_test(self):
|
||||
"""Run the complete stress test"""
|
||||
print("="*60)
|
||||
print("TECHIRCD ADVANCED STRESS TESTER")
|
||||
print("="*60)
|
||||
print(f"Target: {CONFIG['SERVER']['host']}:{CONFIG['SERVER']['port']}")
|
||||
print(f"Clients: {CONFIG['TEST']['max_clients']}")
|
||||
print(f"Duration: {CONFIG['TEST']['test_duration']}s")
|
||||
print(f"Channels: {len(CONFIG['CHANNELS'])}")
|
||||
print(f"Commands: {len(CONFIG['COMMANDS'])}")
|
||||
print("="*60)
|
||||
|
||||
self.running = True
|
||||
|
||||
try:
|
||||
# Connect all clients
|
||||
await self.connect_all_clients()
|
||||
|
||||
# Run activity simulation
|
||||
await self.run_activity_simulation()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\nTest interrupted by user!")
|
||||
except Exception as e:
|
||||
print(f"\nTest failed with error: {e}")
|
||||
finally:
|
||||
self.running = False
|
||||
await self.shutdown_all_clients()
|
||||
|
||||
# Final stats
|
||||
print("\nFINAL RESULTS:")
|
||||
self.stats.print_stats()
|
||||
|
||||
async def main():
|
||||
"""Main entry point"""
|
||||
stress_tester = IRCStressTester()
|
||||
await stress_tester.run_stress_test()
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Starting TechIRCd Advanced Stress Tester...")
|
||||
print("Press Ctrl+C to stop the test early.\n")
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\nTest terminated by user.")
|
||||
except Exception as e:
|
||||
print(f"\nFatal error: {e}")
|
||||
533
tools/stress_tester.go
Normal file
533
tools/stress_tester.go
Normal file
@@ -0,0 +1,533 @@
|
||||
package main
|
||||
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
|
||||
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: 300,
|
||||
ConnectDelay: 100,
|
||||
ActionInterval: 2000,
|
||||
TestDuration: 180,
|
||||
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.4,
|
||||
ChannelJoinRate: 0.3,
|
||||
CommandRate: 0.2,
|
||||
},
|
||||
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",
|
||||
"Testing new IRC commands",
|
||||
"Server performance looks good",
|
||||
},
|
||||
Commands: []string{
|
||||
"MOTD", "RULES", "MAP", "TIME", "VERSION", "LUSERS",
|
||||
"WHO #test", "WHOIS testuser", "LIST", "WHOWAS olduser",
|
||||
},
|
||||
}
|
||||
|
||||
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 TechIRCd stress test with %d clients", st.Config.Test.MaxClients)
|
||||
log.Printf("📡 Target server: %s:%d", st.Config.Server.Host, st.Config.Server.Port)
|
||||
log.Printf("⏱️ Test duration: %d seconds", st.Config.Test.TestDuration)
|
||||
|
||||
// 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%25 == 0 {
|
||||
log.Printf("🔗 Connected %d/%d clients", i+1, st.Config.Test.MaxClients)
|
||||
}
|
||||
|
||||
time.Sleep(connectDelay)
|
||||
}
|
||||
|
||||
log.Printf("✅ All clients connected! Starting chaos 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(15 * 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)/3) + 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 && i > 0 {
|
||||
log.Printf("🔌 Disconnected %d/%d clients", i+1, len(st.Clients))
|
||||
}
|
||||
}
|
||||
|
||||
st.PrintStats()
|
||||
log.Println("🎉 Stress test completed successfully!")
|
||||
}
|
||||
|
||||
func main() {
|
||||
configFile := "stress_test_config.json"
|
||||
|
||||
log.Println("🎯 TechIRCd Go Stress Tester v1.0")
|
||||
log.Println("==================================")
|
||||
|
||||
// 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", configFile)
|
||||
log.Println("📖 Please review and modify the config as needed, then run again.")
|
||||
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