]> source.dussan.org Git - aspectj.git/commitdiff
revamped option handling: n-element options, collision/conflict detection/resolution...
authorwisberg <wisberg>
Wed, 29 Oct 2003 08:56:38 +0000 (08:56 +0000)
committerwisberg <wisberg>
Wed, 29 Oct 2003 08:56:38 +0000 (08:56 +0000)
12 files changed:
testing/src/org/aspectj/testing/harness/bridge/AbstractRunSpec.java
testing/src/org/aspectj/testing/harness/bridge/CompilerRun.java
testing/src/org/aspectj/testing/harness/bridge/Globals.java
testing/src/org/aspectj/testing/util/options/Option.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/options/Options.java [new file with mode: 0644]
testing/src/org/aspectj/testing/util/options/Values.java [new file with mode: 0644]
testing/testsrc/TestingModuleTests.java
testing/testsrc/org/aspectj/testing/harness/bridge/CompilerRunSpecTest.java
testing/testsrc/org/aspectj/testing/util/options/OptionChecker.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/util/options/OptionsTest.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/util/options/OptionsTests.java [new file with mode: 0644]
testing/testsrc/org/aspectj/testing/util/options/ValuesTest.java [new file with mode: 0644]

index 8339a25adeb0b9198bb70a35a41db9c7645a7e99..b1e3051e1ad19b284171e3b8d75240e9e87e6e72 100644 (file)
@@ -30,7 +30,10 @@ import org.aspectj.bridge.ISourceLocation;
 import org.aspectj.bridge.MessageHandler;
 import org.aspectj.bridge.MessageUtil;
 import org.aspectj.testing.run.IRunIterator;
+import org.aspectj.testing.util.*;
 import org.aspectj.testing.util.BridgeUtil;
+import org.aspectj.testing.util.options.*;
+import org.aspectj.testing.util.options.Option.InvalidInputException;
 import org.aspectj.testing.xml.IXmlWritable;
 import org.aspectj.testing.xml.SoftMessage;
 import org.aspectj.testing.xml.XMLWriter;
@@ -955,6 +958,66 @@ abstract public class AbstractRunSpec implements IRunSpec { // XXX use MessageHa
             verbose = toCopy.verbose;
         }
         
+        /** 
+         * Return any parent option accepted by validOptions,
+         * optionally removing the parent option.
+         * @param validOptions String[] of options to extract
+         * @param remove if true, then remove any parent option matched
+         * @return String[] containing any validOptions[i] in parentOptions
+         *          
+         */
+        public Values extractOptions(
+            Options validOptions, 
+            boolean remove,
+            StringBuffer errors) {
+            Values result = Values.EMPTY;
+            if (null == errors) {
+                errors = new StringBuffer();
+            }
+            if (null == validOptions) {
+                errors.append("null options");
+                return result;                
+            }
+            if (LangUtil.isEmpty(parentOptions)) {
+                return result;
+            }
+            boolean haveOption = false;
+            String[] parents = (String[]) parentOptions.toArray(new String[0]);
+            try {
+                result = validOptions.acceptInput(parents);
+            } catch (InvalidInputException e) {
+                errors.append(e.getFullMessage());
+                return result;
+            }
+            if (remove) {
+                Option.Value[] values = result.asArray();
+                for (int i = 0; i < values.length; i++) {
+                    Option.Value value = values[i];
+                    if (null == value) {
+                        continue;
+                    }
+                    final int max = i + value.option.numArguments();
+                    if (max > i) {
+                        if (max >= parents.length) {
+                            errors.append("expecting more args for "
+                                + value.option
+                                + " at ["
+                                + i
+                                + "]: "
+                                + Arrays.asList(parents));
+                            return result;
+                        }
+                        // XXX verify
+                        for (int j = i;  j < max ; j++) {
+                            parentOptions.remove(parents[j]);
+                        }
+                        i = max-1;
+                    }
+                }
+            }
+            return result;
+        }
+
         /** 
          * Return any parent option which has one of validOptions as a prefix,
          * optionally absorbing (removing) the parent option.
index 172f0de74738179b1494b0b0dbd60f685ebcb5ac..0f0385ac27cebf5678becbeb3d7184a15e88e259 100644 (file)
@@ -1,5 +1,6 @@
 /* *******************************************************************
- * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC).
+ * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC),
+ *               2003 Contributors.
  * All rights reserved. 
  * This program and the accompanying materials are made available 
  * under the terms of the Common Public License v1.0 
@@ -8,34 +9,22 @@
  *  
  * Contributors: 
  *     Xerox/PARC     initial implementation 
+ *     Wes Isberg     2003 updates
  * ******************************************************************/
 
 package org.aspectj.testing.harness.bridge;
 
-import org.aspectj.bridge.ICommand;
-import org.aspectj.bridge.IMessage;
-import org.aspectj.bridge.IMessageHandler;
-import org.aspectj.bridge.MessageUtil;
-import org.aspectj.bridge.ReflectionFactory;
+import java.io.*;
+import java.util.*;
+
+import org.aspectj.bridge.*;
 import org.aspectj.testing.ajde.CompileCommand;
-import org.aspectj.testing.run.IRunIterator;
-import org.aspectj.testing.run.IRunStatus;
-import org.aspectj.testing.run.WrappedRunIterator;
+import org.aspectj.testing.run.*;
 import org.aspectj.testing.taskdefs.AjcTaskCompileCommand;
