pre-upload: check_manifests: improve check significantly

A number of improvements:
- Read the Manifest from the commit, not from the filesystem
- Allow comments/blank lines in the middle
- Make sure there is always a newline at the end of the file
- Disallow blank lines at the start/end of the file

BUG=None
TEST=unittests pass

Change-Id: I91d4d7b283ac08410e83d653dacb981a07524fca
Reviewed-on: https://chromium-review.googlesource.com/849136
Commit-Ready: Mike Frysinger <vapier@chromium.org>
Tested-by: Mike Frysinger <vapier@chromium.org>
Reviewed-by: Ben Chan <benchan@chromium.org>
diff --git a/pre-upload.py b/pre-upload.py
index 7db459e..a04678b 100755
--- a/pre-upload.py
+++ b/pre-upload.py
@@ -1249,24 +1249,47 @@
 
 
 def _check_manifests(_project, commit):
-  """Make sure Manifest files only have DIST lines"""
-  paths = []
+  """Make sure Manifest files only have comments & DIST lines."""
+  ret = []
 
-  for path in _get_affected_files(commit):
-    if os.path.basename(path) != 'Manifest':
-      continue
-    if not os.path.exists(path):
+  manifests = _filter_files(_get_affected_files(commit, relative=True),
+                            [r'.*/Manifest$'])
+  for path in manifests:
+    data = _get_file_content(path, commit)
+
+    # Disallow blank files.
+    if not data.strip():
+      ret.append('%s: delete empty file' % (path,))
       continue
 
-    with open(path, 'r') as f:
-      for line in f.readlines():
-        if not line.startswith('DIST '):
-          paths.append(path)
-          break
+    # Make sure the last newline isn't omitted.
+    if data[-1] != '\n':
+      ret.append('%s: missing trailing newline' % (path,))
 
-  if paths:
-    return HookFailure('Please remove lines that do not start with DIST:\n%s' %
-                       ('\n'.join(paths),))
+    # Do not allow leading or trailing blank lines.
+    lines = data.splitlines()
+    if not lines[0]:
+      ret.append('%s: delete leading blank lines' % (path,))
+    if not lines[-1]:
+      ret.append('%s: delete trailing blank lines' % (path,))
+
+    for line in lines:
+      # Disallow leading/trailing whitespace.
+      if line != line.strip():
+        ret.append('%s: remove leading/trailing whitespace: %s' % (path, line))
+
+      # Allow blank lines & comments.
+      line = line.split('#', 1)[0]
+      if not line:
+        continue
+
+      # All other linse should start with DIST.
+      if not line.startswith('DIST '):
+        ret.append('%s: remove non-DIST lines: %s' % (path, line))
+        break
+
+  if ret:
+    return HookFailure('\n'.join(ret))
 
 
 def _check_change_has_branch_field(_project, commit):
diff --git a/pre-upload_unittest.py b/pre-upload_unittest.py
index 5d43166..81bee92 100755
--- a/pre-upload_unittest.py
+++ b/pre-upload_unittest.py
@@ -222,6 +222,79 @@
     self.assertFalse(failure)
 
 
+class CheckManifests(cros_test_lib.MockTestCase):
+  """Tests _check_manifests."""
+
+  def setUp(self):
+    self.file_mock = self.PatchObject(pre_upload, '_get_affected_files')
+    self.content_mock = self.PatchObject(pre_upload, '_get_file_content')
+
+  def testNoManifests(self):
+    """Nothing should be checked w/no Manifest files."""
+    self.file_mock.return_value = [
+        '/foo/bar.txt',
+        '/readme.md',
+        '/manifest',
+        '/Manifest.txt',
+    ]
+    ret = pre_upload._check_manifests('PROJECT', 'COMMIT')
+    self.assertIsNone(ret)
+
+  def testValidManifest(self):
+    """Accept valid Manifest files."""
+    self.file_mock.return_value = [
+        '/foo/bar.txt',
+        '/cat/pkg/Manifest',
+    ]
+    self.content_mock.return_value = '''# Comment and blank lines.
+
+DIST lines
+'''
+    ret = pre_upload._check_manifests('PROJECT', 'COMMIT')
+    self.assertIsNone(ret)
+    self.content_mock.assert_called_once_with('/cat/pkg/Manifest', 'COMMIT')
+
+  def _testReject(self, content):
+    """Make sure |content| is rejected."""
+    self.file_mock.return_value = ('/Manifest',)
+    self.content_mock.reset_mock()
+    self.content_mock.return_value = content
+    ret = pre_upload._check_manifests('PROJECT', 'COMMIT')
+    self.assertIsNotNone(ret)
+    self.content_mock.assert_called_once_with('/Manifest', 'COMMIT')
+
+  def testRejectBlank(self):
+    """Reject empty manifests."""
+    self._testReject('')
+
+  def testNoTrailingNewLine(self):
+    """Reject manifests w/out trailing newline."""
+    self._testReject('DIST foo')
+
+  def testNoLeadingBlankLines(self):
+    """Reject manifests w/leading blank lines."""
+    self._testReject('\nDIST foo\n')
+
+  def testNoTrailingBlankLines(self):
+    """Reject manifests w/trailing blank lines."""
+    self._testReject('DIST foo\n\n')
+
+  def testNoLeadingWhitespace(self):
+    """Reject manifests w/lines w/leading spaces."""
+    self._testReject(' DIST foo\n')
+    self._testReject('  # Comment\n')
+
+  def testNoTrailingWhitespace(self):
+    """Reject manifests w/lines w/trailing spaces."""
+    self._testReject('DIST foo \n')
+    self._testReject('# Comment \n')
+    self._testReject('  \n')
+
+  def testOnlyDistLines(self):
+    """Only allow DIST lines in here."""
+    self._testReject('EBUILD foo\n')
+
+
 class CheckConfigParsing(cros_test_lib.MockTestCase):
   """Tests _check_cq_ini_well_formed."""