Models

Models in Grammarinator are responsible for making decisions at each branching point while generating test cases. These decisions include selecting an alternative from a set of options, quantifying a subrule, or choosing a character from a character set. By using models, the generation process can be guided while keeping the source grammar clean and separate from the decision-making logic. (Another way of guiding the selection of alternatives is by injecting semantic predicates into the grammar.) A model can be registered with using the --model argument in the grammarinator-generate script or through the constructor of grammarinator.runtime.Generator. If no custom model is specified, the default model (grammarinator.runtime.DefaultModel) will be used.

Grammarinator provides three built-in models, each of which is inherited from the grammarinator.runtime.Model class. The models can be subclassed and further customized according to specific requirements. The built-in models are:

  1. grammarinator.runtime.DefaultModel: This model provides a default implementation for three key functionalities:

    1. Alternative selection: The grammarinator.runtime.DefaultModel.choice() method chooses an alternative based on their assigned weights. The weights are normalized so that their sum is 1, and then they are treated as probabilities.

    2. Subrule quantification: The grammarinator.runtime.DefaultModel.quantify() method generates the minimum required amount of subrules and then decides iteratively whether to generate one more item or terminate quantification. It is implemented as a generator method.

    3. Character selection from a charset: The grammarinator.runtime.DefaultModel.charset() method randomly selects characters from the options with uniform distribution.

Example grammar to represent the usage of DefaultModel
grammar Primitives;

primitive : String | (Decimal | Float) | Bool ;
String : [a-zA-Z] ;
Decimal : [0-9]+;
Float : '0'? '.' [0-9]+ ;
Bool : 'true' | 'false' ;
Example subclassing of DefaultModel
class PrimitiveModel(DefaultModel):

    def choice(self, node, idx, weights):
        # Increase the probability of generating numeric values
        # (decimal and float), i.e., choosing the second alternative.
        if node.name == 'primitive' and idx == 1:
            weights[1] *= 5
        return super().choice(node, idx, weights)

    def quantify(self, node, idx, cnt, start, stop):
        if node.name == 'Float' and idx == 1:
            # Generate floats with two decimal digits at least.
            start = 2
        return super().quantify(node, idx, cnt, start, stop)

    def charset(self, node, idx, chars):
        # Ensure not choosing `0` as the first digit of a decimal.
        if node.name == 'Decimal' and len(node.src) == 0:
            chars = tuple(c for c in chars if c != '0')
        return super().charset(node, idx, chars)
  1. grammarinator.runtime.DispatchingModel: This model is a specialized version of grammarinator.runtime.DefaultModel that allows overriding the default behavior for specific rules. It enables the creation of separate methods for each rule, such as choice_<ruleName>, quantify_<ruleName>, and charset_<ruleName>, to customize their behavior.

The following example shows how the previous snippet would look like with grammarinator.runtime.DispatchingModel:

Example subclassing of DispatchingModel
class PrimitiveModel(DispatchingModel):

    def choice_primitive(self, node, idx, weights):
        # Increase the probability of generating numeric values
        # (decimal and float), i.e., choosing the second alternative.
        if idx == 1:
            weights[1] *= 5
        return super(DispatchingModel, self).choice(node, idx, weights)

    def quantify_Float(self, node, idx, cnt, start, stop):
        if idx == 1:
            # Generate floats with two decimal digits at least.
            start = 2
        return super(DispatchingModel, self).quantify(node, idx, cnt, start, stop)

    def charset_Decimal(self, node, idx, chars):
        # Ensure not choosing `0` as the first digit of a decimal.
        if len(node.src) == 0:
            chars = tuple(c for c in chars if c != '0')
        return super(DispatchingModel, self).charset(node, idx, chars)
  1. grammarinator.runtime.WeightedModel: This model modifies the behavior of another model by adjusting (pre-multiplying) the weights of alternatives and by setting the probability of repeating a quantified subexpression. By default, the multiplier of each alternative starts from 1 and the probability of each quantifier is 0.5, unless custom values are assigned to specific alternatives or quantifiers. This assignment can happen through the constructor of WeightedModel (when using the API) or with the --weigths CLI option of the grammarinator-generate utility by providing a file containing the weights.

    The expected format of the weights differs depending on whether Grammarinator is used from API or from CLI. When using the API, a compact representation is used, which is not JSON serializable. For API usage, refer to the documention of grammarinator.runtime.WeightedModel. When providing weights from the CLI, then the input JSON file should have the following format:

{
  "alts": { "ruleName_A": {"alternation_B_idx": {"alternative_C_idx": weight_ABC, ...}, ...}, ... ,
  "quants": { "ruleName_C": {"quant_D_idx": weight_ABC, ...}, ... ,
}