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 }