bcpm 0.11

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,8 @@
1
+ require 'socket'
2
+
3
+ unless Socket.respond_to? :hostname
4
+ # :nodoc: Monkey-patch Socket to add hostname method for old Rubies.
5
+ def Socket.hostname
6
+ @__hostname ||= `hostname`.strip
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ # :nodoc: namespace
2
+ module Bcpm
3
+
4
+ # :nodoc: namespace
5
+ module Tests
6
+
7
+ # Raised when a test assertion fails.
8
+ class AssertionError < RuntimeError
9
+ end # class Bcpm::Tests::AssertionError
10
+
11
+ end # namespace Bcpm::Tests
12
+
13
+ end # namespace Bcpm
@@ -0,0 +1,102 @@
1
+ # :nodoc: namespace
2
+ module Bcpm
3
+
4
+ # :nodoc: namespace
5
+ module Tests
6
+
7
+ # Assertions for match tests.
8
+ module Assertions
9
+ # Fails unless the match was won.
10
+ def should_win
11
+ return if match.winner == :a
12
+ raise Bcpm::Tests::AssertionError, "Player was expected to win, but didn't! " + match.outcome
13
+ end
14
+
15
+ # Fails unless the match was won, and the Reason: line includes the argument text.
16
+ def should_win_by(reason)
17
+ should_win
18
+
19
+ return if match.reason.index(reason)
20
+ raise Bcpm::Tests::AssertionError, "Player was expected to win by #{reason} and didn't. " +
21
+ match.reason
22
+ end
23
+
24
+ # Fails unless the match was lost.
25
+ def should_lose
26
+ return if match.winner == :b
27
+ raise Bcpm::Tests::AssertionError, "Player was expected to lose, but didn't! " + match.outcome
28
+ end
29
+
30
+ # Fails unless the match was lost, and the Reason: line includes the argument text.
31
+ def should_lose_by(reason)
32
+ should_lose
33
+
34
+ return if match.reason.index(reason)
35
+ raise Bcpm::Tests::AssertionError, "Player was expected to win by #{reason} and didn't. " +
36
+ match.reason
37
+ end
38
+
39
+ # Fails if the player code threw any exception.
40
+ def should_not_throw
41
+ if match.output.index(/\n(\S*)Exception(.*?)\n\S/m)
42
+ raise Bcpm::Tests::AssertionError, "Player should not have thrown exceptions! " +
43
+ "It threw #{$1}Exception#{$2}"
44
+ end
45
+ if match.chatter.index(/\n(\S*)Exception(.*?)\n\S/m)
46
+ raise Bcpm::Tests::AssertionError, "Player should not have thrown exceptions! " +
47
+ "It threw #{$1}Exception#{$2}"
48
+ end
49
+ end
50
+
51
+ # Always fails. Useful for obtaining the game log.
52
+ def fail(reason = 'Test case called fail!')
53
+ raise Bcpm::Tests::AssertionError, reason
54
+ end
55
+
56
+ # Fails unless a unit's output matches the given regular expression.
57
+ #
58
+ # If a block is given, yields to the block for every match.
59
+ def should_match_unit_output(pattern)
60
+ matched = false
61
+
62
+ match.output_lines.each do |line|
63
+ next unless unit_output = _parse_unit_output(line)
64
+ if match = pattern.match(unit_output[:output])
65
+ matched = true
66
+ if Kernel.block_given?
67
+ yield unit_output, match
68
+ else
69
+ break
70
+ end
71
+ end
72
+ end
73
+
74
+ raise Bcpm::Tests::AssertionError, "No unit output matched #{pattern.inspect}!" unless matched
75
+ end
76
+
77
+ # Parses a unit's console output (usually via System.out.print*).
78
+ #
79
+ # If the given line looks like a unit's console output, returns a hash with the following keys:
80
+ # :team:: 'A' or 'B' (should always be 'A', unless the case enables team B's console output)
81
+ # :unit_type:: e.g., 'ARCHON'
82
+ # :unit_id:: the robot ID of the unit who wrote the line, parsed as an Integer
83
+ # :round:: the round when the line was output, parsed as an Integer
84
+ # :output:: the (first line of the) string that the unit produced
85
+ #
86
+ # If the line doesn't parse out, returns nil.
87
+ def _parse_unit_output(line)
88
+ line_match = /^\[([AB])\:([A-Z]+)\#(\d+)\@(\d+)\](.*)$/.match line
89
+ return nil unless line_match
90
+ {
91
+ :team => line_match[1],
92
+ :unit_type => line_match[2],
93
+ :unit_id => line_match[3].to_i,
94
+ :round => line_match[4].to_i,
95
+ :output => line_match[5]
96
+ }
97
+ end
98
+ end
99
+
100
+ end # namespace Bcpm::Tests
101
+
102
+ end # namespace Bcpm
@@ -0,0 +1,166 @@
1
+ # :nodoc: namespace
2
+ module Bcpm
3
+
4
+ # :nodoc: namespace
5
+ module Tests
6
+
7
+ # Base class for test cases.
8
+ #
9
+ # Each test case is its own anonymous class.
10
+ class CaseBase
11
+ class <<self
12
+ # Called before any code is evaluated in the class context.
13
+ def _setup
14
+ @map = nil
15
+ @suite_map = false
16
+ @vs = nil
17
+ @side = :a
18
+ @match = nil
19
+ @options = {}
20
+ @env = Bcpm::Tests::Environment.new
21
+ @env_used = false
22
+
23
+ @tests = []
24
+ @environments = []
25
+ @matches = []
26
+ end
27
+
28
+ # Called after all code is evaluated in the class context.
29
+ def _post_eval
30
+ @environments << @env if @env_used
31
+ @env = nil
32
+ @options = nil
33
+ end
34
+
35
+ # Called by public methods before they change the environment.
36
+ def _env_change
37
+ if @env_used
38
+ @environments << @env
39
+ @env = Bcpm::Tests::Environment.new
40
+ @env_used = false
41
+ end
42
+ end
43
+
44
+ # Set the map for following matches.
45
+ def map(map_name)
46
+ @map = map_name.dup.to_s
47
+ @suite_map = false
48
+ end
49
+
50
+ # Set the map for the following match. Use a map in the player's test suite.
51
+ def suite_map(map_name)
52
+ @map = map_name.dup.to_s
53
+ @suite_map = true
54
+ end
55
+
56
+ # Set the enemy for following matches.
57
+ def vs(player_name)
58
+ @vs = player_name.dup.to_s
59
+ end
60
+
61
+ # Set our side for the following matches.
62
+ def side(side)
63
+ @side = side.to_s.downcase.to_sym
64
+ end
65
+
66
+ # Set a simulation option.
67
+ def option(key, value)
68
+ key = Bcpm::Match.engine_options[key.to_s] if key.kind_of? Symbol
69
+
70
+ if value.nil?
71
+ @options.delete key
72
+ else
73
+ @options[key] = value
74
+ end
75
+ end
76
+
77
+ # Replaces a class implementation file (.java) with another one (presumably from tests).
78
+ def replace_class(target, source)
79
+ _env_change
80
+ @env.file_op [:file, target, source]
81
+ end
82
+
83
+ # Replaces all fragments labeled with target_fragment in target_class with another fragment.
84
+ def replace_code(target_class, target_fragment, source_class, source_fragment)
85
+ _env_change
86
+ @env.file_op [:fragment, [target_class, target_fragment], [source_class, source_fragment]]
87
+ end
88
+
89
+ # Redirects all method calls using a method name to a static method.
90
+ #
91
+ # Assumes the target method is a member method, and passes "this" to the static method.
92
+ def stub_member_call(source, target)
93
+ _env_change
94
+ @env.patch_op [:stub_member, target, source]
95
+ end
96
+
97
+ # Redirects all method calls using a method name to a static method.
98
+ #
99
+ # Assumes the target method is a static method, and ignores the call target.
100
+ def stub_static_call(source, target)
101
+ _env_change
102
+ @env.patch_op [:stub_static, target, source]
103
+ end
104
+
105
+ # Create a test match. The block contains test cases for the match.
106
+ def match(&block)
107
+ begin
108
+ @env_used = true
109
+ map = @suite_map ? @env.suite_map_path(@map) : @map
110
+ @match = Bcpm::Tests::TestMatch.new @side, @vs, map, @env, @options
111
+ self.class_eval(&block)
112
+ @matches << @match
113
+ ensure
114
+ @match = nil
115
+ end
116
+ end
117
+
118
+ # Create a test match.
119
+ def it(label, &block)
120
+ raise "it can only be called within match blocks!" if @match.nil?
121
+ @tests << self.new(label, @match, block)
122
+ end
123
+
124
+ # All the environments used in the tests.
125
+ attr_reader :environments
126
+ # All the matches used in the tests.
127
+ attr_reader :matches
128
+ # All test cases.
129
+ attr_reader :tests
130
+ end
131
+
132
+ # Descriptive label for the test case.
133
+ attr_reader :label
134
+ # Test match used by the test case.
135
+ attr_reader :match
136
+
137
+ # Called by match.
138
+ def initialize(label, match, block)
139
+ @label = label
140
+ @match = match
141
+ @block = block
142
+ end
143
+
144
+ # User-readable description of test conditions.
145
+ def description
146
+ "#{match.description} #{label}"
147
+ end
148
+
149
+ # Verifies the match output against the test case.
150
+ #
151
+ # Returns nil for success, or an AssertionError exception if the case failed.
152
+ def check_output
153
+ begin
154
+ self.instance_eval &@block
155
+ return nil
156
+ rescue Bcpm::Tests::AssertionError => e
157
+ return e
158
+ end
159
+ end
160
+
161
+ include Bcpm::Tests::Assertions
162
+ end # class Bcpm::Tests::CaseBase
163
+
164
+ end # namespace Bcpm::Tests
165
+
166
+ end # namespace Bcpm
@@ -0,0 +1,256 @@
1
+ require 'English'
2
+ require 'fileutils'
3
+ require 'socket'
4
+
5
+ # :nodoc: namespace
6
+ module Bcpm
7
+
8
+ # :nodoc: namespace
9
+ module Tests
10
+
11
+ # A match run for simulation purposes.
12
+ #
13
+ # Each test case is its own anonymous class.
14
+ class Environment
15
+ # Name of the player container for the enviornment.
16
+ attr_reader :player_name
17
+
18
+ # The build log, if the build happened.
19
+ attr_reader :build_log
20
+
21
+ # Creates a new environment blueprint.
22
+ #
23
+ # Args:
24
+ # prebuilt_name:: if given, the created blueprint points to an already-built environment
25
+ def initialize(prebuilt_name = nil)
26
+ @file_ops = []
27
+ @patch_ops = []
28
+ @build_log = nil
29
+
30
+ if prebuilt_name
31
+ @player_name = prebuilt_name
32
+ @available = true
33
+ else
34
+ @player_name = self.class.new_player_name
35
+ @available = false
36
+ end
37
+ end
38
+
39
+ # Puts together an environment according to the blueprint.
40
+ def setup(suite_path)
41
+ return true if @available
42
+
43
+ begin
44
+ test_player = File.basename suite_path
45
+
46
+ @player_path = Bcpm::Player.checkpoint test_player, 'master', player_name
47
+ raise "Failed to checkpoint player at #{suite_path}" unless @player_path
48
+ @player_src = Bcpm::Player.package_path(@player_path)
49
+
50
+ file_ops
51
+ patch_ops
52
+
53
+ unless build
54
+ print "Test environment build failed! Some tests will not run!\n"
55
+ print "#{@build_log}\n"
56
+ return false
57
+ end
58
+ rescue Exception => e
59
+ print "Failed setting up test environment! Some tests will not run!\n"
60
+ print "#{e.class.name}: #{e.to_s}\n#{e.backtrace.join("\n")}\n\n"
61
+ return false
62
+ end
63
+ @available = true
64
+ end
65
+
66
+ # True if the environment has been setup and can be used to run tests.
67
+ def available?
68
+ @available
69
+ end
70
+
71
+ # Undoes the effects of setup.
72
+ def teardown
73
+ Bcpm::Player.uninstall player_name
74
+ @available = false
75
+ end
76
+
77
+ # Path to the maps in the test suite for the player.
78
+ def suite_map_path(map_name)
79
+ File.join Bcpm::Player.local_root, player_name, 'suite', 'maps',
80
+ map_name + '.xml'
81
+ end
82
+
83
+ # Queue an operation that adds a file.
84
+ def file_op(op)
85
+ @file_ops << op
86
+ end
87
+
88
+ # Queue an operation that patches all source files.
89
+ def patch_op(op)
90
+ @patch_ops << op
91
+ end
92
+
93
+ # Copies files from the test suite to the environment.
94
+ #
95
+ # Called by setup, uses its environment.
96
+ def file_ops
97
+ @file_ops.each do |op|
98
+ op_type, target, source = *op
99
+
100
+ if op_type == :fragment
101
+ target, target_fragment = *target
102
+ source, source_fragment = *source
103
+ end
104
+
105
+ target = "#{@player_name}.#{target}"
106
+ source = "#{@player_name}.#{source}"
107
+ file_path = java_path @player_src, source
108
+
109
+ next unless File.exist? file_path
110
+ source_contents = File.read file_path
111
+
112
+ case op_type
113
+ when :file
114
+ contents = source_contents
115
+ when :fragment
116
+ next unless fragment_match = fragment_regexp(source_fragment).match(source_contents)
117
+ contents = fragment_match[0]
118
+ end
119
+
120
+ source_pkg = java_package source
121
+ target_pkg = java_package target
122
+ unless source_pkg == target_pkg
123
+ contents.gsub! /(^|[^A-Za-z0-9_.])#{source_pkg}([^A-Za-z0-9_]|$)/, "\\1#{target_pkg}\\2"
124
+ end
125
+
126
+ source_class = java_class source
127
+ target_class = java_class target
128
+ unless source_class == target_class
129
+ contents.gsub! /(^|[^A-Za-z0-9_])#{source_class}([^A-Za-z0-9_]|$)/, "\\1#{target_class}\\2"
130
+ end
131
+
132
+ file_path = java_path @player_src, target
133
+
134
+ case op_type
135
+ when :file
136
+ next unless File.exist?(File.dirname(file_path))
137
+ when :fragment
138
+ next unless File.exist?(file_path)
139
+ source_contents = File.read file_path
140
+ # Not using a string because source code might contain \1 which would confuse gsub.
141
+ source_contents.gsub! fragment_regexp(target_fragment) do |match|
142
+ "#{$1}\n#{contents}\n#{$3}"
143
+ end
144
+ contents = source_contents
145
+ end
146
+
147
+ File.open(file_path, 'wb') { |f| f.write contents }
148
+ end
149
+ end
150
+
151
+ # Applies the patch operations to the source code in the environment.
152
+ #
153
+ # Called by setup, uses its environment.
154
+ def patch_ops
155
+ return if @patch_ops.empty?
156
+
157
+ Dir.glob(File.join(@player_src, '**', '*.java')).each do |file|
158
+ old_contents = File.read file
159
+ lines = old_contents.split("\n")
160
+
161
+ stubs_enabled = true
162
+
163
+ 0.upto(lines.count - 1) do |i|
164
+ line = lines[i]
165
+ if directive_match = /^\s*\/\/\$(.*)$/.match(line)
166
+ directive = directive_match[1]
167
+ case directive.strip.downcase
168
+ when '+stubs', '-stubs'
169
+ stubs_enabled = directive[0] == ?+
170
+ end
171
+ else
172
+ @patch_ops.each do |op|
173
+ op_type, target, source = *op
174
+
175
+ case op_type
176
+ when :stub_member
177
+ if stubs_enabled
178
+ line.gsub!(/(^|[^A-Za-z0-9_.])([A-Za-z0-9_.]*\.)?#{source}\(/) do |match|
179
+ arg = ($2.nil? || $2.empty?) ? 'this' : $2[0..-2]
180
+ "#{$1}#{player_name}.#{target}(#{arg}, "
181
+ end
182
+ end
183
+ when :stub_static
184
+ if stubs_enabled
185
+ line.gsub! /(^|[^A-Za-z0-9_.])([A-Za-z0-9_.]*\.)?#{source}\(/,
186
+ "\\1#{player_name}.#{target}("
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+ contents = lines.join("\n")
193
+ File.open(file, 'wb') { |f| f.write contents } unless contents == old_contents
194
+ end
195
+ end
196
+
197
+ # Builds the binaries for the player in this environment.
198
+ #
199
+ # Called by setup, uses its environment.
200
+ #
201
+ # Returns true for success, false for failure.
202
+ def build
203
+ uid = "bcpmbuild_#{Socket.hostname}_#{(Time.now.to_f * 1000).to_i}_#{$PID}"
204
+ tempdir = File.expand_path File.join(Dir.tmpdir, 'bcpm', uid)
205
+ FileUtils.mkdir_p tempdir
206
+ build_log = File.join tempdir, 'build.log'
207
+ build_file = File.join tempdir, 'build.xml'
208
+ Bcpm::Match.write_build build_file, 'bc.conf'
209
+
210
+ Bcpm::Match.run_build_script tempdir, build_file, build_log, 'build'
211
+ @build_log = File.exist?(build_log) ? File.open(build_log, 'rb') { |f| f.read } : ''
212
+ FileUtils.rm_rf tempdir
213
+
214
+ @build_log.index("\nBUILD SUCCESSFUL\n") ? true : false
215
+ end
216
+
217
+ # Regular expression matching a code fragment.
218
+ #
219
+ # The expression captures three groups: the fragment start marker, the fragment, and the fragment
220
+ # end marker.
221
+ def fragment_regexp(label)
222
+ /^([ \t]*\/\/\$[ \t]*\+mark\:[ \t]*#{label}\s)(.*)(\n[ \t]*\/\/\$[ \t]*\-mark\:[ \t]*#{label}\s)/m
223
+ end
224
+
225
+ # Path to .java source for a class.
226
+ def java_path(package_path, class_name)
227
+ File.join(File.dirname(package_path), class_name.gsub('.', '/') + '.java')
228
+ end
229
+
230
+ # Package for a Java class given its fully qualified name.
231
+ def java_package(class_name)
232
+ index = class_name.rindex '.'
233
+ index ? class_name[0, index] : ''
234
+ end
235
+
236
+ # Short class name for a Java class given its fully qualified name.
237
+ def java_class(class_name)
238
+ index = class_name.rindex '.'
239
+ index ? class_name[index + 1, class_name.length - index - 1] : class_name
240
+ end
241
+
242
+ # A player name guaranteed to be unique across the systme.
243
+ def self.new_player_name
244
+ # NOTE: Java doesn't like .s in it package names :)
245
+ host = Socket.hostname.gsub(/[^A-Za-z_]/, '_')
246
+ @prefix ||=
247
+ "bcpmtest_#{host}_#{(Time.now.to_f * 1000).to_i}_#{$PID}"
248
+ @counter ||= 0
249
+ @counter += 1
250
+ "#{@prefix}_#{@counter}"
251
+ end
252
+ end # class Bcpm::Tests::TestMatch
253
+
254
+ end # namespace Bcpm::Tests
255
+
256
+ end # namespace Bcpm