Patchwork testing: [sprint] [RFC] add hypothesis fuzz testing

login
register
mail settings
Submitter David MacIver
Date Oct. 24, 2015, 11:51 a.m.
Message ID <9f4fcdad9054deda9a08.1445687510@waste.org>
Download mbox | patch
Permalink /patch/11230/
State Accepted
Headers show

Comments

David MacIver - Oct. 24, 2015, 11:51 a.m.
# HG changeset patch
# User David R. MacIver <david@drmaciver.com>
# Date 1445687163 -3600
#      Sat Oct 24 12:46:03 2015 +0100
# Node ID 9f4fcdad9054deda9a08a08146d309882ddeca94
# Parent  a9ed5a8fc5e0554d5cb81b7206d2203cc49a2d23
testing: [sprint] [RFC] add hypothesis fuzz testing

Hypothesis a library for adding fuzzing over a range of structure
data to your test suite: http://hypothesis.readthedocs.org/en/latest/

This adds two tests using Hypothesis to the Mercurial test suite,
including a feature flag for detecting whether Hypothesis is installed
and a helper module for more natural integration with the Mercurial
test suite.

These two tests both currently fail. This should be because they are
demonstrating genuine bugs in Mercurial. In particular:

* JSON encoding from mercurial.templatefilter assumes that any character
  with a codepoint < 80 may be validly passed through as a JSON string.
  This is not correct, as characters below 40 are non-printable control
  characters.
* utf8b encoding will sometimes pass through characters that cannot be
  validly decoded as utf8b.
Pierre-Yves David - Nov. 2, 2015, 1:28 p.m.
On 10/24/2015 12:51 PM, David R. MacIver wrote:
> # HG changeset patch
> # User David R. MacIver <david@drmaciver.com>
> # Date 1445687163 -3600
> #      Sat Oct 24 12:46:03 2015 +0100
> # Node ID 9f4fcdad9054deda9a08a08146d309882ddeca94
> # Parent  a9ed5a8fc5e0554d5cb81b7206d2203cc49a2d23
> testing: [sprint] [RFC] add hypothesis fuzz testing

I've pushed to the clowncopter a slightly reworked documented and split 
version of this patch. (with grammar checked by a native)

Patch

diff --git a/hypothesishelpers.py b/hypothesishelpers.py
new file mode 100644
--- /dev/null
+++ b/hypothesishelpers.py
@@ -0,0 +1,50 @@ 
+import os
+import sys
+
+from hypothesis.settings import set_hypothesis_home_dir
+import hypothesis.strategies as st
+from hypothesis import given, Settings
+
+set_hypothesis_home_dir(os.path.join(
+    os.getenv('TESTTMP'), ".hypothesis"
+))
+
+
+def check(*args, **kwargs):
+    def accept(f):
+        print(f.__name__)
+        # Workaround for https://github.com/DRMacIver/hypothesis/issues/206
+        f.__module__ = '__anon__'
+        import traceback
+        try:
+            given(*args, settings=Settings(max_examples=2000), **kwargs)(f)()
+        except Exception:
+            traceback.print_exc(file=sys.stdout)
+            sys.exit(1)
+    return accept
+
+
+def roundtrips(data, decode, encode):
+    @given(data)
+    def testroundtrips(value):
+        encoded = encode(value)
+        decoded = decode(encoded)
+        if decoded != value:
+            raise ValueError(
+                "Round trip failed: %s(%r) -> %s(%r) -> %r" % (
+                    encode.__name__, value, decode.__name__, encoded,
+                    decoded
+                ))
+    import traceback
+    try:
+        testroundtrips()
+    except Exception:
+        traceback.print_exc(file=sys.stdout)
+        sys.exit(1)
+    print("Round trip OK")
+
+
+bytestrings = (
+    st.builds(lambda s, e: s.encode(e), st.text(), st.sampled_from([
+        'utf-8', 'utf-16',
+    ]))) | st.binary()
diff --git a/tests/hghave.py b/tests/hghave.py
--- a/tests/hghave.py
+++ b/tests/hghave.py
@@ -463,3 +463,12 @@ 
 @check("slow", "allow slow tests")
 def has_slow():
     return os.environ.get('HGTEST_SLOW') == 'slow'
+
+@check("hypothesis", "Is Hypothesis installed")
+def has_hypothesis():
+    try:
+        import hypothesis
+        hypothesis.given
+        return True
+    except ImportError:
+        return False
diff --git a/tests/test-fuzzing-json.t b/tests/test-fuzzing-json.t
new file mode 100644
--- /dev/null
+++ b/tests/test-fuzzing-json.t
@@ -0,0 +1,9 @@ 
+#require hypothesis
+
+  >>> from hypothesishelpers import *
+  >>> import mercurial.templatefilters as tf
+  >>> import json
+  >>> @check(st.text().map(lambda s: s.encode('utf-8')))
+  ... def testtfescapeproducesvalidjson(obj):
+  ...     json.loads('"' + tf.jsonescape(obj) + '"')
+  testtfescapeproducesvalidjson
diff --git a/tests/test-fuzzing-round-tripping.t b/tests/test-fuzzing-round-tripping.t
new file mode 100644
--- /dev/null
+++ b/tests/test-fuzzing-round-tripping.t
@@ -0,0 +1,8 @@ 
+#require hypothesis
+
+utf8b round trips
+
+  >>> from hypothesishelpers import *
+  >>> from mercurial.encoding import fromutf8b, toutf8b
+  >>> roundtrips(st.binary(), fromutf8b, toutf8b)
+  Round trip OK