Add test API to create/retrieve changes

This API allows to create and retrieve changes in a fluent manner with
minimal effort. Tests only need to specify the parameters which are
essential for them. They also don't need to care about permissions or
the current user context.

Examples:
- changeOperations.newChange().create()
- changeOperations.newChange().branch("test-branch").create()
- changeOperations.newChange().file("foo").content("bar").create()
- changeOperations.change(changeId).get()

Besides being much more convenient and readable than the existing
mechanisms to create test changes, this API tries to avoid
confusing/unintended behaviors. For instance, the traditional mechanism
for creating test changes involves the creation of a commit and its push
to the server. Subsequent change creations automatically create a chain
of changes if the repository is not explicitly reset. That behavior is
not obvious to callers in tests and can lead to frustrating/confusing
moments when writing tests. The new test API creates independent changes
on each call. Support for chains of changes is not implemented yet but
it would simply be a matter of passing the ID of a parent change/commit
along.

This API follows the same structure as the test APIs of groups and
accounts. The implementation is more complicated, though, as we
currently don't have clean internal APIs for change creation as we do
for groups and accounts. In addition, changes have some mandatory values
(project and change owner) which cannot be easily substituted with
arbitrary, hard-coded values. For those situations, we decided to look
for existing projects and users and to arbitrarily select one of them.
Of course, we could have created a new project/user instead but we
thought it's better not to introduce such a behavior as a side-effect to
a call for change creation. Overall, we tried to hide most of the
complexity behind the test API surface so that usages in tests can be as
simple as "changeOperations.newChange().create()".

Change retrieval currently supports only the barest of attributes.
Change creation is already more sophisticated and features quite some
input parameters including the handy "file("myFilePath").content
("myFileContent")" notation. Now that the base implementation is laid
out, it should be easy to add additional attributes when necessary.

Change-Id: I10b7455990a8439cf255853dae66b8f2da9085ab
This commit is contained in:
Alice Kober-Sotzek
2020-07-30 17:50:13 +02:00
parent d18582477e
commit 3871ea3513
9 changed files with 876 additions and 0 deletions

View File

