Skip to content
This repository

RSpec2 upgrade, faster :truncation strategy, spec/support/ design to test real db interactions (for AR), already working travis setup. (mainly addresses #126) #127

Closed
wants to merge 6 commits into from

3 participants

Stanislaw Pankevich Ben Mabey Vito Botta
Stanislaw Pankevich

1) 'Shared example group 'a generic strategy' already exists'.

See https://www.relishapp.com/rspec/rspec-core/docs/example-groups/shared-examples

If shared examples are in a separate file, it should be named *.rb, not
_spec.rb. This was the problem of double inclusion.

2) I did an upgrade to RSpec2.

3 of 4 specs are failing because of I don't have MongoDB, and right now
it is not an easy task to install it because of messy dependendencies my
Gentoo version has.

About the fourth failing spec - configuration_spec#210 - it seems to me that its flow is broken. Please, check carefully what you test there.

3) lib/database_cleaner/active_record/truncation.rb.

See the pieces of code for MySQL and MySQL2 and PostgreSQL adapters: they look very
similar at first glance, but notice the difference how 'mysql' and
'mysql2' gem handle parsing of results - each gem does it its own way. PostgreSQL fast truncation methods in their turn operate on groups of tables, while MySQL and MySQL2 operate each on one table. By exposing this, I just mean, that it is hard to write some common code they all reuse. I think it is up to you to decide, whether fast truncation methods deserve their own module in fx:
lib/database_cleaner/active_record/fast_truncation.rb, like

module DatabaseCleaner
  module ActiveRecord
    module FastTruncation
      module MySQL
      module MySQL2
      module PostgreSQL
      # ...

4) I made additional option :fast. Let's take some time for testing fast
truncation among the users which will like to try it. Also, it is good to have
it as an option, to just compare performance thus timing advantage, when
running large test suites! After you gather feedback from developers,
who succeeded using it, you can easily remove :fast option, making the
fast strategy used by default.

5) Travis

You need:

Go on http://travis-ci.org/ - an sign in there with your github account,
then visit your 'Profile' page, then toggle into on your database_cleaner
repository.

Then go to the 'home' page, 'My repositories' tab there and see if
database_cleaner appeared to begin doing builds.

Also here: https://github.com/bmabey/database_cleaner/hooks you will
see that travis hook activated - click on it - then to press "Test hook"
to begin doing first build.

Remember, that I've already added status image that points to database_cleaner
on travis-ci.org. It is not displayed before you enable database_cleaner on
travis.

.travis.yml - contains "1.9.3" version commented now, I'm sure specs should all pass too, it is 'linecache' gem which refuses to compile on Travis workers having Ubuntu. We need to check it, and all will be fine with 1.9.3.

Ben Mabey
Owner
bmabey commented July 09, 2012

Thanks @stanislaw! I've merged in the RSpec2 upgrade already and have been going over the fast truncation stuff now.

In DatabaseCleaner's README I mention that the :deletion strategy has been reported to be faster than truncate in certain circumstances. I decided to update your benchmark to use DELETE to see how it compares. These are the results I get with no records:

Truncate non-empty tables (AUTO_INCREMENT ensured)
  0.010000   0.010000   0.020000 (  0.028967)
Truncate non-empty tables (AUTO_INCREMENT is not ensured)
  0.010000   0.000000   0.010000 (  0.010334)
Truncate all tables one by one:
  0.000000   0.000000   0.000000 (  0.035987)
Truncate all tables with DatabaseCleaner:
  0.010000   0.000000   0.010000 (  0.036416)
Delete all tables one by one:
  0.000000   0.000000   0.000000 (  0.007189)
Delete non-empty tables one by one:
  0.000000   0.000000   0.000000 (  0.009258)

These are the results I get with 100 records:

Truncate non-empty tables (AUTO_INCREMENT ensured) 
  0.010000   0.000000   0.010000 (  0.042821)
Truncate non-empty tables (AUTO_INCREMENT is not ensured)
  0.010000   0.010000   0.020000 (  0.043030)
Truncate all tables one by one:
  0.000000   0.000000   0.000000 (  0.034902)
Truncate all tables with DatabaseCleaner:
  0.010000   0.000000   0.010000 (  0.085237)
Delete all tables one by one:
  0.000000   0.000000   0.000000 (  0.058040)
Delete non-empty tables one by one:
  0.020000   0.010000   0.030000 (  0.078900)

Here is running the same benchmark above but doing it 10 times for each method:

Truncate non-empty tables (AUTO_INCREMENT ensured)
  0.260000   0.040000   0.300000 (  1.164267)
Truncate non-empty tables (AUTO_INCREMENT is not ensured)
  0.300000   0.040000   0.340000 (  2.160942)
Truncate all tables one by one:
  0.180000   0.030000   0.210000 (  1.510205)
Truncate all tables with DatabaseCleaner:
  0.190000   0.020000   0.210000 (  1.137671)
Delete all tables one by one:
  0.200000   0.030000   0.230000 (  1.612719)
Delete non-empty tables one by one:
  0.250000   0.040000   0.290000 (  1.585001)

(I should really be taking the average and standard deviation of each run and plotting them, but I don't have the time to do that now. I am skeptical of this benchmark in general since I do see a good amount of variance. If I get more time I'll improve on it.)

The benchmark isn't perfect but the general trends I've seen is that:

  • When you have a lot of tables that haven't been modified under a test then the fastest approach is to use DELETE (or the :deletion strategy in DatabaseCleaner).
  • When all your tables are populated truncating all the tables (without any checks) is the fastest approach.

In real life situations you will end up with a mix of the above. That is why for some people DELETE is faster for them, while for others TRUNCATE is faster. It all depends on the test suite. That said, I would bet that on average DELETE would be the better option and I should probably update the README to reflect this finding.

Have you tried using the :deletion strategy on your test suite? I suspect that it would outperform your fast truncate. Give it a try and let me know how it goes.

Stanislaw Pankevich

I push improved procedure in https://github.com/stanislaw/truncate-vs-count, please run it to see results yourself. Now I'm going to write expanded answer about your comment.

Stanislaw Pankevich

MySQL: DELETE wins only in the case if it operates on empty tables. This is very specific situation, so when building a strategy covering all possible test cases you cannot rely on just DELETE without SELECT EXISTS check, if you want a win for all setups of N & NUM_RECORDS. SELECT EXISTS CHECKS - are very fast (less than 1ms), so you can improve :delete strategy for both MySQL and PostgreSQL with fast option exactly using the code you've added to mysql code in truncate_vs_count repository (I've added lightly modified version using DELETE FROM for PG as well).

PostgreSQL: DELETE operates AMAZINGLY fast, faster than TRUNCATE. It even outperforms fast truncation - it is very big difference. But! it is only in PG. In MySQL DELETE DOESN'T behave SUCH FAST!

But the most important point, that your comment actually is out of scope of my pull request addresses. I was acting in the scope of :truncation strategy DatabaseCleaner has, so I think my PR code could be merged in :truncation strategy without any change.

But if you want the best performance possible, we could think of kind of :mixed strategy for DatabaseCleaner, having TRUNCATE and DELETE in appropriate places. But this not an easy task to guess the best option working for all versions of PG and MySQL and do a test covering all possible cases.

Stanislaw Pankevich

General conclusion I make is:
Truncation strategy works its best when :fast is enabled (code from my PR) for both MySQL and PG.
Deletion strategy works the best as it is for PostgreSQL and with DELETE FROM on empty tables for MySQL.
I thing this not about creating :mixed strategy, but just using :fast truncation for MySQL and :deletion strategy for PG (actually non-empty deletion does not improve deletion strategy for PG somehow seriously, so we could just drop the idea of possible adding :fast option to deletion strategy for any adapters at all).

