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]

Reply via email to