codeclimate 0.69.0 → 0.70.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.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/bin/prep-release +1 -1
  3. data/config/engines.yml +32 -323
  4. data/lib/cc/analyzer.rb +5 -4
  5. data/lib/cc/analyzer/bridge.rb +106 -0
  6. data/lib/cc/analyzer/composite_container_listener.rb +4 -8
  7. data/lib/cc/analyzer/container.rb +44 -41
  8. data/lib/cc/analyzer/container/result.rb +74 -0
  9. data/lib/cc/analyzer/container_listener.rb +2 -7
  10. data/lib/cc/analyzer/engine.rb +53 -45
  11. data/lib/cc/analyzer/engine_output.rb +40 -10
  12. data/lib/cc/analyzer/formatters/formatter.rb +2 -0
  13. data/lib/cc/analyzer/formatters/html_formatter.rb +4 -0
  14. data/lib/cc/analyzer/formatters/json_formatter.rb +1 -0
  15. data/lib/cc/analyzer/formatters/plain_text_formatter.rb +8 -1
  16. data/lib/cc/analyzer/issue.rb +4 -2
  17. data/lib/cc/analyzer/issue_validations/relative_path_validation.rb +6 -2
  18. data/lib/cc/analyzer/issue_validator.rb +3 -32
  19. data/lib/cc/analyzer/logging_container_listener.rb +9 -7
  20. data/lib/cc/analyzer/measurement.rb +22 -0
  21. data/lib/cc/analyzer/measurement_validations.rb +16 -0
  22. data/lib/cc/analyzer/measurement_validations/name_validation.rb +23 -0
  23. data/lib/cc/analyzer/measurement_validations/type_validation.rb +15 -0
  24. data/lib/cc/analyzer/measurement_validations/validation.rb +27 -0
  25. data/lib/cc/analyzer/measurement_validations/value_validation.rb +21 -0
  26. data/lib/cc/analyzer/measurement_validator.rb +11 -0
  27. data/lib/cc/analyzer/raising_container_listener.rb +18 -18
  28. data/lib/cc/analyzer/statsd_container_listener.rb +22 -22
  29. data/lib/cc/analyzer/validator.rb +38 -0
  30. data/lib/cc/cli.rb +12 -12
  31. data/lib/cc/cli/analyze.rb +42 -60
  32. data/lib/cc/cli/analyze/engine_failure.rb +11 -0
  33. data/lib/cc/cli/command.rb +0 -10
  34. data/lib/cc/cli/engines.rb +0 -3
  35. data/lib/cc/cli/engines/engine_command.rb +2 -34
  36. data/lib/cc/cli/engines/install.rb +11 -17
  37. data/lib/cc/cli/engines/list.rb +5 -3
  38. data/lib/cc/cli/prepare.rb +5 -11
  39. data/lib/cc/cli/runner.rb +1 -2
  40. data/lib/cc/cli/test.rb +0 -1
  41. data/lib/cc/cli/validate_config.rb +49 -63
  42. data/lib/cc/cli/version_checker.rb +3 -3
  43. data/lib/cc/config.rb +70 -0
  44. data/lib/cc/config/checks_adapter.rb +40 -0
  45. data/lib/cc/config/default_adapter.rb +52 -0
  46. data/lib/cc/config/engine.rb +41 -0
  47. data/lib/cc/config/engine_set.rb +47 -0
  48. data/lib/cc/config/json_adapter.rb +17 -0
  49. data/lib/cc/config/prepare.rb +92 -0
  50. data/lib/cc/config/validation/check_validator.rb +34 -0
  51. data/lib/cc/config/validation/engine_validator.rb +89 -0
  52. data/lib/cc/config/validation/fetch_validator.rb +78 -0
  53. data/lib/cc/config/validation/file_validator.rb +112 -0
  54. data/lib/cc/config/validation/hash_validations.rb +52 -0
  55. data/lib/cc/config/validation/json.rb +31 -0
  56. data/lib/cc/config/validation/prepare_validator.rb +40 -0
  57. data/lib/cc/config/validation/yaml.rb +66 -0
  58. data/lib/cc/config/yaml_adapter.rb +73 -0
  59. data/lib/cc/engine_registry.rb +74 -0
  60. data/lib/cc/workspace/path_tree/dir_node.rb +1 -1
  61. metadata +36 -55
  62. data/bin/codeclimate-init +0 -6
  63. data/config/coffeelint/coffeelint.json +0 -129
  64. data/config/csslint/.csslintrc +0 -2
  65. data/config/eslint/.eslintignore +0 -1
  66. data/config/eslint/.eslintrc.yml +0 -277
  67. data/config/rubocop/.rubocop.yml +0 -1156
  68. data/lib/cc/analyzer/config.rb +0 -86
  69. data/lib/cc/analyzer/engine_registry.rb +0 -36
  70. data/lib/cc/analyzer/engines_config_builder.rb +0 -97
  71. data/lib/cc/analyzer/engines_runner.rb +0 -64
  72. data/lib/cc/cli/config.rb +0 -44
  73. data/lib/cc/cli/config_generator.rb +0 -108
  74. data/lib/cc/cli/engines/disable.rb +0 -38
  75. data/lib/cc/cli/engines/enable.rb +0 -41
  76. data/lib/cc/cli/engines/remove.rb +0 -35
  77. data/lib/cc/cli/init.rb +0 -117
  78. data/lib/cc/cli/prepare/quality.rb +0 -64
  79. data/lib/cc/cli/upgrade_config_generator.rb +0 -42
