// // @(#)TestParser.java 3/2003 // // Copyright 2003 Zachary DelProposto. All rights reserved. // Use is subject to license terms. // // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. // Or from http://www.gnu.org/ // package dip.misc; import dip.order.*; import dip.order.result.*; import dip.world.*; import dip.world.Phase.*; import dip.misc.*; import dip.world.variant.VariantManager; import dip.world.variant.data.*; import dip.process.*; import java.util.*; import java.io.*; import java.lang.reflect.*; import jcmdline.*; /** * A test harness that allows testing of the Order Parser (OrderParser.java). * <p> * <pre> * VARIANT: xxxxxxx * SETUP * ENDSETUP * ORD: * RES (or RESULT): * * Remove(Power power, Location src, Unit.Type srcUnit) * Build(Power power, Location src, Unit.Type srcUnit) * * Disband(Power power, Location src, Unit.Type srcUnit) * Retreat(Power power, Location src, Unit.Type srcUnitType, Location dest) * * Hold(Power power, Location src, Unit.Type srcUnit) * * Move(Power power, Location src, Unit.Type srcUnitType, Location dest, boolean isConvoying) * * Convoy(Power power, Location src, Unit.Type srcUnit, Location convoySrc, Unit.Type convoyUnitType, Location convoyDest) * * Support(Power power, Location src, Unit.Type srcUnit, Location supSrc, Unit.Type supUnit, Location supDest) * * Waive(Power power, Location src) * * </pre> */ public class TestParser { // instance fields String variantName; World world = null; TurnState turnState = null; // the first & only TurnState in the World object dip.world.Map map = null; List cases = null; // a List of ORPairs OrderParser op = null; boolean isLogging = false; // OrderParser internal logging enabled ValidationOptions valOpts = null; // validation options // constants private static final String VARIANT_DIR = "variants"; private static final String KEY_VARIANT = "variant"; private static final String KEY_SETUP = "setup"; private static final String KEY_SETUPDISLODGED = "setupdislodged"; private static final String KEY_END = "end"; private static final String KEY_ORDER = "order"; private static final String KEY_RESULT = "result"; private static final String KEY_COMMENT = "comment"; // note: order is important; e.g., 'setupdislodged' before 'setup' private static final String KEYWORDS[][] = { {"variant", KEY_VARIANT}, {"setupdislodged", KEY_SETUPDISLODGED}, {"setup", KEY_SETUP}, {"end", KEY_END}, {"order", KEY_ORDER}, {"ord", KEY_ORDER}, {"result", KEY_RESULT}, {"res", KEY_RESULT}, {"#", KEY_COMMENT} }; // argument type specifiers private static final String T_POWER = "power"; private static final String T_LOCATION = "location"; private static final String T_UTYPE = "unittype"; private static final String T_BOOLEAN = "boolean"; // order types and their acceptable arguments. private static final String[][] ORDER_ARGS = { // really simple types (2 args) {"waive", T_POWER, T_LOCATION}, // simple types (3 args) {"hold", T_POWER, T_LOCATION, T_UTYPE}, {"disband", T_POWER, T_LOCATION, T_UTYPE}, {"build", T_POWER, T_LOCATION, T_UTYPE}, {"remove", T_POWER, T_LOCATION, T_UTYPE}, // complex types {"move", T_POWER, T_LOCATION, T_UTYPE, T_LOCATION, T_BOOLEAN}, {"retreat", T_POWER, T_LOCATION, T_UTYPE, T_LOCATION}, {"convoy", T_POWER, T_LOCATION, T_UTYPE, T_LOCATION, T_UTYPE, T_LOCATION}, {"support", T_POWER, T_LOCATION, T_UTYPE, T_LOCATION, T_UTYPE, T_LOCATION}, }; // 3 main phase types for proper validation private static final Phase PHASE_MOVE = new Phase(SeasonType.FALL, 1900, PhaseType.MOVEMENT); private static final Phase PHASE_ADJUSTMENT = new Phase(SeasonType.FALL, 1900, PhaseType.ADJUSTMENT); private static final Phase PHASE_RETREAT = new Phase(SeasonType.FALL, 1900, PhaseType.RETREAT); /** Start the parser. */ public static void main(String args[]) { // input file specifier FileParam argInputFile = new FileParam("input", "the input file of test-case definitions", FileParam.IS_FILE & FileParam.IS_READABLE & FileParam.EXISTS, FileParam.REQUIRED, FileParam.SINGLE_VALUED); // are we logging? or not. BooleanParam logOpt = new BooleanParam("log", "log OrderParser internal processing"); // verbose help text String helpText = " "; // main command line handler CmdLineHandler cl = new VersionCmdLineHandler( "TestParser 1.0", new HelpCmdLineHandler(helpText, "TestParser", "Test harness for testing order parsing", // options new Parameter[] {argInputFile, logOpt}, // arguments [left on command line] new Parameter[] {} ) ); // parse command line cl.parse(args); // Start the parser new TestParser(argInputFile.getFile(), logOpt.isTrue()); }// main() /** Creates a TestParser using the given input file. */ private TestParser(File input, boolean isLogging) { this.isLogging = isLogging; System.out.println("TestParser started on: "+new Date()); //Log.setLogging(isLogging); parseCaseFile(input); Log.setLogging(isLogging); runTest(); }// TestParser() /** Test each case and keep stats */ private void runTest() { // list of failed case descriptions. int numprocessed = 0; List failedCases = new LinkedList(); // set the validation options; lenient -- we only care about syntax! valOpts = new ValidationOptions(); valOpts.setOption(ValidationOptions.KEY_GLOBAL_PARSING, ValidationOptions.VALUE_GLOBAL_PARSING_LOOSE); Iterator iter = cases.iterator(); while(iter.hasNext()) { ORPair orp = (ORPair) iter.next(); // determine if case is marked with a "FAIL" result line. boolean isMarkedFail = false; String res = orp.getResult().trim().toLowerCase(); if(res.startsWith("fail")) { isMarkedFail = true; } // First, attempt to parse the order. Guessing is allowed. try { numprocessed++; Order o = op.parse(OrderFactory.getDefault(), orp.getOrder(), null, turnState, false, true); // validate order: note that we need to set the phase appropriately // first if(o instanceof Build || o instanceof Remove) { turnState.setPhase(PHASE_ADJUSTMENT); } else if(o instanceof Retreat || o instanceof Disband) { turnState.setPhase(PHASE_RETREAT); } else { turnState.setPhase(PHASE_MOVE); } // now, do the validation o.validate(turnState, valOpts, world.getRuleOptions()); // if marked as fail, and we succeed, it's a failure! if(isMarkedFail) { StringBuffer sb = new StringBuffer(128); sb.append("Order line "); sb.append(String.valueOf(orp.getLineNumber())); sb.append("\""); sb.append(orp.getOrder()); sb.append("\""); sb.append(" succeeded, but should have failed."); failedCases.add(sb.toString()); } else { // check order normally checkORP(orp, o, failedCases); } } catch(OrderException e) { // only count as a failure if RESULT line does NOT have a "FAIL" result. if(!isMarkedFail) { StringBuffer sb = new StringBuffer(128); sb.append("Order line "); sb.append(String.valueOf(orp.getLineNumber())); sb.append(" \""); sb.append(orp.getOrder()); sb.append("\""); sb.append(" failed: "); sb.append(e); failedCases.add(sb.toString()); } } } // print stats System.out.println(numprocessed+" order(s) parsed"); System.out.println(failedCases.size()+" order(s) failed"); if(failedCases.size() > 0) { System.out.println("\nFailed orders, and failure reasons follow:"); iter = failedCases.iterator(); while(iter.hasNext()) { System.out.println(iter.next()); System.out.println(""); } } System.out.println("TestParser completed on "+new Date()); // clean or error exit if(failedCases.size() > 0) { System.exit(1); } else { System.exit(0); } }// runTest() /** * * Check the order against what actually should be there. * Originally, reflection was to be used but this is much easier, * though less flexible. */ private void checkORP(ORPair orp, Order o, List failedCases) { String[] toks = getORPTokens(orp.getResult()); if(toks.length == 0) { System.out.println("ERROR: in result of order pair starting at line: "+orp.getLineNumber()); System.out.println("No result type was given."); System.exit(1); } // find order. String[] params = null; for(int i=0; i<ORDER_ARGS.length; i++) { if(toks[0].equalsIgnoreCase(ORDER_ARGS[i][0])) { if(toks.length != ORDER_ARGS[i].length) { System.out.println("ERROR: in result of order pair starting at line: "+orp.getLineNumber()); System.out.println("Invalid number of arguments; "+(ORDER_ARGS[i].length-1)+" are required."); System.exit(1); } else { params = ORDER_ARGS[i]; } } } // order not found. if(params == null) { System.out.println("ERROR: in result of order pair starting at line: "+orp.getLineNumber()); System.out.println("Order type \""+toks[0]+"\" not found."); System.exit(1); } // name String name = toks[0].toLowerCase(); // validate name if(!name.equalsIgnoreCase(o.getFullName())) { StringBuffer sb = new StringBuffer(128); sb.append("Order line "); sb.append(String.valueOf(orp.getLineNumber())); sb.append(" \""); sb.append(orp.getOrder()); sb.append("\""); sb.append(" failed; a "); sb.append(o.getFullName()); sb.append(" was parsed but a "); sb.append(name); sb.append(" order was expected."); failedCases.add(sb.toString()); return; } // exception: Waive has 2 parameters; valBasicParam expects 3. if(name.equals("waive")) { // validate power, and validate location. if(!valPower(orp, o.getPower(), toks[1], failedCases)) { return; } if(!valLocation(orp, o.getSource(), toks[2], failedCases)) { } // we are OK return; } // validate basic params -- same for all orders if(!valBasicParams(orp, o, toks, failedCases)) { return; } // validate extended parameters if(name.equals("move")) { if(!valLocation(orp, (((Move) o).getDest()), toks[4], failedCases)) { return; } if(!valBoolean(orp, (((Move) o).isConvoying()), toks[5], failedCases)) { return; } } else if(name.equals("retreat")) { if(!valLocation(orp, (((Retreat) o).getDest()), toks[4], failedCases)) { return; } } else if(name.equals("support")) { if(!valLocation(orp, (((Support) o).getSupportedSrc()), toks[4], failedCases)) { return; } if(!valUnitType(orp, (((Support) o).getSupportedUnitType()), toks[5], failedCases)) { return; } if(!valLocation(orp, (((Support) o).getSupportedDest()), toks[6], failedCases)) { return; } } else if(name.equals("convoy")) { if(!valLocation(orp, (((Convoy) o).getConvoySrc()), toks[4], failedCases)) { return; } if(!valUnitType(orp, (((Convoy) o).getConvoyUnitType()), toks[5], failedCases)) { return; } if(!valLocation(orp, (((Convoy) o).getConvoyDest()), toks[6], failedCases)) { return; } } }// checkORP() /** Validate basic params -- for all orders; always 3 params */ private boolean valBasicParams(ORPair orp, Order o, String[] toks, List failedCases) { boolean isOK = valPower(orp, o.getPower(), toks[1], failedCases); if(isOK) { isOK = valLocation(orp, o.getSource(), toks[2], failedCases); } if(isOK) { isOK = valUnitType(orp, o.getSourceUnitType(), toks[3], failedCases); } return isOK; }// valBasicParams() /** Validate a Power */ private boolean valPower(ORPair orp, Power thePower, String tok, List failedCases) { // is tok a valid Power name? if not, error-exit Power power = map.getPower(tok); if(power == null) { System.out.println("ERROR: in result of order pair starting at line: "+orp.getLineNumber()); System.out.println("Power \""+tok+"\" not found."); System.exit(1); } // does tok match? if not, add to failed cases, return false if(power != thePower) { StringBuffer sb = new StringBuffer(128); sb.append("Order line "); sb.append(String.valueOf(orp.getLineNumber())); sb.append(" \""); sb.append(orp.getOrder()); sb.append("\""); sb.append(" failed; the powers do not match. "); failedCases.add(sb.toString()); return false; } return true; }// valPower() /** Validate a Location */ private boolean valLocation(ORPair orp, Location theLoc, String tok, List failedCases) { // is tok a valid Power name? if not, error-exit Location loc = map.parseLocation(tok); if(loc == null) { System.out.println("ERROR: in result of order pair starting at line: "+orp.getLineNumber()); System.out.println("Location \""+tok+"\" not found."); System.exit(1); } // does tok match? if not, add to failed cases, return false // cannot use identity-equals here if(!loc.equals(theLoc)) { StringBuffer sb = new StringBuffer(128); sb.append("Order line "); sb.append(String.valueOf(orp.getLineNumber())); sb.append(" \""); sb.append(orp.getOrder()); sb.append("\""); sb.append(" failed; the location "); sb.append("\""); sb.append(loc); sb.append("\""); sb.append(" does not match "); sb.append("\""); sb.append(theLoc); sb.append("\""); failedCases.add(sb.toString()); return false; } return true; }// valPower() /** Validate a Unit Type */ private boolean valUnitType(ORPair orp, Unit.Type theUnitType, String tok, List failedCases) { // is tok a valid Power name? if not, error-exit Unit.Type ut = Unit.Type.parse(tok); if(ut == null || ut == Unit.Type.UNDEFINED) { System.out.println("ERROR: in result of order pair starting at line: "+orp.getLineNumber()); System.out.println("Unit Type \""+tok+"\" was not found."); System.exit(1); } // does tok match? if not, add to failed cases, return false if(ut != theUnitType) { StringBuffer sb = new StringBuffer(128); sb.append("Order line "); sb.append(String.valueOf(orp.getLineNumber())); sb.append(" \""); sb.append(orp.getOrder()); sb.append("\""); sb.append(" failed; the Unit Type "); sb.append("\""); sb.append(ut); sb.append("\""); sb.append(" does not match "); sb.append("\""); sb.append(theUnitType); sb.append("\""); failedCases.add(sb.toString()); return false; } return true; }// valPower() /** Validate a Boolean */ private boolean valBoolean(ORPair orp, boolean theBoolean, String tok, List failedCases) { // is tok a valid Power name? if not, error-exit boolean bool = false; if(tok.equalsIgnoreCase("true")) { bool = true; } else if(tok.equalsIgnoreCase("false")) { bool = false; } else { System.out.println("ERROR: in result of order pair starting at line: "+orp.getLineNumber()); System.out.println("Boolean value \""+tok+"\" must be \"true\" or \"false\"."); System.exit(1); } // does tok match? if not, add to failed cases, return false if(bool != theBoolean) { StringBuffer sb = new StringBuffer(128); sb.append("Order line "); sb.append(String.valueOf(orp.getLineNumber())); sb.append(" \""); sb.append(orp.getOrder()); sb.append("\""); sb.append(" failed; the value "); sb.append("\""); sb.append(bool); sb.append("\""); sb.append(" does not match "); sb.append("\""); sb.append(theBoolean); sb.append("\""); failedCases.add(sb.toString()); return false; } return true; }// valPower() /** Get tokens from an order result (non-failure) as a string array */ private String[] getORPTokens(String in) { ArrayList al = new ArrayList(10); // parse result. // format is like xxxx(a, b, c, d) StringTokenizer st = new StringTokenizer(in, "(),;"); while(st.hasMoreTokens()) { al.add(st.nextToken().trim()); } return (String[]) al.toArray(new String[al.size()]); }// getORPTokens() /** Setup the variant, using variantName, which shouldn't be null */ private void setupVariant() { assert(variantName != null); // get default variant directory. File defaultVariantSearchDir = null; if(System.getProperty("user.dir") == null) { defaultVariantSearchDir = new File(".", VARIANT_DIR); } else { defaultVariantSearchDir = new File(System.getProperty("user.dir"), VARIANT_DIR ); } try { // parse variants VariantManager.init(new File[]{defaultVariantSearchDir}, false); // load the default variant (Standard) // error if it cannot be found!! Variant variant = VariantManager.getVariant(variantName, VariantManager.VERSION_NEWEST); if(variant == null) { System.out.println("ERROR: cannot find variant: "+variantName); System.exit(1); } // create the world world = WorldFactory.getInstance().createWorld(variant); turnState = world.getLastTurnState(); map = world.getMap(); // set the RuleOptions in the World (this is normally done // by the GUI) world.setRuleOptions(RuleOptions.createFromVariant(variant)); } catch(Exception e) { System.out.println("ERROR: could not create variant."); System.out.println(e); e.printStackTrace(); System.exit(1); } // clear positions in this world Position pos = turnState.getPosition(); Province[] provs = pos.getProvinces(); for(int i=0; i<provs.length; i++) { pos.setUnit(provs[i], null); pos.setDislodgedUnit(provs[i], null); } System.out.println("Variant \""+variantName+"\" loaded successfully."); // setup the order parser op = OrderParser.getInstance(); System.out.println("OrderParser created."); }// setupVariant() /** * A bunch of DefineState orders used to set unit positions * for subsequent order processing. */ private void setupPositions(List nonDislodged, List dislodged) { assert(nonDislodged != null); assert(dislodged != null); assert(turnState != null); Position pos = turnState.getPosition(); int count = 0; Iterator iter = nonDislodged.iterator(); while(iter.hasNext()) { String line = (String) iter.next(); DefineState ds = parseDSOrder(line.trim()); Unit unit = new Unit(ds.getPower(), ds.getSourceUnitType()); unit.setCoast(ds.getSource().getCoast()); pos.setUnit(ds.getSource().getProvince(), unit); count++; } System.out.println(count+" (non-dislodged) unit positions set."); count = 0; iter = dislodged.iterator(); while(iter.hasNext()) { String line = (String) iter.next(); DefineState ds = parseDSOrder(line.trim()); Unit unit = new Unit(ds.getPower(), ds.getSourceUnitType()); unit.setCoast(ds.getSource().getCoast()); pos.setDislodgedUnit(ds.getSource().getProvince(), unit); count++; } System.out.println(count+" (dislodged) unit positions set."); }// setupPositions() /** Parse the Case file. */ private void parseCaseFile(File caseFile) { LineNumberReader lnr = null; try { boolean setupDone = false; List accum = null; cases = new ArrayList(200); ORPair currentCase = null; List posList = null; List dislodgedPosList = null; lnr = new LineNumberReader(new BufferedReader(new FileReader(caseFile))); String line = lnr.readLine(); while(line != null) { // trim line line = line.trim(); // cutoff any text after first '#' final int cidx = line.indexOf('#'); if(cidx != -1) { line = line.substring(0, cidx).trim(); } // allow blank/empty/only-whitespace lines (but don't parse them) if(line.length() > 0) { // cut off any text after the first '#' String key = getKeyword(line); if(key == KEY_VARIANT) { if(variantName != null) { lnrErrorExit(lnr, "VARIANT already defined."); } variantName = getPostKeywordText(line).trim(); setupVariant(); } else if(key == KEY_SETUP) { if(posList != null) { lnrErrorExit(lnr, "SETUP block already defined."); } accum = new ArrayList(50); } else if(key == KEY_END) { if(accum == null) { lnrErrorExit(lnr, "END line must be after a SETUP line or SETUPDISLODGED line."); } // send accumulated line to setup parser if(posList == null) { posList = accum; } else { dislodgedPosList = accum; } // if we are done, mark the setupDone flag // and setup the positions. if(posList != null && dislodgedPosList != null) { setupPositions(posList, dislodgedPosList); setupDone = true; } } else if(key == KEY_SETUPDISLODGED) { if(posList == null) { lnrErrorExit(lnr, "SETUPDISLODGED must be after a SETUP block."); } if(dislodgedPosList != null) { lnrErrorExit(lnr, "SETUPDISLODGED block already defined."); } accum = new ArrayList(50); } else if(key == KEY_ORDER) { if(!setupDone) { lnrErrorExit(lnr, "ORDER (or ORD) keyword must be after SETUP block complete."); } if(currentCase != null) { lnrErrorExit(lnr, "ORDER (or ORD) keyword must precede and be paired with a RESULT (or RES) line."); } currentCase = new ORPair(); currentCase.setOrder( getPostKeywordText(line) ); currentCase.setLineNumber( lnr.getLineNumber() ); } else if(key == KEY_RESULT) { if(!setupDone) { lnrErrorExit(lnr, "RESULT (or RES) keyword must be after SETUP block complete."); } if(currentCase == null) { lnrErrorExit(lnr, "RESULT (or RES) line must follow and be paired with an ORDER (or ORD) line."); } currentCase.setResult( getPostKeywordText(line) ); cases.add(currentCase); currentCase = null; } else if(key == KEY_COMMENT) { // do nothing } else { // add lines to accumulator if(accum != null) { accum.add(line); } else { // no accumulator? we don't want no non-blank lines with werdz System.out.println("ERROR: line: "+lnr.getLineNumber()); System.out.println("Unknown action or non-comment line."); System.exit(1); } } } line = lnr.readLine(); } } catch(IOException e) { System.out.println("ERROR reading the case file \""+caseFile+"\"."); System.out.println(e); System.exit(1); } finally { if(lnr != null) { try { lnr.close(); } catch(Exception e) {} } } System.out.println("Case file succesfully parsed."); System.out.println(cases.size()+" order/result pairs read."); }// parseCaseFile() /** * Gets if lien starts with keyword (case-insensitive); if not, returns * null; if so, returns normalized keyword * */ private String getKeyword(String line) { String lcLine = line.trim().toLowerCase(); for(int i=0; i<KEYWORDS.length; i++) { if(lcLine.startsWith(KEYWORDS[i][0])) { return KEYWORDS[i][1]; } } return null; }// getKeyword() /** Gets text after a keyword, ignoring comments, "" if no text found */ private static String getPostKeywordText(String line) { line = stripComment(line); int idx = getWSIndex(line); if(idx == -1 || idx+1 > line.length()) { return ""; } return line.substring(idx+1); }// getPostKeywordText() /** Strips the comment after a line (starts with #) if present. */ private static String stripComment(String line) { int idx = line.indexOf('#'); if(idx >= 0) { return line.substring(0, idx); } return line; }// stripComment() /** * Gets first index of a whitespace character, and returns it's * index of -1 if not found */ private static int getWSIndex(String in) { for(int i=0; i<in.length(); i++) { if( Character.isWhitespace(in.charAt(i)) ) { return i; } } return -1; }// getWSIndex() private void lnrErrorExit(LineNumberReader lnr, String msg) { System.out.println("ERROR: line: "+lnr.getLineNumber()); System.out.println("SETUP block already defined."); System.exit(1); }// lnrErrorExit() /** * Parse order as a DefineState order, and if it fails, * error exit. */ private DefineState parseDSOrder(String line) { // parse each line as a DefineState order. // add each unit to the turnState position. try { // no guessing (but not locked); we must ALWAYS specify the power. Order o = op.parse(OrderFactory.getDefault(), line, null, turnState, false, false); if(o instanceof DefineState) { // we just want to check if the DefineState order does not have // an undefined coast for a fleet unit. Location newLoc = o.getSource().getValidatedSetup(o.getSourceUnitType()); // create a new DefineState with a validated loc return OrderFactory.getDefault().createDefineState(o.getPower(), newLoc, o.getSourceUnitType()); } else { throw new OrderException("A DefineState order (e.g., \"England: A Lon\" is required."); } } catch(OrderException e) { System.out.println("ERROR in SETUP or SETUPDISLODGED position."); System.out.println(" line: "+line); System.out.println("parseOrder() OrderException: "+e); System.exit(1); } // shouldn't occur. assert(false); return null; }// parseDSOrder() /** * Order-Result pair class * Pairs an Order with its Result * */ private class ORPair { private String order; private String result; private int line; public ORPair() {} public String getOrder() { return order; } public String getResult() { return result; } public int getLineNumber() { return line; } public void setOrder(String s) { order = s; } public void setResult(String s) { result = s; } public void setLineNumber(int i) { line = i; } } }// class TestParser