-import org.aspectj.testing.xml.SoftMessage;
-import org.aspectj.testing.xml.XMLWriter;
-import org.aspectj.util.FileUtil;
-import org.aspectj.util.LangUtil;
-
-import java.io.File;
-import java.io.FileFilter;
-import java.io.IOException;
-import java.io.PrintStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Iterator;
-import java.util.List;
-import java.util.ListIterator;
+import org.aspectj.testing.util.options.*;
+import org.aspectj.testing.util.options.Option.*;
+import org.aspectj.testing.xml.*;
+import org.aspectj.util.*;
 
 /**
  * Run the compiler once.
@@ -62,40 +51,37 @@ import java.util.ListIterator;
  * </ul>
  */
 public class CompilerRun implements IAjcRun {
-    static final String BUILDER_COMPILER = 
-        "org.aspectj.ajdt.internal.core.builder.Builder.Command";
-    static final String AJDE_COMPILER = CompileCommand.class.getName();
-    static final String AJCTASK_COMPILER 
-        = AjcTaskCompileCommand.class.getName();
-//    static final String JAVAC_COMPILER 
-//        = JavacCompileCommand.class.getName();
-
-       static final String[] RA_String = new String[0];
-    
+    //    static final String JAVAC_COMPILER 
+    //        = JavacCompileCommand.class.getName();
+
+    static final String[] RA_String = new String[0];
+
     static final String[] JAR_SUFFIXES = new String[] { ".jar", ".zip" };
 
-    static final String[] SOURCE_SUFFIXES 
-        (String[]) FileUtil.SOURCE_SUFFIXES.toArray(new String[0]);
-        
+    static final String[] SOURCE_SUFFIXES =
+        (String[]) FileUtil.SOURCE_SUFFIXES.toArray(new String[0]);
+
     /** specifications, set on construction */
     Spec spec;
 
     //------------ calculated during setup
-    /** get shared stuff during setup */       
+    /** get shared stuff during setup */
     Sandbox sandbox;
-    
+
     /** 
      * During run, these String are passed as the source and arg files to compile.
      * The list is set up in setupAjcRun(..), when arg files are prefixed with "@".
      */
-    final List /*String*/ arguments;
-    
+    final List /*String*/
+    arguments;
+
     /** 
      * During run, these String are collapsed and passed as the injar option.
      * The list is set up in setupAjcRun(..).
      */
-    final List /*String*/ injars;
-   
+    final List /*String*/
+    injars;
+
     private CompilerRun(Spec spec) {
         if (null == spec) {
             throw new IllegalArgumentException("null spec");
@@ -103,8 +89,8 @@ public class CompilerRun implements IAjcRun {
         this.spec = spec;
         arguments = new ArrayList();
         injars = new ArrayList();
-    }           
-    
+    }
+
     /** 
      * This checks that the spec is reasonable and does setup:
      * <ul>
@@ -124,72 +110,98 @@ public class CompilerRun implements IAjcRun {
      * for incremental tests.   Note that (as of this writing) the
      * compiler only handles source roots for incremental tests.
      * @param classesDir the File
-        * @see org.aspectj.testing.harness.bridge.AjcTest.IAjcRun#setup(File, File)
+     * @see org.aspectj.testing.harness.bridge.AjcTest.IAjcRun#setup(File, File)
      * @throws AbortException containing IOException or IllegalArgumentException
      *          if the staging operations fail
-        */
-       public boolean setupAjcRun(Sandbox sandbox, Validator validator) {
+     */
+    public boolean setupAjcRun(Sandbox sandbox, Validator validator) {
 
         if (!validator.nullcheck(spec.getOptionsArray(), "localOptions")
             || !validator.nullcheck(sandbox, "sandbox")
             || !validator.nullcheck(spec.compiler, "compilerName")
             || !validator.canRead(Globals.F_aspectjrt_jar, "aspectjrt.jar")
-            || !validator.canRead(Globals.F_testingclient_jar, "testing-client.jar")
-            ) {
+            || !validator.canRead(
+                Globals.F_testingclient_jar,
+                "testing-client.jar")) {
             return false;
         }
-         
+
         this.sandbox = sandbox;
-        
+
         String rdir = spec.testSrcDirOffset;
         File testBaseSrcDir;
         if ((null == rdir) || (0 == rdir.length())) {
             testBaseSrcDir = sandbox.testBaseDir;
         } else {
-            testBaseSrcDir = new File(sandbox.testBaseDir, rdir); // XXX what if rdir is two levels deep?
-            if (!validator.canReadDir(testBaseSrcDir, "sandbox.testBaseSrcDir")) {
+            testBaseSrcDir = new File(sandbox.testBaseDir, rdir);
+            // XXX what if rdir is two levels deep?
+            if (!validator
+                .canReadDir(testBaseSrcDir, "sandbox.testBaseSrcDir")) {
                 return false;
             }
         }
         sandbox.setTestBaseSrcDir(testBaseSrcDir, this);
-        
-        
+
         // Sources come as relative paths - check read, copy if staging.
         // This renders paths absolute before run(RunStatusI) is called.
         // For a compile run to support relative paths + source base,
         // change so the run calculates the paths (differently when staging)
 
-        final String[] injarPaths; 
+        final String[] injarPaths;
         final String[] srcPaths;
-        { 
+        {
             final String[] paths = spec.getPathsArray();
-            srcPaths = LangUtil.endsWith(paths, CompilerRun.SOURCE_SUFFIXES, true);
-            injarPaths = LangUtil.endsWith(paths, CompilerRun.JAR_SUFFIXES, true);
-        } 
+            srcPaths =
+                LangUtil.endsWith(
+                    paths,
+                    CompilerRun.SOURCE_SUFFIXES,
+                    true);
+            injarPaths =
+                LangUtil.endsWith(paths, CompilerRun.JAR_SUFFIXES, true);
+        }
         // validate readable for sources
         if (!spec.badInput) {
             if (!validator.canRead(testBaseSrcDir, srcPaths, "sources")
                 || !validator.canRead(testBaseSrcDir, injarPaths, "injars")
-                || !validator.canRead(testBaseSrcDir, spec.argfiles, "argfiles")
-                || !validator.canRead(testBaseSrcDir, spec.classpath, "classpath")
-                || !validator.canRead(testBaseSrcDir, spec.aspectpath, "aspectpath")
-                || !validator.canRead(testBaseSrcDir, spec.sourceroots, "sourceroots")
-                ) {
+                || !validator.canRead(
+                    testBaseSrcDir,
+                    spec.argfiles,
+                    "argfiles")
+                || !validator.canRead(
+                    testBaseSrcDir,
+                    spec.classpath,
+                    "classpath")
+                || !validator.canRead(
+                    testBaseSrcDir,
+                    spec.aspectpath,
+                    "aspectpath")
+                || !validator.canRead(
+                    testBaseSrcDir,
+                    spec.sourceroots,
+                    "sourceroots")) {
                 return false;
             }
         }
-        
-        int numSources = srcPaths.length + injarPaths.length 
-            + spec.argfiles.length + spec.sourceroots.length;
+
+        int numSources =
+            srcPaths.length
+                + injarPaths.length
+                + spec.argfiles.length
+                + spec.sourceroots.length;
         if (!spec.badInput && (numSources < 1)) {
-            validator.fail("no input jars, arg files, or source files or roots");
-               return false;
-        } 
-        
-        final File[] argFiles = FileUtil.getBaseDirFiles(testBaseSrcDir, spec.argfiles);
-        final File[] injarFiles = FileUtil.getBaseDirFiles(testBaseSrcDir, injarPaths);
-        final File[] aspectFiles = FileUtil.getBaseDirFiles(testBaseSrcDir, spec.aspectpath);
-        final File[] classFiles = FileUtil.getBaseDirFiles(testBaseSrcDir, spec.classpath);
+            validator.fail(
+                "no input jars, arg files, or source files or roots");
+            return false;
+        }
+
+        final File[] argFiles =
+            FileUtil.getBaseDirFiles(testBaseSrcDir, spec.argfiles);
+        final File[] injarFiles =
+            FileUtil.getBaseDirFiles(testBaseSrcDir, injarPaths);
+        final File[] aspectFiles =
+            FileUtil.getBaseDirFiles(testBaseSrcDir, spec.aspectpath);
+        final File[] classFiles =
+            FileUtil.getBaseDirFiles(testBaseSrcDir, spec.classpath);
         // hmm - duplicates validation above, verifying getBaseDirFiles?
         if (!spec.badInput) {
             if (!validator.canRead(argFiles, "argFiles")
@@ -203,34 +215,53 @@ public class CompilerRun implements IAjcRun {
         final File[] srcFiles;
         File[] sourcerootFiles = new File[0];
         // source text files are copied when staging incremental tests
-        if (!spec.isStaging()) { // XXX why this? was always? || (testBaseSrcDir != sandbox.stagingDir))) {
-            srcFiles = FileUtil.getBaseDirFiles(testBaseSrcDir, srcPaths, CompilerRun.SOURCE_SUFFIXES);
+        if (!spec.isStaging()) {
+            // XXX why this? was always? || (testBaseSrcDir != sandbox.stagingDir))) {
+            srcFiles =
+                FileUtil.getBaseDirFiles(
+                    testBaseSrcDir,
+                    srcPaths,
+                    CompilerRun.SOURCE_SUFFIXES);
             if (!LangUtil.isEmpty(spec.sourceroots)) {
-                sourcerootFiles = FileUtil.getBaseDirFiles(testBaseSrcDir, spec.sourceroots, null);
+                sourcerootFiles =
+                    FileUtil.getBaseDirFiles(
+                        testBaseSrcDir,
+                        spec.sourceroots,
+                        null);
             }
         } else { // staging - copy files
             if (spec.badInput) {
-                validator.info("badInput ignored - files checked when staging");
+                validator.info(
+                    "badInput ignored - files checked when staging");
             }
             try {
                 // copy all files, then remove tagged ones
                 // XXX make copyFiles support a filter?
-                srcFiles = FileUtil.copyFiles(testBaseSrcDir, srcPaths, sandbox.stagingDir);
+                srcFiles =
+                    FileUtil.copyFiles(
+                        testBaseSrcDir,
+                        srcPaths,
+                        sandbox.stagingDir);
                 if (!LangUtil.isEmpty(spec.sourceroots)) {
-                    sourcerootFiles = FileUtil.copyFiles(testBaseSrcDir, spec.sourceroots, sandbox.stagingDir);
+                    sourcerootFiles =
+                        FileUtil.copyFiles(
+                            testBaseSrcDir,
+                            spec.sourceroots,
+                            sandbox.stagingDir);
                     // delete incremental files in sourceroot after copying // XXX inefficient
                     FileFilter pickIncFiles = new FileFilter() {
-                        // an incremental file has an extra "." in name
-                        // most .java files don't, because they are named after
-                        // the principle type they contain, and simple type names
-                        // have no dots.
-                        public boolean accept(File file) {
-                            if (file.isDirectory()) { // continue recursion
+                            // an incremental file has an extra "." in name
+        // most .java files don't, because they are named after
+        // the principle type they contain, and simple type names
+        // have no dots.
+    public boolean accept(File file) {
+                            if (file.isDirectory()) {
+                                // continue recursion
                                 return true;
                             }
                             String path = file.getPath();
                             // only source files are relevant to staging
-                            if (!FileUtil.hasSourceSuffix(path)) { 
+                            if (!FileUtil.hasSourceSuffix(path)) {
                                 return false;
                             }
                             int first = path.indexOf(".");
@@ -239,13 +270,18 @@ public class CompilerRun implements IAjcRun {
                         }
                     };
                     for (int i = 0; i < sourcerootFiles.length; i++) {
-                        FileUtil.deleteContents(sourcerootFiles[i], pickIncFiles, false);
+                        FileUtil.deleteContents(
+                            sourcerootFiles[i],
+                            pickIncFiles,
+                            false);
                     }
                     if (0 < sourcerootFiles.length) {
-                        FileUtil.sleepPastFinalModifiedTime(sourcerootFiles);
+                        FileUtil.sleepPastFinalModifiedTime(
+                            sourcerootFiles);
                     }
                 }
-                File[] files = FileUtil.getBaseDirFiles(sandbox.stagingDir, srcPaths); 
+                File[] files =
+                    FileUtil.getBaseDirFiles(sandbox.stagingDir, srcPaths);
                 if (0 < files.length) {
                     FileUtil.sleepPastFinalModifiedTime(files);
                 }
@@ -257,7 +293,8 @@ public class CompilerRun implements IAjcRun {
                 return false;
             }
         }
-        if (!spec.badInput && !validator.canRead(srcFiles, "copied paths")) {
+        if (!spec.badInput
+            && !validator.canRead(srcFiles, "copied paths")) {
             return false;
         }
         arguments.clear();
@@ -279,12 +316,13 @@ public class CompilerRun implements IAjcRun {
                 arguments.add("@" + ra[j]);
             }
             if (!spec.badInput && spec.isStaging) {
-                validator.fail("warning: files listed in argfiles not staged");
-            }               
+                validator.fail(
+                    "warning: files listed in argfiles not staged");
+            }
         }
 
         // save classpath and aspectpath in sandbox for this and other clients
-        final boolean checkReadable = !spec.badInput; 
+        final boolean checkReadable = !spec.badInput;
         int size = spec.includeClassesDir ? 3 : 2;
         File[] cp = new File[size + classFiles.length];
         System.arraycopy(classFiles, 0, cp, 0, classFiles.length);
@@ -299,7 +337,7 @@ public class CompilerRun implements IAjcRun {
         if (0 < aspectFiles.length) {
             sandbox.setAspectpath(aspectFiles, checkReadable, this);
         }
-        
+
         // set bootclasspath if set for forking
         AbstractRunSpec.Fork fork = spec.getFork();
         String bootclasspath = fork.getJavaBootclasspath();
@@ -308,7 +346,7 @@ public class CompilerRun implements IAjcRun {
         }
         return true;
     }
-    
+
     /**
      * Setup result evaluation and command line, run, and evaluate result.
      * <li>setup an AjcMessageHandler using the expected messages from
@@ -325,20 +363,23 @@ public class CompilerRun implements IAjcRun {
      * <li>After running, report AjcMessageHandler results to the status parameter.
      *     If the AjcMessageHandler reports a failure, then send info messages
      *     for the Spec, TestSetup, and command line.<li>
-        * @see org.aspectj.testing.run.IRun#run(IRunStatus)
-        */
-       public boolean run(IRunStatus status) {
+     * @see org.aspectj.testing.run.IRun#run(IRunStatus)
+     */
+    public boolean run(IRunStatus status) {
         if (null == spec.testSetup) {
-            MessageUtil.abort(status, "no test setup - adoptParentValues not called");
+            MessageUtil.abort(
+                status,
+                "no test setup - adoptParentValues not called");
             return false;
         } else if (!spec.testSetup.result) {
             MessageUtil.abort(status, spec.testSetup.failureReason);
             return false;
         }
-        boolean ignoreWarnings = (spec.testSetup.ignoreWarningsSet
+        boolean ignoreWarnings =
+            (spec.testSetup.ignoreWarningsSet
                 && spec.testSetup.ignoreWarnings);
-        AjcMessageHandler handler 
-            new AjcMessageHandler(spec.getMessages());
+        AjcMessageHandler handler =
+            new AjcMessageHandler(spec.getMessages());
         handler.init();
         boolean handlerResult = false;
         boolean result = false;
@@ -349,9 +390,12 @@ public class CompilerRun implements IAjcRun {
             argList.add("-d");
             String outputDirPath = sandbox.classesDir.getAbsolutePath();
             try { // worth it to try for canonical?
-                outputDirPath = sandbox.classesDir.getCanonicalPath();        
+                outputDirPath = sandbox.classesDir.getCanonicalPath();
             } catch (IOException e) {
-                MessageUtil.abort(status, "canonical " + sandbox.classesDir, e);
+                MessageUtil.abort(
+                    status,
+                    "canonical " + sandbox.classesDir,
+                    e);
             }
             argList.add(outputDirPath);
 
@@ -365,7 +409,7 @@ public class CompilerRun implements IAjcRun {
                 argList.add("-bootclasspath");
                 argList.add(path);
             }
-            
+
             path = sandbox.aspectpathToString(this);
             if (!LangUtil.isEmpty(path)) {
                 argList.add("-aspectpath");
@@ -374,54 +418,81 @@ public class CompilerRun implements IAjcRun {
 
             if (0 < injars.size()) {
                 argList.add("-injars");
-                argList.add(FileUtil.flatten((String[]) injars.toArray(new String[0]), null));
+                argList.add(
+                    FileUtil.flatten(
+                        (String[]) injars.toArray(new String[0]),
+                        null));
             }
-            
+
             // put specified arguments last, for better badInput tests
             argList.addAll(setupResult.commandOptions);
 
             // add both java/aspectj and argfiles
             argList.addAll(arguments);
-            
+
             // XXX hack - seek on request as a side effect. reimplement as listener 
             if (null != setupResult.seek) {
                 String slopPrefix = Spec.SEEK_MESSAGE_PREFIX + " slop - ";
-                PrintStream slop = MessageUtil.handlerPrintStream(
-                    status, 
-                    IMessage.INFO, 
-                    System.err, 
-                    slopPrefix);
-                List found = FileUtil.lineSeek(setupResult.seek, arguments, false, slop);
+                PrintStream slop =
+                    MessageUtil.handlerPrintStream(
+                        status,
+                        IMessage.INFO,
+                        System.err,
+                        slopPrefix);
+                List found =
+                    FileUtil.lineSeek(
+                        setupResult.seek,
+                        arguments,
+                        false,
+                        slop);
                 if (!LangUtil.isEmpty(found)) {
-                    for (Iterator iter = found.iterator(); iter.hasNext();) {
-                        MessageUtil.info(status, Spec.SEEK_MESSAGE_PREFIX + iter.next());
-                                       }
+                    for (Iterator iter = found.iterator();
+                        iter.hasNext();
+                        ) {
+                        MessageUtil.info(
+                            status,
+                            Spec.SEEK_MESSAGE_PREFIX + iter.next());
+                    }
                 }
             }
-            ICommand compiler = spec.reuseCompiler 
-                ? sandbox.getCommand(this) // throws IllegalStateException if null
-                : ReflectionFactory.makeCommand(setupResult.compilerName, status);            
+            ICommand compiler = spec.reuseCompiler
+                // throws IllegalStateException if null
+    ? sandbox.getCommand(this)
+    : ReflectionFactory.makeCommand(setupResult.compilerName, status);
             DirChanges dirChanges = null;
             if (null == compiler) {
-                MessageUtil.fail(status, "unable to make compiler " + setupResult.compilerName);
+                MessageUtil.fail(
+                    status,
+                    "unable to make compiler " + setupResult.compilerName);
                 return false;
             } else {
                 if (setupResult.compilerName != Spec.DEFAULT_COMPILER) {
-                    MessageUtil.info(status, "compiler: " + setupResult.compilerName);
+                    MessageUtil.info(
+                        status,
+                        "compiler: " + setupResult.compilerName);
                 }
                 if (status.aborted()) {
-                    MessageUtil.debug(status, "aborted, but compiler valid?: " + compiler);
+                    MessageUtil.debug(
+                        status,
+                        "aborted, but compiler valid?: " + compiler);
                 } else {
                     // same DirChanges handling for JavaRun, CompilerRun, IncCompilerRun 
                     // XXX around advice or template method/class
                     if (!LangUtil.isEmpty(spec.dirChanges)) {
-                        LangUtil.throwIaxIfFalse(1 == spec.dirChanges.size(), "expecting 0..1 dirChanges");
-                        dirChanges = new DirChanges((DirChanges.Spec) spec.dirChanges.get(0));
-                        if (!dirChanges.start(status, sandbox.classesDir)) {
+                        LangUtil.throwIaxIfFalse(
+                            1 == spec.dirChanges.size(),
+                            "expecting 0..1 dirChanges");
+                        dirChanges =
+                            new DirChanges(
+                                (DirChanges.Spec) spec.dirChanges.get(0));
+                        if (!dirChanges
+                            .start(status, sandbox.classesDir)) {
                             return false; // setup failed
                         }
                     }
-                    MessageUtil.info(status, compiler + "(" + argList + ")");
+                    MessageUtil.info(
+                        status,
+                        compiler + "(" + argList + ")");
                     sandbox.setCommand(compiler, this);
                     String[] args = (String[]) argList.toArray(RA_String);
                     commandResult = compiler.runCommand(args, handler);
@@ -431,11 +502,12 @@ public class CompilerRun implements IAjcRun {
             if (!handlerResult) {
                 return false;
             } else {
-                result = (commandResult == handler.expectingCommandTrue()); 
-                if (! result) {
-                    String m = commandResult 
-                        ? "compile did not fail as expected"
-                        : "compile failed unexpectedly";                        
+                result = (commandResult == handler.expectingCommandTrue());
+                if (!result) {
+                    String m =
+                        commandResult
+                            ? "compile did not fail as expected"
+                            : "compile failed unexpectedly";
                     MessageUtil.fail(status, m);
                 } else if (null != dirChanges) {
                     result = dirChanges.end(status, sandbox.testBaseDir);
@@ -450,111 +522,163 @@ public class CompilerRun implements IAjcRun {
                     MessageUtil.info(handler, "" + setupResult);
                 }
             }
-            handler.report(status); // XXX weak - actual messages not reported in real-time, no fast-fail
-        }             
+            handler.report(status);
+            // XXX weak - actual messages not reported in real-time, no fast-fail
+        }
     }
-    
 
     public String toString() {
         return "CompilerRun(" + spec + ")";
     }
-    
+
     /** 
      * Initializer/factory for CompilerRun
      * any path or file is relative to this test base dir
      */
     public static class Spec extends AbstractRunSpec {
         public static final String XMLNAME = "compile";
+        public static final String DEFAULT_COMPILER =
+            ReflectionFactory.ECLIPSE;
         static final String SEEK_PREFIX = "-seek:";
         static final String SEEK_MESSAGE_PREFIX = "found: ";
-        
-        /** can't specify these as options */
-        private static final String[] INVALID_OPTIONS = new String[]
-            { "-workingdir", "-argfile", "-sourceroots", "-outjar"}; 
-            // when updating these, update tests/harness/selectionTest.xml
-        
-
-        /** no support in the javac for these otherwise-valid options */
-        private static final String[] INVALID_JAVAC_OPTIONS = new String[]
-            { "-lenient", "-strict", "-usejavac", "-preprocess",
-              "-XOcodeSize", "-XSerializable", "-XaddSafePrefix",
-              "-XtargetNearSource",
-              "-incremental", "-Xlint", "-aspectpath",
-              "workingdir", "-argfile", "-sourceroots", "-outjar",
-               };
-               
-        /** no support in the eclipse-based compiler for these otherwise-valid options */
-        private static final String[] INVALID_ECLIPSE_OPTIONS = new String[]
-            { "-lenient", "-strict", "-usejavac", "-preprocess",
-              "-XOcodeSize", "-XSerializable", "-XaddSafePrefix",
-              "-XtargetNearSource" };
-
-        /** options supported by the harness */
-        private static final String[] VALID_OPTIONS = new String[]
-            {
-                SEEK_PREFIX,
-                // eajc does not support -usejavac, -preprocess
-                // testFlag() handles -ajc, -eclipse, -javac, -ajdeCompiler, -ignoreWarnings 
-                "-usejavac", "-preprocess",          
-                "-Xlint",  "-lenient", "-strict", 
-                "-source14", "-verbose", "-emacssym", 
-                "-ajc", "-eclipse", "-ajdeCompiler", "-ajctaskCompiler", "-javac",
-                "-ignoreWarnings", "-1.3", "-1.4", "-1.5",
-                // XXX consider adding [!^]ajdeCompiler
-                "!usejavac", "!preprocess",          
-                "!Xlint",  "!lenient", "!strict", 
-                "!source14", "!verbose", "!emacssym", 
-                "!ajc", "!eclipse", 
-
-                "^usejavac", "^preprocess",          
-                "^Xlint",  "^lenient", "^strict", 
-                "^source14", "^verbose", "^emacssym", 
-                "^ajc", "^eclipse"
-            };
-        public static final String DEFAULT_COMPILER 
-            = ReflectionFactory.ECLIPSE;
+
+        private static final CRSOptions CRSOPTIONS = new CRSOptions();
+        /**
+          * Retitle description to title, paths to files, do comment,
+          * staging, badInput,
+          * do dirChanges, and print no chidren. 
+          */
+        private static final AbstractRunSpec.XMLNames NAMES =
+            new AbstractRunSpec.XMLNames(
+                AbstractRunSpec.XMLNames.DEFAULT,
+                "title",
+                null,
+                null,
+                null,
+                "files",
+                null,
+                null,
+                null,
+                false,
+                false,
+                true);
 
         /**
-         * Retitle description to title, paths to files, do comment,
-         * staging, badInput,
-         * do dirChanges, and print no chidren. 
+         * If the source version warrants, add a -bootclasspath
+         * entry to the list of arguments to add. 
+         * This will fail and return an error String if the 
+         * required library is not found.
+         * @param sourceVersion the String (if any) describing the -source option
+         *        (expecting one of [null, "1.3", "1.4", "1.5"].
+         * @param compilerName the String name of the target compiler
+         * @param toAdd the ArrayList to add -bootclasspath to
+         * @return the String describing any errors, or null if no errors
          */
-        private static final XMLNames NAMES = new XMLNames(XMLNames.DEFAULT,
-            "title", null, null, null, "files", null, null, null, false, false, true);
-        
+        private static String updateBootclasspathForSourceVersion(
+            String sourceVersion,
+            String compilerName,
+            ArrayList toAdd) {
+            if (null == sourceVersion) {
+                return null;
+            }
+            if (3 != sourceVersion.length()) {
+                throw new IllegalArgumentException(
+                    "bad version: " + sourceVersion);
+            }
+            if (null == toAdd) {
+                throw new IllegalArgumentException("null toAdd");
+            }
+            int version = sourceVersion.charAt(2) - '0';
+            switch (version) {
+                case (3) :
+                    if (LangUtil.supportsJava("1.4")) {
+                        if (!FileUtil.canReadFile(Globals.J2SE13_RTJAR)) {
+                            return "no 1.3 libraries to handle -source 1.3";
+                        }
+                        toAdd.add("-bootclasspath");
+                        toAdd.add(Globals.J2SE13_RTJAR.getAbsolutePath());
+                    }
+                    break;
+                case (4) :
+                    if (!LangUtil.supportsJava("1.4")) {
+                        if (ReflectionFactory
+                            .ECLIPSE
+                            .equals(compilerName)) {
+                            return "run eclipse under 1.4 to handle -source 1.4";
+                        }
+                        if (!FileUtil.canReadFile(Globals.J2SE14_RTJAR)) {
+                            return "no 1.4 libraries to handle -source 1.4";
+                        }
+                        toAdd.add("-bootclasspath");
+                        toAdd.add(Globals.J2SE14_RTJAR.getAbsolutePath());
+                    }
+                    break;
+                case (5) :
+                    return "1.5 not supported in CompilerRun";
+                case (0) :
+                    // ignore - no version specified
+                    break;
+                default :
+                    throw new Error("unexpected version: " + version);
+            }
+            return null;
+        }
+
+        static CRSOptions testAccessToCRSOptions() {
+            return CRSOPTIONS;
+        }
+
+        static Options testAccessToOptions() {
+            return CRSOPTIONS.getOptions();
+        }
+
+        private static String[] copy(String[] input) {
+            if (null == input) {
+                return null;
+            }
+            String[] result = new String[input.length];
+            System.arraycopy(input, 0, result, 0, input.length);
+            return result;
+        }
+
+        /** @return true if javac is available on the classpath */
+        private static boolean haveJavac() { // XXX copy/paste from JavaCWrapper.java
+            Class compilerClass = null;
+            try {
+                compilerClass = Class.forName("com.sun.tools.javac.Main");
+            } catch (ClassNotFoundException ce1) {
+                try {
+                    compilerClass = Class.forName("sun.tools.javac.Main");
+                } catch (ClassNotFoundException ce2) {
+                }
+            }
+            return (null != compilerClass);
+        }
+
         protected String compiler;
-        
+
         // use same command - see also IncCompiler.Spec.fresh
         protected boolean reuseCompiler;
         protected boolean includeClassesDir;
-        
+
         protected TestSetup testSetup;
-        
+
         protected String[] argfiles = new String[0];
         protected String[] aspectpath = new String[0];
         protected String[] classpath = new String[0]; // XXX unused
         protected String[] sourceroots = new String[0];
-        
+
         /** src path = {suiteParentDir}/{testBaseDirOffset}/{testSrcDirOffset}/{path} */
         protected String testSrcDirOffset;
-        
+
         public Spec() {
             super(XMLNAME);
             setXMLNames(NAMES);
             compiler = DEFAULT_COMPILER;
         }
-        
-        private static String[] copy(String[] input) {
-            if (null == input) {
-                return null;
-            }
-            String[] result = new String[input.length];
-            System.arraycopy(input, 0, result, 0, input.length);
-            return result;
-        }
 
-        protected void initClone(Spec spec) 
-                throws CloneNotSupportedException {
+        protected void initClone(Spec spec)
+            throws CloneNotSupportedException {
             super.initClone(spec);
             spec.argfiles = copy(argfiles);
             spec.aspectpath = copy(aspectpath);
@@ -569,11 +693,11 @@ public class CompilerRun implements IAjcRun {
             }
             spec.testSrcDirOffset = testSrcDirOffset;
         }
-        
+
         public Object clone() throws CloneNotSupportedException {
             Spec result = new Spec();
             initClone(result);
-            return result;    
+            return result;
         }
 
         public void setIncludeClassesDir(boolean include) {
@@ -582,17 +706,17 @@ public class CompilerRun implements IAjcRun {
         public void setReuseCompiler(boolean reuse) {
             this.reuseCompiler = reuse;
         }
-        
+
         public void setCompiler(String compilerName) {
             this.compiler = compilerName;
-        }        
+        }
 
         public void setTestSrcDirOffset(String s) {
             if (null != s) {
                 testSrcDirOffset = s;
             }
         }
-        
+
         /** override to set dirToken to Sandbox.CLASSES and default suffix to ".class" */
         public void addDirChanges(DirChanges.Spec spec) {
             if (null == spec) {
@@ -602,7 +726,7 @@ public class CompilerRun implements IAjcRun {
             spec.setDefaultSuffix(".class");
             super.addDirChanges(spec);
         }
-        
+
         public String toLongString() {
             return getPrintName() + "(" + super.containedSummary() + ")";
         }
@@ -611,11 +735,11 @@ public class CompilerRun implements IAjcRun {
             return getPrintName() + "(" + super.containedSummary() + ")";
         }
 
-        /** bean mapping for writers */        
+        /** bean mapping for writers */
         public void setFiles(String paths) {
             addPaths(paths);
         }
-        
+
         /**
          * Add to default classpath
          * (which includes aspectjrt.jar and testing-client.jar).
@@ -632,7 +756,7 @@ public class CompilerRun implements IAjcRun {
          * Set source roots, deleting any old ones
          * @param files comma-delimited list of directories
          *  - ignored if null or empty
-         */        
+         */
         public void setSourceroots(String dirs) {
             if (!LangUtil.isEmpty(dirs)) {
                 sourceroots = XMLWriter.unflattenList(dirs);
@@ -643,7 +767,7 @@ public class CompilerRun implements IAjcRun {
          * Set aspectpath, deleting any old ones
          * @param files comma-delimited list of aspect jars - ignored if null or
          * empty
-         */        
+         */
         public void setAspectpath(String files) {
             if (!LangUtil.isEmpty(files)) {
                 aspectpath = XMLWriter.unflattenList(files);
@@ -653,7 +777,7 @@ public class CompilerRun implements IAjcRun {
         /** 
          * Set argfiles, deleting any old ones
          * @param files comma-delimited list of argfiles - ignored if null or empty
-         */        
+         */
         public void setArgfiles(String files) {
             if (!LangUtil.isEmpty(files)) {
                 argfiles = XMLWriter.unflattenList(files);
@@ -668,20 +792,22 @@ public class CompilerRun implements IAjcRun {
             }
             return (String[]) LangUtil.copy(argfiles);
         }
-        
+
         /**
          * This implementation skips if:
          * <ul>
          * <li>incremental test, but using ajc (not eclipse)</li>
          * <li>usejavac, but javac is not available on the classpath</li>
          * <li>eclipse, but -usejavac or -preprocess test</li>
-         * <li>-source14, but running under 1.2 (XXX design)</li>
+         * <li>-source 1.4, but running under 1.2 (XXX design)</li>
          * <li>local/global option conflicts (-lenient/-strict)</li>
          * <li>semantic conflicts (e.g., -lenient/-strict)</li>
          * </ul>
          * @return false if this wants to be skipped, true otherwise
          */
-        protected boolean doAdoptParentValues(RT parentRuntime, IMessageHandler handler) {
+        protected boolean doAdoptParentValues(
+            RT parentRuntime,
+            IMessageHandler handler) {
             if (!super.doAdoptParentValues(parentRuntime, handler)) {
                 return false;
             }
@@ -690,33 +816,35 @@ public class CompilerRun implements IAjcRun {
                 skipMessage(handler, testSetup.failureReason);
             }
             return testSetup.result;
-        }  
-        
+        }
 
-           private String getShortCompilerName() {
-                       String cname = compiler;
+        private String getShortCompilerName() {
+            String compilerClassName = compiler;
             if (null != testSetup) {
-                cname = testSetup.compilerName;
-            }
-                       if (null != cname) {
-                           int loc = cname.lastIndexOf(".");
-                           if (-1 != loc) {
-                               cname = cname.substring(loc+1);
-                           }
-                       }
-                       return cname;
-               }
-        
+                compilerClassName = testSetup.compilerName;
+            }
+            if (null != compilerClassName) {
+                int loc = compilerClassName.lastIndexOf(".");
+                if (-1 != loc) {
+                    compilerClassName =
+                        compilerClassName.substring(loc + 1);
+                }
+            }
+            return compilerClassName;
+        }
+
         /** @return a CompilerRun with this as spec if setup completes successfully. */
-               public IRunIterator makeRunIterator(Sandbox sandbox, Validator validator) {
-                       CompilerRun run = new CompilerRun(this);
+        public IRunIterator makeRunIterator(
+            Sandbox sandbox,
+            Validator validator) {
+            CompilerRun run = new CompilerRun(this);
             if (run.setupAjcRun(sandbox, validator)) {
                 // XXX need name for compilerRun
                 return new WrappedRunIterator(this, run);
             }
             return null;
-               }
-        
+        }
+
         protected String getPrintName() {
             return "CompilerRun.Spec " + getShortCompilerName();
         }
@@ -750,9 +878,6 @@ public class CompilerRun implements IAjcRun {
          * <li>lenient, strict</li>
          * </ul>
          * <p>
-         * The -source 1.4 flag should always be specified as -source14, 
-         * as this will otherwise fail to process it correctly.  
-         * This converts it back to -source 1.4.
          * <p>
          * This also interprets any relevant System properties,
          * e.g., from <code>JavaRun.BOOTCLASSPATH_KEY</code>.
@@ -773,298 +898,346 @@ public class CompilerRun implements IAjcRun {
          * and commandOptions is not null, and any test flags (ignore warning,
          * compiler) are reflected in the TestSetup.
          * If this fails, then TestSetup.result is false,
-         * and a TestSetup.failreason is set.  
+         * and a TestSetup.failreason is set. 
+         * This means the test is skipped. 
          * @return TestSetup with results 
          *          (TestSetup result=false if the run should not continue)
          */
         protected TestSetup setupArgs(IMessageHandler handler) {
             // warning: HarnessSelectionTest checks for specific error wording
-            ArrayList argList = new ArrayList();
-            argList.addAll(getOptionsList());
             final Spec spec = this;
-            TestSetup result = new TestSetup();
-            if (argList.contains("-source")) {
-                result.failureReason = "use -source14 for -source 1.4: " + argList;
-                return result;                
-            }
+            final TestSetup result = new TestSetup();
             result.compilerName = spec.compiler;
-            String[] globalOptions = spec.runtime.extractOptions(Spec.VALID_OPTIONS, true);
-            if ((null != globalOptions) && (globalOptions.length > 0)) {
-                // --- fold in globals, removing conflicts, etc.
-                for (int i = 0; i < globalOptions.length; i++) {
-                    String globalArg = globalOptions[i];
-                    if ((null == globalArg) || (2 > globalArg.length())) {
-                        continue;
-                    } else if (globalArg.startsWith(SEEK_PREFIX)) { 
-                        result.seek = globalArg.substring(SEEK_PREFIX.length());
-                        continue;
-                    } else if ("-source".equals(globalArg)) {
-                        result.failureReason = "use -source14 for -source 1.4 [" + i + "]";
-                        return result;                
-                    }
-                    char first = globalArg.charAt(0);
-                    globalArg = globalArg.substring(1);
-                    boolean globalForceOn   = (first == '!');
-                    boolean globalForceOff  = (first == '^');
-                    boolean globalSet       = (first == '-');
-                    if (!globalSet && !globalForceOn && !globalForceOff) {
-                        MessageUtil.info(handler, "ignoring bad global: " + globalOptions[i]);
-                        continue;
-                    }
-                    int argIndex = indexOf(globalArg, argList);
-                    if (-1 == argIndex) { // no apparent conflict - look for eclipse/ajc conflicts XXX unresolved
-                        boolean ajcGlobal = true;
-                        if ("ajc".equals(globalArg)) {
-                            argIndex = indexOf("eclipse", argList);
-                        } else if ("eclipse".equals(globalArg)) {
-                            argIndex = indexOf("ajc", argList);                            
-                            ajcGlobal = false;
-                        }
-                        if (-1 != argIndex) {   // resolve eclipse/ajc conflict
-                            String arg = ((String) argList.get(argIndex));
-                            char argFirst = arg.charAt(0);
-                            argList.remove(arg);      // replace with resolved variant...
-                            char ajcFirst;
-                            char eclipseFirst;
-                            if (ajcGlobal) {
-                                ajcFirst = first;
-                                eclipseFirst = argFirst;
-                            } else {
-                                ajcFirst = argFirst;
-                                eclipseFirst = first;
-                            }
-                            if ('!' == eclipseFirst) {
-                                if ('!' == ajcFirst) {
-                                    result.failureReason = "conflict between !eclipse and !ajc";
-                                    return result;
-                                } else {
-                                    argList.add("-eclipse");
-                                }
-                            } else if (('!' == ajcFirst)  || ('^' == eclipseFirst)) {
-                                argList.add("-ajc");
-                            } else if ('^' == ajcFirst) {
-                                argList.add("-eclipse");
-                            } else if (('-' != ajcFirst) || ('-' != eclipseFirst)) {
-                                result.failureReason = "harness logic error resolving "
-                                    + arg + " and global " + globalArg;
-                                return result;
-                            } else if (ajcGlobal) {
-                                argList.add("-ajc");
-                            } else {
-                                argList.add("-eclipse");
-                            }
-                            continue; // resolved 
-                        }
-                    }
-                    
-                    if (-1 == argIndex) { // no dup, so no conflict
-                        if (!globalForceOff) {
-                            argList.add("-" + globalArg);
-                        }
-                    } else { // have conflict - resolve
-                        String arg = (String) argList.get(argIndex);
-                        first = arg.charAt(0);
-                        boolean localForceOn   = (first == '!');
-                        boolean localForceOff  = (first == '^');
-                        boolean localSet       = (first == '-');
-                        if (!localSet && !localForceOn && !localForceOff) {
-                            result.failureReason = "only handling [-^!]{arg}: " + arg;
-                            return result;
-                        }
-                        if ((localForceOn && globalForceOff)
-                            || (localForceOff && globalForceOn)) {
-                            result.failureReason = "force conflict between arg=" 
-                                + arg + " and global=" + globalOptions[i];
-                            return result;
-                        } 
-                        if (globalForceOn) {
-                            if (localForceOn) { // localSet is already correct, localForceOff was conflict
-                                argList.remove(arg);      // no !funkiness
-                                argList.add("-" + globalArg);
-                            }
-                        } else if (globalSet) {
-                            if (localSet) {
-                                // do nothing - already correct
-                            } else if (localForceOn) {
-                                argList.remove(arg);      // no !funkiness
-                                argList.add("-" + globalArg);
-                            }
-                        } else if (globalForceOff) {
-                            argList.remove(arg);
-                        } else {
-                            throw new Error("illegal arg state?? : " + arg);
-                            //MessageUtil.info(handler, "illegal arg state??: " + arg);
-                        }
-                    }
-                }
+            // urk - s.b. null, but expected
+            Values values = gatherValues(result);
+
+            if ((null == values) || (null != result.failureReason)) {
+                return checkResult(result);
             }
+
             // send info messages about
             // forced staging when -incremental
             // or staging but no -incremental flag
-            int incLoc = argList.indexOf("-incremental");
+            Option.Family getFamily =
+                CRSOPTIONS.crsIncrementalOption.getFamily();
+            final boolean haveIncrementalFlag =
+                (null != values.firstInFamily(getFamily));
+
             if (spec.isStaging()) {
-                if (-1 == incLoc) { // staging and no flag
-                    MessageUtil.info(handler, "staging but no -incremental flag");
+                if (!haveIncrementalFlag) {
+                    MessageUtil.info(
+                        handler,
+                        "staging but no -incremental flag");
                 }
-            } else if (-1 != incLoc) { // flagged but not staging - stage
+            } else if (haveIncrementalFlag) {
                 spec.setStaging(true);
                 MessageUtil.info(handler, "-incremental forcing staging");
             }
-            // remove funky prefixes from remainder, fixup two-part flags
-            // and interpret special flags
-            boolean source14 = false;
-            ArrayList toAdd = new ArrayList();
-            for (ListIterator iter = argList.listIterator(); iter.hasNext();) {
-                String arg = (String) iter.next();
-                if (testFlag(arg, result)) {
-                    iter.remove();
-                    continue;
-                }
-                char c = arg.charAt(0);
-                String rest = arg.substring(1);
-                if (c == '^') {
-                    iter.remove();
-                    continue;
-                }
-                if (c == '!') {
-                    iter.remove();
-                    if (!("source14".equals(rest))) {
-                        toAdd.add("-" + rest);
-                    } else {
-                        source14 = true;
-                    }
-                    rest = null;
-                } else if ("source14".equals(rest)) {
-                    iter.remove();
-                    source14 = true;
-                }
+
+            if (hasInvalidOptions(values, result)) {
+                return checkResult(result);
             }
-            if (source14) {
-                // must run under 1.4 VM or (if ajc) set up bootclasspath
-                if (!LangUtil.supportsJava("1.4")) {
-                    if (ReflectionFactory.ECLIPSE.equals(result.compilerName)) {
-                        result.failureReason 
-                            = "eclipse must run under 1.4 to implement -source 1.4";
-                        return result;                        
-                    }
-                    
-                    if (!FileUtil.canReadFile(Globals.J2SE14_RTJAR)) {
-                        result.failureReason 
-                            = "unable to get 1.4 libraries to implement -source 1.4";
-                        return result;
-                    }
-                    toAdd.add("-bootclasspath");
-                    toAdd.add(Globals.J2SE14_RTJAR.getAbsolutePath());
-                }
-                toAdd.add("-source");
-                toAdd.add("1.4");
-            }
-            argList.addAll(toAdd);
-    
-            // finally, check for semantic conflicts
-            String[] badOptions = LangUtil.selectOptions(argList, Spec.INVALID_OPTIONS);
-            if (!LangUtil.isEmpty(badOptions)) {
-                result.failureReason = "no support for (normally-valid) options "
-                     + Arrays.asList(badOptions);
-            } else if (argList.contains("-lenient") && argList.contains("-strict")) {
-                result.failureReason = "semantic conflict -lenient | -strict";
-            } else if (ReflectionFactory.OLD_AJC.equals(result.compilerName)) {
-                if (spec.isStaging) {
-                    result.failureReason = "OLD_AJC does not do incremental compiles";
-                } else if (argList.contains("-usejavac") && !haveJavac()) {
-                    result.failureReason = "-usejavac but no javac on classpath";
-                } else {
-                    result.result = true;
-                }
-            } else if (ReflectionFactory.ECLIPSE.equals(result.compilerName)
-                || AJDE_COMPILER.equals(result.compilerName)
-                || AJCTASK_COMPILER.equals(result.compilerName)
-                || BUILDER_COMPILER.equals(result.compilerName)) {
-                badOptions = LangUtil.selectOptions(argList, Spec.INVALID_ECLIPSE_OPTIONS);
-                if (!LangUtil.isEmpty(badOptions)) {                    
-                    result.failureReason = "no support in eclipse-based compiler"
-                        + " for (normally-valid) options "+ Arrays.asList(badOptions);
-                } else {
-                    result.result = true;
+
+            // set compiler in result
+            getFamily = CRSOPTIONS.ajccompilerOption.getFamily();
+            Option.Value compiler = values.firstInFamily(getFamily);
+            if (null != compiler) {
+                result.compilerName 
+                    = CRSOPTIONS.compilerClassName(compiler.option);
+                if (null == result.compilerName) {
+                    result.failureReason =
+                        "unable to get class name for " + compiler;
+                    return checkResult(result);
                 }
-//            } else if (JAVAC_COMPILER.equals(result.compilerName)) {
-//                // XXX vet
-//                badOptions = LangUtil.selectOptions(argList, Spec.INVALID_JAVAC_OPTIONS);
-//                if (!LangUtil.isEmpty(badOptions)) {                    
-//                    result.failureReason = "no support in javac"
-//                        + " for (normally-valid) options " + Arrays.asList(badOptions);
-//                } else {
-//                    result.result = true;
-//                }
-            } else {
-                result.failureReason = "unrecognized compiler: " + result.compilerName;
             }
-            if (result.result) {
-                result.commandOptions = argList;
+            String compilerName =
+                (null == result.compilerName
+                    ? spec.compiler
+                    : result.compilerName);
+            // check compiler semantics
+            if (hasCompilerSpecErrors(compilerName, values, result)) {
+                return checkResult(result);
+            }
+            // add toadd and finish result
+            ArrayList args = new ArrayList();
+            String[] rendered = values.render();
+            if (!LangUtil.isEmpty(rendered)) {
+                args.addAll(Arrays.asList(rendered));
             }
-            return result; 
+            // update bootclasspath
+            getFamily = CRSOPTIONS.crsSourceOption.getFamily();
+            Option.Value source = values.firstInFamily(getFamily);
+            if (null != source) {
+                String sourceVersion = source.unflatten()[1];
+                ArrayList toAdd = new ArrayList();
+                String err =
+                    updateBootclasspathForSourceVersion(
+                        sourceVersion,
+                        spec.compiler,
+                        toAdd);
+                args.addAll(toAdd);
+            }
+            result.commandOptions = args;
+            result.result = true;
+            return checkResult(result);
         }
 
-               /** @return true if javac is available on the classpath */
-               private boolean haveJavac() { // XXX copy/paste from JavaCWrapper.java
-            Class compilerClass = null;
-            try {
-                compilerClass = Class.forName("com.sun.tools.javac.Main");
-            } catch (ClassNotFoundException ce1) {
-                try {
-                    compilerClass = Class.forName("sun.tools.javac.Main");
-                } catch (ClassNotFoundException ce2) {
+        /**
+         * Ensure exit invariant:
+         * <code>result.result == (null == result.failureReason)
+         *       == (null != result.commandOptions)</code>
+         * @param result the TestSetup to verify
+         * @return result
+         * @throws Error if invariant is not true
+         */
+        TestSetup checkResult(TestSetup result) {
+            String err = null;
+            if (null == result) {
+                err = "null result";
+            } else if (result.result != (null == result.failureReason)) {
+                err =
+                    result.result
+                        ? "expected no failure: " + result.failureReason
+                        : "fail for no reason";
+            } else if (result.result != (null != result.commandOptions)) {
+                err =
+                    result.result
+                        ? "expected command options"
+                        : "unexpected command options";
+            }
+            if (null != err) {
+                throw new Error(err);
+            }
+            return result;
+        }
+
+        boolean hasInvalidOptions(Values values, TestSetup result) {
+            // not supporting 1.0 options any more
+            for (Iterator iter = CRSOPTIONS.invalidOptions.iterator();
+                iter.hasNext();
+                ) {
+                Option option = (Option) iter.next();
+                if (null != values.firstOption(option)) {
+                    result.failureReason =
+                        "invalid option in harness: " + option;
+                    return true;
                 }
             }
-            return (null != compilerClass);
-               }
+            return false;
+        }
 
-        
-        /**
-         * Handle flags that are interpreted by the test rather than the
-         * underlying command.  These flags are to be removed from the
-         * arg list.
-         *  @return true if this is a flag to remove from the arg list
-         */
-        protected boolean testFlag(String arg, TestSetup result) {
-            if ("-ajdeCompiler".equals(arg)) {
-                result.compilerName = AJDE_COMPILER;
-                return true;
-            } else if ("-builderCompiler".equals(arg)) {
-                result.compilerName = BUILDER_COMPILER;
-                return true;
-            } else if ("-ajctaskCompiler".equals(arg)) {
-                result.compilerName = AJCTASK_COMPILER;
-                return true;
-//            } else if ("-javac".equals(arg)) {
-//                result.compilerName = JAVAC_COMPILER;
-//                return true;
-            } else if ("-eclipse".equals(arg) || "!eclipse".equals(arg) || "^ajc".equals(arg)) {
-                result.compilerName = ReflectionFactory.ECLIPSE;
-                return true;
-            } else if ("-ajc".equals(arg) || "!ajc".equals(arg) || "^eclipse".equals(arg)) {
-                result.compilerName = ReflectionFactory.OLD_AJC;
+        boolean hasCompilerSpecErrors(
+            String compilerName,
+            Values values,
+            TestSetup result) {
+            /*
+             * Describe any semantic conflicts between options.
+             * This skips:
+             * - old 1.0 options, including lenient v. strict
+             * - old ajc options, include !incremental and usejavac w/o javac
+             * - invalid eclipse options (mostly ajc) 
+             * @param compilerName the String name of the target compiler
+             * @return a String describing any conflicts, or null if none
+             */
+            if (!(ReflectionFactory.ECLIPSE.equals(compilerName)
+                || ReflectionFactory.OLD_AJC.equals(compilerName)
+                || CRSOPTIONS.AJDE_COMPILER.equals(compilerName)
+                || CRSOPTIONS.AJCTASK_COMPILER.equals(compilerName) 
+                )) {
+                //|| BUILDER_COMPILER.equals(compilerName))
+                result.failureReason =
+                    "unrecognized compiler: " + compilerName;
                 return true;
-            } else if ("-ignoreWarnings".equals(arg)) {
-                result.ignoreWarnings = true;
-                result.ignoreWarningsSet = true;
+            }
+            // not supporting ajc right now
+            if (null
+                != values.firstOption(CRSOPTIONS.ajccompilerOption)) {
+                result.failureReason = "ajc not supported";
                 return true;
             }
+            // not supporting 1.0 options any more
+            for (Iterator iter = CRSOPTIONS.ajc10Options.iterator();
+                iter.hasNext();
+                ) {
+                Option option = (Option) iter.next();
+                if (null != values.firstOption(option)) {
+                    result.failureReason = "old ajc 1.0 option: " + option;
+                    return true;
+                }
+            }
+
             return false;
         }
-        
+
+        protected Values gatherValues(TestSetup result) {
+            final Spec spec = this;
+            // ---- local option values
+            final Values localValues;
+            final Options options = CRSOPTIONS.getOptions();
+            try {
+                String[] input = getOptionsArray();
+                // this handles reading options, 
+                // flattening two-String options, etc.
+                localValues = options.acceptInput(input);
+                // all local values should be picked up
+                String err = Options.missedMatchError(input, localValues);
+                if (!LangUtil.isEmpty(err)) {
+                    result.failureReason = err;
+                    return null;
+                }
+            } catch (InvalidInputException e) {
+                result.failureReason = e.getFullMessage();
+                return null;
+            }
+
+            // ---- global option values
+            StringBuffer errs = new StringBuffer();
+            final Values globalValues =
+                spec.runtime.extractOptions(options, true, errs);
+            if (errs.length() > 0) {
+                result.failureReason = errs.toString();
+                return null;
+            }
+            final Values combined =
+                Values.wrapValues(
+                    new Values[] { localValues, globalValues });
+
+            String err = combined.resolve();
+            if (null != err) {
+                result.failureReason = err;
+                return null;
+            }
+
+            return handleTestArgs(combined, result);
+        }
+
+        //            final int len = globalValues.length() + localValues.length();
+        //            final Option.Value[] combinedValues = new Option.Value[len];
+        //            System.arraycopy(
+        //                globalValues,
+        //                0,
+        //                combinedValues,
+        //                0,
+        //                globalValues.length());
+        //            System.arraycopy(
+        //                localValues,
+        //                0,
+        //                combinedValues,
+        //                globalValues.length(),
+        //                localValues.length());
+        //
+        //            result.compilerName = spec.compiler;
+        //            if (0 < combinedValues.length) {
+        //                // this handles option forcing, etc.
+        //                String err = Options.resolve(combinedValues);
+        //                if (null != err) {
+        //                    result.failureReason = err;
+        //                    return null;
+        //                }
+        //                if (!handleTestArgs(combinedValues, result)) {
+        //                    return null;
+        //                }
+        //            }
+        //            return Values.wrapValues(combinedValues);
+        //        }
+        /**
+         * This interprets and nullifies values for the test.
+         * @param values the Option.Value[] being processed
+         * @param result the TestSetup to modify
+         * @return false if error (caller should return), true otherwise
+         */
+        Values handleTestArgs(Values values, final TestSetup result) {
+            final Option.Family compilerFamily =
+                CRSOPTIONS.ajccompilerOption.getFamily();
+            Values.Selector selector = new Values.Selector() {
+                protected boolean accept(Option.Value value) {
+                    if (null == value) {
+                        return false;
+                    }
+                    Option option = value.option;
+                    if (compilerFamily.sameFamily(option.getFamily())) {
+                        if (value.prefix.isSet()) {
+                            String compilerClass
+                                = CRSOPTIONS.compilerClassName(option);
+                            if (null == compilerClass) {
+                                result.failureReason =
+                                    "unrecognized compiler: " + value;
+                                throw Values.Selector.STOP;
+                            }
+                            if (!CRSOPTIONS.compilerIsLoadable(option)) {
+                                result.failureReason =
+                                    "unable to load compiler: " + option;
+                                throw Values.Selector.STOP;
+                            }
+                            result.compilerName = compilerClass;
+                        }
+                        return true;
+                    } else if (
+                        CRSOPTIONS.crsIgnoreWarnings.sameOptionIdentifier(
+                            option)) {
+                        result.ignoreWarnings = value.prefix.isSet();
+                        result.ignoreWarningsSet = true;
+                        return true;
+                    }
+                    return false;
+                }
+            };
+            return values.nullify(selector);
+        }
+
+        //        /**
+        //         * This interprets and nullifies values for the test.
+        //         * @param values the Option.Value[] being processed
+        //         * @param result the TestSetup to modify
+        //         * @return false if error (caller should return), true otherwise
+        //         */
+        //        boolean handleTestArgs(Option.Value[] values, TestSetup result) {
+        //            if (!LangUtil.isEmpty(values)) {
+        //                for (int i = 0; i < values.length; i++) {
+        //                    Option.Value value = values[i];
+        //                    if (null == value) {
+        //                        continue;
+        //                    }
+        //                    Option option = value.option;
+        //                    if (option.sameOptionFamily(ECLIPSE_OPTION)) {
+        //                        if (!value.prefix.isSet()) {
+        //                            values[i] = null;
+        //                            continue;
+        //                        }
+        //                        String compilerClass =
+        //                            (String) COMPILER_OPTION_TO_CLASSNAME.get(
+        //                                option);
+        //                        if (null == compilerClass) {
+        //                            result.failureReason =
+        //                                "unrecognized compiler: " + value;
+        //                            return false;
+        //                        }
+        //                        result.compilerName = compilerClass;
+        //                        values[i] = null;
+        //                    } else if (
+        //                        option.sameOptionFamily(crsIgnoreWarnings)) {
+        //                        result.ignoreWarnings = value.prefix.isSet();
+        //                        result.ignoreWarningsSet = true;
+        //                        values[i] = null;
+        //                    }
+        //                }
+        //            }
+        //            return true;
+        //        }
+
         // XXX need keys, cache...
         /** @return index of global in argList, ignoring first char */
         protected int indexOf(String global, ArrayList argList) {
             int max = argList.size();
             for (int i = 0; i < max; i++) {
-                 if (global.equals(((String) argList.get(i)).substring(1))) {
+                if (global
+                    .equals(((String) argList.get(i)).substring(1))) {
                     return i;
-                 }
+                }
             }
             return -1;
         }
-        
+
         /** 
          * Write this out as a compile element as defined in
          * AjcSpecXmlReader.DOCTYPE.
@@ -1087,13 +1260,19 @@ public class CompilerRun implements IAjcRun {
                 out.printAttribute("includeClassesDir", "true");
             }
             if (!LangUtil.isEmpty(argfiles)) {
-                out.printAttribute("argfiles", XMLWriter.flattenFiles(argfiles));
+                out.printAttribute(
+                    "argfiles",
+                    XMLWriter.flattenFiles(argfiles));
             }
             if (!LangUtil.isEmpty(aspectpath)) {
-                out.printAttribute("aspectpath", XMLWriter.flattenFiles(argfiles));
+                out.printAttribute(
+                    "aspectpath",
+                    XMLWriter.flattenFiles(argfiles));
             }
             if (!LangUtil.isEmpty(sourceroots)) {
-                out.printAttribute("sourceroots", XMLWriter.flattenFiles(argfiles));
+                out.printAttribute(
+                    "sourceroots",
+                    XMLWriter.flattenFiles(argfiles));
             }
             out.endAttributes();
             if (!LangUtil.isEmpty(dirChanges)) {
@@ -1102,7 +1281,7 @@ public class CompilerRun implements IAjcRun {
             SoftMessage.writeXml(out, getMessages());
             out.endElement(xmlElementName);
         }
-        
+
         /** 
          * Encapsulate the directives that can be set using
          * global arguments supplied in {@link Spec.getOptions()}.
@@ -1116,22 +1295,22 @@ public class CompilerRun implements IAjcRun {
              * to ignore warnings in its result evaluation
              */
             boolean ignoreWarningsSet;
-            
+
             /** if telling AjcMessageHandler, what we tell it */
             boolean ignoreWarnings;
-            
+
             /** false if setup failed */
             boolean result;
-            
+
             /** if setup failed, this has the reason why */
             String failureReason;
-            
+
             /** beyond running test, also seek text in sources */
             String seek;
-            
+
             /** if setup completed, this has the combined global/local options */
             ArrayList commandOptions;
-            
+
             public Object clone() {
                 TestSetup testSetup = new TestSetup();
                 testSetup.compilerName = compilerName;
@@ -1149,13 +1328,326 @@ public class CompilerRun implements IAjcRun {
             public String toString() {
                 return "TestSetup("
                     + (null == compilerName ? "" : compilerName + " ")
-                    + (!ignoreWarningsSet ?  "" 
+                    + (!ignoreWarningsSet
+                        ? ""
                         : (ignoreWarnings ? "" : "do not ")
                             + "ignore warnings ")
                     + (result ? "" : "setup failed")
                     + ")";
             }
         }
+
+        /**
+         * Options-related stuff in the spec.
+         */
+        static class CRSOptions {
+            //    static final String BUILDER_COMPILER =
+            //        "org.aspectj.ajdt.internal.core.builder.Builder.Command";
+            static final String AJDE_COMPILER =
+                CompileCommand.class.getName();
+
+            static final String AJCTASK_COMPILER =
+                AjcTaskCompileCommand.class.getName();
+
+            private final Map compilerOptionToLoadable = new TreeMap();
+            /*
+            * The options field in a compiler test permits some arbitrary
+            * command-line options to be set.  It does not permit things
+            * like classpath,  aspectpath, files, etc. which are set
+            * using other fields in the test specification, so the options
+            * permitted are a subset of those permitted on the command-line.
+            * 
+            * Global options specified on the harness command-line are
+            * adopted for the compiler command-line if they are permitted
+            * in the options field.  That means we have to detect each
+            * permitted option, rather than just letting all through
+            * for the compiler.
+            * 
+            * Conversely, some options are targeted not at the compiler,
+            * but at the test itself (e.g., to ignore warnings, or to 
+            * select a compiler.
+            * 
+            * The harness can run many compilers, and they differ in
+            * which options are permitted.  You can specify a compiler
+            * as an option (e.g., -eclipse).  So the set of constraints
+            * on the list of permitted options can differ from test to test.
+            * 
+            * The following code sets up the lists of permitted options
+            * and breaks out subsets for different compiler-variant checks.
+            * Most options are created but not named, but some options
+            * are named to detect corresponding values for further 
+            * processing.  e.g., the compiler options are saved so 
+            * we can do per-compiler option verification.
+            *
+            */
+            private final Options crsOptions;
+            private final Family compilerFamily;
+            private final Option crsIncrementalOption;
+            private final Option crsSourceOption;
+            // these are options handled/absorbed by CompilerRun
+            private final Option crsIgnoreWarnings;
+            private final Option eclipseOption;
+            private final Option buildercompilerOption;
+            private final Option ajdecompilerOption;
+            private final Option javacOption;
+            private final Option ajctaskcompilerOption;
+            private final Option ajccompilerOption;
+            private final Map compilerOptionToClassname;
+            private final Set compilerOptions;
+            // compiler verification - permit but flag ajc 1.0 options
+            private final List ajc10Options;
+            private final List invalidOptions;
+
+            private CRSOptions() {
+                crsOptions = new Options(true);
+                Option.Factory factory = new Option.Factory("CompilerRun");
+                // compiler options go in map            
+                eclipseOption =
+                    factory.create(
+                        "eclipse",
+                        "compiler",
+                        Option.FORCE_PREFIXES,
+                        false);
+                compilerFamily = eclipseOption.getFamily();
+                buildercompilerOption =
+                    factory.create(
+                        "builderCompiler",
+                        "compiler",
+                        Option.FORCE_PREFIXES,
+                        false);
+                ajctaskcompilerOption =
+                    factory.create(
+                        "ajctaskCompiler",
+                        "compiler",
+                        Option.FORCE_PREFIXES,
+                        false);
+                ajdecompilerOption =
+                    factory.create(
+                        "ajdeCompiler",
+                        "compiler",
+                        Option.FORCE_PREFIXES,
+                        false);
+                ajccompilerOption =
+                    factory.create(
+                        "ajc",
+                        "compiler",
+                        Option.FORCE_PREFIXES,
+                        false);
+                javacOption =
+                    factory.create(
+                        "javac",
+                        "compiler",
+                        Option.FORCE_PREFIXES,
+                        false);
+
+                Map map = new TreeMap();
+                map.put(eclipseOption, ReflectionFactory.ECLIPSE);
+                //map.put(BUILDERCOMPILER_OPTION, BUILDER_COMPILER);
+                map.put(
+                    ajctaskcompilerOption,
+                    AJCTASK_COMPILER);
+                map.put(ajdecompilerOption, AJDE_COMPILER);
+                map.put(ajccompilerOption, ReflectionFactory.OLD_AJC);
+                //map.put(JAVAC_OPTION, "XXX javac option not supported");
+                compilerOptionToClassname =
+                    Collections.unmodifiableMap(map);
+
+                compilerOptions =
+                    Collections.unmodifiableSet(
+                        compilerOptionToClassname.keySet());
+                // options not permitted in the harness
+                List list = new ArrayList();
+                list.add(factory.create("workingdir"));
+                list.add(factory.create("argfile"));
+                list.add(factory.create("sourceroots"));
+                list.add(factory.create("outjar"));
+                invalidOptions = Collections.unmodifiableList(list);
+
+                // other options added directly
+                crsIncrementalOption = factory.create("incremental");
+
+                crsIgnoreWarnings = factory.create("ignoreWarnings");
+
+                crsSourceOption =
+                    factory
+                        .create(
+                            "source",
+                            "source",
+                            Option.FORCE_PREFIXES,
+                            false,
+                            new String[][] { new String[] { "1.3", "1.4" }
+                });
+
+                // ajc 1.0 options
+                // workingdir above in invalid options
+                list = new ArrayList();
+                list.add(factory.create("usejavac"));
+                list.add(factory.create("preprocess"));
+                list.add(factory.create("nocomment"));
+                list.add(factory.create("porting"));
+                list.add(factory.create("XOcodeSize"));
+                list.add(factory.create("XTargetNearSource"));
+                list.add(factory.create("XaddSafePrefix"));
+                list.add(
+                    factory.create(
+                        "lenient",
+                        "lenient",
+                        Option.FORCE_PREFIXES,
+                        false));
+                list.add(
+                    factory.create(
+                        "strict",
+                        "lenient",
+                        Option.FORCE_PREFIXES,
+                        false));
+                ajc10Options = Collections.unmodifiableList(list);
+
+                // -warn:.. and -g/-g:.. are not exclusive
+                if (!(factory.setupFamily("debug", true)
+                    && factory.setupFamily("warning", true))) {
+                    System.err.println("CompilerRun debug/warning fail!");
+                }
+                Option[] options =
+                    new Option[] {
+                        crsIncrementalOption,
+                        crsIgnoreWarnings,
+                        crsSourceOption,
+                        factory.create(
+                            "Xlint",
+                            "XLint",
+                            Option.FORCE_PREFIXES,
+                            true),
+                        factory.create("verbose"),
+                        factory.create("emacssym"),
+                        factory.create("referenceInfo"),
+                        factory.create("nowarn"),
+                        factory.create("deprecation"),
+                        factory.create("noImportError"),
+                        factory.create("proceedOnError"),
+                        factory.create("preserveAllLocals"),
+                        factory.create(
+                            "warn",
+                            "warning",
+                            Option.STANDARD_PREFIXES,
+                            true),
+                        factory.create(
+                            "g",
+                            "debug",
+                            Option.STANDARD_PREFIXES,
+                            false),
+                        factory.create(
+                            "g:",
+                            "debug",
+                            Option.STANDARD_PREFIXES,
+                            true),
+                        factory.create(
+                            "1.3",
+                            "compliance",
+                            Option.FORCE_PREFIXES,
+                            false),
+                        factory.create(
+                            "1.4",
+                            "compliance",
+                            Option.FORCE_PREFIXES,
+                            false),
+                        factory
+                            .create(
+                                "target",
+                                "target",
+                                Option.FORCE_PREFIXES,
+                                false,
+                                new String[][] { new String[] {
+                                    "1.1",
+                                    "1.2" }}),
+                        factory.create("XnoInline"),
+                        factory.create("XnoWeave"),
+                        factory.create("XserializableAspects")
+                    };
+                for (int i = 0; i < options.length; i++) {
+                    crsOptions.addOption(options[i]);
+                }
+                for (Iterator iter = compilerOptions.iterator();
+                    iter.hasNext();
+                    ) {
+                    crsOptions.addOption((Option) iter.next());
+                }
+                // these are recognized but records with them are skipped
+                for (Iterator iter = ajc10Options.iterator();
+                    iter.hasNext();
+                    ) {
+                    crsOptions.addOption((Option) iter.next());
+                }
+                crsOptions.freeze();
+            }
+
+            Options getOptions() {
+                return crsOptions;
+            }
+            
+            /** 
+             * @return unmodifiable Set of options sharing the
+             * family "compiler".
+             */
+            Set compilerOptions() {
+                return compilerOptions;
+            }
+
+            /**
+             * @param option the compiler Option to get name for
+             * @return null if option is null or not a compiler option,
+             *     or the fully-qualified classname of the ICommand
+             *     implementing the compiler.
+             */
+            String compilerClassName(Option option) {
+                if ((null == option)
+                    || (!compilerFamily.sameFamily(option.getFamily()))) {
+                    return null;
+                }
+                return (String) compilerOptionToClassname.get(option);
+            }
+
+            /**
+             * Check that the compiler class associated with a compiler
+             * option can be loaded.  This check only happens once;
+             * the result is cached (by compilerOption key) for later calls.
+             * @param compilerOption the Option (family compiler) to check
+             * @return true if compiler class for this option can be loaded
+             */
+            boolean compilerIsLoadable(Option compilerOption) {
+                LangUtil.throwIaxIfNull(compilerOption, "compilerName");
+                synchronized (compilerOptionToLoadable) {
+                    Boolean result =
+                        (Boolean) compilerOptionToLoadable.get(
+                            compilerOption);
+                    if (null == result) {
+                        MessageHandler sink = new MessageHandler();
+                        String compilerClassname =
+                            (String) compilerOptionToClassname.get(
+                                compilerOption);
+                        if (null == compilerClassname) {
+                            result = Boolean.FALSE;
+                        } else {
+                            ICommand c =
+                                ReflectionFactory.makeCommand(
+                                    compilerClassname,
+                                    sink);
+
+                            if ((null == c)
+                                || sink.hasAnyMessage(
+                                    IMessage.ERROR,
+                                    true)) {
+                                result = Boolean.FALSE;
+                            } else {
+                                result = Boolean.TRUE;
+                            }
+                        }
+                        compilerOptionToLoadable.put(
+                            compilerOption,
+                            result);
+                    }
+                    return result.booleanValue();
+                }
+            }
+        } // CompilerRun.Spec.CRSOptions
     } // CompilerRun.Spec
-} // CompilerRun
\ No newline at end of file
+} // CompilerRun
\ No newline at end of file
index 9b0b7736c572821b0df65985f432307a023561e7..e4060d50adc9d2df479a2bca7cf893cf14974a8d 100644 (file)
@@ -12,7 +12,7 @@
  *     Xerox/PARC     initial implementation 
  *     Wes Isberg     removed unused globals.
  * ******************************************************************/
+
 package org.aspectj.testing.harness.bridge;
 
 import java.io.File;
@@ -23,71 +23,105 @@ import org.aspectj.util.LangUtil;
 /**
  */
 public class Globals {
-       public static final String  FORK_NAME = "harness.fork";  // XXX in testing-drivers/../package.htm
+    public static final String FORK_NAME = "harness.fork";
+    // XXX in testing-drivers/../package.htm
     /** name/key of the System property to set to define library dir */
     public static final String LIBDIR_NAME = "harness.libdir";
 
     /** name/key of the System property to set to define J2SE_HOME dir */
     public static final String J2SE14_RTJAR_NAME = "j2se14.rtjar";
+    public static final String J2SE13_RTJAR_NAME = "j2se13.rtjar";
 
     /** assumed relative location of a library with required jars */
-    public static final String LIBDIR = getSystemProperty(LIBDIR_NAME, "../lib/test");
+    public static final String LIBDIR =
+        getSystemProperty(LIBDIR_NAME, "../lib/test");
 
     /** Path to J2SE_HOME */
-    public static final File J2SE14_RTJAR; // XXX used only by 1.0 compiler tests - deprecate?
+    public static final File J2SE14_RTJAR;
+    // XXX used only by 1.0 compiler tests - deprecate?
+    public static final File J2SE13_RTJAR;
 
     /** array of parameter types for main(String[]) */
-    public static final Class[] MAIN_PARM_TYPES = new Class[] {String[].class};    
-    public static final String S_testingclient_jar  = LIBDIR + "/testing-client.jar";
-    public static final String S_aspectjrt_jar      = LIBDIR + "/aspectjrt.jar";
-    public static final File F_testingclient_jar    = new File(S_testingclient_jar);
-    public static final File F_aspectjrt_jar        = new File(S_aspectjrt_jar);
+    public static final Class[] MAIN_PARM_TYPES =
+        new Class[] { String[].class };
+    public static final String S_testingclient_jar =
+        LIBDIR + "/testing-client.jar";
+    public static final String S_aspectjrt_jar = LIBDIR + "/aspectjrt.jar";
+    public static final File F_testingclient_jar =
+        new File(S_testingclient_jar);
+    public static final File F_aspectjrt_jar = new File(S_aspectjrt_jar);
     public static final boolean globalsValid;
-    
+
     static {
+        J2SE13_RTJAR =
+            getRtJarFor(J2SE13_RTJAR_NAME, "c:/home/apps/jdk13");
+        J2SE14_RTJAR =
+            getRtJarFor(J2SE14_RTJAR_NAME, "c:/home/apps/jdk14");
+
+        String forkSpec = getSystemProperty(FORK_NAME, null);
+        globalsValid =
+            (FileUtil.canReadFile(F_testingclient_jar)
+                && FileUtil.canReadFile(F_aspectjrt_jar)
+                && FileUtil.canReadFile(J2SE13_RTJAR)
+                && FileUtil.canReadFile(J2SE14_RTJAR));
+    }
+
+    private static File getRtJarFor(
+        String propertyName,
+        String defaultLocation) {
         File j2seJar = null;
-        String path = getSystemProperty(J2SE14_RTJAR_NAME, "c:/home/apps/jdk14");
-        File j2seHome = new File(path);
-        if (j2seHome.exists() && j2seHome.isDirectory()) {
-            File rtjar = new File(j2seHome.getAbsolutePath() + "/jre/lib/rt.jar");
-            if (rtjar.canRead() && rtjar.isFile()) {
-                j2seJar = rtjar;
+        try {
+            String path = getSystemProperty(propertyName, defaultLocation);
+            File file = new File(path);
+            if (file.exists()) {
+                File rtjar = null;
+                if (path.endsWith("rt.jar")) {
+                    rtjar = file;
+                } else if (file.isDirectory()) {
+                    path = file.getAbsolutePath() + "/jre/lib/rt.jar";
+                    rtjar = new File(path);                            
+                }
+                if (rtjar.canRead() && rtjar.isFile()) {
+                    j2seJar = rtjar;
+                }
+            }
+        } catch (Throwable t) { // avoid at all costs during static init
+            try {
+                t.printStackTrace(System.err);
+            } catch (Throwable x) {
+                // unable to log, better to just fail...
             }
         }
-        J2SE14_RTJAR = j2seJar;
-       globalsValid = 
-               (FileUtil.canReadFile(F_testingclient_jar)
-                 && FileUtil.canReadFile(F_aspectjrt_jar)
-              && FileUtil.canReadFile(J2SE14_RTJAR)
-            );
+        return j2seJar;
     }
 
-       /**
-        
-        * @return null if not found, or 
+    /**
+     * 
+     * @return null if not found, or 
      *          String with class path for compiler to load J2SE 1.4 classes from.
-        */
-       public static String get14Bootclasspath() {
+     */
+    public static String get14Bootclasspath() {
         return null;
-       }
+    }
 
-       /**
-        * Get System property completely safely.
-        * @param propertyName the String name of the property to get
-        * @param defaultValue the String default value to return value is null or empty
-        * @return String value or defaultValue if not available.
-        */
-       static String getSystemProperty(
-               String propertyName,
-               String defaultValue) {
+    /**
+     * Get System property completely safely.
+     * @param propertyName the String name of the property to get
+     * @param defaultValue the String default value to return value is null or empty
+     * @return String value or defaultValue if not available.
+     */
+    static String getSystemProperty(
+        String propertyName,
+        String defaultValue) {
         String result = defaultValue;
         try {
             String value = System.getProperty(propertyName);
             if (!LangUtil.isEmpty(value)) {
                 result = value;
             }
-        } catch (Throwable t) {}
-        return result;      
-       }
+        } catch (Throwable t) {
+        }
+        return result;
+    }
 
 }
diff --git a/testing/src/org/aspectj/testing/util/options/Option.java b/testing/src/org/aspectj/testing/util/options/Option.java
new file mode 100644 (file)
index 0000000..8591792
--- /dev/null
@@ -0,0 +1,707 @@
+/* *******************************************************************
+ * Copyright (c) 2003 Contributors.
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Wes Isberg     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util.options;
+
+import java.util.*;
+
+import org.aspectj.util.LangUtil;
+
+/**
+ * Immutable schema for an input (command-line) option.
+ * The schema contains the expected name/label,
+ * the family (for comparison purposes), 
+ * and permitted prefixes.
+ * This has operations to accept input values and compare options.
+ * Options cannot be created directly; for that, use an 
+ * <code>Option.Factory</code>, since it enforces uniqueness
+ * within the families and options created by the factory.
+ * <p>
+ * Option is used with related nested classes to implement relations:
+ * <ul>
+ * <li>Option.Factory produces Option</li>
+ * <li>An Option has a set of Option.Prefixes, 
+ *     which are variants of Option.Prefix 
+ *     valid for the option (e.g., on/set, force-off, and force-on)</li>
+ * <li>Option evaluates input, produces Option.Value</li>
+ * <li>Related instances of Option share an Option.Family, 
+ *     which enforce option exclusion, etc.</li>
+ * </ul>
+ * The classes are nested as "friends" in order to hide private
+ * members (esp. constructors) that can be used within relations.
+ * Most of these classes are immutable.
+ */
+public class Option implements Comparable {
+    public static final Prefix ON = new Prefix("-", "on", true, false);
+    public static final Prefix NONE = new Prefix("", "none", true, false);
+    public static final Prefix FORCE_ON =
+                                new Prefix("!", "force-on", true, true);
+
+    public static final Prefix FORCE_OFF =
+                                new Prefix("^", "force-off", false, true);
+    public static final Prefixes LITERAL_PREFIXES =
+        new Prefixes(new Prefix[] { NONE });
+    public static final Prefixes STANDARD_PREFIXES =
+        new Prefixes(new Prefix[] { ON });
+    public static final Prefixes FORCE_PREFIXES =
+        new Prefixes(new Prefix[] { ON, FORCE_ON, FORCE_OFF });
+
+    /** family is the key for comparing two options */
+    private final Family family;
+
+    /** label expected for the option */
+    private final String name;
+
+    /** unique identifier for option */
+    private final String optionIdentifier;
+
+    /** prefixes permitted for the option in input */
+    private final Prefixes permittedPrefixes;
+
+    /** if true, then match on input that has extra suffix beyond prefix and name */
+    private final boolean acceptSuffixedInput;
+
+    /** 
+     * If true, no collision if there are multiple values 
+     * that share the same family but not the same literal value 
+     */
+    private final boolean permitMultipleValues;
+
+    /** int number of arguments expected after the option itself */
+    private final int numArguments;
+
+    /**
+     * If numArguments > 0, each element has a list of arguments
+     * permitted at that index from the initial matching value.
+     * Elements are not null.
+     */
+    private final String[][] permittedArguments;
+
+    private final int nameLength;
+
+    /*
+     * Create a standard named boolean option,
+     * permitting force-on and force-off.
+     * @param name the String name of the option, e.g., "1.3" for "-1.3"
+     * @param family
+     * @param permittedPrefixes
+     * @param acceptSuffixedInput
+     * @param permittedArguments
+     */
+    public Option(
+        String name,
+        Family family,
+        Prefixes permittedPrefixes,
+        boolean acceptSuffixedInput,
+        String[][] permittedArguments) {
+        LangUtil.throwIaxIfNull(name, "name");
+        LangUtil.throwIaxIfNull(family, "family");
+        LangUtil.throwIaxIfNull(permittedPrefixes, "permittedPrefixes");
+        this.name = name;
+        this.nameLength = name.length();
+        this.family = family;
+        this.permittedPrefixes = permittedPrefixes;
+        this.acceptSuffixedInput = acceptSuffixedInput;
+        this.permitMultipleValues = false;
+        if (LangUtil.isEmpty(permittedArguments)) {
+            permittedArguments = new String[][] { };
+            // nominal, unused
+        } else {
+            String[][] temp = new String[permittedArguments.length][];
+            for (int i = 0; i < temp.length; i++) {
+                String[] toCopy = permittedArguments[i];
+                LangUtil.throwIaxIfNull(toCopy, "no permitted args");
+                final int len = toCopy.length;
+                String[] variants = new String[len];
+                System.arraycopy(toCopy, 0, variants, 0, len);
+                temp[i] = variants;
+            }
+            permittedArguments = temp;
+        }
+        this.permittedArguments = permittedArguments;
+        numArguments = permittedArguments.length;
+        optionIdentifier = family.familyName + "." + name;
+    }
+
+    public int compareTo(Object other) {
+        Option option = (Option) other;
+        int diff = family.compareTo(option.family);
+        if (0 == diff) {
+            diff = name.compareTo(option.name);
+        }
+        return diff;
+    }
+
+    public Family getFamily() {
+        return family;
+    }
+
+    public boolean permitMultipleValues() {
+        return permitMultipleValues;
+    }
+
+    /**
+     * @return int number of elements in this option,
+     *   e.g., 0 for -g or 1 for -source 1.4
+     */
+    public int numArguments() {
+        return numArguments;
+    }
+
+    /**
+     * If this value String represents a valid input for this option,
+     * then create and return the associated Value.
+     * 
+     * @param value the Value created, or null if invalid
+     * @return Value if this value is permitted by this option
+     */
+    public Value acceptValue(String value) {
+        Prefix prefix = hasPrefix(value);
+        if (null != prefix) {
+            if (value.startsWith(name, prefix.length())) {
+                value = value.substring(prefix.length());
+                if (value.length() == nameLength) {
+                    return new Value(value, prefix, this);
+                } else if (acceptSuffixedInput) {
+                    return new Value(value, prefix, this);
+                } else {
+                    return rejectingSuffixedInput(value);
+                }
+            }
+        }
+        return null;
+    }
+
+    /** @return true if we have same option family */
+    public boolean sameOptionFamily(Option other) {
+        return ((null != other) && other.family.equals(family));
+    }
+
+    /** @return true if we have same option family and name */
+    public boolean sameOptionIdentifier(Option other) {
+        return (sameOptionFamily(other) && name.equals(other.name));
+    }
+
+    public String toString() {
+        return name;
+    }
+
+    /** 
+     * Called when ignoreSuffix is off but we got value with suffix.
+     */
+    protected Value rejectingSuffixedInput(String value) {
+        return null;
+    }
+
+    /** 
+     * Verify that the input is permitted at this position.
+     * @param input the String input to check for validity
+     * @param position the int proposed position (0-based) 
+     *   for the input (position 0 is for first argument)
+     * @return null if this input is valid at this position,
+     *         or a String error message otherwise.
+     */
+    String validArgument(String input, int position) {
+        if (null == input) {
+            return "null input";
+        }
+        // assert numArguments == permittedInput.length
+        if ((position < 0) || (position >= numArguments)) {
+            return "no input permitted at " + position;
+        }
+        String[] permitted = permittedArguments[position];
+        for (int i = 0; i < permitted.length; i++) {
+            if (input.equals(permitted[i])) {
+                return null;
+            }
+        }
+        return input + " not permitted, expecting one of "
+            + Arrays.asList(permitted);
+    }
+
+    String getName() {
+        return name;
+    }
+    Object getKey() {
+        return family;
+    }
+
+    private String optionIdentifier() {
+        return optionIdentifier;
+    }
+
+    private Prefix hasPrefix(String value) {
+        for (Iterator iter = permittedPrefixes.iterator();
+            iter.hasNext();
+            ) {
+            Prefix prefix = (Prefix) iter.next();
+            if (-1 != prefix.prefixLength(value)) {
+                return prefix;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * An option family identifies a set of related options that
+     * might share no literal specification.
+     * E.g., the compiler family of options might include
+     * -ajc and -eclipse, and the debugInfo family of options
+     * might include -g and -g:vars.
+     * Option families may permit or avoid option collisions.
+     * <p>
+     * For subclasses to permit some collisions and not others,
+     * they should set permitMultipleFamilyValues to false 
+     * and implement <code>doCollision(Option, Option)</code>.
+     * <p>
+     * This relies on Factory to ensure that familyName is
+     * a unique identifier for the factory.
+     */
+    public static class Family implements Comparable {
+
+        /** unique String identifier for this family */
+        private final String familyName;
+
+        /** if true, then report no collisions */
+        private final boolean permitMultipleFamilyValues;
+
+        protected Family(
+            String familyName,
+            boolean permitMultipleFamilyValues) {
+            this.familyName = familyName;
+            this.permitMultipleFamilyValues = permitMultipleFamilyValues;
+        }
+
+        public int compareTo(Object arg0) {
+            Family family = (Family) arg0;
+            return familyName.compareTo(family.familyName);
+        }
+
+        public boolean sameFamily(Family family) {
+            return (
+                (null != family) && familyName.equals(family.familyName));
+        }
+
+        boolean permitMultipleFamilyValues() {
+            return permitMultipleFamilyValues;
+        }
+
+        /**
+         * Options collide if they share the same family
+         * but are not the same,
+         * and multiple values are not permitted by the family.
+         * @param lhs the Option to compare with rhs
+         * @param rhs the Option to compare with lhs
+         * @return true if the two options collide, false otherwise
+         * @throws IllegalArgumentException if the input differ
+         *         and share the same family, but this isn't it.
+         */
+        public final boolean collision(Option lhs, Option rhs) {
+            if ((lhs == rhs) || (null == lhs) || (null == rhs)) {
+                return false;
+            }
+            Family lhsFamily = lhs.getFamily();
+            Family rhsFamily = rhs.getFamily();
+            if (!(lhsFamily.sameFamily(rhsFamily))) {
+                return false;
+            }
+            if (lhs.sameOptionIdentifier(rhs)) {
+                return false;
+            }
+            if (this != lhsFamily) {
+                String s =
+                    "expected family " + this +" got family " + lhsFamily;
+                throw new IllegalArgumentException(s);
+            }
+            return doCollision(lhs, rhs);
+        }
+
+        /**
+         * Subclasses implement this to resolve collisions on
+         * a case-by-case basis.  Input are guaranteed to be 
+         * non-null, different, and to share this family.
+         * This implementation returns
+         * <code>!permitMultipleFamilyValues</code>.
+         * 
+         * @param lhs the Option to compare
+         * @param rhs the other Option to compare
+         * @return true if there is a collision.
+         */
+        protected boolean doCollision(Option lhs, Option rhs) {
+            return !permitMultipleFamilyValues;
+        }
+    }
+
+    /** 
+     * A factory enforces a namespace on options.
+     * All options produced from a given factory are unique,
+     * as are all families.
+     * Once an option or family is created, it cannot be changed.
+     * To have a family permit multiple values
+     * (i.e., ignore collisions), set up the family before any
+     * associated options are created using
+     * <code>setupFamily(String, boolean)</code>.
+     */
+    public static class Factory {
+        private final String factoryName;
+
+        /** enforce uniqueness of family */
+        private final Map familyNameToFamily = new TreeMap();
+
+        /** enforce uniqueness of options */
+        private final ArrayList names = new ArrayList();
+
+        public Factory(String factoryName) {
+            this.factoryName = factoryName;
+        }
+
+        /**
+         * Ensure that the family with this name has the 
+         * specified permission.  If the family does not exist,
+         * it is created.  If it does, the permission is checked.
+         * If this returns false, there is no way to change the
+         * family permission.
+         * @param name the String identifier for the family
+         * @param permitMultipleValues the boolean permission whether to
+         *  allow multiple values in this family
+         * @return true if family exists with this name and permission
+         */
+        public boolean setupFamily(
+            String name,
+            boolean permitMultipleValues) {
+            LangUtil.throwIaxIfNull(name, "name");
+            Family family;
+            synchronized (familyNameToFamily) {
+                family = (Family) familyNameToFamily.get(name);
+                if (null == family) {
+                    family = new Family(name, permitMultipleValues);
+                    familyNameToFamily.put(name, family);
+                } else if (
+                    permitMultipleValues
+                        != family.permitMultipleFamilyValues) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        /**
+         * Register a family with this factory.
+         * @return null if the family was successfully registered,
+         *        or a String error otherwise
+         */
+        public String registerFamily(Family family) {
+            if (null == family) {
+                return "null family";
+            }
+            synchronized (familyNameToFamily) {
+                Family knownFamily =
+                    (Family) familyNameToFamily.get(family.familyName);
+                if (null == knownFamily) {
+                    familyNameToFamily.put(family.familyName, family);
+                } else if (!knownFamily.equals(family)) {
+                    return "different family registered, have "
+                        + knownFamily
+                        + " registering "
+                        + family;
+                }
+            }
+
+            return null;
+        }
+
+        public Option create(String name) {
+            return create(name, name, FORCE_PREFIXES, false);
+        }
+
+        public Option create(
+            String name,
+            String family,
+            Prefixes permittedPrefixes,
+            boolean acceptSuffixedInput) {
+            return create(
+                name,
+                family,
+                permittedPrefixes,
+                acceptSuffixedInput,
+                (String[][]) null);
+        }
+
+        public Option create(
+            String name,
+            String family,
+            Prefixes permittedPrefixes,
+            boolean acceptSuffixedInput,
+            String[][] permittedArguments) {
+            LangUtil.throwIaxIfNull(name, "name");
+            LangUtil.throwIaxIfNull(family, "family");
+            LangUtil.throwIaxIfNull(
+                permittedPrefixes,
+                "permittedPrefixes");
+            Family resolvedFamily;
+            synchronized (familyNameToFamily) {
+                resolvedFamily = (Family) familyNameToFamily.get(family);
+                if (null == resolvedFamily) {
+                    resolvedFamily = new Family(family, false);
+                    familyNameToFamily.put(family, resolvedFamily);
+                }
+            }
+            Option result =
+                new Option(
+                    name,
+                    resolvedFamily,
+                    permittedPrefixes,
+                    acceptSuffixedInput,
+                    permittedArguments);
+            synchronized (names) {
+                String optionIdentifier = result.optionIdentifier();
+                if (names.contains(optionIdentifier)) {
+                    String s = "not unique: " + result;
+                    throw new IllegalArgumentException(s);
+                } else {
+                    names.add(optionIdentifier);
+                }
+            }
+            return result;
+        }
+
+        public String toString() {
+            return Factory.class.getName() + ": " + factoryName;
+        }
+
+        private void checkUnique(Option result) {
+            String name = result.family + "." + result.name;
+        }
+    }
+
+    /**
+     * The actual input value for an option.
+     * When an option takes arguments, all the arguments 
+     * are absorbed/flattened into its value.
+     */
+    public static class Value {
+        private static final String FLATTEN_DELIM = "_";
+
+        private static final int NOTARGUMENT = -1;    
+
+        private static String flatten(String prefix, String suffix) {
+            return prefix + FLATTEN_DELIM + suffix;
+        }
+
+        private static String[] unflatten(Value value) {
+            if (value.argIndex == Value.NOTARGUMENT) {
+                return new String[] { value.value };
+            }
+            StringTokenizer st =
+                new StringTokenizer(value.value, FLATTEN_DELIM);
+            String[] result = new String[st.countTokens()];
+            // assert result.length == 1+inputIndex
+            for (int i = 0; i < result.length; i++) {
+                result[i] = st.nextToken();
+            }
+            return result;
+
+        }
+
+        public final String value;
+        public final Prefix prefix;
+        public final Option option;
+        private final int argIndex;
+
+        private Value(
+            String value,
+            Prefix prefix,
+            Option option) {
+            this(value, prefix, option, NOTARGUMENT);
+        }
+        private Value(
+            String value,
+            Prefix prefix,
+            Option option,
+            int argIndex) {
+            this.value = value;
+            this.prefix = prefix;
+            this.option = option;
+            this.argIndex = argIndex;
+            // asserts deferred - local clients only
+            // assert null != value
+            // assert null != prefix
+            // assert null != option
+            // assert 0 <= inputIndex
+            // assert inputIndex <= option.numArguments()
+            // assert {number of DELIM} == argIndex
+        }
+
+        public String[] unflatten() {
+            return unflatten(this);
+        }
+
+        /**
+         * Create new value same as this, but with new prefix.
+         * If the prefix is the same, return this.
+         * @param prefix the Prefix to convert to
+         * @return Value with new prefix - never null
+         */
+        public Value convert(Prefix prefix) {
+            LangUtil.throwIaxIfNull(prefix, "prefix");
+            if (this.prefix.equals(prefix)) {
+                return this;
+            }
+            return new Value(
+                this.value,
+                prefix,
+                this.option,
+                this.argIndex);
+        }
+
+        /** 
+         * 
+         * @param other
+         * @return true if other == this for purposes of collisions
+         */
+        public boolean sameValueIdentifier(Value other) {
+            return (
+                (null != other)
+                    && sameValueIdentifier(option, other.value));
+        }
+
+        public boolean sameValueIdentifier(Option option, String value) {
+            return (
+                (null != option)
+                    && this.option.sameOptionIdentifier(option)
+                    && this.value.equals(value));
+        }
+
+        public boolean conflictsWith(Value other) {
+            return (
+                (null != other)
+                    && option.equals(other.option)
+                    && (prefix.force == other.prefix.force)
+                    && ((prefix.set != other.prefix.set)
+                        || !value.equals(other.value)));
+        }
+
+        public String toString() {
+            return option + "=" + prefix + value;
+        }
+
+        final Value nextInput(String input) throws InvalidInputException {
+            final int index = argIndex + 1;
+            String err = option.validArgument(input, index);
+            if (null != err) {
+                throw new InvalidInputException(err, input, option);
+            }
+            return new Value(flatten(value, input), prefix, option, index);
+        }
+    }
+
+    /**
+     * A bunch of prefixes.
+     */
+    public static class Prefixes {
+        final List list;
+        private Prefixes(Prefix[] prefixes) {
+            if (LangUtil.isEmpty(prefixes)) {
+                list = Collections.EMPTY_LIST;
+            } else {
+                list =
+                    Collections.unmodifiableList(
+                        Arrays.asList(
+                            LangUtil.safeCopy(prefixes, new Prefix[0])));
+            }
+        }
+        public Iterator iterator() {
+            return list.iterator();
+        }
+    }
+
+    /**
+     * A permitted prefix for an option, mainly so that options
+     * "-verbose", "^verbose", and "!verbose" can be treated
+     * as variants (set, force-off, and force-on) of the
+     * same "verbose" option.
+     */
+    public static class Prefix {
+        private final String prefix;
+        private final int prefixLength;
+        private final String name;
+        private final boolean set;
+        private final boolean force;
+        private Prefix(
+            String prefix,
+            String name,
+            boolean set,
+            boolean force) {
+            this.prefix = prefix;
+            this.name = name;
+            this.set = set;
+            this.force = force;
+            this.prefixLength = prefix.length();
+        }
+
+        /** 
+         * Render a value for input if this is set.
+         * @param value the String to render as an input value
+         * @return null if value is null or option is not set,
+         *    "-" + value otherwise
+         */
+        public String render(String value) {
+            return ((!set || (null == value)) ? null : "-" + value);
+        }
+
+        boolean forceOff() {
+            return force && !set;
+        }
+        boolean forceOn() {
+            return force && set;
+        }
+        public boolean isSet() {
+            return set;
+        }
+        private int length() {
+            return prefixLength;
+        }
+        private int prefixLength(String input) {
+            if ((null != input) && input.startsWith(prefix)) {
+                return length();
+            }
+            return -1;
+        }
+        public String toString() {
+            return prefix;
+        }
+    }
+
+    /**
+     * Thrown when an Option specifies required arguments,
+     * but the arguments are not available.
+     */
+    public static class InvalidInputException extends Exception {
+        public final String err;
+        public final String input;
+        public final Option option;
+        InvalidInputException(String err, String input, Option option) {
+            super(err);
+            this.err = err;
+            this.input = input;
+            this.option = option;
+        }
+        public String getFullMessage() {
+            return "illegal input \""
+                + input
+                + "\" for option "
+                + option
+                + ": "
+                + err;
+        }
+    }
+}
diff --git a/testing/src/org/aspectj/testing/util/options/Options.java b/testing/src/org/aspectj/testing/util/options/Options.java
new file mode 100644 (file)
index 0000000..642ea0c
--- /dev/null
@@ -0,0 +1,152 @@
+/* *******************************************************************
+ * Copyright (c) 2003 Contributors.
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Wes Isberg     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util.options;
+
+import java.util.*;
+
+import org.aspectj.util.LangUtil;
+
+/**
+ * A bunch of options that handle search boilerplate.
+ * This enforces an initialization phase by permitting
+ * options to be added only until frozen, and 
+ * permitting matching only after frozen.
+ */
+public class Options {
+
+    /** if true, then perform extra checks to debug problems */
+    private static final boolean verifying = false;
+    private static final boolean FROZEN = true;
+
+    /**
+     * List input unmatched by options, if any.
+     * @param input the String[] used to generate the values
+     * @param values the Option.Value[] found from the input
+     * @return null if no values are null, String list of missed otherwise
+     */
+    public static String missedMatchError(
+        String[] input,
+        Values values) {
+        int[] missed = values.indexMissedMatches();
+        LangUtil.throwIaxIfNull(input, "input");
+        LangUtil.throwIaxIfNull(values, "values");
+        LangUtil.throwIaxIfFalse(
+            input.length == values.length(),
+            "input is not matched by values");
+        if (0 == missed.length) {
+            return null;
+        }
+        StringBuffer sb = new StringBuffer();
+        sb.append("missed values: [");
+        for (int i = 0; i < missed.length; i++) {
+            if (i > 0) {
+                sb.append(", ");
+            }
+            sb.append(missed[i] + ": " + input[missed[i]]);
+        }
+        sb.append("]");
+        return sb.toString();
+    }
+
+    private final ArrayList options = new ArrayList();
+    private final boolean stopAtFirstMatch;
+    private boolean frozen = !FROZEN;
+
+    public Options(boolean stopAtFirstMatch) {
+        this.stopAtFirstMatch = stopAtFirstMatch;
+    }
+    
+    public void freeze() {
+        if (frozen != FROZEN) {
+            frozen = FROZEN;
+        } 
+    }
+
+    public boolean isFrozen() {
+        return (frozen == FROZEN);
+    }
+
+    public void addOption(Option option) {
+        checkFrozen("adding option", !FROZEN);
+        LangUtil.throwIaxIfNull(option, "option");
+        options.add(option);
+    }
+
+    /**
+     * Associate options matched, if any, with input by index.
+     * If an input element is not matched, the corresponding
+     * result element will be null.
+     * If there are multi-argument options matched, then
+     * only the initial element will be non-null, but it
+     * will contain the accumulated value of the arguments.
+     * @param input the String[] of input
+     * @return Option.Value[] corresponding to input
+     * @throws Option.InvalidInputException when encountering
+     *         invalid arguments to a matched multi-argument option.
+     */
+    public Values acceptInput(String[] input)
+        throws Option.InvalidInputException {
+        checkFrozen("matching options", FROZEN);
+        if ((null == input) || (0 == input.length)) {
+            return Values.EMPTY;
+        }
+        Option.Value[] results = new Option.Value[input.length];
+        for (int i = 0; i < input.length; i++) {
+            Option.Value result = firstMatch(input[i]);
+            final int index = i;
+            if (null != result) {
+                for (int len = result.option.numArguments();
+                    len > 0;
+                    len--) {
+                    i++;
+                    if (i >= input.length) {
+                        throw new Option.InvalidInputException(
+                            "not enough arguments",
+                            null,
+                            result.option);
+                    }
+                    result = result.nextInput(input[i]);
+                }
+            }
+            results[index] = result;
+        }
+        return Values.wrapValues(results);
+    }
+
+    private void checkFrozen(String actionLabel, boolean expectFrozen) {
+        if (expectFrozen != isFrozen()) {
+            if (null == actionLabel) {
+                actionLabel = "use";
+            }
+            if (expectFrozen) {
+                actionLabel = "must freeze before " + actionLabel;
+            } else {
+                actionLabel = "frozen before " + actionLabel;
+            }
+            throw new IllegalStateException(actionLabel);
+        }
+    }
+
+    private Option.Value firstMatch(String value) {
+        LangUtil.throwIaxIfNull(value, "value");
+        ArrayList list = new ArrayList();
+        for (Iterator iter = options.iterator(); iter.hasNext();) {
+            Option option = (Option) iter.next();
+            Option.Value result = option.acceptValue(value);
+            if (null != result) {
+                return result;
+            }
+        }
+        return null;
+    }
+}
diff --git a/testing/src/org/aspectj/testing/util/options/Values.java b/testing/src/org/aspectj/testing/util/options/Values.java
new file mode 100644 (file)
index 0000000..21cd3b5
--- /dev/null
@@ -0,0 +1,714 @@
+/* *******************************************************************
+ * Copyright (c) 2003 Contributors.
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Wes Isberg     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util.options;
+
+import java.util.*;
+
+import org.aspectj.testing.util.options.Option.Value;
+import org.aspectj.util.LangUtil;
+
+/**
+ * Wrapper for Value[] that handles search boilerplate.
+ */
+public class Values {
+    public static final Values EMPTY;
+    /** used by methods taking Selector to halt processing early */
+    private static final boolean VERIFYING = true;
+    private static final boolean FIND_ALL = true;
+
+    private static final String NO_ERROR = "no error";
+
+    static {
+        EMPTY = new Values(new Value[0]);
+    }
+
+    public static Values wrapValues(Value[] values) {
+        if ((null == values) || (0 == values.length)) {
+            return EMPTY;
+        }
+        return new Values(values);
+    }
+
+    public static Values wrapValues(Values[] values) {
+        if ((null == values) || (0 == values.length)) {
+            return EMPTY;
+        }
+        Value[] input = null;
+        if (values.length == 1) {
+            input = values[0].asArray();
+            LangUtil.throwIaxIfNull(input, "values");
+        } else {
+            int length = 0;
+            for (int i = 0; i < values.length; i++) {            
+                if (values[i] == null) {
+                    LangUtil.throwIaxIfNull(null, "null value[" + i + "]");
+                }
+                length += values[i].length();
+            }
+            input = new Value[length];
+            length = 0;
+            Value[] temp;
+            for (int i = 0; i < values.length; i++) {
+                temp = values[i].asArray();
+                System.arraycopy(temp, 0, input, length, temp.length);
+                length += temp.length;            
+            }
+        }
+        return new Values(input);
+    }
+    
+    static int[] invert(int[] missed, int length) {
+        final int MAX = length;
+        final int len = MAX - missed.length;
+        final int[] result = new int[len];
+        int missedIndex = 0;
+        int resultIndex = 0;
+        for (int counter = 0; counter < MAX; counter++) {
+            // catch result up to missed
+            while (((missedIndex >= missed.length)
+                || (missed[missedIndex] > counter))
+                && (counter < MAX)) {
+                result[resultIndex++] = counter++;
+            }
+            // absorb missed up to counter
+            while ((missedIndex < missed.length)
+                && (missed[missedIndex] <= counter)
+                && (counter < MAX)) {
+                missedIndex++;
+            }
+        }
+        return result;
+    }
+
+    private static Option.Value[] toArray(ArrayList list) {
+        return (Option.Value[]) list.toArray(new Option.Value[0]);
+    }
+
+    /**
+     * Resolve input to remove any values matching the same options,
+     * where option conflicts are handled by option forcing.
+     * First, for any force-off value, all matching set-on and
+     * the force-off itself are removed.  At this time, if there
+     * is a matching force-on, then this will return an error.
+     * Next, for any force-on value, it is converted to set-on,
+     * and any other matching set-on value is removed.
+     * Finally, this signals a collision if two values share 
+     * the same option family and the family reports that this is
+     * a collision.
+     * In all cases, only the first error detected is reported.
+     * @param input the Option.Value[] matched from the input,
+     *              (forced/duplicate options will be set to null)
+     * @return String error during resolution, or null if no error
+     */
+    private static String resolve(Option.Value[] input) {
+        String err = null;
+        if (LangUtil.isEmpty(input)) {
+            return null;
+        }
+
+        Map familyToMatches = new TreeMap();
+        for (int i = 0;(null == err) && (i < input.length); i++) {
+            if (null != input[i]) {
+                Option.Family family = input[i].option.getFamily();
+                int[] matches = (int[]) familyToMatches.get(family);
+                if (null == matches) {
+                    matches = match(input, i);
+                    familyToMatches.put(family, matches);
+                }
+            }
+        }
+
+        familyToMatches = Collections.unmodifiableMap(familyToMatches);
+        for (Iterator iter = familyToMatches.entrySet().iterator();
+            (null == err) && iter.hasNext();
+            ) {
+            Map.Entry entry = (Map.Entry) iter.next();
+            int[] matches = (int[]) entry.getValue();
+            err = resolve(input, matches);
+        }
+        return err;
+    }
+
+    /**
+     * Resolve all related options into one 
+     * by nullifying or modifying the values.
+     * 
+     * First, for any force-off value, 
+     * remove all identical set-on 
+     * and the force-off itself, 
+     * and alert on any identical force-on.
+     *
+     * Next, for any force-on value, 
+     * convert to set-on,
+     * throw Error on any same-family force-off value,
+     * remove any identical force-on or set-on value,
+     * alert on any other non-identical same-family force-on value,
+     * remove any same-family set-on value,
+     * and alert on any same-family set-off value.
+     * 
+     * Finally, alert if any two remaining values share 
+     * the same option family, unless the option is marked
+     * as permitting multiple values.
+     * 
+     * @param input the Option.Value[] matching the input
+     * @param matches the int[] list of indexes into input for
+     *      values for related by option
+     *     (all such values must have option matched by family)
+     * @return String error, if any, or null if no error
+     * @see #match(Option.Value[], int)
+     */
+    private static String resolve(Option.Value[] input, int[] matches) {
+        String err = null;
+        // seek force-off
+        Option.Value forceOff = null;
+        Option option = null;
+        // find and remove any force-off
+        for (int i = 0;(null == err) && (i < matches.length); i++) {
+            Option.Value value = input[matches[i]];
+            if (null != value) {
+                // verify that matches are in the same family
+                if (VERIFYING) {
+                    if (null == option) {
+                        option = value.option;
+                    } else if (!(option.sameOptionFamily(value.option))) {
+                        String s =
+                            value.option
+                                + " has different family from "
+                                + option;
+                        throw new IllegalArgumentException(s);
+                    }
+                }
+                if (value.prefix.forceOff()) {
+                    err = removeForceOff(input, value, matches);
+                }
+            }
+        }
+        // find and set any force-on, removing others
+        for (int i = 0;(null == err) && (i < matches.length); i++) {
+            Option.Value value = input[matches[i]];
+            if (null != value) {
+                if (value.prefix.forceOn()) {
+                    err = convertForceOn(input, i, matches);
+                }
+            }
+        }
+        // remove any exact duplicates
+        for (int i = 0;(null == err) && (i < matches.length); i++) {
+            Option.Value value = input[matches[i]];
+            if (null != value) {
+                for (int j = i + 1; j < matches.length; j++) {
+                    if (value.sameValueIdentifier(input[matches[j]])) {
+                        input[matches[j]] = null;
+                    }
+                }
+            }
+        }
+        // signal error if two left unless permitMultipleFamilyValues
+        Option.Value first = null;
+        for (int i = 0;(null == err) && (i < matches.length); i++) {
+            Option.Value value = input[matches[i]];
+            if (null != value) {
+                if (null == first) {
+                    first = value;
+                    if (first
+                        .option
+                        .getFamily()
+                        .permitMultipleFamilyValues()) {
+                        break;
+                    }
+                } else {
+                    err = "collision between " + first + " and " + value;
+                }
+            }
+        }
+
+        return err;
+    }
+
+    /**
+     * For any force-off value, 
+     * remove all  set-on or force-off with same value
+     * (including the force-off itself), 
+     * and alert on any identical force-on.
+     * @param input the Option.Value[] matching the input
+     * @param value the force-off Option.Value to remove
+     * @param matches the int[] list of indexes into input for
+     *      values for related by option
+     *     (all such values must have matching option)
+     * @return String error if any
+     */
+    private static String removeForceOff(
+        Option.Value[] input,
+        Option.Value value,
+        int[] matches) {
+        if (!value.prefix.forceOff()) {
+            throw new IllegalArgumentException(
+                "expecting force-off: " + value);
+        }
+        for (int i = 0; i < matches.length; i++) {
+            Option.Value match = input[matches[i]];
+            if ((null != match) && value.sameValueIdentifier(match)) {
+                if (match.prefix.forceOn()) {
+                    return "force conflict between "
+                        + value
+                        + " and "
+                        + match;
+                } else {
+                    input[matches[i]] = null; // unset matches[i]? 
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * For this force-on value, convert to set-on,
+     * throw Error on any same-family force-off value,
+     * remove any identical force-on or set-on value,
+     * alert on any other non-identical same-family force-on value,
+     * remove any same-family set-on value,
+     * and alert on any same-family set-off value.
+     * This must be called after <code>removeForceOff(..)</code>.
+     * @param input the Option.Value[] to modify
+     * @param valueIndex the int index in matches to find the force-on
+     *                   and to start after
+     * @param matches the int[] map into input entries with matching options
+     * @return
+     * @throw Error if any matching force-off found
+     */
+    private static String convertForceOn(
+        Option.Value[] input,
+        int valueIndex,
+        int[] matches) {
+        Option.Value value = input[matches[valueIndex]];
+        if (!value.prefix.forceOn()) {
+            throw new IllegalArgumentException(
+                "expecting force-on: " + value);
+        }
+        input[matches[valueIndex]] = value.convert(Option.ON);
+        for (int i = 0; i < matches.length; i++) {
+            if (i == valueIndex) {
+                continue;
+            }
+            Option.Value match = input[matches[i]];
+            if (null != match) {
+                // assert match.sameOptionFamily(value);
+                if (match.prefix.forceOff()) {
+                    throw new Error(
+                        "unexpected force-off:"
+                            + match
+                            + " when processing "
+                            + value);
+                }
+                if (value.option.sameOptionIdentifier(match.option)) {
+                    input[matches[i]] = null;
+                    // remove any identical force-on or set
+                } else if (match.prefix.forceOn()) {
+                    return "conflict between " + match + " and " + value;
+                } else if (match.prefix.isSet()) {
+                    input[matches[i]] = null;
+                    // remove any same-value set-on value
+                } else { // same family, force-off
+                    return "collision between " + match + " and " + value;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get a list of input matching the option in the initial value,
+     * rendered as indexes into the input array.
+     * @param input the Option.Value[] to seek in
+     * @param start the int index of the starting position
+     * @return int[] of indexes into input with the same option
+     *         as index[start] - never null, but can be empty
+     */
+    private static int[] match(Option.Value[] input, int start) {
+        IntList result = new IntList();
+        Option.Family key = null;
+        Option.Family nextKey = null;
+        for (int i = start; i < input.length; i++) {
+            if (null != input[i]) {
+                nextKey = input[i].option.getFamily();
+                if (null == key) {
+                    key = nextKey;
+                    result.add(i);
+                } else if (key.equals(nextKey)) {
+                    result.add(i);
+                }
+            }
+        }
+        return result.getList();
+    }
+       
+    static int nullify(Option.Value[] values, Selector selector) {
+        LangUtil.throwIaxIfNull(selector, "selector");
+        int changed = 0;
+        for (int i = 0; i < values.length; i++) {
+            final boolean accepted;
+            try {
+                accepted = selector.accept(values[i]);
+            } catch (Error e) {
+                if (e != Selector.STOP) {
+                    throw e;
+                }
+                break;
+            }
+            if (accepted) {
+                if (null != values[i]) {
+                    values[i] = null;
+                    changed++;
+                }
+            }
+        }
+        return changed;
+    }
+
+    /**
+     * Render set values as String using associated prefix.
+     * @param values the Value[] to render
+     * @return String[] of values rendered for output
+     *   (never null or longer than values, but might be shorter)
+     */
+    private static String[] render(Value[] values) {
+        ArrayList list = new ArrayList();
+        for (int i = 0; i < values.length; i++) {
+            if (null != values[i]) {
+                String[] output = values[i].unflatten();
+                if (LangUtil.isEmpty(output)) {
+                    throw new Error("no output for " + values[i]);
+                }
+
+                String s = values[i].prefix.render(output[0]);
+                if (null != s) { // this means the prefix is set
+                    list.add(s);
+                    for (int j = 1; j < output.length; j++) {
+                        list.add(output[j]);
+                    }
+                }
+            }
+        }
+        return (String[]) list.toArray(new String[list.size()]);
+    }
+
+    private final Option.Value[] values;
+    private Option.Value[] valuesNotNull;
+    private String resolveError;
+
+    private Values(Value[] values) {
+        this.values = new Value[values.length];
+        System.arraycopy(values, 0, this.values, 0, values.length);
+    }
+
+    public int length() {
+        return values.length;
+    }
+
+    public Option.Value[] asArray() {
+        Option.Value[] result = new Option.Value[values.length];
+        System.arraycopy(values, 0, result, 0, result.length);
+        return result;
+    }
+
+    /**
+     * Emit as String[] the non-null values. 
+     * @return String[] of matched entries (never null, elements not null)
+     */
+    public String[] render() {
+        return Values.render(valuesNotNull());
+    }
+
+    public String toString() {
+        return Arrays.asList(values).toString();
+    }
+
+    /**
+     * Create index into values of those that were matched,
+     * including the options and their arguments.
+     * @return int[] of elements in values that are not null (options)
+     *         or that represent option arguments
+     */
+    public int[] indexMatches() {
+        // must be in order, low to high
+        final int[] missed = indexMissedMatches();
+        return invert(missed, length());
+    }
+
+    /**
+     * Create index into values of missed input,
+     * taking into account that matched arguments are 
+     * represented as null.
+     * @return int[] of elements in values that are null
+     *         or optionally represent option arguments
+     */
+    public int[] indexMissedMatches() {
+        MissedSelector selector = new MissedSelector();
+        find(selector, FIND_ALL);
+        String errors = selector.getErrors();
+        if (null != errors) {
+            throw new Error(errors);
+        }
+        return selector.getResult();
+    }
+
+    public Value firstInFamily(Option.Family family) {
+        return findFirst(new ValueSelector(family));
+    }
+
+    public Value[] allInFamily(Option.Family family) {
+        return find(new ValueSelector(family), FIND_ALL);
+    }
+
+    public Value firstOption(Option option) {
+        return findFirst(new ValueSelector(option));
+    }
+
+    public Value[] allOption(Option option) {
+        return find(new ValueSelector(option), FIND_ALL);
+    }
+
+    public Value firstValue(Option option, String value) {
+        LangUtil.throwIaxIfNull(value, "value");
+        return findFirst(new ValueSelector(option, value));
+    }
+
+    public Value[] allValues(Option option, String value) {
+        LangUtil.throwIaxIfNull(value, "value");
+        return find(new ValueSelector(option, value), FIND_ALL);
+    }
+
+    public boolean isResolved() {
+        return ((this != EMPTY) && (null != resolveError));
+    }
+
+    /**
+     * 
+     * @param selector the Selector to pick out entries to nullify
+     *    (should throw STOP to halt processing)
+     * @return Values resulting from nullifying entries,
+     * or this if none were changed
+     */
+    public Values nullify(Selector selector) {
+        if (null == selector) {
+            return this;
+        }
+        Value[] temp = asArray();
+        int changed = nullify(temp, selector);
+        if (0 == changed) {
+            return this;    
+        }
+        return new Values(temp);        
+    }
+    
+    /**
+     * Resolve options, removing duplicates by force if necessary.
+     * If any error is returned, then the values are left unchanged.
+     * @return String error, if any
+     * @throws IllegalStateException if <code>isResolved()</code>
+     */
+    public String resolve() {
+        if (isResolved()) {
+            throw new IllegalStateException("already resolved");
+        }
+        Option.Value[] temp = asArray();
+        resolveError = resolve(temp);
+        if (null == resolveError) {
+            System.arraycopy(temp, 0, values, 0, temp.length);
+            valuesNotNull = null;
+            resolveError = NO_ERROR;
+            return null;
+        }
+        return resolveError;
+    }
+
+    protected Option.Value findFirst(Selector filter) {
+        Option.Value[] result = find(filter, !FIND_ALL);
+        return (0 == result.length ? null : result[0]);
+    }
+
+    protected Option.Value[] find(Selector filter, boolean findAll) {
+        LangUtil.throwIaxIfNull(filter, "filter");
+        ArrayList result = new ArrayList();
+        for (int i = 0; i < values.length; i++) {
+            final boolean accepted;
+            try {
+                accepted = filter.accept(values[i]);
+            } catch (Error e) {
+                if (Selector.STOP != e) {
+                    throw e;
+                }
+                break;
+            }
+            if (accepted) {
+                result.add(values[i]);
+                if (findAll != FIND_ALL) {
+                    break;
+                }
+            }
+        }
+        return toArray(result);
+    }
+
+    private Option.Value[] valuesNotNull() {
+        if (null == valuesNotNull) {
+            ArrayList list = new ArrayList();
+            for (int i = 0; i < this.values.length; i++) {
+                if (null != this.values[i]) {
+                    list.add(this.values[i]);
+                }
+            }
+            valuesNotNull = toArray(list);
+        }
+        return valuesNotNull;
+    }
+
+    public static class Selector {
+        public static final Error STOP = new Error("stop invoking Selector");
+        protected Selector() {
+        }
+        protected boolean accept(Value value) {
+            return false;
+        }
+    }
+    protected static class ValueSelector extends Selector {
+
+        private final Option option;
+        private final Option.Family family;
+        private final String value;
+        ValueSelector(Option.Family family) {
+            LangUtil.throwIaxIfNull(family, "family");
+            this.family = family;
+            option = null;
+            value = null;
+        }
+        ValueSelector(Option option) {
+            this(option, (String) null);
+        }
+        ValueSelector(Option option, String value) {
+            LangUtil.throwIaxIfNull(option, "option");
+            this.option = option;
+            family = null;
+            this.value = value;
+        }
+        protected boolean accept(Value value) {
+            if (null == value) {
+                return false;
+            }
+            if (null != family) {
+                return family.sameFamily(value.option.getFamily());
+            } else if (!option.sameOptionIdentifier(value.option)) {
+                return false;
+            } else {
+                return ((null == this.value)
+                    || (this.value.equals(value.value)));
+            }
+        }
+    }
+
+    /** pick all null entries (except for args), return as int[] */
+    protected static class MissedSelector extends Selector {
+        public static final String DELIM = "; ";
+        final IntList result = new IntList();
+        int index;
+        final StringBuffer errors = new StringBuffer();
+        int argsExpected;
+        Option argsExpectedFor;
+        MissedSelector() {
+        }
+
+        int[] getResult() {
+            return result.getList();
+        }
+
+        /**
+         * add index if value is null
+         * unless skipArguments 
+         */
+        protected boolean accept(Value value) {
+            index++;
+            if (null != value) {
+                if (0 < argsExpected) { // expected more (null) args
+                    missedArgsFor(argsExpectedFor, argsExpected);
+                }
+                argsExpected = value.option.numArguments();
+                argsExpectedFor = value.option;
+            } else if (0 < argsExpected) { // ignore null in arg position
+                argsExpected--;
+                if (0 == argsExpected) {
+                    argsExpectedFor = null;
+                }
+            } else { // null, not expecting arg, so missing
+                result.add(index - 1);
+                return true;
+            }
+            return false;
+        }
+
+        private void missedArgsFor(Option option, int numArgs) {
+            errors.append("missed ");
+            errors.append(numArgs + " args for ");
+            errors.append(option + DELIM);
+        }
+
+        String getErrors() {
+            if (0 < argsExpected) {
+            }
+            if (0 == errors.length()) {
+                return null;
+            }
+            return errors.toString();
+        }
+    }
+
+    static class IntList {
+        // not synchronized - used only in one thread
+        static String render(int[] input) {
+            if (null == input) {
+                return "null";
+            }
+            StringBuffer sb = new StringBuffer();
+            sb.append("[");
+            for (int i = 0; i < input.length; i++) {
+                if (i > 0) {
+                    sb.append(", " + input[i]);
+                } else {
+                    sb.append("" + input[i]);
+                }
+            }
+            sb.append("]");
+            return sb.toString();
+        }
+
+        private int[] input = new int[256];
+        private int insert;
+        private void add(int i) {
+            if (insert >= input.length) {
+                int[] temp = new int[insert + 256];
+                for (int j = 0; j < input.length; j++) {
+                    temp[j] = input[j];
+                }
+                input = temp;
+            }
+            input[insert++] = i;
+        }
+
+        private int[] getList() {
+            int[] result = new int[insert];
+            for (int i = 0; i < result.length; i++) {
+                result[i] = input[i];
+            }
+            return result;
+        }
+    }
+}
index 452d2e0b1dd3bbf59ffd23dad631244294c8e75e..4c904ac90d15d197deefa65d559144fbed85b94a 100644 (file)
@@ -25,6 +25,7 @@ public class TestingModuleTests extends TestCase {
         suite.addTest(org.aspectj.testing.harness.bridge.TestingBridgeTests.suite()); 
         suite.addTest(org.aspectj.testing.taskdefs.TaskdefTests.suite()); 
         suite.addTest(org.aspectj.testing.util.UtilTests.suite()); 
+        suite.addTest(org.aspectj.testing.util.options.OptionsTests.suite()); 
         suite.addTest(org.aspectj.testing.xml.TestingXmlTests.suite()); 
         return suite;
     }
index a876db105a986dac751564c27791dea451eafa12..8d05160b44d1c4b6509cbd479efa77d15806fdc1 100644 (file)
@@ -1,6 +1,6 @@
 /* *******************************************************************
- * Copyright (c) 1999-2001 Xerox Corporation, 
- *               2002 Palo Alto Research Center, Incorporated (PARC).
+ * Copyright (c) 2002 Palo Alto Research Center, Incorporated (PARC),
+ *               2003 Contributors.
  * All rights reserved. 
  * This program and the accompanying materials are made available 
  * under the terms of the Common Public License v1.0 
@@ -9,14 +9,22 @@
  *  
  * Contributors: 
  *     Xerox/PARC     initial implementation 
+ *     Wes Isberg     2003 modifications
  * ******************************************************************/
 
 package org.aspectj.testing.harness.bridge;
 
+import java.util.*;
+import java.util.Arrays;
+
 import junit.framework.TestCase;
 
 import org.aspectj.bridge.MessageHandler;
 import org.aspectj.bridge.MessageUtil;
+import org.aspectj.testing.harness.bridge.CompilerRun.Spec.CRSOptions;
+import org.aspectj.testing.util.options.*;
+import org.aspectj.testing.util.options.Options;
+import org.aspectj.util.LangUtil;
 
 /**
  * 
@@ -24,147 +32,515 @@ import org.aspectj.bridge.MessageUtil;
 public class CompilerRunSpecTest extends TestCase {
 
     private static boolean PRINTING = true;
-    
-       /**
-        * Constructor for CompilerRunSpecTest.
-        * @param name
-        */
-       public CompilerRunSpecTest(String name) {
-               super(name);
-       }
+    private static final boolean SETUP_JAVA13 =
+        haveProperty(Globals.J2SE13_RTJAR_NAME);
+    private static final boolean SETUP_JAVA14 =
+        haveProperty(Globals.J2SE14_RTJAR_NAME);
+    static {
+        if (!SETUP_JAVA14) {
+            System.err.println(
+                "warning: set -D"
+                    + Globals.J2SE14_RTJAR_NAME
+                    + "=... in order to run all tests");
+        }
+        if (!SETUP_JAVA13) {
+            System.err.println(
+                "warning: set -D"
+                    + Globals.J2SE13_RTJAR_NAME
+                    + "=... in order to run all tests");
+        }
+    }
+    private static boolean haveProperty(String key) {
+        try {
+            return (null != System.getProperty(key));
+        } catch (Throwable t) {
+            // ignore
+        }
+        return false;
+    }
+
+    /**
+     * Constructor for CompilerRunSpecTest.
+     * @param name
+     */
+    public CompilerRunSpecTest(String name) {
+        super(name);
+    }
 
     public void testSetupArgs() {
-        checkSetupArgs("verbose", false);
-        // XXX skipping since eclipse is default
-        // checkSetupArgs("lenient", false);
-        // checkSetupArgs("strict", false);
-        // checkSetupArgs("ajc", true);   // XXX need to predict/test compiler selection
-        // eclipse-only
         checkSetupArgs("eclipse", true);
+        checkSetupArgs("verbose", false);
+    }
+
+    public void testCompliance() {
+        // 1.3 should work
+        String specOptions = "-1.3";
+        String[] globalOptions = new String[0];
+        boolean expectAdopted = true;
+        String resultContains = "1.3";
+        String messagesContain = null;
+        MessageHandler handler =
+            runTest(
+                specOptions,
+                globalOptions,
+                expectAdopted,
+                resultContains,
+                messagesContain);
+        checkMessages(handler, null);
+
+        // 1.3 should work with collision?
+        globalOptions = new String[] { "-1.3" };
+        resultContains = "1.3";
+        handler =
+            runTest(
+                specOptions,
+                globalOptions,
+                expectAdopted,
+                resultContains,
+                messagesContain);
+        checkMessages(handler, null);
+
+        // 1.4 should work
+        globalOptions = new String[0];
+        specOptions = "-1.4";
+        resultContains = "1.4";
+        handler =
+            runTest(
+                specOptions,
+                globalOptions,
+                expectAdopted,
+                resultContains,
+                messagesContain);
+        checkMessages(handler, null);
+
+        // compliance not checked for valid numbers, so -1.2 would pass
+    }
+
+    private void checkMessages(MessageHandler handler, String contains) {
+        if (null == contains) {
+            if (0 != handler.numMessages(null, true)) {
+                assertTrue("" + handler, false);
+            }
+        } else {
+            String messages = "" + handler;
+            if (-1 == messages.indexOf(contains)) {
+                assertTrue(messages, false);
+            }
+        }
+    }
+
+    /** @testcase check -target converts for 1.1 and 1.2, not others */
+    public void testTarget() {
+        final boolean PASS = true;
+        final boolean FAIL = false;
+        checkSourceTargetVersionConversion("target", 1, PASS, null);
+        checkSourceTargetVersionConversion("target", 2, PASS, null);
+        checkSourceTargetVersionConversion(
+            "target",
+            3,
+            FAIL,
+            "illegal input");
+    }
+
+    /** @testcase check -source converts for 1.3 and 1.4, not others */
+    public void testSource() {
+        final boolean PASS = true;
+        final boolean FAIL = false;
+        if (SETUP_JAVA13) {
+            checkSourceTargetVersionConversion("source", 3, PASS, null);
+        }
+        if (SETUP_JAVA14) {
+            checkSourceTargetVersionConversion("source", 4, PASS, null);
+        }
+
+        checkSourceTargetVersionConversion(
+            "source",
+            2,
+            FAIL,
+            "not permitted");
+        checkSourceTargetVersionConversion(
+            "source",
+            6,
+            FAIL,
+            "not permitted");
+    }
+
+    public void testSourceOverride() {
+        if (SETUP_JAVA13 && SETUP_JAVA14) {
+            checkSourceTargetOverride("source", 3, 4);
+            checkSourceTargetOverride("source", 4, 3);
+            checkSourceTargetOverride("source", 3, 3);
+            checkSourceTargetOverride("source", 4, 4);
+        }
+    }
+
+    public void testTargetOverride() {
+        checkSourceTargetOverride("target", 1, 2);
+        checkSourceTargetOverride("target", 2, 1);
+        checkSourceTargetOverride("target", 1, 1);
+        checkSourceTargetOverride("target", 2, 2);
     }
 
-    void checkSetupArgs(String arg, boolean isTestArg) {
+    public void testCompilerOptions() {
+        checkCompilerOption(null, CompilerRun.Spec.DEFAULT_COMPILER);
+        CRSOptions crsOptions = CompilerRun.Spec.testAccessToCRSOptions();
+        Set options = crsOptions.compilerOptions();
+        assertTrue(null != options);
+        StringBuffer notLoaded = new StringBuffer();
+        for (Iterator iter = options.iterator(); iter.hasNext();) {
+            Option compilerOption = (Option) iter.next();
+            if (!(crsOptions.compilerIsLoadable(compilerOption))) {
+                notLoaded.append(" " + compilerOption);
+            } else {                
+                String className = crsOptions.compilerClassName(compilerOption);
+                String argValue = compilerOption.toString(); // XXX snoop
+                String arg = Option.ON.render(argValue);
+                checkCompilerOption(arg, className);
+            }
+        }
+        if (0 < notLoaded.length()) {
+            System.err.println(
+                getClass().getName()
+                    + ".testCompilerOptions()"
+                    + " warning: compiler options not tested because not loadable: "
+                    + notLoaded);
+        }
+    }
+
+    void checkCompilerOption(String arg, String className) {
+        MessageHandler handler = new MessageHandler();
+        try {
+            CompilerRun.Spec spec = null;
+            try {
+                spec = new CompilerRun.Spec();
+            } catch (Throwable t) {
+                t.printStackTrace(System.err);
+            }
+            assertTrue(spec != null);
+            AbstractRunSpec.RT parentRuntime = new AbstractRunSpec.RT();
+            String result;
+            String expResult;
+
+            if (null != arg) {
+                parentRuntime.setOptions(new String[] { arg });
+            }
+            if (!spec.adoptParentValues(parentRuntime, handler)) {
+                if (0 != handler.numMessages(null, true)) {
+                    assertTrue(handler.toString(), false);
+                } else {
+                    assertTrue("adopt failed, but no messages", false);
+                }
+            }
+            result = "" + spec.testSetup.commandOptions;
+            assertEquals("[]", result);
+            assertEquals(className, spec.testSetup.compilerName);
+        } finally {
+        }
+    }
+
+    public void testSpecOptions() {
+        Options options = CompilerRun.Spec.testAccessToOptions();
+        OptionChecker optionChecker = new OptionChecker(options);
+        // known failures: extdirs, aspectpath, Xlintfile <file>
+        // progress, time, noExit, repeat <n>,
+        // help, 
+        String[][] input =
+            new String[][] {
+                new String[] { "-verbose" },
+                new String[] { "-incremental" },
+                new String[] { "-emacssym" },
+                new String[] { "-Xlint" },
+                new String[] { "-Xlint:error" },
+                new String[] { "-1.3" },
+                new String[] { "-1.4" },
+                new String[] { "-source", "1.3" },
+                new String[] { "-source", "1.4" },
+                new String[] { "-target", "1.1" },
+                new String[] { "-target", "1.2" },
+                new String[] { "-preserveAllLocals" },
+                new String[] { "-referenceInfo" },
+                new String[] { "-deprecation" },
+                new String[] { "-noImportError" },
+                new String[] { "-proceedOnError" }
+        };
+        String[][] literalInput =
+            new String[][] {
+                new String[] { "-nowarn" },
+                new String[] { "-warn:deprecated" },
+                new String[] { "-warn:deprecated,unverified" },
+                new String[] { "-warn:deprecated", "-warn:unusedLocals" },
+                new String[] { "-g" },
+                new String[] { "-g", "-g:none" },
+                new String[] { "-g:vars,source" },
+                new String[] { "-verbose", "-g:vars,source" },
+                };
+        // normal
+        for (int i = 0; i < input.length; i++) {
+            optionChecker.checkOptions(input[i], input[i]);
+        }
+        for (int i = 0; i < literalInput.length; i++) {
+            optionChecker.checkOptions(literalInput[i], literalInput[i]);
+        }
+        // force-on
+        String[][] forceInput = duplicate(input, "!");
+        for (int i = 0; i < input.length; i++) {
+            optionChecker.checkOptions(forceInput[i], input[i]);
+        }
+        // force-off
+        forceInput = duplicate(input, "^");
+        String[] none = new String[0];
+        for (int i = 0; i < input.length; i++) {
+            optionChecker.checkOptions(forceInput[i], none);
+        }
+    }
+
+    String[][] duplicate(String[][] input, String prefix) {
+        String[][] output = new String[input.length][];
+        final int prefixLength = (null == prefix ? 0 : prefix.length());
+        for (int i = 0; i < output.length; i++) {
+            int length = input[i].length;
+            output[i] = new String[length];
+            if ((length > 0) && (prefixLength > 0)) {
+                System.arraycopy(input[i], 0, output[i], 0, length);
+                output[i][0] =
+                    prefix + output[i][0].substring(prefixLength);
+            }
+        }
+        return output;
+    }
+
+    public void checkSourceTargetOverride(String name, int from, int to) {
+        final String specOptions = "-" + name + ", 1." + from;
+        String[] globalOptions = new String[] { "!" + name, "1." + to };
+        boolean expectAdopted = true;
+        String resultContains = "[-" + name + ", 1." + to;
+        String messagesContain = null;
+        MessageHandler handler =
+            runTest(
+                specOptions,
+                globalOptions,
+                expectAdopted,
+                resultContains,
+                messagesContain);
+        checkMessages(handler, null);
+    }
+
+    void checkSourceTargetVersionConversion(
+        String name,
+        int i,
+        boolean expectPass,
+        String expectedErr) {
+        final String specOptions = "-" + name + ", 1." + i;
+        String[] globalOptions = new String[0];
+        boolean expectAdopted = expectPass;
+        String resultContains =
+            !expectPass ? null : "[-" + name + ", 1." + i;
+        String messagesContain = expectedErr;
+        MessageHandler handler =
+            runTest(
+                specOptions,
+                globalOptions,
+                expectAdopted,
+                resultContains,
+                messagesContain);
+    }
+
+    MessageHandler runTest(
+        String specOptions,
+        String[] globalOptions,
+        boolean expectAdopted,
+        String resultContains,
+        String messagesContain) {
         MessageHandler handler = new MessageHandler();
         try {
             CompilerRun.Spec spec = new CompilerRun.Spec();
             AbstractRunSpec.RT parentRuntime = new AbstractRunSpec.RT();
+
+            if (!LangUtil.isEmpty(specOptions)) {
+                spec.setOptions(specOptions);
+            }
+            if (!LangUtil.isEmpty(globalOptions)) {
+                parentRuntime.setOptions(globalOptions);
+            }
+            boolean adopted =
+                spec.adoptParentValues(parentRuntime, handler);
+            if (adopted != expectAdopted) {
+                String s =
+                    (expectAdopted ? "not " : "")
+                        + "adopted spec="
+                        + specOptions
+                        + " globals="
+                        + (LangUtil.isEmpty(globalOptions)
+                            ? "[]"
+                            : Arrays.asList(globalOptions).toString())
+                        + " -- "
+                        + handler;
+                assertTrue(s, false);
+            }
+            if (null != resultContains) {
+                String result = "" + spec.testSetup.commandOptions;
+                if (-1 == result.indexOf(resultContains)) {
+                    assertTrue(
+                        "expected " + resultContains + " got " + result,
+                        false);
+                }
+            }
+            if (null != messagesContain) {
+                boolean haveMessages =
+                    (0 != handler.numMessages(null, true));
+                if (!haveMessages) {
+                    assertTrue("expected " + messagesContain, false);
+                } else {
+                    String messages = handler.toString();
+                    if (-1 == messages.indexOf(messagesContain)) {
+                        assertTrue(
+                            "expected "
+                                + messagesContain
+                                + " got "
+                                + messages,
+                            false);
+                    }
+                }
+            }
+            return handler;
+        } finally {
+        }
+    }
+
+    void checkSetupArgs(String arg, final boolean isTestArg) {
+        MessageHandler handler = new MessageHandler();
+        try {
+            CompilerRun.Spec spec = null;
+            try {
+                spec = new CompilerRun.Spec();
+            } catch (Throwable t) {
+                t.printStackTrace(System.err);
+            }
+            assertTrue(spec != null);
+            AbstractRunSpec.RT parentRuntime = new AbstractRunSpec.RT();
             String result;
             String expResult;
-            
+
             // -------- local set
+            // global - (set) does not change local-set
+            parentRuntime.setOptions(new String[] { "-" + arg });
+            assertTrue(spec.adoptParentValues(parentRuntime, handler));
+            if (0 != handler.numMessages(null, true)) {
+                assertTrue(handler.toString(), false);
+            }
+            result = "" + spec.testSetup.commandOptions;
+            expResult = (isTestArg ? "[]" : "[-" + arg + "]");
+            assertTrue(result, expResult.equals(result));
+
             // global ^ (force-off) to disable
             spec.setOptions("-" + arg);
-            parentRuntime.setOptions(new String[] {"^" + arg});
+            parentRuntime.setOptions(new String[] { "^" + arg });
             assertTrue(spec.adoptParentValues(parentRuntime, handler));
             if (0 != handler.numMessages(null, true)) {
                 assertTrue(handler.toString(), false);
             }
-            result = ""+spec.testSetup.commandOptions;
+            result = "" + spec.testSetup.commandOptions;
             assertTrue(result, "[]".equals(result));
-    
+
             // global ! (force-on) does not change local-set
-            parentRuntime.setOptions(new String[] {"!" + arg});
+            parentRuntime.setOptions(new String[] { "!" + arg });
             assertTrue(spec.adoptParentValues(parentRuntime, handler));
             if (0 != handler.numMessages(null, true)) {
                 assertTrue(handler.toString(), false);
             }
-            result = ""+spec.testSetup.commandOptions;
+            result = "" + spec.testSetup.commandOptions;
             expResult = (isTestArg ? "[]" : "[-" + arg + "]");
-            assertTrue(result, expResult.equals(result));
-    
-            // global - (set) does not change local-set
-            parentRuntime.setOptions(new String[] {"-" + arg});
-            assertTrue(spec.adoptParentValues(parentRuntime, handler));
-            if (0 != handler.numMessages(null, true)) {
-                assertTrue(handler.toString(), false);
+            if (!expResult.equals(result)) {
+                assertTrue(
+                    "expected " + expResult + " got " + result,
+                    false);
             }
-            result = ""+spec.testSetup.commandOptions;
-            expResult = (isTestArg ? "[]" : "[-" + arg + "]");
-            assertTrue(result, expResult.equals(result));
-    
+
             // global (unset) does not change local-set
-            parentRuntime.setOptions(new String[] {""});
+            parentRuntime.setOptions(new String[] { "" });
             assertTrue(spec.adoptParentValues(parentRuntime, handler));
             if (0 != handler.numMessages(null, true)) {
                 assertTrue(handler.toString(), false);
             }
-            result = ""+spec.testSetup.commandOptions;
+            result = "" + spec.testSetup.commandOptions;
             expResult = (isTestArg ? "[]" : "[-" + arg + "]");
             assertTrue(result, expResult.equals(result));
-    
+
             // -------- local force-on
             // global ^ (force-off) conflicts with local force-on
             spec.setOptions("!" + arg);
-            parentRuntime.setOptions(new String[] {"^" + arg});
+            parentRuntime.setOptions(new String[] { "^" + arg });
             assertTrue(!spec.adoptParentValues(parentRuntime, handler));
             assertTrue(0 != handler.numMessages(null, true));
             handler.init();
-    
+
             // global ! (force-on) does not change local force-on
-            parentRuntime.setOptions(new String[] {"!" + arg});
+            parentRuntime.setOptions(new String[] { "!" + arg });
             assertTrue(spec.adoptParentValues(parentRuntime, handler));
             if (0 != handler.numMessages(null, true)) {
                 assertTrue(handler.toString(), false);
             }
-            result = ""+spec.testSetup.commandOptions;
+            result = "" + spec.testSetup.commandOptions;
             expResult = (isTestArg ? "[]" : "[-" + arg + "]");
             assertTrue(result, expResult.equals(result));
-    
+
             // global - (set) does not change local force-on
-            parentRuntime.setOptions(new String[] {"-" + arg});
+            parentRuntime.setOptions(new String[] { "-" + arg });
             assertTrue(spec.adoptParentValues(parentRuntime, handler));
             if (0 != handler.numMessages(null, true)) {
                 assertTrue(handler.toString(), false);
             }
-            result = ""+spec.testSetup.commandOptions;
+            result = "" + spec.testSetup.commandOptions;
             expResult = (isTestArg ? "[]" : "[-" + arg + "]");
             assertTrue(result, expResult.equals(result));
-    
+
             // global (unset) does not change local force-on
-            parentRuntime.setOptions(new String[] {""});
+            parentRuntime.setOptions(new String[] { "" });
             assertTrue(spec.adoptParentValues(parentRuntime, handler));
             if (0 != handler.numMessages(null, true)) {
                 assertTrue(handler.toString(), false);
             }
-            result = ""+spec.testSetup.commandOptions;
+            result = "" + spec.testSetup.commandOptions;
             expResult = (isTestArg ? "[]" : "[-" + arg + "]");
             assertTrue(result, expResult.equals(result));
-    
-    
+
             // -------- local force-off
             // global ^ (force-off) does not change local force-off
             spec.setOptions("^" + arg);
-            parentRuntime.setOptions(new String[] {"^" + arg});
+            parentRuntime.setOptions(new String[] { "^" + arg });
             assertTrue(spec.adoptParentValues(parentRuntime, handler));
             if (0 != handler.numMessages(null, true)) {
                 assertTrue(handler.toString(), false);
             }
-            result = ""+spec.testSetup.commandOptions;
+            result = "" + spec.testSetup.commandOptions;
             assertTrue(result, ("[]").equals(result));
-    
+
             // global ! (force-on) conflicts with local force-off
-            parentRuntime.setOptions(new String[] {"!" + arg});
+            parentRuntime.setOptions(new String[] { "!" + arg });
             assertTrue(!spec.adoptParentValues(parentRuntime, handler));
             assertTrue(0 != handler.numMessages(null, true));
             handler.init();
-    
-            // global - (set) overridden by local force-off // XXX??
-            parentRuntime.setOptions(new String[] {"-" + arg});
+
+            // global - (set) overridden by local force-off
+            parentRuntime.setOptions(new String[] { "-" + arg });
             assertTrue(spec.adoptParentValues(parentRuntime, handler));
             if (0 != handler.numMessages(null, true)) {
                 assertTrue(handler.toString(), false);
             }
-            result = ""+spec.testSetup.commandOptions;
+            result = "" + spec.testSetup.commandOptions;
             assertTrue(result, ("[]").equals(result));
-    
+
             // global (unset) does not change local force-off
-            parentRuntime.setOptions(new String[] {""});
+            parentRuntime.setOptions(new String[] { "" });
             assertTrue(spec.adoptParentValues(parentRuntime, handler));
             if (0 != handler.numMessages(null, true)) {
                 assertTrue(handler.toString(), false);
             }
-            result = ""+spec.testSetup.commandOptions;
+            result = "" + spec.testSetup.commandOptions;
             assertTrue(result, ("[]").equals(result));
+
+            // undefined whether global set overrides local set
+            // for different sibling options 
         } finally {
             if (PRINTING && (0 < handler.numMessages(null, true))) {
                 MessageUtil.print(System.err, handler, "checkSetupArgs: ");
diff --git a/testing/testsrc/org/aspectj/testing/util/options/OptionChecker.java b/testing/testsrc/org/aspectj/testing/util/options/OptionChecker.java
new file mode 100644 (file)
index 0000000..f2ae8ac
--- /dev/null
@@ -0,0 +1,195 @@
+/* *******************************************************************
+ * Copyright (c) 2003 Contributors.
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Wes Isberg     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util.options;
+
+import java.util.Arrays;
+
+import junit.framework.Assert;
+
+import org.aspectj.testing.util.LangUtil;
+
+/**
+ * Checks that throw AssertionFailedError on failure.
+ * Subclasses reimplement <code>assertionFailed(String)</code>
+ * to handle failures differently.
+ */
+public class OptionChecker {
+    private final Options options;
+
+    public OptionChecker(Options options) {
+        this.options = options;
+        LangUtil.throwIaxIfNull(options, "options");
+    }
+
+    /**
+     * Subclasses override this to throw different exceptions
+     * on assertion failures.
+     * This implementation delegates to 
+     * <code>Assert.assertTrue(label, false)</code>.
+     * @param label the String message for the assertion
+     * 
+     */
+    public void assertionFailed(String label) {
+        Assert.assertTrue(label, false);
+    }
+
+    public void checkAssertion(String label, boolean test) {
+        if (!test) {
+            assertionFailed(label);
+        }
+    }
+
+    public void checkOptions(String[] input, String[] expected) {
+        checkOptions(input, expected, true);
+    }
+
+    public void checkOptions(
+        String[] input,
+        String[] expected,
+        boolean resolve) {
+        Values values = getValues(input);
+        if (resolve) {
+            String err = values.resolve();
+            checkAssertion("error: \"" + err + "\"", null == err);
+        }
+        String[] actual = values.render(); // Value.render(values);
+        checkEqual(expected, actual);
+    }
+
+    public void checkOptionsNegative(
+        String[] input,
+        String expectedMissedMatchErr,
+        String expectedResolveErr) {
+        checkOptionsNegative(
+            input,
+            null,
+            expectedMissedMatchErr,
+            expectedResolveErr);
+    }
+
+    public void checkOptionsNegative(
+        String[] input,
+        String expectedInValuesException,
+        String expectedMissedMatchErr,
+        String expectedResolveErr) {
+        Values values =
+            getValuesNegative(input, expectedInValuesException);
+        if (null == expectedInValuesException) {
+            String err = Options.missedMatchError(input, values);
+            checkContains(expectedMissedMatchErr, err);
+            err = values.resolve();
+            checkContains(expectedResolveErr, err);
+        }
+    }
+
+    private Values getValuesNegative(
+        String[] input,
+        String expectedInExceptionMessage,
+        Options options) {
+        try {
+            return options.acceptInput(input);
+        } catch (Option.InvalidInputException e) {
+            String m = e.getFullMessage();
+            boolean ok =
+                (null != expectedInExceptionMessage)
+                    && (-1 != m.indexOf(expectedInExceptionMessage));
+            if (!ok) {
+                e.printStackTrace(System.err);
+                if (null != expectedInExceptionMessage) {
+                    m =
+                        "expected \""
+                            + expectedInExceptionMessage
+                            + "\" in "
+                            + m;
+                }
+                assertionFailed(m);
+            }
+            return null; // actually never executed
+        }
+    }
+
+    private Values getValuesNegative(
+        String[] input,
+        String expectedInExceptionMessage) {
+        return getValuesNegative(
+            input,
+            expectedInExceptionMessage,
+            options);
+    }
+
+    private Values getValues(String[] input, Options options) {
+        return getValuesNegative(input, null, options);
+    }
+
+    private Values getValues(String[] input) {
+        return getValuesNegative(input, null);
+    }
+
+    private void checkContains(String expected, String expectedIn) {
+        if (null == expected) {
+            if (null != expectedIn) {
+                assertionFailed("did not expect \"" + expectedIn + "\"");
+            }
+        } else {
+            if ((null == expectedIn)
+                || (-1 == expectedIn.indexOf(expected))) {
+                assertionFailed(
+                    "expected \""
+                        + expected
+                        + "\" in \""
+                        + expectedIn
+                        + "\"");
+            }
+        }
+    }
+
+    private String safeString(String[] ra) {
+        return (null == ra ? "null" : Arrays.asList(ra).toString());
+    }
+
+    private void checkEqual(String[] expected, String[] actual) {
+        if (!isEqual(expected, actual)) {
+            assertionFailed(
+                "expected \""
+                    + safeString(expected)
+                    + "\" got \""
+                    + safeString(actual)
+                    + "\"");
+        }
+    }
+
+    private boolean isEqual(String[] expected, String[] actual) {
+        if (null == expected) {
+            return (null == actual ? true : false);
+        } else if (null == actual) {
+            return false;
+        } else if (expected.length != actual.length) {
+            return false;
+        }
+        for (int i = 0; i < actual.length; i++) {
+            String e = expected[i];
+            String a = actual[i];
+            if (null == e) {
+                if (null != a) {
+                    return false;
+                }
+            } else if (null == a) {
+                return false;
+            } else if (!(e.equals(a))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+}
diff --git a/testing/testsrc/org/aspectj/testing/util/options/OptionsTest.java b/testing/testsrc/org/aspectj/testing/util/options/OptionsTest.java
new file mode 100644 (file)
index 0000000..29048aa
--- /dev/null
@@ -0,0 +1,486 @@
+/* *******************************************************************
+ * Copyright (c) 2003 Contributors.
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Wes Isberg     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util.options;
+
+import junit.framework.*;
+
+import org.aspectj.testing.util.options.Option.InvalidInputException;
+
+/**
+ */
+public class OptionsTest extends TestCase {
+
+    private static Options OPTIONS;
+
+    private static Options getOptions() {
+        if (null == OPTIONS) {
+            OPTIONS = new Options(true);
+            Option.Factory factory = new Option.Factory("OptionsTest");
+            OPTIONS.addOption(factory.create("verbose"));
+            OPTIONS.addOption(factory.create("quiet"));
+            OPTIONS.addOption(factory.create("debug"));
+            OPTIONS.addOption(
+                factory.create("ajc", "compiler", Option.FORCE_PREFIXES, false));
+            OPTIONS.addOption(
+                factory.create(
+                    "eclipse",
+                    "compiler",
+                    Option.FORCE_PREFIXES,
+                    false));
+            OPTIONS.addOption(
+                factory.create(
+                    "ajdeCompiler",
+                    "compiler",
+                    Option.FORCE_PREFIXES,
+                    false));
+            OPTIONS.addOption(
+                factory.create("1.3", "compliance", Option.FORCE_PREFIXES, false));
+            OPTIONS.addOption(
+                factory.create("1.4", "compliance", Option.FORCE_PREFIXES, false));
+
+            // treating multi-arg as single - extrinsic flatten/unflatten
+            OPTIONS.addOption(
+                factory.create("target11", "target", Option.FORCE_PREFIXES, false));
+            OPTIONS.addOption(
+                factory.create("target12", "target", Option.FORCE_PREFIXES, false));
+            OPTIONS.addOption(
+                factory.create("source13", "source", Option.FORCE_PREFIXES, false));
+            OPTIONS.addOption(
+                factory.create("source14", "source", Option.FORCE_PREFIXES, false));
+
+            // suffix options (a) -warn:... (b) -g, -g:...
+            Assert.assertTrue(factory.setupFamily("warning", true));
+            OPTIONS.addOption(
+                factory.create("warn", "warning", Option.STANDARD_PREFIXES, true));
+
+            Assert.assertTrue(factory.setupFamily("debugSymbols", true));
+            OPTIONS.addOption(
+                factory.create(
+                    "g",
+                    "debugSymbols",
+                    Option.STANDARD_PREFIXES,
+                    false));
+            OPTIONS.addOption(
+                factory.create(
+                    "g:",
+                    "debugSymbols",
+                    Option.STANDARD_PREFIXES,
+                    true));
+
+            // treating multi-arg as single - intrinsic flatten/unflatten
+            OPTIONS
+                .addOption(
+                    factory
+                    .create(
+                        "target",
+                        "target",
+                        Option.FORCE_PREFIXES,
+                        false,
+                        new String[][] {
+                            new String[] { "1.1", "1.2" }
+            }));
+            OPTIONS
+                .addOption(
+                    factory
+                    .create(
+                        "source",
+                        "source",
+                        Option.FORCE_PREFIXES,
+                        false,
+                        new String[][] {
+                            new String[] { "1.3", "1.4" }
+            }));
+            OPTIONS.freeze();
+        }
+        return OPTIONS;
+    }
+
+    private boolean verbose;
+    private OptionChecker localOptionChecker;
+
+    public void testDebugCase() {
+        OptionChecker optionChecker = getOptionChecker();
+        String[] input = new String[] {
+        };
+        String[] expected = new String[0];
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-target", "1.1", "^target", "1.2" };
+        expected = new String[] { "-target", "1.1" };
+
+        optionChecker.checkOptions(input, expected);
+    }
+    public void testNegDebugCase() {
+        OptionChecker optionChecker = getOptionChecker();
+        String[] input = new String[] { "-target" };
+        String expectedInValuesException = "not enough arguments";
+        String expectedMissedMatchErr = null;
+        String expectedResolveErr = null;
+
+        optionChecker.checkOptionsNegative(
+            input,
+            expectedInValuesException,
+            expectedMissedMatchErr,
+            expectedResolveErr);
+    }
+
+    public void testOptionsPositive() {
+        OptionChecker optionChecker = getOptionChecker();
+        String[] input = new String[] {
+        };
+        String[] expected = new String[0];
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-verbose" };
+        expected = new String[] { "-verbose" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "!verbose" };
+        expected = new String[] { "-verbose" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "!verbose", "-verbose" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-verbose", "!verbose" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "^verbose" };
+        expected = new String[] {
+        };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "^verbose", "-verbose" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-verbose", "^verbose" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-verbose", "-debug" };
+        expected = new String[] { "-verbose", "-debug" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "!verbose", "!debug" };
+        expected = new String[] { "-verbose", "-debug" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-verbose", "^verbose", "!debug" };
+        expected = new String[] { "-debug" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "^verbose", "-verbose", "!debug" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "!debug", "^verbose", "-verbose" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-ajc" };
+        expected = new String[] { "-ajc" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-eclipse" };
+        expected = new String[] { "-eclipse" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-eclipse", "-ajc", "!ajc" };
+        expected = new String[] { "-ajc" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-eclipse", "!ajc" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-ajdeCompiler", "^ajc" };
+        expected = new String[] { "-ajdeCompiler" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "!ajdeCompiler", "^ajc" };
+        optionChecker.checkOptions(input, expected);
+
+        input =
+            new String[] {
+                "-verbose",
+                "^verbose",
+                "!quiet",
+                "-quiet",
+                "-debug",
+                "^debug" };
+        expected = new String[] { "-quiet" };
+        optionChecker.checkOptions(input, expected);
+
+        input =
+            new String[] {
+                "-verbose",
+                "^debug",
+                "!quiet",
+                "!quiet",
+                "-debug",
+                "^verbose" };
+        expected = new String[] { "-quiet" };
+        optionChecker.checkOptions(input, expected);
+    }
+
+    public void testOptionsNegative() {
+        OptionChecker optionChecker = getOptionChecker();
+        String[] input = new String[] { "-unknown" };
+        String expectedMissedMatchErr = "-unknown";
+        String expectedResolveErr = null;
+        optionChecker.checkOptionsNegative(
+            input,
+            expectedMissedMatchErr,
+            expectedResolveErr);
+
+        input = new String[] { "!verbose", "^verbose" };
+        expectedMissedMatchErr = null;
+        expectedResolveErr = "conflict";
+        optionChecker.checkOptionsNegative(
+            input,
+            expectedMissedMatchErr,
+            expectedResolveErr);
+
+        input = new String[] { "!ajc", "!eclipse" };
+        optionChecker.checkOptionsNegative(
+            input,
+            expectedMissedMatchErr,
+            expectedResolveErr);
+
+        input = new String[] { "-ajc", "-eclipse" };
+        expectedResolveErr = "collision";
+        optionChecker.checkOptionsNegative(
+            input,
+            expectedMissedMatchErr,
+            expectedResolveErr);
+
+        input = new String[] { "-verbose", "-verbose" };
+        expectedResolveErr = null; // duplicates redundant, not colliding
+        optionChecker.checkOptionsNegative(
+            input,
+            expectedMissedMatchErr,
+            expectedResolveErr);
+    }
+
+    public void testMissedMatches() throws InvalidInputException {
+        checkMissedMatches(new int[0], Values.EMPTY);
+        checkMissedMatches(new int[] { 0 }, 
+            Values.wrapValues(new Option.Value[1])); // null in [0]
+        checkMissedMatches(
+            new int[] { 0, 1, 2 },
+            Values.wrapValues(new Option.Value[] { null, null, null }));
+
+        Option.Factory factory = new Option.Factory("testMissedMatches");
+        Option single = factory.create("verbose");
+        Option multiple =
+            factory
+                .create(
+                    "source",
+                    "source",
+                    Option.STANDARD_PREFIXES,
+                    false,
+                    new String[][] { new String[] { "1.3", "1.4" }
+        });
+
+        Options options = new Options(false);
+        options.addOption(single);
+        options.addOption(multiple);
+        options.freeze();
+        int[] expectNone = new int[0];
+        String[] input = new String[] { "-verbose" };
+        Values result = options.acceptInput(input);
+        checkMissedMatches(expectNone, result);
+
+        input = new String[] { "-verbose", "-verbose" };
+        result = options.acceptInput(input);
+        checkMissedMatches(expectNone, result);
+
+        input = new String[] { "-source", "1.3" };
+        result = options.acceptInput(input);
+        checkMissedMatches(expectNone, result);
+
+        input = new String[] { "-source", "1.4" };
+        result = options.acceptInput(input);
+        checkMissedMatches(expectNone, result);
+
+        input = new String[] { "-verbose", "-missed" };
+        result = options.acceptInput(input);
+        checkMissedMatches(new int[] { 1 }, result);
+
+        input = new String[] { "-source", "1.4", "-missed" };
+        result = options.acceptInput(input);
+        checkMissedMatches(new int[] { 2 }, result);
+
+        input = new String[] { "-source", "1.4", "-missed", "-verbose" };
+        result = options.acceptInput(input);
+        checkMissedMatches(new int[] { 2 }, result);
+
+    }
+    
+    void checkMissedMatches(int[] expected, Values actual) {
+        int[] result = actual.indexMissedMatches();
+        boolean failed = (result.length != expected.length);
+        
+        for (int i = 0; !failed && (i < result.length); i++) {
+            failed = (result[i] != expected[i]);
+        }
+        if (failed) {
+            assertTrue(
+                "expected "
+                    + Values.IntList.render(expected)
+                    + " got "
+                    + Values.IntList.render(result)
+                    + " for "
+                    + actual,
+                false);
+        }
+    }
+
+    public void testComplexOptionsPositive() {
+        OptionChecker optionChecker = getOptionChecker();
+        String[] input = new String[] {
+        };
+        String[] expected = new String[0];
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-target11" };
+        expected = new String[] { "-target11" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "!target12" };
+        expected = new String[] { "-target12" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "!target12", "-target11" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "!target12", "^target11" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "^target12", "^target11" };
+        expected = new String[] {
+        };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "!1.3" };
+        expected = new String[] { "-1.3" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "!1.3", "-1.4" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "!1.3", "^1.4" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "^1.3", "^1.4" };
+        expected = new String[] {
+        };
+        optionChecker.checkOptions(input, expected);
+
+    }
+
+    public void testMultiArgOptionsPositive() {
+        OptionChecker optionChecker = getOptionChecker();
+        String[] input = new String[] {
+        };
+        String[] expected = new String[0];
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-target", "1.1" };
+        expected = new String[] { "-target", "1.1" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "!target", "1.1" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "!target", "1.1", "-target", "1.2" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-target", "1.1", "^target", "1.2" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-source", "1.3" };
+        expected = new String[] { "-source", "1.3" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "!source", "1.3", "-source", "1.4" };
+        optionChecker.checkOptions(input, expected);
+
+        input = new String[] { "-source", "1.3", "^source", "1.4" };
+        input =
+            new String[] {
+                "-source",
+                "1.3",
+                "^source",
+                "1.4",
+                "!source",
+                "1.3",
+                "-source",
+                "1.4" };
+        optionChecker.checkOptions(input, expected);
+    }
+
+    public void testMultiArgOptionsNegative() {
+        OptionChecker optionChecker = getOptionChecker();
+        String[] input = new String[] { "-target" };
+        String expectedException = "not enough arguments";
+
+        optionChecker.checkOptionsNegative(
+            input,
+            expectedException,
+            null,
+            null);
+
+        input = new String[] { "-source" };
+        optionChecker.checkOptionsNegative(
+            input,
+            expectedException,
+            null,
+            null);
+
+        input = new String[] { "-source", "1.1" };
+        expectedException = "not permitted";
+        optionChecker.checkOptionsNegative(
+            input,
+            expectedException,
+            null,
+            null);
+
+        input = new String[] { "-target", "1.3" };
+        optionChecker.checkOptionsNegative(
+            input,
+            expectedException,
+            null,
+            null);
+    }
+
+    public void testMultipleInput() {
+        OptionChecker optionChecker = getOptionChecker();
+        String[][] input =
+            new String[][] {
+                new String[] { "-warn:deprecated" },
+                new String[] { "-warn:deprecated,unverified" },
+                new String[] { "-warn:deprecated", "-warn:unusedLocals" },
+                new String[] { "-g" },
+                new String[] { "-g", "-g:none" },
+                new String[] { "-g:vars,source" },
+                new String[] { "-verbose", "-g:vars,source" },
+                };
+        for (int i = 0; i < input.length; i++) {
+            optionChecker.checkOptions(input[i], input[i]);
+        }
+    }
+
+    private OptionChecker getOptionChecker() {
+        if (null == localOptionChecker) {
+            localOptionChecker = new OptionChecker(getOptions());
+        }
+        return localOptionChecker;
+    }
+}
diff --git a/testing/testsrc/org/aspectj/testing/util/options/OptionsTests.java b/testing/testsrc/org/aspectj/testing/util/options/OptionsTests.java
new file mode 100644 (file)
index 0000000..2cc9379
--- /dev/null
@@ -0,0 +1,30 @@
+/* *******************************************************************
+ * Copyright (c) 2003 Contributors.
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Wes Isberg     initial implementation 
+ * ******************************************************************/
+
+
+package org.aspectj.testing.util.options;
+
+import junit.framework.*;
+
+public class OptionsTests extends TestCase {
+
+    public static Test suite() { 
+        TestSuite suite = new TestSuite(OptionsTests.class.getName());
+        //$JUnit-BEGIN$
+        suite.addTestSuite(OptionsTest.class); 
+        //$JUnit-END$
+        return suite;
+    }
+
+    public OptionsTests(String name) { super(name); }
+
+}  
diff --git a/testing/testsrc/org/aspectj/testing/util/options/ValuesTest.java b/testing/testsrc/org/aspectj/testing/util/options/ValuesTest.java
new file mode 100644 (file)
index 0000000..cc6623f
--- /dev/null
@@ -0,0 +1,69 @@
+/* *******************************************************************
+ * Copyright (c) 2003 Contributors.
+ * All rights reserved. 
+ * This program and the accompanying materials are made available 
+ * under the terms of the Common Public License v1.0 
+ * which accompanies this distribution and is available at 
+ * http://www.eclipse.org/legal/cpl-v10.html 
+ *  
+ * Contributors: 
+ *     Wes Isberg     initial implementation 
+ * ******************************************************************/
+
+package org.aspectj.testing.util.options;
+
+import junit.framework.TestCase;
+
+/**
+ */
+public class ValuesTest extends TestCase {
+
+    public ValuesTest(String s) {
+        super(s);
+    }
+    public void testInvert() {
+        checkInvert(new int[0], 0, new int[0]);  // no input or missed => none found
+        checkInvert(new int[0], 1, new int[] {0});  // no missed, input 1 => 1 found
+        checkInvert(new int[] {0}, 1, new int[] {}); // 1 (all) missed, input 1 => none found
+        checkInvert(new int[] {}, 1, new int[] {0}); // 0 (none) missed, input 1 => 1 found
+        checkInvert(new int[] {1,2}, 3, new int[] {0}); // 2 missed, input 3 => 1 (first) found
+        checkInvert(new int[] {0,2}, 3, new int[] {1}); // 2 missed, input 3 => 1 (middle) found
+        checkInvert(new int[] {0,1}, 3, new int[] {2}); // 2 missed, input 3 => 1 (last) found
+        checkInvert(new int[] {1,3}, 4, new int[] {0,2}); // 2 missed, input 4 => 2 found
+        checkInvert(new int[] {5,6,7}, 8, new int[] {0,1,2,3,4}); // starting run
+        checkInvert(new int[] {0,1,2,3,4}, 8, new int[] {5,6,7}); // ending run
+        checkInvert(new int[] {0,5,6,7}, 8, new int[] {1,2,3,4}); // middle run
+        checkInvert(new int[] {0,5,6,9},10, new int[] {1,2,3,4,7,8}); // two middle run
+        checkInvert(new int[] {1,2,5,6,9},10, new int[] {0,3,4,7,8}); // start, 2 middle run
+        checkInvert(new int[] {0,1,2,5,6},10, new int[] {3,4,7,8,9}); // middle, end run
+    }
+    
+    void checkInvert(int[] missed, int length, int[] expected) {
+        int[] actual = Values.invert(missed, length);
+        assertTrue(null != actual);
+        assertTrue(actual.length == expected.length);
+        for (int i = 0; i < actual.length; i++) {
+            if (expected[i] != actual[i]) {
+                assertTrue("failed at " + i + render(expected, actual), false);
+            }
+        }
+    }
+    static String render(int[] expected, int[] actual) {
+        StringBuffer sb = new StringBuffer();
+        sb.append(" expected ");
+        render(expected, sb);
+        sb.append(" actual ");
+        render(actual, sb);
+        return sb.toString();
+    }
+    static void render(int[] ra, StringBuffer sb) {
+        sb.append("[");
+        for (int i = 0; i < ra.length; i++) {
+            if (i > 0) {
+                sb.append(", ");
+            }
+            sb.append("" + ra[i]);
+        }
+        sb.append("]");
+    }
+}