codeclimate-fede 0.85.23
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/check +18 -0
- data/bin/codeclimate +21 -0
- data/bin/prep-release +45 -0
- data/bin/release +41 -0
- data/bin/validate-release +18 -0
- data/config/engines.yml +322 -0
- data/lib/cc/analyzer.rb +50 -0
- data/lib/cc/analyzer/bridge.rb +106 -0
- data/lib/cc/analyzer/composite_container_listener.rb +21 -0
- data/lib/cc/analyzer/container.rb +208 -0
- data/lib/cc/analyzer/container/result.rb +74 -0
- data/lib/cc/analyzer/container_listener.rb +9 -0
- data/lib/cc/analyzer/engine.rb +125 -0
- data/lib/cc/analyzer/engine_output.rb +74 -0
- data/lib/cc/analyzer/engine_output_filter.rb +36 -0
- data/lib/cc/analyzer/engine_output_overrider.rb +31 -0
- data/lib/cc/analyzer/filesystem.rb +50 -0
- data/lib/cc/analyzer/formatters.rb +21 -0
- data/lib/cc/analyzer/formatters/formatter.rb +53 -0
- data/lib/cc/analyzer/formatters/html_formatter.rb +415 -0
- data/lib/cc/analyzer/formatters/json_formatter.rb +38 -0
- data/lib/cc/analyzer/formatters/plain_text_formatter.rb +101 -0
- data/lib/cc/analyzer/formatters/spinner.rb +35 -0
- data/lib/cc/analyzer/issue.rb +69 -0
- data/lib/cc/analyzer/issue_sorter.rb +30 -0
- data/lib/cc/analyzer/issue_validations.rb +26 -0
- data/lib/cc/analyzer/issue_validations/category_validation.rb +32 -0
- data/lib/cc/analyzer/issue_validations/check_name_presence_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/content_validation.rb +21 -0
- data/lib/cc/analyzer/issue_validations/description_presence_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/location_format_validation.rb +72 -0
- data/lib/cc/analyzer/issue_validations/other_locations_format_validation.rb +41 -0
- data/lib/cc/analyzer/issue_validations/path_existence_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/path_is_file_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/path_presence_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/relative_path_validation.rb +32 -0
- data/lib/cc/analyzer/issue_validations/remediation_points_validation.rb +25 -0
- data/lib/cc/analyzer/issue_validations/severity_validation.rb +39 -0
- data/lib/cc/analyzer/issue_validations/type_validation.rb +15 -0
- data/lib/cc/analyzer/issue_validations/validation.rb +35 -0
- data/lib/cc/analyzer/issue_validator.rb +11 -0
- data/lib/cc/analyzer/location_description.rb +45 -0
- data/lib/cc/analyzer/logging_container_listener.rb +24 -0
- data/lib/cc/analyzer/measurement.rb +22 -0
- data/lib/cc/analyzer/measurement_validations.rb +16 -0
- data/lib/cc/analyzer/measurement_validations/name_validation.rb +23 -0
- data/lib/cc/analyzer/measurement_validations/type_validation.rb +15 -0
- data/lib/cc/analyzer/measurement_validations/validation.rb +27 -0
- data/lib/cc/analyzer/measurement_validations/value_validation.rb +21 -0
- data/lib/cc/analyzer/measurement_validator.rb +11 -0
- data/lib/cc/analyzer/mounted_path.rb +80 -0
- data/lib/cc/analyzer/raising_container_listener.rb +32 -0
- data/lib/cc/analyzer/source_buffer.rb +47 -0
- data/lib/cc/analyzer/source_extractor.rb +79 -0
- data/lib/cc/analyzer/source_fingerprint.rb +40 -0
- data/lib/cc/analyzer/statsd_container_listener.rb +51 -0
- data/lib/cc/analyzer/validator.rb +38 -0
- data/lib/cc/cli.rb +39 -0
- data/lib/cc/cli/analyze.rb +90 -0
- data/lib/cc/cli/analyze/engine_failure.rb +11 -0
- data/lib/cc/cli/command.rb +85 -0
- data/lib/cc/cli/console.rb +12 -0
- data/lib/cc/cli/engines.rb +5 -0
- data/lib/cc/cli/engines/engine_command.rb +15 -0
- data/lib/cc/cli/engines/install.rb +35 -0
- data/lib/cc/cli/engines/list.rb +18 -0
- data/lib/cc/cli/file_store.rb +42 -0
- data/lib/cc/cli/global_cache.rb +47 -0
- data/lib/cc/cli/global_config.rb +35 -0
- data/lib/cc/cli/help.rb +51 -0
- data/lib/cc/cli/output.rb +34 -0
- data/lib/cc/cli/prepare.rb +98 -0
- data/lib/cc/cli/runner.rb +75 -0
- data/lib/cc/cli/validate_config.rb +84 -0
- data/lib/cc/cli/version.rb +16 -0
- data/lib/cc/cli/version_checker.rb +107 -0
- data/lib/cc/config.rb +70 -0
- data/lib/cc/config/checks_adapter.rb +40 -0
- data/lib/cc/config/default_adapter.rb +54 -0
- data/lib/cc/config/engine.rb +41 -0
- data/lib/cc/config/engine_set.rb +47 -0
- data/lib/cc/config/json_adapter.rb +17 -0
- data/lib/cc/config/prepare.rb +92 -0
- data/lib/cc/config/validation/check_validator.rb +34 -0
- data/lib/cc/config/validation/engine_validator.rb +93 -0
- data/lib/cc/config/validation/fetch_validator.rb +78 -0
- data/lib/cc/config/validation/file_validator.rb +112 -0
- data/lib/cc/config/validation/hash_validations.rb +52 -0
- data/lib/cc/config/validation/json.rb +31 -0
- data/lib/cc/config/validation/prepare_validator.rb +40 -0
- data/lib/cc/config/validation/yaml.rb +66 -0
- data/lib/cc/config/yaml_adapter.rb +73 -0
- data/lib/cc/engine_registry.rb +74 -0
- data/lib/cc/resolv.rb +39 -0
- data/lib/cc/workspace.rb +39 -0
- data/lib/cc/workspace/exclusion.rb +34 -0
- data/lib/cc/workspace/path_tree.rb +49 -0
- data/lib/cc/workspace/path_tree/dir_node.rb +67 -0
- data/lib/cc/workspace/path_tree/file_node.rb +31 -0
- metadata +277 -0
@@ -0,0 +1,112 @@
|
|
1
|
+
module CC
|
2
|
+
class Config
|
3
|
+
module Validation
|
4
|
+
class FileValidator
|
5
|
+
include HashValidations
|
6
|
+
|
7
|
+
attr_reader :errors, :path, :warnings
|
8
|
+
|
9
|
+
def initialize(path, registry)
|
10
|
+
@path = path
|
11
|
+
@registry = registry
|
12
|
+
|
13
|
+
@errors = []
|
14
|
+
@warnings = []
|
15
|
+
|
16
|
+
validate
|
17
|
+
end
|
18
|
+
|
19
|
+
def valid?
|
20
|
+
errors.none?
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
attr_reader :data, :registry
|
26
|
+
|
27
|
+
def validate
|
28
|
+
raise NotImplementedError, "use a subclass"
|
29
|
+
end
|
30
|
+
|
31
|
+
def denormalize_subvalidator(validator, prefix)
|
32
|
+
validator.errors.each do |msg|
|
33
|
+
errors << "#{prefix}: #{msg}"
|
34
|
+
end
|
35
|
+
validator.warnings.each do |msg|
|
36
|
+
warnings << "#{prefix}: #{msg}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def validate_prepare
|
41
|
+
return unless validate_key_type("prepare", Hash)
|
42
|
+
|
43
|
+
validator = PrepareValidator.new(data.fetch("prepare", {}))
|
44
|
+
denormalize_subvalidator(validator, "prepare section")
|
45
|
+
end
|
46
|
+
|
47
|
+
def validate_engines(key, legacy: false)
|
48
|
+
return unless validate_key_type(key, Hash)
|
49
|
+
|
50
|
+
data.fetch(key, {}).each do |engine_name, engine_data|
|
51
|
+
engine_validator = EngineValidator.new(engine_data, legacy: legacy)
|
52
|
+
denormalize_subvalidator(engine_validator, "engine #{engine_name}")
|
53
|
+
|
54
|
+
if engine_validator.valid?
|
55
|
+
validate_engine_existence(engine_name, engine_data)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def validate_engine_existence(engine_name, engine_data)
|
61
|
+
if [true, false].include?(engine_data)
|
62
|
+
engine_data = {
|
63
|
+
"enabled" => true,
|
64
|
+
"channel" => Engine::DEFAULT_CHANNEL,
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
engine = Engine.new(
|
69
|
+
engine_name,
|
70
|
+
enabled: engine_data.fetch("enabled", true),
|
71
|
+
channel: engine_data["channel"],
|
72
|
+
config: engine_data["config"],
|
73
|
+
)
|
74
|
+
unless engine_exists?(engine)
|
75
|
+
warnings << "unknown engine or channel <#{engine.name}:#{engine.channel}>"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def engine_exists?(engine)
|
80
|
+
!registry.fetch_engine_details(engine).nil?
|
81
|
+
rescue CC::EngineRegistry::EngineDetailsNotFoundError
|
82
|
+
false
|
83
|
+
end
|
84
|
+
|
85
|
+
def validate_checks
|
86
|
+
return unless validate_key_type("checks", Hash)
|
87
|
+
|
88
|
+
data.fetch("checks", {}).each do |check_name, check_data|
|
89
|
+
validator = CheckValidator.new(check_data)
|
90
|
+
denormalize_subvalidator(validator, "check #{check_name}")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def validate_exclude_pattern(key, legacy: false)
|
95
|
+
types =
|
96
|
+
if legacy
|
97
|
+
[Array, String]
|
98
|
+
else
|
99
|
+
Array
|
100
|
+
end
|
101
|
+
return unless validate_key_type(key, types)
|
102
|
+
|
103
|
+
Array(data.fetch(key, [])).each do |pattern|
|
104
|
+
unless pattern.is_a?(String)
|
105
|
+
errors << "each exclude pattern should be a string, but '#{pattern.inspect}' is a #{pattern.class.to_s.downcase}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module CC
|
2
|
+
class Config
|
3
|
+
module Validation
|
4
|
+
module HashValidations
|
5
|
+
def validate_hash_data
|
6
|
+
unless data.is_a?(Hash)
|
7
|
+
errors << "Config file should contain a hash, not a #{data.class.to_s.downcase}"
|
8
|
+
return false
|
9
|
+
end
|
10
|
+
true
|
11
|
+
end
|
12
|
+
|
13
|
+
def validate_key_type(key, types)
|
14
|
+
if types.is_a?(Class)
|
15
|
+
return validate_key_type(key, [types])
|
16
|
+
elsif data.key?(key)
|
17
|
+
unless types.include?(data[key].class)
|
18
|
+
errors << key_type_error_message(key, types)
|
19
|
+
return false
|
20
|
+
end
|
21
|
+
end
|
22
|
+
true
|
23
|
+
end
|
24
|
+
|
25
|
+
def key_type_error_message(key, types)
|
26
|
+
if types.one?
|
27
|
+
klass_name = types[0].to_s.downcase
|
28
|
+
article =
|
29
|
+
if klass_name[0] == "a"
|
30
|
+
"an"
|
31
|
+
else
|
32
|
+
"a"
|
33
|
+
end
|
34
|
+
"'#{key}' must be #{article} #{klass_name}"
|
35
|
+
elsif types == [TrueClass, FalseClass]
|
36
|
+
"'#{key}' must be a boolean"
|
37
|
+
else
|
38
|
+
type_names = types.map(&:to_s).map(&:downcase)
|
39
|
+
"'#{key}' must be one of #{type_names.join(", ")}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def warn_unrecognized_keys(recognized_keys)
|
44
|
+
unknown_keys = data.keys.reject { |k| recognized_keys.include?(k) }
|
45
|
+
unknown_keys.each do |key|
|
46
|
+
warnings << "unrecognized key '#{key}'"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module CC
|
2
|
+
class Config
|
3
|
+
module Validation
|
4
|
+
class JSON < FileValidator
|
5
|
+
private
|
6
|
+
|
7
|
+
def validate
|
8
|
+
@data = ::JSON.parse(File.read(path))
|
9
|
+
|
10
|
+
return unless validate_hash_data
|
11
|
+
|
12
|
+
validate_version
|
13
|
+
validate_prepare
|
14
|
+
validate_engines("plugins")
|
15
|
+
validate_checks
|
16
|
+
validate_exclude_pattern("exclude_patterns")
|
17
|
+
|
18
|
+
warn_unrecognized_keys(%w[checks prepare plugins exclude_patterns version])
|
19
|
+
rescue ::JSON::ParserError => ex
|
20
|
+
errors << "Unable to parse: #{ex.message}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate_version
|
24
|
+
unless data.key?("version")
|
25
|
+
warnings << %(missing 'version' key. Please add `"version": "2"`)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module CC
|
2
|
+
class Config
|
3
|
+
module Validation
|
4
|
+
class PrepareValidator
|
5
|
+
include HashValidations
|
6
|
+
|
7
|
+
attr_reader :errors, :warnings
|
8
|
+
|
9
|
+
def initialize(data)
|
10
|
+
@data = data
|
11
|
+
|
12
|
+
@errors = []
|
13
|
+
@warnings = []
|
14
|
+
|
15
|
+
validate
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_reader :data
|
21
|
+
|
22
|
+
def validate
|
23
|
+
return unless validate_key_type("fetch", Array)
|
24
|
+
|
25
|
+
data.fetch("fetch", []).each do |fetch_data|
|
26
|
+
validator = FetchValidator.new(fetch_data)
|
27
|
+
validator.errors.each do |msg|
|
28
|
+
errors << msg
|
29
|
+
end
|
30
|
+
validator.warnings.each do |msg|
|
31
|
+
warnings << msg
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
warn_unrecognized_keys(%w[fetch])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module CC
|
2
|
+
class Config
|
3
|
+
module Validation
|
4
|
+
class YAML < FileValidator
|
5
|
+
private
|
6
|
+
|
7
|
+
def validate
|
8
|
+
@data = ::YAML.safe_load(File.read(path))
|
9
|
+
|
10
|
+
return unless validate_hash_data
|
11
|
+
|
12
|
+
validate_version
|
13
|
+
validate_prepare
|
14
|
+
|
15
|
+
validate_one_of(%w[engines plugins])
|
16
|
+
validate_one_of(%w[exclude_paths exclude_patterns])
|
17
|
+
|
18
|
+
validate_engines("engines", legacy: true)
|
19
|
+
validate_engines("plugins")
|
20
|
+
|
21
|
+
validate_checks
|
22
|
+
|
23
|
+
validate_exclude_pattern("exclude_patterns")
|
24
|
+
validate_exclude_pattern("exclude_paths", legacy: true)
|
25
|
+
|
26
|
+
deprecated_key_warnings
|
27
|
+
warn_unrecognized_keys(%w[checks prepare engines plugins ratings languages exclude_paths exclude_patterns version])
|
28
|
+
rescue Psych::SyntaxError => ex
|
29
|
+
errors << "Unable to parse: #{ex.message}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate_version
|
33
|
+
if !data.key?("version") && (data.key?("plugins") || data.key?("exclude_patterns"))
|
34
|
+
warnings << %(missing 'version' key. Please add `version: "2"`)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def validate_one_of(keys)
|
39
|
+
num = keys.map { |k| data.key?(k) }.select(&:present?).count
|
40
|
+
if num > 1
|
41
|
+
wrapped_keys = keys.map { |k| "'#{k}'" }
|
42
|
+
errors << "only use one of #{wrapped_keys.join(", ")}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def deprecated_key_warnings
|
47
|
+
deprecate_key("engines", "plugins")
|
48
|
+
deprecate_key("exclude_paths", "exclude_patterns")
|
49
|
+
deprecate_key("languages")
|
50
|
+
deprecate_key("ratings")
|
51
|
+
end
|
52
|
+
|
53
|
+
def deprecate_key(key, new_key = nil)
|
54
|
+
if data.key?(key)
|
55
|
+
warnings <<
|
56
|
+
if new_key.nil?
|
57
|
+
"'#{key}' has been deprecated, and will not be used"
|
58
|
+
else
|
59
|
+
"'#{key}' has been deprecated, please use '#{new_key}' instead"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module CC
|
2
|
+
class Config
|
3
|
+
class YAMLAdapter
|
4
|
+
DEFAULT_PATH = ".codeclimate.yml".freeze
|
5
|
+
|
6
|
+
attr_reader :config
|
7
|
+
|
8
|
+
def self.load(path = DEFAULT_PATH)
|
9
|
+
new(::YAML.safe_load(File.read(path)))
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(yaml = {})
|
13
|
+
@config = yaml || {}
|
14
|
+
|
15
|
+
upconvert_legacy_yaml!
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def coerce_engine(data)
|
21
|
+
if [true, false].include?(data)
|
22
|
+
{ "enabled" => data }
|
23
|
+
elsif data.is_a?(Hash)
|
24
|
+
data
|
25
|
+
else
|
26
|
+
{}
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Many of our plugins still expect:
|
31
|
+
#
|
32
|
+
# { config: PATH }
|
33
|
+
#
|
34
|
+
# But we document, and hope to eventually move to:
|
35
|
+
#
|
36
|
+
# { config: { file: PATH } }
|
37
|
+
#
|
38
|
+
# We need to munge from the latter to the former when/if we encounter it
|
39
|
+
def convert_to_legacy_file_config(config)
|
40
|
+
if config.is_a?(Hash) && config.keys.one? && config.key?("file")
|
41
|
+
config["file"]
|
42
|
+
else
|
43
|
+
config
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def upconvert_legacy_yaml!
|
48
|
+
config.delete("ratings")
|
49
|
+
|
50
|
+
if config.key?("engines")
|
51
|
+
config["plugins"] ||= config.delete("engines")
|
52
|
+
end
|
53
|
+
|
54
|
+
plugins = config.fetch("plugins", {})
|
55
|
+
plugins.each do |engine, data|
|
56
|
+
plugins[engine] = coerce_engine(data)
|
57
|
+
if plugins.fetch(engine)["exclude_paths"]
|
58
|
+
plugins.fetch(engine)["exclude_patterns"] ||= Array(plugins.fetch(engine).delete("exclude_paths"))
|
59
|
+
end
|
60
|
+
if plugins.fetch(engine)["config"]
|
61
|
+
plugins.fetch(engine)["config"] = convert_to_legacy_file_config(
|
62
|
+
plugins.fetch(engine).fetch("config"),
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
if config.key?("exclude_paths")
|
68
|
+
config["exclude_patterns"] ||= Array(config.delete("exclude_paths"))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module CC
|
2
|
+
class EngineRegistry
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
DEFAULT_MEMORY_LIMIT = 1_024_000_000
|
6
|
+
DEFAULT_COMMAND = nil
|
7
|
+
DEFAULT_MANIFEST_PATH = File.expand_path("../../../config/engines.yml", __FILE__)
|
8
|
+
|
9
|
+
EngineDetails = Struct.new(:image, :command, :description, :memory)
|
10
|
+
EngineDetailsNotFoundError = Class.new(StandardError)
|
11
|
+
|
12
|
+
def initialize(path = DEFAULT_MANIFEST_PATH, prefix = nil)
|
13
|
+
@yaml = YAML.safe_load(File.read(path))
|
14
|
+
@prefix = prefix || ENV["CODECLIMATE_PREFIX"] || ""
|
15
|
+
end
|
16
|
+
|
17
|
+
def each
|
18
|
+
yaml.each do |name, metadata|
|
19
|
+
engine = Config::Engine.new(
|
20
|
+
name,
|
21
|
+
channel: metadata.fetch("channels").keys.first,
|
22
|
+
)
|
23
|
+
engine_details = fetch_engine_details(engine)
|
24
|
+
|
25
|
+
yield(engine, engine_details)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def fetch_engine_details(engine, development: false)
|
30
|
+
if development
|
31
|
+
EngineDetails.new("codeclimate/codeclimate-#{engine.name}", nil, "")
|
32
|
+
else
|
33
|
+
metadata = yaml.fetch(engine.name)
|
34
|
+
channels = metadata.fetch("channels")
|
35
|
+
|
36
|
+
EngineDetails.new(
|
37
|
+
[prefix, channels.fetch(engine.channel)].join,
|
38
|
+
metadata.fetch("command", DEFAULT_COMMAND),
|
39
|
+
metadata.fetch("description", "(No description available)"),
|
40
|
+
memory_limit(metadata["minimum_memory_limit"]),
|
41
|
+
)
|
42
|
+
end
|
43
|
+
rescue KeyError
|
44
|
+
raise EngineDetailsNotFoundError, not_found_message(engine, channels)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
attr_reader :yaml, :prefix
|
50
|
+
|
51
|
+
def memory_limit(minimum_memory_limit)
|
52
|
+
[
|
53
|
+
minimum_memory_limit.to_i,
|
54
|
+
default_memory_limit.to_i,
|
55
|
+
].max
|
56
|
+
end
|
57
|
+
|
58
|
+
def default_memory_limit
|
59
|
+
ENV["ENGINE_MEMORY_LIMIT_BYTES"] || DEFAULT_MEMORY_LIMIT
|
60
|
+
end
|
61
|
+
|
62
|
+
def not_found_message(engine, available_channels)
|
63
|
+
if available_channels
|
64
|
+
# Known engine, unknown channel
|
65
|
+
"Channel #{engine.channel} not found" \
|
66
|
+
" for #{engine.name}," \
|
67
|
+
" available channels: #{available_channels.keys.inspect}"
|
68
|
+
else
|
69
|
+
# Unknown engine
|
70
|
+
"No engine named #{engine.name} found"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|