rox-client-ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ Mzg1YWZlOWM0MGUyZDA2MGJlZDM2MzdmODBiZWJmM2U0OTFmZTE2Nw==
5
+ data.tar.gz: !binary |-
6
+ NjMxZDRmYmU3NGY2Yjc3M2Q1OTk5Yjk1NmJhY2U4ZGIwYzhhZmJhMA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ NGExMDQ3MmJhMTdmMDI3NzQ5NDhjMjcwNzkxNTMxNjE2ZjhhNmNlYzI4Yjdm
10
+ NDFmNTY0NzZmM2IyMzE5NzNmOWYzYzdjZDVkODE2OTFhNDMwNjJkMzkzMGI5
11
+ OTU2MTUwZDE1YjNkMDRhMThkMTVmOGVhNjMzNmI0YmEyMmRiMzM=
12
+ data.tar.gz: !binary |-
13
+ NjViNDZmM2E2Y2Y3Y2RjNGJiZDUxZTc3ZWIwOGJiNmI5YzgxYTAxZDYwYjEw
14
+ YmY1MTFmZTI1Njc4OTYxYmQ2MzNmNGZhZjNlZGVlMjM0ODRmOWJkZDQxMDA1
15
+ NTVkZDFkMDkxODdhZTc4OGIyM2ViMjg4MTAwZGU3OWExNDQ4ZTQ=
data/Gemfile ADDED
@@ -0,0 +1,20 @@
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 'oj', '~> 2.1'
7
+ gem 'httparty', '~> 0.13'
8
+ gem 'paint', '~> 0.8'
9
+ gem 'rake', '~> 10.1'
10
+
11
+ # Add dependencies to develop your gem here.
12
+ # Include everything needed to run rake, tests, features, etc.
13
+ group :development do
14
+ gem 'rspec', '~> 2.14'
15
+ gem 'bundler', '~> 1.0'
16
+ gem 'jeweler', '~> 2.0'
17
+ gem 'rake-version', '~> 0.4'
18
+ gem 'simplecov', '~> 0.8'
19
+ gem 'fakefs', '~> 0.5', require: 'fakefs/safe'
20
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2014 Lotaris SA
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # Ruby ROX Client
2
+
3
+ **Utilities for [ROX Center](https://github.com/lotaris/rox-center) ruby clients.**
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/rox-client-ruby.png)](http://badge.fury.io/rb/rox-client-ruby)
6
+
7
+ ## Requirements
8
+
9
+ * Ruby 1.9.3 or higher
10
+
11
+ ## Installation
12
+
13
+ In your Gemfile:
14
+
15
+ ```rb
16
+ gem 'rox-client-ruby', '~> 0.1.0'
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ This project is a library for ROX Center clients in Ruby.
22
+ It must be integrated into a testing framework, e.g. [rox-client-rspec](https://github.com/lotaris/rox-client-rspec) for [RSpec](https://relishapp.com/rspec).
23
+
24
+ The [Setup](#setup) procedure is common to all clients and describes how to use ROX configuration files.
25
+
26
+ ## Setup
27
+
28
+ ROX clients use [YAML](http://yaml.org) files for configuration.
29
+ To use a ROX Client, you need two configuration files.
30
+
31
+ In your home folder, you must create the `~/.rox/config.yml` configuration file.
32
+
33
+ ```yml
34
+ # List of ROX Center servers you can submit test results to.
35
+ servers:
36
+ rox.example.com: # A custom name for your ROX Center server.
37
+ # You will use this in the client configuration file.
38
+ # We recommend using the domain name where you deployed it.
39
+
40
+ apiUrl: https://rox.example.com/api # The URL of your ROX Center server's API.
41
+ # This is the domain where you deployed it with /api.
42
+
43
+ apiKeyId: 39fuc7x85lsoy9c0ek2d # Your user credentials on this server.
44
+ apiKeySecret: mwpqvvmagzoegxnqptxdaxkxonjmvrlctwcrfmowibqcpnsdqd
45
+
46
+ # If true, test results will be uploaded to ROX Center.
47
+ # Set to false to temporarily disable publishing.
48
+ # You can change this at runtime from the command line by setting the
49
+ # ROX_PUBLISH environment variable to 0 (false) or 1 (true).
50
+ publish: true
51
+ ```
52
+
53
+ In the project directory where you run your tests, you must add the `rox.yml` client configuration file:
54
+
55
+ ```yml
56
+ # Configuration specific to your project.
57
+ project:
58
+ apiId: 154sic93pxs0 # The API key of your project in the ROX Center server.
59
+ version: 1.2.3
60
+
61
+ # Where the client should store its temporary files.
62
+ # The client will work without it but it is required for some advanced features.
63
+ workspace: tmp/rox
64
+
65
+ # Client advanced features.
66
+ payload:
67
+
68
+ # Saves a copy of the test payload sent to the ROX Center server for debugging.
69
+ # The file will be saved in rspec/servers/<SERVER_NAME>/payload.json.
70
+ save: false
71
+
72
+ # If you track a large number of tests (more than a thousand), enabling this
73
+ # will reduce the size of the test payloads sent to ROX Center server by caching
74
+ # test information that doesn't change often such as the name.
75
+ cache: false
76
+
77
+ # Prints a copy of the test payload sent to the ROX Center server for debugging.
78
+ # Temporarily enable at runtime by setting the ROX_PRINT_PAYLOAD environment variable to 1.
79
+ print: false
80
+
81
+ # The name of the ROX Center server to upload test results to.
82
+ # This name must be one of the server names in the ~/.rox/config.yml file.
83
+ # You can change this at runtime from the command line by setting the
84
+ # ROX_SERVER environment variable.
85
+ server: rox.example.com
86
+ ```
87
+
88
+ Finally, if using a standalone ROX client like [rox-client-rspec](https://github.com/lotaris/rox-client-rspec),
89
+ you must plug ROX into the testing framework.
90
+ This procedure is documented by each client.
91
+
92
+ ## Contributing
93
+
94
+ * [Fork](https://help.github.com/articles/fork-a-repo)
95
+ * Create a topic branch - `git checkout -b my_feature`
96
+ * Push to your branch - `git push origin my_feature`
97
+ * Create a [pull request](http://help.github.com/pull-requests/) from your branch
98
+
99
+ Please add a [changelog](CHANGELOG.md) entry with your name for new features and bug fixes.
100
+
101
+ ## License
102
+
103
+ The Ruby ROX Client is licensed under the [MIT License](http://opensource.org/licenses/MIT).
104
+ See [LICENSE.txt](LICENSE.txt) for the full license.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,8 @@
1
+ module RoxClient
2
+ VERSION = '0.1.0'
3
+
4
+ class Error < StandardError; end
5
+ class PayloadError < Error; end
6
+ end
7
+
8
+ Dir[File.join File.dirname(__FILE__), File.basename(__FILE__, '.*'), '*.rb'].each{ |lib| require lib }
@@ -0,0 +1,61 @@
1
+ module RoxClient
2
+
3
+ class Cache
4
+
5
+ class Error < RoxClient::Error; end
6
+
7
+ def initialize options = {}
8
+ @tests = {}
9
+ @workspace, @server_name, @project_api_id = options[:workspace], options[:server_name], options[:project_api_id]
10
+ @client_name = options[:client_name]
11
+ end
12
+
13
+ def save test_run
14
+ validate!
15
+
16
+ @tests = { @project_api_id => @tests[@project_api_id] || {} }
17
+ test_run.results.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
+ @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, "client name" => @client_name, "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, @client_name, 'servers', @server_name, 'cache.json')
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,123 @@
1
+ require 'paint'
2
+
3
+ module RoxClient
4
+
5
+ class Client
6
+
7
+ def initialize server, options = {}
8
+
9
+ @server = server
10
+ @publish, @local_mode, @workspace = options[:publish], options[:local_mode], options[:workspace]
11
+ @cache_payload, @print_payload, @save_payload = options[:cache_payload], options[:print_payload], options[:save_payload]
12
+ @client_name = options[:client_name]
13
+
14
+ cache_options = { workspace: @workspace }
15
+ cache_options.merge! server_name: @server.name, project_api_id: @server.project_api_id if @server
16
+ cache_options.merge! client_name: @client_name if @client_name
17
+ @cache = Cache.new cache_options
18
+
19
+ @uid = UID.new workspace: @workspace
20
+ end
21
+
22
+ def process test_run
23
+
24
+ puts
25
+ return fail "No server to publish results to" unless @server
26
+ return fail "Client name must be specified" unless @client_name
27
+
28
+ test_run.uid = @uid.load_uid
29
+
30
+ payload_options = @server.payload_options
31
+
32
+ cache_enabled = @cache_payload && load_cache
33
+ payload_options[:cache] = @cache if cache_enabled
34
+
35
+ return false unless payload = build_payload(test_run, payload_options)
36
+
37
+ published = if !@publish
38
+ puts Paint["ROX - Publishing disabled", :yellow]
39
+ false
40
+ elsif publish_payload payload
41
+ @cache.save test_run if cache_enabled
42
+ true
43
+ else
44
+ false
45
+ end
46
+
47
+ save_payload payload if @save_payload
48
+ print_payload payload if @print_payload
49
+
50
+ published
51
+ end
52
+
53
+ private
54
+
55
+ def build_payload test_run, options = {}
56
+ begin
57
+ TestPayload.new(test_run).to_h options
58
+ rescue PayloadError => e
59
+ fail e.message
60
+ end
61
+ end
62
+
63
+ def fail msg, type = :error
64
+ styles = { warning: [ :yellow ], error: [ :bold, :red ] }
65
+ warn Paint["ROX - #{msg}", *styles[type]]
66
+ false
67
+ end
68
+
69
+ def load_cache
70
+ begin
71
+ @cache.load
72
+ rescue Cache::Error => e
73
+ warn Paint["ROX - #{e.message}", :yellow]
74
+ false
75
+ end
76
+ end
77
+
78
+ def print_payload payload
79
+ puts Paint['ROX - Printing payload...', :yellow]
80
+ begin
81
+ puts JSON.pretty_generate(payload)
82
+ rescue
83
+ puts payload.inspect
84
+ end
85
+ end
86
+
87
+ def save_payload payload
88
+
89
+ missing = { "workspace" => @workspace, "server" => @server }.inject([]){ |memo,(k,v)| !v ? memo << k : memo }
90
+ return fail "Cannot save payload without a #{missing.join ' and '}" if missing.any?
91
+
92
+ FileUtils.mkdir_p File.dirname(payload_file)
93
+ File.open(payload_file, 'w'){ |f| f.write Oj.dump(payload, mode: :strict) }
94
+ end
95
+
96
+ def payload_file
97
+ @payload_file ||= File.join(@workspace, @client_name, 'servers', @server.name, 'payload.json')
98
+ end
99
+
100
+ def publish_payload payload
101
+
102
+ puts Paint["ROX - Sending payload to #{@server.api_url}...", :magenta]
103
+
104
+ begin
105
+ if @local_mode
106
+ puts Paint['ROX - LOCAL MODE: not actually sending payload.', :yellow]
107
+ else
108
+ @server.upload payload
109
+ end
110
+ puts Paint["ROX - Done!", :green]
111
+ true
112
+ rescue Server::Error => e
113
+ warn Paint["ROX - Upload failed!", :red, :bold]
114
+ warn Paint["ROX - #{e.message}", :red, :bold]
115
+ if e.response
116
+ warn Paint["ROX - Dumping response body...", :red, :bold]
117
+ warn e.response.body
118
+ end
119
+ false
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,174 @@
1
+ require 'yaml'
2
+ require 'paint'
3
+
4
+ module RoxClient
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
+ @integration.each{ |block| block.call config }
13
+ config.load_warnings.each{ |w| warn Paint["ROX - #{w}", :yellow] }
14
+ config
15
+ end
16
+
17
+ @integration = []
18
+
19
+ def self.integrate &block
20
+ @integration << block
21
+ end
22
+
23
+ class Config
24
+ # TODO: add silent/verbose option(s)
25
+ class Error < RoxClient::Error; end
26
+ attr_writer :publish, :local_mode, :cache_payload, :print_payload, :save_payload, :client_name
27
+ attr_reader :project, :server, :workspace, :load_warnings
28
+
29
+ def initialize
30
+ @servers = []
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
+ %w(publish local_mode cache_payload print_payload save_payload).each do |name|
45
+ define_method("#{name}?"){ instance_variable_get("@#{name}") }
46
+ end
47
+
48
+ def client_options
49
+ {
50
+ publish: @publish,
51
+ local_mode: @local_mode,
52
+ workspace: @workspace,
53
+ cache_payload: @cache_payload,
54
+ print_payload: @print_payload,
55
+ save_payload: @save_payload,
56
+ client_name: @client_name
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('~'), '.rox', 'config.yml'
136
+ end
137
+
138
+ def working_config_file
139
+ File.expand_path ENV['ROX_CONFIG'] || 'rox.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 = "ROX_#{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 apiKeyId apiKeySecret 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
+ # TODO: remove project name once API v0 is dead
166
+ parse_options h, %w(name version apiId category tags tickets)
167
+ end
168
+
169
+ def parse_options h, keys
170
+ return {} unless h.kind_of? Hash
171
+ keys.inject({}){ |memo,k| memo[k.gsub(/(.)([A-Z])/, '\1_\2').downcase.to_sym] = h[k] if h.key?(k); memo }
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,31 @@
1
+ module RoxClient
2
+
3
+ class Project
4
+ # TODO: remove project name once API v0 is dead
5
+ attr_accessor :name, :version, :api_id, :category, :tags, :tickets
6
+
7
+ def initialize options = {}
8
+ update options
9
+ end
10
+
11
+ def update options = {}
12
+ %w(name version api_id category).each do |k|
13
+ instance_variable_set "@#{k}", options[k.to_sym] ? options[k.to_sym].to_s : nil if options.key? k.to_sym
14
+ end
15
+ @tags = wrap(options[:tags]).compact if options.key? :tags
16
+ @tickets = wrap(options[:tickets]).compact if options.key? :tickets
17
+ end
18
+
19
+ def validate!
20
+ required = { "version" => @version, "API identifier" => @api_id }
21
+ missing = required.inject([]){ |memo,(k,v)| v.to_s.strip.length <= 0 ? memo << k : memo }
22
+ raise PayloadError.new("Missing project options: #{missing.join ', '}") if missing.any?
23
+ end
24
+
25
+ private
26
+
27
+ def wrap a
28
+ a.kind_of?(Array) ? a : [ a ]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,97 @@
1
+ require 'oj'
2
+ require 'httparty'
3
+
4
+ module RoxClient
5
+
6
+ class Server
7
+ attr_reader :name, :api_url, :api_key_id, :api_key_secret, :api_version, :project_api_id
8
+
9
+ class Error < RoxClient::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_key_id = options[:api_key_id].to_s if options[:api_key_id]
22
+ @api_key_secret = options[:api_key_secret].to_s if options[:api_key_secret]
23
+ @api_version = options[:api_version] || 1
24
+ @project_api_id = options[:project_api_id].to_s if options[:project_api_id]
25
+ end
26
+
27
+ def payload_options
28
+ { version: @api_version }
29
+ end
30
+
31
+ def upload payload
32
+ validate!
33
+
34
+ uri = payload_uri
35
+ body = Oj.dump payload, mode: :strict
36
+
37
+ res = case @api_version
38
+ when 0
39
+ HTTParty.post uri, body: body
40
+ else
41
+ HTTParty.post uri, body: body, headers: payload_headers.merge(authentication_headers)
42
+ end
43
+
44
+ if res.code != 202
45
+ raise Error.new("Expected HTTP 202 Accepted when submitting payload, got #{res.code}", res)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def validate!
52
+
53
+ raise Error.new("Server #{@name} requires $ROX_RUNNER_KEY to be set (API v0)") if @api_version == 0 and !ENV['ROX_RUNNER_KEY']
54
+
55
+ required = { "apiUrl" => @api_url }
56
+ required.merge!({ "apiKeyId" => @api_key_id, "apiKeySecret" => @api_key_secret, "projectApiId" => @project_api_id }) if @api_version >= 1
57
+ missing = required.inject([]){ |memo,(k,v)| v.to_s.strip.length <= 0 ? memo << k : memo }
58
+ raise Error.new("Server #{@name} is missing the following options: #{missing.join ', '}") if missing.any?
59
+ end
60
+
61
+ def payload_headers
62
+ { 'Content-Type' => 'application/vnd.lotaris.rox.payload.v1+json' }
63
+ end
64
+
65
+ def payload_uri
66
+ case @api_version
67
+
68
+ when 0
69
+ "#{@api_url}/v1/payload"
70
+
71
+ else
72
+
73
+ # get api root
74
+ res = HTTParty.get @api_url, headers: authentication_headers
75
+ if res.code != 200
76
+ raise Error.new("Expected HTTP 200 OK status code for API root, got #{res.code}", res)
77
+ elsif res.content_type != 'application/hal+json'
78
+ raise Error.new("Expected API root in the application/hal+json content type, got #{res.content_type}", res)
79
+ end
80
+
81
+ body = Oj.load res.body, mode: :strict
82
+
83
+ links = body['_links'] || {}
84
+ if !links.key?('v1:test-payloads')
85
+ raise Error.new("Expected API root to have a v1:test-payloads link", res)
86
+ end
87
+
88
+ # extract payload uri
89
+ links['v1:test-payloads']['href']
90
+ end
91
+ end
92
+
93
+ def authentication_headers
94
+ { 'Authorization' => %|RoxApiKey id="#{@api_key_id}" secret="#{@api_key_secret}"| }
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,63 @@
1
+ require 'fileutils'
2
+ require 'rake/tasklib'
3
+ require 'paint'
4
+
5
+ module RoxClient
6
+
7
+ class Tasks < ::Rake::TaskLib
8
+
9
+ def initialize
10
+
11
+ namespace :spec do
12
+
13
+ namespace :rox do
14
+
15
+ desc "Generate a test run UID to group test results in ROX Center (stored in an environment variable)"
16
+ task :uid do
17
+ trace do
18
+ uid = uid_manager.generate_uid_to_env
19
+ puts Paint["ROX - Generated UID for test run: #{uid}", :cyan]
20
+ end
21
+ end
22
+
23
+ namespace :uid do
24
+
25
+ desc "Generate a test run UID to group test results in ROX Center (stored in a file)"
26
+ task :file do
27
+ trace do
28
+ uid = uid_manager.generate_uid_to_file
29
+ puts Paint["ROX - Generated UID for test run: #{uid}", :cyan]
30
+ end
31
+ end
32
+
33
+ desc "Clean the test run UID (file and environment variable)"
34
+ task :clean do
35
+ trace do
36
+ uid_manager.clean_uid
37
+ puts Paint["ROX - Cleaned test run UID", :cyan]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def trace &block
48
+ if Rake.application.options.trace
49
+ block.call
50
+ else
51
+ begin
52
+ block.call
53
+ rescue UID::Error => e
54
+ warn Paint["ROX - #{e.message}", :red]
55
+ end
56
+ end
57
+ end
58
+
59
+ def uid_manager
60
+ UID.new RoxClient.config.client_options
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,19 @@
1
+ require 'fileutils'
2
+ require 'digest/sha2'
3
+
4
+ module RoxClient
5
+
6
+ class TestPayload
7
+
8
+ class Error < RoxClient::Error; end
9
+
10
+ def initialize run
11
+ @run = run
12
+ end
13
+
14
+ def to_h options = {}
15
+ # version 1 payload consists of one test run
16
+ @run.to_h options
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,61 @@
1
+ module RoxClient
2
+
3
+ class TestResult
4
+ attr_reader :key, :name, :category, :tags, :tickets, :duration, :message
5
+
6
+ def initialize project, options = {}
7
+
8
+ @key = options[:key]
9
+ @name = options[:name]
10
+
11
+ @category = project.category || options[:category]
12
+ @tags = (wrap(project.tags) + wrap(options[:tags])).compact.collect(&:to_s).uniq
13
+ @tickets = (wrap(project.tickets) + wrap(options[:tickets])).compact.collect(&:to_s).uniq
14
+
15
+ @grouped = !!options[:grouped]
16
+
17
+ @passed = !!options[:passed]
18
+ @duration = options[:duration]
19
+ @message = options[:message]
20
+ end
21
+
22
+ def passed?
23
+ @passed
24
+ end
25
+
26
+ def grouped?
27
+ @grouped
28
+ end
29
+
30
+ def update options = {}
31
+ @passed &&= !!options[:passed]
32
+ @duration += options[:duration]
33
+ @message = [ @message, options[:message] ].select{ |m| m }.join("\n\n") if options[:message]
34
+ end
35
+
36
+ def to_h options = {}
37
+ {
38
+ 'k' => @key,
39
+ 'p' => @passed,
40
+ 'd' => @duration
41
+ }.tap do |h|
42
+
43
+ h['m'] = @message if @message
44
+
45
+ cache = options[:cache]
46
+ first = !cache || !cache.known?(self)
47
+ stale = !first && cache.stale?(self)
48
+ h['n'] = @name if stale or first
49
+ h['c'] = @category if stale or (first and @category)
50
+ h['g'] = @tags if stale or (first and !@tags.empty?)
51
+ h['t'] = @tickets if stale or (first and !@tickets.empty?)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def wrap a
58
+ a.kind_of?(Array) ? a : [ a ]
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,88 @@
1
+ module RoxClient
2
+
3
+ class TestRun
4
+ attr_reader :results, :project
5
+ attr_accessor :duration, :uid
6
+
7
+ def initialize project
8
+ @results = []
9
+ @project = project
10
+ end
11
+
12
+ def add_result options = {}
13
+
14
+ if options[:grouped] && existing_result = @results.find{ |r| r.grouped? && r.key == options[:key] }
15
+ existing_result.update options
16
+ else
17
+ @results << TestResult.new(@project, 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
+ 'd' => @duration,
37
+ 'r' => [
38
+ {
39
+ 'j' => @project.api_id,
40
+ 'v' => @project.version,
41
+ 't' => @results.collect{ |r| r.to_h options }
42
+ }
43
+ ]
44
+ }.tap do |h|
45
+ h['u'] = @uid if @uid
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ def validate!
52
+ # TODO: validate duration
53
+
54
+ raise PayloadError.new("Missing project") if !@project
55
+ @project.validate!
56
+
57
+ validate_no_results_without_key
58
+ validate_no_duplicate_keys
59
+ end
60
+
61
+ def validate_no_duplicate_keys
62
+
63
+ results_with_duplicate_key = results_by_key.select{ |k,r| r.length >= 2 }
64
+ return if results_with_duplicate_key.none?
65
+
66
+ msg = "the following keys are used by multiple test results".tap do |s|
67
+ results_with_duplicate_key.each_pair do |key,results|
68
+ s << "\n - #{key}"
69
+ results.each{ |r| s << "\n - #{r.name}" }
70
+ end
71
+ end
72
+
73
+ raise PayloadError.new(msg)
74
+ end
75
+
76
+ def validate_no_results_without_key
77
+
78
+ keyless = results_without_key
79
+ return if keyless.empty?
80
+
81
+ msg = "the following test results are missing a key".tap do |s|
82
+ keyless.each{ |r| s << "\n - #{r.name}" }
83
+ end
84
+
85
+ raise PayloadError.new(msg)
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,60 @@
1
+ require 'securerandom'
2
+
3
+ module RoxClient
4
+
5
+ class UID
6
+ ENVIRONMENT_VARIABLE = 'ROX_TEST_RUN_UID'
7
+
8
+ class Error < RoxClient::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("$ROX_TEST_RUN_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,200 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rox-client-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Simon Oulevay
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: oj
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '2.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '2.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: httparty
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '0.13'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '0.13'
41
+ - !ruby/object:Gem::Dependency
42
+ name: paint
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '0.8'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '0.8'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '10.1'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ version: '10.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ~>
74
+ - !ruby/object:Gem::Version
75
+ version: '2.14'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ~>
81
+ - !ruby/object:Gem::Version
82
+ version: '2.14'
83
+ - !ruby/object:Gem::Dependency
84
+ name: bundler
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: '1.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ~>
95
+ - !ruby/object:Gem::Version
96
+ version: '1.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: jeweler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ~>
102
+ - !ruby/object:Gem::Version
103
+ version: '2.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ~>
109
+ - !ruby/object:Gem::Version
110
+ version: '2.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake-version
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ~>
116
+ - !ruby/object:Gem::Version
117
+ version: '0.4'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ~>
123
+ - !ruby/object:Gem::Version
124
+ version: '0.4'
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ~>
130
+ - !ruby/object:Gem::Version
131
+ version: '0.8'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ~>
137
+ - !ruby/object:Gem::Version
138
+ version: '0.8'
139
+ - !ruby/object:Gem::Dependency
140
+ name: fakefs
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ~>
144
+ - !ruby/object:Gem::Version
145
+ version: '0.5'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ~>
151
+ - !ruby/object:Gem::Version
152
+ version: '0.5'
153
+ description: Collects test results and uploads them to a ROX Center server.
154
+ email: simon.oulevay@lotaris.com
155
+ executables: []
156
+ extensions: []
157
+ extra_rdoc_files:
158
+ - LICENSE.txt
159
+ - README.md
160
+ files:
161
+ - Gemfile
162
+ - LICENSE.txt
163
+ - README.md
164
+ - VERSION
165
+ - lib/rox-client-ruby.rb
166
+ - lib/rox-client-ruby/cache.rb
167
+ - lib/rox-client-ruby/client.rb
168
+ - lib/rox-client-ruby/config.rb
169
+ - lib/rox-client-ruby/project.rb
170
+ - lib/rox-client-ruby/server.rb
171
+ - lib/rox-client-ruby/tasks.rb
172
+ - lib/rox-client-ruby/test_payload.rb
173
+ - lib/rox-client-ruby/test_result.rb
174
+ - lib/rox-client-ruby/test_run.rb
175
+ - lib/rox-client-ruby/uid.rb
176
+ homepage: https://github.com/lotaris/rox-client-ruby
177
+ licenses:
178
+ - MIT
179
+ metadata: {}
180
+ post_install_message:
181
+ rdoc_options: []
182
+ require_paths:
183
+ - lib
184
+ required_ruby_version: !ruby/object:Gem::Requirement
185
+ requirements:
186
+ - - ! '>='
187
+ - !ruby/object:Gem::Version
188
+ version: '0'
189
+ required_rubygems_version: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - ! '>='
192
+ - !ruby/object:Gem::Version
193
+ version: '0'
194
+ requirements: []
195
+ rubyforge_project:
196
+ rubygems_version: 2.2.1
197
+ signing_key:
198
+ specification_version: 4
199
+ summary: Ruby utilities to send results to ROX Center.
200
+ test_files: []