lopata 0.1.1 → 0.1.6

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.
@@ -1,7 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
  gem 'selenium-webdriver'
3
- gem 'rspec', :require => 'spec'
4
- gem 'rake'
5
3
  gem 'activerecord', '~> 6.0.2'
6
4
  gem 'capybara', '~> 3.11.1'
7
5
  gem 'thor'
@@ -1,5 +1,4 @@
1
1
  url: http://yourapp.com
2
- name: QA
3
2
  # db:
4
3
  # adapter: postgresql
5
4
  # host: localhost
@@ -0,0 +1,103 @@
1
+ module Lopata
2
+ module Observers
3
+ # @private
4
+ # Based on RSpec::Core::BacktraceFormatter
5
+ #
6
+ # Provides ability to format backtrace and find source code by file and line number.
7
+ class BacktraceFormatter
8
+ # @private
9
+ attr_accessor :exclusion_patterns, :inclusion_patterns
10
+
11
+ def initialize
12
+ patterns = %w[ /lib\d*/ruby/ bin/ exe/lopata /lib/bundler/ /exe/bundle /\.rvm/ ]
13
+ patterns.map! { |s| Regexp.new(s.gsub("/", File::SEPARATOR)) }
14
+
15
+ @exclusion_patterns = [Regexp.union(*patterns)]
16
+ @inclusion_patterns = []
17
+
18
+ inclusion_patterns << Regexp.new(Dir.getwd)
19
+ end
20
+
21
+ # Filter backtrace.
22
+ #
23
+ # @param backtrace [Array<String>] exception backtrace
24
+ # @return [Array<String>] backtrace lines except ruby libraries, gems and executable files.
25
+ def format(backtrace)
26
+ return [] unless backtrace
27
+ return backtrace if backtrace.empty?
28
+
29
+ backtrace.map { |l| backtrace_line(l) }.compact.
30
+ tap do |filtered|
31
+ if filtered.empty?
32
+ filtered.concat backtrace
33
+ filtered << ""
34
+ filtered << " Showing full backtrace because every line was filtered out."
35
+ end
36
+ end
37
+ end
38
+
39
+
40
+ # Extracts error message from excetion
41
+ #
42
+ # @param exeption [Exception]
43
+ # @param include_backtrace [Boolean] whether to add formatted backtrace to output
44
+ # @return [String] error message from excetion, incuding source code line.
45
+ def error_message(exception, include_backtrace: false)
46
+ backtrace = format(exception.backtrace)
47
+ source_line = extract_source_line(backtrace.first)
48
+ msg = ''
49
+ msg << "\n#{source_line}\n" if source_line
50
+ msg << "#{exception.class.name}: " unless exception.class.name =~ /RSpec/
51
+ msg << exception.message if exception.message
52
+ msg << "\n#{backtrace.join("\n")}\n" if include_backtrace
53
+ msg
54
+ end
55
+
56
+ def extract_source_line(backtrace_line)
57
+ file_and_line_number = backtrace_line.match(/(.+?):(\d+)(|:\d+)/)
58
+ return nil unless file_and_line_number
59
+ file_path, line_number = file_and_line_number[1..2]
60
+ return nil unless File.exist?(file_path)
61
+ lines = File.read(file_path).split("\n")
62
+ lines[line_number.to_i - 1]
63
+ end
64
+
65
+ def backtrace_line(line)
66
+ relative_path(line) unless exclude?(line)
67
+ end
68
+
69
+ def exclude?(line)
70
+ matches?(exclusion_patterns, line) && !matches?(inclusion_patterns, line)
71
+ end
72
+
73
+ private
74
+
75
+ def matches?(patterns, line)
76
+ patterns.any? { |p| line =~ p }
77
+ end
78
+
79
+ # Matches strings either at the beginning of the input or prefixed with a
80
+ # whitespace, containing the current path, either postfixed with the
81
+ # separator, or at the end of the string. Match groups are the character
82
+ # before and the character after the string if any.
83
+ #
84
+ # http://rubular.com/r/fT0gmX6VJX
85
+ # http://rubular.com/r/duOrD4i3wb
86
+ # http://rubular.com/r/sbAMHFrOx1
87
+ def relative_path_regex
88
+ @relative_path_regex ||= /(\A|\s)#{File.expand_path('.')}(#{File::SEPARATOR}|\s|\Z)/
89
+ end
90
+
91
+ # @param line [String] current code line
92
+ # @return [String] relative path to line
93
+ def relative_path(line)
94
+ line = line.sub(relative_path_regex, "\\1.\\2".freeze)
95
+ line = line.sub(/\A([^:]+:\d+)$/, '\\1'.freeze)
96
+ return nil if line == '-e:1'.freeze
97
+ line
98
+ rescue SecurityError
99
+ nil
100
+ end
101
+ end
102
+ end
103
+ end
@@ -1,23 +1,34 @@
1
1
  module Lopata
