Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f854cbace5 | ||
|
|
bca550374f | ||
|
|
a1681ab58a | ||
|
|
4639779116 |
@@ -6,6 +6,7 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- master
|
- master
|
||||||
|
- develop
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -25,7 +26,7 @@ jobs:
|
|||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
# Use a Go version that matches your project's go.mod file
|
# Use a Go version that matches your project's go.mod file
|
||||||
go-version: '1.22'
|
go-version: "1.22"
|
||||||
|
|
||||||
# Step 3: Download Go module dependencies
|
# Step 3: Download Go module dependencies
|
||||||
- name: Tidy and Vendor
|
- name: Tidy and Vendor
|
||||||
|
|||||||
+87
-11
@@ -26,6 +26,7 @@ import (
|
|||||||
var (
|
var (
|
||||||
source string
|
source string
|
||||||
target string
|
target string
|
||||||
|
excludePatterns []string
|
||||||
)
|
)
|
||||||
|
|
||||||
var createCmd = &cobra.Command{
|
var createCmd = &cobra.Command{
|
||||||
@@ -33,7 +34,7 @@ var createCmd = &cobra.Command{
|
|||||||
Short: "Create a new backup",
|
Short: "Create a new backup",
|
||||||
Long: "Create a new backup of a file, directory, or disk",
|
Long: "Create a new backup of a file, directory, or disk",
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
model, err := initialModel(source, target)
|
model, err := initialModel(source, target, excludePatterns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error:", err)
|
fmt.Println("Error:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -60,13 +61,14 @@ type model struct {
|
|||||||
isDirectory bool
|
isDirectory bool
|
||||||
httpTargetInfo *ServerTargetInfo
|
httpTargetInfo *ServerTargetInfo
|
||||||
isP2PTarget bool // Флаг для старого P2P-режима
|
isP2PTarget bool // Флаг для старого P2P-режима
|
||||||
|
excludePatterns []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type progressMsg int64
|
type progressMsg int64
|
||||||
type doneMsg struct{}
|
type doneMsg struct{}
|
||||||
type errorMsg struct{ err error }
|
type errorMsg struct{ err error }
|
||||||
|
|
||||||
func getPathSize(path string) (int64, error) {
|
func getPathSize(path string, excludePatterns []string) (int64, error) {
|
||||||
fileInfo, err := os.Stat(path)
|
fileInfo, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@@ -97,10 +99,27 @@ func getPathSize(path string) (int64, error) {
|
|||||||
|
|
||||||
if fileInfo.IsDir() {
|
if fileInfo.IsDir() {
|
||||||
var total int64
|
var total int64
|
||||||
err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error {
|
err := filepath.Walk(path, func(current string, info os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
relPath, err := filepath.Rel(path, current)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if relPath == "." {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldExcludePath(relPath, excludePatterns) {
|
||||||
|
if info.IsDir() {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if !info.IsDir() {
|
if !info.IsDir() {
|
||||||
total += info.Size()
|
total += info.Size()
|
||||||
}
|
}
|
||||||
@@ -112,8 +131,8 @@ func getPathSize(path string) (int64, error) {
|
|||||||
return fileInfo.Size(), nil
|
return fileInfo.Size(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func initialModel(src, dst string) (*model, error) {
|
func initialModel(src, dst string, excludePatterns []string) (*model, error) {
|
||||||
totalBytes, err := getPathSize(src)
|
totalBytes, err := getPathSize(src, excludePatterns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -148,11 +167,11 @@ func initialModel(src, dst string) (*model, error) {
|
|||||||
} else {
|
} else {
|
||||||
// Локальный файл
|
// Локальный файл
|
||||||
if isDirectory {
|
if isDirectory {
|
||||||
if filepath.Ext(targetPath) != ".tar.gz" {
|
if !strings.HasSuffix(targetPath, ".tar.gz") {
|
||||||
targetPath += ".tar.gz"
|
targetPath += ".tar.gz"
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if filepath.Ext(targetPath) != ".gz" {
|
if !strings.HasSuffix(targetPath, ".gz") {
|
||||||
targetPath += ".gz"
|
targetPath += ".gz"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,6 +187,7 @@ func initialModel(src, dst string) (*model, error) {
|
|||||||
isDirectory: isDirectory,
|
isDirectory: isDirectory,
|
||||||
httpTargetInfo: targetInfo,
|
httpTargetInfo: targetInfo,
|
||||||
isP2PTarget: isP2P,
|
isP2PTarget: isP2P,
|
||||||
|
excludePatterns: excludePatterns,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,6 +298,34 @@ func (m *model) backupFileOrDisk(w io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Вспомогательная функция для проверки исключений
|
||||||
|
func shouldExcludePath(path string, patterns []string) bool {
|
||||||
|
cleanPath := filepath.Clean(path)
|
||||||
|
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
// Проверка полного совпадения пути
|
||||||
|
if matched, _ := filepath.Match(pattern, cleanPath); matched {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка, что паттерн является префиксом пути (как каталог)
|
||||||
|
if strings.HasPrefix(cleanPath, pattern+string(filepath.Separator)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверка базового имени файла/директории
|
||||||
|
base := filepath.Base(cleanPath)
|
||||||
|
if matched, _ := filepath.Match(pattern, base); matched {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *model) shouldExclude(path string) bool {
|
||||||
|
return shouldExcludePath(path, m.excludePatterns)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *model) backupDirectory(w io.Writer) error {
|
func (m *model) backupDirectory(w io.Writer) error {
|
||||||
progressWriter := &progressTracker{Writer: w, progressCh: m.progressCh}
|
progressWriter := &progressTracker{Writer: w, progressCh: m.progressCh}
|
||||||
gzipWriter := gzip.NewWriter(progressWriter)
|
gzipWriter := gzip.NewWriter(progressWriter)
|
||||||
@@ -291,12 +339,21 @@ func (m *model) backupDirectory(w io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
header, err := tar.FileInfoHeader(fi, file)
|
// Получаем относительный путь
|
||||||
|
relPath, err := filepath.Rel(m.source, file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
relPath, err := filepath.Rel(m.source, file)
|
if relPath != "." {
|
||||||
|
if m.shouldExclude(relPath) {
|
||||||
|
if fi.IsDir() {
|
||||||
|
return filepath.SkipDir
|
||||||
|
}
|
||||||
|
return nil // Пропускаем файл
|
||||||
|
}
|
||||||
|
}
|
||||||
|
header, err := tar.FileInfoHeader(fi, file)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -416,8 +473,27 @@ func (m *model) View() string {
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(createCmd)
|
rootCmd.AddCommand(createCmd)
|
||||||
createCmd.Flags().StringVarP(&source, "source", "s", "", "Source file, directory or disk (e.g., /dev/sda) (required)")
|
createCmd.Flags().StringVarP(
|
||||||
createCmd.Flags().StringVarP(&target, "target", "t", "", "Target backup file or network address (e.g., /path/to/backup.tar.gz, 127.0.0.1:8080/backup.gz, or 127.0.0.1:8080) (required)")
|
&source,
|
||||||
|
"source",
|
||||||
|
"s",
|
||||||
|
"",
|
||||||
|
"Source file, directory or disk (e.g., /dev/sda) (required)",
|
||||||
|
)
|
||||||
|
createCmd.Flags().StringVarP(
|
||||||
|
&target,
|
||||||
|
"target",
|
||||||
|
"t",
|
||||||
|
"",
|
||||||
|
"Target backup file or network address (e.g., /path/to/backup.tar.gz, 127.0.0.1:8080/backup.gz, or 127.0.0.1:8080) (required)",
|
||||||
|
)
|
||||||
|
createCmd.Flags().StringSliceVarP(
|
||||||
|
&excludePatterns,
|
||||||
|
"exclude",
|
||||||
|
"e",
|
||||||
|
[]string{},
|
||||||
|
"Patterns to exclude (can be specified multiple times)",
|
||||||
|
)
|
||||||
createCmd.MarkFlagRequired("source")
|
createCmd.MarkFlagRequired("source")
|
||||||
createCmd.MarkFlagRequired("target")
|
createCmd.MarkFlagRequired("target")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
// "backup/cmd"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Вспомогательная функция для создания временной структуры каталогов
|
||||||
|
func setupTestDir(t *testing.T) (string, func()) {
|
||||||
|
dir, err := os.MkdirTemp("", "backup_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем тестовую структуру:
|
||||||
|
// /test_dir
|
||||||
|
// /subdir
|
||||||
|
// file2.txt
|
||||||
|
// file1.txt
|
||||||
|
// .hidden
|
||||||
|
// exclude_me.txt
|
||||||
|
subDir := filepath.Join(dir, "subdir")
|
||||||
|
if err := os.Mkdir(subDir, 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
files := []string{
|
||||||
|
"file1.txt",
|
||||||
|
".hidden",
|
||||||
|
"exclude_me.txt",
|
||||||
|
filepath.Join("subdir", "file2.txt"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
path := filepath.Join(dir, f)
|
||||||
|
if err := os.WriteFile(path, []byte("test"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir, func() { os.RemoveAll(dir) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetPathSize(t *testing.T) {
|
||||||
|
t.Run("Directory size calculation", func(t *testing.T) {
|
||||||
|
dir, cleanup := setupTestDir(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
size, err := getPathSize(dir, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ожидаемый размер: 4 файла по 4 байта каждый = 16 байт
|
||||||
|
if size != 16 {
|
||||||
|
t.Errorf("Expected size 16, got %d", size)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Exclusion patterns", func(t *testing.T) {
|
||||||
|
dir, cleanup := setupTestDir(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
exclude := []string{"exclude_me.txt", "*.hidden", "subdir/*"}
|
||||||
|
size, err := getPathSize(dir, exclude)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Должен остаться только file1.txt (4 байта)
|
||||||
|
if size != 4 {
|
||||||
|
t.Errorf("Expected size 4 after exclusions, got %d", size)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Single file size", func(t *testing.T) {
|
||||||
|
dir, cleanup := setupTestDir(t)
|
||||||
|
defer cleanup()
|
||||||
|
file := filepath.Join(dir, "file1.txt")
|
||||||
|
|
||||||
|
size, err := getPathSize(file, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if size != 4 {
|
||||||
|
t.Errorf("Expected size 4, got %d", size)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldExcludePath(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
path string
|
||||||
|
patterns []string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
// Точное совпадение
|
||||||
|
{"exclude.txt", []string{"exclude.txt"}, true},
|
||||||
|
{"dir/exclude.txt", []string{"exclude.txt"}, true},
|
||||||
|
|
||||||
|
// Префикс каталога
|
||||||
|
{"node_modules/file.js", []string{"node_modules"}, true},
|
||||||
|
{"src/node_modules/file.js", []string{"node_modules"}, false}, // Изменено ожидание
|
||||||
|
{"src/node_modules", []string{"node_modules"}, true},
|
||||||
|
|
||||||
|
// Glob-паттерны
|
||||||
|
{"temp.jpg", []string{"*.jpg"}, true},
|
||||||
|
{"backup.tmp", []string{"*.tmp"}, true},
|
||||||
|
{"image.png", []string{"*.jpg", "*.png"}, true},
|
||||||
|
{"document.pdf", []string{"*.doc"}, false},
|
||||||
|
|
||||||
|
// Базовое имя
|
||||||
|
{"deeply/nested/.env", []string{".env"}, true},
|
||||||
|
{"config/.env", []string{".*"}, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
result := shouldExcludePath(test.path, test.patterns)
|
||||||
|
if result != test.expected {
|
||||||
|
t.Errorf("Test %d: For path '%s' and patterns %v expected %t, got %t",
|
||||||
|
i+1, test.path, test.patterns, test.expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTargetNaming(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
source string
|
||||||
|
target string
|
||||||
|
isDirectory bool
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{source: "test_dir", target: "backup", isDirectory: true, expected: "backup.tar.gz"},
|
||||||
|
{source: "test_file.txt", target: "backup_file", isDirectory: false, expected: "backup_file.gz"},
|
||||||
|
{source: "test_dir", target: "archive.tar.gz", isDirectory: true, expected: "archive.tar.gz"},
|
||||||
|
{source: "test_file.txt", target: "backup.gz", isDirectory: false, expected: "backup.gz"},
|
||||||
|
{
|
||||||
|
source: "test_dir",
|
||||||
|
target: "server:8000/test_dir",
|
||||||
|
isDirectory: true,
|
||||||
|
expected: "http://server:8000/backup/test_dir.tar.gz",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "test_file.txt",
|
||||||
|
target: "server:8000/file",
|
||||||
|
isDirectory: false,
|
||||||
|
expected: "http://server:8000/backup/file.gz",
|
||||||
|
},
|
||||||
|
{source: "test_dir", target: "192.168.1.100:8080", isDirectory: true, expected: "192.168.1.100:8080"},
|
||||||
|
{source: "test_file.txt", target: "10.0.0.5:9000", isDirectory: false, expected: "10.0.0.5:9000"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
// Создаем временную структуру для теста
|
||||||
|
var tempPath string
|
||||||
|
if test.isDirectory {
|
||||||
|
tempPath, _ = os.MkdirTemp("", "test_dir")
|
||||||
|
defer os.RemoveAll(tempPath)
|
||||||
|
} else {
|
||||||
|
tempFile, _ := os.CreateTemp("", "test_file.txt")
|
||||||
|
tempPath = tempFile.Name()
|
||||||
|
tempFile.Close()
|
||||||
|
defer os.Remove(tempPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
model, err := initialModel(tempPath, test.target, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error creating model: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := model.target
|
||||||
|
if actual != test.expected {
|
||||||
|
t.Errorf("For source '%s' (dir:%t) and target '%s' expected '%s', got '%s'",
|
||||||
|
test.source, test.isDirectory, test.target, test.expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -6,6 +6,8 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var osExit = os.Exit
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
Use: "backup",
|
Use: "backup",
|
||||||
Short: "Backup your data",
|
Short: "Backup your data",
|
||||||
@@ -14,6 +16,6 @@ var rootCmd = &cobra.Command{
|
|||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
os.Exit(1)
|
osExit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@@ -37,6 +38,7 @@ var serveCmd = &cobra.Command{
|
|||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
r.HandleFunc("/backup/{filename}", uploadHandler).Methods("POST")
|
r.HandleFunc("/backup/{filename}", uploadHandler).Methods("POST")
|
||||||
r.HandleFunc("/backup/{filename}", downloadHandler).Methods("GET")
|
r.HandleFunc("/backup/{filename}", downloadHandler).Methods("GET")
|
||||||
|
r.HandleFunc("/backup", listHandler).Methods("GET")
|
||||||
|
|
||||||
log.Printf("Starting server on %s", serveAddress)
|
log.Printf("Starting server on %s", serveAddress)
|
||||||
log.Printf("Using backup directory: %s", serveDirectory)
|
log.Printf("Using backup directory: %s", serveDirectory)
|
||||||
@@ -89,6 +91,26 @@ func downloadHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
log.Printf("Downloaded %s", filename)
|
log.Printf("Downloaded %s", filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func listHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
files, err := os.ReadDir(serveDirectory)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Failed to read directory", http.StatusInternalServerError)
|
||||||
|
log.Printf("Error reading directory: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var filenames []string
|
||||||
|
for _, file := range files {
|
||||||
|
if !file.IsDir() {
|
||||||
|
filenames = append(filenames, file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(filenames)
|
||||||
|
log.Printf("Listed files")
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(serveCmd)
|
rootCmd.AddCommand(serveCmd)
|
||||||
serveCmd.Flags().StringVarP(&serveAddress, "address", "a", "localhost:8080", "Address and port for the server")
|
serveCmd.Flags().StringVarP(&serveAddress, "address", "a", "localhost:8080", "Address and port for the server")
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setupTest(t *testing.T) (string, func()) {
|
||||||
|
tempDir := t.TempDir()
|
||||||
|
oldDir := serveDirectory
|
||||||
|
serveDirectory = tempDir
|
||||||
|
|
||||||
|
return tempDir, func() {
|
||||||
|
serveDirectory = oldDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUploadHandler_Success(t *testing.T) {
|
||||||
|
tempDir, cleanup := setupTest(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
r := mux.NewRouter()
|
||||||
|
r.HandleFunc("/backup/{filename}", uploadHandler).Methods("POST")
|
||||||
|
|
||||||
|
body := bytes.NewBufferString("test data")
|
||||||
|
req := httptest.NewRequest("POST", "/backup/testfile", body)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"filename": "testfile"})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := filepath.Join(tempDir, "testfile")
|
||||||
|
data, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("File not created:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(data) != "test data" {
|
||||||
|
t.Errorf("Expected 'test data', got '%s'", string(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUploadHandler_CreateFileError(t *testing.T) {
|
||||||
|
_, cleanup := setupTest(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Создаем директорию без прав на запись
|
||||||
|
serveDirectory = "/proc/invalid_path"
|
||||||
|
|
||||||
|
r := mux.NewRouter()
|
||||||
|
r.HandleFunc("/backup/{filename}", uploadHandler).Methods("POST")
|
||||||
|
|
||||||
|
body := bytes.NewBufferString("test")
|
||||||
|
req := httptest.NewRequest("POST", "/backup/testfile", body)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"filename": "testfile"})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusInternalServerError {
|
||||||
|
t.Errorf("Expected status 500, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloadHandler_Success(t *testing.T) {
|
||||||
|
tempDir, cleanup := setupTest(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Создаем тестовый файл
|
||||||
|
filePath := filepath.Join(tempDir, "testfile")
|
||||||
|
if err := os.WriteFile(filePath, []byte("test data"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := mux.NewRouter()
|
||||||
|
r.HandleFunc("/backup/{filename}", downloadHandler).Methods("GET")
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/backup/testfile", nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"filename": "testfile"})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
if body := w.Body.String(); body != "test data" {
|
||||||
|
t.Errorf("Expected 'test data', got '%s'", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloadHandler_NotFound(t *testing.T) {
|
||||||
|
_, cleanup := setupTest(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
r := mux.NewRouter()
|
||||||
|
r.HandleFunc("/backup/{filename}", downloadHandler).Methods("GET")
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/backup/missing", nil)
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"filename": "missing"})
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusNotFound {
|
||||||
|
t.Errorf("Expected status 404, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListHandler_Success(t *testing.T) {
|
||||||
|
tempDir, cleanup := setupTest(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Создаем тестовые файлы
|
||||||
|
files := []string{"file1.txt", "file2.txt"}
|
||||||
|
for _, f := range files {
|
||||||
|
path := filepath.Join(tempDir, f)
|
||||||
|
if err := os.WriteFile(path, []byte("data"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r := mux.NewRouter()
|
||||||
|
r.HandleFunc("/backup", listHandler).Methods("GET")
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/backup", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Errorf("Expected status 200, got %d", w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &result); err != nil {
|
||||||
|
t.Fatal("Failed to parse JSON:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(result)
|
||||||
|
expected := []string{"file1.txt", "file2.txt"}
|
||||||
|
sort.Strings(expected)
|
||||||
|
|
||||||
|
if len(result) != len(expected) {
|
||||||
|
t.Fatalf("Expected %d files, got %d", len(expected), len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range expected {
|
||||||
|
if expected[i] != result[i] {
|
||||||
|
t.Errorf("Expected %s, got %s", expected[i], result[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListHandler_ReadDirError(t *testing.T) {
|
||||||
|
tempDir, cleanup := setupTest(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
// Удаляем директорию чтобы вызвать ошибку
|
||||||
|
os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
r := mux.NewRouter()
|
||||||
|
r.HandleFunc("/backup", listHandler).Methods("GET")
|
||||||
|
|
||||||
|
req := httptest.NewRequest("GET", "/backup", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusInternalServerError {
|
||||||
|
t.Errorf("Expected status 500, got %d", w.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,6 +48,15 @@ func parseServerTarget(target string) (*ServerTargetInfo, bool) {
|
|||||||
return nil, false // Invalid format
|
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]
|
address := parts[0]
|
||||||
filename := parts[1]
|
filename := parts[1]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ require (
|
|||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/gorilla/mux v1.8.1
|
github.com/gorilla/mux v1.8.1
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
|
github.com/stretchr/testify v1.10.0
|
||||||
golang.org/x/sys v0.30.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/ansi v0.8.0 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.1 // 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/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.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/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
github.com/muesli/termenv v0.16.0 // 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/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sync v0.11.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
golang.org/x/text v0.3.8 // indirect
|
golang.org/x/text v0.3.8 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package main
|
|||||||
|
|
||||||
import "backup/cmd"
|
import "backup/cmd"
|
||||||
|
|
||||||
|
var execute = cmd.Execute
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cmd.Execute()
|
execute()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user