specjour 0.2.5 → 0.3.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/History.markdown +51 -20
  2. data/README.markdown +53 -26
  3. data/Rakefile +3 -5
  4. data/VERSION +1 -1
  5. data/bin/specjour +1 -48
  6. data/lib/specjour/cli.rb +97 -0
  7. data/lib/specjour/configuration.rb +73 -0
  8. data/lib/specjour/connection.rb +1 -1
  9. data/lib/specjour/cucumber/distributed_formatter.rb +2 -5
  10. data/lib/specjour/cucumber/preloader.rb +13 -0
  11. data/lib/specjour/cucumber.rb +3 -2
  12. data/lib/specjour/db_scrub.rb +8 -1
  13. data/lib/specjour/dispatcher.rb +99 -36
  14. data/lib/specjour/manager.rb +63 -37
  15. data/lib/specjour/printer.rb +33 -11
  16. data/lib/specjour/quiet_fork.rb +11 -0
  17. data/lib/specjour/rspec/distributed_formatter.rb +4 -15
  18. data/lib/specjour/rspec/preloader.rb +8 -0
  19. data/lib/specjour/rspec.rb +1 -0
  20. data/lib/specjour/rsync_daemon.rb +1 -1
  21. data/lib/specjour/socket_helper.rb +28 -0
  22. data/lib/specjour/worker.rb +42 -25
  23. data/lib/specjour.rb +13 -5
  24. data/spec/spec_helper.rb +12 -0
  25. data/spec/specjour/cli_spec.rb +104 -0
  26. data/spec/specjour/configuration_spec.rb +112 -0
  27. data/spec/{cpu_spec.rb → specjour/cpu_spec.rb} +0 -0
  28. data/spec/{manager_spec.rb → specjour/manager_spec.rb} +2 -2
  29. data/spec/specjour/printer_spec.rb +101 -0
  30. data/spec/{rsync_daemon_spec.rb → specjour/rsync_daemon_spec.rb} +2 -0
  31. data/spec/specjour_spec.rb +13 -2
  32. data/specjour.gemspec +33 -26
  33. metadata +69 -31
  34. data/lib/specjour/cucumber/dispatcher.rb +0 -18
  35. data/lib/specjour/cucumber/printer.rb +0 -9
  36. data/lib/specjour/socket_helpers.rb +0 -11
  37. data/lib/specjour/tasks/dispatch.rake +0 -21
  38. data/lib/specjour/tasks/specjour.rb +0 -1
  39. data/rails/init.rb +0 -6
  40. data/spec/lib/specjour/worker_spec.rb +0 -14
data/History.markdown CHANGED
@@ -1,22 +1,59 @@
1
1
  History
2
2
  =======
3
- 0.2.5
4
- -----
5
- *2010-05-13*
6
3
 
7
- * [changed] The rails plugin now runs in a Rails.configuration.after_intialize
4
+ ## 0.3.0.rc1 / thor
5
+
6
+ * [removed] Rake tasks have been removed, use the command-line instead.
7
+
8
+ * [added] Thor is now used to parse command-line arguments. Try `specjour help`
9
+ for more details.
10
+
11
+ * [added] Test discovery. Features will be autodiscovered by looking for a
12
+ `features` directory in your project. If you only want to run features, use
13
+ `specjour dispatch project_path/features`.
14
+
15
+ * [changed] No longer need to run a manager and a dispatcher in separate
16
+ processes. When not distributing to other computers, simply run `specjour` in
17
+ your project directory to run the suite among the number of cores on your
18
+ machine.
19
+
20
+ * [added] Project aliases. If you want to isolate a few computers in the
21
+ cluster, tell them to listen for a different project name and run the
22
+ dispatcher with that new name.
23
+
24
+ $ specjour listen --projects foo2
25
+ $ specjour dispatch --alias foo2
26
+
27
+ * [added] Preparation. Running `specjour prepare` invokes the
28
+ `Specjour::Configuration.prepare` block on each worker. By default this
29
+ drops the worker's database and brings it back up.
30
+
31
+ * [removed] --batch option which sent back results in batches. Now that each
32
+ spec is run one at a time, batching no longer makes sense.
33
+
34
+ * [removed] Global listening. You now must provide the project names you want to
35
+ run specs for. Defaults to the project in your current working directory.
36
+
37
+ $ specjour listen --projects foo bar
38
+
39
+
40
+ ## 0.2.6 / master
41
+
42
+ * [fixed] Rsync copies symbolic links. gh-6
43
+ * [fixed] DbScrub explicitly requires its dependencies and no longer loads the
44
+ Rakefile. gh-10
45
+
46
+ ## 0.2.5 / 2010-05-13
47
+
48
+ * [changed] The rails plugin now runs in a Rails.configuration.after\_initialize
8
49
  block
