testrailtagging 0.3.6.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,385 @@
1
+ require "rspec"
2
+ require_relative "testrail_operations"
3
+
4
+ module TestRailRSpecIntegration
5
+ # This class responsible for communicating a test run back to testrail.
6
+ # The status of a test example is NOT set until the 'after' hooks run.
7
+ # Because the 'after' hook is not meant for observing the status of examples.
8
+ # Thus you can NOT run an after block to get the completion status.
9
+ # For a better explanation see:
10
+ # https://github.com/rspec/rspec-core/issues/2011
11
+
12
+ # For pushing results up to an existing test run in TestRail, no matter whether the test run is independent
13
+ # or grouped under a test plan
14
+ # # This is different from simply creating a stand-alone test run from the results of the test.
15
+ # The tricky part about this is Jenkins run rspecs on multiple processes with different batches of
16
+ # rspec tests.
17
+
18
+ @@total_count = 0
19
+ @@run_count = 0
20
+ @@skip_count = 0
21
+
22
+ class TestRailPlanFormatter
23
+ RSpec::Core::Formatters.register self, :example_passed, :example_pending, :example_failed, :start, :stop
24
+ public
25
+ def initialize(out)
26
+ @out = out
27
+ end
28
+
29
+ def self.set_product(product)
30
+ @@product = product
31
+ end
32
+
33
+ def test_id_key
34
+ case @@product
35
+ when :bridge
36
+ :testrail_id
37
+ when :canvas
38
+ :test_id
39
+ end
40
+ end
41
+
42
+ # Gets whether the formatter is active or not.
43
+ # We don't want to push results up to test rail for instance if --dry-run is specified on the command line.
44
+ def active
45
+ !RSpec.configuration.dry_run
46
+ end
47
+
48
+ # This gets called before all tests are run
49
+ def start(_start_notification)
50
+ # It's been verified that these 4 environment variables already exist
51
+ # These three are not actively used in this class, but their presence governs whether
52
+ # this class is instantiated and used in the first place.
53
+
54
+ if is_for_test_rail_run
55
+ @testrail_run_id = ENV["TESTRAIL_RUN_ID"]
56
+ elsif !ENV["TESTRAIL_PLAN_ID"].nil?
57
+ @testrail_plan_id = ENV["TESTRAIL_PLAN_ID"]
58
+ @testrail_run_name = ENV["TESTRAIL_RUN"]
59
+ if is_for_test_rail_plan # run on jenkins
60
+ @testrail_run_id = ENV["TESTRAIL_ENTRY_RUN_ID"]
61
+ @testrail_entry_id = ENV["TESTRAIL_ENTRY_ID"]
62
+ else # run locally, and only one thread
63
+ ids = TestRailOperations.create_test_plan_entry(@testrail_plan_id, @testrail_run_name, include_all_cases: true)
64
+ @testrail_run_id = ids[:run_id]
65
+ @testrail_entry_id = ids[:entry_id]
66
+ end
67
+ end
68
+
69
+ # Pull down ALL the test cases from testrail. Granted this is more than what rspec will actually
70
+ # execute. But there is no safe way to append a test case to a test run in a parallel environment.
71
+ # The Testrail API is just too limited.
72
+ puts "Using test run ID: #{@testrail_run_id}"
73
+ puts "Using test entry ID: #{@testrail_entry_id}"
74
+
75
+ puts "Count of skipped tests: #{TestRailRSpecIntegration.get_skip_count}"
76
+ puts "Count of tests to be run: #{TestRailRSpecIntegration.get_run_count}"
77
+ puts "Count of tests that entered filter: #{TestRailRSpecIntegration.get_total_count}"
78
+
79
+ @test_case_hash = TestRailOperations.get_test_run_cases(@testrail_run_id)
80
+ # save the test case ID's that were actually executed
81
+ @executed_test_ids = []
82
+ end
83
+
84
+ # This gets called after all tests are run
85
+ def stop(_examples_notification)
86
+ if @testrail_plan_id
87
+ # Need to prune un-executed tests from the test run on testrail
88
+ if is_for_test_rail_plan # run on jenkins, multiple threads doing this
89
+ # Need to dump a list of executed tests so unexecuted tests can be pruned later (on testrail)
90
+ # after all the rspec tests are done.
91
+ File.open("executed_tests_#{Process.pid}.json", 'w') do |f|
92
+ f.puts @executed_test_ids.to_json
93
+ end
94
+ # Another process will take the json file and use it to prune the test run.
95
+ else # run locally, and only one thread
96
+ # prune the test cases to only what was run
97
+ response = TestRailOperations.keep_only(@testrail_plan_id, @testrail_entry_id, @executed_test_ids)
98
+ end
99
+ elsif !ENV["TESTRAIL_RUN_ID"].nil?
100
+ # Results were already pushed to an existing testrail run. Nothing more to do here, we are done! :)
101
+ else
102
+ puts "Unknown condition"
103
+ end
104
+ end
105
+
106
+ # This gets called after all `after` hooks are run after each example is completed
107
+ def example_finished(notification)
108
+ return unless active
109
+ example = notification.example
110
+ result = example.execution_result
111
+
112
+ testrail_ids = example.metadata[test_id_key]
113
+ return unless testrail_ids.present?
114
+ completion_message = ""
115
+
116
+ if (result.status == :failed)
117
+ # This is the best format, unfortunately it has bash console color codes embedded in it.
118
+ completion_message = notification.fully_formatted(1)
119
+ # need to remove those color codes from the string
120
+ completion_message.gsub!(/\[(\d)+m/, '')
121
+ end
122
+
123
+ cases = [] # the test cases
124
+ Array(testrail_ids).each do |id|
125
+ tc = @test_case_hash[id.to_i]
126
+ next unless tc # A test case ID exists in the rspec file, but not on testrail
127
+ tc.set_status(result.status, completion_message)
128
+ cases << tc
129
+ @executed_test_ids << id.to_i
130
+ end
131
+
132
+ post_results cases
133
+ end
134
+
135
+ # test_cases is an array of TestCase instances
136
+ def post_results(test_cases)
137
+ data = []
138
+ test_cases.each do |tc|
139
+
140
+ status_value = TestRailOperations.status_rspec_to_testrail(tc.status)
141
+ if status_value == TestRailOperations::UNTESTED
142
+ # ! SUPER IMPORTANT !
143
+ # test rail does NOT allow you to set the status of a test to untested.
144
+ # so skip them
145
+ next
146
+ end
147
+
148
+ # id was not found in the list of test run id's. Due to incorrect include pattern in rspec.
149
+ next unless tc.temp_id
150
+
151
+ data << {
152
+ "test_id" => tc.temp_id, # results require the new test case temporary ID's, not the static ID's
153
+ "status_id" => status_value,
154
+ "comment" => tc.result_message
155
+ }
156
+ end
157
+
158
+ if data.size > 0
159
+ TestRailOperations.post_run_results(@testrail_run_id, data)
160
+ test_case_ids = test_cases.collect { |tc| tc.id }
161
+ @out.puts "Successfully posted results for testcases: #{test_case_ids} to test run: #{@testrail_run_id}"
162
+ else
163
+ @out.puts "No results sent to test rail"
164
+ end
165
+ end
166
+
167
+ alias_method :example_passed, :example_finished
168
+ alias_method :example_pending, :example_finished
169
+ alias_method :example_failed, :example_finished
170
+
171
+ private
172
+ # For pushing results up to a test plan in TestRail.
173
+ def is_for_test_rail_plan
174
+ !ENV["TESTRAIL_RUN"].nil? && !ENV["TESTRAIL_PLAN_ID"].nil? && !ENV["TESTRAIL_ENTRY_ID"].nil? && !ENV["TESTRAIL_ENTRY_RUN_ID"].nil?
175
+ end
176
+
177
+ # For pushing results to a single, existing test run in TestRail
178
+ def is_for_test_rail_run
179
+ !ENV["TESTRAIL_RUN_ID"].nil? && ENV["TESTRAIL_RUN"].nil? && ENV["TESTRAIL_PLAN_ID"].nil?
180
+ end
181
+ end
182
+
183
+ def self.get_total_count
184
+ @@total_count
185
+ end
186
+
187
+ def self.get_skip_count
188
+ @@skip_count
189
+ end
190
+
191
+ def self.get_run_count
192
+ @@run_count
193
+ end
194
+
195
+ # Adds a documentation formatter to the rspec if one is not there already.
196
+ def self.add_formatter_for(config)
197
+ # For some reason, adding a custom formatter will remove any other formatters.
198
+ # Thus during execution nothing gets printed to the screen. Need to add another
199
+ # formatter to indicate some sort of progress
200
+ found_doc_formatter = false
201
+ config.formatters.each do |fm|
202
+ if (fm.class == RSpec::Core::Formatters::DocumentationFormatter)
203
+ found_doc_formatter = true
204
+ break
205
+ end
206
+ end
207
+ unless found_doc_formatter
208
+ config.add_formatter "doc"
209
+ end
210
+ end
211
+
212
+ # Takes care of filtering out tests that are NOT assigned to the user. So essentially runs only
213
+ # tests specified in a testrun in testrail, and that are assigned to a particular user.
214
+ # \config - The Rspec configuration
215
+ # \user_id - An integer ID corresponding to the testrail user
216
+ # \test_run_cases - A hash of TestCase instances
217
+ def self.filter_rspecs_by_test_run_and_user(config, user_id, test_run_cases)
218
+ config.filter_run_including testrail_id: lambda { |value|
219
+ # The test id's are strings. Convert them to integers to make comparison easier
220
+ test_ids = value.collect { |str| str.to_i }
221
+ # Compute the intersection using the handy &() method
222
+ intersect = test_run_cases.keys & test_ids
223
+ assigned_to_ids = []
224
+ # Do include if the intersection contains a test id
225
+ if intersect.size > 0
226
+ test_ids.each do |id|
227
+ test_case = test_run_cases[id]
228
+ if test_case.nil?
229
+ next
230
+ end
231
+ assigned_to_ids << test_case.assigned_to
232
+ end
233
+ # return true to execute the test if any one of the testcase ID's is assigned to the user
234
+ do_execute = assigned_to_ids.include? user_id
235
+ if do_execute
236
+ puts "Assigned to user. Including testcase ID's: #{value}"
237
+ else
238
+ puts "Not assigned to user: Skipping #{value}"
239
+ end
240
+ do_execute
241
+ else
242
+ false
243
+ end
244
+ }
245
+ end
246
+
247
+ # Filters an rspec run by testrail_id's for bridge
248
+ # Filters an rspec run by testcases found in a particular testrun on testrail.
249
+ def self.filter_rspecs_by_test_run(config, test_run_cases)
250
+ # This lambda gets called once for each example
251
+ # Here value is an array of string test case ID's.
252
+ config.filter_run_including testrail_id: lambda { |value|
253
+ @@total_count += 1
254
+ unless value.is_a? Array
255
+ @@skip_count += 1
256
+ puts "ERROR! testcase has invalid testrail ID: #{value}. Value should be an array, got: #{value.class}".red
257
+ return false
258
+ end
259
+ # The test id's are strings. Convert them to integers to make comparison easier
260
+ test_ids = value.collect { |str| str.to_i }
261
+ # Compute the intersection using the handy &() method
262
+ intersect = test_run_cases.keys & test_ids
263
+ # Do not include if the test cases have already been run and have ALL passed.
264
+ # (That would be a waste of time to rerun test's that have already passed)
265
+ pass_count = 0
266
+ skip_count = 0
267
+ # Do include if the intersection contains a test id
268
+ if intersect.size > 0
269
+ test_ids.each do |id|
270
+ test_case = test_run_cases[id]
271
+ if test_case.nil?
272
+ next
273
+ end
274
+ # puts " #{id} temp id: #{test_case.temp_id} Status: #{test_case.status}, "
275
+ pass_count += 1 if test_case.status == :passed
276
+ skip_count += 1 if test_case.status == :pending
277
+ end
278
+ all_passed = pass_count == test_ids.count
279
+ all_skipped = skip_count == test_ids.count
280
+ if all_passed
281
+ @@skip_count += 1
282
+ puts "Skipping test case #{value}, because all tests already passed"
283
+ end
284
+ if all_skipped
285
+ @@skip_count += 1
286
+ puts "Skipping test case #{value}, because all tests marked pending"
287
+ end
288
+ do_execute = (pass_count + skip_count) != test_ids.count
289
+ @@run_count += 1 if do_execute
290
+ do_execute
291
+ else
292
+ @@skip_count += 1
293
+ false
294
+ end
295
+ }
296
+ end
297
+
298
+ # Filters an rspec run by test_id's for canvas.
299
+ # This is used for filtering out test cases that have already been run previously, say on a previous
300
+ # test run that was aborted early and restarted.
301
+ # In this case we skip tests that already passed or were marked as pending (rspec for skipped)
302
+ def self.filter_rspecs_by_testid(config, test_run_cases)
303
+ # This lambda gets called once for each example
304
+ # Here value is an array of string test case ID's.
305
+ config.filter_run_including test_id: lambda { |id|
306
+ @@total_count += 1
307
+ id = id.to_i
308
+ # The test id's are integers, and in canvas there is only one ID per test case, NOT an array like Bridge
309
+ in_run = test_run_cases.keys.include?( id )
310
+
311
+ # Do include if the intersection contains a test id
312
+ if in_run
313
+ test_case = test_run_cases[id]
314
+
315
+ if (test_case.status == :passed)
316
+ @@skip_count += 1
317
+ puts "Skipping test case #{id}, because it has already passed"
318
+ return false
319
+ end
320
+
321
+ if (test_case.status == :pending)
322
+ @@skip_count += 1
323
+ puts "Skipping test case #{id}, because it is marked pending"
324
+ return false
325
+ end
326
+
327
+ @@run_count += 1
328
+ return true # do execute this test
329
+ else
330
+ @@skip_count += 1
331
+ puts "Skipping test case #{id}, because it was not in test_run_cases"
332
+ return false
333
+ end
334
+ }
335
+ end
336
+
337
+ # The param is an RSPEC config
338
+ # The second param is a symbol for which product to hook into
339
+ def self.register_rspec_integration(config, product, add_formatter: true)
340
+ # Runs test cases as found in a test run on testrail
341
+
342
+ # This will select test examples to run based off of what test rail defines, not what
343
+ # the file pattern on the command line defines.
344
+ # That is, this will take a test run (int test rail), and run all the cases defined in it.
345
+
346
+ # First clear any filters passed in from the command line
347
+ config.inclusion_filter = nil
348
+ test_run_cases = TestRailOperations.get_test_run_cases(ENV["TESTRAIL_RUN_ID"].to_i)
349
+
350
+ user_id = nil
351
+ unless ENV["TESTRAIL_ASSIGNED_TO"].nil?
352
+ user_json = TestRailOperations.get_test_rail_user_by_email(ENV["TESTRAIL_ASSIGNED_TO"])
353
+ user_id = user_json["id"]
354
+ puts "Testrail assigned to: #{user_json}"
355
+ end
356
+
357
+ if user_id
358
+ TestRailRSpecIntegration.filter_rspecs_by_test_run_and_user(config, user_id, test_run_cases)
359
+ else
360
+ case(product)
361
+ when :bridge
362
+ TestRailRSpecIntegration.filter_rspecs_by_test_run(config, test_run_cases)
363
+ when :canvas
364
+ TestRailRSpecIntegration.filter_rspecs_by_testid(config, test_run_cases)
365
+ end
366
+ end
367
+
368
+ config.add_formatter TestRailRSpecIntegration::TestRailPlanFormatter
369
+ TestRailRSpecIntegration::TestRailPlanFormatter.set_product(product)
370
+ if add_formatter
371
+ TestRailRSpecIntegration.add_formatter_for(config)
372
+ end
373
+ end
374
+
375
+ # Registers a callback custom formatter to an rspec. The new test run is created from
376
+ # the results of the tests. This is in effect the opposite of the method above
377
+ # (register_rspec_integration).
378
+ def self.add_rspec_callback(config, product, add_formatter: true)
379
+ config.add_formatter TestRailRSpecIntegration::TestRailPlanFormatter
380
+ TestRailRSpecIntegration::TestRailPlanFormatter.set_product(product)
381
+ if add_formatter
382
+ TestRailRSpecIntegration.add_formatter_for(config)
383
+ end
384
+ end
385
+ end
@@ -0,0 +1,3 @@
1
+ module Testrailtagging
2
+ VERSION = "0.3.6.8"
3
+ end
@@ -0,0 +1,4 @@
1
+ require_relative "files/version"
2
+ require_relative "files/testcase_modifications"
3
+ require_relative "files/testrail_rspec_integration"
4
+ require_relative "files/testrail_queries"
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'files/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "testrailtagging"
8
+ spec.version = Testrailtagging::VERSION
9
+ spec.authors = ["Chris Johnson"]
10
+ spec.email = ["cjohnson@instructure.com"]
11
+
12
+ spec.summary = "Utilities for working with testrail."
13
+ spec.description = "Contains code for pushing rspec results up to testrail."
14
+ spec.homepage = "https://github.com/instructure/testrailtagging"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.11"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+
25
+ spec.add_dependency 'testrail_client'
26
+ spec.add_dependency 'parser'
27
+ spec.add_dependency 'rspec'
28
+ end
metadata ADDED
@@ -0,0 +1,133 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: testrailtagging
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.6.8
5
+ platform: ruby
6
+ authors:
7
+ - Chris Johnson
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-08-01 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.11'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.11'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: testrail_client
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: parser
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Contains code for pushing rspec results up to testrail.
84
+ email:
85
+ - cjohnson@instructure.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - CODE_OF_CONDUCT.md
92
+ - Gemfile
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - bin/console
97
+ - bin/setup
98
+ - lib/files/RSpecParser.rb
99
+ - lib/files/TestCase.rb
100
+ - lib/files/testcase_modifications.rb
101
+ - lib/files/testrail_apiclient_retry.rb
102
+ - lib/files/testrail_operations.rb
103
+ - lib/files/testrail_queries.rb
104
+ - lib/files/testrail_rspec_integration.rb
105
+ - lib/files/version.rb
106
+ - lib/testrailtagging.rb
107
+ - testrailtagging.gemspec
108
+ homepage: https://github.com/instructure/testrailtagging
109
+ licenses:
110
+ - MIT
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubyforge_project:
128
+ rubygems_version: 2.4.5
129
+ signing_key:
130
+ specification_version: 4
131
+ summary: Utilities for working with testrail.
132
+ test_files: []
133
+ has_rdoc: