are_we_there_yet 0.2.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +4 -2
- data/Gemfile.lock +6 -2
- data/README.md +50 -14
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/are_we_there_yet.gemspec +39 -14
- data/bin/are_we_there_yet +14 -0
- data/lib/are_we_there_yet.rb +2 -106
- data/lib/are_we_there_yet/exceptions.rb +3 -0
- data/lib/are_we_there_yet/formatter.rb +17 -0
- data/lib/are_we_there_yet/metric.rb +27 -0
- data/lib/are_we_there_yet/persistence/connection.rb +13 -0
- data/lib/are_we_there_yet/persistence/schema.rb +32 -0
- data/lib/are_we_there_yet/profiler.rb +60 -0
- data/lib/are_we_there_yet/profiler_ui.rb +15 -0
- data/lib/are_we_there_yet/recorder.rb +47 -0
- data/lib/are_we_there_yet/run.rb +12 -0
- data/spec/lib/formatter_spec.rb +33 -0
- data/spec/lib/metric_spec.rb +63 -0
- data/spec/lib/persistence/connection_spec.rb +21 -0
- data/spec/lib/persistence/schema_spec.rb +94 -0
- data/spec/lib/profiler_spec.rb +51 -0
- data/spec/lib/profiler_ui_spec.rb +50 -0
- data/spec/lib/recorder_spec.rb +78 -0
- data/spec/lib/run_spec.rb +49 -0
- data/spec/spec_helper.rb +2 -31
- data/spec/support/spec_classes.rb +22 -0
- data/spec/support/symbol.rb +6 -0
- metadata +86 -45
- data/spec/are_we_there_yet_spec.rb +0 -252
@@ -0,0 +1,60 @@
|
|
1
|
+
module AreWeThereYet
|
2
|
+
class Profiler
|
3
|
+
def initialize(db_connection_string)
|
4
|
+
@db = AreWeThereYet::Persistence::Connection.create(db_connection_string)
|
5
|
+
end
|
6
|
+
|
7
|
+
def list_files
|
8
|
+
averages_by_file = get_average_per_key(metrics_by_file)
|
9
|
+
|
10
|
+
sorted_output(transform_averages_for_sorting(averages_by_file, :file))
|
11
|
+
end
|
12
|
+
|
13
|
+
def list_examples(file_path)
|
14
|
+
if metrics_by_file[file_path]
|
15
|
+
metrics_by_example = metrics_by_file[file_path].group_by { |m| m.description }
|
16
|
+
|
17
|
+
averages_by_example = get_average_per_key(metrics_by_example)
|
18
|
+
|
19
|
+
sorted_output(transform_averages_for_sorting(averages_by_example, :example))
|
20
|
+
else
|
21
|
+
[]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def sorted_output(data_to_sort)
|
28
|
+
data_to_sort.sort { |x,y| y[:average_execution_time] <=> x[:average_execution_time] }
|
29
|
+
end
|
30
|
+
|
31
|
+
def find_average_time_for(runs)
|
32
|
+
total_per_run = runs.inject([]) do |memo, (run,metrics)|
|
33
|
+
memo << metrics.inject(0.0) { |total,m| total + m.execution_time }
|
34
|
+
end
|
35
|
+
|
36
|
+
(total_per_run.inject(:+))/total_per_run.size
|
37
|
+
end
|
38
|
+
|
39
|
+
def metrics_by_file
|
40
|
+
@metrics_by_file || Metric.all(@db).group_by { |m| m.path }
|
41
|
+
end
|
42
|
+
|
43
|
+
def get_average_per_key(metric_set)
|
44
|
+
# Merging a hash with itself is really just a sneaky way to do a map
|
45
|
+
metrics_by_key_per_run = metric_set.merge(metric_set) do |key,metrics,metrics|
|
46
|
+
metrics.group_by { |m| m.run_id }
|
47
|
+
end
|
48
|
+
|
49
|
+
averages_by_key = metrics_by_key_per_run.merge(metrics_by_key_per_run) do |key, runs, runs|
|
50
|
+
find_average_time_for runs
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def transform_averages_for_sorting(averages_by_key, key_name)
|
55
|
+
averages_by_key.map do |key, average_execution_time|
|
56
|
+
{ key_name.to_sym => key, :average_execution_time => average_execution_time }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module AreWeThereYet
|
2
|
+
class ProfilerUI
|
3
|
+
class UnknownListingError < StandardError; end
|
4
|
+
def self.get_profiler_output(location, output, options={})
|
5
|
+
profiler = Profiler.new(location)
|
6
|
+
if options[:list] == 'files'
|
7
|
+
output.write(Formatter.format_for_output(profiler.list_files))
|
8
|
+
elsif options[:list] == 'examples'
|
9
|
+
output.write(Formatter.format_for_output(profiler.list_examples(options[:file_path])))
|
10
|
+
else
|
11
|
+
raise UnknownListingError
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module AreWeThereYet
|
2
|
+
class Recorder < Spec::Runner::Formatter::BaseFormatter
|
3
|
+
def initialize(options,where)
|
4
|
+
@db = AreWeThereYet::Persistence::Connection.create(where)
|
5
|
+
|
6
|
+
AreWeThereYet::Persistence::Schema.create(@db)
|
7
|
+
|
8
|
+
log_run
|
9
|
+
end
|
10
|
+
|
11
|
+
def example_started(example)
|
12
|
+
@start = Time.now
|
13
|
+
end
|
14
|
+
|
15
|
+
def example_passed(example)
|
16
|
+
persist_metric(example)
|
17
|
+
end
|
18
|
+
|
19
|
+
def close
|
20
|
+
@run.finish(@db)
|
21
|
+
@db.disconnect
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def log_run
|
27
|
+
@run = AreWeThereYet::Run.new
|
28
|
+
@run.start(@db)
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_file_path_from(example)
|
32
|
+
example.location.split(':').first
|
33
|
+
end
|
34
|
+
|
35
|
+
def persist_metric(example)
|
36
|
+
|
37
|
+
metric = AreWeThereYet::Metric.new(
|
38
|
+
:execution_time => Time.now - @start,
|
39
|
+
:path => get_file_path_from(example),
|
40
|
+
:description => example.description,
|
41
|
+
:run_id => @run.id
|
42
|
+
)
|
43
|
+
|
44
|
+
metric.save(@db)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AreWeThereYet::Formatter do
|
4
|
+
it "returns formatted data for file metrics" do
|
5
|
+
input = [
|
6
|
+
{ :file => "/path/to/spec", :average_execution_time => 32.0 },
|
7
|
+
{ :file => "/path/to/other/spec", :average_execution_time => 5.0 },
|
8
|
+
]
|
9
|
+
output = [
|
10
|
+
%Q{"File Path","Average Execution Time"},
|
11
|
+
"",
|
12
|
+
%Q{"#{input[0][:file]}",#{input[0][:average_execution_time]}},
|
13
|
+
%Q{"#{input[1][:file]}",#{input[1][:average_execution_time]}},
|
14
|
+
].join("\n") + "\n"
|
15
|
+
|
16
|
+
AreWeThereYet::Formatter.format_for_output(input).should == output
|
17
|
+
end
|
18
|
+
|
19
|
+
it "returns formatted data for exampel metrics" do
|
20
|
+
input = [
|
21
|
+
{ :example => "blaah", :average_execution_time => 20.0 },
|
22
|
+
{ :example => "blah", :average_execution_time => 12.0 }
|
23
|
+
]
|
24
|
+
output = [
|
25
|
+
%Q{"Example","Average Execution Time"},
|
26
|
+
"",
|
27
|
+
%Q{"#{input[0][:example]}",#{input[0][:average_execution_time]}},
|
28
|
+
%Q{"#{input[1][:example]}",#{input[1][:average_execution_time]}},
|
29
|
+
].join("\n") + "\n"
|
30
|
+
|
31
|
+
AreWeThereYet::Formatter.format_for_output(input).should == output
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AreWeThereYet::Metric do
|
4
|
+
before(:each) do
|
5
|
+
@db_name = "/tmp/are_we_there_yet_#{Time.now.to_i}_#{rand(100000000)}_spec.sqlite"
|
6
|
+
@db = AreWeThereYet::Persistence::Connection.create("sqlite://#{@db_name}")
|
7
|
+
AreWeThereYet::Persistence::Schema.create(@db)
|
8
|
+
|
9
|
+
@properties = {:id => 9999, :execution_time => 30.0, :path => '/path/to/file', :run_id => 99, :description => 'blah'}
|
10
|
+
end
|
11
|
+
|
12
|
+
after(:each) do
|
13
|
+
@db.disconnect
|
14
|
+
File.unlink(@db_name) if File.exists? @db_name
|
15
|
+
end
|
16
|
+
|
17
|
+
it "is instantiated from a hash of options" do
|
18
|
+
m = AreWeThereYet::Metric.new(@properties)
|
19
|
+
m.id.should == @properties[:id]
|
20
|
+
m.execution_time.should == @properties[:execution_time]
|
21
|
+
m.path.should == @properties[:path]
|
22
|
+
m.run_id.should == @properties[:run_id]
|
23
|
+
m.description.should == @properties[:description]
|
24
|
+
end
|
25
|
+
|
26
|
+
it "returns all metrics currently in the provided database" do
|
27
|
+
@db[:metrics].insert_multiple([{:path => 'abc'}, {:path => 'def'}])
|
28
|
+
|
29
|
+
all_metrics = AreWeThereYet::Metric.all(@db)
|
30
|
+
all_metrics.first.path.should == 'abc'
|
31
|
+
all_metrics.last.path.should == 'def'
|
32
|
+
end
|
33
|
+
|
34
|
+
it "persists itself to the database" do
|
35
|
+
AreWeThereYet::Metric.all(@db).should be_empty
|
36
|
+
m = AreWeThereYet::Metric.new(@properties)
|
37
|
+
m.save(@db)
|
38
|
+
|
39
|
+
metric = AreWeThereYet::Metric.all(@db).first
|
40
|
+
metric.id.should_not == @properties[:id] # even if an id is provided it is not saved - no provision for updates
|
41
|
+
metric.execution_time.should == @properties[:execution_time]
|
42
|
+
metric.path.should == @properties[:path]
|
43
|
+
metric.description.should == @properties[:description]
|
44
|
+
metric.run_id.should == @properties[:run_id]
|
45
|
+
end
|
46
|
+
|
47
|
+
it "sets a UTC timestamp in the database to indicate when it was created" do
|
48
|
+
fake_created_at = Time.at(0) #use an obviously fake time
|
49
|
+
|
50
|
+
Time.stub_chain(:now, :utc).and_return(fake_created_at)
|
51
|
+
m = AreWeThereYet::Metric.new(@properties)
|
52
|
+
m.save(@db)
|
53
|
+
|
54
|
+
@db[:metrics].first[:created_at].should == fake_created_at
|
55
|
+
end
|
56
|
+
|
57
|
+
it "overwrites any existing id with the id of the record created" do
|
58
|
+
m = AreWeThereYet::Metric.new(@properties)
|
59
|
+
m.save(@db)
|
60
|
+
|
61
|
+
m.id.should == 1
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AreWeThereYet::Persistence::Connection do
|
4
|
+
before(:each) do
|
5
|
+
@db_name = "/tmp/arewethereyet.sqlite"
|
6
|
+
@db = "sqlite://#{@db_name}"
|
7
|
+
|
8
|
+
File.unlink(@db_name) if File.exists? @db_name
|
9
|
+
end
|
10
|
+
|
11
|
+
it "returns a connection object for the database uri provided" do
|
12
|
+
db = AreWeThereYet::Persistence::Connection.create(@db)
|
13
|
+
db.url.should == "sqlite:/#{@db_name}"
|
14
|
+
db.test_connection.should be_true
|
15
|
+
end
|
16
|
+
|
17
|
+
it "raises an error if it cannot connect to the URI specified" do
|
18
|
+
expect { AreWeThereYet::Persistence::Connection.create('obviously_bogus') }.
|
19
|
+
should raise_error(AreWeThereYet::Persistence::Connection::InvalidDBLocation, /check that the location is valid/)
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec::Matchers.define :have_field do |expected|
|
4
|
+
def find_field_entry(actual,expected)
|
5
|
+
field_entries = actual.select { |f| f.first == expected[:field] }
|
6
|
+
field_entries.any? ? field_entries.first : nil
|
7
|
+
end
|
8
|
+
|
9
|
+
def attributes_match?(actual_attributes, expected_attributes)
|
10
|
+
expected_attributes.each do |attr,value|
|
11
|
+
return false if actual_attributes[attr] != value
|
12
|
+
end
|
13
|
+
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
match do |actual|
|
18
|
+
(actual_field = find_field_entry(actual,expected)) && attributes_match?(actual_field.last, expected[:attributes])
|
19
|
+
end
|
20
|
+
|
21
|
+
failure_message_for_should do |actual|
|
22
|
+
"expected that #{actual.inspect} would have a field conforming to #{expected.inspect}"
|
23
|
+
end
|
24
|
+
|
25
|
+
failure_message_for_should_not do |actual|
|
26
|
+
"expected that #{actual.inspect} would not have a field conforming to #{expected.inspect}"
|
27
|
+
end
|
28
|
+
|
29
|
+
description do
|
30
|
+
"has a field conforming to #{expected}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe AreWeThereYet::Persistence::Schema do
|
35
|
+
before(:each) do
|
36
|
+
@db_name = "/tmp/arewethereyet.sqlite"
|
37
|
+
@db = "sqlite://#{@db_name}"
|
38
|
+
|
39
|
+
File.unlink(@db_name) if File.exists? @db_name
|
40
|
+
|
41
|
+
@connection = Sequel.connect(@db)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "creates a table for tracking spec runs" do
|
45
|
+
AreWeThereYet::Persistence::Schema.create(@connection)
|
46
|
+
|
47
|
+
schema = @connection.schema(:runs)
|
48
|
+
schema.should have_field :field => :id, :attributes => {:type => :integer, :primary_key => true}
|
49
|
+
schema.should have_field :field => :started_at, :attributes => {:type => :datetime}
|
50
|
+
schema.should have_field :field => :ended_at, :attributes => {:type => :datetime}
|
51
|
+
end
|
52
|
+
|
53
|
+
it "creates a table for tracking metrics" do
|
54
|
+
AreWeThereYet::Persistence::Schema.create(@connection)
|
55
|
+
|
56
|
+
schema = @connection.schema(:metrics)
|
57
|
+
schema.should have_field :field => :id, :attributes => {:type => :integer, :primary_key => true}
|
58
|
+
schema.should have_field :field => :path, :attributes => {:type => :string}
|
59
|
+
schema.should have_field :field => :description, :attributes => {:type => :string}
|
60
|
+
schema.should have_field :field => :execution_time, :attributes => {:type => :float}
|
61
|
+
schema.should have_field :field => :created_at, :attributes => {:type => :datetime}
|
62
|
+
schema.should have_field :field => :run_id, :attributes => {:type => :integer}
|
63
|
+
|
64
|
+
@connection.indexes(:metrics).should == {
|
65
|
+
:metrics_path_index => {:unique=>false, :columns=>[:path]},
|
66
|
+
:metrics_description_index => { :unique => false, :columns => [:description]}
|
67
|
+
}
|
68
|
+
end
|
69
|
+
|
70
|
+
it "rolls back table creation if there is a problem creating any one of the tables" do
|
71
|
+
broken_connection = mock(Sequel::SQLite::Database, :tables => [])
|
72
|
+
broken_connection.stub(:create_table) do |arg, bl|
|
73
|
+
if arg == :metrics
|
74
|
+
raise RuntimeError
|
75
|
+
else
|
76
|
+
@connection.create_table(arg, &bl)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
expect { AreWeThereYet::Persistence::Schema.create(broken_connection) }.should raise_error
|
81
|
+
|
82
|
+
@connection.tables.should be_empty
|
83
|
+
end
|
84
|
+
|
85
|
+
it "does not create tables if there already tables present in the database" do
|
86
|
+
@connection.create_table(:dummy) do
|
87
|
+
primary_key :id
|
88
|
+
end
|
89
|
+
|
90
|
+
AreWeThereYet::Persistence::Schema.create(@connection)
|
91
|
+
|
92
|
+
@connection.tables.should == [:dummy]
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AreWeThereYet::Profiler do
|
4
|
+
before(:all) do
|
5
|
+
@db_name = "/tmp/are_we_there_yet_#{Time.now.to_i}_#{rand(100000000)}_spec.sqlite"
|
6
|
+
@db = "sqlite://#{@db_name}"
|
7
|
+
|
8
|
+
@connection = AreWeThereYet::Persistence::Connection.create(@db)
|
9
|
+
AreWeThereYet::Persistence::Schema.create(@connection)
|
10
|
+
|
11
|
+
@connection[:metrics].insert_multiple(
|
12
|
+
[
|
13
|
+
{ :path => "/path/to/spec", :description => "blah", :execution_time => 5, :run_id => 1 },
|
14
|
+
{ :path => "/path/to/spec", :description => "blaah", :execution_time => 10, :run_id => 1 },
|
15
|
+
{ :path => "/path/to/other/spec", :description => "asdfghij", :execution_time => 5, :run_id => 1 },
|
16
|
+
{ :path => "/path/to/spec", :description => "blah", :execution_time => 18, :run_id => 2 },
|
17
|
+
{ :path => "/path/to/spec", :description => "blaah", :execution_time => 30, :run_id => 2 },
|
18
|
+
]
|
19
|
+
)
|
20
|
+
|
21
|
+
@profiler = AreWeThereYet::Profiler.new(@db)
|
22
|
+
end
|
23
|
+
|
24
|
+
after(:all) do
|
25
|
+
@connection.disconnect
|
26
|
+
File.unlink(@db_name) if File.exists? @db_name
|
27
|
+
end
|
28
|
+
|
29
|
+
it "returns a list of the spec files ordered by descending average execution time" do
|
30
|
+
file_list = @profiler.list_files
|
31
|
+
file_list.should == [
|
32
|
+
{ :file => "/path/to/spec", :average_execution_time => 31.5 },
|
33
|
+
{ :file => "/path/to/other/spec", :average_execution_time => 5.0 },
|
34
|
+
]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "returns a sorted list of examples together with run times for a given file" do
|
38
|
+
examples_for_file = @profiler.list_examples("/path/to/spec")
|
39
|
+
examples_for_file.should == [
|
40
|
+
{ :example => "blaah", :average_execution_time => 20.0 },
|
41
|
+
{ :example => "blah", :average_execution_time => 11.5 }
|
42
|
+
]
|
43
|
+
end
|
44
|
+
|
45
|
+
it "returns an empty list of examples if the given file path cannot be found" do
|
46
|
+
examples_for_file = @profiler.list_examples("/un/known/file")
|
47
|
+
|
48
|
+
examples_for_file.should respond_to :each
|
49
|
+
examples_for_file.should be_empty
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AreWeThereYet::ProfilerUI do
|
4
|
+
before(:all) do
|
5
|
+
@db_name = "/tmp/are_we_there_yet_#{Time.now.to_i}_#{rand(100000000)}_spec.sqlite"
|
6
|
+
@db = "sqlite://#{@db_name}"
|
7
|
+
@connection = AreWeThereYet::Persistence::Connection.create(@db)
|
8
|
+
AreWeThereYet::Persistence::Schema.create(@connection)
|
9
|
+
|
10
|
+
@path = "/path/to/spec"
|
11
|
+
@time = 9.5
|
12
|
+
@description = "blaah"
|
13
|
+
|
14
|
+
@connection[:metrics].insert(:path => @path, :description => @description, :execution_time => @time, :run_id => 1)
|
15
|
+
end
|
16
|
+
|
17
|
+
before(:each) do
|
18
|
+
@mock_io = double(IO)
|
19
|
+
end
|
20
|
+
|
21
|
+
after(:all) do
|
22
|
+
@connection.disconnect
|
23
|
+
File.unlink(@db_name) if File.exists? @db_name
|
24
|
+
end
|
25
|
+
|
26
|
+
context "file listing" do
|
27
|
+
it "writes a file listing togther with headers to STDOUT" do
|
28
|
+
output_matcher = /File Path.*Average Execution Time.*\n\n.*#{@path}.*#{@time}\n/
|
29
|
+
@mock_io.should_receive(:write).with(output_matcher)
|
30
|
+
|
31
|
+
AreWeThereYet::ProfilerUI.get_profiler_output(@db, @mock_io, :list => 'files')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "example listing" do
|
36
|
+
it "outputs an example listing for a given file" do
|
37
|
+
output_matcher = /Example.*Average Execution Time.*\n\n.*#{@description}.*#{@time}\n/
|
38
|
+
@mock_io.should_receive(:write).with(output_matcher)
|
39
|
+
|
40
|
+
AreWeThereYet::ProfilerUI.get_profiler_output(@db, @mock_io, :list => 'examples', :file_path => '/path/to/spec')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "unknown listing" do
|
45
|
+
it "raises an exception" do
|
46
|
+
expect { AreWeThereYet::ProfilerUI.get_profiler_output(@db, @mock_io, :list => 'awesome') }.
|
47
|
+
should raise_error AreWeThereYet::ProfilerUI::UnknownListingError
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|