Repository: google/clusterfuzz

Issue:
Show fuzz target + engine on testcase details instead of fully qualified fuzzer name.
This seems especially important when the fully qualified name is long and after normalizing. 

Right now, on the testcase details page, we will show:

```
Fuzzer: libFuzzer_a-b-c-d-e-fuzzer
```
( where a-b-c-d-e is the normalized project name)

Proposed change:
```
Engine: libFuzzer
Target: fuzzer
```

We can still link to the testcase list page with the full libFuzzer_a-b-c-d-e-fuzzer filter on the "fuzzer" text.


Discussion:
Similarly, we should do this for our issue report format also (omit the long name).
I will do this first thing tmrw.

Resolving patch:
diff --git a/src/appengine/handlers/testcase_detail/show.py b/src/appengine/handlers/testcase_detail/show.py
--- a/src/appengine/handlers/testcase_detail/show.py
+++ b/src/appengine/handlers/testcase_detail/show.py
@@ -383,6 +383,8 @@ def get_testcase_detail(testcase):
   minimized_testcase_size = _get_blob_size_string(testcase.minimized_keys)
   has_issue_tracker = bool(data_handler.get_issue_tracker_name())
 
+  fuzzer_display = data_handler.get_fuzzer_display(testcase)
+
   formatted_reproduction_help = _format_reproduction_help(
       data_handler.get_formatted_reproduction_help(testcase))
   # When we have a HELP_TEMPLATE, ignore any default values set for HELP_URL.
@@ -601,6 +603,8 @@ def get_testcase_detail(testcase):
           reproduction_help_url,
       'is_local_development':
           environment.is_running_on_app_engine_development(),
+      'fuzzer_display':
+          vars(fuzzer_display),
   }
 
 
diff --git a/src/python/datastore/data_handler.py b/src/python/datastore/data_handler.py
--- a/src/python/datastore/data_handler.py
+++ b/src/python/datastore/data_handler.py
@@ -13,6 +13,7 @@
 # limitations under the License.
 """Data handler functions."""
 
+import collections
 import datetime
 import os
 import re
@@ -74,6 +75,9 @@
     '</b>' % (data_types.FILE_CONSISTENT_UNREPRODUCIBLE_TESTCASE_DEADLINE,
               data_types.UNREPRODUCIBLE_TESTCASE_WITH_BUG_DEADLINE))
 
+FuzzerDisplay = collections.namedtuple(
+    'FuzzerDisplay', ['engine', 'target', 'name', 'fully_qualified_name'])
+
 # ------------------------------------------------------------------------------
 # Testcase, TestcaseUploadMetadata database related functions
 # ------------------------------------------------------------------------------
@@ -269,6 +273,32 @@ def get_reproduction_help_url(testcase, config):
       testcase.job_type, 'HELP_URL', default=config.reproduction_help_url)
 
 
