blob: a65bf95b7dd7194f3c73077041982a25d9b20c01 [file] [log] [blame]
// Copyright (C) 2019 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.plugins.checks.acceptance.testsuite;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.gerrit.testing.TestActionRefUpdateContext.testRefAction;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Streams;
import com.google.gerrit.entities.Project;
import com.google.gerrit.exceptions.DuplicateKeyException;
import com.google.gerrit.plugins.checks.Checker;
import com.google.gerrit.plugins.checks.CheckerCreation;
import com.google.gerrit.plugins.checks.CheckerJson;
import com.google.gerrit.plugins.checks.CheckerRef;
import com.google.gerrit.plugins.checks.CheckerUpdate;
import com.google.gerrit.plugins.checks.CheckerUuid;
import com.google.gerrit.plugins.checks.Checkers;
import com.google.gerrit.plugins.checks.CheckersUpdate;
import com.google.gerrit.plugins.checks.api.CheckerInfo;
import com.google.gerrit.plugins.checks.db.CheckerConfig;
import com.google.gerrit.plugins.checks.db.CheckersByRepositoryNotes;
import com.google.gerrit.server.ServerInitiated;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.IOException;
import java.util.Arrays;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.BlobBasedConfig;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.treewalk.TreeWalk;
/**
* The implementation of {@code CheckerOperations}.
*
* <p>There is only one implementation of {@code CheckerOperations}. Nevertheless, we keep the
* separation between interface and implementation to enhance clarity.
*/
@Singleton
public class CheckerOperationsImpl implements CheckerOperations {
private final Checkers checkers;
private final Provider<CheckersUpdate> checkersUpdate;
private final GitRepositoryManager repoManager;
private final AllProjectsName allProjectsName;
private final CheckerJson checkerJson;
private final AtomicInteger checkerCounter;
@Inject
public CheckerOperationsImpl(
Checkers checkers,
@ServerInitiated Provider<CheckersUpdate> checkersUpdate,
GitRepositoryManager repoManager,
AllProjectsName allProjectsName,
CheckerJson checkerJson) {
this.checkers = checkers;
this.checkersUpdate = checkersUpdate;
this.repoManager = repoManager;
this.allProjectsName = allProjectsName;
this.checkerJson = checkerJson;
this.checkerCounter = new AtomicInteger();
}
@Override
public PerCheckerOperations checker(CheckerUuid checkerUuid) {
return new PerCheckerOperationsImpl(checkerUuid);
}
@Override
public TestCheckerCreation.Builder newChecker() {
return TestCheckerCreation.builder(this::createNewChecker);
}
private CheckerUuid createNewChecker(TestCheckerCreation testCheckerCreation)
throws DuplicateKeyException, ConfigInvalidException, IOException {
CheckerCreation checkerCreation = toCheckerCreation(testCheckerCreation);
CheckerUpdate checkerUpdate = toCheckerUpdate(testCheckerCreation);
Checker checker = checkersUpdate.get().createChecker(checkerCreation, checkerUpdate);
return checker.getUuid();
}
private CheckerCreation toCheckerCreation(TestCheckerCreation checkerCreation) {
CheckerUuid checkerUuid =
checkerCreation
.uuid()
.orElseGet(() -> CheckerUuid.parse("test:checker-" + checkerCounter.incrementAndGet()));
String checkerName = checkerCreation.name().orElse("Test Checker");
Project.NameKey repository = checkerCreation.repository().orElse(allProjectsName);
return CheckerCreation.builder()
.setCheckerUuid(checkerUuid)
.setName(checkerName)
.setRepository(repository)
.build();
}
private static CheckerUpdate toCheckerUpdate(TestCheckerCreation checkerCreation) {
CheckerUpdate.Builder builder = CheckerUpdate.builder();
checkerCreation.name().ifPresent(builder::setName);
checkerCreation.description().ifPresent(builder::setDescription);
checkerCreation.url().ifPresent(builder::setUrl);
checkerCreation.repository().ifPresent(builder::setRepository);
checkerCreation.status().ifPresent(builder::setStatus);
checkerCreation.blockingConditions().ifPresent(builder::setBlockingConditions);
checkerCreation.query().ifPresent(builder::setQuery);
return builder.build();
}
@Override
public ImmutableSet<CheckerUuid> checkersOf(Project.NameKey repositoryName) throws IOException {
try (Repository repo = repoManager.openRepository(allProjectsName);
RevWalk rw = new RevWalk(repo);
ObjectReader or = repo.newObjectReader()) {
Ref ref = repo.exactRef(CheckerRef.REFS_META_CHECKERS);
if (ref == null) {
return ImmutableSet.of();
}
RevCommit c = rw.parseCommit(ref.getObjectId());
try (TreeWalk tw =
TreeWalk.forPath(
or,
CheckersByRepositoryNotes.computeRepositorySha1(repositoryName).getName(),
c.getTree())) {
if (tw == null) {
return ImmutableSet.of();
}
return Streams.stream(
Splitter.on('\n')
.split(new String(or.open(tw.getObjectId(0), OBJ_BLOB).getBytes(), UTF_8)))
.map(CheckerUuid::parse)
.collect(toImmutableSet());
}
}
}
@Override
public ImmutableSet<ObjectId> sha1sOfRepositoriesWithCheckers() throws IOException {
try (Repository repo = repoManager.openRepository(allProjectsName);
RevWalk rw = new RevWalk(repo)) {
Ref ref = repo.exactRef(CheckerRef.REFS_META_CHECKERS);
if (ref == null) {
return ImmutableSet.of();
}
return Streams.stream(NoteMap.read(rw.getObjectReader(), rw.parseCommit(ref.getObjectId())))
.map(ObjectId::copy)
.collect(toImmutableSet());
}
}
private class PerCheckerOperationsImpl implements PerCheckerOperations {
private final CheckerUuid checkerUuid;
PerCheckerOperationsImpl(CheckerUuid checkerUuid) {
this.checkerUuid = checkerUuid;
}
@Override
public boolean exists() {
return getChecker(checkerUuid).isPresent();
}
@Override
public Checker get() {
return getChecker(checkerUuid)
.orElseThrow(() -> new IllegalStateException("Tried to get non-existing test checker"));
}
private Optional<Checker> getChecker(CheckerUuid checkerUuid) {
try {
return checkers.getChecker(checkerUuid);
} catch (IOException | ConfigInvalidException e) {
throw new IllegalStateException(e);
}
}
@Override
public RevCommit commit() throws IOException {
Optional<Checker> checker = getChecker(checkerUuid);
checkState(checker.isPresent(), "Tried to get commit for a non-existing test checker");
try (Repository repo = repoManager.openRepository(allProjectsName);
RevWalk rw = new RevWalk(repo)) {
return rw.parseCommit(checker.get().getRefState());
}
}
@Override
public String configText() throws IOException, ConfigInvalidException {
Optional<Checker> checker = getChecker(checkerUuid);
checkState(checker.isPresent(), "Tried to get config text for a non-existing test checker");
try (Repository repo = repoManager.openRepository(allProjectsName)) {
// Parse as Config to ensure it's a valid config file.
return new BlobBasedConfig(
null, repo, checker.get().getRefState(), CheckerConfig.CHECKER_CONFIG_FILE)
.toText();
}
}
@Override
public CheckerInfo asInfo() {
Optional<Checker> checker = getChecker(checkerUuid);
checkState(checker.isPresent(), "Tried to get a non-existing test checker as CheckerInfo");
return checkerJson.format(checker.get());
}
@Override
public TestCheckerUpdate.Builder forUpdate() {
return TestCheckerUpdate.builder(this::updateChecker);
}
private void updateChecker(TestCheckerUpdate testCheckerUpdate) throws Exception {
CheckerUpdate checkerUpdate = toCheckerUpdate(testCheckerUpdate);
checkersUpdate.get().updateChecker(checkerUuid, checkerUpdate);
}
@Override
public TestCheckerInvalidation.Builder forInvalidation() {
return TestCheckerInvalidation.builder(this::invalidateChecker);
}
private void invalidateChecker(TestCheckerInvalidation testCheckerInvalidation)
throws Exception {
Optional<Checker> checker = getChecker(checkerUuid);
checkState(checker.isPresent(), "Tried to invalidate a non-existing test checker");
if (testCheckerInvalidation.invalidUuid()) {
setValueInCheckerConfig("uuid", "invalid");
}
if (testCheckerInvalidation.invalidBlockingCondition()) {
addValueInCheckerConfig("blocking", "invalid");
}
if (testCheckerInvalidation.invalidStatus()) {
setValueInCheckerConfig("status", "invalid");
}
if (testCheckerInvalidation.unsetUuid()) {
unsetValueInCheckerConfig("uuid");
}
if (testCheckerInvalidation.unsetName()) {
unsetValueInCheckerConfig("name");
}
if (testCheckerInvalidation.unsetRepository()) {
unsetValueInCheckerConfig("repository");
}
if (testCheckerInvalidation.unsetStatus()) {
unsetValueInCheckerConfig("status");
}
if (testCheckerInvalidation.nonParseableConfig()) {
try (Repository repo = repoManager.openRepository(allProjectsName);
TestRepository<Repository> testRepo = new TestRepository<>(repo)) {
testRepo
.branch(checkerUuid.toRefName())
.commit()
.add(CheckerConfig.CHECKER_CONFIG_FILE, "non-parseable-config")
.create();
}
}
if (testCheckerInvalidation.deleteRef()) {
try (Repository repo = repoManager.openRepository(allProjectsName);
TestRepository<Repository> testRepo = new TestRepository<>(repo)) {
RefUpdate ru = testRepo.getRepository().updateRef(checkerUuid.toRefName(), true);
ru.setForceUpdate(true);
testRefAction(() -> ru.delete());
}
}
}
private void setValueInCheckerConfig(String key, String value) throws Exception {
updateCheckerConfig(cfg -> cfg.setString("checker", null, key, value));
}
private void addValueInCheckerConfig(String key, String value) throws Exception {
updateCheckerConfig(
cfg ->
cfg.setStringList(
"checker",
null,
key,
Streams.concat(
Arrays.stream(cfg.getStringList("checker", null, key)), Stream.of(value))
.collect(toList())));
}
private void unsetValueInCheckerConfig(String key) throws Exception {
updateCheckerConfig(cfg -> cfg.unset("checker", null, key));
}
private void updateCheckerConfig(Consumer<Config> configUpdater) throws Exception {
try (Repository repo = repoManager.openRepository(allProjectsName)) {
TestRepository<Repository> testRepo = new TestRepository<>(repo);
Config checkerConfig =
readConfig(testRepo, checkerUuid.toRefName(), CheckerConfig.CHECKER_CONFIG_FILE);
configUpdater.accept(checkerConfig);
testRepo
.branch(checkerUuid.toRefName())
.commit()
.add(CheckerConfig.CHECKER_CONFIG_FILE, checkerConfig.toText())
.create();
}
}
private CheckerUpdate toCheckerUpdate(TestCheckerUpdate checkerUpdate) {
CheckerUpdate.Builder builder = CheckerUpdate.builder();
checkerUpdate.name().ifPresent(builder::setName);
checkerUpdate.description().ifPresent(builder::setDescription);
checkerUpdate.url().ifPresent(builder::setUrl);
checkerUpdate.repository().ifPresent(builder::setRepository);
checkerUpdate.status().ifPresent(builder::setStatus);
checkerUpdate.blockingConditions().ifPresent(builder::setBlockingConditions);
checkerUpdate.query().ifPresent(builder::setQuery);
return builder.build();
}
private Config readConfig(TestRepository<?> testRepo, String ref, String fileName)
throws Exception {
Repository repo = testRepo.getRepository();
return new BlobBasedConfig(null, repo, repo.resolve(ref), fileName);
}
}
}