blinka-reporter 0.5.2 → 0.6.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,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
+ }