t-ruby 0.0.1
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/LICENSE +21 -0
- data/README.md +221 -0
- data/bin/trc +6 -0
- data/lib/t_ruby/benchmark.rb +592 -0
- data/lib/t_ruby/bundler_integration.rb +569 -0
- data/lib/t_ruby/cache.rb +774 -0
- data/lib/t_ruby/cli.rb +106 -0
- data/lib/t_ruby/compiler.rb +299 -0
- data/lib/t_ruby/config.rb +53 -0
- data/lib/t_ruby/constraint_checker.rb +441 -0
- data/lib/t_ruby/declaration_generator.rb +298 -0
- data/lib/t_ruby/doc_generator.rb +474 -0
- data/lib/t_ruby/error_handler.rb +132 -0
- data/lib/t_ruby/generic_type_parser.rb +68 -0
- data/lib/t_ruby/intersection_type_parser.rb +30 -0
- data/lib/t_ruby/ir.rb +1301 -0
- data/lib/t_ruby/lsp_server.rb +994 -0
- data/lib/t_ruby/package_manager.rb +735 -0
- data/lib/t_ruby/parser.rb +245 -0
- data/lib/t_ruby/parser_combinator.rb +942 -0
- data/lib/t_ruby/rbs_generator.rb +71 -0
- data/lib/t_ruby/runtime_validator.rb +367 -0
- data/lib/t_ruby/smt_solver.rb +1076 -0
- data/lib/t_ruby/type_alias_registry.rb +102 -0
- data/lib/t_ruby/type_checker.rb +770 -0
- data/lib/t_ruby/type_erasure.rb +26 -0
- data/lib/t_ruby/type_inferencer.rb +580 -0
- data/lib/t_ruby/union_type_parser.rb +38 -0
- data/lib/t_ruby/version.rb +5 -0
- data/lib/t_ruby/watcher.rb +320 -0
- data/lib/t_ruby.rb +42 -0
- metadata +87 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "listen"
|
|
4
|
+
|
|
5
|
+
module TRuby
|
|
6
|
+
class Watcher
|
|
7
|
+
# ANSI color codes (similar to tsc output style)
|
|
8
|
+
COLORS = {
|
|
9
|
+
reset: "\e[0m",
|
|
10
|
+
bold: "\e[1m",
|
|
11
|
+
dim: "\e[2m",
|
|
12
|
+
red: "\e[31m",
|
|
13
|
+
green: "\e[32m",
|
|
14
|
+
yellow: "\e[33m",
|
|
15
|
+
blue: "\e[34m",
|
|
16
|
+
cyan: "\e[36m",
|
|
17
|
+
gray: "\e[90m"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
attr_reader :incremental_compiler, :stats
|
|
21
|
+
|
|
22
|
+
def initialize(paths: ["."], config: nil, incremental: true, cross_file_check: true, parallel: true)
|
|
23
|
+
@paths = paths.map { |p| File.expand_path(p) }
|
|
24
|
+
@config = config || Config.new
|
|
25
|
+
@compiler = Compiler.new(@config)
|
|
26
|
+
@error_count = 0
|
|
27
|
+
@file_count = 0
|
|
28
|
+
@use_colors = $stdout.tty?
|
|
29
|
+
|
|
30
|
+
# Enhanced features
|
|
31
|
+
@incremental = incremental
|
|
32
|
+
@cross_file_check = cross_file_check
|
|
33
|
+
@parallel = parallel
|
|
34
|
+
|
|
35
|
+
# Initialize incremental compiler
|
|
36
|
+
if @incremental
|
|
37
|
+
@incremental_compiler = EnhancedIncrementalCompiler.new(
|
|
38
|
+
@compiler,
|
|
39
|
+
enable_cross_file: @cross_file_check
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Parallel processor
|
|
44
|
+
@parallel_processor = ParallelProcessor.new if @parallel
|
|
45
|
+
|
|
46
|
+
# Statistics
|
|
47
|
+
@stats = {
|
|
48
|
+
total_compilations: 0,
|
|
49
|
+
incremental_hits: 0,
|
|
50
|
+
total_time: 0.0
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def watch
|
|
55
|
+
print_start_message
|
|
56
|
+
|
|
57
|
+
# Initial compilation
|
|
58
|
+
start_time = Time.now
|
|
59
|
+
compile_all
|
|
60
|
+
@stats[:total_time] += Time.now - start_time
|
|
61
|
+
|
|
62
|
+
# Start watching
|
|
63
|
+
listener = Listen.to(*watch_directories, only: /\.trb$/) do |modified, added, removed|
|
|
64
|
+
handle_changes(modified, added, removed)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
listener.start
|
|
68
|
+
|
|
69
|
+
print_watching_message
|
|
70
|
+
|
|
71
|
+
# Keep the process running
|
|
72
|
+
begin
|
|
73
|
+
sleep
|
|
74
|
+
rescue Interrupt
|
|
75
|
+
puts "\n#{colorize(:dim, timestamp)} #{colorize(:cyan, "Stopping watch mode...")}"
|
|
76
|
+
print_stats if @incremental
|
|
77
|
+
listener.stop
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def watch_directories
|
|
84
|
+
@paths.map do |path|
|
|
85
|
+
if File.directory?(path)
|
|
86
|
+
path
|
|
87
|
+
else
|
|
88
|
+
File.dirname(path)
|
|
89
|
+
end
|
|
90
|
+
end.uniq
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def handle_changes(modified, added, removed)
|
|
94
|
+
changed_files = (modified + added).select { |f| f.end_with?(".trb") }
|
|
95
|
+
return if changed_files.empty? && removed.empty?
|
|
96
|
+
|
|
97
|
+
puts
|
|
98
|
+
print_file_change_message
|
|
99
|
+
|
|
100
|
+
if removed.any?
|
|
101
|
+
removed.each do |file|
|
|
102
|
+
puts "#{colorize(:gray, timestamp)} #{colorize(:yellow, "File removed:")} #{relative_path(file)}"
|
|
103
|
+
# Clear from incremental compiler cache
|
|
104
|
+
@incremental_compiler&.file_hashes&.delete(file)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
if changed_files.any?
|
|
109
|
+
start_time = Time.now
|
|
110
|
+
compile_files_incremental(changed_files)
|
|
111
|
+
@stats[:total_time] += Time.now - start_time
|
|
112
|
+
else
|
|
113
|
+
print_watching_message
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def compile_all
|
|
118
|
+
@error_count = 0
|
|
119
|
+
@file_count = 0
|
|
120
|
+
errors = []
|
|
121
|
+
|
|
122
|
+
trb_files = find_trb_files
|
|
123
|
+
@file_count = trb_files.size
|
|
124
|
+
|
|
125
|
+
if @incremental && @cross_file_check
|
|
126
|
+
# Use enhanced incremental compiler with cross-file checking
|
|
127
|
+
result = @incremental_compiler.compile_all_with_checking(trb_files)
|
|
128
|
+
errors = result[:errors].map { |e| format_error(e[:file], e[:error] || e[:message]) }
|
|
129
|
+
@error_count = errors.size
|
|
130
|
+
@stats[:total_compilations] += trb_files.size
|
|
131
|
+
elsif @parallel && trb_files.size > 1
|
|
132
|
+
# Parallel compilation
|
|
133
|
+
results = @parallel_processor.process_files(trb_files) do |file|
|
|
134
|
+
compile_file(file)
|
|
135
|
+
end
|
|
136
|
+
results.each do |result|
|
|
137
|
+
errors.concat(result[:errors]) if result[:errors]&.any?
|
|
138
|
+
end
|
|
139
|
+
else
|
|
140
|
+
# Sequential compilation
|
|
141
|
+
trb_files.each do |file|
|
|
142
|
+
result = compile_file(file)
|
|
143
|
+
errors.concat(result[:errors]) if result[:errors].any?
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
print_errors(errors)
|
|
148
|
+
print_summary
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def compile_files_incremental(files)
|
|
152
|
+
@error_count = 0
|
|
153
|
+
errors = []
|
|
154
|
+
compiled_count = 0
|
|
155
|
+
|
|
156
|
+
if @incremental
|
|
157
|
+
files.each do |file|
|
|
158
|
+
if @incremental_compiler.needs_compile?(file)
|
|
159
|
+
@stats[:total_compilations] += 1
|
|
160
|
+
result = compile_file_with_ir(file)
|
|
161
|
+
errors.concat(result[:errors]) if result[:errors].any?
|
|
162
|
+
compiled_count += 1
|
|
163
|
+
else
|
|
164
|
+
@stats[:incremental_hits] += 1
|
|
165
|
+
puts "#{colorize(:gray, timestamp)} #{colorize(:dim, "Skipping unchanged:")} #{relative_path(file)}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Run cross-file check if enabled
|
|
170
|
+
if @cross_file_check && @incremental_compiler.cross_file_checker
|
|
171
|
+
check_result = @incremental_compiler.cross_file_checker.check_all
|
|
172
|
+
check_result[:errors].each do |e|
|
|
173
|
+
errors << format_error(e[:file], e[:message])
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
else
|
|
177
|
+
files.each do |file|
|
|
178
|
+
result = compile_file(file)
|
|
179
|
+
errors.concat(result[:errors]) if result[:errors].any?
|
|
180
|
+
compiled_count += 1
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
@file_count = compiled_count
|
|
185
|
+
print_errors(errors)
|
|
186
|
+
print_summary
|
|
187
|
+
print_watching_message
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def compile_file_with_ir(file)
|
|
191
|
+
result = { file: file, errors: [], success: false }
|
|
192
|
+
|
|
193
|
+
begin
|
|
194
|
+
@incremental_compiler.compile_with_ir(file)
|
|
195
|
+
result[:success] = true
|
|
196
|
+
rescue ArgumentError => e
|
|
197
|
+
@error_count += 1
|
|
198
|
+
result[:errors] << format_error(file, e.message)
|
|
199
|
+
rescue StandardError => e
|
|
200
|
+
@error_count += 1
|
|
201
|
+
result[:errors] << format_error(file, e.message)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
result
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def compile_file(file)
|
|
208
|
+
result = { file: file, errors: [], success: false }
|
|
209
|
+
|
|
210
|
+
begin
|
|
211
|
+
@compiler.compile(file)
|
|
212
|
+
result[:success] = true
|
|
213
|
+
@stats[:total_compilations] += 1
|
|
214
|
+
rescue ArgumentError => e
|
|
215
|
+
@error_count += 1
|
|
216
|
+
result[:errors] << format_error(file, e.message)
|
|
217
|
+
rescue StandardError => e
|
|
218
|
+
@error_count += 1
|
|
219
|
+
result[:errors] << format_error(file, e.message)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
result
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def find_trb_files
|
|
226
|
+
files = []
|
|
227
|
+
@paths.each do |path|
|
|
228
|
+
if File.directory?(path)
|
|
229
|
+
files.concat(Dir.glob(File.join(path, "**", "*.trb")))
|
|
230
|
+
elsif File.file?(path) && path.end_with?(".trb")
|
|
231
|
+
files << path
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
files.uniq
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def format_error(file, message)
|
|
238
|
+
# Parse error message for line/column info if available
|
|
239
|
+
# Format: file:line:col - error TRB0001: message
|
|
240
|
+
line = 1
|
|
241
|
+
col = 1
|
|
242
|
+
|
|
243
|
+
# Try to extract line info from error message
|
|
244
|
+
if message =~ /line (\d+)/i
|
|
245
|
+
line = ::Regexp.last_match(1).to_i
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
{
|
|
249
|
+
file: file,
|
|
250
|
+
line: line,
|
|
251
|
+
col: col,
|
|
252
|
+
message: message
|
|
253
|
+
}
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def print_errors(errors)
|
|
257
|
+
errors.each do |error|
|
|
258
|
+
puts
|
|
259
|
+
# TypeScript-style error format: file:line:col - error TSXXXX: message
|
|
260
|
+
location = "#{colorize(:cyan, relative_path(error[:file]))}:#{colorize(:yellow, error[:line])}:#{colorize(:yellow, error[:col])}"
|
|
261
|
+
puts "#{location} - #{colorize(:red, "error")} #{colorize(:gray, "TRB0001")}: #{error[:message]}"
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def print_start_message
|
|
266
|
+
puts "#{colorize(:gray, timestamp)} #{colorize(:bold, "Starting compilation in watch mode...")}"
|
|
267
|
+
puts
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def print_file_change_message
|
|
271
|
+
puts "#{colorize(:gray, timestamp)} #{colorize(:bold, "File change detected. Starting incremental compilation...")}"
|
|
272
|
+
puts
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def print_summary
|
|
276
|
+
puts
|
|
277
|
+
if @error_count.zero?
|
|
278
|
+
msg = "Found #{colorize(:green, "0 errors")}. Watching for file changes."
|
|
279
|
+
puts "#{colorize(:gray, timestamp)} #{msg}"
|
|
280
|
+
else
|
|
281
|
+
error_word = @error_count == 1 ? "error" : "errors"
|
|
282
|
+
msg = "Found #{colorize(:red, "#{@error_count} #{error_word}")}. Watching for file changes."
|
|
283
|
+
puts "#{colorize(:gray, timestamp)} #{msg}"
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def print_watching_message
|
|
288
|
+
# Just print a blank line for readability
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def print_stats
|
|
292
|
+
puts
|
|
293
|
+
puts "#{colorize(:gray, timestamp)} #{colorize(:bold, "Watch Mode Statistics:")}"
|
|
294
|
+
puts " Total compilations: #{@stats[:total_compilations]}"
|
|
295
|
+
puts " Incremental cache hits: #{@stats[:incremental_hits]}"
|
|
296
|
+
hit_rate = if @stats[:total_compilations] + @stats[:incremental_hits] > 0
|
|
297
|
+
(@stats[:incremental_hits].to_f / (@stats[:total_compilations] + @stats[:incremental_hits]) * 100).round(1)
|
|
298
|
+
else
|
|
299
|
+
0
|
|
300
|
+
end
|
|
301
|
+
puts " Cache hit rate: #{hit_rate}%"
|
|
302
|
+
puts " Total compile time: #{@stats[:total_time].round(2)}s"
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def timestamp
|
|
306
|
+
Time.now.strftime("[%I:%M:%S %p]")
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def relative_path(file)
|
|
310
|
+
file.sub("#{Dir.pwd}/", "")
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def colorize(color, text)
|
|
314
|
+
return text unless @use_colors
|
|
315
|
+
return text unless COLORS[color]
|
|
316
|
+
|
|
317
|
+
"#{COLORS[color]}#{text}#{COLORS[:reset]}"
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
data/lib/t_ruby.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "t_ruby/version"
|
|
4
|
+
require_relative "t_ruby/config"
|
|
5
|
+
|
|
6
|
+
# Core infrastructure (must be loaded first)
|
|
7
|
+
require_relative "t_ruby/ir"
|
|
8
|
+
require_relative "t_ruby/parser_combinator"
|
|
9
|
+
require_relative "t_ruby/smt_solver"
|
|
10
|
+
|
|
11
|
+
# Basic components
|
|
12
|
+
require_relative "t_ruby/type_alias_registry"
|
|
13
|
+
require_relative "t_ruby/parser"
|
|
14
|
+
require_relative "t_ruby/union_type_parser"
|
|
15
|
+
require_relative "t_ruby/generic_type_parser"
|
|
16
|
+
require_relative "t_ruby/intersection_type_parser"
|
|
17
|
+
require_relative "t_ruby/type_erasure"
|
|
18
|
+
require_relative "t_ruby/error_handler"
|
|
19
|
+
require_relative "t_ruby/rbs_generator"
|
|
20
|
+
require_relative "t_ruby/declaration_generator"
|
|
21
|
+
require_relative "t_ruby/compiler"
|
|
22
|
+
require_relative "t_ruby/lsp_server"
|
|
23
|
+
require_relative "t_ruby/watcher"
|
|
24
|
+
require_relative "t_ruby/cli"
|
|
25
|
+
|
|
26
|
+
# Milestone 4: Advanced Features
|
|
27
|
+
require_relative "t_ruby/constraint_checker"
|
|
28
|
+
require_relative "t_ruby/type_inferencer"
|
|
29
|
+
require_relative "t_ruby/runtime_validator"
|
|
30
|
+
require_relative "t_ruby/type_checker"
|
|
31
|
+
require_relative "t_ruby/cache"
|
|
32
|
+
require_relative "t_ruby/package_manager"
|
|
33
|
+
|
|
34
|
+
# Milestone 5: Bundler Integration
|
|
35
|
+
require_relative "t_ruby/bundler_integration"
|
|
36
|
+
|
|
37
|
+
# Milestone 6: Quality & Documentation
|
|
38
|
+
require_relative "t_ruby/benchmark"
|
|
39
|
+
require_relative "t_ruby/doc_generator"
|
|
40
|
+
|
|
41
|
+
module TRuby
|
|
42
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: t-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Y. Fred Kim
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: listen
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.8'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.8'
|
|
26
|
+
description: t-ruby compiles .trb files with type annotations to executable Ruby (.rb)
|
|
27
|
+
and optional type signature files (.rbs)
|
|
28
|
+
email:
|
|
29
|
+
- yhkks1038@gmail.com
|
|
30
|
+
executables:
|
|
31
|
+
- trc
|
|
32
|
+
extensions: []
|
|
33
|
+
extra_rdoc_files: []
|
|
34
|
+
files:
|
|
35
|
+
- LICENSE
|
|
36
|
+
- README.md
|
|
37
|
+
- bin/trc
|
|
38
|
+
- lib/t_ruby.rb
|
|
39
|
+
- lib/t_ruby/benchmark.rb
|
|
40
|
+
- lib/t_ruby/bundler_integration.rb
|
|
41
|
+
- lib/t_ruby/cache.rb
|
|
42
|
+
- lib/t_ruby/cli.rb
|
|
43
|
+
- lib/t_ruby/compiler.rb
|
|
44
|
+
- lib/t_ruby/config.rb
|
|
45
|
+
- lib/t_ruby/constraint_checker.rb
|
|
46
|
+
- lib/t_ruby/declaration_generator.rb
|
|
47
|
+
- lib/t_ruby/doc_generator.rb
|
|
48
|
+
- lib/t_ruby/error_handler.rb
|
|
49
|
+
- lib/t_ruby/generic_type_parser.rb
|
|
50
|
+
- lib/t_ruby/intersection_type_parser.rb
|
|
51
|
+
- lib/t_ruby/ir.rb
|
|
52
|
+
- lib/t_ruby/lsp_server.rb
|
|
53
|
+
- lib/t_ruby/package_manager.rb
|
|
54
|
+
- lib/t_ruby/parser.rb
|
|
55
|
+
- lib/t_ruby/parser_combinator.rb
|
|
56
|
+
- lib/t_ruby/rbs_generator.rb
|
|
57
|
+
- lib/t_ruby/runtime_validator.rb
|
|
58
|
+
- lib/t_ruby/smt_solver.rb
|
|
59
|
+
- lib/t_ruby/type_alias_registry.rb
|
|
60
|
+
- lib/t_ruby/type_checker.rb
|
|
61
|
+
- lib/t_ruby/type_erasure.rb
|
|
62
|
+
- lib/t_ruby/type_inferencer.rb
|
|
63
|
+
- lib/t_ruby/union_type_parser.rb
|
|
64
|
+
- lib/t_ruby/version.rb
|
|
65
|
+
- lib/t_ruby/watcher.rb
|
|
66
|
+
homepage: https://github.com/type-ruby/t-ruby
|
|
67
|
+
licenses:
|
|
68
|
+
- MIT
|
|
69
|
+
metadata: {}
|
|
70
|
+
rdoc_options: []
|
|
71
|
+
require_paths:
|
|
72
|
+
- lib
|
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: 3.0.0
|
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
requirements: []
|
|
84
|
+
rubygems_version: 3.7.2
|
|
85
|
+
specification_version: 4
|
|
86
|
+
summary: T-Ruby - A TypeScript-inspired typed layer for Ruby
|
|
87
|
+
test_files: []
|