drake 0.8.4.1.1.0 → 0.8.4.1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/rake/parallel.rb CHANGED
@@ -1,43 +1,148 @@
1
+ #
2
+ # Parallel task execution for Rake.
3
+ #
4
+ # The comp_tree package is used to create a computation tree which
5
+ # executes Rake tasks.
6
+ #
7
+ # Tasks are first collected with a single-threaded dry run. This
8
+ # expands file rules, resolves prequisite names and finds task
9
+ # arguments. A computation tree is then built with the gathered
10
+ # tasks.
11
+ #
12
+ # Note that prerequisites are context-dependent; it is therefore not
13
+ # possible to create a 1-to-1 mapping between Rake::Tasks and
14
+ # CompTree::Nodes.
15
+ #
16
+ # Author: James M. Lawrence <quixoticsycophant@gmail.com>
17
+ #
1
18
 
2
- require 'rake/comp_tree/comp_tree'
19
+ require 'comp_tree'
3
20
 
4
21
  module Rake
5
- module TaskManager
6
- def invoke_parallel(root_task_name) # :nodoc:
7
- CompTree.build do |driver|
8
- #
9
- # Build the computation tree from task prereqs.
10
- #
11
- self.parallel.tasks.each_pair { |task_name, cache|
12
- task = self[task_name]
13
- task_args, prereqs = cache
14
- children_names = prereqs.map { |child| child.name }
15
- driver.define(task_name, *children_names) {
16
- task.execute(task_args)
17
- }
18
- }
22
+ module Parallel #:nodoc:
23
+ class Driver
24
+ # Tasks collected during the dry-run phase.
25
+ attr_reader :tasks
26
+
27
+ # Prevent invoke inside invoke.
28
+ attr_reader :mutex
29
+
30
+ def initialize
31
+ @tasks = Hash.new
32
+ @mutex = Mutex.new
33
+ end
19
34
 
20
- root_node = driver.nodes[root_task_name]
21
-
22
- #
23
- # If there were nothing to do, there would be no root node.
24
- #
25
- if root_node
26
- #
27
- # Mark computation nodes without a function as computed.
28
- #
29
- root_node.each_downward { |node|
30
- unless node.function
31
- node.computed = true
35
+ #
36
+ # Top-level parallel invocation.
37
+ #
38
+ # Called from Task#invoke (routed through Task#invoke_parallel).
39
+ #
40
+ def invoke(threads, task, *task_args)
41
+ if @mutex.try_lock
42
+ begin
43
+ @tasks.clear
44
+
45
+ # dry run task collector
46
+ task.invoke_serial(*task_args)
47
+
48
+ if @tasks.has_key? task
49
+ # hand it off to comp_tree
50
+ compute(task, threads)
32
51
  end
33
- }
52
+ ensure
53
+ @mutex.unlock
54
+ end
55
+ else
56
+ raise InvokeInsideInvoke
57
+ end
58
+ end
59
+
60
+ #
61
+ # Build and run the computation tree.
62
+ #
63
+ # Called from Parallel::Driver#invoke.
64
+ #
65
+ def compute(root_task, threads)
66
+ CompTree.build do |driver|
67
+ # keep this around for optimization
68
+ needed_prereq_names = Array.new
69
+
70
+ @tasks.each_pair do |task, (task_args, prereqs)|
71
+ # if a prereq is not needed then it didn't get into @tasks
72
+ needed_prereq_names.clear
73
+ prereqs.each do |prereq|
74
+ needed_prereq_names << prereq.name if @tasks.has_key? prereq
75
+ end
76
+
77
+ # define a computation node which executes the task
78
+ driver.define(task.name, *needed_prereq_names) {
79
+ task.execute(task_args)
80
+ }
81
+ end
34
82
 
35
- #
36
- # Launch the computation.
37
- #
38
- driver.compute(root_node.name, self.num_threads)
83
+ # punch it
84
+ driver.compute(root_task.name, threads)
39
85
  end
40
86
  end
41
87
  end
