snarfed / granary

💬 The social web translator
https://granary.io
Creative Commons Zero v1.0 Universal
443 stars 57 forks source link

add mutation testing #115

Open snarfed opened 7 years ago

snarfed commented 7 years ago

...via something like https://github.com/sixty-north/cosmic-ray. mutation testing is very cool. description in https://cosmic-ray.readthedocs.io/en/latest/#theory

long pole is that cosmic ray is python 3 only, which implies that this depends on #114. sixty-north/cosmic-ray#101 has more details about cosmic ray's lack of python 2 support.

snarfed commented 6 years ago

we're on python 3 now! looking forward to trying this.

snarfed commented 6 years ago

done! results:

total jobs: 1113
complete: 1113 (100.00%)
survival rate: 4.49%

config file:

# https://cosmic-ray.readthedocs.io/en/latest/quickstart.html#configuration-file

module: granary

baseline: 10

exclude-modules:
    - granary.test

test-runner:
  name: unittest
  args: granary/test/

execution-engine:
  name: local

full report (debugging details in sixty-north/cosmic-ray#352):

job ID 751ca09b450d4c3b995500e7cc352e2c:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceComparisonOperator_NotIn_Is 0
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -59,7 +59,7 @@

 def _activity_or_object(activity):
     "Returns the base item we care about, activity or activity['object'].\n\n  Used in :func:`activity_to_json()` and :func:`activities_to_html()`.\n  "
-    if (activity.get('object') and (activity.get('verb') not in source.VERBS_WITH_OBJECT)):
+    if (activity.get('object') and (activity.get('verb') is source.VERBS_WITH_OBJECT)):
         return activity['object']
     return activity

job ID d7556ddf202c43f9abffa600ade13d0c:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceOrWithAnd 7
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -70,7 +70,7 @@
     obj_type = (source.object_type(obj) or default_object_type)
     if (obj_type == 'post'):
         primary = obj.get('object', {})
-        obj_type = (source.object_type(primary) or default_object_type)
+        obj_type = (source.object_type(primary) and default_object_type)
     else:
         primary = obj
     name = primary.get('displayName', primary.get('title'))

job ID e9f99780983f49ee824b68821814506b:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceOrWithAnd 17
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -85,7 +85,7 @@
     for prop in ('attachments', 'tags'):
         for elem in get_list(primary, prop):
             attachments[elem.get('objectType')].append(elem)
-    ret = {'type': ((AS_TO_MF2_TYPE.get(obj_type) or [entry_class]) if isinstance(entry_class, basestring) else list(entry_class)), 'properties': {'uid': [(obj.get('id') or '')], 'numeric-id': [(obj.get('numeric_id') or '')], 'name': [name], 'nickname': [(obj.get('username') or '')], 'summary': [summary], 'url': (list((object_urls(obj) or object_urls(primary))) + obj.get('upstreamDuplicates', [])), 'photo': dedupe_urls((get_urls(attachments, 'image', 'image') + get_urls(primary, 'image'))), 'video': dedupe_urls((get_urls(attachments, 'video', 'stream') + get_urls(primary, 'stream'))), 'audio': get_urls(attachments, 'audio', 'stream'), 'published': [obj.get('published', primary.get('published', ''))], 'updated': [obj.get('updated', primary.get('updated', ''))], 'content': [{'value': xml.sax.saxutils.unescape(primary.get('content', '')), 'html': render_content(primary, include_location=False, synthesize_content=synthesize_content)}], 'in-reply-to': util.trim_nulls([o.get('url') for o in in_reply_tos]), 'author': [object_to_json(author, trim_nulls=False, default_object_type='person')], 'location': [object_to_json(primary.get('location', {}), trim_nulls=False, default_object_type='place')], 'comment': [object_to_json(c, trim_nulls=False, entry_class='h-cite') for c in obj.get('replies', {}).get('items', [])], 'start': [primary.get('startTime')], 'end': [primary.get('endTime')]}, 'children': [object_to_json(a, trim_nulls=False, entry_class=['u-quotation-of', 'h-cite']) for a in (attachments['note'] + attachments['article'])]}
+    ret = {'type': ((AS_TO_MF2_TYPE.get(obj_type) or [entry_class]) if isinstance(entry_class, basestring) else list(entry_class)), 'properties': {'uid': [(obj.get('id') or '')], 'numeric-id': [(obj.get('numeric_id') or '')], 'name': [name], 'nickname': [(obj.get('username') or '')], 'summary': [summary], 'url': (list((object_urls(obj) and object_urls(primary))) + obj.get('upstreamDuplicates', [])), 'photo': dedupe_urls((get_urls(attachments, 'image', 'image') + get_urls(primary, 'image'))), 'video': dedupe_urls((get_urls(attachments, 'video', 'stream') + get_urls(primary, 'stream'))), 'audio': get_urls(attachments, 'audio', 'stream'), 'published': [obj.get('published', primary.get('published', ''))], 'updated': [obj.get('updated', primary.get('updated', ''))], 'content': [{'value': xml.sax.saxutils.unescape(primary.get('content', '')), 'html': render_content(primary, include_location=False, synthesize_content=synthesize_content)}], 'in-reply-to': util.trim_nulls([o.get('url') for o in in_reply_tos]), 'author': [object_to_json(author, trim_nulls=False, default_object_type='person')], 'location': [object_to_json(primary.get('location', {}), trim_nulls=False, default_object_type='place')], 'comment': [object_to_json(c, trim_nulls=False, entry_class='h-cite') for c in obj.get('replies', {}).get('items', [])], 'start': [primary.get('startTime')], 'end': [primary.get('endTime')]}, 'children': [object_to_json(a, trim_nulls=False, entry_class=['u-quotation-of', 'h-cite']) for a in (attachments['note'] + attachments['article'])]}
     tags = (obj.get('tags', []) or get_first(obj, 'object', {}).get('tags', []))
     ret['properties']['category'] = []
     for tag in tags:

job ID 35e6d2407f4f4a748da02d19ec8d226b:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceTrueFalse 3
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -85,7 +85,7 @@
     for prop in ('attachments', 'tags'):
         for elem in get_list(primary, prop):
             attachments[elem.get('objectType')].append(elem)
-    ret = {'type': ((AS_TO_MF2_TYPE.get(obj_type) or [entry_class]) if isinstance(entry_class, basestring) else list(entry_class)), 'properties': {'uid': [(obj.get('id') or '')], 'numeric-id': [(obj.get('numeric_id') or '')], 'name': [name], 'nickname': [(obj.get('username') or '')], 'summary': [summary], 'url': (list((object_urls(obj) or object_urls(primary))) + obj.get('upstreamDuplicates', [])), 'photo': dedupe_urls((get_urls(attachments, 'image', 'image') + get_urls(primary, 'image'))), 'video': dedupe_urls((get_urls(attachments, 'video', 'stream') + get_urls(primary, 'stream'))), 'audio': get_urls(attachments, 'audio', 'stream'), 'published': [obj.get('published', primary.get('published', ''))], 'updated': [obj.get('updated', primary.get('updated', ''))], 'content': [{'value': xml.sax.saxutils.unescape(primary.get('content', '')), 'html': render_content(primary, include_location=False, synthesize_content=synthesize_content)}], 'in-reply-to': util.trim_nulls([o.get('url') for o in in_reply_tos]), 'author': [object_to_json(author, trim_nulls=False, default_object_type='person')], 'location': [object_to_json(primary.get('location', {}), trim_nulls=False, default_object_type='place')], 'comment': [object_to_json(c, trim_nulls=False, entry_class='h-cite') for c in obj.get('replies', {}).get('items', [])], 'start': [primary.get('startTime')], 'end': [primary.get('endTime')]}, 'children': [object_to_json(a, trim_nulls=False, entry_class=['u-quotation-of', 'h-cite']) for a in (attachments['note'] + attachments['article'])]}
+    ret = {'type': ((AS_TO_MF2_TYPE.get(obj_type) or [entry_class]) if isinstance(entry_class, basestring) else list(entry_class)), 'properties': {'uid': [(obj.get('id') or '')], 'numeric-id': [(obj.get('numeric_id') or '')], 'name': [name], 'nickname': [(obj.get('username') or '')], 'summary': [summary], 'url': (list((object_urls(obj) or object_urls(primary))) + obj.get('upstreamDuplicates', [])), 'photo': dedupe_urls((get_urls(attachments, 'image', 'image') + get_urls(primary, 'image'))), 'video': dedupe_urls((get_urls(attachments, 'video', 'stream') + get_urls(primary, 'stream'))), 'audio': get_urls(attachments, 'audio', 'stream'), 'published': [obj.get('published', primary.get('published', ''))], 'updated': [obj.get('updated', primary.get('updated', ''))], 'content': [{'value': xml.sax.saxutils.unescape(primary.get('content', '')), 'html': render_content(primary, include_location=False, synthesize_content=synthesize_content)}], 'in-reply-to': util.trim_nulls([o.get('url') for o in in_reply_tos]), 'author': [object_to_json(author, trim_nulls=True, default_object_type='person')], 'location': [object_to_json(primary.get('location', {}), trim_nulls=False, default_object_type='place')], 'comment': [object_to_json(c, trim_nulls=False, entry_class='h-cite') for c in obj.get('replies', {}).get('items', [])], 'start': [primary.get('startTime')], 'end': [primary.get('endTime')]}, 'children': [object_to_json(a, trim_nulls=False, entry_class=['u-quotation-of', 'h-cite']) for a in (attachments['note'] + attachments['article'])]}
     tags = (obj.get('tags', []) or get_first(obj, 'object', {}).get('tags', []))
     ret['properties']['category'] = []
     for tag in tags:

job ID 679f16baadaf4a6aa8bf9f80df1381a0:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceTrueFalse 4
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -85,7 +85,7 @@
     for prop in ('attachments', 'tags'):
         for elem in get_list(primary, prop):
             attachments[elem.get('objectType')].append(elem)
-    ret = {'type': ((AS_TO_MF2_TYPE.get(obj_type) or [entry_class]) if isinstance(entry_class, basestring) else list(entry_class)), 'properties': {'uid': [(obj.get('id') or '')], 'numeric-id': [(obj.get('numeric_id') or '')], 'name': [name], 'nickname': [(obj.get('username') or '')], 'summary': [summary], 'url': (list((object_urls(obj) or object_urls(primary))) + obj.get('upstreamDuplicates', [])), 'photo': dedupe_urls((get_urls(attachments, 'image', 'image') + get_urls(primary, 'image'))), 'video': dedupe_urls((get_urls(attachments, 'video', 'stream') + get_urls(primary, 'stream'))), 'audio': get_urls(attachments, 'audio', 'stream'), 'published': [obj.get('published', primary.get('published', ''))], 'updated': [obj.get('updated', primary.get('updated', ''))], 'content': [{'value': xml.sax.saxutils.unescape(primary.get('content', '')), 'html': render_content(primary, include_location=False, synthesize_content=synthesize_content)}], 'in-reply-to': util.trim_nulls([o.get('url') for o in in_reply_tos]), 'author': [object_to_json(author, trim_nulls=False, default_object_type='person')], 'location': [object_to_json(primary.get('location', {}), trim_nulls=False, default_object_type='place')], 'comment': [object_to_json(c, trim_nulls=False, entry_class='h-cite') for c in obj.get('replies', {}).get('items', [])], 'start': [primary.get('startTime')], 'end': [primary.get('endTime')]}, 'children': [object_to_json(a, trim_nulls=False, entry_class=['u-quotation-of', 'h-cite']) for a in (attachments['note'] + attachments['article'])]}
+    ret = {'type': ((AS_TO_MF2_TYPE.get(obj_type) or [entry_class]) if isinstance(entry_class, basestring) else list(entry_class)), 'properties': {'uid': [(obj.get('id') or '')], 'numeric-id': [(obj.get('numeric_id') or '')], 'name': [name], 'nickname': [(obj.get('username') or '')], 'summary': [summary], 'url': (list((object_urls(obj) or object_urls(primary))) + obj.get('upstreamDuplicates', [])), 'photo': dedupe_urls((get_urls(attachments, 'image', 'image') + get_urls(primary, 'image'))), 'video': dedupe_urls((get_urls(attachments, 'video', 'stream') + get_urls(primary, 'stream'))), 'audio': get_urls(attachments, 'audio', 'stream'), 'published': [obj.get('published', primary.get('published', ''))], 'updated': [obj.get('updated', primary.get('updated', ''))], 'content': [{'value': xml.sax.saxutils.unescape(primary.get('content', '')), 'html': render_content(primary, include_location=False, synthesize_content=synthesize_content)}], 'in-reply-to': util.trim_nulls([o.get('url') for o in in_reply_tos]), 'author': [object_to_json(author, trim_nulls=False, default_object_type='person')], 'location': [object_to_json(primary.get('location', {}), trim_nulls=True, default_object_type='place')], 'comment': [object_to_json(c, trim_nulls=False, entry_class='h-cite') for c in obj.get('replies', {}).get('items', [])], 'start': [primary.get('startTime')], 'end': [primary.get('endTime')]}, 'children': [object_to_json(a, trim_nulls=False, entry_class=['u-quotation-of', 'h-cite']) for a in (attachments['note'] + attachments['article'])]}
     tags = (obj.get('tags', []) or get_first(obj, 'object', {}).get('tags', []))
     ret['properties']['category'] = []
     for tag in tags:

job ID 85b5f2c644634774a78d5ff3609c17c7:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceTrueFalse 7
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -98,7 +98,7 @@
     if is_rsvp:
         ret['properties']['rsvp'] = [obj_type[len('rsvp-'):]]
     elif (obj_type == 'invite'):
-        invitee = object_to_json(obj.get('object'), trim_nulls=False, default_object_type='person')
+        invitee = object_to_json(obj.get('object'), trim_nulls=True, default_object_type='person')
         ret['properties']['invitee'] = [invitee]
     for (type, prop) in (('favorite', 'like'), ('like', 'like'), ('share', 'repost')):
         if (obj_type == type):

job ID 3e8b31cbe0d741859fa95a44e59d6f9f:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceTrueFalse 8
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -103,7 +103,7 @@
     for (type, prop) in (('favorite', 'like'), ('like', 'like'), ('share', 'repost')):
         if (obj_type == type):
             objs = get_list(obj, 'object')
-            ret['properties'][(prop + '-of')] = [(o['url'] if (('url' in o) and (set(o.keys()) <= set(['url', 'objectType']))) else object_to_json(o, trim_nulls=False, entry_class='h-cite')) for o in objs]
+            ret['properties'][(prop + '-of')] = [(o['url'] if (('url' in o) and (set(o.keys()) <= set(['url', 'objectType']))) else object_to_json(o, trim_nulls=True, entry_class='h-cite')) for o in objs]
         else:
             ret['properties'][prop] = [object_to_json(t, trim_nulls=False, entry_class='h-cite') for t in tags if (source.object_type(t) == type)]
     lat = long = None

job ID bea1deff4cf04359a51bf38d07b43467:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceComparisonOperator_In_IsNot 1
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -103,7 +103,7 @@
     for (type, prop) in (('favorite', 'like'), ('like', 'like'), ('share', 'repost')):
         if (obj_type == type):
             objs = get_list(obj, 'object')
-            ret['properties'][(prop + '-of')] = [(o['url'] if (('url' in o) and (set(o.keys()) <= set(['url', 'objectType']))) else object_to_json(o, trim_nulls=False, entry_class='h-cite')) for o in objs]
+            ret['properties'][(prop + '-of')] = [(o['url'] if (('url' is not o) and (set(o.keys()) <= set(['url', 'objectType']))) else object_to_json(o, trim_nulls=False, entry_class='h-cite')) for o in objs]
         else:
             ret['properties'][prop] = [object_to_json(t, trim_nulls=False, entry_class='h-cite') for t in tags if (source.object_type(t) == type)]
     lat = long = None

job ID ed97ceb5bb09489f82aceb27ff0f09ca:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceTrueFalse 9
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -105,7 +105,7 @@
             objs = get_list(obj, 'object')
             ret['properties'][(prop + '-of')] = [(o['url'] if (('url' in o) and (set(o.keys()) <= set(['url', 'objectType']))) else object_to_json(o, trim_nulls=False, entry_class='h-cite')) for o in objs]
         else:
-            ret['properties'][prop] = [object_to_json(t, trim_nulls=False, entry_class='h-cite') for t in tags if (source.object_type(t) == type)]
+            ret['properties'][prop] = [object_to_json(t, trim_nulls=True, entry_class='h-cite') for t in tags if (source.object_type(t) == type)]
     lat = long = None
     position = ISO_6709_RE.match((primary.get('position') or ''))
     if position:

job ID 3db7d4456a9d4941b26e5abc0830d322:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceOrWithAnd 23
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -124,7 +124,7 @@

 def json_to_object(mf2, actor=None, fetch_mf2=False):
     'Converts microformats2 JSON to an ActivityStreams object.\n\n  Args:\n    mf2: dict, decoded JSON microformats2 object\n    actor: optional author AS actor object. usually comes from a rel="author"\n      link. if mf2 has its own author, that will override this.\n    fetch_mf2: boolean, whether to fetch additional pages via HTTP if necessary,\n      e.g. to determine authorship: https://indieweb.org/authorship\n\n  Returns:\n    dict, ActivityStreams object\n  '
-    if ((not mf2) or (not isinstance(mf2, dict))):
+    if ((not mf2) and (not isinstance(mf2, dict))):
         return {}
     mf2 = copy.copy(mf2)
     props = mf2.setdefault('properties', {})

job ID d3ee375384ce4a9eb3d2410628e10d5a:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ZeroIterationLoop 4
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -153,7 +153,7 @@
     if rsvp:
         as_verb = ('rsvp-%s' % rsvp)
     in_reply_tos = get_string_urls(props.get('in-reply-to', []))
-    for url in in_reply_tos:
+    for url in []:
         if re.match('^https?://github.com/[^/]+/[^/]+(/issues)?/?$', url):
             as_type = 'issue'

job ID b66c3d4a386249de95b4700fefde8f68:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceComparisonOperator_In_IsNot 4
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -160,7 +160,7 @@
     def absolute_urls(prop):
         return [url for url in get_string_urls(props.get(prop, [])) if urllib.parse.urlparse(url).netloc]
     urls = (props.get('url') and get_string_urls(props.get('url')))
-    attachments = [json_to_object(quote) for quote in (mf2.get('children', []) + props.get('quotation-of', [])) if (isinstance(quote, dict) and ('h-cite' in set(quote.get('type', []))))]
+    attachments = [json_to_object(quote) for quote in (mf2.get('children', []) + props.get('quotation-of', [])) if (isinstance(quote, dict) and ('h-cite' is not set(quote.get('type', []))))]
     for type in ('audio', 'video'):
         attachments.extend(({'objectType': type, 'stream': {'url': url}} for url in get_string_urls(props.get(type, []))))
     obj = {'id': prop.get('uid'), 'objectType': as_type, 'verb': as_verb, 'published': prop.get('published', ''), 'updated': prop.get('updated', ''), 'startTime': prop.get('start'), 'endTime': prop.get('end'), 'displayName': get_text(prop.get('name')), 'summary': get_text(prop.get('summary')), 'content': get_html(prop.get('content')), 'url': (urls[0] if urls else None), 'urls': ([{'value': u} for u in urls] if (urls and (len(urls) > 1)) else None), 'image': [{'url': url} for url in dedupe_urls((absolute_urls('photo') + absolute_urls('featured')))], 'stream': [{'url': url} for url in absolute_urls('video')], 'location': json_to_object(prop.get('location')), 'replies': {'items': [json_to_object(c) for c in props.get('comment', [])]}, 'tags': [({'objectType': 'hashtag', 'displayName': cat} if isinstance(cat, basestring) else json_to_object(cat)) for cat in props.get('category', [])], 'attachments': attachments}

job ID 172cd68866ba4dbea3ed75084b42fc08:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceComparisonOperator_Gt_NotEq 0
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -163,7 +163,7 @@
     attachments = [json_to_object(quote) for quote in (mf2.get('children', []) + props.get('quotation-of', [])) if (isinstance(quote, dict) and ('h-cite' in set(quote.get('type', []))))]
     for type in ('audio', 'video'):
         attachments.extend(({'objectType': type, 'stream': {'url': url}} for url in get_string_urls(props.get(type, []))))
-    obj = {'id': prop.get('uid'), 'objectType': as_type, 'verb': as_verb, 'published': prop.get('published', ''), 'updated': prop.get('updated', ''), 'startTime': prop.get('start'), 'endTime': prop.get('end'), 'displayName': get_text(prop.get('name')), 'summary': get_text(prop.get('summary')), 'content': get_html(prop.get('content')), 'url': (urls[0] if urls else None), 'urls': ([{'value': u} for u in urls] if (urls and (len(urls) > 1)) else None), 'image': [{'url': url} for url in dedupe_urls((absolute_urls('photo') + absolute_urls('featured')))], 'stream': [{'url': url} for url in absolute_urls('video')], 'location': json_to_object(prop.get('location')), 'replies': {'items': [json_to_object(c) for c in props.get('comment', [])]}, 'tags': [({'objectType': 'hashtag', 'displayName': cat} if isinstance(cat, basestring) else json_to_object(cat)) for cat in props.get('category', [])], 'attachments': attachments}
+    obj = {'id': prop.get('uid'), 'objectType': as_type, 'verb': as_verb, 'published': prop.get('published', ''), 'updated': prop.get('updated', ''), 'startTime': prop.get('start'), 'endTime': prop.get('end'), 'displayName': get_text(prop.get('name')), 'summary': get_text(prop.get('summary')), 'content': get_html(prop.get('content')), 'url': (urls[0] if urls else None), 'urls': ([{'value': u} for u in urls] if (urls and (len(urls) != 1)) else None), 'image': [{'url': url} for url in dedupe_urls((absolute_urls('photo') + absolute_urls('featured')))], 'stream': [{'url': url} for url in absolute_urls('video')], 'location': json_to_object(prop.get('location')), 'replies': {'items': [json_to_object(c) for c in props.get('comment', [])]}, 'tags': [({'objectType': 'hashtag', 'displayName': cat} if isinstance(cat, basestring) else json_to_object(cat)) for cat in props.get('category', [])], 'attachments': attachments}
     interpreted = mf2util.interpret({'items': [mf2]}, None)
     if interpreted:
         loc = interpreted.get('location')

job ID a8b4c90581bc44d287f79e93a13f93b3:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceAndWithOr 17
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -170,7 +170,7 @@
         if loc:
             obj['location']['objectType'] = 'place'
             (lat, lng) = (loc.get('latitude'), loc.get('longitude'))
-            if (lat and lng):
+            if (lat or lng):
                 try:
                     obj['location'].update({'latitude': float(lat), 'longitude': float(lng)})
                 except ValueError:

job ID 54b8ee2910ef40728186a90d4353dec3:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceComparisonOperator_NotIn_NotEq 1
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -179,7 +179,7 @@
         objects = []
         for target in itertools.chain.from_iterable((props.get(field, []) for field in ('like', 'like-of', 'repost', 'repost-of', 'in-reply-to', 'invitee'))):
             t = (json_to_object(target) if isinstance(target, dict) else {'url': target})
-            if (t not in objects):
+            if (t != objects):
                 objects.append(t)
         obj.update({'object': (objects[0] if (len(objects) == 1) else objects), 'actor': author})
     else:

job ID b37d3abbb2ae4f7eaf5f06756984163c:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/NumberReplacer 15
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -230,7 +230,7 @@
     if rsvp:
         if (not props.get('name')):
             props['name'] = [{'yes': 'is attending.', 'no': 'is not attending.', 'maybe': 'might attend.'}.get(rsvp)]
-        props['name'][0] = ('<data class="p-rsvp" value="%s">%s</data>' % (rsvp, props['name'][0]))
+        props['name'][0] = ('<data class="p-rsvp" value="%s">%s</data>' % (rsvp, props['name'][-1]))
     elif (props.get('invitee') and (not props.get('name'))):
         props['name'] = ['invited']
     children = []

job ID e213bc5b5c0741cebb8d70925ebecae0:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceComparisonOperator_NotIn_IsNot 2
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -257,7 +257,7 @@
     tags = [('<span class="u-category">%s</span>' % cat) for cat in cats if isinstance(cat, basestring)]
     comments_html = '\n'.join((json_to_html(c, ['p-comment']) for c in props.get('comment', [])))
     for verb in ('like', 'repost'):
-        if ((verb + '-of') not in props):
+        if ((verb + '-of') is not props):
             vals = props.get(verb, [])
             if (vals and isinstance(vals[0], dict)):
                 children += [json_to_html(v, [('u-' + verb)]) for v in vals]

job ID 6a005e48360c40469beb7b67a7ed1432:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/NumberReplacer 17
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -259,7 +259,7 @@
     for verb in ('like', 'repost'):
         if ((verb + '-of') not in props):
             vals = props.get(verb, [])
-            if (vals and isinstance(vals[0], dict)):
+            if (vals and isinstance(vals[-1], dict)):
                 children += [json_to_html(v, [('u-' + verb)]) for v in vals]
     children += [json_to_html(c) for c in obj.get('children', [])]
     location = prop.get('location')

job ID 6fde419f4bdb4c38a2433e5e2234ed46:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceAndWithOr 26
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -269,7 +269,7 @@
     start = props.get('start', [])
     end = props.get('end', [])
     event_times += [('  <time class="dt-start">%s</time>' % time) for time in start]
-    if (start and end):
+    if (start or end):
         event_times.append('  to')
     event_times += [('  <time class="dt-end">%s</time>' % time) for time in end]
     return HENTRY.substitute(prop, published=maybe_datetime(prop.get('published'), 'dt-published'), updated=maybe_datetime(prop.get('updated'), 'dt-updated'), types=' '.join((parent_props + types)), author=hcard_to_html(author, ['p-author']), location=hcard_to_html(location, ['p-location']), categories='\n'.join((people + tags)), attachments='\n'.join(attachments), in_reply_tos=in_reply_tos, invitees='\n'.join([hcard_to_html(i, ['p-invitee']) for i in props.get('invitee', [])]), content=content_html, content_classes=' '.join(content_classes), comments=comments_html, children='\n'.join(children), linked_name=maybe_linked_name(props), summary=summary, event_times='\n'.join(event_times))

job ID be1f8761dfde4dceb145500ac49be72a:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceAndWithOr 30
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -297,7 +297,7 @@
         if (id and (id in seen_ids)):
             continue
         seen_ids.add(id)
-        if (('startIndex' in t) and ('length' in t)):
+        if (('startIndex' in t) or ('length' in t)):
             mentions.append(t)
         else:
             tags.setdefault(source.object_type(t), []).append(t)

job ID 4814bb601a3b428d957a7c2069646e26:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceComparisonOperator_NotIn_Is 4
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -319,7 +319,7 @@
         content += _render_attachments((atts + tags.pop('article', [])), obj)
     obj_type = source.object_type(obj)
     for (as_type, verb) in (('favorite', 'Favorites'), ('like', 'Likes'), ('share', 'Shared')):
-        if ((not synthesize_content) or (obj_type != as_type) or ('object' not in obj) or ('content' in obj)):
+        if ((not synthesize_content) or (obj_type != as_type) or ('object' is obj) or ('content' in obj)):
             continue
         targets = get_list(obj, 'object')
         if (not targets):

job ID 48bf3f011a2b49ea9ef878badcc179c9:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceBinaryOperator_Mod_Add 11
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -330,7 +330,7 @@
             else:
                 author = target.get('author', target.get('actor', {}))
                 if ((obj_type == 'share') and ('url' in obj) and re.search('^https?://(?:www\\.|mobile\\.)?twitter\\.com/', obj.get('url'))):
-                    content += ('RT <a href="%s">@%s</a> ' % (target.get('url', '#'), author.get('username')))
+                    content += ('RT <a href="%s">@%s</a> ' + (target.get('url', '#'), author.get('username')))
                 else:
                     author = {k: v for (k, v) in author.items() if (k != 'image')}
                     content += ('%s <a href="%s">%s</a> by %s' % (verb, target.get('url', '#'), target.get('displayName', target.get('title', 'a post')), hcard_to_html(object_to_json(author, default_object_type='person'))))

job ID 62cb22b52f6b4fa9a40e5afa849797b9:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/ReplaceComparisonOperator_NotIn_NotEq 5
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -338,7 +338,7 @@
             break
         break
     if (render_attachments and (obj.get('verb') == 'share')):
-        atts = [a for a in obj.get('object', {}).get('attachments', []) if (a.get('objectType') not in ('note', 'article'))]
+        atts = [a for a in obj.get('object', {}).get('attachments', []) if (a.get('objectType') != ('note', 'article'))]
         content += _render_attachments(atts, obj)
     loc = obj.get('location')
     if (include_location and loc):

job ID a5b47eec08e346d4800919a1848cc750:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/AddNot 61
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -410,7 +410,7 @@
 def author_display_name(hcard):
     'Returns a human-readable string display name for an h-card object.'
     name = None
-    if hcard:
+    if (not hcard):
         prop = first_props(hcard.get('properties'))
         name = (prop.get('name') or prop.get('uid'))
     return (name if name else 'Unknown')

job ID 0dbd3d16692043f89d7e66971ea5f39b:survived:granary.microformats2
command: cosmic-ray worker granary.microformats2 core/AddNot 62
--- mutation diff ---
--- a/Users/ryan/src/granary/granary/microformats2.py
+++ b/Users/ryan/src/granary/granary/microformats2.py
@@ -413,7 +413,7 @@
     if hcard:
         prop = first_props(hcard.get('properties'))
         name = (prop.get('name') or prop.get('uid'))
-    return (name if name else 'Unknown')
+    return (name if (not name) else 'Unknown')

 def maybe_linked_name(props):
     'Returns the HTML for a p-name with an optional u-url inside.\n\n  Args:\n    props: *multiply-valued* properties dict\n\n  Returns:\n    string HTML\n  '

total jobs: 1113
complete: 1113 (100.00%)
survival rate: 4.49%
snarfed commented 6 years ago

i also tried to do this for https://github.com/snarfed/webutil , but got stuck since only util.py is python 3, and the cosmic-ray config lets me run a specific directory's tests, but not a specific file. (i got it running, but it reported that almost all mutants survived, which they didn't.)

the fix would be to add a new cosmic ray config option and pass it to TestLoader.discover() here.

snarfed commented 6 years ago

i skimmed the granary mutations. they generally all look real, but not high priority, so i'm deprioritizing adding tests for them, at least for now.

...also i expect granary's cosmic ray config is buggy too, since we only got mutant reports in microformats2.py, no other files.