captive-stack-detector 0.1.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/lib/captive/stack/detector.rb +3 -0
- data/lib/captive_stack_detector/env_vars_scanner.rb +37 -0
- data/lib/captive_stack_detector/file_content_parser.rb +26 -0
- data/lib/captive_stack_detector/file_reader.rb +47 -0
- data/lib/captive_stack_detector/gemfile_analyzer.rb +37 -0
- data/lib/captive_stack_detector/github_api_client.rb +29 -0
- data/lib/captive_stack_detector/js_stack_detector.rb +40 -0
- data/lib/captive_stack_detector/node_version_detector.rb +35 -0
- data/lib/captive_stack_detector/package_json_analyzer.rb +32 -0
- data/lib/captive_stack_detector/rails_stack_detector.rb +32 -0
- data/lib/captive_stack_detector/ruby_version_detector.rb +31 -0
- data/lib/captive_stack_detector/types.rb +10 -0
- data/lib/captive_stack_detector/version.rb +5 -0
- data/lib/captive_stack_detector.rb +26 -0
- metadata +56 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: '086052c42d95d9caf63c44c467e86e28c24c940c260d7752616ce8da6cea846f'
|
|
4
|
+
data.tar.gz: ab4f55fb35c1476f42e166a8df0f0f68ae7f13ea8b920061bd603c6f0cbb3d24
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 001df9a9e62af902bc8bbd8816991a5c9af415d0a815912217c4ef0fe8dda479f4228cde5be4fa8a715eaf22fd109e335f73db182597fc0d8b737c9b53f866e9
|
|
7
|
+
data.tar.gz: c986ed711a7475e9c9f585e050f3d9deb7acc46e5f86dba6ffdcfa0ba7d03f0af5c7057c1e4d7ec937083f6f4276e62d8a2d8b66bbef70dacb6af03ef0c4c51b
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveStackDetector
|
|
4
|
+
module EnvVarsScanner
|
|
5
|
+
HANDLED_VARS = %w[
|
|
6
|
+
RAILS_ENV SECRET_KEY_BASE DATABASE_URL REDIS_URL
|
|
7
|
+
RAILS_LOG_TO_STDOUT RAILS_SERVE_STATIC_FILES PORT RACK_ENV
|
|
8
|
+
].freeze
|
|
9
|
+
|
|
10
|
+
ENV_KEY_PATTERN = /ENV\.fetch\(['"]([A-Z_][A-Z0-9_]*)['"]|ENV\[['"]([A-Z_][A-Z0-9_]*)['"]\]/
|
|
11
|
+
SAFE_DEFAULT_PATTERN = /\A,\s*(?:nil\b|true\b|false\b|'[^']*'|"[^"]*"|\d+)\s*[,)]/
|
|
12
|
+
BLOCK_DEFAULT_PATTERN = /\A\s*\)\s*(?:do|\{)/
|
|
13
|
+
ASSIGNMENT_PATTERN = /\A\s*\|\|=/
|
|
14
|
+
|
|
15
|
+
def self.scan(content)
|
|
16
|
+
result = {}
|
|
17
|
+
uncommented(content).scan(ENV_KEY_PATTERN) do |fetch_key, bracket_key|
|
|
18
|
+
after = Regexp.last_match.post_match
|
|
19
|
+
if bracket_key
|
|
20
|
+
next if ASSIGNMENT_PATTERN.match?(after)
|
|
21
|
+
|
|
22
|
+
result[bracket_key] ||= "placeholder"
|
|
23
|
+
else
|
|
24
|
+
next if SAFE_DEFAULT_PATTERN.match?(after) || BLOCK_DEFAULT_PATTERN.match?(after)
|
|
25
|
+
|
|
26
|
+
result[fetch_key] ||= "placeholder"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
result.reject { |k, _| HANDLED_VARS.include?(k) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.uncommented(content)
|
|
33
|
+
content.lines.reject { |l| l.match?(/^\s*#/) }.join
|
|
34
|
+
end
|
|
35
|
+
private_class_method :uncommented
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "env_vars_scanner"
|
|
4
|
+
require_relative "node_version_detector"
|
|
5
|
+
require_relative "ruby_version_detector"
|
|
6
|
+
|
|
7
|
+
module CaptiveStackDetector
|
|
8
|
+
class FileContentParser
|
|
9
|
+
def initialize(reader)
|
|
10
|
+
@reader = reader
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def ruby_version
|
|
14
|
+
RubyVersionDetector.new(@reader).detect
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def node_version
|
|
18
|
+
NodeVersionDetector.new(@reader).detect
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def env_vars
|
|
22
|
+
content = @reader.read("config/storage.yml")
|
|
23
|
+
content ? EnvVarsScanner.scan(content) : {}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
require_relative "file_content_parser"
|
|
5
|
+
require_relative "github_api_client"
|
|
6
|
+
|
|
7
|
+
module CaptiveStackDetector
|
|
8
|
+
class FileReader
|
|
9
|
+
def self.build(local_path: nil, github_token: nil, repo: nil)
|
|
10
|
+
return LocalFileReader.new(local_path) if local_path
|
|
11
|
+
return GithubFileReader.new(github_token, repo) if github_token && repo
|
|
12
|
+
|
|
13
|
+
raise ArgumentError, "local_path: ou github_token: + repo: requis"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class LocalFileReader
|
|
18
|
+
extend Forwardable
|
|
19
|
+
|
|
20
|
+
def_delegators :@parser, :ruby_version, :node_version, :env_vars
|
|
21
|
+
|
|
22
|
+
def initialize(path)
|
|
23
|
+
@path = path
|
|
24
|
+
@parser = FileContentParser.new(self)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def read(filename)
|
|
28
|
+
full = File.join(@path, filename)
|
|
29
|
+
File.read(full, encoding: "utf-8") if File.exist?(full)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class GithubFileReader
|
|
34
|
+
extend Forwardable
|
|
35
|
+
|
|
36
|
+
def_delegators :@parser, :ruby_version, :node_version, :env_vars
|
|
37
|
+
|
|
38
|
+
def initialize(token, repo)
|
|
39
|
+
@client = GithubApiClient.new(token, repo)
|
|
40
|
+
@parser = FileContentParser.new(self)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def read(filename)
|
|
44
|
+
@client.fetch(filename)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveStackDetector
|
|
4
|
+
class GemfileAnalyzer
|
|
5
|
+
ASSET_GEMS = %w[
|
|
6
|
+
sprockets sprockets-rails propshaft importmap-rails
|
|
7
|
+
cssbundling-rails jsbundling-rails dartsass-rails tailwindcss-rails
|
|
8
|
+
].freeze
|
|
9
|
+
|
|
10
|
+
REDIS_GEMS = %w[redis redis-client sidekiq].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(gemfile)
|
|
13
|
+
@gemfile = gemfile
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def rails? = gem?("rails")
|
|
17
|
+
def subtype = ASSET_GEMS.any? { |g| gem?(g) } ? "app" : "api"
|
|
18
|
+
def database = gem?("pg") ? "postgres" : nil
|
|
19
|
+
def queue = REDIS_GEMS.any? { |g| gem?(g) } ? "redis" : nil
|
|
20
|
+
|
|
21
|
+
def worker_command(procfile)
|
|
22
|
+
if procfile
|
|
23
|
+
procfile.each_line do |line|
|
|
24
|
+
m = line.match(/^worker:\s*(.+)$/)
|
|
25
|
+
return m[1].strip if m
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
gem?("sidekiq") ? "bundle exec sidekiq" : nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def gem?(name)
|
|
34
|
+
@gemfile.match?(/gem ['"]#{Regexp.escape(name)}['"]/)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "base64"
|
|
6
|
+
require "json"
|
|
7
|
+
|
|
8
|
+
module CaptiveStackDetector
|
|
9
|
+
class GithubApiClient
|
|
10
|
+
def initialize(token, repo)
|
|
11
|
+
@token = token
|
|
12
|
+
@repo = repo
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fetch(filename)
|
|
16
|
+
uri = URI("https://api.github.com/repos/#{@repo}/contents/#{filename}")
|
|
17
|
+
req = Net::HTTP::Get.new(uri)
|
|
18
|
+
req["Authorization"] = "Bearer #{@token}"
|
|
19
|
+
req["Accept"] = "application/vnd.github+json"
|
|
20
|
+
|
|
21
|
+
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
|
|
22
|
+
return nil unless res.is_a?(Net::HTTPSuccess)
|
|
23
|
+
|
|
24
|
+
Base64.decode64(JSON.parse(res.body)["content"]).force_encoding("utf-8")
|
|
25
|
+
rescue StandardError
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../captive_stack_detector"
|
|
4
|
+
require_relative "package_json_analyzer"
|
|
5
|
+
|
|
6
|
+
module CaptiveStackDetector
|
|
7
|
+
class JsStackDetector
|
|
8
|
+
def initialize(reader, package_json)
|
|
9
|
+
@reader = reader
|
|
10
|
+
@analyzer = PackageJsonAnalyzer.new(package_json)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def detect
|
|
14
|
+
type = @analyzer.type
|
|
15
|
+
raise UnsupportedStack unless type
|
|
16
|
+
|
|
17
|
+
Result.new(
|
|
18
|
+
type: type,
|
|
19
|
+
subtype: nil,
|
|
20
|
+
services: Services.new(database: @analyzer.database, queue: @analyzer.queue),
|
|
21
|
+
worker: build_worker,
|
|
22
|
+
runtime: Runtime.new(ruby: nil, node: @reader.node_version),
|
|
23
|
+
env_vars: {},
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def build_worker
|
|
30
|
+
procfile = @reader.read("Procfile")
|
|
31
|
+
return nil unless procfile
|
|
32
|
+
|
|
33
|
+
procfile.each_line do |line|
|
|
34
|
+
m = line.match(/^worker:\s*(.+)$/)
|
|
35
|
+
return Worker.new(command: m[1].strip) if m
|
|
36
|
+
end
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module CaptiveStackDetector
|
|
6
|
+
class NodeVersionDetector
|
|
7
|
+
def initialize(reader)
|
|
8
|
+
@reader = reader
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def detect
|
|
12
|
+
return from_nvmrc if (nvmrc = @reader.read(".nvmrc"))
|
|
13
|
+
return from_tool_versions if (tv = @reader.read(".tool-versions"))
|
|
14
|
+
return from_package_json if (pkg = @reader.read("package.json"))
|
|
15
|
+
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def from_nvmrc
|
|
22
|
+
@reader.read(".nvmrc").strip.sub(/^v/, "").split(".").first
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def from_tool_versions
|
|
26
|
+
m = @reader.read(".tool-versions")&.match(/^nodejs\s+(\d+)/)
|
|
27
|
+
m&.[](1)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def from_package_json
|
|
31
|
+
m = JSON.parse(@reader.read("package.json")).dig("engines", "node")&.match(/\d+/)
|
|
32
|
+
m&.[](0)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module CaptiveStackDetector
|
|
6
|
+
class PackageJsonAnalyzer
|
|
7
|
+
def initialize(package_json)
|
|
8
|
+
@parsed = JSON.parse(package_json)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def type
|
|
12
|
+
return "expo" if deps.key?("expo")
|
|
13
|
+
return "node" if @parsed.dig("scripts", "start")
|
|
14
|
+
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def database
|
|
19
|
+
deps.key?("pg") ? "postgres" : nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def queue
|
|
23
|
+
(deps.key?("redis") || deps.key?("ioredis")) ? "redis" : nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def deps
|
|
29
|
+
@parsed.fetch("dependencies", {}).merge(@parsed.fetch("devDependencies", {}))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../captive_stack_detector"
|
|
4
|
+
|
|
5
|
+
module CaptiveStackDetector
|
|
6
|
+
class RailsStackDetector
|
|
7
|
+
def initialize(reader, analyzer)
|
|
8
|
+
@reader = reader
|
|
9
|
+
@analyzer = analyzer
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def detect
|
|
13
|
+
raise UnsupportedStack unless @analyzer.rails?
|
|
14
|
+
|
|
15
|
+
Result.new(
|
|
16
|
+
type: "rails",
|
|
17
|
+
subtype: @analyzer.subtype,
|
|
18
|
+
services: Services.new(database: @analyzer.database, queue: @analyzer.queue),
|
|
19
|
+
worker: build_worker,
|
|
20
|
+
runtime: Runtime.new(ruby: @reader.ruby_version, node: nil),
|
|
21
|
+
env_vars: @reader.env_vars,
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def build_worker
|
|
28
|
+
command = @analyzer.worker_command(@reader.read("Procfile"))
|
|
29
|
+
command ? Worker.new(command: command) : nil
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveStackDetector
|
|
4
|
+
class RubyVersionDetector
|
|
5
|
+
def initialize(reader)
|
|
6
|
+
@reader = reader
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def detect
|
|
10
|
+
return from_tool_versions if @reader.read(".tool-versions")
|
|
11
|
+
return from_ruby_version if @reader.read(".ruby-version")
|
|
12
|
+
return from_gemfile if @reader.read("Gemfile")
|
|
13
|
+
|
|
14
|
+
nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def from_tool_versions
|
|
20
|
+
@reader.read(".tool-versions")&.match(/^ruby\s+(\S+)/)&.[](1)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def from_ruby_version
|
|
24
|
+
@reader.read(".ruby-version")&.strip&.sub(/^ruby-/, "")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def from_gemfile
|
|
28
|
+
@reader.read("Gemfile")&.match(/^\s*ruby\s+['"](\d+\.\d+\.\d+)['"]/)&.[](1)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CaptiveStackDetector
|
|
4
|
+
Services = Data.define(:database, :queue)
|
|
5
|
+
Worker = Data.define(:command)
|
|
6
|
+
Runtime = Data.define(:ruby, :node)
|
|
7
|
+
Result = Data.define(:type, :subtype, :services, :worker, :runtime, :env_vars)
|
|
8
|
+
|
|
9
|
+
UnsupportedStack = Class.new(StandardError)
|
|
10
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require_relative "captive_stack_detector/types"
|
|
5
|
+
require_relative "captive_stack_detector/file_reader"
|
|
6
|
+
require_relative "captive_stack_detector/gemfile_analyzer"
|
|
7
|
+
require_relative "captive_stack_detector/rails_stack_detector"
|
|
8
|
+
require_relative "captive_stack_detector/js_stack_detector"
|
|
9
|
+
|
|
10
|
+
module CaptiveStackDetector
|
|
11
|
+
def self.detect(local_path: nil, github_token: nil, repo: nil)
|
|
12
|
+
reader = FileReader.build(local_path: local_path, github_token: github_token, repo: repo)
|
|
13
|
+
detect_from(reader)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.detect_from(reader)
|
|
17
|
+
gemfile = reader.read("Gemfile")
|
|
18
|
+
package_json = reader.read("package.json")
|
|
19
|
+
|
|
20
|
+
return RailsStackDetector.new(reader, GemfileAnalyzer.new(gemfile)).detect if gemfile
|
|
21
|
+
return JsStackDetector.new(reader, package_json).detect if package_json
|
|
22
|
+
|
|
23
|
+
raise UnsupportedStack
|
|
24
|
+
end
|
|
25
|
+
private_class_method :detect_from
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: captive-stack-detector
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Captive Studio
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: 'Logique pure sans I/O : reçoit des strings de contenu de fichiers, retourne
|
|
13
|
+
un StackResult typé.'
|
|
14
|
+
email:
|
|
15
|
+
- dev@captive.fr
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- lib/captive/stack/detector.rb
|
|
21
|
+
- lib/captive_stack_detector.rb
|
|
22
|
+
- lib/captive_stack_detector/env_vars_scanner.rb
|
|
23
|
+
- lib/captive_stack_detector/file_content_parser.rb
|
|
24
|
+
- lib/captive_stack_detector/file_reader.rb
|
|
25
|
+
- lib/captive_stack_detector/gemfile_analyzer.rb
|
|
26
|
+
- lib/captive_stack_detector/github_api_client.rb
|
|
27
|
+
- lib/captive_stack_detector/js_stack_detector.rb
|
|
28
|
+
- lib/captive_stack_detector/node_version_detector.rb
|
|
29
|
+
- lib/captive_stack_detector/package_json_analyzer.rb
|
|
30
|
+
- lib/captive_stack_detector/rails_stack_detector.rb
|
|
31
|
+
- lib/captive_stack_detector/ruby_version_detector.rb
|
|
32
|
+
- lib/captive_stack_detector/types.rb
|
|
33
|
+
- lib/captive_stack_detector/version.rb
|
|
34
|
+
homepage: https://github.com/captive-studio/captive-stack-detector
|
|
35
|
+
licenses:
|
|
36
|
+
- MIT
|
|
37
|
+
metadata: {}
|
|
38
|
+
rdoc_options: []
|
|
39
|
+
require_paths:
|
|
40
|
+
- lib
|
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '3.2'
|
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '0'
|
|
51
|
+
requirements: []
|
|
52
|
+
rubygems_version: 4.0.10
|
|
53
|
+
specification_version: 4
|
|
54
|
+
summary: Détection de stack (Rails, Node, Expo) à partir du contenu de fichiers de
|
|
55
|
+
repo
|
|
56
|
+
test_files: []
|