activerecord 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (311) hide show
  1. data/CHANGELOG +4928 -3
  2. data/README +45 -46
  3. data/RUNNING_UNIT_TESTS +8 -11
  4. data/Rakefile +247 -0
  5. data/install.rb +8 -38
  6. data/lib/active_record/aggregations.rb +64 -49
  7. data/lib/active_record/associations/association_collection.rb +217 -47
  8. data/lib/active_record/associations/association_proxy.rb +159 -0
  9. data/lib/active_record/associations/belongs_to_association.rb +56 -0
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +50 -0
  11. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +155 -37
  12. data/lib/active_record/associations/has_many_association.rb +145 -75
  13. data/lib/active_record/associations/has_many_through_association.rb +283 -0
  14. data/lib/active_record/associations/has_one_association.rb +96 -0
  15. data/lib/active_record/associations.rb +1537 -304
  16. data/lib/active_record/attribute_methods.rb +328 -0
  17. data/lib/active_record/base.rb +2001 -588
  18. data/lib/active_record/calculations.rb +269 -0
  19. data/lib/active_record/callbacks.rb +169 -165
  20. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +308 -0
  21. data/lib/active_record/connection_adapters/abstract/database_statements.rb +171 -0
  22. data/lib/active_record/connection_adapters/abstract/query_cache.rb +87 -0
  23. data/lib/active_record/connection_adapters/abstract/quoting.rb +69 -0
  24. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +472 -0
  25. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +306 -0
  26. data/lib/active_record/connection_adapters/abstract_adapter.rb +125 -279
  27. data/lib/active_record/connection_adapters/mysql_adapter.rb +442 -77
  28. data/lib/active_record/connection_adapters/postgresql_adapter.rb +805 -135
  29. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +34 -0
  30. data/lib/active_record/connection_adapters/sqlite_adapter.rb +353 -69
  31. data/lib/active_record/fixtures.rb +946 -100
  32. data/lib/active_record/locking/optimistic.rb +144 -0
  33. data/lib/active_record/locking/pessimistic.rb +77 -0
  34. data/lib/active_record/migration.rb +417 -0
  35. data/lib/active_record/observer.rb +142 -32
  36. data/lib/active_record/query_cache.rb +23 -0
  37. data/lib/active_record/reflection.rb +163 -70
  38. data/lib/active_record/schema.rb +58 -0
  39. data/lib/active_record/schema_dumper.rb +171 -0
  40. data/lib/active_record/serialization.rb +98 -0
  41. data/lib/active_record/serializers/json_serializer.rb +71 -0
  42. data/lib/active_record/serializers/xml_serializer.rb +315 -0
  43. data/lib/active_record/timestamp.rb +41 -0
  44. data/lib/active_record/transactions.rb +87 -57
  45. data/lib/active_record/validations.rb +909 -122
  46. data/lib/active_record/vendor/db2.rb +362 -0
  47. data/lib/active_record/vendor/mysql.rb +126 -29
  48. data/lib/active_record/version.rb +9 -0
  49. data/lib/active_record.rb +35 -7
  50. data/lib/activerecord.rb +1 -0
  51. data/test/aaa_create_tables_test.rb +72 -0
  52. data/test/abstract_unit.rb +73 -5
  53. data/test/active_schema_test_mysql.rb +43 -0
  54. data/test/adapter_test.rb +105 -0
  55. data/test/adapter_test_sqlserver.rb +95 -0
  56. data/test/aggregations_test.rb +110 -16
  57. data/test/all.sh +2 -2
  58. data/test/ar_schema_test.rb +33 -0
  59. data/test/association_inheritance_reload.rb +14 -0
  60. data/test/associations/ar_joins_test.rb +0 -0
  61. data/test/associations/callbacks_test.rb +147 -0
  62. data/test/associations/cascaded_eager_loading_test.rb +110 -0
  63. data/test/associations/eager_singularization_test.rb +145 -0
  64. data/test/associations/eager_test.rb +442 -0
  65. data/test/associations/extension_test.rb +47 -0
  66. data/test/associations/inner_join_association_test.rb +88 -0
  67. data/test/associations/join_model_test.rb +553 -0
  68. data/test/associations_test.rb +1930 -267
  69. data/test/attribute_methods_test.rb +146 -0
  70. data/test/base_test.rb +1316 -84
  71. data/test/binary_test.rb +32 -0
  72. data/test/calculations_test.rb +251 -0
  73. data/test/callbacks_test.rb +400 -0
  74. data/test/class_inheritable_attributes_test.rb +3 -4
  75. data/test/column_alias_test.rb +17 -0
  76. data/test/connection_test_firebird.rb +8 -0
  77. data/test/connection_test_mysql.rb +30 -0
  78. data/test/connections/native_db2/connection.rb +25 -0
  79. data/test/connections/native_firebird/connection.rb +26 -0
  80. data/test/connections/native_frontbase/connection.rb +27 -0
  81. data/test/connections/native_mysql/connection.rb +21 -18
  82. data/test/connections/native_openbase/connection.rb +21 -0
  83. data/test/connections/native_oracle/connection.rb +27 -0
  84. data/test/connections/native_postgresql/connection.rb +17 -18
  85. data/test/connections/native_sqlite/connection.rb +17 -16
  86. data/test/connections/native_sqlite3/connection.rb +25 -0
  87. data/test/connections/native_sqlite3/in_memory_connection.rb +18 -0
  88. data/test/connections/native_sybase/connection.rb +23 -0
  89. data/test/copy_table_test_sqlite.rb +69 -0
  90. data/test/datatype_test_postgresql.rb +203 -0
  91. data/test/date_time_test.rb +37 -0
  92. data/test/default_test_firebird.rb +16 -0
  93. data/test/defaults_test.rb +67 -0
  94. data/test/deprecated_finder_test.rb +30 -0
  95. data/test/finder_test.rb +607 -32
  96. data/test/fixtures/accounts.yml +28 -0
  97. data/test/fixtures/all/developers.yml +0 -0
  98. data/test/fixtures/all/people.csv +0 -0
  99. data/test/fixtures/all/tasks.yml +0 -0
  100. data/test/fixtures/author.rb +107 -0
  101. data/test/fixtures/author_favorites.yml +4 -0
  102. data/test/fixtures/authors.yml +7 -0
  103. data/test/fixtures/bad_fixtures/attr_with_numeric_first_char +1 -0
  104. data/test/fixtures/bad_fixtures/attr_with_spaces +1 -0
  105. data/test/fixtures/bad_fixtures/blank_line +3 -0
  106. data/test/fixtures/bad_fixtures/duplicate_attributes +3 -0
  107. data/test/fixtures/bad_fixtures/missing_value +1 -0
  108. data/test/fixtures/binaries.yml +132 -0
  109. data/test/fixtures/binary.rb +2 -0
  110. data/test/fixtures/book.rb +4 -0
  111. data/test/fixtures/books.yml +7 -0
  112. data/test/fixtures/categories/special_categories.yml +9 -0
  113. data/test/fixtures/categories/subsubdir/arbitrary_filename.yml +4 -0
  114. data/test/fixtures/categories.yml +14 -0
  115. data/test/fixtures/categories_ordered.yml +7 -0
  116. data/test/fixtures/categories_posts.yml +23 -0
  117. data/test/fixtures/categorization.rb +5 -0
  118. data/test/fixtures/categorizations.yml +17 -0
  119. data/test/fixtures/category.rb +26 -0
  120. data/test/fixtures/citation.rb +6 -0
  121. data/test/fixtures/comment.rb +23 -0
  122. data/test/fixtures/comments.yml +59 -0
  123. data/test/fixtures/companies.yml +55 -0
  124. data/test/fixtures/company.rb +81 -4
  125. data/test/fixtures/company_in_module.rb +32 -6
  126. data/test/fixtures/computer.rb +4 -0
  127. data/test/fixtures/computers.yml +4 -0
  128. data/test/fixtures/contact.rb +16 -0
  129. data/test/fixtures/courses.yml +7 -0
  130. data/test/fixtures/customer.rb +28 -3
  131. data/test/fixtures/customers.yml +17 -0
  132. data/test/fixtures/db_definitions/db2.drop.sql +33 -0
  133. data/test/fixtures/db_definitions/db2.sql +235 -0
  134. data/test/fixtures/db_definitions/db22.drop.sql +2 -0
  135. data/test/fixtures/db_definitions/db22.sql +5 -0
  136. data/test/fixtures/db_definitions/firebird.drop.sql +65 -0
  137. data/test/fixtures/db_definitions/firebird.sql +310 -0
  138. data/test/fixtures/db_definitions/firebird2.drop.sql +2 -0
  139. data/test/fixtures/db_definitions/firebird2.sql +6 -0
  140. data/test/fixtures/db_definitions/frontbase.drop.sql +33 -0
  141. data/test/fixtures/db_definitions/frontbase.sql +273 -0
  142. data/test/fixtures/db_definitions/frontbase2.drop.sql +1 -0
  143. data/test/fixtures/db_definitions/frontbase2.sql +4 -0
  144. data/test/fixtures/db_definitions/openbase.drop.sql +2 -0
  145. data/test/fixtures/db_definitions/openbase.sql +318 -0
  146. data/test/fixtures/db_definitions/openbase2.drop.sql +2 -0
  147. data/test/fixtures/db_definitions/openbase2.sql +7 -0
  148. data/test/fixtures/db_definitions/oracle.drop.sql +67 -0
  149. data/test/fixtures/db_definitions/oracle.sql +330 -0
  150. data/test/fixtures/db_definitions/oracle2.drop.sql +2 -0
  151. data/test/fixtures/db_definitions/oracle2.sql +6 -0
  152. data/test/fixtures/db_definitions/postgresql.drop.sql +44 -0
  153. data/test/fixtures/db_definitions/postgresql.sql +217 -38
  154. data/test/fixtures/db_definitions/postgresql2.drop.sql +2 -0
  155. data/test/fixtures/db_definitions/postgresql2.sql +2 -2
  156. data/test/fixtures/db_definitions/schema.rb +354 -0
  157. data/test/fixtures/db_definitions/schema2.rb +11 -0
  158. data/test/fixtures/db_definitions/sqlite.drop.sql +33 -0
  159. data/test/fixtures/db_definitions/sqlite.sql +139 -5
  160. data/test/fixtures/db_definitions/sqlite2.drop.sql +2 -0
  161. data/test/fixtures/db_definitions/sqlite2.sql +1 -0
  162. data/test/fixtures/db_definitions/sybase.drop.sql +35 -0
  163. data/test/fixtures/db_definitions/sybase.sql +222 -0
  164. data/test/fixtures/db_definitions/sybase2.drop.sql +4 -0
  165. data/test/fixtures/db_definitions/sybase2.sql +5 -0
  166. data/test/fixtures/developer.rb +70 -6
  167. data/test/fixtures/developers.yml +21 -0
  168. data/test/fixtures/developers_projects/david_action_controller +2 -1
  169. data/test/fixtures/developers_projects/david_active_record +2 -1
  170. data/test/fixtures/developers_projects.yml +17 -0
  171. data/test/fixtures/edge.rb +5 -0
  172. data/test/fixtures/edges.yml +6 -0
  173. data/test/fixtures/entrants.yml +14 -0
  174. data/test/fixtures/example.log +1 -0
  175. data/test/fixtures/fk_test_has_fk.yml +3 -0
  176. data/test/fixtures/fk_test_has_pk.yml +2 -0
  177. data/test/fixtures/flowers.jpg +0 -0
  178. data/test/fixtures/funny_jokes.yml +10 -0
  179. data/test/fixtures/item.rb +7 -0
  180. data/test/fixtures/items.yml +4 -0
  181. data/test/fixtures/joke.rb +3 -0
  182. data/test/fixtures/keyboard.rb +3 -0
  183. data/test/fixtures/legacy_thing.rb +3 -0
  184. data/test/fixtures/legacy_things.yml +3 -0
  185. data/test/fixtures/matey.rb +4 -0
  186. data/test/fixtures/mateys.yml +4 -0
  187. data/test/fixtures/migrations/1_people_have_last_names.rb +9 -0
  188. data/test/fixtures/migrations/2_we_need_reminders.rb +12 -0
  189. data/test/fixtures/migrations/3_innocent_jointable.rb +12 -0
  190. data/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb +15 -0
  191. data/test/fixtures/migrations_with_duplicate/1_people_have_last_names.rb +9 -0
  192. data/test/fixtures/migrations_with_duplicate/2_we_need_reminders.rb +12 -0
  193. data/test/fixtures/migrations_with_duplicate/3_foo.rb +7 -0
  194. data/test/fixtures/migrations_with_duplicate/3_innocent_jointable.rb +12 -0
  195. data/test/fixtures/migrations_with_missing_versions/1000_people_have_middle_names.rb +9 -0
  196. data/test/fixtures/migrations_with_missing_versions/1_people_have_last_names.rb +9 -0
  197. data/test/fixtures/migrations_with_missing_versions/3_we_need_reminders.rb +12 -0
  198. data/test/fixtures/migrations_with_missing_versions/4_innocent_jointable.rb +12 -0
  199. data/test/fixtures/minimalistic.rb +2 -0
  200. data/test/fixtures/minimalistics.yml +2 -0
  201. data/test/fixtures/mixed_case_monkey.rb +3 -0
  202. data/test/fixtures/mixed_case_monkeys.yml +6 -0
  203. data/test/fixtures/mixins.yml +29 -0
  204. data/test/fixtures/movies.yml +7 -0
  205. data/test/fixtures/naked/csv/accounts.csv +1 -0
  206. data/test/fixtures/naked/yml/accounts.yml +1 -0
  207. data/test/fixtures/naked/yml/companies.yml +1 -0
  208. data/test/fixtures/naked/yml/courses.yml +1 -0
  209. data/test/fixtures/order.rb +4 -0
  210. data/test/fixtures/parrot.rb +13 -0
  211. data/test/fixtures/parrots.yml +27 -0
  212. data/test/fixtures/parrots_pirates.yml +7 -0
  213. data/test/fixtures/people.yml +3 -0
  214. data/test/fixtures/person.rb +4 -0
  215. data/test/fixtures/pirate.rb +5 -0
  216. data/test/fixtures/pirates.yml +9 -0
  217. data/test/fixtures/post.rb +59 -0
  218. data/test/fixtures/posts.yml +48 -0
  219. data/test/fixtures/project.rb +27 -2
  220. data/test/fixtures/projects.yml +7 -0
  221. data/test/fixtures/reader.rb +4 -0
  222. data/test/fixtures/readers.yml +4 -0
  223. data/test/fixtures/reply.rb +18 -2
  224. data/test/fixtures/reserved_words/distinct.yml +5 -0
  225. data/test/fixtures/reserved_words/distincts_selects.yml +11 -0
  226. data/test/fixtures/reserved_words/group.yml +14 -0
  227. data/test/fixtures/reserved_words/select.yml +8 -0
  228. data/test/fixtures/reserved_words/values.yml +7 -0
  229. data/test/fixtures/ship.rb +3 -0
  230. data/test/fixtures/ships.yml +5 -0
  231. data/test/fixtures/subject.rb +4 -0
  232. data/test/fixtures/subscriber.rb +4 -3
  233. data/test/fixtures/tag.rb +7 -0
  234. data/test/fixtures/tagging.rb +10 -0
  235. data/test/fixtures/taggings.yml +25 -0
  236. data/test/fixtures/tags.yml +7 -0
  237. data/test/fixtures/task.rb +3 -0
  238. data/test/fixtures/tasks.yml +7 -0
  239. data/test/fixtures/topic.rb +20 -3
  240. data/test/fixtures/topics.yml +22 -0
  241. data/test/fixtures/treasure.rb +4 -0
  242. data/test/fixtures/treasures.yml +10 -0
  243. data/test/fixtures/vertex.rb +9 -0
  244. data/test/fixtures/vertices.yml +4 -0
  245. data/test/fixtures_test.rb +574 -8
  246. data/test/inheritance_test.rb +113 -27
  247. data/test/json_serialization_test.rb +180 -0
  248. data/test/lifecycle_test.rb +56 -29
  249. data/test/locking_test.rb +273 -0
  250. data/test/method_scoping_test.rb +416 -0
  251. data/test/migration_test.rb +933 -0
  252. data/test/migration_test_firebird.rb +124 -0
  253. data/test/mixin_test.rb +95 -0
  254. data/test/modules_test.rb +23 -10
  255. data/test/multiple_db_test.rb +17 -3
  256. data/test/pk_test.rb +59 -15
  257. data/test/query_cache_test.rb +104 -0
  258. data/test/readonly_test.rb +107 -0
  259. data/test/reflection_test.rb +124 -27
  260. data/test/reserved_word_test_mysql.rb +177 -0
  261. data/test/schema_authorization_test_postgresql.rb +75 -0
  262. data/test/schema_dumper_test.rb +131 -0
  263. data/test/schema_test_postgresql.rb +64 -0
  264. data/test/serialization_test.rb +47 -0
  265. data/test/synonym_test_oracle.rb +17 -0
  266. data/test/table_name_test_sqlserver.rb +23 -0
  267. data/test/threaded_connections_test.rb +48 -0
  268. data/test/transactions_test.rb +227 -29
  269. data/test/unconnected_test.rb +14 -6
  270. data/test/validations_test.rb +1293 -32
  271. data/test/xml_serialization_test.rb +202 -0
  272. metadata +347 -143
  273. data/dev-utils/eval_debugger.rb +0 -9
  274. data/examples/associations.rb +0 -87
  275. data/examples/shared_setup.rb +0 -15
  276. data/examples/validation.rb +0 -88
  277. data/lib/active_record/deprecated_associations.rb +0 -70
  278. data/lib/active_record/support/class_attribute_accessors.rb +0 -43
  279. data/lib/active_record/support/class_inheritable_attributes.rb +0 -37
  280. data/lib/active_record/support/clean_logger.rb +0 -10
  281. data/lib/active_record/support/inflector.rb +0 -70
  282. data/lib/active_record/vendor/simple.rb +0 -702
  283. data/lib/active_record/wrappers/yaml_wrapper.rb +0 -15
  284. data/lib/active_record/wrappings.rb +0 -59
  285. data/rakefile +0 -122
  286. data/test/deprecated_associations_test.rb +0 -336
  287. data/test/fixtures/accounts/signals37 +0 -3
  288. data/test/fixtures/accounts/unknown +0 -2
  289. data/test/fixtures/companies/first_client +0 -6
  290. data/test/fixtures/companies/first_firm +0 -4
  291. data/test/fixtures/companies/second_client +0 -6
  292. data/test/fixtures/courses/java +0 -2
  293. data/test/fixtures/courses/ruby +0 -2
  294. data/test/fixtures/customers/david +0 -6
  295. data/test/fixtures/db_definitions/mysql.sql +0 -96
  296. data/test/fixtures/db_definitions/mysql2.sql +0 -4
  297. data/test/fixtures/developers/david +0 -2
  298. data/test/fixtures/developers/jamis +0 -2
  299. data/test/fixtures/entrants/first +0 -3
  300. data/test/fixtures/entrants/second +0 -3
  301. data/test/fixtures/entrants/third +0 -3
  302. data/test/fixtures/fixture_database.sqlite +0 -0
  303. data/test/fixtures/fixture_database_2.sqlite +0 -0
  304. data/test/fixtures/movies/first +0 -2
  305. data/test/fixtures/movies/second +0 -2
  306. data/test/fixtures/projects/action_controller +0 -2
  307. data/test/fixtures/projects/active_record +0 -2
  308. data/test/fixtures/topics/first +0 -9
  309. data/test/fixtures/topics/second +0 -8
  310. data/test/inflector_test.rb +0 -104
  311. data/test/thread_safety_test.rb +0 -33
