From 189cf664cd742d366be813692b7699d220822a4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=B9=BF?= Date: Mon, 7 Jul 2025 15:16:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=9E=84=E5=BB=BA?= =?UTF-8?q?=E5=99=A8=E6=B5=8B=E8=AF=95=E6=A1=86=E6=9E=B6=E5=92=8CLinux?= =?UTF-8?q?=E5=8C=85=E6=B5=8B=E8=AF=95=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 4 +- go.sum | 5 - internal/builder/builder_test.go | 181 ++++++++++++++++ internal/builder/linux/deb_test.go | 296 ++++++++++++++++++++++++++ internal/builder/linux/rpm_test.go | 306 +++++++++++++++++++++++++++ internal/builder/windows/msi_test.go | 204 ++++++++++++++++++ internal/config/config_test.go | 254 ++++++++++++++++++++++ internal/config/validate_test.go | 231 ++++++++++++++++++++ 8 files changed, 1475 insertions(+), 6 deletions(-) create mode 100644 internal/builder/builder_test.go create mode 100644 internal/builder/linux/deb_test.go create mode 100644 internal/builder/linux/rpm_test.go create mode 100644 internal/builder/windows/msi_test.go create mode 100644 internal/config/config_test.go create mode 100644 internal/config/validate_test.go diff --git a/go.mod b/go.mod index 738e84b..46d7420 100644 --- a/go.mod +++ b/go.mod @@ -6,21 +6,23 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.9.1 github.com/spf13/viper v1.16.0 + github.com/stretchr/testify v1.8.4 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.6 // indirect - github.com/stretchr/testify v1.8.4 // indirect github.com/subosito/gotenv v1.4.2 // indirect golang.org/x/sys v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect diff --git a/go.sum b/go.sum index df9657b..758409c 100644 --- a/go.sum +++ b/go.sum @@ -46,7 +46,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -155,14 +154,10 @@ github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= diff --git a/internal/builder/builder_test.go b/internal/builder/builder_test.go new file mode 100644 index 0000000..226627d --- /dev/null +++ b/internal/builder/builder_test.go @@ -0,0 +1,181 @@ +package builder + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "git.kingecg.top/kingecg/installerbuilder/internal/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// 创建一个测试用的构建器实现 +type MockBuilder struct { + name string + supportedPlatforms []string + supportedFormats []string + buildFunc func() error +} + +func (b *MockBuilder) Build() error { + if b.buildFunc != nil { + return b.buildFunc() + } + return nil +} + +func (b *MockBuilder) Name() string { + return b.name +} + +func (b *MockBuilder) SupportedPlatforms() []string { + return b.supportedPlatforms +} + +func (b *MockBuilder) SupportedFormats() []string { + return b.supportedFormats +} + +func TestRegisterAndGetBuilder(t *testing.T) { + // 清理注册的构建器 + builderFactories = make(map[string]BuilderFactory) + + // 注册一个测试构建器 + RegisterBuilder("test-builder", func(cfg *config.Config, outputDir string) Builder { + return &MockBuilder{ + name: "Test Builder", + supportedPlatforms: []string{"test-platform"}, + supportedFormats: []string{"test-format"}, + } + }) + + // 测试获取已注册的构建器 + cfg := &config.Config{Name: "TestApp", Version: "1.0.0"} + builder, err := GetBuilder("test-builder", cfg, "output") + assert.NoError(t, err) + assert.NotNil(t, builder) + assert.Equal(t, "Test Builder", builder.Name()) + assert.Equal(t, []string{"test-platform"}, builder.SupportedPlatforms()) + assert.Equal(t, []string{"test-format"}, builder.SupportedFormats()) + + // 测试获取未注册的构建器 + _, err = GetBuilder("non-existent", cfg, "output") + assert.Error(t, err) + assert.Contains(t, err.Error(), "未找到名为 'non-existent' 的构建器") +} + +func TestBuildAll(t *testing.T) { + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "builder-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建配置文件 + configPath := filepath.Join(tempDir, "config.yaml") + configContent := ` +name: TestApp +version: 1.0.0 +description: Test Application +author: Test Author +license: MIT + +build: + outputDir: dist + targets: + - windows + - linux + formats: + - msi + - deb +` + err = os.WriteFile(configPath, []byte(configContent), 0644) + require.NoError(t, err) + + // 清理注册的构建器 + builderFactories = make(map[string]BuilderFactory) + + // 注册测试构建器 + RegisterBuilder("windows-msi", func(cfg *config.Config, outputDir string) Builder { + return &MockBuilder{ + name: "Windows MSI Builder", + supportedPlatforms: []string{"windows"}, + supportedFormats: []string{"msi"}, + } + }) + + RegisterBuilder("linux-deb", func(cfg *config.Config, outputDir string) Builder { + return &MockBuilder{ + name: "Linux DEB Builder", + supportedPlatforms: []string{"linux"}, + supportedFormats: []string{"deb"}, + } + }) + + // 测试成功构建 + opts := BuildOptions{ + ConfigPath: configPath, + OutputDir: filepath.Join(tempDir, "output"), + Targets: []string{"windows", "linux"}, + Formats: []string{"msi", "deb"}, + Parallel: 2, + } + + err = BuildAll(opts) + assert.NoError(t, err) + + // 测试构建失败 + RegisterBuilder("windows-msi", func(cfg *config.Config, outputDir string) Builder { + return &MockBuilder{ + name: "Windows MSI Builder", + supportedPlatforms: []string{"windows"}, + supportedFormats: []string{"msi"}, + buildFunc: func() error { return errors.New("构建失败") }, + } + }) + + err = BuildAll(opts) + assert.Error(t, err) + assert.Contains(t, err.Error(), "构建过程中发生") + assert.Contains(t, err.Error(), "构建失败") + + // 测试无效配置 + opts.ConfigPath = filepath.Join(tempDir, "nonexistent.yaml") + err = BuildAll(opts) + assert.Error(t, err) + assert.Contains(t, err.Error(), "加载配置失败") + + // 测试无构建任务 + opts.ConfigPath = configPath + opts.Targets = []string{"invalid-platform"} + err = BuildAll(opts) + assert.Error(t, err) + assert.Contains(t, err.Error(), "没有可构建的任务") +} + +func TestIsFormatSupportedForTarget(t *testing.T) { + tests := []struct { + target string + format string + expected bool + }{ + {"windows", "msi", true}, + {"windows", "zip", true}, + {"windows", "deb", false}, + {"windows", "rpm", false}, + {"linux", "deb", true}, + {"linux", "rpm", true}, + {"linux", "tar.gz", true}, + {"linux", "msi", false}, + {"invalid", "msi", false}, + {"windows", "invalid", false}, + } + + for _, tt := range tests { + t.Run(tt.target+"-"+tt.format, func(t *testing.T) { + result := isFormatSupportedForTarget(tt.target, tt.format) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/internal/builder/linux/deb_test.go b/internal/builder/linux/deb_test.go new file mode 100644 index 0000000..e05bc65 --- /dev/null +++ b/internal/builder/linux/deb_test.go @@ -0,0 +1,296 @@ +package linux + +import ( + "os" + "path/filepath" + "testing" + + "git.kingecg.top/kingecg/installerbuilder/internal/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDEBBuilder_Name(t *testing.T) { + cfg := &config.Config{Name: "TestApp", Version: "1.0.0"} + builder := NewDEBBuilder(cfg, "output") + assert.Equal(t, "Linux DEB Builder", builder.Name()) +} + +func TestDEBBuilder_SupportedPlatforms(t *testing.T) { + cfg := &config.Config{Name: "TestApp", Version: "1.0.0"} + builder := NewDEBBuilder(cfg, "output") + platforms := builder.SupportedPlatforms() + assert.Equal(t, []string{"linux"}, platforms) +} + +func TestDEBBuilder_SupportedFormats(t *testing.T) { + cfg := &config.Config{Name: "TestApp", Version: "1.0.0"} + builder := NewDEBBuilder(cfg, "output") + formats := builder.SupportedFormats() + assert.Equal(t, []string{"deb"}, formats) +} + +func TestDEBBuilder_CreateDebStructure(t *testing.T) { + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "deb-builder-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建测试配置 + cfg := &config.Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + } + + // 创建DEB构建器 + builder := NewDEBBuilder(cfg, tempDir) + + // 测试创建DEB包结构 + err = builder.createDebStructure(tempDir) + assert.NoError(t, err) + + // 验证目录结构 + debianDir := filepath.Join(tempDir, "DEBIAN") + _, err = os.Stat(debianDir) + assert.NoError(t, err) + + installDir := filepath.Join(tempDir, "usr", "local", "bin") + _, err = os.Stat(installDir) + assert.NoError(t, err) + + docDir := filepath.Join(tempDir, "usr", "share", "doc", "TestApp") + _, err = os.Stat(docDir) + assert.NoError(t, err) +} + +func TestDEBBuilder_CreateControlFile(t *testing.T) { + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "deb-builder-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建DEBIAN目录 + debianDir := filepath.Join(tempDir, "DEBIAN") + err = os.MkdirAll(debianDir, 0755) + require.NoError(t, err) + + // 创建测试配置 + cfg := &config.Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + Platforms: config.PlatformsConfig{ + Linux: config.LinuxConfig{ + Deb: config.LinuxDebConfig{ + Section: "utils", + Priority: "optional", + Architecture: "amd64", + Depends: []string{"libc6", "libstdc++6"}, + Recommends: []string{"curl"}, + Suggests: []string{"wget"}, + Conflicts: []string{"old-package"}, + Replaces: []string{"legacy-package"}, + }, + }, + }, + } + + // 创建DEB构建器 + builder := NewDEBBuilder(cfg, tempDir) + + // 测试创建控制文件 + err = builder.createControlFile(tempDir) + assert.NoError(t, err) + + // 验证控制文件是否存在 + controlFile := filepath.Join(tempDir, "DEBIAN", "control") + _, err = os.Stat(controlFile) + assert.NoError(t, err) + + // 读取控制文件内容 + content, err := os.ReadFile(controlFile) + assert.NoError(t, err) + + // 验证文件内容 + contentStr := string(content) + assert.Contains(t, contentStr, "Package: TestApp") + assert.Contains(t, contentStr, "Version: 1.0.0") + assert.Contains(t, contentStr, "Section: utils") + assert.Contains(t, contentStr, "Priority: optional") + assert.Contains(t, contentStr, "Architecture: amd64") + assert.Contains(t, contentStr, "Maintainer: Test Author") + assert.Contains(t, contentStr, "Description: Test Application") + assert.Contains(t, contentStr, "Depends: libc6, libstdc++6") + assert.Contains(t, contentStr, "Recommends: curl") + assert.Contains(t, contentStr, "Suggests: wget") + assert.Contains(t, contentStr, "Conflicts: old-package") + assert.Contains(t, contentStr, "Replaces: legacy-package") +} + +func TestDEBBuilder_CreateScripts(t *testing.T) { + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "deb-builder-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建DEBIAN目录 + debianDir := filepath.Join(tempDir, "DEBIAN") + err = os.MkdirAll(debianDir, 0755) + require.NoError(t, err) + + // 创建测试脚本源文件 + scriptDir := filepath.Join(tempDir, "scripts") + err = os.MkdirAll(scriptDir, 0755) + require.NoError(t, err) + + preinstScript := filepath.Join(scriptDir, "preinst.sh") + err = os.WriteFile(preinstScript, []byte("#!/bin/sh\necho 'Pre-install script'"), 0644) + require.NoError(t, err) + + // 创建测试配置 + cfg := &config.Config{ + Name: "TestApp", + Version: "1.0.0", + Contents: config.ContentsConfig{ + Scripts: []config.ScriptConfig{ + { + Type: "preinstall", + Content: "#!/bin/sh\necho 'Pre-install script'", + }, + { + Type: "postinstall", + Source: preinstScript, + }, + { + Type: "preuninstall", + Content: "echo 'Pre-uninstall script'", // 没有shebang + }, + { + Type: "invalid", + Content: "echo 'Invalid script'", + }, + }, + }, + } + + // 创建DEB构建器 + builder := NewDEBBuilder(cfg, tempDir) + + // 测试创建脚本文件 + err = builder.createScripts(tempDir) + assert.NoError(t, err) + + // 验证脚本文件是否存在 + preinstFile := filepath.Join(tempDir, "DEBIAN", "preinst") + _, err = os.Stat(preinstFile) + assert.NoError(t, err) + + postinstFile := filepath.Join(tempDir, "DEBIAN", "postinst") + _, err = os.Stat(postinstFile) + assert.NoError(t, err) + + prermFile := filepath.Join(tempDir, "DEBIAN", "prerm") + _, err = os.Stat(prermFile) + assert.NoError(t, err) + + // 读取脚本文件内容 + content, err := os.ReadFile(preinstFile) + assert.NoError(t, err) + assert.Contains(t, string(content), "#!/bin/sh") + assert.Contains(t, string(content), "echo 'Pre-install script'") + + content, err = os.ReadFile(prermFile) + assert.NoError(t, err) + assert.Contains(t, string(content), "#!/bin/sh") + assert.Contains(t, string(content), "echo 'Pre-uninstall script'") +} + +func TestDEBBuilder_Build(t *testing.T) { + // 跳过实际构建测试,因为它需要dpkg-deb工具 + t.Skip("跳过实际构建测试,因为它需要dpkg-deb工具") + + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "deb-builder-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建输出目录 + outputDir := filepath.Join(tempDir, "output") + err = os.MkdirAll(outputDir, 0755) + require.NoError(t, err) + + // 创建测试配置 + cfg := &config.Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + Platforms: config.PlatformsConfig{ + Linux: config.LinuxConfig{ + Deb: config.LinuxDebConfig{ + Section: "utils", + Priority: "optional", + Architecture: "amd64", + }, + }, + }, + } + + // 创建DEB构建器 + builder := NewDEBBuilder(cfg, outputDir) + + // 测试构建 + err = builder.Build() + assert.NoError(t, err) + + // 验证输出文件是否存在 + outputFile := filepath.Join(outputDir, "TestApp_1.0.0_amd64.deb") + _, err = os.Stat(outputFile) + assert.NoError(t, err) +} + +func TestDEBBuilder_BuildDeb(t *testing.T) { + // 跳过实际构建测试,因为它需要dpkg-deb工具 + t.Skip("跳过实际构建测试,因为它需要dpkg-deb工具") + + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "deb-builder-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建输出目录 + outputDir := filepath.Join(tempDir, "output") + err = os.MkdirAll(outputDir, 0755) + require.NoError(t, err) + + // 创建测试配置 + cfg := &config.Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + } + + // 创建DEB构建器 + builder := NewDEBBuilder(cfg, outputDir) + + // 创建DEB包结构 + err = builder.createDebStructure(tempDir) + assert.NoError(t, err) + + // 创建控制文件 + err = builder.createControlFile(tempDir) + assert.NoError(t, err) + + // 测试构建DEB包 + outputFile := filepath.Join(outputDir, "TestApp_1.0.0_amd64.deb") + err = builder.buildDeb(tempDir, outputFile) + assert.NoError(t, err) + + // 验证输出文件是否存在 + _, err = os.Stat(outputFile) + assert.NoError(t, err) +} diff --git a/internal/builder/linux/rpm_test.go b/internal/builder/linux/rpm_test.go new file mode 100644 index 0000000..01a284c --- /dev/null +++ b/internal/builder/linux/rpm_test.go @@ -0,0 +1,306 @@ +package linux + +import ( + "os" + "path/filepath" + "testing" + + "git.kingecg.top/kingecg/installerbuilder/internal/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRPMBuilder_Name(t *testing.T) { + cfg := &config.Config{Name: "TestApp", Version: "1.0.0"} + builder := NewRPMBuilder(cfg, "output") + assert.Equal(t, "Linux RPM Builder", builder.Name()) +} + +func TestRPMBuilder_SupportedPlatforms(t *testing.T) { + cfg := &config.Config{Name: "TestApp", Version: "1.0.0"} + builder := NewRPMBuilder(cfg, "output") + platforms := builder.SupportedPlatforms() + assert.Equal(t, []string{"linux"}, platforms) +} + +func TestRPMBuilder_SupportedFormats(t *testing.T) { + cfg := &config.Config{Name: "TestApp", Version: "1.0.0"} + builder := NewRPMBuilder(cfg, "output") + formats := builder.SupportedFormats() + assert.Equal(t, []string{"rpm"}, formats) +} + +func TestRPMBuilder_CreateSourceTarball(t *testing.T) { + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "rpm-builder-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建源代码目录 + sourceDir := filepath.Join(tempDir, "SOURCES", "TestApp-1.0.0") + err = os.MkdirAll(sourceDir, 0755) + require.NoError(t, err) + + // 创建测试文件 + testFile := filepath.Join(sourceDir, "test.txt") + err = os.WriteFile(testFile, []byte("test content"), 0644) + require.NoError(t, err) + + // 创建测试配置 + cfg := &config.Config{ + Name: "TestApp", + Version: "1.0.0", + } + + // 创建RPM构建器 + builder := NewRPMBuilder(cfg, tempDir) + + // 测试创建源代码压缩包 + tarFile := filepath.Join(tempDir, "SOURCES", "TestApp-1.0.0.tar.gz") + err = builder.createSourceTarball(sourceDir, tarFile) + + // 由于createSourceTarball需要执行tar命令,可能在测试环境中不可用,所以我们跳过错误检查 + if err != nil { + t.Skip("跳过tar命令测试,可能在测试环境中不可用") + } + + // 验证压缩包是否存在 + _, err = os.Stat(tarFile) + assert.NoError(t, err) +} + +func TestRPMBuilder_CreateSpecFile(t *testing.T) { + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "rpm-builder-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建SPECS目录 + specsDir := filepath.Join(tempDir, "SPECS") + err = os.MkdirAll(specsDir, 0755) + require.NoError(t, err) + + // 创建测试脚本源文件 + scriptDir := filepath.Join(tempDir, "scripts") + err = os.MkdirAll(scriptDir, 0755) + require.NoError(t, err) + + preinstScript := filepath.Join(scriptDir, "preinst.sh") + err = os.WriteFile(preinstScript, []byte("#!/bin/sh\necho 'Pre-install script'"), 0644) + require.NoError(t, err) + + // 创建测试配置 + cfg := &config.Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + License: "MIT", + Platforms: config.PlatformsConfig{ + Linux: config.LinuxConfig{ + Rpm: config.LinuxRpmConfig{ + Group: "Applications/System", + URL: "https://example.com", + Requires: []string{"glibc", "libstdc++"}, + Provides: []string{"testapp"}, + Conflicts: []string{"old-package"}, + Obsoletes: []string{"legacy-package"}, + AutoReqProv: true, + }, + }, + }, + Contents: config.ContentsConfig{ + Scripts: []config.ScriptConfig{ + { + Type: "preinstall", + Content: "#!/bin/sh\necho 'Pre-install script'", + }, + { + Type: "postinstall", + Source: preinstScript, + }, + { + Type: "preuninstall", + Content: "echo 'Pre-uninstall script'", + }, + { + Type: "postuninstall", + Content: "echo 'Post-uninstall script'", + }, + { + Type: "invalid", + Content: "echo 'Invalid script'", + }, + }, + }, + } + + // 创建RPM构建器 + builder := NewRPMBuilder(cfg, tempDir) + + // 测试创建SPEC文件 + specFile := filepath.Join(specsDir, "TestApp.spec") + err = builder.createSpecFile(specFile) + assert.NoError(t, err) + + // 验证SPEC文件是否存在 + _, err = os.Stat(specFile) + assert.NoError(t, err) + + // 读取SPEC文件内容 + content, err := os.ReadFile(specFile) + assert.NoError(t, err) + + // 验证文件内容 + contentStr := string(content) + assert.Contains(t, contentStr, "Name: TestApp") + assert.Contains(t, contentStr, "Version: 1.0.0") + assert.Contains(t, contentStr, "Summary: Test Application") + assert.Contains(t, contentStr, "Group: Applications/System") + assert.Contains(t, contentStr, "License: MIT") + assert.Contains(t, contentStr, "URL: https://example.com") + assert.Contains(t, contentStr, "Requires: glibc, libstdc++") + assert.Contains(t, contentStr, "Provides: testapp") + assert.Contains(t, contentStr, "Conflicts: old-package") + assert.Contains(t, contentStr, "Obsoletes: legacy-package") + assert.Contains(t, contentStr, "%pre") + assert.Contains(t, contentStr, "%post") + assert.Contains(t, contentStr, "%preun") + assert.Contains(t, contentStr, "%postun") +} + +func TestRPMBuilder_Build(t *testing.T) { + // 跳过实际构建测试,因为它需要rpmbuild工具 + t.Skip("跳过实际构建测试,因为它需要rpmbuild工具") + + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "rpm-builder-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建输出目录 + outputDir := filepath.Join(tempDir, "output") + err = os.MkdirAll(outputDir, 0755) + require.NoError(t, err) + + // 创建测试配置 + cfg := &config.Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + License: "MIT", + Platforms: config.PlatformsConfig{ + Linux: config.LinuxConfig{ + Rpm: config.LinuxRpmConfig{ + Group: "Applications/System", + URL: "https://example.com", + AutoReqProv: true, + }, + }, + }, + } + + // 创建RPM构建器 + builder := NewRPMBuilder(cfg, outputDir) + + // 测试构建 + err = builder.Build() + assert.NoError(t, err) + + // 验证输出文件是否存在 + outputFile := filepath.Join(outputDir, "TestApp-1.0.0.x86_64.rpm") + _, err = os.Stat(outputFile) + assert.NoError(t, err) +} + +func TestRPMBuilder_BuildRpm(t *testing.T) { + // 跳过实际构建测试,因为它需要rpmbuild工具 + t.Skip("跳过实际构建测试,因为它需要rpmbuild工具") + + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "rpm-builder-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建RPM构建目录结构 + buildDirs := []string{ + filepath.Join(tempDir, "BUILD"), + filepath.Join(tempDir, "RPMS"), + filepath.Join(tempDir, "SOURCES"), + filepath.Join(tempDir, "SPECS"), + filepath.Join(tempDir, "SRPMS"), + } + + for _, dir := range buildDirs { + err = os.MkdirAll(dir, 0755) + require.NoError(t, err) + } + + // 创建输出目录 + outputDir := filepath.Join(tempDir, "output") + err = os.MkdirAll(outputDir, 0755) + require.NoError(t, err) + + // 创建测试配置 + cfg := &config.Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + License: "MIT", + } + + // 创建RPM构建器 + builder := NewRPMBuilder(cfg, outputDir) + + // 创建SPEC文件 + specFile := filepath.Join(tempDir, "SPECS", "TestApp.spec") + err = builder.createSpecFile(specFile) + assert.NoError(t, err) + + // 测试构建RPM包 + outputFile := filepath.Join(outputDir, "TestApp-1.0.0.x86_64.rpm") + err = builder.buildRpm(tempDir, specFile, outputFile) + assert.NoError(t, err) + + // 验证输出文件是否存在 + _, err = os.Stat(outputFile) + assert.NoError(t, err) +} + +func TestCopyFile(t *testing.T) { + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "rpm-builder-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建源文件 + srcFile := filepath.Join(tempDir, "source.txt") + err = os.WriteFile(srcFile, []byte("test content"), 0644) + require.NoError(t, err) + + // 创建目标文件路径 + dstFile := filepath.Join(tempDir, "destination.txt") + + // 测试复制文件 + err = copyFile(srcFile, dstFile) + assert.NoError(t, err) + + // 验证目标文件是否存在 + _, err = os.Stat(dstFile) + assert.NoError(t, err) + + // 读取目标文件内容 + content, err := os.ReadFile(dstFile) + assert.NoError(t, err) + assert.Equal(t, "test content", string(content)) + + // 测试复制不存在的文件 + err = copyFile(filepath.Join(tempDir, "nonexistent.txt"), dstFile) + assert.Error(t, err) + + // 测试复制到无效路径 + err = copyFile(srcFile, "/invalid/path/file.txt") + assert.Error(t, err) +} diff --git a/internal/builder/windows/msi_test.go b/internal/builder/windows/msi_test.go new file mode 100644 index 0000000..30c216a --- /dev/null +++ b/internal/builder/windows/msi_test.go @@ -0,0 +1,204 @@ +package windows + +import ( + "os" + "path/filepath" + "testing" + + "git.kingecg.top/kingecg/installerbuilder/internal/config" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMSIBuilder_Name(t *testing.T) { + cfg := &config.Config{Name: "TestApp", Version: "1.0.0"} + builder := NewMSIBuilder(cfg, "output") + assert.Equal(t, "Windows MSI Builder", builder.Name()) +} + +func TestMSIBuilder_SupportedPlatforms(t *testing.T) { + cfg := &config.Config{Name: "TestApp", Version: "1.0.0"} + builder := NewMSIBuilder(cfg, "output") + platforms := builder.SupportedPlatforms() + assert.Equal(t, []string{"windows"}, platforms) +} + +func TestMSIBuilder_SupportedFormats(t *testing.T) { + cfg := &config.Config{Name: "TestApp", Version: "1.0.0"} + builder := NewMSIBuilder(cfg, "output") + formats := builder.SupportedFormats() + assert.Equal(t, []string{"msi"}, formats) +} + +func TestMSIBuilder_CreateWixSource(t *testing.T) { + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "msi-builder-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建测试配置 + cfg := &config.Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + License: "MIT", + Platforms: config.PlatformsConfig{ + Windows: config.WindowsConfig{ + Msi: config.WindowsMsiConfig{ + Manufacturer: "Test Manufacturer", + UpgradeCode: "12345678-1234-1234-1234-123456789012", + InstallDir: "TestApp", + }, + }, + }, + } + + // 创建MSI构建器 + builder := NewMSIBuilder(cfg, tempDir) + + // 测试创建WiX源文件 + wixFile := filepath.Join(tempDir, "installer.wxs") + err = builder.createWixSource(wixFile) + assert.NoError(t, err) + + // 验证文件是否存在 + _, err = os.Stat(wixFile) + assert.NoError(t, err) + + // 读取文件内容 + content, err := os.ReadFile(wixFile) + assert.NoError(t, err) + + // 验证文件内容 + contentStr := string(content) + assert.Contains(t, contentStr, `Name="TestApp"`) + assert.Contains(t, contentStr, `Version="1.0.0"`) + assert.Contains(t, contentStr, `Manufacturer="Test Manufacturer"`) + assert.Contains(t, contentStr, `UpgradeCode="12345678-1234-1234-1234-123456789012"`) + assert.Contains(t, contentStr, `Description="Test Application"`) + assert.Contains(t, contentStr, `Name="TestApp"`) +} + +func TestMSIBuilder_Build(t *testing.T) { + // 跳过实际构建测试,因为它需要WiX工具集 + t.Skip("跳过实际构建测试,因为它需要WiX工具集") + + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "msi-builder-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建输出目录 + outputDir := filepath.Join(tempDir, "output") + err = os.MkdirAll(outputDir, 0755) + require.NoError(t, err) + + // 创建测试配置 + cfg := &config.Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + License: "MIT", + Platforms: config.PlatformsConfig{ + Windows: config.WindowsConfig{ + Msi: config.WindowsMsiConfig{ + Manufacturer: "Test Manufacturer", + UpgradeCode: "12345678-1234-1234-1234-123456789012", + InstallDir: "TestApp", + }, + }, + }, + } + + // 创建MSI构建器 + builder := NewMSIBuilder(cfg, outputDir) + + // 测试构建 + err = builder.Build() + assert.NoError(t, err) + + // 验证输出文件是否存在 + outputFile := filepath.Join(outputDir, "TestApp-1.0.0.msi") + _, err = os.Stat(outputFile) + assert.NoError(t, err) +} + +func TestMSIBuilder_CompileWixSource(t *testing.T) { + // 跳过实际编译测试,因为它需要WiX工具集 + t.Skip("跳过实际编译测试,因为它需要WiX工具集") + + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "msi-builder-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建测试配置 + cfg := &config.Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + } + + // 创建MSI构建器 + builder := NewMSIBuilder(cfg, tempDir) + + // 创建WiX源文件 + wixFile := filepath.Join(tempDir, "installer.wxs") + err = builder.createWixSource(wixFile) + assert.NoError(t, err) + + // 测试编译WiX源文件 + objFile := filepath.Join(tempDir, "installer.wixobj") + err = builder.compileWixSource(wixFile, objFile) + assert.NoError(t, err) + + // 验证输出文件是否存在 + _, err = os.Stat(objFile) + assert.NoError(t, err) +} + +func TestMSIBuilder_LinkMsi(t *testing.T) { + // 跳过实际链接测试,因为它需要WiX工具集 + t.Skip("跳过实际链接测试,因为它需要WiX工具集") + + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "msi-builder-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建输出目录 + outputDir := filepath.Join(tempDir, "output") + err = os.MkdirAll(outputDir, 0755) + require.NoError(t, err) + + // 创建测试配置 + cfg := &config.Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + } + + // 创建MSI构建器 + builder := NewMSIBuilder(cfg, outputDir) + + // 创建WiX源文件 + wixFile := filepath.Join(tempDir, "installer.wxs") + err = builder.createWixSource(wixFile) + assert.NoError(t, err) + + // 编译WiX源文件 + objFile := filepath.Join(tempDir, "installer.wixobj") + err = builder.compileWixSource(wixFile, objFile) + assert.NoError(t, err) + + // 测试链接生成MSI + msiFile := filepath.Join(outputDir, "TestApp-1.0.0.msi") + err = builder.linkMsi(objFile, msiFile) + assert.NoError(t, err) + + // 验证输出文件是否存在 + _, err = os.Stat(msiFile) + assert.NoError(t, err) +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..a63d81b --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,254 @@ +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLoadConfig(t *testing.T) { + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "config-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建有效的YAML配置文件 + validYAML := ` +name: TestApp +version: 1.0.0 +description: Test Application +author: Test Author +license: MIT + +build: + outputDir: dist + targets: + - windows + - linux + formats: + - msi + - deb + +contents: + files: + - source: test.txt + destination: test.txt + mode: "0644" + scripts: + - type: preinstall + content: "echo 'Pre-install script'" + - type: postinstall + source: scripts/post-install.sh +` + validYAMLPath := filepath.Join(tempDir, "valid.yaml") + err = os.WriteFile(validYAMLPath, []byte(validYAML), 0644) + require.NoError(t, err) + + // 创建无效的YAML配置文件 + invalidYAML := ` +name: TestApp +version: 1.0.0 +invalid_field: value +` + invalidYAMLPath := filepath.Join(tempDir, "invalid.yaml") + err = os.WriteFile(invalidYAMLPath, []byte(invalidYAML), 0644) + require.NoError(t, err) + + // 创建有效的JSON配置文件 + validJSON := `{ + "name": "TestApp", + "version": "1.0.0", + "description": "Test Application", + "author": "Test Author", + "license": "MIT", + "build": { + "outputDir": "dist", + "targets": ["windows", "linux"], + "formats": ["msi", "deb"] + }, + "contents": { + "files": [ + { + "source": "test.txt", + "destination": "test.txt", + "mode": "0644" + } + ], + "scripts": [ + { + "type": "preinstall", + "content": "echo 'Pre-install script'" + }, + { + "type": "postinstall", + "source": "scripts/post-install.sh" + } + ] + } +}` + validJSONPath := filepath.Join(tempDir, "valid.json") + err = os.WriteFile(validJSONPath, []byte(validJSON), 0644) + require.NoError(t, err) + + // 测试用例 + tests := []struct { + name string + path string + wantErr bool + checkCfg func(*Config) + }{ + { + name: "Valid YAML config", + path: validYAMLPath, + wantErr: false, + checkCfg: func(cfg *Config) { + assert.Equal(t, "TestApp", cfg.Name) + assert.Equal(t, "1.0.0", cfg.Version) + assert.Equal(t, "Test Application", cfg.Description) + assert.Equal(t, "Test Author", cfg.Author) + assert.Equal(t, "MIT", cfg.License) + assert.Equal(t, "dist", cfg.Build.OutputDir) + assert.Contains(t, cfg.Build.Targets, "windows") + assert.Contains(t, cfg.Build.Targets, "linux") + assert.Contains(t, cfg.Build.Formats, "msi") + assert.Contains(t, cfg.Build.Formats, "deb") + assert.Len(t, cfg.Contents.Files, 1) + assert.Equal(t, "test.txt", cfg.Contents.Files[0].Source) + assert.Equal(t, "test.txt", cfg.Contents.Files[0].Destination) + assert.Equal(t, "0644", cfg.Contents.Files[0].Mode) + assert.Len(t, cfg.Contents.Scripts, 2) + assert.Equal(t, "preinstall", cfg.Contents.Scripts[0].Type) + assert.Equal(t, "echo 'Pre-install script'", cfg.Contents.Scripts[0].Content) + assert.Equal(t, "postinstall", cfg.Contents.Scripts[1].Type) + assert.Equal(t, "scripts/post-install.sh", cfg.Contents.Scripts[1].Source) + }, + }, + { + name: "Valid JSON config", + path: validJSONPath, + wantErr: false, + checkCfg: func(cfg *Config) { + assert.Equal(t, "TestApp", cfg.Name) + assert.Equal(t, "1.0.0", cfg.Version) + assert.Equal(t, "Test Application", cfg.Description) + assert.Equal(t, "Test Author", cfg.Author) + assert.Equal(t, "MIT", cfg.License) + }, + }, + { + name: "Invalid YAML config", + path: invalidYAMLPath, + wantErr: false, // 不会报错,但会有警告 + checkCfg: func(cfg *Config) { + assert.Equal(t, "TestApp", cfg.Name) + assert.Equal(t, "1.0.0", cfg.Version) + }, + }, + { + name: "Non-existent file", + path: filepath.Join(tempDir, "nonexistent.yaml"), + wantErr: true, + checkCfg: func(cfg *Config) { + assert.Nil(t, cfg) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg, err := LoadConfig(tt.path) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + tt.checkCfg(cfg) + } + }) + } +} + +func TestSaveConfig(t *testing.T) { + // 创建临时测试目录 + tempDir, err := os.MkdirTemp("", "config-test-*") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + // 创建测试配置 + cfg := &Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + License: "MIT", + Build: BuildConfig{ + OutputDir: "dist", + Targets: []string{"windows", "linux"}, + Formats: []string{"msi", "deb"}, + }, + Contents: ContentsConfig{ + Files: []FileConfig{ + { + Source: "test.txt", + Destination: "test.txt", + Mode: "0644", + }, + }, + Scripts: []ScriptConfig{ + { + Type: "preinstall", + Content: "echo 'Pre-install script'", + }, + }, + }, + } + + // 测试YAML保存 + yamlPath := filepath.Join(tempDir, "config.yaml") + err = SaveConfig(cfg, yamlPath) + assert.NoError(t, err) + + // 验证保存的YAML文件 + loadedCfg, err := LoadConfig(yamlPath) + assert.NoError(t, err) + assert.Equal(t, cfg.Name, loadedCfg.Name) + assert.Equal(t, cfg.Version, loadedCfg.Version) + assert.Equal(t, cfg.Description, loadedCfg.Description) + assert.Equal(t, cfg.Build.OutputDir, loadedCfg.Build.OutputDir) + assert.ElementsMatch(t, cfg.Build.Targets, loadedCfg.Build.Targets) + assert.ElementsMatch(t, cfg.Build.Formats, loadedCfg.Build.Formats) + assert.Len(t, loadedCfg.Contents.Files, 1) + assert.Equal(t, cfg.Contents.Files[0].Source, loadedCfg.Contents.Files[0].Source) + assert.Equal(t, cfg.Contents.Files[0].Destination, loadedCfg.Contents.Files[0].Destination) + assert.Equal(t, cfg.Contents.Files[0].Mode, loadedCfg.Contents.Files[0].Mode) + assert.Len(t, loadedCfg.Contents.Scripts, 1) + assert.Equal(t, cfg.Contents.Scripts[0].Type, loadedCfg.Contents.Scripts[0].Type) + assert.Equal(t, cfg.Contents.Scripts[0].Content, loadedCfg.Contents.Scripts[0].Content) + + // 测试JSON保存 + jsonPath := filepath.Join(tempDir, "config.json") + err = SaveConfig(cfg, jsonPath) + assert.NoError(t, err) + + // 验证保存的JSON文件 + loadedCfg, err = LoadConfig(jsonPath) + assert.NoError(t, err) + assert.Equal(t, cfg.Name, loadedCfg.Name) + assert.Equal(t, cfg.Version, loadedCfg.Version) + assert.Equal(t, cfg.Description, loadedCfg.Description) + + // 测试无效路径 + err = SaveConfig(cfg, "/invalid/path/config.yaml") + assert.Error(t, err) +} + +func TestDefaultConfig(t *testing.T) { + cfg := DefaultConfig() + assert.NotEmpty(t, cfg.Name) + assert.NotEmpty(t, cfg.Version) + assert.NotEmpty(t, cfg.Build.OutputDir) + assert.NotEmpty(t, cfg.Build.Targets) + assert.NotEmpty(t, cfg.Build.Formats) +} diff --git a/internal/config/validate_test.go b/internal/config/validate_test.go new file mode 100644 index 0000000..7e1b034 --- /dev/null +++ b/internal/config/validate_test.go @@ -0,0 +1,231 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidate(t *testing.T) { + tests := []struct { + name string + config *Config + strict bool + wantValid bool + wantErrors int + }{ + { + name: "Valid config", + config: &Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + License: "MIT", + Build: BuildConfig{ + OutputDir: "dist", + Targets: []string{"windows", "linux"}, + Formats: []string{"msi", "deb"}, + }, + Contents: ContentsConfig{ + Files: []FileConfig{ + { + Source: "test.txt", + Destination: "test.txt", + Mode: "0644", + }, + }, + }, + }, + strict: true, + wantValid: true, + wantErrors: 0, + }, + { + name: "Missing name", + config: &Config{ + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + License: "MIT", + Build: BuildConfig{ + OutputDir: "dist", + Targets: []string{"windows", "linux"}, + Formats: []string{"msi", "deb"}, + }, + }, + strict: true, + wantValid: false, + wantErrors: 1, + }, + { + name: "Missing version", + config: &Config{ + Name: "TestApp", + Description: "Test Application", + Author: "Test Author", + License: "MIT", + Build: BuildConfig{ + OutputDir: "dist", + Targets: []string{"windows", "linux"}, + Formats: []string{"msi", "deb"}, + }, + }, + strict: true, + wantValid: false, + wantErrors: 1, + }, + { + name: "Invalid target", + config: &Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + License: "MIT", + Build: BuildConfig{ + OutputDir: "dist", + Targets: []string{"windows", "invalid"}, + Formats: []string{"msi", "deb"}, + }, + }, + strict: true, + wantValid: false, + wantErrors: 1, + }, + { + name: "Invalid format", + config: &Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + License: "MIT", + Build: BuildConfig{ + OutputDir: "dist", + Targets: []string{"windows", "linux"}, + Formats: []string{"msi", "invalid"}, + }, + }, + strict: true, + wantValid: false, + wantErrors: 1, + }, + { + name: "Missing optional fields in non-strict mode", + config: &Config{ + Name: "TestApp", + Version: "1.0.0", + Build: BuildConfig{ + Targets: []string{"windows"}, + Formats: []string{"msi"}, + }, + }, + strict: false, + wantValid: true, + wantErrors: 0, + }, + { + name: "Missing optional fields in strict mode", + config: &Config{ + Name: "TestApp", + Version: "1.0.0", + Build: BuildConfig{ + Targets: []string{"windows"}, + Formats: []string{"msi"}, + }, + }, + strict: true, + wantValid: false, + wantErrors: 3, // 缺少描述、作者和许可证 + }, + { + name: "Invalid script type", + config: &Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + License: "MIT", + Build: BuildConfig{ + OutputDir: "dist", + Targets: []string{"windows"}, + Formats: []string{"msi"}, + }, + Contents: ContentsConfig{ + Scripts: []ScriptConfig{ + { + Type: "invalid", + Content: "echo 'Invalid script'", + }, + }, + }, + }, + strict: true, + wantValid: false, + wantErrors: 1, + }, + { + name: "Empty script", + config: &Config{ + Name: "TestApp", + Version: "1.0.0", + Description: "Test Application", + Author: "Test Author", + License: "MIT", + Build: BuildConfig{ + OutputDir: "dist", + Targets: []string{"windows"}, + Formats: []string{"msi"}, + }, + Contents: ContentsConfig{ + Scripts: []ScriptConfig{ + { + Type: "preinstall", + }, + }, + }, + }, + strict: true, + wantValid: false, + wantErrors: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.config.Validate(tt.strict) + assert.Equal(t, tt.wantValid, result.Valid) + assert.Equal(t, tt.wantErrors, len(result.Errors)) + }) + } +} + +func TestValidationResult_String(t *testing.T) { + // 测试空错误列表 + result := ValidationResult{ + Valid: true, + Errors: []ValidationError{}, + } + assert.Equal(t, "配置有效", result.String()) + + // 测试单个错误 + result = ValidationResult{ + Valid: false, + Errors: []ValidationError{ + {Field: "test", Message: "错误1"}, + }, + } + assert.Equal(t, "配置无效: 错误1", result.String()) + + // 测试多个错误 + result = ValidationResult{ + Valid: false, + Errors: []ValidationError{ + {Message: "错误1"}, + {Message: "错误2"}, + {Message: "错误3"}, + }, + } + assert.Equal(t, "配置无效: 错误1, 错误2, 错误3", result.String()) +}