changeset 11:b940f267419e

Options class created & changes to mergetag exception messages. Options class created (barebones). Loading/saving from Init. Init no longer runs cleanup functions after initialisation failure. Improved mergetag exception messages & error reporting. committer: Diggory Hardy <diggory.hardy@gmail.com>
author Diggory Hardy <diggory.hardy@gmail.com>
date Thu, 21 Feb 2008 09:05:33 +0000
parents 4c3575400769
children bff0d802cb7d
files conf/options.mtt doc/jobs dsss.conf mde/exception.d mde/init.d mde/input/input.d mde/mde.d mde/mergetag/exception.d mde/mergetag/read.d mde/mergetag/write.d mde/options.d test/mdeTest.d
diffstat 12 files changed, 275 insertions(+), 88 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/conf/options.mtt	Thu Feb 21 09:05:33 2008 +0000
@@ -0,0 +1,5 @@
+{MT01}
+{Default}
+<bool|useThreads=false>
+<char[]|greeting="Hi − options now work!">
+
--- a/doc/jobs	Mon Feb 18 11:54:56 2008 +0000
+++ b/doc/jobs	Thu Feb 21 09:05:33 2008 +0000
@@ -1,7 +1,6 @@
 In progress:
 
 To do:
-*	Write Input unittests; remove untested notes
 *	Why doesn't input.config filtering via headers "Configs" work?
 *	add options support; in particular for whether or not to use threads (and adjust Init to use this).
 *	OutOfMemoryException is not currently checked for − it should be at least in critical places (use high-level catching of all errors?).
@@ -34,11 +33,6 @@
         +/
 
 Done (for git log message):
-*	Init threads now catch own exceptions.
-*	Assigned some inputID devisions.
-*	add remaining SDL event support
-*	Rewrote most of mde.mergetag.defaultdata using generic programming to generate read & write rules for all types. As a result, defaultdata can now write properly.
-*	Axis output now stored with short instead of real.
-*	Input unittest
-*       Moved mde/text to scrapple
-*	DefaultData unittest. Then commit.
+*   Options class created (barebones). Loading/saving from Init.
+*   Init no longer runs cleanup functions after initialisation failure.
+*   Improved mergetag exception messages & error reporting.
--- a/dsss.conf	Mon Feb 18 11:54:56 2008 +0000
+++ b/dsss.conf	Thu Feb 21 09:05:33 2008 +0000
@@ -8,19 +8,14 @@
 }
 
 [mde/mde.d]
-version (Posix) {
-    target=bin/mde
-} else version (Windows) {
-    target=bin/mde.exe
-}
+target=bin/mde
 
 [test/mdeTest.d]
+buildflags=-debug -debug=mdeUnitTest -unittest
+target=bin/mdeTest
+noinstall
 version (Posix) {
-    buildflags=-L-ldl -debug=mdeUnitTest -unittest
-    target=bin/mdeTest
-} else version (Windows) {
-    warn Extra linking probably needed!
-    buildflags=-debug=mdeUnitTest -unittest
-    target=bin/mdeTest.exe
+    buildflags+=-L-ldl
+} else {
+    warn Only posix builds have been tested; elsewhere other libraries will probably need to be linked.
 }
-noinstall
\ No newline at end of file
--- a/mde/exception.d	Mon Feb 18 11:54:56 2008 +0000
+++ b/mde/exception.d	Thu Feb 21 09:05:33 2008 +0000
@@ -38,12 +38,26 @@
     }
 }
 
