jiminy 0.1.0.pre1

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 (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