• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

renatahodovan / grammarinator / 12298256429

12 Dec 2024 02:17PM UTC coverage: 85.518% (-0.5%) from 86.052%
12298256429

Pull #259

github

web-flow
Merge dd42a94be into f5de911fe
Pull Request #259: Don't import importlib_metadata since Py3.8 already has it as importlib.metadata

1 of 1 new or added line in 1 file covered. (100.0%)

14 existing lines in 3 files now uncovered.

1990 of 2327 relevant lines covered (85.52%)

0.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.16
/grammarinator/tool/generator.py
1
# Copyright (c) 2017-2024 Renata Hodovan, Akos Kiss.
2
#
3
# Licensed under the BSD 3-Clause License
4
# <LICENSE.rst or https://opensource.org/licenses/BSD-3-Clause>.
5
# This file may not be copied, modified, or distributed except
6
# according to those terms.
7

8
import codecs
1✔
9
import logging
1✔
10
import os
1✔
11
import random
1✔
12

13
from contextlib import nullcontext
1✔
14
from copy import deepcopy
1✔
15
from os.path import abspath, dirname
1✔
16
from shutil import rmtree
1✔
17

18
from ..runtime import DefaultModel, RuleSize, WeightedModel
1✔
19

20
logger = logging.getLogger(__name__)
1✔
21

22

23
class GeneratorFactory:
1✔
24
    """
25
    Base class of generator factories. A generator factory is a generalization
26
    of a generator class. It has to be a callable that, when called, must return
27
    a generator instance. It must also expose some properties of the generator
28
    class it generalizes that are required to guide generation or mutation by
29
    :class:`GeneratorTool`.
30

31
    This factory generalizes a generator class by simply wrapping it and
32
    forwarding call operations to instantiations of the wrapped class.
33
    Furthermore, generator factories deriving from this base class are
34
    guaranteed to expose all the required generator class properties.
35
    """
36

37
    def __init__(self, generator_class):
1✔
38
        """
39
        :param type[~grammarinator.runtime.Generator] generator_class: The class
40
            of the wrapped generator.
41

42
        :ivar type[~grammarinator.runtime.Generator] _generator_class: The class
43
            of the wrapped generator.
44
        """
45
        self._generator_class = generator_class
1✔
46
        # Exposing some class variables of the encapsulated generator.
47
        # In the generator class, they start with `_` to avoid any kind of
48
        # collision with rule names, so they start with `_` here as well.
49
        self._rule_sizes = generator_class._rule_sizes
1✔
50
        self._alt_sizes = generator_class._alt_sizes
1✔
51
        self._quant_sizes = generator_class._quant_sizes
1✔
52

53
    def __call__(self, limit=None):
1✔
54
        """
55
        Create a new generator instance.
56

57
        :param RuleSize limit: The limit on the depth of the trees and on the
58
            number of tokens (number of unlexer rule calls), i.e., it must be
59
            possible to finish generation from the selected node so that the
60
            overall depth and token count of the tree does not exceed these
61
            limits (default: :class:`~grammarinator.runtime.RuleSize`. ``max``).
62
            Used to instantiate the generator.
63
        :return: The created generator instance.
64
        :rtype: ~grammarinator.runtime.Generator
65
        """
66
        return self._generator_class(limit=limit)
×
67

68

69
class DefaultGeneratorFactory(GeneratorFactory):
1✔
70
    """
71
    The default generator factory implementation. When called, a new generator
72
    instance is created backed by a new decision model instance and a set of
73
    newly created listener objects is attached.
74
    """
75

76
    def __init__(self, generator_class, *,
1✔
77
                 model_class=None, weights=None,
78
                 listener_classes=None):
79
        """
80
        :param type[~grammarinator.runtime.Generator] generator_class: The class
81
            of the generator to instantiate.
82
        :param type[~grammarinator.runtime.Model] model_class: The class of the
83
            model to instantiate. The model instance is used to instantiate the
84
            generator.
85
        :param dict[tuple,float] weights: Initial multipliers of alternatives.
86
            Used to instantiate a :class:`~grammarinator.runtime.WeightedModel`
87
            wrapper around the model.
88
        :param list[type[~grammarinator.runtime.Listener]] listener_classes:
89
            List of listener classes to instantiate and attach to the generator.
90
        """
91
        super().__init__(generator_class)
1✔
92
        self._model_class = model_class or DefaultModel
1✔
93
        self._weights = weights
1✔
94
        self._listener_classes = listener_classes or []
1✔
95

