LCOV - code coverage report
Current view: top level - server/rules - PrologRuleEvaluator.java (source / functions) Hit Total Coverage
Test: _coverage_report.dat Lines: 174 231 75.3 %
Date: 2022-11-19 15:00:39 Functions: 19 23 82.6 %

          Line data    Source code
       1             : // Copyright (C) 2012 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.rules;
      16             : 
      17             : import static com.google.common.base.Preconditions.checkState;
      18             : import static com.google.gerrit.server.project.ProjectCache.illegalState;
      19             : 
      20             : import com.google.common.annotations.VisibleForTesting;
      21             : import com.google.common.base.CharMatcher;
      22             : import com.google.common.flogger.FluentLogger;
      23             : import com.google.gerrit.entities.Account;
      24             : import com.google.gerrit.entities.Change;
      25             : import com.google.gerrit.entities.LabelType;
      26             : import com.google.gerrit.entities.SubmitRecord;
      27             : import com.google.gerrit.entities.SubmitTypeRecord;
      28             : import com.google.gerrit.exceptions.StorageException;
      29             : import com.google.gerrit.extensions.client.SubmitType;
      30             : import com.google.gerrit.server.account.AccountCache;
      31             : import com.google.gerrit.server.account.Accounts;
      32             : import com.google.gerrit.server.account.Emails;
      33             : import com.google.gerrit.server.project.NoSuchProjectException;
      34             : import com.google.gerrit.server.project.ProjectCache;
      35             : import com.google.gerrit.server.project.ProjectState;
      36             : import com.google.gerrit.server.project.RuleEvalException;
      37             : import com.google.gerrit.server.query.change.ChangeData;
      38             : import com.google.inject.assistedinject.Assisted;
      39             : import com.google.inject.assistedinject.AssistedInject;
      40             : import com.googlecode.prolog_cafe.exceptions.CompileException;
      41             : import com.googlecode.prolog_cafe.exceptions.ReductionLimitException;
      42             : import com.googlecode.prolog_cafe.lang.IntegerTerm;
      43             : import com.googlecode.prolog_cafe.lang.ListTerm;
      44             : import com.googlecode.prolog_cafe.lang.Prolog;
      45             : import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
      46             : import com.googlecode.prolog_cafe.lang.StructureTerm;
      47             : import com.googlecode.prolog_cafe.lang.SymbolTerm;
      48             : import com.googlecode.prolog_cafe.lang.Term;
      49             : import com.googlecode.prolog_cafe.lang.VariableTerm;
      50             : import java.io.StringReader;
      51             : import java.util.ArrayList;
      52             : import java.util.Collections;
      53             : import java.util.List;
      54             : 
      55             : /**
      56             :  * Evaluates a submit-like Prolog rule found in the rules.pl file of the current project and filters
      57             :  * the results through rules found in the parent projects, all the way up to All-Projects.
      58             :  */
      59             : public class PrologRuleEvaluator {
      60         104 :   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
      61             : 
      62             :   private static final String DEFAULT_MSG = "Error evaluating project rules, check server log";
      63             : 
      64             :   /**
      65             :    * List of characters to allow in the label name, when an invalid name is used. Dash is allowed as
      66             :    * it can't be the first character: we use a prefix.
      67             :    */
      68         104 :   private static final CharMatcher VALID_LABEL_MATCHER =
      69         104 :       CharMatcher.is('-')
      70         104 :           .or(CharMatcher.inRange('a', 'z'))
      71         104 :           .or(CharMatcher.inRange('A', 'Z'))
      72         104 :           .or(CharMatcher.inRange('0', '9'));
      73             : 
      74             :   public interface Factory {
      75             :     /** Returns a new {@link PrologRuleEvaluator} with the specified options */
      76             :     PrologRuleEvaluator create(ChangeData cd, PrologOptions options);
      77             :   }
      78             : 
      79             :   /**
      80             :    * Exception thrown when the label term of a submit record unexpectedly didn't contain a user
      81             :    * term.
      82             :    */
      83             :   private static class UserTermExpected extends Exception {
      84             :     private static final long serialVersionUID = 1L;
      85             : 
      86             :     UserTermExpected(SubmitRecord.Label label) {
      87           0 :       super(String.format("A label with the status %s must contain a user.", label.toString()));
      88           0 :     }
      89             :   }
      90             : 
      91             :   private final AccountCache accountCache;
      92             :   private final Accounts accounts;
      93             :   private final Emails emails;
      94             :   private final RulesCache rulesCache;
      95             :   private final PrologEnvironment.Factory envFactory;
      96             :   private final ChangeData cd;
      97             :   private final ProjectState projectState;
      98             :   private final PrologOptions opts;
      99             :   private Term submitRule;
     100             : 
     101             :   @SuppressWarnings("UnusedMethod")
     102             :   @AssistedInject
     103             :   private PrologRuleEvaluator(
     104             :       AccountCache accountCache,
     105             :       Accounts accounts,
     106             :       Emails emails,
     107             :       RulesCache rulesCache,
     108             :       PrologEnvironment.Factory envFactory,
     109             :       ProjectCache projectCache,
     110             :       @Assisted ChangeData cd,
     111         103 :       @Assisted PrologOptions options) {
     112         103 :     this.accountCache = accountCache;
     113         103 :     this.accounts = accounts;
     114         103 :     this.emails = emails;
     115         103 :     this.rulesCache = rulesCache;
     116         103 :     this.envFactory = envFactory;
     117         103 :     this.cd = cd;
     118         103 :     this.opts = options;
     119             : 
     120         103 :     this.projectState = projectCache.get(cd.project()).orElseThrow(illegalState(cd.project()));
     121         103 :   }
     122             : 
     123             :   private static Term toListTerm(List<Term> terms) {
     124         103 :     Term list = Prolog.Nil;
     125         103 :     for (int i = terms.size() - 1; i >= 0; i--) {
     126         103 :       list = new ListTerm(terms.get(i), list);
     127             :     }
     128         103 :     return list;
     129             :   }
     130             : 
     131             :   private static boolean isUser(Term who) {
     132           5 :     return who instanceof StructureTerm
     133           5 :         && who.arity() == 1
     134           5 :         && who.name().equals("user")
     135           5 :         && who.arg(0) instanceof IntegerTerm;
     136             :   }
     137             : 
     138             :   private Term getSubmitRule() {
     139           5 :     return submitRule;
     140             :   }
     141             : 
     142             :   /**
     143             :    * Evaluate the submit rules.
     144             :    *
     145             :    * @return {@link SubmitRecord} returned from the evaluated rules. Can include errors.
     146             :    */
     147             :   public SubmitRecord evaluate() {
     148             :     Change change;
     149             :     try {
     150           5 :       change = cd.change();
     151           5 :       if (change == null) {
     152           0 :         throw new StorageException("No change found");
     153             :       }
     154             : 
     155           5 :       if (projectState == null) {
     156           0 :         throw new NoSuchProjectException(cd.project());
     157             :       }
     158           0 :     } catch (StorageException | NoSuchProjectException e) {
     159           0 :       return ruleError("Error looking up change " + cd.getId(), e);
     160           5 :     }
     161             : 
     162           5 :     logger.atFine().log("input approvals: %s", cd.approvals());
     163             : 
     164             :     List<Term> results;
     165             :     try {
     166           5 :       results =
     167           5 :           evaluateImpl(
     168             :               "locate_submit_rule", "can_submit", "locate_submit_filter", "filter_submit_results");
     169           2 :     } catch (RuleEvalException e) {
     170           2 :       return ruleError(e.getMessage(), e);
     171           5 :     }
     172             : 
     173           5 :     if (results.isEmpty()) {
     174             :       // This should never occur. A well written submit rule will always produce
     175             :       // at least one result informing the caller of the labels that are
     176             :       // required for this change to be submittable. Each label will indicate
     177             :       // whether or not that is actually possible given the permissions.
     178           1 :       return ruleError(
     179           1 :           String.format(
     180             :               "Submit rule '%s' for change %s of %s has no solution.",
     181           1 :               getSubmitRuleName(), cd.getId(), projectState.getName()));
     182             :     }
     183             : 
     184           5 :     SubmitRecord submitRecord = resultsToSubmitRecord(getSubmitRule(), results);
     185           5 :     logger.atFine().log("submit record: %s", submitRecord);
     186           5 :     return submitRecord;
     187             :   }
     188             : 
     189             :   private String getSubmitRuleName() {
     190           1 :     return submitRule == null ? "<unknown>" : submitRule.name();
     191             :   }
     192             : 
     193             :   /**
     194             :    * Convert the results from Prolog Cafe's format to Gerrit's common format.
     195             :    *
     196             :    * <p>can_submit/1 terminates when an ok(P) record is found. Therefore walk the results backwards,
     197             :    * using only that ok(P) record if it exists. This skips partial results that occur early in the
     198             :    * output. Later after the loop the out collection is reversed to restore it to the original
     199             :    * ordering.
     200             :    */
     201             :   public SubmitRecord resultsToSubmitRecord(Term submitRule, List<Term> results) {
     202           5 :     checkState(!results.isEmpty(), "the list of Prolog terms must not be empty");
     203             : 
     204           5 :     SubmitRecord resultSubmitRecord = new SubmitRecord();
     205           5 :     resultSubmitRecord.labels = new ArrayList<>();
     206           5 :     for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
     207           5 :       Term submitRecord = results.get(resultIdx);
     208             : 
     209           5 :       if (!(submitRecord instanceof StructureTerm) || 1 != submitRecord.arity()) {
     210           0 :         return invalidResult(submitRule, submitRecord);
     211             :       }
     212             : 
     213           5 :       if (!"ok".equals(submitRecord.name()) && !"not_ready".equals(submitRecord.name())) {
     214           0 :         return invalidResult(submitRule, submitRecord);
     215             :       }
     216             : 
     217             :       // This transformation is required to adapt Prolog's behavior to the way Gerrit handles
     218             :       // SubmitRecords, as defined in the SubmitRecord#allRecordsOK method.
     219             :       // When several rules are defined in Prolog, they are all matched to a SubmitRecord. We want
     220             :       // the change to be submittable when at least one result is OK.
     221           5 :       if ("ok".equals(submitRecord.name())) {
     222           5 :         resultSubmitRecord.status = SubmitRecord.Status.OK;
     223           5 :       } else if ("not_ready".equals(submitRecord.name()) && resultSubmitRecord.status == null) {
     224           5 :         resultSubmitRecord.status = SubmitRecord.Status.NOT_READY;
     225             :       }
     226             : 
     227             :       // Unpack the one argument. This should also be a structure with one
     228             :       // argument per label that needs to be reported on to the caller.
     229             :       //
     230           5 :       submitRecord = submitRecord.arg(0);
     231             : 
     232           5 :       if (!(submitRecord instanceof StructureTerm)) {
     233           0 :         return invalidResult(submitRule, submitRecord);
     234             :       }
     235             : 
     236           5 :       for (Term state : ((StructureTerm) submitRecord).args()) {
     237           5 :         if (!(state instanceof StructureTerm)
     238           5 :             || 2 != state.arity()
     239           5 :             || !"label".equals(state.name())) {
     240           0 :           return invalidResult(submitRule, submitRecord);
     241             :         }
     242             : 
     243           5 :         SubmitRecord.Label lbl = new SubmitRecord.Label();
     244           5 :         resultSubmitRecord.labels.add(lbl);
     245             : 
     246           5 :         lbl.label = checkLabelName(state.arg(0).name());
     247           5 :         Term status = state.arg(1);
     248             : 
     249             :         try {
     250           5 :           if ("ok".equals(status.name())) {
     251           5 :             lbl.status = SubmitRecord.Label.Status.OK;
     252           5 :             appliedBy(lbl, status);
     253             : 
     254           5 :           } else if ("reject".equals(status.name())) {
     255           1 :             lbl.status = SubmitRecord.Label.Status.REJECT;
     256           1 :             appliedBy(lbl, status);
     257             : 
     258           5 :           } else if ("need".equals(status.name())) {
     259           4 :             lbl.status = SubmitRecord.Label.Status.NEED;
     260             : 
     261           1 :           } else if ("may".equals(status.name())) {
     262           1 :             lbl.status = SubmitRecord.Label.Status.MAY;
     263             : 
     264           0 :           } else if ("impossible".equals(status.name())) {
     265           0 :             lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
     266             : 
     267             :           } else {
     268           0 :             return invalidResult(submitRule, submitRecord);
     269             :           }
     270           0 :         } catch (UserTermExpected e) {
     271           0 :           return invalidResult(submitRule, submitRecord, e.getMessage());
     272           5 :         }
     273             :       }
     274             : 
     275           5 :       if (resultSubmitRecord.status == SubmitRecord.Status.OK) {
     276           5 :         break;
     277             :       }
     278             :     }
     279           5 :     Collections.reverse(resultSubmitRecord.labels);
     280           5 :     return resultSubmitRecord;
     281             :   }
     282             : 
     283             :   @VisibleForTesting
     284             :   static String checkLabelName(String name) {
     285             :     try {
     286           6 :       return LabelType.checkName(name);
     287           1 :     } catch (IllegalArgumentException e) {
     288           1 :       String newName = "Invalid-Prolog-Rules-Label-Name-" + sanitizeLabelName(name);
     289           1 :       return LabelType.checkName(newName.replace("--", "-"));
     290             :     }
     291             :   }
     292             : 
     293             :   private static String sanitizeLabelName(String name) {
     294           1 :     return VALID_LABEL_MATCHER.retainFrom(name);
     295             :   }
     296             : 
     297             :   private static SubmitRecord createRuleError(String err) {
     298           2 :     SubmitRecord rec = new SubmitRecord();
     299           2 :     rec.status = SubmitRecord.Status.RULE_ERROR;
     300           2 :     rec.errorMessage = err;
     301           2 :     return rec;
     302             :   }
     303             : 
     304             :   private SubmitRecord invalidResult(Term rule, Term record, String reason) {
     305           0 :     return ruleError(
     306           0 :         String.format(
     307             :             "Submit rule %s for change %s of %s output invalid result: %s%s",
     308             :             rule,
     309           0 :             cd.getId(),
     310           0 :             cd.project().get(),
     311             :             record,
     312           0 :             (reason == null ? "" : ". Reason: " + reason)));
     313             :   }
     314             : 
     315             :   private SubmitRecord invalidResult(Term rule, Term record) {
     316           0 :     return invalidResult(rule, record, null);
     317             :   }
     318             : 
     319             :   private SubmitRecord ruleError(String err) {
     320           1 :     return ruleError(err, null);
     321             :   }
     322             : 
     323             :   private SubmitRecord ruleError(String err, Exception e) {
     324           2 :     if (opts.logErrors()) {
     325           1 :       logger.atSevere().withCause(e).log("%s", err);
     326           1 :       return createRuleError(DEFAULT_MSG);
     327             :     }
     328           1 :     logger.atFine().log("rule error: %s", err);
     329           1 :     return createRuleError(err);
     330             :   }
     331             : 
     332             :   /**
     333             :    * Evaluate the submit type rules to get the submit type.
     334             :    *
     335             :    * @return record from the evaluated rules.
     336             :    */
     337             :   public SubmitTypeRecord getSubmitType() {
     338             :     try {
     339         103 :       if (projectState == null) {
     340           0 :         throw new NoSuchProjectException(cd.project());
     341             :       }
     342           0 :     } catch (NoSuchProjectException e) {
     343           0 :       return typeError("Error looking up change " + cd.getId(), e);
     344         103 :     }
     345             : 
     346             :     List<Term> results;
     347             :     try {
     348         103 :       results =
     349         103 :           evaluateImpl(
     350             :               "locate_submit_type",
     351             :               "get_submit_type",
     352             :               "locate_submit_type_filter",
     353             :               "filter_submit_type_results");
     354           1 :     } catch (RuleEvalException e) {
     355           1 :       return typeError(e.getMessage(), e);
     356         103 :     }
     357             : 
     358         103 :     if (results.isEmpty()) {
     359             :       // Should never occur for a well written rule
     360           0 :       return typeError(
     361             :           "Submit rule '"
     362           0 :               + getSubmitRuleName()
     363             :               + "' for change "
     364           0 :               + cd.getId()
     365             :               + " of "
     366           0 :               + projectState.getName()
     367             :               + " has no solution.");
     368             :     }
     369             : 
     370         103 :     Term typeTerm = results.get(0);
     371         103 :     if (!(typeTerm instanceof SymbolTerm)) {
     372           0 :       return typeError(
     373             :           "Submit rule '"
     374           0 :               + getSubmitRuleName()
     375             :               + "' for change "
     376           0 :               + cd.getId()
     377             :               + " of "
     378           0 :               + projectState.getName()
     379             :               + " did not return a symbol.");
     380             :     }
     381             : 
     382         103 :     String typeName = typeTerm.name();
     383             :     try {
     384         103 :       return SubmitTypeRecord.OK(SubmitType.valueOf(typeName.toUpperCase()));
     385           0 :     } catch (IllegalArgumentException e) {
     386           0 :       return typeError(
     387             :           "Submit type rule "
     388           0 :               + getSubmitRule()
     389             :               + " for change "
     390           0 :               + cd.getId()
     391             :               + " of "
     392           0 :               + projectState.getName()
     393             :               + " output invalid result: "
     394             :               + typeName);
     395             :     }
     396             :   }
     397             : 
     398             :   private SubmitTypeRecord typeError(String err) {
     399           0 :     return typeError(err, null);
     400             :   }
     401             : 
     402             :   private SubmitTypeRecord typeError(String err, Exception e) {
     403           1 :     if (opts.logErrors()) {
     404           1 :       logger.atSevere().withCause(e).log("%s", err);
     405             :     }
     406           1 :     return SubmitTypeRecord.error(err);
     407             :   }
     408             : 
     409             :   private List<Term> evaluateImpl(
     410             :       String userRuleLocatorName,
     411             :       String userRuleWrapperName,
     412             :       String filterRuleLocatorName,
     413             :       String filterRuleWrapperName)
     414             :       throws RuleEvalException {
     415         103 :     PrologEnvironment env = getPrologEnvironment();
     416             :     try {
     417         103 :       Term sr = env.once("gerrit", userRuleLocatorName, new VariableTerm());
     418         103 :       List<Term> results = new ArrayList<>();
     419             :       try {
     420         103 :         for (Term[] template : env.all("gerrit", userRuleWrapperName, sr, new VariableTerm())) {
     421         103 :           results.add(template[1]);
     422         103 :         }
     423           0 :       } catch (ReductionLimitException err) {
     424           0 :         throw new RuleEvalException(
     425           0 :             String.format(
     426             :                 "%s on change %d of %s",
     427           0 :                 err.getMessage(), cd.getId().get(), projectState.getName()));
     428           0 :       } catch (RuntimeException err) {
     429           0 :         throw new RuleEvalException(
     430           0 :             String.format(
     431             :                 "Exception calling %s on change %d of %s",
     432           0 :                 sr, cd.getId().get(), projectState.getName()),
     433             :             err);
     434         103 :       }
     435             : 
     436         103 :       Term resultsTerm = toListTerm(results);
     437         103 :       if (!opts.skipFilters()) {
     438         103 :         resultsTerm =
     439         103 :             runSubmitFilters(resultsTerm, env, filterRuleLocatorName, filterRuleWrapperName);
     440             :       }
     441             :       List<Term> r;
     442         103 :       if (resultsTerm instanceof ListTerm) {
     443         103 :         r = new ArrayList<>();
     444         103 :         for (Term t = resultsTerm; t instanceof ListTerm; ) {
     445         103 :           ListTerm l = (ListTerm) t;
     446         103 :           r.add(l.car().dereference());
     447         103 :           t = l.cdr().dereference();
     448         103 :         }
     449             :       } else {
     450           1 :         r = Collections.emptyList();
     451             :       }
     452         103 :       submitRule = sr;
     453         103 :       return r;
     454             :     } finally {
     455         103 :       env.close();
     456             :     }
     457             :   }
     458             : 
     459             :   private PrologEnvironment getPrologEnvironment() throws RuleEvalException {
     460             :     PrologEnvironment env;
     461             :     try {
     462             :       PrologMachineCopy pmc;
     463         103 :       if (opts.rule().isPresent()) {
     464           1 :         pmc = rulesCache.loadMachine("stdin", new StringReader(opts.rule().get()));
     465             :       } else {
     466         103 :         pmc =
     467         103 :             rulesCache.loadMachine(
     468         103 :                 projectState.getNameKey(), projectState.getConfig().getRulesId().orElse(null));
     469             :       }
     470         103 :       env = envFactory.create(pmc);
     471           2 :     } catch (CompileException err) {
     472             :       String msg;
     473           2 :       if (opts.rule().isPresent()) {
     474           1 :         msg = err.getMessage();
     475             :       } else {
     476           1 :         msg =
     477           1 :             String.format(
     478           1 :                 "Cannot load rules.pl for %s: %s", projectState.getName(), err.getMessage());
     479             :       }
     480           2 :       throw new RuleEvalException(msg, err);
     481         103 :     }
     482         103 :     env.set(StoredValues.ACCOUNTS, accounts);
     483         103 :     env.set(StoredValues.ACCOUNT_CACHE, accountCache);
     484         103 :     env.set(StoredValues.EMAILS, emails);
     485         103 :     env.set(StoredValues.CHANGE_DATA, cd);
     486         103 :     env.set(StoredValues.PROJECT_STATE, projectState);
     487         103 :     return env;
     488             :   }
     489             : 
     490             :   private Term runSubmitFilters(
     491             :       Term results,
     492             :       PrologEnvironment env,
     493             :       String filterRuleLocatorName,
     494             :       String filterRuleWrapperName)
     495             :       throws RuleEvalException {
     496         103 :     PrologEnvironment childEnv = env;
     497         103 :     ChangeData cd = env.get(StoredValues.CHANGE_DATA);
     498         103 :     ProjectState projectState = env.get(StoredValues.PROJECT_STATE);
     499         103 :     for (ProjectState parentState : projectState.parents()) {
     500             :       PrologEnvironment parentEnv;
     501             :       try {
     502         102 :         parentEnv =
     503         102 :             envFactory.create(
     504         102 :                 rulesCache.loadMachine(
     505         102 :                     parentState.getNameKey(), parentState.getConfig().getRulesId().orElse(null)));
     506           0 :       } catch (CompileException err) {
     507           0 :         throw new RuleEvalException("Cannot consult rules.pl for " + parentState.getName(), err);
     508         102 :       }
     509             : 
     510         102 :       parentEnv.copyStoredValues(childEnv);
     511         102 :       Term filterRule = parentEnv.once("gerrit", filterRuleLocatorName, new VariableTerm());
     512             :       try {
     513         102 :         Term[] template =
     514         102 :             parentEnv.once(
     515             :                 "gerrit", filterRuleWrapperName, filterRule, results, new VariableTerm());
     516         102 :         results = template[2];
     517           0 :       } catch (ReductionLimitException err) {
     518           0 :         throw new RuleEvalException(
     519           0 :             String.format(
     520             :                 "%s on change %d of %s",
     521           0 :                 err.getMessage(), cd.getId().get(), parentState.getName()));
     522           0 :       } catch (RuntimeException err) {
     523           0 :         throw new RuleEvalException(
     524           0 :             String.format(
     525             :                 "Exception calling %s on change %d of %s",
     526           0 :                 filterRule, cd.getId().get(), parentState.getName()),
     527             :             err);
     528         102 :       }
     529         102 :       childEnv = parentEnv;
     530         102 :     }
     531         103 :     return results;
     532             :   }
     533             : 
     534             :   private void appliedBy(SubmitRecord.Label label, Term status) throws UserTermExpected {
     535           5 :     if (status instanceof StructureTerm && status.arity() == 1) {
     536           5 :       Term who = status.arg(0);
     537           5 :       if (isUser(who)) {
     538           5 :         label.appliedBy = Account.id(((IntegerTerm) who.arg(0)).intValue());
     539             :       } else {
     540           0 :         throw new UserTermExpected(label);
     541             :       }
     542             :     }
     543           5 :   }
     544             : }

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