LCOV - code coverage report
Current view: top level - server/change - LabelsJson.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 236 241 97.9 %
Date: 2022-11-19 15:00:39 Functions: 23 23 100.0 %

          Line data    Source code
       1             : // Copyright (C) 2018 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.change;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkState;
      18             : import static java.util.stream.Collectors.toList;
      19             : 
      20             : import com.google.auto.value.AutoValue;
      21             : import com.google.common.collect.HashBasedTable;
      22             : import com.google.common.collect.ImmutableMap;
      23             : import com.google.common.collect.Iterables;
      24             : import com.google.common.collect.LinkedHashMultimap;
      25             : import com.google.common.collect.Lists;
      26             : import com.google.common.collect.Maps;
      27             : import com.google.common.collect.MultimapBuilder;
      28             : import com.google.common.collect.SetMultimap;
      29             : import com.google.common.collect.Table;
      30             : import com.google.common.flogger.FluentLogger;
      31             : import com.google.common.primitives.Ints;
      32             : import com.google.gerrit.common.Nullable;
      33             : import com.google.gerrit.entities.Account;
      34             : import com.google.gerrit.entities.LabelType;
      35             : import com.google.gerrit.entities.LabelTypes;
      36             : import com.google.gerrit.entities.LabelValue;
      37             : import com.google.gerrit.entities.PatchSetApproval;
      38             : import com.google.gerrit.entities.SubmitRecord;
      39             : import com.google.gerrit.extensions.common.ApprovalInfo;
      40             : import com.google.gerrit.extensions.common.LabelInfo;
      41             : import com.google.gerrit.extensions.common.VotingRangeInfo;
      42             : import com.google.gerrit.server.ChangeUtil;
      43             : import com.google.gerrit.server.account.AccountLoader;
      44             : import com.google.gerrit.server.notedb.ReviewerStateInternal;
      45             : import com.google.gerrit.server.permissions.LabelPermission;
      46             : import com.google.gerrit.server.permissions.PermissionBackend;
      47             : import com.google.gerrit.server.permissions.PermissionBackendException;
      48             : import com.google.gerrit.server.query.change.ChangeData;
      49             : import com.google.inject.Inject;
      50             : import com.google.inject.Singleton;
      51             : import java.time.Instant;
      52             : import java.util.ArrayList;
      53             : import java.util.Collection;
      54             : import java.util.Collections;
      55             : import java.util.HashMap;
      56             : import java.util.HashSet;
      57             : import java.util.LinkedHashMap;
      58             : import java.util.List;
      59             : import java.util.Map;
      60             : import java.util.Optional;
      61             : import java.util.Set;
      62             : import java.util.TreeMap;
      63             : 
      64             : /**
      65             :  * Produces label-related entities, like {@link LabelInfo}s, which is serialized to JSON afterwards.
      66             :  */
      67             : @Singleton
      68             : public class LabelsJson {
      69         146 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      70             : 
      71             :   private final PermissionBackend permissionBackend;
      72             : 
      73             :   @Inject
      74         146 :   LabelsJson(PermissionBackend permissionBackend) {
      75         146 :     this.permissionBackend = permissionBackend;
      76         146 :   }
      77             : 
      78             :   /**
      79             :    * Returns all {@link LabelInfo}s for a single change. Uses the provided {@link AccountLoader} to
      80             :    * lazily populate accounts. Callers have to call {@link AccountLoader#fill()} afterwards to
      81             :    * populate all accounts in the returned {@link LabelInfo}s.
      82             :    */
      83             :   @Nullable
      84             :   Map<String, LabelInfo> labelsFor(
      85             :       AccountLoader accountLoader, ChangeData cd, boolean standard, boolean detailed)
      86             :       throws PermissionBackendException {
      87         103 :     if (!standard && !detailed) {
      88          71 :       return null;
      89             :     }
      90             : 
      91         103 :     LabelTypes labelTypes = cd.getLabelTypes();
      92             :     Map<String, LabelWithStatus> withStatus =
      93         103 :         cd.change().isMerged()
      94          55 :             ? labelsForSubmittedChange(accountLoader, cd, labelTypes, standard, detailed)
      95         103 :             : labelsForUnsubmittedChange(accountLoader, cd, labelTypes, standard, detailed);
      96         103 :     return ImmutableMap.copyOf(Maps.transformValues(withStatus, LabelWithStatus::label));
      97             :   }
      98             : 
      99             :   /**
     100             :    * Returns A map of all label names and the values that the provided user has permission to vote
     101             :    * on.
     102             :    *
     103             :    * @param filterApprovalsBy a Gerrit user ID.
     104             :    * @param cd {@link ChangeData} corresponding to a specific gerrit change.
     105             :    * @return A Map where the key contain a label name, and the value is a list of the permissible
     106             :    *     vote values that the user can vote on.
     107             :    */
     108             :   Map<String, Collection<String>> permittedLabels(Account.Id filterApprovalsBy, ChangeData cd)
     109             :       throws PermissionBackendException {
     110         103 :     SetMultimap<String, String> permitted = LinkedHashMultimap.create();
     111         103 :     boolean isMerged = cd.change().isMerged();
     112         103 :     Map<String, Short> currentUserVotes = currentLabels(filterApprovalsBy, cd);
     113         103 :     for (LabelType labelType : cd.getLabelTypes().getLabelTypes()) {
     114         103 :       if (isMerged && !labelType.isAllowPostSubmit()) {
     115           1 :         continue;
     116             :       }
     117         103 :       Set<LabelPermission.WithValue> can =
     118         103 :           permissionBackend.absentUser(filterApprovalsBy).change(cd).test(labelType);
     119         103 :       for (LabelValue v : labelType.getValues()) {
     120         103 :         boolean ok = can.contains(new LabelPermission.WithValue(labelType, v));
     121         103 :         if (isMerged) {
     122             :           // Votes cannot be decreased if the change is merged. Only accept the label value if it's
     123             :           // greater or equal than the user's latest vote.
     124          55 :           short prev = currentUserVotes.getOrDefault(labelType.getName(), (short) 0);
     125          55 :           ok &= v.getValue() >= prev;
     126             :         }
     127         103 :         if (ok) {
     128         103 :           permitted.put(labelType.getName(), v.formatValue());
     129             :         }
     130         103 :       }
     131         103 :     }
     132         103 :     clearOnlyZerosEntries(permitted);
     133         103 :     return permitted.asMap();
     134             :   }
     135             : 
     136             :   private static void clearOnlyZerosEntries(SetMultimap<String, String> permitted) {
     137         103 :     List<String> toClear = Lists.newArrayListWithCapacity(permitted.keySet().size());
     138         103 :     for (Map.Entry<String, Collection<String>> e : permitted.asMap().entrySet()) {
     139         103 :       if (isOnlyZero(e.getValue())) {
     140           9 :         toClear.add(e.getKey());
     141             :       }
     142         103 :     }
     143         103 :     for (String label : toClear) {
     144           9 :       permitted.removeAll(label);
     145           9 :     }
     146         103 :   }
     147             : 
     148             :   private static boolean isOnlyZero(Collection<String> values) {
     149         103 :     return values.isEmpty() || (values.size() == 1 && values.contains(" 0"));
     150             :   }
     151             : 
     152             :   private static void addApproval(LabelInfo label, ApprovalInfo approval) {
     153          71 :     if (label.all == null) {
     154          71 :       label.all = new ArrayList<>();
     155             :     }
     156          71 :     label.all.add(approval);
     157          71 :   }
     158             : 
     159             :   private Map<String, LabelWithStatus> labelsForUnsubmittedChange(
     160             :       AccountLoader accountLoader,
     161             :       ChangeData cd,
     162             :       LabelTypes labelTypes,
     163             :       boolean standard,
     164             :       boolean detailed)
     165             :       throws PermissionBackendException {
     166         103 :     Map<String, LabelWithStatus> labels = initLabels(accountLoader, cd, labelTypes, standard);
     167         103 :     setAllApprovals(accountLoader, cd, labels, detailed);
     168             : 
     169         103 :     for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
     170         103 :       Optional<LabelType> type = labelTypes.byLabel(e.getKey());
     171         103 :       if (!type.isPresent()) {
     172           5 :         continue;
     173             :       }
     174         103 :       if (standard) {
     175         103 :         for (PatchSetApproval psa : cd.currentApprovals()) {
     176          58 :           if (type.get().matches(psa)) {
     177          58 :             short val = psa.value();
     178          58 :             Account.Id accountId = psa.accountId();
     179          58 :             setLabelScores(accountLoader, type.get(), e.getValue(), val, accountId);
     180             :           }
     181          58 :         }
     182             :       }
     183         103 :       setLabelValues(type.get(), e.getValue());
     184         103 :     }
     185         103 :     return labels;
     186             :   }
     187             : 
     188             :   private Integer parseRangeValue(String value) {
     189          71 :     if (value.startsWith("+")) {
     190          71 :       value = value.substring(1);
     191          71 :     } else if (value.startsWith(" ")) {
     192          71 :       value = value.trim();
     193             :     }
     194          71 :     return Ints.tryParse(value);
     195             :   }
     196             : 
     197             :   private ApprovalInfo approvalInfo(
     198             :       AccountLoader accountLoader,
     199             :       Account.Id id,
     200             :       @Nullable Integer value,
     201             :       @Nullable VotingRangeInfo permittedVotingRange,
     202             :       @Nullable String tag,
     203             :       @Nullable Instant date) {
     204          71 :     ApprovalInfo ai = new ApprovalInfo(id.get(), value, permittedVotingRange, tag, date);
     205          71 :     accountLoader.put(ai);
     206          71 :     return ai;
     207             :   }
     208             : 
     209             :   private void setLabelValues(LabelType type, LabelWithStatus l) {
     210         103 :     l.label().defaultValue = type.getDefaultValue();
     211         103 :     l.label().values = new LinkedHashMap<>();
     212         103 :     for (LabelValue v : type.getValues()) {
     213         103 :       l.label().values.put(v.formatValue(), v.getText());
     214         103 :     }
     215         103 :     if (isOnlyZero(l.label().values.keySet())) {
     216           0 :       l.label().values = null;
     217             :     }
     218         103 :   }
     219             : 
     220             :   private Map<String, Short> currentLabels(Account.Id accountId, ChangeData cd) {
     221         103 :     Map<String, Short> result = new HashMap<>();
     222         103 :     for (PatchSetApproval psa : cd.currentApprovals()) {
     223          67 :       if (psa.accountId().equals(accountId)) {
     224          67 :         result.put(psa.label(), psa.value());
     225             :       }
     226          67 :     }
     227         103 :     return result;
     228             :   }
     229             : 
     230             :   private Map<String, LabelWithStatus> labelsForSubmittedChange(
     231             :       AccountLoader accountLoader,
     232             :       ChangeData cd,
     233             :       LabelTypes labelTypes,
     234             :       boolean standard,
     235             :       boolean detailed)
     236             :       throws PermissionBackendException {
     237          55 :     Set<Account.Id> allUsers = new HashSet<>();
     238          55 :     if (detailed) {
     239             :       // Users expect to see all reviewers on closed changes, even if they
     240             :       // didn't vote on the latest patch set. If we don't need detailed labels,
     241             :       // we aren't including 0 votes for all users below, so we can just look at
     242             :       // the latest patch set (in the next loop).
     243          55 :       for (PatchSetApproval psa : cd.approvals().values()) {
     244          55 :         allUsers.add(psa.accountId());
     245          55 :       }
     246             :     }
     247             : 
     248          55 :     Set<String> labelNames = new HashSet<>();
     249             :     SetMultimap<Account.Id, PatchSetApproval> current =
     250          55 :         MultimapBuilder.hashKeys().hashSetValues().build();
     251          55 :     for (PatchSetApproval a : cd.currentApprovals()) {
     252          55 :       allUsers.add(a.accountId());
     253          55 :       Optional<LabelType> type = labelTypes.byLabel(a.labelId());
     254          55 :       if (type.isPresent()) {
     255          46 :         labelNames.add(type.get().getName());
     256             :         // Not worth the effort to distinguish between votable/non-votable for 0
     257             :         // values on closed changes, since they can't vote anyway.
     258          46 :         current.put(a.accountId(), a);
     259             :       }
     260          55 :     }
     261             : 
     262             :     // Since voting on merged changes is allowed all labels which apply to
     263             :     // the change must be returned. All applying labels can be retrieved from
     264             :     // the submit records, which is what initLabels does.
     265             :     // It's not possible to only compute the labels based on the approvals
     266             :     // since merged changes may not have approvals for all labels (e.g. if not
     267             :     // all labels are required for submit or if the change was auto-closed due
     268             :     // to direct push or if new labels were defined after the change was
     269             :     // merged).
     270             :     Map<String, LabelWithStatus> labels;
     271          55 :     labels = initLabels(accountLoader, cd, labelTypes, standard);
     272             : 
     273             :     // Also include all labels for which approvals exists. E.g. there can be
     274             :     // approvals for labels that are ignored by a Prolog submit rule and hence
     275             :     // it wouldn't be included in the submit records.
     276          55 :     for (String name : labelNames) {
     277          46 :       if (!labels.containsKey(name)) {
     278           2 :         labels.put(name, LabelWithStatus.create(new LabelInfo(), null));
     279             :       }
     280          46 :     }
     281             : 
     282          55 :     labels.entrySet().stream()
     283          55 :         .filter(e -> labelTypes.byLabel(e.getKey()).isPresent())
     284          55 :         .forEach(e -> setLabelValues(labelTypes.byLabel(e.getKey()).get(), e.getValue()));
     285             : 
     286          55 :     for (Account.Id accountId : allUsers) {
     287          55 :       Map<String, ApprovalInfo> byLabel = Maps.newHashMapWithExpectedSize(labels.size());
     288          55 :       Map<String, VotingRangeInfo> pvr = Collections.emptyMap();
     289          55 :       if (detailed) {
     290          55 :         pvr = getPermittedVotingRanges(permittedLabels(accountId, cd));
     291             :       }
     292          55 :       for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) {
     293          55 :         ApprovalInfo ai = approvalInfo(accountLoader, accountId, 0, null, null, null);
     294          55 :         byLabel.put(entry.getKey(), ai);
     295          55 :         addApproval(entry.getValue().label(), ai);
     296          55 :       }
     297          55 :       for (PatchSetApproval psa : current.get(accountId)) {
     298          46 :         Optional<LabelType> type = labelTypes.byLabel(psa.labelId());
     299          46 :         if (!type.isPresent()) {
     300           0 :           continue;
     301             :         }
     302             : 
     303          46 :         short val = psa.value();
     304          46 :         ApprovalInfo info = byLabel.get(type.get().getName());
     305          46 :         if (info != null) {
     306          46 :           info.value = Integer.valueOf(val);
     307          46 :           info.permittedVotingRange = pvr.getOrDefault(type.get().getName(), null);
     308          46 :           info.setDate(psa.granted());
     309          46 :           info.tag = psa.tag().orElse(null);
     310          46 :           if (psa.postSubmit()) {
     311           3 :             info.postSubmit = true;
     312             :           }
     313             :         }
     314          46 :         if (!standard) {
     315           6 :           continue;
     316             :         }
     317             : 
     318          46 :         setLabelScores(accountLoader, type.get(), labels.get(type.get().getName()), val, accountId);
     319          46 :       }
     320          55 :     }
     321          55 :     return labels;
     322             :   }
     323             : 
     324             :   private Map<String, LabelWithStatus> initLabels(
     325             :       AccountLoader accountLoader, ChangeData cd, LabelTypes labelTypes, boolean standard) {
     326         103 :     Map<String, LabelWithStatus> labels = new TreeMap<>(labelTypes.nameComparator());
     327         103 :     for (SubmitRecord rec : submitRecords(cd)) {
     328         103 :       if (rec.labels == null) {
     329           5 :         continue;
     330             :       }
     331         103 :       for (SubmitRecord.Label r : rec.labels) {
     332         103 :         LabelWithStatus p = labels.get(r.label);
     333         103 :         if (p == null || p.status().compareTo(r.status) < 0) {
     334         103 :           LabelInfo n = new LabelInfo();
     335         103 :           if (standard) {
     336         103 :             switch (r.status) {
     337             :               case OK:
     338          56 :                 n.approved = accountLoader.get(r.appliedBy);
     339          56 :                 break;
     340             :               case REJECT:
     341          19 :                 n.rejected = accountLoader.get(r.appliedBy);
     342          19 :                 n.blocking = true;
     343          19 :                 break;
     344             :               case IMPOSSIBLE:
     345             :               case MAY:
     346             :               case NEED:
     347             :               default:
     348             :                 break;
     349             :             }
     350             :           }
     351             : 
     352         103 :           n.optional = r.status == SubmitRecord.Label.Status.MAY ? true : null;
     353         103 :           labels.put(r.label, LabelWithStatus.create(n, r.status));
     354             :         }
     355         103 :       }
     356         103 :     }
     357         103 :     setLabelsDescription(labels, labelTypes);
     358         103 :     return labels;
     359             :   }
     360             : 
     361             :   private void setLabelsDescription(
     362             :       Map<String, LabelsJson.LabelWithStatus> labels, LabelTypes labelTypes) {
     363         103 :     for (Map.Entry<String, LabelWithStatus> entry : labels.entrySet()) {
     364         103 :       String labelName = entry.getKey();
     365         103 :       Optional<LabelType> type = labelTypes.byLabel(labelName);
     366         103 :       if (!type.isPresent()) {
     367           5 :         continue;
     368             :       }
     369         103 :       LabelWithStatus labelWithStatus = entry.getValue();
     370         103 :       labelWithStatus.label().description = type.get().getDescription().orElse(null);
     371         103 :     }
     372         103 :   }
     373             : 
     374             :   private void setLabelScores(
     375             :       AccountLoader accountLoader,
     376             :       LabelType type,
     377             :       LabelWithStatus l,
     378             :       short score,
     379             :       Account.Id accountId) {
     380          58 :     if (l.label().approved != null || l.label().rejected != null) {
     381          56 :       return;
     382             :     }
     383             : 
     384          27 :     if (type.getMin() == null || type.getMax() == null) {
     385             :       // Can't set score for unknown or misconfigured type.
     386           0 :       return;
     387             :     }
     388             : 
     389          27 :     if (score != 0) {
     390          25 :       if (score == type.getMin().getValue()) {
     391           3 :         l.label().rejected = accountLoader.get(accountId);
     392          25 :       } else if (score == type.getMax().getValue()) {
     393          10 :         l.label().approved = accountLoader.get(accountId);
     394          22 :       } else if (score < 0) {
     395           9 :         l.label().disliked = accountLoader.get(accountId);
     396           9 :         l.label().value = score;
     397          22 :       } else if (score > 0 && l.label().disliked == null) {
     398          22 :         l.label().recommended = accountLoader.get(accountId);
     399          22 :         l.label().value = score;
     400             :       }
     401             :     }
     402          27 :   }
     403             : 
     404             :   private void setAllApprovals(
     405             :       AccountLoader accountLoader,
     406             :       ChangeData cd,
     407             :       Map<String, LabelWithStatus> labels,
     408             :       boolean detailed)
     409             :       throws PermissionBackendException {
     410         103 :     checkState(
     411         103 :         !cd.change().isMerged(),
     412             :         "should not call setAllApprovals on %s change",
     413         103 :         ChangeUtil.status(cd.change()));
     414             : 
     415             :     // Include a user in the output for this label if either:
     416             :     //  - They are an explicit reviewer.
     417             :     //  - They ever voted on this change.
     418         103 :     Set<Account.Id> allUsers = new HashSet<>();
     419         103 :     allUsers.addAll(cd.reviewers().byState(ReviewerStateInternal.REVIEWER));
     420         103 :     for (PatchSetApproval psa : cd.approvals().values()) {
     421          58 :       allUsers.add(psa.accountId());
     422          58 :     }
     423             : 
     424         103 :     Table<Account.Id, String, PatchSetApproval> current =
     425         103 :         HashBasedTable.create(allUsers.size(), cd.getLabelTypes().getLabelTypes().size());
     426         103 :     for (PatchSetApproval psa : cd.currentApprovals()) {
     427          58 :       current.put(psa.accountId(), psa.label(), psa);
     428          58 :     }
     429             : 
     430         103 :     LabelTypes labelTypes = cd.getLabelTypes();
     431         103 :     for (Account.Id accountId : allUsers) {
     432          66 :       Map<String, VotingRangeInfo> pvr = null;
     433          66 :       PermissionBackend.ForChange perm = null;
     434          66 :       if (detailed) {
     435          66 :         perm = permissionBackend.absentUser(accountId).change(cd);
     436          66 :         pvr = getPermittedVotingRanges(permittedLabels(accountId, cd));
     437             :       }
     438          66 :       for (Map.Entry<String, LabelWithStatus> e : labels.entrySet()) {
     439          66 :         Optional<LabelType> lt = labelTypes.byLabel(e.getKey());
     440          66 :         if (!lt.isPresent()) {
     441             :           // Ignore submit record for undefined label; likely the submit rule
     442             :           // author didn't intend for the label to show up in the table.
     443           3 :           continue;
     444             :         }
     445             :         Integer value;
     446             :         VotingRangeInfo permittedVotingRange =
     447          66 :             pvr == null ? null : pvr.getOrDefault(lt.get().getName(), null);
     448          66 :         String tag = null;
     449          66 :         Instant date = null;
     450          66 :         PatchSetApproval psa = current.get(accountId, lt.get().getName());
     451          66 :         if (psa != null) {
     452          58 :           value = Integer.valueOf(psa.value());
     453          58 :           if (value == 0) {
     454             :             // This may be a dummy approval that was inserted when the reviewer
     455             :             // was added. Explicitly check whether the user can vote on this
     456             :             // label.
     457          12 :             value = perm != null && perm.test(new LabelPermission(lt.get())) ? 0 : null;
     458             :           }
     459          58 :           tag = psa.tag().orElse(null);
     460          58 :           date = psa.granted();
     461          58 :           if (psa.postSubmit()) {
     462           0 :             logger.atWarning().log("unexpected post-submit approval on open change: %s", psa);
     463             :           }
     464             :         } else {
     465             :           // Either the user cannot vote on this label, or they were added as a
     466             :           // reviewer but have not responded yet. Explicitly check whether the
     467             :           // user can vote on this label.
     468          37 :           value = perm != null && perm.test(new LabelPermission(lt.get())) ? 0 : null;
     469             :         }
     470          66 :         addApproval(
     471          66 :             e.getValue().label(),
     472          66 :             approvalInfo(accountLoader, accountId, value, permittedVotingRange, tag, date));
     473          66 :       }
     474          66 :     }
     475         103 :   }
     476             : 
     477             :   private List<SubmitRecord> submitRecords(ChangeData cd) {
     478         103 :     return cd.submitRecords(ChangeJson.SUBMIT_RULE_OPTIONS_LENIENT);
     479             :   }
     480             : 
     481             :   private Map<String, VotingRangeInfo> getPermittedVotingRanges(
     482             :       Map<String, Collection<String>> permittedLabels) {
     483          71 :     Map<String, VotingRangeInfo> permittedVotingRanges =
     484          71 :         Maps.newHashMapWithExpectedSize(permittedLabels.size());
     485          71 :     for (String label : permittedLabels.keySet()) {
     486          71 :       List<Integer> permittedVotingRange =
     487          71 :           permittedLabels.get(label).stream()
     488          71 :               .map(this::parseRangeValue)
     489          71 :               .filter(java.util.Objects::nonNull)
     490          71 :               .sorted()
     491          71 :               .collect(toList());
     492             : 
     493          71 :       if (permittedVotingRange.isEmpty()) {
     494           0 :         permittedVotingRanges.put(label, null);
     495             :       } else {
     496          71 :         int minPermittedValue = permittedVotingRange.get(0);
     497          71 :         int maxPermittedValue = Iterables.getLast(permittedVotingRange);
     498          71 :         permittedVotingRanges.put(label, new VotingRangeInfo(minPermittedValue, maxPermittedValue));
     499             :       }
     500          71 :     }
     501          71 :     return permittedVotingRanges;
     502             :   }
     503             : 
     504             :   @AutoValue
     505         103 :   abstract static class LabelWithStatus {
     506             :     private static LabelWithStatus create(LabelInfo label, SubmitRecord.Label.Status status) {
     507         103 :       return new AutoValue_LabelsJson_LabelWithStatus(label, status);
     508             :     }
     509             : 
     510             :     abstract LabelInfo label();
     511             : 
     512             :     @Nullable
     513             :     abstract SubmitRecord.Label.Status status();
     514             :   }
     515             : }

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