9
50
 
10
- 0.2.4
11
- -----
12
- *2010-05-10*
51
+ ## 0.2.4 / 2010-05-10
13
52
 
14
53
  * [added] Correct exit status
15
54
  * [fixed] Will reconnect when connection is lost while requesting tests
16
55
 
17
- 0.2.3
18
- -----
19
- *2010-04-25*
56
+ ## 0.2.3 / 2010-04-25
20
57
 
21
58
  * [fixed] Absolute paths in rsyncd.conf restrict portability. The rsync daemon
22
59
  completely fails when it can't find the path to serve which typically happens
@@ -26,23 +63,17 @@ History
26
63
 
27
64
  * [fixed] CPU core detection works on OSX Core i7 (thanks Hashrocket!)
28
65
 
29
- 0.2.2
30
- -----
31
- *2010-04-22*
66
+ ## 0.2.2 / 2010-04-22
32
67
 
33
68
  * [added] Backtrace for cucumber failures
34
69
 
35
- 0.2.1
36
- -----
37
- *2010-04-21*
70
+ ## 0.2.1 / 2010-04-21
38
71
 
39
72
  * [added] The rsync daemon configuration file now lives in
40
- project_path/.specjour/rsyncd.conf. Edit your rsync exclusions there.
73
+ project\_path/.specjour/rsyncd.conf. Edit your rsync exclusions there.
41
74
  * [fixed] Don't report connection errors when CTRL-C is sent.
42
75
 
43
- 0.2.0
44
- -----
45
- *2010-04-20*
76
+ ## 0.2.0 / 2010-04-20
46
77
 
47
78
  * [added] Cucumber support. `rake specjour:cucumber`
48
79
  * [added] CPU Core detection, use -w to override with less or more workers
data/README.markdown CHANGED
@@ -16,25 +16,35 @@ _Distribute your spec suite amongst your LAN via Bonjour._
16
16
  ## Installation
17
17
  gem install specjour
18
18
 
19
- ## Start a manager
20
- Running `specjour` on the command-line will start a manager which advertises that it's ready to run specs. By default, the manager will use your system cores to determine the number of workers to use. Two cores equals two workers. If you only want to dedicate 1 core to running specs, use `$ specjour --workers 1`.
19
+ ## Give it a try
20
+ Running `specjour` starts a dispatcher, a manager, and multiple workers - all
21
+ of the componenets necessary for distributing your test suite.
21
22
 
23
+ $ cd myproject
22
24
  $ specjour
23
25
 
24
- ## Setup the dispatcher
25
- Require specjour's rake tasks in your project's `Rakefile`.
26
+ ## Start a manager
27
+ Running `specjour listen` will start a manager which advertises that it's ready
28
+ to run specs. By default, the manager runs tests for the project in the
29
+ current directory and uses your system cores to determine the number of workers
30
+ to start. If your system has two cores, two workers will run tests.
26
31
 
27
- require 'specjour/tasks/specjour'
32
+ $ specjour listen
28
33
 
29
- ## Distribute the specs
30
- Run the rake task to distribute the specs among the managers you started.
34
+ ## Distribute the tests
35
+ Dispatch the tests among the managers you started. Specjour checks the 'spec' and
36
+ 'features' directories for tests.
31
37
 
32
- $ rake specjour
38
+ $ specjour
33
39
 