96
    def __call__(self, limit=None):
1✔
97
        """
98
        Create a new generator instance according to the settings specified for
99
        the factory instance and for this method.
100

101
        :param RuleSize limit: The limit on the depth of the trees and on the
102
            number of tokens (number of unlexer rule calls), i.e., it must be
103
            possible to finish generation from the selected node so that the
104
            overall depth and token count of the tree does not exceed these
105
            limits (default: :class:`~grammarinator.runtime.RuleSize`. ``max``).
106
            Used to instantiate the generator.
107
        :return: The created generator instance.
108
        :rtype: ~grammarinator.runtime.Generator
109
        """
110
        model = self._model_class()
1✔
111
        if self._weights:
1✔
112
            model = WeightedModel(model, weights=self._weights)
1✔
113

114
        listeners = []
1✔
115
        for listener_class in self._listener_classes:
1✔
116
            listeners.append(listener_class())
1✔
117

118
        generator = self._generator_class(model=model, listeners=listeners, limit=limit)
1✔
119

120
        return generator
1✔
121

122

123
class GeneratorTool:
1✔
124
    """
125
    Tool to create new test cases using the generator produced by ``grammarinator-process``.
126
    """
127

128
    def __init__(self, generator_factory, out_format, lock=None, rule=None, limit=None,
1✔
129
                 population=None, generate=True, mutate=True, recombine=True, keep_trees=False,
130
                 transformers=None, serializer=None,
131
                 cleanup=True, encoding='utf-8', errors='strict', dry_run=False):
132
        """
133
        :param type[~grammarinator.runtime.Generator] or GeneratorFactory generator_factory:
134
            A callable that can produce instances of a generator. It is a
135
            generalization of a generator class: it has to instantiate a
136
            generator object, and it may also set the decision model and the
137
            listeners of the generator as well. It also has to expose some
138
            properties of the generator class necessary to guide generation or
139
            mutation. In the simplest case, it can be a
140
            ``grammarinator-process``-created subclass of
141
            :class:`~grammarinator.runtime.Generator`, but in more complex
142
            scenarios a factory can be used, e.g., an instance of a subclass of
143
            :class:`GeneratorFactory`, like :class:`DefaultGeneratorFactory`.
144
        :param str rule: Name of the rule to start generation from (default: the
145
            default rule of the generator).
146
        :param str out_format: Test output description. It can be a file path pattern possibly including the ``%d``
147
               placeholder which will be replaced by the index of the test case. Otherwise, it can be an empty string,
148
               which will result in printing the test case to the stdout (i.e., not saving to file system).
149
        :param multiprocessing.Lock lock: Lock object necessary when printing test cases in parallel (optional).
150
        :param RuleSize limit: The limit on the depth of the trees and on the
151
            number of tokens (number of unlexer rule calls), i.e., it must be
152
            possible to finish generation from the selected node so that the
153
            overall depth and token count of the tree does not exceed these
154
            limits (default: :class:`~grammarinator.runtime.RuleSize`. ``max``).
155
        :param ~grammarinator.runtime.Population population: Tree pool for
156
            mutation and recombination, e.g., an instance of
157
            :class:`DefaultPopulation`.
158
        :param bool generate: Enable generating new test cases from scratch, i.e., purely based on grammar.
159
        :param bool mutate: Enable mutating existing test cases, i.e., re-generate part of an existing test case based on grammar.
160
        :param bool recombine: Enable recombining existing test cases, i.e., replace part of a test case with a compatible part from another test case.
161
        :param bool keep_trees: Keep generated trees to participate in further mutations or recombinations
162
               (otherwise, only the initial population will be mutated or recombined). It has effect only if
163
               population is defined.
164
        :param list transformers: List of transformers to be applied to postprocess
165
               the generated tree before serializing it.
166
        :param serializer: A serializer that takes a tree and produces a string from it (default: :class:`str`).
167
               See :func:`grammarinator.runtime.simple_space_serializer` for a simple solution that concatenates tokens with spaces.
168
        :param bool cleanup: Enable deleting the generated tests at :meth:`__exit__`.
169
        :param str encoding: Output file encoding.
170
        :param str errors: Encoding error handling scheme.
171
        :param bool dry_run: Enable or disable the saving or printing of the result of generation.
172
        """
173

174
        self._generator_factory = generator_factory
1✔
175
        self._transformers = transformers or []
1✔
176
        self._serializer = serializer or str
1✔
177
        self._rule = rule
1✔
178

179
        if out_format and not dry_run:
