codeclimate 0.69.0 → 0.70.0

Sign up to get free protection for your applications and to get access to all the features.
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