probedock-rspec 0.5.3

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 8085834c08513ed1483ffe926f80cf4ebd331652
4
+ data.tar.gz: f31f332aea49f4e204f426f502684a3552a6d334
5
+ SHA512:
6
+ metadata.gz: 056f30e646719712bd6975d7486882c534a67755af7ae2f4d4184252359f32924b1edc3f262be7e7de5a3157a5b622b155fadb30dcba4d27c7ae8d9605be7ccc
7
+ data.tar.gz: 03bfe700e0990b6f8da525f5b191d937acc38794bb715e96a246075635a21e2e2cdde830fbfdfefd1cd7c9d393abf96c8e3a14910a2f28247a15c4b6bd3ca0be
data/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # Changelog
2
+
3
+ ## v0.4.0 - September 11, 2014
4
+
5
+ * Upgraded to RSpec 3.1.
6
+
7
+ ## v0.3.1 - December 13, 2013
8
+
9
+ * The `servers` key in the YAML configuration file is now a hash instead of an array.
10
+
11
+ ## v0.3.0 - October 25, 2013
12
+
13
+ * First open source version.
data/Gemfile ADDED
@@ -0,0 +1,22 @@
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', require: false
18
+ gem 'fakefs', '~> 0.6', require: 'fakefs/safe'
19
+ gem 'rspec-its', '~> 1.2'
20
+ gem 'rspec-collection_matchers', '~> 1.1'
21
+ gem 'coveralls', '~> 0.8', require: false
22
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2015 ProbeDock
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.
data/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # ProbeDock RSpec
2
+
3
+ **RSpec probe for [ProbeDock](https://github.com/probedock/probedock).**
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/probedock-rspec.svg)](http://badge.fury.io/rb/probedock-rspec)
6
+ [![Dependency Status](https://gemnasium.com/probedock/probedock-rspec.svg)](https://gemnasium.com/probedock/probedock-rspec)
7
+ [![Build Status](https://secure.travis-ci.org/probedock/probedock-rspec.svg)](http://travis-ci.org/probedock/probedock-rspec)
8
+ [![Coverage Status](https://coveralls.io/repos/probedock/probedock-rspec/badge.svg)](https://coveralls.io/r/probedock/probedock-rspec?branch=master)
9
+ [![License](https://img.shields.io/github/license/probedock/probedock-rspec.svg)](LICENSE.txt)
10
+
11
+ ## Requirements
12
+
13
+ * RSpec 3+
14
+
15
+ ## Installation
16
+
17
+ In your Gemfile:
18
+
19
+ ```rb
20
+ gem 'probedock-rspec', '~> 0.4.1'
21
+ ```
22
+
23
+ Manually:
24
+
25
+ gem install probedock-rspec
26
+
27
+ ## Usage
28
+
29
+ If you haven't done it already, follow the [setup procedure](#setup) below.
30
+
31
+ To track a test with a ProbeDock test key, use RSpec metadata:
32
+
33
+ ```rb
34
+ it "should work", probe_dock: { key: 'abcdefghijkl' } do
35
+ expect(true).to be(true)
36
+ end
37
+
38
+ it(nil, probe_dock: { key: 'bcdefghijklm' }){ should validate_presence_of(:name) }
39
+ ```
40
+
41
+ <a name="setup"></a>
42
+ ## Setup
43
+
44
+ You must first set up the configuration file(s) for the project.
45
+ This procedure is described here:
46
+
47
+ * [Probe Setup Procedure](https://github.com/probedock/probedock-clients#setup-procedure)
48
+
49
+ You must then enable the client in your spec helper file (e.g. `spec/spec_helper.rb`).
50
+
51
+ ```yml
52
+ ProbeDockRSpec.configure do |config|
53
+
54
+ # Optional category to add to all the tests sent with this client.
55
+ config.project.category = 'RSpec'
56
+ end
57
+ ```
58
+
59
+ The next time you run your test suite, the RSpec probe will send the results to your ProbeDock server.
60
+
61
+ ## Contributing
62
+
63
+ * [Fork](https://help.github.com/articles/fork-a-repo)
64
+ * Create a topic branch - `git checkout -b my_feature`
65
+ * Push to your branch - `git push origin my_feature`
66
+ * Create a [pull request](http://help.github.com/pull-requests/) from your branch
67
+
68
+ Please add a [changelog](CHANGELOG.md) entry with your name for new features and bug fixes.
69
+
70
+ ## License
71
+
72
+ ProbeDock RSpec is licensed under the [MIT License](http://opensource.org/licenses/MIT).
73
+ See [LICENSE.txt](LICENSE.txt) for the full license.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.5.3
@@ -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 = {}
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["ProbeDock - 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["ProbeDock - #{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["ProbeDock - #{e.message}", :yellow]
71
+ false
72
+ end
73
+ end
74
+
75
+ def print_payload payload
76
+ puts Paint['ProbeDock - 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["ProbeDock - Sending payload to #{@server.api_url}...", :magenta]
100
+
101
+ begin
102
+ if @local_mode
103
+ puts Paint['ProbeDock - LOCAL MODE: not actually sending payload.', :yellow]
104
+ else
105
+ @server.upload payload
106
+ end
107
+ puts Paint["ProbeDock - Done!", :green]
108
+ true
109
+ rescue Server::Error => e
110
+ warn Paint["ProbeDock - Upload failed!", :red, :bold]
111
+ warn Paint["ProbeDock - #{e.message}", :red, :bold]
112
+ if e.response
113
+ warn Paint["ProbeDock - 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,198 @@
1
+ require 'yaml'
2
+
3
+ # Utilities to send test results to ProbeDock.
4
+ module ProbeDockRSpec
5
+
6
+ def self.config
7
+ @config ||= Config.new.tap(&:load!)
8
+ end
9
+
10
+ def self.configure options = {}
11
+
12
+ yield config if block_given?
13
+
14
+ config.check!
15
+ config.load_warnings.each{ |w| warn Paint["ProbeDock - #{w}", :yellow] }
16
+
17
+ config.setup! if options[:setup] != false
18
+
19
+ config
20
+ end
21
+
22
+ class Config
23
+ # TODO: add silent/verbose option(s)
24
+ class Error < ProbeDockRSpec::Error; end
25
+ attr_writer :publish, :local_mode, :cache_payload, :print_payload, :save_payload
26
+ attr_reader :project, :server, :workspace, :load_warnings
27
+
28
+ def initialize
29
+ @servers = []
30
+ @server = Server.new
31
+ @project = Project.new
32
+ @publish, @local_mode, @cache_payload, @print_payload, @save_payload = false, false, false, false, false
33
+ @load_warnings = []
34
+ end
35
+
36
+ def workspace= dir
37
+ @workspace = dir ? File.expand_path(dir) : nil
38
+ end
39
+
40
+ def servers
41
+ @servers.dup
42
+ end
43
+
44
+ # Plugs ProbeDock utilities into RSpec.
45
+ def setup!
46
+ ::RSpec.configure do |c|
47
+ c.add_formatter Formatter
48
+ end
49
+ end
50
+
51
+ %w(publish local_mode cache_payload print_payload save_payload).each do |name|
52
+ define_method("#{name}?"){ instance_variable_get("@#{name}") }
53
+ end
54
+
55
+ def client_options
56
+ {
57
+ publish: @publish,
58
+ local_mode: @local_mode,
59
+ workspace: @workspace,
60
+ cache_payload: @cache_payload,
61
+ print_payload: @print_payload,
62
+ save_payload: @save_payload
63
+ }.select{ |k,v| !v.nil? }
64
+ end
65
+
66
+ def load!
67
+
68
+ @server.clear
69
+ @servers.clear
70
+
71
+ @load_warnings = []
72
+ return unless config = load_config_files
73
+
74
+ @publish = parse_env_flag :publish, !!config[:publish]
75
+ @server_name = parse_env_option(:server) || config[:server]
76
+ @local_mode = parse_env_flag(:local) || !!config[:local]
77
+
78
+ self.workspace = parse_env_option(:workspace) || config[:workspace]
79
+ @cache_payload = parse_env_flag :cache_payload, !!config[:payload][:cache]
80
+ @print_payload = parse_env_flag :print_payload, !!config[:payload][:print]
81
+ @save_payload = parse_env_flag :save_payload, !!config[:payload][:save]
82
+
83
+ @servers, server = build_servers config
84
+
85
+ if server
86
+ @server = server
87
+ else
88
+ @server.name = @server_name
89
+ end
90
+
91
+ {
92
+ api_url: parse_env_option(:server_api_url),
93
+ api_token: parse_env_option(:server_api_token),
94
+ project_api_id: parse_env_option(:server_project_api_id)
95
+ }.each{ |k,v| @server.send "#{k}=", v if v }
96
+
97
+ project_options = config[:project]
98
+ project_options.merge! api_id: @server.project_api_id if @server and @server.project_api_id
99
+ @project.update project_options
100
+
101
+ self
102
+ end
103
+
104
+ def check!
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
+ end
112
+
113
+ if @servers.empty?
114
+ @load_warnings << "No server defined"
115
+ elsif !@server_name && !@server.name
116
+ @load_warnings << "No server name given"
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ def build_servers config
123
+
124
+ default_server_options = { project_api_id: config[:project][:api_id] }
125
+ servers = config[:servers].inject({}) do |memo,(name, options)|
126
+ memo[name] = Server.new default_server_options.merge(options).merge(name: name)
127
+ memo
128
+ end
129
+
130
+ [ servers.values, servers[@server_name.to_s.strip] ]
131
+ end
132
+
133
+ def load_config_files
134
+
135
+ configs = [ home_config_file, working_config_file ]
136
+ actual_configs = configs.select{ |f| File.exists? f }
137
+ return false if actual_configs.empty?
138
+
139
+ actual_configs.collect!{ |f| YAML.load_file f }
140
+
141
+ actual_configs.inject({ servers: {} }) do |memo,yml|
142
+ memo.merge! parse_general_options(yml)
143
+
144
+ if yml['servers'].kind_of? Hash
145
+ yml['servers'].each_pair do |k,v|
146
+ if v.kind_of? Hash
147
+ memo[:servers][k] = (memo[:servers][k] || {}).merge(parse_server_options(v))
148
+ end
149
+ end
150
+ end
151
+
152
+ memo[:payload] = (memo[:payload] || {}).merge parse_payload_options(yml['payload'])
153
+ memo[:project] = (memo[:project] || {}).merge parse_project_options(yml['project'])
154
+
155
+ memo
156
+ end
157
+ end
158
+
159
+ def home_config_file
160
+ File.join File.expand_path('~'), '.probedock', 'config.yml'
161
+ end
162
+
163
+ def working_config_file
164
+ File.expand_path ENV['PROBE_DOCK_CONFIG'] || 'probedock.yml', Dir.pwd
165
+ end
166
+
167
+ def parse_env_flag name, default = false
168
+ val = parse_env_option name
169
+ val ? !!val.to_s.strip.match(/\A(1|y|yes|t|true)\Z/i) : default
170
+ end
171
+
172
+ def parse_env_option name
173
+ var = "PROBE_DOCK_#{name.upcase}"
174
+ ENV.key?(var) ? ENV[var] : nil
175
+ end
176
+
177
+ def parse_general_options h
178
+ parse_options h, %w(publish server local workspace)
179
+ end
180
+
181
+ def parse_server_options h
182
+ parse_options h, %w(name apiUrl apiToken apiVersion projectApiId)
183
+ end
184
+
185
+ def parse_payload_options h
186
+ parse_options h, %w(save cache print)
187
+ end
188
+
189
+ def parse_project_options h
190
+ parse_options h, %w(version apiId category tags tickets)
191
+ end
192
+
193
+ def parse_options h, keys
194
+ return {} unless h.kind_of? Hash
195
+ keys.inject({}){ |memo,k| memo[k.gsub(/(.)([A-Z])/, '\1_\2').downcase.to_sym] = h[k] if h.key?(k); memo }
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,82 @@
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
+ @start_time = Time.now
23
+ end
24
+
25
+ def example_group_started group_notification
26
+ @groups << group_notification.group
27
+ end
28
+
29
+ def example_group_finished group_notification
30
+ @groups.pop
31
+ end
32
+
33
+ def example_started example_notification
34
+ @current_time = Time.now
35
+ end
36
+
37
+ def example_passed example_notification
38
+ add_result example_notification, true
39
+ end
40
+
41
+ def example_failed example_notification
42
+ add_result example_notification, false
43
+ end
44
+
45
+ def stop notification
46
+ end_time = Time.now
47
+ @test_run.duration = ((end_time - @start_time) * 1000).round
48
+ end
49
+
50
+ def close notification
51
+ @client.process @test_run
52
+ end
53
+
54
+ private
55
+
56
+ def add_result example_notification, successful
57
+
58
+ options = {
59
+ passed: successful,
60
+ duration: ((Time.now - @current_time) * 1000).round
61
+ }
62
+
63
+ options[:message] = failure_message example_notification unless successful
64
+
65
+ @test_run.add_result example_notification.example, @groups, options
66
+ end
67
+
68
+ def failure_message example_notification
69
+ String.new.tap do |m|
70
+ m << example_notification.description
71
+ m << "\n"
72
+ m << example_notification.message_lines.collect{ |l| " #{l}" }.join("\n")
73
+ m << "\n"
74
+ m << example_notification.formatted_backtrace.collect{ |l| " # #{l}" }.join("\n")
75
+ end
76
+ end
77
+
78
+ def full_example_name example
79
+ (@groups.collect{ |g| g.description.strip } << example.description.strip).join ' '
80
+ end
81
+ end
82
+ 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,59 @@
1
+ require 'oj'
2
+ require 'httparty'
3
+
4
+ module ProbeDockRSpec
5
+
6
+ class Server
7
+ attr_accessor :name, :api_url, :api_token, :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 if options[:name]
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
+ @project_api_id = options[:project_api_id].to_s if options[:project_api_id]
23
+ end
24
+
25
+ def clear
26
+ @name = nil
27
+ @api_url = nil
28
+ @api_token = nil
29
+ @project_api_id = nil
30
+ end
31
+
32
+ def upload payload
33
+ validate!
34
+
35
+ body = Oj.dump payload, mode: :strict
36
+ res = HTTParty.post payload_uri, body: body, headers: payload_headers
37
+
38
+ if res.code != 202
39
+ raise Error.new("Expected HTTP 202 Accepted when submitting payload, got #{res.code}", res)
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def validate!
46
+ required = { "name" => @name, "apiUrl" => @api_url, "apiToken" => @api_token }
47
+ missing = required.inject([]){ |memo,(k,v)| v.to_s.strip.length <= 0 ? memo << k : memo }
48
+ raise Error.new("Server #{@name} is missing the following options: #{missing.join ', '}") if missing.any?
49
+ end
50
+
51
+ def payload_headers
52
+ { 'Authorization' => "Bearer #{@api_token}", 'Content-Type' => 'application/vnd.probedock.payload.v1+json' }
53
+ end
54
+
55
+ def payload_uri
56
+ "#{@api_url}/publish"
57
+ end
58
+ end
59
+ 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 'probedock' do
13
+
14
+ desc "Generate a test run UID to group test results in ProbeDock (stored in an environment variable)"
15
+ task :uid do
16
+ trace do
17
+ uid = uid_manager.generate_uid_to_env
18
+ puts Paint["ProbeDock - 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 ProbeDock (stored in a file)"
25
+ task :file do
26
+ trace do
27
+ uid = uid_manager.generate_uid_to_file
28
+ puts Paint["ProbeDock - 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["ProbeDock - 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["ProbeDock - #{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
@@ -0,0 +1,11 @@
1
+ # encoding: UTF-8
2
+ require 'rspec'
3
+
4
+ module ProbeDockRSpec
5
+ VERSION = '0.5.3'
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 @@
1
+ require 'probe_dock_rspec'
metadata ADDED
@@ -0,0 +1,233 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: probedock-rspec
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.3
5
+ platform: ruby
6
+ authors:
7
+ - Simon Oulevay (Alpha Hydrae)
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-27 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: '0.13'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.13'
55
+ - !ruby/object:Gem::Dependency
56
+ name: paint
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rake
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '10.1'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '10.1'
83
+ - !ruby/object:Gem::Dependency
84
+ name: jeweler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '2.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '2.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rake-version
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.4'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.4'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.10'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.10'
125
+ - !ruby/object:Gem::Dependency
126
+ name: fakefs
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '0.6'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '0.6'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rspec-its
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '1.2'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '1.2'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rspec-collection_matchers
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '1.1'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '1.1'
167
+ - !ruby/object:Gem::Dependency
168
+ name: coveralls
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '0.8'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '0.8'
181
+ description: RSpec client to publish test results to ProbeDock, a test tracking and
182
+ analysis server.
183
+ email: simon.oulevay@gmail.com
184
+ executables: []
185
+ extensions: []
186
+ extra_rdoc_files:
187
+ - CHANGELOG.md
188
+ - LICENSE.txt
189
+ - README.md
190
+ files:
191
+ - CHANGELOG.md
192
+ - Gemfile
193
+ - LICENSE.txt
194
+ - README.md
195
+ - VERSION
196
+ - lib/probe_dock_rspec.rb
197
+ - lib/probe_dock_rspec/cache.rb
198
+ - lib/probe_dock_rspec/client.rb
199
+ - lib/probe_dock_rspec/config.rb
200
+ - lib/probe_dock_rspec/formatter.rb
201
+ - lib/probe_dock_rspec/project.rb
202
+ - lib/probe_dock_rspec/server.rb
203
+ - lib/probe_dock_rspec/tasks.rb
204
+ - lib/probe_dock_rspec/test_payload.rb
205
+ - lib/probe_dock_rspec/test_result.rb
206
+ - lib/probe_dock_rspec/test_run.rb
207
+ - lib/probe_dock_rspec/uid.rb
208
+ - lib/probedock-rspec.rb
209
+ homepage: https://github.com/probedock/probedock-rspec
210
+ licenses:
211
+ - MIT
212
+ metadata: {}
213
+ post_install_message:
214
+ rdoc_options: []
215
+ require_paths:
216
+ - lib
217
+ required_ruby_version: !ruby/object:Gem::Requirement
218
+ requirements:
219
+ - - ">="
220
+ - !ruby/object:Gem::Version
221
+ version: '0'
222
+ required_rubygems_version: !ruby/object:Gem::Requirement
223
+ requirements:
224
+ - - ">="
225
+ - !ruby/object:Gem::Version
226
+ version: '0'
227
+ requirements: []
228
+ rubyforge_project:
229
+ rubygems_version: 2.4.6
230
+ signing_key:
231
+ specification_version: 4
232
+ summary: RSpec client to publish test results to ProbeDock.
233
+ test_files: []