loman: fix subcommand parsing

The conversion to argparse didn't fully mimic the optparse behavior.
Convert to subcommand support to make it work and add a unittest to
cover the basic expected API.

BUG=chromium:496565
TEST=`cros_workon-$BOARD start google-breakpad` works w/minilayout

Change-Id: I2d3879edaf4a6d2c9a199a7ac1d3d6cbc62b00a2
Reviewed-on: https://chromium-review.googlesource.com/384252
Commit-Ready: Mike Frysinger <vapier@chromium.org>
Tested-by: Mike Frysinger <vapier@chromium.org>
Reviewed-by: Ningning Xia <nxia@chromium.org>
diff --git a/scripts/loman_unittest.py b/scripts/loman_unittest.py
new file mode 100644
index 0000000..5a5a26c
--- /dev/null
+++ b/scripts/loman_unittest.py
@@ -0,0 +1,114 @@
+# Copyright 2016 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.
+
+"""Unittests for loman.py"""
+
+from __future__ import print_function
+
+import os
+
+from chromite.lib import cros_test_lib
+from chromite.lib import osutils
+from chromite.scripts import loman
+
+
+class ParserTest(cros_test_lib.OutputTestCase):
+  """Tests for the CLI parser."""
+
+  def setUp(self):
+    self.parser = loman.GetParser()
+
+  def testNoCommand(self):
+    """Require a command at least."""
+    with self.OutputCapturer():
+      self.assertRaises(SystemExit, self.parser.parse_args, [])
+
+  def testBadCommand(self):
+    """Reject unknown commands."""
+    with self.OutputCapturer():
+      self.assertRaises(SystemExit, self.parser.parse_args, ['flyaway'])
+
+  def testAddCommand(self):
+    """Verify basic add command behavior."""
+    with self.OutputCapturer():
+      self.parser.parse_args(['add', '--workon', 'project'])
+      self.parser.parse_args(['add', 'project', 'path', '--remote', 'foo'])
+
+  def testUpgradeMinilayoutCommand(self):
+    """Verify basic upgrade command behavior."""
+    with self.OutputCapturer():
+      self.parser.parse_args(['upgrade-minilayout'])
+
+
+class ManifestTest(cros_test_lib.TempDirTestCase):
+  """Tests that need a real .repo/ manifest layout."""
+
+  def setUp(self):
+    # The loman code looks for the repo root, so make one, and chdir there.
+    os.chdir(self.tempdir)
+
+    for d in ('repo', 'manifests', 'manifests.git'):
+      osutils.SafeMakedirs(os.path.join('.repo', d))
+
+    for m in ('default.xml', 'full.xml', 'minilayout.xml'):
+      osutils.Touch(os.path.join('.repo', 'manifests', m))
+
+    self._SetManifest('default.xml')
+
+  def _SetManifest(self, manifest):
+    """Set active manifest to point to |manifest|."""
+    source = os.path.join('.repo', 'manifest.xml')
+    target = os.path.join('.repo', 'manifests', manifest)
+    osutils.SafeUnlink(source)
+    os.symlink(target, source)
+
+
+class UpgradeMinilayoutTest(cros_test_lib.MockOutputTestCase, ManifestTest):
+  """Tests for the upgrade minilayout logic."""
+
+  def testNoUpgradeNeeded(self):
+    """Don't run update when it isn't needed."""
+    with self.OutputCapturer():
+      self.PatchObject(loman, '_UpgradeMinilayout', side_effect=Exception)
+      loman.main(['upgrade-minilayout'])
+
+  def testUpgradeNeeded(self):
+    """Run update when it's needed."""
+    class _Exception(Exception):
+      """Custom exception."""
+
+    self._SetManifest('minilayout.xml')
+    with self.OutputCapturer():
+      self.PatchObject(loman, '_UpgradeMinilayout', side_effect=_Exception)
+      self.assertRaises(_Exception, loman.main, ['upgrade-minilayout'])
+
+  def testAutoUpgrade(self):
+    """Run update automatically."""
+    class _Exception(Exception):
+      """Custom exception."""
+
+    self._SetManifest('minilayout.xml')
+    with self.OutputCapturer():
+      self.PatchObject(loman, '_UpgradeMinilayout', side_effect=_Exception)
+      self.assertRaises(_Exception, loman.main, ['add', '--workon', 'proj'])
+
+
+class AddTest(cros_test_lib.MockOutputTestCase, ManifestTest):
+  """Tests for the add command."""
+
+  def testRejectBadCommands(self):
+    """Reject bad invocations."""
+    bad_cmds = (
+        # Missing path.
+        ['add'],
+        # Extra project.
+        ['add', '--workon', 'path', 'project'],
+        # Missing --remote.
+        ['add', 'path', 'project'],
+        # Missing project.
+        ['add', 'path', '--remote', 'remote'],
+    )
+    with self.OutputCapturer():
+      for cmd in bad_cmds:
+        self.assertRaises(SystemExit, loman.main, cmd)