@@ -2,17 +2,14 @@ require "yaml"
2
2
 
3
3
  module CC
4
4
  module Analyzer
5
+ autoload :Bridge, "cc/analyzer/bridge"
5
6
  autoload :CompositeContainerListener, "cc/analyzer/composite_container_listener"
6
- autoload :Config, "cc/analyzer/config"
7
7
  autoload :Container, "cc/analyzer/container"
8
8
  autoload :ContainerListener, "cc/analyzer/container_listener"
9
9
  autoload :Engine, "cc/analyzer/engine"
10
10
  autoload :EngineOutput, "cc/analyzer/engine_output"
11
11
  autoload :EngineOutputFilter, "cc/analyzer/engine_output_filter"
12
12
  autoload :EngineOutputOverrider, "cc/analyzer/engine_output_overrider"
13
- autoload :EngineRegistry, "cc/analyzer/engine_registry"
14
- autoload :EnginesConfigBuilder, "cc/analyzer/engines_config_builder"
15
- autoload :EnginesRunner, "cc/analyzer/engines_runner"
16
13
  autoload :Filesystem, "cc/analyzer/filesystem"
17
14
  autoload :Formatters, "cc/analyzer/formatters"
18
15
  autoload :Issue, "cc/analyzer/issue"
@@ -21,12 +18,16 @@ module CC
21
18
  autoload :IssueValidator, "cc/analyzer/issue_validator"
22
19
  autoload :LocationDescription, "cc/analyzer/location_description"
23
20
  autoload :LoggingContainerListener, "cc/analyzer/logging_container_listener"
21
+ autoload :Measurement, "cc/analyzer/measurement"
22
+ autoload :MeasurementValidations, "cc/analyzer/measurement_validations"
23
+ autoload :MeasurementValidator, "cc/analyzer/measurement_validator"
24
24
  autoload :MountedPath, "cc/analyzer/mounted_path"
25
25
  autoload :RaisingContainerListener, "cc/analyzer/raising_container_listener"
26
26
  autoload :SourceBuffer, "cc/analyzer/source_buffer"
27
27
  autoload :SourceExtractor, "cc/analyzer/source_extractor"
28
28
  autoload :SourceFingerprint, "cc/analyzer/source_fingerprint"
29
29
  autoload :StatsdContainerListener, "cc/analyzer/statsd_container_listener"
30
+ autoload :Validator, "cc/analyzer/validator"
30
31
 
31
32
  class DummyStatsd
32
33
  def method_missing(*)
