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