@@ -30,6 +30,8 @@ import com.google.gerrit.acceptance.config.GlobalPluginConfig;
import com.google.gerrit.acceptance.config.GlobalPluginConfigs;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.account.AccountOperationsImpl;
import com.google.gerrit.acceptance.testsuite.change.ChangeOperations;
import com.google.gerrit.acceptance.testsuite.change.ChangeOperationsImpl;
import com.google.gerrit.acceptance.testsuite.group.GroupOperations;
import com.google.gerrit.acceptance.testsuite.group.GroupOperationsImpl;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
@@ -506,6 +508,7 @@ public class GerritServer implements AutoCloseable {
bind(GroupOperations.class).to(GroupOperationsImpl.class);
bind(ProjectOperations.class).to(ProjectOperationsImpl.class);
bind(RequestScopeOperations.class).to(RequestScopeOperationsImpl.class);
bind(ChangeOperations.class).to(ChangeOperationsImpl.class);
factory(PushOneCommit.Factory.class);
install(InProcessProtocol.module());
install(new NoSshModule());

View File

@@ -0,0 +1,79 @@
// Copyright (C) 2020 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.testsuite.change;
import com.google.gerrit.entities.Change;
/**
* An aggregation of operations on changes for test purposes.
*
* <p>To execute the operations, no Gerrit permissions are necessary.
*
* <p><strong>Note:</strong> This interface is not implemented using the REST or extension API.
* Hence, it cannot be used for testing those APIs.
*/
public interface ChangeOperations {
/**
* Starts the fluent chain for querying or modifying a change. Please see the methods of {@link
* PerChangeOperations} for details on possible operations.
*
* @return an aggregation of operations on a specific change
*/
PerChangeOperations change(Change.Id changeId);
/**
* Starts the fluent chain to create a change. The returned builder can be used to specify the
* attributes of the new change. To create the change for real, {@link
* TestChangeCreation.Builder#create()} must be called.
*
* <p>Example:
*
* <pre>
* Change.Id createdChangeId = changeOperations
* .newChange()
* .file("file1")
* .content("Line 1\nLine2\n")
* .create();
* </pre>
*
* <p><strong>Note:</strong> There must be at least one existing user and repository.
*
* @return a builder to create the new change
*/
TestChangeCreation.Builder newChange();
/** An aggregation of methods on a specific change. */
interface PerChangeOperations {
/**
* Checks whether the change exists.
*
* @return {@code true} if the change exists
*/
boolean exists();
/**
* Retrieves the change.
*
* <p><strong>Note:</strong> This call will fail with an exception if the requested change
* doesn't exist. If you want to check for the existence of a change, use {@link #exists()}
* instead.
*
* @return the corresponding {@code TestChange}
*/
TestChange get();
}
}

View File

@@ -0,0 +1,289 @@
// Copyright (C) 2020 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.testsuite.change;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.change.ChangeFinder;
import com.google.gerrit.server.change.ChangeInserter;
import com.google.gerrit.server.edit.tree.TreeCreator;
import com.google.gerrit.server.edit.tree.TreeModification;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.notedb.Sequences;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.util.CommitMessageUtil;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Inject;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.Objects;
import java.util.Optional;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
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.RevWalk;
import org.eclipse.jgit.util.ChangeIdUtil;
/**
* The implementation of {@link ChangeOperations}.
*
* <p>There is only one implementation of {@link ChangeOperations}. Nevertheless, we keep the
* separation between interface and implementation to enhance clarity.
*/
public class ChangeOperationsImpl implements ChangeOperations {
private final Sequences seq;
private final ChangeInserter.Factory changeInserterFactory;
private final GitRepositoryManager repositoryManager;
private final AccountResolver resolver;
private final IdentifiedUser.GenericFactory userFactory;
private final PersonIdent serverIdent;
private final BatchUpdate.Factory batchUpdateFactory;
private final ProjectCache projectCache;
private final ChangeFinder changeFinder;
@Inject
public ChangeOperationsImpl(
Sequences seq,
ChangeInserter.Factory changeInserterFactory,
GitRepositoryManager repositoryManager,
AccountResolver resolver,
IdentifiedUser.GenericFactory userFactory,
@GerritPersonIdent PersonIdent serverIdent,
BatchUpdate.Factory batchUpdateFactory,
ProjectCache projectCache,
ChangeFinder changeFinder) {
this.seq = seq;
this.changeInserterFactory = changeInserterFactory;
this.repositoryManager = repositoryManager;
this.resolver = resolver;
this.userFactory = userFactory;
this.serverIdent = serverIdent;
this.batchUpdateFactory = batchUpdateFactory;
this.projectCache = projectCache;
this.changeFinder = changeFinder;
}
@Override
public PerChangeOperations change(Change.Id changeId) {
return new PerChangeOperationsImpl(changeId);
}
@Override
public TestChangeCreation.Builder newChange() {
return TestChangeCreation.builder(this::createChange);
}
private Change.Id createChange(TestChangeCreation changeCreation) throws Exception {
Change.Id changeId = Change.id(seq.nextChangeId());
Project.NameKey project = getTargetProject(changeCreation);
try (Repository repository = repositoryManager.openRepository(project);
ObjectInserter objectInserter = repository.newObjectInserter();
RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
Timestamp now = TimeUtil.nowTs();
IdentifiedUser changeOwner = getChangeOwner(changeCreation);
PersonIdent authorAndCommitter =
changeOwner.newCommitterIdent(now, serverIdent.getTimeZone());
ObjectId commitId =
createCommit(repository, revWalk, objectInserter, changeCreation, authorAndCommitter);
String refName = RefNames.fullName(changeCreation.branch());
ChangeInserter inserter = getChangeInserter(changeId, refName, commitId);
try (BatchUpdate batchUpdate = batchUpdateFactory.create(project, changeOwner, now)) {
batchUpdate.setRepository(repository, revWalk, objectInserter);
batchUpdate.insertChange(inserter);
batchUpdate.execute();
}
return changeId;
}
}
private Project.NameKey getTargetProject(TestChangeCreation changeCreation) {
if (changeCreation.project().isPresent()) {
return changeCreation.project().get();
}
return getArbitraryProject();
}
private Project.NameKey getArbitraryProject() {
Project.NameKey allProjectsName = projectCache.getAllProjects().getNameKey();
Project.NameKey allUsersName = projectCache.getAllUsers().getNameKey();
Optional<Project.NameKey> arbitraryProject =
projectCache.all().stream()
.filter(
name ->
!Objects.equals(name, allProjectsName) && !Objects.equals(name, allUsersName))
.findFirst();
checkState(
arbitraryProject.isPresent(),
"At least one repository must be available on the Gerrit server");
return arbitraryProject.get();
}
private IdentifiedUser getChangeOwner(TestChangeCreation changeCreation)
throws IOException, ConfigInvalidException {
if (changeCreation.owner().isPresent()) {
return userFactory.create(changeCreation.owner().get());
}
return getArbitraryUser();
}
private IdentifiedUser getArbitraryUser() throws ConfigInvalidException, IOException {
ImmutableSet<Account.Id> foundAccounts = resolver.resolveIgnoreVisibility("").asIdSet();
checkState(
!foundAccounts.isEmpty(),
"At least one user account must be available on the Gerrit server");
return userFactory.create(foundAccounts.iterator().next());
}
private ObjectId createCommit(
Repository repository,
RevWalk revWalk,
ObjectInserter objectInserter,
TestChangeCreation changeCreation,
PersonIdent authorAndCommitter)
throws IOException, BadRequestException {
Optional<ObjectId> branchTip = getTip(repository, changeCreation.branch());
ObjectId tree =
createNewTree(
repository,
revWalk,
branchTip.orElse(ObjectId.zeroId()),
changeCreation.treeModifications());
String commitMessage = correctCommitMessage(changeCreation.commitMessage());
ImmutableList<ObjectId> parentCommitIds = Streams.stream(branchTip).collect(toImmutableList());
return createCommit(
objectInserter,
tree,
parentCommitIds,
authorAndCommitter,
authorAndCommitter,
commitMessage);
}
private Optional<ObjectId> getTip(Repository repository, String branch) throws IOException {
Optional<Ref> ref = Optional.ofNullable(repository.findRef(branch));
return ref.map(Ref::getObjectId);
}
private static ObjectId createNewTree(
Repository repository,
RevWalk revWalk,
ObjectId baseCommitId,
ImmutableList<TreeModification> treeModifications)
throws IOException {
TreeCreator treeCreator = getTreeCreator(revWalk, baseCommitId);
treeCreator.addTreeModifications(treeModifications);
return treeCreator.createNewTreeAndGetId(repository);
}
private static TreeCreator getTreeCreator(RevWalk revWalk, ObjectId baseCommitId)
throws IOException {
if (ObjectId.zeroId().equals(baseCommitId)) {
return TreeCreator.basedOnEmptyTree();
}
RevCommit baseCommit = revWalk.parseCommit(baseCommitId);
return TreeCreator.basedOn(baseCommit);
}
private String correctCommitMessage(String desiredCommitMessage) throws BadRequestException {
String commitMessage = CommitMessageUtil.checkAndSanitizeCommitMessage(desiredCommitMessage);
if (ChangeIdUtil.indexOfChangeId(commitMessage, "\n") == -1) {
ObjectId id = CommitMessageUtil.generateChangeId();
commitMessage = ChangeIdUtil.insertId(commitMessage, id);
}
return commitMessage;
}
private ObjectId createCommit(
ObjectInserter objectInserter,
ObjectId tree,
ImmutableList<ObjectId> parentCommitIds,
PersonIdent author,
PersonIdent committer,
String commitMessage)
throws IOException {
CommitBuilder builder = new CommitBuilder();
builder.setTreeId(tree);
builder.setParentIds(parentCommitIds);
builder.setAuthor(author);
builder.setCommitter(committer);
builder.setMessage(commitMessage);
ObjectId newCommitId = objectInserter.insert(builder);
objectInserter.flush();
return newCommitId;
}
private ChangeInserter getChangeInserter(Change.Id changeId, String refName, ObjectId commitId) {
ChangeInserter inserter = changeInserterFactory.create(changeId, commitId, refName);
inserter.setMessage(String.format("Uploaded patchset %d.", inserter.getPatchSetId().get()));
return inserter;
}
private class PerChangeOperationsImpl implements PerChangeOperations {
private final Change.Id changeId;
public PerChangeOperationsImpl(Change.Id changeId) {
this.changeId = changeId;
}
@Override
public boolean exists() {
return changeFinder.findOne(changeId).isPresent();
}
@Override
public TestChange get() {
Optional<ChangeNotes> changeNotes = changeFinder.findOne(changeId);
checkState(changeNotes.isPresent(), "Tried to get non-existing test change.");
return toTestChange(changeNotes.get().getChange());
}
private TestChange toTestChange(Change change) {
return TestChange.builder()
.numericChangeId(change.getId())
.changeId(change.getKey().get())
.build();
}
}
}

View File

@@ -0,0 +1,48 @@
// Copyright (C) 2020 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.testsuite.change;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.Strings;
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.server.edit.tree.ChangeFileContentModification;
import com.google.gerrit.server.edit.tree.TreeModification;
import java.util.function.Consumer;
/** Builder to simplify file content specification. */
public class FileContentBuilder<T> {
private final T builder;
private final String filePath;
private final Consumer<TreeModification> modificationToBuilderAdder;
FileContentBuilder(
T builder, String filePath, Consumer<TreeModification> modificationToBuilderAdder) {
checkNotNull(Strings.emptyToNull(filePath), "File path must not be null or empty.");
this.builder = builder;
this.filePath = filePath;
this.modificationToBuilderAdder = modificationToBuilderAdder;
}
/** Content of the file. Must not be empty. */
public T content(String content) {
checkNotNull(
Strings.emptyToNull(content),
"Empty file content is not supported. Adjust test API if necessary.");
modificationToBuilderAdder.accept(
new ChangeFileContentModification(filePath, RawInputUtil.create(content)));
return builder;
}
}

View File

@@ -0,0 +1,48 @@
// Copyright (C) 2020 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.testsuite.change;
import com.google.auto.value.AutoValue;
import com.google.gerrit.entities.Change;
/** Representation of a change used for testing purposes. */
@AutoValue
public abstract class TestChange {
/**
* The numeric change ID, sometimes also called change number or legacy change ID. Unique per
* host.
*/
public abstract Change.Id numericChangeId();
/**
* The Change-Id as specified in the commit message. Consists of an {@code I} followed by a 40-hex
* string. Only unique per project-branch.
*/
public abstract String changeId();
static Builder builder() {
return new AutoValue_TestChange.Builder();
}
@AutoValue.Builder
abstract static class Builder {
abstract Builder numericChangeId(Change.Id numericChangeId);
abstract Builder changeId(String changeId);
abstract TestChange build();
}
}

View File

@@ -0,0 +1,89 @@
// Copyright (C) 2020 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.testsuite.change;
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.testsuite.ThrowingFunction;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.edit.tree.TreeModification;
import java.util.Optional;
import org.eclipse.jgit.lib.Constants;
/** Initial attributes of the change. If not provided, arbitrary values will be used. */
@AutoValue
public abstract class TestChangeCreation {
public abstract Optional<Project.NameKey> project();
public abstract String branch();
public abstract Optional<Account.Id> owner();
public abstract String commitMessage();
public abstract ImmutableList<TreeModification> treeModifications();
abstract ThrowingFunction<TestChangeCreation, Change.Id> changeCreator();
public static Builder builder(ThrowingFunction<TestChangeCreation, Change.Id> changeCreator) {
return new AutoValue_TestChangeCreation.Builder()
.changeCreator(changeCreator)
.branch(Constants.R_HEADS + Constants.MASTER)
.commitMessage("A test change");
}
@AutoValue.Builder
public abstract static class Builder {
/** Target project/Repository of the change. Must be an existing project. */
public abstract Builder project(Project.NameKey project);
/**
* Target branch of the change. Neither needs to exist nor needs to point to an actual commit.
*/
public abstract Builder branch(String branch);
/** The change owner. Must be an existing user account. */
public abstract Builder owner(Account.Id owner);
/**
* The commit message. The message may contain a {@code Change-Id} footer but does not need to.
* If the footer is absent, it will be generated.
*/
public abstract Builder commitMessage(String commitMessage);
/** Modified file of the change. The file content is specified via the returned builder. */
public FileContentBuilder<Builder> file(String filePath) {
return new FileContentBuilder<>(this, filePath, treeModificationsBuilder()::add);
}
abstract ImmutableList.Builder<TreeModification> treeModificationsBuilder();
abstract Builder changeCreator(ThrowingFunction<TestChangeCreation, Change.Id> changeCreator);
abstract TestChangeCreation autoBuild();
/**
* Creates the change.
*
* @return the {@code Change.Id} of the created change
*/
public Change.Id create() {
TestChangeCreation changeUpdate = autoBuild();
return changeUpdate.changeCreator().applyAndThrowSilently(changeUpdate);
}
}
}

View File

@@ -18,11 +18,13 @@ import static com.google.common.truth.Truth.assertAbout;
import static com.google.gerrit.extensions.common.testing.GitPersonSubject.gitPersons;
import static com.google.gerrit.truth.ListSubject.elements;
import com.google.common.truth.Correspondence;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.StringSubject;
import com.google.common.truth.Subject;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.truth.ListSubject;
import com.google.gerrit.truth.NullAwareCorrespondence;
public class CommitInfoSubject extends Subject {
@@ -65,4 +67,8 @@ public class CommitInfoSubject extends Subject {
isNotNull();
return check("message").that(commitInfo.message);
}
public static Correspondence<CommitInfo, String> hasCommit() {
return NullAwareCorrespondence.transforming(commitInfo -> commitInfo.commit, "hasCommit");
}
}

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.truth;
import static com.google.common.truth.Truth.assertAbout;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.IntegerSubject;
import com.google.common.truth.IterableSubject;
import com.google.common.truth.Subject;
import java.util.Map;
@@ -51,4 +52,9 @@ public class MapSubject extends com.google.common.truth.MapSubject {
isNotNull();
return check("values()").that(map.values());
}
public IntegerSubject size() {
isNotNull();
return check("size()").that(map.size());
}
}

