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 }