cuelang.org/go@v0.10.1/mod/modfile/modfile_test.go (about)

     1  // Copyright 2023 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package modfile
    16  
    17  import (
    18  	"strings"
    19  	"testing"
    20  
    21  	"github.com/go-quicktest/qt"
    22  	"github.com/google/go-cmp/cmp/cmpopts"
    23  
    24  	"cuelang.org/go/cue/errors"
    25  	"cuelang.org/go/internal/cuetest"
    26  	"cuelang.org/go/internal/cueversion"
    27  	"cuelang.org/go/mod/module"
    28  )
    29  
    30  var parseTests = []struct {
    31  	testName     string
    32  	parse        func(modfile []byte, filename string) (*File, error)
    33  	data         string
    34  	wantError    string
    35  	want         *File
    36  	wantVersions []module.Version
    37  	wantDefaults map[string]string
    38  }{{
    39  	testName: "NoDeps",
    40  	parse:    Parse,
    41  	data: `
    42  module: "foo.com/bar@v0"
    43  language: version: "v0.8.0-alpha.0"
    44  `,
    45  	want: &File{
    46  		Module: "foo.com/bar@v0",
    47  		Language: &Language{
    48  			Version: "v0.8.0-alpha.0",
    49  		},
    50  	},
    51  	wantDefaults: map[string]string{
    52  		"foo.com/bar": "v0",
    53  	},
    54  }, {
    55  	testName: "WithDeps",
    56  	parse:    Parse,
    57  	data: `
    58  module: "foo.com/bar@v0"
    59  language: version: "v0.8.1"
    60  deps: "example.com@v1": {
    61  	default: true
    62  	v: "v1.2.3"
    63  }
    64  deps: "other.com/something@v0": v: "v0.2.3"
    65  `,
    66  	want: &File{
    67  		Language: &Language{
    68  			Version: "v0.8.1",
    69  		},
    70  		Module: "foo.com/bar@v0",
    71  		Deps: map[string]*Dep{
    72  			"example.com@v1": {
    73  				Default: true,
    74  				Version: "v1.2.3",
    75  			},
    76  			"other.com/something@v0": {
    77  				Version: "v0.2.3",
    78  			},
    79  		},
    80  	},
    81  	wantVersions: parseVersions("example.com@v1.2.3", "other.com/something@v0.2.3"),
    82  	wantDefaults: map[string]string{
    83  		"foo.com/bar": "v0",
    84  		"example.com": "v1",
    85  	},
    86  }, {
    87  	testName: "WithSource",
    88  	parse:    Parse,
    89  	data: `
    90  module: "foo.com/bar@v0"
    91  language: version: "v0.9.0-alpha.0"
    92  source: kind: "git"
    93  `,
    94  	want: &File{
    95  		Language: &Language{
    96  			Version: "v0.9.0-alpha.0",
    97  		},
    98  		Module: "foo.com/bar@v0",
    99  		Source: &Source{
   100  			Kind: "git",
   101  		},
   102  	},
   103  	wantDefaults: map[string]string{
   104  		"foo.com/bar": "v0",
   105  	},
   106  }, {
   107  	testName: "WithExplicitSource",
   108  	parse:    Parse,
   109  	data: `
   110  module: "foo.com/bar@v0"
   111  language: version: "v0.9.0-alpha.0"
   112  source: kind: "self"
   113  `,
   114  	want: &File{
   115  		Language: &Language{
   116  			Version: "v0.9.0-alpha.0",
   117  		},
   118  		Module: "foo.com/bar@v0",
   119  		Source: &Source{
   120  			Kind: "self",
   121  		},
   122  	},
   123  	wantDefaults: map[string]string{
   124  		"foo.com/bar": "v0",
   125  	},
   126  }, {
   127  	testName: "WithUnknownSourceKind",
   128  	parse:    Parse,
   129  	data: `
   130  module: "foo.com/bar@v0"
   131  language: version: "v0.9.0-alpha.0"
   132  source: kind: "bad"
   133  `,
   134  	wantError: `source.kind: 2 errors in empty disjunction:(.|\n)+`,
   135  }, {
   136  	testName: "WithEarlierVersionAndSource",
   137  	parse:    Parse,
   138  	data: `
   139  module: "foo.com/bar@v0"
   140  language: version: "v0.8.6"
   141  source: kind: "git"
   142  `,
   143  	wantError: `invalid module.cue file: source field is not allowed at this language version; need at least v0.9.0-alpha.0`,
   144  }, {
   145  	testName: "AmbiguousDefaults",
   146  	parse:    Parse,
   147  	data: `
   148  module: "foo.com/bar@v0"
   149  language: version: "v0.8.0"
   150  deps: "example.com@v1": {
   151  	default: true
   152  	v: "v1.2.3"
   153  }
   154  deps: "example.com@v2": {
   155  	default: true
   156  	v: "v2.0.0"
   157  }
   158  `,
   159  	wantError: `multiple default major versions found for example.com`,
   160  }, {
   161  	testName: "AmbiguousDefaultsWithMainModule",
   162  	parse:    Parse,
   163  	data: `
   164  module: "foo.com/bar@v0"
   165  language: version: "v0.8.0"
   166  deps: "foo.com/bar@v1": {
   167  	default: true
   168  	v: "v1.2.3"
   169  }
   170  `,
   171  	wantError: `multiple default major versions found for foo.com/bar`,
   172  }, {
   173  	testName: "MisspelledLanguageVersionField",
   174  	parse:    Parse,
   175  	data: `
   176  module: "foo.com/bar@v0"
   177  langugage: version: "v0.4.3"
   178  `,
   179  	wantError: `no language version declared in module.cue`,
   180  }, {
   181  	testName: "MissingLanguageVersionField",
   182  	parse:    Parse,
   183  	data: `
   184  module: "foo.com/bar@v0"
   185  `,
   186  	wantError: `no language version declared in module.cue`,
   187  }, {
   188  	testName: "InvalidLanguageVersion",
   189  	parse:    Parse,
   190  	data: `
   191  language: version: "vblah"
   192  module: "foo.com/bar@v0"`,
   193  	wantError: `language version "vblah" in module.cue is not valid semantic version`,
   194  }, {
   195  	testName: "EmptyLanguageVersion",
   196  	parse:    Parse,
   197  	data: `
   198  language: {}
   199  module: "foo.com/bar@v0"`,
   200  	wantError: `no language version declared in module.cue`,
   201  }, {
   202  	testName: "NonCanonicalLanguageVersion",
   203  	parse:    Parse,
   204  	data: `
   205  module: "foo.com/bar@v0"
   206  language: version: "v0.8"
   207  `,
   208  	wantError: `language version v0.8 in module.cue is not canonical`,
   209  }, {
   210  	testName: "InvalidDepVersion",
   211  	parse:    Parse,
   212  	data: `
   213  module: "foo.com/bar@v1"
   214  language: version: "v0.8.0"
   215  deps: "example.com@v1": v: "1.2.3"
   216  `,
   217  	wantError: `invalid module.cue file module.cue: cannot make version from module "example.com@v1", version "1.2.3": version "1.2.3" \(of module "example.com@v1"\) is not well formed`,
   218  }, {
   219  	testName: "NonCanonicalVersion",
   220  	parse:    Parse,
   221  	data: `
   222  module: "foo.com/bar@v1"
   223  language: version: "v0.8.0"
   224  deps: "example.com@v1": v: "v1.2"
   225  `,
   226  	wantError: `invalid module.cue file module.cue: cannot make version from module "example.com@v1", version "v1.2": version "v1.2" \(of module "example.com@v1"\) is not canonical`,
   227  }, {
   228  	testName: "NonCanonicalModule",
   229  	parse:    Parse,
   230  	data: `
   231  module: "foo.com/bar@v0.1.2"
   232  language: version: "v0.8.0"
   233  `,
   234  	wantError: `module path foo.com/bar@v0.1.2 in "module.cue" should contain the major version only`,
   235  }, {
   236  	testName: "NonCanonicalDep",
   237  	parse:    Parse,
   238  	data: `
   239  module: "foo.com/bar@v1"
   240  language: version: "v0.8.0"
   241  deps: "example.com": v: "v1.2.3"
   242  `,
   243  	wantError: `invalid module.cue file module.cue: no major version in "example.com"`,
   244  }, {
   245  	testName: "MismatchedMajorVersion",
   246  	parse:    Parse,
   247  	data: `
   248  module: "foo.com/bar@v1"
   249  language: version: "v0.8.0"
   250  deps: "example.com@v1": v: "v0.1.2"
   251  `,
   252  	wantError: `invalid module.cue file module.cue: cannot make version from module "example.com@v1", version "v0.1.2": mismatched major version suffix in "example.com@v1" \(version v0.1.2\)`,
   253  }, {
   254  	testName: "NonStrictNoMajorVersions",
   255  	parse:    ParseNonStrict,
   256  	data: `
   257  module: "foo.com/bar"
   258  language: version: "v0.8.0"
   259  deps: "example.com": v: "v1.2.3"
   260  `,
   261  	want: &File{
   262  		Module:   "foo.com/bar",
   263  		Language: &Language{Version: "v0.8.0"},
   264  		Deps: map[string]*Dep{
   265  			"example.com": {
   266  				Version: "v1.2.3",
   267  			},
   268  		},
   269  	},
   270  	wantVersions: parseVersions("example.com@v1.2.3"),
   271  	wantDefaults: map[string]string{
   272  		"foo.com/bar": "v0",
   273  	},
   274  }, {
   275  	testName: "LegacyWithExtraFields",
   276  	parse:    ParseLegacy,
   277  	data: `
   278  module: "foo.com/bar"
   279  something: 4
   280  language: version: "xxx"
   281  `,
   282  	want: &File{
   283  		Module: "foo.com/bar",
   284  	},
   285  }, {
   286  	testName: "LegacyReferencesNotAllowed",
   287  	parse:    ParseLegacy,
   288  	data: `
   289  module: _foo
   290  _foo: "blah.example"
   291  `,
   292  	wantError: `invalid module.cue file syntax: references not allowed in data mode:
   293      module.cue:2:9`,
   294  }, {
   295  	testName: "LegacyNoModule",
   296  	parse:    ParseLegacy,
   297  	data:     "",
   298  	want:     &File{},
   299  }, {
   300  	testName: "LegacyEmptyModule",
   301  	parse:    ParseLegacy,
   302  	data:     `module: ""`,
   303  	want:     &File{},
   304  }, {
   305  	testName: "ReferencesNotAllowed#1",
   306  	parse:    Parse,
   307  	data: `
   308  module: "foo.com/bar"
   309  _foo: "v0.9.0"
   310  language: version: _foo
   311  `,
   312  	wantError: `invalid module.cue file syntax: references not allowed in data mode:
   313      module.cue:4:20`,
   314  }, {
   315  	testName: "ReferencesNotAllowed#2",
   316  	parse:    Parse,
   317  	data: `
   318  module: "foo.com/bar"
   319  let foo = "v0.9.0"
   320  language: version: foo
   321  `,
   322  	wantError: `invalid module.cue file syntax: references not allowed in data mode:
   323      module.cue:3:1
   324  invalid module.cue file syntax: references not allowed in data mode:
   325      module.cue:4:20`,
   326  }, {
   327  	testName: "DefinitionsNotAllowed",
   328  	parse:    Parse,
   329  	data: `
   330  module: "foo.com/bar"
   331  #x: "v0.9.0"
   332  language: version: "v0.9.0"
   333  `,
   334  	wantError: `invalid module.cue file syntax: definitions not allowed in data mode:
   335      module.cue:3:1`,
   336  }, {
   337  	testName: "CustomData",
   338  	parse:    Parse,
   339  	data: `
   340  module: "foo.com/bar@v0"
   341  language: version: "v0.9.0"
   342  custom: "somewhere.com": foo: true
   343  `,
   344  	want: &File{
   345  		Module:   "foo.com/bar@v0",
   346  		Language: &Language{Version: "v0.9.0"},
   347  		Custom: map[string]map[string]any{
   348  			"somewhere.com": {
   349  				"foo": true,
   350  			},
   351  		},
   352  	},
   353  	wantDefaults: map[string]string{
   354  		"foo.com/bar": "v0",
   355  	},
   356  }, {
   357  	testName: "FixLegacyWithModulePath",
   358  	parse:    FixLegacy,
   359  	data: `
   360  module: "foo.com/bar"
   361  `,
   362  	want: &File{
   363  		Module:   "foo.com/bar",
   364  		Language: &Language{Version: "v0.9.0"},
   365  	},
   366  	wantDefaults: map[string]string{
   367  		"foo.com/bar": "v0",
   368  	},
   369  }, {
   370  	testName: "FixLegacyWithoutModulePath",
   371  	parse:    FixLegacy,
   372  	data: `
   373  `,
   374  	want: &File{
   375  		Module:   "test.example",
   376  		Language: &Language{Version: "v0.9.0"},
   377  	},
   378  	wantDefaults: map[string]string{
   379  		"test.example": "v0",
   380  	},
   381  }, {
   382  	testName: "FixLegacyWithEmptyModulePath",
   383  	parse:    FixLegacy,
   384  	data: `
   385  module: ""
   386  `,
   387  	want: &File{
   388  		Module:   "test.example",
   389  		Language: &Language{Version: "v0.9.0"},
   390  	},
   391  	wantDefaults: map[string]string{
   392  		"test.example": "v0",
   393  	},
   394  }, {
   395  	testName: "FixLegacyWithCustomFields",
   396  	parse:    FixLegacy,
   397  	data: `
   398  module: "foo.com"
   399  some: true
   400  other: field: 123
   401  `,
   402  	want: &File{
   403  		Module:   "foo.com",
   404  		Language: &Language{Version: "v0.9.0"},
   405  		Custom: map[string]map[string]any{
   406  			"legacy": {
   407  				"some":  true,
   408  				"other": map[string]any{"field": 123},
   409  			},
   410  		},
   411  	},
   412  	wantDefaults: map[string]string{
   413  		"foo.com": "v0",
   414  	},
   415  }}
   416  
   417  func TestParse(t *testing.T) {
   418  	for _, test := range parseTests {
   419  		t.Run(test.testName, func(t *testing.T) {
   420  			f, err := test.parse([]byte(test.data), "module.cue")
   421  			if test.wantError != "" {
   422  				gotErr := strings.TrimSuffix(errors.Details(err, nil), "\n")
   423  				qt.Assert(t, qt.Matches(gotErr, test.wantError), qt.Commentf("error %v", err))
   424  				return
   425  			}
   426  			qt.Assert(t, qt.IsNil(err), qt.Commentf("details: %v", strings.TrimSuffix(errors.Details(err, nil), "\n")))
   427  			qt.Assert(t, fileEquals(f, test.want))
   428  			qt.Assert(t, qt.DeepEquals(f.DepVersions(), test.wantVersions))
   429  			qt.Assert(t, qt.DeepEquals(f.DefaultMajorVersions(), test.wantDefaults))
   430  			path, vers, ok := strings.Cut(f.Module, "@")
   431  			if ok {
   432  				qt.Assert(t, qt.Equals(f.QualifiedModule(), f.Module))
   433  				qt.Assert(t, qt.Equals(f.ModulePath(), path))
   434  				qt.Assert(t, qt.Equals(f.MajorVersion(), vers))
   435  			} else if f.Module == "" {
   436  				qt.Assert(t, qt.Equals(f.QualifiedModule(), ""))
   437  				qt.Assert(t, qt.Equals(f.ModulePath(), ""))
   438  				qt.Assert(t, qt.Equals(f.MajorVersion(), ""))
   439  			} else {
   440  				qt.Assert(t, qt.Equals(f.QualifiedModule(), f.Module+"@v0"))
   441  				qt.Assert(t, qt.Equals(f.ModulePath(), f.Module))
   442  				qt.Assert(t, qt.Equals(f.MajorVersion(), "v0"))
   443  			}
   444  		})
   445  	}
   446  }
   447  
   448  func TestFormat(t *testing.T) {
   449  	type formatTest struct {
   450  		name      string
   451  		file      *File
   452  		wantError string
   453  		want      string
   454  	}
   455  	tests := []formatTest{{
   456  		name: "WithLanguage",
   457  		file: &File{
   458  			Language: &Language{
   459  				Version: "v0.8.0",
   460  			},
   461  			Module: "foo.com/bar@v0",
   462  			Deps: map[string]*Dep{
   463  				"example.com@v1": {
   464  					Version: "v1.2.3",
   465  				},
   466  				"other.com/something@v0": {
   467  					Version: "v0.2.3",
   468  				},
   469  			},
   470  		},
   471  		want: `module: "foo.com/bar@v0"
   472  language: {
   473  	version: "v0.8.0"
   474  }
   475  deps: {
   476  	"example.com@v1": {
   477  		v: "v1.2.3"
   478  	}
   479  	"other.com/something@v0": {
   480  		v: "v0.2.3"
   481  	}
   482  }
   483  `}, {
   484  		name: "WithoutLanguage",
   485  		file: &File{
   486  			Module: "foo.com/bar@v0",
   487  			Language: &Language{
   488  				Version: "v0.8.0",
   489  			},
   490  		},
   491  		want: `module: "foo.com/bar@v0"
   492  language: {
   493  	version: "v0.8.0"
   494  }
   495  `}, {
   496  		name: "WithVersionTooEarly",
   497  		file: &File{
   498  			Module: "foo.com/bar@v0",
   499  			Language: &Language{
   500  				Version: "v0.4.3",
   501  			},
   502  		},
   503  		wantError: `cannot parse result: cannot find schema suitable for reading module file with language version "v0.4.3"`,
   504  	}, {
   505  		name: "WithInvalidModuleVersion",
   506  		file: &File{
   507  			Module: "foo.com/bar@v0",
   508  			Language: &Language{
   509  				Version: "badversion--",
   510  			},
   511  		},
   512  		wantError: `cannot parse result: language version "badversion--" in module.cue is not valid semantic version`,
   513  	}, {
   514  		name: "WithNonNilEmptyDeps",
   515  		file: &File{
   516  			Module: "foo.com/bar@v0",
   517  			Language: &Language{
   518  				Version: "v0.8.0",
   519  			},
   520  			Deps: map[string]*Dep{},
   521  		},
   522  		want: `module: "foo.com/bar@v0"
   523  language: {
   524  	version: "v0.8.0"
   525  }
   526  `,
   527  	}}
   528  	cuetest.Run(t, tests, func(t *cuetest.T, test *formatTest) {
   529  		data, err := test.file.Format()
   530  		if test.wantError != "" {
   531  			qt.Assert(t, qt.ErrorMatches(err, test.wantError))
   532  			return
   533  		}
   534  		qt.Assert(t, qt.IsNil(err))
   535  		t.Equal(string(data), test.want)
   536  
   537  		// Check that it round-trips.
   538  		f, err := Parse(data, "")
   539  		qt.Assert(t, qt.IsNil(err))
   540  		qt.Assert(t, fileEquals(f, test.file))
   541  	})
   542  }
   543  
   544  func TestEarliestClosedSchemaVersion(t *testing.T) {
   545  	qt.Assert(t, qt.Equals(EarliestClosedSchemaVersion(), "v0.8.0-alpha.0"))
   546  }
   547  
   548  func TestLatestKnownSchemaVersion(t *testing.T) {
   549  	qt.Assert(t, qt.Equals(LatestKnownSchemaVersion(), cueversion.LanguageVersion()))
   550  }
   551  
   552  func parseVersions(vs ...string) []module.Version {
   553  	vvs := make([]module.Version, 0, len(vs))
   554  	for _, v := range vs {
   555  		vvs = append(vvs, module.MustParseVersion(v))
   556  	}
   557  	return vvs
   558  }
   559  
   560  // fileEquals returns a checker that checks whether two File instances
   561  // are equal.
   562  func fileEquals(got, want *File) qt.Checker {
   563  	return qt.CmpEquals(got, want,
   564  		cmpopts.IgnoreUnexported(File{}),
   565  		cmpopts.EquateEmpty(),
   566  	)
   567  }