bcpm 0.11

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.
@@ -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