probedock-rspec 0.5.3

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