Skip to content

Commit 5c79e2b

Browse files
committed
setup unit testing infrastructure [#7170859 complete]
1 parent 176fdb0 commit 5c79e2b

File tree

5 files changed

+200
-28
lines changed

5 files changed

+200
-28
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
build
2+
build_test
23
download
34
dist
45
jruby4max.jar

Rakefile

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,48 +12,48 @@ Built-Date: #{BUILD_DATE}
1212
Author: Adam Murray
1313
URL: http://compusition.com
1414
"
15-
16-
SRC = 'src'
1715
LIB = 'lib'
1816
PATCHES = 'jruby_for_max'
1917
LICENSE = 'license'
2018
BUILD = 'build'
19+
TESTS_BUILD = 'build_test'
2120
DIST = 'dist'
22-
PROJ = "jruby_for_max-#{PROJECT_VERSION}"
21+
PROJ = "jruby_for_max-#{PROJECT_VERSION}"
2322
PACKAGE = "#{DIST}/#{PROJ}"
2423

25-
SOURCES = FileList["#{SRC}/**/*.java"].exclude(/Test\.java$/)
26-
CLASSPATH = FileList["#{LIB}/**/*.jar"].exclude(/^jruby_for_max.jar$/)
27-
JAR = "#{LIB}/jruby4max.jar"
24+
JAR = "jruby4max.jar"
25+
JAR_FILE = "#{LIB}/#{JAR}"
26+
27+
SOURCES = FileList['src/**/*.java']
28+
TESTS = FileList['test/**/*.java']
29+
CLASSPATH = FileList["#{LIB}/**/*.jar"].exclude(/^#{JAR}$/)
30+
TESTS_CLASSPATH = CLASSPATH.clone.add(BUILD)
2831

2932
WINDOWS = Config::CONFIG['host_os'] =~ /mswin/
3033
CLASSPATH_SEPARATOR = if WINDOWS then ';' else ':' end
3134

3235
##############################################################################
3336
# TASK DEFINITIONS
3437

35-
CLEAN.include BUILD, JAR, DIST
36-
CLOBBER.include 'out'
38+
CLEAN.include BUILD, TESTS_BUILD, JAR_FILE, DIST
39+
CLOBBER.include 'out' # default IntelliJ build folder
3740

3841

39-
desc 'compile the java source files'
42+
desc 'Compile java source files'
4043
task :compile do
41-
mkdir BUILD
42-
puts "Building java classes"
43-
`javac -classpath #{CLASSPATH.join CLASSPATH_SEPARATOR} -d #{BUILD} -g -source 1.5 -target 1.5 #{SOURCES}`
44+
puts "Compiling java source files"
45+
javac CLASSPATH, SOURCES, BUILD
4446
end
4547

4648

47-
desc 'construct the jar archive of the compiled java sources'
4849
task :jar => [:clean, :compile] do
4950
manifest = Tempfile.new('manifest')
50-
File.open(manifest.path, 'w') {|io| io.write MANIFEST }
51-
puts "Archiving build: #{JAR}"
52-
`jar cvfm #{JAR} #{manifest.path} -C #{BUILD} .`
51+
write_file manifest, MANIFEST
52+
puts "Archiving build: #{JAR_FILE}"
53+
jar JAR_FILE, manifest, BUILD
5354
end
5455

5556

56-
desc 'prepare the files for distribution'
5757
task :package => [:jar] do
5858
puts "Preparing distribution"
5959
package_lib = "#{PACKAGE}/#{LIB}"
@@ -65,15 +65,14 @@ task :package => [:jar] do
6565
FileList['*.txt', '*.example'].each do |filename|
6666
cp filename, PACKAGE
6767
end
68-
FileList["#{LIB}/jruby.jar", JAR].each do |filename|
68+
FileList["#{LIB}/jruby.jar", JAR_FILE].each do |filename|
6969
cp filename, package_lib
7070
end
7171
cp_r PATCHES, PACKAGE
7272
cp_r LICENSE, PACKAGE
7373
end
7474

7575

76-
desc 'search and replace variable values in text files'
7776
task :replace_vars => [:package] do
7877
puts "Performing search and replace for the VERSION and BUILD_DATE variables"
7978
plaintext_filetypes = ['txt', 'maxpat', 'maxhelp']
@@ -83,20 +82,55 @@ task :replace_vars => [:package] do
8382
end
8483

8584

86-
desc 'construct the distribution archive'
85+
desc 'Build distribution archive'
8786
task :dist => [:replace_vars] do
8887
mkdir DIST
8988
archive = "#{PROJ}.zip"
9089
puts "Archiving distribution: #{DIST}/#{archive}"
90+
91+
# NOTE: this only works on OS X. It might work with the correct Cygwin setup on Windows but I never tested this.
9192
`cd #{DIST} && zip -l -m -r #{archive} #{PROJ}`
9293
# The -l option converts newlines to crlf, which should display correctly on both OS X and Windows.
9394
# Otherwise, since I write these txt files on OS X, newlines would disappear when viewed in Notepad on Windows.
94-
end
95+
end
96+
97+
98+
task :compile_tests => [:compile] do
99+
puts 'Compiling java test files'
100+
javac TESTS_CLASSPATH, TESTS, TESTS_BUILD
101+
end
102+
103+
desc 'Run unit tests'
104+
task :test => [:compile_tests] do
105+
puts 'Running tests'
106+
junit_class_path = TESTS_CLASSPATH.clone.add(TESTS_BUILD)
107+
test_classes = FileList["#{TESTS_BUILD}/**/*.class"]
108+
test_classnames = test_classes.map{|path| path.sub("#{TESTS_BUILD}/", '').sub(".class", '').gsub('/', '.') }
109+
puts java junit_class_path, 'org.junit.runner.JUnitCore', test_classnames
110+
end
111+
95112

96113

97114
##############################################################################
98115
# SUPPORT CODE:
99116

117+
def write_file file, contents
118+
File.open(file.path, 'w') {|io| io.write contents }
119+
end
120+
121+
def java classpath, main_class, args
122+
`java -classpath #{classpath.join CLASSPATH_SEPARATOR} #{main_class} #{args}`
123+
end
124+
125+
def javac classpath, src_files, dst_folder
126+
mkdir dst_folder
127+
`javac -classpath #{classpath.join CLASSPATH_SEPARATOR} -d #{dst_folder} -g -source 1.5 -target 1.5 #{src_files}`
128+
end
129+
130+
def jar filename, manifest, dst_folder
131+
`jar cvfm #{filename} #{manifest.path} -C #{dst_folder} .`
132+
end
133+
100134
# I find it annoying that I always have to check if a directory exists
101135
# before creating it, so I monkey patch mkdir() to handle it automatically:
102136
alias original_mkdir mkdir
@@ -120,3 +154,4 @@ class FileList
120154
end
121155
end
122156
end
157+

src/jruby4max/rubysupport/ScriptEvaluatorManager.java

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,28 +37,49 @@ LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
3737
/**
3838
* Factory for Ruby evaluators that manages shared contexts.
3939
*
40+
* Every Ruby evaluator maintains its own set of class/method definitions, and global/constant/attribute variables.
41+
* Each evaluator is uniquely identified by a context String. This context is either the @context attribute of
42+
* the Max object, or is generated if no @context is provided. Additionally, every MaxObject has a unique @id, which
43+
* is stored in the data structures here.
44+
*
45+
* This class provides evaluator instances and keeps track of which evaluator goes with which MaxObject.
46+
*
47+
* The maxObjectMap in this class is expose to all Ruby scripts via the $max_object_map global variable.
48+
*
4049
* @author Adam Murray ([email protected])
4150
*/
4251
public class ScriptEvaluatorManager {
4352

4453
private static Map<String, IScriptEvaluator> evaluatorContexts = new HashMap<String, IScriptEvaluator>();
54+
55+
/**
56+
* Map @context => number of evaluators currently using
57+
*/
4558
private static Map<String, Integer> evaluatorContextCounter = new HashMap<String, Integer>();
59+
60+
/**
61+
* Map @context => a Set of MaxObjects
62+
*/
4663
private static MappedSet<String, Object> objectsUsingEvaluator = new MappedSet<String, Object>();
64+
65+
/**
66+
* Map @context => a Map of ( @id => MaxObject )
67+
*/
4768
private static Map<String, Map<String, Object>> maxObjectMap = new HashMap<String, Map<String, Object>>();
4869

4970
/**
50-
* Stores a mapping from max objects to their context and id
71+
* Stores a mapping from max objects to their @context and @id
5172
*/
5273
private static Map<Object, String[]> objectMetadata = new HashMap<Object, String[]>();
5374

54-
// This is a singleton, so no instances allowed.
75+
// This is a singleton (or more accurately: a static utility class), so no instances allowed to be constructed.
5576
private ScriptEvaluatorManager() {
5677
}
5778

5879
/**
5980
* Get a Ruby evaluator for the specified context.
6081
*
61-
* @return
82+
* @return an implementation of IScriptEvaluator
6283
*/
6384
public static IScriptEvaluator getRubyEvaluator( String maxContext, String id, Object maxObject, CompatVersion rubyVersion ) {
6485

@@ -95,7 +116,8 @@ public static IScriptEvaluator getRubyEvaluator( String maxContext, String id, O
95116
}
96117

97118
/**
98-
* @return true if the entire context was removed
119+
* Signal that a MaxObject was destroyed, and terminate the evaluator for this MaxObject if no other objects are
120+
* referencing it.
99121
*/
100122
public static void removeRubyEvaluator( Object maxObject ) {
101123
String[] contextAndId = objectMetadata.remove( maxObject );
@@ -127,6 +149,9 @@ public static void removeRubyEvaluator( Object maxObject ) {
127149
}
128150
}
129151

152+
/**
153+
* Update the id for an existing MaxObject
154+
*/
130155
public static void updateId( Object maxObject, String id ) {
131156
String[] contextAndId = objectMetadata.get( maxObject );
132157
if( contextAndId != null ) {
@@ -142,7 +167,14 @@ public static void updateId( Object maxObject, String id ) {
142167
}
143168
}
144169

145-
private static void ensureIdAvailable( Map<String, Object> idMap, Object maxObject, String id ) {
170+
/**
171+
* Validate that an id is unique within a context.
172+
* @param idMap - a map from @id to MaxObject within a specific context
173+
* @param maxObject - the MaxObject requesting this id (it's ok for a MaxObject to set its id to its current id)
174+
* @param id - the requested id for this MaxObject
175+
* @throws IdInUseException if the @id is not available. The message of the Exception provides an available id.
176+
*/
177+
protected static void ensureIdAvailable( Map<String, Object> idMap, Object maxObject, String id ) {
146178
Object existingObject = idMap.get( id );
147179
if( existingObject != null && !existingObject.equals( maxObject ) ) {
148180
String base = id;
@@ -163,7 +195,13 @@ private static void ensureIdAvailable( Map<String, Object> idMap, Object maxObje
163195
}
164196
}
165197

166-
private static String getEvaluatorContext( String maxContext, Object maxObject ) {
198+
/**
199+
* Either return the requested context, or generate one.
200+
* @param maxContext - the requested context. May be null in which case a context is generated.
201+
* @param maxObject - the MaxObject which will be using thie context.
202+
* @return the context String
203+
*/
204+
protected static String getEvaluatorContext( String maxContext, Object maxObject ) {
167205
if( maxContext == null ) {
168206
return "__" + Integer.toHexString( maxObject.hashCode() );
169207
}
@@ -172,11 +210,19 @@ private static String getEvaluatorContext( String maxContext, Object maxObject )
172210
}
173211
}
174212

213+
/**
214+
* Notify a ruby evaluator that a MaxObject has been destroyed.
215+
*
216+
* NOTE: typically you should use removeRubyEvaluator() instead, but this method is exposed publicly for
217+
* the scenario where a MaxObject is being reset when reloading a @file.
218+
*
219+
* @param maxContext - the context String that identifies a specific evaluator
220+
* @param maxObject - the MaxObject being destroyed
221+
*/
175222
public static void notifyContextDestroyedListener( String maxContext, Object maxObject ) {
176223
String evaluatorContext = getEvaluatorContext( maxContext, maxObject );
177224
IScriptEvaluator ruby = evaluatorContexts.get( evaluatorContext );
178225
if( ruby != null ) {
179-
// The callback method behavior above should be phased out in favor of this:
180226
ruby.exit();
181227
}
182228
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package jruby4max.rubysupport;
2+
3+
import org.junit.Assert;
4+
import org.junit.Test;
5+
6+
import java.util.HashMap;
7+
import java.util.Map;
8+
9+
public class ScriptEvaluatorManagerTest {
10+
11+
@Test
12+
public void ensureIdAvailable_should_do_nothing_when_the_id_is_available() {
13+
Map<String, Object> idMap = new HashMap<String, Object>();
14+
Object o = new Object();
15+
ScriptEvaluatorManager.ensureIdAvailable(idMap, o, "id");
16+
}
17+
18+
@Test
19+
public void ensureIdAvailable_should_do_nothing_when_the_id_is_taken_by_the_current_object() {
20+
Map<String, Object> idMap = new HashMap<String, Object>();
21+
Object o = new Object();
22+
idMap.put("id", o);
23+
ScriptEvaluatorManager.ensureIdAvailable(idMap, o, "id");
24+
}
25+
26+
@Test
27+
public void ensureIdAvailable_should_throw_an_exception_with_an_available_id_when_the_id_is_taken() {
28+
Map<String, Object> idMap = new HashMap<String, Object>();
29+
idMap.put("id", new Object());
30+
Object o = new Object();
31+
try {
32+
ScriptEvaluatorManager.ensureIdAvailable(idMap, o, "id");
33+
}
34+
catch(IdInUseException e) {
35+
Assert.assertEquals("id[1]", e.getMessage());
36+
}
37+
}
38+
39+
@Test
40+
public void ensureIdAvailable_should_increment_suggested_id_index() {
41+
Map<String, Object> idMap = new HashMap<String, Object>();
42+
idMap.put("id[1]", new Object());
43+
Object o = new Object();
44+
try {
45+
ScriptEvaluatorManager.ensureIdAvailable(idMap, o, "id[1]");
46+
}
47+
catch(IdInUseException e) {
48+
Assert.assertEquals("id[2]", e.getMessage());
49+
}
50+
}
51+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package jruby4max.util;
2+
3+
import org.junit.Assert;
4+
import org.junit.Before;
5+
import org.junit.Test;
6+
7+
import java.util.Arrays;
8+
import java.util.HashSet;
9+
import java.util.Set;
10+
11+
public class MappedSetTest {
12+
13+
private MappedSet<String,Integer> subject;
14+
15+
@Before
16+
public void before() {
17+
subject = new MappedSet<String,Integer>();
18+
}
19+
20+
@Test
21+
public void addValue_should_prevent_duplicates() {
22+
subject.addValue("key", 5);
23+
subject.addValue("key", 5);
24+
Object[] value = subject.get("key").toArray();
25+
Assert.assertEquals(1, value.length);
26+
Assert.assertEquals(5, value[0]);
27+
}
28+
29+
@Test
30+
public void addValue_should_maintain_insertion_order() {
31+
subject.addValue("key", 1);
32+
subject.addValue("key", 2);
33+
long counter = 1;
34+
for(Integer value : subject.get("key")) {
35+
Assert.assertEquals(counter, (long)value);
36+
counter++;
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)