| // 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 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.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.reviewdb.client.Project; |
| 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); |
| 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); |
| } |
| } |
| } |