Skip to content

Commit 0c80377

Browse files
committed
enhance resource interpreter test framework
Signed-off-by: zhzhuang-zju <[email protected]>
1 parent a0dbc48 commit 0c80377

File tree

3 files changed

+242
-8
lines changed

3 files changed

+242
-8
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Thirdparty Resource Interpreter
2+
3+
This directory contains third-party resource interpreters for Karmada. These interpreters define how Karmada should handle custom resources from various third-party applications and operators.
4+
5+
## Files
6+
7+
- `thirdparty.go` - Main implementation of the third-party resource interpreter
8+
- `thirdparty_test.go` - Test suite for validating resource interpreter customizations
9+
- `resourcecustomizations/` - Directory containing resource customization definitions organized by API version and kind
10+
11+
## Directory Structure
12+
13+
The resource customizations are organized in the following structure:
14+
15+
```
16+
resourcecustomizations/
17+
├── <group>/
18+
│ └── <version>/
19+
│ └── <kind>/
20+
│ ├── customizations.yaml # Resource interpreter customization rules
21+
│ ├── customizations_tests.yaml # Test cases for the customizations
22+
│ └── testdata/ # Test input and expected output files
23+
│ ├── desired_xxx.yaml # Input resource for desired state
24+
│ ├── observed_xxx.yaml # Input resource for observed state
25+
│ ├── status_xxx.yaml # Input aggregated status items
26+
│ ├── output_xxx.yaml # Expected output for various operations
27+
```
28+
29+
## How to test
30+
31+
### Running Tests
32+
33+
To run all third-party resource interpreter tests:
34+
35+
```bash
36+
cd pkg/resourceinterpreter/default/thirdparty
37+
go test -v
38+
```
39+
40+
### Creating Test Cases
41+
42+
#### 1. Create Test Structure
43+
44+
For a new resource type, create the directory structure:
45+
46+
```bash
47+
mkdir -p resourcecustomizations/<group>/<version>/<kind>/testdata
48+
```
49+
50+
#### 2. Create Test Data Files
51+
52+
Test data files are generally divided into four categories:
53+
54+
- `desired_xxx.yaml`: Resource definitions deployed on the control plane.
55+
- `observed_xxx.yaml`: Resource definitions observed in a member cluster.
56+
- `status_xxx.yaml`: Status information of the resource on each member cluster, with structure `[]workv1alpha2.AggregatedStatusItem`.
57+
- `output_xxx.yaml`: Expected output for various operations, with structure `map[string]interface`.
58+
59+
Multiple test data files can be created for each category as needed. Pay attention to naming distinctions, as they will ultimately be referenced in `customizations_tests.yaml`.
60+
61+
#### 3. Create Test Configuration
62+
63+
Test configuration is defined in `customizations_tests.yaml` within the resource customization directory. It specifies
64+
the test cases to be executed. Its structure is as follows:
65+
```go
66+
type TestStructure struct {
67+
Tests []IndividualTest `yaml:"tests"`
68+
}
69+
70+
type IndividualTest struct {
71+
DesiredInputPath string `yaml:"desiredInputPath,omitempty"` // the path of desired_xxx.yaml
72+
ObservedInputPath string `yaml:"observedInputPath,omitempty"` // the path of observed_xxx.yaml
73+
StatusInputPath string `yaml:"statusInputPath,omitempty"` // the path of status_xxx.yaml
74+
InputReplicas int64 `yaml:"inputReplicas,omitempty"` // the input replicas for revise operation
75+
OutputResultsPath string `yaml:"outputResultsPath,omitempty"` // the path of output_xxx.yaml
76+
Operation string `yaml:"operation"` // the operation of resource interpreter
77+
}
78+
```
79+
80+
Create `customizations_tests.yaml` to define test cases:
81+
82+
```yaml
83+
tests:
84+
- desiredInputPath: testdata/desired-flinkdeployment.yaml
85+
statusInputPath: testdata/status-file.yaml
86+
operation: AggregateStatus
87+
outputResultsPath: testdata/output-flinkdeployment.yaml
88+
- desiredInputPath: testdata/desired-flinkdeployment.yaml
89+
operation: InterpretReplica
90+
outputResultsPath: testdata/output-flinkdeployment.yaml
91+
```
92+
93+
Where:
94+
- `operation` specifies the operation of resource interpreter
95+
- `outputResultsPath` defines the file path for expected output results. The output results are key-value mapping where the key is the field name of the expected result and the value is the expected result.
96+
97+
The keys in output results for different operations correspond to the Name field of the results returned by the corresponding resource interpreter operation `RuleResult.Results`.
98+
99+
For example:
100+
```go
101+
func (h *healthInterpretationRule) Run(interpreter *declarative.ConfigurableInterpreter, args RuleArgs) *RuleResult {
102+
obj, err := args.getObjectOrError()
103+
if err != nil {
104+
return newRuleResultWithError(err)
105+
}
106+
healthy, enabled, err := interpreter.InterpretHealth(obj)
107+
if err != nil {
108+
return newRuleResultWithError(err)
109+
}
110+
if !enabled {
111+
return newRuleResultWithError(fmt.Errorf("rule is not enabled"))
112+
}
113+
return newRuleResult().add("healthy", healthy)
114+
}
115+
```
116+
117+
The output results for operation `InterpretHealth` should contain the `healthy` key.
118+
119+
### Supported Operations
120+
121+
The test framework supports the following operations:
122+
123+
- `InterpretReplica` - Extract replica count from resource
124+
- `InterpretComponent` - Extract component information from resource
125+
- `ReviseReplica` - Modify replica count in resource
126+
- `InterpretStatus` - Extract status information
127+
- `InterpretHealth` - Determine resource health status
128+
- `InterpretDependency` - Extract resource dependencies
129+
- `AggregateStatus` - Aggregate status from multiple clusters
130+
- `Retain` - Retain the desired resource template.
131+
132+
### Test Validation
133+
134+
The test framework validates:
135+
136+
1. **Lua Script Syntax** - Ensures all Lua scripts are syntactically correct
137+
2. **Execution Results** - Compares actual results with expected results
138+
139+
### Debugging Tests
140+
141+
To debug failing tests:
142+
143+
1. **Check Lua Script Syntax** - Ensure your Lua scripts are valid
144+
2. **Verify Test Data** - Confirm test input files are properly formatted
145+
3. **Review Expected Results** - Make sure expected results match the actual operation output
146+
4. **Use Verbose Output** - Run tests with `-v` flag for detailed output
147+
148+
### Best Practices
149+
150+
1. **Comprehensive Coverage** - Test all supported operations for your resource type
151+
2. **Edge Cases** - Include tests for edge cases and error conditions
152+
3. **Realistic Data** - Use realistic resource definitions in test data
153+
4. **Clear Naming** - Use descriptive names for test files and cases
154+
155+
For more information about resource interpreter customizations, see the [Karmada documentation](https://karmada.io/docs/userguide/globalview/customizing-resource-interpreter/).

pkg/resourceinterpreter/default/thirdparty/resourcecustomizations/flink.apache.org/v1beta1/FlinkDeployment/customizations_tests.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@ tests:
22
- desiredInputPath: testdata/desired-flinkdeployment.yaml
33
statusInputPath: testdata/status-file.yaml
44
operation: AggregateStatus
5-
- observedInputPath: testdata/observed-flinkdeployment.yaml
5+
outputResultsPath: testdata/output-flinkdeployment.yaml
6+
- desiredInputPath: testdata/desired-flinkdeployment.yaml
67
operation: InterpretReplica
8+
outputResultsPath: testdata/output-flinkdeployment.yaml
9+
- desiredInputPath: testdata/desired-flinkdeployment.yaml
10+
operation: InterpretComponent
11+
outputResultsPath: testdata/output-flinkdeployment.yaml
712
- observedInputPath: testdata/observed-flinkdeployment.yaml
813
operation: InterpretHealth
14+
outputResultsPath: testdata/output-flinkdeployment.yaml
915
- observedInputPath: testdata/observed-flinkdeployment.yaml
1016
operation: InterpretStatus
17+
outputResultsPath: testdata/output-flinkdeployment.yaml

pkg/resourceinterpreter/default/thirdparty/thirdparty_test.go

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ import (
2727
"testing"
2828
"time"
2929

30+
"k8s.io/apimachinery/pkg/api/resource"
3031
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
32+
"k8s.io/apimachinery/pkg/conversion"
3133
"k8s.io/apimachinery/pkg/util/json"
3234
"k8s.io/apimachinery/pkg/util/yaml"
3335

@@ -39,6 +41,10 @@ import (
3941
)
4042

4143
var rules interpreter.Rules = interpreter.AllResourceInterpreterCustomizationRules
44+
var checker = conversion.EqualitiesOrDie(
45+
func(a, b resource.Quantity) bool {
46+
return a.Equal(b)
47+
})
4248

4349
func checkScript(script string) error {
4450
ctx, cancel := context.WithTimeout(context.TODO(), time.Second)
@@ -102,11 +108,12 @@ type TestStructure struct {
102108
}
103109

104110
type IndividualTest struct {
105-
DesiredInputPath string `yaml:"desiredInputPath,omitempty"`
106-
ObservedInputPath string `yaml:"observedInputPath,omitempty"`
107-
StatusInputPath string `yaml:"statusInputPath,omitempty"`
108-
DesiredReplica int64 `yaml:"desiredReplicas,omitempty"`
109-
Operation string `yaml:"operation"`
111+
DesiredInputPath string `yaml:"desiredInputPath,omitempty"` // the path of desired_xxx.yaml
112+
ObservedInputPath string `yaml:"observedInputPath,omitempty"` // the path of observed_xxx.yaml
113+
StatusInputPath string `yaml:"statusInputPath,omitempty"` // the path of status_xxx.yaml
114+
InputReplicas int64 `yaml:"inputReplicas,omitempty"` // the input replicas for revise operation
115+
OutputResultsPath string `yaml:"outputResultsPath,omitempty"` // the path of output_xxx.yaml
116+
Operation string `yaml:"operation"` // the operation of resource interpreter
110117
}
111118

112119
func checkInterpretationRule(t *testing.T, path string, configs []*configv1alpha1.ResourceInterpreterCustomization) {
@@ -133,7 +140,7 @@ func checkInterpretationRule(t *testing.T, path string, configs []*configv1alpha
133140
if err != nil {
134141
t.Fatalf("checking %s of %s, expected nil, but got: %v", rule.Name(), customization.Name, err)
135142
}
136-
args := interpreter.RuleArgs{Replica: input.DesiredReplica}
143+
args := interpreter.RuleArgs{Replica: input.InputReplicas}
137144
if input.DesiredInputPath != "" {
138145
args.Desired = getObj(t, dir+"/"+strings.TrimPrefix(input.DesiredInputPath, "/"))
139146
}
@@ -143,10 +150,75 @@ func checkInterpretationRule(t *testing.T, path string, configs []*configv1alpha
143150
if input.StatusInputPath != "" {
144151
args.Status = getAggregatedStatusItems(t, dir+"/"+strings.TrimPrefix(input.StatusInputPath, "/"))
145152
}
146-
if result := rule.Run(ipt, args); result.Err != nil {
153+
var outputResults = make(map[string]interface{})
154+
if input.OutputResultsPath != "" {
155+
outputResults = getObj(t, dir+"/"+strings.TrimPrefix(input.OutputResultsPath, "/")).Object
156+
}
157+
result := rule.Run(ipt, args)
158+
if result.Err != nil {
147159
t.Fatalf("execute %s %s error: %v\n", customization.Name, rule.Name(), result.Err)
148160
}
161+
for _, res := range result.Results {
162+
expected, ok := outputResults[res.Name]
163+
if !ok {
164+
// TODO(@zhzhuang-zju): Once we have a complete set of test cases, change this to t.Fatal.
165+
t.Logf("no expected result for %s of %s\n", res.Name, customization.Name)
166+
continue
167+
}
168+
169+
if equal, err := deepEqual(expected, res.Value); err != nil || !equal {
170+
t.Fatal("unexpected result for", res.Name, "expected:", expected, "got:", res.Value, "error:", err)
171+
}
172+
}
173+
}
174+
}
175+
}
176+
177+
func deepEqual(expected, actualValue interface{}) (bool, error) {
178+
expectedJSONBytes, err := json.Marshal(expected)
179+
if err != nil {
180+
return false, fmt.Errorf("failed to marshal expected value: %w", err)
181+
}
182+
183+
// Handle known types for semantic comparison
184+
switch typedActual := actualValue.(type) {
185+
case *workv1alpha2.ReplicaRequirements:
186+
var unmarshaledExpected workv1alpha2.ReplicaRequirements
187+
if err := json.Unmarshal(expectedJSONBytes, &unmarshaledExpected); err != nil {
188+
return false, fmt.Errorf("failed to unmarshal expected JSON into ReplicaRequirements: %w", err)
189+
}
190+
return checker.DeepEqual(&unmarshaledExpected, typedActual), nil
191+
192+
case *configv1alpha1.DependentObjectReference:
193+
var unmarshaledExpected configv1alpha1.DependentObjectReference
194+
if err := json.Unmarshal(expectedJSONBytes, &unmarshaledExpected); err != nil {
195+
return false, fmt.Errorf("failed to unmarshal expected JSON into DependentObjectReference: %w", err)
196+
}
197+
return checker.DeepEqual(&unmarshaledExpected, typedActual), nil
198+
199+
case []workv1alpha2.Component:
200+
var unmarshaledExpected []workv1alpha2.Component
201+
if err := json.Unmarshal(expectedJSONBytes, &unmarshaledExpected); err != nil {
202+
return false, fmt.Errorf("failed to unmarshal expected JSON into []Component: %w", err)
203+
}
204+
205+
return checker.DeepEqual(unmarshaledExpected, typedActual), nil
206+
207+
case *unstructured.Unstructured:
208+
var unmarshaledExpected unstructured.Unstructured
209+
210+
if err := json.Unmarshal(expectedJSONBytes, &unmarshaledExpected); err != nil {
211+
return false, fmt.Errorf("failed to unmarshal expected JSON into Unstructured: %w", err)
212+
}
213+
return checker.DeepEqual(&unmarshaledExpected, typedActual), nil
214+
215+
default:
216+
// Fallback: marshal actualValue and do byte-wise comparison
217+
actualJSON, err := json.Marshal(actualValue)
218+
if err != nil {
219+
return false, fmt.Errorf("failed to marshal actual value: %w", err)
149220
}
221+
return bytes.Equal(expectedJSONBytes, actualJSON), nil
150222
}
151223
}
152224

0 commit comments

Comments
 (0)