1✔
180
            os.makedirs(abspath(dirname(out_format)), exist_ok=True)
1✔
181

182
        self._out_format = out_format
1✔
183
        self._lock = lock or nullcontext()
1✔
184
        self._limit = limit or RuleSize.max
1✔
185
        self._population = population
1✔
186
        self._enable_generation = generate
1✔
187
        self._enable_mutation = mutate
1✔
188
        self._enable_recombination = recombine
1✔
189
        self._keep_trees = keep_trees
1✔
190
        self._cleanup = cleanup
1✔
191
        self._encoding = encoding
1✔
192
        self._errors = errors
1✔
193
        self._dry_run = dry_run
1✔
194

195
        self._generators = [self.generate]
1✔
196
        self._mutators = [
1✔
197
            self.regenerate_rule,
198
            self.delete_quantified,
199
            self.replicate_quantified,
200
            self.shuffle_quantifieds,
201
            self.hoist_rule,
202
        ]
203
        self._recombiners = [
1✔
204
            self.replace_node,
205
            self.insert_quantified,
206
        ]
207

208
    def __enter__(self):
1✔
209
        return self
1✔
210

211
    def __exit__(self, exc_type, exc_val, exc_tb):
1✔
212
        """
213
        Delete the output directory if the tests were saved to files and if ``cleanup`` was enabled.
214
        """
215
        if self._cleanup and self._out_format and not self._dry_run:
1✔
216
            rmtree(dirname(self._out_format))
×
217

218
    def create_test(self, index):
1✔
219
        """
220
        Create a new test case with a randomly selected generator method from the
221
        available options (see :meth:`generate`, :meth:`mutate`, and
222
        :meth:`recombine`). The generated tree is transformed, serialized and saved
223
        according to the parameters used to initialize the current tool object.
224

225
        :param int index: Index of the test case to be generated.
226
        :return: Path to the generated serialized test file. It may be empty if
227
            the tool object was initialized with an empty ``out_format`` or
228
            ``None`` if ``dry_run`` was enabled, and hence the test file was not
229
            saved.
230
        :rtype: str
231
        """
232
        root = self.create()
1✔
233
        test = self._serializer(root)
1✔
234
        if self._dry_run:
1✔
235
            return None
×
236

237
        test_fn = self._out_format % index if '%d' in self._out_format else self._out_format
1✔
238

239
        if self._population is not None and self._keep_trees:
1✔
240
            self._population.add_individual(root, path=test_fn)
1✔
241

242
        if test_fn:
1✔
243
            with codecs.open(test_fn, 'w', self._encoding, self._errors) as f:
1✔
244
                f.write(test)
1✔
245
        else:
246
            with self._lock:
1✔
247
                print(test)
1✔
248

249
        return test_fn
1✔
250

251
    def _select_creator(self, creators, individual1, individual2):  # pylint: disable=unused-argument
1✔
252
        # NOTE: May be overridden.
253
        return random.choice(creators)
1✔
254

255
    def _create_tree(self, creators, individual1, individual2):
1✔
256
        creator = self._select_creator(creators, individual1, individual2)
1✔
257
        root = creator(individual1, individual2)
1✔
258
        for transformer in self._transformers:
1✔
259
            root = transformer(root)
×
260
        return root
1✔
261

262
    def create(self):
1✔
263
        """
264
        Create a new tree with a randomly selected generator method from the
265
        available options (see :meth:`generate`, :meth:`mutate`, and
266
        :meth:`recombine`). The generated tree is also transformed according to the
267
        parameters used to initialize the current tool object.
268

269
        :return: The root of the created tree.
270
        :rtype: Rule
271
        """
272
        individual1, individual2 = (self._ensure_individuals(None, None)) if self._population else (None, None)
1✔
273
        creators = []
1✔
274
        if self._enable_generation:
1✔
275
            creators.extend(self._generators)
1✔
276
        if self._population:
1✔
277
            if self._enable_mutation:
1✔
278
                creators.extend(self._mutators)
1✔
279
            if self._enable_recombination:
1✔
280
                creators.extend(self._recombiners)
1✔
281
        return self._create_tree(creators, individual1, individual2)
1✔
282

283
    def mutate(self, individual=None):