@@ -1,124 +1,775 @@
1
+ require 'erb'
1
2
  require 'yaml'
3
+ require 'csv'
2
4
 
3
- # Fixtures are a way of organizing data that you want to test against. Each fixture file is created as a row
4
- # in the database and created as a hash with column names as keys and data as values. All of these fixture hashes
5
- # are kept in an overall hash where they can be accessed by their file name.
5
+ module YAML #:nodoc:
6
+ class Omap #:nodoc:
7
+ def keys; map { |k, v| k } end
8
+ def values; map { |k, v| v } end
9
+ end
10
+ end
11
+
12
+ class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
13
+ end
14
+
15
+ # Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavors:
16
+ #
17
+ # 1. YAML fixtures
18
+ # 2. CSV fixtures
19
+ # 3. Single-file fixtures
20
+ #
21
+ # = YAML fixtures
22
+ #
23
+ # This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures
24
+ # in a non-verbose, human-readable format. It ships with Ruby 1.8.1+.
25
+ #
26
+ # Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed in the directory appointed
27
+ # by <tt>Test::Unit::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
28
+ # put your files in <your-rails-app>/test/fixtures/). The fixture file ends with the .yml file extension (Rails example:
29
+ # "<your-rails-app>/test/fixtures/web_sites.yml"). The format of a YAML fixture file looks like this:
30
+ #
31
+ # rubyonrails:
32
+ # id: 1
33
+ # name: Ruby on Rails
34
+ # url: http://www.rubyonrails.org
35
+ #
36
+ # google:
37
+ # id: 2
38
+ # name: Google
39
+ # url: http://www.google.com
40
+ #
41
+ # This YAML fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and is followed by an
42
+ # indented list of key/value pairs in the "key: value" format. Records are separated by a blank line for your viewing
43
+ # pleasure.
44
+ #
45
+ # Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type. See http://yaml.org/type/omap.html
46
+ # for the specification. You will need ordered fixtures when you have foreign key constraints on keys in the same table.
47
+ # This is commonly needed for tree structures. Example:
48
+ #
49
+ # --- !omap
50
+ # - parent:
51
+ # id: 1
52
+ # parent_id: NULL
53
+ # title: Parent
54
+ # - child:
55
+ # id: 2
56
+ # parent_id: 1
57
+ # title: Child
58
+ #
59
+ # = CSV fixtures
60
+ #
61
+ # Fixtures can also be kept in the Comma Separated Value format. Akin to YAML fixtures, CSV fixtures are stored
62
+ # in a single file, but instead end with the .csv file extension (Rails example: "<your-rails-app>/test/fixtures/web_sites.csv")
63
+ #
64
+ # The format of this type of fixture file is much more compact than the others, but also a little harder to read by us
65
+ # humans. The first line of the CSV file is a comma-separated list of field names. The rest of the file is then comprised
66
+ # of the actual data (1 per line). Here's an example:
6
67
  #
