Add presubmit checks for translation screenshots

This CL adds a new presubmit check to verify that new or modified
translation strings have associated screenshots. It does this by parsing
the modified grd/grdp file, loading the messages and comparing them with
old messages. It then checks whether the corresponding screenshot file
exists.

See go/chrome-translation-screenshots for design details.

Change-Id: I981b3a95b66e2df8eee1d2b48223251fa0ff03a7
Bug: 814899
Reviewed-on: https://chromium-review.googlesource.com/872199
Reviewed-by: Aaron Gable <agable@chromium.org>
Reviewed-by: Dirk Pranke <dpranke@chromium.org>
Commit-Queue: Mustafa Emre Acer <meacer@chromium.org>
Cr-Commit-Position: refs/heads/master@{#579182}
diff --git a/PRESUBMIT_test.py b/PRESUBMIT_test.py
index e5eb2ec..1654af1 100755
--- a/PRESUBMIT_test.py
+++ b/PRESUBMIT_test.py
@@ -1844,5 +1844,160 @@
     self.assertEqual(0, len(results))
 
 
+class TranslationScreenshotsTest(unittest.TestCase):
+  # An empty grd file.
+  OLD_GRD_CONTENTS = """<?xml version="1.0" encoding="UTF-8"?>
+           <grit latest_public_release="1" current_release="1">
+             <release seq="1">
+               <messages></messages>
+             </release>
+           </grit>
+        """.splitlines()
+  # A grd file with a single message.
+  NEW_GRD_CONTENTS1 = """<?xml version="1.0" encoding="UTF-8"?>
+           <grit latest_public_release="1" current_release="1">
+             <release seq="1">
+               <messages>
+                 <message name="IDS_TEST1">
+                   Test string 1
+                 </message>
+               </messages>
+             </release>
+           </grit>
+        """.splitlines()
+  # A grd file with two messages.
+  NEW_GRD_CONTENTS2 = """<?xml version="1.0" encoding="UTF-8"?>
+           <grit latest_public_release="1" current_release="1">
+             <release seq="1">
+               <messages>
+                 <message name="IDS_TEST1">
+                   Test string 1
+                 </message>
+                 <message name="IDS_TEST2">
+                   Test string 2
+                 </message>
+               </messages>
+             </release>
+           </grit>
+        """.splitlines()
+
+  DO_NOT_UPLOAD_PNG_MESSAGE = ("Do not include actual screenshots in the CL. "
+                               "Run tools/translate/upload_screenshots to "
+                               "upload them instead")
+  GENERATE_SIGNATURES_MESSAGE = ("You are adding or modifying UI messages. "
+                                 "Add screenshots and run "
+                                 "tools/translate/upload_screenshots to "
+                                 "generate and add these files to the CL:")
+  REMOVE_SIGNATURES_MESSAGE = ("You removed messages associated with these "
+                               "files. Consider removing:")
+
+  def makeInputApi(self, files):
+    input_api = MockInputApi()
+    input_api.files = files
+    return input_api
+
+  def testNoScreenshots(self):
+    # CL modified and added messages, but didn't add any screenshots.
+    input_api = self.makeInputApi([
+      MockAffectedFile('test.grd', self.NEW_GRD_CONTENTS2,
+                       self.OLD_GRD_CONTENTS, action='M')])
+    warnings = PRESUBMIT._CheckTranslationScreenshots(input_api,
+                                                      MockOutputApi())
+    self.assertEqual(1, len(warnings))
+    self.assertEqual(self.GENERATE_SIGNATURES_MESSAGE, warnings[0].message)
+    self.assertEqual(
+      ['test_grd/IDS_TEST1.png.sha1', 'test_grd/IDS_TEST2.png.sha1'],
+      warnings[0].items)
+
+    input_api = self.makeInputApi([
+      MockAffectedFile('test.grd', self.NEW_GRD_CONTENTS2,
+                       self.NEW_GRD_CONTENTS1, action='M')])
+    warnings = PRESUBMIT._CheckTranslationScreenshots(input_api,
+                                                      MockOutputApi())
+    self.assertEqual(1, len(warnings))
+    self.assertEqual(self.GENERATE_SIGNATURES_MESSAGE, warnings[0].message)
+    self.assertEqual(['test_grd/IDS_TEST2.png.sha1'], warnings[0].items)
+
+
+  def testUnnecessaryScreenshots(self):
+    # CL added a single message and added the png file, but not the sha1 file.
+    input_api = self.makeInputApi([
+      MockAffectedFile('test.grd', self.NEW_GRD_CONTENTS1,
+                       self.OLD_GRD_CONTENTS, action='M'),
+      MockAffectedFile('test_grd/IDS_TEST1.png', 'binary', action='A')])
+    warnings = PRESUBMIT._CheckTranslationScreenshots(input_api,
+                                                      MockOutputApi())
+    self.assertEqual(2, len(warnings))
+    self.assertEqual(self.DO_NOT_UPLOAD_PNG_MESSAGE, warnings[0].message)
+    self.assertEqual(['test_grd/IDS_TEST1.png'], warnings[0].items)
+    self.assertEqual(self.GENERATE_SIGNATURES_MESSAGE, warnings[1].message)
+    self.assertEqual(['test_grd/IDS_TEST1.png.sha1'], warnings[1].items)
+
+    # CL added two messages, one has a png. Expect two messages:
+    # - One for the unnecessary png.
+    # - Another one for missing .sha1 files.
+    input_api = self.makeInputApi([
+      MockAffectedFile('test.grd', self.NEW_GRD_CONTENTS2,
+                       self.OLD_GRD_CONTENTS, action='M'),
+      MockAffectedFile('test_grd/IDS_TEST1.png', 'binary', action='A')])
+    warnings = PRESUBMIT._CheckTranslationScreenshots(input_api,
+                                                      MockOutputApi())
+    self.assertEqual(2, len(warnings))
+    self.assertEqual(self.DO_NOT_UPLOAD_PNG_MESSAGE, warnings[0].message)
+    self.assertEqual(['test_grd/IDS_TEST1.png'], warnings[0].items)
+    self.assertEqual(self.GENERATE_SIGNATURES_MESSAGE, warnings[1].message)
+    self.assertEqual(['test_grd/IDS_TEST1.png.sha1',
+                      'test_grd/IDS_TEST2.png.sha1'], warnings[1].items)
+
+  def testScreenshotsWithSha1(self):
+    # CL added two messages and their corresponding .sha1 files. No warnings.
+    input_api = self.makeInputApi([
+      MockAffectedFile('test.grd', self.NEW_GRD_CONTENTS2,
+                       self.OLD_GRD_CONTENTS, action='M'),
+      MockFile('test_grd/IDS_TEST1.png.sha1', 'binary', action='A'),
+      MockFile('test_grd/IDS_TEST2.png.sha1', 'binary', action='A')])
+    warnings = PRESUBMIT._CheckTranslationScreenshots(input_api,
+                                                      MockOutputApi())
+    self.assertEqual([], warnings)
+
+  def testScreenshotsRemovedWithSha1(self):
+    # Swap old contents with new contents, remove IDS_TEST1 and IDS_TEST2. The
+    # sha1 files associated with the messages should also be removed by the CL.
+    input_api = self.makeInputApi([
+      MockAffectedFile('test.grd', self.OLD_GRD_CONTENTS,
+                       self.NEW_GRD_CONTENTS2, action='M'),
+      MockFile('test_grd/IDS_TEST1.png.sha1', 'binary', ""),
+      MockFile('test_grd/IDS_TEST2.png.sha1', 'binary', "")])
+    warnings = PRESUBMIT._CheckTranslationScreenshots(input_api,
+                                                      MockOutputApi())
+    self.assertEqual(1, len(warnings))
+    self.assertEqual(self.REMOVE_SIGNATURES_MESSAGE, warnings[0].message)
+    self.assertEqual(['test_grd/IDS_TEST1.png.sha1',
+                      'test_grd/IDS_TEST2.png.sha1'], warnings[0].items)
+
+    # Same as above, but this time one of the .sha1 files is removed.
+    input_api = self.makeInputApi([
+      MockAffectedFile('test.grd', self.OLD_GRD_CONTENTS,
+                       self.NEW_GRD_CONTENTS2, action='M'),
+      MockFile('test_grd/IDS_TEST1.png.sha1', 'binary', ''),
+      MockAffectedFile('test_grd/IDS_TEST2.png.sha1',
+                       '', 'old_contents', action='D')])
+    warnings = PRESUBMIT._CheckTranslationScreenshots(input_api,
+                                                      MockOutputApi())
+    self.assertEqual(1, len(warnings))
+    self.assertEqual(self.REMOVE_SIGNATURES_MESSAGE, warnings[0].message)
+    self.assertEqual(['test_grd/IDS_TEST1.png.sha1'], warnings[0].items)
+
+    # Remove both sha1 files. No presubmit warnings.
+    input_api = self.makeInputApi([
+      MockAffectedFile('test.grd', self.OLD_GRD_CONTENTS,
+                       self.NEW_GRD_CONTENTS2, action='M'),
+      MockFile('test_grd/IDS_TEST1.png.sha1', 'binary', action='D'),
+      MockFile('test_grd/IDS_TEST2.png.sha1', 'binary', action='D')])
+    warnings = PRESUBMIT._CheckTranslationScreenshots(input_api,
+                                                      MockOutputApi())
+    self.assertEqual([], warnings)
+
+
 if __name__ == '__main__':
   unittest.main()