[01/13] tests: Add 'store_samples' decorator

Message ID 20181030113153.7855-2-stephen@that.guru
State New
Headers show
Series
  • Start generating API examples from tests
Related show

Commit Message

Stephen Finucane Oct. 30, 2018, 11:31 a.m.
We want to start including sample API requests and responses in our
documentation.  Given that these may get out of date over time, we
should really generate these things dynamically. Create a decorator that
will allow us to do just that.

Signed-off-by: Stephen Finucane <stephen@that.guru>
---
 .gitignore                   |  1 +
 patchwork/tests/api/utils.py | 93 ++++++++++++++++++++++++++++++++++++
 2 files changed, 94 insertions(+)
 create mode 100644 patchwork/tests/api/utils.py

Patch

diff --git a/.gitignore b/.gitignore
index a33d1029..1282bc9f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,6 +44,7 @@  htmlcov/
 
 # Sphinx stuff
 /docs/_build
+/docs/api/samples
 
 # Selenium test artifacts
 /selenium.log
diff --git a/patchwork/tests/api/utils.py b/patchwork/tests/api/utils.py
new file mode 100644
index 00000000..1097bb07
--- /dev/null
+++ b/patchwork/tests/api/utils.py
@@ -0,0 +1,93 @@ 
+# Patchwork - automated patch tracking system
+# Copyright (C) 2018 Stephen Finucane <stephen@that.guru>
+#
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+import functools
+import json
+import os
+
+# docs/examples
+OUT_DIR = os.path.join(
+    os.path.dirname(os.path.abspath(__file__)), os.pardir, os.pardir,
+    os.pardir, 'docs', 'api', 'samples')
+
+_WRITTEN_FILES = {}
+
+
+def _duplicate_sample(filename, func):
+    global _WRITTEN_FILES
+
+    # sanity check to make sure we're not writing to the same file
+    # twice
+    if filename in _WRITTEN_FILES:
+        # though if tests do this, we simply ignore subsequent
+        # writes
+        if _WRITTEN_FILES[filename] == func:
+            return True
+
+        raise Exception(
+            "Tests '{}' and '{}' write to the same file".format(
+                _WRITTEN_FILES[filename], func))
+
+    _WRITTEN_FILES[filename] = func
+
+    return False
+
+
+def store_samples(filename):
+    """Wrapper to store responses and requests generated in tests.
+
+    These can be used in documentation. Only the first response or request body
+    is saved per test.
+    """
+
+    if not os.path.exists(OUT_DIR):
+        os.mkdir(OUT_DIR)
+
+    def inner(func):
+
+        def wrapper(self, *args, **kwargs):
+
+            def client_wrapper(orig_func, path, data=None, *orig_args,
+                               **orig_kwargs):
+
+                req_filename = filename + '-req.json'
+                resp_filename = filename + '-resp.json'
+
+                # we don't have a request body for GET requests
+                if orig_func != _get and not _duplicate_sample(
+                        req_filename, func):
+                    with open(os.path.join(OUT_DIR, req_filename), 'w') as fh:
+                        json.dump(data, fh, indent=4, separators=(',', ': '))
+
+                resp = orig_func(path, data, *orig_args, **orig_kwargs)
+
+                if not _duplicate_sample(resp_filename, func):
+                    with open(os.path.join(OUT_DIR, resp_filename), 'w') as fh:
+                        json.dump(resp.data, fh, indent=4,
+                                  separators=(',', ': '))
+
+                return resp
+
+            # replace client.* with our own implementations
+            _get = self.client.get
+            self.client.get = functools.partial(client_wrapper, _get)
+            _post = self.client.post
+            self.client.post = functools.partial(client_wrapper, _post)
+            _put = self.client.put
+            self.client.put = functools.partial(client_wrapper, _put)
+            _patch = self.client.patch
+            self.client.patch = functools.partial(client_wrapper, _patch)
+
+            func(self, *args, **kwargs)
+
+            # ...then reverse
+            self.client.patch = _patch
+            self.client.put = _put
+            self.client.post = _post
+            self.client.get = _get
+
+        return wrapper
+
+    return inner