7
- # Example:
68
+ # id, name, url
69
+ # 1, Ruby On Rails, http://www.rubyonrails.org
70
+ # 2, Google, http://www.google.com
8
71
  #
9
- # Directory with the fixture files
72
+ # Should you have a piece of data with a comma character in it, you can place double quotes around that value. If you
73
+ # need to use a double quote character, you must escape it with another double quote.
10
74
  #
11
- # developers/
12
- # david
13
- # luke
14
- # jamis
75
+ # Another unique attribute of the CSV fixture is that it has *no* fixture name like the other two formats. Instead, the
76
+ # fixture names are automatically generated by deriving the class name of the fixture file and adding an incrementing
77
+ # number to the end. In our example, the 1st fixture would be called "web_site_1" and the 2nd one would be called
78
+ # "web_site_2".
15
79
  #
16
- # The file +david+ then contains:
80
+ # Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you
81
+ # have existing data somewhere already.
82
+ #
83
+ # = Single-file fixtures
84
+ #
85
+ # This type of fixture was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats.
86
+ # Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory
87
+ # appointed by <tt>Test::Unit::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
88
+ # put your files in <your-rails-app>/test/fixtures/<your-model-name>/ -- like <your-rails-app>/test/fixtures/web_sites/ for the WebSite
89
+ # model).
90
+ #
91
+ # Each text file placed in this directory represents a "record". Usually these types of fixtures are named without
92
+ # extensions, but if you are on a Windows machine, you might consider adding .txt as the extension. Here's what the
93
+ # above example might look like:
94
+ #
95
+ # web_sites/google
96
+ # web_sites/yahoo.txt
97
+ # web_sites/ruby-on-rails
98
+ #
99
+ # The file format of a standard fixture is simple. Each line is a property (or column in db speak) and has the syntax
100
+ # of "name => value". Here's an example of the ruby-on-rails fixture above:
17
101
  #
18
102
  # id => 1
19
- # name => David Heinemeier Hansson
20
- # birthday => 1979-10-15
21
- # profession => Systems development
103
+ # name => Ruby on Rails
104
+ # url => http://www.rubyonrails.org
22
105
  #
23
- # Now when we call <tt>@developers = Fixtures.new(ActiveRecord::Base.connection, "developers", "developers/")</tt> all three
24
- # developers will get inserted into the "developers" table through the active Active Record connection (that must be setup
25
- # before-hand). And we can now query the fixture data through the <tt>@developers</tt> hash, so <tt>@developers["david"]["name"]</tt>
26
- # will return <tt>"David Heinemeier Hansson"</tt> and <tt>@developers["david"]["birthday"]</tt> will return <tt>Date.new(1979, 10, 15)</tt>.
106
+ # = Using Fixtures
107
+ #
108
+ # Since fixtures are a testing construct, we use them in our unit and functional tests. There are two ways to use the
109
+ # fixtures, but first let's take a look at a sample unit test:
110
+ #
111
+ # require 'web_site'
112
+ #
113
+ # class WebSiteTest < Test::Unit::TestCase
114
+ # def test_web_site_count
115
+ # assert_equal 2, WebSite.count
116
+ # end
117
+ # end
27
118
  #
28
- # This can then be used for comparison in a unit test. Something like:
119
+ # As it stands, unless we pre-load the web_site table in our database with two records, this test will fail. Here's the
120
+ # easiest way to add fixtures to the database:
29
121
  #
122
+ # ...
123
+ # class WebSiteTest < Test::Unit::TestCase
124
+ # fixtures :web_sites # add more by separating the symbols with commas
125
+ # ...
126
+ #
127
+ # By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here though), we trigger
128
+ # the testing environment to automatically load the appropriate fixtures into the database before each test.
129
+ # To ensure consistent data, the environment deletes the fixtures before running the load.
130
+ #
131
+ # In addition to being available in the database, the fixtures are also loaded into a hash stored in an instance variable
132
+ # of the test case. It is named after the symbol... so, in our example, there would be a hash available called
133
+ # @web_sites. This is where the "fixture name" comes into play.
134
+ #
135
+ # On top of that, each record is automatically "found" (using Model.find(id)) and placed in the instance variable of its name.
136
+ # So for the YAML fixtures, we'd get @rubyonrails and @google, which could be interrogated using regular Active Record semantics:
137
+ #
138
+ # # test if the object created from the fixture data has the same attributes as the data itself
30
139
  # def test_find
31
- # assert_equal @developers["david"]["name"], Developer.find(@developers["david"]["id"]).name
140
+ # assert_equal @web_sites["rubyonrails"]["name"], @rubyonrails.name
32
141
  # end
33
142
  #
34
- # == YAML fixtures
143
+ # As seen above, the data hash created from the YAML fixtures would have @web_sites["rubyonrails"]["url"] return
144
+ # "http://www.rubyonrails.org" and @web_sites["google"]["name"] would return "Google". The same fixtures, but loaded
145
+ # from a CSV fixture file, would be accessible via @web_sites["web_site_1"]["name"] == "Ruby on Rails" and have the individual
146
+ # fixtures available as instance variables @web_site_1 and @web_site_2.
147
+ #
148
+ # If you do not wish to use instantiated fixtures (usually for performance reasons) there are two options.
149
+ #
150
+ # - to completely disable instantiated fixtures:
151
+ # self.use_instantiated_fixtures = false
152
+ #
153
+ # - to keep the fixture instance (@web_sites) available, but do not automatically 'find' each instance:
154
+ # self.use_instantiated_fixtures = :no_instances
155
+ #
156
+ # Even if auto-instantiated fixtures are disabled, you can still access them
157
+ # by name via special dynamic methods. Each method has the same name as the
158
+ # model, and accepts the name of the fixture to instantiate:
159
+ #
160
+ # fixtures :web_sites
161
+ #
162
+ # def test_find
163
+ # assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
164
+ # end
165
+ #
166
+ # = Dynamic fixtures with ERb
167
+ #
168
+ # Some times you don't care about the content of the fixtures as much as you care about the volume. In these cases, you can
169
+ # mix ERb in with your YAML or CSV fixtures to create a bunch of fixtures for load testing, like:
170
+ #
171
+ # <% for i in 1..1000 %>
172
+ # fix_<%= i %>:
173
+ # id: <%= i %>
174
+ # name: guy_<%= 1 %>
175
+ # <% end %>
176
+ #
177
+ # This will create 1000 very simple YAML fixtures.
178
+ #
179
+ # Using ERb, you can also inject dynamic values into your fixtures with inserts like <%= Date.today.strftime("%Y-%m-%d") %>.
180
+ # This is however a feature to be used with some caution. The point of fixtures are that they're stable units of predictable
181
+ # sample data. If you feel that you need to inject dynamic values, then perhaps you should reexamine whether your application
182
+ # is properly testable. Hence, dynamic values in fixtures are to be considered a code smell.
183
+ #
184
+ # = Transactional fixtures
185
+ #
186
+ # TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case.
187
+ # They can also turn off auto-instantiation of fixture data since the feature is costly and often unused.
188
+ #
189
+ # class FooTest < Test::Unit::TestCase
190
+ # self.use_transactional_fixtures = true
191
+ # self.use_instantiated_fixtures = false
192
+ #
193
+ # fixtures :foos
194
+ #
195
+ # def test_godzilla
196
+ # assert !Foo.find(:all).empty?
197
+ # Foo.destroy_all
198
+ # assert Foo.find(:all).empty?
199
+ # end
200
+ #
201
+ # def test_godzilla_aftermath
202
+ # assert !Foo.find(:all).empty?
203
+ # end
204
+ # end
205
+ #
206
+ # If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures,
207
+ # then you may omit all fixtures declarations in your test cases since all the data's already there and every case rolls back its changes.
208
+ #
209
+ # In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide
210
+ # access to fixture data for every table that has been loaded through fixtures (depending on the value of +use_instantiated_fixtures+)
211
+ #
212
+ # When *not* to use transactional fixtures:
213
+ # 1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit,
214
+ # particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify
215
+ # the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
216
+ # 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
217
+ # Use InnoDB, MaxDB, or NDB instead.
218
+ #
219
+ # = Advanced YAML Fixtures
220
+ #
221
+ # YAML fixtures that don't specify an ID get some extra features:
222
+ #
223
+ # * Stable, autogenerated ID's
224
+ # * Label references for associations (belongs_to, has_one, has_many)
225
+ # * HABTM associations as inline lists
226
+ # * Autofilled timestamp columns
227
+ # * Fixture label interpolation
228
+ # * Support for YAML defaults
229
+ #
230
+ # == Stable, autogenerated ID's
231
+ #
232
+ # Here, have a monkey fixture:
233
+ #
234
+ # george:
235
+ # id: 1
236
+ # name: George the Monkey
237
+ #
238
+ # reginald:
239
+ # id: 2
240
+ # name: Reginald the Pirate
241
+ #
242
+ # Each of these fixtures has two unique identifiers: one for the database
243
+ # and one for the humans. Why don't we generate the primary key instead?
244
+ # Hashing each fixture's label yields a consistent ID:
245
+ #
246
+ # george: # generated id: 503576764
247
+ # name: George the Monkey
248
+ #
249
+ # reginald: # generated id: 324201669
250
+ # name: Reginald the Pirate
251
+ #
252
+ # ActiveRecord looks at the fixture's model class, discovers the correct
253
+ # primary key, and generates it right before inserting the fixture
254
+ # into the database.
255
+ #
256
+ # The generated ID for a given label is constant, so we can discover
257
+ # any fixture's ID without loading anything, as long as we know the label.
258
+ #
259
+ # == Label references for associations (belongs_to, has_one, has_many)
260
+ #
261
+ # Specifying foreign keys in fixtures can be very fragile, not to
262
+ # mention difficult to read. Since ActiveRecord can figure out the ID of
263
+ # any fixture from its label, you can specify FK's by label instead of ID.
264
+ #
265
+ # === belongs_to
266
+ #
267
+ # Let's break out some more monkeys and pirates.
268
+ #
269
+ # ### in pirates.yml
270
+ #
271
+ # reginald:
272
+ # id: 1
273
+ # name: Reginald the Pirate
274
+ # monkey_id: 1
35
275
  #
