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:
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user