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