2
2
  module Observers
3
+ # Lopata allows observe scenarios execution.
4
+ # All the observers are subclasses of Lopata::Observers::BaseObserver.
5
+ #
6
+ # @see Lopata::Observers::ConsoleOutputObserver for implementation example
3
7
  class BaseObserver
8
+ # Called before scenarios execution.
9
+ # All the scenarios are prepared at the moment, so it may be used to get number of scenarios
10
+ # via world.scenarios.count
11
+ #
12
+ # @param world [Lopata::World]
4
13
  def started(world)
5
14
  end
6
15
 
16
+ # Called after all scenarios execution.
17
+ # All the scenarios are finished at the moment, so it may be used for output statistics.
18
+ #
19
+ # @param world [Lopata::World]
7
20
  def finished(world)
8
21
  end
9
22
 
23
+ # Called before single scenario execution.
24
+ # @param world [Lopata::Scenario::Execution]
10
25
  def scenario_started(scenario)
11
26
  end
12
27
 
28
+ # Called after single scenario execution.
29
+ # @param world [Lopata::Scenario::Execution]
13
30
  def scenario_finished(scenario)
14
31
  end
15
-
16
- def step_started(step)
17
- end
18
-
19
- def step_finished(step)
20
- end
21
32
  end
22
33
  end
23
34
  end
@@ -1,35 +1,45 @@
1
+ require_relative 'backtrace_formatter'
2
+
1
3
  module Lopata
2
4
  module Observers
5
+ # @private
3
6
  class ConsoleOutputObserver < BaseObserver
7
+ extend Forwardable
8
+ # @private
9
+ attr_reader :output
10
+ # @private
11
+ def_delegators :output, :puts, :flush
12
+
13
+ def initialize
14
+ @output = $stdout
15
+ end
16
+
17
+ # @see Lopata::Observers::BaseObserver#finished
4
18
  def finished(world)
5
- total = world.scenarios.length
6
- statuses = world.scenarios.map(&:status)
7
- counts = statuses.uniq.map do |status|
8
- colored("%d %s", status) % [statuses.count { |s| s == status }, status]
19
+ total = statuses.values.inject(0, &:+)
20
+ counts = statuses.map do |status, count|
21
+ colored("%d %s", status) % [count, status]
9
22
  end
10
23
  details = counts.empty? ? "" : "(%s)" % counts.join(', ')
11
24
  puts "#{total} scenario%s %s" % [total == 1 ? '' : 's', details]
12
25
  end
13
26
 
14
- def step_finished(step)
15
- @failed_steps << step if step.failed?
16
- end
17
-
18
- def scenario_started(scenario)
19
- @failed_steps = []
20
- end
21
-
27
+ # @see Lopata::Observers::BaseObserver#scenario_finished
22
28
  def scenario_finished(scenario)
23
29
  message = "#{scenario.title} #{bold(scenario.status.to_s.upcase)}"
24
30
  puts colored(message, scenario.status)
25
31
 
26
- @failed_steps.each do |step|
27
- if step.exception
28
- puts step.exception.message
29
- puts step.exception.backtrace.join("\n")
30
- puts
32
+ statuses[scenario.status] ||= 0
33
+ statuses[scenario.status] += 1
34
+
35
+ if scenario.failed?
36
+ scenario.steps.each do |step|
37
+ puts colored(" #{status_marker(step.status)} #{step.title}", step.status)
38
+ puts indent(4, backtrace_formatter.error_message(step.exception, include_backtrace: true)) if step.failed?
31
39
  end