+class optionsLoadException : mdeException {
+    // NOTE: if symbol is final, it can't be modified in the static this(), but as const it can
+    static const char[] symbol;
+    static this () {	symbol = super.symbol ~ ".options";	}
+    char[] getSymbol () {
+        return symbol;
+    }
+    
+    this (char[] msg) {
+        super(msg);
+    }
+}
+
 debug (mdeUnitTest) {
     import tango.util.log.Log : Log, Logger;
 
     private Logger logger;
     static this() {
         logger = Log.getLogger ("mde.events");
+        logger.info ("Got logger!");
     }
     
     unittest {
--- a/mde/init.d	Mon Feb 18 11:54:56 2008 +0000
+++ b/mde/init.d	Thu Feb 21 09:05:33 2008 +0000
@@ -7,6 +7,7 @@
 
 import mde.exception;
 
+import mde.options;
 import mde.events;
 import global = mde.global;
 import mde.input.input;
@@ -22,8 +23,6 @@
 import derelict.sdl.sdl;
 import derelict.util.exception;
 
-private Logger logger;
-
 /**
  * Static CTOR
  *
@@ -39,8 +38,6 @@
         root.setLevel(root.Level.Trace);
         root.addAppender(new ConsoleAppender);
     }
-    
-    logger = Log.getLogger ("mde.init");
 }
 static ~this()
 {
@@ -78,13 +75,13 @@
         * These should each handle a separate area of initialisation such that these functions could
         * be run simultaneously in separate threads. */
         
-        bool initFailure = false;	// Set true if something goes wrong and we need to abort.
         void setFailure () {		// For synchronization, although shouldn't be necessary
             synchronized initFailure = true;
         }
         void delegate() [] initFuncs = [
         delegate void() {
-            logger = Log.getLogger ("mde.init.Init.SDL");
+            Logger logger = Log.getLogger ("mde.init.Init.SDL");
+            
             // Inits SDL and related stuff (joystick).
             try {
                 DerelictSDL.load();
@@ -113,6 +110,19 @@
             
             openJoysticks ();		// after SDL init
             addCleanupFct (&closeJoysticks);
+        },
+        delegate void() {
+            Logger logger = Log.getLogger ("mde.init.Init.Options");
+            
+            try {
+                Options.load();
+            } catch (optionsLoadException e) {
+                logger.fatal ("Loading options failed; message:");
+                logger.fatal (e.msg);
+                setFailure();
+                return;
+            }
+            addCleanupFct (&Options.save);  // not strictly cleanup, but needs to be called somewhere
         }
         ];
         
@@ -157,6 +167,7 @@
             // All cleanup-on-failure must be done here.
             runCleanupFcts();
             
+            logger.fatal ("Init failed");
             // Throw an exception to signal failure and prevent DTOR from also running.
             throw new initException ("Initialisation failed due to above exceptions.");
         }
@@ -167,7 +178,11 @@
     * Currently unthreaded; probably might as well stay that way. */
     ~this()
     {
-        runCleanupFcts();	// if threading, note not all functions can be called simultaeneously
+        if (!initFailure) {
+            logger.info ("Cleaning up...");
+            runCleanupFcts();	// if threading, note not all functions can be called simultaeneously
+            logger.info ("Done!");
+        }
     }
     
     /* Cleanup Functions.
@@ -188,24 +203,29 @@
             foreach_reverse (fct; cleanup) fct();
         }
     }
-}
-
-debug (mdeUnitTest) unittest {
-    /* Fake init and cleanup. This happens before the CTOR runs so the extra Init.runCleanupFcts()
-    * call isn't going to mess things up. The extra function called by runCleanupFcts won't cause
-    * any harm either. */
-    static bool initialised = false;
-    static void cleanup () {	initialised = false;	}
+    
+    /* This is set true by this() if something goes wrong and we need to abort.
+    * If it's true, no cleanup should be done by the DTOR (since the DTOR still runs after an
+    * exception has been thrown). */
+    private bool initFailure = false;
+    
+    debug (mdeUnitTest) unittest {
+        /* Fake init and cleanup. This happens before the CTOR runs so the extra Init.runCleanupFcts()
+        * call isn't going to mess things up. The extra function called by runCleanupFcts won't cause
+        * any harm either. */
+        static bool initialised = false;
+        static void cleanup () {	initialised = false;	}
         
-    static void init () {
-        initialised = true;
-        Init.addCleanupFct (&cleanup);
-    }
+        static void init () {
+            initialised = true;
+            Init.addCleanupFct (&cleanup);
+        }
         
-    init();
-    assert (initialised);
-    Init.runCleanupFcts();
-    assert (!initialised);
+        init();
+        assert (initialised);
+        Init.runCleanupFcts();
+        assert (!initialised);
 
-    logger.info ("Unittest complete.");
+        logger.info ("Unittest complete.");
+    }
 }
--- a/mde/input/input.d	Mon Feb 18 11:54:56 2008 +0000
+++ b/mde/input/input.d	Thu Feb 21 09:05:33 2008 +0000
@@ -180,14 +180,14 @@
                 }
                 break;
             
-            case SDL_JOYBALLMOTION:	// NOTE: untested
+            case SDL_JOYBALLMOTION:
                 outQueue[]* p = (Config.M.JOYBALL | (event.jball.which << 12) | event.jball.ball) in config.relMotion;
                 if (p) foreach (outQueue q; *p) {
                     mEvent (this, event.jball.xrel, event.jball.yrel, readOutQueue(q));
                 }
                 break;
             