88
+
89
+ module ApplicationMixin
90
+ def parallel
91
+ @parallel ||= Driver.new
92
+ end
93
+ end
94
+
95
+ module TaskMixin
96
+ #
97
+ # Top-level parallel invocation.
98
+ #
99
+ # Called from Task#invoke.
100
+ #
101
+ def invoke_parallel(*task_args)
102
+ application.parallel.invoke(application.options.threads, self, *task_args)
103
+ end
104
+
105
+ #
106
+ # Collect tasks for parallel execution.
107
+ #
108
+ # Called from Task#invoke_with_call_chain.
109
+ #
110
+ def invoke_with_call_chain_collector(task_args, new_chain, previous_chain)
111
+ prereqs = invoke_prerequisites_collector(task_args, new_chain)
112
+ parallel = application.parallel
113
+ if needed? or parallel.tasks[self]
114
+ parallel.tasks[self] = [task_args, prereqs]
115
+ unless previous_chain == InvocationChain::EMPTY
116
+ #
117
+ # Touch the parent to propagate 'needed?' upwards. This
118
+ # works because the recursion is depth-first.
119
+ #
120
+ parallel.tasks[previous_chain.value] = true
121
+ end
122
+ end
123
+ end
124
+
125
+ #
126
+ # Dry-run invoke prereqs and return the prereq instances.
127
+ # This also serves to avoid MultiTask#invoke_prerequisites.
128
+ #
129
+ # Called from Task#invoke_with_call_chain_collector.
130
+ #
131
+ def invoke_prerequisites_collector(task_args, invocation_chain)
132
+ @prerequisites.map { |n|
133
+ invoke_prerequisite(n, task_args, invocation_chain)
134
+ }
135
+ end
136
+ end
137
+ end
138
+
139
+ #
140
+ # Error indicating Task#invoke was called inside Task#invoke
141
+ # during parallel execution.
142
+ #
143
+ class InvokeInsideInvoke < StandardError
144
+ def message
145
+ "Cannot call Task#invoke within a task during parallel execution."
146
+ end
42
147
  end
43
148
  end
data/test/filecreation.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  module FileCreation
4
+ ANCIENT_FILE = 'testdata/anc'
4
5
  OLDFILE = "testdata/old"
6
+ MIDDLE_AGED_FILE = "testdata/mid"
5
7
  NEWFILE = "testdata/new"
6
8
 
7
9
  def create_timed_files(oldfile, *newfiles)
@@ -15,6 +17,16 @@ module FileCreation
15
17
  end
16
18
  end
17
19
 
20
+ def create_dispersed_timed_files(*files)
21
+ create_file(files.first)
22
+ (1...files.size).each do |index|
23
+ while create_file(files[index]) <= File.stat(files[index - 1]).mtime
24
+ sleep(0.1)
25
+ File.delete(files[index])
26
+ end
27
+ end
28
+ end
29
+
18
30
  def create_dir(dirname)
19
31
  FileUtils.mkdir_p(dirname) unless File.exist?(dirname)
20
32
  File.stat(dirname).mtime
@@ -0,0 +1,3 @@
1
+ require 'rake'
2
+ Rake.application.options.threads = 8
3
+ puts "- Testing parallel execution"
@@ -7,6 +7,7 @@ rescue LoadError
7
7
  # got no gems
8
8
  end
9
9
 
10
+ require 'thread'
10
11
  require 'flexmock/test_unit'
11
12
 
12
13
  if RUBY_VERSION >= "1.9.0"
@@ -22,3 +23,23 @@ module TestMethods
22
23
  assert_raise(ex, msg, &block)
23
24
  end
24
25
  end
26
+
27
+ class SerializedArray
28
+ def initialize
29
+ @mutex = Mutex.new
30
+ @array = Array.new
31
+ end
32
+
33
+ Array.public_instance_methods.each do |method_name|
34
+ unless method_name =~ %r!\A__! or method_name =~ %r!\A(object_)?id\Z!
35
+ # TODO: jettison 1.8.6; use define_method with |&block|
36
+ eval %{
37
+ def #{method_name}(*args, &block)
38
+ @mutex.synchronize {
39
+ @array.send('#{method_name}', *args, &block)
40
+ }
41
+ end
42
+ }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,3 @@
1
+ require 'rake'
2
+ Rake.application.options.threads = 1
3
+ puts "- Testing serial execution"
@@ -22,6 +22,7 @@ class TestApplication < Test::Unit::TestCase
22
22
 
23
23
  def setup
24
24
  @app = Rake::Application.new
25
+ @app.options.threads = Rake.application.options.threads
25
26
  @app.options.rakelib = []
26
27
  end
27
28
 
@@ -570,6 +571,7 @@ class TestApplicationOptions < Test::Unit::TestCase
570
571
  def command_line(*options)
571
572
  options.each do |opt| ARGV << opt end
572
573
  @app = Rake::Application.new
574
+ @app.options.threads = Rake.application.options.threads
573
575
  def @app.exit(*args)
574
576
  throw :system_exit, :exit
575
577
  end
@@ -585,6 +587,7 @@ end
585
587
  class TestTaskArgumentParsing < Test::Unit::TestCase
586
588
  def setup
587
589
  @app = Rake::Application.new
