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