-            case SDL_JOYHATMOTION:	// NOTE: untested
+            case SDL_JOYHATMOTION:
                 static ubyte[uint] oldJHatVals;		// necessary to store this to know which "axis" changed
         
                 uint index = (event.jhat.which << 12) | event.jhat.hat;
@@ -297,10 +297,9 @@
     bool[inputID] button;		// Table of button states
     short[inputID] axis;		// Table of axes states
     ushort mouse_x, mouse_y;		// Current screen coords of the window manager mouse
-    // FIXME: might need a bit of work... at any rate defining a default ID.
     RelPair[inputID] relMotion;		// Table of relative mouse / joystick ball motions
     
-    // FIXME: these need to be more like multimaps, supporting multiple dgs (also some means of removal?)
+    // FIXME: currently no means of removal
     ButtonCallback[][inputID]	buttonCallbacks;
     AxisCallback[][inputID]	axisCallbacks;
     RelMotionCallback[][inputID] relMotionCallbacks;
--- a/mde/mde.d	Mon Feb 18 11:54:56 2008 +0000
+++ b/mde/mde.d	Thu Feb 21 09:05:33 2008 +0000
@@ -9,6 +9,7 @@
 import global = mde.global;
 import mde.events;
 import mde.scheduler;
+import mde.options;     // greeting message
 import mde.exception;
 
 import mde.input.input;
@@ -23,6 +24,7 @@
 
 int main()
 {
+    //BEGIN Initialisation
     Logger logger = Log.getLogger ("mde.mde");
     
     scope Init init;
@@ -40,6 +42,9 @@
             global.run = false;
         }
     } );
