From bce264eba167eec59a9f9b6a17f09cec09a06dff Mon Sep 17 00:00:00 2001 From: peanut996 Date: Mon, 18 Nov 2024 22:57:55 +0800 Subject: [PATCH] refactor: update github workflow and add test files --- .github/workflows/build.yml | 9 +- i18n/i18n_test.go | 141 +++++++++++++++++++++++++++ task/ip_test.go | 137 ++++++++++++++++++++++++++ task/warping_test.go | 187 ++++++++++++++++++++++++++++++++++++ utils/csv_test.go | 143 +++++++++++++++++++++++++++ utils/encoding.go | 22 ++++- utils/encoding_test.go | 53 ++++++++++ utils/progress_test.go | 99 +++++++++++++++++++ 8 files changed, 785 insertions(+), 6 deletions(-) create mode 100644 i18n/i18n_test.go create mode 100644 task/ip_test.go create mode 100644 task/warping_test.go create mode 100644 utils/csv_test.go create mode 100644 utils/encoding_test.go create mode 100644 utils/progress_test.go diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15033b5..888ed8b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,7 +1,7 @@ # This workflow will build a golang project # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go -name: Go Build +name: Go Build and Test on: push: @@ -10,8 +10,7 @@ on: branches: [ "*" ] jobs: - - build: + build-and-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -20,6 +19,10 @@ jobs: uses: actions/setup-go@v5 with: go-version: '1.22' + cache: true - name: Build run: go build -v ./... + + - name: Run Tests + run: go test -race ./... diff --git a/i18n/i18n_test.go b/i18n/i18n_test.go new file mode 100644 index 0000000..3b23368 --- /dev/null +++ b/i18n/i18n_test.go @@ -0,0 +1,141 @@ +package i18n + +import ( + "os" + "testing" +) + +func TestQueryI18n(t *testing.T) { + tests := []struct { + name string + lang string + messageID string + want string + }{ + { + name: "english message", + lang: "en", + messageID: TestThreadCount, + want: "Latency test threads; the more threads, the faster the latency test, but do not set it too high on low-performance devices (such as routers); [maximum 1000]", + }, + { + name: "chinese message", + lang: "zh", + messageID: TestThreadCount, + want: "指定延迟测试线程的数量。增加此值可以加快延迟测试过程,但不适合性能较低的设备,如路由器 [默认值为 200,最大为 1000]", + }, + { + name: "default to english for unknown language", + lang: "fr", + messageID: TestThreadCount, + want: "Latency test threads; the more threads, the faster the latency test, but do not set it too high on low-performance devices (such as routers); [maximum 1000]", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save current LANG environment variable + oldLang := os.Getenv("LANG") + defer os.Setenv("LANG", oldLang) + + // Set test language + os.Setenv("LANG", tt.lang) + + // Reset localizer for new language + localizer = nil + + got := QueryI18n(tt.messageID) + if got != tt.want { + t.Errorf("QueryI18n() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestQueryTemplateI18n(t *testing.T) { + tests := []struct { + name string + lang string + messageID string + tpData map[string]interface{} + want string + }{ + { + name: "english template", + lang: "en", + messageID: OutputResultFile, + tpData: map[string]interface{}{ + "file": "test.csv", + }, + want: "Write result to file; add quotes if the path contains spaces; empty value means not writing to a file [-o \"\"]; ", + }, + { + name: "chinese template", + lang: "zh", + messageID: OutputResultFile, + tpData: map[string]interface{}{ + "file": "test.csv", + }, + want: "设置输出结果文件 [默认文件为 \"result.csv\"]", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save current LANG environment variable + oldLang := os.Getenv("LANG") + defer os.Setenv("LANG", oldLang) + + // Set test language + os.Setenv("LANG", tt.lang) + + // Reset localizer for new language + localizer = nil + + got := QueryTemplateI18n(tt.messageID, tt.tpData) + if got != tt.want { + t.Errorf("QueryTemplateI18n() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestInit(t *testing.T) { + tests := []struct { + name string + lang string + }{ + { + name: "initialize with english", + lang: "en", + }, + { + name: "initialize with chinese", + lang: "zh", + }, + { + name: "initialize with unknown language", + lang: "fr", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Save current LANG environment variable + oldLang := os.Getenv("LANG") + defer os.Setenv("LANG", oldLang) + + // Set test language + os.Setenv("LANG", tt.lang) + + // Reset and reinitialize + localizer = nil + initLocalizer() + + // Verify localizer is initialized + if localizer == nil { + t.Error("initLocalizer() failed to initialize localizer") + } + }) + } +} diff --git a/task/ip_test.go b/task/ip_test.go new file mode 100644 index 0000000..b5a9487 --- /dev/null +++ b/task/ip_test.go @@ -0,0 +1,137 @@ +package task + +import ( + "net" + "testing" +) + +func TestIsIPv4(t *testing.T) { + tests := []struct { + name string + ip string + want bool + }{ + { + name: "valid ipv4", + ip: "192.168.1.1", + want: true, + }, + { + name: "valid ipv6", + ip: "2001:db8::1", + want: false, + }, + { + name: "empty string", + ip: "", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isIPv4(tt.ip); got != tt.want { + t.Errorf("isIPv4() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestIPRanges_FixIP(t *testing.T) { + tests := []struct { + name string + ip string + wantIP string + wantv4 bool + }{ + { + name: "ipv4 without mask", + ip: "192.168.1.1", + wantIP: "192.168.1.1/32", + wantv4: true, + }, + { + name: "ipv4 with mask", + ip: "192.168.1.0/24", + wantIP: "192.168.1.0/24", + wantv4: true, + }, + { + name: "ipv6 without mask", + ip: "2001:db8::1", + wantIP: "2001:db8::1/128", + wantv4: false, + }, + { + name: "ipv6 with mask", + ip: "2001:db8::/64", + wantIP: "2001:db8::/64", + wantv4: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := newIPRanges() + got := r.fixIP(tt.ip) + if got != tt.wantIP { + t.Errorf("fixIP() = %v, want %v", got, tt.wantIP) + } + }) + } +} + +func TestIPRanges_GetIPRange(t *testing.T) { + tests := []struct { + name string + setupIP string + wantMin byte + wantHost byte + }{ + { + name: "ipv4 /32", + setupIP: "192.168.1.1/32", + wantMin: 1, + wantHost: 0, + }, + { + name: "ipv4 /24", + setupIP: "192.168.1.0/24", + wantMin: 0, + wantHost: 255, + }, + { + name: "ipv4 /16", + setupIP: "192.168.0.0/16", + wantMin: 0, + wantHost: 255, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := newIPRanges() + r.parseCIDR(r.fixIP(tt.setupIP)) + gotMin, gotHosts := r.getIPRange() + if gotMin != tt.wantMin || gotHosts != tt.wantHost { + t.Errorf("getIPRange() = (%v, %v), want (%v, %v)", + gotMin, gotHosts, tt.wantMin, tt.wantHost) + } + }) + } +} + +func TestIPRanges_AppendIP(t *testing.T) { + r := newIPRanges() + testIP := net.ParseIP("192.168.1.1") + + r.appendIP(testIP) + + if len(r.ips) != 1 { + t.Errorf("appendIP() did not append IP, got len = %v, want 1", len(r.ips)) + } + + if !r.ips[0].IP.Equal(testIP) { + t.Errorf("appendIP() appended wrong IP, got %v, want %v", r.ips[0].IP, testIP) + } +} diff --git a/task/warping_test.go b/task/warping_test.go new file mode 100644 index 0000000..a100ff3 --- /dev/null +++ b/task/warping_test.go @@ -0,0 +1,187 @@ +package task + +import ( + "net" + "testing" + "time" + + "github.com/peanut996/CloudflareWarpSpeedTest/utils" +) + +func TestUDPAddr_FullAddress(t *testing.T) { + tests := []struct { + name string + ip string + port int + want string + }{ + { + name: "ipv4 address", + ip: "192.168.1.1", + port: 8080, + want: "192.168.1.1:8080", + }, + { + name: "ipv6 address", + ip: "2001:db8::1", + port: 8080, + want: "[2001:db8::1]:8080", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ipAddr := &net.IPAddr{IP: net.ParseIP(tt.ip)} + addr := &UDPAddr{ + IP: ipAddr, + Port: tt.port, + } + if got := addr.FullAddress(); got != tt.want { + t.Errorf("UDPAddr.FullAddress() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestUDPAddr_ToUDPAddr(t *testing.T) { + tests := []struct { + name string + ip string + port int + wantErr bool + }{ + { + name: "valid ipv4", + ip: "192.168.1.1", + port: 8080, + wantErr: false, + }, + { + name: "valid ipv6", + ip: "2001:db8::1", + port: 8080, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ipAddr := &net.IPAddr{IP: net.ParseIP(tt.ip)} + addr := &UDPAddr{ + IP: ipAddr, + Port: tt.port, + } + got := addr.ToUDPAddr() + if (got == nil) != tt.wantErr { + t.Errorf("UDPAddr.ToUDPAddr() error = %v, wantErr %v", got == nil, tt.wantErr) + return + } + if !tt.wantErr && got.Port != tt.port { + t.Errorf("UDPAddr.ToUDPAddr() port = %v, want %v", got.Port, tt.port) + } + }) + } +} + +func TestWarping_AppendIPData(t *testing.T) { + w := NewWarping() + testIP := &net.IPAddr{IP: net.ParseIP("192.168.1.1")} + pingData := &utils.PingData{ + IP: &net.UDPAddr{IP: testIP.IP, Port: 8080}, + Sent: 10, + Received: 8, + Delay: 100 * time.Millisecond, + } + + w.appendIPData(pingData) + + if len(w.csv) != 1 { + t.Errorf("Warping.appendIPData() did not append data, got len = %v, want 1", len(w.csv)) + return + } + + got := w.csv[0] + if got.Delay != pingData.Delay { + t.Errorf("Warping.appendIPData() delay = %v, want %v", got.Delay, pingData.Delay) + } + if got.Sent != pingData.Sent { + t.Errorf("Warping.appendIPData() sent = %v, want %v", got.Sent, pingData.Sent) + } + if got.Received != pingData.Received { + t.Errorf("Warping.appendIPData() received = %v, want %v", got.Received, pingData.Received) + } +} + +func TestEncodeBase64ToHex(t *testing.T) { + tests := []struct { + name string + key string + want string + wantErr bool + }{ + { + name: "valid base64", + key: warpPublicKey, // Use the actual WARP public key which is known to be valid + want: "6e65ce0be17517110c17d77288ad87e7fd5252dcc7d09b95a39d61db03df832a", + wantErr: false, + }, + { + name: "invalid base64", + key: "invalid@@", + want: "", + wantErr: true, + }, + { + name: "empty string", + key: "", + want: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := encodeBase64ToHex(tt.key) + if (err != nil) != tt.wantErr { + t.Errorf("encodeBase64ToHex() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("encodeBase64ToHex() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetNoisePublicKeyFromBase64(t *testing.T) { + tests := []struct { + name string + key string + wantErr bool + }{ + { + name: "valid warp public key", + key: warpPublicKey, + wantErr: false, + }, + { + name: "invalid key", + key: "invalid@@", + wantErr: true, + }, + { + name: "empty string", + key: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := getNoisePublicKeyFromBase64(tt.key) + if (err != nil) != tt.wantErr { + t.Errorf("getNoisePublicKeyFromBase64() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/utils/csv_test.go b/utils/csv_test.go new file mode 100644 index 0000000..23f19a1 --- /dev/null +++ b/utils/csv_test.go @@ -0,0 +1,143 @@ +package utils + +import ( + "net" + "testing" + "time" +) + +func TestCloudflareIPData_getLossRate(t *testing.T) { + tests := []struct { + name string + pingData *PingData + want float32 + }{ + { + name: "no loss", + pingData: &PingData{ + Sent: 10, + Received: 10, + }, + want: 0, + }, + { + name: "50% loss", + pingData: &PingData{ + Sent: 10, + Received: 5, + }, + want: 0.5, + }, + { + name: "100% loss", + pingData: &PingData{ + Sent: 10, + Received: 0, + }, + want: 1.0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cf := &CloudflareIPData{ + PingData: tt.pingData, + } + if got := cf.getLossRate(); got != tt.want { + t.Errorf("CloudflareIPData.getLossRate() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestPingDelaySet_FilterDelay(t *testing.T) { + // Save original values + origMaxDelay := InputMaxDelay + origMinDelay := InputMinDelay + defer func() { + InputMaxDelay = origMaxDelay + InputMinDelay = origMinDelay + }() + + // Set test values + InputMaxDelay = 100 * time.Millisecond + InputMinDelay = 10 * time.Millisecond + + testIP, _ := net.ResolveUDPAddr("udp", "1.1.1.1:0") + + tests := []struct { + name string + set PingDelaySet + want int // number of items that should remain after filtering + }{ + { + name: "all within range", + set: PingDelaySet{ + {PingData: &PingData{IP: testIP, Delay: 50 * time.Millisecond}}, + {PingData: &PingData{IP: testIP, Delay: 75 * time.Millisecond}}, + }, + want: 2, + }, + { + name: "some outside range", + set: PingDelaySet{ + {PingData: &PingData{IP: testIP, Delay: 5 * time.Millisecond}}, + {PingData: &PingData{IP: testIP, Delay: 50 * time.Millisecond}}, + {PingData: &PingData{IP: testIP, Delay: 150 * time.Millisecond}}, + }, + want: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.set.FilterDelay(); len(got) != tt.want { + t.Errorf("PingDelaySet.FilterDelay() returned %v items, want %v", len(got), tt.want) + } + }) + } +} + +func TestPingDelaySet_FilterLossRate(t *testing.T) { + // Save original value + origMaxLossRate := InputMaxLossRate + defer func() { + InputMaxLossRate = origMaxLossRate + }() + + // Set test value + InputMaxLossRate = 0.5 + + testIP, _ := net.ResolveUDPAddr("udp", "1.1.1.1:0") + + tests := []struct { + name string + set PingDelaySet + want int // number of items that should remain after filtering + }{ + { + name: "all within range", + set: PingDelaySet{ + {PingData: &PingData{IP: testIP, Sent: 10, Received: 8}}, // 20% loss + {PingData: &PingData{IP: testIP, Sent: 10, Received: 7}}, // 30% loss + }, + want: 2, + }, + { + name: "some outside range", + set: PingDelaySet{ + {PingData: &PingData{IP: testIP, Sent: 10, Received: 8}}, // 20% loss + {PingData: &PingData{IP: testIP, Sent: 10, Received: 4}}, // 60% loss + }, + want: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.set.FilterLossRate(); len(got) != tt.want { + t.Errorf("PingDelaySet.FilterLossRate() returned %v items, want %v", len(got), tt.want) + } + }) + } +} diff --git a/utils/encoding.go b/utils/encoding.go index 263e4b8..9e56d62 100644 --- a/utils/encoding.go +++ b/utils/encoding.go @@ -1,12 +1,28 @@ package utils -import "encoding/json" +import ( + "encoding/json" + "fmt" +) func ParseReservedString(reservedString string) (reserved [3]byte, err error) { if reservedString == "" { return } - reserved = [3]byte{} - err = json.Unmarshal([]byte(reservedString), &reserved) + + // First unmarshal into a slice to validate length + var tempSlice []byte + if err = json.Unmarshal([]byte(reservedString), &tempSlice); err != nil { + return + } + + // Validate length + if len(tempSlice) != 3 { + err = fmt.Errorf("reserved array must have exactly 3 elements, got %d", len(tempSlice)) + return + } + + // Copy to fixed-size array + reserved = [3]byte{tempSlice[0], tempSlice[1], tempSlice[2]} return } diff --git a/utils/encoding_test.go b/utils/encoding_test.go new file mode 100644 index 0000000..6a9ef45 --- /dev/null +++ b/utils/encoding_test.go @@ -0,0 +1,53 @@ +package utils + +import ( + "reflect" + "testing" +) + +func TestParseReservedString(t *testing.T) { + tests := []struct { + name string + reservedString string + wantReserved [3]byte + wantErr bool + }{ + { + name: "empty string", + reservedString: "", + wantReserved: [3]byte{}, + wantErr: false, + }, + { + name: "valid array", + reservedString: "[1, 2, 3]", + wantReserved: [3]byte{1, 2, 3}, + wantErr: false, + }, + { + name: "invalid json", + reservedString: "invalid", + wantReserved: [3]byte{}, + wantErr: true, + }, + { + name: "wrong length", + reservedString: "[1, 2, 3, 4]", + wantReserved: [3]byte{}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotReserved, err := ParseReservedString(tt.reservedString) + if (err != nil) != tt.wantErr { + t.Errorf("ParseReservedString() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && !reflect.DeepEqual(gotReserved, tt.wantReserved) { + t.Errorf("ParseReservedString() = %v, want %v", gotReserved, tt.wantReserved) + } + }) + } +} diff --git a/utils/progress_test.go b/utils/progress_test.go new file mode 100644 index 0000000..05c88f6 --- /dev/null +++ b/utils/progress_test.go @@ -0,0 +1,99 @@ +package utils + +import ( + "testing" + "time" +) + +func TestNewBar(t *testing.T) { + tests := []struct { + name string + count int + strStart string + strEnd string + wantCount int + }{ + { + name: "basic progress bar", + count: 100, + strStart: "Processing", + strEnd: "items", + wantCount: 100, + }, + { + name: "zero count", + count: 0, + strStart: "Starting", + strEnd: "tasks", + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bar := NewBar(tt.count, tt.strStart, tt.strEnd) + if bar == nil { + t.Error("NewBar() returned nil") + } + if bar.pb == nil { + t.Error("NewBar() returned bar with nil pb") + } + }) + } +} + +func TestBar_Grow(t *testing.T) { + tests := []struct { + name string + initialSize int + growBy int + strVal string + }{ + { + name: "grow by one", + initialSize: 10, + growBy: 1, + strVal: "processing item 1", + }, + { + name: "grow by multiple", + initialSize: 100, + growBy: 5, + strVal: "batch processing", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bar := NewBar(tt.initialSize, "Start", "End") + bar.Grow(tt.growBy, tt.strVal) + // Allow a short time for the progress bar to update + time.Sleep(10 * time.Millisecond) + }) + } +} + +func TestBar_Done(t *testing.T) { + tests := []struct { + name string + initialSize int + }{ + { + name: "complete small bar", + initialSize: 10, + }, + { + name: "complete large bar", + initialSize: 1000, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bar := NewBar(tt.initialSize, "Start", "End") + bar.Done() + // Allow a short time for the progress bar to finish + time.Sleep(10 * time.Millisecond) + }) + } +}