github.com/kanishk98/terraform@v1.3.0-dev.0.20220917174235-661ca8088a6a/internal/refactoring/move_validate_test.go (about) 1 package refactoring 2 3 import ( 4 "context" 5 "strings" 6 "testing" 7 8 "github.com/hashicorp/hcl/v2" 9 "github.com/hashicorp/hcl/v2/hclsyntax" 10 "github.com/zclconf/go-cty/cty/gocty" 11 12 "github.com/hashicorp/terraform/internal/addrs" 13 "github.com/hashicorp/terraform/internal/configs" 14 "github.com/hashicorp/terraform/internal/configs/configload" 15 "github.com/hashicorp/terraform/internal/initwd" 16 "github.com/hashicorp/terraform/internal/instances" 17 "github.com/hashicorp/terraform/internal/registry" 18 "github.com/hashicorp/terraform/internal/tfdiags" 19 ) 20 21 func TestValidateMoves(t *testing.T) { 22 rootCfg, instances := loadRefactoringFixture(t, "testdata/move-validate-zoo") 23 24 tests := map[string]struct { 25 Statements []MoveStatement 26 WantError string 27 }{ 28 "no move statements": { 29 Statements: nil, 30 WantError: ``, 31 }, 32 "some valid statements": { 33 Statements: []MoveStatement{ 34 // This is just a grab bag of various valid cases that don't 35 // generate any errors at all. 36 makeTestMoveStmt(t, 37 ``, 38 `test.nonexist1`, 39 `test.target1`, 40 ), 41 makeTestMoveStmt(t, 42 `single`, 43 `test.nonexist1`, 44 `test.target1`, 45 ), 46 makeTestMoveStmt(t, 47 ``, 48 `test.nonexist2`, 49 `module.nonexist.test.nonexist2`, 50 ), 51 makeTestMoveStmt(t, 52 ``, 53 `module.single.test.nonexist3`, 54 `module.single.test.single`, 55 ), 56 makeTestMoveStmt(t, 57 ``, 58 `module.single.test.nonexist4`, 59 `test.target2`, 60 ), 61 makeTestMoveStmt(t, 62 ``, 63 `test.single[0]`, // valid because test.single doesn't have "count" set 64 `test.target3`, 65 ), 66 makeTestMoveStmt(t, 67 ``, 68 `test.zero_count[0]`, // valid because test.zero_count has count = 0 69 `test.target4`, 70 ), 71 makeTestMoveStmt(t, 72 ``, 73 `test.zero_count[1]`, // valid because test.zero_count has count = 0 74 `test.zero_count[0]`, 75 ), 76 makeTestMoveStmt(t, 77 ``, 78 `module.nonexist1`, 79 `module.target3`, 80 ), 81 makeTestMoveStmt(t, 82 ``, 83 `module.nonexist1[0]`, 84 `module.target4`, 85 ), 86 makeTestMoveStmt(t, 87 ``, 88 `module.single[0]`, // valid because module.single doesn't have "count" set 89 `module.target5`, 90 ), 91 makeTestMoveStmt(t, 92 ``, 93 `module.for_each["nonexist1"]`, 94 `module.for_each["a"]`, 95 ), 96 makeTestMoveStmt(t, 97 ``, 98 `module.for_each["nonexist2"]`, 99 `module.nonexist.module.nonexist`, 100 ), 101 makeTestMoveStmt(t, 102 ``, 103 `module.for_each["nonexist3"].test.single`, // valid because module.for_each doesn't currently have a "nonexist3" 104 `module.for_each["a"].test.single`, 105 ), 106 }, 107 WantError: ``, 108 }, 109 "two statements with the same endpoints": { 110 Statements: []MoveStatement{ 111 makeTestMoveStmt(t, 112 ``, 113 `module.a`, 114 `module.b`, 115 ), 116 makeTestMoveStmt(t, 117 ``, 118 `module.a`, 119 `module.b`, 120 ), 121 }, 122 WantError: ``, 123 }, 124 "moving nowhere": { 125 Statements: []MoveStatement{ 126 makeTestMoveStmt(t, 127 ``, 128 `module.a`, 129 `module.a`, 130 ), 131 }, 132 WantError: `Redundant move statement: This statement declares a move from module.a to the same address, which is the same as not declaring this move at all.`, 133 }, 134 "cyclic chain": { 135 Statements: []MoveStatement{ 136 makeTestMoveStmt(t, 137 ``, 138 `module.a`, 139 `module.b`, 140 ), 141 makeTestMoveStmt(t, 142 ``, 143 `module.b`, 144 `module.c`, 145 ), 146 makeTestMoveStmt(t, 147 ``, 148 `module.c`, 149 `module.a`, 150 ), 151 }, 152 WantError: `Cyclic dependency in move statements: The following chained move statements form a cycle, and so there is no final location to move objects to: 153 - test:1,1: module.a[*] → module.b[*] 154 - test:1,1: module.b[*] → module.c[*] 155 - test:1,1: module.c[*] → module.a[*] 156 157 A chain of move statements must end with an address that doesn't appear in any other statements, and which typically also refers to an object still declared in the configuration.`, 158 }, 159 "module.single as a call still exists in configuration": { 160 Statements: []MoveStatement{ 161 makeTestMoveStmt(t, 162 ``, 163 `module.single`, 164 `module.other`, 165 ), 166 }, 167 WantError: `Moved object still exists: This statement declares a move from module.single, but that module call is still declared at testdata/move-validate-zoo/move-validate-root.tf:6,1. 168 169 Change your configuration so that this call will be declared as module.other instead.`, 170 }, 171 "module.single as an instance still exists in configuration": { 172 Statements: []MoveStatement{ 173 makeTestMoveStmt(t, 174 ``, 175 `module.single`, 176 `module.other[0]`, 177 ), 178 }, 179 WantError: `Moved object still exists: This statement declares a move from module.single, but that module instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:6,1. 180 181 Change your configuration so that this instance will be declared as module.other[0] instead.`, 182 }, 183 "module.count[0] still exists in configuration": { 184 Statements: []MoveStatement{ 185 makeTestMoveStmt(t, 186 ``, 187 `module.count[0]`, 188 `module.other`, 189 ), 190 }, 191 WantError: `Moved object still exists: This statement declares a move from module.count[0], but that module instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:12,12. 192 193 Change your configuration so that this instance will be declared as module.other instead.`, 194 }, 195 `module.for_each["a"] still exists in configuration`: { 196 Statements: []MoveStatement{ 197 makeTestMoveStmt(t, 198 ``, 199 `module.for_each["a"]`, 200 `module.other`, 201 ), 202 }, 203 WantError: `Moved object still exists: This statement declares a move from module.for_each["a"], but that module instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:22,14. 204 205 Change your configuration so that this instance will be declared as module.other instead.`, 206 }, 207 "test.single as a resource still exists in configuration": { 208 Statements: []MoveStatement{ 209 makeTestMoveStmt(t, 210 ``, 211 `test.single`, 212 `test.other`, 213 ), 214 }, 215 WantError: `Moved object still exists: This statement declares a move from test.single, but that resource is still declared at testdata/move-validate-zoo/move-validate-root.tf:27,1. 216 217 Change your configuration so that this resource will be declared as test.other instead.`, 218 }, 219 "test.single as an instance still exists in configuration": { 220 Statements: []MoveStatement{ 221 makeTestMoveStmt(t, 222 ``, 223 `test.single`, 224 `test.other[0]`, 225 ), 226 }, 227 WantError: `Moved object still exists: This statement declares a move from test.single, but that resource instance is still declared at testdata/move-validate-zoo/move-validate-root.tf:27,1. 228 229 Change your configuration so that this instance will be declared as test.other[0] instead.`, 230 }, 231 "module.single.test.single as a resource still exists in configuration": { 232 Statements: []MoveStatement{ 233 makeTestMoveStmt(t, 234 ``, 235 `module.single.test.single`, 236 `test.other`, 237 ), 238 }, 239 WantError: `Moved object still exists: This statement declares a move from module.single.test.single, but that resource is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1. 240 241 Change your configuration so that this resource will be declared as test.other instead.`, 242 }, 243 "module.single.test.single as a resource declared in module.single still exists in configuration": { 244 Statements: []MoveStatement{ 245 makeTestMoveStmt(t, 246 `single`, 247 `test.single`, 248 `test.other`, 249 ), 250 }, 251 WantError: `Moved object still exists: This statement declares a move from module.single.test.single, but that resource is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1. 252 253 Change your configuration so that this resource will be declared as module.single.test.other instead.`, 254 }, 255 "module.single.test.single as an instance still exists in configuration": { 256 Statements: []MoveStatement{ 257 makeTestMoveStmt(t, 258 ``, 259 `module.single.test.single`, 260 `test.other[0]`, 261 ), 262 }, 263 WantError: `Moved object still exists: This statement declares a move from module.single.test.single, but that resource instance is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1. 264 265 Change your configuration so that this instance will be declared as test.other[0] instead.`, 266 }, 267 "module.count[0].test.single still exists in configuration": { 268 Statements: []MoveStatement{ 269 makeTestMoveStmt(t, 270 ``, 271 `module.count[0].test.single`, 272 `test.other`, 273 ), 274 }, 275 WantError: `Moved object still exists: This statement declares a move from module.count[0].test.single, but that resource is still declared at testdata/move-validate-zoo/child/move-validate-child.tf:6,1. 276 277 Change your configuration so that this resource will be declared as test.other instead.`, 278 }, 279 "two different moves from test.nonexist": { 280 Statements: []MoveStatement{ 281 makeTestMoveStmt(t, 282 ``, 283 `test.nonexist`, 284 `test.other1`, 285 ), 286 makeTestMoveStmt(t, 287 ``, 288 `test.nonexist`, 289 `test.other2`, 290 ), 291 }, 292 WantError: `Ambiguous move statements: A statement at test:1,1 declared that test.nonexist moved to test.other1, but this statement instead declares that it moved to test.other2. 293 294 Each resource can move to only one destination resource.`, 295 }, 296 "two different moves to test.single": { 297 Statements: []MoveStatement{ 298 makeTestMoveStmt(t, 299 ``, 300 `test.other1`, 301 `test.single`, 302 ), 303 makeTestMoveStmt(t, 304 ``, 305 `test.other2`, 306 `test.single`, 307 ), 308 }, 309 WantError: `Ambiguous move statements: A statement at test:1,1 declared that test.other1 moved to test.single, but this statement instead declares that test.other2 moved there. 310 311 Each resource can have moved from only one source resource.`, 312 }, 313 "two different moves to module.count[0].test.single across two modules": { 314 Statements: []MoveStatement{ 315 makeTestMoveStmt(t, 316 ``, 317 `test.other1`, 318 `module.count[0].test.single`, 319 ), 320 makeTestMoveStmt(t, 321 `count`, 322 `test.other2`, 323 `test.single`, 324 ), 325 }, 326 WantError: `Ambiguous move statements: A statement at test:1,1 declared that test.other1 moved to module.count[0].test.single, but this statement instead declares that module.count[0].test.other2 moved there. 327 328 Each resource can have moved from only one source resource.`, 329 }, 330 "move from resource in another module package": { 331 Statements: []MoveStatement{ 332 makeTestMoveStmt(t, 333 ``, 334 `module.fake_external.test.thing`, 335 `test.thing`, 336 ), 337 }, 338 WantError: ``, 339 }, 340 "move to resource in another module package": { 341 Statements: []MoveStatement{ 342 makeTestMoveStmt(t, 343 ``, 344 `test.thing`, 345 `module.fake_external.test.thing`, 346 ), 347 }, 348 WantError: ``, 349 }, 350 "move from module call in another module package": { 351 Statements: []MoveStatement{ 352 makeTestMoveStmt(t, 353 ``, 354 `module.fake_external.module.a`, 355 `module.b`, 356 ), 357 }, 358 WantError: ``, 359 }, 360 "move to module call in another module package": { 361 Statements: []MoveStatement{ 362 makeTestMoveStmt(t, 363 ``, 364 `module.a`, 365 `module.fake_external.module.b`, 366 ), 367 }, 368 WantError: ``, 369 }, 370 "implied move from resource in another module package": { 371 Statements: []MoveStatement{ 372 makeTestImpliedMoveStmt(t, 373 ``, 374 `module.fake_external.test.thing`, 375 `test.thing`, 376 ), 377 }, 378 // Implied move statements are not subject to the cross-package restriction 379 WantError: ``, 380 }, 381 "implied move to resource in another module package": { 382 Statements: []MoveStatement{ 383 makeTestImpliedMoveStmt(t, 384 ``, 385 `test.thing`, 386 `module.fake_external.test.thing`, 387 ), 388 }, 389 // Implied move statements are not subject to the cross-package restriction 390 WantError: ``, 391 }, 392 "implied move from module call in another module package": { 393 Statements: []MoveStatement{ 394 makeTestImpliedMoveStmt(t, 395 ``, 396 `module.fake_external.module.a`, 397 `module.b`, 398 ), 399 }, 400 // Implied move statements are not subject to the cross-package restriction 401 WantError: ``, 402 }, 403 "implied move to module call in another module package": { 404 Statements: []MoveStatement{ 405 makeTestImpliedMoveStmt(t, 406 ``, 407 `module.a`, 408 `module.fake_external.module.b`, 409 ), 410 }, 411 // Implied move statements are not subject to the cross-package restriction 412 WantError: ``, 413 }, 414 "move to a call that refers to another module package": { 415 Statements: []MoveStatement{ 416 makeTestMoveStmt(t, 417 ``, 418 `module.nonexist`, 419 `module.fake_external`, 420 ), 421 }, 422 WantError: ``, // This is okay because the call itself is not considered to be inside the package it refers to 423 }, 424 "move to instance of a call that refers to another module package": { 425 Statements: []MoveStatement{ 426 makeTestMoveStmt(t, 427 ``, 428 `module.nonexist`, 429 `module.fake_external[0]`, 430 ), 431 }, 432 WantError: ``, // This is okay because the call itself is not considered to be inside the package it refers to 433 }, 434 "resource type mismatch": { 435 Statements: []MoveStatement{ 436 makeTestMoveStmt(t, ``, 437 `test.nonexist1`, 438 `other.single`, 439 ), 440 }, 441 WantError: `Resource type mismatch: This statement declares a move from test.nonexist1 to other.single, which is a resource of a different type.`, 442 }, 443 "resource instance type mismatch": { 444 Statements: []MoveStatement{ 445 makeTestMoveStmt(t, ``, 446 `test.nonexist1[0]`, 447 `other.single`, 448 ), 449 }, 450 WantError: `Resource type mismatch: This statement declares a move from test.nonexist1[0] to other.single, which is a resource instance of a different type.`, 451 }, 452 "crossing nested statements": { 453 // overlapping nested moves will result in a cycle. 454 Statements: []MoveStatement{ 455 makeTestMoveStmt(t, ``, 456 `module.nonexist.test.single`, 457 `module.count[0].test.count[0]`, 458 ), 459 makeTestMoveStmt(t, ``, 460 `module.nonexist`, 461 `module.count[0]`, 462 ), 463 }, 464 WantError: `Cyclic dependency in move statements: The following chained move statements form a cycle, and so there is no final location to move objects to: 465 - test:1,1: module.nonexist → module.count[0] 466 - test:1,1: module.nonexist.test.single → module.count[0].test.count[0] 467 468 A chain of move statements must end with an address that doesn't appear in any other statements, and which typically also refers to an object still declared in the configuration.`, 469 }, 470 "fully contained nested statements": { 471 // we have to avoid a cycle because the nested moves appear in both 472 // the from and to address of the parent when only the module index 473 // is changing. 474 Statements: []MoveStatement{ 475 makeTestMoveStmt(t, `count`, 476 `test.count`, 477 `test.count[0]`, 478 ), 479 makeTestMoveStmt(t, ``, 480 `module.count`, 481 `module.count[0]`, 482 ), 483 }, 484 }, 485 "double fully contained nested statements": { 486 // we have to avoid a cycle because the nested moves appear in both 487 // the from and to address of the parent when only the module index 488 // is changing. 489 Statements: []MoveStatement{ 490 makeTestMoveStmt(t, `count`, 491 `module.count`, 492 `module.count[0]`, 493 ), 494 makeTestMoveStmt(t, `count.count`, 495 `test.count`, 496 `test.count[0]`, 497 ), 498 makeTestMoveStmt(t, ``, 499 `module.count`, 500 `module.count[0]`, 501 ), 502 }, 503 }, 504 } 505 506 for name, test := range tests { 507 t.Run(name, func(t *testing.T) { 508 gotDiags := ValidateMoves(test.Statements, rootCfg, instances) 509 510 switch { 511 case test.WantError != "": 512 if !gotDiags.HasErrors() { 513 t.Fatalf("unexpected success\nwant error: %s", test.WantError) 514 } 515 if got, want := gotDiags.Err().Error(), test.WantError; got != want { 516 t.Fatalf("wrong error\ngot error: %s\nwant error: %s", got, want) 517 } 518 default: 519 if gotDiags.HasErrors() { 520 t.Fatalf("unexpected error\ngot error: %s", gotDiags.Err().Error()) 521 } 522 } 523 }) 524 } 525 } 526 527 // loadRefactoringFixture reads a configuration from the given directory and 528 // does some naive static processing on any count and for_each expressions 529 // inside, in order to get a realistic-looking instances.Set for what it 530 // declares without having to run a full Terraform plan. 531 func loadRefactoringFixture(t *testing.T, dir string) (*configs.Config, instances.Set) { 532 t.Helper() 533 534 loader, cleanup := configload.NewLoaderForTests(t) 535 defer cleanup() 536 537 inst := initwd.NewModuleInstaller(loader.ModulesDir(), registry.NewClient(nil, nil)) 538 _, instDiags := inst.InstallModules(context.Background(), dir, true, initwd.ModuleInstallHooksImpl{}) 539 if instDiags.HasErrors() { 540 t.Fatal(instDiags.Err()) 541 } 542 543 // Since module installer has modified the module manifest on disk, we need 544 // to refresh the cache of it in the loader. 545 if err := loader.RefreshModules(); err != nil { 546 t.Fatalf("failed to refresh modules after installation: %s", err) 547 } 548 549 rootCfg, diags := loader.LoadConfig(dir) 550 if diags.HasErrors() { 551 t.Fatalf("failed to load root module: %s", diags.Error()) 552 } 553 554 expander := instances.NewExpander() 555 staticPopulateExpanderModule(t, rootCfg, addrs.RootModuleInstance, expander) 556 return rootCfg, expander.AllInstances() 557 } 558 559 func staticPopulateExpanderModule(t *testing.T, rootCfg *configs.Config, moduleAddr addrs.ModuleInstance, expander *instances.Expander) { 560 t.Helper() 561 562 modCfg := rootCfg.DescendentForInstance(moduleAddr) 563 if modCfg == nil { 564 t.Fatalf("no configuration for %s", moduleAddr) 565 } 566 567 if len(modCfg.Path) > 0 && modCfg.Path[len(modCfg.Path)-1] == "fake_external" { 568 // As a funny special case we modify the source address of this 569 // module to be something that counts as a separate package, 570 // so we can test rules relating to crossing package boundaries 571 // even though we really just loaded the module from a local path. 572 modCfg.SourceAddr = fakeExternalModuleSource 573 } 574 575 for _, call := range modCfg.Module.ModuleCalls { 576 callAddr := addrs.ModuleCall{Name: call.Name} 577 578 if call.Name == "fake_external" { 579 // As a funny special case we modify the source address of this 580 // module to be something that counts as a separate package, 581 // so we can test rules relating to crossing package boundaries 582 // even though we really just loaded the module from a local path. 583 call.SourceAddr = fakeExternalModuleSource 584 } 585 586 // In order to get a valid, useful set of instances here we're going 587 // to just statically evaluate the count and for_each expressions. 588 // Normally it's valid to use references and functions there, but for 589 // our unit tests we'll just limit it to literal values to avoid 590 // bringing all of the core evaluator complexity. 591 switch { 592 case call.ForEach != nil: 593 val, diags := call.ForEach.Value(nil) 594 if diags.HasErrors() { 595 t.Fatalf("invalid for_each: %s", diags.Error()) 596 } 597 expander.SetModuleForEach(moduleAddr, callAddr, val.AsValueMap()) 598 case call.Count != nil: 599 val, diags := call.Count.Value(nil) 600 if diags.HasErrors() { 601 t.Fatalf("invalid count: %s", diags.Error()) 602 } 603 var count int 604 err := gocty.FromCtyValue(val, &count) 605 if err != nil { 606 t.Fatalf("invalid count at %s: %s", call.Count.Range(), err) 607 } 608 expander.SetModuleCount(moduleAddr, callAddr, count) 609 default: 610 expander.SetModuleSingle(moduleAddr, callAddr) 611 } 612 613 // We need to recursively analyze the child modules too. 614 calledMod := modCfg.Path.Child(call.Name) 615 for _, inst := range expander.ExpandModule(calledMod) { 616 staticPopulateExpanderModule(t, rootCfg, inst, expander) 617 } 618 } 619 620 for _, rc := range modCfg.Module.ManagedResources { 621 staticPopulateExpanderResource(t, moduleAddr, rc, expander) 622 } 623 for _, rc := range modCfg.Module.DataResources { 624 staticPopulateExpanderResource(t, moduleAddr, rc, expander) 625 } 626 627 } 628 629 func staticPopulateExpanderResource(t *testing.T, moduleAddr addrs.ModuleInstance, rCfg *configs.Resource, expander *instances.Expander) { 630 t.Helper() 631 632 addr := rCfg.Addr() 633 switch { 634 case rCfg.ForEach != nil: 635 val, diags := rCfg.ForEach.Value(nil) 636 if diags.HasErrors() { 637 t.Fatalf("invalid for_each: %s", diags.Error()) 638 } 639 expander.SetResourceForEach(moduleAddr, addr, val.AsValueMap()) 640 case rCfg.Count != nil: 641 val, diags := rCfg.Count.Value(nil) 642 if diags.HasErrors() { 643 t.Fatalf("invalid count: %s", diags.Error()) 644 } 645 var count int 646 err := gocty.FromCtyValue(val, &count) 647 if err != nil { 648 t.Fatalf("invalid count at %s: %s", rCfg.Count.Range(), err) 649 } 650 expander.SetResourceCount(moduleAddr, addr, count) 651 default: 652 expander.SetResourceSingle(moduleAddr, addr) 653 } 654 } 655 656 func makeTestMoveStmt(t *testing.T, moduleStr, fromStr, toStr string) MoveStatement { 657 t.Helper() 658 659 module := addrs.RootModule 660 if moduleStr != "" { 661 module = addrs.Module(strings.Split(moduleStr, ".")) 662 } 663 664 traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(fromStr), "", hcl.InitialPos) 665 if hclDiags.HasErrors() { 666 t.Fatalf("invalid from address: %s", hclDiags.Error()) 667 } 668 fromEP, diags := addrs.ParseMoveEndpoint(traversal) 669 if diags.HasErrors() { 670 t.Fatalf("invalid from address: %s", diags.Err().Error()) 671 } 672 673 traversal, hclDiags = hclsyntax.ParseTraversalAbs([]byte(toStr), "", hcl.InitialPos) 674 if hclDiags.HasErrors() { 675 t.Fatalf("invalid to address: %s", hclDiags.Error()) 676 } 677 toEP, diags := addrs.ParseMoveEndpoint(traversal) 678 if diags.HasErrors() { 679 t.Fatalf("invalid to address: %s", diags.Err().Error()) 680 } 681 682 fromInModule, toInModule := addrs.UnifyMoveEndpoints(module, fromEP, toEP) 683 if fromInModule == nil || toInModule == nil { 684 t.Fatalf("incompatible move endpoints") 685 } 686 687 return MoveStatement{ 688 From: fromInModule, 689 To: toInModule, 690 DeclRange: tfdiags.SourceRange{ 691 Filename: "test", 692 Start: tfdiags.SourcePos{Line: 1, Column: 1}, 693 End: tfdiags.SourcePos{Line: 1, Column: 1}, 694 }, 695 } 696 } 697 698 func makeTestImpliedMoveStmt(t *testing.T, moduleStr, fromStr, toStr string) MoveStatement { 699 t.Helper() 700 ret := makeTestMoveStmt(t, moduleStr, fromStr, toStr) 701 ret.Implied = true 702 return ret 703 } 704 705 var fakeExternalModuleSource = addrs.ModuleSourceRemote{ 706 Package: addrs.ModulePackage("fake-external:///"), 707 }