blob: 3de4ec9973dec07add47527462e3a2682828408b [file] [log] [blame] [edit]
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.acceptance;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.OptionalSubject.optionals;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.common.truth.TruthJUnit.assume;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.entities.Patch.COMMIT_MSG;
import static com.google.gerrit.entities.Patch.MERGE_LIST;
import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.NON_VISIBLE_CHANGES;
import static com.google.gerrit.extensions.api.changes.SubmittedTogetherOption.TOPIC_CLOSURE;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static com.google.gerrit.server.project.testing.TestLabels.label;
import static com.google.gerrit.server.project.testing.TestLabels.value;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static org.eclipse.jgit.lib.Constants.HEAD;
import com.github.rholder.retry.BlockStrategy;
import com.google.common.base.Strings;
import com.google.common.base.Ticker;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.primitives.Chars;
import com.google.common.testing.FakeTicker;
import com.google.gerrit.acceptance.AcceptanceTestRequestScope.Context;
import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.acceptance.testsuite.account.TestSshKeys;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.acceptance.testsuite.request.SshSessionFactory;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.AccessSection;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.AccountGroup;
import com.google.gerrit.entities.Address;
import com.google.gerrit.entities.BooleanProjectConfig;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.EmailHeader;
import com.google.gerrit.entities.EmailHeader.StringEmailHeader;
import com.google.gerrit.entities.GroupDescription;
import com.google.gerrit.entities.GroupReference;
import com.google.gerrit.entities.InternalGroup;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LabelValue;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.PermissionRule;
import com.google.gerrit.entities.PermissionRule.Action;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.changes.SubmittedTogetherInfo;
import com.google.gerrit.extensions.api.changes.SubmittedTogetherOption;
import com.google.gerrit.extensions.api.projects.BranchApi;
import com.google.gerrit.extensions.api.projects.BranchInfo;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.api.projects.ProjectInput;
import com.google.gerrit.extensions.client.InheritableBoolean;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.ProjectWatchInfo;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeType;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.IdString;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.json.OutputFormat;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.PluginUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.Accounts;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.change.BatchAbandon;
import com.google.gerrit.server.change.ChangeFinder;
import com.google.gerrit.server.change.ChangeResource;
import com.google.gerrit.server.change.FileContentUtil;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.GerritInstanceId;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.PluginConfigFactory;
import com.google.gerrit.server.config.SitePaths;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.index.account.AccountIndexer;
import com.google.gerrit.server.index.change.ChangeIndexer;
import com.google.gerrit.server.notedb.AbstractChangeNotes;
import com.google.gerrit.server.notedb.ChangeNoteUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.ChangeNotesCommit;
import com.google.gerrit.server.notedb.ChangeNotesCommit.ChangeNotesRevWalk;
import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
import com.google.gerrit.server.plugins.TestServerPlugin;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.restapi.change.Revisions;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.util.git.DelegateSystemReader;
import com.google.gerrit.testing.ConfigSuite;
import com.google.gerrit.testing.FakeEmailSender;
import com.google.gerrit.testing.FakeEmailSender.Message;
import com.google.gerrit.testing.SshMode;
import com.google.gerrit.testing.TestTimeUtil;
import com.google.gson.Gson;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Provider;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileBasedConfig;
import org.eclipse.jgit.transport.Transport;
import org.eclipse.jgit.util.FS;
import org.eclipse.jgit.util.SystemReader;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runner.RunWith;
import org.junit.runners.model.Statement;
@RunWith(ConfigSuite.class)
public abstract class AbstractDaemonTest {
/**
* Test methods without special annotations will use a common server for efficiency reasons. The
* server is torn down after the test class is done.
*/
private static GerritServer commonServer;
private static Description firstTest;
@ClassRule public static TemporaryFolder temporaryFolder = new TemporaryFolder();
@ConfigSuite.Parameter public Config baseConfig;
@ConfigSuite.Name private String configName;
@Rule
public TestRule testRunner =
new TestRule() {
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
if (firstTest == null) {
firstTest = description;
}
beforeTest(description);
ProjectResetter.Config input = requireNonNull(resetProjects());
try (ProjectResetter resetter =
projectResetter != null ? projectResetter.builder().build(input) : null) {
AbstractDaemonTest.this.resetter = resetter;
base.evaluate();
} finally {
AbstractDaemonTest.this.resetter = null;
afterTest();
}
}
};
}
};
@Inject @CanonicalWebUrl protected Provider<String> canonicalWebUrl;
@Inject @GerritPersonIdent protected Provider<PersonIdent> serverIdent;
@Inject @GerritServerConfig protected Config cfg;
@Inject @GerritInstanceId @Nullable protected String instanceId;
@Inject protected AcceptanceTestRequestScope atrScope;
@Inject protected AccountCache accountCache;
@Inject protected AccountCreator accountCreator;
@Inject protected Accounts accounts;
@Inject protected AllProjectsName allProjects;
@Inject protected AllUsersName allUsers;
@Inject protected BatchUpdate.Factory batchUpdateFactory;
@Inject protected ChangeData.Factory changeDataFactory;
@Inject protected ChangeFinder changeFinder;
@Inject protected ChangeIndexer indexer;
@Inject protected ChangeNoteUtil changeNoteUtil;
@Inject protected ChangeResource.Factory changeResourceFactory;
@Inject protected FakeEmailSender sender;
@Inject protected GerritApi gApi;
@Inject protected GitRepositoryManager repoManager;
@Inject protected GroupBackend groupBackend;
@Inject protected GroupCache groupCache;
@Inject protected IdentifiedUser.GenericFactory identifiedUserFactory;
@Inject protected MetaDataUpdate.Server metaDataUpdateFactory;
@Inject protected PatchSetUtil psUtil;
@Inject protected ProjectCache projectCache;
@Inject protected ProjectConfig.Factory projectConfigFactory;
@Inject protected ProjectResetter.Builder.Factory projectResetter;
@Inject protected Provider<InternalChangeQuery> queryProvider;
@Inject protected PushOneCommit.Factory pushFactory;
@Inject protected PluginConfigFactory pluginConfig;
@Inject protected Revisions revisions;
@Inject protected SystemGroupBackend systemGroupBackend;
@Inject protected ChangeNotes.Factory notesFactory;
@Inject protected BatchAbandon batchAbandon;
@Inject protected TestSshKeys sshKeys;
@Inject protected TestTicker testTicker;
protected EventRecorder eventRecorder;
protected GerritServer server;
protected Project.NameKey project;
protected RestSession adminRestSession;
protected RestSession userRestSession;
protected RestSession anonymousRestSession;
protected SshSession adminSshSession;
protected SshSession userSshSession;
protected TestAccount admin;
protected TestAccount user;
protected TestRepository<InMemoryRepository> testRepo;
protected String testMethodName;
protected String resourcePrefix;
protected Description description;
protected GerritServer.Description testMethodDescription;
protected boolean testRequiresSsh;
protected BlockStrategy noSleepBlockStrategy = t -> {}; // Don't sleep in tests.
@Inject private AbstractChangeNotes.Args changeNotesArgs;
@Inject private AccountIndexer accountIndexer;
@Inject private EventRecorder.Factory eventRecorderFactory;
@Inject private InProcessProtocol inProcessProtocol;
@Inject private PluginGuiceEnvironment pluginGuiceEnvironment;
@Inject private PluginUser.Factory pluginUserFactory;
@Inject private RequestScopeOperations requestScopeOperations;
@Inject private SitePaths sitePaths;
@Inject private ProjectOperations projectOperations;
private ProjectResetter resetter;
private List<Repository> toClose;
private String systemTimeZone;
private SystemReader oldSystemReader;
@Before
public void clearSender() {
if (sender != null) {
sender.clear();
}
}
@Before
public void startEventRecorder() {
if (eventRecorderFactory != null) {
eventRecorder = eventRecorderFactory.create(admin);
}
}
@Before
public void assumeSshIfRequired() {
if (testRequiresSsh) {
// If the test uses ssh, we use assume() to make sure ssh is enabled on
// the test suite. JUnit will skip tests annotated with @UseSsh if we
// disable them using the command line flag.
assume().that(SshMode.useSsh()).isTrue();
}
}
@After
public void verifyNoPiiInChangeNotes() throws RestApiException, IOException {
if (testMethodDescription.verifyNoPiiInChangeNotes()) {
verifyNoAccountDetailsInChangeNotes();
}
}
@After
public void closeEventRecorder() {
if (eventRecorder != null) {
eventRecorder.close();
}
}
@ConfigSuite.AfterConfig
public static void stopCommonServer() throws Exception {
if (commonServer != null) {
try {
commonServer.close();
} catch (Exception e) {
throw new AssertionError(
"Error stopping common server in "
+ (firstTest != null ? firstTest.getTestClass().getName() : "unknown test class"),
e);
} finally {
commonServer = null;
}
}
}
/** Controls which project and branches should be reset after each test case. */
protected ProjectResetter.Config resetProjects() {
return new ProjectResetter.Config()
// Don't reset all refs so that refs/sequences/changes is not touched and change IDs are
// not reused.
.reset(allProjects, RefNames.REFS_CONFIG)
// Don't reset refs/sequences/accounts so that account IDs are not reused.
.reset(
allUsers,
RefNames.REFS_CONFIG,
RefNames.REFS_USERS + "*",
RefNames.REFS_EXTERNAL_IDS,
RefNames.REFS_GROUPNAMES,
RefNames.REFS_GROUPS + "*",
RefNames.REFS_STARRED_CHANGES + "*",
RefNames.REFS_DRAFT_COMMENTS + "*");
}
protected void restartAsSlave() throws Exception {
closeSsh();
server = GerritServer.restartAsSlave(server);
server.getTestInjector().injectMembers(this);
if (resetter != null) {
server.getTestInjector().injectMembers(resetter);
}
initSsh();
}
protected void restart() throws Exception {
server = GerritServer.restart(server, createModule(), createSshModule());
server.getTestInjector().injectMembers(this);
if (resetter != null) {
server.getTestInjector().injectMembers(resetter);
}
initSsh();
}
protected void reindexAccount(Account.Id accountId) {
accountIndexer.index(accountId);
}
protected static Config submitWholeTopicEnabledConfig() {
Config cfg = new Config();
cfg.setBoolean("change", null, "submitWholeTopic", true);
return cfg;
}
protected boolean isSubmitWholeTopicEnabled() {
return cfg.getBoolean("change", null, "submitWholeTopic", false);
}
protected boolean isContributorAgreementsEnabled() {
return cfg.getBoolean("auth", null, "contributorAgreements", false);
}
protected void beforeTest(Description description) throws Exception {
// SystemReader must be overridden before creating any repos, since they read the user/system
// configs at initialization time, and are then stored in the RepositoryCache forever.
oldSystemReader = setFakeSystemReader(temporaryFolder.getRoot());
this.description = description;
GerritServer.Description classDesc =
GerritServer.Description.forTestClass(description, configName);
GerritServer.Description methodDesc =
GerritServer.Description.forTestMethod(description, configName);
testMethodDescription = methodDesc;
testRequiresSsh = classDesc.useSshAnnotation() || methodDesc.useSshAnnotation();
if (!testRequiresSsh) {
baseConfig.setString("sshd", null, "listenAddress", "off");
}
baseConfig.unset("gerrit", null, "canonicalWebUrl");
baseConfig.unset("httpd", null, "listenUrl");
baseConfig.setInt("index", null, "batchThreads", -1);
Module module = createModule();
Module auditModule = createAuditModule();
Module sshModule = createSshModule();
if (classDesc.equals(methodDesc) && !classDesc.sandboxed() && !methodDesc.sandboxed()) {
if (commonServer == null) {
commonServer =
GerritServer.initAndStart(
temporaryFolder, classDesc, baseConfig, module, auditModule, sshModule);
}
server = commonServer;
} else {
server =
GerritServer.initAndStart(
temporaryFolder, methodDesc, baseConfig, module, auditModule, sshModule);
}
server.getTestInjector().injectMembers(this);
Transport.register(inProcessProtocol);
toClose = Collections.synchronizedList(new ArrayList<>());
admin = accountCreator.admin();
user = accountCreator.user1();
// Evict and reindex accounts in case tests modify them.
reindexAccount(admin.id());
reindexAccount(user.id());
adminRestSession = new RestSession(server, admin);
userRestSession = new RestSession(server, user);
anonymousRestSession = new RestSession(server, null);
initSsh();
testMethodName = description.getMethodName();
resourcePrefix =
UNSAFE_PROJECT_NAME
.matcher(description.getClassName() + "_" + testMethodName + "_")
.replaceAll("");
Context ctx = newRequestContext(admin);
atrScope.set(ctx);
ProjectInput in = projectInput(description);
gApi.projects().create(in);
project = Project.nameKey(in.name);
if (!classDesc.skipProjectClone()) {
testRepo = cloneProject(project, getCloneAsAccount(description));
}
// Set the clock step last, so that the test setup isn't consuming any timestamps after the
// clock has been set.
setTimeSettings(classDesc.useSystemTime(), classDesc.useClockStep(), classDesc.useTimezone());
setTimeSettings(
methodDesc.useSystemTime(), methodDesc.useClockStep(), methodDesc.useTimezone());
}
private static SystemReader setFakeSystemReader(File tempDir) {
SystemReader oldSystemReader = SystemReader.getInstance();
SystemReader.setInstance(
new DelegateSystemReader(oldSystemReader) {
@Override
public FileBasedConfig openJGitConfig(Config parent, FS fs) {
return new FileBasedConfig(parent, new File(tempDir, "jgit.config"), FS.detect());
}
@Override
public FileBasedConfig openUserConfig(Config parent, FS fs) {
return new FileBasedConfig(parent, new File(tempDir, "user.config"), FS.detect());
}
@Override
public FileBasedConfig openSystemConfig(Config parent, FS fs) {
return new FileBasedConfig(parent, new File(tempDir, "system.config"), FS.detect());
}
});
return oldSystemReader;
}
private void setTimeSettings(
boolean useSystemTime,
@Nullable UseClockStep useClockStep,
@Nullable UseTimezone useTimezone) {
if (useSystemTime) {
TestTimeUtil.useSystemTime();
} else if (useClockStep != null) {
TestTimeUtil.resetWithClockStep(useClockStep.clockStep(), useClockStep.clockStepUnit());
if (useClockStep.startAtEpoch()) {
TestTimeUtil.setClock(Timestamp.from(Instant.EPOCH));
}
}
if (useTimezone != null) {
systemTimeZone = System.setProperty("user.timezone", useTimezone.timezone());
}
}
private void resetTimeSettings() {
TestTimeUtil.useSystemTime();
if (systemTimeZone != null) {
System.setProperty("user.timezone", systemTimeZone);
systemTimeZone = null;
}
}
/** Override to bind an additional Guice module */
public Module createModule() {
return null;
}
/** Override to bind an alternative audit Guice module */
public Module createAuditModule() {
return null;
}
/** Override to bind an additional Guice module for SSH injector */
public Module createSshModule() {
return null;
}
protected void initSsh() throws Exception {
if (testRequiresSsh
&& SshMode.useSsh()
&& (adminSshSession == null || userSshSession == null)) {
// Create Ssh sessions
SshSessionFactory.initSsh();
Context ctx = newRequestContext(user);
atrScope.set(ctx);
userSshSession = ctx.getSession();
userSshSession.open();
ctx = newRequestContext(admin);
atrScope.set(ctx);
adminSshSession = ctx.getSession();
adminSshSession.open();
}
}
private TestAccount getCloneAsAccount(Description description) {
TestProjectInput ann = description.getAnnotation(TestProjectInput.class);
return accountCreator.get(ann != null ? ann.cloneAs() : "admin");
}
/** Generate default project properties based on test description */
private ProjectInput projectInput(Description description) {
ProjectInput in = new ProjectInput();
TestProjectInput ann = description.getAnnotation(TestProjectInput.class);
in.name = name("project");
in.branches = ImmutableList.of(Constants.R_HEADS + Constants.MASTER);
if (ann != null) {
in.parent = Strings.emptyToNull(ann.parent());
in.description = Strings.emptyToNull(ann.description());
in.createEmptyCommit = ann.createEmptyCommit();
in.submitType = ann.submitType();
in.useContentMerge = ann.useContributorAgreements();
in.useSignedOffBy = ann.useSignedOffBy();
in.useContentMerge = ann.useContentMerge();
in.rejectEmptyCommit = ann.rejectEmptyCommit();
in.enableSignedPush = ann.enableSignedPush();
in.requireSignedPush = ann.requireSignedPush();
} else {
// Defaults should match TestProjectConfig, omitting nullable values.
in.createEmptyCommit = true;
}
updateProjectInput(in);
return in;
}
/**
* Modify a project input before creating the initial test project.
*
* @param in input; may be modified in place.
*/
protected void updateProjectInput(ProjectInput in) {
// Default implementation does nothing.
}
private static final Pattern UNSAFE_PROJECT_NAME = Pattern.compile("[^a-zA-Z0-9._/-]+");
protected Git git() {
return testRepo.git();
}
protected InMemoryRepository repo() {
return testRepo.getRepository();
}
/**
* Return a resource name scoped to this test method.
*
* <p>Test methods in a single class by default share a running server. For any resource name you
* require to be unique to a test method, wrap it in a call to this method.
*
* @param name resource name (group, project, topic, etc.)
* @return name prefixed by a string unique to this test method.
*/
protected String name(String name) {
return resourcePrefix + name;
}
protected Project.NameKey createProjectOverAPI(
String nameSuffix,
@Nullable Project.NameKey parent,
boolean createEmptyCommit,
@Nullable SubmitType submitType)
throws RestApiException {
ProjectInput in = new ProjectInput();
in.name = name(nameSuffix);
in.parent = parent != null ? parent.get() : null;
in.submitType = submitType;
in.createEmptyCommit = createEmptyCommit;
gApi.projects().create(in);
return Project.nameKey(in.name);
}
protected TestRepository<InMemoryRepository> cloneProject(Project.NameKey p) throws Exception {
return cloneProject(p, admin);
}
protected TestRepository<InMemoryRepository> cloneProject(
Project.NameKey p, TestAccount testAccount) throws Exception {
return GitUtil.cloneProject(p, registerRepoConnection(p, testAccount));
}
/**
* Register a repository connection over the test protocol.
*
* @return a URI string that can be used to connect to this repository for both fetch and push.
*/
protected String registerRepoConnection(Project.NameKey p, TestAccount testAccount)
throws Exception {
InProcessProtocol.Context ctx =
new InProcessProtocol.Context(identifiedUserFactory, testAccount.id(), p);
Repository repo = repoManager.openRepository(p);
toClose.add(repo);
return inProcessProtocol.register(ctx, repo).toString();
}
protected void afterTest() throws Exception {
Transport.unregister(inProcessProtocol);
for (Repository repo : toClose) {
repo.close();
}
closeSsh();
resetTimeSettings();
if (server != commonServer) {
server.close();
server = null;
}
SystemReader.setInstance(oldSystemReader);
oldSystemReader = null;
// Set useDefaultTicker in afterTest, so the next beforeTest will use the default ticker
testTicker.useDefaultTicker();
}
protected void closeSsh() {
if (adminSshSession != null) {
adminSshSession.close();
adminSshSession = null;
}
if (userSshSession != null) {
userSshSession.close();
userSshSession = null;
}
}
/**
* Verify that NoteDB commits do not persist user-sensitive information, by running checks for all
* commits in {@link RefNames#changeMetaRef} for all changes, created during the test.
*
* <p>These tests prevent regression, assuming appropriate test coverage for new features. The
* verification is disabled by default and can be enabled using {@link VerifyNoPiiInChangeNotes}
* annotation either on test class or method.
*/
protected void verifyNoAccountDetailsInChangeNotes() throws RestApiException, IOException {
List<ChangeInfo> allChanges = gApi.changes().query().get();
List<AccountState> allAccounts = accounts.all();
for (ChangeInfo change : allChanges) {
try (Repository repo = repoManager.openRepository(Project.nameKey(change.project))) {
String metaRefName =
RefNames.changeMetaRef(Change.Id.tryParse(change._number.toString()).get());
ObjectId metaTip = repo.getRefDatabase().exactRef(metaRefName).getObjectId();
ChangeNotesRevWalk revWalk = ChangeNotesCommit.newRevWalk(repo);
revWalk.reset();
revWalk.markStart(revWalk.parseCommit(metaTip));
ChangeNotesCommit commit;
while ((commit = revWalk.next()) != null) {
String fullMessage = commit.getFullMessage();
for (AccountState accountState : allAccounts) {
Account account = accountState.account();
assertThat(fullMessage).doesNotContain(account.getName());
if (account.fullName() != null) {
assertThat(fullMessage).doesNotContain(account.fullName());
}
if (account.displayName() != null) {
assertThat(fullMessage).doesNotContain(account.displayName());
}
if (account.preferredEmail() != null) {
assertThat(fullMessage).doesNotContain(account.preferredEmail());
}
if (accountState.userName().isPresent()) {
assertThat(fullMessage).doesNotContain(accountState.userName().get());
}
List<String> allEmails =
accountState.externalIds().stream()
.map(ExternalId::email)
.filter(Objects::nonNull)
.collect(toImmutableList());
for (String email : allEmails) {
assertThat(fullMessage).doesNotContain(email);
}
}
}
}
}
}
protected TestRepository<?>.CommitBuilder commitBuilder() throws Exception {
return testRepo.branch("HEAD").commit().insertChangeId();
}
protected TestRepository<?>.CommitBuilder amendBuilder() throws Exception {
ObjectId head = repo().exactRef("HEAD").getObjectId();
TestRepository<?>.CommitBuilder b = testRepo.amendRef("HEAD");
Optional<String> id = GitUtil.getChangeId(testRepo, head);
// TestRepository behaves like "git commit --amend -m foo", which does not
// preserve an existing Change-Id. Tests probably want this.
if (id.isPresent()) {
b.insertChangeId(id.get().substring(1));
} else {
b.insertChangeId();
}
return b;
}
protected PushOneCommit.Result createChange() throws Exception {
return createChange("refs/for/master");
}
protected PushOneCommit.Result createChange(String ref) throws Exception {
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
PushOneCommit.Result result = push.to(ref);
result.assertOkStatus();
return result;
}
protected PushOneCommit.Result createChange(TestRepository<InMemoryRepository> repo)
throws Exception {
PushOneCommit push = pushFactory.create(admin.newIdent(), repo);
PushOneCommit.Result result = push.to("refs/for/master");
result.assertOkStatus();
return result;
}
protected PushOneCommit.Result createMergeCommitChange(String ref) throws Exception {
return createMergeCommitChange(ref, "foo");
}
protected PushOneCommit.Result createMergeCommitChange(String ref, String file) throws Exception {
ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
PushOneCommit.Result p1 =
pushFactory
.create(
admin.newIdent(),
testRepo,
"parent 1",
ImmutableMap.of(file, "foo-1", "bar", "bar-1"))
.to(ref);
// reset HEAD in order to create a sibling of the first change
testRepo.reset(initial);
PushOneCommit.Result p2 =
pushFactory
.create(
admin.newIdent(),
testRepo,
"parent 2",
ImmutableMap.of(file, "foo-2", "bar", "bar-2"))
.to(ref);
PushOneCommit m =
pushFactory.create(
admin.newIdent(), testRepo, "merge", ImmutableMap.of(file, "foo-1", "bar", "bar-2"));
m.setParents(ImmutableList.of(p1.getCommit(), p2.getCommit()));
PushOneCommit.Result result = m.to(ref);
result.assertOkStatus();
return result;
}
protected PushOneCommit.Result createNParentsMergeCommitChange(String ref, List<String> fileNames)
throws Exception {
// This method creates n different commits and creates a merge commit pointing to all n parents.
// Each commit will contain all the fileNames. Commit i will have the following file names and
// their contents:
// {$file_1_name, ${file_1_name}-1}
// {$file_2_name, ${file_2_name}-1}, etc...
// The merge commit will have:
// {$file_1_name, ${file_1_name}-1}
// {$file_2_name, ${file_2_name}-2},
// {$file_3_name, ${file_3_name}-3}, etc...
// i.e. taking the ith file from the ith commit.
int n = fileNames.size();
ObjectId initial = repo().exactRef(HEAD).getLeaf().getObjectId();
List<PushOneCommit.Result> pushResults = new ArrayList<>();
for (int i = 1; i <= n; i++) {
int finalI = i;
pushResults.add(
pushFactory
.create(
admin.newIdent(),
testRepo,
"parent " + i,
fileNames.stream().collect(Collectors.toMap(f -> f, f -> f + "-" + finalI)))
.to(ref));
// reset HEAD in order to create a sibling of the first change
if (i < n) {
testRepo.reset(initial);
}
}
PushOneCommit m =
pushFactory.create(
admin.newIdent(),
testRepo,
"merge",
IntStream.range(1, n + 1)
.boxed()
.collect(
Collectors.toMap(
i -> fileNames.get(i - 1), i -> fileNames.get(i - 1) + "-" + i)));
m.setParents(pushResults.stream().map(PushOneCommit.Result::getCommit).collect(toList()));
PushOneCommit.Result result = m.to(ref);
result.assertOkStatus();
return result;
}
protected PushOneCommit.Result createCommitAndPush(
TestRepository<InMemoryRepository> repo,
String ref,
String commitMsg,
String fileName,
String content)
throws Exception {
PushOneCommit.Result result =
pushFactory.create(admin.newIdent(), repo, commitMsg, fileName, content).to(ref);
result.assertOkStatus();
return result;
}
protected PushOneCommit.Result createChangeWithTopic(
TestRepository<InMemoryRepository> repo,
String topic,
String commitMsg,
String fileName,
String content)
throws Exception {
assertThat(topic).isNotEmpty();
return createCommitAndPush(
repo, "refs/for/master%topic=" + name(topic), commitMsg, fileName, content);
}
protected PushOneCommit.Result createChange(String subject, String fileName, String content)
throws Exception {
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo, subject, fileName, content);
return push.to("refs/for/master");
}
protected PushOneCommit.Result createChange(
TestRepository<?> repo,
String branch,
String subject,
String fileName,
String content,
@Nullable String topic)
throws Exception {
PushOneCommit push = pushFactory.create(admin.newIdent(), repo, subject, fileName, content);
return push.to(
"refs/for/" + branch + (Strings.isNullOrEmpty(topic) ? "" : "%topic=" + name(topic)));
}
protected BranchApi createBranch(BranchNameKey branch) throws Exception {
return gApi.projects()
.name(branch.project().get())
.branch(branch.branch())
.create(new BranchInput());
}
protected BranchApi createBranchWithRevision(BranchNameKey branch, String revision)
throws Exception {
BranchInput in = new BranchInput();
in.revision = revision;
return gApi.projects().name(branch.project().get()).branch(branch.branch()).create(in);
}
private static final List<Character> RANDOM =
Chars.asList('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h');
protected PushOneCommit.Result amendChangeWithUploader(
PushOneCommit.Result change, Project.NameKey projectName, TestAccount account)
throws Exception {
TestRepository<InMemoryRepository> repo = cloneProject(projectName, account);
GitUtil.fetch(repo, "refs/*:refs/*");
repo.reset(change.getCommit());
PushOneCommit.Result result =
amendChange(
change.getChangeId(),
"refs/for/master",
account,
repo,
"new subject",
"new file",
"new content " + UUID.randomUUID());
return result;
}
protected PushOneCommit.Result amendChange(String changeId) throws Exception {
return amendChange(changeId, "refs/for/master", admin, testRepo);
}
protected PushOneCommit.Result amendChange(
String changeId, String ref, TestAccount testAccount, TestRepository<?> repo)
throws Exception {
Collections.shuffle(RANDOM);
return amendChange(
changeId,
ref,
testAccount,
repo,
PushOneCommit.SUBJECT,
PushOneCommit.FILE_NAME,
new String(Chars.toArray(RANDOM)));
}
protected PushOneCommit.Result amendChange(
String changeId, String subject, String fileName, String content) throws Exception {
return amendChange(changeId, "refs/for/master", admin, testRepo, subject, fileName, content);
}
protected PushOneCommit.Result amendChange(
String changeId,
String ref,
TestAccount testAccount,
TestRepository<?> repo,
String subject,
String fileName,
String content)
throws Exception {
PushOneCommit push =
pushFactory.create(testAccount.newIdent(), repo, subject, fileName, content, changeId);
return push.to(ref);
}
protected void merge(PushOneCommit.Result r) throws Exception {
revision(r).review(ReviewInput.approve());
revision(r).submit();
}
protected ChangeInfo info(String id) throws RestApiException {
return gApi.changes().id(id).info();
}
protected ChangeApi change(Result r) throws RestApiException {
return gApi.changes().id(r.getChange().getId().get());
}
protected Optional<EditInfo> getEdit(String id) throws RestApiException {
return gApi.changes().id(id).edit().get();
}
protected ChangeInfo get(String id, ListChangesOption... options) throws RestApiException {
return gApi.changes().id(id).get(options);
}
protected AccountInfo getAccountInfo(Account.Id accountId) throws RestApiException {
return gApi.accounts().id(accountId.get()).get();
}
protected List<ChangeInfo> query(String q) throws RestApiException {
return gApi.changes().query(q).get();
}
private Context newRequestContext(TestAccount account) {
requestScopeOperations.setApiUser(account.id());
return atrScope.get();
}
protected Account getAccount(Account.Id accountId) {
return getAccountState(accountId).account();
}
protected AccountState getAccountState(Account.Id accountId) {
Optional<AccountState> accountState = accountCache.get(accountId);
assertWithMessage("account %s", accountId.get())
.about(optionals())
.that(accountState)
.isPresent();
return accountState.get();
}
protected AutoCloseable disableNoteDb() {
changeNotesArgs.failOnLoadForTest.set(true);
Context oldContext = atrScope.disableNoteDb();
return () -> {
changeNotesArgs.failOnLoadForTest.set(false);
atrScope.set(oldContext);
};
}
protected static Gson newGson() {
return OutputFormat.JSON_COMPACT.newGson();
}
protected RevisionApi revision(PushOneCommit.Result r) throws Exception {
return gApi.changes().id(r.getChangeId()).current();
}
protected void setUseSignedOffBy(InheritableBoolean value) throws Exception {
try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
ProjectConfig config = projectConfigFactory.read(md);
config.updateProject(p -> p.setBooleanConfig(BooleanProjectConfig.USE_SIGNED_OFF_BY, value));
config.commit(md);
projectCache.evictAndReindex(config.getProject());
}
}
protected void setRequireChangeId(InheritableBoolean value) throws Exception {
try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
ProjectConfig config = projectConfigFactory.read(md);
config.updateProject(p -> p.setBooleanConfig(BooleanProjectConfig.REQUIRE_CHANGE_ID, value));
config.commit(md);
projectCache.evictAndReindex(config.getProject());
}
}
protected void setSkipAddingAuthorAndCommitterAsReviewers(InheritableBoolean value)
throws Exception {
try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
ProjectConfig config = projectConfigFactory.read(md);
config.updateProject(
p ->
p.setBooleanConfig(
BooleanProjectConfig.SKIP_ADDING_AUTHOR_AND_COMMITTER_AS_REVIEWERS, value));
config.commit(md);
projectCache.evictAndReindex(config.getProject());
}
}
protected void blockAnonymousRead() throws Exception {
String allRefs = RefNames.REFS + "*";
projectOperations
.project(project)
.forUpdate()
.add(block(Permission.READ).ref(allRefs).group(ANONYMOUS_USERS))
.add(allow(Permission.READ).ref(allRefs).group(REGISTERED_USERS))
.update();
}
protected PushOneCommit.Result pushTo(String ref) throws Exception {
PushOneCommit push = pushFactory.create(admin.newIdent(), testRepo);
return push.to(ref);
}
protected void approve(String id) throws Exception {
gApi.changes().id(id).current().review(ReviewInput.approve());
}
protected void recommend(String id) throws Exception {
gApi.changes().id(id).current().review(ReviewInput.recommend());
}
protected void assertThatAccountIsNotVisible(TestAccount... testAccounts) {
for (TestAccount testAccount : testAccounts) {
assertThrows(
ResourceNotFoundException.class, () -> gApi.accounts().id(testAccount.id().get()).get());
}
}
protected void assertReviewers(String changeId, TestAccount... expectedReviewers)
throws RestApiException {
Map<ReviewerState, Collection<AccountInfo>> reviewerMap =
gApi.changes().id(changeId).get().reviewers;
assertThat(reviewerMap).containsKey(ReviewerState.REVIEWER);
List<Integer> reviewers =
reviewerMap.get(ReviewerState.REVIEWER).stream().map(a -> a._accountId).collect(toList());
assertThat(reviewers)
.containsExactlyElementsIn(
Arrays.stream(expectedReviewers).map(a -> a.id().get()).collect(toList()));
}
protected void assertCcs(String changeId, TestAccount... expectedCcs) throws RestApiException {
Map<ReviewerState, Collection<AccountInfo>> reviewerMap =
gApi.changes().id(changeId).get().reviewers;
assertThat(reviewerMap).containsKey(ReviewerState.CC);
List<Integer> ccs =
reviewerMap.get(ReviewerState.CC).stream().map(a -> a._accountId).collect(toList());
assertThat(ccs)
.containsExactlyElementsIn(
Arrays.stream(expectedCcs).map(a -> a.id().get()).collect(toList()));
}
protected void assertNoCcs(String changeId) throws RestApiException {
Map<ReviewerState, Collection<AccountInfo>> reviewerMap =
gApi.changes().id(changeId).get().reviewers;
assertThat(reviewerMap).doesNotContainKey(ReviewerState.CC);
}
protected void assertSubmittedTogether(String chId, String... expected) throws Exception {
assertSubmittedTogether(chId, ImmutableSet.of(), expected);
}
protected void assertSubmittedTogetherWithTopicClosure(String chId, String... expected)
throws Exception {
assertSubmittedTogether(chId, ImmutableSet.of(TOPIC_CLOSURE), expected);
}
protected void assertSubmittedTogether(
String chId,
ImmutableSet<SubmittedTogetherOption> submittedTogetherOptions,
String... expected)
throws Exception {
// This does not include NON_VISIBILE_CHANGES
List<ChangeInfo> actual =
submittedTogetherOptions.isEmpty()
? gApi.changes().id(chId).submittedTogether()
: gApi.changes()
.id(chId)
.submittedTogether(EnumSet.copyOf(submittedTogetherOptions))
.changes;
EnumSet<SubmittedTogetherOption> enumSetIncludingNonVisibleChanges =
submittedTogetherOptions.isEmpty()
? EnumSet.of(NON_VISIBLE_CHANGES)
: EnumSet.copyOf(submittedTogetherOptions);
enumSetIncludingNonVisibleChanges.add(NON_VISIBLE_CHANGES);
// This includes NON_VISIBLE_CHANGES for comparison.
SubmittedTogetherInfo info =
gApi.changes().id(chId).submittedTogether(enumSetIncludingNonVisibleChanges);
assertThat(info.nonVisibleChanges).isEqualTo(0);
assertThat(Iterables.transform(actual, i1 -> i1.changeId))
.containsExactly((Object[]) expected)
.inOrder();
assertThat(Iterables.transform(info.changes, i -> i.changeId))
.containsExactly((Object[]) expected)
.inOrder();
}
protected PatchSet getPatchSet(PatchSet.Id psId) {
return changeDataFactory.create(project, psId.changeId()).patchSet(psId);
}
protected IdentifiedUser user(TestAccount testAccount) {
return identifiedUserFactory.create(testAccount.id());
}
protected RevisionResource parseCurrentRevisionResource(String changeId) throws Exception {
ChangeResource cr = parseChangeResource(changeId);
int psId = cr.getChange().currentPatchSetId().get();
return revisions.parse(cr, IdString.fromDecoded(Integer.toString(psId)));
}
protected RevisionResource parseRevisionResource(String changeId, int n) throws Exception {
return revisions.parse(
parseChangeResource(changeId), IdString.fromDecoded(Integer.toString(n)));
}
protected RevisionResource parseRevisionResource(PushOneCommit.Result r) throws Exception {
PatchSet.Id psId = r.getPatchSetId();
return parseRevisionResource(psId.changeId().toString(), psId.get());
}
protected ChangeResource parseChangeResource(String changeId) throws Exception {
List<ChangeNotes> notes = changeFinder.find(changeId);
assertThat(notes).hasSize(1);
return changeResourceFactory.create(notes.get(0), atrScope.get().getUser());
}
protected RevCommit getHead(Repository repo, String name) throws Exception {
try (RevWalk rw = new RevWalk(repo)) {
Ref r = repo.exactRef(name);
return rw.parseCommit(r.getObjectId());
}
}
protected void assertMailReplyTo(Message message, String email) throws Exception {
assertThat(message.headers()).containsKey("Reply-To");
StringEmailHeader replyTo = (StringEmailHeader) message.headers().get("Reply-To");
assertThat(replyTo.getString()).contains(email);
}
protected void assertMailNotReplyTo(Message message, String email) throws Exception {
assertThat(message.headers()).containsKey("Reply-To");
StringEmailHeader replyTo = (StringEmailHeader) message.headers().get("Reply-To");
assertThat(replyTo.getString()).doesNotContain(email);
}
/** Assert that the given branches have the given tree ids. */
protected void assertTrees(Project.NameKey proj, Map<BranchNameKey, ObjectId> trees)
throws Exception {
TestRepository<?> localRepo = cloneProject(proj);
GitUtil.fetch(localRepo, "refs/*:refs/*");
Map<BranchNameKey, RevTree> refValues = new HashMap<>();
for (BranchNameKey b : trees.keySet()) {
if (!b.project().equals(proj)) {
continue;
}
Ref r = localRepo.getRepository().exactRef(b.branch());
assertThat(r).isNotNull();
RevWalk rw = localRepo.getRevWalk();
RevCommit c = rw.parseCommit(r.getObjectId());
refValues.put(b, c.getTree());
assertThat(trees.get(b)).isEqualTo(refValues.get(b));
}
assertThat(refValues.keySet()).containsAnyIn(trees.keySet());
}
protected void assertDiffForFullyModifiedFile(
DiffInfo diff,
String commitName,
String path,
String expectedContentSideA,
String expectedContentSideB)
throws Exception {
assertDiffForFile(diff, commitName, path);
ImmutableList<String> expectedOldLines =
ImmutableList.copyOf(expectedContentSideA.split("\n", -1));
ImmutableList<String> expectedNewLines =
ImmutableList.copyOf(expectedContentSideB.split("\n", -1));
assertThat(diff.changeType).isEqualTo(ChangeType.MODIFIED);
assertThat(diff.metaA).isNotNull();
assertThat(diff.metaB).isNotNull();
assertThat(diff.metaA.name).isEqualTo(path);
assertThat(diff.metaA.lines).isEqualTo(expectedOldLines.size());
assertThat(diff.metaB.name).isEqualTo(path);
assertThat(diff.metaB.lines).isEqualTo(expectedNewLines.size());
DiffInfo.ContentEntry contentEntry = diff.content.get(0);
assertThat(contentEntry.a).containsExactlyElementsIn(expectedOldLines).inOrder();
assertThat(contentEntry.b).containsExactlyElementsIn(expectedNewLines).inOrder();
assertThat(contentEntry.ab).isNull();
assertThat(contentEntry.common).isNull();
assertThat(contentEntry.editA).isNull();
assertThat(contentEntry.editB).isNull();
assertThat(contentEntry.skip).isNull();
}
protected void assertDiffForNewFile(
DiffInfo diff, @Nullable RevCommit commit, String path, String expectedContentSideB)
throws Exception {
assertDiffForNewFile(diff, commit.name(), path, expectedContentSideB);
}
protected void assertDiffForNewFile(
DiffInfo diff, String commitName, String path, String expectedContentSideB) throws Exception {
assertDiffForFile(diff, commitName, path);
ImmutableList<String> expectedLines =
ImmutableList.copyOf(expectedContentSideB.split("\n", -1));
assertThat(diff.changeType).isEqualTo(ChangeType.ADDED);
assertThat(diff.metaA).isNull();
assertThat(diff.metaB).isNotNull();
assertThat(diff.metaB.name).isEqualTo(path);
assertThat(diff.metaB.lines).isEqualTo(expectedLines.size());
DiffInfo.ContentEntry contentEntry = diff.content.get(0);
assertThat(contentEntry.b).containsExactlyElementsIn(expectedLines).inOrder();
assertThat(contentEntry.a).isNull();
assertThat(contentEntry.ab).isNull();
assertThat(contentEntry.common).isNull();
assertThat(contentEntry.editA).isNull();
assertThat(contentEntry.editB).isNull();
assertThat(contentEntry.skip).isNull();
}
protected void assertDiffForDeletedFile(DiffInfo diff, String path, String expectedContentSideA)
throws Exception {
assertDiffHeaders(diff);
ImmutableList<String> expectedOriginalLines =
ImmutableList.copyOf(expectedContentSideA.split("\n", -1));
assertThat(diff.changeType).isEqualTo(ChangeType.DELETED);
assertThat(diff.metaA).isNotNull();
assertThat(diff.metaB).isNull();
assertThat(diff.metaA.name).isEqualTo(path);
assertThat(diff.metaA.lines).isEqualTo(expectedOriginalLines.size());
DiffInfo.ContentEntry contentEntry = diff.content.get(0);
assertThat(contentEntry.a).containsExactlyElementsIn(expectedOriginalLines).inOrder();
assertThat(contentEntry.b).isNull();
assertThat(contentEntry.ab).isNull();
assertThat(contentEntry.common).isNull();
assertThat(contentEntry.editA).isNull();
assertThat(contentEntry.editB).isNull();
assertThat(contentEntry.skip).isNull();
}
private void assertDiffForFile(DiffInfo diff, String commitName, String path) throws Exception {
assertDiffHeaders(diff);
assertThat(diff.metaB.commitId).isEqualTo(commitName);
String expectedContentType = "text/plain";
if (COMMIT_MSG.equals(path)) {
expectedContentType = FileContentUtil.TEXT_X_GERRIT_COMMIT_MESSAGE;
} else if (MERGE_LIST.equals(path)) {
expectedContentType = FileContentUtil.TEXT_X_GERRIT_MERGE_LIST;
}
assertThat(diff.metaB.contentType).isEqualTo(expectedContentType);
assertThat(diff.metaB.name).isEqualTo(path);
assertThat(diff.metaB.webLinks).isNull();
}
private void assertDiffHeaders(DiffInfo diff) throws Exception {
assertThat(diff.binary).isNull();
assertThat(diff.diffHeader).isNotNull();
assertThat(diff.intralineStatus).isNull();
assertThat(diff.webLinks).isNull();
assertThat(diff.editWebLinks).isNull();
}
protected void assertPermitted(ChangeInfo info, String label, Integer... expected) {
assertThat(info.permittedLabels).isNotNull();
Collection<String> strs = info.permittedLabels.get(label);
if (expected.length == 0) {
assertThat(strs).isNull();
} else {
assertThat(strs.stream().map(s -> Integer.valueOf(s.trim())).collect(toList()))
.containsExactlyElementsIn(Arrays.asList(expected));
}
}
protected void assertOnlyRemovableLabel(
ChangeInfo info, String labelId, String labelValue, TestAccount reviewer) {
assertThat(info.removableLabels).hasSize(1);
assertThat(info.removableLabels).containsKey(labelId);
assertThat(info.removableLabels.get(labelId)).hasSize(1);
assertThat(info.removableLabels.get(labelId)).containsKey(labelValue);
assertThat(info.removableLabels.get(labelId).get(labelValue)).hasSize(1);
assertThat(info.removableLabels.get(labelId).get(labelValue).get(0).email)
.isEqualTo(reviewer.email());
}
protected void assertPermissions(
Project.NameKey project,
GroupReference groupReference,
String ref,
boolean exclusive,
String... permissionNames) {
Optional<AccessSection> accessSection =
projectCache
.get(project)
.orElseThrow(illegalState(project))
.getConfig()
.getAccessSection(ref);
assertThat(accessSection).isPresent();
for (String permissionName : permissionNames) {
Permission permission = accessSection.get().getPermission(permissionName);
assertPermission(permission, permissionName, exclusive, null);
assertPermissionRule(
permission.getRule(groupReference), groupReference, Action.ALLOW, false, 0, 0);
}
}
protected void assertPermission(
Permission permission,
String expectedName,
boolean expectedExclusive,
@Nullable String expectedLabelName) {
assertThat(permission).isNotNull();
assertThat(permission.getName()).isEqualTo(expectedName);
assertThat(permission.getExclusiveGroup()).isEqualTo(expectedExclusive);
assertThat(permission.getLabel()).isEqualTo(expectedLabelName);
}
protected void assertPermissionRule(
PermissionRule rule,
GroupReference expectedGroupReference,
Action expectedAction,
boolean expectedForce,
int expectedMin,
int expectedMax) {
assertThat(rule.getGroup()).isEqualTo(expectedGroupReference);
assertThat(rule.getAction()).isEqualTo(expectedAction);
assertThat(rule.getForce()).isEqualTo(expectedForce);
assertThat(rule.getMin()).isEqualTo(expectedMin);
assertThat(rule.getMax()).isEqualTo(expectedMax);
}
protected void assertHead(String projectName, String expectedRef) throws Exception {
// Assert gerrit's project head points to the correct branch
assertThat(getProjectBranches(projectName).get(Constants.HEAD).revision)
.isEqualTo(RefNames.shortName(expectedRef));
// Assert git head points to the correct branch
try (Repository repo = repoManager.openRepository(Project.nameKey(projectName))) {
assertThat(repo.exactRef(Constants.HEAD).getTarget().getName()).isEqualTo(expectedRef);
}
}
protected InternalGroup group(AccountGroup.UUID groupUuid) {
InternalGroup group = groupCache.get(groupUuid).orElse(null);
assertWithMessage(groupUuid.get()).that(group).isNotNull();
return group;
}
protected GroupReference groupRef(AccountGroup.UUID groupUuid) {
GroupDescription.Basic groupDescription = groupBackend.get(groupUuid);
return GroupReference.create(groupDescription.getGroupUUID(), groupDescription.getName());
}
protected InternalGroup group(String groupName) {
InternalGroup group = groupCache.get(AccountGroup.nameKey(groupName)).orElse(null);
assertWithMessage(groupName).that(group).isNotNull();
return group;
}
protected GroupReference groupRef(String groupName) {
InternalGroup group = groupCache.get(AccountGroup.nameKey(groupName)).orElse(null);
assertThat(group).isNotNull();
return GroupReference.create(group.getGroupUUID(), group.getName());
}
protected AccountGroup.UUID groupUuid(String groupName) {
return group(groupName).getGroupUUID();
}
protected InternalGroup adminGroup() {
return group("Administrators");
}
protected GroupReference adminGroupRef() {
return groupRef("Administrators");
}
protected AccountGroup.UUID adminGroupUuid() {
return groupUuid("Administrators");
}
protected void assertGroupDoesNotExist(String groupName) {
InternalGroup group = groupCache.get(AccountGroup.nameKey(groupName)).orElse(null);
assertWithMessage(groupName).that(group).isNull();
}
protected void assertNotifyTo(TestAccount expected) {
assertNotifyTo(expected.email(), expected.fullName());
}
protected void assertNotifyTo(String expectedEmail, String expectedFullname) {
Address expectedAddress = Address.create(expectedFullname, expectedEmail);
assertThat(sender.getMessages()).hasSize(1);
Message m = sender.getMessages().get(0);
assertThat(m.rcpt()).containsExactly(expectedAddress);
assertThat(((EmailHeader.AddressList) m.headers().get("To")).getAddressList())
.containsExactly(expectedAddress);
assertThat(m.headers().get("Cc").isEmpty()).isTrue();
}
protected void assertNotifyCc(TestAccount expected) {
assertNotifyCc(expected.getNameEmail());
}
protected void assertNotifyCc(String expectedEmail, String expectedFullname) {
Address expectedAddress = Address.create(expectedFullname, expectedEmail);
assertNotifyCc(expectedAddress);
}
protected void assertNotifyCc(Address expectedAddress) {
assertThat(sender.getMessages()).hasSize(1);
Message m = sender.getMessages().get(0);
assertThat(m.rcpt()).containsExactly(expectedAddress);
assertThat(m.headers().get("To").isEmpty()).isTrue();
assertThat(((EmailHeader.AddressList) m.headers().get("Cc")).getAddressList())
.containsExactly(expectedAddress);
}
protected void assertNotifyBcc(TestAccount expected) {
assertThat(sender.getMessages()).hasSize(1);
Message m = sender.getMessages().get(0);
assertThat(m.rcpt()).containsExactly(expected.getNameEmail());
assertThat(m.headers().get("To").isEmpty()).isTrue();
assertThat(m.headers().get("Cc").isEmpty()).isTrue();
}
protected void assertNotifyBcc(String expectedEmail, String expectedFullName) {
assertThat(sender.getMessages()).hasSize(1);
Message m = sender.getMessages().get(0);
assertThat(m.rcpt()).containsExactly(Address.create(expectedFullName, expectedEmail));
assertThat(m.headers().get("To").isEmpty()).isTrue();
assertThat(m.headers().get("Cc").isEmpty()).isTrue();
}
protected interface ProjectWatchInfoConfiguration {
void configure(ProjectWatchInfo pwi);
}
protected void watch(String project, ProjectWatchInfoConfiguration config)
throws RestApiException {
ProjectWatchInfo pwi = new ProjectWatchInfo();
pwi.project = project;
config.configure(pwi);
gApi.accounts().self().setWatchedProjects(ImmutableList.of(pwi));
}
protected void watch(PushOneCommit.Result r, ProjectWatchInfoConfiguration config)
throws RestApiException {
watch(r.getChange().project().get(), config);
}
protected void watch(String project, String filter) throws RestApiException {
watch(
project,
pwi -> {
pwi.filter = filter;
pwi.notifyAbandonedChanges = true;
pwi.notifyNewChanges = true;
pwi.notifyNewPatchSets = true;
pwi.notifyAllComments = true;
});
}
protected void watch(String project) throws RestApiException {
watch(project, (String) null);
}
protected void assertContent(PushOneCommit.Result pushResult, String path, String expectedContent)
throws Exception {
BinaryResult bin =
gApi.changes()
.id(pushResult.getChangeId())
.revision(pushResult.getCommit().name())
.file(path)
.content();
ByteArrayOutputStream os = new ByteArrayOutputStream();
bin.writeTo(os);
String res = new String(os.toByteArray(), UTF_8);
assertThat(res).isEqualTo(expectedContent);
}
protected RevCommit createNewCommitWithoutChangeId(String branch, String file, String content)
throws Exception {
try (Repository repo = repoManager.openRepository(project);
RevWalk walk = new RevWalk(repo)) {
Ref ref = repo.exactRef(branch);
RevCommit tip = null;
if (ref != null) {
tip = walk.parseCommit(ref.getObjectId());
}
TestRepository<?> testSrcRepo = new TestRepository<>(repo);
TestRepository<?>.BranchBuilder builder = testSrcRepo.branch(branch);
RevCommit revCommit =
tip == null
? builder.commit().message("commit 1").add(file, content).create()
: builder.commit().parent(tip).message("commit 1").add(file, content).create();
assertThat(GitUtil.getChangeId(testSrcRepo, revCommit)).isEmpty();
return revCommit;
}
}
protected RevCommit parseCurrentRevision(RevWalk rw, String changeId) throws Exception {
return rw.parseCommit(
ObjectId.fromString(get(changeId, ListChangesOption.CURRENT_REVISION).currentRevision));
}
protected void configSubmitRequirement(
Project.NameKey project, SubmitRequirement submitRequirement) throws Exception {
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig().upsertSubmitRequirement(submitRequirement);
u.save();
}
}
protected void clearSubmitRequirements(Project.NameKey project) throws Exception {
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig().clearSubmitRequirements();
u.save();
}
}
protected void configLabel(String label, LabelFunction func) throws Exception {
configLabel(label, func, ImmutableList.of());
}
protected void configLabel(String label, LabelFunction func, List<String> refPatterns)
throws Exception {
configLabel(
project,
label,
func,
refPatterns,
value(1, "Passes"),
value(0, "No score"),
value(-1, "Failed"));
}
protected void configLabel(
Project.NameKey project, String label, LabelFunction func, LabelValue... value)
throws Exception {
configLabel(project, label, func, ImmutableList.of(), value);
}
private void configLabel(
Project.NameKey project,
String label,
LabelFunction func,
List<String> refPatterns,
LabelValue... value)
throws Exception {
try (ProjectConfigUpdate u = updateProject(project)) {
LabelType.Builder labelType = label(label, value).toBuilder();
labelType.setFunction(func);
labelType.setRefPatterns(ImmutableList.copyOf(refPatterns));
u.getConfig().upsertLabelType(labelType.build());
u.save();
}
}
protected void enableCreateNewChangeForAllNotInTarget() throws Exception {
try (ProjectConfigUpdate u = updateProject(project)) {
u.getConfig()
.updateProject(
p ->
p.setBooleanConfig(
BooleanProjectConfig.CREATE_NEW_CHANGE_FOR_ALL_NOT_IN_TARGET,
InheritableBoolean.TRUE));
u.save();
}
}
protected ProjectConfigUpdate updateProject(Project.NameKey projectName) throws Exception {
return new ProjectConfigUpdate(projectName);
}
protected class ProjectConfigUpdate implements AutoCloseable {
private final ProjectConfig projectConfig;
private MetaDataUpdate metaDataUpdate;
private ProjectConfigUpdate(Project.NameKey projectName) throws Exception {
metaDataUpdate = metaDataUpdateFactory.create(projectName);
projectConfig = projectConfigFactory.read(metaDataUpdate);
}
public ProjectConfig getConfig() {
return projectConfig;
}
public void save() throws Exception {
testRefAction(
() -> {
metaDataUpdate.setAuthor(identifiedUserFactory.create(admin.id()));
projectConfig.commit(metaDataUpdate);
metaDataUpdate.close();
metaDataUpdate = null;
projectCache.evictAndReindex(projectConfig.getProject());
});
}
@Override
public void close() {
if (metaDataUpdate != null) {
metaDataUpdate.close();
}
}
}
protected List<RevCommit> getChangeMetaCommitsInReverseOrder(Change.Id changeId)
throws IOException {
try (Repository repo = repoManager.openRepository(project);
RevWalk revWalk = new RevWalk(repo)) {
revWalk.sort(RevSort.TOPO);
revWalk.sort(RevSort.REVERSE);
Ref metaRef = repo.exactRef(RefNames.changeMetaRef(changeId));
revWalk.markStart(revWalk.parseCommit(metaRef.getObjectId()));
return Lists.newArrayList(revWalk);
}
}
protected List<CommentInfo> getChangeSortedComments(int changeNum) throws Exception {
List<CommentInfo> comments = new ArrayList<>();
Map<String, List<CommentInfo>> commentsMap =
gApi.changes().id(changeNum).commentsRequest().get();
for (Map.Entry<String, List<CommentInfo>> e : commentsMap.entrySet()) {
for (CommentInfo c : e.getValue()) {
c.path = e.getKey(); // Set the comment's path field.
comments.add(c);
}
}
comments.sort(Comparator.comparing(c -> c.id));
return comments;
}
protected ImmutableMap<String, BranchInfo> getProjectBranches(String projectName)
throws RestApiException {
return gApi.projects().name(projectName).branches().get().stream()
.collect(ImmutableMap.toImmutableMap(branch -> branch.ref, branch -> branch));
}
protected void createRefLogFileIfMissing(Repository repo, String ref) throws IOException {
File log = new File(repo.getDirectory(), "logs/" + ref);
if (!log.exists()) {
log.getParentFile().mkdirs();
assertThat(log.createNewFile()).isTrue();
}
}
protected AutoCloseable installPlugin(String pluginName, Class<? extends Module> sysModuleClass)
throws Exception {
return installPlugin(pluginName, sysModuleClass, null, null);
}
protected AutoCloseable installPlugin(
String pluginName,
@Nullable Class<? extends Module> sysModuleClass,
@Nullable Class<? extends Module> httpModuleClass,
@Nullable Class<? extends Module> sshModuleClass)
throws Exception {
checkStatic(sysModuleClass);
checkStatic(httpModuleClass);
checkStatic(sshModuleClass);
TestServerPlugin plugin =
new TestServerPlugin(
pluginName,
"http://example.com/" + pluginName,
pluginUserFactory.create(pluginName),
getClass().getClassLoader(),
sysModuleClass != null ? sysModuleClass.getName() : null,
httpModuleClass != null ? httpModuleClass.getName() : null,
sshModuleClass != null ? sshModuleClass.getName() : null,
sitePaths.data_dir.resolve(pluginName));
plugin.start(pluginGuiceEnvironment);
pluginGuiceEnvironment.onStartPlugin(plugin);
return () -> {
plugin.stop(pluginGuiceEnvironment);
pluginGuiceEnvironment.onStopPlugin(plugin);
};
}
private static void checkStatic(@Nullable Class<? extends Module> moduleClass) {
if (moduleClass != null) {
checkArgument(
(moduleClass.getModifiers() & Modifier.STATIC) != 0,
"module must be static: %s",
moduleClass.getName());
}
}
/** {@link Ticker} implementation for mocking without restarting GerritServer */
public static class TestTicker extends Ticker {
Ticker actualTicker;
public TestTicker() {
useDefaultTicker();
}
/** Switches to system ticker */
public Ticker useDefaultTicker() {
this.actualTicker = Ticker.systemTicker();
return actualTicker;
}
/** Switches to {@link FakeTicker} */
public FakeTicker useFakeTicker() {
if (!(this.actualTicker instanceof FakeTicker)) {
this.actualTicker = new FakeTicker();
}
return (FakeTicker) actualTicker;
}
@Override
public long read() {
return actualTicker.read();
}
}
}