mutato 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE +21 -0
- data/README.md +218 -0
- data/exe/mutato +21 -0
- data/lib/mutato/abort.rb +12 -0
- data/lib/mutato/annotations.rb +65 -0
- data/lib/mutato/arid.rb +104 -0
- data/lib/mutato/baseline.rb +51 -0
- data/lib/mutato/boot.rb +99 -0
- data/lib/mutato/campaign.rb +52 -0
- data/lib/mutato/change.rb +49 -0
- data/lib/mutato/child.rb +135 -0
- data/lib/mutato/cli.rb +138 -0
- data/lib/mutato/clock.rb +21 -0
- data/lib/mutato/config.rb +39 -0
- data/lib/mutato/console.rb +33 -0
- data/lib/mutato/coverage_listener/tally.rb +26 -0
- data/lib/mutato/coverage_listener.rb +46 -0
- data/lib/mutato/covering.rb +45 -0
- data/lib/mutato/diff.rb +42 -0
- data/lib/mutato/emitter.rb +46 -0
- data/lib/mutato/equivalence.rb +30 -0
- data/lib/mutato/experience.rb +11 -0
- data/lib/mutato/flags.rb +58 -0
- data/lib/mutato/generation.rb +36 -0
- data/lib/mutato/generator.rb +10 -0
- data/lib/mutato/genres/conditions.rb +70 -0
- data/lib/mutato/genres/elements.rb +49 -0
- data/lib/mutato/genres/genre.rb +23 -0
- data/lib/mutato/genres/operators.rb +81 -0
- data/lib/mutato/genres/statements.rb +122 -0
- data/lib/mutato/genres/swaps.rb +58 -0
- data/lib/mutato/genres/values.rb +41 -0
- data/lib/mutato/genres.rb +53 -0
- data/lib/mutato/group_hooks.rb +45 -0
- data/lib/mutato/hits.rb +18 -0
- data/lib/mutato/installer/header.rb +40 -0
- data/lib/mutato/installer.rb +133 -0
- data/lib/mutato/measure.rb +37 -0
- data/lib/mutato/mode/control.rb +43 -0
- data/lib/mutato/mode/run.rb +33 -0
- data/lib/mutato/mode.rb +26 -0
- data/lib/mutato/mutation.rb +85 -0
- data/lib/mutato/nodes.rb +150 -0
- data/lib/mutato/options.rb +33 -0
- data/lib/mutato/output.rb +76 -0
- data/lib/mutato/privates.rb +46 -0
- data/lib/mutato/report.rb +36 -0
- data/lib/mutato/rspec_adapter/progress.rb +20 -0
- data/lib/mutato/rspec_adapter/result.rb +15 -0
- data/lib/mutato/rspec_adapter.rb +129 -0
- data/lib/mutato/run.rb +44 -0
- data/lib/mutato/selection.rb +100 -0
- data/lib/mutato/session.rb +53 -0
- data/lib/mutato/skips.rb +54 -0
- data/lib/mutato/soft_timeout.rb +8 -0
- data/lib/mutato/subject.rb +21 -0
- data/lib/mutato/subjects/scope.rb +44 -0
- data/lib/mutato/subjects.rb +67 -0
- data/lib/mutato/suite.rb +6 -0
- data/lib/mutato/trial.rb +93 -0
- data/lib/mutato/usage.rb +41 -0
- data/lib/mutato/version.rb +6 -0
- data/lib/mutato/visitor/cursor.rb +28 -0
- data/lib/mutato/visitor/walk.rb +17 -0
- data/lib/mutato/visitor.rb +88 -0
- data/lib/mutato/watchdog.rb +54 -0
- data/lib/mutato.rb +55 -0
- metadata +128 -0
data/lib/mutato/usage.rb
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
module Mutato
|
|
6
|
+
# Which methods are only called from logging: a file-wide fact no single method shows.
|
|
7
|
+
class Usage
|
|
8
|
+
def initialize(tree)
|
|
9
|
+
# Names called inside logging, and names called anywhere else.
|
|
10
|
+
@calls = { true => Set.new, false => Set.new }
|
|
11
|
+
@private = Set.new
|
|
12
|
+
scan(tree, in_arid: false)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def logging_only?(name)
|
|
16
|
+
@calls.fetch(true).include?(name) && !@calls.fetch(false).include?(name)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A private method only ever called from logging is as arid as the logging. A public one
|
|
20
|
+
# may be called from another file.
|
|
21
|
+
def demoted(subject)
|
|
22
|
+
name = subject.name
|
|
23
|
+
return subject if subject.skip || !logging_only?(name) || !@private.include?(name)
|
|
24
|
+
|
|
25
|
+
subject.with(skip: "only used in logging")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def scan(node, in_arid:)
|
|
31
|
+
in_arid = called(node, in_arid:) if node.is_a?(Prism::CallNode)
|
|
32
|
+
@private.merge(Privates.names(node)) if node.is_a?(Prism::StatementsNode)
|
|
33
|
+
node.compact_child_nodes.each { |child| scan(child, in_arid:) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def called(call, in_arid:)
|
|
37
|
+
@calls.fetch(in_arid) << call.name
|
|
38
|
+
in_arid || Arid.arid_node?(call)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutato
|
|
4
|
+
class Visitor
|
|
5
|
+
# Inside a block, `outer` stays the statement outside it, whose tests judge the mutant.
|
|
6
|
+
Cursor = Data.define(:statement, :outer, :in_block) do
|
|
7
|
+
def self.start
|
|
8
|
+
new(statement: nil, outer: nil, in_block: false)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def at(statement)
|
|
12
|
+
with(statement:, outer: in_block ? outer : statement)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def inside_block
|
|
16
|
+
with(in_block: true)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def outer_lines(node)
|
|
20
|
+
return unless in_block
|
|
21
|
+
|
|
22
|
+
location = (outer || node).location
|
|
23
|
+
location.start_line..location.end_line
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
private_constant :Cursor
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutato
|
|
4
|
+
class Visitor
|
|
5
|
+
Walk = Struct.new(:cursor, :deleted, :spared, :dead) do
|
|
6
|
+
def self.start
|
|
7
|
+
new(Cursor.start, identity_set, identity_set, identity_set)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Nodes compare by structure; the walk needs the very node.
|
|
11
|
+
def self.identity_set
|
|
12
|
+
Set.new.compare_by_identity
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
private_constant :Walk
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
require_relative "visitor/cursor"
|
|
5
|
+
require_relative "visitor/walk"
|
|
6
|
+
|
|
7
|
+
module Mutato
|
|
8
|
+
class Visitor
|
|
9
|
+
DEFINITIONS = [Prism::DefNode, Prism::ClassNode, Prism::ModuleNode].freeze
|
|
10
|
+
private_constant :DEFINITIONS
|
|
11
|
+
|
|
12
|
+
attr_reader :subject, :walk
|
|
13
|
+
|
|
14
|
+
def initialize(subject, emitter)
|
|
15
|
+
@subject = subject
|
|
16
|
+
@emitter = emitter
|
|
17
|
+
@walk = Walk.start
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The last statement is the value and stays, unless the caller drops it: `new`, `x.y = 1`.
|
|
21
|
+
def run
|
|
22
|
+
body = subject.node.body
|
|
23
|
+
Genres::Values.new(self).mutate(body)
|
|
24
|
+
statements = Nodes.statements_of(body)
|
|
25
|
+
walk.spared << statements.last if statements && !Nodes.value_dropped?(subject.name)
|
|
26
|
+
visit(body)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def source
|
|
30
|
+
@emitter.source
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def emit(genre, change)
|
|
34
|
+
@emitter.add(change.with(genre:), subject, walk.cursor)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def with_statement(statement, &)
|
|
38
|
+
with_cursor(walk.cursor.at(statement), &)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def visit(node)
|
|
42
|
+
case node
|
|
43
|
+
when Prism::StatementsNode then visit_statements(node)
|
|
44
|
+
when Prism::BlockNode, Prism::LambdaNode then visit_block(node)
|
|
45
|
+
when Prism::EmbeddedStatementsNode, Prism::ParenthesesNode then visit_expression(node)
|
|
46
|
+
when nil, *DEFINITIONS then nil
|
|
47
|
+
else visit_other(node)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def visit_statements(node)
|
|
54
|
+
Genres::Statements.new(self).mutate(node)
|
|
55
|
+
node.body.each { |statement| visit_statement(statement) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Nothing inside a store no one reads is mutated either.
|
|
59
|
+
def visit_statement(statement)
|
|
60
|
+
with_statement(statement) { visit(statement) } unless walk.dead.include?(statement)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def visit_block(node)
|
|
64
|
+
with_cursor(walk.cursor.inside_block) { visit_children(node) }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Statements inside `#{}` or parentheses are expressions: mutated, never deleted.
|
|
68
|
+
def visit_expression(node)
|
|
69
|
+
node.compact_child_nodes.flat_map(&:body).each { |child| visit(child) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def visit_other(node)
|
|
73
|
+
visit_children(node) unless Genres.mutate(self, node) == :stop
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def visit_children(node)
|
|
77
|
+
node.compact_child_nodes.each { |child| visit(child) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def with_cursor(cursor)
|
|
81
|
+
previous = walk.cursor
|
|
82
|
+
walk.cursor = cursor
|
|
83
|
+
yield
|
|
84
|
+
ensure
|
|
85
|
+
walk.cursor = previous
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mutato
|
|
4
|
+
# Past the budget it stops the framework and raises; the parent's SIGKILL is the backstop.
|
|
5
|
+
class Watchdog
|
|
6
|
+
def initialize(adapter, budget)
|
|
7
|
+
@adapter = adapter
|
|
8
|
+
@budget = budget
|
|
9
|
+
@expired = false
|
|
10
|
+
@running = nil
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# A timeout whatever the tests made of it, as RSpec rescues the exception like any other,
|
|
14
|
+
# unless a test had failed before time ran out.
|
|
15
|
+
def run(ids)
|
|
16
|
+
timer = Thread.new { expire }
|
|
17
|
+
judged(guarded(ids))
|
|
18
|
+
ensure
|
|
19
|
+
timer&.kill
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def judged(verdict)
|
|
25
|
+
return verdict unless @expired
|
|
26
|
+
|
|
27
|
+
failing = verdict[:failing]
|
|
28
|
+
failing && failing != @running ? verdict : { outcome: :timeout, running: @running }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def guarded(ids)
|
|
32
|
+
@adapter.run(ids).outcome
|
|
33
|
+
rescue SoftTimeout
|
|
34
|
+
{ outcome: :timeout, running: @running }
|
|
35
|
+
rescue SystemExit, SignalException => error
|
|
36
|
+
exited(error)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def expire
|
|
40
|
+
sleep @budget
|
|
41
|
+
@running = @adapter.current
|
|
42
|
+
@expired = true
|
|
43
|
+
@adapter.stop
|
|
44
|
+
Thread.main.raise(SoftTimeout)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A mutant forced an exit path.
|
|
48
|
+
def exited(error)
|
|
49
|
+
puts "#{error.class}: #{error.message}"
|
|
50
|
+
current = @adapter.current
|
|
51
|
+
{ outcome: :caught, failing: current, ran: [current].compact }
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/mutato.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "mutato/arid"
|
|
4
|
+
require_relative "mutato/change"
|
|
5
|
+
require_relative "mutato/config"
|
|
6
|
+
require_relative "mutato/emitter"
|
|
7
|
+
require_relative "mutato/generation"
|
|
8
|
+
require_relative "mutato/generator"
|
|
9
|
+
require_relative "mutato/genres"
|
|
10
|
+
require_relative "mutato/installer"
|
|
11
|
+
require_relative "mutato/mutation"
|
|
12
|
+
require_relative "mutato/nodes"
|
|
13
|
+
require_relative "mutato/privates"
|
|
14
|
+
require_relative "mutato/skips"
|
|
15
|
+
require_relative "mutato/subject"
|
|
16
|
+
require_relative "mutato/subjects"
|
|
17
|
+
require_relative "mutato/usage"
|
|
18
|
+
require_relative "mutato/version"
|
|
19
|
+
require_relative "mutato/visitor"
|
|
20
|
+
|
|
21
|
+
module Mutato
|
|
22
|
+
def self.config
|
|
23
|
+
@config ||= Config.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.before_fork(&block)
|
|
27
|
+
config.hooks[:before_fork] << block
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.after_fork(&block)
|
|
31
|
+
config.hooks[:after_fork] << block
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.after_boot(&block)
|
|
35
|
+
config.hooks[:after_boot] << block
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.before_mutant(&block)
|
|
39
|
+
config.hooks[:before_mutant] << block
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.skip(pattern, reason)
|
|
43
|
+
config.skip(pattern, reason)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.arid(*names)
|
|
47
|
+
config.arid(*names)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# A forked child must not share the parent's database sockets.
|
|
51
|
+
before_fork { Sequel::DATABASES.each(&:disconnect) if defined?(Sequel) }
|
|
52
|
+
before_fork do
|
|
53
|
+
ActiveRecord::Base.connection_handler.clear_all_connections! if defined?(ActiveRecord)
|
|
54
|
+
end
|
|
55
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mutato
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Mathias Løkke Madsen
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: prism
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.8'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.8'
|
|
26
|
+
description: |
|
|
27
|
+
Mutates methods with Prism, redefines them in forked children of a
|
|
28
|
+
test process booted once, selects the tests that cover each mutant from
|
|
29
|
+
per-test coverage, and reports the survivors. RSpec.
|
|
30
|
+
executables:
|
|
31
|
+
- mutato
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- CHANGELOG.md
|
|
36
|
+
- LICENSE
|
|
37
|
+
- README.md
|
|
38
|
+
- exe/mutato
|
|
39
|
+
- lib/mutato.rb
|
|
40
|
+
- lib/mutato/abort.rb
|
|
41
|
+
- lib/mutato/annotations.rb
|
|
42
|
+
- lib/mutato/arid.rb
|
|
43
|
+
- lib/mutato/baseline.rb
|
|
44
|
+
- lib/mutato/boot.rb
|
|
45
|
+
- lib/mutato/campaign.rb
|
|
46
|
+
- lib/mutato/change.rb
|
|
47
|
+
- lib/mutato/child.rb
|
|
48
|
+
- lib/mutato/cli.rb
|
|
49
|
+
- lib/mutato/clock.rb
|
|
50
|
+
- lib/mutato/config.rb
|
|
51
|
+
- lib/mutato/console.rb
|
|
52
|
+
- lib/mutato/coverage_listener.rb
|
|
53
|
+
- lib/mutato/coverage_listener/tally.rb
|
|
54
|
+
- lib/mutato/covering.rb
|
|
55
|
+
- lib/mutato/diff.rb
|
|
56
|
+
- lib/mutato/emitter.rb
|
|
57
|
+
- lib/mutato/equivalence.rb
|
|
58
|
+
- lib/mutato/experience.rb
|
|
59
|
+
- lib/mutato/flags.rb
|
|
60
|
+
- lib/mutato/generation.rb
|
|
61
|
+
- lib/mutato/generator.rb
|
|
62
|
+
- lib/mutato/genres.rb
|
|
63
|
+
- lib/mutato/genres/conditions.rb
|
|
64
|
+
- lib/mutato/genres/elements.rb
|
|
65
|
+
- lib/mutato/genres/genre.rb
|
|
66
|
+
- lib/mutato/genres/operators.rb
|
|
67
|
+
- lib/mutato/genres/statements.rb
|
|
68
|
+
- lib/mutato/genres/swaps.rb
|
|
69
|
+
- lib/mutato/genres/values.rb
|
|
70
|
+
- lib/mutato/group_hooks.rb
|
|
71
|
+
- lib/mutato/hits.rb
|
|
72
|
+
- lib/mutato/installer.rb
|
|
73
|
+
- lib/mutato/installer/header.rb
|
|
74
|
+
- lib/mutato/measure.rb
|
|
75
|
+
- lib/mutato/mode.rb
|
|
76
|
+
- lib/mutato/mode/control.rb
|
|
77
|
+
- lib/mutato/mode/run.rb
|
|
78
|
+
- lib/mutato/mutation.rb
|
|
79
|
+
- lib/mutato/nodes.rb
|
|
80
|
+
- lib/mutato/options.rb
|
|
81
|
+
- lib/mutato/output.rb
|
|
82
|
+
- lib/mutato/privates.rb
|
|
83
|
+
- lib/mutato/report.rb
|
|
84
|
+
- lib/mutato/rspec_adapter.rb
|
|
85
|
+
- lib/mutato/rspec_adapter/progress.rb
|
|
86
|
+
- lib/mutato/rspec_adapter/result.rb
|
|
87
|
+
- lib/mutato/run.rb
|
|
88
|
+
- lib/mutato/selection.rb
|
|
89
|
+
- lib/mutato/session.rb
|
|
90
|
+
- lib/mutato/skips.rb
|
|
91
|
+
- lib/mutato/soft_timeout.rb
|
|
92
|
+
- lib/mutato/subject.rb
|
|
93
|
+
- lib/mutato/subjects.rb
|
|
94
|
+
- lib/mutato/subjects/scope.rb
|
|
95
|
+
- lib/mutato/suite.rb
|
|
96
|
+
- lib/mutato/trial.rb
|
|
97
|
+
- lib/mutato/usage.rb
|
|
98
|
+
- lib/mutato/version.rb
|
|
99
|
+
- lib/mutato/visitor.rb
|
|
100
|
+
- lib/mutato/visitor/cursor.rb
|
|
101
|
+
- lib/mutato/visitor/walk.rb
|
|
102
|
+
- lib/mutato/watchdog.rb
|
|
103
|
+
homepage: https://github.com/LQkkeN/mutato
|
|
104
|
+
licenses:
|
|
105
|
+
- MIT
|
|
106
|
+
metadata:
|
|
107
|
+
source_code_uri: https://github.com/LQkkeN/mutato
|
|
108
|
+
changelog_uri: https://github.com/LQkkeN/mutato/blob/main/CHANGELOG.md
|
|
109
|
+
bug_tracker_uri: https://github.com/LQkkeN/mutato/issues
|
|
110
|
+
rubygems_mfa_required: 'true'
|
|
111
|
+
rdoc_options: []
|
|
112
|
+
require_paths:
|
|
113
|
+
- lib
|
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
115
|
+
requirements:
|
|
116
|
+
- - ">="
|
|
117
|
+
- !ruby/object:Gem::Version
|
|
118
|
+
version: '3.2'
|
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - ">="
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '0'
|
|
124
|
+
requirements: []
|
|
125
|
+
rubygems_version: 4.0.20
|
|
126
|
+
specification_version: 4
|
|
127
|
+
summary: 'Mutation testing for Ruby: boot once, fork per mutant'
|
|
128
|
+
test_files: []
|