Add arg replacement to generate_buildbot_json.py

Adds the ability to replace or remove arguments for tests generated
using generate_buildbot_json.py by specifying a 'replacements' field in
test_suite_exceptions.pyl.

This is useful in cases where a subset of bots have slightly different
args from usual. Previously, this required a duplicate test suite
definition with the updated args. With this change, the same definition
can be used with the differences listed in the exceptions.

Bug: 980631
Change-Id: If3c0c22f2046ce67123ec06dea88a6bf8de9102f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1691266
Reviewed-by: Kenneth Russell <[email protected]>
Commit-Queue: Brian Sheedy <[email protected]>
Cr-Commit-Position: refs/heads/master@{#676270}
diff --git a/testing/buildbot/generate_buildbot_json.py b/testing/buildbot/generate_buildbot_json.py
index 4b31b82..61efc004 100755
--- a/testing/buildbot/generate_buildbot_json.py
+++ b/testing/buildbot/generate_buildbot_json.py
@@ -279,6 +279,12 @@
       return None
     return exception.get('modifications', {}).get(tester_name)
 
+  def get_test_replacements(self, test, test_name, tester_name):
+    exception = self.get_exception_for_test(test_name, test)
+    if not exception:
+      return None
+    return exception.get('replacements', {}).get(tester_name)
+
   def merge_command_line_args(self, arr, prefix, splitter):
     prefix_len = len(prefix)
     idx = 0
@@ -447,9 +453,44 @@
       for d in test['swarming'].get('dimension_sets', []):
         if d.get('os') == 'Android' and not d.get('device_os_type'):
           d['device_os_type'] = 'userdebug'
+    self.replace_test_args(test, test_name, tester_name)
 
     return test
 
+  def replace_test_args(self, test, test_name, tester_name):
+    replacements = self.get_test_replacements(
+        test, test_name, tester_name) or {}
+    valid_replacement_keys = ['args', 'non_precommit_args', 'precommit_args']
+    for key, replacement_dict in replacements.iteritems():
+      if key not in valid_replacement_keys:
+        raise BBGenErr(
+            'Given replacement key %s for %s on %s is not in the list of valid '
+            'keys %s' % (key, test_name, tester_name, valid_replacement_keys))
+      for replacement_key, replacement_val in replacement_dict.iteritems():
+        found_key = False
+        for i, test_key in enumerate(test.get(key, [])):
+          # Handle both the key/value being replaced being defined as two
+          # separate items or as key=value.
+          if test_key == replacement_key:
+            found_key = True
+            # Handle flags without values.
+            if replacement_val == None:
+              del test[key][i]
+            else:
+              test[key][i+1] = replacement_val
+            break
+          elif test_key.startswith(replacement_key + '='):
+            found_key = True
+            if replacement_val == None:
+              del test[key][i]
+            else:
+              test[key][i] = '%s=%s' % (replacement_key, replacement_val)
+            break
+        if not found_key:
+          raise BBGenErr('Could not find %s in existing list of values for key '
+                         '%s in %s on %s' % (replacement_key, key, test_name,
+                             tester_name))
+
   def add_common_test_properties(self, test, tester_config):
     if tester_config.get('use_multi_dimension_trigger_script'):
       # Assumes update_and_cleanup_test has already been called, so the