1✔
284
        """
285
        Dispatcher method for mutation operators: it picks one operator randomly and
286
        creates a new tree by applying the operator to an individual. The generated
287
        tree is also transformed according to the parameters used to initialize the
288
        current tool object.
289

290
        Supported mutation operators: :meth:`regenerate_rule`,
291
        :meth:`delete_quantified`, :meth:`replicate_quantified`,
292
        :meth:`shuffle_quantifieds`, :meth:`hoist_rule`
293

294
        :param ~grammarinator.runtime.Individual individual: The population item to
295
            be mutated.
296
        :return: The root of the mutated tree.
297
        :rtype: Rule
298
        """
299
        # NOTE: Intentionally does not check self._enable_mutation!
300
        # If you call this explicitly, then so be it, even if mutation is disabled.
301
        # If individual is None, population MUST exist.
302
        individual = self._ensure_individual(individual)
×
303
        return self._create_tree(self._mutators, individual, None)
×
304

305
    def recombine(self, individual1=None, individual2=None):
1✔
306
        """
307
        Dispatcher method for recombination operators: it picks one operator
308
        randomly and creates a new tree by applying the operator to an individual.
309
        The generated tree is also transformed according to the parameters used to
310
        initialize the current tool object.
311

312
        Supported recombination operators: :meth:`replace_node`,
313
        :meth:`insert_quantified`
314

315
        :param ~grammarinator.runtime.Individual individual1: The population item to
316
            be used as a recipient during crossover.
317
        :param ~grammarinator.runtime.Individual individual2: The population item to
318
            be used as a donor during crossover.
319
        :return: The root of the recombined tree.
320
        :rtype: Rule
321
        """
322
        # NOTE: Intentionally does not check self._enable_recombination!
323
        # If you call this explicitly, then so be it, even if recombination is disabled.
324
        # If any of the individuals is None, population MUST exist.
325
        individual1, individual2 = self._ensure_individuals(individual1, individual2)
×
326
        return self._create_tree(self._recombiners, individual1, individual2)
×
327

328
    def generate(self, _individual1=None, _individual2=None, *, rule=None, reserve=None):
1✔
329
        """
330
        Instantiate a new generator and generate a new tree from scratch.
331

332
        :param str rule: Name of the rule to start generation from.
333
        :param RuleSize reserve: Size budget that needs to be put in reserve
334
            before generating the tree. Practically, deduced from the initially
335
            specified limit. (default values: 0, 0)
336
        :return: The root of the generated tree.
337
        :rtype: Rule
338
        """
339
        # NOTE: Intentionally does not check self._enable_generation!
340
        # If you call this explicitly, then so be it, even if generation is disabled.
341
        reserve = reserve if reserve is not None else RuleSize()
1✔
342
        generator = self._generator_factory(limit=self._limit - reserve)
1✔
343
        rule = rule or self._rule or generator._default_rule.__name__
1✔
344
        return getattr(generator, rule)()
1✔
345

346
    def _ensure_individual(self, individual):
1✔
347
        return individual or self._population.select_individual()
1✔
348

349
    def _ensure_individuals(self, individual1, individual2):
1✔
350
        individual1 = self._ensure_individual(individual1)
1✔
351
        individual2 = individual2 or self._population.select_individual()
1✔
352
        return individual1, individual2
1✔
353

354
    def regenerate_rule(self, individual=None, _=None):
1✔
355
        """
356
        Mutate a tree at a random position, i.e., discard and re-generate its
357
        sub-tree at a randomly selected node.
358

359
        :param ~grammarinator.runtime.Individual individual: The population item to be mutated.
360
        :return: The root of the mutated tree.
361
        :rtype: Rule
362
        """
UNCOV
363
        individual = self._ensure_individual(individual)
×
UNCOV
364
        root, annot = individual.root, individual.annotations
×
365

366
        # Filter items from the nodes of the selected tree that can be regenerated
367
        # within the current maximum depth and token limit (except immutable nodes).
UNCOV
368
        root_token_counts = annot.token_counts[root]
×
UNCOV
369
        options = [node for nodes in annot.rules_by_name.values() for node in nodes
×
370
                   if (node.parent is not None
371
                       and annot.node_levels[node] + self._generator_factory._rule_sizes.get(node.name, RuleSize(0, 0)).depth < self._limit.depth
372
                       and root_token_counts - annot.token_counts[node] + self._generator_factory._rule_sizes.get(node.name, RuleSize(0, 0)).tokens < self._limit.tokens)]
UNCOV
373
        if options:
×
UNCOV
374
            mutated_node = random.choice(options)
×
UNCOV
375
            reserve = RuleSize(depth=annot.node_levels[mutated_node],
×
376
                               tokens=root_token_counts - annot.token_counts[mutated_node])
