1// Copyright 2019 Google Inc. All rights reserved. 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 15package android 16 17import ( 18 "fmt" 19 "path/filepath" 20 "reflect" 21 "strings" 22 "testing" 23 24 "github.com/google/blueprint" 25 26 "android/soong/shared" 27) 28 29func pathContext() PathContext { 30 return PathContextForTesting(TestConfig("out", nil, "", map[string][]byte{ 31 "ld": nil, 32 "a.o": nil, 33 "b.o": nil, 34 "cp": nil, 35 "a": nil, 36 "b": nil, 37 "ls": nil, 38 "turbine": nil, 39 "java": nil, 40 "javac": nil, 41 })) 42} 43 44func ExampleRuleBuilder() { 45 rule := NewRuleBuilder() 46 47 ctx := pathContext() 48 49 rule.Command(). 50 Tool(PathForSource(ctx, "ld")). 51 Inputs(PathsForTesting("a.o", "b.o")). 52 FlagWithOutput("-o ", PathForOutput(ctx, "linked")) 53 rule.Command().Text("echo success") 54 55 // To add the command to the build graph: 56 // rule.Build(pctx, ctx, "link", "link") 57 58 fmt.Printf("commands: %q\n", strings.Join(rule.Commands(), " && ")) 59 fmt.Printf("tools: %q\n", rule.Tools()) 60 fmt.Printf("inputs: %q\n", rule.Inputs()) 61 fmt.Printf("outputs: %q\n", rule.Outputs()) 62 63 // Output: 64 // commands: "ld a.o b.o -o out/linked && echo success" 65 // tools: ["ld"] 66 // inputs: ["a.o" "b.o"] 67 // outputs: ["out/linked"] 68} 69 70func ExampleRuleBuilder_Temporary() { 71 rule := NewRuleBuilder() 72 73 ctx := pathContext() 74 75 rule.Command(). 76 Tool(PathForSource(ctx, "cp")). 77 Input(PathForSource(ctx, "a")). 78 Output(PathForOutput(ctx, "b")) 79 rule.Command(). 80 Tool(PathForSource(ctx, "cp")). 81 Input(PathForOutput(ctx, "b")). 82 Output(PathForOutput(ctx, "c")) 83 rule.Temporary(PathForOutput(ctx, "b")) 84 85 fmt.Printf("commands: %q\n", strings.Join(rule.Commands(), " && ")) 86 fmt.Printf("tools: %q\n", rule.Tools()) 87 fmt.Printf("inputs: %q\n", rule.Inputs()) 88 fmt.Printf("outputs: %q\n", rule.Outputs()) 89 90 // Output: 91 // commands: "cp a out/b && cp out/b out/c" 92 // tools: ["cp"] 93 // inputs: ["a"] 94 // outputs: ["out/c"] 95} 96 97func ExampleRuleBuilder_DeleteTemporaryFiles() { 98 rule := NewRuleBuilder() 99 100 ctx := pathContext() 101 102 rule.Command(). 103 Tool(PathForSource(ctx, "cp")). 104 Input(PathForSource(ctx, "a")). 105 Output(PathForOutput(ctx, "b")) 106 rule.Command(). 107 Tool(PathForSource(ctx, "cp")). 108 Input(PathForOutput(ctx, "b")). 109 Output(PathForOutput(ctx, "c")) 110 rule.Temporary(PathForOutput(ctx, "b")) 111 rule.DeleteTemporaryFiles() 112 113 fmt.Printf("commands: %q\n", strings.Join(rule.Commands(), " && ")) 114 fmt.Printf("tools: %q\n", rule.Tools()) 115 fmt.Printf("inputs: %q\n", rule.Inputs()) 116 fmt.Printf("outputs: %q\n", rule.Outputs()) 117 118 // Output: 119 // commands: "cp a out/b && cp out/b out/c && rm -f out/b" 120 // tools: ["cp"] 121 // inputs: ["a"] 122 // outputs: ["out/c"] 123} 124 125func ExampleRuleBuilder_Installs() { 126 rule := NewRuleBuilder() 127 128 ctx := pathContext() 129 130 out := PathForOutput(ctx, "linked") 131 132 rule.Command(). 133 Tool(PathForSource(ctx, "ld")). 134 Inputs(PathsForTesting("a.o", "b.o")). 135 FlagWithOutput("-o ", out) 136 rule.Install(out, "/bin/linked") 137 rule.Install(out, "/sbin/linked") 138 139 fmt.Printf("rule.Installs().String() = %q\n", rule.Installs().String()) 140 141 // Output: 142 // rule.Installs().String() = "out/linked:/bin/linked out/linked:/sbin/linked" 143} 144 145func ExampleRuleBuilderCommand() { 146 rule := NewRuleBuilder() 147 148 ctx := pathContext() 149 150 // chained 151 rule.Command(). 152 Tool(PathForSource(ctx, "ld")). 153 Inputs(PathsForTesting("a.o", "b.o")). 154 FlagWithOutput("-o ", PathForOutput(ctx, "linked")) 155 156 // unchained 157 cmd := rule.Command() 158 cmd.Tool(PathForSource(ctx, "ld")) 159 cmd.Inputs(PathsForTesting("a.o", "b.o")) 160 cmd.FlagWithOutput("-o ", PathForOutput(ctx, "linked")) 161 162 // mixed: 163 cmd = rule.Command().Tool(PathForSource(ctx, "ld")) 164 cmd.Inputs(PathsForTesting("a.o", "b.o")) 165 cmd.FlagWithOutput("-o ", PathForOutput(ctx, "linked")) 166} 167 168func ExampleRuleBuilderCommand_Flag() { 169 ctx := pathContext() 170 fmt.Println(NewRuleBuilder().Command(). 171 Tool(PathForSource(ctx, "ls")).Flag("-l")) 172 // Output: 173 // ls -l 174} 175 176func ExampleRuleBuilderCommand_Flags() { 177 ctx := pathContext() 178 fmt.Println(NewRuleBuilder().Command(). 179 Tool(PathForSource(ctx, "ls")).Flags([]string{"-l", "-a"})) 180 // Output: 181 // ls -l -a 182} 183 184func ExampleRuleBuilderCommand_FlagWithArg() { 185 ctx := pathContext() 186 fmt.Println(NewRuleBuilder().Command(). 187 Tool(PathForSource(ctx, "ls")). 188 FlagWithArg("--sort=", "time")) 189 // Output: 190 // ls --sort=time 191} 192 193func ExampleRuleBuilderCommand_FlagForEachArg() { 194 ctx := pathContext() 195 fmt.Println(NewRuleBuilder().Command(). 196 Tool(PathForSource(ctx, "ls")). 197 FlagForEachArg("--sort=", []string{"time", "size"})) 198 // Output: 199 // ls --sort=time --sort=size 200} 201 202func ExampleRuleBuilderCommand_FlagForEachInput() { 203 ctx := pathContext() 204 fmt.Println(NewRuleBuilder().Command(). 205 Tool(PathForSource(ctx, "turbine")). 206 FlagForEachInput("--classpath ", PathsForTesting("a.jar", "b.jar"))) 207 // Output: 208 // turbine --classpath a.jar --classpath b.jar 209} 210 211func ExampleRuleBuilderCommand_FlagWithInputList() { 212 ctx := pathContext() 213 fmt.Println(NewRuleBuilder().Command(). 214 Tool(PathForSource(ctx, "java")). 215 FlagWithInputList("-classpath=", PathsForTesting("a.jar", "b.jar"), ":")) 216 // Output: 217 // java -classpath=a.jar:b.jar 218} 219 220func ExampleRuleBuilderCommand_FlagWithInput() { 221 ctx := pathContext() 222 fmt.Println(NewRuleBuilder().Command(). 223 Tool(PathForSource(ctx, "java")). 224 FlagWithInput("-classpath=", PathForSource(ctx, "a"))) 225 // Output: 226 // java -classpath=a 227} 228 229func ExampleRuleBuilderCommand_FlagWithList() { 230 ctx := pathContext() 231 fmt.Println(NewRuleBuilder().Command(). 232 Tool(PathForSource(ctx, "ls")). 233 FlagWithList("--sort=", []string{"time", "size"}, ",")) 234 // Output: 235 // ls --sort=time,size 236} 237 238func ExampleRuleBuilderCommand_FlagWithRspFileInputList() { 239 ctx := pathContext() 240 fmt.Println(NewRuleBuilder().Command(). 241 Tool(PathForSource(ctx, "javac")). 242 FlagWithRspFileInputList("@", PathsForTesting("a.java", "b.java")). 243 NinjaEscapedString()) 244 // Output: 245 // javac @$out.rsp 246} 247 248func ExampleRuleBuilderCommand_String() { 249 fmt.Println(NewRuleBuilder().Command(). 250 Text("FOO=foo"). 251 Text("echo $FOO"). 252 String()) 253 // Output: 254 // FOO=foo echo $FOO 255} 256 257func ExampleRuleBuilderCommand_NinjaEscapedString() { 258 fmt.Println(NewRuleBuilder().Command(). 259 Text("FOO=foo"). 260 Text("echo $FOO"). 261 NinjaEscapedString()) 262 // Output: 263 // FOO=foo echo $$FOO 264} 265 266func TestRuleBuilder(t *testing.T) { 267 fs := map[string][]byte{ 268 "dep_fixer": nil, 269 "input": nil, 270 "Implicit": nil, 271 "Input": nil, 272 "OrderOnly": nil, 273 "OrderOnlys": nil, 274 "Tool": nil, 275 "input2": nil, 276 "tool2": nil, 277 "input3": nil, 278 } 279 280 ctx := PathContextForTesting(TestConfig("out", nil, "", fs)) 281 282 addCommands := func(rule *RuleBuilder) { 283 cmd := rule.Command(). 284 DepFile(PathForOutput(ctx, "DepFile")). 285 Flag("Flag"). 286 FlagWithArg("FlagWithArg=", "arg"). 287 FlagWithDepFile("FlagWithDepFile=", PathForOutput(ctx, "depfile")). 288 FlagWithInput("FlagWithInput=", PathForSource(ctx, "input")). 289 FlagWithOutput("FlagWithOutput=", PathForOutput(ctx, "output")). 290 Implicit(PathForSource(ctx, "Implicit")). 291 ImplicitDepFile(PathForOutput(ctx, "ImplicitDepFile")). 292 ImplicitOutput(PathForOutput(ctx, "ImplicitOutput")). 293 Input(PathForSource(ctx, "Input")). 294 Output(PathForOutput(ctx, "Output")). 295 OrderOnly(PathForSource(ctx, "OrderOnly")). 296 Text("Text"). 297 Tool(PathForSource(ctx, "Tool")) 298 299 rule.Command(). 300 Text("command2"). 301 DepFile(PathForOutput(ctx, "depfile2")). 302 Input(PathForSource(ctx, "input2")). 303 Output(PathForOutput(ctx, "output2")). 304 OrderOnlys(PathsForSource(ctx, []string{"OrderOnlys"})). 305 Tool(PathForSource(ctx, "tool2")) 306 307 // Test updates to the first command after the second command has been started 308 cmd.Text("after command2") 309 // Test updating a command when the previous update did not replace the cmd variable 310 cmd.Text("old cmd") 311 312 // Test a command that uses the output of a previous command as an input 313 rule.Command(). 314 Text("command3"). 315 Input(PathForSource(ctx, "input3")). 316 Input(PathForOutput(ctx, "output2")). 317 Output(PathForOutput(ctx, "output3")) 318 } 319 320 wantInputs := PathsForSource(ctx, []string{"Implicit", "Input", "input", "input2", "input3"}) 321 wantOutputs := PathsForOutput(ctx, []string{"ImplicitOutput", "Output", "output", "output2", "output3"}) 322 wantDepFiles := PathsForOutput(ctx, []string{"DepFile", "depfile", "ImplicitDepFile", "depfile2"}) 323 wantTools := PathsForSource(ctx, []string{"Tool", "tool2"}) 324 wantOrderOnlys := PathsForSource(ctx, []string{"OrderOnly", "OrderOnlys"}) 325 326 t.Run("normal", func(t *testing.T) { 327 rule := NewRuleBuilder() 328 addCommands(rule) 329 330 wantCommands := []string{ 331 "out/DepFile Flag FlagWithArg=arg FlagWithDepFile=out/depfile FlagWithInput=input FlagWithOutput=out/output Input out/Output Text Tool after command2 old cmd", 332 "command2 out/depfile2 input2 out/output2 tool2", 333 "command3 input3 out/output2 out/output3", 334 } 335 336 wantDepMergerCommand := "out/host/" + ctx.Config().PrebuiltOS() + "/bin/dep_fixer out/DepFile out/depfile out/ImplicitDepFile out/depfile2" 337 338 if g, w := rule.Commands(), wantCommands; !reflect.DeepEqual(g, w) { 339 t.Errorf("\nwant rule.Commands() = %#v\n got %#v", w, g) 340 } 341 342 if g, w := rule.Inputs(), wantInputs; !reflect.DeepEqual(w, g) { 343 t.Errorf("\nwant rule.Inputs() = %#v\n got %#v", w, g) 344 } 345 if g, w := rule.Outputs(), wantOutputs; !reflect.DeepEqual(w, g) { 346 t.Errorf("\nwant rule.Outputs() = %#v\n got %#v", w, g) 347 } 348 if g, w := rule.DepFiles(), wantDepFiles; !reflect.DeepEqual(w, g) { 349 t.Errorf("\nwant rule.DepFiles() = %#v\n got %#v", w, g) 350 } 351 if g, w := rule.Tools(), wantTools; !reflect.DeepEqual(w, g) { 352 t.Errorf("\nwant rule.Tools() = %#v\n got %#v", w, g) 353 } 354 if g, w := rule.OrderOnlys(), wantOrderOnlys; !reflect.DeepEqual(w, g) { 355 t.Errorf("\nwant rule.OrderOnlys() = %#v\n got %#v", w, g) 356 } 357 358 if g, w := rule.depFileMergerCmd(ctx, rule.DepFiles()).String(), wantDepMergerCommand; g != w { 359 t.Errorf("\nwant rule.depFileMergerCmd() = %#v\n got %#v", w, g) 360 } 361 }) 362 363 t.Run("sbox", func(t *testing.T) { 364 rule := NewRuleBuilder().Sbox(PathForOutput(ctx)) 365 addCommands(rule) 366 367 wantCommands := []string{ 368 "__SBOX_OUT_DIR__/DepFile Flag FlagWithArg=arg FlagWithDepFile=__SBOX_OUT_DIR__/depfile FlagWithInput=input FlagWithOutput=__SBOX_OUT_DIR__/output Input __SBOX_OUT_DIR__/Output Text Tool after command2 old cmd", 369 "command2 __SBOX_OUT_DIR__/depfile2 input2 __SBOX_OUT_DIR__/output2 tool2", 370 "command3 input3 __SBOX_OUT_DIR__/output2 __SBOX_OUT_DIR__/output3", 371 } 372 373 wantDepMergerCommand := "out/host/" + ctx.Config().PrebuiltOS() + "/bin/dep_fixer __SBOX_OUT_DIR__/DepFile __SBOX_OUT_DIR__/depfile __SBOX_OUT_DIR__/ImplicitDepFile __SBOX_OUT_DIR__/depfile2" 374 375 if g, w := rule.Commands(), wantCommands; !reflect.DeepEqual(g, w) { 376 t.Errorf("\nwant rule.Commands() = %#v\n got %#v", w, g) 377 } 378 379 if g, w := rule.Inputs(), wantInputs; !reflect.DeepEqual(w, g) { 380 t.Errorf("\nwant rule.Inputs() = %#v\n got %#v", w, g) 381 } 382 if g, w := rule.Outputs(), wantOutputs; !reflect.DeepEqual(w, g) { 383 t.Errorf("\nwant rule.Outputs() = %#v\n got %#v", w, g) 384 } 385 if g, w := rule.DepFiles(), wantDepFiles; !reflect.DeepEqual(w, g) { 386 t.Errorf("\nwant rule.DepFiles() = %#v\n got %#v", w, g) 387 } 388 if g, w := rule.Tools(), wantTools; !reflect.DeepEqual(w, g) { 389 t.Errorf("\nwant rule.Tools() = %#v\n got %#v", w, g) 390 } 391 if g, w := rule.OrderOnlys(), wantOrderOnlys; !reflect.DeepEqual(w, g) { 392 t.Errorf("\nwant rule.OrderOnlys() = %#v\n got %#v", w, g) 393 } 394 395 if g, w := rule.depFileMergerCmd(ctx, rule.DepFiles()).String(), wantDepMergerCommand; g != w { 396 t.Errorf("\nwant rule.depFileMergerCmd() = %#v\n got %#v", w, g) 397 } 398 }) 399} 400 401func testRuleBuilderFactory() Module { 402 module := &testRuleBuilderModule{} 403 module.AddProperties(&module.properties) 404 InitAndroidModule(module) 405 return module 406} 407 408type testRuleBuilderModule struct { 409 ModuleBase 410 properties struct { 411 Src string 412 413 Restat bool 414 Sbox bool 415 } 416} 417 418func (t *testRuleBuilderModule) GenerateAndroidBuildActions(ctx ModuleContext) { 419 in := PathForSource(ctx, t.properties.Src) 420 out := PathForModuleOut(ctx, ctx.ModuleName()) 421 outDep := PathForModuleOut(ctx, ctx.ModuleName()+".d") 422 outDir := PathForModuleOut(ctx) 423 424 testRuleBuilder_Build(ctx, in, out, outDep, outDir, t.properties.Restat, t.properties.Sbox) 425} 426 427type testRuleBuilderSingleton struct{} 428 429func testRuleBuilderSingletonFactory() Singleton { 430 return &testRuleBuilderSingleton{} 431} 432 433func (t *testRuleBuilderSingleton) GenerateBuildActions(ctx SingletonContext) { 434 in := PathForSource(ctx, "bar") 435 out := PathForOutput(ctx, "baz") 436 outDep := PathForOutput(ctx, "baz.d") 437 outDir := PathForOutput(ctx) 438 testRuleBuilder_Build(ctx, in, out, outDep, outDir, true, false) 439} 440 441func testRuleBuilder_Build(ctx BuilderContext, in Path, out, outDep, outDir WritablePath, restat, sbox bool) { 442 rule := NewRuleBuilder() 443 444 if sbox { 445 rule.Sbox(outDir) 446 } 447 448 rule.Command().Tool(PathForSource(ctx, "cp")).Input(in).Output(out).ImplicitDepFile(outDep) 449 450 if restat { 451 rule.Restat() 452 } 453 454 rule.Build(pctx, ctx, "rule", "desc") 455} 456 457func TestRuleBuilder_Build(t *testing.T) { 458 fs := map[string][]byte{ 459 "bar": nil, 460 "cp": nil, 461 } 462 463 bp := ` 464 rule_builder_test { 465 name: "foo", 466 src: "bar", 467 restat: true, 468 } 469 rule_builder_test { 470 name: "foo_sbox", 471 src: "bar", 472 sbox: true, 473 } 474 ` 475 476 config := TestConfig(buildDir, nil, bp, fs) 477 ctx := NewTestContext() 478 ctx.RegisterModuleType("rule_builder_test", testRuleBuilderFactory) 479 ctx.RegisterSingletonType("rule_builder_test", testRuleBuilderSingletonFactory) 480 ctx.Register(config) 481 482 _, errs := ctx.ParseFileList(".", []string{"Android.bp"}) 483 FailIfErrored(t, errs) 484 _, errs = ctx.PrepareBuildActions(config) 485 FailIfErrored(t, errs) 486 487 check := func(t *testing.T, params TestingBuildParams, wantCommand, wantOutput, wantDepfile string, wantRestat bool, extraCmdDeps []string) { 488 t.Helper() 489 if params.RuleParams.Command != wantCommand { 490 t.Errorf("\nwant RuleParams.Command = %q\n got %q", wantCommand, params.RuleParams.Command) 491 } 492 493 wantDeps := append([]string{"cp"}, extraCmdDeps...) 494 if !reflect.DeepEqual(params.RuleParams.CommandDeps, wantDeps) { 495 t.Errorf("\nwant RuleParams.CommandDeps = %q\n got %q", wantDeps, params.RuleParams.CommandDeps) 496 } 497 498 if params.RuleParams.Restat != wantRestat { 499 t.Errorf("want RuleParams.Restat = %v, got %v", wantRestat, params.RuleParams.Restat) 500 } 501 502 if len(params.Implicits) != 1 || params.Implicits[0].String() != "bar" { 503 t.Errorf("want Implicits = [%q], got %q", "bar", params.Implicits.Strings()) 504 } 505 506 if params.Output.String() != wantOutput { 507 t.Errorf("want Output = %q, got %q", wantOutput, params.Output) 508 } 509 510 if len(params.ImplicitOutputs) != 0 { 511 t.Errorf("want ImplicitOutputs = [], got %q", params.ImplicitOutputs.Strings()) 512 } 513 514 if params.Depfile.String() != wantDepfile { 515 t.Errorf("want Depfile = %q, got %q", wantDepfile, params.Depfile) 516 } 517 518 if params.Deps != blueprint.DepsGCC { 519 t.Errorf("want Deps = %q, got %q", blueprint.DepsGCC, params.Deps) 520 } 521 } 522 523 t.Run("module", func(t *testing.T) { 524 outFile := filepath.Join(buildDir, ".intermediates", "foo", "foo") 525 check(t, ctx.ModuleForTests("foo", "").Rule("rule"), 526 "cp bar "+outFile, 527 outFile, outFile+".d", true, nil) 528 }) 529 t.Run("sbox", func(t *testing.T) { 530 outDir := filepath.Join(buildDir, ".intermediates", "foo_sbox") 531 outFile := filepath.Join(outDir, "foo_sbox") 532 depFile := filepath.Join(outDir, "foo_sbox.d") 533 sbox := filepath.Join(buildDir, "host", config.PrebuiltOS(), "bin/sbox") 534 sandboxPath := shared.TempDirForOutDir(buildDir) 535 536 cmd := sbox + ` -c 'cp bar __SBOX_OUT_DIR__/foo_sbox' --sandbox-path ` + sandboxPath + " --output-root " + outDir + " --depfile-out " + depFile + " __SBOX_OUT_DIR__/foo_sbox" 537 538 check(t, ctx.ModuleForTests("foo_sbox", "").Rule("rule"), 539 cmd, outFile, depFile, false, []string{sbox}) 540 }) 541 t.Run("singleton", func(t *testing.T) { 542 outFile := filepath.Join(buildDir, "baz") 543 check(t, ctx.SingletonForTests("rule_builder_test").Rule("rule"), 544 "cp bar "+outFile, outFile, outFile+".d", true, nil) 545 }) 546} 547 548func Test_ninjaEscapeExceptForSpans(t *testing.T) { 549 type args struct { 550 s string 551 spans [][2]int 552 } 553 tests := []struct { 554 name string 555 args args 556 want string 557 }{ 558 { 559 name: "empty", 560 args: args{ 561 s: "", 562 }, 563 want: "", 564 }, 565 { 566 name: "unescape none", 567 args: args{ 568 s: "$abc", 569 }, 570 want: "$$abc", 571 }, 572 { 573 name: "unescape all", 574 args: args{ 575 s: "$abc", 576 spans: [][2]int{{0, 4}}, 577 }, 578 want: "$abc", 579 }, 580 { 581 name: "unescape first", 582 args: args{ 583 s: "$abc$", 584 spans: [][2]int{{0, 1}}, 585 }, 586 want: "$abc$$", 587 }, 588 { 589 name: "unescape last", 590 args: args{ 591 s: "$abc$", 592 spans: [][2]int{{4, 5}}, 593 }, 594 want: "$$abc$", 595 }, 596 { 597 name: "unescape middle", 598 args: args{ 599 s: "$a$b$c$", 600 spans: [][2]int{{2, 5}}, 601 }, 602 want: "$$a$b$c$$", 603 }, 604 { 605 name: "unescape multiple", 606 args: args{ 607 s: "$a$b$c$", 608 spans: [][2]int{{2, 3}, {4, 5}}, 609 }, 610 want: "$$a$b$c$$", 611 }, 612 } 613 for _, tt := range tests { 614 t.Run(tt.name, func(t *testing.T) { 615 if got := ninjaEscapeExceptForSpans(tt.args.s, tt.args.spans); got != tt.want { 616 t.Errorf("ninjaEscapeExceptForSpans() = %v, want %v", got, tt.want) 617 } 618 }) 619 } 620} 621