pre-upload: check rust formatting with rustfmt

Check all changed files with rustfmt, and block upload if any
errors are found.

BUG=None
TEST=check that hook catches bad commit on 'repo upload'
TEST=check that hook doesn't catch innocuous rust commits

Change-Id: I656e8ea2f8af39d31537aadc510a000bff925ca7
Reviewed-on: https://chromium-review.googlesource.com/1759129
Tested-by: Fletcher Woodruff <fletcherw@chromium.org>
Commit-Ready: Fletcher Woodruff <fletcherw@chromium.org>
Legacy-Commit-Queue: Commit Bot <commit-bot@chromium.org>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
diff --git a/pre-upload.py b/pre-upload.py
index c609530..bb73ee0 100755
--- a/pre-upload.py
+++ b/pre-upload.py
@@ -567,6 +567,23 @@
     return HookFailure('Files not formatted with gofmt:', errors)
 
 
+def _check_rustfmt(_project, commit):
+  """Checks that Rust files are formatted with rustfmt."""
+  errors = []
+  files = _filter_files(_get_affected_files(commit, relative=True),
+                        [r'\.rs$'])
+
+  for rustfile in files:
+    contents = _get_file_content(rustfile, commit)
+    output = _run_command(cmd=['rustfmt'], input=contents,
+                          combine_stdout_stderr=True)
+    if output != contents:
+      errors.append(rustfile)
+  if errors:
+    return HookFailure('Files not formatted with rustfmt: '
+                       "(run 'cargo fmt' to fix)", errors)
+
+
 def _check_change_has_test_field(_project, commit):
   """Check for a non-empty 'TEST=' field in the commit message."""
   TEST_RE = r'\nTEST=\S+'
@@ -1714,6 +1731,7 @@
     _check_no_stray_whitespace,
     _check_no_tabs,
     _check_portage_make_use_var,
+    _check_rustfmt,
     _check_tabbed_indents,
 ]
 
diff --git a/pre-upload_unittest.py b/pre-upload_unittest.py
index 179617d..640aef8 100755
--- a/pre-upload_unittest.py
+++ b/pre-upload_unittest.py
@@ -1623,5 +1623,36 @@
         mock.ANY, proj_dir=mock.ANY, commit_list=commits, presubmit=mock.ANY)
 
 
+class CheckRustfmtTest(cros_test_lib.MockTestCase):
+  """Tests for _check_rustfmt."""
+
+  def setUp(self):
+    self.content_mock = self.PatchObject(pre_upload, '_get_file_content')
+
+  def testBadRustFile(self):
+    self.PatchObject(pre_upload, '_get_affected_files', return_value=['a.rs'])
+    # Bad because it's missing trailing newline.
+    self.content_mock.return_value = 'fn main() {}'
+    failure = pre_upload._check_rustfmt(ProjectNamed('PROJECT'), 'COMMIT')
+    self.assertIsNotNone(failure)
+    self.assertEquals('Files not formatted with rustfmt: '
+                      "(run 'cargo fmt' to fix)",
+                      failure.msg)
+    self.assertEquals(['a.rs'], failure.items)
+
+  def testGoodRustFile(self):
+    self.PatchObject(pre_upload, '_get_affected_files', return_value=['a.rs'])
+    self.content_mock.return_value = 'fn main() {}\n'
+    failure = pre_upload._check_rustfmt(ProjectNamed('PROJECT'), 'COMMIT')
+    self.assertIsNone(failure)
+
+  def testFilterNonRustFiles(self):
+    self.PatchObject(pre_upload, '_get_affected_files',
+                     return_value=['a.cc', 'a.rsa', 'a.irs', 'rs.cc'])
+    self.content_mock.return_value = 'fn main() {\n}'
+    failure = pre_upload._check_rustfmt(ProjectNamed('PROJECT'), 'COMMIT')
+    self.assertIsNone(failure)
+
+
 if __name__ == '__main__':
   cros_test_lib.main(module=__name__)