diff --git a/wlauto/tests/data/test-agenda-bad-syntax.yaml b/wlauto/tests/data/test-agenda-bad-syntax.yaml
new file mode 100644
index 00000000..11d0e2ef
--- /dev/null
+++ b/wlauto/tests/data/test-agenda-bad-syntax.yaml
@@ -0,0 +1 @@
+[ewqh
diff --git a/wlauto/tests/data/test-agenda-not-dict.yaml b/wlauto/tests/data/test-agenda-not-dict.yaml
new file mode 100644
index 00000000..345e6aef
--- /dev/null
+++ b/wlauto/tests/data/test-agenda-not-dict.yaml
@@ -0,0 +1 @@
+Test
diff --git a/wlauto/tests/test_agenda.py b/wlauto/tests/test_agenda.py
index feadee87..7b05d03d 100644
--- a/wlauto/tests/test_agenda.py
+++ b/wlauto/tests/test_agenda.py
@@ -24,6 +24,7 @@ from nose.tools import assert_equal, assert_in, raises
 
 from wlauto.core.agenda import Agenda
 from wlauto.exceptions import ConfigError
+from wlauto.utils.serializer import SerializerSyntaxError
 
 
 YAML_TEST_FILE = os.path.join(os.path.dirname(__file__), 'data', 'test-agenda.yaml')
@@ -35,7 +36,7 @@ workloads:
           test: 1
 """
 invalid_agenda = StringIO(invalid_agenda_text)
-invalid_agenda.name = 'invalid1'
+invalid_agenda.name = 'invalid1.yaml'
 
 duplicate_agenda_text = """
 global:
@@ -49,13 +50,13 @@ workloads:
       workload_name: andebench
 """
 duplicate_agenda = StringIO(duplicate_agenda_text)
-duplicate_agenda.name = 'invalid2'
+duplicate_agenda.name = 'invalid2.yaml'
 
 short_agenda_text = """
 workloads: [antutu, linpack, andebench]
 """
 short_agenda = StringIO(short_agenda_text)
-short_agenda.name = 'short'
+short_agenda.name = 'short.yaml'
 
 default_ids_agenda_text = """
 workloads:
@@ -69,7 +70,7 @@ workloads:
     - vellamo
 """
 default_ids_agenda = StringIO(default_ids_agenda_text)
-default_ids_agenda.name = 'default_ids'
+default_ids_agenda.name = 'default_ids.yaml'
 
 sectioned_agenda_text = """
 sections:
@@ -91,7 +92,7 @@ workloads:
     - nenamark
 """
 sectioned_agenda = StringIO(sectioned_agenda_text)
-sectioned_agenda.name = 'sectioned'
+sectioned_agenda.name = 'sectioned.yaml'
 
 dup_sectioned_agenda_text = """
 sections:
@@ -105,7 +106,7 @@ workloads:
     - nenamark
 """
 dup_sectioned_agenda = StringIO(dup_sectioned_agenda_text)
-dup_sectioned_agenda.name = 'dup-sectioned'
+dup_sectioned_agenda.name = 'dup-sectioned.yaml'
 
 caps_agenda_text = """
 config:
@@ -120,17 +121,17 @@ workloads:
       name: linpack
 """
 caps_agenda = StringIO(caps_agenda_text)
-caps_agenda.name = 'caps'
+caps_agenda.name = 'caps.yaml'
 
 bad_syntax_agenda_text = """
 config:
     # tab on the following line
-	reboot_policy: never
+    reboot_policy: never
 workloads:
     - antutu
 """
 bad_syntax_agenda = StringIO(bad_syntax_agenda_text)
-bad_syntax_agenda.name = 'bad_syntax'
+bad_syntax_agenda.name = 'bad_syntax.yaml'
 
 section_ids_test_text = """
 config:
@@ -145,7 +146,7 @@ sections:
     - id: bar
 """
 section_ids_agenda = StringIO(section_ids_test_text)
-section_ids_agenda.name = 'section_ids'
+section_ids_agenda.name = 'section_ids.yaml'
 
 
 class AgendaTest(TestCase):
@@ -154,42 +155,18 @@ class AgendaTest(TestCase):
         agenda = Agenda(YAML_TEST_FILE)
         assert_equal(len(agenda.workloads), 4)
 
-    def test_duplicate_id(self):
-        try:
-            Agenda(duplicate_agenda)
-        except ConfigError, e:
-            assert_in('duplicate', e.message.lower())  # pylint: disable=E1101
-        else:
-            raise Exception('ConfigError was not raised for an agenda with duplicate ids.')
-
     def test_yaml_missing_field(self):
         try:
-            Agenda(invalid_agenda_text)
+            Agenda(invalid_agenda)
         except ConfigError, e:
             assert_in('workload name', e.message)
         else:
             raise Exception('ConfigError was not raised for an invalid agenda.')
 
-    def test_defaults(self):
-        agenda = Agenda(short_agenda)
-        assert_equal(len(agenda.workloads), 3)
-        assert_equal(agenda.workloads[0].workload_name, 'antutu')
-        assert_equal(agenda.workloads[0].id, '1')
-
-    def test_default_id_assignment(self):
-        agenda = Agenda(default_ids_agenda)
-        assert_equal(agenda.workloads[0].id, '2')
-        assert_equal(agenda.workloads[3].id, '3')
-
-    def test_sections(self):
-        agenda = Agenda(sectioned_agenda)
-        assert_equal(agenda.sections[0].workloads[0].workload_name, 'antutu')
-        assert_equal(agenda.sections[1].runtime_parameters['dp'], 'three')
-
     @raises(ConfigError)
     def test_dup_sections(self):
         Agenda(dup_sectioned_agenda)
 
-    @raises(ConfigError)
+    @raises(SerializerSyntaxError)
     def test_bad_syntax(self):
         Agenda(bad_syntax_agenda)
diff --git a/wlauto/tests/test_configuration.py b/wlauto/tests/test_configuration.py
new file mode 100644
index 00000000..22124ccf
--- /dev/null
+++ b/wlauto/tests/test_configuration.py
@@ -0,0 +1,292 @@
+# pylint: disable=R0201
+
+from unittest import TestCase
+
+from nose.tools import assert_equal, assert_is
+from mock.mock import MagicMock, Mock
+
+from wlauto.exceptions import ConfigError
+from wlauto.core.configuration.tree import Node
+from wlauto.core.configuration.configuration import (ConfigurationPoint, Configuration,
+                                                     JobsConfiguration)
+
+#       A1
+#     /    \
+#   B1      B2
+#  /  \    /  \
+# C1  C2  C3  C4
+#      \
+#      D1
+a1 = Node("A1")
+b1 = a1.add_section("B1")
+b2 = a1.add_section("B2")
+c1 = b1.add_section("C1")
+c2 = b1.add_section("C2")
+c3 = b2.add_section("C3")
+c4 = b2.add_section("C4")
+d1 = c2.add_section("D1")
+
+
+class NodeTest(TestCase):
+
+    def test_node(self):
+        node = Node(1)
+        assert_equal(node.config, 1)
+        assert_is(node.parent, None)
+        assert_equal(node.workloads, [])
+        assert_equal(node.children, [])
+
+    def test_add_workload(self):
+        node = Node(1)
+        node.add_workload(2)
+        assert_equal(node.workloads, [2])
+
+    def test_add_section(self):
+        node = Node(1)
+        new_node = node.add_section(2)
+        assert_equal(len(node.children), 1)
+        assert_is(node.children[0], new_node)
+        assert_is(new_node.parent, node)
+        assert_equal(node.is_leaf, False)
+        assert_equal(new_node.is_leaf, True)
+
+    def test_descendants(self):
+        for got, expected in zip(b1.descendants(), [c1, d1, c2]):
+            print "GOT:{} EXPECTED:{}".format(got.config, expected.config)
+            assert_is(got, expected)
+        print "----"
+        for got, expected in zip(a1.descendants(), [c1, d1, c2, b1, c3, c4, b2]):
+            print "GOT:{} EXPECTED:{}".format(got.config, expected.config)
+            assert_is(got, expected)
+
+    def test_ancestors(self):
+        for got, expected in zip(d1.ancestors(), [c2, b1, a1]):
+            print "GOT:{} EXPECTED:{}".format(got.config, expected.config)
+            assert_is(got, expected)
+        for _ in a1.ancestors():
+            raise Exception("A1 is the root, it shouldn't have ancestors")
+
+    def test_leaves(self):
+        for got, expected in zip(a1.leaves(), [c1, d1, c3, c4]):
+            print "GOT:{} EXPECTED:{}".format(got.config, expected.config)
+            assert_is(got, expected)
+        print "----"
+        for got, expected in zip(d1.leaves(), [d1]):
+            print "GOT:{} EXPECTED:{}".format(got.config, expected.config)
+            assert_is(got, expected)
+
+
+class ConfigurationPointTest(TestCase):
+
+    def test_match(self):
+        cp1 = ConfigurationPoint("test1", aliases=["foo", "bar"])
+        cp2 = ConfigurationPoint("test2", aliases=["fizz", "buzz"])
+
+        assert_equal(cp1.match("test1"), True)
+        assert_equal(cp1.match("foo"), True)
+        assert_equal(cp1.match("bar"), True)
+        assert_equal(cp1.match("fizz"), False)
+        assert_equal(cp1.match("NOT VALID"), False)
+
+        assert_equal(cp2.match("test2"), True)
+        assert_equal(cp2.match("fizz"), True)
+        assert_equal(cp2.match("buzz"), True)
+        assert_equal(cp2.match("foo"), False)
+        assert_equal(cp2.match("NOT VALID"), False)
+
+    def test_set_value(self):
+        cp1 = ConfigurationPoint("test", default="hello")
+        cp2 = ConfigurationPoint("test", mandatory=True)
+        cp3 = ConfigurationPoint("test", mandatory=True, default="Hello")
+        cp4 = ConfigurationPoint("test", default=["hello"], merge=True, kind=list)
+        cp5 = ConfigurationPoint("test", kind=int)
+        cp6 = ConfigurationPoint("test5", kind=list, allowed_values=[1, 2, 3, 4, 5])
+
+        mock = Mock()
+        mock.name = "ConfigurationPoint Unit Test"
+
+        # Testing defaults and basic functionality
+        cp1.set_value(mock)
+        assert_equal(mock.test, "hello")
+        cp1.set_value(mock, value="there")
+        assert_equal(mock.test, "there")
+
+        # Testing mandatory flag
+        err_msg = 'No values specified for mandatory parameter "test" in ' \
+                  'ConfigurationPoint Unit Test'
+        with self.assertRaisesRegexp(ConfigError, err_msg):
+            cp2.set_value(mock)
+        cp3.set_value(mock)  # Should ignore mandatory
+        assert_equal(mock.test, "Hello")
+
+        # Testing Merging - not in depth that is done in the unit test for merge_config
+        cp4.set_value(mock, value=["there"])
+        assert_equal(mock.test, ["Hello", "there"])
+
+        # Testing type conversion
+        cp5.set_value(mock, value="100")
+        assert_equal(isinstance(mock.test, int), True)
+        msg = 'Bad value "abc" for test; must be an integer'
+        with self.assertRaisesRegexp(ConfigError, msg):
+            cp5.set_value(mock, value="abc")
+
+        # Testing that validation is not called when no value is set
+        # if it is it will error because it cannot iterate over None
+        cp6.set_value(mock)
+
+    def test_validation(self):
+        def is_even(value):
+            if value % 2:
+                return False
+            return True
+
+        cp1 = ConfigurationPoint("test", kind=int, allowed_values=[1, 2, 3, 4, 5])
+        cp2 = ConfigurationPoint("test", kind=list, allowed_values=[1, 2, 3, 4, 5])
+        cp3 = ConfigurationPoint("test", kind=int, constraint=is_even)
+        cp4 = ConfigurationPoint("test", kind=list, mandatory=True, allowed_values=[1, 99])
+        mock = MagicMock()
+        mock.name = "ConfigurationPoint Validation Unit Test"
+
+        # Test allowed values
+        cp1.validate_value(mock.name, 1)
+        with self.assertRaises(ConfigError):
+            cp1.validate_value(mock.name, 100)
+        with self.assertRaises(ConfigError):
+            cp1.validate_value(mock.name, [1, 2, 3])
+
+        # Test allowed values for lists
+        cp2.validate_value(mock.name, [1, 2, 3])
+        with self.assertRaises(ConfigError):
+            cp2.validate_value(mock.name, [1, 2, 100])
+
+        # Test constraints
+        cp3.validate_value(mock.name, 2)
+        cp3.validate_value(mock.name, 4)
+        cp3.validate_value(mock.name, 6)
+        msg = '"3" failed constraint validation for "test" in "ConfigurationPoint' \
+              ' Validation Unit Test".'
+        with self.assertRaisesRegexp(ConfigError, msg):
+            cp3.validate_value(mock.name, 3)
+
+        with self.assertRaises(ValueError):
+            ConfigurationPoint("test", constraint=100)
+
+        # Test "validate" methods
+        mock.test = None
+        # Mandatory config point not set
+        with self.assertRaises(ConfigError):
+            cp4.validate(mock)
+        cp1.validate(mock)  # cp1 doesnt have mandatory set
+        cp4.set_value(mock, value=[99])
+        cp4.validate(mock)
+
+    def test_get_type_name(self):
+        def dummy():
+            pass
+        types = [str, list, int, dummy]
+        names = ["str", "list", "integer", "dummy"]
+        for kind, name in zip(types, names):
+            cp = ConfigurationPoint("test", kind=kind)
+            assert_equal(cp.get_type_name(), name)
+
+
+# Subclass just to add some config points to use in testing
+class TestConfiguration(Configuration):
+    name = "Test Config"
+    __configuration = [
+        ConfigurationPoint("test1", default="hello"),
+        ConfigurationPoint("test2", mandatory=True),
+        ConfigurationPoint("test3", default=["hello"], merge=True, kind=list),
+        ConfigurationPoint("test4", kind=int, default=123),
+        ConfigurationPoint("test5", kind=list, allowed_values=[1, 2, 3, 4, 5]),
+    ]
+    configuration = {cp.name: cp for cp in __configuration}
+
+
+class ConfigurationTest(TestCase):
+
+    def test(self):
+        # Test loading defaults
+        cfg = TestConfiguration()
+        expected = {
+            "test1": "hello",
+            "test2": None,
+            "test3": ["hello"],
+            "test4": 123,
+            "test5": None,
+        }
+        # If a cfg point is not set an attribute with value None should still be created
+        for name, value in expected.iteritems():
+            assert_equal(getattr(cfg, name), value)
+
+        # Testing pre finalization "set"
+        cfg.set("test1", "there")
+        assert_equal(cfg.test1, "there")  # pylint: disable=E1101
+        with self.assertRaisesRegexp(ConfigError, 'Unknown Test Config configuration "nope"'):
+            cfg.set("nope", 123)
+
+        # Testing setting values from a dict
+        new_values = {
+            "test1": "This",
+            "test2": "is",
+            "test3": ["a"],
+            "test4": 7357,
+            "test5": [5],
+        }
+        cfg.update_config(new_values)
+        new_values["test3"] = ["hello", "a"]  # This cfg point has merge == True
+        for k, v in new_values.iteritems():
+            assert_equal(getattr(cfg, k), v)
+
+        # Test finalization
+
+        # This is a madatory cfg point so finalization should fail
+        cfg.configuration["test2"].set_value(cfg, value=None, check_mandatory=False)
+        msg = 'No value specified for mandatory parameter "test2" in Test Config'
+        with self.assertRaisesRegexp(ConfigError, msg):
+            cfg.finalize()
+        assert_equal(cfg._finalized, False)  # pylint: disable=W0212
+
+        # Valid finalization
+        cfg.set("test2", "is")
+        cfg.finalize()
+        assert_equal(cfg._finalized, True)  # pylint: disable=W0212
+
+        # post finalization set should failed
+        with self.assertRaises(RuntimeError):
+            cfg.set("test2", "is")
+
+
+class JobsConfigurationTest(TestCase):
+
+    def test_set_global_config(self):
+        jc = JobsConfiguration()
+
+        jc.set_global_config("workload_name", "test")
+        assert_equal(jc.root_node.config.workload_name, "test")
+        # Aliased names (e.g. "name") should be resolved by the parser
+        # before being passed here.
+
+        with self.assertRaises(ConfigError):
+            jc.set_global_config("unknown", "test")
+
+        jc.finalise_global_config()
+        with self.assertRaises(RuntimeError):
+            jc.set_global_config("workload_name", "test")
+
+    def test_tree_manipulation(self):
+        jc = JobsConfiguration()
+
+        workloads = [123, "hello", True]
+        for w in workloads:
+            jc.add_workload(w)
+        assert_equal(jc.root_node.workloads, workloads)
+
+        jc.add_section("section", workloads)
+        assert_equal(jc.root_node.children[0].config, "section")
+        assert_equal(jc.root_node.workloads, workloads)
+
+    def test_generate_job_specs(self):
+
+    # disable_instruments
+    # only_run_ids
diff --git a/wlauto/tests/test_parsers.py b/wlauto/tests/test_parsers.py
new file mode 100644
index 00000000..93aeb11d
--- /dev/null
+++ b/wlauto/tests/test_parsers.py
@@ -0,0 +1,231 @@
+import os
+from unittest import TestCase
+
+from nose.tools import assert_equal  # pylint: disable=E0611
+from mock.mock import Mock, MagicMock, call
+
+from wlauto.exceptions import ConfigError
+from wlauto.core.configuration.parsers import (get_aliased_param,
+                                               _load_file, ConfigParser, EnvironmentVarsParser,
+                                               CommandLineArgsParser)
+from wlauto.core.configuration import (WAConfiguration, RunConfiguration, JobsConfiguration,
+                                       PluginCache)
+from wlauto.utils.types import toggle_set
+
+
+class TestFunctions(TestCase):
+
+    def test_load_file(self):
+        # This does not test read_pod
+
+        # Non-existant file
+        with self.assertRaises(ValueError):
+            _load_file("THIS-IS-NOT-A-FILE", "test file")
+        base_path = os.path.dirname(os.path.realpath(__file__))
+
+        # Top level entry not a dict
+        with self.assertRaisesRegexp(ConfigError, r".+ does not contain a valid test file structure; top level must be a dict\."):
+            _load_file(os.path.join(base_path, "data", "test-agenda-not-dict.yaml"), "test file")
+
+        # Yaml syntax error
+        with self.assertRaisesRegexp(ConfigError, r"Error parsing test file .+: Syntax Error on line 1"):
+            _load_file(os.path.join(base_path, "data", "test-agenda-bad-syntax.yaml"), "test file")
+
+        # Ideal case
+        _load_file(os.path.join(base_path, "data", "test-agenda.yaml"), "test file")
+
+    def test_get_aliased_param(self):
+        # Ideal case
+        d_correct = {"workload_parameters": [1, 2, 3],
+                     "instruments": [2, 3, 4],
+                     "some_other_param": 1234}
+        assert_equal(get_aliased_param(d_correct, [
+            'workload_parameters',
+            'workload_params',
+            'params'
+        ], default=[], pop=False), [1, 2, 3])
+
+        # Two aliases for the same parameter given
+        d_duplicate = {"workload_parameters": [1, 2, 3],
+                       "workload_params": [2, 3, 4]}
+        with self.assertRaises(ConfigError):
+            get_aliased_param(d_duplicate, [
+                'workload_parameters',
+                'workload_params',
+                'params'
+            ], default=[])
+
+        # Empty dict
+        d_none = {}
+        assert_equal(get_aliased_param(d_none, [
+            'workload_parameters',
+            'workload_params',
+            'params'
+        ], default=[]), [])
+
+        # Aliased parameter not present in dict
+        d_not_present = {"instruments": [2, 3, 4],
+                         "some_other_param": 1234}
+        assert_equal(get_aliased_param(d_not_present, [
+            'workload_parameters',
+            'workload_params',
+            'params'
+        ], default=1), 1)
+
+        # Testing pop functionality
+        assert_equal("workload_parameters" in d_correct, True)
+        get_aliased_param(d_correct, [
+            'workload_parameters',
+            'workload_params',
+            'params'
+        ], default=[])
+        assert_equal("workload_parameters" in d_correct, False)
+
+
+class TestConfigParser(TestCase):
+
+    def test_error_cases(self):
+        wa_config = Mock(spec=WAConfiguration)
+        wa_config.configuration = WAConfiguration.configuration
+        run_config = Mock(spec=RunConfiguration)
+        run_config.configuration = RunConfiguration.configuration
+        config_parser = ConfigParser(wa_config,
+                                     run_config,
+                                     Mock(spec=JobsConfiguration),
+                                     Mock(spec=PluginCache))
+
+        # "run_name" can only be in agenda config sections
+        #' and is handled by AgendaParser
+        err = 'Error in "Unit test":\n' \
+              '"run_name" can only be specified in the config section of an agenda'
+        with self.assertRaisesRegexp(ConfigError, err):
+            config_parser.load({"run_name": "test"}, "Unit test")
+
+        # Instrument and result_processor lists in the same config cannot
+        # have conflicting entries.
+        err = 'Error in "Unit test":\n' \
+              '"instrumentation" and "result_processors" have conflicting entries:'
+        with self.assertRaisesRegexp(ConfigError, err):
+            config_parser.load({"instruments": ["one", "two", "three"],
+                                "result_processors": ["~one", "~two", "~three"]},
+                               "Unit test")
+
+    def test_config_points(self):
+        wa_config = Mock(spec=WAConfiguration)
+        wa_config.configuration = WAConfiguration.configuration
+
+        run_config = Mock(spec=RunConfiguration)
+        run_config.configuration = RunConfiguration.configuration
+
+        jobs_config = Mock(spec=JobsConfiguration)
+        plugin_cache = Mock(spec=PluginCache)
+        config_parser = ConfigParser(wa_config, run_config, jobs_config, plugin_cache)
+
+        cfg = {
+            "assets_repository": "/somewhere/",
+            "logging": "verbose",
+            "project": "some project",
+            "project_stage": "stage 1",
+            "iterations": 9001,
+            "workload_name": "name"
+        }
+        config_parser.load(cfg, "Unit test")
+        wa_config.set.assert_has_calls([
+            call("assets_repository", "/somewhere/"),
+            call("logging", "verbose")
+        ], any_order=True)
+        run_config.set.assert_has_calls([
+            call("project", "some project"),
+            call("project_stage", "stage 1")
+        ], any_order=True)
+        print jobs_config.set_global_config.call_args_list
+        jobs_config.set_global_config.assert_has_calls([
+            call("iterations", 9001),
+            call("workload_name", "name"),
+            call("instrumentation", toggle_set()),
+            call("instrumentation", toggle_set())
+        ], any_order=True)
+
+        # Test setting global instruments including a non-conflicting duplicate ("two")
+        jobs_config.reset_mock()
+        instruments_and_result_processors = {
+            "instruments": ["one", "two"],
+            "result_processors": ["two", "three"]
+        }
+        config_parser.load(instruments_and_result_processors, "Unit test")
+        jobs_config.set_global_config.assert_has_calls([
+            call("instrumentation", toggle_set(["one", "two"])),
+            call("instrumentation", toggle_set(["two", "three"]))
+        ], any_order=True)
+
+        # Testing a empty config
+        jobs_config.reset_mock()
+        config_parser.load({}, "Unit test")
+        jobs_config.set_global_config.assert_has_calls([], any_order=True)
+        wa_config.set.assert_has_calls([], any_order=True)
+        run_config.set.assert_has_calls([], any_order=True)
+
+
+class TestEnvironmentVarsParser(TestCase):
+
+    def test_environmentvarsparser(self):
+        wa_config = Mock(spec=WAConfiguration)
+        calls = [call('user_directory', '/testdir'),
+                 call('plugin_paths', ['/test', '/some/other/path', '/testy/mc/test/face'])]
+
+        # Valid env vars
+        valid_environ = {"WA_USER_DIRECTORY": "/testdir",
+                         "WA_PLUGIN_PATHS": "/test:/some/other/path:/testy/mc/test/face"}
+        EnvironmentVarsParser(wa_config, valid_environ)
+        wa_config.set.assert_has_calls(calls)
+
+        # Alternative env var name
+        wa_config.reset_mock()
+        alt_valid_environ = {"WA_USER_DIRECTORY": "/testdir",
+                             "WA_EXTENSION_PATHS": "/test:/some/other/path:/testy/mc/test/face"}
+        EnvironmentVarsParser(wa_config, alt_valid_environ)
+        wa_config.set.assert_has_calls(calls)
+
+        # Test that WA_EXTENSION_PATHS gets merged with WA_PLUGIN_PATHS.
+        # Also checks that other enviroment variables don't cause errors
+        wa_config.reset_mock()
+        calls = [call('user_directory', '/testdir'),
+                 call('plugin_paths', ['/test', '/some/other/path']),
+                 call('plugin_paths', ['/testy/mc/test/face'])]
+        ext_and_plgin = {"WA_USER_DIRECTORY": "/testdir",
+                         "WA_PLUGIN_PATHS": "/test:/some/other/path",
+                         "WA_EXTENSION_PATHS": "/testy/mc/test/face",
+                         "RANDOM_VAR": "random_value"}
+        EnvironmentVarsParser(wa_config, ext_and_plgin)
+        # If any_order=True then the calls can be in any order, but they must all appear
+        wa_config.set.assert_has_calls(calls, any_order=True)
+
+        # No WA enviroment variables present
+        wa_config.reset_mock()
+        EnvironmentVarsParser(wa_config, {"RANDOM_VAR": "random_value"})
+        wa_config.set.assert_not_called()
+
+
+class TestCommandLineArgsParser(TestCase):
+    wa_config = Mock(spec=WAConfiguration)
+    run_config = Mock(spec=RunConfiguration)
+    jobs_config = Mock(spec=JobsConfiguration)
+
+    cmd_args = MagicMock(
+        verbosity=1,
+        output_directory="my_results",
+        instruments_to_disable=["abc", "def", "ghi"],
+        only_run_ids=["wk1", "s1_wk4"],
+        some_other_setting="value123"
+    )
+    CommandLineArgsParser(cmd_args, wa_config, run_config, jobs_config)
+    wa_config.set.assert_has_calls([call("verbosity", 1)], any_order=True)
+    run_config.set.assert_has_calls([call("output_directory", "my_results")], any_order=True)
+    jobs_config.disable_instruments.assert_has_calls([
+        call(toggle_set(["~abc", "~def", "~ghi"]))
+    ], any_order=True)
+    jobs_config.only_run_ids.assert_has_calls([call(["wk1", "s1_wk4"])], any_order=True)
+
+
+class TestAgendaParser(TestCase):
+    pass
diff --git a/wlauto/tests/test_utils.py b/wlauto/tests/test_utils.py
index 39c11def..63f45661 100644
--- a/wlauto/tests/test_utils.py
+++ b/wlauto/tests/test_utils.py
@@ -21,7 +21,7 @@ from nose.tools import raises, assert_equal, assert_not_equal  # pylint: disable
 
 from wlauto.utils.android import check_output
 from wlauto.utils.misc import merge_dicts, merge_lists, TimeoutError
-from wlauto.utils.types import list_or_integer, list_or_bool, caseless_string, arguments, enable_disable_list
+from wlauto.utils.types import list_or_integer, list_or_bool, caseless_string, arguments, toggle_set
 
 
 class TestCheckOutput(TestCase):
@@ -89,10 +89,10 @@ class TestTypes(TestCase):
                      ['--foo', '7', '--bar', 'fizz buzz'])
         assert_equal(arguments(['test', 42]), ['test', '42'])
 
-    def enable_disable_list_test():
+    def toggle_set_test():
 
-        a = enable_disable_list(['qaz', 'qwert', 'asd',  '~fgh', '~seb'])
-        b = enable_disable_list(['qaz', 'xyz',   '~asd', 'fgh',  '~seb'])
+        a = toggle_set(['qaz', 'qwert', 'asd',  '~fgh', '~seb'])
+        b = toggle_set(['qaz', 'xyz',   '~asd', 'fgh',  '~seb'])
 
         a_into_b = ['qaz', 'xyz', '~seb', 'qwert', 'asd', '~fgh']
         assert_equal(a.merge_into(b), a_into_b)
@@ -104,3 +104,6 @@ class TestTypes(TestCase):
 
         assert_equal(a.values(), ['qaz', 'qwert', 'asd'])
         assert_equal(b.merge_with(a).values(), ['qaz', 'xyz', 'qwert', 'asd'])
+
+        assert_equal(a.values(), ['qaz', 'qwert', 'asd'])
+        assert_equal(a.conflicts_with(b), ['~asd', '~fgh'])