Added all of the existing code

This commit is contained in:
2025-09-27 14:43:52 +01:00
commit 6772bfd842
58 changed files with 19587 additions and 0 deletions

272
tools/build.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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)
}
}