lopata 0.1.1 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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