Repository: google/clusterfuzz

Issue:
Add default testcase metadata (components, hotlists) for testcase uploads.



Discussion:

Resolving patch:
diff --git a/src/python/bot/fuzzers/engine_common.py b/src/python/bot/fuzzers/engine_common.py
--- a/src/python/bot/fuzzers/engine_common.py
+++ b/src/python/bot/fuzzers/engine_common.py
@@ -440,6 +440,25 @@ def get_all_issue_metadata(fuzz_target_path):
   return metadata
 
 
+def get_all_issue_metadata_for_testcase(testcase):
+  """Get issue related metadata given a testcase."""
+  if environment.is_trusted_host():
+    # Not applicable.
+    return None
+
+  fuzz_target = testcase.get_fuzz_target()
+  if not fuzz_target:
+    return None
+
+  build_dir = environment.get_value('BUILD_DIR')
+  target_path = find_fuzzer_path(build_dir, fuzz_target.binary)
+  if not target_path:
+    logs.log_error('Failed to find target path for ' + fuzz_target.binary)
+    return None
+
+  return get_all_issue_metadata(target_path)
+
+
 def format_fuzzing_strategies(fuzzing_strategies):
   """Format the strategies used for logging purposes."""
   if not fuzzing_strategies:
diff --git a/src/python/bot/tasks/analyze_task.py b/src/python/bot/tasks/analyze_task.py
--- a/src/python/bot/tasks/analyze_task.py
+++ b/src/python/bot/tasks/analyze_task.py
@@ -17,10 +17,12 @@
 standard_library.install_aliases()
 import datetime
 import os
+import six
 
 from base import tasks
 from base import utils
 from bot import testcase_manager
+from bot.fuzzers import engine_common
 from bot.tasks import setup
 from bot.tasks import task_creation
 from build_management import build_manager
@@ -36,6 +38,36 @@
 from system import shell
 
 
+def _add_default_issue_metadata(testcase):
+  """Adds the default issue metadata (e.g. components, labels) to testcase."""
+  default_metadata = engine_common.get_all_issue_metadata_for_testcase(testcase)
+  if not default_metadata:
+    return
+
+  testcase_metadata = testcase.get_metadata()
+  for key, default_value in six.iteritems(default_metadata):
+    # Add the default issue metadata first. This gives preference to uploader
+    # specified issue metadata.
+    new_value_list = utils.parse_delimited(
+        default_value, delimiter=',', strip=True, remove_empty=True)
+
+    # Append uploader specified testcase metadata value to end (for preference).
+    uploader_value = testcase_metadata.get(key, '')
+    uploader_value_list = utils.parse_delimited(
+        uploader_value, delimiter=',', strip=True, remove_empty=True)
+    for value in uploader_value_list:
+      if value not in new_value_list:
+        new_value_list.append(value)
+
+    new_value = ','.join(new_value_list)
+    if new_value == uploader_value:
+      continue
+
+    logs.log('Updating issue metadata for {} from {} to {}.'.format(
+        key, uploader_value, new_value))
+    testcase.set_metadata(key, new_value)
+
+
 def close_invalid_testcase_and_update_status(testcase, metadata, status):
   """Closes an invalid testcase and updates metadata."""
   testcase.status = status
@@ -371,6 +403,8 @@ def execute_task(testcase_id, job_type):
   metadata.security_flag = security_flag
   metadata.put()
 
+  _add_default_issue_metadata(testcase)
+
   # Create tasks to
   # 1. Minimize testcase (minimize).
   # 2. Find regression range (regression).
diff --git a/src/python/bot/tasks/progression_task.py b/src/python/bot/tasks/progression_task.py
--- a/src/python/bot/tasks/progression_task.py
+++ b/src/python/bot/tasks/progression_task.py
@@ -141,25 +141,14 @@ def _check_fixed_for_custom_binary(testcase, job_type, testcase_file_path):
 
 def _update_issue_metadata(testcase):
   """Update issue metadata."""
-  if environment.is_trusted_host():
-    # Not applicable.
-    return
-
   if testcase.uploader_email:
     # Trust the uploader specified metadata.
     return
 
-  fuzz_target = testcase.get_fuzz_target()
-  if not fuzz_target:
-    return
-
-  build_dir = environment.get_value('BUILD_DIR')
-  target_path = engine_common.find_fuzzer_path(build_dir, fuzz_target.binary)
-  if not target_path:
-    logs.log_error('Failed to find target path for ' + fuzz_target.binary)
+  metadata = engine_common.get_all_issue_metadata_for_testcase(testcase)
+  if not metadata:
     return
 
-  metadata = engine_common.get_all_issue_metadata(target_path)
   for key, value in six.iteritems(metadata):
     old_value = testcase.get_metadata(key)
     if old_value != value:


Test patch:
diff --git a/src/python/tests/core/bot/tasks/analyze_task_test.py b/src/python/tests/core/bot/tasks/analyze_task_test.py
new file mode 100644
--- /dev/null
+++ b/src/python/tests/core/bot/tasks/analyze_task_test.py
@@ -0,0 +1,114 @@
+# Copyright 2019 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests for analyze task."""
+
+import unittest
+
+from bot.tasks import analyze_task
+from tests.test_libs import helpers
+from tests.test_libs import test_utils
+
+
+@test_utils.with_cloud_emulators('datastore')
+class AddDefaultIssueMetadataTest(unittest.TestCase):
+  """Test _add_default_issue_metadata."""
+
+  def setUp(self):
+    helpers.patch(
+        self,
+        [
+            'bot.fuzzers.engine_common.get_all_issue_metadata_for_testcase',
+            'datastore.data_types.Testcase._post_put_hook',  # Disable logging.
+            'metrics.logs.log',
+        ])
+
+  def test_union(self):
+    """Test union of current testcase metadata and default issue metadata."""
+    self.mock.get_all_issue_metadata_for_testcase.return_value = {
+        'issue_owners': 'dev1@example1.com, dev2@example2.com',
+        'issue_components': 'component1',
+        'issue_labels': 'label1, label2 ,label3'
+    }
+
+    testcase = test_utils.create_generic_testcase()
+    testcase.set_metadata('issue_owners', 'dev3@example3.com,dev2@example2.com')
+    testcase.set_metadata('issue_components', 'component2')
+    testcase.set_metadata('issue_labels', 'label4,label5, label2,')
+
+    analyze_task._add_default_issue_metadata(testcase)  # pylint: disable=protected-access
+    self.assertEqual('dev1@example1.com,dev2@example2.com,dev3@example3.com',
+                     testcase.get_metadata('issue_owners'))
+    self.assertEqual('component1,component2',
+                     testcase.get_metadata('issue_components'))
+    self.assertEqual('label1,label2,label3,label4,label5',
+                     testcase.get_metadata('issue_labels'))
+    self.assertEqual(3, self.mock.log.call_count)
+
+  def test_no_testcase_metadata(self):
+    """Test when we only have default issue metadata and no testcase
+    metadata."""
+    self.mock.get_all_issue_metadata_for_testcase.return_value = None
+
+    testcase = test_utils.create_generic_testcase()
+    testcase.set_metadata('issue_owners', 'dev1@example1.com,dev2@example2.com')
+    testcase.set_metadata('issue_components', 'component1')
+    testcase.set_metadata('issue_labels', 'label1,label2,label3')
+
+    analyze_task._add_default_issue_metadata(testcase)  # pylint: disable=protected-access
+    self.assertEqual('dev1@example1.com,dev2@example2.com',
+                     testcase.get_metadata('issue_owners'))
+    self.assertEqual('component1', testcase.get_metadata('issue_components'))
+    self.assertEqual('label1,label2,label3',
+                     testcase.get_metadata('issue_labels'))
+    self.assertEqual(0, self.mock.log.call_count)
+
+  def test_no_default_issue_metadata(self):
+    """Test when we only have testcase metadata and no default issue
+    metadata."""
+    self.mock.get_all_issue_metadata_for_testcase.return_value = {
+        'issue_owners': 'dev1@example1.com,dev2@example2.com',
+        'issue_components': 'component1',
+        'issue_labels': 'label1,label2,label3'
+    }
+
+    testcase = test_utils.create_generic_testcase()
+
+    analyze_task._add_default_issue_metadata(testcase)  # pylint: disable=protected-access
+    self.assertEqual('dev1@example1.com,dev2@example2.com',
+                     testcase.get_metadata('issue_owners'))
+    self.assertEqual('component1', testcase.get_metadata('issue_components'))
+    self.assertEqual('label1,label2,label3',
+                     testcase.get_metadata('issue_labels'))
+    self.assertEqual(3, self.mock.log.call_count)
+
+  def test_same_testcase_and_default_issue_metadata(self):
+    """Test when we have same testcase metadata and default issue metadata."""
+    self.mock.get_all_issue_metadata_for_testcase.return_value = {
+        'issue_owners': 'dev1@example1.com,dev2@example2.com',
+        'issue_components': 'component1',
+        'issue_labels': 'label1,label2,label3'
+    }
+
+    testcase = test_utils.create_generic_testcase()
+    testcase.set_metadata('issue_owners', 'dev1@example1.com,dev2@example2.com')
+    testcase.set_metadata('issue_components', 'component1')
+    testcase.set_metadata('issue_labels', 'label1,label2,label3')
+
+    analyze_task._add_default_issue_metadata(testcase)  # pylint: disable=protected-access
+    self.assertEqual('dev1@example1.com,dev2@example2.com',
+                     testcase.get_metadata('issue_owners'))
+    self.assertEqual('component1', testcase.get_metadata('issue_components'))
+    self.assertEqual('label1,label2,label3',
+                     testcase.get_metadata('issue_labels'))
+    self.assertEqual(0, self.mock.log.call_count)