What do you think?

Stanislaw Pankevich

I've run more different setups of N, NUM_RECORDS - so generalizing it even more:

Best strategy for MySQL is fast :truncation,
for PG - :deletion with checking empty tables.

I am waiting for your results.

Stanislaw Pankevich

And the last: for MySQL fast :deletion is faster in general than just :deletion.

Stanislaw Pankevich

I did real testing of :fast truncation strategy applied to my rather large project using MySQL and having large number of tables.

I did the following setup of my env.rb file:

# Null strategy to force Cucumber to not interact with DatabaseCleaner
# in any possible ways.
# For example, Cucumber's default :truncation js strategy 
# is hardcoded to use DatabaseCleaner in old non-fast way.
class NullStrategy < Cucumber::Rails::Database::Strategy
  def before_js
  end

  def before_non_js
  end
end

Cucumber::Rails::Database.javascript_strategy = NullStrategy

AfterConfiguration do
   DatabaseCleaner.clean_with :truncation, { :fast => true, :reset_ids => false }
end

After do
  DatabaseCleaner.clean_with :truncation, { :fast => true, :reset_ids => false }
end

It works AMAZINGLY FAST without any problems. I am going to use my fork until the moment when you will merge :fast truncation so that I hope very much.

Also I can suggest myself doing analogous commit for fast :deletion with counts strategy, if you like.

Ben Mabey
Owner
bmabey commented July 10, 2012

Would you mind sharing more information about your particular app? For example, how many tables exist, how many tables on average does a single scenario populate.. Also, it would be interesting to see the time difference between :truncation, :truncation, { :fast => true, :reset_ids => false } and :deletion so I have an idea of the kind of gains other people may expect to see.

Ben Mabey
Owner
bmabey commented July 10, 2012

BTW, here are the results from my computer (MySQL)...

With no records:

Truncate non-empty tables (AUTO_INCREMENT ensured)
  0.010000   0.000000   0.010000 (  0.030524)
Truncate non-empty tables (AUTO_INCREMENT is not ensured)
  0.000000   0.000000   0.000000 (  0.009543)
Truncate all tables one by one:
  0.000000   0.000000   0.000000 (  0.031045)
Truncate all tables with DatabaseCleaner:
  0.010000   0.000000   0.010000 (  0.031126)
Delete all tables one by one:
  0.010000   0.000000   0.010000 (  0.007151)
Delete non-empty tables one by one:
  0.000000   0.000000   0.000000 (  0.008464)

With 100 records in each of the 30 tables:

Truncate non-empty tables (AUTO_INCREMENT ensured)
  0.010000   0.010000   0.020000 (  0.062481)
Truncate non-empty tables (AUTO_INCREMENT is not ensured)
  0.010000   0.010000   0.020000 (  0.041582)
Truncate all tables one by one:
  0.010000   0.000000   0.010000 (  0.032512)
Truncate all tables with DatabaseCleaner:
  0.010000   0.000000   0.010000 (  0.051319)
Delete all tables one by one:
  0.010000   0.010000   0.020000 (  0.058828)
Delete non-empty tables one by one:
  0.010000   0.000000   0.010000 (  0.099430)

And because I was curious, here is when only 5 of the 30 tables are populated with 100 records (since often time in tests you may not touch the entire system):

Truncate non-empty tables (AUTO_INCREMENT ensured)
  0.010000   0.000000   0.010000 (  0.048525)
Truncate non-empty tables (AUTO_INCREMENT is not ensured)
  0.010000   0.000000   0.010000 (  0.014333)
Truncate all tables one by one:
  0.000000   0.000000   0.000000 (  0.030471)
Truncate all tables with DatabaseCleaner:
  0.000000   0.000000   0.000000 (  0.031658)
Delete all tables one by one:
  0.000000   0.000000   0.000000 (  0.014889)
Delete non-empty tables one by one:
  0.000000   0.000000   0.000000 (  0.018202)
Stanislaw Pankevich

I wonder, how DELETE can perform so fast on your machine. I've NEVER seen, it behaves such fast on two mine machines. I use MySQL 5.1.62-r1 on Gentoo Linux.

Ben Mabey
Owner
bmabey commented July 10, 2012

I'm using 5.1.49 on OSx Lion, on a Macbook Pro with a 2.53 GHz Intel Core i5.

Stanislaw Pankevich

Would you mind sharing more information about your particular app?

Sure!

I have Rails 3.2.6 app, 46 tables total. MySQL and 'mysql2' gem.

My test suite has 74 scenarios, 588 steps.

Each test fills 3-10 tables.

DatabaseCleaner.strategy = :truncation
# 9m56.995s
DatabaseCleaner.strategy = :deletion
# 5m32,286s
DatabaseCleaner.strategy = :truncation, { :fast => true, :reset_ids => false }
# 5m0.997s

UPD: As I said earlier, I can't rely on "shared connection + :transaction strategy" now because of :webkit Capybara driver, so these are the only options I can use. Definitely, I will be using :truncation, { :fast => true, :reset_ids => false }, in all my Rails projects having webkit or poltergeist.

Stanislaw Pankevich

Are you going to merge this commits? I am interested in this, because I am still holding my attention on this case, thus being ready to place some new additions or improvements, you may like to add.

Actually, using my own fork is already enough for my needs, but consistency question is very valuable for me.

Please, let me know, if you are hesitating about doing it for some reasons.

Ben Mabey
Owner
bmabey commented July 11, 2012

I plan on merging in the functionality. I have it merged in locally, but before I push I want to do some cleanup first (e.g. creating an easy way for people to setup the needed DBs for mysql and postgres). I think I will also change the :fast option to :pre_count or :empty_check since I think that communicates better what it is actually doing.

You can continue working off of your fork and I'll let you know when it has been merged into master along with the option name change.

Stanislaw Pankevich

Good! Thanks!

Stanislaw Pankevich

By the way, if you rewrite your README with markdown markup, you will have github-colored ruby-syntax for your examples. This is because of Github favours the usage of markdown (Github flavored markdown) instead of textile.

I've already did this painless migration from Textile to Markdown for a bunch of my gems.

Ben Mabey
Owner
bmabey commented July 11, 2012

Textile's table formatting is helpful for this README.. But I may still convert to markdown.

Stanislaw Pankevich

I am +1 for :empty_check as a name for the option.

Ben Mabey
Owner
bmabey commented July 11, 2012

Okay, I'll go with :empty_check. Thanks for the feedback!

Vito Botta

Just wanted to say thanks to bmabey for this so useful gem, and to stanislaw for his changes - I was struggling to get capybara+poltergeist to work properly with the transaction strategy and the shared connection method, due to the "This connection is still waiting for a result issue" and the tons of hacks I have tried to mitigate it.

I tried your fork and the fast truncation method, and all works great and, to my surprise, the speed is the same as with the transaction method, with the difference that I don't have to fight with the connection issues.

I have a very fast, last generation SSD though on my MBP2011. Do you think I would see different results with a normal hard drive?

Ben Mabey
Owner
bmabey commented July 12, 2012

Thanks for the field report @vitobotta!

WRT SSD vs harddrive... The hardrive would certainly be slower but if the whole database was small enough to fit into ram you probably wouldn't notice much of a difference.

Out of curiosity, could you share a similar report to what @stanislaw posted about his app? (Number of tables, average number of tables used per test, and then the speed for the various cleaning strategies.)

Vito Botta

The app I am working on at the moment has 30 tables, and I'd say that perhaps 3-4 is the average number of tables used per test (just guessing really, I would need to properly investigate to give an accurate figure...).

