jiminy 0.1.0.pre1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +20 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +0 -0
  6. data/Gemfile +12 -0
  7. data/Gemfile.lock +245 -0
  8. data/LICENSE +21 -0
  9. data/README.md +173 -0
  10. data/Rakefile +12 -0
  11. data/bin/console +15 -0
  12. data/bin/setup +8 -0
  13. data/example.png +0 -0
  14. data/exe/jiminy +9 -0
  15. data/jiminy.gemspec +42 -0
  16. data/lib/jiminy/cli.rb +137 -0
  17. data/lib/jiminy/configuration.rb +97 -0
  18. data/lib/jiminy/github_apiable.rb +17 -0
  19. data/lib/jiminy/recording/n_plus_one.rb +50 -0
  20. data/lib/jiminy/recording/prosopite_ext/send_notifications_with_tmp_file.rb +44 -0
  21. data/lib/jiminy/recording/prosopite_ext/tmp_file_recorder.rb +40 -0
  22. data/lib/jiminy/recording/rspec.rb +22 -0
  23. data/lib/jiminy/recording/test_controller_concern.rb +26 -0
  24. data/lib/jiminy/recording.rb +14 -0
  25. data/lib/jiminy/reporting/ci_providers/circle_ci/api_request.rb +45 -0
  26. data/lib/jiminy/reporting/ci_providers/circle_ci/artifact.rb +17 -0
  27. data/lib/jiminy/reporting/ci_providers/circle_ci/base.rb +46 -0
  28. data/lib/jiminy/reporting/ci_providers/circle_ci/job.rb +17 -0
  29. data/lib/jiminy/reporting/ci_providers/circle_ci/pipeline.rb +49 -0
  30. data/lib/jiminy/reporting/ci_providers/circle_ci/vcs.rb +13 -0
  31. data/lib/jiminy/reporting/ci_providers/circle_ci/workflow.rb +41 -0
  32. data/lib/jiminy/reporting/ci_providers/circle_ci.rb +54 -0
  33. data/lib/jiminy/reporting/ci_providers/github.rb +29 -0
  34. data/lib/jiminy/reporting/ci_providers/local/artifact.rb +25 -0
  35. data/lib/jiminy/reporting/ci_providers/local.rb +30 -0
  36. data/lib/jiminy/reporting/ci_providers/provider_configuration.rb +28 -0
  37. data/lib/jiminy/reporting/ci_providers.rb +12 -0
  38. data/lib/jiminy/reporting/n_plus_one.rb +39 -0
  39. data/lib/jiminy/reporting/reporters/base_reporter.rb +26 -0
  40. data/lib/jiminy/reporting/reporters/dry_run_reporter.rb +13 -0
  41. data/lib/jiminy/reporting/reporters/github_reporter.rb +28 -0
  42. data/lib/jiminy/reporting/reporters.rb +11 -0
  43. data/lib/jiminy/reporting/yaml_file_comment_presenter.rb +71 -0
  44. data/lib/jiminy/reporting.rb +32 -0
  45. data/lib/jiminy/rspec.rb +3 -0
  46. data/lib/jiminy/setup.rb +10 -0
  47. data/lib/jiminy/templates/reporting/comment_header.md.erb +5 -0
  48. data/lib/jiminy/templates/reporting/n_plus_one.md.erb +12 -0
  49. data/lib/jiminy/version.rb +5 -0
  50. data/lib/jiminy.rb +13 -0
  51. metadata +254 -0
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "provider_configuration"
4
+
5
+ require_relative "circle_ci/base"
6
+ require_relative "circle_ci/api_request"
7
+ require_relative "circle_ci/artifact"
8
+ require_relative "circle_ci/job"
9
+ require_relative "circle_ci/pipeline"
10
+ require_relative "circle_ci/vcs"
11
+ require_relative "circle_ci/workflow"
12
+
13
+ module Jiminy
14
+ module Reporting
15
+ module CIProviders
16
+ module CircleCI
17
+ # Not currently used, but kept for future use.
18
+ class Configuration < ProviderConfiguration
19
+ PR_URL_MATCHERS = %r{github\.com/
20
+ (?<username>[\w\-_]+)/
21
+ (?<reponame>[\w\-_]+)/
22
+ pull/
23
+ (?<pr_number>\d+)}x.freeze
24
+
25
+ def repo_path
26
+ [project_username, project_reponame].join("/")
27
+ end
28
+
29
+ def pr_number
30
+ match_data[:pr_number]
31
+ end
32
+
33
+ def project_username
34
+ match_data[:username]
35
+ end
36
+
37
+ def project_reponame
38
+ match_data[:reponame]
39
+ end
40
+
41
+ def github_token
42
+ ensure_env_variable("GITHUB_TOKEN")
43
+ end
44
+
45
+ private
46
+
47
+ def match_data
48
+ @_match_data ||= ensure_env_variable("CIRCLE_PULL_REQUEST").match(PR_URL_MATCHERS)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "provider_configuration"
4
+
5
+ module Jiminy
6
+ module Reporting
7
+ module CIProviders
8
+ module Github
9
+ class Configuration < ProviderConfiguration
10
+ def repo_path
11
+ ensure_configuration(:repo_path)
12
+ end
13
+
14
+ def project_username
15
+ repo_path.to_s.split("/").first
16
+ end
17
+
18
+ def project_reponame
19
+ repo_path.to_s.split("/").last
20
+ end
21
+
22
+ def github_token
23
+ ensure_configuration(:github_token)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ module Local
7
+ class Artifact
8
+ attr_accessor :url
9
+
10
+ def self.all
11
+ Dir[Jiminy.configuration.temp_file_location].map do |filepath|
12
+ new(url: filepath)
13
+ end
14
+ end
15
+
16
+ def initialize(attributes)
17
+ attributes.each do |key, value|
18
+ public_send(:"#{key}=", value)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "provider_configuration"
4
+ require_relative "local/artifact"
5
+
6
+ module Jiminy
7
+ module Reporting
8
+ module CIProviders
9
+ module Local
10
+ class Configuration < ProviderConfiguration
11
+ def repo_path
12
+ ensure_configuration(:repo_path)
13
+ end
14
+
15
+ def project_username
16
+ repo_path.to_s.split("/").first
17
+ end
18
+
19
+ def project_reponame
20
+ repo_path.to_s.split("/").last
21
+ end
22
+
23
+ def github_token
24
+ ensure_configuration(:github_token)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ class ProviderConfiguration
7
+ TEMPLATE_METHOD_PROC = -> { raise NotImplementedError, "Define #{__callee__} in #{self}" }
8
+
9
+ define_method(:repo_path, TEMPLATE_METHOD_PROC)
10
+
11
+ define_method(:project_username, TEMPLATE_METHOD_PROC)
12
+
13
+ define_method(:project_reponame, TEMPLATE_METHOD_PROC)
14
+
15
+ define_method(:github_token, TEMPLATE_METHOD_PROC)
16
+
17
+ private
18
+
19
+ def ensure_configuration(name)
20
+ value = Jiminy.config.public_send(name)
21
+ return value unless value.empty?
22
+
23
+ raise("Please provide a value for Jiminy.config.#{name}")
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ require_relative "ci_providers/provider_configuration"
7
+ require_relative "ci_providers/circle_ci"
8
+ require_relative "ci_providers/github"
9
+ require_relative "ci_providers/local"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ class NPlusOne
6
+ require "erb"
7
+
8
+ # https://docs.ruby-lang.org/en/2.3.0/ERB.html#method-c-new
9
+ ERB_SAFE_LEVEL = nil
10
+
11
+ TRIM_MODE = "-"
12
+
13
+ attr_reader :file, :line, :method, :examples
14
+
15
+ attr_accessor :blob_url
16
+
17
+ def initialize(file:, line:, method:, examples: [])
18
+ @examples = examples
19
+ @file = file
20
+ @line = line
21
+ @method = method
22
+ end
23
+
24
+ def to_markdown
25
+ ERB.new(markdown_template, trim_mode: TRIM_MODE).result(binding)
26
+ end
27
+
28
+ def blob_url_with_line
29
+ "#{blob_url}#L#{line}"
30
+ end
31
+
32
+ private
33
+
34
+ def markdown_template
35
+ @_markdown_template ||= File.read(File.join(TEMPLATES_DIR, "n_plus_one.md.erb"))
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module Reporters
6
+ class BaseReporter
7
+ def initialize(header:, body:)
8
+ @header = header
9
+ @body = body
10
+ end
11
+
12
+ def report!
13
+ raise NotImplementedError, "Please define #{report!} in #{self}"
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :header, :body
19
+
20
+ def report_body
21
+ "#{header}\n\n#{body}"
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module Reporters
6
+ class DryRunReporter < BaseReporter
7
+ def report!
8
+ $stdout.puts report_body
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module Reporters
6
+ class GithubReporter < BaseReporter
7
+ require "jiminy/github_apiable"
8
+
9
+ include GithubAPIable
10
+
11
+ def initialize(header:, body:, pr_number:)
12
+ super(header: header, body: body)
13
+ @pr_number = pr_number
14
+ end
15
+
16
+ def report!
17
+ client.add_comment(env_config.repo_path, pr_number, comment_body)
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :pr_number
23
+
24
+ alias comment_body report_body
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module Reporters
6
+ require_relative "reporters/base_reporter"
7
+ require_relative "reporters/dry_run_reporter"
8
+ require_relative "reporters/github_reporter"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ class YAMLFileCommentPresenter
6
+ require "open-uri"
7
+ require "yaml"
8
+ require "jiminy/github_apiable"
9
+
10
+ class MissingFileError < StandardError; end
11
+
12
+ include GithubAPIable
13
+
14
+ INSTANCE_SEPARATOR = "\n"
15
+
16
+ def initialize(source_filepath:, pr_number:)
17
+ @source_filepath = source_filepath
18
+ @pr_number = pr_number
19
+ end
20
+
21
+ def comment_body
22
+ @_comment_body ||= build_comment_body
23
+ end
24
+
25
+ alias to_s comment_body
26
+
27
+ private
28
+
29
+ attr_reader :source_filepath, :pr_number
30
+
31
+ def instances
32
+ @_instances ||= YAML.safe_load(file_content).map do |hash|
33
+ options = hash.values.first.transform_keys!(&:to_sym)
34
+ NPlusOne.new(**options)
35
+ end
36
+ end
37
+
38
+ def file_content
39
+ @_file_content ||= file_content_for_filepath(source_filepath)
40
+ end
41
+
42
+ def file_content_for_filepath(source_filepath)
43
+ return file_content_for_remote_file(source_filepath) if source_filepath.start_with?("https://")
44
+
45
+ file_content_for_local_file(source_filepath)
46
+ end
47
+
48
+ def file_content_for_remote_file(source_filepath)
49
+ URI.parse(source_filepath).open({ "Circle-Token" => ENV["CIRCLE_CI_API_TOKEN"] }).read
50
+ end
51
+
52
+ def file_content_for_local_file(source_filepath)
53
+ File.read(source_filepath)
54
+ end
55
+
56
+ def build_comment_body
57
+ instances.map do |instance|
58
+ file = file_from_instance(instance)
59
+ instance.blob_url = file.blob_url
60
+ instance.to_markdown
61
+ end.join(INSTANCE_SEPARATOR)
62
+ end
63
+
64
+ def file_from_instance(instance)
65
+ client.pull_request_files(env_config.repo_path, pr_number).detect do |file|
66
+ file.filename == instance.file
67
+ end or raise(MissingFileError)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ require_relative "reporting/n_plus_one"
6
+ require_relative "reporting/yaml_file_comment_presenter"
7
+ require_relative "reporting/ci_providers"
8
+ require_relative "reporting/reporters"
9
+
10
+ TEMPLATES_DIR = File.expand_path("templates/reporting", __dir__).freeze
11
+
12
+ COMMENT_HEADER = ERB.new(File.read(File.join(TEMPLATES_DIR, "comment_header.md.erb"))).result.freeze
13
+
14
+ LINE_SEPARATOR = "\n"
15
+
16
+ module_function
17
+
18
+ def report!(*yaml_files, **options)
19
+ return if yaml_files.empty?
20
+
21
+ comment_content = yaml_files.map do |yaml_file|
22
+ YAMLFileCommentPresenter.new(source_filepath: yaml_file, pr_number: options[:pr_number]).to_s
23
+ end.join(LINE_SEPARATOR)
24
+ if options[:dry_run]
25
+ Reporters::DryRunReporter.new(header: COMMENT_HEADER, body: comment_content).report!
26
+ else
27
+ Reporters::GithubReporter.new(header: COMMENT_HEADER, body: comment_content,
28
+ pr_number: options[:pr_number]).report!
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jiminy/recording/rspec"
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "configuration"
4
+
5
+ Jiminy.extend(Jiminy::ConfigurationMethods)
6
+
7
+ begin
8
+ load "./config/initializers/jiminy.rb"
9
+ rescue LoadError; nil
10
+ end
@@ -0,0 +1,5 @@
1
+ It looks like the following changes might have introduced new N+1 queries.
2
+
3
+ Please investigate this issue before merging these changes into `main`.
4
+
5
+ (You can ignore this issue by adding the file path to `.jiminy_ignores.yml`)
@@ -0,0 +1,12 @@
1
+
2
+ - [<%= file %>](<%= blob_url_with_line %>)
3
+
4
+ <details>
5
+ <summary>SQL sample from the issue detected</summary>
6
+
7
+ ``` sql
8
+ <%- for example in examples %>
9
+ <%= example -%>
10
+ <% end %>
11
+ ```
12
+ </details>
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ VERSION = "0.1.0.pre1"
5
+ end
data/lib/jiminy.rb ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "jiminy/version"
4
+ require_relative "jiminy/setup"
5
+ require_relative "jiminy/recording" if defined?(Rails)
6
+
7
+ module Jiminy
8
+ module_function
9
+
10
+ def reset_results_file!
11
+ Jiminy::Recording.reset_results_file!
12
+ end
13
+ end