parallel_tests_report 0.1.0

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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4c6de34b2f648c93e2faaf8c9c041b358fce7c0be16586a4f7f80b812d56cf3a
4
+ data.tar.gz: 5946b56b3b7fef0c6ce10ceef38eab92d12baf2cb32a9962c4b9ff575c19bf57
5
+ SHA512:
6
+ metadata.gz: 30162444f35a72cf1c0378ad3403a4f0e66229095dca13de719777af66994e60334bde49f49ffd743d95b3122a240167c4a4329c0c7145275e17679ab56045fa
7
+ data.tar.gz: 9b134d077a65d5b132e2eead49ef3b694c56734dafca2080df13b19ed059e029730cce78ef4d5a8372a00d910b3e7bbb08818aea22512dd759281ad4cf5d0f4d
@@ -0,0 +1,34 @@
1
+ # parallel-tests-report
2
+
3
+ Works with [parallel_tests](https://github.com/grosser/parallel_tests) gem to generate a consolidated report for the spec groups executed by parallel_tests.
4
+
5
+ The report generated will include:
6
+ - List of top 20 slowest examples.
7
+ - Rspec command to reproduce the failed example with the bisect option and seed value used.
8
+
9
+ This gem will also verify the time taken for a test against configured threshold value and report if the time has exceeded.
10
+
11
+ ## How it works
12
+ - parallel_tests gem is configured to use a custom formatter provided by this gem using `--format` and `--out` options.
13
+ - Once tests are executed a rake task provided by this gem can be executed to parse the json and generate the report.
14
+
15
+ ## Installation
16
+ Include the gem in your Gemfile
17
+
18
+ `gem 'parallel_tests_report'`
19
+
20
+ `$ bundle install`
21
+
22
+ Add the following to the Rakefile before load_task(In Rails application):
23
+
24
+ `require 'parallel_tests_report'`
25
+
26
+ ## Usage
27
+ - add `--format` and `--out` option to `.rspec` or `.rspec_parallel`
28
+   - `--format ParallelTestsReport::JsonFormatter --out tmp/test-results/rspec.json`
29
+ - execute the rake task after specs are executed
30
+   - `bundle exec parallel_tests_report rake generate:report <TIME_LIMIT_IN_SECONDS> tmp/test-results/rspec.json`
31
+   - <TIME_LIMIT_IN_SECONDS> is the maximum time an example can take. Default is 10 seconds.
32
+   - <OUTPUT_FILE> is the file specified in the --out option. Default is 'tmp/test-results/rspec.json'
33
+
34
+ #### This rake task can be configured to run after specs are executed in a continuous integration setup, it also produces a junit xml file for time limit exceeding check.
@@ -0,0 +1,3 @@
1
+ require 'parallel_tests_report'
2
+
3
+ Dir.glob("lib/parallel_tests_report/tasks/*.rake").each { |f| import f }
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ gem_dir = File.expand_path("..",File.dirname(__FILE__))
4
+ $LOAD_PATH.unshift gem_dir
5
+ exec_type = ARGV[0]
6
+ if exec_type == 'rake' then
7
+ require 'rake'
8
+ require 'pp'
9
+ pwd=Dir.pwd
10
+ Dir.chdir(gem_dir)
11
+ Rake.application.init
12
+ Rake.application.load_rakefile
13
+ Dir.chdir(pwd)
14
+ Rake::Task[ARGV[1]].invoke(ARGV[2], ARGV[3])
15
+ end
@@ -0,0 +1,6 @@
1
+ require "rspec/core"
2
+ require "rspec/core/formatters/base_formatter"
3
+
4
+ module ParallelTestsReport
5
+ require 'parallel_tests_report/railtie' if defined?(Rails)
6
+ end
@@ -0,0 +1,114 @@
1
+ require 'parallel_tests_report'
2
+ require 'json'
3
+ require 'nokogiri'
4
+
5
+ class ParallelTestsReport::GenerateReport
6
+ def start(time_limit, output)
7
+ all_examples = []
8
+ slowest_examples = []
9
+ failed_examples = []
10
+ time_exceeding_examples = []
11
+ rerun_failed = []
12
+ errors = []
13
+
14
+ return if File.zero?(output)
15
+
16
+ File.foreach(output) do |line|
17
+ parallel_suite = JSON.parse(line)
18
+ all_examples += parallel_suite["examples"]
19
+ slowest_examples += parallel_suite["profile"]["examples"]
20
+ failed_examples += parallel_suite["examples"].select {|ex| ex["status"] == "failed" }
21
+ time_exceeding_examples += parallel_suite["examples"].select {|ex| ex["run_time"] >= time_limit}
22
+ errors << parallel_suite["messages"][0] if parallel_suite["examples"].size == 0
23
+ end
24
+
25
+ if slowest_examples.size > 0
26
+ slowest_examples = slowest_examples.sort_by do |ex|
27
+ -ex["run_time"]
28
+ end.first(20)
29
+ puts "Top #{slowest_examples.size} slowest examples\n"
30
+ slowest_examples.each do |ex|
31
+ puts <<-TEXT
32
+ #{ex["full_description"]}
33
+ #{ex["run_time"]} #{"seconds"} #{ex["file_path"]} #{ex["line_number"]}
34
+ TEXT
35
+ end
36
+ end
37
+
38
+ if failed_examples.size > 0
39
+ puts "\nFailed Examples:\n"
40
+ failed_examples.each do |ex|
41
+ puts <<-TEXT
42
+ => #{ex["full_description"]}
43
+ #{ex["run_time"]} #{"seconds"} #{ex["file_path"]} #{ex["line_number"]}
44
+ #{ex["exception"]["message"]}
45
+ TEXT
46
+ all_examples.each do |e|
47
+ rerun_failed << e["file_path"].to_s if e["parallel_test_proessor"] == ex["parallel_test_proessor"] && !rerun_failed.include?(e["file_path"])
48
+ end
49
+ str = ""
50
+ rerun_failed.each do |e|
51
+ str += e + " "
52
+ end
53
+ puts <<-TEXT
54
+ \n\s\sIn case the failure: "#{ex["full_description"]}" is due to random ordering, run the following command to isolate the minimal set of examples that reproduce the same failures:
55
+ `bundle exec rspec #{str} --seed #{ex['seed']} --bisect`\n
56
+ TEXT
57
+ rerun_failed.clear
58
+ end
59
+ end
60
+
61
+ if errors.size > 0
62
+ puts "\Errors:\n"
63
+ errors.each do |err|
64
+ puts <<-TEXT
65
+ #{err}
66
+ TEXT
67
+ end
68
+ end
69
+
70
+ if time_exceeding_examples.size > 0 || errors.size > 0
71
+ generate_xml(errors, time_exceeding_examples, time_limit)
72
+ end
73
+
74
+ if time_exceeding_examples.size > 0
75
+ puts "\nExecution time is exceeding the threshold of #{@time_limit} seconds for following tests:"
76
+ time_exceeding_examples.each do |ex|
77
+ puts <<-TEXT
78
+ => #{ex["full_description"]}: #{ex["run_time"]} #{"Seconds"}
79
+ TEXT
80
+ end
81
+ else
82
+ puts "Runtime check Passed."
83
+ end
84
+
85
+ if failed_examples.size > 0 || errors.size > 0 || time_exceeding_examples.size > 0
86
+ fail_message = "Tests Failed"
87
+ puts "\e[31m#{fail_message}\e[0m"
88
+ exit 1
89
+ end
90
+ end
91
+
92
+ def generate_xml(errors, time_exceeding_examples, time_limit)
93
+ builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
94
+ xml.testsuite {
95
+ time_exceeding_examples.each do |arr|
96
+ classname = "#{arr["file_path"]}".sub(%r{\.[^/]*\Z}, "").gsub("/", ".").gsub(%r{\A\.+|\.+\Z}, "")
97
+ xml.testcase("classname" => "#{classname}", "name" => "#{arr["full_description"]}", "file" => "#{arr["file_path"]}", "time" => "#{arr["run_time"]}") {
98
+ xml.failure "Execution time is exceeding the threshold of #{time_limit} seconds"
99
+ }
100
+ end
101
+ errors.each do |arr|
102
+ file_path = arr[/(?<=An error occurred while loading ).*/]
103
+ classname = "#{file_path}".sub(%r{\.[^/]*\Z}, "").gsub("/", ".").gsub(%r{\A\.+|\.+\Z}, "")
104
+ xml.testcase("classname" => "#{classname}", "name" => "An error occurred while loading", "file" => "#{file_path}", "time" => "0.0") {
105
+ xml.failure arr.gsub(/\e\[([;\d]+)?m/, "").gsub(/An error occurred while loading #{file_path}\n/, "")
106
+ }
107
+ end
108
+ }
109
+ end
110
+ File.open('tmp/test-results/time_limit_exceeded.xml', 'w') do |file|
111
+ file << builder.to_xml
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,113 @@
1
+ # With this JsonFormatter we are generating a file which contains a json for each and every parallel_test_suite. It contains all the passed, failed, pending and profiled examples with their full_description, file_path, run_time, seed value and parallel_test_proessor number.
2
+ # We parse each line of this file, to generate a report to show slowest examples, failed examples, errors and runtime checks.
3
+
4
+ # And example to show the structure of the file:
5
+ # This is a single line containing details for one parallel_test_suite
6
+ =begin
7
+ {"messages":["Run options: exclude {:asrun=\u003etrue, :to_be_implemented=\u003etrue, :integration=\u003etrue}"],"seed":2121,"examples":[{"full_description":"An example", updated_at, id, media_id","status":"passed","file_path":"/path/to/the/example","line_number":01,"run_time":2.269736609,"parallel_test_proessor":1,"seed":2121},{"full_description":"Another Example","status":"passed","file_path":"path/to/another/exmple","line_number":20,"run_time":4.139183023,"parallel_test_proessor":1,"seed":2121}],"profile":{"examples":[{"full_description":"An example", updated_at, id, media_id","status":"passed","file_path":"/path/to/the/example","line_number":01,"run_time":2.269736609,"parallel_test_proessor":1,"seed":2121},{"full_description":"Another Example","status":"passed","file_path":"path/to/another/exmple","line_number":20,"run_time":4.139183023,"parallel_test_proessor":1,"seed":2121}]}}
8
+ =end
9
+
10
+ require 'parallel_tests_report'
11
+
12
+ class ParallelTestsReport::JsonFormatter < RSpec::Core::Formatters::BaseFormatter
13
+ RSpec::Core::Formatters.register self, :message, :dump_profile, :seed, :stop, :close
14
+ attr_reader :output_hash, :output
15
+ def initialize(output)
16
+ super
17
+ @output ||= output
18
+ if String === @output
19
+ #open the file given as argument in --out
20
+ FileUtils.mkdir_p(File.dirname(@output))
21
+ # overwrite previous results
22
+ File.open(@output, 'w'){}
23
+ @output = File.open(@output, 'a')
24
+ # close and restart in append mode
25
+ elsif File === @output
26
+ @output.close
27
+ @output = File.open(@output.path, 'a')
28
+ end
29
+ @output_hash = {}
30
+
31
+ if ENV['TEST_ENV_NUMBER'].to_i != 0
32
+ @n = ENV['TEST_ENV_NUMBER'].to_i
33
+ else
34
+ @n = 1
35
+ end
36
+ end
37
+
38
+ def message(notification)
39
+ (@output_hash[:messages] ||= []) << notification.message
40
+ end
41
+
42
+ def seed(notification)
43
+ return unless notification.seed_used?
44
+ @output_hash[:seed] = notification.seed
45
+ end
46
+
47
+ def close(_notification)
48
+ #close the file after all the processes are finished
49
+ @output.close if (IO === @output) & (@output != $stdout)
50
+ end
51
+
52
+ def stop(notification)
53
+ #adds to @output_hash, an array of examples which run in a particular processor
54
+ @output_hash[:examples] = notification.examples.map do |example|
55
+ format_example(example).tap do |hash|
56
+ e = example.exception
57
+ if e
58
+ hash[:exception] = {
59
+ :class => e.class.name,
60
+ :message => e.message,
61
+ :backtrace => e.backtrace,
62
+ }
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ def dump_profile(profile)
69
+ dump_profile_slowest_examples(profile)
70
+ end
71
+
72
+ def dump_profile_slowest_examples(profile)
73
+ #adds to @output_hash, an array of 20 slowest examples
74
+ lock_output do
75
+ @output_hash[:profile] = {}
76
+ @output_hash[:profile][:examples] = profile.slowest_examples.map do |example|
77
+ format_example(example)
78
+ end
79
+ end
80
+ #write the @output_hash to the file
81
+ output.puts @output_hash.to_json
82
+ output.flush
83
+ end
84
+
85
+ protected
86
+ #to make a single file for all the parallel processes
87
+ def lock_output
88
+ if File === @output
89
+ begin
90
+ @output.flock File::LOCK_EX
91
+ yield
92
+ ensure
93
+ @output.flock File::LOCK_UN
94
+ end
95
+ else
96
+ yield
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def format_example(example)
103
+ {
104
+ :full_description => example.full_description,
105
+ :status => example.execution_result.status.to_s,
106
+ :file_path => example.metadata[:file_path],
107
+ :line_number => example.metadata[:line_number],
108
+ :run_time => example.execution_result.run_time,
109
+ :parallel_test_proessor => @n,
110
+ :seed => @output_hash[:seed]
111
+ }
112
+ end
113
+ end
@@ -0,0 +1,13 @@
1
+ require 'parallel_tests_report'
2
+ require 'rails'
3
+
4
+ module ParallelTestsReport
5
+ class Railtie < Rails::Railtie
6
+ railtie_name :parallel_tests_report
7
+
8
+ rake_tasks do
9
+ path = File.expand_path(__dir__)
10
+ Dir.glob("#{path}/tasks/**/*.rake").each { |f| load f }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+ require_relative '../../parallel_tests_report/generate_report.rb'
2
+
3
+ namespace :generate do
4
+ task :report, [:time_limit, :output] do |t,args|
5
+ output = args[:output].to_s
6
+ if output == ""
7
+ if(args[:time_limit] != nil && args[:time_limit].to_f == 0.0 && args[:time_limit] != '0.0') # If only one argument is given while calling the rake_task and that is :output.
8
+ #Since, first argument is :time_limit, assigning that to output.
9
+ output = args[:time_limit].to_s
10
+ else
11
+ output = 'tmp/test-results/rspec.json' # default :output file
12
+ end
13
+ end
14
+ time_limit = args[:time_limit].to_f
15
+ if time_limit == 0.0
16
+ if args[:time_limit] == "0.0" #if :time_limit itself is 0.0
17
+ time_limit = 0.0
18
+ else
19
+ time_limit = 10.0 # default :time_limit
20
+ end
21
+ end
22
+ ParallelTestsReport::GenerateReport.new.start time_limit,output
23
+ end
24
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: parallel_tests_report
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Akshat Birani
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-08-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 12.3.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 12.3.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: nokogiri
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: Works with parallel_tests ruby gem to generate a report having a list
42
+ of slowest and failed examples.
43
+ email: akshat@amagi.com
44
+ executables:
45
+ - parallel_tests_report
46
+ extensions: []
47
+ extra_rdoc_files: []
48
+ files:
49
+ - README.md
50
+ - Rakefile
51
+ - bin/parallel_tests_report
52
+ - lib/parallel_tests_report.rb
53
+ - lib/parallel_tests_report/generate_report.rb
54
+ - lib/parallel_tests_report/json_formatter.rb
55
+ - lib/parallel_tests_report/railtie.rb
56
+ - lib/parallel_tests_report/tasks/generate_report.rake
57
+ homepage: https://github.com/amagimedia/parallel-tests-report
58
+ licenses: []
59
+ metadata: {}
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.1.2
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: Generate report for parallel_tests
79
+ test_files: []