@@ -0,0 +1,106 @@
1
+ module CC
2
+ module Analyzer
3
+ # The shared interface, invoked by Builder or CLI::Analyze
4
+ #
5
+ # Input:
6
+ # - config
7
+ # - engines
8
+ # - exclude_patterns
9
+ # - development?
10
+ # - analysis_paths
11
+ # - formatter
12
+ # - started
13
+ # - engine_running
14
+ # - finished
15
+ # - close
16
+ # - listener
17
+ # - started(engine, details)
18
+ # - finished(engine, details, result)
19
+ # - registry
20
+ #
21
+ # Only raises if Listener raises
22
+ #
23
+ class Bridge
24
+ def initialize(config:, formatter:, listener:, registry:)
25
+ @config = config
26
+ @formatter = formatter
27
+ @listener = listener
28
+ @registry = registry
29
+ end
30
+
31
+ def run
32
+ formatter.started
33
+
34
+ config.engines.each do |engine|
35
+ next unless engine.enabled?
36
+
37
+ formatter.engine_running(engine) do
38
+ result = nil
39
+ engine_details = nil
40
+
41
+ begin
42
+ engine_details = registry.fetch_engine_details(
43
+ engine,
44
+ development: config.development?,
45
+ )
46
+ listener.started(engine, engine_details)
47
+ result = run_engine(engine, engine_details)
48
+ rescue CC::EngineRegistry::EngineDetailsNotFoundError => ex
49
+ result = Container::Result.skipped(ex)
50
+ end
51
+
52
+ listener.finished(engine, engine_details, result)
53
+ result
54
+ end
55
+ end
56
+
57
+ formatter.finished
58
+ ensure
59
+ formatter.close
60
+ end
61
+
62
+ private
63
+
64
+ attr_reader :config, :formatter, :listener, :registry
65
+
66
+ def run_engine(engine, engine_details)
67
+ # Analyzer::Engine doesn't have the best interface, but we're limiting
68
+ # our refactors for now.
69
+ Engine.new(
70
+ engine.name,
71
+ {
72
+ "image" => engine_details.image,
73
+ "command" => engine_details.command,
74
+ "memory" => engine_details.memory,
75
+ },
76
+ engine.config.merge(
77
+ "channel" => engine.channel,
78
+ "include_paths" => engine_workspace(engine).paths,
79
+ ),
80
+ engine.container_label,
81
+ ).run(formatter)
82
+ end
83
+
84
+ def engine_workspace(engine)
85
+ if engine.exclude_patterns.any?
86
+ workspace.clone.tap do |engine_workspace|
87
+ engine_workspace.remove(engine.exclude_patterns)
88
+ end
89
+ else
90
+ workspace
91
+ end
92
+ end
93
+
94
+ def workspace
95
+ @workspace ||= Workspace.new.tap do |workspace|
96
+ workspace.add(config.analysis_paths)
97
+
98
+ unless config.analysis_paths.any?
99
+ workspace.remove([".git"])
100
+ workspace.remove(config.exclude_patterns)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
@@ -5,16 +5,12 @@ module CC
5
5
  @listeners = listeners
6
6
  end
7
7
 
8
- def started(data)
9
- listeners.each { |listener| listener.started(data) }
8
+ def started(*args)
9
+ listeners.each { |listener| listener.started(*args) }
10
10
  end
11
11
 
12
- def timed_out(data)
13
- listeners.each { |listener| listener.timed_out(data) }
14
- end
15
-
16
- def finished(data)
17
- listeners.each { |listener| listener.finished(data) }
12
+ def finished(*args)
13
+ listeners.each { |listener| listener.finished(*args) }
18
14
  end
19
15
 
20
16
  private
@@ -1,42 +1,47 @@
1
1
  require "posix/spawn"
2
2
  require "thread"
3
3
 
4
+ require "cc/analyzer/container/result"
5
+
4
6
  module CC
5
7
  module Analyzer
8
+ #
9
+ # Running an abstract docker container
10
+ #
11
+ # Input:
12
+ # - image
13
+ # - name
14
+ # - command (Optional)
15
+ #
16
+ # Output:
17
+ # - Result
18
+ # - exit_status
19
+ # - timed_out?
20
+ # - duration
21
+ # - maximum_output_exceeded?
22
+ # - output_byte_count
23
+ # - stderr
24
+ #
25
+ # Never raises (unless broken)
26
+ #
6
27
  class Container