View File

@@ -0,0 +1,308 @@
// Copyright (C) 2020 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.testsuite.change;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.permissionKey;
import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.assertThat;
import static com.google.gerrit.extensions.common.testing.CommitInfoSubject.hasCommit;
import static com.google.gerrit.extensions.restapi.testing.BinaryResultSubject.assertThat;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static com.google.gerrit.truth.MapSubject.assertThatMap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.testsuite.account.AccountOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.FileInfo;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.inject.Inject;
import java.util.Map;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Test;
public class ChangeOperationsImplTest extends AbstractDaemonTest {
@Inject private ChangeOperations changeOperations;
@Inject private ProjectOperations projectOperations;
@Inject private AccountOperations accountOperations;
@Inject private RequestScopeOperations requestScopeOperations;
@Test
public void changeCanBeCreatedWithoutSpecifyingAnyParameters() throws Exception {
Change.Id numericChangeId = changeOperations.newChange().create();
ChangeInfo change = getChangeFromServer(numericChangeId);
assertThat(change._number).isEqualTo(numericChangeId.get());
assertThat(change.changeId).isNotEmpty();
}
@Test
public void changeCanBeCreatedEvenWithRequestScopeOfArbitraryUser() throws Exception {
Account.Id user = accountOperations.newAccount().create();
requestScopeOperations.setApiUser(user);
Change.Id numericChangeId = changeOperations.newChange().create();
ChangeInfo change = getChangeFromServer(numericChangeId);
assertThat(change._number).isEqualTo(numericChangeId.get());
}
@Test
public void twoChangesWithoutAnyParametersDoNotClash() {
Change.Id changeId1 = changeOperations.newChange().create();
Change.Id changeId2 = changeOperations.newChange().create();
TestChange change1 = changeOperations.change(changeId1).get();
TestChange change2 = changeOperations.change(changeId2).get();
assertThat(change1.numericChangeId()).isNotEqualTo(change2.numericChangeId());
assertThat(change1.changeId()).isNotEqualTo(change2.changeId());
}
@Test
public void twoSubsequentlyCreatedChangesDoNotDependOnEachOther() throws Exception {
Change.Id changeId1 = changeOperations.newChange().create();
Change.Id changeId2 = changeOperations.newChange().create();
ChangeInfo change1 = getChangeFromServer(changeId1);
ChangeInfo change2 = getChangeFromServer(changeId2);
CommitInfo currentPatchsetCommit1 = change1.revisions.get(change1.currentRevision).commit;
CommitInfo currentPatchsetCommit2 = change2.revisions.get(change2.currentRevision).commit;
assertThat(currentPatchsetCommit1)
.parents()
.comparingElementsUsing(hasCommit())
.doesNotContain(currentPatchsetCommit2.commit);
assertThat(currentPatchsetCommit2)
.parents()
.comparingElementsUsing(hasCommit())
.doesNotContain(currentPatchsetCommit1.commit);
}
@Test
public void createdChangeHasAtLeastOnePatchset() throws Exception {
Change.Id changeId = changeOperations.newChange().create();
ChangeInfo change = getChangeFromServer(changeId);
assertThatMap(change.revisions).size().isAtLeast(1);
}
@Test
public void createdChangeIsInSpecifiedProject() throws Exception {
Project.NameKey project = projectOperations.newProject().create();
Change.Id changeId = changeOperations.newChange().project(project).create();
ChangeInfo change = getChangeFromServer(changeId);
assertThat(change.project).isEqualTo(project.get());
}
@Test
public void changeCanBeCreatedInEmptyRepository() throws Exception {
Project.NameKey project = projectOperations.newProject().noEmptyCommit().create();
Change.Id changeId = changeOperations.newChange().project(project).create();
ChangeInfo change = getChangeFromServer(changeId);
assertThat(change.project).isEqualTo(project.get());
}
@Test
public void createdChangeHasSpecifiedTargetBranch() throws Exception {
Project.NameKey project = projectOperations.newProject().branches("test-branch").create();
Change.Id changeId =
changeOperations.newChange().project(project).branch("test-branch").create();
ChangeInfo change = getChangeFromServer(changeId);
assertThat(change.branch).isEqualTo("test-branch");
}
@Test
public void createdChangeUsesTipOfTargetBranchAsParentByDefault() throws Exception {
Project.NameKey project = projectOperations.newProject().branches("test-branch").create();
ObjectId parentCommitId = projectOperations.project(project).getHead("test-branch").getId();
Change.Id changeId =
changeOperations.newChange().project(project).branch("test-branch").create();
ChangeInfo change = getChangeFromServer(changeId);
CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
assertThat(currentPatchsetCommit)
.parents()
.onlyElement()
.commit()
.isEqualTo(parentCommitId.getName());
}
@Test
public void createdChangeHasSpecifiedOwner() throws Exception {
Account.Id changeOwner = accountOperations.newAccount().create();
Change.Id changeId = changeOperations.newChange().owner(changeOwner).create();
ChangeInfo change = getChangeFromServer(changeId);
assertThat(change.owner._accountId).isEqualTo(changeOwner.get());
}
@Test
public void changeOwnerDoesNotNeedAnyPermissionsForChangeCreation() throws Exception {
Account.Id changeOwner = accountOperations.newAccount().create();
Project.NameKey project = projectOperations.newProject().branches("test-branch").create();
// Remove any read and push permissions which might potentially exist. Without read, users
// shouldn't be able to do anything. The newly created project should only inherit from
// All-Projects.
projectOperations
.project(project)
.forUpdate()
.remove(permissionKey(Permission.READ).ref("refs/heads/test-branch"))
.remove(permissionKey(Permission.PUSH).ref("refs/heads/test-branch"))
.update();
projectOperations
.allProjectsForUpdate()
.remove(permissionKey(Permission.READ).ref("refs/heads/test-branch"))
.remove(permissionKey(Permission.PUSH).ref("refs/heads/test-branch"))
.update();
Change.Id changeId =
changeOperations
.newChange()
.owner(changeOwner)
.branch("test-branch")
.project(project)
.create();
ChangeInfo change = getChangeFromServer(changeId);
assertThat(change.owner._accountId).isEqualTo(changeOwner.get());
}
@Test
public void createdChangeHasSpecifiedCommitMessage() throws Exception {
Change.Id changeId =
changeOperations
.newChange()
.commitMessage("Summary line\n\nDetailed description.")
.create();
ChangeInfo change = getChangeFromServer(changeId);
CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
assertThat(currentPatchsetCommit).message().startsWith("Summary line\n\nDetailed description.");
}
@Test
public void changeCannotBeCreatedWithoutCommitMessage() {
assertThrows(
IllegalStateException.class, () -> changeOperations.newChange().commitMessage("").create());
}
@Test
public void commitMessageOfCreatedChangeAutomaticallyGetsChangeId() throws Exception {
Change.Id changeId =
changeOperations
.newChange()
.commitMessage("Summary line\n\nDetailed description.")
.create();
ChangeInfo change = getChangeFromServer(changeId);
CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
assertThat(currentPatchsetCommit).message().contains("Change-Id:");
}
@Test
public void changeIdSpecifiedInCommitMessageIsKeptForCreatedChange() throws Exception {
Change.Id changeId =
changeOperations
.newChange()
.commitMessage("Summary line\n\nChange-Id: I0123456789012345678901234567890123456789")
.create();
ChangeInfo change = getChangeFromServer(changeId);
CommitInfo currentPatchsetCommit = change.revisions.get(change.currentRevision).commit;
assertThat(currentPatchsetCommit)
.message()
.contains("Change-Id: I0123456789012345678901234567890123456789");
assertThat(change.changeId).isEqualTo("I0123456789012345678901234567890123456789");
}
@Test
public void createdChangeHasSpecifiedFiles() throws Exception {
Change.Id changeId =
changeOperations
.newChange()
.file("file1")
.content("Line 1")
.file("path/to/file2.txt")
.content("Line one")
.create();
ChangeInfo change = getChangeFromServer(changeId);
Map<String, FileInfo> files = change.revisions.get(change.currentRevision).files;
assertThatMap(files).keys().containsExactly("file1", "path/to/file2.txt");
BinaryResult fileContent1 = gApi.changes().id(changeId.get()).current().file("file1").content();
assertThat(fileContent1).asString().isEqualTo("Line 1");
BinaryResult fileContent2 =
gApi.changes().id(changeId.get()).current().file("path/to/file2.txt").content();
assertThat(fileContent2).asString().isEqualTo("Line one");
}
@Test
public void existingChangeCanBeCheckedForExistence() {
Change.Id changeId = changeOperations.newChange().create();
boolean exists = changeOperations.change(changeId).exists();
assertThat(exists).isTrue();
}
@Test
public void notExistingChangeCanBeCheckedForExistence() {
Change.Id changeId = Change.id(123456789);
boolean exists = changeOperations.change(changeId).exists();
assertThat(exists).isFalse();
}
@Test
public void retrievingNotExistingChangeFails() {
Change.Id changeId = Change.id(123456789);
assertThrows(IllegalStateException.class, () -> changeOperations.change(changeId).get());
}
@Test
public void numericChangeIdOfExistingChangeCanBeRetrieved() {
Change.Id changeId = changeOperations.newChange().create();
TestChange change = changeOperations.change(changeId).get();
assertThat(change.numericChangeId()).isEqualTo(changeId);
}
@Test
public void changeIdOfExistingChangeCanBeRetrieved() {
Change.Id changeId =
changeOperations
.newChange()
.commitMessage("Summary line\n\nChange-Id: I0123456789012345678901234567890123456789")
.create();
TestChange change = changeOperations.change(changeId).get();
assertThat(change.changeId()).isEqualTo("I0123456789012345678901234567890123456789");
}
private ChangeInfo getChangeFromServer(Change.Id changeId) throws RestApiException {
return gApi.changes().id(changeId.get()).get();
}
}