34
- ## Distribute the features
35
- Run the rake task to distribute the features among the managers you started.
40
+ ## Distribute a subset of tests
41
+ The first parameter to the specjour command is a test directory. It defalts to
42
+ the current directory and searches for 'spec' and 'features' paths therein.
36
43
 
37
- $ rake specjour:cucumber
44
+ $ specjour spec # all rspec tests
45
+ $ specjour spec/models # only model tests
46
+ $ specjour features # only features
47
+ $ specjour ~/my_other_project/features
38
48
 
39
49
  ## Rails
40
50
  Each worker should run their specs in an isolated database. Modify the test database name in your `config/database.yml` to include the following environment variable (Influenced by [parallel\_tests](http://github.com/grosser/parallel_tests)):
@@ -42,32 +52,52 @@ Each worker should run their specs in an isolated database. Modify the test data
42
52
  test:
43
53
  database: blog_test<%=ENV['TEST_ENV_NUMBER']%>
44
54
 
45
- Add the specjour gem to your project:
55
+ ### ActiveRecord Hooks
56
+ Specjour contains ActiveRecord hooks that clear database tables before running tests using `DELETE FROM <table_name>;`. Additionally, test databases will be created if they don't exist (i.e. `CREATE DATABASE blog_test8` for the 8th worker) and your schema will be loaded when the database is out of date.
46
57
 
47
- config.gem 'specjour'
58
+ ## Custom Hooks
59
+ Specjour allows you to hook in to the test process on a per-machine and
60
+ per-worker level through the before_fork and after_fork configuration blocks.
61
+ If the default ActiveRecord hook doesn't set up the database properly for your
62
+ test suite, override it with a custom after_fork hook.
48
63
 
49
- Doing this enables a rails plugin wherein each worker will attempt to clear its database tables before running any specs via `DELETE FROM <table_name>;`. Additionally, test databases will be created if they don't exist (i.e. `CREATE DATABASE blog_test8` for the 8th worker) and your schema will be loaded when the database is out of date.
64
+ # config/initializers/specjour.rb
65
+ Rails.configuration.after_initialize do
50
66
 
51
- ### Customizing database setup
52
- If the plugin doesn't set up the database properly for your test suite, bypass it entirely. Remove specjour as a project gem and create your own initializer to setup the database. Specjour sets the environment variable PREPARE\_DB when it runs your specs so you can look for that when setting up the database.
67
+ # Modify the way you use bundler
68
+ Specjour::Configuration.before_fork = lambda do
69
+ # TODO: not working as advertised
70
+ system('bundle install --without production')
71
+ end
53
72
 
54
- # config/initializers/specjour.rb
73
+ # Modify your database setup
74
+ Specjour::Configuration.after_fork = lambda do
75
+ # custom database setup here
76
+ end
55
77
 
56
- if ENV['PREPARE_DB']
57
- load 'Rakefile'
78
+ end
79
+
80
+ A preparation hook is run when `specjour prepare` is invoked. This hook allows
81
+ you to run arbitrary code on all of the listening workers. By default, it drops
82
+ and recreates the database on all workers.
83
+
84
+ Rails.configuration.after_initialize do
85
+
86
+ # Modify preparation
87
+ Specjour::Configuration.prepare = lambda do
88
+ # custom preparation code
89
+ end
58
90
 
59
- # clear the db and load db/seeds.rb
60
- Rake::Task['db:reset'].invoke
61
91
  end
62
92
 
63
93
  ## Only listen to supported projects
64
94
  By default, a manager will listen to all projects trying to distribute specs over the network. Sometimes you'll only want a manager to respond to one specific spec suite. You can accomplish this with the `--projects` flag.
65
95
 
66
- $ specjour --projects bizconf # only run specs for the bizconf project
96
+ $ specjour listen --projects bizconf # only run specs for the bizconf project
67
97
 
68
98
  You could also listen to multiple projects:
69
99
 
70
- $ specjour --projects bizconf,workbeast # only run specs for the bizconf and workbeast projects
100
+ $ specjour listen --projects bizconf,workbeast # only run specs for the bizconf and workbeast projects
71
101
 
72
102
  ## Customize what gets rsync'd
73
103
  The standard rsync configuration file may be too broad for your
@@ -76,9 +106,6 @@ directory, add an exclusion to your projects rsyncd.conf file.
76
106
 
77
107
  $ vi workbeast/.specjour/rsyncd.conf
78
108
 
79
- ## Use one machine
80
- Distributed testing doesn't have to happen over multiple machines, just multiple processes. Specjour is an excellent candidiate for running 4 tests at once on one machine with 4 cores. Just run `$ specjour` in one window and `$ rake specjour` in another.
81
-
82
109
  ## Thanks
83
110
 
84
111
  * shayarnett - Cucumber support, pairing and other various patches
data/Rakefile CHANGED
@@ -12,9 +12,10 @@ begin
12
12
  gem.authors = ["Sandro Turriate"]
13
13
  gem.add_dependency "dnssd", "1.3.1"
14
14
  gem.add_dependency "rspec"
15
+ gem.add_dependency "thor", "0.13.6"
15
16
  gem.add_development_dependency "rspec", "1.3.0"
16
- gem.add_development_dependency "rr", "0.10.11"
17
- gem.add_development_dependency "yard", "0.5.3"
17
+ gem.add_development_dependency "rr", ">=0.10.11"
18
+ gem.add_development_dependency "yard", ">=0.5.3"
18
19
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
19
20
  end
20
21
  Jeweler::GemcutterTasks.new
@@ -46,6 +47,3 @@ rescue LoadError
46
47
  abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
47
48
  end
48
49
  end
49
-
50
- $:.unshift(File.dirname(__FILE__) + "/lib")
51
- require 'specjour/tasks/specjour'
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.5
1
+ 0.3.0.rc1
data/bin/specjour CHANGED
@@ -1,51 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
- require 'optparse'
3
2
  require 'specjour'
4
3
 
5
- options = {:batch_size => 1}
6
-
7
- optparse = OptionParser.new do |opts|
8
- opts.banner = "Usage: specjour [options]"
9
-
10
- opts.on('-w', '--workers WORKERS', Numeric, "Number of WORKERS to spin up, defaults to available cores") do |n|
11
- options[:worker_size] = n
12
- end
13
-
14
- opts.on('-b', '--batch-size [SIZE]', Integer, "Number of specs to run before reporting back to the dispatcher, defaults to #{options[:batch_size]}") do |n|
15
- options[:batch_size] = n
16
- end
17
-
18
- opts.on('-p', '--projects PROJECTS', Array, "Only run specs for these comma delimited project names, i.e. workbeast,taigan") do |project_names|
19
- options[:registered_projects] = project_names
20
- end
21
-
22
- opts.on('--do-work OPTIONS', Array, 'INTERNAL USE ONLY') do |args|
23
- options[:worker_args] = args[0], args[1], args[2]
24
- end
25
-
26
- opts.on('--log', TrueClass, 'print debug messages to stdout') do |val|
27
- Specjour.new_logger Logger::DEBUG
28
- end
29
-
30
- opts.on_tail('-v', '--version', 'Show the version of specjour') do
31
- abort Specjour::VERSION
32
- end
33
-
34
- opts.on_tail("-h", "--help", "Show this message") do
35
- summary = opts.to_a
36
- summary.first << "\n"
37
- abort summary.reject {|s| s =~ /INTERNAL/}.join
38
- end
39
- end
40
-
41
- optparse.parse!
42
-
43
- abort(%(ERROR: I don't understand the following flags: "#{ARGV.join(', ')}")) if ARGV.any?
44
-
45
- if options[:worker_args]
46
- options[:worker_args] << options[:batch_size]
47
- Specjour::Worker.new(*options[:worker_args]).run
48
- else
49
- options[:worker_size] ||= Specjour::CPU.cores
50
- Specjour::Manager.new(options).start
51
- end
4
+ Specjour::CLI.start
@@ -0,0 +1,97 @@
1
+ module Specjour
2
+ require 'thor'
3
+ class CLI < Thor
4
+
5
+ def self.printable_tasks
6
+ super.reject{|t| t.last =~ /INTERNAL USE/ }
7
+ end
8
+
9
+ def self.worker_option
10
+ method_option :workers, :aliases => "-w", :type => :numeric, :desc => "Number of concurent processes to run. Defaults to your system's available cores."
11
+ end
12
+
13
+ def self.dispatcher_option
14
+ method_option :alias, :aliases => "-a", :desc => "Project name advertised to listeners"
15
+ end
16
+
17
+ def self.start(original_args=ARGV, config={})
18
+ real_tasks = all_tasks.keys | HELP_MAPPINGS
19
+ unless real_tasks.include? original_args.first
20
+ original_args.unshift default_task
21
+ end
22
+ super(original_args)
23
+ end
24
+
25
+ default_task :dispatch
26
+
27
+ class_option :log, :aliases => "-l", :type => :boolean, :desc => "Print debug messages to $stdout"
28
+
29
+ desc "listen", "Advertise availability to run specs\nDefaults to current directory"
30
+ worker_option
31
+ method_option :projects, :aliases => "-p", :type => :array, :desc => "Projects supported by this listener"
32
+ def listen
33
+ handle_logging
34
+ handle_workers
35
+ args[:registered_projects] = args.delete(:projects) || [File.basename(Dir.pwd)]
36
+ Specjour::Manager.new(args).start
37
+ end
38
+
39
+ desc "dispatch [PROJECT_PATH]", "Run specs in this project"
40
+ worker_option
41
+ dispatcher_option
42
+ def dispatch(path = Dir.pwd)
43
+ handle_logging
44
+ handle_workers
45
+ handle_dispatcher(path)
46
+ Specjour::Dispatcher.new(args).start
47
+ end
48
+
49
+ desc "prepare [PROJECT_PATH]", "Run the prepare block on all listening workers"
50
+ worker_option
51
+ dispatcher_option
52
+ def prepare(path = Dir.pwd)
53
+ handle_logging
54
+ handle_workers
55
+ handle_dispatcher(path)
56
+ args[:worker_task] = 'prepare'
57
+ Specjour::Dispatcher.new(args).start
58
+ end
59
+
60
+ desc "version", "Show the version of specjour"
61
+ def version
62
+ puts Specjour::VERSION
63
+ end
64
+
65
+ desc "work", "INTERNAL USE ONLY"
66
+ method_option :project_path, :required => true
67
+ method_option :printer_uri, :required => true
68
+ method_option :number, :type => :numeric, :required => true
69
+ method_option :preload_spec
70
+ method_option :preload_feature
71
+ method_option :task, :required => true
72
+ method_option :quiet, :type => :boolean
73
+ def work
74
+ handle_logging
75
+ Specjour::Worker.new(args).start
76
+ end
77
+
78
+ protected
79
+
80
+ def args
81
+ @args ||= options.dup
82
+ end
83
+
84
+ def handle_logging
85
+ Specjour.new_logger(Logger::DEBUG) if options['log']
86
+ end
87
+
88
+ def handle_workers
89
+ args[:worker_size] = options["workers"] || CPU.cores
90
+ end
91
+
92
+ def handle_dispatcher(path)
93
+ args[:project_path] = path
94
+ args[:project_alias] = args.delete(:alias)
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,73 @@
1
+ module Specjour
2
+ module Configuration
3
+ extend self
4
+
5
+ attr_writer :before_fork, :after_fork, :prepare
6
+
7
+ # This block is run by each worker the manager forks.
8
+ # The Rails plugin uses this block to clear the databases defined in
9
+ # ActiveRecord.
10
+ # Set your own block if the default doesn't work for you.
11
+ def after_fork
12
+ @after_fork ||= default_after_fork
13
+ end
14
+
15
+ # This block is run after before forking. When ActiveRecord is
16
+ # defined, the default before_block disconnects from the database.
17
+ def before_fork
18
+ @before_fork ||= default_before_fork
19
+ end
20
+
21
+ # This block is run on all workers when invoking `specjour prepare`
22
+ # Defaults to dropping the worker's database and recreating it. This
23
+ # is especially useful when two teams are sharing workers and writing
24
+ # migrations at around the same time causing databases to get out of sync.
25
+ def prepare
26
+ @prepare ||= default_prepare
27
+ end
28
+
29
+ def reset
30
+ @before_fork = nil
31
+ @after_fork = nil
32
+ @prepare = nil
33
+ end
34
+
35
+ def bundle_install
36
+ if system('which bundle')
37
+ system('bundle check') || system('bundle install')
38
+ end
39
+ end
40
+
41
+ def default_before_fork
42
+ lambda do
43
+ ActiveRecord::Base.remove_connection if defined?(ActiveRecord::Base)
44
+ bundle_install
45
+ end
46
+ end
47
+
48
+ def default_after_fork
49
+ lambda do
50
+ DbScrub.scrub if rails_with_ar?
51
+ end
52
+ end
53
+
54
+ def default_prepare
55
+ lambda do
56
+ if rails_with_ar?
57
+ DbScrub.drop
58
+ DbScrub.scrub
59
+ end
60
+ end
61
+ end
62
+
63
+ protected
64
+
65
+ def rails_with_ar?
66
+ defined?(Rails) && defined?(ActiveRecord::Base)
67
+ end
68
+
69
+ def system(cmd)
70
+ Kernel.system("#{cmd} > /dev/null")
71
+ end
72
+ end
73
+ end
@@ -36,7 +36,7 @@ module Specjour
36
36
  def timeout(&block)
37
37
  Timeout.timeout(2, &block)
38
38
  rescue Timeout::Error
39
- raise Error, "Connection to dispatcher timed out"
39
+ raise Error, "Connection to dispatcher timed out", []
40
40
  end
41
41
 
42
42
  def next_test
@@ -1,9 +1,6 @@
1
1
  module Specjour::Cucumber
2
+ ::Term::ANSIColor.coloring = true
2
3
  class DistributedFormatter < ::Cucumber::Formatter::Progress
3
- class << self
4
- attr_accessor :batch_size
5
- end
6
- @batch_size = 1
7
4
 
8
5
  def initialize(step_mother, io, options)
9
6
  @step_mother = step_mother
@@ -63,7 +60,7 @@ module Specjour::Cucumber
63
60
  prepare_steps(:failed)
64
61
  prepare_steps(:undefined)
65
62
 
66
- @io.send_message(:worker_summary=, to_hash)
63
+ @io.send_message(:cucumber_summary=, to_hash)
67
64
  end
68
65
 
69
66
  OUTCOMES = [:failed, :skipped, :undefined, :pending, :passed]
@@ -0,0 +1,13 @@
1
+ class Specjour::Cucumber::Preloader
2
+ def self.load(feature_file)
3
+ # preload all features
4
+ cli = ::Cucumber::Cli::Main.new []
5
+ step_mother = cli.class.step_mother
6
+
7
+ step_mother.log = cli.configuration.log
8
+ step_mother.load_code_files(cli.configuration.support_to_load)
9
+ step_mother.after_configuration(cli.configuration)
10
+ features = step_mother.load_plain_text_features(cli.configuration.feature_files)
11
+ step_mother.load_code_files(cli.configuration.step_defs_to_load)
12
+ end
13
+ end
@@ -4,10 +4,11 @@ module Specjour
4
4
  require 'cucumber'
5
5
  require 'cucumber/formatter/progress'
6
6
 
7
- require 'specjour/cucumber/dispatcher'
8
7
  require 'specjour/cucumber/distributed_formatter'
9
8
  require 'specjour/cucumber/final_report'
10
- require 'specjour/cucumber/printer'
9
+ require 'specjour/cucumber/preloader'
10
+
11
+ ::Cucumber::Cli::Options.class_eval { def print_profile_information; end }
11
12
  rescue LoadError
12
13
  end
13
14
  end
@@ -1,8 +1,15 @@
1
1
  module Specjour
2
2
  module DbScrub
3
- load 'Rakefile'
3
+ require 'rake'
4
+ load 'tasks/misc.rake'
5
+ load 'tasks/databases.rake'
6
+
4
7
  extend self
5
8
 
9
+ def drop
10
+ Rake::Task['db:drop']
11
+ end
12
+
6
13
  def scrub
7
14
  connect_to_database
8
15
  if pending_migrations?