blob: 294a9a81dbd8cf140b8167f1caa750e346630e42 [file] [log] [blame]
package com.googlesource.gerrit.plugins.chatgpt;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import com.github.tomakehurst.wiremock.matching.RequestPatternBuilder;
import com.github.tomakehurst.wiremock.verification.LoggedRequest;
import com.google.common.net.HttpHeaders;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.config.PluginConfig;
import com.google.gerrit.server.data.AccountAttribute;
import com.google.gerrit.server.data.PatchSetAttribute;
import com.google.gerrit.server.events.CommentAddedEvent;
import com.google.gerrit.server.events.PatchSetCreatedEvent;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gson.JsonArray;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.googlesource.gerrit.plugins.chatgpt.client.GerritClient;
import com.googlesource.gerrit.plugins.chatgpt.client.OpenAiClient;
import com.googlesource.gerrit.plugins.chatgpt.client.UriResourceLocator;
import com.googlesource.gerrit.plugins.chatgpt.config.ConfigCreator;
import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
import com.googlesource.gerrit.plugins.chatgpt.listener.EventListenerHandler;
import com.googlesource.gerrit.plugins.chatgpt.listener.GerritListener;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.entity.ContentType;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.IOException;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import static com.google.gerrit.extensions.client.ChangeKind.REWORK;
import static com.googlesource.gerrit.plugins.chatgpt.client.UriResourceLocator.*;
import static com.googlesource.gerrit.plugins.chatgpt.listener.EventListenerHandler.buildFullChangeId;
import static java.net.HttpURLConnection.HTTP_OK;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@Slf4j
@RunWith(MockitoJUnitRunner.class)
public class ChatGptReviewTest {
private static final Path basePath = Paths.get("src/test/resources");
private static final String GERRIT_AUTH_BASE_URL = "http://localhost:9527";
private static final int GERRIT_ACCOUNT_ID = 1000000;
private static final String GERRIT_ACCOUNT_NAME = "Test";
private static final String GERRIT_ACCOUNT_EMAIL = "test@example.com";
private static final String GERRIT_USER_NAME = "test";
private static final String GERRIT_PASSWORD = "test";
private static final String GERRIT_USER_GROUP = "Test";
private static final String GPT_TOKEN = "tk-test";
private static final String GPT_DOMAIN = "http://localhost:9527";
private static final Project.NameKey PROJECT_NAME = Project.NameKey.parse("myProject");
private static final Change.Key CHANGE_ID = Change.Key.parse("myChangeId");
private static final BranchNameKey BRANCH_NAME = BranchNameKey.create(PROJECT_NAME, "myBranchName");
private static final boolean GPT_STREAM_OUTPUT = true;
private static final long TEST_TIMESTAMP = 1699270812;
private static final String REVIEW_TAG_COMMENTS = "[ID:0] comment 2\n" +
"[ID:1] In reference to the code `TypeClassOrPath` (from line 5 of file \"test_file.py\"), message\n";
private final Gson gson = new Gson();
private String reviewUserPrompt;
private String reviewUserPromptByPoints;
private String commentUserPrompt;
private String gerritPatchSetByPoints;
@Rule
public WireMockRule wireMockRule = new WireMockRule(9527);
private PluginConfig globalConfig;
private PluginConfig projectConfig;
@Before
public void before() throws IOException {
initConfig();
setupMockRequests();
initComparisonContent();
}
private void initConfig() {
globalConfig = mock(PluginConfig.class);
Answer<Object> returnDefaultArgument = invocation -> {
// Return the second argument (i.e., the Default value) passed to the method
return invocation.getArgument(1);
};
// Mock the Global Config values not provided by Default
when(globalConfig.getString("gerritAuthBaseUrl")).thenReturn(GERRIT_AUTH_BASE_URL);
when(globalConfig.getString("gerritUserName")).thenReturn(GERRIT_USER_NAME);
when(globalConfig.getString("gerritPassword")).thenReturn(GERRIT_PASSWORD);
when(globalConfig.getString("gptToken")).thenReturn(GPT_TOKEN);
// Mock the Global Config values to the Defaults passed as second arguments of the `get*` methods.
when(globalConfig.getString(Mockito.anyString(), Mockito.anyString())).thenAnswer(returnDefaultArgument);
when(globalConfig.getInt(Mockito.anyString(), Mockito.anyInt())).thenAnswer(returnDefaultArgument);
when(globalConfig.getBoolean(Mockito.anyString(), Mockito.anyBoolean())).thenAnswer(returnDefaultArgument);
// Mock the Global Config values that differ from the ones provided by Default
when(globalConfig.getString(Mockito.eq("gptDomain"), Mockito.anyString()))
.thenReturn(GPT_DOMAIN);
when(globalConfig.getBoolean(Mockito.eq("gptStreamOutput"), Mockito.anyBoolean()))
.thenReturn(GPT_STREAM_OUTPUT);
when(globalConfig.getBoolean(Mockito.eq("gptReviewCommitMessages"), Mockito.anyBoolean()))
.thenReturn(true);
projectConfig = mock(PluginConfig.class);
// Mock the Project Config values
when(projectConfig.getBoolean(Mockito.eq("isEnabled"), Mockito.anyBoolean())).thenReturn(true);
}
private void setupMockRequests() {
Configuration config = new Configuration(globalConfig, projectConfig);
String fullChangeId = buildFullChangeId(PROJECT_NAME, BRANCH_NAME, CHANGE_ID);
// Mock the behavior of the gerritAccountIdUri request
WireMock.stubFor(WireMock.get(gerritAccountIdUri(GERRIT_USER_NAME))
.willReturn(WireMock.aResponse()
.withStatus(HTTP_OK)
.withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
.withBody("[{\"_account_id\": " + GERRIT_ACCOUNT_ID + "}]")));
// Mock the behavior of the gerritAccountGroups request
WireMock.stubFor(WireMock.get(UriResourceLocator.gerritAccountsUri() +
gerritGroupPostfixUri(GERRIT_ACCOUNT_ID))
.willReturn(WireMock.aResponse()
.withStatus(HTTP_OK)
.withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
.withBodyFile("gerritAccountGroups.json")));
// Mock the behavior of the gerritPatchSetFiles request
WireMock.stubFor(WireMock.get(gerritPatchSetFilesUri(fullChangeId))
.willReturn(WireMock.aResponse()
.withStatus(HTTP_OK)
.withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
.withBodyFile("gerritPatchSetFiles.json")));
// Mock the behavior of the gerritPatchSet diff requests
WireMock.stubFor(WireMock.get(gerritPatchSetFilesUri(fullChangeId) +
gerritDiffPostfixUri("/COMMIT_MSG"))
.willReturn(WireMock.aResponse()
.withStatus(HTTP_OK)
.withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
.withBodyFile("gerritPatchSetDiffCommitMsg.json")));
WireMock.stubFor(WireMock.get(gerritPatchSetFilesUri(fullChangeId) +
gerritDiffPostfixUri("test_file.py"))
.willReturn(WireMock.aResponse()
.withStatus(HTTP_OK)
.withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
.withBodyFile("gerritPatchSetDiffTestFile.json")));
// Mock the behavior of the gerritPatchSet comments request
WireMock.stubFor(WireMock.get(gerritGetAllPatchSetCommentsUri(fullChangeId))
.willReturn(WireMock.aResponse()
.withStatus(HTTP_OK)
.withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
.withBodyFile("gerritPatchSetComments.json")));
// Mock the behavior of the askGpt request
byte[] gptAnswer = Base64.getDecoder().decode("ZGF0YTogeyJpZCI6ImNoYXRjbXBsLTdSZDVOYVpEOGJNVTRkdnBVV2" +
"9hM3Q2RG83RkkzIiwib2JqZWN0IjoiY2hhdC5jb21wbGV0aW9uLmNodW5rIiwiY3JlYXRlZCI6MTY4NjgxOTQ1NywibW9kZWw" +
"iOiJncHQtMy41LXR1cmJvLTAzMDEiLCJjaG9pY2VzIjpbeyJkZWx0YSI6eyJyb2xlIjoiYXNzaXN0YW50In0sImluZGV4Ijow" +
"LCJmaW5pc2hfcmVhc29uIjpudWxsfV19CgpkYXRhOiB7ImlkIjoiY2hhdGNtcGwtN1JkNU5hWkQ4Yk1VNGR2cFVXb2EzdDZEb" +
"zdGSTMiLCJvYmplY3QiOiJjaGF0LmNvbXBsZXRpb24uY2h1bmsiLCJjcmVhdGVkIjoxNjg2ODE5NDU3LCJtb2RlbCI6ImdwdC0" +
"zLjUtdHVyYm8tMDMwMSIsImNob2ljZXMiOlt7ImRlbHRhIjp7ImNvbnRlbnQiOiJIZWxsbyJ9LCJpbmRleCI6MCwiZmluaXNo" +
"X3JlYXNvbiI6bnVsbH1dfQoKZGF0YTogeyJpZCI6ImNoYXRjbXBsLTdSZDVOYVpEOGJNVTRkdnBVV29hM3Q2RG83RkkzIiwib" +
"2JqZWN0IjoiY2hhdC5jb21wbGV0aW9uLmNodW5rIiwiY3JlYXRlZCI6MTY4NjgxOTQ1NywibW9kZWwiOiJncHQtMy41LXR1cm" +
"JvLTAzMDEiLCJjaG9pY2VzIjpbeyJkZWx0YSI6eyJjb250ZW50IjoiISJ9LCJpbmRleCI6MCwiZmluaXNoX3JlYXNvbiI" +
"6bnVsbH1dfQ==");
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo(URI.create(config.getGptDomain()
+ UriResourceLocator.chatCompletionsUri()).getPath()))
.willReturn(WireMock.aResponse()
.withStatus(HTTP_OK)
.withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
.withBody(new String(gptAnswer))));
// Mock the behavior of the postReview request
WireMock.stubFor(WireMock.post(gerritCommentUri(fullChangeId))
.willReturn(WireMock.aResponse()
.withStatus(HTTP_OK)));
}
private void initComparisonContent() throws IOException {
String diffContent = new String(Files.readAllBytes(basePath.resolve("reducePatchSet/patchSetDiffOutput.json")));
gerritPatchSetByPoints = new String(Files.readAllBytes(basePath.resolve(
"__files/gerritPatchSetByPoints.json")));
reviewUserPrompt = String.join("\n", Arrays.asList(
Configuration.DEFAULT_GPT_USER_PROMPT,
Configuration.DEFAULT_GPT_COMMIT_MESSAGES_REVIEW_USER_PROMPT,
diffContent
));
reviewUserPromptByPoints = String.join("\n", Arrays.asList(
Configuration.DEFAULT_GPT_USER_PROMPT,
Configuration.DEFAULT_GPT_USER_PROMPT_JSON,
Configuration.DEFAULT_GPT_COMMIT_MESSAGES_REVIEW_USER_PROMPT,
diffContent
));
commentUserPrompt = String.join("\n", Arrays.asList(
Configuration.DEFAULT_GPT_CUSTOM_USER_PROMPT_1,
diffContent,
Configuration.DEFAULT_GPT_CUSTOM_USER_PROMPT_2,
REVIEW_TAG_COMMENTS
));
}
private AccountAttribute createTestAccountAttribute() {
AccountAttribute accountAttribute = new AccountAttribute();
accountAttribute.name = GERRIT_ACCOUNT_NAME;
accountAttribute.username = GERRIT_USER_NAME;
accountAttribute.email = GERRIT_ACCOUNT_EMAIL;
return accountAttribute;
}
private PatchSetAttribute createPatchSetAttribute() {
PatchSetAttribute patchSetAttribute = new PatchSetAttribute();
patchSetAttribute.kind = REWORK;
patchSetAttribute.author = createTestAccountAttribute();
return patchSetAttribute;
}
@Test
public void patchSetCreatedOrUpdated() throws InterruptedException, NoSuchProjectException, ExecutionException {
when(globalConfig.getBoolean(Mockito.eq("gptReviewByPoints"), Mockito.anyBoolean()))
.thenReturn(false);
Configuration config = new Configuration(globalConfig, projectConfig);
GerritClient gerritClient = new GerritClient();
OpenAiClient openAiClient = new OpenAiClient();
PatchSetReviewer patchSetReviewer = new PatchSetReviewer(gerritClient, openAiClient);
ConfigCreator mockConfigCreator = mock(ConfigCreator.class);
when(mockConfigCreator.createConfig(ArgumentMatchers.any())).thenReturn(config);
PatchSetCreatedEvent event = mock(PatchSetCreatedEvent.class);
when(event.getProjectNameKey()).thenReturn(PROJECT_NAME);
when(event.getBranchNameKey()).thenReturn(BRANCH_NAME);
when(event.getChangeKey()).thenReturn(CHANGE_ID);
when(event.getType()).thenReturn("patchset-created");
event.patchSet = this::createPatchSetAttribute;
EventListenerHandler eventListenerHandler = new EventListenerHandler(patchSetReviewer, gerritClient);
GerritListener gerritListener = new GerritListener(mockConfigCreator, eventListenerHandler);
gerritListener.onEvent(event);
CompletableFuture<Void> future = eventListenerHandler.getLatestFuture();
future.get();
RequestPatternBuilder requestPatternBuilder = WireMock.postRequestedFor(
WireMock.urlEqualTo(gerritCommentUri(buildFullChangeId(PROJECT_NAME, BRANCH_NAME, CHANGE_ID))));
List<LoggedRequest> loggedRequests = WireMock.findAll(requestPatternBuilder);
Assert.assertEquals(1, loggedRequests.size());
JsonObject gptRequestBody = gson.fromJson(openAiClient.getRequestBody(), JsonObject.class);
JsonArray prompts = gptRequestBody.get("messages").getAsJsonArray();
String systemPrompt = prompts.get(0).getAsJsonObject().get("content").getAsString();
Assert.assertEquals(Configuration.DEFAULT_GPT_SYSTEM_PROMPT, systemPrompt);
String userPrompt = prompts.get(1).getAsJsonObject().get("content").getAsString();
Assert.assertEquals(reviewUserPrompt, userPrompt);
String requestBody = loggedRequests.get(0).getBodyAsString();
Assert.assertEquals("{\"message\":\"Hello!\\n\"}", requestBody);
}
@Test
public void patchSetCreatedOrUpdatedByPoints() throws InterruptedException, NoSuchProjectException, ExecutionException {
when(globalConfig.getBoolean(Mockito.eq("gptStreamOutput"), Mockito.anyBoolean()))
.thenReturn(false);
Configuration config = new Configuration(globalConfig, projectConfig);
GerritClient gerritClient = new GerritClient();
OpenAiClient openAiClient = new OpenAiClient();
PatchSetReviewer patchSetReviewer = new PatchSetReviewer(gerritClient, openAiClient);
ConfigCreator mockConfigCreator = mock(ConfigCreator.class);
when(mockConfigCreator.createConfig(ArgumentMatchers.any())).thenReturn(config);
PatchSetCreatedEvent event = mock(PatchSetCreatedEvent.class);
when(event.getProjectNameKey()).thenReturn(PROJECT_NAME);
when(event.getBranchNameKey()).thenReturn(BRANCH_NAME);
when(event.getChangeKey()).thenReturn(CHANGE_ID);
when(event.getType()).thenReturn("patchset-created");
event.patchSet = this::createPatchSetAttribute;
EventListenerHandler eventListenerHandler = new EventListenerHandler(patchSetReviewer, gerritClient);
WireMock.stubFor(WireMock.post(WireMock.urlEqualTo(URI.create(config.getGptDomain()
+ UriResourceLocator.chatCompletionsUri()).getPath()))
.willReturn(WireMock.aResponse()
.withStatus(HTTP_OK)
.withHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString())
.withBodyFile("chatGptResponseByPoints.json")));
GerritListener gerritListener = new GerritListener(mockConfigCreator, eventListenerHandler);
gerritListener.onEvent(event);
CompletableFuture<Void> future = eventListenerHandler.getLatestFuture();
future.get();
RequestPatternBuilder requestPatternBuilder = WireMock.postRequestedFor(
WireMock.urlEqualTo(gerritCommentUri(buildFullChangeId(PROJECT_NAME, BRANCH_NAME, CHANGE_ID))));
List<LoggedRequest> loggedRequests = WireMock.findAll(requestPatternBuilder);
Assert.assertEquals(1, loggedRequests.size());
JsonObject gptRequestBody = gson.fromJson(openAiClient.getRequestBody(), JsonObject.class);
JsonArray prompts = gptRequestBody.get("messages").getAsJsonArray();
String userPrompt = prompts.get(1).getAsJsonObject().get("content").getAsString();
Assert.assertEquals(reviewUserPromptByPoints, userPrompt);
String requestBody = loggedRequests.get(0).getBodyAsString();
Assert.assertEquals(gerritPatchSetByPoints, requestBody);
}
@Test
public void patchSetDisableUserGroup() throws NoSuchProjectException {
when(globalConfig.getString(Mockito.eq("disabledGroups"), Mockito.anyString()))
.thenReturn(GERRIT_USER_GROUP);
Configuration config = new Configuration(globalConfig, projectConfig);
GerritClient gerritClient = new GerritClient();
OpenAiClient openAiClient = new OpenAiClient();
PatchSetReviewer patchSetReviewer = new PatchSetReviewer(gerritClient, openAiClient);
ConfigCreator mockConfigCreator = mock(ConfigCreator.class);
when(mockConfigCreator.createConfig(ArgumentMatchers.any())).thenReturn(config);
PatchSetCreatedEvent event = mock(PatchSetCreatedEvent.class);
when(event.getProjectNameKey()).thenReturn(PROJECT_NAME);
when(event.getBranchNameKey()).thenReturn(BRANCH_NAME);
when(event.getChangeKey()).thenReturn(CHANGE_ID);
when(event.getType()).thenReturn("patchset-created");
event.patchSet = this::createPatchSetAttribute;
EventListenerHandler eventListenerHandler = new EventListenerHandler(patchSetReviewer, gerritClient);
GerritListener gerritListener = new GerritListener(mockConfigCreator, eventListenerHandler);
gerritListener.onEvent(event);
CompletableFuture<Void> future = eventListenerHandler.getLatestFuture();
Assert.assertThrows(NullPointerException.class, () -> future.get());
}
@Test
public void gptMentionedInComment() throws InterruptedException, NoSuchProjectException, ExecutionException {
Configuration config = new Configuration(globalConfig, projectConfig);
GerritClient gerritClient = new GerritClient();
OpenAiClient openAiClient = new OpenAiClient();
PatchSetReviewer patchSetReviewer = new PatchSetReviewer(gerritClient, openAiClient);
ConfigCreator mockConfigCreator = mock(ConfigCreator.class);
when(config.getGerritUserName()).thenReturn("gpt");
when(mockConfigCreator.createConfig(ArgumentMatchers.any())).thenReturn(config);
CommentAddedEvent event = mock(CommentAddedEvent.class);
when(event.getProjectNameKey()).thenReturn(PROJECT_NAME);
when(event.getBranchNameKey()).thenReturn(BRANCH_NAME);
when(event.getChangeKey()).thenReturn(CHANGE_ID);
when(event.getType()).thenReturn("comment-added");
event.author = this::createTestAccountAttribute;
event.patchSet = this::createPatchSetAttribute;
event.eventCreatedOn = TEST_TIMESTAMP;
EventListenerHandler eventListenerHandler = new EventListenerHandler(patchSetReviewer, gerritClient);
GerritListener gerritListener = new GerritListener(mockConfigCreator, eventListenerHandler);
gerritListener.onEvent(event);
CompletableFuture<Void> future = eventListenerHandler.getLatestFuture();
future.get();
RequestPatternBuilder requestPatternBuilder = WireMock.postRequestedFor(
WireMock.urlEqualTo(gerritCommentUri(buildFullChangeId(PROJECT_NAME, BRANCH_NAME, CHANGE_ID))));
List<LoggedRequest> loggedRequests = WireMock.findAll(requestPatternBuilder);
Assert.assertEquals(1, loggedRequests.size());
JsonObject gptRequestBody = gson.fromJson(openAiClient.getRequestBody(), JsonObject.class);
JsonArray prompts = gptRequestBody.get("messages").getAsJsonArray();
String userPrompt = prompts.get(1).getAsJsonObject().get("content").getAsString();
Assert.assertEquals(commentUserPrompt, userPrompt);
}
}