github.com/ouraigua/jenkins-library@v0.0.0-20231028010029-fbeaf2f3aa9b/test/groovy/com/sap/piper/cm/ChangeManagementTest.groovy (about) 1 package com.sap.piper.cm 2 import static org.hamcrest.Matchers.allOf 3 import static org.hamcrest.Matchers.contains 4 import static org.hamcrest.Matchers.containsString 5 import static org.hamcrest.Matchers.equalTo 6 import static org.hamcrest.Matchers.hasItem 7 import static org.hamcrest.Matchers.is 8 import static org.hamcrest.Matchers.not 9 import static org.junit.Assert.assertThat 10 11 import static org.junit.Assert.assertEquals 12 13 import org.hamcrest.Matchers 14 import org.junit.Assert 15 import org.junit.Rule 16 import org.junit.Test 17 import org.junit.rules.ExpectedException 18 import org.junit.rules.RuleChain 19 20 import com.sap.piper.GitUtils 21 22 import util.BasePiperTest 23 import util.JenkinsLoggingRule 24 import util.JenkinsScriptLoaderRule 25 import util.JenkinsShellCallRule 26 import util.JenkinsCredentialsRule 27 import util.JenkinsDockerExecuteRule 28 import util.JenkinsFileExistsRule 29 import util.Rules 30 31 import hudson.AbortException 32 33 public class ChangeManagementTest extends BasePiperTest { 34 35 private ExpectedException thrown = ExpectedException.none() 36 37 private JenkinsShellCallRule script = new JenkinsShellCallRule(this) 38 private JenkinsLoggingRule logging = new JenkinsLoggingRule(this) 39 private JenkinsDockerExecuteRule dockerExecuteRule = new JenkinsDockerExecuteRule(this) 40 private JenkinsFileExistsRule files = new JenkinsFileExistsRule(this) 41 42 @Rule 43 public RuleChain rules = Rules.getCommonRules(this) 44 .around(thrown) 45 .around(script) 46 .around(logging) 47 .around(new JenkinsCredentialsRule(this).withCredentials('me','user','password')) 48 .around(dockerExecuteRule) 49 .around(files) 50 51 @Test 52 public void testRetrieveChangeDocumentIdOutsideGitWorkTreeTest() { 53 54 thrown.expect(ChangeManagementException) 55 thrown.expectMessage('Cannot retrieve ChangeDocumentId. ' + 56 'Not in a git work tree. ' + 57 'ChangeDocumentId is extracted from git commit messages.') 58 59 new ChangeManagement(nullScript, gitUtilsMock(false, new String[0])).getChangeDocumentId() 60 } 61 62 @Test 63 public void testRetrieveChangeDocumentIdNothingFound() { 64 65 thrown.expect(ChangeManagementException) 66 thrown.expectMessage('Cannot retrieve ChangeDocumentId from git commits.') 67 68 new ChangeManagement(nullScript, gitUtilsMock(true, new String[0])).getChangeDocumentId() 69 } 70 71 @Test 72 public void testRetrieveChangeDocumentIdReturnsArrayWithNullValue() { 73 74 thrown.expect(ChangeManagementException) 75 thrown.expectMessage('Cannot retrieve ChangeDocumentId from git commits.') 76 77 new ChangeManagement(nullScript, gitUtilsMock(true, (String[])[ null ])).getChangeDocumentId() 78 } 79 80 @Test 81 public void testRetrieveChangeDocumentNotUnique() { 82 83 thrown.expect(ChangeManagementException) 84 thrown.expectMessage('Multiple ChangeDocumentIds found') 85 86 String[] changeIds = [ 'a', 'b' ] 87 new ChangeManagement(nullScript, gitUtilsMock(true, changeIds)).getChangeDocumentId() 88 } 89 90 @Test 91 public void testRetrieveChangeDocumentSameChangeIdFoundTwice() { 92 93 String[] changeIds = [ 'a', 'a' ] 94 def changeID = new ChangeManagement(nullScript, gitUtilsMock(true, changeIds)).getChangeDocumentId() 95 96 assert changeID == 'a' 97 } 98 99 @Test 100 public void testIsChangeInDevelopmentReturnsTrueWhenChangeIsInDevelopent() { 101 102 script.setReturnValue(JenkinsShellCallRule.Type.REGEX, "cmclient.*is-change-in-development -cID '001'", 0) 103 boolean inDevelopment = new ChangeManagement(nullScript, null).isChangeInDevelopment( 104 [ 105 image: 'ppiper/cm-client', 106 pullImage: true, 107 ], 108 '001', 'endpoint', 'me') 109 110 assertThat(inDevelopment, is(equalTo(true))) 111 assertThat(script.shell[0], allOf(containsString("cmclient"), 112 containsString("-u 'user'"), 113 containsString("-p 'password'"), 114 containsString("-e 'endpoint'"), 115 containsString('is-change-in-development'), 116 containsString("-cID '001'"), 117 containsString("-t SOLMAN"))) 118 119 assert dockerExecuteRule.getDockerParams().dockerImage == 'ppiper/cm-client' 120 assert dockerExecuteRule.getDockerParams().dockerPullImage == true 121 } 122 123 @Test 124 public void testIsChangeInDevelopmentReturnsFalseWhenChangeIsNotInDevelopent() { 125 126 script.setReturnValue(JenkinsShellCallRule.Type.REGEX, "cmclient.*is-change-in-development -cID '001'", 3) 127 128 boolean inDevelopment = new ChangeManagement(nullScript, null) 129 .isChangeInDevelopment([:], 130 '001', 131 'endpoint', 132 'me') 133 134 assertThat(inDevelopment, is(equalTo(false))) 135 } 136 137 @Test 138 public void testIsChangeInDevelopmentThrowsExceptionWhenCMClientReturnsUnexpectedExitCode() { 139 140 thrown.expect(ChangeManagementException) 141 thrown.expectMessage('Cannot retrieve status for change document \'001\'. Does this change exist? Return code from cmclient: 1.') 142 143 script.setReturnValue(JenkinsShellCallRule.Type.REGEX, "cmclient.*is-change-in-development -cID '001'", 1) 144 new ChangeManagement(nullScript, null).isChangeInDevelopment([:], '001', 'endpoint', 'me') 145 } 146 147 @Test 148 public void testGetCommandLineWithoutCMClientOpts() { 149 String commandLine = new ChangeManagement(nullScript, null) 150 .getCMCommandLine(BackendType.SOLMAN, 151 'https://example.org/cm', 152 "me", 153 "topSecret", 154 "the-command", 155 ["-key1", "val1", "-key2", "val2"]) 156 commandLine = commandLine.replaceAll(' +', " ") 157 assertThat(commandLine, not(containsString("CMCLIENT_OPTS"))) 158 assertThat(commandLine, containsString("cmclient -e 'https://example.org/cm' -u 'me' -p 'topSecret' -t SOLMAN the-command -key1 val1 -key2 val2")) 159 } 160 161 @Test 162 public void testGetCommandLineWithCMClientOpts() { 163 String commandLine = new ChangeManagement(nullScript, null) 164 .getCMCommandLine(BackendType.SOLMAN, 165 'https://example.org/cm', 166 "me", 167 "topSecret", 168 "the-command", 169 ["-key1", "val1", "-key2", "val2"], 170 '-Djavax.net.debug=all') 171 commandLine = commandLine.replaceAll(' +', " ") 172 assertThat(commandLine, containsString('export CMCLIENT_OPTS="-Djavax.net.debug=all"')) 173 } 174 175 @Test 176 public void testCreateTransportRequestSOLMANSucceeds() { 177 178 script.setReturnValue(JenkinsShellCallRule.Type.REGEX, ".*cmclient.*create-transport -cID 001 -dID 002.*", '004') 179 def transportRequestId = new ChangeManagement(nullScript).createTransportRequestSOLMAN( 180 [ 181 image: 'ppiper/cm-client', 182 pullImage: true, 183 ], 184 '001', '002', '003', 'me') 185 186 // the check for the transportRequestID is sufficient. This checks implicit the command line since that value is 187 // returned only in case the shell call matches. 188 assert transportRequestId == '004' 189 190 assert dockerExecuteRule.getDockerParams().dockerImage == 'ppiper/cm-client' 191 assert dockerExecuteRule.getDockerParams().dockerPullImage == true 192 193 } 194 195 @Test 196 public void testCreateTransportRequestRFCSucceeds() { 197 198 script.setReturnValue('cts createTransportRequest', '{"REQUESTID":"XYZK9000004"}') 199 200 def transportRequestId = new ChangeManagement(nullScript).createTransportRequestRFC( 201 [image: 'rfc', options: []], 202 'https://example.org/rfc', // endpoint 203 '01', // instance 204 '001', // client 205 'me', // credentialsId 206 'Lorem ipsum', // description 207 true // verbose 208 ) 209 210 assert dockerExecuteRule.dockerParams.dockerImage == 'rfc' 211 212 assert dockerExecuteRule.dockerParams.dockerEnvVars == [ 213 TRANSPORT_DESCRIPTION: 'Lorem ipsum', 214 ABAP_DEVELOPMENT_INSTANCE: '01', 215 ABAP_DEVELOPMENT_CLIENT: '001', 216 ABAP_DEVELOPMENT_SERVER: 'https://example.org/rfc', 217 ABAP_DEVELOPMENT_USER: 'user', 218 ABAP_DEVELOPMENT_PASSWORD: 'password', 219 VERBOSE: true 220 ] 221 222 assert transportRequestId == 'XYZK9000004' 223 224 } 225 226 @Test 227 public void testCreateTransportRequestRFCFails() { 228 229 thrown.expect(ChangeManagementException) 230 thrown.expectMessage('Cannot create transport request: script returned exit code 3') 231 232 script.setReturnValue('cts createTransportRequest', 233 { throw new AbortException('script returned exit code 3')}) 234 235 def transportRequestId = new ChangeManagement(nullScript).createTransportRequestRFC( 236 [image: 'rfc', options: []], 237 'https://example.org/rfc', // endpoint 238 '001', // client 239 '01', // instance 240 'me', // credentialsId 241 'Lorem ipsum', // description 242 true, //verbose 243 ) 244 } 245 246 @Test 247 public void testCreateTransportRequestCTSSucceeds() { 248 249 script.setReturnValue(JenkinsShellCallRule.Type.REGEX, 'cmclient.* -t CTS .*create-transport -tt W -ts XYZ -d "desc 123"$', '004') 250 def transportRequestId = new ChangeManagement(nullScript) 251 .createTransportRequestCTS( 252 [ 253 image: 'ppiper/cmclient', 254 pullImage: true 255 ], 256 'W', // transport type 257 'XYZ', // target system 258 'desc 123', // description 259 'https://example.org/cm', 260 'me') 261 262 // the check for the transportRequestID is sufficient. This checks implicit the command line since that value is 263 // returned only in case the shell call matches. 264 assert transportRequestId == '004' 265 266 dockerExecuteRule.getDockerParams().dockerImage = 'ppiper/cmclient' 267 dockerExecuteRule.getDockerParams().dockerPullImage = true 268 269 } 270 271 @Test 272 public void testUploadFileToTransportSucceedsSOLMAN() { 273 274 // the regex provided below is an implicit check that the command line is fine. 275 script.setReturnValue(JenkinsShellCallRule.Type.REGEX, 'upload-file-to-transport.*-cID 001 -tID 002 XXX "/path"', 0) 276 277 new ChangeManagement(nullScript).uploadFileToTransportRequestSOLMAN( 278 [ 279 image: 'ppiper/cm-client', 280 pullImage: true, 281 volumeBind: ['/home/me/certs' : '/certs'], 282 ], 283 '001', 284 '002', 285 'XXX', 286 '/path', 287 'https://example.org/cm', 288 'me') 289 290 // no assert required here for the shell script, since the regex registered above 291 // to the script rule is an implicit check for the command line. 292 293 assert dockerExecuteRule.getDockerParams().dockerImage == 'ppiper/cm-client' 294 assert dockerExecuteRule.getDockerParams().dockerPullImage 295 assert dockerExecuteRule.getDockerParams().dockerVolumeBind == ['/home/me/certs' : '/certs'] 296 } 297 298 @Test 299 public void testUploadFileToTransportSucceedsCTSDeployConfigYamlExists() { 300 301 files.existingFiles.add('ui5-deploy.yaml') 302 303 new ChangeManagement(nullScript).uploadFileToTransportRequestCTS( 304 [ 305 image: 'node', 306 pullImage: true 307 ], 308 '002', 309 'https://example.org/cm', 310 '001', 311 'myApp', 312 'the description', 313 'aPackage', 314 'node2', 315 ['@ui5/cli', '@sap/ux-ui5-tooling', '@ui5/logger', '@ui5/fs', '@dummy/foo'], 316 ['--verbose'], 317 'ui5-deploy.yaml', 318 'me', 319 ) 320 321 assert script.shell[0].contains('npm install --global --verbose @ui5/cli @sap/ux-ui5-tooling @ui5/logger @ui5/fs @dummy/foo') 322 323 assert script.shell[0].contains("fiori deploy -c \"ui5-deploy.yaml\" -t 002 -u https://example.org/cm") 324 325 assert dockerExecuteRule.getDockerParams().dockerImage == 'node' 326 assert dockerExecuteRule.getDockerParams().dockerPullImage == true 327 assert dockerExecuteRule.getDockerParams().dockerEnvVars == [ABAP_USER: "user", ABAP_PASSWORD: 'password'] 328 // we launch the container as root (uid 0) in order to be able to install 329 // the deploytool. Before deploying we su to another user. 330 assert dockerExecuteRule.getDockerParams().dockerOptions == ['-u', '0'] 331 } 332 333 @Test 334 public void testUploadFileToTransportSucceedsCTSDefaultDeployConfigYamlDoesNotExist() { 335 336 // the file does not exist, since it was not explicity added to the files rule 337 338 new ChangeManagement(nullScript).uploadFileToTransportRequestCTS( 339 [ 340 image: 'node', 341 pullImage: true 342 ], 343 '002', 344 'https://example.org/cm', 345 '001', 346 'myApp', 347 'the description', 348 'aPackage', 349 'node2', 350 ['@ui5/cli', '@sap/ux-ui5-tooling', '@ui5/logger', '@ui5/fs', '@dummy/foo'], 351 ['--verbose'], 352 'ui5-deploy.yaml', 353 'me', 354 ) 355 356 // more details already checked with test "testUploadFileToTransportSucceedsCTSDeployConfigYamlExists" 357 assert script.shell[0].contains("fiori deploy --noConfig -t 002 -u https://example.org/cm") 358 } 359 360 @Test 361 public void testUploadFileToTransportFailsCTSExplicitlyConfiguredDeployConfigYamlDoesNotExist() { 362 363 // the file does not exist, since it was not explicitly added to the files rule 364 365 thrown.expect(AbortException) 366 thrown.expectMessage('Configured deploy config file \'my-deploy.yaml\' does not exists.') 367 368 new ChangeManagement(nullScript).uploadFileToTransportRequestCTS( 369 [ 370 image: 'node', 371 pullImage: true 372 ], 373 '002', 374 'https://example.org/cm', 375 '001', 376 'myApp', 377 'the description', 378 'aPackage', 379 'node2', 380 ['@ui5/cli', '@sap/ux-ui5-tooling', '@ui5/logger', '@ui5/fs', '@dummy/foo'], 381 ['--verbose'], 382 'my-deploy.yaml', 383 'me', 384 ) 385 386 // more details already checked with test "testUploadFileToTransportSucceedsCTSDeployConfigYamlExists" 387 assert script.shell[0].contains("fiori deploy --noConfig -t 002 -u https://example.org/cm") 388 } 389 390 @Test 391 public void testUploadFileToTransportSucceesCTSExplicitlyConfiguredDeployConfigYamExists() { 392 393 files.existingFiles.add('my-deploy.yaml') 394 395 new ChangeManagement(nullScript).uploadFileToTransportRequestCTS( 396 [ 397 image: 'node', 398 pullImage: true 399 ], 400 '002', 401 'https://example.org/cm', 402 '001', 403 'myApp', 404 'the description', 405 'aPackage', 406 'node2', 407 ['@ui5/cli', '@sap/ux-ui5-tooling', '@ui5/logger', '@ui5/fs', '@dummy/foo'], 408 ['--verbose'], 409 'my-deploy.yaml', 410 'me', 411 ) 412 413 // more details already checked with test "testUploadFileToTransportSucceedsCTSDeployConfigYamlExists" 414 assert script.shell[0].contains("fiori deploy -c \"my-deploy.yaml\" -t 002 -u https://example.org/cm") 415 } 416 417 @Test 418 public void testUploadFileToTransportSucceedsEmptyDeployToolDependenciesCTS() { 419 420 new ChangeManagement(nullScript).uploadFileToTransportRequestCTS( 421 [ 422 image: 'fioriDeployImage', 423 pullImage: true 424 ], 425 '002', 426 'https://example.org/cm', 427 '001', 428 'myApp', 429 'aPackage', 430 'the description', 431 'node2', 432 [], 433 [], 434 'ui5-deploy.yaml', 435 'me', 436 ) 437 438 assert ! script.shell[0].contains('npm install') 439 assert ! script.shell[0].contains('su') 440 441 assert script.shell[0].contains("fiori deploy") 442 443 assert dockerExecuteRule.getDockerParams().dockerImage == 'fioriDeployImage' 444 assert dockerExecuteRule.getDockerParams().dockerPullImage == true 445 assert dockerExecuteRule.getDockerParams().dockerEnvVars == [ABAP_USER: "user", ABAP_PASSWORD: 'password'] 446 // we don't start with the root user since there is no need to install something (globally) 447 assert dockerExecuteRule.getDockerParams().dockerOptions == [] 448 } 449 450 @Test 451 public void testUploadFileToTransportShellFailsCTS() { 452 453 thrown.expect(AbortException) 454 thrown.expectMessage('script returned exit code 1') 455 456 script.setReturnValue(JenkinsShellCallRule.Type.REGEX, '.*fiori deploy.*', 457 { throw new AbortException('script returned exit code 1') }) 458 459 new ChangeManagement(nullScript).uploadFileToTransportRequestCTS( 460 [ 461 image: 'node', 462 pullImage: true 463 ], 464 '002', 465 'https://example.org/cm', 466 '001', 467 'myApp', 468 'aPackage', 469 'the description', 470 'node', 471 '@ui5/cli @sap/ux-ui5-tooling @ui5/logger @ui5/fs', 472 [], 473 'ui5-deploy.yaml', 474 'me', 475 ) 476 } 477 478 @Test 479 public void testUploadFileToTransportSucceedsRFC() { 480 481 new ChangeManagement(nullScript).uploadFileToTransportRequestRFC( 482 [image:'rfc', options: [], pullImage: true], 483 '002', //transportRequestId 484 '001', // applicationId 485 'https://example.org/mypath/deployArtifact.zip', 486 'https://example.org/rfc', 487 'me', 488 '00', //developmentInstance 489 '001', // developmentClient 490 'Lorem ipsum', // applicationDescription 491 'XYZ', // abapPackage 492 'UTF-9', //codePage 493 true, // accept unix style EOL 494 true, // failUploadOnWarning 495 false, // verbose 496 ) 497 498 499 assert dockerExecuteRule.dockerParams.dockerImage == 'rfc' 500 assert dockerExecuteRule.dockerParams.dockerPullImage == true 501 502 assert dockerExecuteRule.dockerParams.dockerEnvVars == 503 [ 504 ABAP_DEVELOPMENT_INSTANCE: '00', 505 ABAP_DEVELOPMENT_CLIENT: '001', 506 ABAP_APPLICATION_NAME: '001', 507 ABAP_APPLICATION_DESC: 'Lorem ipsum', 508 ABAP_PACKAGE: 'XYZ', 509 ZIP_FILE_URL: 'https://example.org/mypath/deployArtifact.zip', 510 ABAP_DEVELOPMENT_SERVER: 'https://example.org/rfc', 511 ABAP_DEVELOPMENT_USER: 'user', 512 ABAP_DEVELOPMENT_PASSWORD: 'password', 513 CODE_PAGE: 'UTF-9', 514 ABAP_ACCEPT_UNIX_STYLE_EOL: 'X', 515 FAIL_UPLOAD_ON_WARNING: 'true', 516 VERBOSE: 'false' 517 ] 518 519 assertThat(script.shell, contains('cts uploadToABAP:002')) 520 } 521 522 @Test 523 public void testUploadFileToTransportFailsRFC() { 524 525 thrown.expect(ChangeManagementException) 526 thrown.expectMessage('Cannot upload file into transport request. Return code from rfc client: 1.') 527 528 script.setReturnValue('cts uploadToABAP:002', 1) 529 530 new ChangeManagement(nullScript).uploadFileToTransportRequestRFC( 531 [:], 532 '002', //transportRequestId 533 '001', // applicationId 534 'https://example.org/mypath/deployArtifact.zip', 535 'https://example.org/rfc', 536 'me', 537 '00', //developmentInstance 538 '001', // developmentClient 539 'Lorem ipsum', // applicationDescription 540 'XYZ', // abapPackage 541 'UTF-9', // codePage 542 true, // accept unix style EOL 543 true, // failUploadOnWarning 544 false, // verbose 545 ) 546 } 547 548 @Test 549 public void testUploadFileToTransportFailsSOLMAN() { 550 551 thrown.expect(ChangeManagementException) 552 thrown.expectMessage("Cannot upload file into transport request. " + 553 "Return code from cm client: 1.") 554 555 script.setReturnValue(JenkinsShellCallRule.Type.REGEX,, 'upload-file-to-transport', 1) 556 557 new ChangeManagement(nullScript).uploadFileToTransportRequestSOLMAN( 558 [:], 559 '001', 560 '002', 561 'XXX', 562 '/path', 563 'https://example.org/cm', 564 'me') 565 } 566 567 @Test 568 public void testReleaseTransportRequestSucceedsSOLMAN() { 569 570 // the regex provided below is an implicit check that the command line is fine. 571 script.setReturnValue(JenkinsShellCallRule.Type.REGEX, '-t SOLMAN release-transport.*-cID 001.*-tID 002', 0) 572 573 new ChangeManagement(nullScript).releaseTransportRequestSOLMAN( 574 [ 575 image: 'ppiper/cm-client', 576 imagePull: true, 577 ], 578 '001', 579 '002', 580 'https://example.org', 581 'me', 582 'openSesame') 583 584 // no assert required here, since the regex registered above to the script rule is an implicit check for 585 // the command line. 586 587 dockerExecuteRule.getDockerParams().dockerImage == 'ppiper/cm-client' 588 dockerExecuteRule.getDockerParams().pullImage == true 589 } 590 591 @Test 592 public void testReleaseTransportRequestSucceedsCTS() { 593 594 // the regex provided below is an implicit check that the command line is fine. 595 script.setReturnValue(JenkinsShellCallRule.Type.REGEX, '-t CTS export-transport.*-tID 002', 0) 596 597 new ChangeManagement(nullScript).releaseTransportRequestCTS( 598 [ 599 image: 'ppiper/cm-client', 600 pullImage: true, 601 ], 602 '002', 603 'https://example.org', 604 'me', 605 'openSesame') 606 607 // no assert required here, since the regex registered above to the script rule is an implicit check for 608 // the command line. 609 610 assert dockerExecuteRule.getDockerParams().dockerImage == 'ppiper/cm-client' 611 assert dockerExecuteRule.getDockerParams().dockerPullImage == true 612 } 613 614 @Test 615 public void testReleaseTransportRequestSucceedsRFC() { 616 617 new ChangeManagement(nullScript).releaseTransportRequestRFC( 618 [:], 619 '002', 620 'https://example.org', 621 '002', 622 '001', 623 'me', 624 true) 625 626 assert dockerExecuteRule.dockerParams.dockerEnvVars == [ 627 ABAP_DEVELOPMENT_SERVER: 'https://example.org', 628 ABAP_DEVELOPMENT_USER: 'user', 629 ABAP_DEVELOPMENT_PASSWORD: 'password', 630 ABAP_DEVELOPMENT_CLIENT: '001', 631 ABAP_DEVELOPMENT_INSTANCE: '002', 632 VERBOSE: true, 633 ] 634 635 assertThat(script.shell, hasItem('cts releaseTransport:002')) 636 } 637 638 @Test 639 public void testReleaseTransportRequestFailsSOLMAN() { 640 641 thrown.expect(ChangeManagementException) 642 thrown.expectMessage("Cannot release Transport Request '002'. Return code from cmclient: 1.") 643 644 // the regex provided below is an implicit check that the command line is fine. 645 script.setReturnValue(JenkinsShellCallRule.Type.REGEX, 'release-transport.*-cID 001.*-tID 002', 1) 646 647 new ChangeManagement(nullScript).releaseTransportRequestSOLMAN( 648 [ 649 image: 'ppiper/cm-client', 650 imagePull: true, 651 ], 652 '001', 653 '002', 654 'https://example.org', 655 'me') 656 } 657 658 private GitUtils gitUtilsMock(boolean insideWorkTree, String[] changeIds) { 659 return new GitUtils() { 660 public boolean insideWorkTree() { 661 return insideWorkTree 662 } 663 664 public String[] extractLogLines( 665 String filter = '', 666 String from = 'origin/master', 667 String to = 'HEAD', 668 String format = '%b') { 669 return changeIds 670 } 671 } 672 } 673 }