package water.rapids; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; import water.*; import water.fvec.Frame; import water.fvec.NFSFileVec; import water.fvec.Vec; import water.parser.ParseDataset; import water.parser.ParseSetup; import water.rapids.ast.AstRoot; import water.rapids.ast.params.AstNumList; import water.rapids.ast.params.AstStr; import water.rapids.vals.ValFrame; import water.util.ArrayUtils; import water.util.FileUtils; import water.util.Log; import java.io.File; import java.util.Arrays; import static org.junit.Assert.*; import static water.rapids.Rapids.IllegalASTException; public class RapidsTest extends TestUtil { @BeforeClass public static void setup() { stall_till_cloudsize(1); } @Test public void bigSlice() { // check that large slices do something sane String tree = "(rows a.hex [0:2147483647])"; checkTree(tree); } @Test public void testParseString() { astStr_ok("'hello'", "hello"); astStr_ok("\"one two three\"", "one two three"); astStr_ok("\" \\\" \"", " \" "); astStr_ok("\"\\\\\"", "\\"); astStr_ok("'test\"omg'", "test\"omg"); astStr_ok("'sun\nmoon'", "sun\nmoon"); astStr_ok("'a\\nb'", "a\nb"); astStr_ok("'\\n\\r\\t\\b\\f\\'\\\"\\\\'", "\n\r\t\b\f\'\"\\"); astStr_ok("'\\x00\\xa2\\xBC\\xDe\\xFF\\xcb'", "\u0000\u00A2\u00BC\u00DE\u00FF\u00CB"); astStr_ok("\"\\uABCD\\u0000\\uffff\"", "\uABCD\u0000\uFFFF"); astStr_ok("\"\\U0001F578\"", new String(Character.toChars(0x1F578))); parse_err("\"hello"); parse_err("\"one\"two\""); parse_err("\"something\'"); parse_err("'\\+'"); parse_err("'\\0'"); parse_err("'\\xA'"); parse_err("'\\xHI"); parse_err("'\\u123 spam'"); parse_err("'\\U'"); parse_err("'\\U12345678'"); parse_err("'\\U1F578'"); parse_err("'\\U+1F578'"); parse_err("'\\u{1F578}'"); } @Test public void testParseNumList() { astNumList_ok("[]", new double[0]); astNumList_ok("[1 2 3]", ard(1, 2, 3)); astNumList_ok("[1, 2, 3]", ard(1, 2, 3)); astNumList_ok("[1 , 2\t, 3,4]", ard(1, 2, 3, 4)); astNumList_ok("[000.1 -3 17.003 2e+01 +11.1 1234567890]", ard(0.1, -3, 17.003, 20, 11.1, 1234567890)); astNumList_ok("[NaN nan]", ard(Double.NaN, Double.NaN)); astNumList_ok("[1 2:3 5:10:2]", ard(1, 2, 3, 4, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23)); astNumList_ok("[-0.5:10:0.1]", ard(-0.5, -0.4, -0.3, -0.2, -0.1, 0, 0.1, 0.2, 0.3, 0.4)); parse_err("[21 11"); parse_err("[1 0.00.0]"); parse_err("[0 1 true false]"); parse_err("[#1 #2 #3]"); parse_err("[0 1 'hello']"); parse_err("[1:0]"); parse_err("[0:nan:2]"); parse_err("[0:2:nan]"); parse_err("[1:0:5]"); parse_err("[1:-20]"); parse_err("[1:20:-5]"); } @Test public void test1() { // Checking `hex + 5` String tree = "(+ a.hex 5)"; checkTree(tree); } @Test public void test2() { // Checking `hex + 5 + 10` String tree = "(+ a.hex (+ 5 10))"; checkTree(tree); } @Test public void test3() { // Checking `hex + 5 - 1 * hex + 15 * (23 / hex)` String tree = "(+ (- (+ a.hex 5) (* 1 a.hex)) (* 15 (/ 23 a.hex)))"; checkTree(tree); } @Test public void test4() { //Checking `hex == 5`, <=, >=, <, >, != String tree = "(== a.hex 5)"; checkTree(tree); tree = "(<= a.hex 5)"; checkTree(tree); tree = "(>= a.hex 1.25132)"; checkTree(tree); tree = "(< a.hex 112.341e-5)"; checkTree(tree); tree = "(> a.hex 0.0123)"; checkTree(tree); tree = "(!= a.hex 0)"; checkTree(tree); } @Test public void test4_throws() { String tree = "(== a.hex (cols a.hex [1 2]))"; checkTree(tree, "Frames must have same columns, found 4 columns and 2 columns."); } @Test public void test5() { // Checking `hex && hex`, ||, &, | String tree = "(&& a.hex a.hex)"; checkTree(tree); tree = "(|| a.hex a.hex)"; checkTree(tree); tree = "(& a.hex a.hex)"; checkTree(tree); tree = "(| a.hex a.hex)"; checkTree(tree); } @Test public void test6() { // Checking `hex[,1]` String tree = "(cols a.hex [0])"; checkTree(tree); // Checking `hex[1,5]` tree = "(rows (cols a.hex [0]) [5])"; checkTree(tree); // Checking `hex[c(1:5,7,9),6]` tree = "(cols (rows a.hex [0:4 6 7]) [0])"; checkTree(tree); // Checking `hex[c(8,1,1,7),1]` // No longer handle dup or out-of-order rows tree = "(rows a.hex [2 7 8])"; checkTree(tree); } @Test public void testRowAssign() { String tree; // Assign column 3 over column 0 tree = "(:= a.hex (cols a.hex [3]) 0 [0:150])"; checkTree(tree); // Assign 17 over column 0 tree = "(:= a.hex 17 [0] [0:150])"; checkTree(tree); // Assign 17 over column 0, row 5 tree = "(:= a.hex 17 [0] [5])"; checkTree(tree); // Append 17 tree = "(append a.hex 17 \"nnn\")"; checkTree(tree); } @Test public void testFun() { // Compute 3*3; single variable defined in function body String tree = "({var1 . (* var1 var1)} 3)"; checkTree(tree); // Unknown var2 tree = "({var1 . (* var1 var2)} 3)"; checkTree(tree, "Name lookup of 'var2' failed"); // Compute 3* a.hex[0,0] tree = "({var1 . (* var1 (rows a.hex [0]))} 3)"; checkTree(tree); // Some more horrible functions. Drop the passed function and return a 3 tree = "({fun . 3} {y . (* y y)})"; checkTree(tree); // Apply a 3 to the passed function tree = "({fun . (fun 3)} {y . (* y y)})"; checkTree(tree); // Pass the squaring function thru the ID function tree = "({fun . fun} {y . (* y y)})"; checkTree(tree); // Pass the squaring function thru the twice-apply-3 function tree = "({fun . (fun (fun 3))} {y . (* y y)})"; checkTree(tree); // Pass the squaring function thru the twice-apply-x function tree = "({fun x . (fun (fun x))} {y . (* y y)} 3)"; checkTree(tree); // Pass the squaring function thru the twice-apply function tree = " ({fun . {x . (fun (fun x))}} {y . (* y y)}) "; checkTree(tree); // Pass the squaring function thru the twice-apply function, and apply it tree = "(({fun . {x . (fun (fun x))}} {y . (* y y)}) 3)"; checkTree(tree); } @Test public void testCBind() { String tree = "(cbind 1 2)"; checkTree(tree); tree = "(cbind 1 a.hex 2)"; checkTree(tree); tree = "(cbind a.hex (cols a.hex 0) 2)"; checkTree(tree); } @Test public void testRBind() { String tree = "(rbind 1 2)"; checkTree(tree); //tree = "(rbind a.hex 1 2)"; //checkTree(tree); } @Test public void testApply() { // Sum, reduction. 1 row result String tree = "(apply a.hex 2 {x . (sum x)})"; checkTree(tree); // Return ID column results. Shared data result. tree = "(apply a.hex 2 {x . x})"; checkTree(tree); // Return column results, new data result. tree = "(apply a.hex 2 abs)"; checkTree(tree); // Return two results tree = "(apply a.hex 2 {x . (rbind (sumNA x) (sum x))})"; checkTree(tree); } @Test public void testRowApply() { String tree = "(apply a.hex 1 sum)"; checkTree(tree); tree = "(apply a.hex 1 max)"; checkTree(tree); tree = "(apply a.hex 1 {x . (sum x)})"; checkTree(tree); tree = "(apply a.hex 1 {x . (sum (* x x))})"; checkTree(tree); // require lookup of 'y' outside the scope of the applied function. // doubles all values. tree = "({y . (apply a.hex 1 {x . (sum (* x y))})} 2)"; checkTree(tree); } @Test public void testMath() { for( String s : new String[] {"abs", "cos", "sin", "acos", "ceiling", "floor", "cosh", "exp", "log", "sqrt", "tan", "tanh"} ) checkTree("("+s+" a.hex)"); } @Test public void testVariance() { // Checking variance: scalar String tree = "({x . (var x x \"everything\" FALSE)} (rows a.hex [0]))"; checkTree(tree); tree = "({x . (var x x \"everything\" FALSE)} a.hex)"; checkTree(tree); tree = "(table (trunc (cols a.hex 1)) FALSE)"; checkTree(tree); tree = "(table (cols a.hex 1) FALSE)"; checkTree(tree); tree = "(table (cols a.hex 1) (cols a.hex 2) FALSE)"; checkTree(tree); } private void checkTree(String tree) { checkTree(tree, null); } private void checkTree(String tree, String thrownMessage) { //Frame r = frame(new double[][]{{-1},{1},{2},{3},{4},{5},{6},{254}}); //Key ahex = Key.make("a.hex"); //Frame fr = new Frame(ahex, null, new Vec[]{r.remove(0)}); //r.delete(); //DKV.put(ahex, fr); Frame fr = parse_test_file(Key.make("a.hex"),"smalldata/iris/iris_wheader.csv"); fr.remove(4).remove(); try { Val val = Rapids.exec(tree); Assert.assertNull(thrownMessage); System.out.println(val.toString()); if (val instanceof ValFrame) { Frame fr2 = val.getFrame(); System.out.println(fr2.vec(0)); fr2.remove(); } } catch( IllegalArgumentException iae ) { if (thrownMessage != null) { Assert.assertEquals(thrownMessage, iae.getMessage()); Log.debug("Expected Exception suppressed", iae); } else throw iae; } finally { fr.delete(); } } @Test public void testProstate_assign_frame_scalar() { Frame fr = parse_test_file(Key.make("prostate.hex"), "smalldata/logreg/prostate.csv"); try { Val val = Rapids.exec("(tmp= py_1 (:= prostate.hex -1 1 (== (cols_py prostate.hex 1) 0)))"); if (val instanceof ValFrame ) { Frame fr2 = val.getFrame(); System.out.println(fr2.vec(0)); fr2.remove(); } } finally { fr.delete(); } } @Test public void testCombo() { Frame fr = parse_test_file(Key.make("a.hex"),"smalldata/iris/iris_wheader.csv"); String tree = "(tmp= py_2 (:= (tmp= py_1 (cbind a.hex (== (cols_py a.hex 4.0 ) \"Iris-setosa\" ) ) ) (as.factor (cols_py py_1 5.0 ) ) 5.0 [] ) )"; //String tree = "(:= (tmp= py_1 a.hex) (h2o.runif a.hex -1) 4 [])"; Val val = Rapids.exec(tree); if (val instanceof ValFrame ) { Frame fr2 = val.getFrame(); System.out.println(fr2.vec(0)); fr2.remove(); } fr.delete(); } @Test public void testMerge() { Frame l=null,r=null,f=null; try { l = ArrayUtils.frame("name" ,vec(ar("Cliff","Arno","Tomas","Spencer"),ari(0,1,2,3))); l. add("age" ,vec(ar(">dirt" ,"middle","middle","young'n"),ari(0,1,2,3))); l = new Frame(l); DKV.put(l); System.out.println(l); r = ArrayUtils.frame("name" ,vec(ar("Arno","Tomas","Michael","Cliff"),ari(0,1,2,3))); r. add("skill",vec(ar("science","linearmath","sparkling","hacker"),ari(0,1,2,3))); r = new Frame(r); DKV.put(r); System.out.println(r); String x = String.format("(merge %s %s 1 0 [] [] \"auto\")",l._key,r._key); Val res = Rapids.exec(x); f = res.getFrame(); System.out.println(f); Vec names = f.vec(0); Assert.assertEquals(names.factor(names.at8(0)),"Cliff"); Vec ages = f.vec(1); Assert.assertEquals(ages .factor(ages .at8(0)),">dirt"); Vec skilz = f.vec(2); Assert.assertEquals(skilz.factor(skilz.at8(0)),"hacker"); } finally { if( f != null ) f.delete(); if( r != null ) r.delete(); if( l != null ) l.delete(); } } @Test public void testQuantile() { Frame f = null; try { Frame fr = ArrayUtils.frame(ard(ard(1.223292e-02), ard(1.635312e-25), ard(1.601522e-11), ard(8.452298e-10), ard(2.643733e-10), ard(2.671520e-06), ard(1.165381e-06), ard(7.193265e-10), ard(3.383532e-04), ard(2.561221e-05))); double[] probs = new double[]{0.001, 0.005, .01, .02, .05, .10, .50, .8883, .90, .99}; String x = String.format("(quantile %s %s \"interpolate\" _)", fr._key, Arrays.toString(probs)); Val val = Rapids.exec(x); fr.delete(); f = val.getFrame(); Assert.assertEquals(2,f.numCols()); // Expected values computed as golden values from R's quantile call double[] exp = ard(1.4413698000016206E-13, 7.206849000001562E-13, 1.4413698000001489E-12, 2.882739600000134E-12, 7.20684900000009E-12, 1.4413698000000017E-11, 5.831131148999999E-07, 3.3669567275300000E-04, 0.00152780988 , 0.011162408988 ); for( int i=0; i<exp.length; i++ ) Assert.assertTrue( "expected "+exp[i]+" got "+f.vec(1).at(i), water.util.MathUtils.compare(exp[i],f.vec(1).at(i),1e-6,1e-6) ); } finally { if( f != null ) f.delete(); } } static void exec_str( String str, Session ses ) { Val val = Rapids.exec(str,ses); switch( val.type() ) { case Val.FRM: Frame fr = val.getFrame(); System.out.println(fr); checkSaneFrame(); break; case Val.NUM: System.out.println("num= "+val.getNum()); checkSaneFrame(); break; case Val.STR: System.out.println("str= "+val.getStr()); checkSaneFrame(); break; default: throw water.H2O.fail(); } } static void checkSaneFrame() { assert checkSaneFrame_impl(); } static boolean checkSaneFrame_impl() { for( Key k : H2O.localKeySet() ) { Value val = Value.STORE_get(k); if( val != null && val.isFrame() ) { Frame fr = val.get(); Vec vecs[] = fr.vecs(); for( int i=0; i<vecs.length; i++ ) { Vec v = vecs[i]; if( DKV.get(v._key) == null ) { System.err.println("Frame "+fr._key+" in the DKV, is missing Vec "+v._key+", name="+fr._names[i]); return false; } } } } return true; } @Test public void testRowSlice() { Session ses = new Session(); Frame fr = null; try { fr = parse_test_file(Key.make("a.hex"),"smalldata/airlines/AirlinesTrainMM.csv.zip"); System.out.printf(fr.toString()); Rapids.exec("(h2o.runif a.hex -1)->flow_1",ses); Rapids.exec("(tmp= f.25 (rows a.hex (< flow_1 0.25) ) )",ses); Rapids.exec("(rows a.hex (>= flow_1 0.25) )->f.75",ses); Rapids.exec("(h2o.runif a.hex -1)->flow_2",ses); ses.end(null); } catch( Throwable ex ) { throw ses.endQuietly(ex); } finally { if (fr != null) fr.delete(); } } @Test public void testChicago() { String oldtz = Rapids.exec("(getTimeZone)").getStr(); Session ses = new Session(); try { parse_test_file(Key.make("weather.hex"),"smalldata/chicago/chicagoAllWeather.csv"); parse_test_file(Key.make( "crimes.hex"),"smalldata/chicago/chicagoCrimes10k.csv.zip"); String fname = "smalldata/chicago/chicagoCensus.csv"; File f = FileUtils.locateFile(fname); assert f != null && f.exists():" file not found: " + fname; NFSFileVec nfs = NFSFileVec.make(f); ParseSetup ps = ParseSetup.guessSetup(new Key[]{nfs._key}, false, 1); ps.getColumnTypes()[1] = Vec.T_CAT; ParseDataset.parse(Key.make( "census.hex"), new Key[]{nfs._key}, true, ps); exec_str("(assign census.hex (colnames= census.hex\t[0 1 2 3 4 5 6 7 8] \n" + "['Community.Area.Number' 'COMMUNITY.AREA.NAME' \"PERCENT.OF.HOUSING.CROWDED\" \r\n" + " \"PERCENT.HOUSEHOLDS.BELOW.POVERTY\" \"PERCENT.AGED.16..UNEMPLOYED\" " + " \"PERCENT.AGED.25..WITHOUT.HIGH.SCHOOL.DIPLOMA\" \"PERCENT.AGED.UNDER.18.OR.OVER.64\" " + " \"PER.CAPITA.INCOME.\" \"HARDSHIP.INDEX\"]))", ses); exec_str("(assign crimes.hex (colnames= crimes.hex [0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21] [\"ID\" \"Case.Number\" \"Date\" \"Block\" \"IUCR\" \"Primary.Type\" \"Description\" \"Location.Description\" \"Arrest\" \"Domestic\" \"Beat\" \"District\" \"Ward\" \"Community.Area\" \"FBI.Code\" \"X.Coordinate\" \"Y.Coordinate\" \"Year\" \"Updated.On\" \"Latitude\" \"Longitude\" \"Location\"]))", ses); exec_str("(setTimeZone \"Etc/UTC\")", ses); exec_str("(assign crimes.hex (append crimes.hex (tmp= unary_op_6 (day (tmp= nary_op_5 (cols crimes.hex [2])))) \"Day\"))", ses); checkSaneFrame(); exec_str("(assign crimes.hex (append crimes.hex (tmp= binary_op_31 (+ (tmp= unary_op_7 (month nary_op_5)) 1)) \"Month\"))", ses); exec_str("(rm nary_op_30)",ses); exec_str("(assign crimes.hex (append crimes.hex (tmp= binary_op_32 (+ (tmp= binary_op_9 (- (tmp= unary_op_8 (year nary_op_5)) 1900)) 1900)) \"Year\"))", ses); exec_str("(assign crimes.hex (append crimes.hex (tmp= unary_op_10 (week nary_op_5)) \"WeekNum\"))", ses); exec_str("(rm binary_op_32)",ses); exec_str("(rm binary_op_31)",ses); exec_str("(rm unary_op_8)",ses); checkSaneFrame(); exec_str("(assign crimes.hex (append crimes.hex (tmp= unary_op_11 (dayOfWeek nary_op_5)) \"WeekDay\"))", ses); exec_str("(rm 'nfs:\\\\C:\\\\Users\\\\cliffc\\\\Desktop\\\\h2o-3\\\\smalldata\\\\chicago\\\\chicagoCrimes10k.csv.zip')", ses); exec_str("(assign crimes.hex (append crimes.hex (tmp= unary_op_12 (hour nary_op_5)) \"HourOfDay\"))", ses); exec_str("(assign crimes.hex (append crimes.hex (tmp= nary_op_16 (ifelse (tmp= binary_op_15 (| (tmp= binary_op_13 (== unary_op_11 \"Sun\")) (tmp= binary_op_14 (== unary_op_11 \"Sat\")))) 1 0)) \"Weekend\"))", ses); // Season is incorrectly assigned in the original chicago demo; picks up the Weekend flag exec_str("(assign crimes.hex (append crimes.hex nary_op_16 \"Season\"))", ses); // Standard "head of 10 rows" pattern for printing exec_str("(tmp= subset_33 (rows crimes.hex [0:10]))", ses); exec_str("(rm subset_33)",ses); exec_str("(rm subset_33)",ses); exec_str("(rm unary_op_29)",ses); exec_str("(rm nary_op_28)",ses); exec_str("(rm nary_op_27)",ses); exec_str("(rm nary_op_26)",ses); exec_str("(rm binary_op_25)",ses); exec_str("(rm binary_op_24)",ses); exec_str("(rm binary_op_23)",ses); exec_str("(rm binary_op_22)",ses); exec_str("(rm binary_op_21)",ses); exec_str("(rm binary_op_20)",ses); exec_str("(rm binary_op_19)",ses); exec_str("(rm binary_op_18)",ses); exec_str("(rm binary_op_17)",ses); exec_str("(rm nary_op_16)",ses); exec_str("(rm binary_op_15)",ses); exec_str("(rm binary_op_14)",ses); exec_str("(rm binary_op_13)",ses); exec_str("(rm unary_op_12)",ses); exec_str("(rm unary_op_11)",ses); exec_str("(rm unary_op_10)",ses); exec_str("(rm binary_op_9)",ses); exec_str("(rm unary_op_8)",ses); exec_str("(rm unary_op_7)",ses); exec_str("(rm unary_op_6)",ses); exec_str("(rm nary_op_5)",ses); checkSaneFrame(); // Standard "head of 10 rows" pattern for printing exec_str("(tmp= subset_34 (rows crimes.hex [0:10]))", ses); exec_str("(rm subset_34)",ses); exec_str("(assign census.hex (colnames= census.hex [0 1 2 3 4 5 6 7 8] [\"Community.Area\" \"COMMUNITY.AREA.NAME\" \"PERCENT.OF.HOUSING.CROWDED\" \"PERCENT.HOUSEHOLDS.BELOW.POVERTY\" \"PERCENT.AGED.16..UNEMPLOYED\" \"PERCENT.AGED.25..WITHOUT.HIGH.SCHOOL.DIPLOMA\" \"PERCENT.AGED.UNDER.18.OR.OVER.64\" \"PER.CAPITA.INCOME.\" \"HARDSHIP.INDEX\"]))", ses); exec_str("(rm subset_34)",ses); exec_str("(tmp= subset_35 (cols crimes.hex [-3]))", ses); exec_str("(tmp= subset_36 (cols weather.hex [-1]))", ses); exec_str("(tmp= subset_36_2 (colnames= subset_36 [0 1 2 3 4 5] [\"Month\" \"Day\" \"Year\" \"maxTemp\" \"meanTemp\" \"minTemp\"]))", ses); exec_str("(rm crimes.hex)",ses); exec_str("(rm weather.hex)",ses); // nary_op_37 = merge( X Y ); Vecs in X & nary_op_37 shared exec_str("(tmp= nary_op_37 (merge subset_35 census.hex TRUE FALSE [] [] \"auto\"))", ses); // nary_op_38 = merge( nary_op_37 subset_36_2); Vecs in nary_op_38 and nary_pop_37 and X shared exec_str("(tmp= subset_41 (rows (tmp= nary_op_38 (merge nary_op_37 subset_36_2 TRUE FALSE [] [] \"auto\")) (tmp= binary_op_40 (<= (tmp= nary_op_39 (h2o.runif nary_op_38 30792152736.5179)) 0.8))))", ses); // Standard "head of 10 rows" pattern for printing exec_str("(tmp= subset_44 (rows subset_41 [0:10]))", ses); exec_str("(rm subset_44)",ses); exec_str("(rm subset_44)",ses); exec_str("(rm binary_op_40)",ses); exec_str("(rm nary_op_37)",ses); exec_str("(tmp= subset_43 (rows nary_op_38 (tmp= binary_op_42 (> nary_op_39 0.8))))", ses); // Chicago demo continues on past, but this is all I've captured for now checkSaneFrame(); ses.end(null); } catch( Throwable ex ) { throw ses.endQuietly(ex); } finally { Rapids.exec("(setTimeZone \""+oldtz+"\")"); // Restore time zone (which is global, and will affect following tests) for( String s : new String[]{"weather.hex","crimes.hex","census.hex", "nary_op_5", "unary_op_6", "unary_op_7", "unary_op_8", "binary_op_9", "unary_op_10", "unary_op_11", "unary_op_12", "binary_op_13", "binary_op_14", "binary_op_15", "nary_op_16", "binary_op_17", "binary_op_18", "binary_op_19", "binary_op_20", "binary_op_21", "binary_op_22", "binary_op_23", "binary_op_24", "binary_op_25", "nary_op_26", "nary_op_27", "nary_op_28", "unary_op_29", "binary_op_30", "binary_op_31", "binary_op_32", "subset_33", "subset_34", "subset_35", "subset_36", "subset_36_2", "nary_op_37", "nary_op_38", "nary_op_39", "binary_op_40", "subset_41", "binary_op_42", "subset_43", "subset_44", } ) Keyed.remove(Key.make(s)); } } private static void astNumList_ok(String expr, double[] expected) { AstRoot res = Rapids.parse(expr); assertTrue(res instanceof AstNumList); if (expected != null) assertArrayEquals(expected, ((AstNumList)res).expand(), 1e-10); } private static void astStr_ok(String expr, String expected) { AstRoot res = Rapids.parse(expr); assertTrue(res instanceof AstStr); assertEquals(expected, ((AstStr)res).getStr()); } private static void parse_err(String expr) { try { Rapids.parse(expr); fail("Expression " + expr + " expected to fail, however it did not."); } catch (IllegalASTException ignored) {} } }