Roman2K-rails-test-serving 0.1.4 → 0.1.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,79 @@
1
+ # RailsTestServing
2
+
3
+ Tired of waiting 10 seconds before your tests run? `RailsTestServing` can make them run almost instantly. This library is described more thoroughly in its [introduction article](http://roman.flucti.com/a-test-server-for-rails-applications).
4
+
5
+ ## Usage
6
+
7
+ 1. Install the gem:
8
+
9
+ gem install Roman2K-rails-test-serving -s http://gems.github.com
10
+
11
+ 2. Insert the following lines at the very top of `test/test_helper.rb`:
12
+
13
+ require 'rubygems'
14
+ require 'rails_test_serving'
15
+ RailsTestServing.boot
16
+
17
+ 3. Append the following line to `~/.bash_profile`:
18
+
19
+ export RUBYLIB=".:test:$RUBYLIB"
20
+
21
+ If you get loading errors during the next steps:
22
+ * Move the `RUBYLIB` line from `~/.bash_profile` to `~/.bash_login` instead.
23
+ * If you are using TextMate, you may try to apply [this (hopefully temporary) fix](http://roman.flucti.com/textmate-fix-for-relative-require-test_helper).
24
+
25
+ 4. Start the server:
26
+
27
+ cd <project-dir>
28
+ ruby test/test_helper.rb --serve
29
+
30
+ 5. Run tests as you usually do:
31
+
32
+ ruby test/unit/account_test.rb
33
+ ruby test/unit/account_test.rb -n /balance/
34
+
35
+ As a consequence, they work in RubyMate too (⌘R in TextMate).
36
+
37
+ 6. Have a lot a models and/or mixins? You can reduce reloading time to almost nothing by using `RailsTestServing` in combination with [RailsDevelopmentBoost](https://github.com/Roman2K/rails-dev-boost). Since was originally intended to speed up web-browsing in development mode, be sure to read the note in the `README` for how to enable it in test mode.
38
+
39
+ **Note:** if the server is not started, tests fall back to running the usual way.
40
+
41
+ ## Options
42
+
43
+ An option hash can be specified for `RailsTestServing` to use, by defining `$test_server_options` right before `require 'rails_test_serving'`. It must be a hash with symbol keys. Currently available options are:
44
+
45
+ * `reload`: An array of regular expressions (or any object responding to `===`) matching the name of the files that should be forced to reload right after the regular constant cleanup. Note that the constants these files have defined are kept around before being re-`require`'d.
46
+
47
+ Example `test_helper.rb` head:
48
+
49
+ require 'rubygems'
50
+
51
+ $test_server_options = { :reload => [/blueprint/] }
52
+ require 'rails_test_serving'
53
+ RailsTestServing.boot
54
+
55
+ # ...remainder here...
56
+
57
+ When running tests from the command line, you can bypass the server, forcing tests to be run locally, by passing the `--local` flag. For example:
58
+
59
+ ruby test/unit/account_test.rb --local
60
+
61
+ ## Caveats
62
+
63
+ * Tested working with Rails 2.1.2 up to 2.2.2. Compatibility with versions of Rails out of that range is not guaranteed.
64
+ * There might exist some quirks: search for "TODO" in the source. I can bear them but contributions are welcome.
65
+ * Some unit tests are left to be written.
66
+
67
+ ## Credits
68
+
69
+ Code:
70
+
71
+ * [Roman Le Négrate](http://roman.flucti.com), a.k.a. Roman2K ([contact](mailto:roman.lenegrate@gmail.com))
72
+ * [Jack Chen](http://github.com/chendo), a.k.a. chendo
73
+
74
+ Feedback:
75
+
76
+ * Justin Ko
77
+ * [Dr Nic Williams](http://drnicwilliams.com)
78
+
79
+ Released under the MIT license: see the `LICENSE` file.
data/Rakefile CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'echoe'
2
2
 
3
- Echoe.new('rails-test-serving', '0.1.4') do |p|
3
+ Echoe.new('rails-test-serving', '0.1.4.1') do |p|
4
4
  p.description = "Makes unit tests of a Rails application run instantly"
5
5
  p.url = "https://github.com/Roman2K/rails-test-serving"
6
6
  p.author = "Roman Le Négrate"
@@ -0,0 +1,58 @@
1
+ module RailsTestServing
2
+ module Bootstrap
3
+ SOCKET_PATH = ['tmp', 'sockets', 'test_server.sock']
4
+
5
+ def boot(argv=ARGV)
6
+ if argv.delete('--serve')
7
+ start_server
8
+ elsif !argv.delete('--local')
9
+ Client.run_tests
10
+ end
11
+ end
12
+
13
+ def service_uri
14
+ @service_uri ||= begin
15
+ # Determine RAILS_ROOT
16
+ root, max_depth = Pathname('.'), Pathname.pwd.expand_path.to_s.split(File::SEPARATOR).size
17
+ until root.join('config', 'boot.rb').file?
18
+ root = root.parent
19
+ if root.to_s.split(File::SEPARATOR).size >= max_depth
20
+ raise "RAILS_ROOT could not be determined"
21
+ end
22
+ end
23
+ root = root.cleanpath
24
+
25
+ # Adjust load path
26
+ $: << root.to_s << root.join('test').to_s
27
+
28
+ # Ensure socket directory exists
29
+ path = root.join(*SOCKET_PATH)
30
+ path.dirname.mkpath
31
+
32
+ # URI
33
+ "drbunix:#{path}"
34
+ end
35
+ end
36
+
37
+ def options
38
+ @options ||= begin
39
+ options = $test_server_options || {}
40
+ options[:reload] ||= []
41
+ options
42
+ end
43
+ end
44
+
45
+ def active?
46
+ @active
47
+ end
48
+
49
+ private
50
+
51
+ def start_server
52
+ @active = true
53
+ Server.start
54
+ ensure
55
+ @active = false
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,95 @@
1
+ module RailsTestServing
2
+ class Cleaner
3
+ include ConstantManagement
4
+
5
+ PAUSE = 0.01
6
+ TESTCASE_CLASS_NAMES = %w( Test::Unit::TestCase
7
+ ActiveSupport::TestCase
8
+ ActionView::TestCase
9
+ ActionController::TestCase
10
+ ActionController::IntegrationTest
11
+ ActionMailer::TestCase )
12
+
13
+ def initialize
14
+ start_worker
15
+ end
16
+
17
+ def clean_up_around
18
+ check_worker_health
19
+ sleep PAUSE while @working
20
+ begin
21
+ reload_app
22
+ yield
23
+ ensure
24
+ @working = true
25
+ sleep PAUSE until @worker.stop?
26
+ @worker.wakeup
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def start_worker
33
+ @worker = Thread.new do
34
+ Thread.abort_on_exception = true
35
+ loop do
36
+ Thread.stop
37
+ begin
38
+ clean_up_app
39
+ remove_tests
40
+ ensure
41
+ @working = false
42
+ end
43
+ end
44
+ end
45
+ @working = false
46
+ end
47
+
48
+ def check_worker_health
49
+ unless @worker.alive?
50
+ $stderr.puts "cleaning thread died, restarting"
51
+ start_worker
52
+ end
53
+ end
54
+
55
+ def clean_up_app
56
+ ActionController::Dispatcher.new(StringIO.new).cleanup_application
57
+ if defined?(Fixtures) && Fixtures.respond_to?(:reset_cache)
58
+ Fixtures.reset_cache
59
+ end
60
+
61
+ # Reload files that match :reload here instead of in reload_app since the
62
+ # :reload option is intended to target files that don't change between two
63
+ # consecutive runs (an external library for example). That way, they are
64
+ # reloaded in the background instead of slowing down the next run.
65
+ reload_specified_source_files
66
+ end
67
+
68
+ def remove_tests
69
+ TESTCASE_CLASS_NAMES.each do |name|
70
+ next unless klass = constantize(name)
71
+ remove_constants(*subclasses_of(klass).map { |c| c.to_s }.grep(/Test$/) - TESTCASE_CLASS_NAMES)
72
+ end
73
+ end
74
+
75
+ def reload_app
76
+ ActionController::Dispatcher.new(StringIO.new).reload_application
77
+ end
78
+
79
+ def reload_specified_source_files
80
+ to_reload =
81
+ $".select do |path|
82
+ RailsTestServing.options[:reload].any? do |matcher|
83
+ matcher === path
84
+ end
85
+ end
86
+
87
+ # Force a reload by removing matched files from $"
88
+ $".replace($" - to_reload)
89
+
90
+ to_reload.each do |file|
91
+ require file
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,53 @@
1
+ module RailsTestServing
2
+ module Client
3
+ extend self
4
+
5
+ # Setting this variable to true inhibits #run_tests.
6
+ @@disabled = false
7
+
8
+ def disable
9
+ @@disabled = true
10
+ yield
11
+ ensure
12
+ @@disabled = false
13
+ end
14
+
15
+ def tests_on_exit
16
+ !Test::Unit.run?
17
+ end
18
+
19
+ def tests_on_exit=(yes)
20
+ Test::Unit.run = !yes
21
+ end
22
+
23
+ def run_tests
24
+ return if @@disabled
25
+ run_tests!
26
+ end
27
+
28
+ private
29
+
30
+ def run_tests!
31
+ handle_process_lifecycle do
32
+ server = DRbObject.new_with_uri(RailsTestServing.service_uri)
33
+ begin
34
+ puts(server.run($0, ARGV))
35
+ rescue DRb::DRbConnError
36
+ raise ServerUnavailable
37
+ end
38
+ end
39
+ end
40
+
41
+ def handle_process_lifecycle
42
+ Client.tests_on_exit = false
43
+ begin
44
+ yield
45
+ rescue ServerUnavailable, InvalidArgumentPattern
46
+ Client.tests_on_exit = true
47
+ else
48
+ # TODO exit with a status code reflecting the result of the tests
49
+ exit 0
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,31 @@
1
+ module RailsTestServing
2
+ module ConstantManagement
3
+ extend self
4
+
5
+ def legit?(const)
6
+ !const.to_s.empty? && constantize(const) == const
7
+ end
8
+
9
+ def constantize(name)
10
+ eval("#{name} if defined? #{name}", TOPLEVEL_BINDING)
11
+ end
12
+
13
+ def constantize!(name)
14
+ name.to_s.split('::').inject(Object) { |namespace, short| namespace.const_get(short) }
15
+ end
16
+
17
+ # ActiveSupport's Module#remove_class doesn't behave quite the way I would expect it to.
18
+ def remove_constants(*names)
19
+ names.map do |name|
20
+ namespace, short = name.to_s =~ /^(.+)::(.+?)$/ ? [$1, $2] : ['Object', name]
21
+ constantize!(namespace).module_eval { remove_const(short) if const_defined?(short) }
22
+ end
23
+ end
24
+
25
+ def subclasses_of(parent, options={})
26
+ children = []
27
+ ObjectSpace.each_object(Class) { |klass| children << klass if klass < parent && (!options[:legit] || legit?(klass)) }
28
+ children
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,131 @@
1
+ module RailsTestServing
2
+ class Server
3
+ GUARD = Mutex.new
4
+ PREPARATION_GUARD = Mutex.new
5
+
6
+ def self.start
7
+ server = Server.new
8
+ DRb.start_service(RailsTestServing.service_uri, server)
9
+ Thread.new { server.prepare }
10
+ DRb.thread.join
11
+ end
12
+
13
+ include Utilities
14
+
15
+ def run(file, argv)
16
+ GUARD.synchronize do
17
+ prepare
18
+ perform_run(file, argv)
19
+ end
20
+ end
21
+
22
+ def prepare
23
+ PREPARATION_GUARD.synchronize do
24
+ @prepared ||= begin
25
+ ENV['RAILS_ENV'] = 'test'
26
+ log "** Test server starting [##{$$}]..." do
27
+ enable_dependency_tracking
28
+ start_cleaner
29
+ load_framework
30
+ end
31
+ install_signal_traps
32
+ true
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def enable_dependency_tracking
40
+ require 'config/boot'
41
+
42
+ Rails::Configuration.class_eval do
43
+ unless method_defined? :cache_classes
44
+ raise "#{self.class} out of sync with current Rails version"
45
+ end
46
+
47
+ def cache_classes
48
+ false
49
+ end
50
+ end
51
+ end
52
+
53
+ def start_cleaner
54
+ @cleaner = Cleaner.new
55
+ end
56
+
57
+ def load_framework
58
+ Client.disable do
59
+ $: << 'test'
60
+ require 'test_helper'
61
+ end
62
+ end
63
+
64
+ def install_signal_traps
65
+ log " - CTRL+C: Stop the server\n"
66
+ trap(:INT) do
67
+ GUARD.synchronize do
68
+ log "** Stopping the server..." do
69
+ DRb.thread.raise Interrupt, "stop"
70
+ end
71
+ end
72
+ end
73
+
74
+ log " - CTRL+Z: Reset database column information cache\n"
75
+ trap(:TSTP) do
76
+ GUARD.synchronize do
77
+ log "** Resetting database column information cache..." do
78
+ ActiveRecord::Base.instance_eval { subclasses }.each { |c| c.reset_column_information }
79
+ end
80
+ end
81
+ end
82
+
83
+ log " - CTRL+`: Reset lazy-loaded constants\n"
84
+ trap(:QUIT) do
85
+ GUARD.synchronize do
86
+ log "** Resetting lazy-loaded constants..." do
87
+ (defined?(ActiveSupport::Dependencies) ? ActiveSupport::Dependencies : Dependencies).clear
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ def perform_run(file, argv)
94
+ sanitize_arguments!(file, argv)
95
+ log ">> " + [shorten_path(file), *argv].join(' ') do
96
+ capture_test_result(file, argv)
97
+ end
98
+ end
99
+
100
+ def sanitize_arguments!(file, argv)
101
+ if file =~ /^-/
102
+ # No file was specified for loading, only options. It's the case with
103
+ # Autotest.
104
+ raise InvalidArgumentPattern
105
+ end
106
+
107
+ # Filter out the junk that TextMate seems to inject into ARGV when running
108
+ # focused tests.
109
+ while a = find_index_by_pattern(argv, /^\[/) and z = find_index_by_pattern(argv[a..-1], /\]$/)
110
+ argv[a..a+z] = []
111
+ end
112
+ end
113
+
114
+ def capture_test_result(file, argv)
115
+ result = []
116
+ @cleaner.clean_up_around do
117
+ result << capture_standard_stream('err') do
118
+ result << capture_standard_stream('out') do
119
+ result << capture_testrunner_result do
120
+ fix_objectspace_collector do
121
+ Client.disable { load(file) }
122
+ Test::Unit::AutoRunner.run(false, nil, argv)
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
128
+ result.reverse.join
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,102 @@
1
+ module RailsTestServing
2
+ module Utilities
3
+ def log(message, stream=$stdout)
4
+ print = lambda do |str|
5
+ stream.print(str)
6
+ stream.flush
7
+ end
8
+
9
+ print[message]
10
+ if block_given?
11
+ result = nil
12
+ elapsed = Benchmark.realtime do
13
+ result = yield
14
+ end
15
+ print[" (%d ms)\n" % (elapsed * 1000)]
16
+ result
17
+ end
18
+ end
19
+
20
+ def capture_standard_stream(name)
21
+ eval("old, $std#{name} = $std#{name}, StringIO.new")
22
+ begin
23
+ yield
24
+ return eval("$std#{name}").string
25
+ ensure
26
+ eval("$std#{name} = old")
27
+ end
28
+ end
29
+
30
+ def capture_testrunner_result
31
+ set_default_testrunner_stream(io = StringIO.new) { yield }
32
+ io.string
33
+ end
34
+
35
+ # The default output stream of TestRunner is STDOUT which cannot be captured
36
+ # and, as a consequence, neither can TestRunner output when not instantiated
37
+ # explicitely. The following method can change the default output stream
38
+ # argument so that it can be set to a stream that can be captured instead.
39
+ def set_default_testrunner_stream(io)
40
+ require 'test/unit/ui/console/testrunner'
41
+
42
+ Test::Unit::UI::Console::TestRunner.class_eval do
43
+ alias_method :old_initialize, :initialize
44
+ def initialize(suite, output_level, io=Thread.current["test_runner_io"])
45
+ old_initialize(suite, output_level, io)
46
+ end
47
+ end
48
+ Thread.current["test_runner_io"] = io
49
+
50
+ begin
51
+ return yield
52
+ ensure
53
+ Thread.current["test_runner_io"] = nil
54
+ Test::Unit::UI::Console::TestRunner.class_eval do
55
+ alias_method :initialize, :old_initialize
56
+ remove_method :old_initialize
57
+ end
58
+ end
59
+ end
60
+
61
+ # The stock ObjectSpace collector collects every single class that inherits
62
+ # from Test::Unit, including those which have just been unassigned from
63
+ # their constant and not yet garbage collected. This method fixes that
64
+ # behaviour by filtering out these soon-to-be-garbage-collected classes.
65
+ def fix_objectspace_collector
66
+ require 'test/unit/collector/objectspace'
67
+
68
+ Test::Unit::Collector::ObjectSpace.class_eval do
69
+ alias_method :old_collect, :collect
70
+ def collect(name)
71
+ tests = []
72
+ ConstantManagement.subclasses_of(Test::Unit::TestCase, :legit => true).each { |klass| add_suite(tests, klass.suite) }
73
+ suite = Test::Unit::TestSuite.new(name)
74
+ sort(tests).each { |t| suite << t }
75
+ suite
76
+ end
77
+ end
78
+
79
+ begin
80
+ return yield
81
+ ensure
82
+ Test::Unit::Collector::ObjectSpace.class_eval do
83
+ alias_method :collect, :old_collect
84
+ remove_method :old_collect
85
+ end
86
+ end
87
+ end
88
+
89
+ def shorten_path(path)
90
+ shortenable, base = File.expand_path(path), File.expand_path(Dir.pwd)
91
+ attempt = shortenable.sub(/^#{Regexp.escape base + File::SEPARATOR}/, '')
92
+ attempt.length < path.length ? attempt : path
93
+ end
94
+
95
+ def find_index_by_pattern(enumerable, pattern)
96
+ enumerable.each_with_index do |element, index|
97
+ return index if pattern === element
98
+ end
99
+ nil
100
+ end
101
+ end
102
+ end
@@ -2,15 +2,15 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{rails-test-serving}
5
- s.version = "0.1.4"
5
+ s.version = "0.1.4.1"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
8
  s.authors = ["Roman Le N\303\251grate"]
9
- s.date = %q{2009-02-08}
9
+ s.date = %q{2009-03-12}
10
10
  s.description = %q{Makes unit tests of a Rails application run instantly}
11
11
  s.email = %q{roman.lenegrate@gmail.com}
12
- s.extra_rdoc_files = ["lib/rails_test_serving.rb", "LICENSE", "README.rdoc"]
13
- s.files = ["lib/rails_test_serving.rb", "LICENSE", "Rakefile", "README.rdoc", "test/rails_test_serving_test.rb", "Manifest", "rails-test-serving.gemspec", "test/rails_test_serving/bootstrap_test.rb", "test/rails_test_serving/cleaner_test.rb", "test/rails_test_serving/client_test.rb", "test/rails_test_serving/constant_management_test.rb", "test/rails_test_serving/server_test.rb", "test/rails_test_serving/utilities_test.rb", "test/test_helper.rb"]
12
+ s.extra_rdoc_files = ["lib/rails_test_serving/bootstrap.rb", "lib/rails_test_serving/cleaner.rb", "lib/rails_test_serving/client.rb", "lib/rails_test_serving/constant_management.rb", "lib/rails_test_serving/server.rb", "lib/rails_test_serving/utilities.rb", "lib/rails_test_serving.rb", "LICENSE", "README.mdown"]
13
+ s.files = ["lib/rails_test_serving/bootstrap.rb", "lib/rails_test_serving/cleaner.rb", "lib/rails_test_serving/client.rb", "lib/rails_test_serving/constant_management.rb", "lib/rails_test_serving/server.rb", "lib/rails_test_serving/utilities.rb", "lib/rails_test_serving.rb", "LICENSE", "Manifest", "Rakefile", "README.mdown", "test/rails_test_serving/bootstrap_test.rb", "test/rails_test_serving/cleaner_test.rb", "test/rails_test_serving/client_test.rb", "test/rails_test_serving/constant_management_test.rb", "test/rails_test_serving/server_test.rb", "test/rails_test_serving/utilities_test.rb", "test/test_helper.rb", "rails-test-serving.gemspec"]
14
14
  s.has_rdoc = true
15
15
  s.homepage = %q{https://github.com/Roman2K/rails-test-serving}
16
16
  s.rdoc_options = ["--main", "README.mdown", "--inline-source", "--line-numbers", "--charset", "UTF-8"]
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: Roman2K-rails-test-serving
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - "Roman Le N\xC3\xA9grate"
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-02-08 00:00:00 -08:00
12
+ date: 2009-03-12 00:00:00 -07:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -20,17 +20,27 @@ executables: []
20
20
  extensions: []
21
21
 
22
22
  extra_rdoc_files:
23
+ - lib/rails_test_serving/bootstrap.rb
24
+ - lib/rails_test_serving/cleaner.rb
25
+ - lib/rails_test_serving/client.rb
26
+ - lib/rails_test_serving/constant_management.rb
27
+ - lib/rails_test_serving/server.rb
28
+ - lib/rails_test_serving/utilities.rb
23
29
  - lib/rails_test_serving.rb
24
30
  - LICENSE
25
- - README.rdoc
31
+ - README.mdown
26
32
  files:
33
+ - lib/rails_test_serving/bootstrap.rb
34
+ - lib/rails_test_serving/cleaner.rb
35
+ - lib/rails_test_serving/client.rb
36
+ - lib/rails_test_serving/constant_management.rb
37
+ - lib/rails_test_serving/server.rb
38
+ - lib/rails_test_serving/utilities.rb
27
39
  - lib/rails_test_serving.rb
28
40
  - LICENSE
29
- - Rakefile
30
- - README.rdoc
31
- - test/rails_test_serving_test.rb
32
41
  - Manifest
33
- - rails-test-serving.gemspec
42
+ - Rakefile
43
+ - README.mdown
34
44
  - test/rails_test_serving/bootstrap_test.rb
35
45
  - test/rails_test_serving/cleaner_test.rb
36
46
  - test/rails_test_serving/client_test.rb
@@ -38,6 +48,7 @@ files:
38
48
  - test/rails_test_serving/server_test.rb
39
49
  - test/rails_test_serving/utilities_test.rb
40
50
  - test/test_helper.rb
51
+ - rails-test-serving.gemspec
41
52
  has_rdoc: true
42
53
  homepage: https://github.com/Roman2K/rails-test-serving
43
54
  post_install_message: