blinka-reporter 0.5.2 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,169 @@
1
+ require 'httparty'
2
+ require 'blinka_reporter/error'
3
+
4
+ module BlinkaReporter
5
+ class Blinka
6
+ SUPPORTED_MIME_TYPES = {
7
+ jpg: 'image/jpeg',
8
+ jpeg: 'image/jpeg',
9
+ png: 'image/png'
10
+ }
11
+
12
+ include HTTParty
13
+
14
+ def self.report(data:, config:)
15
+ Blinka.new(data: data, config: config).report
16
+ end
17
+
18
+ def initialize(data:, config:)
19
+ @data = data
20
+ @config = config
21
+ self.class.base_uri("#{@config.host}/api/v1")
22
+ end
23
+
24
+ def report
25
+ if ENV.fetch('BLINKA_ALLOW_WEBMOCK_DISABLE', 'true') == 'true' &&
26
+ defined?(WebMock) && WebMock.respond_to?(:disable!)
27
+ WebMock.disable!
28
+ end
29
+
30
+ @config.validate_blinka
31
+ self.authenticate
32
+
33
+ results =
34
+ @data
35
+ .fetch(:results, [])
36
+ .map do |result|
37
+ if !result[:image].nil?
38
+ result[:image] = Blinka.upload_image(filepath: result[:image])
39
+ result
40
+ else
41
+ result
42
+ end
43
+ end
44
+
45
+ body = {
46
+ report: {
47
+ repository: @config.repository,
48
+ tag: @config.tag,
49
+ commit: @config.commit,
50
+ metadata: {
51
+ total_time: @data[:total_time],
52
+ nbr_tests: @data[:nbr_tests],
53
+ nbr_assertions: @data[:nbr_assertions],
54
+ seed: @data[:seed]
55
+ }.compact,
56
+ results: results
57
+ }
58
+ }
59
+
60
+ response =
61
+ self.class.post(
62
+ '/report',
63
+ body: body.to_json,
64
+ headers: {
65
+ 'Content-Type' => 'application/json',
66
+ 'Authorization' => "Bearer #{@jwt_token}"
67
+ }
68
+ )
69
+ case response.code
70
+ when 200
71
+ puts "Reported #{@data[:nbr_tests]} tests of commit #{@config.commit}!"
72
+ else
73
+ raise(
74
+ BlinkaReporter::Error,
75
+ "Could not report, got response code #{response.code}"
76
+ )
77
+ end
78
+ rescue => error
79
+ raise(BlinkaReporter::Error, <<-EOS)
80
+ BLINKA:
81
+ Failed to create report because of #{error.class} with message:
82
+ #{error.message}
83
+ EOS
84
+ ensure
85
+ WebMock.enable! if defined?(WebMock) && WebMock.respond_to?(:enable!)
86
+ end
87
+
88
+ def authenticate
89
+ response =
90
+ self.class.post(
91
+ '/authentication',
92
+ body: {
93
+ token_id: @config.team_id,
94
+ token_secret: @config.team_secret
95
+ }.to_json,
96
+ headers: { 'Content-Type' => 'application/json' }
97
+ )
98
+ case response.code
99
+ when 200
100
+ @jwt_token = JSON.parse(response.body).dig('auth_token')
101
+ else
102
+ raise(BlinkaReporter::Error, 'Could not authenticate to API')
103
+ end
104
+ end
105
+
106
+ def self.upload_image(filepath:)
107
+ return unless File.exist?(filepath)
108
+
109
+ file = File.open(filepath)
110
+ filename = File.basename(filepath)
111
+ extension = File.extname(filepath).delete('.').to_sym
112
+ content_type = SUPPORTED_MIME_TYPES[extension]
113
+ return if content_type.nil?
114
+
115
+ presigned_post =
116
+ Blinka.presign_image(filename: filename, content_type: content_type)
117
+ Blinka.upload_to_storage(presigned_post: presigned_post, file: file)
118
+
119
+ puts "Uploaded: #{filename}"
120
+ Blinka.to_shrine_object(
121
+ presigned_post: presigned_post,
122
+ file: file,
123
+ filename: filename
124
+ )
125
+ end
126
+
127
+ def self.presign_image(filename:, content_type:)
128
+ response =
129
+ self.get(
130
+ '/presign',
131
+ body: { filename: filename, content_type: content_type }
132
+ )
133
+
134
+ case response.code
135
+ when 200
136
+ JSON.parse(response.body)
137
+ else
138
+ raise(BlinkaReporter::Error, 'Could not presign file')
139
+ end
140
+ end
141
+
142
+ def self.upload_to_storage(presigned_post:, file:)
143
+ url = URI.parse(presigned_post.fetch('url'))
144
+
145
+ body = presigned_post['fields'].merge({ 'file' => file.read })
146
+ response = HTTParty.post(url, multipart: true, body: body)
147
+
148
+ case response.code
149
+ when 204
150
+ true
151
+ else
152
+ raise(BlinkaReporter::Error, 'Could not upload file to storage')
153
+ end
154
+ end
155
+
156
+ def self.to_shrine_object(presigned_post:, file:, filename:)
157
+ storage, idx = presigned_post.dig('fields', 'key').split('/')
158
+ {
159
+ "id": idx,
160
+ "storage": storage,
161
+ "metadata": {
162
+ "size": file.size,
163
+ "filename": filename,
164
+ "mime_type": presigned_post.dig('fields', 'Content-Type')
165
+ }
166
+ }
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,36 @@
1
+ require 'blinka_reporter/client'
2
+ require 'blinka_reporter/version'
3
+
4
+ module BlinkaReporter
5
+ class Cli
6
+ def self.run(argv)
7
+ if (argv.index('--help') || -1) >= 0
8
+ puts(<<~EOS)
9
+ blinka_reporter version #{BlinkaReporter::VERSION}
10
+
11
+ Options:
12
+ --tap: Flag for outputting test results in TAP-protocol, helpful on Heroku CI.
13
+ --blinka: Flag for reporting test results to blinka.app, requires setting environment:
14
+ - BLINKA_TEAM_ID
15
+ - BLINKA_TEAM_SECRET
16
+ - BLINKA_REPOSITORY
17
+ --path <path>: Path to test results file, works for
18
+ - ./blinka_results.json blinka json format [default]
19
+ - ./rspec.xml from https://github.com/sj26/rspec_junit_formatter
20
+ EOS
21
+ return 0
22
+ end
23
+
24
+ tap = (argv.index('--tap') || -1) >= 0
25
+ blinka = (argv.index('--blinka') || -1) >= 0
26
+ path = argv_value_for(argv, '--path') || './blinka_results.json'
27
+ BlinkaReporter::Client.report(blinka: blinka, tap: tap, path: path)
28
+ 0
29
+ end
30
+
31
+ def self.argv_value_for(argv, option_name)
32
+ return unless (index = argv.index(option_name))
33
+ argv[index + 1]
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,109 @@
1
+ require 'httparty'
2
+ require 'ox'
3
+
4
+ require 'blinka_reporter/blinka'
5
+ require 'blinka_reporter/config'
6
+ require 'blinka_reporter/error'
7
+ require 'blinka_reporter/tap'
8
+
9
+ module BlinkaReporter
10
+ class Client
11
+ def initialize
12
+ @config = BlinkaReporter::Config.new
13
+ end
14
+
15
+ def report(path: './blinka_results.json', blinka: false, tap: false)
16
+ unless File.exist?(path)
17
+ raise(
18
+ BlinkaReporter::Error,
19
+ "Could not find #{path}, was it generated when running the tests?"
20
+ )
21
+ end
22
+
23
+ data =
24
+ if path.end_with?('.xml')
25
+ self.class.parse_xml(path: path)
26
+ elsif path.end_with?('.json')
27
+ self.class.parse_json(path: path)
28
+ else
29
+ raise(
30
+ BlinkaReporter::Error,
31
+ "Unknown format of #{path}, needs to be .json or .xml"
32
+ )
33
+ end
34
+
35
+ BlinkaReporter::Tap.report(data) if tap
36
+ BlinkaReporter::Blinka.report(config: @config, data: data) if blinka
37
+ 0
38
+ end
39
+
40
+ def self.report(path:, tap: false, blinka: false)
41
+ Client.new.report(path: path, tap: tap, blinka: blinka)
42
+ end
43
+
44
+ def self.parse_json(path:)
45
+ JSON.parse(File.open(path).read, symbolize_names: true)
46
+ end
47
+
48
+ def self.parse_xml(path:)
49
+ data = Ox.load_file(path, { symbolize_keys: true, skip: :skip_none })
50
+ test_suite = data.root
51
+ unless test_suite.name == 'testsuite'
52
+ raise("Root element is not <testsuite>, instead #{test_suite.name}")
53
+ end
54
+
55
+ properties = test_suite.nodes.select { |node| node.name == 'properties' }
56
+ test_cases = test_suite.nodes.select { |node| node.name == 'testcase' }
57
+ {
58
+ nbr_tests: Integer(test_suite.tests || 0),
59
+ total_time: Float(test_suite.time),
60
+ seed: xml_seed(properties),
61
+ results: xml_test_cases(test_cases)
62
+ }
63
+ end
64
+
65
+ def self.xml_seed(ox_properties)
66
+ ox_properties.each do |property|
67
+ property.nodes.each do |node|
68
+ return node.attributes[:value] if node.attributes[:name] == 'seed'
69
+ end
70
+ end
71
+ nil
72
+ end
73
+
74
+ # Kind is extracted from the second part of spec.models.customer_spec
75
+ def self.xml_test_cases(test_cases)
76
+ test_cases.map do |test_case|
77
+ result = {
78
+ kind: Array(test_case.attributes[:classname]&.split('.'))[1],
79
+ name: test_case.attributes[:name],
80
+ path: test_case.attributes[:file]&.delete_prefix('./'),
81
+ time: Float(test_case.attributes[:time] || 0)
82
+ }
83
+ if test_case.nodes.any?
84
+ skipped = test_case.nodes.any? { |node| node.name == 'skipped' }
85
+ result[:result] = 'skip' if skipped
86
+ failure =
87
+ test_case.nodes.select { |node| node.name == 'failure' }.first
88
+ if failure
89
+ result[:image] = get_image_path(failure.text)
90
+ result[:result] = 'fail'
91
+ result[:backtrace] = failure.text.split('\n')
92
+ result[:message] = failure.attributes[:message]
93
+ end
94
+ else
95
+ result[:result] = 'pass'
96
+ end
97
+ result
98
+ end
99
+ end
100
+
101
+ def self.get_image_path(text)
102
+ path = /^\[Screenshot\]:\s([\S]*)$/.match(text)
103
+ return if path.nil?
104
+ path = path[1]
105
+ return unless File.exists?(path)
106
+ path
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,34 @@
1
+ require 'blinka_reporter/error'
2
+
3
+ module BlinkaReporter
4
+ class Config
5
+ attr_reader(:commit, :host, :repository, :tag, :team_id, :team_secret)
6
+
7
+ def initialize
8
+ @commit = find_commit
9
+ @host = ENV.fetch('BLINKA_HOST', 'https://www.blinka.app')
10
+ @repository = ENV.fetch('BLINKA_REPOSITORY', nil)
11
+ @tag = ENV.fetch('BLINKA_TAG', nil)
12
+ @team_id = ENV.fetch('BLINKA_TEAM_ID', nil)
13
+ @team_secret = ENV.fetch('BLINKA_TEAM_SECRET', nil)
14
+ end
15
+
16
+ def validate_blinka
17
+ if @team_id.nil? || @team_secret.nil? || @repository.nil?
18
+ raise(BlinkaReporter::Error, <<~EOS)
19
+ Missing configuration, make sure to set required environment variables:
20
+ - BLINKA_TEAM_ID
21
+ - BLINKA_TEAM_SECRET
22
+ - BLINKA_REPOSITORY
23
+ EOS
24
+ end
25
+ end
26
+
27
+ def find_commit
28
+ ENV.fetch(
29
+ 'BLINKA_COMMIT',
30
+ ENV.fetch('HEROKU_TEST_RUN_COMMIT_VERSION', `git rev-parse HEAD`.chomp)
31
+ )
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module BlinkaReporter
2
+ class Error < StandardError; end
3
+ end
@@ -0,0 +1,92 @@
1
+ module BlinkaReporter
2
+ class MinitestAdapter
3
+ def initialize(test_result)
4
+ @test_result = test_result
5
+ end
6
+
7
+ def path
8
+ @path ||= source_location.first.gsub(Dir.getwd, '').delete_prefix('/')
9
+ end
10
+
11
+ def line
12
+ @line ||= source_location.last
13
+ end
14
+
15
+ # Handle broken API in Minitest between 5.10 and 5.11
16
+ # https://github.com/minitest-reporters/minitest-reporters/blob/e9092460b5a5cf5ca9eb375428217cbb2a7f6dbb/lib/minitest/reporters/default_reporter.rb#L159
17
+ def source_location
18
+ @source_location ||=
19
+ if @test_result.respond_to?(:klass)
20
+ @test_result.source_location
21
+ else
22
+ @test_result.method(@test_result.name).source_location
23
+ end
24
+ end
25
+
26
+ def kind
27
+ parts = self.path.gsub('test/', '').split('/')
28
+ parts.length > 1 ? parts.first : 'general'
29
+ end
30
+
31
+ def message
32
+ failure = @test_result.failure
33
+ return unless failure
34
+ "#{failure.error.class}: #{failure.error.message}"
35
+ end
36
+
37
+ def backtrace
38
+ return unless @test_result.failure
39
+ Minitest.filter_backtrace(@test_result.failure.backtrace)
40
+ end
41
+
42
+ def result
43
+ if @test_result.error?
44
+ :error
45
+ elsif @test_result.skipped?
46
+ :skip
47
+ elsif @test_result.failure
48
+ :fail
49
+ else
50
+ :pass
51
+ end
52
+ end
53
+
54
+ def time
55
+ @test_result.time
56
+ end
57
+
58
+ def name
59
+ @test_result.name
60
+ end
61
+
62
+ def image
63
+ return unless kind == 'system'
64
+
65
+ image_path =
66
+ if defined?(Capybara) && Capybara.respond_to?(:save_path) &&
67
+ Capybara.save_path.present?
68
+ "#{Capybara.save_path}/failures_#{name}.png"
69
+ else
70
+ "./tmp/screenshots/failures_#{name}.png"
71
+ end
72
+
73
+ return unless File.exist?(image_path)
74
+
75
+ image_path
76
+ end
77
+
78
+ def report
79
+ {
80
+ backtrace: backtrace,
81
+ message: message,
82
+ line: line,
83
+ image: image,
84
+ kind: kind,
85
+ name: name,
86
+ path: path,
87
+ result: result,
88
+ time: time
89
+ }.compact
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,51 @@
1
+ module BlinkaReporter
2
+ class Tap
3
+ # Based on https://github.com/kern/minitest-reporters/blob/master/lib/minitest/reporters/progress_reporter.rb
4
+ # Tries to adhere to https://testanything.org/tap-specification.html
5
+ TAP_COMMENT_PAD = 8
6
+ attr_reader(:data)
7
+
8
+ def self.report(data)
9
+ Tap.new(data).report
10
+ end
11
+
12
+ def initialize(data)
13
+ results = Array(data[:results])
14
+ return if results.size == 0
15
+
16
+ @data = <<~REPORT
17
+ TAP version 13
18
+ 1..#{results.size}
19
+ #{test_results(results)}
20
+ REPORT
21
+ end
22
+
23
+ def test_results(results)
24
+ report = []
25
+ results.each_with_index do |test, index|
26
+ test_str = "#{test[:path]} - #{test[:name].tr('#', '_')}"
27
+ result = test[:result]
28
+ if result == 'pass'
29
+ report << "ok #{index + 1} - #{test_str}"
30
+ elsif result == 'skip'
31
+ report << "ok #{index + 1} # skip: #{test_str}"
32
+ elsif result == 'fail'
33
+ report << "not ok #{index + 1} - failed: #{test_str}"
34
+ test[:message].split('\n') do |line|
35
+ report << "##{' ' * TAP_COMMENT_PAD + line}"
36
+ end
37
+ report << '#'
38
+ Array(test[:backtrace]).each do |line|
39
+ report << "##{' ' * TAP_COMMENT_PAD + line}"
40
+ end
41
+ report << ''
42
+ end
43
+ end
44
+ report.join("\n")
45
+ end
46
+
47
+ def report
48
+ puts(@data)
49
+ end
50
+ end
51
+ end
@@ -1,3 +1,3 @@
1
1
  module BlinkaReporter
2
- VERSION = '0.5.2'.freeze
2
+ VERSION = '0.6.0'.freeze
3
3
  end
@@ -0,0 +1,4 @@
1
+ require 'blinka_reporter/version'
2
+ require 'blinka_reporter/cli'
3
+
4
+ module BlinkaReporter; end
@@ -1,7 +1,7 @@
1
1
  require 'minitest'
2
2
  require 'json'
3
- require 'blinka_minitest'
4
- require 'blinka_client'
3
+ require 'blinka_reporter/minitest_adapter'
4
+ require 'blinka_reporter/client'
5
5
 
6
6
  module Minitest
7
7
  def self.plugin_blinka_init(options)
@@ -12,7 +12,6 @@ module Minitest
12
12
 
13
13
  module BlinkaPlugin
14
14
  REPORT_PATH = 'blinka_results.json'.freeze
15
- TAP_COMMENT_PAD = 8
16
15
  class Reporter < Minitest::StatisticsReporter
17
16
  attr_accessor :tests
18
17
 
@@ -29,12 +28,8 @@ module Minitest
29
28
  def report
30
29
  super
31
30
 
32
- tap_report if ENV['BLINKA_TAP']
33
- if ENV['BLINKA_JSON'] || ENV['BLINKA_REPORT']
34
- json_report(append: !ENV['BLINKA_APPEND'].nil?)
35
- end
36
- BlinkaClient.new.report if ENV['BLINKA_REPORT']
37
- rescue BlinkaClient::BlinkaError => error
31
+ json_report(append: !ENV['BLINKA_APPEND'].nil?) if ENV['BLINKA_JSON']
32
+ rescue BlinkaReporter::Client::BlinkaReporter::Error => error
38
33
  puts(error)
39
34
  end
40
35
 
@@ -45,11 +40,11 @@ module Minitest
45
40
  total_time: total_time,
46
41
  nbr_tests: count,
47
42
  nbr_assertions: assertions,
48
- commit: find_commit,
49
- tag: ENV.fetch('BLINKA_TAG', ''),
50
43
  seed: options[:seed],
51
44
  results:
52
- tests.map { |test_result| BlinkaMinitest.new(test_result).report }
45
+ tests.map do |test_result|
46
+ BlinkaReporter::MinitestAdapter.new(test_result).report
47
+ end
53
48
  }
54
49
  result = append_previous(result) if append
55
50
 
@@ -61,63 +56,23 @@ module Minitest
61
56
  puts("Test results written to `#{REPORT_PATH}`")
62
57
  end
63
58
 
64
- # Based on https://github.com/kern/minitest-reporters/blob/master/lib/minitest/reporters/progress_reporter.rb
65
- # Tries to adhere to https://testanything.org/tap-specification.html
66
- def tap_report
67
- puts
68
- puts('TAP version 13')
69
- puts("1..#{tests.length}")
70
- tests.each_with_index do |test, index|
71
- blinka = BlinkaMinitest.new(test)
72
- test_str = "#{blinka.path} - #{test.name.tr('#', '_')}"
73
- if test.passed?
74
- puts "ok #{index + 1} - #{test_str}"
75
- elsif test.skipped?
76
- puts "ok #{index + 1} # skip: #{test_str}"
77
- elsif test.failure
78
- puts "not ok #{index + 1} - failed: #{test_str}"
79
- blinka.message.each_line { |line| print_padded_comment(line) }
80
-
81
- # test.failure.message.each_line { |line| print_padded_comment(line) }
82
- unless test.failure.is_a?(MiniTest::UnexpectedError)
83
- blinka.backtrace.each { |line| print_padded_comment(line) }
84
- end
85
- puts
86
- end
87
- end
88
- end
89
-
90
- def print_padded_comment(line)
91
- puts "##{' ' * TAP_COMMENT_PAD + line}"
92
- end
93
-
94
- def find_commit
95
- ENV.fetch(
96
- 'BLINKA_COMMIT',
97
- ENV.fetch(
98
- 'HEROKU_TEST_RUN_COMMIT_VERSION',
99
- `git rev-parse HEAD`.chomp
100
- )
101
- )
102
- end
103
-
104
59
  private
105
60
 
106
61
  def parse_report
107
62
  return unless File.exist?(REPORT_PATH)
108
- JSON.parse(File.read(REPORT_PATH))
63
+ JSON.parse(File.read(REPORT_PATH), symbolize_names: true)
109
64
  end
110
65
 
111
66
  def append_previous(result)
112
67
  previous = parse_report
113
68
  return if previous.nil?
114
- return if result[:commit] != previous['commit']
115
- return if result[:tag] != previous['tag']
69
+ return if result[:commit] != previous[:commit]
70
+ return if result[:tag] != previous[:tag]
116
71
 
117
- result[:total_time] += previous['total_time'] || 0
118
- result[:nbr_tests] += previous['nbr_tests'] || 0
119
- result[:nbr_assertions] += previous['nbr_assertions'] || 0
120
- result[:results] += previous['results'] || []
72
+ result[:total_time] += previous[:total_time] || 0
73
+ result[:nbr_tests] += previous[:nbr_tests] || 0
74
+ result[:nbr_assertions] += previous[:nbr_assertions] || 0
75
+ result[:results] += previous[:results] || []
121
76
  result
122
77
  end
123
78
  end
data/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "blinka_reporter",
3
+ "version": "0.0.1",
4
+ "author": "David Wessman <david@wessman.co>",
5
+ "license": "MIT",
6
+ "devDependencies": {
7
+ "@prettier/plugin-ruby": "^2.0.0",
8
+ "husky": "^7.0.4",
9
+ "lint-staged": "^12.3.4"
10
+ },
11
+ "scripts": {
12
+ "pretty": "./node_modules/.bin/prettier --write ."
13
+ },
14
+ "husky": {
15
+ "hooks": {
16
+ "pre-commit": "lint-staged"
17
+ }
18
+ },
19
+ "lint-staged": {
20
+ "*.{md,rb,json,yml,gemspec}": "./node_modules/.bin/prettier --write"
21
+ }
22
+ }