36
- # Additionally, fixtures supports yaml files. Like fixture files, these yaml files have a pre-defined format. The document
37
- # must be formatted like this:
276
+ # ### in monkeys.yml
38
277
  #
39
- # name: david
40
- # data:
41
- # id: 1
42
- # name: David Heinemeier Hansson
43
- # birthday: 1979-10-15
44
- # profession: Systems development
45
- # ---
46
- # name: steve
47
- # data:
48
- # id: 2
49
- # name: Steve Ross Kellock
50
- # birthday: 1974-09-27
51
- # profession: guy with keyboard
278
+ # george:
279
+ # id: 1
280
+ # name: George the Monkey
281
+ # pirate_id: 1
52
282
  #
53
- # In that file, there's two records. Each record must have two parts: 'name' and 'data'. The data that you add
54
- # must be indented like you see above.
283
+ # Add a few more monkeys and pirates and break this into multiple files,
284
+ # and it gets pretty hard to keep track of what's going on. Let's
285
+ # use labels instead of ID's:
55
286
  #
56
- # Yaml fixtures file names must end with .yaml as in people.yaml or camel.yaml. The yaml fixtures are placed in the same
57
- # directory as the normal fixtures and can happy co-exist. :)
58
- class Fixtures
59
- def self.create_fixtures(fixtures_directory, *table_names)
60
- connection = block_given? ? yield : ActiveRecord::Base.connection
61
- ActiveRecord::Base.logger.level = Logger::ERROR
287
+ # ### in pirates.yml
288
+ #
289
+ # reginald:
290
+ # name: Reginald the Pirate
291
+ # monkey: george
292
+ #
293
+ # ### in monkeys.yml
294
+ #
295
+ # george:
296
+ # name: George the Monkey
297
+ # pirate: reginald
298
+ #
299
+ # Pow! All is made clear. ActiveRecord reflects on the fixture's model class,
300
+ # finds all the +belongs_to+ associations, and allows you to specify
301
+ # a target *label* for the *association* (monkey: george) rather than
302
+ # a target *id* for the *FK* (monkey_id: 1).
303
+ #
304
+ # ==== Polymorphic belongs_to
305
+ #
306
+ # Supporting polymorphic relationships is a little bit more complicated, since
307
+ # ActiveRecord needs to know what type your association is pointing at. Something
308
+ # like this should look familiar:
309
+ #
310
+ # ### in fruit.rb
311
+ #
312
+ # belongs_to :eater, :polymorphic => true
313
+ #
314
+ # ### in fruits.yml
315
+ #
316
+ # apple:
317
+ # id: 1
318
+ # name: apple
319
+ # eater_id: 1
320
+ # eater_type: Monkey
321
+ #
322
+ # Can we do better? You bet!
323
+ #
324
+ # apple:
325
+ # eater: george (Monkey)
326
+ #
327
+ # Just provide the polymorphic target type and ActiveRecord will take care of the rest.
328
+ #
329
+ # === has_and_belongs_to_many
330
+ #
331
+ # Time to give our monkey some fruit.
332
+ #
333
+ # ### in monkeys.yml
334
+ #
335
+ # george:
336
+ # id: 1
337
+ # name: George the Monkey
338
+ # pirate_id: 1
339
+ #
340
+ # ### in fruits.yml
341
+ #
342
+ # apple:
343
+ # id: 1
344
+ # name: apple
345
+ #
346
+ # orange:
347
+ # id: 2
348
+ # name: orange
349
+ #
350
+ # grape:
351
+ # id: 3
352
+ # name: grape
353
+ #
354
+ # ### in fruits_monkeys.yml
355
+ #
356
+ # apple_george:
357
+ # fruit_id: 1
358
+ # monkey_id: 1
359
+ #
360
+ # orange_george:
361
+ # fruit_id: 2
362
+ # monkey_id: 1
363
+ #
364
+ # grape_george:
365
+ # fruit_id: 3
366
+ # monkey_id: 1
367
+ #
368
+ # Let's make the HABTM fixture go away.
369
+ #
370
+ # ### in monkeys.yml
371
+ #
372
+ # george:
373
+ # name: George the Monkey
374
+ # pirate: reginald
375
+ # fruits: apple, orange, grape
376
+ #
377
+ # ### in fruits.yml
378
+ #
379
+ # apple:
380
+ # name: apple
381
+ #
382
+ # orange:
383
+ # name: orange
384
+ #
385
+ # grape:
386
+ # name: grape
387
+ #
388
+ # Zap! No more fruits_monkeys.yml file. We've specified the list of fruits
389
+ # on George's fixture, but we could've just as easily specified a list
390
+ # of monkeys on each fruit. As with +belongs_to+, ActiveRecord reflects on
391
+ # the fixture's model class and discovers the +has_and_belongs_to_many+
392
+ # associations.
393
+ #
394
+ # == Autofilled timestamp columns
395
+ #
396
+ # If your table/model specifies any of ActiveRecord's
397
+ # standard timestamp columns (created_at, created_on, updated_at, updated_on),
398
+ # they will automatically be set to Time.now.
399
+ #
400
+ # If you've set specific values, they'll be left alone.
401
+ #
402
+ # == Fixture label interpolation
403
+ #
404
+ # The label of the current fixture is always available as a column value:
405
+ #
406
+ # geeksomnia:
407
+ # name: Geeksomnia's Account
408
+ # subdomain: $LABEL
409
+ #
410
+ # Also, sometimes (like when porting older join table fixtures) you'll need
411
+ # to be able to get ahold of the identifier for a given label. ERB
412
+ # to the rescue:
413
+ #
414
+ # george_reginald:
415
+ # monkey_id: <%= Fixtures.identify(:reginald) %>
416
+ # pirate_id: <%= Fixtures.identify(:george) %>
417
+ #
418
+ # == Support for YAML defaults
419
+ #
420
+ # You probably already know how to use YAML to set and reuse defaults in
421
+ # your +database.yml+ file,. You can use the same technique in your fixtures:
422
+ #
423
+ # DEFAULTS: &DEFAULTS
424
+ # created_on: <%= 3.weeks.ago.to_s(:db) %>
425
+ #
426
+ # first:
427
+ # name: Smurf
428
+ # <<: *DEFAULTS
429
+ #
430
+ # second:
431
+ # name: Fraggle
432
+ # <<: *DEFAULTS
433
+ #
434
+ # Any fixture labeled "DEFAULTS" is safely ignored.
435
+
436
+ class Fixtures < YAML::Omap
437
+ DEFAULT_FILTER_RE = /\.ya?ml$/
62
438
 
