probe-dock-rspec 0.5.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,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NTJlNjI0N2E1ZjkzZGZjMzExNWUwZjY1MTlkNDRjZTFjNzZiZjJiMQ==
5
+ data.tar.gz: !binary |-
6
+ NmU1M2JjNzY2Y2E5MjE1OWIyOTQ1ZjI4MjcxYTkxODBkZGZiMGVlNg==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ OGRhYWU3MjIxMjUxMWNlYTAxNWI3NWEzNDE2MTI4YmY0NDlmMGRjNDY3YTYw
10
+ ZjhmN2ExZWQ2MTJjMDFkMjNmMDcyN2U5MGU3ZTcwMjIxOTFlMDVjZTM0Zjhj
11
+ NTM4OGU5MjBiMTQyZTY3MjUxMGU2ZjM3MDNhNDkzMjg2ZTc5MDY=
12
+ data.tar.gz: !binary |-
13
+ NGIwZmM2NWJiZjc2MTExMjU3MWFlNDFiM2UxMjVjYjFlZmRhN2U4M2VjZjBi
14
+ NTI3ODBkMzJjNTg0MGVhMzg2YmZjYTNhNjIxZmU1MzE5ZWQ3MGI3YzI2OWIz
15
+ ODIyYTE4ZWExODQxOTI3NjBiYTFhODFmYzg1ZmUzYjg5MDU5NjE=
data/Gemfile ADDED
@@ -0,0 +1,21 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ gem 'rspec', '~> 3.1'
7
+ gem 'oj', '~> 2.1'
8
+ gem 'httparty', '~> 0.13'
9
+ gem 'paint', '~> 1.0'
10
+ gem 'rake', '~> 10.1'
11
+
12
+ # Add dependencies to develop your gem here.
13
+ # Include everything needed to run rake, tests, features, etc.
14
+ group :development do
15
+ gem 'jeweler', '~> 2.0'
16
+ gem 'rake-version', '~> 0.4'
17
+ gem 'simplecov', '~> 0.10'
18
+ gem 'fakefs', '~> 0.6', require: 'fakefs/safe'
19
+ gem 'rspec-its', '~> 1.2'
20
+ gem 'rspec-collection_matchers', '~> 1.1'
21
+ end
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2015 Probe Dock
2
+ Copyright (c) 2012-2013 Lotaris SA
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,69 @@
1
+ # Probe Dock RSpec
2
+
3
+ **RSpec probe for [Probe Dock](https://github.com/probe-dock/probe-dock).**
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/probe-dock-rspec.png)](http://badge.fury.io/rb/probe-dock-rspec)
6
+
7
+ ## Requirements
8
+
9
+ * RSpec 3+
10
+
11
+ ## Installation
12
+
13
+ In your Gemfile:
14
+
15
+ ```rb
16
+ gem 'probe-dock-rspec', '~> 0.4.1'
17
+ ```
18
+
19
+ Manually:
20
+
21
+ gem install probe-dock-rspec
22
+
23
+ ## Usage
24
+
25
+ If you haven't done it already, follow the [setup procedure](#setup) below.
26
+
27
+ To track a test with a Probe Dock test key, use RSpec metadata:
28
+
29
+ ```rb
30
+ it "should work", probe_dock: { key: 'abcdefghijkl' } do
31
+ expect(true).to be(true)
32
+ end
33
+
34
+ it(nil, probe_dock: { key: 'bcdefghijklm' }){ should validate_presence_of(:name) }
35
+ ```
36
+
37
+ <a name="setup"></a>
38
+ ## Setup
39
+
40
+ You must first set up the configuration file(s) for the project.
41
+ This procedure is described here:
42
+
43
+ * [Probe Setup Procedure](https://github.com/probe-dock/probe-dock-clients#setup-procedure)
44
+
45
+ You must then enable the client in your spec helper file (e.g. `spec/spec_helper.rb`).
46
+
47
+ ```yml
48
+ ProbeDockRSpec.configure do |config|
49
+
50
+ # Optional category to add to all the tests sent with this client.
51
+ config.project.category = 'RSpec'
52
+ end
53
+ ```
54
+
55
+ The next time you run your test suite, the RSpec probe will send the results to your Probe Dock server.
56
+
57
+ ## Contributing
58
+
59
+ * [Fork](https://help.github.com/articles/fork-a-repo)
60
+ * Create a topic branch - `git checkout -b my_feature`
61
+ * Push to your branch - `git push origin my_feature`
62
+ * Create a [pull request](http://help.github.com/pull-requests/) from your branch
63
+
64
+ Please add a [changelog](CHANGELOG.md) entry with your name for new features and bug fixes.
65
+
66
+ ## License
67
+
68
+ Probe Dock RSpec is licensed under the [MIT License](http://opensource.org/licenses/MIT).
69
+ See [LICENSE.txt](LICENSE.txt) for the full license.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.5.0
@@ -0,0 +1 @@
1
+ require 'probe_dock_rspec'
@@ -0,0 +1,11 @@
1
+ # encoding: UTF-8
2
+ require 'rspec'
3
+
4
+ module ProbeDockRSpec
5
+ VERSION = '0.5.0'
6
+
7
+ class Error < StandardError; end
8
+ class PayloadError < Error; end
9
+ end
10
+
11
+ Dir[File.join File.dirname(__FILE__), File.basename(__FILE__, '.*'), '*.rb'].each{ |lib| require lib }
@@ -0,0 +1,61 @@
1
+
2
+ module ProbeDockRSpec
3
+
4
+ class Cache
5
+
6
+ class Error < ProbeDockRSpec::Error; end
7
+
8
+ def initialize options = {}
9
+ @tests = {}
10
+ @workspace, @server_name, @project_api_id = options[:workspace], options[:server_name], options[:project_api_id]
11
+ end
12
+
13
+ def save test_run
14
+ validate!
15
+
16
+ @tests = { @project_api_id => @tests[@project_api_id] || {} }
17
+ test_run.results.select(&:key).each{ |r| @tests[@project_api_id][r.key] = test_result_hash(r) }
18
+
19
+ FileUtils.mkdir_p File.dirname(cache_file)
20
+ File.open(cache_file, 'w'){ |f| f.write Oj.dump(@tests, mode: :strict) }
21
+
22
+ self
23
+ end
24
+
25
+ def load
26
+ validate!
27
+
28
+ @tests = if File.exists? cache_file
29
+ Oj.load(File.read(cache_file), mode: :strict) rescue {}
30
+ else
31
+ {}
32
+ end
33
+
34
+ self
35
+ end
36
+
37
+ def known? test_result
38
+ test_result.key && !!@tests[@project_api_id] && !!@tests[@project_api_id][test_result.key]
39
+ end
40
+
41
+ def stale? test_result
42
+ !!@tests[@project_api_id] && test_result_hash(test_result) != @tests[@project_api_id][test_result.key]
43
+ end
44
+
45
+ private
46
+
47
+ def validate!
48
+ required = { "workspace" => @workspace, "server name" => @server_name, "project API identifier" => @project_api_id }
49
+ missing = required.keys.select{ |k| !required[k] }
50
+ raise Error.new("Missing cache options: #{missing.join ', '}") if missing.any?
51
+ end
52
+
53
+ def test_result_hash r
54
+ Digest::SHA2.hexdigest "#{r.name} || #{r.category} || #{r.tags.collect(&:to_s).sort.join(' ')} || #{r.tickets.collect(&:to_s).sort.join(' ')}"
55
+ end
56
+
57
+ def cache_file
58
+ @cache_file ||= File.join(@workspace, 'rspec', 'servers', @server_name, 'cache.json')
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,120 @@
1
+
2
+ module ProbeDockRSpec
3
+
4
+ class Client
5
+
6
+ def initialize server, options = {}
7
+
8
+ @server = server
9
+ @publish, @local_mode, @workspace = options[:publish], options[:local_mode], options[:workspace]
10
+ @cache_payload, @print_payload, @save_payload = options[:cache_payload], options[:print_payload], options[:save_payload]
11
+
12
+ cache_options = { workspace: @workspace }
13
+ cache_options.merge! server_name: @server.name, project_api_id: @server.project_api_id if @server
14
+ @cache = Cache.new cache_options
15
+
16
+ @uid = UID.new workspace: @workspace
17
+ end
18
+
19
+ def process test_run
20
+
21
+ return fail "No server to publish results to" if !@server
22
+
23
+ test_run.uid = @uid.load_uid
24
+
25
+ payload_options = @server.payload_options
26
+
27
+ cache_enabled = @cache_payload && load_cache
28
+ payload_options[:cache] = @cache if cache_enabled
29
+
30
+ return false unless payload = build_payload(test_run, payload_options)
31
+
32
+ published = if !@publish
33
+ puts Paint["Probe Dock - Publishing disabled", :yellow]
34
+ false
35
+ elsif publish_payload payload
36
+ @cache.save test_run if cache_enabled
37
+ true
38
+ else
39
+ false
40
+ end
41
+
42
+ save_payload payload if @save_payload
43
+ print_payload payload if @print_payload
44
+
45
+ puts
46
+
47
+ published
48
+ end
49
+
50
+ private
51
+
52
+ def build_payload test_run, options = {}
53
+ begin
54
+ TestPayload.new(test_run).to_h options
55
+ rescue PayloadError => e
56
+ fail e.message
57
+ end
58
+ end
59
+
60
+ def fail msg, type = :error
61
+ styles = { warning: [ :yellow ], error: [ :bold, :red ] }
62
+ warn Paint["Probe Dock - #{msg}", *styles[type]]
63
+ false
64
+ end
65
+
66
+ def load_cache
67
+ begin
68
+ @cache.load
69
+ rescue Cache::Error => e
70
+ warn Paint["Probe Dock - #{e.message}", :yellow]
71
+ false
72
+ end
73
+ end
74
+
75
+ def print_payload payload
76
+ puts Paint['Probe Dock - Printing payload...', :yellow]
77
+ begin
78
+ puts JSON.pretty_generate(payload)
79
+ rescue
80
+ puts payload.inspect
81
+ end
82
+ end
83
+
84
+ def save_payload payload
85
+
86
+ missing = { "workspace" => @workspace, "server" => @server }.inject([]){ |memo,(k,v)| !v ? memo << k : memo }
87
+ return fail "Cannot save payload without a #{missing.join ' and '}" if missing.any?
88
+
89
+ FileUtils.mkdir_p File.dirname(payload_file)
90
+ File.open(payload_file, 'w'){ |f| f.write Oj.dump(payload, mode: :strict) }
91
+ end
92
+
93
+ def payload_file
94
+ @payload_file ||= File.join(@workspace, 'rspec', 'servers', @server.name, 'payload.json')
95
+ end
96
+
97
+ def publish_payload payload
98
+
99
+ puts Paint["Probe Dock - Sending payload to #{@server.api_url}...", :magenta]
100
+
101
+ begin
102
+ if @local_mode
103
+ puts Paint['Probe Dock - LOCAL MODE: not actually sending payload.', :yellow]
104
+ else
105
+ @server.upload payload
106
+ end
107
+ puts Paint["Probe Dock - Done!", :green]
108
+ true
109
+ rescue Server::Error => e
110
+ warn Paint["Probe Dock - Upload failed!", :red, :bold]
111
+ warn Paint["Probe Dock - #{e.message}", :red, :bold]
112
+ if e.response
113
+ warn Paint["Probe Dock - Dumping response body...", :red, :bold]
114
+ warn e.response.body
115
+ end
116
+ false
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,173 @@
1
+ require 'yaml'
2
+
3
+ # Utilities to send test results to Probe Dock.
4
+ module ProbeDockRSpec
5
+
6
+ def self.config
7
+ @config ||= Config.new.tap(&:load)
8
+ end
9
+
10
+ def self.configure options = {}
11
+ yield config if block_given?
12
+ config.load_warnings.each{ |w| warn Paint["Probe Dock - #{w}", :yellow] }
13
+ config.setup! if options[:setup] != false
14
+ config
15
+ end
16
+
17
+ class Config
18
+ # TODO: add silent/verbose option(s)
19
+ class Error < ProbeDockRSpec::Error; end
20
+ attr_writer :publish, :local_mode, :cache_payload, :print_payload, :save_payload
21
+ attr_reader :project, :server, :workspace, :load_warnings
22
+
23
+ def initialize
24
+ @servers = []
25
+ @project = Project.new
26
+ @publish, @local_mode, @cache_payload, @print_payload, @save_payload = false, false, false, false, false
27
+ @load_warnings = []
28
+ end
29
+
30
+ def workspace= dir
31
+ @workspace = dir ? File.expand_path(dir) : nil
32
+ end
33
+
34
+ def servers
35
+ @servers.dup
36
+ end
37
+
38
+ # Plugs Probe Dock utilities into RSpec.
39
+ def setup!
40
+ ::RSpec.configure do |c|
41
+ c.add_formatter Formatter
42
+ end
43
+ end
44
+
45
+ %w(publish local_mode cache_payload print_payload save_payload).each do |name|
46
+ define_method("#{name}?"){ instance_variable_get("@#{name}") }
47
+ end
48
+
49
+ def client_options
50
+ {
51
+ publish: @publish,
52
+ local_mode: @local_mode,
53
+ workspace: @workspace,
54
+ cache_payload: @cache_payload,
55
+ print_payload: @print_payload,
56
+ save_payload: @save_payload
57
+ }.select{ |k,v| !v.nil? }
58
+ end
59
+
60
+ def load
61
+
62
+ @load_warnings = []
63
+ return unless config = load_config_files
64
+
65
+ @publish = parse_env_flag :publish, !!config[:publish]
66
+ @server_name = parse_env_option(:server) || config[:server]
67
+ @local_mode = parse_env_flag(:local) || !!config[:local]
68
+
69
+ self.workspace = parse_env_option(:workspace) || config[:workspace]
70
+ @cache_payload = parse_env_flag :cache_payload, !!config[:payload][:cache]
71
+ @print_payload = parse_env_flag :print_payload, !!config[:payload][:print]
72
+ @save_payload = parse_env_flag :save_payload, !!config[:payload][:save]
73
+
74
+ @servers, @server = build_servers config
75
+
76
+ if @servers.empty?
77
+ @load_warnings << "No server defined"
78
+ elsif !@server_name
79
+ @load_warnings << "No server name given"
80
+ elsif !@server
81
+ @load_warnings << "Unknown server '#{@server_name}'"
82
+ end
83
+
84
+ project_options = config[:project]
85
+ project_options.merge! api_id: @server.project_api_id if @server and @server.project_api_id
86
+ @project.update project_options
87
+
88
+ self
89
+ end
90
+
91
+ private
92
+
93
+ def build_servers config
94
+
95
+ default_server_options = { project_api_id: config[:project][:api_id] }
96
+ servers = config[:servers].inject({}) do |memo,(name, options)|
97
+ memo[name] = Server.new default_server_options.merge(options).merge(name: name)
98
+ memo
99
+ end
100
+
101
+ [ servers.values, servers[@server_name.to_s.strip] ]
102
+ end
103
+
104
+ def load_config_files
105
+
106
+ configs = [ home_config_file, working_config_file ]
107
+ actual_configs = configs.select{ |f| File.exists? f }
108
+
109
+ if actual_configs.empty?
110
+ @load_warnings << %|no config file found, looking for:\n #{configs.join "\n "}|
111
+ return false
112
+ end
113
+
114
+ actual_configs.collect!{ |f| YAML.load_file f }
115
+
116
+ actual_configs.inject({ servers: {} }) do |memo,yml|
117
+ memo.merge! parse_general_options(yml)
118
+
119
+ if yml['servers'].kind_of? Hash
120
+ yml['servers'].each_pair do |k,v|
121
+ if v.kind_of? Hash
122
+ memo[:servers][k] = (memo[:servers][k] || {}).merge(parse_server_options(v))
123
+ end
124
+ end
125
+ end
126
+
127
+ memo[:payload] = (memo[:payload] || {}).merge parse_payload_options(yml['payload'])
128
+ memo[:project] = (memo[:project] || {}).merge parse_project_options(yml['project'])
129
+
130
+ memo
131
+ end
132
+ end
133
+
134
+ def home_config_file
135
+ File.join File.expand_path('~'), '.probe-dock', 'config.yml'
136
+ end
137
+
138
+ def working_config_file
139
+ File.expand_path ENV['PROBE_DOCK_CONFIG'] || 'probe-dock.yml', Dir.pwd
140
+ end
141
+
142
+ def parse_env_flag name, default = false
143
+ val = parse_env_option name
144
+ val ? !!val.to_s.strip.match(/\A(1|t|true)\Z/i) : default
145
+ end
146
+
147
+ def parse_env_option name
148
+ var = "PROBE_DOCK_#{name.upcase}"
149
+ ENV.key?(var) ? ENV[var] : nil
150
+ end
151
+
152
+ def parse_general_options h
153
+ parse_options h, %w(publish server local workspace)
154
+ end
155
+
156
+ def parse_server_options h
157
+ parse_options h, %w(name apiUrl apiToken apiVersion projectApiId)
158
+ end
159
+
160
+ def parse_payload_options h
161
+ parse_options h, %w(save cache print)
162
+ end
163
+
164
+ def parse_project_options h
165
+ parse_options h, %w(version apiId category tags tickets)
166
+ end
167
+
168
+ def parse_options h, keys
169
+ return {} unless h.kind_of? Hash
170
+ keys.inject({}){ |memo,k| memo[k.gsub(/(.)([A-Z])/, '\1_\2').downcase.to_sym] = h[k] if h.key?(k); memo }
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,83 @@
1
+ require 'paint'
2
+ require 'fileutils'
3
+ require 'rspec/core/formatters/base_formatter'
4
+
5
+ module ProbeDockRSpec
6
+
7
+ class Formatter
8
+
9
+ RSpec::Core::Formatters.register self, :start, :stop, :close,
10
+ :example_group_started, :example_started, :example_passed, :example_failed, :example_group_finished
11
+
12
+ def initialize output
13
+
14
+ config = ProbeDockRSpec.config
15
+ @client = Client.new config.server, config.client_options
16
+ @test_run = TestRun.new config.project
17
+
18
+ @groups = []
19
+ end
20
+
21
+ def start notification
22
+ # TODO: measure milliseconds
23
+ @start_time = Time.now
24
+ end
25
+
26
+ def example_group_started group_notification
27
+ @groups << group_notification.group
28
+ end
29
+
30
+ def example_group_finished group_notification
31
+ @groups.pop
32
+ end
33
+
34
+ def example_started example_notification
35
+ @current_time = Time.now
36
+ end
37
+
38
+ def example_passed example_notification
39
+ add_result example_notification, true
40
+ end
41
+
42
+ def example_failed example_notification
43
+ add_result example_notification, false
44
+ end
45
+
46
+ def stop notification
47
+ end_time = Time.now
48
+ @test_run.duration = ((end_time - @start_time) * 1000).round
49
+ end
50
+
51
+ def close notification
52
+ @client.process @test_run
53
+ end
54
+
55
+ private
56
+
57
+ def add_result example_notification, successful
58
+
59
+ options = {
60
+ passed: successful,
61
+ duration: ((Time.now - @current_time) * 1000).round
62
+ }
63
+
64
+ options[:message] = failure_message example_notification unless successful
65
+
66
+ @test_run.add_result example_notification.example, @groups, options
67
+ end
68
+
69
+ def failure_message example_notification
70
+ String.new.tap do |m|
71
+ m << example_notification.description
72
+ m << "\n"
73
+ m << example_notification.message_lines.collect{ |l| " #{l}" }.join("\n")
74
+ m << "\n"
75
+ m << example_notification.formatted_backtrace.collect{ |l| " # #{l}" }.join("\n")
76
+ end
77
+ end
78
+
79
+ def full_example_name example
80
+ (@groups.collect{ |g| g.description.strip } << example.description.strip).join ' '
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,30 @@
1
+ module ProbeDockRSpec
2
+
3
+ class Project
4
+ attr_accessor :version, :api_id, :category, :tags, :tickets
5
+
6
+ def initialize options = {}
7
+ update options
8
+ end
9
+
10
+ def update options = {}
11
+ %w(version api_id category).each do |k|
12
+ instance_variable_set "@#{k}", options[k.to_sym] ? options[k.to_sym].to_s : nil if options.key? k.to_sym
13
+ end
14
+ @tags = wrap(options[:tags]).compact if options.key? :tags
15
+ @tickets = wrap(options[:tickets]).compact if options.key? :tickets
16
+ end
17
+
18
+ def validate!
19
+ required = { "version" => @version, "API identifier" => @api_id }
20
+ missing = required.inject([]){ |memo,(k,v)| v.to_s.strip.length <= 0 ? memo << k : memo }
21
+ raise PayloadError.new("Missing project options: #{missing.join ', '}") if missing.any?
22
+ end
23
+
24
+ private
25
+
26
+ def wrap a
27
+ a.kind_of?(Array) ? a : [ a ]
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,58 @@
1
+ require 'oj'
2
+ require 'httparty'
3
+
4
+ module ProbeDockRSpec
5
+
6
+ class Server
7
+ attr_reader :name, :api_url, :api_token, :api_version, :project_api_id
8
+
9
+ class Error < ProbeDockRSpec::Error
10
+ attr_reader :response
11
+
12
+ def initialize msg, response = nil
13
+ super msg
14
+ @response = response
15
+ end
16
+ end
17
+
18
+ def initialize options = {}
19
+ @name = options[:name].to_s.strip
20
+ @api_url = options[:api_url].to_s if options[:api_url]
21
+ @api_token = options[:api_token].to_s if options[:api_token]
22
+ @api_version = options[:api_version] || 1
23
+ @project_api_id = options[:project_api_id].to_s if options[:project_api_id]
24
+ end
25
+
26
+ def payload_options
27
+ { version: @api_version }
28
+ end
29
+
30
+ def upload payload
31
+ validate!
32
+
33
+ body = Oj.dump payload, mode: :strict
34
+ res = HTTParty.post payload_uri, body: body, headers: payload_headers
35
+
36
+ if res.code != 202
37
+ raise Error.new("Expected HTTP 202 Accepted when submitting payload, got #{res.code}", res)
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def validate!
44
+ required = { "apiUrl" => @api_url }
45
+ required.merge!({ "apiToken" => @api_token, "projectApiId" => @project_api_id }) if @api_version >= 1
46
+ missing = required.inject([]){ |memo,(k,v)| v.to_s.strip.length <= 0 ? memo << k : memo }
47
+ raise Error.new("Server #{@name} is missing the following options: #{missing.join ', '}") if missing.any?
48
+ end
49
+
50
+ def payload_headers
51
+ { 'Authorization' => "Bearer #{@api_token}", 'Content-Type' => 'application/vnd.probe-dock.payload.v1+json' }
52
+ end
53
+
54
+ def payload_uri
55
+ "#{@api_url}/publish"
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,62 @@
1
+ require 'fileutils'
2
+ require 'rake/tasklib'
3
+
4
+ module ProbeDockRSpec
5
+
6
+ class Tasks < ::Rake::TaskLib
7
+
8
+ def initialize
9
+
10
+ namespace :spec do
11
+
12
+ namespace 'probe-dock' do
13
+
14
+ desc "Generate a test run UID to group test results in Probe Dock (stored in an environment variable)"
15
+ task :uid do
16
+ trace do
17
+ uid = uid_manager.generate_uid_to_env
18
+ puts Paint["Probe Dock - Generated UID for test run: #{uid}", :cyan]
19
+ end
20
+ end
21
+
22
+ namespace :uid do
23
+
24
+ desc "Generate a test run UID to group test results in Probe Dock (stored in a file)"
25
+ task :file do
26
+ trace do
27
+ uid = uid_manager.generate_uid_to_file
28
+ puts Paint["Probe Dock - Generated UID for test run: #{uid}", :cyan]
29
+ end
30
+ end
31
+
32
+ desc "Clean the test run UID (file and environment variable)"
33
+ task :clean do
34
+ trace do
35
+ uid_manager.clean_uid
36
+ puts Paint["Probe Dock - Cleaned test run UID", :cyan]
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def trace &block
47
+ if Rake.application.options.trace
48
+ block.call
49
+ else
50
+ begin
51
+ block.call
52
+ rescue UID::Error => e
53
+ warn Paint["Probe Dock - #{e.message}", :red]
54
+ end
55
+ end
56
+ end
57
+
58
+ def uid_manager
59
+ UID.new ProbeDockRSpec.config.client_options
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,18 @@
1
+ require 'fileutils'
2
+ require 'digest/sha2'
3
+
4
+ module ProbeDockRSpec
5
+
6
+ class TestPayload
7
+
8
+ class Error < ProbeDockRSpec::Error; end
9
+
10
+ def initialize run
11
+ @run = run
12
+ end
13
+
14
+ def to_h options = {}
15
+ @run.to_h options
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,119 @@
1
+
2
+ module ProbeDockRSpec
3
+
4
+ class TestResult
5
+ attr_reader :key, :name, :category, :tags, :tickets, :data, :duration, :message
6
+
7
+ def initialize project, example, groups = [], options = {}
8
+
9
+ @category = project.category
10
+ @tags = project.tags
11
+ @tickets = project.tickets
12
+
13
+ @grouped = extract_grouped example, groups
14
+
15
+ [ :key, :name, :category, :tags, :tickets, :data ].each do |attr|
16
+ instance_variable_set "@#{attr}".to_sym, send("extract_#{attr}".to_sym, example, groups)
17
+ end
18
+
19
+ @passed = !!options[:passed]
20
+ @duration = options[:duration]
21
+ @message = options[:message]
22
+ end
23
+
24
+ def passed?
25
+ @passed
26
+ end
27
+
28
+ def grouped?
29
+ @grouped
30
+ end
31
+
32
+ def update options = {}
33
+ @passed &&= options[:passed]
34
+ @duration += options[:duration]
35
+ @message = [ @message, options[:message] ].select{ |m| m }.join("\n\n") if options[:message]
36
+ end
37
+
38
+ def to_h options = {}
39
+ {
40
+ 'p' => @passed,
41
+ 'd' => @duration
42
+ }.tap do |h|
43
+
44
+ h['k'] = @key if @key
45
+ h['m'] = @message if @message
46
+
47
+ cache = options[:cache]
48
+ first = !cache || !cache.known?(self)
49
+ stale = !first && cache.stale?(self)
50
+ h['n'] = @name if stale or first
51
+ h['c'] = @category if stale or (first and @category)
52
+ h['g'] = @tags if stale or (first and !@tags.empty?)
53
+ h['t'] = @tickets if stale or (first and !@tickets.empty?)
54
+ h['a'] = @data if @data # FIXME: cache custom data
55
+ end
56
+ end
57
+
58
+ def self.extract_grouped example, groups = []
59
+ !!groups.collect{ |g| meta(g)[:grouped] }.compact.last
60
+ end
61
+
62
+ def self.extract_key example, groups = []
63
+ (groups.collect{ |g| meta(g)[:key] } << meta(example)[:key]).compact.reject{ |k| k.strip.empty? }.last
64
+ end
65
+
66
+ def self.meta holder
67
+ meta = holder.metadata[:probe_dock] || {}
68
+ if meta.kind_of? String
69
+ { key: meta }
70
+ elsif meta.kind_of? Hash
71
+ meta
72
+ else
73
+ {}
74
+ end
75
+ end
76
+
77
+ private
78
+
79
+ def meta *args
80
+ self.class.meta *args
81
+ end
82
+
83
+ def extract_grouped *args
84
+ self.class.extract_grouped *args
85
+ end
86
+
87
+ def extract_key *args
88
+ self.class.extract_key *args
89
+ end
90
+
91
+ def extract_name example, groups = []
92
+ parts = groups.dup
93
+ parts = parts[0, parts.index{ |p| meta(p)[:grouped] } + 1] if @grouped
94
+ parts << example unless @grouped
95
+ parts.collect{ |p| p.description.strip }.join ' '
96
+ end
97
+
98
+ def extract_category example, groups = []
99
+ cat = (groups.collect{ |g| meta(g)[:category] }.unshift(@category) << meta(example)[:category]).compact.last
100
+ cat ? cat.to_s : nil
101
+ end
102
+
103
+ def extract_tags example, groups = []
104
+ (wrap(@tags) + groups.collect{ |g| wrap meta(g)[:tags] } + (wrap meta(example)[:tags])).flatten.compact.uniq.collect(&:to_s)
105
+ end
106
+
107
+ def extract_tickets example, groups = []
108
+ (wrap(@tickets) + groups.collect{ |g| wrap meta(g)[:tickets] } + (wrap meta(example)[:tickets])).flatten.compact.uniq.collect(&:to_s)
109
+ end
110
+
111
+ def extract_data example, groups = []
112
+ meta(example)[:data]
113
+ end
114
+
115
+ def wrap a
116
+ a.kind_of?(Array) ? a : [ a ]
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,85 @@
1
+
2
+ module ProbeDockRSpec
3
+
4
+ class TestRun
5
+ attr_reader :results, :project
6
+ attr_accessor :duration, :uid
7
+
8
+ def initialize project
9
+ @results = []
10
+ @project = project
11
+ end
12
+
13
+ def add_result example, groups = [], options = {}
14
+ if TestResult.extract_grouped(example, groups) and (existing_result = @results.find{ |r| r.grouped? && r.key == TestResult.extract_key(example, groups) })
15
+ existing_result.update options
16
+ else
17
+ @results << TestResult.new(@project, example, groups, options)
18
+ end
19
+ end
20
+
21
+ def results_without_key
22
+ @results.select{ |r| !r.key or r.key.to_s.strip.empty? }
23
+ end
24
+
25
+ def results_by_key
26
+ @results.inject({}) do |memo,r|
27
+ (memo[r.key] ||= []) << r unless !r.key or r.key.to_s.strip.empty?
28
+ memo
29
+ end
30
+ end
31
+
32
+ def to_h options = {}
33
+ validate!
34
+
35
+ {
36
+ 'projectId' => @project.api_id,
37
+ 'version' => @project.version,
38
+ 'duration' => @duration,
39
+ 'results' => @results.collect{ |r| r.to_h options }
40
+ }.tap do |h|
41
+ h['reports'] = [ { 'uid' => @uid } ] if @uid
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def validate!
48
+ # TODO: validate duration
49
+
50
+ raise PayloadError.new("Missing project") if !@project
51
+ @project.validate!
52
+
53
+ # FIXME: log info/warnings
54
+ #validate_no_results_without_key
55
+ #validate_no_duplicate_keys
56
+ end
57
+
58
+ def validate_no_duplicate_keys
59
+
60
+ results_with_duplicate_key = results_by_key.select{ |k,r| r.length >= 2 }
61
+ return if results_with_duplicate_key.none?
62
+
63
+ msg = "the following keys are used by multiple test results".tap do |s|
64
+ results_with_duplicate_key.each_pair do |key,results|
65
+ s << "\n - #{key}"
66
+ results.each{ |r| s << "\n - #{r.name}" }
67
+ end
68
+ end
69
+
70
+ raise PayloadError.new(msg)
71
+ end
72
+
73
+ def validate_no_results_without_key
74
+
75
+ keyless = results_without_key
76
+ return if keyless.empty?
77
+
78
+ msg = "the following test results are missing a key".tap do |s|
79
+ keyless.each{ |r| s << "\n - #{r.name}" }
80
+ end
81
+
82
+ raise PayloadError.new(msg)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,60 @@
1
+ require 'securerandom'
2
+
3
+ module ProbeDockRSpec
4
+
5
+ class UID
6
+ ENVIRONMENT_VARIABLE = 'PROBE_DOCK_TEST_REPORT_UID'
7
+
8
+ class Error < ProbeDockRSpec::Error; end
9
+
10
+ def initialize options = {}
11
+ @workspace = options[:workspace]
12
+ end
13
+
14
+ def load_uid
15
+ if env_var
16
+ return env_var
17
+ elsif @workspace
18
+ current_uid
19
+ end
20
+ end
21
+
22
+ def generate_uid_to_file
23
+ raise Error.new("No workspace specified; cannot save test run UID") if !@workspace
24
+ generate_uid.tap{ |uid| save_uid uid }
25
+ end
26
+
27
+ def generate_uid_to_env
28
+ raise Error.new("$PROBE_DOCK_TEST_REPORT_UID is already defined") if env_var
29
+ ENV[ENVIRONMENT_VARIABLE] = generate_uid
30
+ end
31
+
32
+ def clean_uid
33
+ ENV.delete ENVIRONMENT_VARIABLE
34
+ FileUtils.remove_entry_secure uid_file if @workspace and File.exists?(uid_file)
35
+ end
36
+
37
+ private
38
+
39
+ def save_uid uid
40
+ FileUtils.mkdir_p File.dirname(uid_file)
41
+ File.open(uid_file, 'w'){ |f| f.write uid }
42
+ end
43
+
44
+ def env_var
45
+ ENV[ENVIRONMENT_VARIABLE]
46
+ end
47
+
48
+ def current_uid
49
+ File.file?(uid_file) ? File.read(uid_file) : nil
50
+ end
51
+
52
+ def uid_file
53
+ @uid_file ||= File.join(@workspace, 'uid')
54
+ end
55
+
56
+ def generate_uid
57
+ "#{Time.now.utc.strftime '%Y%m%d%H%M%S'}-#{SecureRandom.uuid}"
58
+ end
59
+ end
60
+ end
metadata ADDED
@@ -0,0 +1,221 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: probe-dock-rspec
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Simon Oulevay (Alpha Hydrae)
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '3.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '3.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: oj
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '2.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '2.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: httparty
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: !binary |-
48
+ MC4xMw==
49
+ type: :runtime
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ~>
54
+ - !ruby/object:Gem::Version
55
+ version: !binary |-
56
+ MC4xMw==
57
+ - !ruby/object:Gem::Dependency
58
+ name: paint
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ~>
62
+ - !ruby/object:Gem::Version
63
+ version: '1.0'
64
+ type: :runtime
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ~>
69
+ - !ruby/object:Gem::Version
70
+ version: '1.0'
71
+ - !ruby/object:Gem::Dependency
72
+ name: rake
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '10.1'
78
+ type: :runtime
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ~>
83
+ - !ruby/object:Gem::Version
84
+ version: '10.1'
85
+ - !ruby/object:Gem::Dependency
86
+ name: jeweler
87
+ requirement: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ~>
90
+ - !ruby/object:Gem::Version
91
+ version: '2.0'
92
+ type: :development
93
+ prerelease: false
94
+ version_requirements: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ~>
97
+ - !ruby/object:Gem::Version
98
+ version: '2.0'
99
+ - !ruby/object:Gem::Dependency
100
+ name: rake-version
101
+ requirement: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ~>
104
+ - !ruby/object:Gem::Version
105
+ version: !binary |-
106
+ MC40
107
+ type: :development
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ~>
112
+ - !ruby/object:Gem::Version
113
+ version: !binary |-
114
+ MC40
115
+ - !ruby/object:Gem::Dependency
116
+ name: simplecov
117
+ requirement: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ~>
120
+ - !ruby/object:Gem::Version
121
+ version: '0.10'
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ~>
127
+ - !ruby/object:Gem::Version
128
+ version: '0.10'
129
+ - !ruby/object:Gem::Dependency
130
+ name: fakefs
131
+ requirement: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ~>
134
+ - !ruby/object:Gem::Version
135
+ version: '0.6'
136
+ type: :development
137
+ prerelease: false
138
+ version_requirements: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ~>
141
+ - !ruby/object:Gem::Version
142
+ version: '0.6'
143
+ - !ruby/object:Gem::Dependency
144
+ name: rspec-its
145
+ requirement: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ~>
148
+ - !ruby/object:Gem::Version
149
+ version: '1.2'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ~>
155
+ - !ruby/object:Gem::Version
156
+ version: '1.2'
157
+ - !ruby/object:Gem::Dependency
158
+ name: rspec-collection_matchers
159
+ requirement: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - ~>
162
+ - !ruby/object:Gem::Version
163
+ version: '1.1'
164
+ type: :development
165
+ prerelease: false
166
+ version_requirements: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - ~>
169
+ - !ruby/object:Gem::Version
170
+ version: '1.1'
171
+ description: RSpec client to publish test results to Probe Dock, a test tracking and
172
+ analysis server.
173
+ email: simon.oulevay@gmail.com
174
+ executables: []
175
+ extensions: []
176
+ extra_rdoc_files:
177
+ - LICENSE.txt
178
+ - README.md
179
+ files:
180
+ - Gemfile
181
+ - LICENSE.txt
182
+ - README.md
183
+ - VERSION
184
+ - lib/probe-dock-rspec.rb
185
+ - lib/probe_dock_rspec.rb
186
+ - lib/probe_dock_rspec/cache.rb
187
+ - lib/probe_dock_rspec/client.rb
188
+ - lib/probe_dock_rspec/config.rb
189
+ - lib/probe_dock_rspec/formatter.rb
190
+ - lib/probe_dock_rspec/project.rb
191
+ - lib/probe_dock_rspec/server.rb
192
+ - lib/probe_dock_rspec/tasks.rb
193
+ - lib/probe_dock_rspec/test_payload.rb
194
+ - lib/probe_dock_rspec/test_result.rb
195
+ - lib/probe_dock_rspec/test_run.rb
196
+ - lib/probe_dock_rspec/uid.rb
197
+ homepage: https://github.com/probe-dock/probe-dock-rspec
198
+ licenses:
199
+ - MIT
200
+ metadata: {}
201
+ post_install_message:
202
+ rdoc_options: []
203
+ require_paths:
204
+ - lib
205
+ required_ruby_version: !ruby/object:Gem::Requirement
206
+ requirements:
207
+ - - ! '>='
208
+ - !ruby/object:Gem::Version
209
+ version: '0'
210
+ required_rubygems_version: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ! '>='
213
+ - !ruby/object:Gem::Version
214
+ version: '0'
215
+ requirements: []
216
+ rubyforge_project:
217
+ rubygems_version: 2.4.5
218
+ signing_key:
219
+ specification_version: 4
220
+ summary: RSpec client to publish test results to Probe Dock.
221
+ test_files: []