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
data/lib/jiminy/cli.rb ADDED
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ require "thor"
5
+ require "byebug"
6
+ class CLI < Thor
7
+ require "jiminy/reporting/ci_providers/circle_ci"
8
+ include Jiminy::Reporting::CIProviders
9
+
10
+ class WorkflowStillRunningError < StandardError; end
11
+ private_constant :WorkflowStillRunningError
12
+
13
+ MAX_TIMEOUT = 1800 # 1 hour
14
+ POLL_INTERVAL = 60 # 1 min
15
+
16
+ def self.exit_on_failure?
17
+ false
18
+ end
19
+
20
+ desc "Report results", "Reports the results of tests"
21
+ method_option :commit, type: :string, aliases: "c", required: true,
22
+ banner: "3e078f8770743549b722382ec5d412a30b9fdcc5",
23
+ desc: "The full SHA for the current HEAD commit"
24
+ method_option :pr_number, type: :numeric, aliases: %w[pr p], required: true,
25
+ banner: "1",
26
+ desc: "The GitHub PR number"
27
+ method_option :dry_run, type: :boolean, default: false, lazy_default: true,
28
+ desc: "Print to STDOUT instead of leaving a comment on GitHub"
29
+ method_option :timeout, type: :numeric, aliases: %w[max-timeout], default: MAX_TIMEOUT,
30
+ desc: "How long to poll CircleCI before timing out (in seconds)"
31
+ method_option :poll_interval, type: :numeric, aliases: %w[poll-interval], default: POLL_INTERVAL,
32
+ desc: "How frequently to poll CircleCI (in seconds)"
33
+ method_option :source, type: :string, default: "circleci",
34
+ desc: "Where are the results.yml files we should report?"
35
+ def report
36
+ self.start_time = Time.now
37
+ artifact_urls = artifacts.map(&:url)
38
+
39
+ Jiminy::Reporting.report!(*artifact_urls,
40
+ pr_number: options[:pr_number],
41
+ dry_run: options[:dry_run])
42
+
43
+ $stdout.puts "Reported N+1s successfully"
44
+ exit(0)
45
+ end
46
+
47
+ # rubocop:disable Metrics/BlockLength
48
+ no_tasks do
49
+ attr_accessor :start_time
50
+
51
+ def poll_interval
52
+ options[:poll_interval] || POLL_INTERVAL
53
+ end
54
+
55
+ def source
56
+ options[:source] || :circleci
57
+ end
58
+
59
+ def max_timeout
60
+ options[:timeout] || MAX_TIMEOUT
61
+ end
62
+
63
+ def timed_out?
64
+ (Time.now - start_time) > max_timeout
65
+ end
66
+
67
+ def git_revision
68
+ options[:commit]
69
+ end
70
+
71
+ def pr_number
72
+ options[:pr_number]
73
+ end
74
+
75
+ def pipeline
76
+ @_pipeline ||= CircleCI::Pipeline.find_by_revision(git_revision: git_revision, pr_number: pr_number) or
77
+ abort("No such Pipeline with commit SHA #{git_revision}")
78
+ end
79
+
80
+ def missing_options
81
+ [].tap do |array|
82
+ array.concat("--pr-number") unless pr_number
83
+ array.concat("--commit") unless git_revision
84
+ end
85
+ end
86
+
87
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
88
+ def workflow
89
+ @_workflow ||= begin
90
+ result = CircleCI::Workflow.find(pipeline_id: pipeline.id, workflow_name: Jiminy.config.ci_workflow_name)
91
+ if result.nil?
92
+ abort("Unable to find workflow called '#{Jiminy.config.ci_workflow_name}' in Pipeline #{pipeline.id}")
93
+ end
94
+
95
+ if result.not_run? || result.running?
96
+ $stdout.puts "Workflow still running..."
97
+ raise(WorkflowStillRunningError)
98
+ end
99
+ abort("Workflow #{result.status}—aborting...") unless result.success?
100
+
101
+ result
102
+ rescue WorkflowStillRunningError
103
+ sleep(poll_interval)
104
+ $stdout.puts "Retrying..."
105
+ retry unless timed_out?
106
+
107
+ abort("Process timed out after #{Time.now - start_time} seconds")
108
+ end
109
+ end
110
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity
111
+
112
+ def jobs
113
+ @_jobs ||= CircleCI::Job.all(workflow_id: workflow.id)
114
+ end
115
+
116
+ def artifacts
117
+ @_artifacts ||= begin
118
+ unless respond_to?(:"artifacts_from_#{source}")
119
+ raise ArgumentError, "Uknown value for source option #{source}"
120
+ end
121
+
122
+ public_send(:"artifacts_from_#{source}")
123
+ end
124
+ end
125
+
126
+ def artifacts_from_local
127
+ Local::Artifact.all
128
+ end
129
+
130
+ def artifacts_from_circle_ci
131
+ CircleCI::Artifact.all(job_number: jobs.first.job_number)
132
+ end
133
+ alias_method :artifacts_from_circleci, :artifacts_from_circle_ci
134
+ end
135
+ # rubocop:enable Metrics/BlockLength
136
+ end
137
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ class Configuration
5
+ DEFAULT_CONFIG_READER = -> { instance_variable_get(:"@_#{__callee__}" || raise_missing_required(__callee__)) }
6
+ DEFAULT_CONFIG_WRITER = ->(value) { instance_variable_set(:"@_#{__callee__}"[0..-2], value) }
7
+
8
+ class MissingConfigError < StandardError
9
+ attr_reader :missing_config_name
10
+
11
+ def initialize(missing_config_name)
12
+ super()
13
+ @missing_config_name = missing_config_name
14
+ end
15
+
16
+ def message
17
+ <<~MESSAGE
18
+ You must provide a configuration value for ##{missing_config_name}
19
+
20
+ This probably means you haven't set Jiminy.config.#{missing_config_name}
21
+ MESSAGE
22
+ end
23
+ end
24
+
25
+ class << self
26
+ def define_config(name, options = {})
27
+ define_method(:"#{name}=", DEFAULT_CONFIG_WRITER)
28
+ define_method(name.to_sym, DEFAULT_CONFIG_READER)
29
+ defined_configs[name].merge!(options)
30
+ end
31
+
32
+ def defined_configs
33
+ @_defined_configs ||= Hash.new { |hash, key| hash[key] = { default: nil, required: true } }
34
+ end
35
+ end
36
+
37
+ define_config :ci_workflow_name, default: "build"
38
+
39
+ define_config :circle_ci_api_token
40
+
41
+ define_config :github_token
42
+
43
+ define_config :ignore_file_path, default: File.join("./jiminy_ignores.yml")
44
+
45
+ define_config :project_reponame
46
+
47
+ define_config :project_username
48
+
49
+ define_config :temp_file_location, default: File.join("./tmp/jiminy/results.yml")
50
+
51
+ def initialize
52
+ apply_defaults!
53
+ end
54
+
55
+ def repo_path
56
+ [project_username, project_reponame].join("/")
57
+ end
58
+
59
+ private
60
+
61
+ def defined_configs_with_defaults
62
+ self.class.defined_configs.select { |_config_name, options| options[:default] }
63
+ end
64
+
65
+ def required_configs
66
+ self.class.defined_configs.select { |_config_name, options| options[:required] }
67
+ end
68
+
69
+ def apply_defaults!
70
+ defined_configs_with_defaults.each do |config_name, options|
71
+ public_send(:"#{config_name}=", options[:default])
72
+ end
73
+ end
74
+
75
+ def raise_missing_required(config_name)
76
+ return unless required_configs.key?(config_name)
77
+
78
+ raise MissingConfigError, config_name
79
+ end
80
+ end
81
+
82
+ module ConfigurationMethods
83
+ def configure(&block)
84
+ block.call(configuration)
85
+ end
86
+
87
+ def configuration
88
+ @_configuration ||= Configuration.new
89
+ end
90
+
91
+ alias config configuration
92
+
93
+ def configured?
94
+ !!configuration
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module GithubAPIable
5
+ require "octokit"
6
+
7
+ private
8
+
9
+ def env_config
10
+ @_env_config ||= Reporting::CIProviders::Github::Configuration.new
11
+ end
12
+
13
+ def client
14
+ @_client ||= Octokit::Client.new(access_token: env_config.github_token)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Recording
5
+ class NPlusOne
6
+ attr_reader :file, :location
7
+
8
+ LOCATION_MATCHER = /(?<file>.+\.rb):
9
+ (?<line>\d+):in\s`
10
+ (?:block\sin\s)?
11
+ (?<method_name>.+)'
12
+ /x.freeze
13
+
14
+ EXAMPLES_COUNT = 3
15
+
16
+ def initialize(location:, queries: [])
17
+ @location = location.to_s.strip
18
+ @queries = queries
19
+ match = location.match(LOCATION_MATCHER)
20
+ @line = match[:line]
21
+ @method_name = match[:method_name]
22
+ @file = match[:file]
23
+ freeze
24
+ end
25
+
26
+ def ==(other)
27
+ location == other.location
28
+ end
29
+
30
+ def to_h
31
+ {
32
+ location => attributes
33
+ }
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :queries, :line, :method_name
39
+
40
+ def attributes
41
+ {
42
+ "file" => file,
43
+ "line" => line,
44
+ "method" => method_name,
45
+ "examples" => queries.take(EXAMPLES_COUNT)
46
+ }
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Recording
5
+ module ProsopiteExt
6
+ require_relative "tmp_file_recorder"
7
+
8
+ module SendNotificationsWithTmpFile
9
+ def prepare_results_file!
10
+ TmpFileRecorder.prepare_results_file!
11
+ end
12
+
13
+ def tmp_file=(value)
14
+ @tmp_file = value
15
+ end
16
+
17
+ def tmp_file
18
+ !!@tmp_file
19
+ end
20
+
21
+ def send_notifications
22
+ super
23
+
24
+ return unless Prosopite.tmp_file
25
+
26
+ # https://github.com/charkost/prosopite/blob/main/lib/prosopite.rb#L157
27
+ tc[:prosopite_notifications].each do |queries, backtrace|
28
+ absolute_location = backtrace.detect { |path| path.exclude?(Bundler.bundle_path.to_s) }
29
+ next unless absolute_location
30
+
31
+ relative_location = absolute_location.gsub("#{Rails.root.realpath}/", "")
32
+ tmp_file_recorder.record(location: relative_location, queries: queries)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def tmp_file_recorder
39
+ @_tmp_file_recorder ||= TmpFileRecorder.new
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Recording
5
+ module ProsopiteExt
6
+ class TmpFileRecorder
7
+ require_relative "../n_plus_one"
8
+
9
+ def record(location:, queries:)
10
+ yaml_content = File.read(Jiminy.config.temp_file_location)
11
+ array = YAML.safe_load(yaml_content)
12
+ n_plus_one = NPlusOne.new(location: location, queries: queries)
13
+
14
+ array << n_plus_one.to_h unless location_in_array?(location, array) || filepath_ignored?(n_plus_one.file)
15
+ File.write(Jiminy.config.temp_file_location, array.to_yaml)
16
+ end
17
+
18
+ private
19
+
20
+ def location_in_array?(location, array)
21
+ array.detect { |hash| hash.key?(location) }
22
+ end
23
+
24
+ def filepath_ignored?(filepath)
25
+ ignored_files.include?(filepath)
26
+ end
27
+
28
+ def ignored_files
29
+ @_ignored_files ||= load_ignored_files
30
+ end
31
+
32
+ def load_ignored_files
33
+ return [] unless File.exist?(Jiminy.config.ignore_file_path)
34
+
35
+ YAML.load_file(Jiminy.config.ignore_file_path) || []
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Recording
5
+ require "jiminy/recording/prosopite_ext/send_notifications_with_tmp_file"
6
+
7
+ module RSpec
8
+ require "prosopite"
9
+ require "jiminy/recording/test_controller_concern"
10
+
11
+ ::Prosopite.singleton_class.prepend ProsopiteExt::SendNotificationsWithTmpFile
12
+
13
+ def wrap_rspec_example(example)
14
+ Prosopite.tmp_file = true
15
+ ActionController::Base.include(Jiminy::Recording::TestControllerConcern)
16
+ example.run
17
+ Prosopite.tmp_file = false
18
+ end
19
+ Jiminy.extend(self)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Recording
5
+ module TestControllerConcern
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ around_action :_test_n_plus_one_detection
10
+ end
11
+
12
+ private
13
+
14
+ def _test_n_plus_one_detection
15
+ yield and return if Prosopite.scan?
16
+
17
+ begin
18
+ Prosopite.scan
19
+ yield
20
+ ensure
21
+ Prosopite.finish
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Recording
5
+ abort("Jiminy::Recording must be run from a Rails app") unless defined?(Rails)
6
+
7
+ module_function
8
+
9
+ def reset_results_file!
10
+ Rails.root.join(File.dirname(Jiminy.config.temp_file_location)).mkpath
11
+ File.write(Jiminy.config.temp_file_location, [].to_yaml)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ module CircleCI
7
+ class APIRequest
8
+ API_BASE = "https://circleci.com/api/v2/"
9
+
10
+ CIRCLE_TOKEN_HEADER = "Circle-Token"
11
+
12
+ def initialize(path)
13
+ @url = URI.join(API_BASE, path)
14
+ end
15
+
16
+ def perform!
17
+ puts url
18
+ response
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :url
24
+
25
+ def response
26
+ @_response ||= http.request(request)
27
+ end
28
+
29
+ def request
30
+ @_request ||= Net::HTTP::Get.new(url).tap do |req|
31
+ req[CIRCLE_TOKEN_HEADER] = Jiminy.config.circle_ci_api_token
32
+ end
33
+ end
34
+
35
+ def http
36
+ @_http ||= Net::HTTP.new(url.host, url.port).tap do |http_instance|
37
+ http_instance.use_ssl = true
38
+ http_instance.verify_mode = OpenSSL::SSL::VERIFY_NONE
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ module CircleCI
7
+ class Artifact < Base
8
+ define_attribute_readers :url
9
+
10
+ def self.all(job_number:)
11
+ fetch_api_resource("project/gh/#{Jiminy.config.repo_path}/#{job_number}/artifacts")
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ module CircleCI
7
+ require_relative "api_request"
8
+
9
+ class Base
10
+ require "uri"
11
+ require "net/http"
12
+ require "json"
13
+
14
+ attr_reader :attributes
15
+
16
+ def self.define_attribute_readers(*attr_names)
17
+ attr_names.each do |attr_name|
18
+ define_method(attr_name, -> { attributes[__callee__.to_s] })
19
+ end
20
+ end
21
+
22
+ class << self
23
+ attr_reader :next_token
24
+ end
25
+
26
+ class << self
27
+ attr_writer :next_token
28
+ end
29
+
30
+ def self.fetch_api_resource(path)
31
+ response = APIRequest.new(path).perform!
32
+ raise "Error response: #{response.body}" unless response.is_a?(Net::HTTPOK)
33
+
34
+ json_body = JSON.parse(response.read_body)
35
+ self.next_token = json_body["next_page_token"]
36
+ json_body["items"].map { |attributes| new(attributes) }
37
+ end
38
+
39
+ def initialize(attributes = {})
40
+ @attributes = attributes.transform_keys!(&:to_s)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ module CircleCI
7
+ class Job < Base
8
+ define_attribute_readers :job_number
9
+
10
+ def self.all(workflow_id:)
11
+ fetch_api_resource("workflow/#{workflow_id}/job")
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ module CircleCI
7
+ require_relative "base"
8
+ require_relative "vcs"
9
+
10
+ class Pipeline < Base
11
+ MAX_PAGE_LOOKUP = 20
12
+
13
+ define_attribute_readers :id, :project_slug, :vcs
14
+
15
+ def self.find_by_revision(git_revision:, pr_number:)
16
+ attempt_count = 0
17
+ matching_pipeline = nil
18
+ until matching_pipeline || attempt_count >= MAX_PAGE_LOOKUP
19
+ page_pipelines = fetch_page_from_api(next_token)
20
+ matching_pipeline = page_pipelines.detect { |p| pipeline_match?(p, git_revision, pr_number) }
21
+ attempt_count += 1
22
+ end
23
+ matching_pipeline
24
+ end
25
+
26
+ def self.fetch_page_from_api(page_token)
27
+ query = "org-slug=gh/#{Jiminy.config.project_username}&mine=false"
28
+ query += "&page-token=#{page_token}" if page_token
29
+ url = "pipeline?#{query}"
30
+ fetch_api_resource(url)
31
+ end
32
+
33
+ def self.pipeline_match?(pipeline, git_revision, _pr_number)
34
+ return false unless pipeline.project_slug.to_s.downcase.end_with?(Jiminy.config.repo_path.downcase)
35
+
36
+ pipeline.vcs.revision == git_revision
37
+
38
+ # TODO: Get PR comparison working too
39
+ # pipeline.vcs.review_url.end_with?("pull/#{pr_number}")
40
+ end
41
+
42
+ def vcs
43
+ VCS.new(attributes["vcs"])
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ module CircleCI
7
+ class VCS < Base
8
+ define_attribute_readers :revision, :review_url
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jiminy
4
+ module Reporting
5
+ module CIProviders
6
+ module CircleCI
7
+ class Workflow < Base
8
+ # Link to states: https://circleci.com/docs/2.0/workflows/#states
9
+ SUCCESS = "success"
10
+ FAILED = "failed"
11
+ NOT_RUN = "not run"
12
+ RUNNING = "running"
13
+
14
+ define_attribute_readers :id, :name, :status
15
+
16
+ def self.find(pipeline_id:, workflow_name:)
17
+ url = "pipeline/#{pipeline_id}/workflow"
18
+ collection = fetch_api_resource(url)
19
+ collection.detect { |w| w.name.to_s == workflow_name.to_s }
20
+ end
21
+
22
+ def success?
23
+ status == SUCCESS
24
+ end
25
+
26
+ def running?
27
+ status == RUNNING
28
+ end
29
+
30
+ def not_run?
31
+ status == NOT_RUN
32
+ end
33
+
34
+ def failed?
35
+ status == FAILED
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end