+def get_fuzzer_display(testcase):
+  """Return FuzzerDisplay tuple."""
+  if (testcase.overridden_fuzzer_name == testcase.fuzzer_name or
+      not testcase.overridden_fuzzer_name):
+    return FuzzerDisplay(
+        engine=None,
+        target=None,
+        name=testcase.fuzzer_name,
+        fully_qualified_name=testcase.fuzzer_name)
+
+  fuzz_target = get_fuzz_target(testcase.overridden_fuzzer_name)
+  if not fuzz_target:
+    # Legacy testcases.
+    return FuzzerDisplay(
+        engine=testcase.fuzzer_name,
+        target=testcase.get_metadata('fuzzer_binary_name'),
+        name=testcase.fuzzer_name,
+        fully_qualified_name=testcase.overridden_fuzzer_name)
+
+  return FuzzerDisplay(
+      engine=fuzz_target.engine,
+      target=fuzz_target.binary,
+      name=fuzz_target.engine,
+      fully_qualified_name=fuzz_target.fully_qualified_name())
+
+
 def get_formatted_reproduction_help(testcase):
   """Return url to reproduce the bug."""
   help_format = get_value_from_job_definition_or_environment(
@@ -281,20 +311,16 @@ def get_formatted_reproduction_help(testcase):
   # that must be converted here (e.g. \n).
   help_format = help_format.decode('unicode-escape')
 
+  fuzzer_display = get_fuzzer_display(testcase)
+  last_tested_crash_revision = (
+      testcase.get_metadata('last_tested_crash_revision') or
+      testcase.crash_revision)
+
   result = help_format.replace('%TESTCASE%', str(testcase.key.id()))
   result = result.replace('%PROJECT%', get_project_name(testcase.job_type))
-  result = result.replace('%FUZZER_NAME%', testcase.fuzzer_name)
-
-  if testcase.overridden_fuzzer_name:
-    # Remove the libfuzzer_ or afl_ prefixes from the target name.
-    target = ndb.Key(data_types.FuzzTarget,
-                     testcase.overridden_fuzzer_name).get()
-    target_name = target.binary
-  else:
-    target_name = ''
-
-  result = result.replace('%FUZZ_TARGET%', target_name)
-
+  result = result.replace('%REVISION%', str(last_tested_crash_revision))
+  result = result.replace('%FUZZER_NAME%', fuzzer_display.name)
+  result = result.replace('%FUZZ_TARGET%', fuzzer_display.target or '')
   return result
 
 
@@ -339,7 +365,6 @@ def get_issue_description(testcase,
   domain = get_domain()
   testcase_id = testcase.key.id()
 
-  fuzzer_name = testcase.actual_fuzzer_name()
   download_url = TESTCASE_DOWNLOAD_URL.format(
       domain=domain, testcase_id=testcase_id)
   report_url = TESTCASE_REPORT_URL.format(
@@ -363,11 +388,12 @@ def get_issue_description(testcase,
   if project_name and project_name != utils.default_project_name():
     content_string += 'Project: %s\n' % project_name
 
-  if fuzzer_name:
-    content_string += 'Fuzzer: %s\n' % fuzzer_name
-    binary_name = testcase.get_metadata('fuzzer_binary_name')
-    if binary_name:
-      content_string += 'Fuzz target binary: %s\n' % binary_name
+  fuzzer_display = get_fuzzer_display(testcase)
+  if fuzzer_display.engine:
+    content_string += 'Fuzzing engine: %s\n' % fuzzer_display.engine
+    content_string += 'Fuzz target: %s\n' % fuzzer_display.target
+  else:
+    content_string += 'Fuzzer: %s\n' % fuzzer_display.name
 
   content_string += 'Job Type: %s\n' % testcase.job_type
 


Test patch:
diff --git a/src/python/tests/core/datastore/data_handler_test.py b/src/python/tests/core/datastore/data_handler_test.py
--- a/src/python/tests/core/datastore/data_handler_test.py
+++ b/src/python/tests/core/datastore/data_handler_test.py
@@ -88,7 +88,8 @@ def setUp(self):
                             'HELP_URL = help_url\n'))
     self.testcase = data_types.Testcase(
         job_type='linux_asan_chrome',
-        fuzzer_name='libfuzzer_binary_name',
+        fuzzer_name='libFuzzer',
+        overridden_fuzzer_name='libfuzzer_binary_name',
         crash_type='Crash-type',
         crash_address='0x1337',
         crash_state='A\nB\nC\n')
@@ -97,7 +98,8 @@ def setUp(self):
 
     self.testcase_assert = data_types.Testcase(
         job_type='linux_asan_chrome',
-        fuzzer_name='libfuzzer_binary_name',
+        fuzzer_name='libFuzzer',
+        overridden_fuzzer_name='libfuzzer_binary_name',
         crash_type='ASSERT',
         crash_address='0x1337',
         crash_state='foo != bar\nB\nC\n')
@@ -106,21 +108,21 @@ def setUp(self):
 
     self.testcase_null = data_types.Testcase(
         job_type='linux_asan_chrome',
-        fuzzer_name='libfuzzer_binary_name',
+        fuzzer_name='fuzzer1',
         crash_type='UNKNOWN',
         crash_address='0x1337',
         crash_state='NULL')
 
     self.testcase_empty = data_types.Testcase(
         job_type='linux_asan_chrome',
-        fuzzer_name='libfuzzer_binary_name',
+        fuzzer_name='fuzzer2',
         crash_type='',
         crash_address='',
         crash_state='')
 
     self.testcase_bad_cast = data_types.Testcase(
         job_type='linux_asan_chrome',
-        fuzzer_name='libfuzzer_binary_name',
+        fuzzer_name='fuzzer3',
         crash_type='Bad-cast',
         crash_address='0x1337',
         crash_state=(
@@ -130,7 +132,7 @@ def setUp(self):
 
     self.testcase_bad_cast_without_crash_function = data_types.Testcase(
         job_type='linux_asan_chrome',
-        fuzzer_name='libfuzzer_binary_name',
+        fuzzer_name='fuzzer3',
         crash_type='Bad-cast',
         crash_address='0x1337',
         crash_state=(
@@ -255,8 +257,8 @@ def test_get_issue_description_oom(self):
     self.assertEqual(
         description, 'Detailed report: https://test-clusterfuzz.appspot.com/'
         'testcase?key=1\n\n'
-        'Fuzzer: libfuzzer_binary_name\n'
-        'Fuzz target binary: binary_name\n'
+        'Fuzzing engine: libFuzzer\n'
+        'Fuzz target: binary_name\n'
         'Job Type: windows_asan_chrome\n'
         'Crash Type: Out-of-memory (exceeds 2048 MB)\n'
         'Crash Address: 0x1337\n'
@@ -281,8 +283,8 @@ def test_get_issue_description_timeout(self):
     self.assertEqual(
         description, 'Detailed report: https://test-clusterfuzz.appspot.com/'
         'testcase?key=1\n\n'
-        'Fuzzer: libfuzzer_binary_name\n'
-        'Fuzz target binary: binary_name\n'
+        'Fuzzing engine: libFuzzer\n'
+        'Fuzz target: binary_name\n'
         'Job Type: linux_asan_chrome\n'
         'Crash Type: Timeout (exceeds 25 secs)\n'
         'Crash Address: 0x1337\n'
@@ -302,8 +304,8 @@ def test_get_issue_description_different_project(self):
         description, 'Detailed report: https://test-clusterfuzz.appspot.com/'
         'testcase?key=1\n\n'
         'Project: project\n'
-        'Fuzzer: libfuzzer_binary_name\n'
-        'Fuzz target binary: binary_name\n'
+        'Fuzzing engine: libFuzzer\n'
+        'Fuzz target: binary_name\n'
         'Job Type: linux_asan_chrome\n'
         'Crash Type: Crash-type\n'
         'Crash Address: 0x1337\n'
@@ -313,6 +315,26 @@ def test_get_issue_description_different_project(self):
         'https://test-clusterfuzz.appspot.com/download?testcase_id=1\n\n'
         'See help_url for instructions to reproduce this bug locally.')
 
+  def test_get_issue_description_blackbox_fuzzer_testcase(self):
+    """Test get_issue_description with a blackbox fuzzer testcase."""
+    self.mock.default_project_name.return_value = 'oss-fuzz'
+    self.mock.get().url = 'url'
+
+    description = data_handler.get_issue_description(self.testcase_null)
+    self.assertEqual(
+        description, 'Detailed report: https://test-clusterfuzz.appspot.com/'
+        'testcase?key=3\n\n'
+        'Project: project\n'
+        'Fuzzer: fuzzer1\n'
+        'Job Type: linux_asan_chrome\n'
+        'Crash Type: UNKNOWN\n'
+        'Crash Address: 0x1337\n'
+        'Crash State:\n  NULL\n'
+        'Sanitizer: address (ASAN)\n\n'
+        'Reproducer Testcase: https://test-clusterfuzz.appspot.com/'
+        'download?testcase_id=3\n\n'
+        'See help_url for instructions to reproduce this bug locally.')
+
   def test_get_issue_summary_with_no_prefix(self):
     """Test get_issue_description on jobs with no prefix."""
     self.job.environment_string = 'HELP_URL = help_url\n'
@@ -556,7 +578,7 @@ def setUp(self):
     job.name = 'job_with_help_format'
     job.environment_string = (
         'HELP_FORMAT = -%TESTCASE%\\n-%FUZZER_NAME%\\n-%FUZZ_TARGET%\\n'
-        '-%PROJECT%\\n\n'
+        '-%PROJECT%\\n-%REVISION%\\n\n'
         'PROJECT_NAME = test_project')
     job.put()
 
@@ -565,7 +587,7 @@ def setUp(self):
     job.environment_string = ('PROJECT_NAME = test_project')
     job.put()
 
-    fuzz_target = data_types.FuzzTarget()
+    fuzz_target = data_types.FuzzTarget(id='libFuzzer_test_project_test_fuzzer')
     fuzz_target.binary = 'test_fuzzer'
     fuzz_target.project = 'test_project'
     fuzz_target.engine = 'libFuzzer'
@@ -577,11 +599,12 @@ def test_libfuzzer_testcase(self):
     testcase.fuzzer_name = 'libFuzzer'
     testcase.overridden_fuzzer_name = 'libFuzzer_test_project_test_fuzzer'
     testcase.job_type = 'job_with_help_format'
+    testcase.crash_revision = 1337
     testcase.put()
 
     self.assertEquals(
         data_handler.get_formatted_reproduction_help(testcase),
-        '-{id}\n-libFuzzer\n-test_fuzzer\n-test_project\n'.format(
+        '-{id}\n-libFuzzer\n-test_fuzzer\n-test_project\n-1337\n'.format(
             id=testcase.key.id()))
 
   def test_traditional_testcase(self):
@@ -589,11 +612,13 @@ def test_traditional_testcase(self):
     testcase = data_types.Testcase()
     testcase.fuzzer_name = 'simple_fuzzer'
     testcase.job_type = 'job_with_help_format'
+    testcase.crash_revision = 1337
     testcase.put()
+    testcase.set_metadata('last_tested_crash_revision', 1338)
 
     self.assertEquals(
         data_handler.get_formatted_reproduction_help(testcase),
-        '-{id}\n-simple_fuzzer\n-\n-test_project\n'.format(
+        '-{id}\n-simple_fuzzer\n-\n-test_project\n-1338\n'.format(
             id=testcase.key.id()))
 
   def test_libfuzzer_testcase_with_default_help_format(self):
@@ -601,14 +626,16 @@ def test_libfuzzer_testcase_with_default_help_format(self):
     environment."""
     environment.set_value(
         'HELP_FORMAT',
-        '-%TESTCASE%\\n-%FUZZER_NAME%\\n-%FUZZ_TARGET%\\n-%PROJECT%\\n')
+        '-%TESTCASE%\\n-%FUZZER_NAME%\\n-%FUZZ_TARGET%\\n-%PROJECT%\\n'
+        '-%REVISION%\\n')
 
     testcase = data_types.Testcase()
     testcase.fuzzer_name = 'simple_fuzzer'
     testcase.job_type = 'job_without_help_format'
+    testcase.crash_revision = 1337
     testcase.put()
 
     self.assertEquals(
         data_handler.get_formatted_reproduction_help(testcase),
-        '-{id}\n-simple_fuzzer\n-\n-test_project\n'.format(
+        '-{id}\n-simple_fuzzer\n-\n-test_project\n-1337\n'.format(
             id=testcase.key.id()))