LCOV - code coverage report
Current view: top level - server/project - SubmitRequirementsUtil.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 72 73 98.6 %
Date: 2022-11-19 15:00:39 Functions: 10 10 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.collect.ImmutableMap;
      18             : import com.google.gerrit.common.Nullable;
      19             : import com.google.gerrit.entities.SubmitRequirement;
      20             : import com.google.gerrit.entities.SubmitRequirementResult;
      21             : import com.google.gerrit.metrics.Counter1;
      22             : import com.google.gerrit.metrics.Description;
      23             : import com.google.gerrit.metrics.Field;
      24             : import com.google.gerrit.metrics.MetricMaker;
      25             : import com.google.gerrit.server.logging.Metadata;
      26             : import com.google.gerrit.server.query.change.ChangeData;
      27             : import com.google.gerrit.server.query.change.ChangeData.StorageConstraint;
      28             : import com.google.inject.Inject;
      29             : import com.google.inject.Singleton;
      30             : import java.util.HashMap;
      31             : import java.util.Map;
      32             : import java.util.Set;
      33             : import java.util.regex.Pattern;
      34             : import java.util.stream.Collectors;
      35             : 
      36             : /**
      37             :  * A utility class for different operations related to {@link
      38             :  * com.google.gerrit.entities.SubmitRequirement}s.
      39             :  */
      40             : @Singleton
      41             : public class SubmitRequirementsUtil {
      42             : 
      43             :   /**
      44             :    * Submit requirement name can only contain alphanumeric characters or hyphen. Name cannot start
      45             :    * with a hyphen or number.
      46             :    */
      47         103 :   private static final Pattern SUBMIT_REQ_NAME_PATTERN = Pattern.compile("[a-zA-Z][a-zA-Z0-9\\-]*");
      48             : 
      49             :   @Singleton
      50             :   static class Metrics {
      51             :     final Counter1<String> submitRequirementsMatchingWithLegacy;
      52             :     final Counter1<String> submitRequirementsMismatchingWithLegacy;
      53             :     final Counter1<String> legacyNotInSrs;
      54             :     final Counter1<String> srsNotInLegacy;
      55             : 
      56             :     @Inject
      57         103 :     Metrics(MetricMaker metricMaker) {
      58         103 :       submitRequirementsMatchingWithLegacy =
      59         103 :           metricMaker.newCounter(
      60             :               "change/submit_requirements/matching_with_legacy",
      61             :               new Description(
      62             :                       "Total number of times there was a legacy and non-legacy "
      63             :                           + "submit requirements with the same name for a change, "
      64             :                           + "and the evaluation of both requirements had the same result "
      65             :                           + "w.r.t. change submittability.")
      66         103 :                   .setRate()
      67         103 :                   .setUnit("count"),
      68         103 :               Field.ofString("sr_name", Metadata.Builder::submitRequirementName)
      69         103 :                   .description("Submit requirement name")
      70         103 :                   .build());
      71         103 :       submitRequirementsMismatchingWithLegacy =
      72         103 :           metricMaker.newCounter(
      73             :               "change/submit_requirements/mismatching_with_legacy",
      74             :               new Description(
      75             :                       "Total number of times there was a legacy and non-legacy "
      76             :                           + "submit requirements with the same name for a change, "
      77             :                           + "and the evaluation of both requirements had a different result "
      78             :                           + "w.r.t. change submittability.")
      79         103 :                   .setRate()
      80         103 :                   .setUnit("count"),
      81         103 :               Field.ofString("sr_name", Metadata.Builder::submitRequirementName)
      82         103 :                   .description("Submit requirement name")
      83         103 :                   .build());
      84         103 :       legacyNotInSrs =
      85         103 :           metricMaker.newCounter(
      86             :               "change/submit_requirements/legacy_not_in_srs",
      87             :               new Description(
      88             :                       "Total number of times there was a legacy submit requirement result "
      89             :                           + "but not a project config requirement with the same name for a change.")
      90         103 :                   .setRate()
      91         103 :                   .setUnit("count"),
      92         103 :               Field.ofString("sr_name", Metadata.Builder::submitRequirementName)
      93         103 :                   .description("Submit requirement name")
      94         103 :                   .build());
      95         103 :       srsNotInLegacy =
      96         103 :           metricMaker.newCounter(
      97             :               "change/submit_requirements/srs_not_in_legacy",
      98             :               new Description(
      99             :                       "Total number of times there was a project config submit requirement "
     100             :                           + "result but not a legacy requirement with the same name for a change.")
     101         103 :                   .setRate()
     102         103 :                   .setUnit("count"),
     103         103 :               Field.ofString("sr_name", Metadata.Builder::submitRequirementName)
     104         103 :                   .description("Submit requirement name")
     105         103 :                   .build());
     106         103 :     }
     107             :   }
     108             : 
     109             :   private final Metrics metrics;
     110             : 
     111             :   @Inject
     112         103 :   public SubmitRequirementsUtil(Metrics metrics) {
     113         103 :     this.metrics = metrics;
     114         103 :   }
     115             : 
     116             :   /**
     117             :    * Merge legacy and non-legacy submit requirement results. If both input maps have submit
     118             :    * requirements with the same name and fulfillment status (according to {@link
     119             :    * SubmitRequirementResult#fulfilled()}), we eliminate the entry from the {@code
     120             :    * legacyRequirements} input map and only include the one from the {@code
     121             :    * projectConfigRequirements} in the result.
     122             :    *
     123             :    * @param projectConfigRequirements map of {@link SubmitRequirement} to {@link
     124             :    *     SubmitRequirementResult} containing results for submit requirements stored in the
     125             :    *     project.config.
     126             :    * @param legacyRequirements map of {@link SubmitRequirement} to {@link SubmitRequirementResult}
     127             :    *     containing the results of converting legacy submit records to submit requirements.
     128             :    * @return a map that is the result of merging both input maps, while eliminating requirements
     129             :    *     with the same name and status.
     130             :    */
     131             :   public ImmutableMap<SubmitRequirement, SubmitRequirementResult>
     132             :       mergeLegacyAndNonLegacyRequirements(
     133             :           Map<SubmitRequirement, SubmitRequirementResult> projectConfigRequirements,
     134             :           Map<SubmitRequirement, SubmitRequirementResult> legacyRequirements,
     135             :           ChangeData cd) {
     136             :     // Cannot use ImmutableMap.Builder here since entries in the map may be overridden.
     137         103 :     Map<SubmitRequirement, SubmitRequirementResult> result = new HashMap<>();
     138         103 :     result.putAll(projectConfigRequirements);
     139         103 :     Map<String, SubmitRequirementResult> requirementsByName =
     140         103 :         projectConfigRequirements.entrySet().stream()
     141             :             // filter out legacy entries as a safety guard for duplicate entries
     142             :             // (projectConfigRequirements should not contain legacy entries)
     143             :             // TODO(ghareeb): remove the filter statement
     144         103 :             .filter(entry -> !entry.getValue().isLegacy())
     145         103 :             .collect(Collectors.toMap(sr -> sr.getKey().name().toLowerCase(), sr -> sr.getValue()));
     146             :     for (Map.Entry<SubmitRequirement, SubmitRequirementResult> legacy :
     147         103 :         legacyRequirements.entrySet()) {
     148         103 :       String srName = legacy.getKey().name().toLowerCase();
     149         103 :       SubmitRequirementResult projectConfigResult = requirementsByName.get(srName);
     150         103 :       SubmitRequirementResult legacyResult = legacy.getValue();
     151             :       // If there's no project config requirement with the same name as the legacy requirement
     152             :       // then add the legacy SR to the result. There is no mismatch in results in this case.
     153         103 :       if (projectConfigResult == null) {
     154         103 :         result.put(legacy.getKey(), legacy.getValue());
     155         103 :         if (shouldReportMetric(cd)) {
     156         103 :           metrics.legacyNotInSrs.increment(srName);
     157             :         }
     158             :         continue;
     159             :       }
     160           2 :       if (matchByStatus(projectConfigResult, legacyResult)) {
     161             :         // There exists a project config SR with the same name as the legacy SR, and they are
     162             :         // matching in result. No need to include the legacy SR in the output since the project
     163             :         // config SR is already there.
     164           2 :         if (shouldReportMetric(cd)) {
     165           2 :           metrics.submitRequirementsMatchingWithLegacy.increment(srName);
     166             :         }
     167             :         continue;
     168             :       }
     169             :       // There exists a project config SR with the same name as the legacy SR but they are not
     170             :       // matching in their result. Increment the mismatch count and add the legacy SR to the result.
     171           2 :       if (shouldReportMetric(cd)) {
     172           1 :         metrics.submitRequirementsMismatchingWithLegacy.increment(srName);
     173             :       }
     174           2 :       result.put(legacy.getKey(), legacy.getValue());
     175           2 :     }
     176         103 :     Set<String> legacyNames =
     177         103 :         legacyRequirements.keySet().stream()
     178         103 :             .map(SubmitRequirement::name)
     179         103 :             .map(String::toLowerCase)
     180         103 :             .collect(Collectors.toSet());
     181         103 :     for (String projectConfigSrName : requirementsByName.keySet()) {
     182           2 :       if (!legacyNames.contains(projectConfigSrName) && shouldReportMetric(cd)) {
     183           1 :         metrics.srsNotInLegacy.increment(projectConfigSrName);
     184             :       }
     185           2 :     }
     186             : 
     187         103 :     return ImmutableMap.copyOf(result);
     188             :   }
     189             : 
     190             :   /** Validates the name of submit requirements. */
     191             :   public static void validateName(@Nullable String name) throws IllegalArgumentException {
     192           4 :     if (name == null || name.isEmpty()) {
     193           0 :       throw new IllegalArgumentException("Empty submit requirement name");
     194             :     }
     195           4 :     if (!SUBMIT_REQ_NAME_PATTERN.matcher(name).matches()) {
     196           2 :       throw new IllegalArgumentException(
     197           2 :           String.format(
     198             :               "Illegal submit requirement name \"%s\". Name can only consist of "
     199             :                   + "alphanumeric characters and '-'. Name cannot start with '-' or number.",
     200             :               name));
     201             :     }
     202           4 :   }
     203             : 
     204             :   private static boolean shouldReportMetric(ChangeData cd) {
     205             :     // We only care about recording differences in old and new requirements for open changes
     206             :     // that did not have their data retrieved from the (potentially stale) change index.
     207         103 :     return cd.change().isNew() && cd.getStorageConstraint() == StorageConstraint.NOTEDB_ONLY;
     208             :   }
     209             : 
     210             :   /** Returns true if both input results are equal in allowing/disallowing change submission. */
     211             :   private static boolean matchByStatus(SubmitRequirementResult r1, SubmitRequirementResult r2) {
     212           2 :     return r1.fulfilled() == r2.fulfilled();
     213             :   }
     214             : }

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