click 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +1 -0
- data/README.md +10 -1
- data/bin/click-console +3 -0
- data/click.gemspec +3 -1
- data/demo/app.rb +7 -0
- data/lib/click/clicker.rb +2 -2
- data/lib/click/database.rb +15 -5
- data/lib/click/database/models/snapshot.rb +16 -1
- data/lib/click/database/writer.rb +1 -5
- data/lib/click/version.rb +1 -1
- data/spec/clicker_spec.rb +55 -61
- data/spec/database/models/object_count_spec.rb +12 -29
- data/spec/database/models/session_spec.rb +11 -25
- data/spec/database/models/snapshot_spec.rb +47 -69
- data/spec/database/writer_spec.rb +46 -23
- data/spec/spec_helper.rb +8 -9
- metadata +35 -3
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
# Click
|
2
2
|
|
3
|
-
|
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
|
data/bin/click-console
CHANGED
@@ -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|
|
data/click.gemspec
CHANGED
@@ -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
|
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
|
|
data/demo/app.rb
CHANGED
@@ -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
|
data/lib/click/clicker.rb
CHANGED
@@ -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
|
55
|
+
Click::Database.with_database(connection_string) do
|
56
56
|
require 'click/database/writer'
|
57
|
-
writer = Click::Database::Writer.new
|
57
|
+
writer = Click::Database::Writer.new
|
58
58
|
clicker = Click::Clicker.new(session_name)
|
59
59
|
clicker.add_observer(writer)
|
60
60
|
yield clicker
|
data/lib/click/database.rb
CHANGED
@@ -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
|
-
|
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
|
29
|
-
Click::Database::Models::Snapshot
|
30
|
-
Click::Database::Models::ObjectCount
|
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 :
|
23
|
+
attr_reader :session
|
28
24
|
end
|
29
25
|
end
|
30
26
|
end
|
data/lib/click/version.rb
CHANGED
data/spec/clicker_spec.rb
CHANGED
@@ -1,99 +1,93 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Click::Clicker do
|
4
|
-
|
4
|
+
Given(:session_name) { SecureRandom.uuid }
|
5
|
+
Given(:clicker) { described_class.new(session_name) }
|
5
6
|
|
6
7
|
describe '#session_name' do
|
7
|
-
|
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
|
-
|
14
|
-
klass = Class.new
|
12
|
+
Given(:klass) { Class.new }
|
15
13
|
|
16
|
-
|
17
|
-
|
14
|
+
describe 'with no instances' do
|
15
|
+
When { clicker.click! }
|
18
16
|
|
19
|
-
|
17
|
+
Then { clicker.instance_count(klass) == 0 }
|
18
|
+
end
|
20
19
|
|
21
|
-
|
22
|
-
|
20
|
+
describe 'with instances' do
|
21
|
+
Given!(:object) { klass.new }
|
23
22
|
|
24
|
-
|
23
|
+
When { clicker.click! }
|
25
24
|
|
26
|
-
clicker.
|
27
|
-
expect(clicker.instance_count(klass)).to eq(0)
|
25
|
+
Then { clicker.instance_count(klass) == 1 }
|
28
26
|
end
|
29
27
|
|
30
|
-
|
31
|
-
|
28
|
+
describe 'symbols' do
|
29
|
+
Given { clicker.click! }
|
30
|
+
Given!(:original_symbol_count) { clicker.instance_count(Symbol) }
|
32
31
|
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
42
|
-
|
43
|
-
|
37
|
+
describe 'BasicObject' do
|
38
|
+
Given { clicker.click! }
|
39
|
+
Given!(:original_count) { clicker.instance_count(Click::Indeterminable) }
|
44
40
|
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
73
|
-
ObjectSpace.
|
74
|
-
Symbol.
|
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
|
-
|
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
|
-
|
88
|
-
|
82
|
+
When { clicker.add_observer(observer) }
|
83
|
+
|
84
|
+
Then { expect(observer).to have_received(:on_add).with(clicker) }
|
89
85
|
|
90
|
-
|
91
|
-
|
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
|
-
|
96
|
-
clicker
|
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
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
17
|
-
|
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
|
-
|
21
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
10
|
+
describe 'the .by_name dataset' do
|
11
|
+
Given(:session) { Session.create(name: session_name, started_at: Time.now) }
|
15
12
|
|
16
|
-
|
17
|
-
|
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
|
-
|
29
|
-
|
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
|
-
|
32
|
-
|
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
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
40
|
-
|
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
|
-
|
43
|
-
|
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
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
55
|
-
|
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
|
-
|
58
|
-
|
59
|
-
end
|
60
|
-
end
|
32
|
+
Then { expect(dataset.sessions.all).to match_array([parent_session, other_parent_session]) }
|
33
|
+
end
|
61
34
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
70
|
-
|
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
|
-
|
74
|
-
|
75
|
-
|
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
|
-
|
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
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
data/spec/spec_helper.rb
CHANGED
@@ -1,15 +1,14 @@
|
|
1
1
|
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
2
|
require 'click'
|
3
|
-
|
4
|
-
|
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.
|
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-
|
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
|
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:
|