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  }