63
- fixtures = [ table_names ].flatten.collect do |table_name|
64
- Fixtures.new(connection, table_name, "#{fixtures_directory}/#{table_name}")
439
+ @@all_cached_fixtures = {}
440
+
441
+ def self.reset_cache(connection = nil)
442
+ connection ||= ActiveRecord::Base.connection
443
+ @@all_cached_fixtures[connection.object_id] = {}
444
+ end
445
+
446
+ def self.cache_for_connection(connection)
447
+ @@all_cached_fixtures[connection.object_id] ||= {}
448
+ @@all_cached_fixtures[connection.object_id]
449
+ end
450
+
451
+ def self.fixture_is_cached?(connection, table_name)
452
+ cache_for_connection(connection)[table_name]
453
+ end
454
+
455
+ def self.cached_fixtures(connection, keys_to_fetch = nil)
456
+ if keys_to_fetch
457
+ fixtures = cache_for_connection(connection).values_at(*keys_to_fetch)
458
+ else
459
+ fixtures = cache_for_connection(connection).values
65
460
  end
461
+ fixtures.size > 1 ? fixtures : fixtures.first
462
+ end
463
+
464
+ def self.cache_fixtures(connection, fixtures)
465
+ cache_for_connection(connection).update(fixtures.index_by(&:table_name))
466
+ end
66
467
 
67
- ActiveRecord::Base.logger.level = Logger::DEBUG
468
+ def self.instantiate_fixtures(object, table_name, fixtures, load_instances = true)
469
+ object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures
470
+ if load_instances
471
+ ActiveRecord::Base.silence do
472
+ fixtures.each do |name, fixture|
473
+ begin
474
+ object.instance_variable_set "@#{name}", fixture.find
475
+ rescue FixtureClassNotFound
476
+ nil
477
+ end
478
+ end
479
+ end
480
+ end
481
+ end
68
482
 
69
- return fixtures.size > 1 ? fixtures : fixtures.first
483
+ def self.instantiate_all_loaded_fixtures(object, load_instances = true)
484
+ all_loaded_fixtures.each do |table_name, fixtures|
485
+ Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances)
486
+ end
70
487
  end
71
488
 
72
- def initialize(connection, table_name, fixture_path, file_filter = /^\.|CVS|\.yaml/)
73
- @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
74
- @fixtures = read_fixtures
489
+ cattr_accessor :all_loaded_fixtures
490
+ self.all_loaded_fixtures = {}
491
+
492
+ def self.create_fixtures(fixtures_directory, table_names, class_names = {})
493
+ table_names = [table_names].flatten.map { |n| n.to_s }
494
+ connection = block_given? ? yield : ActiveRecord::Base.connection
495
+
496
+ table_names_to_fetch = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) }
75
497
 
76
- delete_existing_fixtures
77
- insert_fixtures
498
+ unless table_names_to_fetch.empty?
499
+ ActiveRecord::Base.silence do
500
+ connection.disable_referential_integrity do
501
+ fixtures_map = {}
502
+
503
+ fixtures = table_names_to_fetch.map do |table_name|
504
+ fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s))
505
+ end
506
+
507
+ all_loaded_fixtures.update(fixtures_map)
508
+
509
+ connection.transaction(Thread.current['open_transactions'].to_i == 0) do
510
+ fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
511
+ fixtures.each { |fixture| fixture.insert_fixtures }
512
+
513
+ # Cap primary key sequences to max(pk).
514
+ if connection.respond_to?(:reset_pk_sequence!)
515
+ table_names.each do |table_name|
516
+ connection.reset_pk_sequence!(table_name)
517
+ end
518
+ end
519
+ end
520
+
521
+ cache_fixtures(connection, fixtures)
522
+ end
523
+ end
524
+ end
525
+ cached_fixtures(connection, table_names)
78
526
  end
79
527
 
80
- # Access a fixture hash by using its file name as the key
81
- def [](key)
82
- @fixtures[key]
528
+ # Returns a consistent identifier for +label+. This will always
529
+ # be a positive integer, and will always be the same for a given
530
+ # label, assuming the same OS, platform, and version of Ruby.
531
+ def self.identify(label)
532
+ label.to_s.hash.abs
83
533
  end
84
534
 
85
- # Get the number of fixtures kept in this container
86
- def length
87
- @fixtures.length
535
+ attr_reader :table_name
536
+
537
+ def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
538
+ @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
539
+ @class_name = class_name ||
540
+ (ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
541
+ @table_name = ActiveRecord::Base.table_name_prefix + @table_name + ActiveRecord::Base.table_name_suffix
542
+ @table_name = class_name.table_name if class_name.respond_to?(:table_name)
543
+ @connection = class_name.connection if class_name.respond_to?(:connection)
544
+ read_fixture_files
545
+ end
546
+
547
+ def delete_existing_fixtures
548
+ @connection.delete "DELETE FROM #{@connection.quote_table_name(table_name)}", 'Fixture Delete'
549
+ end
550
+
551
+ def insert_fixtures
552
+ now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
553
+ now = now.to_s(:db)
554
+
555
+ # allow a standard key to be used for doing defaults in YAML
556
+ delete(assoc("DEFAULTS"))
557
+
558
+ # track any join tables we need to insert later
559
+ habtm_fixtures = Hash.new do |h, habtm|
560
+ h[habtm] = HabtmFixtures.new(@connection, habtm.options[:join_table], nil, nil)
561
+ end
562
+
563
+ each do |label, fixture|
564
+ row = fixture.to_hash
565
+
566
+ if model_class && model_class < ActiveRecord::Base
567
+ # fill in timestamp columns if they aren't specified and the model is set to record_timestamps
568
+ if model_class.record_timestamps
569
+ timestamp_column_names.each do |name|
570
+ row[name] = now unless row.key?(name)
571
+ end
572
+ end
573
+
574
+ # interpolate the fixture label
575
+ row.each do |key, value|
576
+ row[key] = label if value == "$LABEL"
577
+ end
578
+
579
+ # generate a primary key if necessary
580
+ if has_primary_key_column? && !row.include?(primary_key_name)
581
+ row[primary_key_name] = Fixtures.identify(label)
582
+ end
583
+
584
+ # If STI is used, find the correct subclass for association reflection
585
+ reflection_class =
586
+ if row.include?(inheritance_column_name)
587
+ row[inheritance_column_name].constantize rescue model_class
588
+ else
589
+ model_class
590
+ end
591
+
592
+ reflection_class.reflect_on_all_associations.each do |association|
593
+ case association.macro
594
+ when :belongs_to
595
+ # Do not replace association name with association foreign key if they are named the same
596
+ fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
597
+
598
+ if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
599
+ if association.options[:polymorphic]
600
+ if value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
601
+ target_type = $1
602
+ target_type_name = (association.options[:foreign_type] || "#{association.name}_type").to_s
603
+
604
+ # support polymorphic belongs_to as "label (Type)"
605
+ row[target_type_name] = target_type
606
+ end
607
+ end
608
+
609
+ row[fk_name] = Fixtures.identify(value)
610
+ end
611
+ when :has_and_belongs_to_many
612
+ if (targets = row.delete(association.name.to_s))
613
+ targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
614
+ join_fixtures = habtm_fixtures[association]
615
+
616
+ targets.each do |target|
617
+ join_fixtures["#{label}_#{target}"] = Fixture.new(
618
+ { association.primary_key_name => row[primary_key_name],
619
+ association.association_foreign_key => Fixtures.identify(target) }, nil)
620
+ end
621
+ end
622
+ end
623
+ end
624
+ end
625
+
626
+ @connection.insert_fixture(fixture, @table_name)
627
+ end
628
+
629
+ # insert any HABTM join tables we discovered
630
+ habtm_fixtures.values.each do |fixture|
631
+ fixture.delete_existing_fixtures
632
+ fixture.insert_fixtures
633
+ end
88
634
  end
89
635
 
90
636
  private
91
- def read_fixtures
92
- Dir.entries(@fixture_path).inject({}) do |fixtures, file|
93
- # is this a regular fixture file?
94
- fixtures[file] = Fixture.new(@fixture_path, file) unless file =~ @file_filter
95
- # is this a *.yaml file?
96
- if file =~ /\.yaml/
97
- YamlFixture.produce( "#{@fixture_path}/#{file}" ).each { |fix| fixtures[fix.yaml_name] = fix }
637
+ class HabtmFixtures < ::Fixtures #:nodoc:
638
+ def read_fixture_files; end
639
+ end
640
+
641
+ def model_class
642
+ @model_class ||= @class_name.is_a?(Class) ?
643
+ @class_name : @class_name.constantize rescue nil
644
+ end
645
+
646
+ def primary_key_name
647
+ @primary_key_name ||= model_class && model_class.primary_key
648
+ end
649
+
650
+ def has_primary_key_column?
651
+ @has_primary_key_column ||= model_class && primary_key_name &&
652
+ model_class.columns.find { |c| c.name == primary_key_name }
653
+ end
654
+
655
+ def timestamp_column_names
656
+ @timestamp_column_names ||= %w(created_at created_on updated_at updated_on).select do |name|
657
+ column_names.include?(name)
658
+ end
659
+ end
660
+
661
+ def inheritance_column_name
662
+ @inheritance_column_name ||= model_class && model_class.inheritance_column
663
+ end
664
+
665
+ def column_names
666
+ @column_names ||= @connection.columns(@table_name).collect(&:name)
667
+ end
668
+
669
+ def read_fixture_files
670
+ if File.file?(yaml_file_path)
671
+ read_yaml_fixture_files
672
+ elsif File.file?(csv_file_path)
673
+ read_csv_fixture_files
674
+ else
675
+ # Standard fixtures
676
+ Dir.entries(@fixture_path).each do |file|
677
+ path = File.join(@fixture_path, file)
678
+ if File.file?(path) and file !~ @file_filter
679
+ self[file] = Fixture.new(path, @class_name)
680
+ end
98
681
  end
99
- fixtures
100
682
  end
101
683
  end
102
684
 
103
- def delete_existing_fixtures
104
- @connection.delete "DELETE FROM #{@table_name}"
685
+ def read_yaml_fixture_files
686
+ yaml_string = ""
687
+ Dir["#{@fixture_path}/**/*.yml"].select { |f| test(?f, f) }.each do |subfixture_path|
688
+ yaml_string << IO.read(subfixture_path)
689
+ end
690
+ yaml_string << IO.read(yaml_file_path)
691
+
692
+ if yaml = parse_yaml_string(yaml_string)
693
+ # If the file is an ordered map, extract its children.
694
+ yaml_value =
695
+ if yaml.respond_to?(:type_id) && yaml.respond_to?(:value)
696
+ yaml.value
697
+ else
698
+ [yaml]
699
+ end
700
+
701
+ yaml_value.each do |fixture|
702
+ fixture.each do |name, data|
703
+ unless data
704
+ raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)"
705
+ end
706
+
707
+ self[name] = Fixture.new(data, @class_name)
708
+ end
709
+ end
710
+ end
105
711
  end
