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.
@@ -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,12 @@
1
+ module AreWeThereYet
2
+ class Run
3
+ attr_reader :id
4
+ def start(database)
5
+ @id = database[:runs].insert(:started_at => Time.now.utc)
6
+ end
7
+
8
+ def finish(database)
9
+ database[:runs].where(:id => id).update(:ended_at => Time.now.utc)
10
+ end
11
+ end
12
+ 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