+    //END Initialisation
+    
+    logger.info (options.greeting);
     
     while (global.run) {
         Scheduler.run (Clock.now());
--- a/mde/mergetag/exception.d	Mon Feb 18 11:54:56 2008 +0000
+++ b/mde/mergetag/exception.d	Thu Feb 21 09:05:33 2008 +0000
@@ -18,23 +18,38 @@
     this (char[] msg) {
         super(msg);
     }
-    this () {}
+    this () {   // Only called when an unexpected exception/error occurs
+        super ("Unknown exception");
+    }
 }
 
 /** Thrown on file IO errors. */
 class MTFileIOException : MTException {
-    this () {}
+    this () {
+        super ("File IO exception");
+    }
 }
 
 /** Thrown on unknown format errors; when reading or writing and the filetype cannot be guessed. */
 class MTFileFormatException : MTException {
-    this () {}
+    this () {
+        super ("File format exception");
+    }
+}
+
+/** Thrown on syntax errors when reading; bad tags or unexpected EOF. */
+class MTSyntaxException : MTException {
+    this () {
+        super ("Syntax exception");
+    }
 }
 
 /** Thrown by addTag (in classes implementing dataset.DataSection) when a tag is read with an
  * unrecognised type field. */
 class MTUnknownTypeException : MTException {
-    this () {}
+    this () {
+        super ("Unknown type");
+    }
     this (char[] msg) {
         super (msg);
     }
@@ -43,17 +58,21 @@
 /** Thrown by addTag (in classes implementing dataset.DataSection) when a data parsing error occurs
 * (really just to make whoever called addTag to log a warning saying where the error occured). */
 class MTaddTagParseException : MTException {
-    this () {}
+    this () {
+        super ("Parse exception within addTag");
+    }
 }
 
+/+
 /// Thrown by TypeView.parse on errors.
 class MTBadTypeStringException : MTException {
     this () {}
 }
++/
 
 /// Thrown by *Writer.write.
 class MTNoDataSetException : MTException {
-    this (char[] msg) {
-        super(msg);
+    this () {
+        super ("No dataset");
     }
 }
--- a/mde/mergetag/read.d	Mon Feb 18 11:54:56 2008 +0000
+++ b/mde/mergetag/read.d	Thu Feb 21 09:05:33 2008 +0000
@@ -38,8 +38,16 @@
  * // get your data from foo.dataset.
  * -----------------------
  *
- * Only a child-class of MTException will ever be thrown, currently MTFileIOException if the file
- * could not be read or MTFileFormatException on any error when parsing the file.
+ * Throws:
+ *  $(TABLE
+ *  $(TR $(TH Exception) $(TH Thrown when))
+ *  $(TR $(TD MTFileIOException) $(TD An error occurs while opening the file))
+ *  $(TR $(TD MTFileFormatException) $(TD The file doesn't start with a recognised header/version))
+ *  $(TR $(TD MTSyntaxException) $(TD A file syntax error occurs))
+ *  $(TR $(TD MTException) $(TD An unexpected error occurs))
+ *  )
+ * Note that all exceptions extend MTException and when any exception is thrown the class is
+ * rendered unusable: any subsequent calls to read will be ignored.
  *
  * Threading: Separate instances of Reader should be thread-safe provided access to the same
  * dataset is synchronized; i.e. no two readers refering to the same dataset should run
@@ -161,9 +169,6 @@
         }
         else endOfHeader = parseSection (6,null);
     }
-    // Was intended to close file, but file is closed within CTOR anyway.
-    public ~this () {
-    }
 //END METHODS: CTOR / DTOR
     
 //BEGIN METHODS: PUBLIC
@@ -306,7 +311,7 @@
                 // Type section of tag:
                 uint pos_s = pos;
                 fbufLocateDataTagChar (pos, false);	// find end of type section
-                if (fbuf[pos] != '|') throwMTErr (ErrDTAG);
+                if (fbuf[pos] != '|') throwMTErr (ErrDTAG, new MTSyntaxException);
                 char[] type = fbuf[pos_s..pos];
                 
                 fbufIncrement (pos);
@@ -314,15 +319,15 @@
                 // ID section of tag:
                 pos_s = pos;
                 fbufLocateDataTagChar (pos, false);	// find end of type section
-                if (fbuf[pos] != '=') throwMTErr (ErrDTAG);
+                if (fbuf[pos] != '=') throwMTErr (ErrDTAG, new MTSyntaxException);
                 ID tagID = cast(ID) fbuf[pos_s..pos];
                 
                 fbufIncrement (pos);
                 
                 // Data section of tag:
                 pos_s = pos;
-                fbufLocateDataTagChar (pos, true);	// find end of data section
-                if (fbuf[pos] != '>') throwMTErr (ErrDTAG);
+                fbufLocateDataTagChar (pos, true);      // find end of data section
+                if (fbuf[pos] != '>') throwMTErr (ErrDTAG, new MTSyntaxException);
                 char[] data = fbuf[pos_s..pos];
                 
                 if (!comment && dsec != null) {
@@ -341,7 +346,7 @@
                     catch (Exception e) {
                         logger.error ("Unknown error occured" ~ ErrInFile ~ ':');
                         logger.error (e.msg);
-                        throw e;                        // Fatal to Reader
+                        throwMTErr (e.msg);             // Fatal to Reader
                     }
                 } else comment = false;			// cancel comment status now
             }
@@ -365,7 +370,7 @@
                 comment = true;				// starting a comment (or an error)
                 					// variable is reset at end of comment
             } else					// must be an error
-                throwMTErr ("Invalid character (or sequence starting \"!\") outside of tag" ~ ErrInFile);
+            throwMTErr ("Invalid character (or sequence starting \"!\") outside of tag" ~ ErrInFile, new MTSyntaxException);
         }
         // if code execution reaches here, we're at EOF
         // possible error: last character was ! (but don't bother checking since it's inconsequential)
@@ -383,7 +388,7 @@
         for (; pos < fbuf.length; ++pos)
             if (fbuf[pos] == '}' || fbuf[pos] == '{') break;
         
-        if (fbuf[pos] != '}') throwMTErr ("Bad section tag format: not {id}" ~ ErrInFile);
+        if (fbuf[pos] != '}') throwMTErr ("Bad section tag format: not {id}" ~ ErrInFile, new MTSyntaxException);
         ID id = cast(ID) fbuf[start..pos];
         fbufIncrement(pos);
         return id;
@@ -392,10 +397,10 @@
     /* Increments pos and checks it hasn't hit fbuf.length . */
     private void fbufIncrement(inout uint pos) {
         ++pos;
-        if (pos >= fbuf.length) throwMTErr("Unexpected EOF" ~ ErrInFile);
+        if (pos >= fbuf.length) throwMTErr("Unexpected EOF" ~ ErrInFile, new MTSyntaxException);
     }
     