UNCOV
377
            mutated_node = mutated_node.replace(self.generate(rule=mutated_node.name, reserve=reserve))
×
UNCOV
378
            return mutated_node.root
×
379

380
        # If selection strategy fails, we fallback and discard the whole tree
381
        # and generate a brand new one instead.
382
        return self.generate(rule=root.name)
×
383

384
    def replace_node(self, recipient_individual=None, donor_individual=None):
1✔
385
        """
386
        Recombine two trees at random positions where the nodes are compatible
387
        with each other (i.e., they share the same node name). One of the trees
388
        is called the recipient while the other is the donor. The sub-tree
389
        rooted at a random node of the recipient is discarded and replaced
390
        by the sub-tree rooted at a random node of the donor.
391

392
        :param ~grammarinator.runtime.Individual recipient_individual:
393
            The population item to be used as a recipient during crossover.
394
        :param ~grammarinator.runtime.Individual donor_individual:
395
            The population item to be used as a donor during crossover.
396
        :return: The root of the recombined tree.
397
        :rtype: Rule
398
        """
399
        recipient_individual, donor_individual = self._ensure_individuals(recipient_individual, donor_individual)
1✔
400
        recipient_root, recipient_annot = recipient_individual.root, recipient_individual.annotations
1✔
401
        donor_annot = donor_individual.annotations
1✔
402

403
        recipient_lookup = dict(recipient_annot.rules_by_name)
1✔
404
        recipient_lookup.update(recipient_annot.quants_by_name)
1✔
405
        recipient_lookup.update(recipient_annot.alts_by_name)
1✔
406

407
        donor_lookup = dict(donor_annot.rules_by_name)
1✔
408
        donor_lookup.update(donor_annot.quants_by_name)
1✔
409
        donor_lookup.update(donor_annot.alts_by_name)
1✔
410
        common_types = sorted(set(recipient_lookup.keys()) & set(donor_lookup.keys()))
1✔
411

412
        recipient_options = [(rule_name, node) for rule_name in common_types for node in recipient_lookup[rule_name] if node.parent]
1✔
413
        recipient_root_token_counts = recipient_annot.token_counts[recipient_root]
1✔
414
        # Shuffle suitable nodes with sample.
415
        for rule_name, recipient_node in random.sample(recipient_options, k=len(recipient_options)):
1✔
416
            donor_options = donor_lookup[rule_name]
1✔
417
            recipient_node_level = recipient_annot.node_levels[recipient_node]
1✔
418
            recipient_node_tokens = recipient_annot.token_counts[recipient_node]
1✔
419
            for donor_node in random.sample(donor_options, k=len(donor_options)):
1✔
420
                # Make sure that the output tree won't exceed the depth limit.
421
                if (recipient_node_level + donor_annot.node_depths[donor_node] <= self._limit.depth
1✔
422
                        and recipient_root_token_counts - recipient_node_tokens + donor_annot.token_counts[donor_node] < self._limit.tokens):
423
                    recipient_node.replace(donor_node)
1✔
424
                    return recipient_root
1✔
425

426
        # If selection strategy fails, we practically cause the whole recipient tree
427
        # to be the result of recombination.
428
        return recipient_root
×
429

430
    def insert_quantified(self, recipient_individual=None, donor_individual=None):
1✔
431
        """
432
        Selects two compatible quantifier nodes from two trees randomly and if
433
        the quantifier node of the recipient tree is not full (the number of
434
        its children is less than the maximum count), then add one new child
435
        to it at a random position from the children of donors quantifier node.
436

437
        :param ~grammarinator.runtime.Individual recipient_individual:
438
            The population item to be used as a recipient during crossover.
439
        :param ~grammarinator.runtime.Individual donor_individual:
440
            The population item to be used as a donor during crossover.
441
        :return: The root of the extended tree.
442
        :rtype: Rule
443
        """
444
        recipient_individual, donor_individual = self._ensure_individuals(recipient_individual, donor_individual)
1✔
445
        recipient_root, recipient_annot = recipient_individual.root, recipient_individual.annotations
1✔
446
        donor_annot = donor_individual.annotations
1✔
447

448
        common_types = sorted(set(recipient_annot.quants_by_name.keys()) & set(donor_annot.quants_by_name.keys()))
1✔
449
        recipient_options = [(name, node) for name in common_types for node in recipient_annot.quants_by_name[name] if len(node.children) < node.stop]
1✔
450
        recipient_root_token_counts = recipient_annot.token_counts[recipient_root]
