LCOV - code coverage report
Current view: top level - server/project - SubmitRequirementsAdapter.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 139 144 96.5 %
Date: 2022-11-19 15:00:39 Functions: 21 21 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2021 The Android Open Source Project
       2             : //
       3             : // Licensed under the Apache License, Version 2.0 (the "License");
       4             : // you may not use this file except in compliance with the License.
       5             : // You may obtain a copy of the License at
       6             : //
       7             : // http://www.apache.org/licenses/LICENSE-2.0
       8             : //
       9             : // Unless required by applicable law or agreed to in writing, software
      10             : // distributed under the License is distributed on an "AS IS" BASIS,
      11             : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      12             : // See the License for the specific language governing permissions and
      13             : // limitations under the License.
      14             : 
      15             : package com.google.gerrit.server.project;
      16             : 
      17             : import com.google.common.annotations.VisibleForTesting;
      18             : import com.google.common.base.Strings;
      19             : import com.google.common.collect.ImmutableList;
      20             : import com.google.common.collect.ImmutableMap;
      21             : import com.google.common.flogger.FluentLogger;
      22             : import com.google.gerrit.common.Nullable;
      23             : import com.google.gerrit.entities.LabelType;
      24             : import com.google.gerrit.entities.SubmitRecord;
      25             : import com.google.gerrit.entities.SubmitRecord.Label;
      26             : import com.google.gerrit.entities.SubmitRequirement;
      27             : import com.google.gerrit.entities.SubmitRequirementExpression;
      28             : import com.google.gerrit.entities.SubmitRequirementExpressionResult;
      29             : import com.google.gerrit.entities.SubmitRequirementExpressionResult.Status;
      30             : import com.google.gerrit.entities.SubmitRequirementResult;
      31             : import com.google.gerrit.server.query.change.ChangeData;
      32             : import com.google.gerrit.server.query.change.ChangeQueryBuilder;
      33             : import com.google.gerrit.server.rules.DefaultSubmitRule;
      34             : import java.util.List;
      35             : import java.util.Map;
      36             : import java.util.Optional;
      37             : import java.util.stream.Collectors;
      38             : import org.eclipse.jgit.lib.ObjectId;
      39             : 
      40             : /**
      41             :  * Convert {@link com.google.gerrit.entities.SubmitRecord} entities to {@link
      42             :  * com.google.gerrit.entities.SubmitRequirementResult}s.
      43             :  */
      44             : public class SubmitRequirementsAdapter {
      45         103 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      46             : 
      47             :   private SubmitRequirementsAdapter() {}
      48             : 
      49             :   /**
      50             :    * Retrieve legacy submit records (created by label functions and other {@link
      51             :    * com.google.gerrit.server.rules.SubmitRule}s) and convert them to submit requirement results.
      52             :    */
      53             :   public static Map<SubmitRequirement, SubmitRequirementResult> getLegacyRequirements(
      54             :       ChangeData cd) {
      55             :     // We use SubmitRuleOptions.defaults() which does not recompute submit rules for closed changes.
      56             :     // This doesn't have an effect since we never call this class (i.e. to evaluate submit
      57             :     // requirements) for closed changes.
      58         103 :     List<SubmitRecord> records = cd.submitRecords(SubmitRuleOptions.defaults());
      59         103 :     boolean areForced =
      60         103 :         records.stream().anyMatch(record -> SubmitRecord.Status.FORCED.equals(record.status));
      61         103 :     List<LabelType> labelTypes = cd.getLabelTypes().getLabelTypes();
      62         103 :     ObjectId commitId = cd.currentPatchSet().commitId();
      63         103 :     Map<String, List<SubmitRequirementResult>> srsByName =
      64         103 :         records.stream()
      65             :             // Filter out the "FORCED" submit record. This is a marker submit record that was just
      66             :             // used to indicate that all other records were forced. "FORCED" means that the change
      67             :             // was pushed with the %submit option bypassing submit rules.
      68         103 :             .filter(r -> !SubmitRecord.Status.FORCED.equals(r.status))
      69         103 :             .map(r -> createResult(r, labelTypes, commitId, areForced))
      70         103 :             .flatMap(List::stream)
      71         103 :             .collect(Collectors.groupingBy(sr -> sr.submitRequirement().name()));
      72             : 
      73             :     // We convert submit records to submit requirements by generating a separate
      74             :     // submit requirement result for each available label in each submit record.
      75             :     // The SR status is derived from the label status of the submit record.
      76             :     // This conversion might result in duplicate entries.
      77             :     // One such example can be a prolog rule emitting the same label name twice.
      78             :     // Another case might happen if two different submit rules emit the same label
      79             :     // name. In such cases, we need to merge these entries and return a single submit
      80             :     // requirement result. If both entries agree in their status, return any of them.
      81             :     // Otherwise, favour the entry that is blocking submission.
      82             :     ImmutableMap.Builder<SubmitRequirement, SubmitRequirementResult> result =
      83         103 :         ImmutableMap.builder();
      84         103 :     for (Map.Entry<String, List<SubmitRequirementResult>> entry : srsByName.entrySet()) {
      85         103 :       if (entry.getValue().size() == 1) {
      86         103 :         SubmitRequirementResult srResult = entry.getValue().iterator().next();
      87         103 :         result.put(srResult.submitRequirement(), srResult);
      88         103 :         continue;
      89             :       }
      90             :       // If all submit requirements with the same name match in status, return the first one.
      91           2 :       List<SubmitRequirementResult> resultsSameName = entry.getValue();
      92           2 :       boolean allNonBlocking = resultsSameName.stream().allMatch(sr -> sr.fulfilled());
      93           2 :       if (allNonBlocking) {
      94           1 :         result.put(resultsSameName.get(0).submitRequirement(), resultsSameName.get(0));
      95             :       } else {
      96             :         // Otherwise, return the first submit requirement result that is blocking submission.
      97           2 :         Optional<SubmitRequirementResult> nonFulfilled =
      98           2 :             resultsSameName.stream().filter(sr -> !sr.fulfilled()).findFirst();
      99           2 :         if (nonFulfilled.isPresent()) {
     100           2 :           result.put(nonFulfilled.get().submitRequirement(), nonFulfilled.get());
     101             :         }
     102             :       }
     103           2 :     }
     104         103 :     return result.build();
     105             :   }
     106             : 
     107             :   @VisibleForTesting
     108             :   static List<SubmitRequirementResult> createResult(
     109             :       SubmitRecord record, List<LabelType> labelTypes, ObjectId psCommitId, boolean isForced) {
     110             :     List<SubmitRequirementResult> results;
     111         103 :     if (record.ruleName != null && record.ruleName.equals(DefaultSubmitRule.RULE_NAME)) {
     112         103 :       results = createFromDefaultSubmitRecord(record.labels, labelTypes, psCommitId, isForced);
     113             :     } else {
     114          14 :       results = createFromCustomSubmitRecord(record, psCommitId, isForced);
     115             :     }
     116         103 :     logger.atFine().log("Converted submit record %s to submit requirements %s", record, results);
     117         103 :     return results;
     118             :   }
     119             : 
     120             :   private static List<SubmitRequirementResult> createFromDefaultSubmitRecord(
     121             :       @Nullable List<Label> labels,
     122             :       List<LabelType> labelTypes,
     123             :       ObjectId psCommitId,
     124             :       boolean isForced) {
     125         103 :     ImmutableList.Builder<SubmitRequirementResult> result = ImmutableList.builder();
     126         103 :     if (labels == null) {
     127           0 :       return result.build();
     128             :     }
     129         103 :     for (Label label : labels) {
     130         103 :       if (skipSubmitRequirementFor(label)) {
     131           8 :         continue;
     132             :       }
     133         103 :       Optional<LabelType> maybeLabelType = getLabelType(labelTypes, label.label);
     134         103 :       if (!maybeLabelType.isPresent()) {
     135             :         // Label type might have been removed from the project config. We don't have information
     136             :         // if it was blocking or not, hence we skip the label.
     137           1 :         continue;
     138             :       }
     139         103 :       LabelType labelType = maybeLabelType.get();
     140         103 :       if (!isBlocking(labelType)) {
     141           0 :         continue;
     142             :       }
     143         103 :       ImmutableList<String> atoms = toExpressionAtomList(labelType);
     144             :       SubmitRequirement.Builder req =
     145         103 :           SubmitRequirement.builder()
     146         103 :               .setName(label.label)
     147         103 :               .setSubmittabilityExpression(toExpression(atoms))
     148         103 :               .setAllowOverrideInChildProjects(labelType.isCanOverride());
     149         103 :       result.add(
     150         103 :           SubmitRequirementResult.builder()
     151         103 :               .legacy(Optional.of(true))
     152         103 :               .submitRequirement(req.build())
     153         103 :               .submittabilityExpressionResult(
     154         103 :                   createExpressionResult(toExpression(atoms), mapStatus(label), atoms))
     155         103 :               .patchSetCommitId(psCommitId)
     156         103 :               .forced(Optional.of(isForced))
     157         103 :               .build());
     158         103 :     }
     159         103 :     return result.build();
     160             :   }
     161             : 
     162             :   private static List<SubmitRequirementResult> createFromCustomSubmitRecord(
     163             :       SubmitRecord record, ObjectId psCommitId, boolean isForced) {
     164          14 :     String ruleName = record.ruleName != null ? record.ruleName : "Custom-Rule";
     165          14 :     if (record.labels == null || record.labels.isEmpty()) {
     166             :       SubmitRequirement sr =
     167           7 :           SubmitRequirement.builder()
     168           7 :               .setName(ruleName)
     169           7 :               .setSubmittabilityExpression(
     170           7 :                   SubmitRequirementExpression.create(String.format("rule:%s", ruleName)))
     171           7 :               .setAllowOverrideInChildProjects(false)
     172           7 :               .build();
     173           7 :       return ImmutableList.of(
     174           7 :           SubmitRequirementResult.builder()
     175           7 :               .legacy(Optional.of(true))
     176           7 :               .submitRequirement(sr)
     177           7 :               .submittabilityExpressionResult(
     178           7 :                   createExpressionResult(
     179           7 :                       sr.submittabilityExpression(),
     180           7 :                       mapStatus(record),
     181           7 :                       ImmutableList.of(ruleName),
     182             :                       record.errorMessage))
     183           7 :               .patchSetCommitId(psCommitId)
     184           7 :               .forced(Optional.of(isForced))
     185           7 :               .build());
     186             :     }
     187           9 :     ImmutableList.Builder<SubmitRequirementResult> result = ImmutableList.builder();
     188           9 :     for (Label label : record.labels) {
     189           9 :       if (skipSubmitRequirementFor(label)) {
     190           1 :         continue;
     191             :       }
     192           9 :       String expressionString = String.format("label:%s=%s", label.label, ruleName);
     193             :       SubmitRequirement sr =
     194           9 :           SubmitRequirement.builder()
     195           9 :               .setName(label.label)
     196           9 :               .setSubmittabilityExpression(SubmitRequirementExpression.create(expressionString))
     197           9 :               .setAllowOverrideInChildProjects(false)
     198           9 :               .build();
     199           9 :       result.add(
     200           9 :           SubmitRequirementResult.builder()
     201           9 :               .legacy(Optional.of(true))
     202           9 :               .submitRequirement(sr)
     203           9 :               .submittabilityExpressionResult(
     204           9 :                   createExpressionResult(
     205           9 :                       sr.submittabilityExpression(),
     206           9 :                       mapStatus(label),
     207           9 :                       ImmutableList.of(expressionString)))
     208           9 :               .patchSetCommitId(psCommitId)
     209           9 :               .build());
     210           9 :     }
     211           9 :     return result.build();
     212             :   }
     213             : 
     214             :   private static boolean isBlocking(LabelType labelType) {
     215         103 :     return labelType.getFunction().isBlock() || labelType.getFunction().isRequired();
     216             :   }
     217             : 
     218             :   private static SubmitRequirementExpression toExpression(List<String> atoms) {
     219         103 :     return SubmitRequirementExpression.create(String.join(" ", atoms));
     220             :   }
     221             : 
     222             :   private static ImmutableList<String> toExpressionAtomList(LabelType lt) {
     223             :     String ignoreSelfApproval =
     224         103 :         lt.isIgnoreSelfApproval() ? ",user=" + ChangeQueryBuilder.ARG_ID_NON_UPLOADER : "";
     225         103 :     switch (lt.getFunction()) {
     226             :       case MAX_WITH_BLOCK:
     227         103 :         return ImmutableList.of(
     228         103 :             String.format("label:%s=MAX", lt.getName()) + ignoreSelfApproval,
     229         103 :             String.format("-label:%s=MIN", lt.getName()));
     230             :       case ANY_WITH_BLOCK:
     231           2 :         return ImmutableList.of(String.format(String.format("-label:%s=MIN", lt.getName())));
     232             :       case MAX_NO_BLOCK:
     233           3 :         return ImmutableList.of(
     234           3 :             String.format(String.format("label:%s=MAX", lt.getName())) + ignoreSelfApproval);
     235             :       case NO_BLOCK:
     236             :       case NO_OP:
     237             :       case PATCH_SET_LOCK:
     238             :       default:
     239           0 :         return ImmutableList.of();
     240             :     }
     241             :   }
     242             : 
     243             :   private static Status mapStatus(Label label) {
     244         103 :     SubmitRequirementExpressionResult.Status status = Status.PASS;
     245         103 :     switch (label.status) {
     246             :       case OK:
     247             :       case MAY:
     248          58 :         status = Status.PASS;
     249          58 :         break;
     250             :       case REJECT:
     251             :       case NEED:
     252             :       case IMPOSSIBLE:
     253         103 :         status = Status.FAIL;
     254             :         break;
     255             :     }
     256         103 :     return status;
     257             :   }
     258             : 
     259             :   private static Status mapStatus(SubmitRecord submitRecord) {
     260           7 :     switch (submitRecord.status) {
     261             :       case OK:
     262             :       case CLOSED:
     263             :       case FORCED:
     264           6 :         return Status.PASS;
     265             :       case NOT_READY:
     266           6 :         return Status.FAIL;
     267             :       case RULE_ERROR:
     268             :       default:
     269           2 :         return Status.ERROR;
     270             :     }
     271             :   }
     272             : 
     273             :   private static SubmitRequirementExpressionResult createExpressionResult(
     274             :       SubmitRequirementExpression expression, Status status, ImmutableList<String> atoms) {
     275         103 :     return SubmitRequirementExpressionResult.create(
     276             :         expression,
     277             :         status,
     278         103 :         status == Status.PASS ? atoms : ImmutableList.of(),
     279         103 :         status == Status.FAIL ? atoms : ImmutableList.of());
     280             :   }
     281             : 
     282             :   private static SubmitRequirementExpressionResult createExpressionResult(
     283             :       SubmitRequirementExpression expression,
     284             :       Status status,
     285             :       ImmutableList<String> atoms,
     286             :       String errorMessage) {
     287           7 :     return SubmitRequirementExpressionResult.create(
     288             :         expression,
     289             :         status,
     290           7 :         status == Status.PASS ? atoms : ImmutableList.of(),
     291           7 :         status == Status.FAIL ? atoms : ImmutableList.of(),
     292           7 :         Optional.ofNullable(Strings.emptyToNull(errorMessage)));
     293             :   }
     294             : 
     295             :   private static Optional<LabelType> getLabelType(List<LabelType> labelTypes, String labelName) {
     296         103 :     List<LabelType> label =
     297         103 :         labelTypes.stream()
     298         103 :             .filter(lt -> lt.getName().equals(labelName))
     299         103 :             .collect(Collectors.toList());
     300         103 :     if (label.isEmpty()) {
     301             :       // Label might have been removed from the project.
     302           1 :       logger.atFine().log("Label '%s' was not found for the project.", labelName);
     303           1 :       return Optional.empty();
     304         103 :     } else if (label.size() > 1) {
     305           0 :       logger.atWarning().log("Found more than one label definition for label name '%s'", labelName);
     306           0 :       return Optional.empty();
     307             :     }
     308         103 :     return Optional.of(label.get(0));
     309             :   }
     310             : 
     311             :   /**
     312             :    * Returns true if we should skip creating a "submit requirement" result out of the "submit
     313             :    * record" label.
     314             :    */
     315             :   private static boolean skipSubmitRequirementFor(SubmitRecord.Label label) {
     316         103 :     return label.status == SubmitRecord.Label.Status.MAY;
     317             :   }
     318             : }

Generated by: LCOV version 1.16+git.20220603.dfeb750