32
40
  end
41
+
42
+ flush
33
43
  end
34
44
 
35
45
  private
@@ -38,25 +48,52 @@ module Lopata
38
48
  case status
39
49
  when :failed then red(text)
40
50
  when :passed then green(text)
51
+ when :skipped then cyan(text)
52
+ when :pending then yellow(text)
41
53
  else text
42
54
  end
43
55
  end
44
56
 
45
- def red(text)
46
- wrap(text, 31)
57
+ {
58
+ red: 31,
59
+ green: 32,
60
+ cyan: 36,
61
+ yellow: 33,
62
+ bold: 1,
63
+ }.each do |color, code|
64
+ define_method(color) do |text|
65
+ wrap(text, code)
66
+ end
47
67
  end
48
68
 
49
- def green(text)
50
- wrap(text, 32)
69
+ def wrap(text, code)
70
+ "\e[#{code}m#{text}\e[0m"
51
71
  end
52
72
 
53
- def bold(text)
54
- wrap(text, 1)
73
+ def backtrace_formatter
74
+ @backtrace_formatter ||= Lopata::Observers::BacktraceFormatter.new
55
75
  end
56
76
 
57
- def wrap(text, code)
58
- "\e[#{code}m#{text}\e[0m"
77
+ def status_marker(status)
78
+ case status
79
+ when :failed then "[!]"
80
+ when :skipped then "[-]"
81
+ when :pending then "[?]"
82
+ else "[+]"
83
+ end
84
+ end
85
+
86
+ # Adds indent to text
87
+ # @param cols [Number] number of spaces to be added
88
+ # @param text [String] text to add indent
89
+ # @return [String] text with indent
90
+ def indent(cols, text)
91
+ text.split("\n").map { |line| " " * cols + line }.join("\n")
92
+ end
93
+
94
+ def statuses
95
+ @statuses ||= {}
59
96
  end
60
97
  end
61
98
  end
62
- end
99
+ end
@@ -1,49 +1,20 @@
1
1
  require 'httparty'
2
2
  require 'json'
3
+ require_relative 'backtrace_formatter'
3
4
 
4
5
  module Lopata
5
6
  module Observers
7
+ # @private
6
8
  class WebLogger < BaseObserver
7
9
  def started(world)
8
- raise "Build number is not initailzed in Lopata::Config" unless Lopata::Config.build_number
9
- @client = Lopata::Client.new(Lopata::Config.build_number)
10
+ @client = Lopata::Client.new
10
11
  @client.start(world.scenarios.count)
12
+ @finished = 0
11
13
  end
12
14
 
13
15
  def scenario_finished(scenario)
14
- if scenario.failed?
15
- backtrace = backtrace_for(scenario)
16
- @client.add_attempt(scenario, Lopata::FAILED, error_message_for(scenario), backtrace)
17
- else
18
- @client.add_attempt(scenario, Lopata::PASSED)
19
- end
20
- end
21
-
22
- # def example_pending(notification)
23
- # example = notification.example
24
- # @client.add_attempt(example, Lopata::PENDING, example.execution_result.pending_message)
25
- # end
26
-
27
- private
28
-
29
- def error_message_for(scenario)
30
- exception = scenario.steps.map(&:exception).compact.last
31
- msg = ''
32
- if exception
33
- msg << "#{exception.class.name}: " unless exception.class.name =~ /RSpec/
34
- msg << "#{exception.message.to_s}" if exception.message
35
- end
36
- (msg.length == 0) ? 'Empty message' : msg
37
- end
38
-
39
- def backtrace_for(scenario)
40
- exception = scenario.steps.map(&:exception).compact.last
41
- msg = ''
42
- if exception
43
- msg = exception.backtrace.join("\n")
44
- msg << "\n"
45
- end
46
- msg
16
+ @finished += 1
17
+ @client.add_attempt(scenario, @finished)
47
18
  end