7
- ContainerData = Struct.new(
8
- :image, # image used to create the container
9
- :name, # name given to the container when created
10
- :duration, # duration, for a finished event
11
- :status, # status, for a finished event
12
- :stderr, # stderr, for a finished event
13
- )
14
- ImageRequired = Class.new(StandardError)
15
- Result = Struct.new(
16
- :exit_status,
17
- :timed_out?,
18
- :duration,
19
- :maximum_output_exceeded?,
20
- :output_byte_count,
21
- :stderr,
22
- )
23
-
24
28
  DEFAULT_TIMEOUT = 15 * 60 # 15m
25
29
  DEFAULT_MAXIMUM_OUTPUT_BYTES = 500_000_000
26
30
 
27
- def initialize(image:, name:, command: nil, listener: ContainerListener.new)
28
- raise ImageRequired if image.blank?
31
+ def initialize(image:, name:, command: nil)
29
32
  @image = image
30
33
  @name = name
31
34
  @command = command
32
- @listener = listener
33
- @output_delimeter = "\n"
34
- @on_output = ->(*) {}
35
35
  @timed_out = false
36
36
  @maximum_output_exceeded = false
37
+ @stdout_io = StringIO.new
37
38
  @stderr_io = StringIO.new
38
39
  @output_byte_count = 0
39
40
  @counter_mutex = Mutex.new
41
+
42
+ # By default accumulate and include stdout in result
43
+ @output_delimeter = "\n"
44
+ @on_output = ->(output) { @stdout_io.puts(output) }
40
45
  end
41
46
 
42
47
  def on_output(delimeter = "\n", &block)
@@ -46,10 +51,9 @@ module CC
46
51
 
47
52
  def run(options = [])
48
53
  started = Time.now
49
- @listener.started(container_data)
50
54
 
51
55
  command = docker_run_command(options)
52
- CLI.debug("docker run: #{command.inspect}")
56
+ Analyzer.logger.debug("docker run: #{command.inspect}")
53
57
  pid, _, out, err = POSIX::Spawn.popen4(*command)
54
58
 
55
59
  @t_out = read_stdout(out)
@@ -70,21 +74,22 @@ module CC
70
74
  # will unblock with the correct value in @timed_out
71
75
  [@t_out, @t_err].each(&:join)
72
76
 
73
- if @timed_out
74
- duration = timeout * 1000
75
- @listener.timed_out(container_data(duration: duration))
76
- else
77
- duration = ((Time.now - started) * 1000).round
78
- @listener.finished(container_data(duration: duration, status: @status))
79
- end
77
+ duration =
78
+ if @timed_out
79
+ timeout * 1000
80
+ else
81
+ ((Time.now - started) * 1000).round
82
+ end
80
83
 
81
84
  Result.new(
82
- @status && @status.exitstatus,
83
- @timed_out,
84
- duration,
85
- @maximum_output_exceeded,
86
- output_byte_count,
87
- @stderr_io.string,
85
+ container_name: @name,
86
+ duration: duration,
87
+ exit_status: @status && @status.exitstatus,
88
+ maximum_output_exceeded: @maximum_output_exceeded,
89
+ output_byte_count: output_byte_count,
90
+ stderr: @stderr_io.string,
91
+ stdout: @stdout_io.string,
92
+ timed_out: @timed_out,
88
93
  )
89
94
  ensure
90
95
  kill_reader_threads
@@ -117,6 +122,7 @@ module CC
117
122
  out.each_line(@output_delimeter) do |chunk|
118
123
  output = chunk.chomp(@output_delimeter)
119
124
 
125
+ Analyzer.logger.debug("engine stdout: #{output}")
120
126
  @on_output.call(output)
121
127
  check_output_bytes(output.bytesize)
122
128
  end
@@ -130,6 +136,7 @@ module CC
130
136
  Thread.new do
131
137
  begin
132
138
  err.each_line do |line|
139
+ Analyzer.logger.debug("engine stderr: #{line.chomp}")
133
140
  @stderr_io.write(line)
134
141
  check_output_bytes(line.bytesize)
135
142
  end
@@ -166,10 +173,6 @@ module CC
166
173
  end
167
174
  end
168
175
 
169
- def container_data(duration: nil, status: nil)
170
- ContainerData.new(@image, @name, duration, status, @stderr_io.string)
171
- end
172
-
173
176
  def kill_reader_threads
