brillo blueprint: create `brillo blueprint` tool.
Creates `brillo blueprint` as a tool to help users create blueprints
without manually creating the .json file.
Also modifies blueprint_lib to match brick_lib closer in behavior. In
particular:
* Raise an error when trying to create a blueprint at a non-empty path.
* Verify during creation that all specified BSP/bricks exist.
Adds some config file helper functions in workspace_lib for
both brick and blueprint use.
BUG=brillo:858
TEST=cbuildbot/run_tests
TEST=brillo blueprint create foo
TEST=brillo blueprint create //foo --brick bricks/my_brick
Change-Id: Idcc3ce05e6be36d46439b1653cb4f422aa5a775c
Reviewed-on: https://chromium-review.googlesource.com/267391
Reviewed-by: Bertrand Simonnet <bsimonnet@chromium.org>
Tested-by: David Pursell <dpursell@chromium.org>
Trybot-Ready: David Pursell <dpursell@chromium.org>
Commit-Queue: David Pursell <dpursell@chromium.org>
diff --git a/cli/brillo/brillo_blueprint.py b/cli/brillo/brillo_blueprint.py
new file mode 100644
index 0000000..4c64439
--- /dev/null
+++ b/cli/brillo/brillo_blueprint.py
@@ -0,0 +1,68 @@
+# Copyright 2015 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""brillo blueprint: Create and manage blueprints."""
+
+from __future__ import print_function
+
+from chromite.cli import command
+from chromite.lib import blueprint_lib
+from chromite.lib import cros_build_lib
+from chromite.lib import cros_logging as logging
+
+
+@command.CommandDecorator('blueprint')
+class BlueprintCommand(command.CliCommand):
+ """Create blueprints."""
+
+ EPILOG = """
+To create a blueprint "foo" in workspace/blueprints/:
+ brillo blueprint create foo
+
+Specify a path to use a directory other than workspace/blueprints/:
+ brillo blueprint create //my_blueprints/foo
+ brillo blueprint create ./foo
+
+You can configure the initial setup as well:
+ brillo blueprint create foo --bsp <bsp> --brick <brick>
+
+Multiple bricks can be specified by repeating -b/--brick:
+ brillo blueprint create foo --bsp <bsp> -b <brick> -b <brick>
+"""
+
+ @classmethod
+ def AddParser(cls, parser):
+ super(cls, BlueprintCommand).AddParser(parser)
+ subparser = parser.add_subparsers()
+ create_parser = subparser.add_parser('create', epilog=cls.EPILOG)
+ create_parser.add_argument('blueprint', help='Blueprint path/locator.')
+ create_parser.add_argument('--brick', '-b', action='append', dest='bricks',
+ help='Add an existing brick to the blueprint.',
+ default=[], metavar='BRICK')
+ create_parser.add_argument('--bsp', help='Set the blueprint BSP.',
+ default='')
+ create_parser.set_defaults(handler_func='Create')
+
+ def Create(self):
+ """Create a new blueprint."""
+ path = blueprint_lib.ExpandBlueprintPath(self.options.blueprint)
+
+ if not self.options.bsp:
+ logging.warning('No BSP was specified; blueprint will not build until a '
+ 'valid BSP is set in %s.', path)
+
+ config = {blueprint_lib.BSP_FIELD: self.options.bsp,
+ blueprint_lib.BRICKS_FIELD: self.options.bricks}
+ blueprint_lib.Blueprint(path, initial_config=config)
+
+ def Run(self):
+ """Dispatch the call to the right handler."""
+ self.options.Freeze()
+ try:
+ getattr(self, self.options.handler_func)()
+ except Exception as e:
+ if self.options.debug:
+ raise
+ else:
+ cros_build_lib.Die(e)
diff --git a/cli/brillo/brillo_blueprint_unittest b/cli/brillo/brillo_blueprint_unittest
new file mode 120000
index 0000000..ef3e37b
--- /dev/null
+++ b/cli/brillo/brillo_blueprint_unittest
@@ -0,0 +1 @@
+../../scripts/wrapper.py
\ No newline at end of file
diff --git a/cli/brillo/brillo_blueprint_unittest.py b/cli/brillo/brillo_blueprint_unittest.py
new file mode 100644
index 0000000..1d16081
--- /dev/null
+++ b/cli/brillo/brillo_blueprint_unittest.py
@@ -0,0 +1,79 @@
+# Copyright 2015 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""This module tests the brillo blueprint command."""
+
+from __future__ import print_function
+
+from chromite.cli import command_unittest
+from chromite.cli.brillo import brillo_blueprint
+from chromite.lib import cros_test_lib
+
+
+class MockBlueprintCommand(command_unittest.MockCommand):
+ """Mock out the `brillo blueprint` command."""
+ TARGET = 'chromite.cli.brillo.brillo_blueprint.BlueprintCommand'
+ TARGET_CLASS = brillo_blueprint.BlueprintCommand
+ COMMAND = 'blueprint'
+
+
+class BlueprintCommandTest(cros_test_lib.OutputTestCase,
+ cros_test_lib.WorkspaceTestCase):
+ """Test the flow of BlueprintCommand.Run()."""
+
+ FOO_BRICK = '//bricks/foo'
+ BAR_BRICK = '//bricks/bar'
+ BSP_BRICK = '//bsp/my_bsp'
+
+ def setUp(self):
+ """Patches objects."""
+ self.cmd_mock = None
+ self.CreateWorkspace()
+ # Create some fake bricks for testing.
+ for brick in (self.FOO_BRICK, self.BAR_BRICK, self.BSP_BRICK):
+ self.CreateBrick(brick)
+
+ def SetupCommandMock(self, cmd_args):
+ """Sets up the MockBlueprintCommand."""
+ self.cmd_mock = MockBlueprintCommand(cmd_args)
+ self.StartPatcher(self.cmd_mock)
+
+ def RunCommandMock(self, *args, **kwargs):
+ """Sets up and runs the MockBlueprintCommand."""
+ self.SetupCommandMock(*args, **kwargs)
+ self.cmd_mock.inst.Run()
+
+ def testCreateBlueprint(self):
+ """Tests basic blueprint creation."""
+ with self.OutputCapturer():
+ self.RunCommandMock(['create', 'foo'])
+ self.AssertBlueprintExists('//blueprints/foo.json')
+ # Also make sure we get a warning for the missing BSP.
+ self.AssertOutputContainsWarning('No BSP was specified', check_stderr=True)
+
+ def testCreateBlueprintBsp(self):
+ """Tests --bsp argument."""
+ self.RunCommandMock(['create', 'foo', '--bsp', self.BSP_BRICK])
+ self.AssertBlueprintExists('//blueprints/foo.json', bsp=self.BSP_BRICK)
+
+ def testCreateBlueprintBricks(self):
+ """Tests --brick and -b arguments."""
+ self.RunCommandMock(['create', 'foo', '--brick', self.FOO_BRICK,
+ '-b%s' % self.BAR_BRICK])
+ self.AssertBlueprintExists('//blueprints/foo.json',
+ bricks=[self.FOO_BRICK, self.BAR_BRICK])
+
+ def testCreateBlueprintAll(self):
+ """Test --brick and --bsp arguments together."""
+ self.RunCommandMock(['create', 'foo', '--brick', self.FOO_BRICK,
+ '--bsp', self.BSP_BRICK])
+ self.AssertBlueprintExists('//blueprints/foo.json', bsp=self.BSP_BRICK,
+ bricks=[self.FOO_BRICK])
+
+ def testBlueprintInvalidCommand(self):
+ """Tests that `brillo blueprint` without `create` prints usage."""
+ with self.OutputCapturer():
+ with self.assertRaises(SystemExit):
+ self.RunCommandMock([])
+ self.AssertOutputContainsLine('usage:', check_stderr=True)
diff --git a/lib/blueprint_lib.py b/lib/blueprint_lib.py
index 0c7f5fc..77d4186 100644
--- a/lib/blueprint_lib.py
+++ b/lib/blueprint_lib.py
@@ -6,18 +6,48 @@
from __future__ import print_function
-import json
import os
from chromite.lib import brick_lib
-from chromite.lib import osutils
from chromite.lib import workspace_lib
-class BlueprintNotFound(Exception):
+# Field names for specifying initial configuration.
+BRICKS_FIELD = 'bricks'
+BSP_FIELD = 'bsp'
+
+
+class BlueprintNotFoundError(Exception):
"""The blueprint does not exist."""
+class BlueprintCreationError(Exception):
+ """Blueprint creation failed."""
+
+
+# TODO(dpursell): Enable this shorthand blueprint specifications for all CLI
+# tools. http://brbug.com/931.
+def ExpandBlueprintPath(path):
+ """Expand a blueprint path using some common assumptions.
+
+ Makes the following changes to |path|:
+ 1. Put non-paths in //blueprints (e.g. foo -> //blueprints/foo).
+ 2. Add .json if no extension was given.
+
+ Args:
+ path: blueprint path.
+
+ Returns:
+ Modified blueprint path.
+ """
+ # Non-path arguments should be put in //blueprints by default.
+ if '/' not in path:
+ path = os.path.join('//blueprints', path)
+ if os.path.splitext(path)[1] != '.json':
+ path += '.json'
+ return path
+
+
class Blueprint(object):
"""Encapsulates the interaction with a blueprint."""
@@ -30,32 +60,64 @@
with '//'.
initial_config: A dictionary of key-value pairs to seed a new blueprint
with if the specified blueprint doesn't already exist.
+
+ Raises:
+ BlueprintNotFoundError: No blueprint exists at |blueprint_loc| and no
+ |initial_config| was given to create a new one.
+ BlueprintCreationError: |initial_config| was specified but a file
+ already exists at |blueprint_loc|.
"""
self._path = (workspace_lib.LocatorToPath(blueprint_loc)
if workspace_lib.IsLocator(blueprint_loc) else blueprint_loc)
self._locator = workspace_lib.PathToLocator(self._path)
- if not os.path.exists(self._path):
- if initial_config:
- osutils.WriteFile(self._path,
- json.dumps(initial_config, sort_keys=True,
- indent=4, separators=(',', ': ')),
- makedirs=True)
- else:
- raise BlueprintNotFound('blueprint %s not found.' % self._path)
- self.config = json.loads(osutils.ReadFile(self._path))
+ if initial_config is not None:
+ self._CreateBlueprintConfig(initial_config)
+
+ try:
+ self.config = workspace_lib.ReadConfigFile(self._path)
+ except IOError:
+ raise BlueprintNotFoundError('Blueprint %s not found.' % self._path)
+
+ def _CreateBlueprintConfig(self, config):
+ """Create an initial blueprint config file.
+
+ Converts all brick paths in |config| into locators then saves the
+ configuration file to |self._path|.
+
+ Currently fails if |self._path| already exists, but could be
+ generalized to allow re-writing config files if needed.
+
+ Args:
+ config: configuration dictionary.
+
+ Raises:
+ BlueprintCreationError: A brick in |config| doesn't exist or an
+ error occurred while saving the config file.
+ """
+ if os.path.exists(self._path):
+ raise BlueprintCreationError('File already exists at %s.' % self._path)
+
+ try:
+ # Turn brick specifications into locators.
+ if config.get(BRICKS_FIELD):
+ config[BRICKS_FIELD] = [brick_lib.Brick(b).brick_locator
+ for b in config[BRICKS_FIELD]]
+ if config.get(BSP_FIELD):
+ config[BSP_FIELD] = brick_lib.Brick(config[BSP_FIELD]).brick_locator
+
+ # Create the config file.
+ workspace_lib.WriteConfigFile(self._path, config)
+ except (brick_lib.BrickNotFound, workspace_lib.ConfigFileError) as e:
+ raise BlueprintCreationError('Blueprint creation failed. %s' % e)
def GetBricks(self):
"""Returns the bricks field of a blueprint."""
- return self.config.get('bricks', [])
+ return self.config.get(BRICKS_FIELD, [])
def GetBSP(self):
"""Returns the BSP field of a blueprint."""
- return self.config.get('bsp')
-
- def GetMainPackage(self):
- """Returns the main_package field of a blueprint."""
- return self.config.get('main_package')
+ return self.config.get(BSP_FIELD)
def FriendlyName(self):
"""Returns the friendly name for this blueprint."""
diff --git a/lib/blueprint_lib_unittest.py b/lib/blueprint_lib_unittest.py
index 1c145d9..6e36dee 100644
--- a/lib/blueprint_lib_unittest.py
+++ b/lib/blueprint_lib_unittest.py
@@ -6,8 +6,40 @@
from __future__ import print_function
+from chromite.lib import blueprint_lib
from chromite.lib import brick_lib
from chromite.lib import cros_test_lib
+from chromite.lib import osutils
+from chromite.lib import workspace_lib
+
+
+class ExpandBlueprintPathTest(cros_test_lib.TestCase):
+ """Tests ExpandBlueprintPath()."""
+
+ def testBasicName(self):
+ """Tests a basic name."""
+ self.assertEqual('//blueprints/foo.json',
+ blueprint_lib.ExpandBlueprintPath('foo'))
+
+ def testLocator(self):
+ """Tests a locator name."""
+ self.assertEqual('//foo/bar.json',
+ blueprint_lib.ExpandBlueprintPath('//foo/bar'))
+
+ def testCurrentDirectory(self):
+ """Tests a path to the current directory."""
+ self.assertEqual('./foo.json',
+ blueprint_lib.ExpandBlueprintPath('./foo'))
+
+ def testJson(self):
+ """Tests that .json isn't added twice."""
+ self.assertEqual('//blueprints/foo.json',
+ blueprint_lib.ExpandBlueprintPath('foo.json'))
+
+ def testTxt(self):
+ """Tests that .txt extension still gets .json appended."""
+ self.assertEqual('//blueprints/foo.txt.json',
+ blueprint_lib.ExpandBlueprintPath('foo.txt'))
class BlueprintLibTest(cros_test_lib.WorkspaceTestCase):
@@ -19,19 +51,19 @@
def testBlueprint(self):
"""Tests getting the basic blueprint getters."""
bricks = ['//foo', '//bar', '//baz']
- blueprint = self.CreateBlueprint(bricks=bricks, bsp='//bsp',
- main_package='virtual/target-os')
+ for brick in bricks:
+ self.CreateBrick(brick)
+ self.CreateBrick('//bsp')
+ blueprint = self.CreateBlueprint(bricks=bricks, bsp='//bsp')
self.assertEqual(blueprint.GetBricks(), bricks)
self.assertEqual(blueprint.GetBSP(), '//bsp')
- self.assertEqual(blueprint.GetMainPackage(), 'virtual/target-os')
def testBlueprintNoBricks(self):
"""Tests that blueprints without bricks return reasonable defaults."""
- blueprint = self.CreateBlueprint(bsp='//bsp2',
- main_package='virtual/target-os-dev')
+ self.CreateBrick('//bsp2')
+ blueprint = self.CreateBlueprint(bsp='//bsp2')
self.assertEqual(blueprint.GetBricks(), [])
self.assertEqual(blueprint.GetBSP(), '//bsp2')
- self.assertEqual(blueprint.GetMainPackage(), 'virtual/target-os-dev')
def testGetUsedBricks(self):
"""Tests that we can list all the bricks used."""
@@ -41,11 +73,46 @@
initial_config={'name':'c',
'dependencies': ['//b']})
- blueprint = self.CreateBlueprint(bsp='//a', bricks=[brick_c.brick_locator])
+ blueprint = self.CreateBlueprint(blueprint_name='foo.json',
+ bsp='//a', bricks=[brick_c.brick_locator])
self.assertEqual(3, len(blueprint.GetUsedBricks()))
# We sort out duplicates: c depends on b and b is explicitly listed in
# bricks too.
- blueprint = self.CreateBlueprint(bsp='//a', bricks=[brick_c.brick_locator,
+ blueprint = self.CreateBlueprint(blueprint_name='bar.json',
+ bsp='//a', bricks=[brick_c.brick_locator,
brick_b.brick_locator])
self.assertEqual(3, len(blueprint.GetUsedBricks()))
+
+ def testBlueprintAlreadyExists(self):
+ """Tests creating a blueprint where one already exists."""
+ self.CreateBrick('//foo')
+ self.CreateBrick('//bar')
+ self.CreateBlueprint(blueprint_name='//my_blueprint', bricks=['//foo'])
+ with self.assertRaises(blueprint_lib.BlueprintCreationError):
+ self.CreateBlueprint(blueprint_name='//my_blueprint', bricks=['//bar'])
+ # Make sure the original blueprint is untouched.
+ self.assertEqual(['//foo'],
+ blueprint_lib.Blueprint('//my_blueprint').GetBricks())
+
+ def testBlueprintBrickNotFound(self):
+ """Tests creating a blueprint with a non-existent brick fails."""
+ with self.assertRaises(blueprint_lib.BlueprintCreationError):
+ self.CreateBlueprint(blueprint_name='//my_blueprint', bricks=['//none'])
+
+ def testBlueprintBSPNotFound(self):
+ """Tests creating a blueprint with a non-existent BSP fails."""
+ with self.assertRaises(blueprint_lib.BlueprintCreationError):
+ self.CreateBlueprint(blueprint_name='//my_blueprint', bsp='//none')
+
+ def testBlueprintNotFound(self):
+ """Tests loading a non-existent blueprint file."""
+ with self.assertRaises(blueprint_lib.BlueprintNotFoundError):
+ blueprint_lib.Blueprint('//not/a/blueprint')
+
+ def testInvalidBlueprint(self):
+ """Tests loading an invalid blueprint file."""
+ path = workspace_lib.LocatorToPath('//invalid_file')
+ osutils.WriteFile(path, 'invalid contents')
+ with self.assertRaises(workspace_lib.ConfigFileError):
+ blueprint_lib.Blueprint(path)
diff --git a/lib/brick_lib.py b/lib/brick_lib.py
index 569969c..0925688 100644
--- a/lib/brick_lib.py
+++ b/lib/brick_lib.py
@@ -6,7 +6,6 @@
from __future__ import print_function
-import json
import os
from chromite.lib import osutils
@@ -17,7 +16,7 @@
'thin-manifests': 'true',
'use-manifests': 'true'}
-_CONFIG_JSON = 'config.json'
+_CONFIG_FILE = 'config.json'
_IGNORED_OVERLAYS = ('portage-stable', 'chromiumos', 'eclass-overlay')
@@ -65,7 +64,7 @@
self.config = None
self.legacy = False
- config_json = os.path.join(self.brick_dir, _CONFIG_JSON)
+ config_json = os.path.join(self.brick_dir, _CONFIG_FILE)
if not os.path.exists(config_json):
if initial_config:
@@ -106,7 +105,7 @@
if self.config is None:
raise BrickNotFound('Brick not found at %s' % self.brick_dir)
elif initial_config is None:
- self.config = json.loads(osutils.ReadFile(config_json))
+ self.config = workspace_lib.ReadConfigFile(config_json)
else:
raise BrickCreationFailed('brick %s already exists.' % self.brick_dir)
@@ -147,13 +146,13 @@
def UpdateConfig(self, config, regenerate=True):
"""Updates the brick's configuration.
- Write the json representation of |config| in config.json.
+ Writes |config| to the configuration file.
If |regenerate| is true, regenerate the portage configuration files in
- this brick to match the new config.json.
+ this brick to match the new configuration.
Args:
- config: brick configuration as a python dict
- regenerate: if True, regenerate autogenerated brick files
+ config: brick configuration as a python dict.
+ regenerate: if True, regenerate autogenerated brick files.
"""
if self.legacy:
raise BrickFeatureNotSupported(
@@ -166,10 +165,8 @@
else workspace_lib.PathToLocator(d)
for d in self.config.get('dependencies', [])]
- formatted_config = json.dumps(config, sort_keys=True, indent=4,
- separators=(',', ': '))
- osutils.WriteFile(os.path.join(self.brick_dir, _CONFIG_JSON),
- formatted_config, makedirs=True)
+ workspace_lib.WriteConfigFile(os.path.join(self.brick_dir, _CONFIG_FILE),
+ config)
if regenerate:
self.GeneratePortageConfig()
diff --git a/lib/brick_lib_unittest.py b/lib/brick_lib_unittest.py
index 02dd2d6..e08a4f7 100644
--- a/lib/brick_lib_unittest.py
+++ b/lib/brick_lib_unittest.py
@@ -50,7 +50,7 @@
self.assertTrue(line in layout_conf)
def testConfigurationGenerated(self):
- """Test that portage's files are generated when brick.json changes."""
+ """Test that portage's files are generated when the config file changes."""
(self.brick, self.brick_path) = self.CreateBrick()
sample_config = {'name': 'hello',
'dependencies': []}
@@ -62,7 +62,7 @@
def testFindBrickInPath(self):
"""Test that we can infer the current brick from the current directory."""
(self.brick, self.brick_path) = self.CreateBrick()
- os.remove(os.path.join(self.brick_path, 'config.json'))
+ os.remove(os.path.join(self.brick_path, brick_lib._CONFIG_FILE))
brick_dir = os.path.join(self.workspace_path, 'foo', 'bar', 'project')
expected_name = 'hello'
brick_lib.Brick(brick_dir, initial_config={'name': 'hello'})
diff --git a/lib/cros_test_lib.py b/lib/cros_test_lib.py
index 7583d87..9694bbd 100644
--- a/lib/cros_test_lib.py
+++ b/lib/cros_test_lib.py
@@ -1582,6 +1582,21 @@
return blueprint_lib.Blueprint(path, initial_config=config)
+ def AssertBlueprintExists(self, locator, bsp=None, bricks=None):
+ """Verifies a blueprint exists with the specified contents.
+
+ Args:
+ locator: blueprint locator to check.
+ bsp: Expected blueprint BSP or None.
+ bricks: Expected blueprint bricks or None.
+ """
+ actual = blueprint_lib.Blueprint(locator)
+
+ if bsp is not None:
+ self.assertEqual(bsp, actual.GetBSP())
+ if bricks is not None:
+ self.assertListEqual(bricks, actual.GetBricks())
+
@contextlib.contextmanager
def SetTimeZone(tz):
diff --git a/lib/workspace_lib.py b/lib/workspace_lib.py
index 2999c65..51a62cf 100644
--- a/lib/workspace_lib.py
+++ b/lib/workspace_lib.py
@@ -43,6 +43,10 @@
"""Given locator could not be resolved."""
+class ConfigFileError(Exception):
+ """Configuration file writing or reading failed."""
+
+
def WorkspacePath(workspace_reference_dir=None):
"""Returns the path to the current workspace.
@@ -169,15 +173,12 @@
Returns:
Local workspace config as a Python dictionary.
"""
- config_file = os.path.join(workspace_path, WORKSPACE_LOCAL_CONFIG)
-
- # If the file doesn't exist, it's an empty dictionary.
- if not os.path.exists(config_file):
+ try:
+ return ReadConfigFile(os.path.join(workspace_path, WORKSPACE_LOCAL_CONFIG))
+ except IOError:
+ # If the file doesn't exist, it's an empty dictionary.
return {}
- with open(config_file, 'r') as config_fp:
- return json.load(config_fp)
-
def _WriteLocalConfig(workspace_path, config):
"""Save out a new local config for a workspace.
@@ -186,11 +187,7 @@
workspace_path: Root directory of the workspace (WorkspacePath()).
config: New local workspace config contents as a Python dictionary.
"""
- config_file = os.path.join(workspace_path, WORKSPACE_LOCAL_CONFIG)
-
- # Overwrite the config file, with the new dictionary.
- with open(config_file, 'w') as config_fp:
- json.dump(config, config_fp)
+ WriteConfigFile(os.path.join(workspace_path, WORKSPACE_LOCAL_CONFIG), config)
def IsLocator(name):
@@ -276,3 +273,47 @@
return locator[len(_WORKSPACE_LOCATOR_PREFIX):].replace('/', '.')
raise ValueError('Not a valid workspace locator: %s' % locator)
+
+
+def WriteConfigFile(path, config):
+ """Writes |config| to a file at |path|.
+
+ Configuration files in a workspace should all use the same format
+ whenever possible. Currently it's JSON, but centralizing config
+ read/write makes it easier to change when needed.
+
+ Args:
+ path: path to write.
+ config: configuration dictionary to write.
+
+ Raises:
+ ConfigFileError: |config| cannot be written as JSON.
+ """
+ # TODO(dpursell): Add support for comments in config files.
+ try:
+ osutils.WriteFile(
+ path,
+ json.dumps(config, sort_keys=True, indent=4, separators=(',', ': ')),
+ makedirs=True)
+ except TypeError as e:
+ raise ConfigFileError('Writing config file %s failed: %s', path, e)
+
+
+def ReadConfigFile(path):
+ """Reads a configuration file at |path|.
+
+ For use with WriteConfigFile().
+
+ Args:
+ path: file path.
+
+ Returns:
+ Result of parsing the JSON file.
+
+ Raises:
+ ConfigFileError: JSON parsing failed.
+ """
+ try:
+ return json.loads(osutils.ReadFile(path))
+ except ValueError as e:
+ raise ConfigFileError('%s is not in valid JSON format: %s' % (path, e))
diff --git a/lib/workspace_lib_unittest.py b/lib/workspace_lib_unittest.py
index 5aa4ef6..4e8426e 100644
--- a/lib/workspace_lib_unittest.py
+++ b/lib/workspace_lib_unittest.py
@@ -186,3 +186,31 @@
assertReversible('//foo')
assertReversible('//foo/bar/baz')
assertReversible('board:gizmo')
+
+
+class ConfigurationTest(cros_test_lib.TempDirTestCase):
+ """Test WriteConfigFile() and ReadConfigFile()."""
+
+ def testWriteReadConfigFile(self):
+ """Tests WriteConfigFile() then ReadConfigFile()."""
+ path = os.path.join(self.tempdir, 'foo.json')
+ config = {'foo': 1, 'bar': 2}
+
+ workspace_lib.WriteConfigFile(path, config)
+ self.assertDictEqual(config, workspace_lib.ReadConfigFile(path))
+
+ def testWriteConfigFileInvalid(self):
+ """Tests writing an invalid configuration file."""
+ path = os.path.join(self.tempdir, 'foo.json')
+ config = Exception()
+
+ with self.assertRaises(workspace_lib.ConfigFileError):
+ workspace_lib.WriteConfigFile(path, config)
+
+ def testReadConfigFileInvalid(self):
+ """Tests reading an invalid configuration file."""
+ path = os.path.join(self.tempdir, 'foo.json')
+ osutils.WriteFile(path, 'invalid contents')
+
+ with self.assertRaises(workspace_lib.ConfigFileError):
+ workspace_lib.ReadConfigFile(path)