106
712
 
107
- def insert_fixtures
108
- @fixtures.values.each do |fixture|
109
- @connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES(#{fixture.value_list})"
713
+ def read_csv_fixture_files
714
+ reader = CSV::Reader.create(erb_render(IO.read(csv_file_path)))
715
+ header = reader.shift
716
+ i = 0
717
+ reader.each do |row|
718
+ data = {}
719
+ row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
720
+ self["#{Inflector::underscore(@class_name)}_#{i+=1}"]= Fixture.new(data, @class_name)
110
721
  end
111
722
  end
112
723
 
113
- def []=(key, value)
114
- @fixtures[key] = value
724
+ def yaml_file_path
725
+ "#{@fixture_path}.yml"
726
+ end
727
+
728
+ def csv_file_path
729
+ @fixture_path + ".csv"
730
+ end
731
+
732
+ def yaml_fixtures_key(path)
733
+ File.basename(@fixture_path).split(".").first
734
+ end
735
+
736
+ def parse_yaml_string(fixture_content)
737
+ YAML::load(erb_render(fixture_content))
738
+ rescue => error
739
+ raise Fixture::FormatError, "a YAML error occurred parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}"
740
+ end
741
+
742
+ def erb_render(fixture_content)
743
+ ERB.new(fixture_content).result
115
744
  end
116
745
  end
117
746
 
118
747
  class Fixture #:nodoc:
119
- def initialize(fixture_path, file)
120
- @fixture_path, @file = fixture_path, file
121
- @fixture = read_fixture
748
+ include Enumerable
749
+
750
+ class FixtureError < StandardError #:nodoc:
751
+ end
752
+
753
+ class FormatError < FixtureError #:nodoc:
754
+ end
755
+
756
+ attr_reader :class_name
757
+
758
+ def initialize(fixture, class_name)
759
+ case fixture
760
+ when Hash, YAML::Omap
761
+ @fixture = fixture
762
+ when String
763
+ @fixture = read_fixture_file(fixture)
764
+ else
765
+ raise ArgumentError, "Bad fixture argument #{fixture.inspect} during creation of #{class_name} fixture"
766
+ end
767
+
768
+ @class_name = class_name
769
+ end
770
+
771
+ def each
772
+ @fixture.each { |item| yield item }
122
773
  end
123
774
 
124
775
  def [](key)
@@ -130,43 +781,238 @@ class Fixture #:nodoc:
130
781
  end
131
782
 
132
783
  def key_list
133
- @fixture.keys.join(", ")
784
+ columns = @fixture.keys.collect{ |column_name| ActiveRecord::Base.connection.quote_column_name(column_name) }
785
+ columns.join(", ")
134
786
  end
135
787
 
136
788
  def value_list
137
- @fixture.values.map { |v| "'#{v}'" }.join(", ")
789
+ klass = @class_name.constantize rescue nil
790
+
791
+ list = @fixture.inject([]) do |fixtures, (key, value)|
792
+ col = klass.columns_hash[key] if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
793
+ fixtures << ActiveRecord::Base.connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
794
+ end
795
+ list * ', '
796
+ end
797
+
798
+ def find
799
+ klass = @class_name.is_a?(Class) ? @class_name : Object.const_get(@class_name) rescue nil
800
+ if klass
801
+ klass.find(self[klass.primary_key])
802
+ else
803
+ raise FixtureClassNotFound, "The class #{@class_name.inspect} was not found."
804
+ end
138
805
  end
139
806
 
140
807
  private
141
- def read_fixture
142
- IO.readlines("#{@fixture_path}/#{@file}").inject({}) do |fixture, line|
143
- key, value = line.split(/ => /)
144
- fixture[key.strip] = value.strip
808
+ def read_fixture_file(fixture_file_path)
809
+ IO.readlines(fixture_file_path).inject({}) do |fixture, line|
810
+ # Mercifully skip empty lines.
811
+ next if line =~ /^\s*$/
812
+
813
+ # Use the same regular expression for attributes as Active Record.
814
+ unless md = /^\s*([a-zA-Z][-_\w]*)\s*=>\s*(.+)\s*$/.match(line)
815
+ raise FormatError, "#{fixture_file_path}: fixture format error at '#{line}'. Expecting 'key => value'."
816
+ end
817
+ key, value = md.captures
818
+
819
+ # Disallow duplicate keys to catch typos.
820
+ raise FormatError, "#{fixture_file_path}: duplicate '#{key}' in fixture." if fixture[key]
821
+ fixture[key] = value.strip
145
822
  fixture
146
823
  end
147
824
  end
148
825
  end
149
826
 
150
- # A YamlFixture is like a fixture, but instead of a name to use as
151
- # a key, it uses a yaml_name.
152
- class YamlFixture < Fixture #:nodoc:
153
- # yaml_name is equivalent to a normal fixture's filename
154
- attr_accessor :yaml_name
827
+ module Test #:nodoc:
828
+ module Unit #:nodoc:
829
+ class TestCase #:nodoc:
830
+ superclass_delegating_accessor :fixture_path
831
+ superclass_delegating_accessor :fixture_table_names
832
+ superclass_delegating_accessor :fixture_class_names
833
+ superclass_delegating_accessor :use_transactional_fixtures
834
+ superclass_delegating_accessor :use_instantiated_fixtures # true, false, or :no_instances
835
+ superclass_delegating_accessor :pre_loaded_fixtures
155
836
 
156
- # constructor is passed the name & the actual instantiate fixture
157
- def initialize(yaml_name, fixture)
158
- @yaml_name, @fixture = yaml_name, fixture
159
- end
837
+ self.fixture_table_names = []
838
+ self.use_transactional_fixtures = false
839
+ self.use_instantiated_fixtures = true
840
+ self.pre_loaded_fixtures = false
841
+
842
+ @@already_loaded_fixtures = {}
843
+ self.fixture_class_names = {}
844
+
845
+ def self.set_fixture_class(class_names = {})
846
+ self.fixture_class_names = self.fixture_class_names.merge(class_names)
847
+ end
848
+
849
+ def self.fixtures(*table_names)
850
+ if table_names.first == :all
851
+ table_names = Dir["#{fixture_path}/*.yml"] + Dir["#{fixture_path}/*.csv"]
852
+ table_names.map! { |f| File.basename(f).split('.')[0..-2].join('.') }
853
+ else
854
+ table_names = table_names.flatten.map { |n| n.to_s }
855
+ end
856
+
857
+ self.fixture_table_names |= table_names
858
+ require_fixture_classes(table_names)
859
+ setup_fixture_accessors(table_names)
860
+ end
861
+
862
+ def self.require_fixture_classes(table_names = nil)
863
+ (table_names || fixture_table_names).each do |table_name|
864
+ file_name = table_name.to_s
865
+ file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
866
+ begin
867
+ require_dependency file_name
868
+ rescue LoadError
869
+ # Let's hope the developer has included it himself
870
+ end
871
+ end
872
+ end
873
+
874
+ def self.setup_fixture_accessors(table_names = nil)
875
+ (table_names || fixture_table_names).each do |table_name|
876
+ table_name = table_name.to_s.tr('.', '_')
877
+
878
+ define_method(table_name) do |*fixtures|
879
+ force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload
880
+
881
+ @fixture_cache[table_name] ||= {}
882
+
883
+ instances = fixtures.map do |fixture|
884
+ @fixture_cache[table_name].delete(fixture) if force_reload
160
885
 
161
- # given a valid yaml file name, create an array of YamlFixture objects
162
- def self.produce( yaml_file_name )
163
- results = []
164
- yaml_file = File.open( yaml_file_name )
165
- YAML::load_documents( yaml_file ) do |doc|
166
- f = YamlFixture.new( doc['name'], doc['data'] )
167
- results << f
886
+ if @loaded_fixtures[table_name][fixture.to_s]
887
+ @fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find
888
+ else
889
+ raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'"
890
+ end
891
+ end
892
+
893
+ instances.size == 1 ? instances.first : instances
894
+ end
895
+ end
896
+ end
897
+
898
+ def self.uses_transaction(*methods)
899
+ @uses_transaction = [] unless defined?(@uses_transaction)
900
+ @uses_transaction.concat methods.map(&:to_s)
901
+ end
902
+
903
+ def self.uses_transaction?(method)
904
+ @uses_transaction = [] unless defined?(@uses_transaction)
905
+ @uses_transaction.include?(method.to_s)
906
+ end
907
+
908
+ def use_transactional_fixtures?
909
+ use_transactional_fixtures &&
910
+ !self.class.uses_transaction?(method_name)
911
+ end
912
+
913
+ def setup_with_fixtures
914
+ return unless defined?(ActiveRecord::Base) && !ActiveRecord::Base.configurations.blank?
915
+
916
+ if pre_loaded_fixtures && !use_transactional_fixtures
917
+ raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
918
+ end
919
+
920
+ @fixture_cache = {}
921
+
922
+ # Load fixtures once and begin transaction.
923
+ if use_transactional_fixtures?
924
+ if @@already_loaded_fixtures[self.class]
925
+ @loaded_fixtures = @@already_loaded_fixtures[self.class]
926
+ else
927
+ load_fixtures
928
+ @@already_loaded_fixtures[self.class] = @loaded_fixtures
929
+ end
930
+ ActiveRecord::Base.send :increment_open_transactions
931
+ ActiveRecord::Base.connection.begin_db_transaction
932
+ # Load fixtures for every test.
933
+ else
934
+ Fixtures.reset_cache
935
+ @@already_loaded_fixtures[self.class] = nil
936
+ load_fixtures
937
+ end
938
+
939
+ # Instantiate fixtures for every test if requested.
940
+ instantiate_fixtures if use_instantiated_fixtures
941
+ end
942
+ alias_method :setup, :setup_with_fixtures
943
+
944
+ def teardown_with_fixtures
945
+ return unless defined?(ActiveRecord::Base) && !ActiveRecord::Base.configurations.blank?
946
+
947
+ unless use_transactional_fixtures?
948
+ Fixtures.reset_cache
949
+ end
950
+
951
+ # Rollback changes if a transaction is active.
952
+ if use_transactional_fixtures? && Thread.current['open_transactions'] != 0
953
+ ActiveRecord::Base.connection.rollback_db_transaction
954
+ Thread.current['open_transactions'] = 0
955
+ end
956
+ ActiveRecord::Base.verify_active_connections!
957
+ end
958
+ alias_method :teardown, :teardown_with_fixtures
959
+
960
+ def self.method_added(method)
961
+ case method.to_s
962
+ when 'setup'
963
+ unless method_defined?(:setup_without_fixtures)
964
+ alias_method :setup_without_fixtures, :setup
965
+ define_method(:setup) do
966
+ setup_with_fixtures
967
+ setup_without_fixtures
968
+ end
969
+ end
970
+ when 'teardown'
971
+ unless method_defined?(:teardown_without_fixtures)
972
+ alias_method :teardown_without_fixtures, :teardown
973
+ define_method(:teardown) do
974
+ teardown_without_fixtures
975
+ teardown_with_fixtures
976
+ end
977
+ end
978
+ end
979
+ end
980
+
981
+ private
982
+ def load_fixtures
983
+ @loaded_fixtures = {}
984
+ fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
985
+ unless fixtures.nil?
986
+ if fixtures.instance_of?(Fixtures)
987
+ @loaded_fixtures[fixtures.table_name] = fixtures
988
+ else
989
+ fixtures.each { |f| @loaded_fixtures[f.table_name] = f }
990
+ end
991
+ end
992
+ end
993
+
994
+ # for pre_loaded_fixtures, only require the classes once. huge speed improvement
995
+ @@required_fixture_classes = false
996
+
997
+ def instantiate_fixtures
998
+ if pre_loaded_fixtures
999
+ raise RuntimeError, 'Load fixtures before instantiating them.' if Fixtures.all_loaded_fixtures.empty?
1000
+ unless @@required_fixture_classes
1001
+ self.class.require_fixture_classes Fixtures.all_loaded_fixtures.keys
1002
+ @@required_fixture_classes = true
1003
+ end
1004
+ Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
1005
+ else
1006
+ raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
1007
+ @loaded_fixtures.each do |table_name, fixtures|
1008
+ Fixtures.instantiate_fixtures(self, table_name, fixtures, load_instances?)
1009
+ end
1010
+ end
1011
+ end
1012
+
1013
+ def load_instances?
1014
+ use_instantiated_fixtures != :no_instances
1015
+ end
168
1016
  end
169
- yaml_file.close
170
- results
171
1017
  end
172
1018
  end