174
177
  @t_out.kill if @t_out
175
178
  @t_err.kill if @t_err
@@ -0,0 +1,74 @@
1
+ module CC
2
+ module Analyzer
3
+ class Container
4
+ class Result
5
+ attr_reader \
6
+ :container_name,
7
+ :duration,
8
+ :exit_status,
9
+ :output_byte_count,
10
+ :stderr,
11
+ :stdout
12
+
13
+ def initialize(
14
+ container_name: "",
15
+ duration: 0,
16
+ exit_status: 0,
17
+ maximum_output_exceeded: false,
18
+ output_byte_count: 0,
19
+ skipped: false,
20
+ stderr: "",
21
+ stdout: "",
22
+ timed_out: false
23
+ )
24
+ @container_name = container_name
25
+ @duration = duration
26
+ @exit_status = exit_status
27
+ @maximum_output_exceeded = maximum_output_exceeded
28
+ @output_byte_count = output_byte_count
29
+ @skipped = skipped
30
+ @stderr = stderr
31
+ @stdout = stdout
32
+ @timed_out = timed_out
33
+ end
34
+
35
+ def self.skipped(ex)
36
+ new(
37
+ exit_status: 0,
38
+ skipped: true,
39
+ stderr: ex.message,
40
+ )
41
+ end
42
+
43
+ def merge_from_exception(ex)
44
+ self.exit_status = 99
45
+ self.stderr = ex.message
46
+ self
47
+ end
48
+
49
+ def timed_out?
50
+ @timed_out
51
+ end
52
+
53
+ def maximum_output_exceeded?
54
+ @maximum_output_exceeded
55
+ end
56
+
57
+ def errored?
58
+ timed_out? ||
59
+ maximum_output_exceeded? ||
60
+ exit_status.nil? ||
61
+ exit_status.nonzero?
62
+ end
63
+
64
+ def skipped?
65
+ @skipped
66
+ end
67
+
68
+ private
69
+
70
+ attr_writer :exit_status, :stderr
71
+ end
72
+ end
73
+ end
74
+ end
@@ -1,14 +1,9 @@
1
1
  module CC
2
2
  module Analyzer
3
3
  class ContainerListener
4
- def started(_data)
5
- end
4
+ def started(_engine, _details); end
6
5
 
7
- def timed_out(_data)
8
- end
9
-
10
- def finished(_data)
11
- end
6
+ def finished(_engine, _details, _result); end
12
7
  end
13
8
  end
14
9
  end
@@ -2,67 +2,73 @@ require "securerandom"
2
2
 
3
3
  module CC
4
4
  module Analyzer
5
+ #
6
+ # Running specifically an Engine container
7
+ #
8
+ # Input:
9
+ # - name
10
+ # - metadata
11
+ # - image
12
+ # - command (optional)
13
+ # - config (becomes /config.json)
14
+ # - label
15
+ # - io (to write filtered, validated output)
16
+ #
17
+ # Output:
18
+ # - Container::Result
19
+ #
5
20
  class Engine
6
- EngineFailure = Class.new(StandardError)
7
- EngineTimeout = Class.new(StandardError)
21
+ Error = Class.new(StandardError)
8
22
 
9
- attr_reader :name
10
-
11
- DEFAULT_MEMORY_LIMIT = 512_000_000.freeze
12
-
13
- def initialize(name, metadata, code_path, config, label)
23
+ def initialize(name, metadata, config, label)
14
24
  @name = name
15
25
  @metadata = metadata
16
- @code_path = code_path
17
26
  @config = config
18
27
  @label = label.to_s
28
+ @error = nil
19
29
  end
20
30
 
21
- def run(stdout_io, container_listener)
22
- composite_listener = CompositeContainerListener.new(
23
- container_listener,
24
- LoggingContainerListener.new(qualified_name, Analyzer.logger),
25
- StatsdContainerListener.new(qualified_name.tr(":", "."), Analyzer.statsd),
26
- RaisingContainerListener.new(qualified_name, EngineFailure, EngineTimeout),
27
- )
31
+ def run(io)
32
+ write_config_file
28
33
 
