This is an automated email from the ASF dual-hosted git repository.
wwei pushed a commit to branch soak-test
in repository https://gitbox.apache.org/repos/asf/yunikorn-release.git
The following commit(s) were added to refs/heads/soak-test by this push:
new 795c080 [Yunikorn-3095] Add basic template processor for soak
testing framework (#201)
795c080 is described below
commit 795c080d6d03981c17f40f26be4579657caea217
Author: Shravan Achar <[email protected]>
AuthorDate: Thu Aug 14 20:24:46 2025 -0700
[Yunikorn-3095] Add basic template processor for soak testing framework
(#201)
Co-authored-by: Shravan Achar <[email protected]>
---
soak/framework/config.go | 10 +-
soak/framework/template.go | 324 ++++++++++++++++
soak/framework/template_test.go | 412 +++++++++++++++++++++
soak/templates/kwok-node-template.yaml | 6 +-
.../basic-scheduler-throughput/pod-default.yaml | 4 +-
5 files changed, 744 insertions(+), 12 deletions(-)
diff --git a/soak/framework/config.go b/soak/framework/config.go
index 70582de..661ccee 100644
--- a/soak/framework/config.go
+++ b/soak/framework/config.go
@@ -25,7 +25,8 @@ import (
)
type KubeconfigFields struct {
- Path string `yaml:"path,omitempty"`
+ Path string `yaml:"path,omitempty"`
+ Context string `yaml:"context,omitempty"`
}
type NodeFields struct {
@@ -61,12 +62,7 @@ type Template struct {
Chaos []ChaosFields `yaml:"chaos,omitempty"`
}
-type TestCaseParams struct {
- NodeMaxCount int `yaml:"nodeMaxCount,omitempty"`
- NodesDesiredCount int `yaml:"nodesDesiredCount,omitempty"`
- NumPods int `yaml:"numPods,omitempty"`
- NumJobs int `yaml:"numJobs,omitempty"`
-}
+type TestCaseParams map[string]interface{}
type Prom struct {
Query string `yaml:"query,omitempty"`
diff --git a/soak/framework/template.go b/soak/framework/template.go
new file mode 100644
index 0000000..23b0e2c
--- /dev/null
+++ b/soak/framework/template.go
@@ -0,0 +1,324 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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.
+
+ Inspired from
https://github.com/kubernetes/perf-tests/tree/master/clusterloader2/pkg/config
+*/
+
+package framework
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "text/template"
+)
+
+// TemplateProcessor handles Go template processing for clusterloader and node
template config files
+type TemplateProcessor struct {
+ params map[string]interface{}
+}
+
+func NewTemplateProcessor(params map[string]interface{}) *TemplateProcessor {
+ return &TemplateProcessor{
+ params: params,
+ }
+}
+
+func (tp *TemplateProcessor) ProcessConfigFile(configPath string) (string,
error) {
+ templateContent, err := os.ReadFile(configPath)
+ if err != nil {
+ return "", fmt.Errorf("failed to read config template file %s:
%w", configPath, err)
+ }
+
+ // Create a new template with custom functions
+ tmpl :=
template.New(filepath.Base(configPath)).Funcs(tp.getTemplateFunctions())
+
+ newTmpl, err := tmpl.Parse(string(templateContent))
+ if err != nil {
+ return "", fmt.Errorf("failed to parse template %s: %w",
configPath, err)
+ }
+
+ var buf bytes.Buffer
+ err = newTmpl.Execute(&buf, tp.params)
+ if err != nil {
+ return "", fmt.Errorf("failed to execute template %s: %w",
configPath, err)
+ }
+
+ return buf.String(), nil
+}
+
+// ProcessAndWriteConfigFile processes a template and writes the result to a
new file
+func (tp *TemplateProcessor) ProcessAndWriteConfigFile(templatePath,
outputPath string) error {
+ processedContent, err := tp.ProcessConfigFile(templatePath)
+ if err != nil {
+ return err
+ }
+
+ // Create output directory if it doesn't exist
+ outputDir := filepath.Dir(outputPath)
+ if err := os.MkdirAll(outputDir, 0755); err != nil {
+ return fmt.Errorf("failed to create output directory %s: %w",
outputDir, err)
+ }
+
+ // Write processed content to output file
+ err = os.WriteFile(outputPath, []byte(processedContent), 0644)
+ if err != nil {
+ return fmt.Errorf("failed to write processed config to %s: %w",
outputPath, err)
+ }
+
+ return nil
+}
+
+// getTemplateFunctions returns custom template functions used in
clusterloader configs
+func (tp *TemplateProcessor) getTemplateFunctions() template.FuncMap {
+ return template.FuncMap{
+ "DefaultParam": tp.defaultParam,
+ "Param": tp.param,
+ "Add": tp.add,
+ "Sub": tp.sub,
+ "Mul": tp.mul,
+ "Div": tp.div,
+ "ToString": tp.safeToString,
+ "ToInt": tp.toInt,
+ "ToFloat": tp.toFloat,
+ }
+}
+
+// safeToString is a helper that handles errors from toString
+func (tp *TemplateProcessor) safeToString(val interface{}) string {
+ result, err := tp.toString(val)
+ if err != nil {
+ // For internal use, just return a simple string representation
+ return fmt.Sprintf("%v", val)
+ }
+ return result
+}
+
+// defaultParam returns the parameter value if it exists, otherwise returns
the default value
+func (tp *TemplateProcessor) defaultParam(paramName interface{}, defaultValue
interface{}) interface{} {
+ paramStr := tp.safeToString(paramName)
+
+ // Check if parameter exists in our params map
+ if val, exists := tp.params[paramStr]; exists {
+ // Convert the parameter value to match the type of the default
value
+ convertedVal := tp.convertToType(tp.safeToString(val),
defaultValue)
+
+ // If the default value is a string, ensure the output is also
a string
+ // This is important for YAML values that need to be quoted
+ // Tested
+ if _, isStringDefault := defaultValue.(string); isStringDefault
{
+ return tp.safeToString(convertedVal)
+ }
+
+ return convertedVal
+ }
+
+ // Check if parameter exists as environment variable (clusterloader2
style)
+ if envVal := os.Getenv(paramStr); envVal != "" {
+ convertedVal := tp.convertToType(envVal, defaultValue)
+
+ // If the default value is a string, output should also be a
string
+ if _, isStringDefault := defaultValue.(string); isStringDefault
{
+ return tp.safeToString(convertedVal)
+ }
+
+ return convertedVal
+ }
+
+ return defaultValue
+}
+
+// param returns the parameter value, panics if not found
+func (tp *TemplateProcessor) param(paramName interface{}) interface{} {
+ paramStr := tp.safeToString(paramName)
+
+ if val, exists := tp.params[paramStr]; exists {
+ return val
+ }
+
+ if envVal := os.Getenv(paramStr); envVal != "" {
+ return envVal
+ }
+
+ panic(fmt.Sprintf("required parameter %s not found", paramStr))
+}
+
+// convertToType attempts to convert a string value to the same type as the
reference value
+func (tp *TemplateProcessor) convertToType(value string, reference
interface{}) interface{} {
+ switch reference.(type) {
+ case int, int32, int64:
+ if intVal, err := strconv.Atoi(value); err == nil {
+ return intVal
+ }
+ case float32, float64:
+ if floatVal, err := strconv.ParseFloat(value, 64); err == nil {
+ return floatVal
+ }
+ case bool:
+ if boolVal, err := strconv.ParseBool(value); err == nil {
+ return boolVal
+ }
+ }
+ return value // Default to string if conversion fails
+}
+
+// Math operations for templates
+func (tp *TemplateProcessor) add(a, b interface{}) interface{} {
+ result, err := tp.mathOp(a, b, "+")
+ if err != nil {
+ // In templates, we need to return a value even on error
+ return fmt.Sprintf("ERROR: %v", err)
+ }
+ return result
+}
+
+func (tp *TemplateProcessor) sub(a, b interface{}) interface{} {
+ result, err := tp.mathOp(a, b, "-")
+ if err != nil {
+ return fmt.Sprintf("ERROR: %v", err)
+ }
+ return result
+}
+
+func (tp *TemplateProcessor) mul(a, b interface{}) interface{} {
+ result, err := tp.mathOp(a, b, "*")
+ if err != nil {
+ return fmt.Sprintf("ERROR: %v", err)
+ }
+ return result
+}
+
+func (tp *TemplateProcessor) div(a, b interface{}) interface{} {
+ result, err := tp.mathOp(a, b, "/")
+ if err != nil {
+ return fmt.Sprintf("ERROR: %v", err)
+ }
+ return result
+}
+
+// mathOp performs math operations on two values
+func (tp *TemplateProcessor) mathOp(a, b interface{}, op string) (float64,
error) {
+ aVal := tp.toFloat(a)
+ bVal := tp.toFloat(b)
+
+ switch op {
+ case "+":
+ return aVal + bVal, nil
+ case "-":
+ return aVal - bVal, nil
+ case "*":
+ return aVal * bVal, nil
+ case "/":
+ if bVal == 0 {
+ return 0, fmt.Errorf("division by zero")
+ }
+ return aVal / bVal, nil
+ default:
+ return 0, fmt.Errorf("unknown operation: %s", op)
+ }
+}
+
+// Type conversion functions
+func (tp *TemplateProcessor) toString(val interface{}) (string, error) {
+ if val == nil {
+ return "", nil
+ }
+
+ switch v := val.(type) {
+ case string:
+ return v, nil
+ case int, int32, int64:
+ return fmt.Sprintf("%d", v), nil
+ case float32, float64:
+ return fmt.Sprintf("%g", v), nil
+ case bool:
+ return strconv.FormatBool(v), nil
+ default:
+ // This is safer as it makes it explicit when an unsupported
type is used
+ return "", fmt.Errorf("unsupported type for conversion to
string: %T", val)
+ }
+}
+
+func (tp *TemplateProcessor) toInt(val interface{}) int {
+ switch v := val.(type) {
+ case int:
+ return v
+ case int32:
+ return int(v)
+ case int64:
+ return int(v)
+ case float32:
+ return int(v)
+ case float64:
+ return int(v)
+ case string:
+ if intVal, err := strconv.Atoi(v); err == nil {
+ return intVal
+ }
+ }
+ return 0
+}
+
+func (tp *TemplateProcessor) toFloat(val interface{}) float64 {
+ switch v := val.(type) {
+ case int:
+ return float64(v)
+ case int32:
+ return float64(v)
+ case int64:
+ return float64(v)
+ case float32:
+ return float64(v)
+ case float64:
+ return v
+ case string:
+ if floatVal, err := strconv.ParseFloat(v, 64); err == nil {
+ return floatVal
+ }
+ }
+ return 0.0
+}
+
+// BuildParameterMap builds a parameter map from test case configuration
+func BuildParameterMap(testCase TestCase) map[string]interface{} {
+ params := make(map[string]interface{})
+
+ // Since TestCaseParams is now a map[string]interface{}, directly copy
all parameters
+ for key, value := range testCase.Params {
+ params[key] = value
+
+ // Also add with common clusterloader2 environment variable
naming convention
+ envName := "CL2_" + strings.ToUpper(strings.ReplaceAll(key,
".", "_"))
+ params[envName] = value
+ }
+
+ // Add some common parameter mappings
+ if numPods, exists := params["numPods"]; exists {
+ params["CL2_SCHEDULER_THROUGHPUT_PODS"] = numPods
+ }
+ if nodesMaxCount, exists := params["nodesMaxCount"]; exists {
+ params["CL2_NODES_MAX_COUNT"] = nodesMaxCount
+ }
+ if nodesDesiredCount, exists := params["nodesDesiredCount"]; exists {
+ params["CL2_NODES_DESIRED_COUNT"] = nodesDesiredCount
+ }
+
+ return params
+}
diff --git a/soak/framework/template_test.go b/soak/framework/template_test.go
new file mode 100644
index 0000000..47552df
--- /dev/null
+++ b/soak/framework/template_test.go
@@ -0,0 +1,412 @@
+/*
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you 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 framework
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+func TestTemplateProcessor(t *testing.T) {
+ // Create a temporary template file using string literals for parameter
names
+ templateContent := `{{$totalPods := DefaultParam
"CL2_SCHEDULER_THROUGHPUT_PODS" 10}}
+{{$defaultQps := DefaultParam "CL2_DEFAULTQPS" 5}}
+{{$threshold := DefaultParam "CL2_SCHEDULERTHROUGHPUTTHRESHOLD" 15}}
+
+name: test-config
+totalPods: {{$totalPods}}
+qps: {{$defaultQps}}
+threshold: {{$threshold}}
+calculatedValue: {{Add $totalPods 100}}
+`
+
+ tempDir, err := os.MkdirTemp("", "template_test")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ templatePath := filepath.Join(tempDir, "test_template.yaml")
+ err = os.WriteFile(templatePath, []byte(templateContent), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write template file: %v", err)
+ }
+
+ // With parameters
+ testCase := TestCase{
+ Name: "test-case",
+ Params: TestCaseParams{
+ "numPods": 5000,
+ "defaultQps": 10,
+ "schedulerThroughputThreshold": 20,
+ },
+ }
+
+ params := BuildParameterMap(testCase)
+
+ processor := NewTemplateProcessor(params)
+
+ result, err := processor.ProcessConfigFile(templatePath)
+ if err != nil {
+ t.Fatalf("Failed to process template: %v", err)
+ }
+
+ // Verify results
+ expectedValues := []string{
+ "totalPods: 5000", // Should use numPods parameter
+ "qps: 10", // Should use defaultQps parameter
+ "threshold: 20", // Should use
schedulerThroughputThreshold parameter
+ "calculatedValue: 5100", // Should be numPods + 100
+ }
+
+ for _, expected := range expectedValues {
+ if !strings.Contains(result, expected) {
+ t.Errorf("Expected result to contain '%s', but
got:\n%s", expected, result)
+ }
+ }
+}
+
+func TestBuildParameterMap(t *testing.T) {
+ testCase := TestCase{
+ Name: "test-case",
+ Params: TestCaseParams{
+ "nodesMaxCount": 1000,
+ "nodesDesiredCount": 20,
+ "numPods": 5000,
+ "customParam": "test-value",
+ },
+ }
+
+ params := BuildParameterMap(testCase)
+
+ // Original parameters must be preserved
+ if params["nodesMaxCount"] != 1000 {
+ t.Errorf("Expected nodesMaxCount to be 1000, got %v",
params["nodesMaxCount"])
+ }
+
+ if params["customParam"] != "test-value" {
+ t.Errorf("Expected customParam to be 'test-value', got %v",
params["customParam"])
+ }
+
+ // CL2 parameters must be created
+ if params["CL2_NODES_MAX_COUNT"] != 1000 {
+ t.Errorf("Expected CL2_NODES_MAX_COUNT to be 1000, got %v",
params["CL2_NODES_MAX_COUNT"])
+ }
+
+ if params["CL2_SCHEDULER_THROUGHPUT_PODS"] != 5000 {
+ t.Errorf("Expected CL2_SCHEDULER_THROUGHPUT_PODS to be 5000,
got %v", params["CL2_SCHEDULER_THROUGHPUT_PODS"])
+ }
+}
+
+func TestDefaultParamFunction(t *testing.T) {
+ params := map[string]interface{}{
+ "existingParam": 42,
+ "CL2_TEST_PARAM": "test-value",
+ }
+
+ processor := NewTemplateProcessor(params)
+
+ // Test existing parameter
+ result := processor.defaultParam("existingParam", 10)
+ if result != 42 {
+ t.Errorf("Expected 42, got %v", result)
+ }
+
+ // Test non-existing parameter (should return default)
+ result = processor.defaultParam("nonExistingParam", 99)
+ if result != 99 {
+ t.Errorf("Expected 99, got %v", result)
+ }
+
+ // Test CL2 style parameter
+ result = processor.defaultParam("CL2_TEST_PARAM", "default")
+ if result != "test-value" {
+ t.Errorf("Expected 'test-value', got %v", result)
+ }
+}
+
+func TestNumericParameterToStringConversion(t *testing.T) {
+ // Numeric parameters should be converted to strings when template
default is string
+ templateContent := `max-count: {{DefaultParam "MAX_COUNT" "100"}}
+min-count: {{DefaultParam "MIN_COUNT" "1"}}
+desired-count: {{DefaultParam "DESIRED_COUNT" "10"}}`
+
+ // Create temp directory and file
+ tempDir, err := os.MkdirTemp("", "numeric_conversion_test")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ templatePath := filepath.Join(tempDir, "numeric_template.yaml")
+ err = os.WriteFile(templatePath, []byte(templateContent), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write template file: %v", err)
+ }
+
+ // Test with numeric parameters (as they would come from conf.yaml)
+ numericParams := map[string]interface{}{
+ "MAX_COUNT": 1000, // numeric value
+ "MIN_COUNT": 20, // numeric value
+ "DESIRED_COUNT": 50, // numeric value
+ }
+
+ processor := NewTemplateProcessor(numericParams)
+ result, err := processor.ProcessConfigFile(templatePath)
+ if err != nil {
+ t.Fatalf("Failed to process template: %v", err)
+ }
+
+ // Verify the numeric parameters were converted to strings
+ // No quotes in YAML output
+ expectedValues := []string{
+ "max-count: 1000",
+ "min-count: 20",
+ "desired-count: 50",
+ }
+
+ for _, expected := range expectedValues {
+ if !strings.Contains(result, expected) {
+ t.Errorf("Expected result to contain '%s', but
got:\n%s", expected, result)
+ }
+ }
+
+ // Verify no numeric formatting issues
+ if strings.Contains(result, "e+") || strings.Contains(result, "E+") {
+ t.Errorf("Result contains scientific notation, should be plain
numbers: %s", result)
+ }
+}
+
+func TestStringParameterToNumericConversion(t *testing.T) {
+ // String parameters should be converted to numbers when template
default is numeric
+ templateContent := `replicas: {{DefaultParam "REPLICA_COUNT" 5}}
+timeout: {{DefaultParam "TIMEOUT_SECONDS" 30}}
+calculated: {{Add (DefaultParam "BASE_VALUE" 100) 50}}`
+
+ // Create temp directory and file
+ tempDir, err := os.MkdirTemp("", "string_to_numeric_test")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ templatePath := filepath.Join(tempDir, "numeric_default_template.yaml")
+ err = os.WriteFile(templatePath, []byte(templateContent), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write template file: %v", err)
+ }
+
+ // String parameters
+ stringParams := map[string]interface{}{
+ "REPLICA_COUNT": "10", // string value
+ "TIMEOUT_SECONDS": "60", // string value
+ "BASE_VALUE": "200", // string value for math operation
+ }
+
+ processor := NewTemplateProcessor(stringParams)
+ result, err := processor.ProcessConfigFile(templatePath)
+ if err != nil {
+ t.Fatalf("Failed to process template: %v", err)
+ }
+
+ // Verify the string parameters were converted to numbers (no quotes in
YAML output)
+ expectedValues := []string{
+ "replicas: 10",
+ "timeout: 60",
+ "calculated: 250", // 200 + 50
+ }
+
+ for _, expected := range expectedValues {
+ if !strings.Contains(result, expected) {
+ t.Errorf("Expected result to contain '%s', but
got:\n%s", expected, result)
+ }
+ }
+
+ // Verify no quotes around numeric values
+ unexpectedValues := []string{
+ `replicas: "10"`,
+ `timeout: "60"`,
+ `calculated: "250"`,
+ }
+
+ for _, unexpected := range unexpectedValues {
+ if strings.Contains(result, unexpected) {
+ t.Errorf("Result should not contain quoted numeric
value '%s', but got:\n%s", unexpected, result)
+ }
+ }
+}
+
+func TestAutoscalerNodeTemplateProcessing(t *testing.T) {
+ // Test the exact scenario used in autoscaler setup
+ nodeTemplateContent := `apiVersion: v1
+kind: Node
+metadata:
+ annotations:
+ cluster-autoscaler.kwok.nodegroup/max-count: "{{DefaultParam
"MAX_COUNT" "100"}}"
+ cluster-autoscaler.kwok.nodegroup/min-count: "{{DefaultParam
"MIN_COUNT" "1"}}"
+ cluster-autoscaler.kwok.nodegroup/desired-count: "{{DefaultParam
"DESIRED_COUNT" "10"}}"
+ name: kwok-node`
+
+ // Create temp directory and file
+ tempDir, err := os.MkdirTemp("", "autoscaler_node_test")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ templatePath := filepath.Join(tempDir, "node_template.yaml")
+ err = os.WriteFile(templatePath, []byte(nodeTemplateContent), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write template file: %v", err)
+ }
+
+ // Simulate the exact parameters passed from autoscaler setup
+ // These are already converted to strings using fmt.Sprintf("%v", val)
+ nodeParams := map[string]interface{}{
+ "MAX_COUNT": "1000", // string value (converted from
numeric config)
+ "MIN_COUNT": "20", // string value (converted from
numeric config)
+ "DESIRED_COUNT": "50", // string value (converted from
numeric config)
+ }
+
+ processor := NewTemplateProcessor(nodeParams)
+ result, err := processor.ProcessConfigFile(templatePath)
+ if err != nil {
+ t.Fatalf("Failed to process node template: %v", err)
+ }
+
+ t.Logf("Processed node template result:\n%s", result)
+
+ // Verify the parameters were substituted correctly as quoted strings
+ expectedValues := []string{
+ `max-count: "1000"`,
+ `min-count: "20"`,
+ `desired-count: "50"`,
+ }
+
+ for _, expected := range expectedValues {
+ if !strings.Contains(result, expected) {
+ t.Errorf("Expected result to contain '%s', but
got:\n%s", expected, result)
+ }
+ }
+}
+
+func TestNodeTemplateProcessing(t *testing.T) {
+ // Test processing of node template similar to kwok-node-template.yaml
+ nodeTemplateContent := `apiVersion: v1
+kind: Node
+metadata:
+ annotations:
+ cluster-autoscaler.kwok.nodegroup/max-count: {{DefaultParam
"MAX_COUNT" "100"}}
+ cluster-autoscaler.kwok.nodegroup/min-count: {{DefaultParam
"MIN_COUNT" "1"}}
+ cluster-autoscaler.kwok.nodegroup/desired-count: {{DefaultParam
"DESIRED_COUNT" "10"}}
+ name: kwok-node
+`
+
+ // Create temp directory and file
+ tempDir, err := os.MkdirTemp("", "node_template_test")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tempDir)
+
+ templatePath := filepath.Join(tempDir, "node_template.yaml")
+ err = os.WriteFile(templatePath, []byte(nodeTemplateContent), 0644)
+ if err != nil {
+ t.Fatalf("Failed to write template file: %v", err)
+ }
+
+ // Test with specific node parameters
+ nodeParams := map[string]interface{}{
+ "MAX_COUNT": "1000",
+ "MIN_COUNT": "20",
+ "DESIRED_COUNT": "50",
+ }
+
+ processor := NewTemplateProcessor(nodeParams)
+ result, err := processor.ProcessConfigFile(templatePath)
+ if err != nil {
+ t.Fatalf("Failed to process node template: %v", err)
+ }
+
+ // Verify the parameters were substituted correctly
+ expectedValues := []string{
+ "max-count: 1000",
+ "min-count: 20",
+ "desired-count: 50",
+ }
+
+ for _, expected := range expectedValues {
+ if !strings.Contains(result, expected) {
+ t.Errorf("Expected result to contain '%s', but
got:\n%s", expected, result)
+ }
+ }
+}
+
+func TestErrorHandling(t *testing.T) {
+ processor := NewTemplateProcessor(nil)
+
+ t.Run("mathOp with invalid operation", func(t *testing.T) {
+ result := processor.add(10, 5)
+ if result != 15.0 {
+ t.Errorf("Expected 15, got %v", result)
+ }
+
+ _, err := processor.mathOp(10, 5, "invalid")
+ if err == nil {
+ t.Error("Expected error for invalid operation, got nil")
+ }
+ if err != nil && !strings.Contains(err.Error(), "unknown
operation") {
+ t.Errorf("Expected 'unknown operation' error, got: %v",
err)
+ }
+
+ _, err = processor.mathOp(10, 0, "/")
+ if err == nil {
+ t.Error("Expected error for division by zero, got nil")
+ }
+ if err != nil && !strings.Contains(err.Error(), "division by
zero") {
+ t.Errorf("Expected 'division by zero' error, got: %v",
err)
+ }
+ })
+
+ t.Run("toString with unsupported type", func(t *testing.T) {
+ result, err := processor.toString(42)
+ if err != nil {
+ t.Errorf("Expected no error for int, got: %v", err)
+ }
+ if result != "42" {
+ t.Errorf("Expected '42', got '%s'", result)
+ }
+
+ complex := complex(1, 2)
+ _, err = processor.toString(complex)
+ if err == nil {
+ t.Error("Expected error for complex number, got nil")
+ }
+ if err != nil && !strings.Contains(err.Error(), "unsupported
type") {
+ t.Errorf("Expected 'unsupported type' error, got: %v",
err)
+ }
+
+ safeResult := processor.safeToString(complex)
+ if safeResult == "" {
+ t.Error("Expected non-empty string from safeToString")
+ }
+ })
+}
\ No newline at end of file
diff --git a/soak/templates/kwok-node-template.yaml
b/soak/templates/kwok-node-template.yaml
index c33e8c1..718541d 100644
--- a/soak/templates/kwok-node-template.yaml
+++ b/soak/templates/kwok-node-template.yaml
@@ -18,9 +18,9 @@ apiVersion: v1
kind: Node
metadata:
annotations:
- cluster-autoscaler.kwok.nodegroup/max-count: {{$MAX_COUNT}}
- cluster-autoscaler.kwok.nodegroup/min-count: {{$MIN_COUNT}}
- cluster-autoscaler.kwok.nodegroup/desired-count: {{$DESIRED_COUNT}}
+ cluster-autoscaler.kwok.nodegroup/max-count: "{{DefaultParam "MAX_COUNT"
"100"}}"
+ cluster-autoscaler.kwok.nodegroup/min-count: "{{DefaultParam "MIN_COUNT"
"1"}}"
+ cluster-autoscaler.kwok.nodegroup/desired-count: "{{DefaultParam
"DESIRED_COUNT" "10"}}"
labels:
beta.kubernetes.io/arch: amd64
beta.kubernetes.io/os: linux
diff --git a/soak/tests/basic-scheduler-throughput/pod-default.yaml
b/soak/tests/basic-scheduler-throughput/pod-default.yaml
index 74d600f..7f2df41 100644
--- a/soak/tests/basic-scheduler-throughput/pod-default.yaml
+++ b/soak/tests/basic-scheduler-throughput/pod-default.yaml
@@ -3,7 +3,7 @@ kind: Pod
metadata:
generateName: pod-churn-
labels:
- group: {{.Group}}
+ group: {{.Group}}
spec:
schedulerName: yunikorn
affinity:
@@ -19,7 +19,7 @@ spec:
tolerations:
- key: "kwok-provider"
operator: "Exists"
- effect: "NoSchedule"
+ effect: "NoSchedule"
containers:
- image: registry.k8s.io/pause:3.9
name: pause
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]