testrailtagging 0.3.6.8

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.
@@ -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: