click 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,4 @@
1
1
  language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
+ script: "bundle exec rspec spec --order rand"
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Click
2
2
 
3
- TODO: Write a gem description
3
+ [![Build Status](https://travis-ci.org/mark-rushakoff/click.png?branch=master)](https://travis-ci.org/mark-rushakoff/click)
4
+ [![Gem Version](https://badge.fury.io/rb/click.png)](http://badge.fury.io/rb/click)
5
+ [![Code Climate](https://codeclimate.com/github/mark-rushakoff/click.png)](https://codeclimate.com/github/mark-rushakoff/click)
6
+
7
+ A tool to track down object leaks in a Ruby process.
4
8
 
5
9
  ## Installation
6
10
 
@@ -20,6 +24,11 @@ Or install it yourself as:
20
24
 
21
25
  TODO: Write usage instructions here
22
26
 
27
+ ## Example
28
+
29
+ See [the demo app](demo/app.rb) for an example of a simple Sinatra app that uses an existing Sequel database and that leaks objects when connecting to a certain endpoint.
30
+ Try running the app and then running `click-console /tmp/click_demo_memory.sqlite` to inspect the live database.
31
+
23
32
  ## Contributing
24
33
 
25
34
  1. Fork it
@@ -8,6 +8,9 @@ module Click::Database
8
8
  module Models
9
9
  default_connection_string = 'sqlite:/'
10
10
  connection_string = "#{ARGV.fetch(0, default_connection_string)}"
11
+ if File.file?(connection_string)
12
+ connection_string = [default_connection_string, File.expand_path(connection_string)].join('/')
13
+ end
11
14
  puts "Using connection string: #{connection_string}"
12
15
 
13
16
  Click.clicker_with_database('click-console', connection_string) do |clicker|
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Click::VERSION
9
9
  spec.authors = ["Mark Rushakoff"]
10
10
  spec.email = ["mark.rushakoff@gmail.com"]
11
- spec.description = %q{A tool to help track down the source of a memory leak in a Ruby process}
11
+ spec.description = %q{A tool to help track down the source of an object leak in a Ruby process}
12
12
  spec.summary = %q{Track the changes in counts of live objects in your Ruby process.}
13
13
  spec.homepage = "https://github.com/mark-rushakoff/click"
14
14
  spec.license = "MIT"
@@ -21,6 +21,8 @@ Gem::Specification.new do |spec|
21
21
  spec.add_development_dependency "bundler", "~> 1.3"
22
22
  spec.add_development_dependency "rake"
23
23
  spec.add_development_dependency "rspec", "~> 2.14"
24
+ spec.add_development_dependency "rspec", "~> 2.14"
25
+ spec.add_development_dependency "rspec-given", "~> 3.1"
24
26
  spec.add_development_dependency "sqlite3"
25
27
  spec.add_development_dependency "pry"
26
28
 
@@ -42,6 +42,13 @@ get '/' do
42
42
  <html><body>
43
43
  <h1>Click demo app</h1>
44
44
  <br/>
45
+ <ul>
46
+ <li><a href="/make_objects/1">Leak 1 object</a></li>
47
+ <li><a href="/make_objects/10">Leak 10 objects</a></li>
48
+ <li><a href="/make_objects/100">Leak 100 objects</a></li>
49
+ <li><a href="/make_objects/1000">Leak 1000 objects</a></li>
50
+ <li><a href="/make_objects/10000">Leak 10000 objects</a></li>
51
+ </ul>
45
52
  Visited #{Visit.count} time(s) by #{Visit.group_by(:ip_address).count} IP address(es).
46
53
  </body></html>
47
54
  HTML
@@ -52,9 +52,9 @@ module Click
52
52
  class << self
53
53
  def clicker_with_database(session_name, connection_string)
54
54
  require 'click/database'
55
- Click::Database.with_database(connection_string) do |db|
55
+ Click::Database.with_database(connection_string) do
56
56
  require 'click/database/writer'
57
- writer = Click::Database::Writer.new(db)
57
+ writer = Click::Database::Writer.new
58
58
  clicker = Click::Clicker.new(session_name)
59
59
  clicker.add_observer(writer)
60
60
  yield clicker
@@ -16,18 +16,28 @@ module Click
16
16
  end
17
17
  end
18
18
 
19
+ def prepare(sequel_db)
20
+ ensure_tables!(sequel_db)
21
+ assign_db_to_models(sequel_db)
22
+ sequel_db
23
+ end
24
+
19
25
  private
20
26
  def _with_db(db)
21
- ensure_tables!(db)
22
- assign_db_to_models(db)
27
+ prepare(db)
23
28
  yield db
24
29
  end
25
30
 
26
31
  def assign_db_to_models(db)
27
32
  require 'click/database/models'
28
- Click::Database::Models::Session.db = db
29
- Click::Database::Models::Snapshot.db = db
30
- Click::Database::Models::ObjectCount.db = db
33
+ assign_db(db, Click::Database::Models::Session)
34
+ assign_db(db, Click::Database::Models::Snapshot)
35
+ assign_db(db, Click::Database::Models::ObjectCount)
36
+ end
37
+
38
+ def assign_db(db, model_class)
39
+ # Repeatedly reassigning the DB seems to cause problems, so only assign if we need it.
40
+ model_class.db = db unless model_class.db == db
31
41
  end
32
42
 
33
43
  def ensure_tables!(db)
@@ -1,4 +1,5 @@
1
1
  require 'sequel'
2
+ require 'set'
2
3
 
3
4
  module Click
4
5
  module Database
@@ -7,13 +8,27 @@ module Click
7
8
  many_to_one :session
8
9
  one_to_many :object_counts
9
10
 
11
+ def difference(earlier)
12
+ earlier_object_count_hash = Hash[earlier.object_counts_dataset.map([:class_name, :count])]
13
+ later_object_count_hash = Hash[object_counts_dataset.map([:class_name, :count])]
14
+
15
+ keys = Set.new(earlier_object_count_hash.keys + later_object_count_hash.keys)
16
+
17
+ diff = {}
18
+ keys.each do |key|
19
+ diff[key] = later_object_count_hash.fetch(key, 0) - earlier_object_count_hash.fetch(key, 0)
20
+ end
21
+
22
+ diff
23
+ end
24
+
10
25
  dataset_module do
11
26
  def after(time)
12
27
  filter { timestamp > time }
13
28
  end
14
29
 
15
30
  def before(time)
16
- filter { timestamp < time}
31
+ filter { timestamp < time }
17
32
  end
18
33
 
19
34
  def by_session_name(session_name)
@@ -6,10 +6,6 @@ module Click
6
6
  class Writer
7
7
  include Click::Observer
8
8
 
9
- def initialize(db)
10
- @db = db
11
- end
12
-
13
9
  def on_add(clicker)
14
10
  @session = Models::Session.create(name: clicker.session_name, started_at: Time.now)
15
11
  end
@@ -24,7 +20,7 @@ module Click
24
20
  end
25
21
 
26
22
  private
27
- attr_reader :db, :session
23
+ attr_reader :session
28
24
  end
29
25
  end
30
26
  end
@@ -1,3 +1,3 @@
1
1
  module Click
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -1,99 +1,93 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Click::Clicker do
4
- subject(:clicker) { described_class.new('test') }
4
+ Given(:session_name) { SecureRandom.uuid }
5
+ Given(:clicker) { described_class.new(session_name) }
5
6
 
6
7
  describe '#session_name' do
7
- it 'is the string given to the constructor' do
8
- expect(clicker.session_name).to eq('test')
9
- end
8
+ Then { clicker.session_name == session_name }
10
9
  end
11
10
 
12
11
  describe '#instance_count' do
13
- it 'counts instances of a single class' do
14
- klass = Class.new
12
+ Given(:klass) { Class.new }
15
13
 
16
- clicker.click!
17
- expect(clicker.instance_count(klass)).to eq(0)
14
+ describe 'with no instances' do
15
+ When { clicker.click! }
18
16
 
19
- obj = klass.new
17
+ Then { clicker.instance_count(klass) == 0 }
18
+ end
20
19
 
21
- clicker.click!
22
- expect(clicker.instance_count(klass)).to eq(1)
20
+ describe 'with instances' do
21
+ Given!(:object) { klass.new }
23
22
 
24
- obj = nil
23
+ When { clicker.click! }
25
24
 
26
- clicker.click!
27
- expect(clicker.instance_count(klass)).to eq(0)
25
+ Then { clicker.instance_count(klass) == 1 }
28
26
  end
29
27
 
30
- it 'tracks differences in symbols' do
31
- difference = 100
28
+ describe 'symbols' do
29
+ Given { clicker.click! }
30
+ Given!(:original_symbol_count) { clicker.instance_count(Symbol) }
32
31
 
33
- expect {
34
- difference.times { |i| [Time.now.to_i, i].join('_-_-_').to_sym }
35
- }.to change {
36
- clicker.click!
37
- clicker.instance_count(Symbol)
38
- }.by_at_least(difference)
32
+ When { 100.times { |i| [Time.now.to_i, i].join('_-_').to_sym }; clicker.click! }
33
+
34
+ Then { clicker.instance_count(Symbol) >= original_symbol_count + 100 }
39
35
  end
40
36
 
41
- it 'counts BasicObjects as indeterminable' do
42
- difference = 100
43
- stuff = []
37
+ describe 'BasicObject' do
38
+ Given { clicker.click! }
39
+ Given!(:original_count) { clicker.instance_count(Click::Indeterminable) }
44
40
 
45
- expect {
46
- difference.times { |i| stuff << BasicObject.new }
47
- }.to change {
48
- clicker.click!
49
- clicker.instance_count(Click::Indeterminable)
50
- }.by_at_least(difference)
41
+ When { stuff = 25.times.map { BasicObject.new }; clicker.click! }
42
+
43
+ Then { clicker.instance_count(Click::Indeterminable) >= original_count + 25 }
51
44
  end
52
45
 
53
- it 'counts objects that have overridden #class as indeterminable' do
54
- difference = 100
55
- stuff = []
56
- klass = Class.new do
57
- def class
58
- nil
46
+ describe 'objects that poorly override #class' do
47
+ # This test was flaking out with the Given setup. *shrug*
48
+ before do
49
+ klass = Class.new do
50
+ def class
51
+ nil
52
+ end
59
53
  end
54
+ clicker.click!
55
+ @original_count = clicker.instance_count(Click::Indeterminable)
56
+
57
+ @stuff = 33.times.map { klass.new }
60
58
  end
61
59
 
62
- expect {
63
- difference.times { |i| stuff << klass.new }
64
- }.to change {
65
- clicker.click!
66
- clicker.instance_count(Click::Indeterminable)
67
- }.by_at_least(difference)
60
+ When { clicker.click! }
61
+
62
+ Then { clicker.instance_count(Click::Indeterminable) >= @original_count + 33 }
68
63
  end
69
64
  end
70
65
 
71
66
  describe '#object_counts' do
72
- it "groups objects by their class" do
73
- ObjectSpace.should_receive(:each_object).and_yield(Object.new).and_yield(":)").and_yield(":(")
74
- Symbol.should_receive(:all_symbols).and_return([:foo, :bar, :baz])
75
-
76
- clicker.click!
77
- expect(clicker.object_counts).to be_a(Hash)
78
- expect(clicker.object_counts.to_a).to eq([[Object, 1], [String, 2], [Symbol, 3]])
67
+ Given do
68
+ ObjectSpace.stub(:each_object).and_yield(Object.new).and_yield(":)").and_yield(":(")
69
+ Symbol.stub(:all_symbols).and_return([:foo, :bar, :baz])
79
70
  end
71
+
72
+ When { clicker.click! }
73
+
74
+ Then { clicker.object_counts == {Object => 1, String => 2, Symbol => 3} }
75
+ And { expect(ObjectSpace).to have_received(:each_object).with(no_args) }
76
+ And { expect(Symbol).to have_received(:all_symbols).with(no_args) }
80
77
  end
81
78
 
82
79
  describe 'observers' do
83
- it 'calls #on_add' do
84
- observer = double(on_add: nil)
85
- observer.should_receive(:on_add).with(clicker)
80
+ Given(:observer) { double(on_add: nil, before_click: nil, after_click: nil) }
86
81
 
87
- clicker.add_observer(observer)
88
- end
82
+ When { clicker.add_observer(observer) }
83
+
84
+ Then { expect(observer).to have_received(:on_add).with(clicker) }
89
85
 
90
- it 'calls #before_click and #after_click' do
91
- observer = double(on_add: nil, before_click: nil, after_click: nil)
92
- observer.should_receive(:before_click).with(clicker).ordered
93
- observer.should_receive(:after_click).with(clicker).ordered
86
+ describe 'after click!' do
87
+ When { clicker.click! }
94
88
 
95
- clicker.add_observer(observer)
96
- clicker.click!
89
+ Then { expect(observer).to have_received(:before_click).with(clicker) }
90
+ Then { expect(observer).to have_received(:after_click).with(clicker) }
97
91
  end
98
92
  end
99
93
  end
@@ -5,40 +5,23 @@ initialize_models
5
5
 
6
6
  module Click::Database::Models
7
7
  describe ObjectCount do
8
- describe '.scopes' do
9
- describe '.by_session_name' do
10
- it 'returns only object counts belonging to the session with that name' do
11
- with_in_memory_db do
12
- ignored_session = Session.create(name: 'ignored', started_at: Time.now)
13
- ignored_snapshot = Snapshot.create(timestamp: Time.now, session_id: ignored_session.id)
14
- 50.times { ObjectCount.create(class_name: "Foo", count: 1, snapshot_id: ignored_snapshot.id) }
8
+ Given(:parent_session) { Session.create(name: SecureRandom.uuid, started_at: Time.now) }
9
+ Given(:parent_snapshot) { parent_session.add_snapshot(Snapshot.new(timestamp: Time.now)) }
10
+ Given(:object_count) { parent_snapshot.add_object_count(ObjectCount.new(class_name: SecureRandom.uuid, count: 1)) }
15
11
 
16
- important_session = Session.create(name: 'important', started_at: Time.now)
17
- important_snapshot = Snapshot.create(timestamp: Time.now, session_id: important_session.id)
18
- important_object_count = ObjectCount.create(class_name: "Bar", count: 2, snapshot_id: important_snapshot.id)
12
+ describe 'the .by_session_name scope' do
13
+ Given(:dataset) { ObjectCount.by_session_name(parent_session.name) }
19
14
 
20
- dataset = ObjectCount.by_session_name('important')
21
- expect(dataset.count).to eq(1)
22
- expect(dataset.map([:id, :class_name, :count])).to eq([[important_object_count.id, "Bar", 2]])
23
- end
24
- end
25
- end
15
+ Then { [object_count] == dataset.all }
16
+ end
26
17
 
27
- describe '.sessions' do
28
- it 'returns the sessions owning the object counts' do
29
- with_in_memory_db do
30
- ignored_session = Session.create(name: 'ignored', started_at: Time.now)
31
- ignored_snapshot = Snapshot.create(timestamp: Time.now, session_id: ignored_session.id)
32
- 50.times { ObjectCount.create(class_name: "Foo", count: 1, snapshot_id: ignored_snapshot.id) }
18
+ describe '.snapshots' do
19
+ Given(:other_snapshot) { parent_session.add_snapshot(Snapshot.new(timestamp: Time.now)) }
20
+ Given(:other_object_count) { other_snapshot.add_object_count(ObjectCount.new(class_name: SecureRandom.uuid, count: 1)) }
33
21
 
34
- important_session = Session.create(name: 'important', started_at: Time.now)
35
- important_snapshot = Snapshot.create(timestamp: Time.now, session_id: important_session.id)
36
- important_object_count = ObjectCount.create(class_name: "Bar", count: 2, snapshot_id: important_snapshot.id)
22
+ Given(:dataset) { ObjectCount.where(id: [object_count.id, other_object_count.id]).snapshots }
37
23
 
38
- expect(ObjectCount.where(class_name: 'Bar').snapshots.all).to eq([important_snapshot])
39
- end
40
- end
41
- end
24
+ Then { expect(dataset.all).to match_array([parent_snapshot, other_snapshot])}
42
25
  end
43
26
  end
44
27
  end
@@ -5,36 +5,22 @@ initialize_models
5
5
 
6
6
  module Click::Database::Models
7
7
  describe Session do
8
- describe '.scopes' do
9
- describe '.by_name' do
10
- it 'returns only object counts belonging to the session with that name' do
11
- with_in_memory_db do
12
- Session.create(name: 'ignored', started_at: Time.now)
8
+ Given(:session_name) { SecureRandom.uuid }
13
9
 
14
- important_session = Session.create(name: 'important', started_at: Time.now)
10
+ describe 'the .by_name dataset' do
11
+ Given(:session) { Session.create(name: session_name, started_at: Time.now) }
15
12
 
16
- dataset = Session.by_name('important')
17
- expect(dataset.all).to eq([important_session])
18
- end
19
- end
20
- end
21
-
22
- describe '.snapshots' do
23
- it 'returns all the snapshots belonging to the set of sessions' do
24
- with_in_memory_db do
25
- ignored_session = Session.create(name: 'ignored', started_at: Time.now)
26
- 50.times { ignored_session.add_snapshot(Snapshot.new(timestamp: Time.now))}
13
+ Then { [session] == Session.by_name(session_name).all }
14
+ end
27
15
 
28
- session = Session.create(name: 'important', started_at: Time.now)
29
- snapshot = session.add_snapshot(Snapshot.new(timestamp: Time.now))
16
+ describe 'the .snapshots dataset' do
17
+ Given!(:first_session) { Session.create(name: session_name, started_at: Time.now) }
18
+ Given!(:first_snapshot) { first_session.add_snapshot(Snapshot.new(timestamp: Time.now)) }
30
19
 
31
- other_session = Session.create(name: 'important', started_at: Time.now)
32
- other_snapshot = other_session.add_snapshot(Snapshot.new(timestamp: Time.now))
20
+ Given!(:other_session) { Session.create(name: session_name, started_at: Time.now) }
21
+ Given!(:other_snapshot) { other_session.add_snapshot(Snapshot.new(timestamp: Time.now)) }
33
22
 
34
- expect(Session.by_name('important').snapshots.map(:id)).to match_array([snapshot.id, other_snapshot.id])
35
- end
36
- end
37
- end
23
+ Then { expect(Session.by_name(session_name).snapshots.all).to match_array([first_snapshot, other_snapshot]) }
38
24
  end
39
25
  end
40
26
  end
@@ -5,85 +5,63 @@ initialize_models
5
5
 
6
6
  module Click::Database::Models
7
7
  describe Snapshot do
8
- let(:session_id) { Click::Database::Models::Session.create(name: "test", started_at: Time.now).id }
9
-
10
- describe 'scopes' do
11
- describe '.after' do
12
- it 'excludes records prior to the given time' do
13
- with_in_memory_db do
14
- described_class.create(timestamp: Date.new(2000), session_id: session_id)
15
- later = described_class.create(timestamp: Date.new(2006), session_id: session_id)
16
-
17
- expect(described_class.after(Date.new(2003)).all).to eq([later])
18
- end
19
- end
20
- end
21
-
22
- describe '.before' do
23
- it 'excludes records after the given time' do
24
- with_in_memory_db do
25
- earlier = described_class.create(timestamp: Date.new(2000), session_id: session_id)
26
- described_class.create(timestamp: Date.new(2006), session_id: session_id)
27
-
28
- expect(described_class.before(Date.new(2003)).all).to eq([earlier])
29
- end
30
- end
31
- end
32
-
33
- describe '.by_session_name' do
34
- it 'returns only snapshots belonging to the session with that name' do
35
- with_in_memory_db do
36
- ignored_session = Session.create(name: 'ignored', started_at: Time.now)
37
- 50.times { Snapshot.create(timestamp: Time.now, session_id: ignored_session.id) }
8
+ Given!(:parent_session) { Session.create(name: SecureRandom.uuid, started_at: Time.now) }
9
+ describe 'the .before, .after scopes' do
10
+ Given!(:earlier) { Snapshot.create(timestamp: Date.new(2000), session_id: parent_session.id) }
11
+ Given!(:later) { Snapshot.create(timestamp: Date.new(2006), session_id: parent_session.id) }
12
+ Given(:dataset) { parent_session.snapshots_dataset }
13
+
14
+ Then { dataset.before(Date.new(2003)).all == [earlier] }
15
+ And { dataset.after(Date.new(2003)).all == [later] }
16
+ end
38
17
 
39
- important_session = Session.create(name: 'important', started_at: Time.now)
40
- important_snapshot = Snapshot.create(timestamp: Time.now, session_id: important_session.id)
18
+ describe 'the .by_session_name dataset' do
19
+ Given!(:snapshot) { Snapshot.create(timestamp: Time.now, session_id: parent_session.id) }
20
+ Given(:dataset) { Snapshot.by_session_name(parent_session.name) }
41
21
 
42
- dataset = Snapshot.by_session_name('important')
43
- expect(dataset.count).to eq(1)
44
- expect(dataset.map([:id, :timestamp, :session_id])).to eq([[important_snapshot.id, important_snapshot.timestamp, important_session.id]])
45
- end
46
- end
47
- end
22
+ Then { dataset.all == [snapshot] }
23
+ end
48
24
 
49
- describe '.sessions' do
50
- it 'returns sessions owning the current snapshots' do
51
- with_in_memory_db do
52
- Session.create(name: 'ignored', started_at: Time.now)
25
+ describe 'the .sessions dataset' do
26
+ Given(:other_parent_session) { Session.create(name: SecureRandom.uuid, started_at: Time.now) }
27
+ Given(:other_snapshot) { other_parent_session.add_snapshot(Snapshot.new(timestamp: Time.now)) }
53
28
 
54
- important_session = Session.create(name: 'important', started_at: Time.now)
55
- Snapshot.create(timestamp: Time.now, session_id: important_session.id)
29
+ Given(:snapshot) { parent_session.add_snapshot(Snapshot.new(timestamp: Time.now)) }
30
+ Given(:dataset) { Snapshot.where(id: [snapshot.id, other_snapshot.id]) }
56
31
 
57
- expect(Snapshot.dataset.sessions.all).to eq([important_session])
58
- end
59
- end
60
- end
32
+ Then { expect(dataset.sessions.all).to match_array([parent_session, other_parent_session]) }
33
+ end
61
34
 
62
- describe '.object_counts' do
63
- it 'returns object counts owned by the current snapshots' do
64
- with_in_memory_db do
65
- Session.create(name: 'ignored', started_at: Time.now).
66
- add_snapshot(Snapshot.new(timestamp: Time.now)).
67
- add_object_count(ObjectCount.new(class_name: 'Bar', count: 1))
35
+ describe '.object_counts' do
36
+ Given(:snapshot) { parent_session.add_snapshot(Snapshot.new(timestamp: Time.now)) }
37
+ Given(:class_name) { SecureRandom.uuid }
38
+ Given(:object_count) { snapshot.add_object_count(ObjectCount.new(class_name: class_name, count: 5)) }
39
+ Given(:dataset) { Snapshot.where(id: snapshot.id) }
68
40
 
69
- session = Session.create(name: 'important', started_at: Time.now)
70
- snapshot = Snapshot.create(timestamp: Time.now, session_id: session.id)
71
- object_count = snapshot.add_object_count(ObjectCount.new(class_name: 'Foo', count: 5))
41
+ Then { [object_count] == dataset.object_counts.all }
42
+ end
72
43
 
73
- expect(Snapshot.where(session_id: session.id).object_counts.all).to eq([object_count])
74
- end
75
- end
44
+ describe '#difference' do
45
+ Given(:first_snapshot) { Snapshot.create(timestamp: Time.now, session_id: parent_session.id) }
46
+ Given { first_snapshot.add_object_count(ObjectCount.new(class_name: "first_only", count: 10)) }
47
+ Given { first_snapshot.add_object_count(ObjectCount.new(class_name: "both", count: 1)) }
48
+ Given { first_snapshot.add_object_count(ObjectCount.new(class_name: "both_same", count: 2)) }
49
+
50
+ Given(:second_snapshot) { Snapshot.create(timestamp: Time.now, session_id: parent_session.id) }
51
+ Given { second_snapshot.add_object_count(ObjectCount.new(class_name: "both", count: 5)) }
52
+ Given { second_snapshot.add_object_count(ObjectCount.new(class_name: "both_same", count: 2)) }
53
+ Given { second_snapshot.add_object_count(ObjectCount.new(class_name: "second_only", count: 9)) }
54
+
55
+ Given(:expected_hash) do
56
+ {
57
+ "both" => 4,
58
+ "both_same" => 0,
59
+ "second_only" => 9,
60
+ "first_only" => -10,
61
+ }
76
62
  end
77
63
 
78
- it 'can combine scopes' do
79
- with_in_memory_db do
80
- described_class.create(timestamp: Date.new(2000), session_id: session_id)
81
- middle = described_class.create(timestamp: Date.new(2003), session_id: session_id)
82
- described_class.create(timestamp: Date.new(2006), session_id: session_id)
83
-
84
- expect(described_class.after(Date.new(2001)).before(Date.new(2005)).all).to eq([middle])
85
- end
86
- end
64
+ Then { second_snapshot.difference(first_snapshot) == expected_hash }
87
65
  end
88
66
  end
89
67
  end
@@ -1,33 +1,56 @@
1
1
  require 'spec_helper'
2
2
  require 'click/database'
3
+ require 'securerandom'
3
4
 
4
5
  initialize_models
5
6
 
6
7
  require 'click/database/writer'
7
8
 
8
- describe Click::Database::Writer do
9
- it 'writes records to the database' do
10
- with_in_memory_db do |db|
11
- writer = Click::Database::Writer.new(db)
12
- clicker = Click::Clicker.new("test")
13
-
14
- expect {
15
- clicker.add_observer(writer)
16
- }.to change { Click::Database::Models::Session.count }.from(0).to(1)
17
-
18
- expect(Click::Database::Models::Snapshot.count).to eq(0)
19
- expect(Click::Database::Models::ObjectCount.count).to eq(0)
20
-
21
- before_time = Time.now
22
- clicker.click!
23
- after_time = Time.now
24
-
25
- expect(Click::Database::Models::Snapshot.count).to eq(1)
26
- expect(Click::Database::Models::ObjectCount.count).to be > 0
27
- snapshot = Click::Database::Models::Snapshot.first
28
- expect(snapshot.object_counts.count).to eq(Click::Database::Models::ObjectCount.count)
29
- expect(snapshot.timestamp).to be >= before_time
30
- expect(snapshot.timestamp).to be <= after_time
9
+ module Click::Database
10
+ describe Writer do
11
+ Given(:writer) { Writer.new }
12
+ Given(:session_name) { SecureRandom.uuid }
13
+ Given(:clicker) { Click::Clicker.new(session_name) }
14
+
15
+ describe 'when added to the clicker' do
16
+ When { clicker.add_observer(writer) }
17
+
18
+ Then { Models::Session.where(name: session_name).count == 1 }
19
+
20
+ describe 'the session' do
21
+ Given(:session) { Models::Session[name: session_name] }
22
+
23
+ Then { session.snapshots_dataset.count == 0 }
24
+
25
+ context 'when a click happens' do
26
+ Given!(:before_click_time) { Time.now }
27
+ When { clicker.click! }
28
+
29
+ Then { session.snapshots_dataset.count == 1 }
30
+
31
+ describe 'the created snapshot' do
32
+ Given(:snapshot) { session.snapshots.first }
33
+
34
+ Then { snapshot.object_counts_dataset.count > 0 }
35
+ And { snapshot.timestamp >= before_click_time }
36
+ And { snapshot.timestamp <= Time.now }
37
+ end
38
+
39
+ context 'and another click happens' do
40
+ When { clicker.click! }
41
+
42
+ Then { session.snapshots_dataset.count == 2 }
43
+
44
+ describe 'the created snapshot' do
45
+ Given(:snapshot) { session.snapshots.last }
46
+
47
+ Then { snapshot.object_counts_dataset.count > 0 }
48
+ And { snapshot.timestamp >= before_click_time }
49
+ And { snapshot.timestamp <= Time.now }
50
+ end
51
+ end
52
+ end
53
+ end
31
54
  end
32
55
  end
33
56
  end
@@ -1,15 +1,14 @@
1
1
  $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
2
  require 'click'
3
-
4
- def with_in_memory_db
5
- Click::Database.with_in_memory_database do |db|
6
- Click::Database::Models::ObjectCount.dataset.delete
7
- Click::Database::Models::Snapshot.dataset.delete
8
-
9
- yield db
10
- end
11
- end
3
+ require 'securerandom'
4
+ require 'rspec/given'
12
5
 
13
6
  def initialize_models
14
7
  Click::Database.with_in_memory_database { }
15
8
  end
9
+
10
+ RSpec.configure do |c|
11
+ c.before(:suite) do
12
+ Click::Database.prepare(Sequel.sqlite)
13
+ end
14
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: click
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-12-04 00:00:00.000000000 Z
12
+ date: 2013-12-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -59,6 +59,38 @@ dependencies:
59
59
  - - ~>
60
60
  - !ruby/object:Gem::Version
61
61
  version: '2.14'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '2.14'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '2.14'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rspec-given
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: '3.1'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ~>
92
+ - !ruby/object:Gem::Version
93
+ version: '3.1'
62
94
  - !ruby/object:Gem::Dependency
63
95
  name: sqlite3
64
96
  requirement: !ruby/object:Gem::Requirement
@@ -107,7 +139,7 @@ dependencies:
107
139
  - - ! '>='
108
140
  - !ruby/object:Gem::Version
109
141
  version: '0'
110
- description: A tool to help track down the source of a memory leak in a Ruby process
142
+ description: A tool to help track down the source of an object leak in a Ruby process
111
143
  email:
112
144
  - mark.rushakoff@gmail.com
113
145
  executables: