github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/runtime/buildscript/buildscript_test.go (about) 1 package buildscript 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "os" 7 "testing" 8 9 "github.com/go-openapi/strfmt" 10 "github.com/stretchr/testify/assert" 11 "github.com/stretchr/testify/require" 12 13 "github.com/ActiveState/cli/internal/rtutils/ptr" 14 "github.com/ActiveState/cli/pkg/platform/runtime/buildexpression" 15 ) 16 17 // toBuildExpression converts given script constructed by Participle into a buildexpression. 18 // This function should not be used to convert an arbitrary script to buildexpression. 19 // New*() populates the Expr field with the equivalent build expression. 20 // This function exists solely for testing that functionality. 21 func toBuildExpression(script *Script) (*buildexpression.BuildExpression, error) { 22 bytes, err := json.Marshal(script) 23 if err != nil { 24 return nil, err 25 } 26 return buildexpression.New(bytes) 27 } 28 29 func TestBasic(t *testing.T) { 30 script, err := New([]byte( 31 `at_time = "2000-01-01T00:00:00.000Z" 32 runtime = solve( 33 at_time = at_time, 34 platforms = ["linux", "windows"], 35 requirements = [ 36 Req(name = "python", namespace = "language"), 37 Req(name = "requests", namespace = "language/python", version = Eq(value = "3.10.10")) 38 ] 39 ) 40 41 main = runtime 42 `)) 43 require.NoError(t, err) 44 45 atTime, err := strfmt.ParseDateTime("2000-01-01T00:00:00.000Z") 46 require.NoError(t, err) 47 48 expr, err := toBuildExpression(script) 49 require.NoError(t, err) 50 51 assert.Equal(t, &Script{ 52 []*Assignment{ 53 {"at_time", &Value{Str: ptr.To(`"2000-01-01T00:00:00.000Z"`)}}, 54 {"runtime", &Value{ 55 FuncCall: &FuncCall{"solve", []*Value{ 56 {Assignment: &Assignment{"at_time", &Value{Ident: ptr.To(`at_time`)}}}, 57 {Assignment: &Assignment{ 58 "platforms", &Value{List: &[]*Value{ 59 {Str: ptr.To(`"linux"`)}, 60 {Str: ptr.To(`"windows"`)}, 61 }}, 62 }}, 63 {Assignment: &Assignment{ 64 "requirements", &Value{List: &[]*Value{ 65 {FuncCall: &FuncCall{ 66 Name: "Req", 67 Arguments: []*Value{ 68 {Assignment: &Assignment{ 69 "name", &Value{Str: ptr.To(`"python"`)}, 70 }}, 71 {Assignment: &Assignment{ 72 "namespace", &Value{Str: ptr.To(`"language"`)}, 73 }}, 74 }}}, 75 {FuncCall: &FuncCall{ 76 Name: "Req", 77 Arguments: []*Value{ 78 {Assignment: &Assignment{ 79 "name", &Value{Str: ptr.To(`"requests"`)}, 80 }}, 81 {Assignment: &Assignment{ 82 "namespace", &Value{Str: ptr.To(`"language/python"`)}, 83 }}, 84 {Assignment: &Assignment{ 85 "version", &Value{FuncCall: &FuncCall{ 86 Name: "Eq", 87 Arguments: []*Value{ 88 {Assignment: &Assignment{Key: "value", Value: &Value{Str: ptr.To(`"3.10.10"`)}}}, 89 }, 90 }}, 91 }}, 92 }, 93 }}, 94 }}, 95 }}, 96 }}, 97 }}, 98 {"main", &Value{Ident: ptr.To("runtime")}}, 99 }, 100 &atTime, 101 expr, 102 }, script) 103 } 104 105 func TestComplex(t *testing.T) { 106 script, err := New([]byte( 107 `at_time = "2000-01-01T00:00:00.000Z" 108 linux_runtime = solve( 109 at_time = at_time, 110 requirements=[ 111 Req(name = "python", namespace = "language") 112 ], 113 platforms=["67890"] 114 ) 115 116 win_runtime = solve( 117 at_time = at_time, 118 requirements=[ 119 Req(name = "perl", namespace = "language") 120 ], 121 platforms=["12345"] 122 ) 123 124 main = merge( 125 win_installer(win_runtime), 126 tar_installer(linux_runtime) 127 ) 128 `)) 129 require.NoError(t, err) 130 131 atTime, err := strfmt.ParseDateTime("2000-01-01T00:00:00.000Z") 132 require.NoError(t, err) 133 134 expr, err := toBuildExpression(script) 135 require.NoError(t, err) 136 137 assert.Equal(t, &Script{ 138 []*Assignment{ 139 {"at_time", &Value{Str: ptr.To(`"2000-01-01T00:00:00.000Z"`)}}, 140 {"linux_runtime", &Value{ 141 FuncCall: &FuncCall{"solve", []*Value{ 142 {Assignment: &Assignment{"at_time", &Value{Ident: ptr.To(`at_time`)}}}, 143 {Assignment: &Assignment{ 144 "requirements", &Value{List: &[]*Value{ 145 {FuncCall: &FuncCall{ 146 Name: "Req", 147 Arguments: []*Value{ 148 {Assignment: &Assignment{ 149 "name", &Value{Str: ptr.To(`"python"`)}, 150 }}, 151 {Assignment: &Assignment{ 152 "namespace", &Value{Str: ptr.To(`"language"`)}, 153 }}, 154 }, 155 }}, 156 }}, 157 }}, 158 {Assignment: &Assignment{ 159 "platforms", &Value{List: &[]*Value{ 160 {Str: ptr.To(`"67890"`)}}, 161 }, 162 }}, 163 }}, 164 }}, 165 {"win_runtime", &Value{ 166 FuncCall: &FuncCall{"solve", []*Value{ 167 {Assignment: &Assignment{"at_time", &Value{Ident: ptr.To(`at_time`)}}}, 168 {Assignment: &Assignment{ 169 "requirements", &Value{List: &[]*Value{ 170 {FuncCall: &FuncCall{ 171 Name: "Req", 172 Arguments: []*Value{ 173 {Assignment: &Assignment{ 174 "name", &Value{Str: ptr.To(`"perl"`)}, 175 }}, 176 {Assignment: &Assignment{ 177 "namespace", &Value{Str: ptr.To(`"language"`)}, 178 }}, 179 }, 180 }}, 181 }}, 182 }}, 183 {Assignment: &Assignment{ 184 "platforms", &Value{List: &[]*Value{ 185 {Str: ptr.To(`"12345"`)}}, 186 }, 187 }}, 188 }}, 189 }}, 190 {"main", &Value{ 191 FuncCall: &FuncCall{"merge", []*Value{ 192 {FuncCall: &FuncCall{"win_installer", []*Value{{Ident: ptr.To("win_runtime")}}}}, 193 {FuncCall: &FuncCall{"tar_installer", []*Value{{Ident: ptr.To("linux_runtime")}}}}, 194 }}}}, 195 }, 196 &atTime, 197 expr, 198 }, script) 199 } 200 201 const example = `at_time = "2023-04-27T17:30:05.999Z" 202 runtime = solve( 203 at_time = at_time, 204 platforms = ["96b7e6f2-bebf-564c-bc1c-f04482398f38", "96b7e6f2-bebf-564c-bc1c-f04482398f38"], 205 requirements = [ 206 Req(name = "python", namespace = "language"), 207 Req(name = "requests", namespace = "language/python", version = Eq(value = "3.10.10")), 208 Req(name = "argparse", namespace = "language/python", version = And(left = Gt(value = "1.0"), right = Lt(value = "2.0"))) 209 ], 210 solver_version = 0 211 ) 212 213 main = runtime` 214 215 func TestExample(t *testing.T) { 216 script, err := New([]byte(example)) 217 require.NoError(t, err) 218 219 atTime, err := strfmt.ParseDateTime("2023-04-27T17:30:05.999Z") 220 require.NoError(t, err) 221 222 expr, err := toBuildExpression(script) 223 require.NoError(t, err) 224 225 assert.Equal(t, &Script{ 226 []*Assignment{ 227 {"at_time", &Value{Str: ptr.To(`"2023-04-27T17:30:05.999Z"`)}}, 228 {"runtime", &Value{ 229 FuncCall: &FuncCall{"solve", []*Value{ 230 {Assignment: &Assignment{ 231 "at_time", &Value{Ident: ptr.To(`at_time`)}, 232 }}, 233 {Assignment: &Assignment{ 234 "platforms", &Value{List: &[]*Value{ 235 {Str: ptr.To(`"96b7e6f2-bebf-564c-bc1c-f04482398f38"`)}, 236 {Str: ptr.To(`"96b7e6f2-bebf-564c-bc1c-f04482398f38"`)}, 237 }}, 238 }}, 239 {Assignment: &Assignment{ 240 "requirements", &Value{List: &[]*Value{ 241 {FuncCall: &FuncCall{ 242 Name: "Req", 243 Arguments: []*Value{ 244 {Assignment: &Assignment{ 245 "name", &Value{Str: ptr.To(`"python"`)}, 246 }}, 247 {Assignment: &Assignment{ 248 "namespace", &Value{Str: ptr.To(`"language"`)}, 249 }}, 250 }, 251 }}, 252 {FuncCall: &FuncCall{ 253 Name: "Req", 254 Arguments: []*Value{ 255 {Assignment: &Assignment{ 256 "name", &Value{Str: ptr.To(`"requests"`)}, 257 }}, 258 {Assignment: &Assignment{ 259 "namespace", &Value{Str: ptr.To(`"language/python"`)}, 260 }}, 261 {Assignment: &Assignment{ 262 "version", &Value{FuncCall: &FuncCall{ 263 Name: "Eq", 264 Arguments: []*Value{ 265 {Assignment: &Assignment{Key: "value", Value: &Value{Str: ptr.To(`"3.10.10"`)}}}, 266 }, 267 }}, 268 }}, 269 }, 270 }}, 271 {FuncCall: &FuncCall{ 272 Name: "Req", 273 Arguments: []*Value{ 274 {Assignment: &Assignment{ 275 "name", &Value{Str: ptr.To(`"argparse"`)}, 276 }}, 277 {Assignment: &Assignment{ 278 "namespace", &Value{Str: ptr.To(`"language/python"`)}, 279 }}, 280 {Assignment: &Assignment{ 281 "version", &Value{FuncCall: &FuncCall{ 282 Name: "And", 283 Arguments: []*Value{ 284 {Assignment: &Assignment{Key: "left", Value: &Value{FuncCall: &FuncCall{ 285 Name: "Gt", 286 Arguments: []*Value{ 287 {Assignment: &Assignment{Key: "value", Value: &Value{Str: ptr.To(`"1.0"`)}}}, 288 }, 289 }}}}, 290 {Assignment: &Assignment{Key: "right", Value: &Value{FuncCall: &FuncCall{ 291 Name: "Lt", 292 Arguments: []*Value{ 293 {Assignment: &Assignment{Key: "value", Value: &Value{Str: ptr.To(`"2.0"`)}}}, 294 }, 295 }}}}, 296 }, 297 }}, 298 }}, 299 }, 300 }}, 301 }}, 302 }}, 303 {Assignment: &Assignment{ 304 "solver_version", &Value{Number: ptr.To(float64(0))}, 305 }}, 306 }}, 307 }}, 308 {"main", &Value{Ident: ptr.To("runtime")}}, 309 }, 310 &atTime, 311 expr, 312 }, script) 313 } 314 315 func TestString(t *testing.T) { 316 script, err := New([]byte( 317 `at_time = "2000-01-01T00:00:00.000Z" 318 runtime = solve( 319 at_time = at_time, 320 platforms=["12345", "67890"], 321 requirements=[Req(name = "python", namespace = "language", version = Eq(value = "3.10.10"))] 322 ) 323 324 main = runtime 325 `)) 326 require.NoError(t, err) 327 328 assert.Equal(t, 329 `at_time = "2000-01-01T00:00:00.000Z" 330 runtime = solve( 331 at_time = at_time, 332 platforms = [ 333 "12345", 334 "67890" 335 ], 336 requirements = [ 337 Req(name = "python", namespace = "language", version = Eq(value = "3.10.10")) 338 ] 339 ) 340 341 main = runtime`, script.String()) 342 } 343 344 func TestRoundTrip(t *testing.T) { 345 tmpfile, err := os.CreateTemp("", "buildscript-") 346 require.NoError(t, err) 347 defer os.Remove(tmpfile.Name()) 348 349 script, err := New([]byte(example)) 350 require.NoError(t, err) 351 352 _, err = tmpfile.Write([]byte(script.String())) 353 require.NoError(t, err) 354 err = tmpfile.Close() 355 require.NoError(t, err) 356 357 roundTripScript, err := ScriptFromFile(tmpfile.Name()) 358 require.NoError(t, err) 359 360 assert.Equal(t, script, roundTripScript) 361 } 362 363 func TestJson(t *testing.T) { 364 script, err := New([]byte( 365 `at_time = "2000-01-01T00:00:00.000Z" 366 runtime = solve( 367 at_time = at_time, 368 requirements=[ 369 Req(name = "python", namespace = "language") 370 ], 371 platforms=["12345", "67890"] 372 ) 373 374 main = runtime 375 `)) 376 require.NoError(t, err) 377 378 inputJson := &bytes.Buffer{} 379 err = json.Compact(inputJson, []byte(`{ 380 "let": { 381 "runtime": { 382 "solve": { 383 "at_time": "$at_time", 384 "requirements": [ 385 { 386 "name": "python", 387 "namespace": "language" 388 } 389 ], 390 "platforms": ["12345", "67890"] 391 } 392 }, 393 "in": "$runtime" 394 } 395 }`)) 396 require.NoError(t, err) 397 // Cannot compare marshaled JSON directly with inputJson due to key sort order, so unmarshal and 398 // remarshal before making the comparison. json.Marshal() produces the same key sort order. 399 marshaledInput := make(map[string]interface{}) 400 err = json.Unmarshal(inputJson.Bytes(), &marshaledInput) 401 require.NoError(t, err) 402 expectedJson, err := json.Marshal(marshaledInput) 403 require.NoError(t, err) 404 405 actualJson, err := json.Marshal(script.Expr) 406 require.NoError(t, err) 407 assert.Equal(t, string(expectedJson), string(actualJson)) 408 } 409 410 func TestBuildExpression(t *testing.T) { 411 expr, err := buildexpression.New([]byte(`{ 412 "let": { 413 "runtime": { 414 "solve_legacy": { 415 "at_time": "2023-04-27T17:30:05.999Z", 416 "build_flags": [], 417 "camel_flags": [], 418 "platforms": [ 419 "96b7e6f2-bebf-564c-bc1c-f04482398f38" 420 ], 421 "requirements": [ 422 { 423 "name": "jinja2-time", 424 "namespace": "language/python" 425 }, 426 { 427 "name": "jupyter-contrib-nbextensions", 428 "namespace": "language/python" 429 }, 430 { 431 "name": "python", 432 "namespace": "language", 433 "version_requirements": [ 434 { 435 "comparator": "eq", 436 "version": "3.10.10" 437 } 438 ] 439 }, 440 { 441 "name": "copier", 442 "namespace": "language/python" 443 }, 444 { 445 "name": "jupyterlab", 446 "namespace": "language/python" 447 } 448 ], 449 "solver_version": null 450 } 451 }, 452 "in": "$runtime" 453 } 454 }`)) 455 require.NoError(t, err) 456 457 // Verify conversions between buildscripts and buildexpressions is accurate. 458 script, err := NewFromBuildExpression(nil, expr) 459 require.NoError(t, err) 460 require.NotNil(t, script) 461 newExpr := script.Expr 462 exprBytes, err := json.Marshal(expr) 463 require.NoError(t, err) 464 newExprBytes, err := json.Marshal(newExpr) 465 require.NoError(t, err) 466 assert.Equal(t, string(exprBytes), string(newExprBytes)) 467 468 // Verify comparisons between buildscripts is accurate. 469 newScript, err := NewFromBuildExpression(nil, newExpr) 470 require.NoError(t, err) 471 assert.True(t, script.Equals(newScript)) 472 473 // Verify null JSON value is handled correctly. 474 var null *string 475 nullHandled := false 476 for _, assignment := range newExpr.Let.Assignments { 477 if assignment.Name == "runtime" { 478 args := assignment.Value.Ap.Arguments 479 require.NotNil(t, args) 480 for _, arg := range args { 481 if arg.Assignment != nil && arg.Assignment.Name == "solver_version" { 482 assert.Equal(t, null, arg.Assignment.Value.Str) 483 nullHandled = true 484 } 485 } 486 } 487 } 488 assert.True(t, nullHandled, "JSON null not encountered") 489 }