590
+ @app.options.threads = Rake.application.options.threads
588
591
  end
589
592
 
590
593
  def test_name_only
@@ -630,6 +633,7 @@ class TestTaskArgumentParsing < Test::Unit::TestCase
630
633
 
631
634
  def test_terminal_width_using_env
632
635
  app = Rake::Application.new
636
+ app.options.threads = Rake.application.options.threads
633
637
  in_environment('RAKE_COLUMNS' => '1234') do
634
638
  assert_equal 1234, app.terminal_width
635
639
  end
@@ -637,6 +641,7 @@ class TestTaskArgumentParsing < Test::Unit::TestCase
637
641
 
638
642
  def test_terminal_width_using_stty
639
643
  app = Rake::Application.new
644
+ app.options.threads = Rake.application.options.threads
640
645
  flexmock(app,
641
646
  :unix? => true,
642
647
  :dynamic_width_stty => 1235,
@@ -648,6 +653,7 @@ class TestTaskArgumentParsing < Test::Unit::TestCase
648
653
 
649
654
  def test_terminal_width_using_tput
650
655
  app = Rake::Application.new
656
+ app.options.threads = Rake.application.options.threads
651
657
  flexmock(app,
652
658
  :unix? => true,
653
659
  :dynamic_width_stty => 0,
@@ -659,6 +665,7 @@ class TestTaskArgumentParsing < Test::Unit::TestCase
659
665
 
660
666
  def test_terminal_width_using_hardcoded_80
661
667
  app = Rake::Application.new
668
+ app.options.threads = Rake.application.options.threads
662
669
  flexmock(app, :unix? => false)
663
670
  in_environment('RAKE_COLUMNS' => nil) do
664
671
  assert_equal 80, app.terminal_width
@@ -667,6 +674,7 @@ class TestTaskArgumentParsing < Test::Unit::TestCase
667
674
 
668
675
  def test_terminal_width_with_failure
669
676
  app = Rake::Application.new
677
+ app.options.threads = Rake.application.options.threads
670
678
  flexmock(app).should_receive(:unix?).and_throw(RuntimeError)
671
679
  in_environment('RAKE_COLUMNS' => nil) do
672
680
  assert_equal 80, app.terminal_width
@@ -48,14 +48,18 @@ class TestDefinitions < Test::Unit::TestCase
48
48
  end
49
49
 
50
50
  def test_incremental_definitions
51
- runs = []
51
+ runs = SerializedArray.new
52
52
  task :t1 => [:t2] do runs << "A"; 4321 end
53
53
  task :t1 => [:t3] do runs << "B"; 1234 end
54
54
  task :t1 => [:t3]
55
55
  task :t2
56
56
  task :t3
57
57
  Task[:t1].invoke
58
- assert_equal ["A", "B"], runs
58
+ if Rake.application.options.threads == 1
59
+ assert_equal ["A", "B"], runs
60
+ else
61
+ assert_equal ["A", "B"], runs.sort
62
+ end
59
63
  assert_equal ["t2", "t3"], Task[:t1].prerequisites
60
64
  end
61
65
 
@@ -12,11 +12,17 @@ class TestFileTask < Test::Unit::TestCase
12
12
  include FileCreation
13
13
  include TestMethods
14
14
 
15
+ FILES = [ANCIENT_FILE, OLDFILE, MIDDLE_AGED_FILE, NEWFILE]
16
+
15
17
  def setup
16
18
  Task.clear
17
- @runs = Array.new
18
- FileUtils.rm_f NEWFILE
19
- FileUtils.rm_f OLDFILE
19
+ @runs = SerializedArray.new
20
+ FileUtils.rm_f FILES
21
+ end
22
+
23
+ def test_create_dispersed_timed_files
24
+ create_dispersed_timed_files(*FILES)
25
+ assert_equal FILES, FILES.sort_by { |f| File.stat(f).mtime }
20
26
  end
21
27
 
22
28
  def test_file_need
@@ -73,6 +79,46 @@ class TestFileTask < Test::Unit::TestCase
73
79
  assert_nothing_raised do Task[OLDFILE].invoke end
74
80
  end
75
81
 
82
+ def test_old_file_in_between
83
+ create_dispersed_timed_files(*FILES)
84
+
85
+ file MIDDLE_AGED_FILE => OLDFILE do |t|
86
+ @runs << t.name
87
+ end
88
+ file OLDFILE => NEWFILE do |t|
89
+ @runs << t.name
90
+ touch OLDFILE, :verbose => false
91
+ end
92
+ file NEWFILE do |t|
93
+ @runs << t.name
94
+ end
95
+
96
+ Task[MIDDLE_AGED_FILE].invoke
97
+ assert_equal([OLDFILE, MIDDLE_AGED_FILE], @runs)
98
+ end
99
+
100
+ def test_two_old_files_in_between
101
+ create_dispersed_timed_files(*FILES)
102
+
103
+ file MIDDLE_AGED_FILE => OLDFILE do |t|
104
+ @runs << t.name
105
+ end
106
+ file OLDFILE => ANCIENT_FILE do |t|
107
+ @runs << t.name
108
+ touch OLDFILE, :verbose => false
109
+ end
110
+ file ANCIENT_FILE => NEWFILE do |t|
111
+ @runs << t.name
112
+ touch ANCIENT_FILE, :verbose => false
113
+ end
114
+ file NEWFILE do |t|
115
+ @runs << t.name
116
+ end
117
+
118
+ Task[MIDDLE_AGED_FILE].invoke
119
+ assert_equal([ANCIENT_FILE, OLDFILE, MIDDLE_AGED_FILE], @runs)
120
+ end
121
+
76
122
  # I have currently disabled this test. I'm not convinced that
77
123
  # deleting the file target on failure is always the proper thing to
78
124
  # do. I'm willing to hear input on this topic.
@@ -9,7 +9,7 @@ class TestMultiTask < Test::Unit::TestCase
9
9
 
10
10
  def setup
11
11
  Task.clear
12
- @runs = Array.new
12
+ @runs = SerializedArray.new
13
13
  end
14
14
 
15
15
  def test_running_multitasks
@@ -1,56 +1,195 @@
1
1
 
2
2
  require 'rbconfig'
3
3
  require 'test/unit'
4
+ require 'rake'
5
+ require 'test/rake_test_setup'
4
6
 
5
- PARALLEL_TEST_MESSAGE = <<'EOS'
7
+ if Rake.application.options.threads > 1
8
+ class TestParallel < Test::Unit::TestCase
9
+ VISUALS = false
10
+ TIME_STEP = 0.25
11
+ TIME_EPSILON = 0.05
12
+ MAX_THREADS = 5
6
13
 
14
+ def trace(str)
15
+ if VISUALS
16
+ puts str
17
+ end
18
+ end
7
19
 
8
- Task graph for sample parallel execution:
9
-
10
- default
11
- / \
12
- / \
13
- a b
14
- / \
15
- / \
16
- x y
20
+ def assert_order(expected, actual)
21
+ assert_in_delta(expected*TIME_STEP, actual, TIME_EPSILON)
22
+ end
17
23
 
18
- EOS
19
-
20
- if Rake.application.num_threads > 1
21
- class TestSimpleParallel < Test::Unit::TestCase
22
- def setup
23
- puts PARALLEL_TEST_MESSAGE
24
+ def teardown
25
+ Rake::Task.clear
24
26
  end
25
27
 
26
28
  def test_parallel
27
- here = File.dirname(__FILE__)
28
- rake = File.expand_path("#{here}/../bin/rake")
29
-
30
- ENV["RUBYLIB"] = lambda {
31
- lib = File.expand_path("#{here}/../lib")
32
- current = ENV["RUBYLIB"]
33
- sep = Rake.application.windows? ? ";" : ":"
34
- if current
35
- "#{lib}#{sep}#{current}"
29
+ trace GRAPH
30
+
31
+ data = Hash.new { |hash, key| hash[key] = Hash.new }
32
+
33
+ (1..MAX_THREADS).each { |threads|
34
+ app = Rake::Application.new
35
+ app.options.threads = threads
36
+
37
+ app.define_task Rake::Task, :default => [:a, :b]
38
+ app.define_task Rake::Task, :a => [:x, :y]
39
+ app.define_task Rake::Task, :b
40
+
41
+ mutex = Mutex.new
42
+ STDOUT.sync = true
43
+ start_time = nil
44
+
45
+ %w[default a b x y].each { |task_name|
46
+ app.define_task Rake::Task, task_name.to_sym do
47
+ mutex.synchronize {
48
+ trace "task #{task_name}"
49
+ data[threads][task_name] = Time.now - start_time
50
+ }
51
+ sleep(TIME_STEP)
52
+ end
53
+ }
54
+
55
+ trace "-"*50
56
+ trace "threads: #{threads}"
57
+ start_time = Time.now
58
+ app[:default].invoke
59
+ }
60
+
61
+ assert_order(0, data[1]["x"])
62
+ assert_order(1, data[1]["y"])
63
+ assert_order(2, data[1]["a"])
64
+ assert_order(3, data[1]["b"])
65
+ assert_order(4, data[1]["default"])
66
+
67
+ assert_order(0, data[2]["x"])
68
+ assert_order(0, data[2]["y"])
69
+ assert_order(1, data[2]["a"])
70
+ assert_order(1, data[2]["b"])
71
+ assert_order(2, data[2]["default"])
72
+
73
+ (3..MAX_THREADS).each { |threads|
74
+ assert_order(0, data[threads]["x"])
75
+ assert_order(0, data[threads]["y"])
76
+ assert_order(0, data[threads]["b"])
77
+ assert_order(1, data[threads]["a"])
78
+ assert_order(2, data[threads]["default"])
79
+ }
80
+ end
81
+
82
+ def test_invoke_inside_invoke
83
+ assert_raises(Rake::InvokeInsideInvoke) {
84
+ app = Rake::Application.new
85
+ app.options.threads = 4
86
+ app.define_task Rake::Task, :root do
87
+ app[:stuff].invoke
88
+ end
89
+ app.define_task Rake::Task, :stuff do
90
+ flunk
91
+ end
92
+ app[:root].invoke
93
+ }
94
+ end
95
+
96
+ def test_randomize
97
+ size = 100
98
+ [false, true].each do |randomize|
99
+ memo = SerializedArray.new
100
+ app = Rake::Application.new
101
+ app.define_task Rake::Task, :root
102
+ size.times { |n|
103
+ app.define_task Rake::Task, :root => n.to_s
104
+ app.define_task Rake::Task, n.to_s do
105
+ memo << n
106
+ end
107
+ }
108
+ app.options.randomize = randomize
109
+ app[:root].invoke
110
+ numbers = (0...size).to_a
111
+ if randomize
112
+ assert_not_equal(numbers, memo)
36
113
  else
37
- lib
114
+ assert_equal(numbers, memo)
38
115
  end
39
- }.call
40
-
41
- [
42
- ["Rakefile.simple", true],
43
- ["Rakefile.seq", false],
44
- ].each { |file, disp|
45
- (1..5).each { |n|
46
- args = [rake, "--threads", n.to_s, "-s", "-f", "test/#{file}"]
47
- if disp
48
- puts "\nvisual check: #{n} thread#{n > 1 ? 's' : ''}"
49
- puts args.join(" ")
116
+ end
117
+ end
118
+
119
+ def test_multitask_not_called
120
+ # ensure MultiTask methods are not called by hijacking all of them
121
+
122
+ originals = [
123
+ :private_instance_methods,
124
+ :protected_instance_methods,
125
+ :public_instance_methods,
126
+ ].inject(Hash.new) { |acc, query|
127
+ result = Rake::MultiTask.send(query, false).inject(Hash.new) {
128
+ |sub_acc, method_name|
129
+ sub_acc.merge!(
130
+ method_name => Rake::MultiTask.instance_method(method_name)
131
+ )
132
+ }
133
+ acc.merge!(result)
134
+ }
135
+
136
+ memo = SerializedArray.new
137
+
138
+ Rake::MultiTask.module_eval {
139
+ originals.each_pair { |method_name, method_object|
140
+ remove_method method_name
141
+ define_method method_name do |*args|
142
+ # missing |&block| due to 1.8.6, but not using it anyway
143
+ memo << 'called'
144
+ method_object.bind(self).call(*args)
50
145
  end
51
- assert(ruby(*args))
52
146
  }
53
147
  }
148
+
149
+ begin
150
+ app = Rake::Application.new
151
+
152
+ define = lambda {
153
+ app.define_task Rake::Task, task(:x) { }
154
+ app.define_task Rake::Task, task(:y) { }
155
+ app.define_task Rake::MultiTask, :root => [:x, :y]
156
+ }
157
+
158
+ app.options.threads = 1
159
+ define.call
160
+ app[:root].invoke
161
+ assert_equal ['called'], memo
162
+
163
+ app.clear
164
+ memo.clear
165
+ assert_raises(RuntimeError) { app[:root].invoke }
166
+
167
+ app.options.threads = 4
168
+ define.call
169
+ app[:root].invoke
170
+ assert_equal [], memo
171
+ ensure
172
+ Rake::MultiTask.module_eval {
173
+ originals.each_pair { |method_name, method_object|
174
+ remove_method method_name
175
+ define_method method_name, method_object
176
+ }
177
+ }
178
+ end
54
179
  end
180
+
181
+ GRAPH = <<-'EOS'
182
+
183
+ Task graph for sample parallel execution:
184
+
185
+ default
186
+ / \
187
+ / \
188
+ a b
189
+ / \
190
+ / \
191
+ x y
192
+
193
+ EOS
55
194
  end
56
195
  end