From f854cbace5a86b66d4249828fd25b2982871bfb3 Mon Sep 17 00:00:00 2001 From: TolaMironcenko Date: Tue, 12 Aug 2025 16:55:55 +0700 Subject: [PATCH] added more tests --- cmd/create_test.go | 23 ----- cmd/root.go | 4 +- cmd/root_test.go | 67 +++++++++++++ cmd/utils.go | 10 ++ cmd/utils_test.go | 229 +++++++++++++++++++++++++++++++++++++++++++++ go.mod | 4 + main.go | 4 +- main_test.go | 27 ++++++ 8 files changed, 343 insertions(+), 25 deletions(-) create mode 100644 cmd/root_test.go create mode 100644 cmd/utils_test.go create mode 100644 main_test.go diff --git a/cmd/create_test.go b/cmd/create_test.go index ae95163..d4a0a48 100644 --- a/cmd/create_test.go +++ b/cmd/create_test.go @@ -178,26 +178,3 @@ func TestTargetNaming(t *testing.T) { } } } - -func TestFormatBytes(t *testing.T) { - tests := []struct { - size int64 - expected string - }{ - {500, "500 B"}, - {1024, "1.0 KiB"}, - {1500, "1.5 KiB"}, - {1048576, "1.0 MiB"}, - {1572864, "1.5 MiB"}, - {1073741824, "1.0 GiB"}, - {1610612736, "1.5 GiB"}, - {1099511627776, "1.0 TiB"}, - } - - for _, test := range tests { - result := formatBytes(test.size) - if result != test.expected { - t.Errorf("For %d bytes expected '%s', got '%s'", test.size, test.expected, result) - } - } -} diff --git a/cmd/root.go b/cmd/root.go index aa4817a..8bc50aa 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -6,6 +6,8 @@ import ( "github.com/spf13/cobra" ) +var osExit = os.Exit + var rootCmd = &cobra.Command{ Use: "backup", Short: "Backup your data", @@ -14,6 +16,6 @@ var rootCmd = &cobra.Command{ func Execute() { if err := rootCmd.Execute(); err != nil { - os.Exit(1) + osExit(1) } } diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000..d649b0a --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,67 @@ +package cmd + +import ( + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" +) + +// Вспомогательная структура для перехвата вызовов osExit +type exitCapture struct { + code int + called bool +} + +func (e *exitCapture) capture(code int) { + e.code = code + e.called = true +} + +func TestExecute_Success(t *testing.T) { + // Сохраняем оригинальный osExit + originalExit := osExit + defer func() { osExit = originalExit }() + + // Настраиваем перехват + capture := &exitCapture{} + osExit = capture.capture + + // Запускаем выполнение + Execute() + + // Проверяем, что выход не вызывался + if capture.called { + t.Errorf("osExit called with code %d, expected no call", capture.code) + } +} + +func TestExecute_Error(t *testing.T) { + originalExit := osExit + defer func() { osExit = originalExit }() + + capture := &exitCapture{} + osExit = capture.capture + + // Подменяем rootCmd на команду, возвращающую ошибку + originalCmd := rootCmd + defer func() { rootCmd = originalCmd }() + + rootCmd = &cobra.Command{ + Use: "error-cmd", + RunE: func(cmd *cobra.Command, args []string) error { + return assert.AnError // Имитация ошибки + }, + } + + // Вызываем Execute + Execute() + + // Проверяем, что osExit вызван с кодом 1 + if !capture.called { + t.Fatal("osExit not called") + } + if capture.code != 1 { + t.Errorf("expected exit code 1, got %d", capture.code) + } +} diff --git a/cmd/utils.go b/cmd/utils.go index cc3340d..a3d9320 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "net" + "strconv" "strings" ) @@ -47,6 +48,15 @@ func parseServerTarget(target string) (*ServerTargetInfo, bool) { return nil, false // Invalid format } + addressport := strings.SplitN(parts[0], ":", 2) + if len(addressport) != 2 { + return nil, false // Invalid format + } + + if _, err := strconv.Atoi(addressport[1]); err != nil { + return nil, false // Invalid port + } + address := parts[0] filename := parts[1] diff --git a/cmd/utils_test.go b/cmd/utils_test.go new file mode 100644 index 0000000..7a50dab --- /dev/null +++ b/cmd/utils_test.go @@ -0,0 +1,229 @@ +package cmd + +import ( + "bytes" + "io" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsNetworkAddress(t *testing.T) { + tests := []struct { + name string + input string + want bool + }{ + {"Valid IPv4 with port", "192.168.1.1:8080", true}, + {"Valid hostname with port", "example.com:443", true}, + {"IPv6 with port", "[::1]:8080", true}, + {"Missing port", "127.0.0.1", false}, + {"Port only", ":8080", true}, // технически валидный формат + {"Empty string", "", false}, + {"Non-address string", "hello/world", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := isNetworkAddress(tt.input) + require.Equal(t, tt.want, got) + }) + } +} + +func TestFormatBytes(t *testing.T) { + tests := []struct { + size int64 + expected string + }{ + {500, "500 B"}, + {1024, "1.0 KiB"}, + {1500, "1.5 KiB"}, + {1048576, "1.0 MiB"}, + {1572864, "1.5 MiB"}, + {1073741824, "1.0 GiB"}, + {1610612736, "1.5 GiB"}, + {1099511627776, "1.0 TiB"}, + } + + for _, test := range tests { + result := formatBytes(test.size) + if result != test.expected { + t.Errorf("For %d bytes expected '%s', got '%s'", test.size, test.expected, result) + } + } +} + +func TestParseServerTarget(t *testing.T) { + tests := []struct { + name string + input string + wantInfo *ServerTargetInfo + wantSuccess bool + }{ + { + name: "Valid address and filename", + input: "localhost:8080/backup.tar.gz", + wantInfo: &ServerTargetInfo{ + URL: "http://localhost:8080/backup/backup.tar.gz", + Address: "localhost:8080", + Filename: "backup.tar.gz", + }, + wantSuccess: true, + }, + { + name: "Missing filename", + input: "127.0.0.1:8000/", + wantInfo: nil, + wantSuccess: false, + }, + { + name: "Missing port", + input: "example.com/backup.gz", + wantInfo: nil, + wantSuccess: false, + }, + { + name: "Invalid host:port", + input: "invalid_host:port/backup", + wantInfo: nil, + wantSuccess: false, + }, + { + name: "Empty string", + input: "", + wantInfo: nil, + wantSuccess: false, + }, + { + name: "No slash separator", + input: "localhost:8080", + wantInfo: nil, + wantSuccess: false, + }, + { + name: "Multiple slashes", + input: "host:1234/path/to/backup.gz", + wantInfo: &ServerTargetInfo{ + URL: "http://host:1234/backup/path/to/backup.gz", + Address: "host:1234", + Filename: "path/to/backup.gz", + }, + wantSuccess: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotInfo, gotSuccess := parseServerTarget(tt.input) + require.Equal(t, tt.wantSuccess, gotSuccess) + + if tt.wantSuccess { + require.Equal(t, tt.wantInfo, gotInfo) + } else { + require.Nil(t, gotInfo) + } + }) + } +} + +func TestProgressTracker(t *testing.T) { + t.Run("Write sends delta", func(t *testing.T) { + var buf bytes.Buffer + ch := make(chan int64, 10) + data := []byte("test data") + + pt := &progressTracker{ + Writer: &buf, + progressCh: ch, + } + + n, err := pt.Write(data) + require.NoError(t, err) + require.Equal(t, len(data), n) + require.Equal(t, data, buf.Bytes()) + + require.Len(t, ch, 1) + require.Equal(t, int64(len(data)), <-ch) + }) + + t.Run("Read sends cumulative total", func(t *testing.T) { + data := []byte("sample data") + src := bytes.NewReader(data) + ch := make(chan int64, 10) + + pt := &progressTracker{ + Reader: src, + progressCh: ch, + } + + // First read + buf1 := make([]byte, 6) + n1, err1 := pt.Read(buf1) + require.NoError(t, err1) + require.Equal(t, 6, n1) + + // Second read + buf2 := make([]byte, 10) + n2, err2 := pt.Read(buf2) + require.NoError(t, err2) + require.Equal(t, 5, n2) + + require.Len(t, ch, 2) + require.Equal(t, int64(6), <-ch) + require.Equal(t, int64(11), <-ch) + }) + + t.Run("Read handles EOF correctly", func(t *testing.T) { + src := bytes.NewReader([]byte{1, 2, 3}) + ch := make(chan int64, 3) + + pt := &progressTracker{ + Reader: src, + progressCh: ch, + } + + buf := make([]byte, 5) + + // First read (3 bytes + no error) + n, err := pt.Read(buf) + require.NoError(t, err) + require.Equal(t, 3, n) + require.Equal(t, []byte{1, 2, 3}, buf[:n]) + + // Second read (0 bytes + EOF) + n, err = pt.Read(buf) + require.Equal(t, io.EOF, err) + require.Equal(t, 0, n) + + // Only first read sends progress + require.Len(t, ch, 1) + require.Equal(t, int64(3), <-ch) + }) + + t.Run("Read with immediate EOF", func(t *testing.T) { + // Custom reader that returns 0 and EOF immediately + r := &eofReader{} + ch := make(chan int64, 1) + + pt := &progressTracker{ + Reader: r, + progressCh: ch, + } + + buf := make([]byte, 5) + n, err := pt.Read(buf) + require.Equal(t, io.EOF, err) + require.Equal(t, 0, n) + + // No progress should be sent + require.Len(t, ch, 0) + }) +} + +// eofReader всегда возвращает EOF +type eofReader struct{} + +func (r *eofReader) Read(p []byte) (int, error) { + return 0, io.EOF +} diff --git a/go.mod b/go.mod index cba140d..60a8095 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/gorilla/mux v1.8.1 github.com/spf13/cobra v1.9.1 + github.com/stretchr/testify v1.10.0 golang.org/x/sys v0.30.0 ) @@ -18,6 +19,7 @@ require ( github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect @@ -27,9 +29,11 @@ require ( github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/text v0.3.8 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/main.go b/main.go index a3c94ef..a50ddda 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,8 @@ package main import "backup/cmd" +var execute = cmd.Execute + func main() { - cmd.Execute() + execute() } diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..19a40bf --- /dev/null +++ b/main_test.go @@ -0,0 +1,27 @@ +package main + +import ( + "testing" +) + +func TestMainFunction(t *testing.T) { + // Сохраняем оригинальную функцию + originalExecute := execute + defer func() { + execute = originalExecute // Восстанавливаем после теста + }() + + // Флаг для проверки вызова + called := false + execute = func() { + called = true // Подменяем функцию + } + + // Запускаем main + main() + + // Проверяем, что функция была вызвана + if !called { + t.Fatal("Expected cmd.Execute to be called, but it wasn't") + } +}