Feat(test): Enhance unit test coverage for webhook, workflow, VELAQL, and monitor packages (#6895)

* feat(monitor): Add unit tests for application metrics watcher

This commit introduces a new test file with comprehensive unit tests for the application metrics watcher functionality in pkg/monitor/watcher.

Key additions include:
  - Test cases for the application metrics watcher's inc() method covering add, delete, and update operations
  - Test cases for report() method that verifies dirty flags are cleared
  - Test cases for helper functions getPhase() and getApp()

These additions improve the overall test coverage and ensure the correctness of the application metrics monitoring functionality.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* feat(velaql): Add comprehensive unit tests for ParseVelaQLFromPath

This commit introduces new unit tests for the ParseVelaQLFromPath function in pkg/velaql, along with test data files to improve test coverage and ensure correctness.

Key additions include:
  - `pkg/velaql/parse_test.go`: Adds TestParseVelaQLFromPath function with comprehensive test cases covering:
    * Valid CUE files with and without export fields
    * Nonexistent and empty file paths
    * Invalid CUE content
    * Files with invalid export types
  - Test data files in pkg/velaql/testdata/:
    * simple-valid.cue: Valid CUE file with export field
    * simple-no-export.cue: Valid CUE file without export field
    * empty.cue: Empty CUE file
    * invalid-cue-content.cue: CUE file with invalid syntax
    * invalid-export.cue: CUE file with invalid export type

These additions improve the overall test coverage and ensure the robustness of the VELAQL parsing functionality.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* feat(webhook): Add unit tests for ValidateDefinitionRevision function

This commit introduces new unit tests for the ValidateDefinitionRevision function in pkg/webhook/utils to improve test coverage and ensure correctness of definition revision validation.

Key additions include:
  - `pkg/webhook/utils/utils_test.go`: Adds TestValidateDefinitionRevision function with comprehensive test cases covering:
    * Success scenarios with matching definition revisions
    * Success scenarios when definition revision does not exist
    * Failure scenarios with revision hash mismatches
    * Failure scenarios with spec mismatches
    * Failure scenarios with invalid definition revision names

These additions improve the overall test coverage and ensure the robustness of the webhook utility functions for validating definition revisions.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* feat(workflow): Add unit tests for OAM apply and query utilities

This commit introduces new unit tests for workflow provider functions in pkg/workflow/providers to improve test coverage and ensure correctness.

Key additions include:
  - `pkg/workflow/providers/oam/apply_test.go`: Adds TestRenderComponent function with comprehensive test cases for component rendering
  - `pkg/workflow/providers/query/utils_test.go`: Adds:
    * TestBuildResourceArray function with comprehensive test cases covering simple, nested, and complex resource tree scenarios
    * TestBuildResourceItem function with test cases for resources with and without annotations

These additions improve the overall test coverage and ensure the robustness of the workflow provider functions for OAM applications.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* fix(velaql): Improve error handling in ParseVelaQLFromPath test

This commit addresses an issue in the TestParseVelaQLFromPath function where file read errors were being silently ignored. The changes include:

- Removing the unused expectedView field from test cases
- Replacing conditional error checking with require.NoError to ensure file read operations are properly validated
- Ensuring that test failures are properly reported when file reading fails

This fix improves the reliability of the test suite by making sure that any file I/O errors are properly caught and reported rather than silently ignored.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* feat: Apply cross-cutting test improvements

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

* feat: Enhance test coverage with file-specific suggestions

This commit applies file-specific suggestions to enhance the test suite's
coverage and robustness.

Key changes include:

- **`pkg/monitor/watcher/application_test.go`**:
  - Added a test case for a multi-step workflow with mixed phases to
    validate `stepPhaseCounter` aggregation.
  - Added a test for idempotence by calling `inc` twice.
  - Added test cases for an empty workflow and an unknown application phase.
  - Strengthened the `report` test to assert that counters are not cleared.

- **`pkg/velaql/parse_test.go`**:
  - Added a test case for `ParseVelaQLFromPath` to handle files with
    leading/trailing whitespace.
  - Added a test case to ensure consistent error messages for relative paths.

- **`pkg/webhook/utils/utils_test.go`**:
  - Added a test case to `TestValidateCueTemplate` for a malformed CUE
    template.

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>

---------

Signed-off-by: Ashvin Bambhaniya <ashvin.bambhaniya@improwised.com>
This commit is contained in:
AshvinBambhaniya2003
2025-10-31 19:19:15 +05:30
committed by GitHub
parent d627ecea2a
commit 44ac92d1ba
11 changed files with 955 additions and 29 deletions

View File

@@ -0,0 +1,261 @@
/*
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 watcher
import (
"testing"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
)
func TestApplicationMetricsWatcher(t *testing.T) {
t.Parallel()
appRunning := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{Name: "app-running"},
Status: common.AppStatus{
Phase: common.ApplicationRunning,
},
}
appRendering := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{Name: "app-rendering"},
Status: common.AppStatus{
Phase: common.ApplicationRendering,
},
}
appWithWorkflow := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{Name: "app-with-workflow"},
Status: common.AppStatus{
Phase: common.ApplicationRunning,
Workflow: &common.WorkflowStatus{
Steps: []workflowv1alpha1.WorkflowStepStatus{
{
StepStatus: workflowv1alpha1.StepStatus{
Name: "step1",
Type: "apply-component",
Phase: workflowv1alpha1.WorkflowStepPhaseSucceeded,
},
},
},
},
},
}
appWithMixedWorkflow := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{Name: "app-with-mixed-workflow"},
Status: common.AppStatus{
Phase: common.ApplicationRunning,
Workflow: &common.WorkflowStatus{
Steps: []workflowv1alpha1.WorkflowStepStatus{
{
StepStatus: workflowv1alpha1.StepStatus{
Name: "step1",
Type: "apply-component",
Phase: workflowv1alpha1.WorkflowStepPhaseSucceeded,
},
},
{
StepStatus: workflowv1alpha1.StepStatus{
Name: "step2",
Type: "apply-component",
Phase: workflowv1alpha1.WorkflowStepPhaseFailed,
},
},
{
StepStatus: workflowv1alpha1.StepStatus{
Name: "step3",
Type: "suspend",
Phase: workflowv1alpha1.WorkflowStepPhaseRunning,
},
},
},
},
},
}
testCases := map[string]struct {
app *v1beta1.Application
op int
wantPC map[string]int
wantSC map[string]int
wantPD map[string]struct{}
wantSD map[string]struct{}
}{
"Add an application": {
app: appRunning,
op: 1,
wantPC: map[string]int{"running": 1},
wantSC: map[string]int{},
wantPD: map[string]struct{}{"running": {}},
wantSD: map[string]struct{}{},
},
"Add an application with workflow": {
app: appWithWorkflow,
op: 1,
wantPC: map[string]int{"running": 1},
wantSC: map[string]int{"apply-component/succeeded#": 1},
wantPD: map[string]struct{}{"running": {}},
wantSD: map[string]struct{}{"apply-component/succeeded#": {}},
},
"Delete an application": {
app: appRunning,
op: -1,
wantPC: map[string]int{"running": -1},
wantSC: map[string]int{},
wantPD: map[string]struct{}{"running": {}},
wantSD: map[string]struct{}{},
},
"Update an application": {
app: appRendering,
op: -1,
wantPC: map[string]int{"rendering": -1},
wantSC: map[string]int{},
wantPD: map[string]struct{}{"rendering": {}},
wantSD: map[string]struct{}{},
},
"Nil app status": {
app: &v1beta1.Application{},
op: 1,
wantPC: map[string]int{"-": 1},
wantSC: map[string]int{},
wantPD: map[string]struct{}{"-": {}},
wantSD: map[string]struct{}{},
},
"Add an application with mixed workflow": {
app: appWithMixedWorkflow,
op: 1,
wantPC: map[string]int{"running": 1},
wantSC: map[string]int{
"apply-component/succeeded#": 1,
"apply-component/failed#": 1,
"suspend/running#": 1,
},
wantPD: map[string]struct{}{"running": {}},
wantSD: map[string]struct{}{
"apply-component/succeeded#": {},
"apply-component/failed#": {},
"suspend/running#": {},
},
},
"Empty workflow steps": {
app: &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{Name: "app-empty-workflow"},
Status: common.AppStatus{
Phase: common.ApplicationRunning,
Workflow: &common.WorkflowStatus{
Steps: []workflowv1alpha1.WorkflowStepStatus{},
},
},
},
op: 1,
wantPC: map[string]int{"running": 1},
wantSC: map[string]int{},
wantPD: map[string]struct{}{"running": {}},
wantSD: map[string]struct{}{},
},
"Unknown phase": {
app: &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{Name: "app-unknown-phase"},
Status: common.AppStatus{
Phase: "unknown",
},
},
op: 1,
wantPC: map[string]int{"unknown": 1},
wantSC: map[string]int{},
wantPD: map[string]struct{}{"unknown": {}},
wantSD: map[string]struct{}{},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
watcher := &applicationMetricsWatcher{
phaseCounter: map[string]int{},
stepPhaseCounter: map[string]int{},
phaseDirty: map[string]struct{}{},
stepPhaseDirty: map[string]struct{}{},
}
watcher.inc(tc.app, tc.op)
assert.Equal(t, tc.wantPC, watcher.phaseCounter)
assert.Equal(t, tc.wantSC, watcher.stepPhaseCounter)
assert.Equal(t, tc.wantPD, watcher.phaseDirty)
assert.Equal(t, tc.wantSD, watcher.stepPhaseDirty)
})
}
t.Run("Idempotence", func(t *testing.T) {
t.Parallel()
watcher := &applicationMetricsWatcher{
phaseCounter: map[string]int{},
stepPhaseCounter: map[string]int{},
phaseDirty: map[string]struct{}{},
stepPhaseDirty: map[string]struct{}{},
}
watcher.inc(appRunning, 1)
watcher.inc(appRunning, 1)
assert.Equal(t, map[string]int{"running": 2}, watcher.phaseCounter)
assert.Equal(t, map[string]struct{}{"running": {}}, watcher.phaseDirty)
})
t.Run("Report should clear dirty flags", func(t *testing.T) {
t.Parallel()
watcher := &applicationMetricsWatcher{
phaseCounter: map[string]int{"running": 1},
stepPhaseCounter: map[string]int{"apply-component/succeeded#": 1},
phaseDirty: map[string]struct{}{"running": {}},
stepPhaseDirty: map[string]struct{}{"apply-component/succeeded#": {}},
}
watcher.report()
assert.Empty(t, watcher.phaseDirty)
assert.Empty(t, watcher.stepPhaseDirty)
assert.Equal(t, map[string]int{"running": 1}, watcher.phaseCounter)
assert.Equal(t, map[string]int{"apply-component/succeeded#": 1}, watcher.stepPhaseCounter)
})
t.Run("getPhase helper function", func(t *testing.T) {
t.Parallel()
watcher := &applicationMetricsWatcher{}
assert.Equal(t, "-", watcher.getPhase(""))
assert.Equal(t, "running", watcher.getPhase("running"))
})
t.Run("getApp helper function", func(t *testing.T) {
t.Parallel()
watcher := &applicationMetricsWatcher{}
inputApp := &v1beta1.Application{
ObjectMeta: metav1.ObjectMeta{
Name: "test-app",
Namespace: "test-ns",
},
Status: common.AppStatus{
Phase: common.ApplicationRunning,
},
}
resultApp := watcher.getApp(inputApp)
assert.NotNil(t, resultApp)
assert.Equal(t, "test-app", resultApp.Name)
assert.Equal(t, "test-ns", resultApp.Namespace)
assert.Equal(t, common.ApplicationRunning, resultApp.Status.Phase)
})
}

View File

@@ -17,13 +17,19 @@
package velaql
import (
"context"
"fmt"
"os"
"path/filepath"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseVelaQL(t *testing.T) {
t.Parallel()
testcases := []struct {
ql string
query QueryView
@@ -67,19 +73,22 @@ func TestParseVelaQL(t *testing.T) {
err: nil,
}}
for _, testcase := range testcases {
q, err := ParseVelaQL(testcase.ql)
assert.Equal(t, testcase.err != nil, err != nil)
if err == nil {
assert.Equal(t, testcase.query.View, q.View)
assert.Equal(t, testcase.query.Export, q.Export)
} else {
assert.Equal(t, testcase.err.Error(), err.Error())
}
for i, testcase := range testcases {
t.Run(fmt.Sprintf("testcase-%d", i), func(t *testing.T) {
q, err := ParseVelaQL(testcase.ql)
if testcase.err != nil {
assert.EqualError(t, testcase.err, err.Error())
} else {
assert.NoError(t, err)
assert.Equal(t, testcase.query.View, q.View)
assert.Equal(t, testcase.query.Export, q.Export)
}
})
}
}
func TestParseParameter(t *testing.T) {
t.Parallel()
testcases := []struct {
parameter string
parameterMap map[string]interface{}
@@ -122,15 +131,114 @@ func TestParseParameter(t *testing.T) {
err: nil,
}}
for _, testcase := range testcases {
result, err := ParseParameter(testcase.parameter)
assert.Equal(t, testcase.err != nil, err != nil)
if err == nil {
for k, v := range result {
assert.Equal(t, testcase.parameterMap[k], v)
for i, testcase := range testcases {
t.Run(fmt.Sprintf("testcase-%d", i), func(t *testing.T) {
result, err := ParseParameter(testcase.parameter)
if testcase.err != nil {
assert.EqualError(t, testcase.err, err.Error())
} else {
assert.NoError(t, err)
for k, v := range result {
assert.Equal(t, testcase.parameterMap[k], v)
}
}
} else {
assert.Equal(t, testcase.err.Error(), err.Error())
}
})
}
}
func TestParseVelaQLFromPath(t *testing.T) {
t.Parallel()
ctx := context.Background()
testdataDir := "testdata"
testcases := []struct {
name string
path string
expectedExport string
expectError bool
errorContains string
}{
{
name: "Simple valid CUE file with export field",
path: filepath.Join(testdataDir, "simple-valid.cue"),
expectedExport: "output.message",
expectError: false,
},
{
name: "Simple valid CUE file without export field",
path: filepath.Join(testdataDir, "simple-no-export.cue"),
expectedExport: DefaultExportValue,
expectError: false,
},
{
name: "Nonexistent file path",
path: filepath.Join(testdataDir, "nonexistent.cue"),
expectError: true,
errorContains: "read view file from",
},
{
name: "Empty file path",
path: "",
expectError: true,
errorContains: "read view file from",
},
{
name: "Invalid CUE content",
path: filepath.Join(testdataDir, "invalid-cue-content.cue"),
expectError: true,
errorContains: "error when parsing view",
},
{
name: "File with invalid export type - should fallback to default",
path: filepath.Join(testdataDir, "invalid-export.cue"),
expectedExport: DefaultExportValue,
expectError: false,
},
{
name: "Empty CUE file",
path: filepath.Join(testdataDir, "empty.cue"),
expectedExport: DefaultExportValue,
expectError: false,
},
{
name: "File with leading/trailing whitespace",
path: filepath.Join(testdataDir, "whitespace.cue"),
expectedExport: "output.message",
expectError: false,
},
{
name: "Relative path",
path: "testdata/nonexistent.cue",
expectError: true,
errorContains: "read view file from",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
result, err := ParseVelaQLFromPath(ctx, tc.path)
if tc.expectError {
assert.Error(t, err)
assert.Nil(t, result)
if tc.errorContains != "" {
assert.Contains(t, err.Error(), tc.errorContains)
}
} else {
assert.NoError(t, err)
assert.NotNil(t, result)
if tc.path != "" {
expectedContent, readErr := os.ReadFile(tc.path)
require.NoError(t, readErr)
assert.Equal(t, string(expectedContent), result.View)
}
assert.Equal(t, tc.expectedExport, result.Export)
assert.Nil(t, result.Parameter)
}
})
}
}

0
pkg/velaql/testdata/empty.cue vendored Normal file
View File

View File

@@ -0,0 +1,4 @@
invalid cue syntax [
missing: colon
invalid: brackets
}

View File

@@ -0,0 +1,9 @@
parameter: {
name: "test"
}
output: {
message: "hello " + parameter.name
}
export: 123 // Invalid export type (should be string)

View File

@@ -0,0 +1,7 @@
parameter: {
name: "test"
}
output: {
message: "hello " + parameter.name
}

9
pkg/velaql/testdata/simple-valid.cue vendored Normal file
View File

@@ -0,0 +1,9 @@
parameter: {
name: "test"
}
output: {
message: "hello " + parameter.name
}
export: "output.message"

12
pkg/velaql/testdata/whitespace.cue vendored Normal file
View File

@@ -0,0 +1,12 @@
parameter: {
name: "test"
}
output: {
message: "hello " + parameter.name
}
export: "output.message"

View File

@@ -29,12 +29,132 @@ import (
dynamicfake "k8s.io/client-go/dynamic/fake"
"cuelang.org/go/cue/errors"
"github.com/crossplane/crossplane-runtime/pkg/test"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1beta1/core"
)
func TestValidateDefinitionRevision(t *testing.T) {
t.Parallel()
scheme := runtime.NewScheme()
v1beta1.AddToScheme(scheme)
baseCompDef := &v1beta1.ComponentDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "test-def",
Namespace: "default",
},
Spec: v1beta1.ComponentDefinitionSpec{
Workload: common.WorkloadTypeDescriptor{
Definition: common.WorkloadGVK{
APIVersion: "apps/v1",
Kind: "Deployment",
},
},
Schematic: &common.Schematic{
CUE: &common.CUE{
Template: `
output: {
apiVersion: "apps/v1"
kind: "Deployment"
metadata: name: context.name
}`,
},
},
},
}
expectedDefRev, _, err := core.GatherRevisionInfo(baseCompDef)
assert.NoError(t, err, "Setup: failed to gather revision info")
expectedDefRev.Name = "test-def-v1"
expectedDefRev.Namespace = "default"
mismatchedHashDefRev := expectedDefRev.DeepCopy()
mismatchedHashDefRev.Spec.RevisionHash = "different-hash"
mismatchedSpecDefRev := expectedDefRev.DeepCopy()
mismatchedSpecDefRev.Spec.ComponentDefinition.Spec.Workload.Definition.Kind = "StatefulSet"
// tweakedCompDef := baseCompDef.DeepCopy()
// tweakedCompDef.Spec.Schematic.CUE.Template = `
// output: {
// apiVersion: "apps/v1"
// kind: "Deployment"
// metadata: name: context.name
// // a tweak
// }`
testCases := map[string]struct {
def runtime.Object
defRevName types.NamespacedName
existingObjs []runtime.Object
expectErr bool
expectedErrContains string
}{
"Success with matching definition revision": {
def: baseCompDef,
defRevName: types.NamespacedName{Name: "test-def-v1", Namespace: "default"},
existingObjs: []runtime.Object{expectedDefRev},
expectErr: false,
},
"Success when definition revision does not exist": {
def: baseCompDef,
defRevName: types.NamespacedName{Name: "test-def-v1", Namespace: "default"},
existingObjs: []runtime.Object{},
expectErr: false,
},
"Failure with revision hash mismatch": {
def: baseCompDef,
defRevName: types.NamespacedName{Name: "test-def-v1", Namespace: "default"},
existingObjs: []runtime.Object{mismatchedHashDefRev},
expectErr: true,
expectedErrContains: "the definition's spec is different with existing definitionRevision's spec",
},
"Failure with spec mismatch (DeepEqual)": {
def: baseCompDef,
defRevName: types.NamespacedName{Name: "test-def-v1", Namespace: "default"},
existingObjs: []runtime.Object{mismatchedSpecDefRev},
expectErr: true,
expectedErrContains: "the definition's spec is different with existing definitionRevision's spec",
},
"Failure with invalid definition revision name": {
def: baseCompDef,
defRevName: types.NamespacedName{Name: "invalid!name", Namespace: "default"},
existingObjs: []runtime.Object{},
expectErr: true,
expectedErrContains: "invalid definitionRevision name",
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
cli := fake.NewClientBuilder().
WithScheme(scheme).
WithRuntimeObjects(tc.existingObjs...).
Build()
err := ValidateDefinitionRevision(context.Background(), cli, tc.def, tc.defRevName)
if tc.expectErr {
assert.Error(t, err)
if tc.expectedErrContains != "" {
assert.Contains(t, err.Error(), tc.expectedErrContains)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidateCueTemplate(t *testing.T) {
t.Parallel()
cases := map[string]struct {
cueTemplate string
want error
@@ -66,19 +186,31 @@ func TestValidateCueTemplate(t *testing.T) {
}`,
want: errors.New("output.hello: reference \"world\" not found"),
},
"emptyCueTemp": {
cueTemplate: "",
want: nil,
},
"malformedCueTemp": {
cueTemplate: "output: { metadata: { name: context.name, label: context.label, annotation: \"default\" }, hello: world ",
want: errors.New("expected '}', found 'EOF'"),
},
}
for caseName, cs := range cases {
t.Run(caseName, func(t *testing.T) {
t.Parallel()
err := ValidateCueTemplate(cs.cueTemplate)
if diff := cmp.Diff(cs.want, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nValidateCueTemplate: -want , +got \n%s\n", cs.want, diff)
if cs.want != nil {
assert.EqualError(t, cs.want, err.Error())
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidateCuexTemplate(t *testing.T) {
t.Parallel()
cases := map[string]struct {
cueTemplate string
want error
@@ -164,15 +296,19 @@ func TestValidateCuexTemplate(t *testing.T) {
for caseName, cs := range cases {
t.Run(caseName, func(t *testing.T) {
t.Parallel()
err := ValidateCuexTemplate(context.Background(), cs.cueTemplate)
if diff := cmp.Diff(cs.want, err, test.EquateErrors()); diff != "" {
t.Errorf("\n%s\nValidateCueTemplate: -want , +got \n%s\n", cs.want, diff)
if cs.want != nil {
assert.Equal(t, cs.want.Error(), err.Error())
} else {
assert.NoError(t, err)
}
})
}
}
func TestValidateSemanticVersion(t *testing.T) {
t.Parallel()
cases := map[string]struct {
version string
want error
@@ -192,18 +328,20 @@ func TestValidateSemanticVersion(t *testing.T) {
}
for caseName, cs := range cases {
t.Run(caseName, func(t *testing.T) {
t.Parallel()
err := ValidateSemanticVersion(cs.version)
if cs.want != nil {
assert.Equal(t, err.Error(), cs.want.Error())
assert.Error(t, err)
assert.EqualError(t, cs.want, err.Error())
} else {
assert.Equal(t, err, cs.want)
assert.NoError(t, err)
}
})
}
}
func TestValidateMultipleDefVersionsNotPresent(t *testing.T) {
t.Parallel()
cases := map[string]struct {
version string
revisionName string
@@ -227,11 +365,13 @@ func TestValidateMultipleDefVersionsNotPresent(t *testing.T) {
}
for caseName, cs := range cases {
t.Run(caseName, func(t *testing.T) {
t.Parallel()
err := ValidateMultipleDefVersionsNotPresent(cs.version, cs.revisionName, "ComponentDefinition")
if cs.want != nil {
assert.Equal(t, err.Error(), cs.want.Error())
assert.Error(t, err)
assert.EqualError(t, cs.want, err.Error())
} else {
assert.Equal(t, err, cs.want)
assert.NoError(t, err)
}
})

View File

@@ -48,6 +48,7 @@ func setupClient(ctx context.Context, t *testing.T) client.Client {
}
func TestParser(t *testing.T) {
t.Parallel()
r := require.New(t)
ctx := context.Background()
act := &mock.Action{}
@@ -106,7 +107,67 @@ func TestParser(t *testing.T) {
r.Equal(act.Phase, "Wait")
}
func TestRenderComponent(t *testing.T) {
t.Parallel()
r := require.New(t)
ctx := context.Background()
cuectx := cuecontext.New()
cli := setupClient(ctx, t)
v := cuectx.CompileString(`$params: {
value: {
name: "test-render",
type: "webservice",
}
}`)
r.NoError(v.Err())
mockComponentRender := func(ctx context.Context, comp common.ApplicationComponent, patcher *cue.Value, clusterName string, overrideNamespace string) (*unstructured.Unstructured, []*unstructured.Unstructured, error) {
r.Equal("test-render", comp.Name)
workload := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "test-workload",
},
},
}
trait := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Service",
"metadata": map[string]interface{}{
"name": "test-trait",
"labels": map[string]interface{}{
"trait.oam.dev/resource": "mytrait",
},
},
},
}
return workload, []*unstructured.Unstructured{trait}, nil
}
res, err := RenderComponent(ctx, &oamprovidertypes.Params[cue.Value]{
Params: v,
RuntimeParams: oamprovidertypes.RuntimeParams{
KubeClient: cli,
ComponentRender: mockComponentRender,
},
})
r.NoError(err)
output, err := res.LookupPath(cue.ParsePath("$returns.output.metadata.name")).String()
r.NoError(err)
r.Equal("test-workload", output)
outputs, err := res.LookupPath(cue.ParsePath("$returns.outputs.mytrait.metadata.name")).String()
r.NoError(err)
r.Equal("test-workload", outputs)
}
func TestLoadComponent(t *testing.T) {
t.Parallel()
r := require.New(t)
ctx := context.Background()
act := &mock.Action{}
@@ -171,6 +232,7 @@ func TestLoadComponent(t *testing.T) {
}
func TestLoadComponentInOrder(t *testing.T) {
t.Parallel()
r := require.New(t)
ctx := context.Background()
act := &mock.Action{}

View File

@@ -15,3 +15,317 @@
*/
package query
import (
"testing"
"github.com/stretchr/testify/assert"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/oam-dev/kubevela/pkg/oam"
querytypes "github.com/oam-dev/kubevela/pkg/utils/types"
)
func TestBuildResourceArray(t *testing.T) {
t.Parallel()
// Define common objects used across tests
pod1 := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "pod1",
},
},
}
pod2 := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "pod2",
"annotations": map[string]interface{}{
oam.AnnotationPublishVersion: "v2.0.0-pod",
oam.AnnotationDeployVersion: "rev2-pod",
},
},
},
}
deployment := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "my-app",
},
},
}
// Define common tree nodes
parentWorkloadNode := &querytypes.ResourceTreeNode{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "my-app",
Namespace: "default",
Object: deployment,
}
pod1Node := &querytypes.ResourceTreeNode{
APIVersion: "v1",
Kind: "Pod",
Name: "pod1",
Namespace: "default",
Object: pod1,
}
pod2Node := &querytypes.ResourceTreeNode{
APIVersion: "v1",
Kind: "Pod",
Name: "pod2",
Namespace: "default",
Object: pod2,
}
replicaSetNode := &querytypes.ResourceTreeNode{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
Name: "my-app-rs",
Namespace: "default",
LeafNodes: []*querytypes.ResourceTreeNode{pod1Node, pod2Node},
}
// Define test cases
testCases := map[string]struct {
res querytypes.AppliedResource
parent *querytypes.ResourceTreeNode
node *querytypes.ResourceTreeNode
kind string
apiVersion string
expected []querytypes.ResourceItem
}{
"simple case with one matching pod": {
res: querytypes.AppliedResource{
Cluster: "local",
Component: "my-comp",
PublishVersion: "v1.0.0",
DeployVersion: "rev1",
},
parent: parentWorkloadNode,
node: pod1Node,
kind: "Pod",
apiVersion: "v1",
expected: []querytypes.ResourceItem{
{
Cluster: "local",
Component: "my-comp",
Workload: querytypes.Workload{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "my-app",
Namespace: "default",
},
Object: pod1,
PublishVersion: "v1.0.0",
DeployVersion: "rev1",
},
},
},
"nested case with multiple matching pods": {
res: querytypes.AppliedResource{
Cluster: "remote",
Component: "my-comp-2",
PublishVersion: "v2.0.0",
DeployVersion: "rev2",
},
parent: parentWorkloadNode,
node: replicaSetNode,
kind: "Pod",
apiVersion: "v1",
expected: []querytypes.ResourceItem{
{
Cluster: "remote",
Component: "my-comp-2",
Workload: querytypes.Workload{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
Name: "my-app-rs",
Namespace: "default",
},
Object: pod1,
PublishVersion: "v2.0.0",
DeployVersion: "rev2",
},
{
Cluster: "remote",
Component: "my-comp-2",
Workload: querytypes.Workload{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
Name: "my-app-rs",
Namespace: "default",
},
Object: pod2,
PublishVersion: "v2.0.0-pod", // From annotation
DeployVersion: "rev2-pod", // From annotation
},
},
},
"no matching nodes": {
res: querytypes.AppliedResource{},
parent: parentWorkloadNode,
node: pod1Node,
kind: "Service",
apiVersion: "v1",
expected: nil,
},
"empty node": {
res: querytypes.AppliedResource{},
parent: parentWorkloadNode,
node: &querytypes.ResourceTreeNode{},
kind: "Pod",
apiVersion: "v1",
expected: nil,
},
"complex tree with mixed resources": {
res: querytypes.AppliedResource{
Cluster: "local",
Component: "my-comp",
PublishVersion: "v1.0.0",
DeployVersion: "rev1",
},
parent: parentWorkloadNode,
node: &querytypes.ResourceTreeNode{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
Name: "my-app-rs",
Namespace: "default",
LeafNodes: []*querytypes.ResourceTreeNode{
pod1Node,
{
APIVersion: "v1",
Kind: "Service",
Name: "my-service",
},
pod2Node,
},
},
kind: "Pod",
apiVersion: "v1",
expected: []querytypes.ResourceItem{
{
Cluster: "local",
Component: "my-comp",
Workload: querytypes.Workload{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
Name: "my-app-rs",
Namespace: "default",
},
Object: pod1,
PublishVersion: "v1.0.0",
DeployVersion: "rev1",
},
{
Cluster: "local",
Component: "my-comp",
Workload: querytypes.Workload{
APIVersion: "apps/v1",
Kind: "ReplicaSet",
Name: "my-app-rs",
Namespace: "default",
},
Object: pod2,
PublishVersion: "v2.0.0-pod",
DeployVersion: "rev2-pod",
},
},
},
"case-insensitive matching": {
res: querytypes.AppliedResource{
Cluster: "local",
Component: "my-comp",
PublishVersion: "v1.0.0",
DeployVersion: "rev1",
},
parent: parentWorkloadNode,
node: pod1Node,
kind: "pod",
apiVersion: "V1",
expected: nil,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
result := buildResourceArray(tc.res, tc.parent, tc.node, tc.kind, tc.apiVersion)
assert.ElementsMatch(t, tc.expected, result, "The returned resource items should match the expected ones")
})
}
}
func TestBuildResourceItem(t *testing.T) {
t.Parallel()
pod := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "test-pod",
},
},
}
podWithAnnotations := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]interface{}{
"name": "test-pod-annotated",
"annotations": map[string]interface{}{
oam.AnnotationPublishVersion: "v2.0.0-annotated",
oam.AnnotationDeployVersion: "rev2-annotated",
},
},
},
}
res := querytypes.AppliedResource{
Cluster: "test-cluster",
Component: "test-comp",
PublishVersion: "v1.0.0-res",
DeployVersion: "rev1-res",
}
workload := querytypes.Workload{
APIVersion: "apps/v1",
Kind: "Deployment",
Name: "test-workload",
Namespace: "test-ns",
}
t.Run("without annotations", func(t *testing.T) {
t.Parallel()
item := buildResourceItem(res, workload, pod)
assert.Equal(t, "test-cluster", item.Cluster)
assert.Equal(t, "test-comp", item.Component)
assert.Equal(t, workload, item.Workload)
assert.Equal(t, pod, item.Object)
assert.Equal(t, "v1.0.0-res", item.PublishVersion)
assert.Equal(t, "rev1-res", item.DeployVersion)
})
t.Run("with annotations", func(t *testing.T) {
t.Parallel()
item := buildResourceItem(res, workload, podWithAnnotations)
assert.Equal(t, "test-cluster", item.Cluster)
assert.Equal(t, "test-comp", item.Component)
assert.Equal(t, workload, item.Workload)
assert.Equal(t, podWithAnnotations, item.Object)
assert.Equal(t, "v2.0.0-annotated", item.PublishVersion)
assert.Equal(t, "rev2-annotated", item.DeployVersion)
})
t.Run("annotation override", func(t *testing.T) {
item := buildResourceItem(res, workload, podWithAnnotations)
assert.Equal(t, "v2.0.0-annotated", item.PublishVersion)
assert.Equal(t, "rev2-annotated", item.DeployVersion)
})
}