The full Cucumber suite runs in around 5'30" (5'22" right now) with the poltergeist driver for javascript scenarios, and the fast truncation strategy. With the transaction strategy and the shared connection method I save ~10 seconds but most often some scenarios fail with the "This connection is still waiting for a result" exception. I have found a few hacks that mitigate this issue by retrying the failing code or forcing reconnection and things like these, but there's no much difference after all, so for now I won't be struggling any more with trying to get the transaction strategy fully working with poltergeist (or webkit).

I've also tried again right now with Akephalos2 + transaction/shared connection method, and the full Cucumber suite ran in 5'39".

Ben Mabey
Owner
bmabey commented July 12, 2012

Would you mind running the suite with :truncation (no fast option) and also with :deletion?

Stanislaw Pankevich

@vitobotta, I have almost exact speedup and the similar issues with :webkit, :poltergeist (i.e. 'This connection is still waiting for result' and need for the hacks with reconnection'). I am glad you found it very useful.

Stanislaw Pankevich

Any updates on this?

Ben Mabey bmabey referenced this pull request from a commit July 25, 2012
Commit has since been removed from the repository and is no longer available.
Ben Mabey bmabey referenced this pull request from a commit July 25, 2012
Ben Mabey WIP - adds DB config and rake tasks so test DBs can be created easily
I had to bump ActiveRecord to get the "standalone_migrations" rake tasks
to work.  The task "rake db:create:all" works but the AR upgrade is
causing errors in some of the other parts of the spec suite.

I'll need to get these errors resolved before moving forward. #127
9a8b60a
Ben Mabey bmabey referenced this pull request from a commit July 25, 2012
Ben Mabey WIP - adds DB config and rake tasks so test DBs can be created easily
I had to bump ActiveRecord to get the "standalone_migrations" rake tasks
to work.  The task "rake db:create:all" works but the AR upgrade is
causing errors in some of the other parts of the spec suite.

I'll need to get these errors resolved before moving forward. #127
9fa407b
Stanislaw Pankevich

So the last thing to be done is to rename strategy :fast => :pre_count, right? Who will do this, you or me? This time, I suggest you to do it, because I'm sure you will anyway need to do a post-work on syntax cleanups after my changes.

Ben Mabey
Owner

I'm doing this now.. I rebased to master and am doing some post-work now. Quick question.. in the MysqlAdapter you have:

def truncate_table_no_id_reset(table_name)
        rows_exist = execute("SELECT EXISTS (SELECT 1 FROM #{quote_table_name(table_name)} LIMIT 1)").fetch_row.first.to_i
        truncate_table(table_name) if rows_exist > 0
      end

(https://github.com/stanislaw/database_cleaner/blob/master/lib/database_cleaner/active_record/truncation.rb#L83)

But in the Mysql2Adapter you have:

     def truncate_table_no_id_reset(table_name)
        rows_exist = execute("SELECT EXISTS(SELECT 1 FROM #{quote_table_name(table_name)} LIMIT 1)").first.first
        truncate_table(table_name) if rows_exist == 1
      end

(https://github.com/stanislaw/database_cleaner/blob/master/lib/database_cleaner/active_record/truncation.rb#L126)

Why is the conditional different? In one you have if rows_exist > 0 and the other you have if rows_exist == 1. Seems like they could be the same, correct?

Ben Mabey
Owner

never mind... I was able to use the same code for MysqlAdapter and Mysql2Adapter and the tests passed so I don't think the difference mattered.

(commit: ca00f7d)

Stanislaw Pankevich

Yeah, you're right. They are the same.

Stanislaw Pankevich

Also, you could uncomment -1.9.3 in travis and see what happens with dependencies, when travis is doing a build for 1.9.3.

If it will require too much time to resolve it, please, let me know. I could try to help you with it, after you finish the work on fast_merge, if you would like.

I think it should be easy to set database_cleaner on travis with 1.9.3.

Ben Mabey bmabey closed this August 05, 2012
Ben Mabey
Owner

Sorry it took me so long to sit down and actually merge this in.. but it is done! There is some more refactoring I'd like to do (related to the per-existing monkeypatching) but I think it is best to get this in master for now. Thanks again for the patch and help!

BTW, any reason why I need the special travis rake task? Why not the default rake task?

Stanislaw Pankevich

rake travis just has a subset of entire spec suite, that works for for me on travis.

As an example, I didn't touch Mongo specs at. If you manage to have a successful build with the whole suite running, it would be great.

Stanislaw Pankevich

Ah, I see, that you've done it already! Now 1.9.3 build fails because of linecache dependency - I had it failing on the same point.

By the way, I really like the way you've merged this ;)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.

Showing 35 changed files with 763 additions and 135 deletions. Show diff stats Hide diff stats

  1. 4  .rspec
  2. 2  .rvmrc
  3. 10  .travis.yml
  4. 5  Gemfile
  5. 127  Gemfile.lock
  6. 2  History.txt
  7. 2  README.textile
  8. 20  Rakefile
  9. 2  examples/features/support/env.rb
  10. 2  features/support/env.rb
  11. 140  lib/database_cleaner/active_record/truncation.rb
  12. 6  lib/database_cleaner/generic/truncation.rb
  13. 2  spec/database_cleaner/active_record/base_spec.rb
  14. 40  spec/database_cleaner/active_record/truncation/mysql2_spec.rb
  15. 40  spec/database_cleaner/active_record/truncation/mysql_spec.rb
  16. 103  spec/database_cleaner/active_record/truncation/postgresql_spec.rb
  17. 63  spec/database_cleaner/active_record/truncation/shared_mysql.rb
  18. 121  spec/database_cleaner/active_record/truncation_spec.rb
  19. 4  spec/database_cleaner/configuration_spec.rb
  20. 2  spec/database_cleaner/data_mapper/base_spec.rb
  21. 2  spec/database_cleaner/data_mapper/transaction_spec.rb
  22. 2  spec/database_cleaner/data_mapper/truncation_spec.rb
  23. 2  spec/database_cleaner/generic/base_spec.rb
  24. 30  spec/database_cleaner/generic/truncation_spec.rb
  25. 2  spec/database_cleaner/mongo_mapper/base_spec.rb
  26. 2  spec/database_cleaner/sequel/base_spec.rb
  27. 2  spec/database_cleaner/sequel/transaction_spec.rb
  28. 2  spec/database_cleaner/sequel/truncation_spec.rb
  29. 0  spec/database_cleaner/{shared_strategy_spec.rb → shared_strategy.rb}
  30. 7  spec/spec.opts
  31. 10  spec/spec_helper.rb
  32. 41  spec/support/active_record/mysql2_setup.rb
  33. 41  spec/support/active_record/mysql_setup.rb
  34. 45  spec/support/active_record/postgresql_setup.rb
  35. 13  spec/travis.rb
4  .rspec
... ...
@@ -0,0 +1,4 @@
  1
+--color
  2
+--format documentation
  3
+mtime
  4
+--backtrace
2  .rvmrc
... ...
@@ -1 +1 @@
1  
-rvm ruby-1.8.7-p248
  1
+rvm 1.8.7
10  .travis.yml
... ...
@@ -0,0 +1,10 @@
  1
+language: ruby
  2
+rvm:
  3
+  - 1.8.7
  4
+  # - 1.9.3
  5
+script: "bundle exec rake travis"
  6
+gemfile:
  7
+  - Gemfile
  8
+before_script:
  9
+  - mysql -e 'create database database_cleaner_test;'
  10
+  - psql -c 'create database database_cleaner_test;' -U postgres
5  Gemfile
@@ -34,10 +34,13 @@ group :development do
34 34
   gem "couch_potato",         "0.3.0"
35 35
   gem "sequel",               "~>3.21.0"
36 36
   #gem "ibm_db"  # I don't want to add this dependency, even as a dev one since it requires DB2 to be installed
  37
+  gem 'mysql'
  38
+  gem 'mysql2', '~> 0.2.0'
  39
+  gem 'pg'
37 40
 end
38 41
 
39 42
 group :test do
40  
-  gem "rspec"
  43
+  gem "rspec-rails"
41 44
   gem "rspactor"
42 45
   gem "rcov"
43 46
   gem "ZenTest"
127  Gemfile.lock
... ...
@@ -1,28 +1,27 @@
1 1
 GEM
2 2
   remote: http://rubygems.org/
3 3
   specs:
4  
-    ZenTest (4.3.3)
  4
+    ZenTest (4.8.1)
5 5
     activerecord (2.3.8)
6 6
       activesupport (= 2.3.8)
7 7
     activesupport (2.3.8)
8  
-    addressable (2.2.0)
9  
-    bson (1.0.4)
10  
-    builder (2.1.2)
11  
-    columnize (0.3.1)
  8
+    addressable (2.2.8)
  9
+    bson (1.0.9)
  10
+    builder (3.0.0)
  11
+    columnize (0.3.6)
12 12
     couch_potato (0.3.0)
13 13
       couchrest (>= 0.24)
14 14
       json
15  
-    couchrest (1.0.1)
  15
+    couchrest (1.1.2)
  16
+      mime-types (~> 1.15)
  17
+      multi_json (~> 1.0.0)
  18
+      rest-client (~> 1.6.1)
  19
+    cucumber (1.2.1)
  20
+      builder (>= 2.1.2)
  21
+      diff-lcs (>= 1.1.3)
  22
+      gherkin (~> 2.11.0)
16 23
       json (>= 1.4.6)
17  
-      mime-types (>= 1.15)
18  
-      rest-client (>= 1.5.1)
19  
-    cucumber (0.8.5)
20  
-      builder (~> 2.1.2)
21  
-      diff-lcs (~> 1.1.2)
22  
-      gherkin (~> 2.1.4)
23  
-      json_pure (~> 1.4.3)
24  
-      term-ansicolor (~> 1.0.4)
25  
-    data_objects (0.10.2)
  24
+    data_objects (0.10.8)
26 25
       addressable (~> 2.1)
27 26
     datamapper (1.0.0)
28 27
       dm-aggregates (= 1.0.0)
@@ -35,7 +34,7 @@ GEM
35 34
       dm-transactions (= 1.0.0)
36 35
       dm-types (= 1.0.0)
37 36
       dm-validations (= 1.0.0)
38  
-    diff-lcs (1.1.2)
  37
+    diff-lcs (1.1.3)
39 38
     dm-aggregates (1.0.0)
40 39
       dm-core (~> 1.0.0)
41 40
     dm-constraints (1.0.0)
@@ -68,32 +67,28 @@ GEM
68 67
       uuidtools (~> 2.1.1)
69 68
     dm-validations (1.0.0)
70 69
       dm-core (~> 1.0.0)
71  
-    do_sqlite3 (0.10.2)
72  
-      data_objects (= 0.10.2)
  70
+    do_sqlite3 (0.10.8)
  71
+      data_objects (= 0.10.8)
73 72
     durran-validatable (2.0.1)
74 73
     extlib (0.9.15)
75  
-    fastercsv (1.5.3)
76  
-    ffi (0.6.3)
77  
-      rake (>= 0.8.7)
78  
-    gemcutter (0.6.1)
79  
-    gherkin (2.1.5)
80  
-      trollop (~> 1.16.2)
  74
+    fastercsv (1.5.5)
  75
+    gherkin (2.11.1)
  76
+      json (>= 1.4.6)
81 77
     git (1.2.5)
82  
-    growl (1.0.3)
83  
-    jeweler (1.4.0)
84  
-      gemcutter (>= 0.1.0)
  78
+    jeweler (1.8.4)
  79
+      bundler (~> 1.0)
85 80
       git (>= 1.2.5)
86  
-      rubyforge (>= 2.0.0)
  81
+      rake
  82
+      rdoc
87 83
     jnunemaker-validatable (1.8.4)
88 84
       activesupport (>= 2.3.4)
89  
-    json (1.4.6)
  85
+    json (1.7.3)
90 86
     json_pure (1.4.6)
91  
-    libnotify (0.2.0)
92  
-      ffi (>= 0.6.2)
93  
-    linecache (0.43)
94  
-    mime-types (1.16)
95  
-    mongo (1.0.7)
96  
-      bson (>= 1.0.4)
  87
+    linecache (0.46)
  88
+      rbx-require-relative (> 0.0.4)
  89
+    mime-types (1.19)
  90
+    mongo (1.0.9)
  91
+      bson (>= 1.0.5)
97 92
     mongo_mapper (0.8.2)
98 93
       activesupport (>= 2.3.4)
99 94
       jnunemaker-validatable (~> 1.8.4)
@@ -104,38 +99,43 @@ GEM
104 99
       durran-validatable (>= 2.0.1)
105 100
       mongo (~> 1.0.1)
106 101
       will_paginate (< 2.9)
107  
-    plucky (0.3.4)
108  
-      mongo (~> 1.0.7)
109  
-    rake (0.8.7)
110  
-    rb-inotify (0.8.1)
111  
-      ffi (>= 0.5.0)
112  
-    rcov (0.9.8)
113  
-    rest-client (1.6.0)
  102
+    multi_json (1.0.4)
  103
+    mysql (2.8.1)
  104
+    mysql2 (0.2.18)
  105
+    pg (0.14.0)
  106
+    plucky (0.3.5)
  107
+      mongo (~> 1.0.8)
  108
+    rake (0.9.2.2)
  109
+    rbx-require-relative (0.0.9)
  110
+    rcov (1.0.0)
  111
+    rdoc (3.12)
  112
+      json (~> 1.4)
  113
+    rest-client (1.6.7)
114 114
       mime-types (>= 1.16)
115  
-    rspactor (0.7.0.beta.6)
116  
-      bundler (>= 1.0.0.rc.5)
117  
-      growl (>= 1.0.3)
118  
-      libnotify (>= 0.1.3)
119  
-      rb-inotify
120  
-      sys-uname (>= 0.8.4)
121  
-      trollop (>= 1.16.2)
122  
-    rspec (1.3.0)
123  
-    ruby-debug (0.10.3)
  115
+    rspactor (0.6.4)
  116
+    rspec (2.1.0)
  117
+      rspec-core (~> 2.1.0)
  118
+      rspec-expectations (~> 2.1.0)
  119
+      rspec-mocks (~> 2.1.0)
  120
+    rspec-core (2.1.0)
  121
+    rspec-expectations (2.1.0)
  122
+      diff-lcs (~> 1.1.2)
  123
+    rspec-mocks (2.1.0)
  124
+    rspec-rails (2.1.0)
  125
+      rspec (~> 2.1.0)
  126
+    ruby-debug (0.10.4)
124 127
       columnize (>= 0.1)
125  
-      ruby-debug-base (~> 0.10.3.0)
126  
-    ruby-debug-base (0.10.3)
  128
+      ruby-debug-base (~> 0.10.4.0)
  129
+    ruby-debug-base (0.10.4)
127 130
       linecache (>= 0.3)
128  
-    rubyforge (2.0.4)
129  
-      json_pure (>= 1.1.7)
130 131
     sequel (3.21.0)
131  
-    sqlite3-ruby (1.3.1)
  132
+    sqlite3 (1.3.6)
  133
+    sqlite3-ruby (1.3.3)
  134
+      sqlite3 (>= 1.3.3)
132 135
     stringex (1.1.0)
133  
-    sys-uname (0.8.4)
134  
-    term-ansicolor (1.0.5)
135  
-    trollop (1.16.2)
136 136
     tzinfo (0.3.22)
137  
-    uuidtools (2.1.1)
138  
-    will_paginate (2.3.14)
  137
+    uuidtools (2.1.2)
  138
+    will_paginate (2.3.16)
139 139
 
140 140
 PLATFORMS
141 141
   ruby
@@ -153,10 +153,13 @@ DEPENDENCIES
153 153
   json_pure
154 154
   mongo_mapper (= 0.8.2)
155 155
   mongoid (= 1.9.1)
  156
+  mysql
  157
+  mysql2 (~> 0.2.0)
  158
+  pg
156 159
   rake
157 160
   rcov
158 161
   rspactor
159  
-  rspec
  162
+  rspec-rails
160 163
   ruby-debug
161 164
   sequel (~> 3.21.0)
162 165
   sqlite3-ruby
2  History.txt
@@ -2,6 +2,8 @@
2 2
 
3 3
 == 0.8.0 (in git)
4 4
 
  5
+  * Faster truncation strategy for ActiveRecord with MySQL or PostgreSQL
  6
+  * Upgrade to RSpec 2
5 7
   * Support for Mongoid 3/Moped (Andrew Bennett)
6 8
 
7 9
 == 0.7.2 2012-03-21
2  README.textile
Source Rendered
@@ -7,6 +7,8 @@ that is testing with a database.
7 7
 
8 8
 ActiveRecord, DataMapper, Sequel, MongoMapper, Mongoid, and CouchPotato are supported.
9 9
 
  10
+!https://secure.travis-ci.org/bmabey/database_cleaner.png(Build Status)!:http://travis-ci.org/stanislaw/database_cleaner
  11
+
10 12
 Here is an overview of the strategies supported for each library:
11 13
 
12 14
 |_. ORM        |_.  Truncation  |_.  Transaction  |_.  Deletion    |
20  Rakefile
@@ -28,16 +28,20 @@ Rake::RDocTask.new do |rdoc|
28 28
   rdoc.rdoc_files.include('lib/**/*.rb')
29 29
 end
30 30
 
31  
-require 'spec/rake/spectask'
32  
-Spec::Rake::SpecTask.new(:spec) do |t|
33  
-  t.libs << 'lib' << 'spec'
34  
-  t.spec_files = FileList['spec/**/*_spec.rb']
  31
+
  32
+require 'rspec/core'
  33
+require 'rspec/core/rake_task'
  34
+RSpec::Core::RakeTask.new(:spec) do |spec|
  35
+  spec.pattern = FileList['spec/**/*_spec.rb']
  36
+end
  37
+
  38
+RSpec::Core::RakeTask.new(:travis) do |spec|
  39
+  spec.pattern = FileList['spec/travis.rb']
35 40
 end
36 41
 
37  
-Spec::Rake::SpecTask.new(:rcov) do |t|
38  
-  t.libs << 'lib' << 'spec'
39  
-  t.spec_files = FileList['spec/**/*_spec.rb']
40  
-  t.rcov = true
  42
+RSpec::Core::RakeTask.new(:rcov) do |spec|
  43
+  spec.pattern = 'spec/**/*_spec.rb'
  44
+  spec.rcov = true
41 45
 end
42 46
 
43 47
 begin
2  examples/features/support/env.rb
@@ -5,7 +5,7 @@
5 5
 require 'bundler'
6 6
 
7 7
 Bundler.setup
8  
-require 'spec/expectations'
  8
+require 'rspec/expectations'
9 9
 require 'ruby-debug'
10 10
 
11 11
 DB_DIR = "#{File.dirname(__FILE__)}/../../db"
2  features/support/env.rb
... ...
@@ -1,7 +1,7 @@
1 1
 $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
2 2
 require 'database_cleaner'
3 3
 
4  
-require 'spec/expectations'
  4
+require 'rspec/expectations'
5 5
 
6 6
 require 'test/unit/assertions'
7 7
 
140  lib/database_cleaner/active_record/truncation.rb
@@ -45,12 +45,86 @@ class MysqlAdapter < MYSQL_ADAPTER_PARENT
45 45
       def truncate_table(table_name)
46 46
         execute("TRUNCATE TABLE #{quote_table_name(table_name)};")
47 47
       end
  48
+
  49
+      def fast_truncate_tables *tables_and_opts
  50
+        opts = tables_and_opts.last.is_a?(::Hash) ? tables_and_opts.pop : {}
  51
+        reset_ids = opts[:reset_ids] != false
  52
+
  53
+        _tables = tables_and_opts.flatten
  54
+
  55
+        _tables.each do |table_name|
  56
+          if reset_ids
  57
+            truncate_table_with_id_reset(table_name)
  58
+          else
  59
+            truncate_table_no_id_reset(table_name)
  60
+          end
  61
+        end
  62
+      end
  63
+
  64
+      def truncate_table_with_id_reset(table_name)
  65
+        rows_exist = execute("SELECT EXISTS(SELECT 1 FROM #{quote_table_name(table_name)} LIMIT 1)").fetch_row.first.to_i
  66
+
  67
+        if rows_exist == 0
  68
+          auto_inc = execute(<<-AUTO_INCREMENT
  69
+              SELECT Auto_increment 
  70
+              FROM information_schema.tables 
  71
+              WHERE table_name='#{table_name}';
  72
+          AUTO_INCREMENT
  73
+          )
  74
+
  75
+          truncate_table(table_name) if auto_inc.fetch_row.first.to_i > 1
  76
+        else
  77
+          truncate_table(table_name)
  78
+        end
  79
+      end
  80
+
  81
+      def truncate_table_no_id_reset(table_name)
  82
+        rows_exist = execute("SELECT EXISTS (SELECT 1 FROM #{quote_table_name(table_name)} LIMIT 1)").fetch_row.first.to_i
  83
+        truncate_table(table_name) if rows_exist > 0
  84
+      end
48 85
     end
49 86
 
50 87
     class Mysql2Adapter < MYSQL2_ADAPTER_PARENT
51 88
       def truncate_table(table_name)
52 89
         execute("TRUNCATE TABLE #{quote_table_name(table_name)};")
53 90
       end
  91
+
  92
+      def fast_truncate_tables *tables_and_opts
  93
+        opts = tables_and_opts.last.is_a?(::Hash) ? tables_and_opts.pop : {}
  94
+        reset_ids = opts[:reset_ids] != false
  95
+
  96
+        _tables = tables_and_opts.flatten
  97
+
  98
+        _tables.each do |table_name|
  99
+          if reset_ids
  100
+            truncate_table_with_id_reset(table_name)
  101
+          else
  102
+            truncate_table_no_id_reset(table_name)
  103
+          end
  104
+        end
  105
+      end
  106
+
  107
+      def truncate_table_with_id_reset(table_name)
  108
+        rows_exist = execute("SELECT EXISTS(SELECT 1 FROM #{quote_table_name(table_name)} LIMIT 1)").first.first.to_i
  109
+
  110
+        if rows_exist == 0
  111
+          auto_inc = execute(<<-AUTO_INCREMENT
  112
+              SELECT Auto_increment 
  113
+              FROM information_schema.tables 
  114
+              WHERE table_name='#{table_name}';
  115
+          AUTO_INCREMENT
  116
+          )
  117
+
  118
+          truncate_table(table_name) if auto_inc.first.first.to_i > 1
  119
+        else
  120
+          truncate_table(table_name)
  121
+        end
  122
+      end
  123
+
  124
+      def truncate_table_no_id_reset(table_name)
  125
+        rows_exist = execute("SELECT EXISTS(SELECT 1 FROM #{quote_table_name(table_name)} LIMIT 1)").first.first
  126
+        truncate_table(table_name) if rows_exist == 1
  127
+      end
54 128
     end
55 129
 
56 130
     class IBM_DBAdapter < AbstractAdapter
@@ -94,12 +168,60 @@ def restart_identity
94 168
       def truncate_table(table_name)
95 169
         truncate_tables([table_name])
96 170
       end
97  
-      
  171
+
98 172
       def truncate_tables(table_names)
99 173
         return if table_names.nil? || table_names.empty?
100 174
         execute("TRUNCATE TABLE #{table_names.map{|name| quote_table_name(name)}.join(', ')} #{restart_identity} #{cascade};")
101 175
       end
102 176
 
  177
+      def fast_truncate_tables(*tables_and_opts)
  178
+        opts = tables_and_opts.last.is_a?(::Hash) ? tables_and_opts.pop : {}
  179
+        reset_ids = opts[:reset_ids] != false
  180
+
  181
+        _tables = tables_and_opts.flatten
  182
+
  183
+        if reset_ids
  184
+          truncate_tables_with_id_reset(_tables)
  185
+        else
  186
+          truncate_tables_no_id_reset(_tables)
  187
+        end
  188
+      end
  189
+
  190
+      def truncate_tables_with_id_reset(_tables)
  191
+        tables_to_truncate = []
  192
+
  193
+        _tables.each do |table|
  194
+          begin
  195
+            table_curr_value = execute(<<-CURR_VAL
  196
+              SELECT currval('#{table}_id_seq');
  197
+            CURR_VAL
  198
+            ).first['currval'].to_i
  199
+          rescue ActiveRecord::StatementInvalid
  200
+            table_curr_value = nil
  201
+          end
  202
+
  203
+          if table_curr_value && table_curr_value > 0
  204
+            tables_to_truncate << table
  205
+          end
  206
+        end
  207
+
  208
+        truncate_tables(tables_to_truncate) if tables_to_truncate.any?
  209
+      end
  210
+
  211
+      def truncate_tables_no_id_reset(_tables)
  212
+        tables_to_truncate = []
  213
+
  214
+        _tables.each do |table|
  215
+          rows_exist = execute(<<-TR
  216
+            SELECT true FROM #{table} LIMIT 1;
  217
+          TR
  218
+          )
  219
+
  220
+          tables_to_truncate << table if rows_exist.any?
  221
+        end
  222
+
  223
+        truncate_tables(tables_to_truncate) if tables_to_truncate.any?
  224
+      end
103 225
     end
104 226
 
105 227
     class SQLServerAdapter < AbstractAdapter
@@ -130,7 +252,11 @@ class Truncation
130 252
     def clean
131 253
       connection = connection_klass.connection
132 254
       connection.disable_referential_integrity do
133  
-        connection.truncate_tables(tables_to_truncate(connection))
  255
+        if fast? && connection.respond_to?(:fast_truncate_tables)
  256
+          connection.fast_truncate_tables(tables_to_truncate(connection), {:reset_ids => reset_ids?})
  257
+        else
  258
+          connection.truncate_tables(tables_to_truncate(connection))
  259
+        end
134 260
       end
135 261
     end
136 262
 
@@ -145,8 +271,12 @@ def migration_storage_name
145 271
       'schema_migrations'
146 272
     end
147 273
 
  274
+    def fast?
  275
+      @fast == true
  276
+    end
  277
+
  278
+    def reset_ids?
  279
+      @reset_ids != false
  280
+    end
148 281
   end
149 282
 end
150  
-
151  
-
152  
-
6  lib/database_cleaner/generic/truncation.rb
@@ -2,8 +2,8 @@ module DatabaseCleaner
2 2
   module Generic
3 3
     module Truncation
4 4
       def initialize(opts={})
5  
-        if !opts.empty? && !(opts.keys - [:only, :except]).empty?
6  
-          raise ArgumentError, "The only valid options are :only and :except. You specified #{opts.keys.join(',')}."
  5
+        if !opts.empty? && !(opts.keys - [:only, :except, :fast, :reset_ids]).empty?
  6
+          raise ArgumentError, "The only valid options are :only, :except, :fast or :reset_ids. You specified #{opts.keys.join(',')}."
7 7
         end
8 8
         if opts.has_key?(:only) && opts.has_key?(:except)
9 9
           raise ArgumentError, "You may only specify either :only or :except.  Doing both doesn't really make sense does it?"
@@ -12,6 +12,8 @@ def initialize(opts={})
12 12
         @only = opts[:only]
13 13
         @tables_to_exclude = (opts[:except] || []).dup
14 14
         @tables_to_exclude << migration_storage_name if migration_storage_name
  15
+        @fast = opts[:fast]
  16
+        @reset_ids = opts[:reset_ids]
15 17
       end
16 18
 
17 19
       def start
2  spec/database_cleaner/active_record/base_spec.rb
... ...
@@ -1,7 +1,7 @@
1 1
 require 'spec_helper'
2 2
 require 'active_record'
3 3
 require 'database_cleaner/active_record/base'
4  
-require 'database_cleaner/shared_strategy_spec'
  4
+require 'database_cleaner/shared_strategy'
5 5
 
6 6
 module DatabaseCleaner
7 7
   describe ActiveRecord do
40  spec/database_cleaner/active_record/truncation/mysql2_spec.rb
... ...
@@ -0,0 +1,40 @@
  1
+require 'spec_helper'
  2
+require 'active_record'
  3
+require 'support/active_record/mysql2_setup'
  4
+require 'database_cleaner/active_record/truncation'
  5
+require 'database_cleaner/active_record/truncation/shared_mysql'
  6
+
  7
+module ActiveRecord
  8
+  module ConnectionAdapters
  9
+    describe do 
  10
+      before(:all) { active_record_mysql2_setup }
  11
+
  12
+      let(:adapter) { Mysql2Adapter }
  13
+      let(:connection) { active_record_mysql2_connection }
  14
+
  15
+      describe "#truncate_table" do
  16
+        it "should truncate the table" do
  17
+          2.times { User.create }
  18
+
  19
+          connection.truncate_table('users')
  20
+          User.count.should == 0
  21
+        end
  22
+
  23
+        it "should reset AUTO_INCREMENT index of table" do
  24
+          2.times { User.create }
  25
+          User.delete_all
  26
+
  27
+          connection.truncate_table('users')
  28
+
  29
+          User.create.id.should == 1
  30
+        end
  31
+      end
  32
+
  33
+      it_behaves_like "Fast truncation" do
  34
+        let(:adapter) { Mysql2Adapter }
  35
+        let(:connection) { active_record_mysql2_connection }
  36
+      end
  37
+    end
  38
+  end
  39
+end
  40
+
40  spec/database_cleaner/active_record/truncation/mysql_spec.rb
... ...
@@ -0,0 +1,40 @@
  1
+require 'spec_helper'
  2
+require 'active_record'
  3
+require 'support/active_record/mysql_setup'
  4
+require 'database_cleaner/active_record/truncation'
  5
+require 'database_cleaner/active_record/truncation/shared_mysql'
  6
+
  7
+module ActiveRecord
  8
+  module ConnectionAdapters
  9
+    describe do 
  10
+      before(:all) { active_record_mysql_setup }
  11
+
  12
+      let(:adapter) { MysqlAdapter }
  13
+      let(:connection) { active_record_mysql_connection }
  14
+
  15
+      describe "#truncate_table" do
  16
+        it "should truncate the table" do
  17
+          2.times { User.create }
  18
+
  19
+          connection.truncate_table('users')
  20
+          User.count.should == 0
  21
+        end
  22
+
  23
+        it "should reset AUTO_INCREMENT index of table" do
  24
+          2.times { User.create }
  25
+          User.delete_all
  26
+
  27
+          connection.truncate_table('users')
  28
+
  29
+          User.create.id.should == 1
  30
+        end
  31
+      end
  32
+ 
  33
+      it_behaves_like "Fast truncation" do
  34
+        let(:adapter) { MysqlAdapter }
  35
+        let(:connection) { active_record_mysql_connection }
  36
+      end
  37
+    end
  38
+  end
  39
+end
  40
+
103  spec/database_cleaner/active_record/truncation/postgresql_spec.rb
... ...
@@ -0,0 +1,103 @@
  1
+require 'spec_helper'
  2
+require 'active_record'
  3
+require 'support/active_record/postgresql_setup'
  4
+require 'database_cleaner/active_record/truncation'
  5
+
  6
+module ActiveRecord
  7
+  module ConnectionAdapters
  8
+    describe do
  9
+      before(:all) { active_record_pg_setup }
  10
+
  11
+      let(:adapter) { PostgreSQLAdapter }
  12
+
  13
+      let(:connection) do
  14
+        active_record_pg_connection
  15
+      end
  16
+
  17
+      before(:each) do
  18
+        connection.truncate_tables connection.tables
  19
+      end
  20
+
  21
+      describe "#truncate_table" do
  22
+        it "should truncate the table" do
  23
+          2.times { User.create }
  24
+
  25
+          connection.truncate_table('users')
  26
+          User.count.should == 0
  27
+        end
  28
+
  29
+        it "should reset AUTO_INCREMENT index of table" do
  30
+          2.times { User.create }
  31
+          User.delete_all
  32
+
  33
+          connection.truncate_table('users')
  34
+
  35
+          User.create.id.should == 1
  36
+        end
  37
+      end
  38
+
  39
+      describe "#truncate_tables_with_id_reset" do
  40
+        it "responds" do
  41
+          adapter.instance_methods.should include('truncate_tables_with_id_reset')
  42
+        end
  43
+
  44
+        it "should truncate the table" do
  45
+          2.times { User.create }
  46
+
  47
+          connection.truncate_tables_with_id_reset('users')
  48
+          User.count.should == 0
  49
+        end
  50
+
  51
+        it "should reset AUTO_INCREMENT index of table" do
  52
+          2.times { User.create }
  53
+          User.delete_all
  54
+
  55
+          connection.truncate_tables_with_id_reset('users')
  56
+
  57
+          User.create.id.should == 1
  58
+        end
  59
+      end
  60
+
  61
+      describe "#truncate_tables_no_id_reset" do
  62
+        it "responds" do
  63
+          adapter.instance_methods.map(&:to_s).should include('truncate_tables_no_id_reset')
  64
+        end
  65
+
  66
+        it "should truncate the table" do
  67
+          2.times { User.create }
  68
+
  69
+          connection.truncate_tables_no_id_reset('users')
  70
+          User.count.should == 0
  71
+        end
  72
+
  73
+        it "should not reset AUTO_INCREMENT index of table" do
  74
+          2.times { User.create }
  75
+          User.delete_all
  76
+
  77
+          connection.truncate_tables_no_id_reset('users')
  78
+
  79
+          User.create.id.should == 3
  80
+        end
  81
+      end
  82
+
  83
+      describe "#fast_truncate_tables" do
  84
+        it "responds" do
  85
+          adapter.instance_methods.should include('fast_truncate_tables')
  86
+        end
  87
+
  88
+        it 'should call #truncate_tables_with_id_reset on each table if :reset_ids option true was given' do
  89
+          connection.should_receive(:truncate_tables_with_id_reset).exactly(connection.tables.size).times
  90
+
  91
+          connection.fast_truncate_tables(connection.tables)
  92
+        end
  93
+
  94
+        it 'should call #truncate_tables_with_id_reset on each table if :reset_ids option false was given' do
  95
+          connection.should_receive(:truncate_tables_no_id_reset).exactly(connection.tables.size).times
  96
+
  97
+          connection.fast_truncate_tables(connection.tables, :reset_ids => false)
  98
+        end
  99
+      end
  100
+    end
  101
+  end
  102
+end
  103
+
63  spec/database_cleaner/active_record/truncation/shared_mysql.rb
... ...
@@ -0,0 +1,63 @@
  1
+shared_examples_for "Fast truncation" do
  2
+  describe "#truncate_table_with_id_reset" do
  3
+    it "responds" do
  4
+      adapter.instance_methods.should include('truncate_table_with_id_reset')
  5
+    end
  6
+
  7
+    it "should truncate the table" do
  8
+      2.times { User.create }
  9
+
  10
+      connection.truncate_table_with_id_reset('users')
  11
+      User.count.should == 0
  12
+    end
  13
+
  14
+    it "should reset AUTO_INCREMENT index of table" do
  15
+      2.times { User.create }
  16
+      User.delete_all
  17
+
  18
+      connection.truncate_table_with_id_reset('users')
  19
+
  20
+      User.create.id.should == 1
  21
+    end
  22
+  end
  23
+
  24
+  describe "#truncate_table_no_id_reset" do
  25
+    it "responds" do
  26
+      adapter.instance_methods.map(&:to_s).should include('truncate_table_no_id_reset')
  27
+    end
  28
+
  29
+    it "should truncate the table" do
  30
+      2.times { User.create }
  31
+
  32
+      connection.truncate_table_no_id_reset('users')
  33
+      User.count.should == 0
  34
+    end
  35
+
  36
+    it "should not reset AUTO_INCREMENT index of table" do
  37
+      2.times { User.create }
  38
+      User.delete_all
  39
+
  40
+      connection.truncate_table_no_id_reset('users')
  41
+
  42
+      User.create.id.should == 3
  43
+    end
  44
+  end
  45
+
  46
+  describe "#fast_truncate_tables" do
  47
+    it "responds" do
  48
+      adapter.instance_methods.should include('fast_truncate_tables')
  49
+    end
  50
+
  51
+    it 'should call #truncate_table_with_id_reset on each table if :reset_ids option true was given' do
  52
+      connection.should_receive(:truncate_table_with_id_reset).exactly(connection.tables.size).times
  53
+
  54
+      connection.fast_truncate_tables(connection.tables)
  55
+    end
  56
+
  57
+    it 'should call #truncate_table_with_id_reset on each table if :reset_ids option false was given' do
  58
+      connection.should_receive(:truncate_table_no_id_reset).exactly(connection.tables.size).times
  59
+
  60
+      connection.fast_truncate_tables(connection.tables, :reset_ids => false)
  61
+    end
  62
+  end
  63
+end
121  spec/database_cleaner/active_record/truncation_spec.rb
... ...
@@ -1,16 +1,15 @@
1 1
 require File.dirname(__FILE__) + '/../../spec_helper'
2 2
 require 'active_record'
3  
-require 'database_cleaner/active_record/truncation'
4 3
 
  4
+require 'database_cleaner/active_record/truncation'
5 5
 
6 6
 module ActiveRecord
7 7
   module ConnectionAdapters
8 8
     [MysqlAdapter, Mysql2Adapter, SQLite3Adapter, JdbcAdapter, PostgreSQLAdapter, IBM_DBAdapter].each do |adapter|
9 9
       describe adapter, "#truncate_table" do
10 10
         it "responds" do
11  
-          adapter.new("foo").should respond_to(:truncate_table)
  11
+          adapter.instance_methods.should include('truncate_table')
12 12
         end
13  
-        it "should truncate the table"
14 13
       end
15 14
     end
16 15
   end
@@ -22,55 +21,113 @@ module ActiveRecord
22 21
     describe Truncation do
23 22
       let(:connection) { mock('connection') }
24 23
 
25  
-
26 24
       before(:each) do
27 25
         connection.stub!(:disable_referential_integrity).and_yield
28 26
         connection.stub!(:views).and_return([])
29 27
         ::ActiveRecord::Base.stub!(:connection).and_return(connection)
30 28
       end
31 29
 
32  
-      it "should truncate all tables except for schema_migrations" do
33  
-        connection.stub!(:tables).and_return(%w[schema_migrations widgets dogs])
34  
-        
35  
-        connection.should_receive(:truncate_tables).with(['widgets', 'dogs'])
36  
-        Truncation.new.clean
37  
-      end
  30
+      describe '#clean' do
  31
+        it "should truncate all tables except for schema_migrations" do
  32
+          connection.stub!(:tables).and_return(%w[schema_migrations widgets dogs])
38 33
 
39  
-      it "should only truncate the tables specified in the :only option when provided" do
40  
-        connection.stub!(:tables).and_return(%w[schema_migrations widgets dogs])
  34
+          connection.should_receive(:truncate_tables).with(['widgets', 'dogs'])
  35
+          Truncation.new.clean
  36
+        end
41 37
 
42  
-        connection.should_receive(:truncate_tables).with(['widgets'])
  38
+        it "should only truncate the tables specified in the :only option when provided" do
  39
+          connection.stub!(:tables).and_return(%w[schema_migrations widgets dogs])
43 40
 
44  
-        Truncation.new(:only => ['widgets']).clean
45  
-      end
  41
+          connection.should_receive(:truncate_tables).with(['widgets'])
46 42
 
47  
-      it "should not truncate the tables specified in the :except option" do
48  
-        connection.stub!(:tables).and_return(%w[schema_migrations widgets dogs])
  43
+          Truncation.new(:only => ['widgets']).clean
  44
+        end
49 45
 
50  
-        connection.should_receive(:truncate_tables).with(['dogs'])
  46
+        it "should not truncate the tables specified in the :except option" do
  47
+          connection.stub!(:tables).and_return(%w[schema_migrations widgets dogs])
51 48
 
52  
-        Truncation.new(:except => ['widgets']).clean
53  
-      end
  49
+          connection.should_receive(:truncate_tables).with(['dogs'])
54 50
 
55  
-      it "should raise an error when :only and :except options are used" do
56  
-        running {
57  
-          Truncation.new(:except => ['widgets'], :only => ['widgets'])
58  
-        }.should raise_error(ArgumentError)
59  
-      end
  51
+          Truncation.new(:except => ['widgets']).clean
  52
+        end
  53
+
  54
+        it "should raise an error when :only and :except options are used" do
  55
+          running {
  56
+            Truncation.new(:except => ['widgets'], :only => ['widgets'])
  57
+          }.should raise_error(ArgumentError)
  58
+        end
  59
+
  60
+        it "should raise an error when invalid options are provided" do
  61
+          running { Truncation.new(:foo => 'bar') }.should raise_error(ArgumentError)
  62
+        end
  63
+
  64
+        it "should not truncate views" do
  65
+          connection.stub!(:tables).and_return(%w[widgets dogs])
  66
+          connection.stub!(:views).and_return(["widgets"])
  67
+
  68
+          connection.should_receive(:truncate_tables).with(['dogs'])
60 69
 
61  
-      it "should raise an error when invalid options are provided" do
62  
-        running { Truncation.new(:foo => 'bar') }.should raise_error(ArgumentError)
  70
+          Truncation.new.clean
  71
+        end
  72
+
  73
+        describe "relying on #fast_truncate_tables if connection allows it" do
  74
+          subject { Truncation.new }
  75
+
  76
+          it "should rely on #fast_truncate_tables if #fast? returns true" do
  77
+            connection.stub!(:tables).and_return(%w[widgets dogs])
  78
+            connection.stub!(:views).and_return(["widgets"])
  79
+
  80
+            subject.instance_variable_set(:"@fast", true)
  81
+
  82
+            connection.should_not_receive(:truncate_tables).with(['dogs'])
  83
+            connection.should_receive(:fast_truncate_tables).with(['dogs'], :reset_ids => true)
  84
+
  85
+            subject.clean
  86
+          end
  87
+
  88
+          it "should not rely on #fast_truncate_tables if #fast? return false" do
  89
+            connection.stub!(:tables).and_return(%w[widgets dogs])
  90
+            connection.stub!(:views).and_return(["widgets"])
  91
+
  92
+            subject.instance_variable_set(:"@fast", false)
  93
+
  94
+            connection.should_not_receive(:fast_truncate_tables).with(['dogs'], :reset_ids => true)
  95
+            connection.should_receive(:truncate_tables).with(['dogs'])
  96
+
  97
+            subject.clean
  98
+          end
  99
+        end
63 100
       end
64 101
 
65  
-      it "should not truncate views" do
66  
-        connection.stub!(:tables).and_return(%w[widgets dogs])
67  
-        connection.stub!(:views).and_return(["widgets"])
  102
+      describe '#fast?' do
  103
+        subject { Truncation.new }
  104
+        its(:fast?) { should == false }
68 105
 
69  
-        connection.should_receive(:truncate_tables).with(['dogs'])
  106
+        it 'should return true if @reset_id is set and non false or nil' do
  107
+          subject.instance_variable_set(:"@fast", true)
  108
+          subject.send(:fast?).should == true
  109
+        end
70 110
 
71  
-        Truncation.new.clean
  111
+        it 'should return false if @reset_id is set to false' do
  112
+          subject.instance_variable_set(:"@fast", false)
  113
+          subject.send(:fast?).should == false
  114
+        end
72 115
       end
  116
+      
  117
+      describe '#reset_ids?' do
  118
+        subject { Truncation.new }
  119
+        its(:reset_ids?) { should == true }
  120
+
  121
+        it 'should return true if @reset_id is set and non false or nil' do
  122
+          subject.instance_variable_set(:"@reset_ids", 'Something')
  123
+          subject.send(:reset_ids?).should == true
  124
+        end
73 125
 
  126
+        it 'should return false if @reset_id is set to false' do
  127
+          subject.instance_variable_set(:"@reset_ids", false)
  128
+          subject.send(:reset_ids?).should == false
  129
+        end
  130
+      end
74 131
     end
75 132
   end
76 133
 end
4  spec/database_cleaner/configuration_spec.rb
... ...
@@ -1,4 +1,4 @@
1  
-require File.dirname(__FILE__) + '/../spec_helper'
  1
+require 'spec_helper'
2 2
 
3 3
 module DatabaseCleaner
4 4
   class << self
@@ -197,7 +197,7 @@ def connections_stub!(array)
197 197
     # plausably want to force orm/strategy change on two sets of orm that differ only on db
198 198
     context "multiple orm proxy methods" do
199 199
 
200  
-      it "should proxy orm to all connections and remove duplicate connections" do
  200
+      pending "should proxy orm to all connections and remove duplicate connections" do
201 201
         active_record_1 = mock("active_mock_on_db_one").as_null_object
202 202
         active_record_2 = mock("active_mock_on_db_two").as_null_object
203 203
         data_mapper_1   = mock("data_mock_on_db_one").as_null_object
2  spec/database_cleaner/data_mapper/base_spec.rb
... ...
@@ -1,6 +1,6 @@
1 1
 require 'spec_helper'
2 2
 require 'database_cleaner/data_mapper/base'
3  
-require 'database_cleaner/shared_strategy_spec'
  3
+require 'database_cleaner/shared_strategy'
4 4
 
5 5
 module DatabaseCleaner
6 6
   describe DataMapper do
2  spec/database_cleaner/data_mapper/transaction_spec.rb
... ...
@@ -1,6 +1,6 @@
1 1
 require File.dirname(__FILE__) + '/../../spec_helper'
2 2
 require 'database_cleaner/data_mapper/transaction'
3  
-require 'database_cleaner/shared_strategy_spec'
  3
+require 'database_cleaner/shared_strategy'