github.com/databricks/cli@v0.203.0/bundle/config/interpolation/interpolation_test.go (about)

     1  package interpolation
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/databricks/cli/bundle/config"
     7  	"github.com/databricks/cli/bundle/config/variable"
     8  	"github.com/stretchr/testify/assert"
     9  	"github.com/stretchr/testify/require"
    10  )
    11  
    12  type nest struct {
    13  	X string            `json:"x"`
    14  	Y *string           `json:"y"`
    15  	Z map[string]string `json:"z"`
    16  }
    17  
    18  type foo struct {
    19  	A string `json:"a"`
    20  	B string `json:"b"`
    21  	C string `json:"c"`
    22  
    23  	// Pointer field
    24  	D *string `json:"d"`
    25  
    26  	// Struct field
    27  	E nest `json:"e"`
    28  
    29  	// Map field
    30  	F map[string]string `json:"f"`
    31  }
    32  
    33  func expand(v any) error {
    34  	a := accumulator{}
    35  	a.start(v)
    36  	return a.expand(DefaultLookup)
    37  }
    38  
    39  func TestInterpolationVariables(t *testing.T) {
    40  	f := foo{
    41  		A: "a",
    42  		B: "${a}",
    43  		C: "${a}",
    44  	}
    45  
    46  	err := expand(&f)
    47  	require.NoError(t, err)
    48  
    49  	assert.Equal(t, "a", f.A)
    50  	assert.Equal(t, "a", f.B)
    51  	assert.Equal(t, "a", f.C)
    52  }
    53  
    54  func TestInterpolationVariablesSpecialChars(t *testing.T) {
    55  	type bar struct {
    56  		A string `json:"a-b"`
    57  		B string `json:"b_c"`
    58  		C string `json:"c-_a"`
    59  	}
    60  	f := bar{
    61  		A: "a",
    62  		B: "${a-b}",
    63  		C: "${a-b}",
    64  	}
    65  
    66  	err := expand(&f)
    67  	require.NoError(t, err)
    68  
    69  	assert.Equal(t, "a", f.A)
    70  	assert.Equal(t, "a", f.B)
    71  	assert.Equal(t, "a", f.C)
    72  }
    73  
    74  func TestInterpolationValidMatches(t *testing.T) {
    75  	expectedMatches := map[string]string{
    76  		"${hello_world.world_world}": "hello_world.world_world",
    77  		"${helloworld.world-world}":  "helloworld.world-world",
    78  		"${hello-world.world-world}": "hello-world.world-world",
    79  	}
    80  	for interpolationStr, expectedMatch := range expectedMatches {
    81  		match := re.FindStringSubmatch(interpolationStr)
    82  		assert.True(t, len(match) > 0,
    83  			"Failed to match %s and find %s", interpolationStr, expectedMatch)
    84  		assert.Equal(t, expectedMatch, match[1],
    85  			"Failed to match the exact pattern %s and find %s", interpolationStr, expectedMatch)
    86  	}
    87  }
    88  
    89  func TestInterpolationInvalidMatches(t *testing.T) {
    90  	invalidMatches := []string{
    91  		"${hello_world-.world_world}",   // the first segment ending must not end with hyphen (-)
    92  		"${hello_world-_.world_world}",  // the first segment ending must not end with underscore (_)
    93  		"${helloworld.world-world-}",    // second segment must not end with hyphen (-)
    94  		"${helloworld-.world-world}",    // first segment must not end with hyphen (-)
    95  		"${helloworld.-world-world}",    // second segment must not start with hyphen (-)
    96  		"${-hello-world.-world-world-}", // must not start or end with hyphen (-)
    97  		"${_-_._-_.id}",                 // cannot use _- in sequence
    98  		"${0helloworld.world-world}",    // interpolated first section shouldn't start with number
    99  		"${helloworld.9world-world}",    // interpolated second section shouldn't start with number
   100  		"${a-a.a-_a-a.id}",              // fails because of -_ in the second segment
   101  		"${a-a.a--a-a.id}",              // fails because of -- in the second segment
   102  	}
   103  	for _, invalidMatch := range invalidMatches {
   104  		match := re.FindStringSubmatch(invalidMatch)
   105  		assert.True(t, len(match) == 0, "Should be invalid interpolation: %s", invalidMatch)
   106  	}
   107  }
   108  
   109  func TestInterpolationWithPointers(t *testing.T) {
   110  	fd := "${a}"
   111  	f := foo{
   112  		A: "a",
   113  		D: &fd,
   114  	}
   115  
   116  	err := expand(&f)
   117  	require.NoError(t, err)
   118  
   119  	assert.Equal(t, "a", f.A)
   120  	assert.Equal(t, "a", *f.D)
   121  }
   122  
   123  func TestInterpolationWithStruct(t *testing.T) {
   124  	fy := "${e.x}"
   125  	f := foo{
   126  		A: "${e.x}",
   127  		E: nest{
   128  			X: "x",
   129  			Y: &fy,
   130  		},
   131  	}
   132  
   133  	err := expand(&f)
   134  	require.NoError(t, err)
   135  
   136  	assert.Equal(t, "x", f.A)
   137  	assert.Equal(t, "x", f.E.X)
   138  	assert.Equal(t, "x", *f.E.Y)
   139  }
   140  
   141  func TestInterpolationWithMap(t *testing.T) {
   142  	f := foo{
   143  		A: "${f.a}",
   144  		F: map[string]string{
   145  			"a": "a",
   146  			"b": "${f.a}",
   147  		},
   148  	}
   149  
   150  	err := expand(&f)
   151  	require.NoError(t, err)
   152  
   153  	assert.Equal(t, "a", f.A)
   154  	assert.Equal(t, "a", f.F["a"])
   155  	assert.Equal(t, "a", f.F["b"])
   156  }
   157  
   158  func TestInterpolationWithResursiveVariableReferences(t *testing.T) {
   159  	f := foo{
   160  		A: "a",
   161  		B: "(${a})",
   162  		C: "${a} ${b}",
   163  	}
   164  
   165  	err := expand(&f)
   166  	require.NoError(t, err)
   167  
   168  	assert.Equal(t, "a", f.A)
   169  	assert.Equal(t, "(a)", f.B)
   170  	assert.Equal(t, "a (a)", f.C)
   171  }
   172  
   173  func TestInterpolationVariableLoopError(t *testing.T) {
   174  	d := "${b}"
   175  	f := foo{
   176  		A: "a",
   177  		B: "${c}",
   178  		C: "${d}",
   179  		D: &d,
   180  	}
   181  
   182  	err := expand(&f)
   183  	assert.ErrorContains(t, err, "cycle detected in field resolution: b -> c -> d -> b")
   184  }
   185  
   186  func TestInterpolationForVariables(t *testing.T) {
   187  	foo := "abc"
   188  	bar := "${var.foo} def"
   189  	apple := "${var.foo} ${var.bar}"
   190  	config := config.Root{
   191  		Variables: map[string]*variable.Variable{
   192  			"foo": {
   193  				Value: &foo,
   194  			},
   195  			"bar": {
   196  				Value: &bar,
   197  			},
   198  			"apple": {
   199  				Value: &apple,
   200  			},
   201  		},
   202  		Bundle: config.Bundle{
   203  			Name: "${var.apple} ${var.foo}",
   204  		},
   205  	}
   206  
   207  	err := expand(&config)
   208  	assert.NoError(t, err)
   209  	assert.Equal(t, "abc", *(config.Variables["foo"].Value))
   210  	assert.Equal(t, "abc def", *(config.Variables["bar"].Value))
   211  	assert.Equal(t, "abc abc def", *(config.Variables["apple"].Value))
   212  	assert.Equal(t, "abc abc def abc", config.Bundle.Name)
   213  }
   214  
   215  func TestInterpolationLoopForVariables(t *testing.T) {
   216  	foo := "${var.bar}"
   217  	bar := "${var.foo}"
   218  	config := config.Root{
   219  		Variables: map[string]*variable.Variable{
   220  			"foo": {
   221  				Value: &foo,
   222  			},
   223  			"bar": {
   224  				Value: &bar,
   225  			},
   226  		},
   227  		Bundle: config.Bundle{
   228  			Name: "${var.foo}",
   229  		},
   230  	}
   231  
   232  	err := expand(&config)
   233  	assert.ErrorContains(t, err, "cycle detected in field resolution: bundle.name -> var.foo -> var.bar -> var.foo")
   234  }
   235  
   236  func TestInterpolationInvalidVariableReference(t *testing.T) {
   237  	foo := "abc"
   238  	config := config.Root{
   239  		Variables: map[string]*variable.Variable{
   240  			"foo": {
   241  				Value: &foo,
   242  			},
   243  		},
   244  		Bundle: config.Bundle{
   245  			Name: "${vars.foo}",
   246  		},
   247  	}
   248  
   249  	err := expand(&config)
   250  	assert.ErrorContains(t, err, "could not resolve reference vars.foo")
   251  }