1✔
451
        for rule_name, recipient_node in random.sample(recipient_options, k=len(recipient_options)):
1✔
452
            recipient_node_level = recipient_annot.node_levels[recipient_node]
×
453
            donor_options = [quantified for quantifier in donor_annot.quants_by_name[rule_name] for quantified in quantifier.children]
×
454
            for donor_node in random.sample(donor_options, k=len(donor_options)):
×
455
                # Make sure that the output tree won't exceed the depth and token limits.
456
                if (recipient_node_level + donor_annot.node_depths[donor_node] <= self._limit.depth
×
457
                        and recipient_root_token_counts + donor_annot.token_counts[donor_node] < self._limit.tokens):
458
                    recipient_node.insert_child(random.randint(0, len(recipient_node.children)), donor_node)
×
459
                    return recipient_root
×
460

461
        # If selection strategy fails, we practically cause the whole recipient tree
462
        # to be the result of insertion.
463
        return recipient_root
1✔
464

465
    def delete_quantified(self, individual=None, _=None):
1✔
466
        """
467
        Removes an optional subtree randomly from a quantifier node.
468

469
        :param ~grammarinator.runtime.Individual individual: The population item to be mutated.
470
        :return: The root of the modified tree.
471
        :rtype: Rule
472
        """
473
        individual = self._ensure_individual(individual)
1✔
474
        root, annot = individual.root, individual.annotations
1✔
475
        options = [child for node in annot.quants if len(node.children) > node.start for child in node.children]
1✔
476
        if options:
1✔
477
            removed_node = random.choice(options)
×
478
            removed_node.remove()
×
479

480
        # Return with the original root, whether the deletion was successful or not.
481
        return root
1✔
482

483
    def replicate_quantified(self, individual=None, _=None):
1✔
484
        """
485
        Select a quantified sub-tree randomly, replicate it and insert it again if
486
        the maximum quantification count is not reached yet.
487

488
        :param ~grammarinator.runtime.Individual individual: The population item to be mutated.
489
        :return: The root of the modified tree.
490
        :rtype: Rule
491
        """
492
        individual = self._ensure_individual(individual)
1✔
493
        root, annot = individual.root, individual.annotations
1✔
494
        root_options = [node for node in annot.quants if node.stop > len(node.children)]
1✔
495
        recipient_root_token_counts = annot.token_counts[root]
1✔
496
        node_options = [child for root in root_options for child in root.children if
1✔
497
                        recipient_root_token_counts + annot.token_counts[child] <= self._limit.tokens]
498
        if node_options:
1✔
499
            node_to_repeat = random.choice(node_options)
×
500
            node_to_repeat.parent.insert_child(idx=random.randint(0, len(node_to_repeat.parent.children)), node=deepcopy(node_to_repeat))
×
501

502
        # Return with the original root, whether the replication was successful or not.
503
        return root
1✔
504

505
    def shuffle_quantifieds(self, individual=None, _=None):
1✔
506
        """
507
        Select a quantifier node and shuffle its quantified sub-trees.
508

509
        :param ~grammarinator.runtime.Individual individual: The population item to be mutated.
510
        :return: The root of the modified tree.
511
        :rtype: Rule
512
        """
513
        individual = self._ensure_individual(individual)
1✔
514
        root, annot = individual.root, individual.annotations
1✔
515
        options = [node for node in annot.quants if len(node.children) > 1]
1✔
516
        if options:
1✔
517
            node_to_shuffle = random.choice(options)
×
518
            random.shuffle(node_to_shuffle.children)
×
519

520
        # Return with the original root, whether the shuffling was successful or not.
521
        return root
1✔
522

523
    def hoist_rule(self, individual=None, _=None):
1✔
524
        """
525
        Select an individual of the population to be mutated and select two
526
        rule nodes from it which share the same rule name and are in
527
        ancestor-descendant relationship making possible for the descendant
528
        to replace its ancestor.
529

530
        :param ~grammarinator.runtime.Individual individual: The population item to be mutated.
531
        :return: The root of the hoisted tree.
532
        :rtype: Rule
533
        """
534
        individual = self._ensure_individual(individual)
1✔
535
        root, annot = individual.root, individual.annotations
1✔
536
        for rule in random.sample(annot.rules, k=len(annot.rules)):
1✔
537
            parent = rule.parent
1✔
538
            while parent:
1✔
539
                if parent.name == rule.name:
1✔
540
                    parent.replace(rule)
×
541
                    return root
×
542
                parent = parent.parent
1✔
543
        return root
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc