1 /* 2 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 package org.openjdk.skara.bots.mlbridge; 24 25 import org.openjdk.skara.email.EmailAddress; 26 import org.openjdk.skara.forge.*; 27 import org.openjdk.skara.network.URIBuilder; 28 import org.openjdk.skara.mailinglist.MailingListServerFactory; 29 import org.openjdk.skara.test.*; 30 import org.openjdk.skara.vcs.Repository; 31 32 import org.junit.jupiter.api.*; 33 34 import java.io.IOException; 35 import java.nio.charset.StandardCharsets; 36 import java.nio.file.*; 37 import java.time.Duration; 38 import java.util.*; 39 import java.util.regex.Pattern; 40 import java.util.stream.Collectors; 41 42 import static org.junit.jupiter.api.Assertions.*; 43 44 class MailingListBridgeBotTests { 45 private Optional<String> archiveContents(Path archive) { 46 try { 47 var mbox = Files.find(archive, 50, (path, attrs) -> path.toString().endsWith(".mbox")).findAny(); 48 if (mbox.isEmpty()) { 49 return Optional.empty(); 50 } 51 return Optional.of(Files.readString(mbox.get(), StandardCharsets.UTF_8)); 52 } catch (IOException e) { 53 return Optional.empty(); 54 } 55 56 } 57 58 private boolean archiveContains(Path archive, String text) { 59 return archiveContainsCount(archive, text) > 0; 60 } 61 62 private int archiveContainsCount(Path archive, String text) { 63 var lines = archiveContents(archive); 64 if (lines.isEmpty()) { 65 return 0; 66 } 67 var pattern = Pattern.compile(text); 68 int count = 0; 69 for (var line : lines.get().split("\\R")) { 70 var matcher = pattern.matcher(line); 71 if (matcher.find()) { 72 count++; 73 } 74 } 75 return count; 76 } 77 78 private boolean webrevContains(Path webrev, String text) { 79 try { 80 var index = Files.find(webrev, 5, (path, attrs) -> path.toString().endsWith("index.html")).findAny(); 81 if (index.isEmpty()) { 82 return false; 83 } 84 var lines = Files.readString(index.get(), StandardCharsets.UTF_8); 85 return lines.contains(text); 86 } catch (IOException e) { 87 return false; 88 } 89 } 90 91 private long countSubstrings(String string, String substring) { 92 return Pattern.compile(substring).matcher(string).results().count(); 93 } 94 95 private String noreplyAddress(HostedRepository repository) { 96 return "test+" + repository.forge().currentUser().id() + "+" + 97 repository.forge().currentUser().userName() + 98 "@openjdk.java.net"; 99 } 100 101 @Test 102 void simpleArchive(TestInfo testInfo) throws IOException { 103 try (var credentials = new HostCredentials(testInfo); 104 var tempFolder = new TemporaryDirectory(); 105 var archiveFolder = new TemporaryDirectory(); 106 var webrevFolder = new TemporaryDirectory(); 107 var listServer = new TestMailmanServer()) { 108 var author = credentials.getHostedRepository(); 109 var archive = credentials.getHostedRepository(); 110 var ignored = credentials.getHostedRepository(); 111 var listAddress = EmailAddress.parse(listServer.createList("test")); 112 var censusBuilder = credentials.getCensusBuilder() 113 .addAuthor(author.forge().currentUser().id()); 114 var from = EmailAddress.from("test", "test@test.mail"); 115 var mlBot = new MailingListBridgeBot(from, author, archive, "master", 116 censusBuilder.build(), "master", listAddress, 117 Set.of(ignored.forge().currentUser().userName()), 118 Set.of(), 119 listServer.getArchive(), listServer.getSMTP(), 120 archive, "webrev", Path.of("test"), 121 URIBuilder.base("http://www.test.test/").build(), 122 Set.of("rfr"), Map.of(ignored.forge().currentUser().userName(), 123 Pattern.compile("ready")), 124 URIBuilder.base("http://issues.test/browse/").build(), 125 Map.of("Extra1", "val1", "Extra2", "val2"), 126 Duration.ZERO); 127 128 // Populate the projects repository 129 var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType()); 130 var masterHash = localRepo.resolve("master").orElseThrow(); 131 localRepo.push(masterHash, author.url(), "master", true); 132 localRepo.push(masterHash, archive.url(), "webrev", true); 133 134 // Make a change with a corresponding PR 135 var editHash = CheckableRepository.appendAndCommit(localRepo, "A simple change", 136 "Change msg\n\nWith several lines"); 137 localRepo.push(editHash, author.url(), "edit", true); 138 var pr = credentials.createPullRequest(archive, "master", "edit", "1234: This is a pull request"); 139 pr.setBody("This should not be ready"); 140 141 // Run an archive pass 142 TestBotRunner.runPeriodicItems(mlBot); 143 144 // A PR that isn't ready for review should not be archived 145 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 146 assertFalse(archiveContains(archiveFolder.path(), "This is a pull request")); 147 148 // Flag it as ready for review 149 pr.setBody("This should now be ready"); 150 pr.addLabel("rfr"); 151 152 // Run another archive pass 153 TestBotRunner.runPeriodicItems(mlBot); 154 155 // But it should still not be archived 156 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 157 assertFalse(archiveContains(archiveFolder.path(), "This is a pull request")); 158 159 // Now post a general comment - not a ready marker 160 var ignoredPr = ignored.pullRequest(pr.id()); 161 ignoredPr.addComment("hello there"); 162 163 // Run another archive pass 164 TestBotRunner.runPeriodicItems(mlBot); 165 166 // It should still not be archived 167 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 168 assertFalse(archiveContains(archiveFolder.path(), "This is a pull request")); 169 170 // Now post a ready comment 171 ignoredPr.addComment("ready"); 172 173 // Run another archive pass 174 TestBotRunner.runPeriodicItems(mlBot); 175 176 // The archive should now contain an entry 177 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 178 assertTrue(archiveContains(archiveFolder.path(), "This is a pull request")); 179 assertTrue(archiveContains(archiveFolder.path(), "This should now be ready")); 180 assertTrue(archiveContains(archiveFolder.path(), "Patch:")); 181 assertTrue(archiveContains(archiveFolder.path(), "Changes:")); 182 assertTrue(archiveContains(archiveFolder.path(), "Webrev:")); 183 assertTrue(archiveContains(archiveFolder.path(), "http://www.test.test/")); 184 assertTrue(archiveContains(archiveFolder.path(), "webrev.00")); 185 assertTrue(archiveContains(archiveFolder.path(), "Issue:")); 186 assertTrue(archiveContains(archiveFolder.path(), "http://issues.test/browse/TSTPRJ-1234")); 187 assertTrue(archiveContains(archiveFolder.path(), "Fetch:")); 188 assertTrue(archiveContains(archiveFolder.path(), "^ - " + editHash.abbreviate() + ": Change msg")); 189 assertFalse(archiveContains(archiveFolder.path(), "With several lines")); 190 191 // The mailing list as well 192 listServer.processIncoming(); 193 var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO); 194 var mailmanList = mailmanServer.getList(listAddress.address()); 195 var conversations = mailmanList.conversations(Duration.ofDays(1)); 196 assertEquals(1, conversations.size()); 197 var mail = conversations.get(0).first(); 198 assertEquals("RFR: 1234: This is a pull request", mail.subject()); 199 assertEquals(pr.author().fullName(), mail.author().fullName().orElseThrow()); 200 assertEquals(noreplyAddress(archive), mail.author().address()); 201 assertEquals(listAddress, mail.sender()); 202 assertEquals("val1", mail.headerValue("Extra1")); 203 assertEquals("val2", mail.headerValue("Extra2")); 204 205 // And there should be a webrev 206 Repository.materialize(webrevFolder.path(), archive.url(), "webrev"); 207 assertTrue(webrevContains(webrevFolder.path(), "1 lines changed")); 208 var comments = pr.comments(); 209 var webrevComments = comments.stream() 210 .filter(comment -> comment.author().equals(author.forge().currentUser())) 211 .filter(comment -> comment.body().contains("webrev")) 212 .filter(comment -> comment.body().contains(editHash.hex())) 213 .collect(Collectors.toList()); 214 assertEquals(1, webrevComments.size()); 215 216 // Add a comment 217 pr.addComment("This is a comment :smile:"); 218 219 // Add a comment from an ignored user as well 220 ignoredPr.addComment("Don't mind me"); 221 222 // Run another archive pass 223 TestBotRunner.runPeriodicItems(mlBot); 224 225 // The archive should now contain the comment, but not the ignored one 226 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 227 assertTrue(archiveContains(archiveFolder.path(), "This is a comment")); 228 assertTrue(archiveContains(archiveFolder.path(), "> This should now be ready")); 229 assertFalse(archiveContains(archiveFolder.path(), "Don't mind me")); 230 231 listServer.processIncoming(); 232 conversations = mailmanList.conversations(Duration.ofDays(1)); 233 assertEquals(1, conversations.size()); 234 assertEquals(2, conversations.get(0).allMessages().size()); 235 236 // Remove the rfr flag and post another comment 237 pr.addLabel("rfr"); 238 pr.addComment("This is another comment"); 239 240 // Run another archive pass 241 TestBotRunner.runPeriodicItems(mlBot); 242 243 // The archive should contain the additional comment 244 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 245 assertTrue(archiveContains(archiveFolder.path(), "This is another comment")); 246 assertTrue(archiveContains(archiveFolder.path(), ">> This should now be ready")); 247 248 listServer.processIncoming(); 249 conversations = mailmanList.conversations(Duration.ofDays(1)); 250 assertEquals(1, conversations.size()); 251 assertEquals(3, conversations.get(0).allMessages().size()); 252 for (var newMail : conversations.get(0).allMessages()) { 253 assertEquals(noreplyAddress(archive), newMail.author().address()); 254 assertEquals(listAddress, newMail.sender()); 255 } 256 assertTrue(conversations.get(0).allMessages().get(2).body().contains("This is a comment 😄")); 257 } 258 } 259 260 @Test 261 void reviewComment(TestInfo testInfo) throws IOException { 262 try (var credentials = new HostCredentials(testInfo); 263 var tempFolder = new TemporaryDirectory(); 264 var archiveFolder = new TemporaryDirectory(); 265 var listServer = new TestMailmanServer()) { 266 var author = credentials.getHostedRepository(); 267 var archive = credentials.getHostedRepository(); 268 var ignored = credentials.getHostedRepository(); 269 var listAddress = EmailAddress.parse(listServer.createList("test")); 270 var censusBuilder = credentials.getCensusBuilder() 271 .addAuthor(author.forge().currentUser().id()); 272 var from = EmailAddress.from("test", "test@test.mail"); 273 var mlBot = new MailingListBridgeBot(from, author, archive, "master", 274 censusBuilder.build(), "master", listAddress, 275 Set.of(ignored.forge().currentUser().userName()), 276 Set.of(), 277 listServer.getArchive(), listServer.getSMTP(), 278 archive, "webrev", Path.of("test"), 279 URIBuilder.base("http://www.test.test/").build(), 280 Set.of(), Map.of(), 281 URIBuilder.base("http://issues.test/browse/").build(), 282 Map.of(), Duration.ZERO); 283 284 // Populate the projects repository 285 var reviewFile = Path.of("reviewfile.txt"); 286 var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile); 287 var masterHash = localRepo.resolve("master").orElseThrow(); 288 localRepo.push(masterHash, author.url(), "master", true); 289 localRepo.push(masterHash, archive.url(), "webrev", true); 290 291 // Make a change with a corresponding PR 292 var editHash = CheckableRepository.appendAndCommit(localRepo); 293 localRepo.push(editHash, author.url(), "edit", true); 294 var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request"); 295 pr.setBody("This is now ready"); 296 TestBotRunner.runPeriodicItems(mlBot); 297 listServer.processIncoming(); 298 299 // And make a file specific comment 300 var currentMaster = localRepo.resolve("master").orElseThrow(); 301 var comment = pr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Review comment"); 302 303 // Add one from an ignored user as well 304 var ignoredPr = ignored.pullRequest(pr.id()); 305 ignoredPr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Don't mind me"); 306 307 // Process comments 308 TestBotRunner.runPeriodicItems(mlBot); 309 listServer.processIncoming(); 310 311 // The archive should now contain an entry 312 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 313 assertTrue(archiveContains(archiveFolder.path(), "This is a pull request")); 314 assertTrue(archiveContains(archiveFolder.path(), "This is now ready")); 315 assertTrue(archiveContains(archiveFolder.path(), "Review comment")); 316 assertTrue(archiveContains(archiveFolder.path(), "> This is now ready")); 317 assertTrue(archiveContains(archiveFolder.path(), reviewFile.toString())); 318 assertFalse(archiveContains(archiveFolder.path(), "Don't mind me")); 319 320 // The mailing list as well 321 var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO); 322 var mailmanList = mailmanServer.getList(listAddress.address()); 323 var conversations = mailmanList.conversations(Duration.ofDays(1)); 324 assertEquals(1, conversations.size()); 325 var mail = conversations.get(0).first(); 326 assertEquals("RFR: This is a pull request", mail.subject()); 327 328 // Comment on the comment 329 pr.addReviewCommentReply(comment, "This is a review reply"); 330 TestBotRunner.runPeriodicItems(mlBot); 331 listServer.processIncoming(); 332 333 // The archive should contain the additional comment (but no quoted footers) 334 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 335 assertTrue(archiveContains(archiveFolder.path(), "This is a review reply")); 336 assertTrue(archiveContains(archiveFolder.path(), ">> This is now ready")); 337 assertFalse(archiveContains(archiveFolder.path(), "^> PR:")); 338 339 // As well as the mailing list 340 conversations = mailmanList.conversations(Duration.ofDays(1)); 341 assertEquals(1, conversations.size()); 342 assertEquals(3, conversations.get(0).allMessages().size()); 343 for (var newMail : conversations.get(0).allMessages()) { 344 assertEquals(noreplyAddress(archive), newMail.author().address()); 345 assertEquals(listAddress, newMail.sender()); 346 } 347 } 348 } 349 350 @Test 351 void combineComments(TestInfo testInfo) throws IOException { 352 try (var credentials = new HostCredentials(testInfo); 353 var tempFolder = new TemporaryDirectory(); 354 var archiveFolder = new TemporaryDirectory(); 355 var listServer = new TestMailmanServer()) { 356 var author = credentials.getHostedRepository(); 357 var archive = credentials.getHostedRepository(); 358 var listAddress = EmailAddress.parse(listServer.createList("test")); 359 var censusBuilder = credentials.getCensusBuilder() 360 .addAuthor(author.forge().currentUser().id()); 361 var from = EmailAddress.from("test", "test@test.mail"); 362 var mlBot = new MailingListBridgeBot(from, author, archive, "master", 363 censusBuilder.build(), "master", 364 listAddress, Set.of(), Set.of(), 365 listServer.getArchive(), 366 listServer.getSMTP(), 367 archive, "webrev", Path.of("test"), 368 URIBuilder.base("http://www.test.test/").build(), 369 Set.of(), Map.of(), 370 URIBuilder.base("http://issues.test/browse/").build(), 371 Map.of(), Duration.ZERO); 372 373 // Populate the projects repository 374 var reviewFile = Path.of("reviewfile.txt"); 375 var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile); 376 var masterHash = localRepo.resolve("master").orElseThrow(); 377 localRepo.push(masterHash, author.url(), "master", true); 378 localRepo.push(masterHash, archive.url(), "webrev", true); 379 380 // Make a change with a corresponding PR 381 var editHash = CheckableRepository.appendAndCommit(localRepo); 382 localRepo.push(editHash, author.url(), "edit", true); 383 var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request"); 384 pr.setBody("This is now ready"); 385 pr.addComment("Avoid combining"); 386 387 TestBotRunner.runPeriodicItems(mlBot); 388 listServer.processIncoming(); 389 listServer.processIncoming(); 390 391 // Make several file specific comments 392 var first = pr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Review comment"); 393 pr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Another review comment"); 394 pr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Further review comment"); 395 pr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Final review comment"); 396 TestBotRunner.runPeriodicItems(mlBot); 397 listServer.processIncoming(); 398 399 // The archive should contain a combined entry 400 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 401 assertEquals(2, archiveContainsCount(archiveFolder.path(), "^On.*wrote:")); 402 403 // As well as the mailing list 404 var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO); 405 var mailmanList = mailmanServer.getList(listAddress.address()); 406 var conversations = mailmanList.conversations(Duration.ofDays(1)); 407 assertEquals(1, conversations.size()); 408 var mail = conversations.get(0).first(); 409 assertEquals("RFR: This is a pull request", mail.subject()); 410 assertEquals(3, conversations.get(0).allMessages().size()); 411 412 var commentReply = conversations.get(0).replies(mail).get(0); 413 assertEquals(2, commentReply.body().split("^On.*wrote:").length); 414 assertTrue(commentReply.body().contains("Avoid combining\n\n"), commentReply.body()); 415 416 var reviewReply = conversations.get(0).replies(mail).get(1); 417 assertEquals(2, reviewReply.body().split("^On.*wrote:").length); 418 assertEquals(2, reviewReply.body().split("> This is now ready").length, reviewReply.body()); 419 assertEquals("Re: RFR: This is a pull request", reviewReply.subject()); 420 assertTrue(reviewReply.body().contains("Review comment\n\n"), reviewReply.body()); 421 assertTrue(reviewReply.body().contains("Another review comment"), reviewReply.body()); 422 423 // Now reply to the first (collapsed) comment 424 pr.addReviewCommentReply(first, "I agree"); 425 TestBotRunner.runPeriodicItems(mlBot); 426 listServer.processIncoming(); 427 428 // The archive should contain a new entry 429 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 430 assertEquals(3, archiveContainsCount(archiveFolder.path(), "^On.*wrote:")); 431 432 // The combined review comments should only appear unquoted once 433 assertEquals(1, archiveContainsCount(archiveFolder.path(), "^Another review comment")); 434 } 435 } 436 437 @Test 438 void commentThreading(TestInfo testInfo) throws IOException { 439 try (var credentials = new HostCredentials(testInfo); 440 var tempFolder = new TemporaryDirectory(); 441 var archiveFolder = new TemporaryDirectory(); 442 var listServer = new TestMailmanServer()) { 443 var author = credentials.getHostedRepository(); 444 var reviewer = credentials.getHostedRepository(); 445 var archive = credentials.getHostedRepository(); 446 var listAddress = EmailAddress.parse(listServer.createList("test")); 447 var censusBuilder = credentials.getCensusBuilder() 448 .addReviewer(reviewer.forge().currentUser().id()) 449 .addAuthor(author.forge().currentUser().id()); 450 var from = EmailAddress.from("test", "test@test.mail"); 451 var mlBot = new MailingListBridgeBot(from, author, archive, "master", 452 censusBuilder.build(), "master", 453 listAddress, Set.of(), Set.of(), 454 listServer.getArchive(), 455 listServer.getSMTP(), 456 archive, "webrev", Path.of("test"), 457 URIBuilder.base("http://www.test.test/").build(), 458 Set.of(), Map.of(), 459 URIBuilder.base("http://issues.test/browse/").build(), 460 Map.of(), Duration.ZERO); 461 462 // Populate the projects repository 463 var reviewFile = Path.of("reviewfile.txt"); 464 var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile); 465 var masterHash = localRepo.resolve("master").orElseThrow(); 466 localRepo.push(masterHash, author.url(), "master", true); 467 localRepo.push(masterHash, archive.url(), "webrev", true); 468 469 // Make a change with a corresponding PR 470 var editHash = CheckableRepository.appendAndCommit(localRepo); 471 localRepo.push(editHash, author.url(), "edit", true); 472 var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request"); 473 pr.setBody("This is now ready"); 474 TestBotRunner.runPeriodicItems(mlBot); 475 listServer.processIncoming(); 476 477 // Make a file specific comment 478 var reviewPr = reviewer.pullRequest(pr.id()); 479 var comment1 = reviewPr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Review comment"); 480 pr.addReviewCommentReply(comment1, "I agree"); 481 reviewPr.addReviewCommentReply(comment1, "Great"); 482 TestBotRunner.runPeriodicItems(mlBot); 483 listServer.processIncoming(); 484 listServer.processIncoming(); 485 listServer.processIncoming(); 486 487 // And a second one by ourselves 488 var comment2 = pr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Another review comment"); 489 reviewPr.addReviewCommentReply(comment2, "Sounds good"); 490 pr.addReviewCommentReply(comment2, "Thanks"); 491 TestBotRunner.runPeriodicItems(mlBot); 492 listServer.processIncoming(); 493 listServer.processIncoming(); 494 listServer.processIncoming(); 495 496 // Finally some approvals and another comment 497 pr.addReview(Review.Verdict.APPROVED, "Nice"); 498 reviewPr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "The final review comment"); 499 reviewPr.addReview(Review.Verdict.APPROVED, "Looks fine"); 500 reviewPr.addReviewCommentReply(comment2, "You are welcome"); 501 TestBotRunner.runPeriodicItems(mlBot); 502 listServer.processIncoming(); 503 listServer.processIncoming(); 504 listServer.processIncoming(); 505 506 // Sanity check the archive 507 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 508 assertEquals(9, archiveContainsCount(archiveFolder.path(), "^On.*wrote:")); 509 510 // File specific comments should appear after the approval 511 var archiveText = archiveContents(archiveFolder.path()).orElseThrow(); 512 assertTrue(archiveText.indexOf("Looks fine") < archiveText.indexOf("The final review comment")); 513 514 // Check the mailing list 515 var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO); 516 var mailmanList = mailmanServer.getList(listAddress.address()); 517 var conversations = mailmanList.conversations(Duration.ofDays(1)); 518 assertEquals(1, conversations.size()); 519 var mail = conversations.get(0).first(); 520 assertEquals("RFR: This is a pull request", mail.subject()); 521 assertEquals(10, conversations.get(0).allMessages().size()); 522 523 // There should be four separate threads 524 var thread1 = conversations.get(0).replies(mail).get(0); 525 assertEquals(2, thread1.body().split("^On.*wrote:").length); 526 assertEquals(2, thread1.body().split("> This is now ready").length, thread1.body()); 527 assertEquals("Re: RFR: This is a pull request", thread1.subject()); 528 assertTrue(thread1.body().contains("Review comment\n\n"), thread1.body()); 529 assertFalse(thread1.body().contains("Another review comment"), thread1.body()); 530 var thread1reply1 = conversations.get(0).replies(thread1).get(0); 531 assertTrue(thread1reply1.body().contains("I agree")); 532 assertEquals(noreplyAddress(archive), thread1reply1.author().address()); 533 assertEquals(archive.forge().currentUser().fullName(), thread1reply1.author().fullName().orElseThrow()); 534 var thread1reply2 = conversations.get(0).replies(thread1reply1).get(0); 535 assertTrue(thread1reply2.body().contains("Great")); 536 assertEquals("integrationreviewer1@openjdk.java.net", thread1reply2.author().address()); 537 assertEquals("Generated Reviewer 1", thread1reply2.author().fullName().orElseThrow()); 538 539 var thread2 = conversations.get(0).replies(mail).get(1); 540 assertEquals(2, thread2.body().split("^On.*wrote:").length); 541 assertEquals(2, thread2.body().split("> This is now ready").length, thread2.body()); 542 assertEquals("Re: RFR: This is a pull request", thread2.subject()); 543 assertFalse(thread2.body().contains("Review comment\n\n"), thread2.body()); 544 assertTrue(thread2.body().contains("Another review comment"), thread2.body()); 545 var thread2reply1 = conversations.get(0).replies(thread2).get(0); 546 assertTrue(thread2reply1.body().contains("Sounds good")); 547 var thread2reply2 = conversations.get(0).replies(thread2reply1).get(0); 548 assertTrue(thread2reply2.body().contains("Thanks")); 549 550 var replies = conversations.get(0).replies(mail); 551 var thread3 = replies.get(2); 552 assertEquals("Re: RFR: This is a pull request", thread3.subject()); 553 var thread4 = replies.get(3); 554 assertEquals("Re: [Approved] RFR: This is a pull request", thread4.subject()); 555 assertTrue(thread4.body().contains("Looks fine")); 556 assertTrue(thread4.body().contains("The final review comment")); 557 assertTrue(thread4.body().contains("Approved by integrationreviewer1 (Reviewer)")); 558 } 559 } 560 561 @Test 562 void reviewContext(TestInfo testInfo) throws IOException { 563 try (var credentials = new HostCredentials(testInfo); 564 var tempFolder = new TemporaryDirectory(); 565 var archiveFolder = new TemporaryDirectory(); 566 var listServer = new TestMailmanServer()) { 567 var author = credentials.getHostedRepository(); 568 var archive = credentials.getHostedRepository(); 569 var listAddress = EmailAddress.parse(listServer.createList("test")); 570 var censusBuilder = credentials.getCensusBuilder() 571 .addAuthor(author.forge().currentUser().id()); 572 var from = EmailAddress.from("test", "test@test.mail"); 573 var mlBot = new MailingListBridgeBot(from, author, archive, "master", 574 censusBuilder.build(), "master", 575 listAddress, Set.of(), Set.of(), 576 listServer.getArchive(), 577 listServer.getSMTP(), 578 archive, "webrev", Path.of("test"), 579 URIBuilder.base("http://www.test.test/").build(), 580 Set.of(), Map.of(), 581 URIBuilder.base("http://issues.test/browse/").build(), 582 Map.of(), Duration.ZERO); 583 584 // Populate the projects repository 585 var reviewFile = Path.of("reviewfile.txt"); 586 var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile); 587 var masterHash = localRepo.resolve("master").orElseThrow(); 588 localRepo.push(masterHash, author.url(), "master", true); 589 localRepo.push(masterHash, archive.url(), "webrev", true); 590 591 // Make a change with a corresponding PR 592 var editHash = CheckableRepository.appendAndCommit(localRepo, "Line 1\nLine 2\nLine 3\nLine 4"); 593 localRepo.push(editHash, author.url(), "edit", true); 594 var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request"); 595 pr.setBody("This is now ready"); 596 TestBotRunner.runPeriodicItems(mlBot); 597 listServer.processIncoming(); 598 599 // Make a file specific comment 600 pr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Review comment"); 601 602 TestBotRunner.runPeriodicItems(mlBot); 603 listServer.processIncoming(); 604 605 // The archive should only contain context around line 2 606 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 607 assertTrue(archiveContains(archiveFolder.path(), "^> 2: Line 1$")); 608 assertTrue(archiveContains(archiveFolder.path(), "^> 3: Line 2$")); 609 assertFalse(archiveContains(archiveFolder.path(), "^> 4: Line 3$")); 610 } 611 } 612 613 @Test 614 void multipleReviewContexts(TestInfo testInfo) throws IOException { 615 try (var credentials = new HostCredentials(testInfo); 616 var tempFolder = new TemporaryDirectory(); 617 var archiveFolder = new TemporaryDirectory(); 618 var listServer = new TestMailmanServer()) { 619 var author = credentials.getHostedRepository(); 620 var archive = credentials.getHostedRepository(); 621 var listAddress = EmailAddress.parse(listServer.createList("test")); 622 var censusBuilder = credentials.getCensusBuilder() 623 .addAuthor(author.forge().currentUser().id()); 624 var from = EmailAddress.from("test", "test@test.mail"); 625 var mlBot = new MailingListBridgeBot(from, author, archive, "master", 626 censusBuilder.build(), "master", 627 listAddress, Set.of(), Set.of(), 628 listServer.getArchive(), 629 listServer.getSMTP(), 630 archive, "webrev", Path.of("test"), 631 URIBuilder.base("http://www.test.test/").build(), 632 Set.of(), Map.of(), 633 URIBuilder.base("http://issues.test/browse/").build(), 634 Map.of(), Duration.ZERO); 635 636 // Populate the projects repository 637 var reviewFile = Path.of("reviewfile.txt"); 638 var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile); 639 var masterHash = localRepo.resolve("master").orElseThrow(); 640 localRepo.push(masterHash, author.url(), "master", true); 641 localRepo.push(masterHash, archive.url(), "webrev", true); 642 var initialHash = CheckableRepository.appendAndCommit(localRepo, 643 "Line 0.1\nLine 0.2\nLine 0.3\nLine 0.4\n" + 644 "Line 1\nLine 2\nLine 3\nLine 4\n" + 645 "Line 5\nLine 6\nLine 7\nLine 8\n" + 646 "Line 8.1\nLine 8.2\nLine 8.3\nLine 8.4\n" + 647 "Line 9\nLine 10\nLine 11\nLine 12\n" + 648 "Line 13\nLine 14\nLine 15\nLine 16\n"); 649 localRepo.push(initialHash, author.url(), "master"); 650 651 // Make a change with a corresponding PR 652 var current = Files.readString(localRepo.root().resolve(reviewFile), StandardCharsets.UTF_8); 653 var updated = current.replaceAll("Line 2", "Line 2 edit\nLine 2.5"); 654 updated = updated.replaceAll("Line 13", "Line 12.5\nLine 13 edit"); 655 Files.writeString(localRepo.root().resolve(reviewFile), updated, StandardCharsets.UTF_8); 656 var editHash = CheckableRepository.appendAndCommit(localRepo); 657 localRepo.push(editHash, author.url(), "edit", true); 658 var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request"); 659 pr.setBody("This is now ready"); 660 TestBotRunner.runPeriodicItems(mlBot); 661 listServer.processIncoming(); 662 663 // Make file specific comments 664 pr.addReviewComment(masterHash, editHash, reviewFile.toString(), 7, "Review comment"); 665 pr.addReviewComment(masterHash, editHash, reviewFile.toString(), 24, "Another review comment"); 666 667 TestBotRunner.runPeriodicItems(mlBot); 668 listServer.processIncoming(); 669 670 // The archive should only contain context around line 2 and 20 671 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 672 assertTrue(archiveContains(archiveFolder.path(), "reviewfile.txt line 7")); 673 assertTrue(archiveContains(archiveFolder.path(), "^> 6: Line 1$")); 674 assertTrue(archiveContains(archiveFolder.path(), "^> 7: Line 2 edit$")); 675 assertFalse(archiveContains(archiveFolder.path(), "Line 3")); 676 677 assertTrue(archiveContains(archiveFolder.path(), "reviewfile.txt line 24")); 678 assertTrue(archiveContains(archiveFolder.path(), "^> 23: Line 12.5$")); 679 assertTrue(archiveContains(archiveFolder.path(), "^> 24: Line 13 edit$")); 680 assertFalse(archiveContains(archiveFolder.path(), "^Line 15")); 681 } 682 } 683 684 @Test 685 void filterComments(TestInfo testInfo) throws IOException { 686 try (var credentials = new HostCredentials(testInfo); 687 var tempFolder = new TemporaryDirectory(); 688 var archiveFolder = new TemporaryDirectory(); 689 var listServer = new TestMailmanServer()) { 690 var author = credentials.getHostedRepository(); 691 var archive = credentials.getHostedRepository(); 692 var listAddress = EmailAddress.parse(listServer.createList("test")); 693 var censusBuilder = credentials.getCensusBuilder() 694 .addAuthor(author.forge().currentUser().id()); 695 var from = EmailAddress.from("test", "test@test.mail"); 696 var mlBot = new MailingListBridgeBot(from, author, archive, "master", 697 censusBuilder.build(), "master", 698 listAddress, Set.of(), Set.of(), 699 listServer.getArchive(), listServer.getSMTP(), 700 archive, "webrev", Path.of("test"), 701 URIBuilder.base("http://www.test.test/").build(), 702 Set.of(), Map.of(), 703 URIBuilder.base("http://issues.test/browse/").build(), 704 Map.of(), Duration.ZERO); 705 706 // Populate the projects repository 707 var reviewFile = Path.of("reviewfile.txt"); 708 var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile); 709 var masterHash = localRepo.resolve("master").orElseThrow(); 710 localRepo.push(masterHash, author.url(), "master", true); 711 localRepo.push(masterHash, archive.url(), "webrev", true); 712 713 // Make a change with a corresponding PR 714 var editHash = CheckableRepository.appendAndCommit(localRepo); 715 localRepo.push(editHash, author.url(), "edit", true); 716 var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request"); 717 pr.setBody("This is now ready\n<!-- this is a comment -->\nAnd this is not\n" + 718 "<!-- Anything below this marker will be hidden -->\nStatus stuff"); 719 720 // Make a bunch of comments 721 pr.addComment("Plain comment\n<!-- this is a comment -->"); 722 pr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Review comment <!-- this is a comment -->\n"); 723 pr.addComment("/integrate stuff"); 724 TestBotRunner.runPeriodicItems(mlBot); 725 726 // Run an archive pass 727 TestBotRunner.runPeriodicItems(mlBot); 728 729 // The archive should not contain the comment 730 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 731 assertTrue(archiveContains(archiveFolder.path(), "This is now ready")); 732 assertFalse(archiveContains(archiveFolder.path(), "this is a comment")); 733 assertFalse(archiveContains(archiveFolder.path(), "Status stuff")); 734 assertTrue(archiveContains(archiveFolder.path(), "And this is not")); 735 assertFalse(archiveContains(archiveFolder.path(), "<!--")); 736 assertFalse(archiveContains(archiveFolder.path(), "-->")); 737 assertTrue(archiveContains(archiveFolder.path(), "Plain comment")); 738 assertTrue(archiveContains(archiveFolder.path(), "Review comment")); 739 assertFalse(archiveContains(archiveFolder.path(), "/integrate")); 740 } 741 } 742 743 @Test 744 void incrementalChanges(TestInfo testInfo) throws IOException { 745 try (var credentials = new HostCredentials(testInfo); 746 var tempFolder = new TemporaryDirectory(); 747 var archiveFolder = new TemporaryDirectory(); 748 var listServer = new TestMailmanServer()) { 749 var author = credentials.getHostedRepository(); 750 var archive = credentials.getHostedRepository(); 751 var commenter = credentials.getHostedRepository(); 752 var listAddress = EmailAddress.parse(listServer.createList("test")); 753 var censusBuilder = credentials.getCensusBuilder() 754 .addAuthor(author.forge().currentUser().id()); 755 var from = EmailAddress.from("test", "test@test.mail"); 756 var mlBot = new MailingListBridgeBot(from, author, archive, "master", 757 censusBuilder.build(), "master", 758 listAddress, Set.of(), Set.of(), 759 listServer.getArchive(), listServer.getSMTP(), 760 archive, "webrev", Path.of("test"), 761 URIBuilder.base("http://www.test.test/").build(), 762 Set.of(), Map.of(), 763 URIBuilder.base("http://issues.test/browse/").build(), 764 Map.of(), Duration.ZERO); 765 766 // Populate the projects repository 767 var reviewFile = Path.of("reviewfile.txt"); 768 var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile); 769 var masterHash = localRepo.resolve("master").orElseThrow(); 770 localRepo.push(masterHash, author.url(), "master", true); 771 localRepo.push(masterHash, archive.url(), "webrev", true); 772 773 // Make a change with a corresponding PR 774 var editHash = CheckableRepository.appendAndCommit(localRepo); 775 localRepo.push(editHash, author.url(), "edit", true); 776 var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request"); 777 pr.setBody("This is now ready"); 778 779 // Run an archive pass 780 TestBotRunner.runPeriodicItems(mlBot); 781 listServer.processIncoming(); 782 783 var nextHash = CheckableRepository.appendAndCommit(localRepo, "Yet one more line", "Fixing"); 784 localRepo.push(nextHash, author.url(), "edit"); 785 786 // Make sure that the push registered 787 var lastHeadHash = pr.headHash(); 788 var refreshCount = 0; 789 do { 790 pr = author.pullRequest(pr.id()); 791 if (refreshCount++ > 100) { 792 fail("The PR did not update after the new push"); 793 } 794 } while (pr.headHash().equals(lastHeadHash)); 795 796 // Run another archive pass 797 TestBotRunner.runPeriodicItems(mlBot); 798 TestBotRunner.runPeriodicItems(mlBot); 799 TestBotRunner.runPeriodicItems(mlBot); 800 listServer.processIncoming(); 801 802 // The archive should reference the updated push 803 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 804 assertTrue(archiveContains(archiveFolder.path(), "additional changes")); 805 assertTrue(archiveContains(archiveFolder.path(), "full.*/" + pr.id() + "/webrev.01")); 806 assertTrue(archiveContains(archiveFolder.path(), "inc.*/" + pr.id() + "/webrev.00-01")); 807 assertTrue(archiveContains(archiveFolder.path(), "Patch")); 808 assertTrue(archiveContains(archiveFolder.path(), "Fetch")); 809 assertTrue(archiveContains(archiveFolder.path(), "Fixing")); 810 811 // The webrev comment should be updated 812 var comments = pr.comments(); 813 var webrevComments = comments.stream() 814 .filter(comment -> comment.author().equals(author.forge().currentUser())) 815 .filter(comment -> comment.body().contains("webrev")) 816 .filter(comment -> comment.body().contains(nextHash.hex())) 817 .filter(comment -> comment.body().contains(editHash.hex())) 818 .collect(Collectors.toList()); 819 assertEquals(1, webrevComments.size()); 820 821 // Check that sender address is set properly 822 var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO); 823 var mailmanList = mailmanServer.getList(listAddress.address()); 824 var conversations = mailmanList.conversations(Duration.ofDays(1)); 825 assertEquals(1, conversations.size()); 826 for (var newMail : conversations.get(0).allMessages()) { 827 assertEquals(noreplyAddress(archive), newMail.author().address()); 828 assertEquals(listAddress, newMail.sender()); 829 } 830 831 // Add a comment 832 var commenterPr = commenter.pullRequest(pr.id()); 833 commenterPr.addReviewComment(masterHash, nextHash, reviewFile.toString(), 2, "Review comment"); 834 TestBotRunner.runPeriodicItems(mlBot); 835 listServer.processIncoming(); 836 837 // Ensure that additional updates are only reported once 838 for (int i = 0; i < 3; ++i) { 839 var anotherHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "Fixing"); 840 localRepo.push(anotherHash, author.url(), "edit"); 841 842 // Make sure that the push registered 843 lastHeadHash = pr.headHash(); 844 refreshCount = 0; 845 do { 846 pr = author.pullRequest(pr.id()); 847 if (refreshCount++ > 100) { 848 fail("The PR did not update after the new push"); 849 } 850 } while (pr.headHash().equals(lastHeadHash)); 851 852 TestBotRunner.runPeriodicItems(mlBot); 853 TestBotRunner.runPeriodicItems(mlBot); 854 listServer.processIncoming(); 855 } 856 var updatedConversations = mailmanList.conversations(Duration.ofDays(1)); 857 assertEquals(1, updatedConversations.size()); 858 var conversation = updatedConversations.get(0); 859 assertEquals(6, conversation.allMessages().size()); 860 assertEquals("Re: [Rev 01] RFR: This is a pull request", conversation.allMessages().get(1).subject()); 861 assertEquals("Re: [Rev 01] RFR: This is a pull request", conversation.allMessages().get(2).subject(), conversation.allMessages().get(2).toString()); 862 assertEquals("Re: [Rev 04] RFR: This is a pull request", conversation.allMessages().get(5).subject()); 863 } 864 } 865 866 @Test 867 void rebased(TestInfo testInfo) throws IOException { 868 try (var credentials = new HostCredentials(testInfo); 869 var tempFolder = new TemporaryDirectory(); 870 var archiveFolder = new TemporaryDirectory(); 871 var listServer = new TestMailmanServer()) { 872 var author = credentials.getHostedRepository(); 873 var archive = credentials.getHostedRepository(); 874 var listAddress = EmailAddress.parse(listServer.createList("test")); 875 var censusBuilder = credentials.getCensusBuilder() 876 .addAuthor(author.forge().currentUser().id()); 877 var sender = EmailAddress.from("test", "test@test.mail"); 878 var mlBot = new MailingListBridgeBot(sender, author, archive, "master", 879 censusBuilder.build(), "master", 880 listAddress, Set.of(), Set.of(), 881 listServer.getArchive(), listServer.getSMTP(), 882 archive, "webrev", Path.of("test"), 883 URIBuilder.base("http://www.test.test/").build(), 884 Set.of(), Map.of(), 885 URIBuilder.base("http://issues.test/browse/").build(), 886 Map.of(), Duration.ZERO); 887 888 // Populate the projects repository 889 var reviewFile = Path.of("reviewfile.txt"); 890 var localRepo = CheckableRepository.init(tempFolder.path().resolve("first"), author.repositoryType(), reviewFile); 891 var masterHash = localRepo.resolve("master").orElseThrow(); 892 localRepo.push(masterHash, author.url(), "master", true); 893 localRepo.push(masterHash, archive.url(), "webrev", true); 894 895 // Make a change with a corresponding PR 896 var editHash = CheckableRepository.appendAndCommit(localRepo, "A line", "Original msg"); 897 localRepo.push(editHash, author.url(), "edit", true); 898 var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request"); 899 pr.setBody("This is now ready"); 900 901 // Run an archive pass 902 TestBotRunner.runPeriodicItems(mlBot); 903 listServer.processIncoming(); 904 905 var newLocalRepo = Repository.materialize(tempFolder.path().resolve("second"), author.url(), "master"); 906 var newEditHash = CheckableRepository.appendAndCommit(newLocalRepo, "Another line", "Replaced msg"); 907 newLocalRepo.push(newEditHash, author.url(), "edit", true); 908 909 // Make sure that the push registered 910 var lastHeadHash = pr.headHash(); 911 var refreshCount = 0; 912 do { 913 pr = author.pullRequest(pr.id()); 914 if (refreshCount++ > 100) { 915 fail("The PR did not update after the new push"); 916 } 917 } while (pr.headHash().equals(lastHeadHash)); 918 919 // Run another archive pass 920 TestBotRunner.runPeriodicItems(mlBot); 921 listServer.processIncoming(); 922 923 // The archive should reference the rebased push 924 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 925 assertTrue(archiveContains(archiveFolder.path(), "complete new set of changes")); 926 assertTrue(archiveContains(archiveFolder.path(), pr.id() + "/webrev.01")); 927 assertFalse(archiveContains(archiveFolder.path(), "Incremental")); 928 assertTrue(archiveContains(archiveFolder.path(), "Patch")); 929 assertTrue(archiveContains(archiveFolder.path(), "Fetch")); 930 assertTrue(archiveContains(archiveFolder.path(), "Original msg")); 931 assertTrue(archiveContains(archiveFolder.path(), "Replaced msg")); 932 933 // The webrev comment should be updated 934 var comments = pr.comments(); 935 var webrevComments = comments.stream() 936 .filter(comment -> comment.author().equals(author.forge().currentUser())) 937 .filter(comment -> comment.body().contains("webrev")) 938 .filter(comment -> comment.body().contains(newEditHash.hex())) 939 .collect(Collectors.toList()); 940 assertEquals(1, webrevComments.size()); 941 942 // Check that sender address is set properly 943 var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO); 944 var mailmanList = mailmanServer.getList(listAddress.address()); 945 var conversations = mailmanList.conversations(Duration.ofDays(1)); 946 assertEquals(1, conversations.size()); 947 for (var newMail : conversations.get(0).allMessages()) { 948 assertEquals(noreplyAddress(archive), newMail.author().address()); 949 assertEquals(listAddress, newMail.sender()); 950 assertFalse(newMail.hasHeader("PR-Head-Hash")); 951 } 952 assertEquals("Re: [Rev 01] RFR: This is a pull request", conversations.get(0).allMessages().get(1).subject()); 953 } 954 } 955 956 @Test 957 void skipAddingExistingWebrev(TestInfo testInfo) throws IOException { 958 try (var credentials = new HostCredentials(testInfo); 959 var tempFolder = new TemporaryDirectory(); 960 var archiveFolder = new TemporaryDirectory(); 961 var webrevFolder = new TemporaryDirectory(); 962 var listServer = new TestMailmanServer()) { 963 var author = credentials.getHostedRepository(); 964 var archive = credentials.getHostedRepository(); 965 var ignored = credentials.getHostedRepository(); 966 var listAddress = EmailAddress.parse(listServer.createList("test")); 967 var censusBuilder = credentials.getCensusBuilder() 968 .addAuthor(author.forge().currentUser().id()); 969 var from = EmailAddress.from("test", "test@test.mail"); 970 var mlBot = new MailingListBridgeBot(from, author, archive, "master", 971 censusBuilder.build(), "master", 972 listAddress, 973 Set.of(ignored.forge().currentUser().userName()), 974 Set.of(), 975 listServer.getArchive(), listServer.getSMTP(), 976 archive, "webrev", Path.of("test"), 977 URIBuilder.base("http://www.test.test/").build(), 978 Set.of(), Map.of(), 979 URIBuilder.base("http://issues.test/browse/").build(), 980 Map.of(), Duration.ZERO); 981 982 // Populate the projects repository 983 var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType()); 984 var masterHash = localRepo.resolve("master").orElseThrow(); 985 localRepo.push(masterHash, author.url(), "master", true); 986 localRepo.push(masterHash, archive.url(), "webrev", true); 987 988 // Make a change with a corresponding PR 989 var editHash = CheckableRepository.appendAndCommit(localRepo, "A simple change", 990 "Change msg\n\nWith several lines"); 991 localRepo.push(editHash, author.url(), "edit", true); 992 var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request"); 993 994 // Flag it as ready for review 995 pr.setBody("This should now be ready"); 996 997 // Run an archive pass 998 TestBotRunner.runPeriodicItems(mlBot); 999 1000 // The archive should now contain an entry 1001 var archiveRepo = Repository.materialize(archiveFolder.path(), archive.url(), "master"); 1002 assertTrue(archiveContains(archiveFolder.path(), editHash.abbreviate())); 1003 1004 // And there should be a webrev comment 1005 var comments = pr.comments(); 1006 var webrevComments = comments.stream() 1007 .filter(comment -> comment.author().equals(author.forge().currentUser())) 1008 .filter(comment -> comment.body().contains("webrev")) 1009 .filter(comment -> comment.body().contains(editHash.hex())) 1010 .collect(Collectors.toList()); 1011 assertEquals(1, webrevComments.size()); 1012 assertEquals(1, countSubstrings(webrevComments.get(0).body(), "webrev.00")); 1013 1014 // Pretend the archive didn't work out 1015 archiveRepo.push(masterHash, archive.url(), "master", true); 1016 1017 // Run another archive pass 1018 TestBotRunner.runPeriodicItems(mlBot); 1019 1020 // The webrev comment should not contain duplicate entries 1021 comments = pr.comments(); 1022 webrevComments = comments.stream() 1023 .filter(comment -> comment.author().equals(author.forge().currentUser())) 1024 .filter(comment -> comment.body().contains("webrev")) 1025 .filter(comment -> comment.body().contains(editHash.hex())) 1026 .collect(Collectors.toList()); 1027 assertEquals(1, webrevComments.size()); 1028 assertEquals(1, countSubstrings(webrevComments.get(0).body(), "webrev.00")); 1029 } 1030 } 1031 1032 @Test 1033 void notifyReviewVerdicts(TestInfo testInfo) throws IOException { 1034 try (var credentials = new HostCredentials(testInfo); 1035 var tempFolder = new TemporaryDirectory(); 1036 var archiveFolder = new TemporaryDirectory(); 1037 var listServer = new TestMailmanServer()) { 1038 var author = credentials.getHostedRepository(); 1039 var archive = credentials.getHostedRepository(); 1040 var reviewer = credentials.getHostedRepository(); 1041 var listAddress = EmailAddress.parse(listServer.createList("test")); 1042 var from = EmailAddress.from("test", "test@test.mail"); 1043 var censusBuilder = credentials.getCensusBuilder() 1044 .addReviewer(reviewer.forge().currentUser().id()) 1045 .addAuthor(author.forge().currentUser().id()); 1046 var mlBot = new MailingListBridgeBot(from, author, archive, "master", 1047 censusBuilder.build(), "master", 1048 listAddress, Set.of(), Set.of(), 1049 listServer.getArchive(), listServer.getSMTP(), 1050 archive, "webrev", Path.of("test"), 1051 URIBuilder.base("http://www.test.test/").build(), 1052 Set.of(), Map.of(), 1053 URIBuilder.base("http://issues.test/browse/").build(), 1054 Map.of(), Duration.ZERO); 1055 1056 // Populate the projects repository 1057 var reviewFile = Path.of("reviewfile.txt"); 1058 var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile); 1059 var masterHash = localRepo.resolve("master").orElseThrow(); 1060 localRepo.push(masterHash, author.url(), "master", true); 1061 localRepo.push(masterHash, archive.url(), "webrev", true); 1062 1063 // Make a change with a corresponding PR 1064 var editHash = CheckableRepository.appendAndCommit(localRepo); 1065 localRepo.push(editHash, author.url(), "edit", true); 1066 var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request"); 1067 pr.setBody("This is now ready"); 1068 1069 // Run an archive pass 1070 TestBotRunner.runPeriodicItems(mlBot); 1071 1072 // First unapprove it 1073 var reviewedPr = reviewer.pullRequest(pr.id()); 1074 reviewedPr.addReview(Review.Verdict.DISAPPROVED, "Reason 1"); 1075 TestBotRunner.runPeriodicItems(mlBot); 1076 TestBotRunner.runPeriodicItems(mlBot); 1077 TestBotRunner.runPeriodicItems(mlBot); 1078 1079 // The archive should contain a note 1080 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 1081 assertEquals(1, archiveContainsCount(archiveFolder.path(), "Changes requested by ")); 1082 assertEquals(1, archiveContainsCount(archiveFolder.path(), " by integrationreviewer1")); 1083 if (author.forge().supportsReviewBody()) { 1084 assertEquals(1, archiveContainsCount(archiveFolder.path(), "Reason 1")); 1085 } 1086 1087 // Then approve it 1088 reviewedPr.addReview(Review.Verdict.APPROVED, "Reason 2"); 1089 TestBotRunner.runPeriodicItems(mlBot); 1090 TestBotRunner.runPeriodicItems(mlBot); 1091 TestBotRunner.runPeriodicItems(mlBot); 1092 1093 // The archive should contain another note 1094 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 1095 assertEquals(1, archiveContainsCount(archiveFolder.path(), "Approved by ")); 1096 if (author.forge().supportsReviewBody()) { 1097 assertEquals(1, archiveContainsCount(archiveFolder.path(), "Reason 2")); 1098 } 1099 assertEquals(1, archiveContainsCount(archiveFolder.path(), "Re: \\[Approved\\] RFR:")); 1100 1101 // Yet another change 1102 reviewedPr.addReview(Review.Verdict.DISAPPROVED, "Reason 3"); 1103 TestBotRunner.runPeriodicItems(mlBot); 1104 TestBotRunner.runPeriodicItems(mlBot); 1105 TestBotRunner.runPeriodicItems(mlBot); 1106 1107 // The archive should contain another note 1108 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 1109 assertEquals(2, archiveContainsCount(archiveFolder.path(), "Changes requested by ")); 1110 if (author.forge().supportsReviewBody()) { 1111 assertEquals(1, archiveContainsCount(archiveFolder.path(), "Reason 3")); 1112 } 1113 } 1114 } 1115 1116 @Test 1117 void ignoreComments(TestInfo testInfo) throws IOException { 1118 try (var credentials = new HostCredentials(testInfo); 1119 var tempFolder = new TemporaryDirectory(); 1120 var archiveFolder = new TemporaryDirectory(); 1121 var listServer = new TestMailmanServer()) { 1122 var author = credentials.getHostedRepository(); 1123 var ignored = credentials.getHostedRepository(); 1124 var archive = credentials.getHostedRepository(); 1125 var listAddress = EmailAddress.parse(listServer.createList("test")); 1126 var censusBuilder = credentials.getCensusBuilder() 1127 .addAuthor(author.forge().currentUser().id()); 1128 var from = EmailAddress.from("test", "test@test.mail"); 1129 var mlBot = new MailingListBridgeBot(from, author, archive, "master", 1130 censusBuilder.build(), "master", 1131 listAddress, 1132 Set.of(ignored.forge().currentUser().userName()), 1133 Set.of(Pattern.compile("ignore this comment", Pattern.MULTILINE | Pattern.DOTALL)), 1134 listServer.getArchive(), listServer.getSMTP(), 1135 archive, "webrev", Path.of("test"), 1136 URIBuilder.base("http://www.test.test/").build(), 1137 Set.of(), Map.of(), 1138 URIBuilder.base("http://issues.test/browse/").build(), 1139 Map.of(), Duration.ZERO); 1140 1141 // Populate the projects repository 1142 var reviewFile = Path.of("reviewfile.txt"); 1143 var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile); 1144 var masterHash = localRepo.resolve("master").orElseThrow(); 1145 localRepo.push(masterHash, author.url(), "master", true); 1146 localRepo.push(masterHash, archive.url(), "webrev", true); 1147 1148 // Make a change with a corresponding PR 1149 var editHash = CheckableRepository.appendAndCommit(localRepo); 1150 localRepo.push(editHash, author.url(), "edit", true); 1151 var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request"); 1152 pr.setBody("This is now ready"); 1153 1154 // Make a bunch of comments 1155 pr.addComment("Plain comment"); 1156 pr.addComment("ignore this comment"); 1157 pr.addComment("I think it is time to\nignore this comment!"); 1158 pr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Review ignore this comment"); 1159 1160 var ignoredPR = ignored.pullRequest(pr.id()); 1161 ignoredPR.addComment("Don't mind me"); 1162 1163 TestBotRunner.runPeriodicItems(mlBot); 1164 TestBotRunner.runPeriodicItems(mlBot); 1165 1166 // The archive should not contain the ignored comments 1167 Repository.materialize(archiveFolder.path(), archive.url(), "master"); 1168 assertTrue(archiveContains(archiveFolder.path(), "This is now ready")); 1169 assertFalse(archiveContains(archiveFolder.path(), "ignore this comment")); 1170 assertFalse(archiveContains(archiveFolder.path(), "it is time to")); 1171 assertFalse(archiveContains(archiveFolder.path(), "Don't mind me")); 1172 assertFalse(archiveContains(archiveFolder.path(), "Review ignore")); 1173 } 1174 } 1175 }