github.com/weaviate/weaviate@v1.24.6/usecases/config/environment_test.go (about) 1 // _ _ 2 // __ _____ __ ___ ___ __ _| |_ ___ 3 // \ \ /\ / / _ \/ _` \ \ / / |/ _` | __/ _ \ 4 // \ V V / __/ (_| |\ V /| | (_| | || __/ 5 // \_/\_/ \___|\__,_| \_/ |_|\__,_|\__\___| 6 // 7 // Copyright © 2016 - 2024 Weaviate B.V. All rights reserved. 8 // 9 // CONTACT: hello@weaviate.io 10 // 11 12 package config 13 14 import ( 15 "errors" 16 "os" 17 "testing" 18 19 "github.com/stretchr/testify/assert" 20 "github.com/stretchr/testify/require" 21 "github.com/weaviate/weaviate/usecases/cluster" 22 ) 23 24 const DefaultGoroutineFactor = 1.5 25 26 func TestEnvironmentImportGoroutineFactor(t *testing.T) { 27 factors := []struct { 28 name string 29 goroutineFactor []string 30 expected float64 31 expectedErr bool 32 }{ 33 {"Valid factor", []string{"1"}, 1, false}, 34 {"Low factor", []string{"0.5"}, 0.5, false}, 35 {"not given", []string{}, DefaultGoroutineFactor, false}, 36 {"High factor", []string{"5"}, 5, false}, 37 {"invalid factor", []string{"-1"}, -1, true}, 38 {"not parsable", []string{"I'm not a number"}, -1, true}, 39 } 40 for _, tt := range factors { 41 t.Run(tt.name, func(t *testing.T) { 42 if len(tt.goroutineFactor) == 1 { 43 t.Setenv("MAX_IMPORT_GOROUTINES_FACTOR", tt.goroutineFactor[0]) 44 } 45 conf := Config{} 46 err := FromEnv(&conf) 47 48 if tt.expectedErr { 49 require.NotNil(t, err) 50 } else { 51 require.Equal(t, tt.expected, conf.MaxImportGoroutinesFactor) 52 } 53 }) 54 } 55 } 56 57 func TestEnvironmentSetFlushAfter_AllNames(t *testing.T) { 58 factors := []struct { 59 name string 60 flushAfter []string 61 expected int 62 expectedErr bool 63 }{ 64 {"Valid", []string{"1"}, 1, false}, 65 {"not given", []string{}, DefaultPersistenceMemtablesFlushDirtyAfter, false}, 66 {"invalid factor", []string{"-1"}, -1, true}, 67 {"zero factor", []string{"0"}, -1, true}, 68 {"not parsable", []string{"I'm not a number"}, -1, true}, 69 } 70 envNames := []struct { 71 name string 72 envName string 73 }{ 74 {name: "fallback idle (1st)", envName: "PERSISTENCE_FLUSH_IDLE_MEMTABLES_AFTER"}, 75 {name: "fallback idle (2nd)", envName: "PERSISTENCE_MEMTABLES_FLUSH_IDLE_AFTER_SECONDS"}, 76 {name: "dirty", envName: "PERSISTENCE_MEMTABLES_FLUSH_DIRTY_AFTER_SECONDS"}, 77 } 78 79 for _, n := range envNames { 80 t.Run(n.name, func(t *testing.T) { 81 for _, tt := range factors { 82 t.Run(tt.name, func(t *testing.T) { 83 if len(tt.flushAfter) == 1 { 84 t.Setenv(n.envName, tt.flushAfter[0]) 85 } 86 conf := Config{} 87 err := FromEnv(&conf) 88 89 if tt.expectedErr { 90 require.NotNil(t, err) 91 } else { 92 require.Equal(t, tt.expected, conf.Persistence.MemtablesFlushDirtyAfter) 93 } 94 }) 95 } 96 }) 97 } 98 } 99 100 func TestEnvironmentFlushConflictingValues(t *testing.T) { 101 // if all 3 variable names are used, the newest variable name 102 // should be taken into consideration 103 os.Clearenv() 104 t.Setenv("PERSISTENCE_FLUSH_IDLE_MEMTABLES_AFTER", "16") 105 t.Setenv("PERSISTENCE_MEMTABLES_FLUSH_IDLE_AFTER_SECONDS", "17") 106 t.Setenv("PERSISTENCE_MEMTABLES_FLUSH_DIRTY_AFTER_SECONDS", "18") 107 conf := Config{} 108 err := FromEnv(&conf) 109 require.Nil(t, err) 110 111 assert.Equal(t, 18, conf.Persistence.MemtablesFlushDirtyAfter) 112 } 113 114 func TestEnvironmentPersistence_dataPath(t *testing.T) { 115 factors := []struct { 116 name string 117 value []string 118 config Config 119 expected string 120 }{ 121 { 122 name: "given", 123 value: []string{"/var/lib/weaviate"}, 124 config: Config{}, 125 expected: "/var/lib/weaviate", 126 }, 127 { 128 name: "given with config set", 129 value: []string{"/var/lib/weaviate"}, 130 config: Config{ 131 Persistence: Persistence{ 132 DataPath: "/var/data/weaviate", 133 }, 134 }, 135 expected: "/var/lib/weaviate", 136 }, 137 { 138 name: "not given", 139 value: []string{}, 140 config: Config{}, 141 expected: DefaultPersistenceDataPath, 142 }, 143 { 144 name: "not given with config set", 145 value: []string{}, 146 config: Config{ 147 Persistence: Persistence{ 148 DataPath: "/var/data/weaviate", 149 }, 150 }, 151 expected: "/var/data/weaviate", 152 }, 153 } 154 for _, tt := range factors { 155 t.Run(tt.name, func(t *testing.T) { 156 if len(tt.value) == 1 { 157 t.Setenv("PERSISTENCE_DATA_PATH", tt.value[0]) 158 } 159 conf := tt.config 160 err := FromEnv(&conf) 161 require.Nil(t, err) 162 require.Equal(t, tt.expected, conf.Persistence.DataPath) 163 }) 164 } 165 } 166 167 func TestEnvironmentMemtable_MaxSize(t *testing.T) { 168 factors := []struct { 169 name string 170 value []string 171 expected int 172 expectedErr bool 173 }{ 174 {"Valid", []string{"100"}, 100, false}, 175 {"not given", []string{}, DefaultPersistenceMemtablesMaxSize, false}, 176 {"invalid factor", []string{"-1"}, -1, true}, 177 {"zero factor", []string{"0"}, -1, true}, 178 {"not parsable", []string{"I'm not a number"}, -1, true}, 179 } 180 for _, tt := range factors { 181 t.Run(tt.name, func(t *testing.T) { 182 if len(tt.value) == 1 { 183 t.Setenv("PERSISTENCE_MEMTABLES_MAX_SIZE_MB", tt.value[0]) 184 } 185 conf := Config{} 186 err := FromEnv(&conf) 187 188 if tt.expectedErr { 189 require.NotNil(t, err) 190 } else { 191 require.Equal(t, tt.expected, conf.Persistence.MemtablesMaxSizeMB) 192 } 193 }) 194 } 195 } 196 197 func TestEnvironmentMemtable_MinDuration(t *testing.T) { 198 factors := []struct { 199 name string 200 value []string 201 expected int 202 expectedErr bool 203 }{ 204 {"Valid", []string{"100"}, 100, false}, 205 {"not given", []string{}, DefaultPersistenceMemtablesMinDuration, false}, 206 {"invalid factor", []string{"-1"}, -1, true}, 207 {"zero factor", []string{"0"}, -1, true}, 208 {"not parsable", []string{"I'm not a number"}, -1, true}, 209 } 210 for _, tt := range factors { 211 t.Run(tt.name, func(t *testing.T) { 212 if len(tt.value) == 1 { 213 t.Setenv("PERSISTENCE_MEMTABLES_MIN_ACTIVE_DURATION_SECONDS", tt.value[0]) 214 } 215 conf := Config{} 216 err := FromEnv(&conf) 217 218 if tt.expectedErr { 219 require.NotNil(t, err) 220 } else { 221 require.Equal(t, tt.expected, conf.Persistence.MemtablesMinActiveDurationSeconds) 222 } 223 }) 224 } 225 } 226 227 func TestEnvironmentMemtable_MaxDuration(t *testing.T) { 228 factors := []struct { 229 name string 230 value []string 231 expected int 232 expectedErr bool 233 }{ 234 {"Valid", []string{"100"}, 100, false}, 235 {"not given", []string{}, DefaultPersistenceMemtablesMaxDuration, false}, 236 {"invalid factor", []string{"-1"}, -1, true}, 237 {"zero factor", []string{"0"}, -1, true}, 238 {"not parsable", []string{"I'm not a number"}, -1, true}, 239 } 240 for _, tt := range factors { 241 t.Run(tt.name, func(t *testing.T) { 242 if len(tt.value) == 1 { 243 t.Setenv("PERSISTENCE_MEMTABLES_MAX_ACTIVE_DURATION_SECONDS", tt.value[0]) 244 } 245 conf := Config{} 246 err := FromEnv(&conf) 247 248 if tt.expectedErr { 249 require.NotNil(t, err) 250 } else { 251 require.Equal(t, tt.expected, conf.Persistence.MemtablesMaxActiveDurationSeconds) 252 } 253 }) 254 } 255 } 256 257 func TestEnvironmentParseClusterConfig(t *testing.T) { 258 tests := []struct { 259 name string 260 envVars map[string]string 261 expectedResult cluster.Config 262 expectedErr error 263 }{ 264 { 265 name: "valid cluster config - ports and advertiseaddr provided", 266 envVars: map[string]string{ 267 "CLUSTER_GOSSIP_BIND_PORT": "7100", 268 "CLUSTER_DATA_BIND_PORT": "7101", 269 "CLUSTER_ADVERTISE_ADDR": "193.0.0.1", 270 "CLUSTER_ADVERTISE_PORT": "9999", 271 }, 272 expectedResult: cluster.Config{ 273 GossipBindPort: 7100, 274 DataBindPort: 7101, 275 AdvertiseAddr: "193.0.0.1", 276 AdvertisePort: 9999, 277 }, 278 }, 279 { 280 name: "valid cluster config - no ports and advertiseaddr provided", 281 expectedResult: cluster.Config{ 282 GossipBindPort: DefaultGossipBindPort, 283 DataBindPort: DefaultGossipBindPort + 1, 284 AdvertiseAddr: "", 285 }, 286 }, 287 { 288 name: "valid cluster config - only gossip bind port provided", 289 envVars: map[string]string{ 290 "CLUSTER_GOSSIP_BIND_PORT": "7777", 291 }, 292 expectedResult: cluster.Config{ 293 GossipBindPort: 7777, 294 DataBindPort: 7778, 295 }, 296 }, 297 { 298 name: "invalid cluster config - both ports provided", 299 envVars: map[string]string{ 300 "CLUSTER_GOSSIP_BIND_PORT": "7100", 301 "CLUSTER_DATA_BIND_PORT": "7111", 302 }, 303 expectedErr: errors.New("CLUSTER_DATA_BIND_PORT must be one port " + 304 "number greater than CLUSTER_GOSSIP_BIND_PORT"), 305 }, 306 { 307 name: "invalid config - only data bind port provided", 308 envVars: map[string]string{ 309 "CLUSTER_DATA_BIND_PORT": "7101", 310 }, 311 expectedErr: errors.New("CLUSTER_DATA_BIND_PORT must be one port " + 312 "number greater than CLUSTER_GOSSIP_BIND_PORT"), 313 }, 314 { 315 name: "schema sync disabled", 316 envVars: map[string]string{ 317 "CLUSTER_IGNORE_SCHEMA_SYNC": "true", 318 }, 319 expectedResult: cluster.Config{ 320 GossipBindPort: 7946, 321 DataBindPort: 7947, 322 IgnoreStartupSchemaSync: true, 323 }, 324 }, 325 } 326 327 for _, test := range tests { 328 t.Run(test.name, func(t *testing.T) { 329 for k, v := range test.envVars { 330 t.Setenv(k, v) 331 } 332 cfg, err := parseClusterConfig() 333 if test.expectedErr != nil { 334 assert.EqualError(t, err, test.expectedErr.Error(), 335 "expected err: %v, got: %v", test.expectedErr, err) 336 } else { 337 assert.Nil(t, err, "expected nil, got: %v", err) 338 assert.EqualValues(t, test.expectedResult, cfg) 339 } 340 }) 341 } 342 } 343 344 func TestEnvironmentSetDefaultVectorDistanceMetric(t *testing.T) { 345 t.Run("DefaultVectorDistanceMetricIsEmpty", func(t *testing.T) { 346 os.Clearenv() 347 conf := Config{} 348 FromEnv(&conf) 349 require.Equal(t, "", conf.DefaultVectorDistanceMetric) 350 }) 351 352 t.Run("NonEmptyDefaultVectorDistanceMetric", func(t *testing.T) { 353 os.Clearenv() 354 t.Setenv("DEFAULT_VECTOR_DISTANCE_METRIC", "l2-squared") 355 conf := Config{} 356 FromEnv(&conf) 357 require.Equal(t, "l2-squared", conf.DefaultVectorDistanceMetric) 358 }) 359 } 360 361 func TestEnvironmentMaxConcurrentGetRequests(t *testing.T) { 362 factors := []struct { 363 name string 364 value []string 365 expected int 366 expectedErr bool 367 }{ 368 {"Valid", []string{"100"}, 100, false}, 369 {"not given", []string{}, DefaultMaxConcurrentGetRequests, false}, 370 {"unlimited", []string{"-1"}, -1, false}, 371 {"not parsable", []string{"I'm not a number"}, -1, true}, 372 } 373 for _, tt := range factors { 374 t.Run(tt.name, func(t *testing.T) { 375 if len(tt.value) == 1 { 376 t.Setenv("MAXIMUM_CONCURRENT_GET_REQUESTS", tt.value[0]) 377 } 378 conf := Config{} 379 err := FromEnv(&conf) 380 381 if tt.expectedErr { 382 require.NotNil(t, err) 383 } else { 384 require.Equal(t, tt.expected, conf.MaximumConcurrentGetRequests) 385 } 386 }) 387 } 388 } 389 390 func TestEnvironmentCORS_Origin(t *testing.T) { 391 factors := []struct { 392 name string 393 value []string 394 expected string 395 expectedErr bool 396 }{ 397 {"Valid", []string{"http://foo.com"}, "http://foo.com", false}, 398 {"not given", []string{}, DefaultCORSAllowOrigin, false}, 399 } 400 for _, tt := range factors { 401 t.Run(tt.name, func(t *testing.T) { 402 os.Clearenv() 403 if len(tt.value) == 1 { 404 os.Setenv("CORS_ALLOW_ORIGIN", tt.value[0]) 405 } 406 conf := Config{} 407 err := FromEnv(&conf) 408 409 if tt.expectedErr { 410 require.NotNil(t, err) 411 } else { 412 require.Equal(t, tt.expected, conf.CORS.AllowOrigin) 413 } 414 }) 415 } 416 } 417 418 func TestEnvironmentGRPCPort(t *testing.T) { 419 factors := []struct { 420 name string 421 value []string 422 expected int 423 expectedErr bool 424 }{ 425 {"Valid", []string{"50052"}, 50052, false}, 426 {"not given", []string{}, DefaultGRPCPort, false}, 427 {"invalid factor", []string{"-1"}, -1, true}, 428 {"zero factor", []string{"0"}, -1, true}, 429 {"not parsable", []string{"I'm not a number"}, -1, true}, 430 } 431 for _, tt := range factors { 432 t.Run(tt.name, func(t *testing.T) { 433 if len(tt.value) == 1 { 434 t.Setenv("GRPC_PORT", tt.value[0]) 435 } 436 conf := Config{} 437 err := FromEnv(&conf) 438 439 if tt.expectedErr { 440 require.NotNil(t, err) 441 } else { 442 require.Equal(t, tt.expected, conf.GRPC.Port) 443 } 444 }) 445 } 446 } 447 448 func TestEnvironmentCORS_Methods(t *testing.T) { 449 factors := []struct { 450 name string 451 value []string 452 expected string 453 expectedErr bool 454 }{ 455 {"Valid", []string{"POST"}, "POST", false}, 456 {"not given", []string{}, DefaultCORSAllowMethods, false}, 457 } 458 for _, tt := range factors { 459 t.Run(tt.name, func(t *testing.T) { 460 os.Clearenv() 461 if len(tt.value) == 1 { 462 os.Setenv("CORS_ALLOW_METHODS", tt.value[0]) 463 } 464 conf := Config{} 465 err := FromEnv(&conf) 466 467 if tt.expectedErr { 468 require.NotNil(t, err) 469 } else { 470 require.Equal(t, tt.expected, conf.CORS.AllowMethods) 471 } 472 }) 473 } 474 } 475 476 func TestEnvironmentDisableGraphQL(t *testing.T) { 477 factors := []struct { 478 name string 479 value []string 480 expected bool 481 expectedErr bool 482 }{ 483 {"Valid: true", []string{"true"}, true, false}, 484 {"Valid: false", []string{"false"}, false, false}, 485 {"Valid: 1", []string{"1"}, true, false}, 486 {"Valid: 0", []string{"0"}, false, false}, 487 {"Valid: on", []string{"on"}, true, false}, 488 {"Valid: off", []string{"off"}, false, false}, 489 {"not given", []string{}, false, false}, 490 } 491 for _, tt := range factors { 492 t.Run(tt.name, func(t *testing.T) { 493 if len(tt.value) == 1 { 494 t.Setenv("DISABLE_GRAPHQL", tt.value[0]) 495 } 496 conf := Config{} 497 err := FromEnv(&conf) 498 499 if tt.expectedErr { 500 require.NotNil(t, err) 501 } else { 502 require.Equal(t, tt.expected, conf.DisableGraphQL) 503 } 504 }) 505 } 506 } 507 508 func TestEnvironmentCORS_Headers(t *testing.T) { 509 factors := []struct { 510 name string 511 value []string 512 expected string 513 expectedErr bool 514 }{ 515 {"Valid", []string{"Authorization"}, "Authorization", false}, 516 {"not given", []string{}, DefaultCORSAllowHeaders, false}, 517 } 518 for _, tt := range factors { 519 t.Run(tt.name, func(t *testing.T) { 520 os.Clearenv() 521 if len(tt.value) == 1 { 522 os.Setenv("CORS_ALLOW_HEADERS", tt.value[0]) 523 } 524 conf := Config{} 525 err := FromEnv(&conf) 526 527 if tt.expectedErr { 528 require.NotNil(t, err) 529 } else { 530 require.Equal(t, tt.expected, conf.CORS.AllowHeaders) 531 } 532 }) 533 } 534 } 535 536 func TestEnvironmentPrometheusGroupClasses_OldName(t *testing.T) { 537 factors := []struct { 538 name string 539 value []string 540 expected bool 541 expectedErr bool 542 }{ 543 {"Valid: true", []string{"true"}, true, false}, 544 {"Valid: false", []string{"false"}, false, false}, 545 {"Valid: 1", []string{"1"}, true, false}, 546 {"Valid: 0", []string{"0"}, false, false}, 547 {"Valid: on", []string{"on"}, true, false}, 548 {"Valid: off", []string{"off"}, false, false}, 549 {"not given", []string{}, false, false}, 550 } 551 for _, tt := range factors { 552 t.Run(tt.name, func(t *testing.T) { 553 t.Setenv("PROMETHEUS_MONITORING_ENABLED", "true") 554 if len(tt.value) == 1 { 555 t.Setenv("PROMETHEUS_MONITORING_GROUP_CLASSES", tt.value[0]) 556 } 557 conf := Config{} 558 err := FromEnv(&conf) 559 560 if tt.expectedErr { 561 require.NotNil(t, err) 562 } else { 563 require.Equal(t, tt.expected, conf.Monitoring.Group) 564 } 565 }) 566 } 567 } 568 569 func TestEnvironmentPrometheusGroupClasses_NewName(t *testing.T) { 570 factors := []struct { 571 name string 572 value []string 573 expected bool 574 expectedErr bool 575 }{ 576 {"Valid: true", []string{"true"}, true, false}, 577 {"Valid: false", []string{"false"}, false, false}, 578 {"Valid: 1", []string{"1"}, true, false}, 579 {"Valid: 0", []string{"0"}, false, false}, 580 {"Valid: on", []string{"on"}, true, false}, 581 {"Valid: off", []string{"off"}, false, false}, 582 {"not given", []string{}, false, false}, 583 } 584 for _, tt := range factors { 585 t.Run(tt.name, func(t *testing.T) { 586 t.Setenv("PROMETHEUS_MONITORING_ENABLED", "true") 587 if len(tt.value) == 1 { 588 t.Setenv("PROMETHEUS_MONITORING_GROUP", tt.value[0]) 589 } 590 conf := Config{} 591 err := FromEnv(&conf) 592 593 if tt.expectedErr { 594 require.NotNil(t, err) 595 } else { 596 require.Equal(t, tt.expected, conf.Monitoring.Group) 597 } 598 }) 599 } 600 } 601 602 func TestEnvironmentMinimumReplicationFactor(t *testing.T) { 603 factors := []struct { 604 name string 605 value []string 606 expected int 607 expectedErr bool 608 }{ 609 {"Valid", []string{"3"}, 3, false}, 610 {"not given", []string{}, DefaultMinimumReplicationFactor, false}, 611 {"invalid factor", []string{"-1"}, -1, true}, 612 {"zero factor", []string{"0"}, -1, true}, 613 {"not parsable", []string{"I'm not a number"}, -1, true}, 614 } 615 for _, tt := range factors { 616 t.Run(tt.name, func(t *testing.T) { 617 if len(tt.value) == 1 { 618 t.Setenv("REPLICATION_MINIMUM_FACTOR", tt.value[0]) 619 } 620 conf := Config{} 621 err := FromEnv(&conf) 622 623 if tt.expectedErr { 624 require.NotNil(t, err) 625 } else { 626 require.Equal(t, tt.expected, conf.Replication.MinimumFactor) 627 } 628 }) 629 } 630 } 631 632 func TestEnvironmentQueryDefaults_Limit(t *testing.T) { 633 factors := []struct { 634 name string 635 value []string 636 config Config 637 expected int64 638 }{ 639 { 640 name: "Valid", 641 value: []string{"3"}, 642 config: Config{}, 643 expected: 3, 644 }, 645 { 646 name: "Valid with config already set", 647 value: []string{"3"}, 648 config: Config{ 649 QueryDefaults: QueryDefaults{ 650 Limit: 20, 651 }, 652 }, 653 expected: 3, 654 }, 655 { 656 name: "not given with config set", 657 value: []string{}, 658 config: Config{ 659 QueryDefaults: QueryDefaults{ 660 Limit: 20, 661 }, 662 }, 663 expected: 20, 664 }, 665 { 666 name: "not given with config set", 667 value: []string{}, 668 config: Config{}, 669 expected: DefaultQueryDefaultsLimit, 670 }, 671 } 672 for _, tt := range factors { 673 t.Run(tt.name, func(t *testing.T) { 674 if len(tt.value) == 1 { 675 t.Setenv("QUERY_DEFAULTS_LIMIT", tt.value[0]) 676 } 677 conf := tt.config 678 err := FromEnv(&conf) 679 680 require.Nil(t, err) 681 require.Equal(t, tt.expected, conf.QueryDefaults.Limit) 682 }) 683 } 684 } 685 686 func TestEnvironmentAuthentication(t *testing.T) { 687 factors := []struct { 688 name string 689 auth_env_var []string 690 expected Authentication 691 }{ 692 { 693 name: "Valid API Key", 694 auth_env_var: []string{"AUTHENTICATION_APIKEY_ENABLED"}, 695 expected: Authentication{ 696 APIKey: APIKey{ 697 Enabled: true, 698 }, 699 }, 700 }, 701 { 702 name: "Valid Anonymous Access", 703 auth_env_var: []string{"AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED"}, 704 expected: Authentication{ 705 AnonymousAccess: AnonymousAccess{ 706 Enabled: true, 707 }, 708 }, 709 }, 710 { 711 name: "Valid OIDC Auth", 712 auth_env_var: []string{"AUTHENTICATION_OIDC_ENABLED"}, 713 expected: Authentication{ 714 OIDC: OIDC{ 715 Enabled: true, 716 }, 717 }, 718 }, 719 { 720 name: "not given", 721 auth_env_var: []string{}, 722 expected: Authentication{ 723 AnonymousAccess: AnonymousAccess{ 724 Enabled: true, 725 }, 726 }, 727 }, 728 } 729 for _, tt := range factors { 730 t.Run(tt.name, func(t *testing.T) { 731 if len(tt.auth_env_var) == 1 { 732 t.Setenv(tt.auth_env_var[0], "true") 733 } 734 conf := Config{} 735 err := FromEnv(&conf) 736 require.Nil(t, err) 737 require.Equal(t, tt.expected, conf.Authentication) 738 }) 739 } 740 }