-    private void throwMTErr (char[] msg, MTException exc = new MTFileFormatException) {
+    private void throwMTErr (char[] msg, MTException exc = new MTException) {
         fatal = true;	// if anyone catches the error and tries to do anything --- we're dead now
         logger.error (msg);	// report the error
         throw exc;		// and signal our error
--- a/mde/mergetag/write.d	Mon Feb 18 11:54:56 2008 +0000
+++ b/mde/mergetag/write.d	Thu Feb 21 09:05:33 2008 +0000
@@ -51,12 +51,27 @@
  * method parameter and using the filename extension as a fallback.
  *
  * An exception is thrown if neither test can deduce the writing method.
+ *
+ * Use as:
+ * -----------------------
+ * DataSet dataset; // contains data to write
+ * IWriter foo;
+ * try {
+ *   foo = makeWriter("foo.mtt", dataset);
+ *   foo.write();
+ * }
+ * catch (MTException) {}
+ * -----------------------
+ *
+ * Throws:
+ *  MTFileFormatException if unable to determine writing format or use requested format.
  */
 IWriter makeWriter (char[] path, DataSet dataset = null, WriterMethod method = WriterMethod.Unspecified) {
     return makeWriter (new FilePath (path), dataset, method);
 }
 /** ditto */
-IWriter makeWriter (PathView path, DataSet dataset = null, WriterMethod method = WriterMethod.Unspecified) {
+IWriter makeWriter (PathView path, DataSet dataset = null, WriterMethod method = WriterMethod.Unspecified)
+{
     void throwMTErr (char[] msg, Exception exc = new MTException) {
         logger.error (msg);
         throw exc;
@@ -70,6 +85,7 @@
     if (method == WriterMethod.Binary) throwMTErr ("Binary writing not supported yet!", new MTFileFormatException);
     else if (method == WriterMethod.Text) return new TextWriter (path, dataset);
     else if (method == WriterMethod.Both) throwMTErr ("Dual writing not supported yet!", new MTFileFormatException);
+    else debug throwMTErr ("Bad value of method", new MTFileFormatException);
 }
 
 /// Interface for methods and data necessarily available in TextWriter and/or BinaryWriter.
@@ -92,8 +108,17 @@
  * Class to write a dataset to a file.
  *
  * Files are only actually open for writing while the write() method is running.
+ *
+ * Throws:
+ *  $(TABLE
+ *  $(TR $(TH Exception) $(TH Thrown when))
+ *  $(TR $(TD MTNoDataSetException) $(TD No dataset is available to write from))
+ *  $(TR $(TD MTFileIOException) $(TD An error occurs while attemting to write the file))
+ *  $(TR $(TD MTException) $(TD An unexpected error occurs))
+ *  )
+ * Note that all exceptions extend MTException; unlike Reader exceptions don't block further calls.
  */
-scope class TextWriter : IWriter
+class TextWriter : IWriter
 {
 //BEGIN DATA
     /// Get or set the DataSet.
@@ -112,11 +137,13 @@
     /* The container where data is written from. */
     DataSet _dataset;
     
-    PathView path;
+    PathView _path;
 //END DATA
     
 //BEGIN CTOR / DTOR
-    /** Tries to open file path for writing.
+    /** Prepares to open file path for writing.
+    *
+    * The call doesn't actually execute any code so cannot fail (unless out of memory).
     *
     * Params:
     * path = The name or FilePath of the file to open.
@@ -124,12 +151,12 @@
     * dataset_ = If null create a new DataSet, else use existing DataSet *dataset_ and merge read
     *     data into it.
     */
-    public this (char[] _path, DataSet ds = null) {
-        this (new FilePath (_path), ds);
+    public this (char[] path, DataSet ds = null) {
+        this (new FilePath (path), ds);
     }
     /** ditto */
-    public this (PathView _path, DataSet ds = null) {
-        path = _path;
+    public this (PathView path, DataSet ds = null) {
+        _path = path;
         _dataset = ds;
     }
 //END CTOR / DTOR
@@ -141,22 +168,17 @@
      * header should be written only once. This behaviour could, for instance, be used to write
      * multiple DataSets into one file without firstly merging them. Note that this behaviour may
      * be changed when binary support is added.
-     *
-     * Throws:
-     *	MTNoDataSetException if the dataset is null,
-     *	MTFileIOException if a file IO error occurs,
-     *	MTException on any other exception (unexpected).
      */
     public void write ()
     {
-        if (!_dataset) throw new MTNoDataSetException ("write(): Dataset needed to write from!");
+        if (!_dataset) throwMTErr ("write(): no Dataset available to write from!", new MTNoDataSetException ());
         
         try {
             FileConduit conduit;	// actual conduit; don't use directly when there's content in the buffer
             IBuffer buffer;		// write strings directly to this (use opCall(void[]) )
             
             // Open a conduit on the file:
-            conduit = new FileConduit (path, FileConduit.WriteCreate);
+            conduit = new FileConduit (_path, FileConduit.WriteCreate);
             scope(exit) conduit.close();
             
             buffer = new Buffer(conduit);	// And a buffer
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mde/options.d	Thu Feb 21 09:05:33 2008 +0000
@@ -0,0 +1,112 @@
+/** This module handles stored options, currently all except input maps.
+*
+* The purpose of having all options centrally controlled is to allow generic handling by the GUI
+* and ease saving and loading of values.
+*/
+module mde.options;
+
+import mde.exception;
+
+import mde.mergetag.read;
+import mde.mergetag.write;
+import mde.mergetag.dataset;
+import mde.mergetag.exception;
+
+import tango.scrapple.text.convert.parseTo : parseTo;
+import tango.scrapple.text.convert.parseFrom : parseFrom;
+
+import tango.io.FilePath : FilePath;
+import tango.util.log.Log : Log, Logger;
+
+private Logger logger;
+static this() {
+    logger = Log.getLogger ("mde.options");
+}
+
+Options options;
+
+// Later: add automatic saving/loading for variables, variable lists with GUI name and description.
+
+/** Class instance stores all options.
+*
+* All options and handling should be non-static to allow possibility of profiles later.
+*/
+class Options : DataSection
+{
+    /* The options.
+    *
+    * For now, static variables. Later, lists (and static variables?).
+    */
+    bool useThreads;
+    char[] greeting;    // just a testing message
+    
+    
+    /* The code to load/save the values.
+    *
+    * Uses mergetag.
+    */
+    void addTag (char[] tp, ID id, char[] dt) {
+        if (tp == "bool") {
+            if (id == cast(ID)"useThreads") useThreads = parseTo!(bool) (dt);
+        } else if (tp == "char[]") {
+            if (id == cast(ID)"greeting") greeting = parseTo!(char[]) (dt);
+        } else
+            throw new MTUnknownTypeException ("Only bool currently supported.");
+    }
+    void writeAll (ItemDelg dlg) {
+        dlg ("bool", cast(ID)"useThreads", parseFrom!(bool) (useThreads));
+        dlg ("char[]", cast(ID)"greeting", parseFrom!(char[]) (greeting));
+    }
+    
+    
+    /* Load/save options from file.
+    *
+    * If the file doesn't exist, no reading is attempted (options are left at default values).
+    */
+    private static const fileName = "conf/options.mtt";
+    private static const secName = cast(ID)"Default";
+    static void load () {
+        scope(failure) options = new Options();     // FIXME necessary while options is accessed statically and access is unprotected!
+        
+        FilePath filePath = new FilePath (fileName);
+        // If it's not a file, stop. It should still be created on exit (assuming it's not a folder).
+        if (!filePath.exists || filePath.fileSize == 0u) {
+            options = new Options();
+            return;
+        }
+        
+        Reader reader;
+        try {
+            reader = new Reader(filePath);
+            reader.dataSecCreator = function DataSection(ID) {
+                return new Options;
+            };
+            reader.read;
+        } catch (MTException e) {
+            logger.error ("Mergetag exception occurred:");
+            logger.error (e.msg);
+            throw new optionsLoadException ("Loading aborted: mergetag exception");
+        }
+        
+        DataSection* secP = secName in reader.dataset.sec;
+        options = cast(Options) *secP;
+        if (options is null) {
+            throw new optionsLoadException ("Loading failed: expected section not found");
+        }
+        // else loading was succesful.
+    }
+    static void save () {
+        DataSet ds = new DataSet();
+        ds.sec[secName] = options;
+        
+        IWriter writer;
+        try {
+            writer = makeWriter (fileName, ds);
+            writer.write();
+        } catch (MTException e) {
+            logger.error ("Mergetag exception occurred; saving aborted:");
+            logger.error (e.msg);
+            //FIXME: currently nothing done besides logging the error
+        }
+    }
+}
--- a/test/mdeTest.d	Mon Feb 18 11:54:56 2008 +0000
+++ b/test/mdeTest.d	Thu Feb 21 09:05:33 2008 +0000
@@ -2,9 +2,6 @@
 */
 module test.mdeTest;
 
-// Define this to run unittests:
-debug=mdeUnitTest;
-
 // This module should import all mde modules containing unittests:
 import mde.input.input;
 import mde.mergetag.dataset;