limited_red 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/README.rdoc +17 -0
- data/lib/limited_red.rb +26 -0
- data/lib/limited_red/adapter/httparty.rb +24 -0
- data/lib/limited_red/client.rb +74 -0
- data/lib/limited_red/config.rb +111 -0
- data/lib/limited_red/formatter/stats.rb +63 -0
- data/lib/limited_red/gzip.rb +22 -0
- data/lib/limited_red/plugin.rb +23 -0
- data/lib/limited_red/stats.rb +18 -0
- data/lib/limited_red/thread_pool.rb +28 -0
- metadata +80 -0
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2010 Joseph Wilk
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
= Limited Red Client
|
2
|
+
|
3
|
+
== Docs
|
4
|
+
|
5
|
+
https://github.com/josephwilk/limited-red-client/wikis
|
6
|
+
|
7
|
+
== Running tests
|
8
|
+
|
9
|
+
rake
|
10
|
+
|
11
|
+
== Authors
|
12
|
+
* Joseph Wilk <joe@josephwilk.net>
|
13
|
+
|
14
|
+
|
15
|
+
== Copyright
|
16
|
+
|
17
|
+
Copyright (c) 2010,2011 Joseph Wilk. See LICENSE for details.
|
data/lib/limited_red.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib') unless $:.include?(File.dirname(__FILE__) + '/../lib')
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'httparty'
|
5
|
+
require 'cucumber'
|
6
|
+
|
7
|
+
module LimitedRed
|
8
|
+
ROOT = File.expand_path('..', __FILE__)
|
9
|
+
|
10
|
+
autoload :Gzip, "#{ROOT}/limited_red/gzip"
|
11
|
+
autoload :Client, "#{ROOT}/limited_red/client"
|
12
|
+
|
13
|
+
autoload :ThreadPool, "#{ROOT}/limited_red/thread_pool"
|
14
|
+
autoload :FakeThreadPool, "#{ROOT}/limited_red/thread_pool"
|
15
|
+
|
16
|
+
autoload :Stats, "#{ROOT}/limited_red/stats"
|
17
|
+
autoload :Config, "#{ROOT}/limited_red/config"
|
18
|
+
|
19
|
+
module Adapter
|
20
|
+
autoload :HttParty, "#{ROOT}/limited_red/adapter/httparty"
|
21
|
+
end
|
22
|
+
|
23
|
+
module Formatter
|
24
|
+
autoload :Stats, "#{ROOT}/limited_red/formatter/stats"
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module LimitedRed
|
2
|
+
module Adapter
|
3
|
+
class HttParty
|
4
|
+
def self.new(config, options = {})
|
5
|
+
host = config['host'] || 'https://limited-red-data-service.heroku.com/'
|
6
|
+
port = config['port'] || ''
|
7
|
+
uri = "#{host}#{port == '' ? '' : ":#{port}"}"
|
8
|
+
|
9
|
+
HttPartyAdapter.base_uri(uri)
|
10
|
+
HttPartyAdapter
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class HttPartyAdapter
|
15
|
+
include HTTParty
|
16
|
+
|
17
|
+
def self.encode_and_compress(data)
|
18
|
+
compressed_result = Gzip.compress(data)
|
19
|
+
Base64.encode64(compressed_result)
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module LimitedRed
|
2
|
+
class Client
|
3
|
+
def initialize(config, out = STDOUT, adapter = nil, thread_pool = ThreadPool)
|
4
|
+
@config ||= config
|
5
|
+
@out = out
|
6
|
+
@project_id = @config['project name']
|
7
|
+
@username = @config['username']
|
8
|
+
@api_key = @config['api key']
|
9
|
+
|
10
|
+
@adapter = adapter || LimitedRed::Adapter::HttParty.new(@config)
|
11
|
+
@thread_pool = thread_pool
|
12
|
+
end
|
13
|
+
|
14
|
+
def log_result(build_id, data)
|
15
|
+
data[:result] = @adapter.encode_and_compress(data[:result])
|
16
|
+
data = data.merge({:user => @username, :token => token_for(data.merge(:build_id => build_id))})
|
17
|
+
|
18
|
+
@thread_pool.with_a_thread_run do
|
19
|
+
result = @adapter.post("/projects/#{@project_id}/builds/#{build_id}/results", :body => data)
|
20
|
+
@out.puts error_message(result) if error?(result)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def log_build(build_id, data)
|
25
|
+
data[:build_id] = build_id
|
26
|
+
data = data.merge({:user => @username, :build_id => build_id, :token => token_for(data)})
|
27
|
+
|
28
|
+
@thread_pool.with_a_thread_run do
|
29
|
+
result = @adapter.post("/projects/#{@project_id}/builds", :body => data)
|
30
|
+
@out.puts error_message(result) if error?(result)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def find_failing_features
|
35
|
+
raise "No project name was found in params: #{@config.inspect}" if @project_id.nil?
|
36
|
+
|
37
|
+
response = @adapter.get("/projects/#{@project_id}/features/fails?user=#{@username}")
|
38
|
+
|
39
|
+
return [] if response.nil? || response.empty?
|
40
|
+
response.code == 200 ? response.body.split(" ") : []
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def token_for(data)
|
45
|
+
data_string = @username.to_s +
|
46
|
+
@project_id.to_s +
|
47
|
+
build_data_string(data[:fails]) +
|
48
|
+
build_data_string(data[:passes]) +
|
49
|
+
(data[:result] ? data[:result] : "") +
|
50
|
+
(data[:build_id].to_s) +
|
51
|
+
@api_key.to_s
|
52
|
+
|
53
|
+
Digest::SHA1.hexdigest(data_string)
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_data_string(data)
|
57
|
+
data == "" || data.nil? ? "" : data.join("")
|
58
|
+
end
|
59
|
+
|
60
|
+
def error?(result)
|
61
|
+
result && !result.body.empty?
|
62
|
+
end
|
63
|
+
|
64
|
+
def error_message(error_msg)
|
65
|
+
if ENV['LIMITED_RED_ENV'] == 'test' || ENV['LIMITED_RED_ENV'] == 'development'
|
66
|
+
message = #{error_msg}
|
67
|
+
else
|
68
|
+
message = ""
|
69
|
+
end
|
70
|
+
|
71
|
+
"\nLimited Red had a problem logging your test results.\n#{message}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
module LimitedRed
|
2
|
+
class Config
|
3
|
+
def initialize(out_stream = STDOUT, in_stream = STDIN)
|
4
|
+
@in = in_stream
|
5
|
+
@out = out_stream
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.load_and_validate_config
|
9
|
+
new.load_and_validate_config
|
10
|
+
end
|
11
|
+
|
12
|
+
def load_and_validate_config
|
13
|
+
ensure_config_exists
|
14
|
+
if valid_config?
|
15
|
+
config
|
16
|
+
else
|
17
|
+
errors = "Make sure you have the following set in your .limited_red:\n"
|
18
|
+
errors += " * project name\n" unless config['project name']
|
19
|
+
errors += " * username\n" unless config['username']
|
20
|
+
errors += " * api key\n" unless config['api key']
|
21
|
+
@out.print errors
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def config
|
26
|
+
@config ||= begin
|
27
|
+
limited_red_project_config = load_yaml(limited_red_project_config_file)
|
28
|
+
limited_red_shared_config = load_yaml(limited_red_shared_config_file)
|
29
|
+
limited_red_shared_config.merge(limited_red_project_config)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def load_yaml(file)
|
36
|
+
if file
|
37
|
+
content = IO.read(file)
|
38
|
+
YAML::load(content)
|
39
|
+
else
|
40
|
+
{}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def ensure_config_exists
|
45
|
+
unless limited_red_project_config_file_exists?
|
46
|
+
details = ask_for_setup_details
|
47
|
+
|
48
|
+
File.open("#{home_dir}/.limited_red", 'w') do |f|
|
49
|
+
f.write("username: #{details['username']}\n")
|
50
|
+
f.write("api key: #{details['api key']}\n")
|
51
|
+
end
|
52
|
+
|
53
|
+
FileUtils.touch(".limited_red")
|
54
|
+
File.open('.limited_red', 'w') do |f|
|
55
|
+
f.write("project name: #{details['project name']}\n")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def ask_for_setup_details
|
61
|
+
details = {}
|
62
|
+
@out.print "Project name"
|
63
|
+
project_name = guess_project_name
|
64
|
+
@out.print " (default: #{project_name})" if project_name
|
65
|
+
@out.print ": "
|
66
|
+
name = @in.readline.strip
|
67
|
+
name = name.empty? ? project_name : name
|
68
|
+
name.downcase
|
69
|
+
details['project name'] = name.gsub(/[^a-zA-Z0-9']+/, "-")
|
70
|
+
@out.print "username: "
|
71
|
+
username = @in.readline.strip
|
72
|
+
details['username'] = username
|
73
|
+
@out.print "api key: "
|
74
|
+
api_key = @in.readline.strip
|
75
|
+
details['api key'] = api_key
|
76
|
+
details
|
77
|
+
end
|
78
|
+
|
79
|
+
def valid_config?
|
80
|
+
config &&
|
81
|
+
config['project name'] &&
|
82
|
+
config['username'] &&
|
83
|
+
config['api key']
|
84
|
+
end
|
85
|
+
|
86
|
+
def limited_red_project_config_file_exists?
|
87
|
+
limited_red_project_config_file
|
88
|
+
end
|
89
|
+
|
90
|
+
def limited_red_shared_config_file
|
91
|
+
@global_config_file ||= Dir.glob("#{home_dir}/.limited_red").first
|
92
|
+
end
|
93
|
+
|
94
|
+
def limited_red_project_config_file
|
95
|
+
@project_config_file ||= Dir.glob('.limited_red').first
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
def home_dir
|
100
|
+
ENV['HOME'] || File.expand_path('~') #Dir.home
|
101
|
+
end
|
102
|
+
|
103
|
+
def guess_project_name
|
104
|
+
dir = Dir.pwd
|
105
|
+
if dir
|
106
|
+
current_dir = dir.split('/')[-1]
|
107
|
+
return current_dir if current_dir && !current_dir.empty?
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'cucumber/formatter/json'
|
2
|
+
require 'limited_red/client'
|
3
|
+
|
4
|
+
module LimitedRed
|
5
|
+
module Formatter
|
6
|
+
class Stats < Cucumber::Formatter::Json
|
7
|
+
|
8
|
+
def initialize(step_mother, path_or_io, options)
|
9
|
+
@t = Time.now
|
10
|
+
@build_id = Time.now.to_i
|
11
|
+
@step_mother = step_mother
|
12
|
+
@client = LimitedRed::Client.new(LimitedRed::Config.load_and_validate_config)
|
13
|
+
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def after_feature(feature)
|
18
|
+
if supports_feature_hash?
|
19
|
+
json = feature_hash.to_json
|
20
|
+
@client.log_result(@build_id, :result => json)
|
21
|
+
else
|
22
|
+
puts "Error: Gherkin version is out of date and does not support @feature_hash: #{gherkin_formatter.instance_variables}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def after_features(features)
|
27
|
+
print_summary
|
28
|
+
end
|
29
|
+
|
30
|
+
def print_summary
|
31
|
+
@client.log_build(@build_id, {:fails => failing_files,
|
32
|
+
:passes => passing_files})
|
33
|
+
|
34
|
+
ThreadPool.wait_for_all_threads_to_finish
|
35
|
+
end
|
36
|
+
|
37
|
+
def failing_files
|
38
|
+
failures = @step_mother.scenarios(:failed).select { |s| s.is_a?(Cucumber::Ast::Scenario) || s.is_a?(Cucumber::Ast::OutlineTable::ExampleRow) }
|
39
|
+
failures = failures.collect { |s| (s.is_a?(Cucumber::Ast::OutlineTable::ExampleRow)) ? s.scenario_outline : s }
|
40
|
+
failures.map{|fail| fail.file_colon_line}
|
41
|
+
end
|
42
|
+
|
43
|
+
def passing_files
|
44
|
+
passing = @step_mother.scenarios(:passed).select { |s| s.is_a?(Cucumber::Ast::Scenario) || s.is_a?(Cucumber::Ast::OutlineTable::ExampleRow) }
|
45
|
+
passing = passing.collect { |s| (s.is_a?(Cucumber::Ast::OutlineTable::ExampleRow)) ? s.scenario_outline : s }
|
46
|
+
passing.map{|pass| pass.file_colon_line}
|
47
|
+
end
|
48
|
+
|
49
|
+
def supports_feature_hash?
|
50
|
+
gherkin_formatter.instance_variables.include?(:@feature_hash)
|
51
|
+
end
|
52
|
+
|
53
|
+
def feature_hash
|
54
|
+
gherkin_formatter.instance_variable_get("@feature_hash")
|
55
|
+
end
|
56
|
+
|
57
|
+
def gherkin_formatter
|
58
|
+
@gf
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
require 'stringio'
|
3
|
+
|
4
|
+
module LimitedRed
|
5
|
+
module Gzip
|
6
|
+
class Stream < StringIO
|
7
|
+
def close; rewind; end
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.compress(source)
|
11
|
+
output = Stream.new
|
12
|
+
gz = Zlib::GzipWriter.new(output)
|
13
|
+
gz.write(source)
|
14
|
+
gz.close
|
15
|
+
output.string
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.decompress(source)
|
19
|
+
Zlib::GzipReader.new(StringIO.new(source)).read
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
AfterConfiguration do |config|
|
2
|
+
unless ENV['STOP_RECORDING']
|
3
|
+
require 'limited_red'
|
4
|
+
|
5
|
+
options = config.instance_variable_get("@options")
|
6
|
+
options[:formats] << ['LimitedRed::Formatter::Stats', config.out_stream]
|
7
|
+
|
8
|
+
limited_red_config = LimitedRed::Config.load_and_validate_config
|
9
|
+
if limited_red_config
|
10
|
+
cuke_stats = LimitedRed::Stats.new(limited_red_config)
|
11
|
+
|
12
|
+
feature_files = options[:paths]
|
13
|
+
feature_files = ["features"] if feature_files.empty?
|
14
|
+
|
15
|
+
prioritised_features = cuke_stats.feature_files & config.feature_files
|
16
|
+
|
17
|
+
options[:paths] = prioritised_features + feature_files
|
18
|
+
else
|
19
|
+
puts "Aborting Cucumber run"
|
20
|
+
exit
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module LimitedRed
|
2
|
+
class Stats
|
3
|
+
def initialize(config)
|
4
|
+
@client = Client.new(config)
|
5
|
+
end
|
6
|
+
|
7
|
+
def feature_files
|
8
|
+
files = failing_features.uniq
|
9
|
+
files.select{|file| File.exists?(file)}
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def failing_features
|
15
|
+
@client.find_failing_features
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module LimitedRed
|
2
|
+
class FakeThreadPool
|
3
|
+
class << self
|
4
|
+
def with_a_thread_run(&block)
|
5
|
+
yield
|
6
|
+
end
|
7
|
+
|
8
|
+
def wait_for_all_threads_to_finish
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class ThreadPool
|
14
|
+
class << self
|
15
|
+
attr_writer :threads
|
16
|
+
|
17
|
+
def with_a_thread_run(&block)
|
18
|
+
@threads ||= []
|
19
|
+
@threads << Thread.new(&block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def wait_for_all_threads_to_finish
|
23
|
+
@threads.each { |thread| thread.join }
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: limited_red
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.4
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Joseph Wilk
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-12-21 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: httparty
|
16
|
+
requirement: &70300184545600 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - =
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.5.2
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *70300184545600
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: cucumber
|
27
|
+
requirement: &70300184545120 !ruby/object:Gem::Requirement
|
28
|
+
none: false
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.8.0
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: *70300184545120
|
36
|
+
description: Run tests priorited by those that are most likely to fail
|
37
|
+
email: joe@josephwilk.net
|
38
|
+
executables: []
|
39
|
+
extensions: []
|
40
|
+
extra_rdoc_files:
|
41
|
+
- LICENSE
|
42
|
+
- README.rdoc
|
43
|
+
files:
|
44
|
+
- lib/limited_red/adapter/httparty.rb
|
45
|
+
- lib/limited_red/client.rb
|
46
|
+
- lib/limited_red/config.rb
|
47
|
+
- lib/limited_red/formatter/stats.rb
|
48
|
+
- lib/limited_red/gzip.rb
|
49
|
+
- lib/limited_red/plugin.rb
|
50
|
+
- lib/limited_red/stats.rb
|
51
|
+
- lib/limited_red/thread_pool.rb
|
52
|
+
- lib/limited_red.rb
|
53
|
+
- LICENSE
|
54
|
+
- README.rdoc
|
55
|
+
homepage:
|
56
|
+
licenses: []
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options:
|
59
|
+
- --charset=UTF-8
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ! '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ! '>='
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
requirements: []
|
75
|
+
rubyforge_project:
|
76
|
+
rubygems_version: 1.8.6
|
77
|
+
signing_key:
|
78
|
+
specification_version: 3
|
79
|
+
summary: Run tests priorited by those that are most likely to fail
|
80
|
+
test_files: []
|