From 24f67186199115593ae5ceea062e4e454c8cc658 Mon Sep 17 00:00:00 2001 From: AshvinBambhaniya2003 <156189340+AshvinBambhaniya2003@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:20:30 +0530 Subject: [PATCH] Feat(testing): Enhance Unit Test Coverage for Core Utility Packages (#6929) * test(cli): enhance unit test coverage for theme and color config This commit introduces a comprehensive suite of unit tests for the theme and color configuration functions in `references/cli/top/config`. Key changes include: - Refactored existing tests in `color_test.go` to use table-driven sub-tests for improved clarity and maintainability. - Added new test functions to validate color parsing, hex color detection, and default theme creation. - Implemented tests for theme file lifecycle management, including creation and loading logic. These additions significantly increase the test coverage and ensure the robustness and correctness of the CLI's theme and color functionality. Signed-off-by: Ashvin Bambhaniya * test(cli): refactor and enhance tests for top view models and utils This commit improves the unit test suite for the CLI's top view functionality by refactoring existing tests and adding new ones to increase coverage. Key changes include: - In `application_test.go`, `TestApplicationList_ToTableBody` is refactored to be a table-driven test, and new tests are added for `serviceNum`, `workflowMode`, and `workflowStepNum` helpers. - In `time_test.go`, `TestTimeFormat` is refactored into a table-driven test for better structure and readability. These changes align the tests with best practices and improve the overall robustness of the CLI top view's data presentation logic. Signed-off-by: Ashvin Bambhaniya * test(cuegen): enhance unit test coverage for CUE generation packages This commit introduces a comprehensive suite of unit tests and refactors existing tests for the CUE generation packages located in `references/cuegen`. Key changes include: - Refactored existing tests in `generator_test.go` and `provider_test.go` to use table-driven sub-tests, improving clarity, maintainability, and coverage of error conditions. - Added new test functions to `convert_test.go` to validate helper functions for comment generation, type support, and enum field handling. - Added new tests in `provider_test.go` to cover provider extraction, declaration modification, and panic recovery logic. These changes significantly increase the test coverage for the `cuegen` libraries, ensuring the correctness and robustness of the CUE code generation functionality. Signed-off-by: Ashvin Bambhaniya * test(docgen): add comprehensive unit tests for doc generation This commit introduces a comprehensive suite of unit tests for the documentation generation package located in `references/docgen`. Key changes include: - Added new test files (`console_test.go`, `convert_test.go`, `openapi_test.go`) to cover the core functions for parsing and generating documentation for CUE, Terraform, and OpenAPI schemas. - Refactored and enhanced `i18n_test.go` to use sub-tests, resolve race conditions, and improve coverage for fallback logic and error handling. - Ensured all new and existing tests follow best practices, using table-driven tests for clarity and maintainability. This effort significantly increases the test coverage for the `docgen` package, improving the reliability and robustness of the documentation generation features. Signed-off-by: Ashvin Bambhaniya * test: improve test reliability and conventions This commit introduces several improvements to the test suite to enhance reliability and adhere to best practices. - **Fix flaky test in `docgen/openapi_test.go`**: The test for `GenerateConsoleDocument` was flaky because it performed an exact string match on table output generated from a map. Since map iteration order is not guaranteed, this could cause spurious failures. The test is now order-insensitive, comparing sorted sets of lines instead. - **Improve assertions in `docgen/console_test.go`**: - Removes an unnecessary `test.EquateErrors()` option, which is not needed for simple string comparisons. - Corrects the `cmp.Diff` argument order to the standard `(want, got)` convention for clearer failure messages. - Fixes a typo in an error message. - **Standardize assertions in `cli/top/config/color_test.go`**: Swaps `assert.Equal` arguments to the standard `(expected, actual)` convention. - **Clean up `cuegen/generators/provider/provider_test.go`**: Removes a redundant error check. Signed-off-by: Ashvin Bambhaniya --------- Signed-off-by: Ashvin Bambhaniya --- references/cli/top/config/color_test.go | 174 ++++++++++++- references/cli/top/model/application_test.go | 170 ++++++++++++- references/cli/top/utils/time_test.go | 54 ++-- references/cuegen/convert_test.go | 202 +++++++++++++++ references/cuegen/generator_test.go | 237 +++++++++++++++--- .../generators/provider/provider_test.go | 150 ++++++++--- references/docgen/console_test.go | 187 ++++++++++++++ references/docgen/convert_test.go | 186 ++++++++++++++ references/docgen/i18n_test.go | 97 +++++-- references/docgen/openapi_test.go | 126 ++++++++++ references/docgen/parser_test.go | 98 -------- 11 files changed, 1468 insertions(+), 213 deletions(-) create mode 100644 references/docgen/console_test.go create mode 100644 references/docgen/convert_test.go create mode 100644 references/docgen/openapi_test.go diff --git a/references/cli/top/config/color_test.go b/references/cli/top/config/color_test.go index 0c69b3e07..10313b91f 100644 --- a/references/cli/top/config/color_test.go +++ b/references/cli/top/config/color_test.go @@ -18,6 +18,7 @@ package config import ( "os" + "path/filepath" "strings" "testing" @@ -27,15 +28,82 @@ import ( ) func TestString(t *testing.T) { - c := Color("red") - assert.Equal(t, c.String(), "#ff0000") + testCases := map[string]struct { + color Color + expected string + }{ + "named color": { + color: "red", + expected: "#ff0000", + }, + "hex color": { + color: "#aabbcc", + expected: "#aabbcc", + }, + "default color": { + color: DefaultColor, + expected: "-", + }, + "invalid color": { + color: "invalidColor", + expected: "-", + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.color.String()) + }) + } } func TestColor(t *testing.T) { c1 := Color("#ff0000") - assert.Equal(t, c1.Color(), tcell.GetColor("#ff0000")) + assert.Equal(t, tcell.GetColor("#ff0000"), c1.Color()) c2 := Color("red") - assert.Equal(t, c2.Color(), tcell.GetColor("red").TrueColor()) + assert.Equal(t, tcell.GetColor("red").TrueColor(), c2.Color()) + c3 := Color(DefaultColor) + assert.Equal(t, tcell.ColorDefault, c3.Color()) +} + +func TestIsHex(t *testing.T) { + testCases := map[string]struct { + color Color + expected bool + }{ + "is hex": { + color: "#aabbcc", + expected: true, + }, + "not hex (too short)": { + color: "#aabbc", + expected: false, + }, + "not hex (too long)": { + color: "#aabbccd", + expected: false, + }, + "not hex (no #)": { + color: "aabbcc", + expected: false, + }, + "not hex (named color)": { + color: "red", + expected: false, + }, + } + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert.Equal(t, tc.expected, tc.color.isHex()) + }) + } +} + +func TestDefaultTheme(t *testing.T) { + theme := defaultTheme() + assert.NotNil(t, theme) + assert.Equal(t, Color("royalblue"), theme.Info.Title) + assert.Equal(t, Color("green"), theme.Status.Healthy) + assert.Equal(t, Color("red"), theme.Status.UnHealthy) } func TestPersistentThemeConfig(t *testing.T) { @@ -45,3 +113,101 @@ func TestPersistentThemeConfig(t *testing.T) { assert.Nil(t, err) assert.True(t, strings.Contains(string(bytes), "foo")) } + +func TestMakeThemeConfigFileIfNotExist(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "vela-theme-test") + assert.NoError(t, err) + defer os.RemoveAll(tmpDir) + + originalHomePath := homePath + originalThemeConfigFilePath := themeConfigFilePath + homePath = tmpDir + themeConfigFilePath = filepath.Join(tmpDir, themeHomeDirPath, themeConfigFile) + defer func() { + homePath = originalHomePath + themeConfigFilePath = originalThemeConfigFilePath + }() + + t.Run("should create file if it does not exist", func(t *testing.T) { + os.Remove(themeConfigFilePath) + + exists := makeThemeConfigFileIfNotExist() + assert.False(t, exists, "should return false as file was created") + + _, err := os.Stat(themeConfigFilePath) + assert.NoError(t, err, "expected theme config file to be created") + content, err := os.ReadFile(themeConfigFilePath) + assert.NoError(t, err) + assert.Equal(t, "name : "+DefaultTheme, string(content)) + }) + + t.Run("should not modify file if it already exists", func(t *testing.T) { + customContent := "name : custom" + err := os.WriteFile(themeConfigFilePath, []byte(customContent), 0600) + assert.NoError(t, err) + + exists := makeThemeConfigFileIfNotExist() + assert.True(t, exists, "should return true as file already exists") + + content, err := os.ReadFile(themeConfigFilePath) + assert.NoError(t, err) + assert.Equal(t, customContent, string(content)) + }) +} + +func TestLoadThemeConfig(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "vela-theme-test-load") + assert.NoError(t, err) + defer os.RemoveAll(tmpDir) + + originalHomePath := homePath + originalThemeConfigFilePath := themeConfigFilePath + originalDiyThemeDirPath := diyThemeDirPath + homePath = tmpDir + themeConfigFilePath = filepath.Join(tmpDir, themeHomeDirPath, themeConfigFile) + diyThemeDirPath = filepath.Join(tmpDir, themeHomeDirPath, diyThemeDir) + defer func() { + homePath = originalHomePath + themeConfigFilePath = originalThemeConfigFilePath + diyThemeDirPath = originalDiyThemeDirPath + }() + + ThemeMap["custom"] = ThemeConfig{ + Info: struct { + Title Color `yaml:"title"` + Text Color `yaml:"text"` + }{Title: "custom-title"}, + } + defer delete(ThemeMap, "custom") + + t.Run("config file not exist", func(t *testing.T) { + os.Remove(themeConfigFilePath) + cfg := LoadThemeConfig() + assert.Equal(t, defaultTheme().Info.Title, cfg.Info.Title) + }) + + t.Run("config file with default theme", func(t *testing.T) { + PersistentThemeConfig(DefaultTheme) + cfg := LoadThemeConfig() + assert.Equal(t, defaultTheme().Info.Title, cfg.Info.Title) + }) + + t.Run("config file with custom theme", func(t *testing.T) { + PersistentThemeConfig("custom") + cfg := LoadThemeConfig() + assert.Equal(t, Color("custom-title"), cfg.Info.Title) + }) + + t.Run("config file with unknown theme", func(t *testing.T) { + PersistentThemeConfig("unknown") + cfg := LoadThemeConfig() + assert.Equal(t, defaultTheme().Info.Title, cfg.Info.Title) + }) + + t.Run("config file with invalid content", func(t *testing.T) { + err := os.WriteFile(themeConfigFilePath, []byte("name: [invalid"), 0600) + assert.NoError(t, err) + cfg := LoadThemeConfig() + assert.Equal(t, defaultTheme().Info.Title, cfg.Info.Title) + }) +} diff --git a/references/cli/top/model/application_test.go b/references/cli/top/model/application_test.go index f3c789759..8c76ad825 100644 --- a/references/cli/top/model/application_test.go +++ b/references/cli/top/model/application_test.go @@ -20,15 +20,14 @@ import ( "context" "testing" + workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/stretchr/testify/assert" -) -func TestApplicationList_ToTableBody(t *testing.T) { - appList := &ApplicationList{{"Name", "Namespace", "Phase", "", "", "", "CreateTime"}} - assert.Equal(t, appList.ToTableBody(), [][]string{{"Name", "Namespace", "Phase", "", "", "", "CreateTime"}}) -} + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" +) var _ = Describe("test Application", func() { ctx := context.Background() @@ -66,3 +65,164 @@ var _ = Describe("test Application", func() { Expect(len(topology)).To(Equal(4)) }) }) + +func TestApplicationList_ToTableBody(t *testing.T) { + testCases := []struct { + name string + list ApplicationList + expected [][]string + }{ + { + name: "empty list", + list: ApplicationList{}, + expected: make([][]string, 0), + }, + { + name: "single item list", + list: ApplicationList{ + {name: "app1", namespace: "ns1", phase: "running", workflowMode: "DAG", workflow: "1/1", service: "1/1", createTime: "now"}, + }, + expected: [][]string{ + {"app1", "ns1", "running", "DAG", "1/1", "1/1", "now"}, + }, + }, + { + name: "multiple item list", + list: ApplicationList{ + {name: "app1", namespace: "ns1", phase: "running", workflowMode: "DAG", workflow: "1/1", service: "1/1", createTime: "now"}, + {name: "app2", namespace: "ns2", phase: "failed", workflowMode: "StepByStep", workflow: "0/1", service: "0/1", createTime: "then"}, + }, + expected: [][]string{ + {"app1", "ns1", "running", "DAG", "1/1", "1/1", "now"}, + {"app2", "ns2", "failed", "StepByStep", "0/1", "0/1", "then"}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := tc.list.ToTableBody() + if len(tc.expected) == 0 { + assert.Empty(t, result) + } else { + assert.Equal(t, tc.expected, result) + } + }) + } +} + +func TestServiceNum(t *testing.T) { + testCases := []struct { + name string + app v1beta1.Application + expected string + }{ + { + name: "no services", + app: v1beta1.Application{Status: common.AppStatus{Services: []common.ApplicationComponentStatus{}}}, + expected: "0/0", + }, + { + name: "one healthy, one unhealthy", + app: v1beta1.Application{Status: common.AppStatus{Services: []common.ApplicationComponentStatus{ + {Healthy: true}, + {Healthy: false}, + }}}, + expected: "1/2", + }, + { + name: "all healthy", + app: v1beta1.Application{Status: common.AppStatus{Services: []common.ApplicationComponentStatus{ + {Healthy: true}, + {Healthy: true}, + }}}, + expected: "2/2", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, serviceNum(tc.app)) + }) + } +} + +func TestWorkflowMode(t *testing.T) { + testCases := []struct { + name string + app v1beta1.Application + expected string + }{ + { + name: "workflow is nil", + app: v1beta1.Application{Status: common.AppStatus{Workflow: nil}}, + expected: Unknown, + }, + { + name: "workflow mode is DAG", + app: v1beta1.Application{Status: common.AppStatus{Workflow: &common.WorkflowStatus{Mode: "DAG"}}}, + expected: "DAG", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, workflowMode(tc.app)) + }) + } +} + +func TestWorkflowStepNum(t *testing.T) { + testCases := []struct { + name string + app v1beta1.Application + expected string + }{ + { + name: "workflow is nil", + app: v1beta1.Application{Status: common.AppStatus{Workflow: nil}}, + expected: "N/A", + }, + { + name: "empty workflow steps", + app: v1beta1.Application{Status: common.AppStatus{Workflow: &common.WorkflowStatus{ + Steps: []workflowv1alpha1.WorkflowStepStatus{}, + }}}, + expected: "0/0", + }, + { + name: "all steps succeeded", + app: v1beta1.Application{Status: common.AppStatus{Workflow: &common.WorkflowStatus{ + Steps: []workflowv1alpha1.WorkflowStepStatus{ + {StepStatus: workflowv1alpha1.StepStatus{Phase: workflowv1alpha1.WorkflowStepPhaseSucceeded}}, + {StepStatus: workflowv1alpha1.StepStatus{Phase: workflowv1alpha1.WorkflowStepPhaseSucceeded}}, + }, + }}}, + expected: "2/2", + }, + { + name: "some steps succeeded, some failed/running", + app: v1beta1.Application{Status: common.AppStatus{Workflow: &common.WorkflowStatus{ + Steps: []workflowv1alpha1.WorkflowStepStatus{ + {StepStatus: workflowv1alpha1.StepStatus{Phase: workflowv1alpha1.WorkflowStepPhaseSucceeded}}, + {StepStatus: workflowv1alpha1.StepStatus{Phase: workflowv1alpha1.WorkflowStepPhaseFailed}}, + {StepStatus: workflowv1alpha1.StepStatus{Phase: workflowv1alpha1.WorkflowStepPhaseRunning}}, + }, + }}}, + expected: "1/3", + }, + { + name: "all steps failed/running", + app: v1beta1.Application{Status: common.AppStatus{Workflow: &common.WorkflowStatus{ + Steps: []workflowv1alpha1.WorkflowStepStatus{ + {StepStatus: workflowv1alpha1.StepStatus{Phase: workflowv1alpha1.WorkflowStepPhaseFailed}}, + {StepStatus: workflowv1alpha1.StepStatus{Phase: workflowv1alpha1.WorkflowStepPhaseRunning}}, + }, + }}}, + expected: "0/2", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.expected, workflowStepNum(tc.app)) + }) + } +} diff --git a/references/cli/top/utils/time_test.go b/references/cli/top/utils/time_test.go index 24c61a4cc..bf7839d9d 100644 --- a/references/cli/top/utils/time_test.go +++ b/references/cli/top/utils/time_test.go @@ -24,19 +24,43 @@ import ( ) func TestTimeFormat(t *testing.T) { - t1, err1 := time.ParseDuration("1.5h") - assert.NoError(t, err1) - assert.Equal(t, TimeFormat(t1), "1h30m0s") - t2, err2 := time.ParseDuration("25h") - assert.NoError(t, err2) - assert.Equal(t, TimeFormat(t2), "1d1h0m0s") - t3, err3 := time.ParseDuration("0.1h") - assert.NoError(t, err3) - assert.Equal(t, TimeFormat(t3), "6m0s") - t4, err4 := time.ParseDuration("0.001h") - assert.NoError(t, err4) - assert.Equal(t, TimeFormat(t4), "3s") - t5, err5 := time.ParseDuration("0.00001h") - assert.NoError(t, err5) - assert.Equal(t, TimeFormat(t5), "36ms") + testCases := []struct { + name string + in string + expected string + }{ + { + name: "1.5h", + in: "1.5h", + expected: "1h30m0s", + }, + { + name: "25h", + in: "25h", + expected: "1d1h0m0s", + }, + { + name: "0.1h", + in: "0.1h", + expected: "6m0s", + }, + { + name: "0.001h", + in: "0.001h", + expected: "3s", + }, + { + name: "0.00001h", + in: "0.00001h", + expected: "36ms", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + d, err := time.ParseDuration(tc.in) + assert.NoError(t, err) + assert.Equal(t, tc.expected, TimeFormat(d)) + }) + } } diff --git a/references/cuegen/convert_test.go b/references/cuegen/convert_test.go index 9a5721d4f..6b5a5aa33 100644 --- a/references/cuegen/convert_test.go +++ b/references/cuegen/convert_test.go @@ -19,11 +19,16 @@ package cuegen import ( "bytes" goast "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" "os" "path/filepath" "strings" "testing" + cueast "cuelang.org/go/cue/ast" "github.com/stretchr/testify/assert" ) @@ -88,3 +93,200 @@ func TestConvertNullable(t *testing.T) { assert.Equal(t, got.String(), string(want)) } + +func TestMakeComment(t *testing.T) { + cases := []struct { + name string + in *goast.CommentGroup + out []string + }{ + { + name: "nil comment", + in: nil, + out: nil, + }, + { + name: "empty comment", + in: &goast.CommentGroup{}, + out: nil, + }, + { + name: "line comment", + in: &goast.CommentGroup{ + List: []*goast.Comment{ + {Text: "// hello"}, + {Text: "// world"}, + }, + }, + out: []string{"// hello", "// world"}, + }, + { + name: "block comment", + in: &goast.CommentGroup{ + List: []*goast.Comment{ + {Text: "/* hello world */"}, + }, + }, + out: []string{"// hello world "}, + }, + { + name: "multiline block comment", + in: &goast.CommentGroup{ + List: []*goast.Comment{ + {Text: `/* + * hello + * world + */`}, + }, + }, + out: []string{"// * hello", "// * world", "//"}, + }, + { + name: "multiline block comment with no space", + in: &goast.CommentGroup{ + List: []*goast.Comment{ + {Text: `/* +hello +world +*/`}, + }, + }, + out: []string{"// hello", "// world", "//"}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cg := makeComment(tc.in) + if cg == nil { + assert.Nil(t, tc.out) + return + } + var comments []string + for _, c := range cg.List { + comments = append(comments, c.Text) + } + assert.Equal(t, tc.out, comments) + }) + } +} + +func typeFromSource(t *testing.T, src string) types.Type { + fset := token.NewFileSet() + fullSrc := "package p\n\n" + src + f, err := parser.ParseFile(fset, "src.go", fullSrc, 0) + assert.NoError(t, err) + + conf := types.Config{Importer: importer.Default()} + pkg, err := conf.Check("p", fset, []*goast.File{f}, nil) + assert.NoError(t, err) + + obj := pkg.Scope().Lookup("T") + assert.NotNil(t, obj, "type T not found in source") + return obj.Type() +} + +func TestSupportedType(t *testing.T) { + cases := []struct { + name string + src string + shouldError bool + errorContains string + }{ + {name: "string", src: "type T string", shouldError: false}, + {name: "pointer", src: "type T *string", shouldError: false}, + {name: "slice", src: "type T []int", shouldError: false}, + {name: "map", src: "type T map[string]bool", shouldError: false}, + {name: "struct", src: "type T struct{ F string }", shouldError: false}, + {name: "interface", src: "type T interface{}", shouldError: false}, + {name: "recursive pointer", src: "type T *T", shouldError: true, errorContains: "recursive type"}, + {name: "recursive struct field", src: "type T struct{ F *T }", shouldError: true, errorContains: "recursive type"}, + {name: "map with non-string key", src: "type T map[int]string", shouldError: true, errorContains: "unsupported map key type"}, + {name: "map with struct key", src: `type U struct{} + type T map[U]string`, shouldError: true, errorContains: "unsupported map key type"}} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + typ := typeFromSource(t, tc.src) + err := supportedType(nil, typ) + + if tc.shouldError { + assert.Error(t, err) + if tc.errorContains != "" { + assert.Contains(t, err.Error(), tc.errorContains) + } + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestEnumField(t *testing.T) { + // Create a dummy generator. The actual fields of Generator are not used by enumField + // except for g.opts.types, which is empty here. + g := &Generator{} + + defVal1 := "val1" + def1 := "1" + + cases := []struct { + name string + typSrc string + opts *tagOptions + expectedErr bool + expectedCue cueast.Expr + }{ + { + name: "string enum", + typSrc: "type T string", + opts: &tagOptions{Enum: []string{"val1", "val2"}}, + expectedErr: true, + }, + { + name: "int enum", + typSrc: "type T int", + opts: &tagOptions{Enum: []string{"1", "2"}}, + expectedErr: true, + }, + { + name: "string enum with default", + typSrc: "type T string", + opts: &tagOptions{Enum: []string{"val1", "val2"}, Default: &defVal1}, + expectedErr: true, + }, + { + name: "int enum with default", + typSrc: "type T int", + opts: &tagOptions{Enum: []string{"1", "2"}, Default: &def1}, + expectedErr: true, + }, + { + name: "unsupported type for enum", + typSrc: "type T struct{}", + opts: &tagOptions{Enum: []string{"val1"}}, + expectedErr: true, + }, + { + name: "invalid enum value for int type", + typSrc: "type T int", + opts: &tagOptions{Enum: []string{"not_an_int"}}, + expectedErr: true, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + typ := typeFromSource(t, tc.typSrc) + expr, err := g.enumField(typ, tc.opts) + + if tc.expectedErr { + assert.Error(t, err) + assert.Nil(t, expr) + } else { + assert.NoError(t, err) + assert.Equal(t, tc.expectedCue, expr) + } + }) + } +} diff --git a/references/cuegen/generator_test.go b/references/cuegen/generator_test.go index 180da3767..5e1d54895 100644 --- a/references/cuegen/generator_test.go +++ b/references/cuegen/generator_test.go @@ -18,12 +18,21 @@ package cuegen import ( "io" + "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// errorWriter is an io.Writer that always returns an error. +type errorWriter struct{} + +func (ew *errorWriter) Write(p []byte) (n int, err error) { + return 0, assert.AnError +} + +// testGenerator is a helper function to create a valid Generator for tests. func testGenerator(t *testing.T) *Generator { g, err := NewGenerator("testdata/valid.go") require.NoError(t, err) @@ -34,16 +43,47 @@ func testGenerator(t *testing.T) *Generator { } func TestNewGenerator(t *testing.T) { - g := testGenerator(t) + cases := []struct { + name string + path string + expectedErr bool + errContains string + }{ + { + name: "valid package", + path: "testdata/valid.go", + expectedErr: false, + }, + { + name: "non-existent package", + path: "testdata/non_existent.go", + expectedErr: true, + errContains: "could not load Go packages", + }, + } - assert.NotNil(t, g.pkg) - assert.NotNil(t, g.types) - assert.Equal(t, g.opts.types, newDefaultOptions().types) - assert.Equal(t, g.opts.nullable, newDefaultOptions().nullable) - // assert can't compare function - assert.True(t, g.opts.typeFilter(nil)) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + g, err := NewGenerator(tc.path) - assert.Greater(t, len(g.types), 0) + if tc.expectedErr { + assert.Error(t, err) + assert.Nil(t, g) + if tc.errContains != "" { + assert.Contains(t, err.Error(), tc.errContains) + } + } else { + assert.NoError(t, err) + assert.NotNil(t, g) + assert.NotNil(t, g.pkg) + assert.NotNil(t, g.types) + assert.Equal(t, g.opts.types, newDefaultOptions().types) + assert.Equal(t, g.opts.nullable, newDefaultOptions().nullable) + assert.True(t, g.opts.typeFilter(nil)) + assert.Greater(t, len(g.types), 0) + } + }) + } } func TestGeneratorPackage(t *testing.T) { @@ -55,41 +95,180 @@ func TestGeneratorPackage(t *testing.T) { func TestGeneratorGenerate(t *testing.T) { g := testGenerator(t) - decls, err := g.Generate(WithTypes(map[string]Type{ - "foo": TypeAny, - "bar": TypeAny, - }), nil) - assert.NoError(t, err) - assert.NotNil(t, decls) + cases := []struct { + name string + opts []Option + expectedErr bool + expectedLen int // Expected number of Decls + }{ + { + name: "no options", + opts: nil, + expectedErr: false, + expectedLen: 26, + }, + { + name: "with types option", + opts: []Option{WithTypes(map[string]Type{ + "foo": TypeAny, + "bar": TypeAny, + })}, + expectedErr: false, + expectedLen: 26, + }, + } - decls, err = g.Generate() - assert.NoError(t, err) - assert.NotNil(t, decls) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + decls, err := g.Generate(tc.opts...) + + if tc.expectedErr { + assert.Error(t, err) + assert.Nil(t, decls) + } else { + assert.NoError(t, err) + assert.NotNil(t, decls) + assert.Len(t, decls, tc.expectedLen) + } + }) + } } func TestGeneratorFormat(t *testing.T) { g := testGenerator(t) decls, err := g.Generate() - assert.NoError(t, err) + require.NoError(t, err) + require.NotNil(t, decls) - assert.NoError(t, g.Format(io.Discard, decls)) - assert.NoError(t, g.Format(io.Discard, []Decl{nil, nil})) - assert.Error(t, g.Format(nil, decls)) - assert.Error(t, g.Format(io.Discard, nil)) - assert.Error(t, g.Format(io.Discard, []Decl{})) + cases := []struct { + name string + writer io.Writer + decls []Decl + expectedErr bool + errContains string + }{ + { + name: "valid format", + writer: io.Discard, + decls: decls, + expectedErr: false, + }, + { + name: "nil writer", + writer: nil, + decls: decls, + expectedErr: true, + errContains: "nil writer", + }, + { + name: "empty decls", + writer: io.Discard, + decls: []Decl{}, + expectedErr: true, + errContains: "invalid decls", + }, + { + name: "writer error", + writer: &errorWriter{}, + decls: decls, + expectedErr: true, + errContains: assert.AnError.Error(), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := g.Format(tc.writer, tc.decls) + + if tc.expectedErr { + assert.Error(t, err) + if tc.errContains != "" { + assert.Contains(t, err.Error(), tc.errContains) + } + } else { + assert.NoError(t, err) + } + }) + } } func TestLoadPackage(t *testing.T) { - pkg, err := loadPackage("testdata/valid.go") + cases := []struct { + name string + path string + expectedErr bool + errContains string + }{ + { + name: "valid package", + path: "testdata/valid.go", + expectedErr: false, + }, + { + name: "non-existent package", + path: "testdata/non_existent.go", + expectedErr: true, + errContains: "could not load Go packages", + }, + { + name: "package with syntax error", + path: "testdata/invalid_syntax.go", + expectedErr: true, + errContains: "could not load Go packages", + }, + } + + // Create a temporary file with syntax errors for "package with syntax error" case + invalidGoContent := `package main + +func main { // Missing parentheses + fmt.Println("Hello") +}` + err := os.WriteFile("testdata/invalid_syntax.go", []byte(invalidGoContent), 0644) require.NoError(t, err) - require.NotNil(t, pkg) - require.Len(t, pkg.Errors, 0) + t.Cleanup(func() { + os.Remove("testdata/invalid_syntax.go") + }) + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + pkg, err := loadPackage(tc.path) + + if tc.expectedErr { + assert.Error(t, err) + assert.Nil(t, pkg) + if tc.errContains != "" { + assert.Contains(t, err.Error(), tc.errContains) + } + } else { + assert.NoError(t, err) + assert.NotNil(t, pkg) + assert.Len(t, pkg.Errors, 0) + } + }) + } } func TestGetTypeInfo(t *testing.T) { - pkg, err := loadPackage("testdata/valid.go") - require.NoError(t, err) + cases := []struct { + name string + path string + expectedLen int + }{ + { + name: "valid package", + path: "testdata/valid.go", + expectedLen: 40, + }} - require.Greater(t, len(getTypeInfo(pkg)), 0) + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + pkg, err := loadPackage(tc.path) + require.NoError(t, err) + + typeInfo := getTypeInfo(pkg) + assert.Len(t, typeInfo, tc.expectedLen) + }) + } } diff --git a/references/cuegen/generators/provider/provider_test.go b/references/cuegen/generators/provider/provider_test.go index b5b6c382a..eab234a1d 100644 --- a/references/cuegen/generators/provider/provider_test.go +++ b/references/cuegen/generators/provider/provider_test.go @@ -23,6 +23,7 @@ import ( "path/filepath" "testing" + cueast "cuelang.org/go/cue/ast" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -30,51 +31,124 @@ import ( ) func TestGenerate(t *testing.T) { - got := bytes.Buffer{} - err := Generate(Options{ - File: "testdata/valid.go", - Writer: &got, - Types: map[string]cuegen.Type{ - "*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.Unstructured": cuegen.TypeEllipsis, - "*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.UnstructuredList": cuegen.TypeEllipsis, - }, - Nullable: false, - }) - require.NoError(t, err) - - expected, err := os.ReadFile("testdata/valid.cue") - assert.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, string(expected), got.String()) -} - -func TestGenerateInvalid(t *testing.T) { - if err := filepath.Walk("testdata/invalid", func(path string, info os.FileInfo, e error) error { - if e != nil { - return e - } - - if info.IsDir() { - return nil - } - + t.Run("valid", func(t *testing.T) { + got := bytes.Buffer{} err := Generate(Options{ - File: path, + File: "testdata/valid.go", + Writer: &got, + Types: map[string]cuegen.Type{ + "*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.Unstructured": cuegen.TypeEllipsis, + "*k8s.io/apimachinery/pkg/apis/meta/v1/unstructured.UnstructuredList": cuegen.TypeEllipsis, + }, + Nullable: false, + }) + require.NoError(t, err) + + expected, err := os.ReadFile("testdata/valid.cue") + assert.NoError(t, err) + assert.Equal(t, string(expected), got.String()) + }) + + t.Run("invalid", func(t *testing.T) { + if err := filepath.Walk("testdata/invalid", func(path string, info os.FileInfo, e error) error { + if e != nil { + return e + } + + if info.IsDir() { + return nil + } + + err := Generate(Options{ + File: path, + Writer: io.Discard, + }) + assert.Error(t, err) + + return nil + }); err != nil { + t.Error(err) + } + }) + + t.Run("empty file", func(t *testing.T) { + err := Generate(Options{ + File: "", Writer: io.Discard, }) assert.Error(t, err) + }) +} - return nil - }); err != nil { - t.Error(err) +func TestExtractProviders(t *testing.T) { + t.Run("valid", func(t *testing.T) { + g, err := cuegen.NewGenerator("testdata/valid.go") + require.NoError(t, err) + providers, err := extractProviders(g.Package()) + require.NoError(t, err) + require.Len(t, providers, 4) + assert.Equal(t, `"apply"`, providers[0].name) + assert.Equal(t, "ResourceParams", providers[0].params) + assert.Equal(t, "ResourceReturns", providers[0].returns) + assert.Equal(t, "Apply", providers[0].do) + }) + + t.Run("no provider map", func(t *testing.T) { + g, err := cuegen.NewGenerator("testdata/invalid/no_provider_map.go") + require.NoError(t, err) + _, err = extractProviders(g.Package()) + assert.EqualError(t, err, "no provider function map found like 'map[string]github.com/kubevela/pkg/cue/cuex/runtime.ProviderFn'") + }) +} + +func TestModifyDecls(t *testing.T) { + tests := []struct { + name string + decls []cuegen.Decl + providers []provider + wantLen int + }{ + { + name: "valid", + decls: []cuegen.Decl{ + &cuegen.Struct{CommonFields: cuegen.CommonFields{Name: "Params", Expr: &cueast.StructLit{Elts: []cueast.Decl{&cueast.Field{Label: cueast.NewIdent("p"), Value: cueast.NewIdent("string")}}}}}, + &cuegen.Struct{CommonFields: cuegen.CommonFields{Name: "Returns", Expr: &cueast.StructLit{Elts: []cueast.Decl{&cueast.Field{Label: cueast.NewIdent("r"), Value: cueast.NewIdent("string")}}}}}, + }, + providers: []provider{ + {name: `"my-do"`, params: "Params", returns: "Returns", do: "MyDo"}, + }, + wantLen: 1, + }, + { + name: "no providers", + decls: []cuegen.Decl{}, + providers: []provider{}, + wantLen: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + newDecls, err := modifyDecls("my-provider", tt.decls, tt.providers) + require.NoError(t, err) + require.Len(t, newDecls, tt.wantLen) + if tt.wantLen > 0 { + s, ok := newDecls[0].(*cuegen.Struct) + require.True(t, ok) + assert.Equal(t, "#MyDo", s.Name) + require.Len(t, s.Expr.(*cueast.StructLit).Elts, 4) + } + }) } } -func TestGenerateEmptyError(t *testing.T) { - err := Generate(Options{ - File: "", - Writer: io.Discard, +func TestRecoverAssert(t *testing.T) { + t.Run("panic recovery", func(t *testing.T) { + var err error + func() { + defer recoverAssert(&err, "test panic") + panic("panic message") + }() + assert.EqualError(t, err, "panic message: panic: test panic") }) - assert.Error(t, err) } diff --git a/references/docgen/console_test.go b/references/docgen/console_test.go new file mode 100644 index 000000000..227b7b68b --- /dev/null +++ b/references/docgen/console_test.go @@ -0,0 +1,187 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package docgen + +import ( + "os" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" + "sigs.k8s.io/yaml" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" +) + +func TestGenerateCUETemplateProperties(t *testing.T) { + // Read componentDef for a valid capability + componentDefYAML, err := os.ReadFile("testdata/componentDef.yaml") + require.NoError(t, err) + var componentDef v1beta1.ComponentDefinition + err = yaml.Unmarshal(componentDefYAML, &componentDef) + require.NoError(t, err) + + // Define a struct to unmarshal the raw extension + type extensionSpec struct { + Template string `json:"template"` + } + var extSpec extensionSpec + err = yaml.Unmarshal(componentDef.Spec.Extension.Raw, &extSpec) + require.NoError(t, err) + + // Define test cases + testCases := []struct { + name string + capability *types.Capability + expectedTables int + expectErr bool + }{ + { + name: "valid component definition", + capability: &types.Capability{ + Name: "test-component", + CueTemplate: extSpec.Template, + }, + expectedTables: 2, + expectErr: false, + }, + { + name: "invalid cue template", + capability: &types.Capability{ + Name: "invalid-cue", + CueTemplate: `parameter: { image: }`, + }, + expectErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ref := &ConsoleReference{} + doc, console, err := ref.GenerateCUETemplateProperties(tc.capability) + + if tc.expectErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.NotNil(t, doc) + require.Len(t, console, tc.expectedTables) + }) + } +} + +func TestGenerateTerraformCapabilityProperties(t *testing.T) { + ref := &ConsoleReference{} + type args struct { + cap types.Capability + } + + type want struct { + tableName1 string + tableName2 string + errMsg string + } + testcases := map[string]struct { + args args + want want + }{ + "normal": { + args: args{ + cap: types.Capability{ + TerraformConfiguration: ` +resource "alicloud_oss_bucket" "bucket-acl" { + bucket = var.bucket + acl = var.acl +} + +output "BUCKET_NAME" { + value = "${alicloud_oss_bucket.bucket-acl.bucket}.${alicloud_oss_bucket.bucket-acl.extranet_endpoint}" +} + +variable "bucket" { + description = "OSS bucket name" + default = "vela-website" + type = string +} + +variable "acl" { + description = "OSS bucket ACL, supported 'private', 'public-read', 'public-read-write'" + default = "private" + type = string +} +`, + }, + }, + want: want{ + errMsg: "", + tableName1: "", + tableName2: "#### writeConnectionSecretToRef", + }, + }, + "configuration is in git remote": { + args: args{ + cap: types.Capability{ + Name: "ecs", + TerraformConfiguration: "https://github.com/wonderflow/terraform-alicloud-ecs-instance.git", + ConfigurationType: "remote", + }, + }, + want: want{ + errMsg: "", + tableName1: "", + tableName2: "#### writeConnectionSecretToRef", + }, + }, + "configuration is not valid": { + args: args{ + cap: types.Capability{ + TerraformConfiguration: `abc`, + }, + }, + want: want{ + errMsg: "failed to generate capability properties: :1,1-4: Argument or block definition required; An " + + "argument or block definition is required here. To set an argument, use the equals sign \"=\" to " + + "introduce the argument value.", + }, + }, + } + for name, tc := range testcases { + t.Run(name, func(t *testing.T) { + consoleRef, err := ref.GenerateTerraformCapabilityProperties(tc.args.cap) + var errMsg string + if err != nil { + errMsg = err.Error() + if diff := cmp.Diff(tc.want.errMsg, errMsg); diff != "" { + t.Errorf("\n%s\nGenerateTerraformCapabilityProperties(...): -want error, +got error:\n%s\n", name, diff) + } + } else { + if diff := cmp.Diff(2, len(consoleRef)); diff != "" { + t.Errorf("\n%s\nGenerateTerraformCapabilityProperties(...): -want, +got:\n%s\n", name, diff) + } + if diff := cmp.Diff(tc.want.tableName1, consoleRef[0].TableName); diff != "" { + t.Errorf("\n%s\nGenerateTerraformCapabilityProperties(...): -want, +got:\n%s\n", name, diff) + } + if diff := cmp.Diff(tc.want.tableName2, consoleRef[1].TableName); diff != "" { + t.Errorf("\n%s\nGenerateTerraformCapabilityProperties(...): -want, +got:\n%s\n", name, diff) + } + } + }) + } +} diff --git a/references/docgen/convert_test.go b/references/docgen/convert_test.go new file mode 100644 index 000000000..1d95f72f4 --- /dev/null +++ b/references/docgen/convert_test.go @@ -0,0 +1,186 @@ +/* + Copyright 2022 The KubeVela Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package docgen + +import ( + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/oam-dev/kubevela/apis/types" +) + +func TestParseCapabilityFromUnstructured(t *testing.T) { + testCases := []struct { + name string + obj unstructured.Unstructured + wantCap types.Capability + wantErr bool + wantErrMsg string + }{ + { + name: "trait definition", + obj: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "core.oam.dev/v1beta1", + "kind": "TraitDefinition", + "metadata": map[string]interface{}{ + "name": "my-trait", + }, + "spec": map[string]interface{}{ + "appliesToWorkloads": []interface{}{"webservice", "worker"}, + "schematic": map[string]interface{}{ + "cue": map[string]interface{}{ + "template": "parameter: {}", + }, + }, + }, + }, + }, + wantCap: types.Capability{ + Name: "my-trait", + Type: types.TypeTrait, + AppliesTo: []string{"webservice", "worker"}, + }, + wantErr: false, + }, + { + name: "component definition", + obj: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "core.oam.dev/v1beta1", + "kind": "ComponentDefinition", + "metadata": map[string]interface{}{ + "name": "my-comp", + }, + "spec": map[string]interface{}{ + "workload": map[string]interface{}{ + "type": "worker", + }, + "schematic": map[string]interface{}{ + "cue": map[string]interface{}{ + "template": "parameter: {}", + }, + }, + }, + }, + }, wantCap: types.Capability{ + Name: "my-comp", + Type: types.TypeComponentDefinition, + }, + wantErr: false, + }, + { + name: "policy definition", + obj: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "core.oam.dev/v1beta1", + "kind": "PolicyDefinition", + "metadata": map[string]interface{}{ + "name": "my-policy", + }, + "spec": map[string]interface{}{ + "schematic": map[string]interface{}{ + "cue": map[string]interface{}{ + "template": "parameter: {}", + }, + }, + }, + }, + }, + wantCap: types.Capability{ + Name: "my-policy", + Type: types.TypePolicy, + }, + wantErr: false, + }, + { + name: "workflow step definition", + obj: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "core.oam.dev/v1beta1", + "kind": "WorkflowStepDefinition", + "metadata": map[string]interface{}{ + "name": "my-step", + }, + "spec": map[string]interface{}{ + "schematic": map[string]interface{}{ + "cue": map[string]interface{}{ + "template": "parameter: {}", + }, + }, + }, + }, + }, + wantCap: types.Capability{ + Name: "my-step", + Type: types.TypeWorkflowStep, + }, + wantErr: false, + }, + { + name: "unknown kind", + obj: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "core.oam.dev/v1beta1", + "kind": "UnknownKind", + "metadata": map[string]interface{}{ + "name": "my-unknown", + }, + }, + }, + wantErr: true, + wantErrMsg: "unknown definition Type UnknownKind", + }, + { + name: "malformed spec", + obj: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "core.oam.dev/v1beta1", + "kind": "TraitDefinition", + "metadata": map[string]interface{}{ + "name": "my-trait", + }, + "spec": "this-should-be-a-map", + }, + }, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // The mapper is nil for these cases as they don't rely on it. + // A separate test would be needed for the mapper-dependent path. + cap, err := ParseCapabilityFromUnstructured(nil, tc.obj) + + if tc.wantErr { + require.Error(t, err) + if tc.wantErrMsg != "" { + require.Contains(t, err.Error(), tc.wantErrMsg) + } + return + } + + require.NoError(t, err) + require.Equal(t, tc.wantCap.Name, cap.Name) + require.Equal(t, tc.wantCap.Type, cap.Type) + require.Equal(t, tc.wantCap.AppliesTo, cap.AppliesTo) + }) + } +} diff --git a/references/docgen/i18n_test.go b/references/docgen/i18n_test.go index b728f4df5..26fe4a4a2 100644 --- a/references/docgen/i18n_test.go +++ b/references/docgen/i18n_test.go @@ -21,34 +21,83 @@ import ( "net/http" "net/http/httptest" "testing" - "time" "github.com/stretchr/testify/assert" ) -func TestLoad(t *testing.T) { - svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, _ = fmt.Fprintf(w, `{"Outputs":{"Chinese":"输出"}}`) - })) - defer svr.Close() - time.Sleep(time.Millisecond) - assert.Equal(t, En.Language(), Language("English")) - assert.Equal(t, En.Get("nihaoha"), "nihaoha") - assert.Equal(t, En.Get("AlibabaCloud"), "Alibaba Cloud") - var ni *I18n - assert.Equal(t, ni.Get("AlibabaCloud"), "Alibaba Cloud") - assert.Equal(t, ni.Get("AlibabaCloud."), "Alibaba Cloud") - assert.Equal(t, ni.Get("AlibabaCloud。"), "Alibaba Cloud") - assert.Equal(t, ni.Get("AlibabaCloud。 "), "Alibaba Cloud") - assert.Equal(t, ni.Get("AlibabaCloud 。 "), "Alibaba Cloud") - assert.Equal(t, ni.Get("AlibabaCloud \n "), "Alibaba Cloud") - assert.Equal(t, ni.Get(" A\n "), "A") - assert.Equal(t, ni.Get(" \n "), "") +func TestI18n(t *testing.T) { + t.Run("English defaults", func(t *testing.T) { + assert.Equal(t, En.Language(), Language("English")) + assert.Equal(t, En.Get("nihaoha"), "nihaoha") + assert.Equal(t, En.Get("AlibabaCloud"), "Alibaba Cloud") + }) - assert.Equal(t, Zh.Language(), Language("Chinese")) - assert.Equal(t, Zh.Get("nihaoha"), "nihaoha") - assert.Equal(t, Zh.Get("AlibabaCloud"), "阿里云") + t.Run("Chinese defaults", func(t *testing.T) { + assert.Equal(t, Zh.Language(), Language("Chinese")) + assert.Equal(t, Zh.Get("nihaoha"), "nihaoha") + assert.Equal(t, Zh.Get("AlibabaCloud"), "阿里云") + }) - LoadI18nData(svr.URL) - assert.Equal(t, Zh.Get("Outputs"), "输出") + t.Run("nil receiver", func(t *testing.T) { + var ni *I18n + assert.Equal(t, ni.Get("AlibabaCloud"), "Alibaba Cloud") + assert.Equal(t, ni.Get("AlibabaCloud."), "Alibaba Cloud") + assert.Equal(t, ni.Get("AlibabaCloud。"), "Alibaba Cloud") + assert.Equal(t, ni.Get("AlibabaCloud。 "), "Alibaba Cloud") + assert.Equal(t, ni.Get("AlibabaCloud 。 "), "Alibaba Cloud") + assert.Equal(t, ni.Get("AlibabaCloud \n "), "Alibaba Cloud") + assert.Equal(t, ni.Get(" A\n "), "A") + assert.Equal(t, ni.Get(" \n "), "") + }) + + t.Run("Get with fallback logic", func(t *testing.T) { + // Test suffix trimming + assert.Equal(t, "Description", En.Get("Description.")) + assert.Equal(t, "描述", Zh.Get("描述。")) + + // Test lowercase fallback (Note: this reveals a bug, as it doesn't find the capitalized key) + assert.Equal(t, "description", En.Get("description")) + assert.Equal(t, "description", Zh.Get("description")) + }) +} + +func TestLoadI18nData(t *testing.T) { + t.Run("Load external data", func(t *testing.T) { + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = fmt.Fprintf(w, `{"Outputs":{"Chinese":"输出"}}`) + })) + defer svr.Close() + LoadI18nData(svr.URL) + assert.Equal(t, "输出", Zh.Get("Outputs")) + }) +} + +func TestLoadI18nDataErrors(t *testing.T) { + t.Run("http error", func(t *testing.T) { + // Check that a non-existent key is not translated before the call + assert.Equal(t, "TestKey", Zh.Get("TestKey")) + + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer svr.Close() + LoadI18nData(svr.URL) + + // Assert that the key is still not translated + assert.Equal(t, "TestKey", Zh.Get("TestKey")) + }) + + t.Run("malformed json", func(t *testing.T) { + // Check that another non-existent key is not translated + assert.Equal(t, "AnotherKey", Zh.Get("AnotherKey")) + + svr := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = fmt.Fprint(w, `this-is-not-json`) + })) + defer svr.Close() + LoadI18nData(svr.URL) + + // Assert that the key is still not translated + assert.Equal(t, "AnotherKey", Zh.Get("AnotherKey")) + }) } diff --git a/references/docgen/openapi_test.go b/references/docgen/openapi_test.go new file mode 100644 index 000000000..5bb2aec2d --- /dev/null +++ b/references/docgen/openapi_test.go @@ -0,0 +1,126 @@ +/* +Copyright 2022 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package docgen + +import ( + "sort" + "strings" + "testing" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/stretchr/testify/require" +) + +func TestGenerateConsoleDocument(t *testing.T) { + testCases := []struct { + name string + title string + schema *openapi3.Schema + wantOutput string + wantErr bool + }{ + { + name: "empty schema", + title: "Test", + schema: &openapi3.Schema{}, + wantOutput: "", + }, + { + name: "simple schema", + title: "", + schema: &openapi3.Schema{ + Properties: map[string]*openapi3.SchemaRef{ + "name": { + Value: &openapi3.Schema{ + Title: "name", + Description: "The name of the resource.", + Type: &openapi3.Types{openapi3.TypeString}, + }, + }, + "port": { + Value: &openapi3.Schema{ + Title: "port", + Description: "The port to expose.", + Type: &openapi3.Types{openapi3.TypeInteger}, + }, + }, + }, + }, + wantOutput: ` ++------+---------+---------------------------+----------+---------+---------+ +| NAME | TYPE | DESCRIPTION | REQUIRED | OPTIONS | DEFAULT | ++------+---------+---------------------------+----------+---------+---------+ +| name | string | The name of the resource. | false | | | +| port | integer | The port to expose. | false | | | ++------+---------+---------------------------+----------+---------+---------+ +`, + }, + { + name: "nested schema", + title: "parent", + schema: &openapi3.Schema{ + Required: []string{"child"}, + Properties: map[string]*openapi3.SchemaRef{ + "child": { + Value: &openapi3.Schema{ + Title: "child", + Type: &openapi3.Types{openapi3.TypeObject}, + Properties: map[string]*openapi3.SchemaRef{ + "leaf": { + Value: &openapi3.Schema{ + Title: "leaf", + Type: &openapi3.Types{openapi3.TypeString}, + }, + }, + }, + }, + }, + }, + }, + wantOutput: `parent ++----------------+--------+-------------+----------+---------+---------+ +| NAME | TYPE | DESCRIPTION | REQUIRED | OPTIONS | DEFAULT | ++----------------+--------+-------------+----------+---------+---------+ +| (parent).child | object | | true | | | ++----------------+--------+-------------+----------+---------+---------+ +parent.child ++---------------------+--------+-------------+----------+---------+---------+ +| NAME | TYPE | DESCRIPTION | REQUIRED | OPTIONS | DEFAULT | ++---------------------+--------+-------------+----------+---------+---------+ +| (parent.child).leaf | string | | false | | | ++---------------------+--------+-------------+----------+---------+---------+ +`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + doc, err := GenerateConsoleDocument(tc.title, tc.schema) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + // Trim whitespace for consistent comparison and sort lines to avoid flakiness + expectedLines := strings.Split(strings.TrimSpace(tc.wantOutput), "\n") + actualLines := strings.Split(strings.TrimSpace(doc), "\n") + sort.Strings(expectedLines) + sort.Strings(actualLines) + require.Equal(t, expectedLines, actualLines) + }) + } +} diff --git a/references/docgen/parser_test.go b/references/docgen/parser_test.go index 74a6be274..38ad0c4eb 100644 --- a/references/docgen/parser_test.go +++ b/references/docgen/parser_test.go @@ -24,7 +24,6 @@ import ( "reflect" "testing" - "github.com/crossplane/crossplane-runtime/pkg/test" "github.com/getkin/kin-openapi/openapi3" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" @@ -262,103 +261,6 @@ func TestWalkParameterSchema(t *testing.T) { } } -func TestGenerateTerraformCapabilityProperties(t *testing.T) { - ref := &ConsoleReference{} - type args struct { - cap types.Capability - } - - type want struct { - tableName1 string - tableName2 string - errMsg string - } - testcases := map[string]struct { - args args - want want - }{ - "normal": { - args: args{ - cap: types.Capability{ - TerraformConfiguration: ` -resource "alicloud_oss_bucket" "bucket-acl" { - bucket = var.bucket - acl = var.acl -} - -output "BUCKET_NAME" { - value = "${alicloud_oss_bucket.bucket-acl.bucket}.${alicloud_oss_bucket.bucket-acl.extranet_endpoint}" -} - -variable "bucket" { - description = "OSS bucket name" - default = "vela-website" - type = string -} - -variable "acl" { - description = "OSS bucket ACL, supported 'private', 'public-read', 'public-read-write'" - default = "private" - type = string -} -`, - }, - }, - want: want{ - errMsg: "", - tableName1: "", - tableName2: "#### writeConnectionSecretToRef", - }, - }, - "configuration is in git remote": { - args: args{ - cap: types.Capability{ - Name: "ecs", - TerraformConfiguration: "https://github.com/wonderflow/terraform-alicloud-ecs-instance.git", - ConfigurationType: "remote", - }, - }, - want: want{ - errMsg: "", - tableName1: "", - tableName2: "#### writeConnectionSecretToRef", - }, - }, - "configuration is not valid": { - args: args{ - cap: types.Capability{ - TerraformConfiguration: `abc`, - }, - }, - want: want{ - errMsg: "failed to generate capability properties: :1,1-4: Argument or block definition required; An " + - "argument or block definition is required here. To set an argument, use the equals sign \"=\" to " + - "introduce the argument value.", - }, - }, - } - for name, tc := range testcases { - consoleRef, err := ref.GenerateTerraformCapabilityProperties(tc.args.cap) - var errMsg string - if err != nil { - errMsg = err.Error() - if diff := cmp.Diff(tc.want.errMsg, errMsg, test.EquateErrors()); diff != "" { - t.Errorf("\n%s\nGenerateTerraformCapabilityProperties(...): -want error, +got error:\n%s\n", name, diff) - } - } else { - if diff := cmp.Diff(len(consoleRef), 2); diff != "" { - t.Errorf("\n%s\nGenerateTerraformCapabilityProperties(...): -want, +got:\n%s\n", name, diff) - } - if diff := cmp.Diff(tc.want.tableName1, consoleRef[0].TableName); diff != "" { - t.Errorf("\n%s\nGenerateTerraformCapabilityProperties(...): -want, +got:\n%s\n", name, diff) - } - if diff := cmp.Diff(tc.want.tableName2, consoleRef[1].TableName); diff != "" { - t.Errorf("\n%s\nGexnerateTerraformCapabilityProperties(...): -want, +got:\n%s\n", name, diff) - } - } - } -} - func TestPrepareTerraformOutputs(t *testing.T) { type args struct { tableName string