29
34
  container = Container.new(
30
- image: @metadata["image"],
31
- command: @metadata["command"],
35
+ image: metadata.fetch("image"),
36
+ command: metadata["command"],
32
37
  name: container_name,
33
- listener: composite_listener,
34
38
  )
35
39
 
36
- container.on_output("\0") do |raw_output|
37
- CLI.debug("#{qualified_name} engine output: #{raw_output.strip}")
38
- output = EngineOutput.new(raw_output)
39
-
40
- unless output.valid?
41
- stdout_io.failed("#{qualified_name} produced invalid output: #{output.error[:message]}")
42
- container.stop
43
- end
44
-
45
- unless output_filter.filter?(output)
46
- stdout_io.write(output_overrider.apply(output).to_json) || container.stop
47
- end
40
+ container.on_output("\0") do |output|
41
+ handle_output(container, io, output)
48
42
  end
49
43
 
50
- write_config_file
51
- CLI.debug("#{qualified_name} engine config: #{config_file.read}")
52
44
  container.run(container_options).tap do |result|
53
- CLI.debug("#{qualified_name} engine stderr: #{result.stderr}")
45
+ result.merge_from_exception(error) if error.present?
54
46
  end
55
- rescue Container::ImageRequired
56
- # Provide a clearer message given the context we have
57
- message = "Unable to find an image for #{qualified_name}."
58
- message << " Available channels: #{@metadata["channels"].keys.inspect}."
59
- raise Container::ImageRequired, message
60
47
  ensure
61
48
  delete_config_file
62
49
  end
63
50
 
64
51
  private
65
52
 
53
+ attr_reader :name, :metadata
54
+ attr_accessor :error
55
+
56
+ def handle_output(container, io, raw_output)
57
+ output = EngineOutput.new(name, raw_output)
58
+
59
+ return if output_filter.filter?(output)
60
+
61
+ unless output.valid?
62
+ self.error = Error.new("engine produced invalid output: #{output.error}")
63
+ container.stop("output invalid")
64
+ end
65
+
66
+ unless io.write(output_overrider.apply(output).to_json)
67
+ self.error = Error.new("#{io.class}#write returned false, indicating an error")
68
+ container.stop("output error")
69
+ end
70
+ end
71
+
66
72
  def qualified_name
67
73
  "#{name}:#{@config.fetch("channel", "stable")}"
68
74
  end
@@ -71,11 +77,12 @@ module CC
71
77
  [
72
78
  "--cap-drop", "all",
73
79
  "--label", "com.codeclimate.label=#{@label}",
74
- "--memory", memory_limit,
80
+ "--log-driver", "none",
81
+ "--memory", metadata["memory"].to_s,
75
82
  "--memory-swap", "-1",
76
83
  "--net", "none",
77
84
  "--rm",
78
- "--volume", "#{@code_path}:/code:ro",
85
+ "--volume", "#{code.host_path}:/code:ro",
79
86
  "--volume", "#{config_file.host_path}:/config.json:ro",
80
87
  "--user", "9000:9000"
81
88
  ]
@@ -86,6 +93,8 @@ module CC
86
93
  end
87
94
 
88
95
  def write_config_file
96
+ @config["debug"] = ENV["CODECLIMATE_DEBUG"]
97
+ Analyzer.logger.debug "/config.json content: #{@config.inspect}"
89
98
  config_file.write(@config.to_json)
90
99
  end
91
100
 
@@ -93,6 +102,10 @@ module CC
93
102
  config_file.delete if config_file.file?
94
103
  end
95
104
 
105
+ def code
106
+ @code ||= MountedPath.code
107
+ end
108
+
96
109
  def config_file
97
110
  @config_file ||= MountedPath.tmp.join(SecureRandom.uuid)
98
111
  end
@@ -104,11 +117,6 @@ module CC
104
117
  def output_overrider
105
118
  @output_overrider ||= EngineOutputOverrider.new(@config)
106
119
  end
107
-
108
- # Memory limit for a running engine in bytes
109
- def memory_limit
110
- (ENV["ENGINE_MEMORY_LIMIT_BYTES"] || DEFAULT_MEMORY_LIMIT).to_s
111
- end
112
120
  end
113
121
  end
114
122
  end