48
19
  end
49
20
  end
@@ -51,28 +22,41 @@ module Lopata
51
22
  PASSED = 0
52
23
  FAILED = 1
53
24
  PENDING = 2
25
+ SKIPPED = 5
54
26
 
27
+ # @private
55
28
  class Client
56
29
  include HTTParty
57
- base_uri Lopata::Config.lopata_host
58
30
 
59
- attr_accessor :build_number
31
+ attr_reader :url, :project_code, :build_number
60
32
 
61
- def initialize(build_number)
62
- @build_number = build_number
33
+ def initialize
34
+ params = Lopata.configuration.web_logging_params
35
+ raise "Web logging is not initailzed" unless params
36
+ @url = HTTParty.normalize_base_uri(params[:url])
37
+ @project_code = params[:project_code]
38
+ @build_number = params[:build_number]
63
39
  end
64
40
 
65
41
  def start(count)
66
42
  @launch_id = JSON.parse(post("/projects/#{project_code}/builds/#{build_number}/launches.json", body: {total: count}).body)['id']
67
43
  end
68
44
 
69
- def add_attempt(scenario, status, msg = nil, backtrace = nil)
45
+ def add_attempt(scenario, finished)
46
+ status = scenario.failed? ? Lopata::FAILED : Lopata::PASSED
47
+ steps = scenario.steps.map { |s| step_hash(s) }
48
+ request = { status: status, steps: steps, launch: { id: @launch_id, finished: finished } }
70
49
  test = test_id(scenario)
71
- request = { status: status}
72
- request[:message] = msg if msg
73
- request[:backtrace] = backtrace if backtrace
74
50
  post("/tests/#{test}/attempts.json", body: request)
75
- inc_finished
51
+ end
52
+
53
+ def step_hash(step)
54
+ hash = { status: step.status, title: step.title }
55
+ if step.failed?
56
+ hash[:message] = error_message_for(step)
57
+ hash[:backtrace] = backtrace_for(step)
58
+ end
59
+ hash
76
60
  end
77
61
 
78
62
  def test_id(scenario)
@@ -98,32 +82,46 @@ module Lopata
98
82
 
99
83
  private
100
84
 
101
- def get_json(url)
102
- JSON.parse(self.class.get(url).body)
85
+ def get_json(path)
86
+ JSON.parse(self.class.get(path, base_uri: url).body)
103
87
  end
104
88
 
105
89
  def post(*args)
106
- self.class.post(*args)
90
+ self.class.post(*with_base_uri(args))
107
91
  end
108
92
 
109
93
  def patch(*args)
110
- self.class.patch(*args)
94
+ self.class.patch(*with_base_uri(args))
95
+ end
96
+
97
+ def with_base_uri(args = [])
98
+ if args.last.is_a? Hash
99
+ args.last[:base_uri] = url
100
+ else
101
+ args << { base_uri: url }
102
+ end
103
+ args
104
+ end
105
+
106
+ def error_message_for(step)
107
+ if step.exception
108
+ backtrace_formatter.error_message(step.exception)
109
+ else
110
+ 'Empty error message'
111
+ end
111
112
  end
112
113
 
113
- def inc_finished
114
- @finished ||= 0
115
- @finished += 1
116
- response = patch("/launches/#{@launch_id}",
117
- body: { finished: @finished }.to_json,
118
- headers: { 'Content-Type' => 'application/json', 'Accept' => 'application/json' })
119
- if response.code == 404
120
- puts 'Launch has been cancelled. Exit.'
121
- exit!
114
+ def backtrace_for(step)
115
+ msg = ''
116
+ if step.exception
117
+ msg = backtrace_formatter.format(step.exception.backtrace).join("\n")
118
+ msg << "\n"
122
119
  end
120
+ msg
123
121
  end
124
122
 
125
- def project_code
126
- Lopata::Config.lopata_code
123
+ def backtrace_formatter
124
+ @backtrace_formatter ||= Lopata::Observers::BacktraceFormatter.new
127
125
  end
128
126
  end
129
127
  end