costan-tem_mr_search 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,167 @@
1
+ # :nodoc: namespace
2
+ module Tem::Mr::Search
3
+
4
+ class QueryBuilder
5
+ # Build a Query.
6
+ def self.query
7
+ builder = self.new
8
+ yield builder
9
+ builder.query
10
+ end
11
+
12
+ # Defines the object attributes imported into the map method.
13
+ def attributes(attributes)
14
+ @attributes = attributes.to_a.map do |k, v|
15
+ { :name => k,
16
+ :type => v,
17
+ :length => Tem::Abi.send(:"#{v}_length")
18
+ }
19
+ end
20
+ end
21
+
22
+ # Defines the object attribute that's used as an object ID.
23
+ def id_attribute(id_attribute)
24
+ @id_attribute = id_attribute.to_sym
25
+ end
26
+
27
+ # Defines the query's map procedure.
28
+ def map
29
+ @map_secpack = Tem::Assembler.assemble do |s|
30
+ s.label :_secret
31
+ s.label :_key
32
+ s.zeros :tem_ubyte, 16
33
+ s.label :_check_bytes
34
+ s.data :tem_ubyte, @check_bytes
35
+
36
+ # User-provided ranking procedure (secret).
37
+ s.label :_ranking
38
+ yield s
39
+ s.ret
40
+
41
+ s.entry
42
+ s.ldbc 16
43
+ s.outnew
44
+ s.call :_ranking
45
+ s.ldbc 3
46
+ s.ldwc :_nonce
47
+ s.rnd
48
+ s.mcfxb :from => :_check_bytes, :to => :_check, :size => 3
49
+ # TODO(costan): encryption instead of plain dump
50
+ s.outfxb :from => :_id, :size => 16
51
+ s.halt
52
+
53
+ s.label :_plain
54
+
55
+ # Make room for query attributes.
56
+ @attributes.each do |attribute|
57
+ s.label attribute[:name]
58
+ s.zeros attribute[:type], 1
59
+ end
60
+ # Object ID.
61
+ s.label :_id
62
+ s.zeros :tem_ubyte, 8
63
+ # Object score.
64
+ s.label :score
65
+ s.zeros :tem_short, 1
66
+ # Random nonce to prevent matching map outputs.
67
+ s.label :_nonce
68
+ s.zeros :tem_ubyte, 3
69
+ # Check bytes to prevent malicious input corruption.
70
+ s.label :_check
71
+ s.zeros :tem_ubyte, 3
72
+
73
+ s.stack 64
74
+ end
75
+ end
76
+
77
+ # Defines the query's reduce procedure.
78
+ def reduce
79
+ @reduce_secpack = Tem::Assembler.assemble do |s|
80
+ s.label :_secret
81
+ s.label :_key
82
+ s.zeros :tem_ubyte, 16
83
+ s.label :_check
84
+ s.data :tem_ubyte, @check_bytes
85
+
86
+ s.label :_signed
87
+ # User-provided comparison procedure (signed).
88
+ s.label :_comparison_proc
89
+ yield s
90
+ s.ret
91
+
92
+ s.entry
93
+ s.ldbc 16
94
+ s.outnew
95
+ # Decode inputs.
96
+ [1, 2].each do |i|
97
+ # TODO(costan): decrypt instead of copying
98
+ s.mcfxb :from => :"_output#{i}", :to => :"_id#{i}", :size => 16
99
+
100
+ # Compare the check bytes and abort if the inputs were tampered with.
101
+ s.mcmpfxb :op1 => :"_check#{i}", :op2 => :"_check", :size => 3
102
+ s.jz :"_check_#{i}_ok"
103
+ s.halt
104
+ s.label :"_check_#{i}_ok"
105
+ end
106
+
107
+ # Compare and output.
108
+ s.call :_comparison_proc
109
+ s.ldw :comparison
110
+ s.jae :_output1_wins
111
+ s.mcfxb :from => :_id2, :to => :_id1, :size => 16
112
+ s.jmp :_output
113
+ s.label :_output1_wins
114
+ # Still do a memcpy, to prevent timing attacks.
115
+ s.mcfxb :from => :_id2, :to => :_id2, :size => 16
116
+ s.jmp :_output
117
+ # Refresh the nonce to prevent learning about the comparison criteria.
118
+ s.label :_output
119
+ s.ldbc 3
120
+ s.ldwc :_nonce1
121
+ s.rnd
122
+ # TODO(costan): encrypt instead of copying
123
+ s.outfxb :from => :_id1, :size => 16
124
+ s.halt
125
+
126
+ s.label :_plain
127
+ # The comparison result produced by the user comparison procedure.
128
+ s.label :comparison
129
+ s.zeros :tem_short, 1
130
+
131
+ # The two inputs to reduce.
132
+ [1, 2].each do |i|
133
+ # Encrypted map/reduce output.
134
+ s.label :"_output#{i}"
135
+ s.zeros :tem_ubyte, 16
136
+ # Unencrypted input (decrypted inside TEM).
137
+ s.label :"_id#{i}"
138
+ s.zeros :tem_ubyte, 8
139
+ s.label :"score#{i}"
140
+ s.zeros :tem_short, 1
141
+ s.label :"_nonce#{i}"
142
+ s.zeros :tem_ubyte, 3
143
+ s.label :"_check#{i}"
144
+ s.zeros :tem_ubyte, 3
145
+ end
146
+ s.stack 8
147
+ end
148
+ end
149
+
150
+ def query
151
+ raise "Map procedure not specified" unless @map_secpack
152
+ raise "Reduce procedure not specified" unless @reduce_secpack
153
+ raise "ID attribute not specified" unless @id_attribute
154
+
155
+ ClientQuery.new :key => @query_key, :attributes => @attributes,
156
+ :map => @map_secpack, :reduce => @reduce_secpack,
157
+ :id_attribute => @id_attribute
158
+ end
159
+
160
+ def initialize
161
+ @check_bytes = [1, 2, 3]
162
+ # TODO(costan): generate query key
163
+ @query_key = nil
164
+ end
165
+ end # class QueryBuilder
166
+
167
+ end # namespace Tem::Mr::Search
@@ -0,0 +1,90 @@
1
+ require 'logger'
2
+ require 'yaml'
3
+
4
+ # :nodoc: namespace
5
+ module Tem::Mr::Search
6
+
7
+ class Server
8
+ DEFAULT_PORT = 9052
9
+
10
+ OP = Zerg::Support::Protocols::ObjectProtocol
11
+ OPAdapter = Zerg::Support::Sockets::ProtocolAdapter.adapter_module OP
12
+
13
+ # Creates a new Map-Reduce server (master).
14
+ def initialize(db_file, cluster_file, port)
15
+ @logger = Logger.new STDERR
16
+ @db = Db.new db_file
17
+ @cluster_file = cluster_file
18
+ @tems = []
19
+ refresh_tems!
20
+ @port = port || DEFAULT_PORT
21
+ end
22
+
23
+ # Reinitializes the TEM cluster connections.
24
+ #
25
+ # This should be called reasonably often to be able to respond to cluster
26
+ # configuration changes.
27
+ def refresh_tems!
28
+ @tems.each { |tem| tem.disconnect }
29
+ @tems = Server.tems_from_cluster_file @cluster_file
30
+ end
31
+
32
+ # This server's loop.
33
+ def serve_loop
34
+ listen_socket = Zerg::Support::SocketFactory.socket :in_port => @port
35
+ listen_socket.listen
36
+ shutdown_received = false
37
+ until shutdown_received
38
+ begin
39
+ client_socket, client_addr = listen_socket.accept
40
+ client_socket.extend OPAdapter
41
+ request = client_socket.recv_object
42
+ begin
43
+ response = process_request request
44
+ rescue Exception => e
45
+ @logger.error e
46
+ response = nil
47
+ end
48
+ client_socket.send_object response if response
49
+ shutdown_received = true if response == :shutdown
50
+ rescue Exception => e
51
+ @logger.error e
52
+ end
53
+ client_socket.close
54
+ end
55
+ listen_socket.close
56
+ end
57
+
58
+ # Computes the response of a single request.
59
+ def process_request(request)
60
+ case request[:type]
61
+ when :search
62
+ refresh_tems!
63
+ job = MapReduceJob.new request[:map_reduce]
64
+ root_tem = request[:root_tem]
65
+ executor = MapReduceExecutor.new job, @db, @tems, root_tem
66
+ return executor.execute
67
+ when :fetch
68
+ return @db.item_by_id(request[:id]) || :not_found
69
+ when :shutdown
70
+ return :shutdown
71
+ when :db_dump
72
+ return (0...@db.length).map { |i| @db.item(i) }
73
+ else
74
+ return :unknown
75
+ end
76
+ end
77
+
78
+ # Creates sessions to all the TEMs in a cluster.
79
+ def self.tems_from_cluster_file(cluster_file)
80
+ cluster_hosts = File.open(cluster_file, 'r') { |f| YAML.load f }
81
+ cluster_configs = cluster_hosts.map { |host|
82
+ Tem::MultiProxy::Client.query_tems host
83
+ }.flatten
84
+ cluster_configs.reject { |config| config.nil? }.map do |config|
85
+ Tem::Session.new Tem::Transport::AutoConfigurator.try_transport(config)
86
+ end
87
+ end
88
+ end
89
+
90
+ end # namespace Tem::Mr::Search
@@ -0,0 +1,50 @@
1
+ # :nodoc: namespace
2
+ module Tem::Mr::Search
3
+
4
+ class WebClientQueryBuilder < MapReduceJob
5
+ # Builds a client query covering preferences expressed in the Web UI.
6
+ #
7
+ # The supported (and required) options are:
8
+ # layovers_cost:: the cost of each layover
9
+ # start_time_cost:: the cost of the flight's departure time, in minutes
10
+ # duration_cost:: the cost of each minute of flying
11
+ def self.query(options)
12
+ QueryBuilder.query { |q|
13
+ q.attributes :price => :tem_short, :start_time => :tem_short,
14
+ :end_time => :tem_short, :layovers => :tem_short
15
+ q.id_attribute :flight
16
+
17
+ # Score: 20000 - price - layover_cost * layovers -
18
+ # start_time * start_time_cost -
19
+ # (end_time - start_time) * duration_cost
20
+ q.map { |s|
21
+ s.ldwc 20000
22
+ s.ldw :price
23
+ s.sub
24
+ s.ldw :end_time
25
+ s.ldw :start_time
26
+ s.sub
27
+ s.ldwc options[:duration_cost]
28
+ s.mul
29
+ s.sub
30
+ [:start_time, :layovers].each do |factor|
31
+ s.ldw factor
32
+ s.ldwc options[:"#{factor}_cost"]
33
+ s.mul
34
+ s.sub
35
+ end
36
+ s.stw :score
37
+ }
38
+
39
+ # The greater score wins.
40
+ q.reduce { |s|
41
+ s.ldw :score1
42
+ s.ldw :score2
43
+ s.cmp
44
+ s.stw :comparison
45
+ }
46
+ }
47
+ end
48
+ end
49
+
50
+ end # namespace Tem::Mr::search
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'tem_multi_proxy'
3
+ require 'tem_ruby'
4
+
5
+ # :nodoc: namespace
6
+ module Tem::Mr
7
+ end
8
+ # :nodoc: namespace
9
+ module Tem::Mr::Search
10
+ end
11
+
12
+ require 'tem_mr_search/client.rb'
13
+ require 'tem_mr_search/db.rb'
14
+ require 'tem_mr_search/map_reduce_executor.rb'
15
+ require 'tem_mr_search/map_reduce_job.rb'
16
+ require 'tem_mr_search/map_reduce_planner.rb'
17
+ require 'tem_mr_search/query_builder.rb'
18
+ require 'tem_mr_search/client_query.rb'
19
+ require 'tem_mr_search/server.rb'
20
+ require 'tem_mr_search/web_client_query_builder.rb'
@@ -0,0 +1,39 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{tem_mr_search}
5
+ s.version = "0.2.1"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Victor Costan"]
9
+ s.date = %q{2009-06-04}
10
+ s.default_executable = %q{tem_mr_search_server}
11
+ s.description = %q{Tem Map-Reduce proof of concept: database search.}
12
+ s.email = %q{victor@costan.us}
13
+ s.executables = ["tem_mr_search_server"]
14
+ s.extra_rdoc_files = ["bin/tem_mr_search_server", "CHANGELOG", "lib/tem_mr_search/client.rb", "lib/tem_mr_search/client_query.rb", "lib/tem_mr_search/db.rb", "lib/tem_mr_search/map_reduce_executor.rb", "lib/tem_mr_search/map_reduce_job.rb", "lib/tem_mr_search/map_reduce_planner.rb", "lib/tem_mr_search/query_builder.rb", "lib/tem_mr_search/server.rb", "lib/tem_mr_search/web_client_query_builder.rb", "lib/tem_mr_search.rb", "LICENSE", "README"]
15
+ s.files = ["bin/tem_mr_search_server", "CHANGELOG", "lib/tem_mr_search/client.rb", "lib/tem_mr_search/client_query.rb", "lib/tem_mr_search/db.rb", "lib/tem_mr_search/map_reduce_executor.rb", "lib/tem_mr_search/map_reduce_job.rb", "lib/tem_mr_search/map_reduce_planner.rb", "lib/tem_mr_search/query_builder.rb", "lib/tem_mr_search/server.rb", "lib/tem_mr_search/web_client_query_builder.rb", "lib/tem_mr_search.rb", "LICENSE", "Manifest", "Rakefile", "README", "tem_mr_search.gemspec", "test/mr_test_case.rb", "test/test_client_server.rb", "test/test_db.rb", "test/test_map_reduce_executor.rb", "test/test_map_reduce_job.rb", "test/test_map_reduce_planner.rb", "test/test_query_builders.rb", "testdata/cluster.yml", "testdata/empty_cluster.yml", "testdata/fares.yml", "testdata/parallel_plan_431.yml", "testdata/parallel_plan_740.yml", "testdata/serial_plan_410.yml", "testdata/serial_plan_431.yml", "testdata/serial_plan_740.yml"]
16
+ s.homepage = %q{http://tem.rubyforge.org}
17
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Tem_mr_search", "--main", "README"]
18
+ s.require_paths = ["lib"]
19
+ s.rubyforge_project = %q{tem}
20
+ s.rubygems_version = %q{1.3.4}
21
+ s.summary = %q{Tem Map-Reduce proof of concept: database search.}
22
+ s.test_files = ["test/test_client_server.rb", "test/test_db.rb", "test/test_map_reduce_executor.rb", "test/test_map_reduce_job.rb", "test/test_map_reduce_planner.rb", "test/test_query_builders.rb"]
23
+
24
+ if s.respond_to? :specification_version then
25
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
26
+ s.specification_version = 3
27
+
28
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
29
+ s.add_runtime_dependency(%q<tem_ruby>, [">= 0.11.2"])
30
+ s.add_runtime_dependency(%q<tem_multi_proxy>, [">= 0.2"])
31
+ else
32
+ s.add_dependency(%q<tem_ruby>, [">= 0.11.2"])
33
+ s.add_dependency(%q<tem_multi_proxy>, [">= 0.2"])
34
+ end
35
+ else
36
+ s.add_dependency(%q<tem_ruby>, [">= 0.11.2"])
37
+ s.add_dependency(%q<tem_multi_proxy>, [">= 0.2"])
38
+ end
39
+ end
@@ -0,0 +1,36 @@
1
+ require 'test/unit'
2
+ require 'tem_mr_search'
3
+
4
+ class MrTestCase < Test::Unit::TestCase
5
+ include Tem::Mr::Search
6
+
7
+ def setup
8
+ super
9
+
10
+ Thread.abort_on_exception = true
11
+
12
+ @db_path = File.join File.dirname(__FILE__), "..", "testdata", "fares.yml"
13
+ @cluster_file = File.join File.dirname(__FILE__), "..", "testdata",
14
+ "cluster.yml"
15
+ @empty_cluster_file = File.join File.dirname(__FILE__), "..", "testdata",
16
+ "empty_cluster.yml"
17
+ @db = Db.new @db_path
18
+
19
+ @client_query = WebClientQueryBuilder.query :layovers_cost => 1000,
20
+ :start_time_cost => -1,
21
+ :duration_cost => 1
22
+ end
23
+
24
+ def fare_score(fare)
25
+ 20000 + fare['start_time'] - fare['price'] - (fare['end_time'] -
26
+ fare['start_time']) - fare['layovers'] * 1000
27
+ end
28
+
29
+ def fare_id(fare)
30
+ fare['flight']
31
+ end
32
+
33
+ # Ensures that everything has loaded.
34
+ def test_smoke
35
+ end
36
+ end
@@ -0,0 +1,57 @@
1
+ require 'test/mr_test_case'
2
+ require 'flexmock/test_unit'
3
+
4
+
5
+ class ClientServerTest < MrTestCase
6
+ Client = Tem::Mr::Search::Client
7
+ Server = Tem::Mr::Search::Server
8
+
9
+ def setup
10
+ super
11
+ @server_port = 29550
12
+ end
13
+
14
+ def _test_request
15
+ Thread.new do
16
+ Server.new(@db_path, @empty_cluster_file, @server_port).serve_loop
17
+ end
18
+ sleep 0.1
19
+ yield "localhost:#{@server_port}"
20
+ Client.shutdown_server "localhost:#{@server_port}"
21
+ end
22
+
23
+ def test_fetch_item
24
+ _test_request do |server_addr|
25
+ fetched_item = Client.fetch_item server_addr, fare_id(@db.item(3))
26
+ assert_equal @db.item(3), fetched_item, 'Fetch fail'
27
+ end
28
+ end
29
+
30
+ def test_dump_database
31
+ @server_port = 29557
32
+ _test_request do |server_addr|
33
+ items = Client.dump_database server_addr
34
+ assert_equal @db.length, items.length, 'Wrong number of items'
35
+ items.each_with_index do |item, i|
36
+ assert_equal @db.item(i), item, "Discrepancy in item #{i}"
37
+ end
38
+ end
39
+ end
40
+
41
+ def test_query
42
+ @server_port = 29551
43
+ flexmock(Server).should_receive(:tems_from_cluster_file).
44
+ with(@empty_cluster_file).and_return do |file|
45
+ Tem.auto_conf
46
+ [$tem]
47
+ end
48
+ _test_request do |server_addr|
49
+ result = Client.search server_addr, @client_query
50
+ gold_item = @db.item 5
51
+ assert_equal fare_id(gold_item), result[:id],
52
+ 'Incorrect Map-Reduce result (ID)'
53
+ assert_equal fare_score(gold_item), result[:score],
54
+ 'Incorrect Map-Reduce result (score)'
55
+ end
56
+ end
57
+ end
data/test/test_db.rb ADDED
@@ -0,0 +1,16 @@
1
+ require 'test/mr_test_case'
2
+
3
+ class DbTest < MrTestCase
4
+ def test_loading
5
+ assert_equal 8, @db.length, 'Number of items in the database'
6
+ gold_item = {"price" => 2500, "from" => "BOS", "to" => "TPE",
7
+ "flight" => 15, "layovers"=>2, "end_time"=>2100,
8
+ "start_time"=>900}
9
+ assert_equal gold_item, @db.item(0), 'First database item'
10
+ end
11
+
12
+ def test_by_id
13
+ assert_equal 18, @db.item_by_id(18)['flight'], 'Finding existing item by ID'
14
+ assert_equal nil, @db.item_by_id(5), 'Finding non-existing item'
15
+ end
16
+ end
@@ -0,0 +1,40 @@
1
+ require 'test/mr_test_case'
2
+
3
+ class MapReduceExecutorTest < MrTestCase
4
+ MRExecutor = Tem::Mr::Search::MapReduceExecutor
5
+
6
+ def setup
7
+ super
8
+ Tem.auto_conf
9
+ $tem.activate
10
+ $tem.emit
11
+ end
12
+
13
+ def teardown
14
+ $tem.disconnect
15
+ end
16
+
17
+ def _test_executor(tems, root_tem)
18
+ executor = MRExecutor.new @client_query, @db, tems, root_tem
19
+ packed_output = executor.execute
20
+ result = @client_query.unpack_output packed_output
21
+ gold_item = @db.item 5
22
+ assert_equal fare_id(gold_item), result[:id],
23
+ 'Incorrect Map-Reduce result (ID)'
24
+ assert_equal fare_score(gold_item), result[:score],
25
+ 'Incorrect Map-Reduce result (score)'
26
+ end
27
+
28
+ def test_executor_with_autoconf
29
+ _test_executor [$tem], 0
30
+ end
31
+
32
+ def test_executor_with_cluster
33
+ tems = Tem::Mr::Search::Server.tems_from_cluster_file @cluster_file
34
+ assert_equal 8, tems.length, 'Incorrect cluster setup'
35
+
36
+ tems.each { |tem| tem.activate; tem.emit }
37
+
38
+ _test_executor tems, 0
39
+ end
40
+ end
@@ -0,0 +1,65 @@
1
+ require 'test/mr_test_case'
2
+
3
+ class MapReduceJobTest < MrTestCase
4
+ MapReduceJob = Tem::Mr::Search::MapReduceJob
5
+
6
+ def setup
7
+ super
8
+
9
+ @obj1 = @db.item 0
10
+ @obj2 = @db.item 1
11
+ @output1 = (1..16).to_a
12
+ @output2 = (17..32).to_a
13
+ end
14
+
15
+ def test_map_for_object
16
+ obj = @obj1.merge 'flight' => 0x12345678
17
+ secpack = @client_query.mapper.map_for_object obj
18
+
19
+ assert_equal [0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78],
20
+ secpack.get_bytes(:_id, 8), 'Object ID embedded incorrectly'
21
+ assert_equal @obj1['price'], secpack.get_value(:price, :tem_short),
22
+ 'Price embedded incorrectly'
23
+ assert_equal @obj1['layovers'], secpack.get_value(:layovers, :tem_short),
24
+ 'Layover count embedded incorrectly'
25
+ assert_equal @obj1['start_time'],
26
+ secpack.get_value(:start_time, :tem_short),
27
+ 'Starting time embedded incorrectly'
28
+ assert_equal @obj1['end_time'],
29
+ secpack.get_value(:end_time, :tem_short),
30
+ 'Ending time embedded incorrectly'
31
+ end
32
+
33
+ def test_reduce_for_outputs
34
+ secpack = @client_query.reducer.reduce_for_outputs @output1, @output2
35
+
36
+ assert_equal @output1, secpack.get_bytes(:_output1, 16),
37
+ 'Output1 embedded incorrectly'
38
+ assert_equal @output2, secpack.get_bytes(:_output2, 16),
39
+ 'Output2 embedded incorrectly'
40
+ end
41
+
42
+ def test_unpack_unencrypted_output
43
+ packed_output = [0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78, 0x31, 0x41, 0xCC, 0xCD,
44
+ 0xCE, 0xBE, 0xEF, 0xFE]
45
+ output = @client_query.unpack_decrypted_output packed_output
46
+
47
+ assert_equal 0x12345678, output[:id], 'ID incorrectly unpacked'
48
+ assert_equal 0x3141, output[:score], 'Score incorrectly unpacked'
49
+ assert_equal [0xBE, 0xEF, 0xFE], output[:check], 'Check bytes'
50
+ end
51
+ end
52
+
53
+ class SerializedMapReduceJobTest < MapReduceJobTest
54
+ # Serialization is successful if a de-serialized job can pass all the tests.
55
+
56
+ def setup
57
+ super
58
+ @hash = @client_query.to_hash
59
+ @client_query = MapReduceJob.new @hash
60
+ end
61
+
62
+ def test_serialized_form
63
+ assert_equal Hash, @hash.class, 'Serialization did not produce a plain hash'
64
+ end
65
+ end
@@ -0,0 +1,55 @@
1
+ require 'test/mr_test_case'
2
+ require 'yaml'
3
+
4
+ class MapReducePlannerTest < Test::Unit::TestCase
5
+ MRPlanner = Tem::Mr::Search::MapReducePlanner
6
+
7
+ def setup
8
+ @testdata_path = File.join(File.dirname(__FILE__), '..', 'testdata')
9
+ end
10
+
11
+ def parallel_planning(planner)
12
+ all_actions = []
13
+ until planner.done?
14
+ actions = planner.next_actions!
15
+ all_actions << actions
16
+ actions.each { |action| planner.action_done action }
17
+ end
18
+ all_actions
19
+ end
20
+
21
+ def serial_planning(planner)
22
+ all_actions = []
23
+ pending_actions = []
24
+ until planner.done?
25
+ actions = planner.next_actions!
26
+ all_actions << actions
27
+ pending_actions += actions
28
+ action = pending_actions.shift
29
+ planner.action_done action if action
30
+ end
31
+ all_actions
32
+ end
33
+
34
+ def _test_planning(method_name, items, tems, root_tem, gold_file)
35
+ planner = MRPlanner.new nil, items, tems, root_tem
36
+ all_actions = self.send method_name, planner
37
+ gold_actions = File.open(File.join(@testdata_path, gold_file), 'r') do |f|
38
+ YAML.load f
39
+ end
40
+ assert_equal gold_actions, all_actions, "Failed #{method_name}: " +
41
+ "#{tems} tems with root #{root_tem}, #{items} items"
42
+ assert_equal items * 2 - 1, planner.output_id, "Wrong final output_id"
43
+ end
44
+
45
+ def test_planning
46
+ [[:parallel_planning, 7, 4, 0, 'parallel_plan_740.yml'],
47
+ [:parallel_planning, 4, 3, 1, 'parallel_plan_431.yml'],
48
+ [:serial_planning, 4, 1, 0, 'serial_plan_410.yml'],
49
+ [:serial_planning, 7, 4, 0, 'serial_plan_740.yml'],
50
+ [:serial_planning, 4, 3, 1, 'serial_plan_431.yml'],
51
+ ].each do |testcase|
52
+ _test_planning *testcase
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,40 @@
1
+ require 'test/mr_test_case'
2
+
3
+ class QueryBuildersTest < MrTestCase
4
+ def setup
5
+ super
6
+ Tem.auto_conf
7
+ $tem.activate
8
+ $tem.emit
9
+ end
10
+
11
+ def _test_map_fare(fare)
12
+ enc_output = @client_query.mapper.map_object fare, $tem
13
+ output = @client_query.unpack_output enc_output
14
+ assert_equal fare_id(fare), output[:id], 'Object ID incorrectly encoded'
15
+ assert_equal fare_score(fare), output[:score],
16
+ 'Score incorrectly computed'
17
+ enc_output
18
+ end
19
+
20
+ def test_map_reduce
21
+ fare1 = @db.item 0
22
+ output1 = _test_map_fare fare1
23
+ fare2 = @db.item 1
24
+ output2 = _test_map_fare fare2
25
+
26
+ win_fare = (fare_score(fare1) > fare_score(fare2)) ? fare1 : fare2
27
+ # Try both permutations to ensure all branches of the reduce code work.
28
+ [[output1, output2], [output2, output1]].each do |o1, o2|
29
+ enc_output = @client_query.reducer.reduce_outputs o1, o2, $tem
30
+ output = @client_query.unpack_output enc_output
31
+ assert_equal fare_id(win_fare), output[:id], 'The wrong fare won (bad ID)'
32
+ assert_equal fare_score(win_fare), output[:score],
33
+ 'The wrong fare won (bad score)'
34
+ assert_equal [1, 2, 3], output[:check], 'Incorrect check bytes'
35
+
36
+ assert_not_equal enc_output, output1, 'Nonce fail'
37
+ assert_not_equal enc_output, output2, 'Nonce fail'
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ ---
2
+ - darkbulb2.local
3
+ - lightbulb2.local
@@ -0,0 +1,2 @@
1
+ --- []
2
+