You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ii...@apache.org on 2019/07/29 11:26:08 UTC

[couchdb] 04/06: Add chained setups

This is an automated email from the ASF dual-hosted git repository.

iilyak pushed a commit to branch exunit-simplified
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit d0ccfa2b463b7087c1c3a766ca3ae963c1d1738d
Author: ILYA Khlopotov <ii...@apache.org>
AuthorDate: Mon May 27 19:17:16 2019 +0000

    Add chained setups
---
 test/elixir/README.md             | 143 ++++++++++++++++++++++++++++++++++++++
 test/elixir/lib/ex_unit.ex        |  44 ++++++++++++
 test/elixir/lib/setup.ex          |  97 ++++++++++++++++++++++++++
 test/elixir/lib/setup/common.ex   |  22 ++++++
 test/elixir/lib/step.ex           |  44 ++++++++++++
 test/elixir/lib/step/config.ex    |  33 +++++++++
 test/elixir/lib/step/create_db.ex |  53 ++++++++++++++
 test/elixir/lib/step/start.ex     |  85 ++++++++++++++++++++++
 test/elixir/lib/step/user.ex      | 104 +++++++++++++++++++++++++++
 test/elixir/lib/utils.ex          |  60 ++++++++++++++++
 10 files changed, 685 insertions(+)

diff --git a/test/elixir/README.md b/test/elixir/README.md
index a59b4df..f7691ad 100644
--- a/test/elixir/README.md
+++ b/test/elixir/README.md
@@ -111,3 +111,146 @@ X means done, - means partially
   - [ ] Port view_pagination.js
   - [ ] Port view_sandboxing.js
   - [ ] Port view_update_seq.js
+
+# Using ExUnit to write unit tests
+
+Elixir has a number of benefits which makes writing unit tests easier.
+For example it is trivial to do codegeneration of tests.
+Bellow we present a few use cases where code-generation is really helpful.
+
+## How to write ExUnit tests
+
+1. Create new file in test/exunit/ directory (the file name should match *_test.exs)
+2. In case it is a first file in the directory create test_helper.exs (look at src/couch/test/exunit/test_helper.exs to get an idea)
+3. define test module which does `use Couch.Test.ExUnit.Case`
+4. Define test cases in the module
+
+You can run tests either:
+- using make: `make exunit`
+- using mix: BUILDDIR=`pwd` ERL_LIBS=`pwd`/src MIX_ENV=test mix test --trace
+
+## Generating tests from spec
+
+Sometimes we have some data in structured format and want
+to generate test cases using that data. This is easy in Elixir.
+For example suppose we have following spec:
+```
+{
+	"{db_name}/_view_cleanup": {
+		"roles": ["_admin"]
+    }
+}
+```
+We can use this spec to generate test cases
+```
+defmodule GenerateTestsFromSpec do
+  use ExUnit.Case
+  require Record
+  Record.defrecordp :user_ctx, Record.extract(:user_ctx, from_lib: "couch/include/couch_db.hrl")
+  Record.defrecordp :httpd, Record.extract(:httpd, from_lib: "couch/include/couch_db.hrl")
+
+  {:ok, spec_bin} = File.read("roles.json")
+  spec = :jiffy.decode(spec_bin, [:return_maps])
+  Enum.each spec, fn {path, path_spec} ->
+    roles = path_spec["roles"]
+    @roles roles
+    @path_parts String.split(path, "/")
+    test "Access with `#{inspect(roles)}` roles" do
+      req = httpd(path_parts: @path_parts, user_ctx: user_ctx(roles: @roles))
+      :chttpd_auth_request.authorize_request(req)
+    end
+  end
+end
+```
+As a result we would get
+```
+GenerateTestsFromSpec
+  * test Access with `["_admin"]` roles (0.00ms)
+```
+
+## Test all possible combinations
+
+Sometimes we want to test all possible permutations for parameters.
+This can be accomplished using something like the following:
+
+```
+defmodule Permutations do
+  use ExUnit.Case
+  pairs = :couch_tests_combinatorics.product([
+    [:remote, :local], [:remote, :local]
+  ])
+  for [source, dest] <- pairs do
+    @source source
+    @dest dest
+    test "Replication #{source} -> #{dest}" do
+     assert :ok == :ok
+    end
+  end
+end
+```
+
+This would produce following tests
+```
+Permutations
+  * test Replication remote -> remote (0.00ms)
+  * test Replication local -> remote (0.00ms)
+  * test Replication remote -> local (0.00ms)
+  * test Replication local -> local (0.00ms)
+```
+
+## Reuseing of common setups
+
+The setup functions are quite similar in lots of tests therefore it makes
+sense to reuse them. The idea is to add shared setup functions into either
+- test/elixir/lib/setup/common.ex
+- test/elixir/lib/setup/<something>.ex
+
+The setup functions looks like the following:
+```
+defmodule Foo do
+  alias Couch.Test.Setup.Step
+
+  def httpd_with_admin(setup) do
+    setup
+      |> Step.Start.new(:start, extra_apps: [:chttpd])
+      |> Step.User.new(:admin, roles: [:server_admin])
+  end
+end
+```
+
+These parts of a setup chain can be invoked as follows:
+```
+defmodule Couch.Test.CRUD do
+  use Couch.Test.ExUnit.Case
+  alias Couch.Test.Utils
+
+  alias Couch.Test.Setup
+
+  alias Couch.Test.Setup.Step
+
+  def with_db(context, setup) do
+    setup =
+      setup
+      |> Setup.Common.httpd_with_db()
+      |> Setup.run()
+
+    context =
+      Map.merge(context, %{
+        db_name: setup |> Setup.get(:db) |> Step.Create.DB.name(),
+        base_url: setup |> Setup.get(:start) |> Step.Start.clustered_url(),
+        user: setup |> Setup.get(:admin) |> Step.User.name()
+      })
+
+    {context, setup}
+  end
+
+  describe "Database CRUD using Fabric API" do
+    @describetag setup: &__MODULE__.with_db/2
+    test "Create DB", ctx do
+      IO.puts("base_url: #{ctx.base_url}")
+      IO.puts("admin: #{ctx.user}")
+      IO.puts("db_name: #{ctx.db_name}")
+    end
+  end
+end
+```
\ No newline at end of file
diff --git a/test/elixir/lib/ex_unit.ex b/test/elixir/lib/ex_unit.ex
new file mode 100644
index 0000000..7abba07
--- /dev/null
+++ b/test/elixir/lib/ex_unit.ex
@@ -0,0 +1,44 @@
+defmodule Couch.Test.ExUnit.Case do
+  @moduledoc """
+  Template for ExUnit test case. It can be used as follows:
+  ```
+  defmodule Couch.Test.CRUD do
+    use Couch.Test.ExUnit.Case
+    ...
+    def with_db(context, setup) do
+      setup = setup
+        |> Step.Start.new(:start, extra_apps: [:chttpd])
+        |> Setup.run
+      context = Map.merge(context, %{
+        base_url: setup |> Setup.get(:start) |> Step.Start.clustered_url
+      })
+      {context, setup}
+    end
+    describe "Group of tests" do
+    @describetag setup: &__MODULE__.with_db/2
+    test "Single test in a group", ctx do
+      ctx.base_url
+    end
+    ...
+  end
+  ```
+  """
+
+  use ExUnit.CaseTemplate
+  alias Couch.Test.Setup
+
+  using do
+    quote do
+      require Logger
+      use ExUnit.Case
+    end
+  end
+
+  setup context do
+    case context do
+      %{:setup => setup_fun} ->
+        {:ok, Setup.setup(context, setup_fun)}
+      _ -> {:ok, context}
+    end
+  end
+end
\ No newline at end of file
diff --git a/test/elixir/lib/setup.ex b/test/elixir/lib/setup.ex
new file mode 100644
index 0000000..0379885
--- /dev/null
+++ b/test/elixir/lib/setup.ex
@@ -0,0 +1,97 @@
+defmodule Couch.Test.Setup do
+  @moduledoc """
+  Allows to chain setup functions.
+  Example of using:
+
+  ```
+    alias Couch,Test.Utils
+    def with_db_name(context, setup) do
+      setup =
+        setup
+          |> Step.Start.new(:start, extra_apps: [:chttpd])
+          |> Step.User.new(:admin, roles: [:server_admin])
+          |> Setup.run()
+
+      context =
+        Map.merge(context, %{
+          db_name: Utils.random_name("db")
+          base_url: setup |> Setup.get(:start) |> Step.Start.clustered_url(),
+          user: setup |> Setup.get(:admin) |> Step.User.name()
+        })
+      {context, setup}
+    end
+
+    @tag setup: &__MODULE__.with_db_name/2
+      test "Create", %{db_name: db_name, user: user} do
+        ...
+      end
+  ```
+  """
+  import ExUnit.Callbacks, only: [on_exit: 1]
+  import ExUnit.Assertions, only: [assert: 2]
+  require Logger
+
+  alias Couch.Test.Setup
+  alias Couch.Test.Setup.Step
+  defstruct stages: [], by_type: %{}, state: %{}
+
+  def step(%Setup{stages: stages} = setup, id, step) do
+    %{setup | stages: [{id, step} | stages]}
+  end
+
+  defp setup_step({id, step}, %Setup{state: state, by_type: by_type} = setup) do
+    %module{} = step
+    # credo:disable-for-next-line Credo.Check.Warning.LazyLogging
+    Logger.debug("Calling 'setup/2' for '#{module}'")
+    step = module.setup(setup, step)
+    state = Map.put(state, id, step)
+    by_type = Map.update(by_type, module, [id], fn ids -> [id | ids] end)
+    on_exit(fn ->
+      # credo:disable-for-next-line Credo.Check.Warning.LazyLogging
+      Logger.debug("Calling 'teardown/3' for '#{module}'")
+      try do
+        module.teardown(setup, step)
+        :ok
+      catch
+        _ -> :ok
+        _, _ -> :ok
+      end
+    end)
+    {{id, step}, %{setup | state: state, by_type: by_type}}
+  end
+
+  def run(%Setup{stages: stages} = setup) do
+    {stages, setup} = stages
+      |> Enum.reverse
+      |> Enum.map_reduce(setup, &setup_step/2)
+    %{setup | stages: stages}
+  end
+
+  def setup(ctx) do
+    Map.get(ctx, :__setup)
+  end
+
+  def setup(ctx, setup_fun) do
+    setup = %Setup{} |> Step.Config.new(:test_config, config_file: nil)
+    {ctx, setup} = setup_fun.(ctx, setup)
+    assert not Map.has_key?(ctx, :__setup), "Key `__setup` is reserved for internal purposes"
+    Map.put(ctx, :__setup, setup)
+  end
+
+  def completed?(%Setup{by_type: by_type}, step) do
+    Map.has_key?(by_type, step)
+  end
+
+  def all_for(%Setup{by_type: by_type, state: state}, step_module) do
+    Map.take(state, by_type[step_module] || [])
+  end
+
+  def reduce_for(setup, step_module, acc, fun) do
+    Enum.reduce(all_for(setup, step_module), acc, fun)
+  end
+
+  def get(%Setup{state: state}, id) do
+    state[id]
+  end
+
+end
\ No newline at end of file
diff --git a/test/elixir/lib/setup/common.ex b/test/elixir/lib/setup/common.ex
new file mode 100644
index 0000000..3b59e94
--- /dev/null
+++ b/test/elixir/lib/setup/common.ex
@@ -0,0 +1,22 @@
+defmodule Couch.Test.Setup.Common do
+  @moduledoc """
+  A set of common setup pipelines for reuse
+
+  - httpd_with_admin - chttpd is started and new admin is created
+  - httpd_with_db - httpd_with_admin and new database is created
+  """
+  alias Couch.Test.Setup.Step
+
+  def httpd_with_admin(setup) do
+    setup
+      |> Step.Start.new(:start, extra_apps: [:chttpd])
+      |> Step.User.new(:admin, roles: [:server_admin])
+  end
+
+  def httpd_with_db(setup) do
+    setup
+      |> httpd_with_admin()
+      |> Step.Create.DB.new(:db)
+  end
+
+end
\ No newline at end of file
diff --git a/test/elixir/lib/step.ex b/test/elixir/lib/step.ex
new file mode 100644
index 0000000..316d765
--- /dev/null
+++ b/test/elixir/lib/step.ex
@@ -0,0 +1,44 @@
+defmodule Couch.Test.Setup.Step do
+  @moduledoc """
+  A behaviour module for implementing custom setup steps for future reuse.
+
+  Every module implementing this behaviour must implement following three functions:
+  - new
+  - setup
+  - teardown
+
+  Here is an example of a custom step
+  ```
+  defmodule Couch.Test.Setup.Step.Foo do
+
+    alias Couch.Test.Setup
+
+    defstruct [:foo_data, :foo_arg]
+
+    def new(setup, id, arg: arg) do
+      setup |> Setup.step(id, %__MODULE__{foo_arg: arg})
+    end
+
+    def setup(_setup, %__MODULE__{foo_arg: arg} = step) do
+      ...
+      foo_data = ...
+      %{step | foo_data: foo_data}
+    end
+
+    def teardown(_setup, _step) do
+    end
+
+    def get_data(%__MODULE__{foo_data: data}) do
+      data
+    end
+  end
+  ```
+  """
+  @type t :: struct()
+  @callback new(setup :: %Couch.Test.Setup{}, id :: atom(), args: Keyword.t()) ::
+    %Couch.Test.Setup{}
+  @callback setup(setup :: %Couch.Test.Setup{}, step :: t()) ::
+    t()
+  @callback teardown(setup :: %Couch.Test.Setup{}, step :: t()) ::
+    any()
+end
\ No newline at end of file
diff --git a/test/elixir/lib/step/config.ex b/test/elixir/lib/step/config.ex
new file mode 100644
index 0000000..9d9ac8e
--- /dev/null
+++ b/test/elixir/lib/step/config.ex
@@ -0,0 +1,33 @@
+defmodule Couch.Test.Setup.Step.Config do
+  @moduledoc """
+    This setup reads configuration for a test run.
+    It is not supposed to be called manually.
+  """
+
+  alias Couch.Test.Setup
+
+  defstruct [:config, :config_file]
+
+  def new(setup, id, config_file: config_file) do
+    setup |> Setup.step(id, %__MODULE__{config_file: config_file})
+  end
+
+  def setup(_setup, %__MODULE__{config_file: config_file} = step) do
+    # TODO we would need to access config file here
+    %{step | config: %{
+       backdoor: %{
+         protocol: "http"
+       },
+       clustered: %{
+         protocol: "http"
+       }
+    }}
+  end
+
+  def teardown(_setup, _step) do
+  end
+
+  def get(%__MODULE__{config: config}) do
+    config
+  end
+end
\ No newline at end of file
diff --git a/test/elixir/lib/step/create_db.ex b/test/elixir/lib/step/create_db.ex
new file mode 100644
index 0000000..3cca3c5
--- /dev/null
+++ b/test/elixir/lib/step/create_db.ex
@@ -0,0 +1,53 @@
+defmodule Couch.Test.Setup.Step.Create.DB do
+  @moduledoc """
+    This setup step creates a database with given name.
+    If name is not provided random name would be used.
+
+    Example
+      setup
+        ...
+        |> Setup.Step.Create.DB.new(:db)
+        ...
+        |> Setup.run
+      ...
+
+      db_name = setup |> Setup.get(:db) |> Setup.Step.Create.DB.name
+  """
+  alias Couch.Test.Setup
+  alias Couch.Test.Setup.Step
+  alias Couch.Test.Utils
+
+  defstruct [:name]
+
+  import ExUnit.Assertions, only: [assert: 1, assert: 2]
+
+  import Utils
+
+  @admin {:user_ctx, user_ctx(roles: ["_admin"])}
+
+  def new(setup, id) do
+    new(setup, id,  name: Utils.random_name("db"))
+  end
+
+  def new(setup, id, name: name) do
+    setup |> Setup.step(id, %__MODULE__{name: name})
+  end
+
+  def setup(setup, %__MODULE__{name: name} = step) do
+    assert Setup.completed?(setup, Step.Start), "Require `Start` step"
+    assert :fabric in Step.Start.apps(), "Fabric is not started"
+    res = :fabric.create_db(name, [@admin])
+    assert res in [:ok, :accepted], "Cannot create `#{name}` database"
+    step
+  end
+
+  def teardown(setup, %__MODULE__{name: name} = step) do
+    :fabric.delete_db(name, [@admin])
+    :ok
+  end
+
+  def name(%__MODULE__{name: name}) do
+    name
+  end
+
+end
\ No newline at end of file
diff --git a/test/elixir/lib/step/start.ex b/test/elixir/lib/step/start.ex
new file mode 100644
index 0000000..ea7c70f
--- /dev/null
+++ b/test/elixir/lib/step/start.ex
@@ -0,0 +1,85 @@
+defmodule Couch.Test.Setup.Step.Start do
+  @moduledoc """
+  Step to start a set of couchdb applications. By default it starts
+  list of applications from DEFAULT_APPS macro defined in `test_util.erl`.
+  At the time of writing this list included:
+    - inets
+    - ibrowse
+    - ssl
+    - config
+    - couch_epi
+    - couch_event
+    - couch
+
+  It is possible to specify additional list of applications to start.
+
+  This setup is also maintains `clustered_url` and `backdoor_url` for future use.
+  The value for `clustered_url` could be nil if :chttpd app is not included in extra_apps.
+
+  Example
+    setup
+      |> Setup.Step.Start.new(:start, extra_apps: [:fabric, :chttpd])
+    ...
+      |> Setup.run
+    ...
+
+    started_apps = Setup.Step.Start.apps
+    clustered_url = setup |> Setup.get(:start) |> Setup.Step.Start.clustered_url
+    backdoor_url = setup |> Setup.get(:start) |> Setup.Step.Start.backdoor_url
+  """
+  alias Couch.Test.Setup
+  alias Couch.Test.Setup.Step
+
+  defstruct [:test_ctx, :extra_apps, :clustered_url, :backdoor_url]
+
+  def new(setup, id, extra_apps: extra_apps) do
+    setup |> Setup.step(id, %__MODULE__{extra_apps: extra_apps || []})
+  end
+
+  def setup(setup, %__MODULE__{extra_apps: extra_apps} = step) do
+    test_config = setup |> Setup.get(:test_config) |> Step.Config.get()
+    protocol = test_config[:backdoor][:protocol] || "http"
+    test_ctx = :test_util.start_couch(extra_apps)
+    addr = :config.get('couch_httpd', 'bind_address', '127.0.0.1')
+    port = :mochiweb_socket_server.get(:couch_httpd, :port)
+    backdoor_url = "#{protocol}://#{addr}:#{port}"
+    clustered_url =
+      if :chttpd in extra_apps do
+        protocol = test_config[:clustered][:protocol] || "http"
+        addr = :config.get('chttpd', 'bind_address', '127.0.0.1')
+        port = :mochiweb_socket_server.get(:chttpd, :port)
+        "#{protocol}://#{addr}:#{port}"
+      else
+        nil
+      end
+    %{step |
+      test_ctx: test_ctx,
+      clustered_url: clustered_url,
+      backdoor_url: backdoor_url
+    }
+  end
+
+  def teardown(_setup, %___MODULE__{test_ctx: test_ctx}) do
+    :test_util.stop_couch(test_ctx)
+  end
+
+  def backdoor_url(%__MODULE__{backdoor_url: url}) do
+    url
+  end
+
+  def clustered_url(%__MODULE__{clustered_url: url}) do
+    url
+  end
+
+  def extra_apps(%__MODULE__{extra_apps: apps}) do
+    apps
+  end
+
+  @doc """
+  Returns list of currently running applications
+  """
+  def apps() do
+    for {x, _, _} <- Application.started_applications, do: x
+  end
+
+end
\ No newline at end of file
diff --git a/test/elixir/lib/step/user.ex b/test/elixir/lib/step/user.ex
new file mode 100644
index 0000000..5a1cab3
--- /dev/null
+++ b/test/elixir/lib/step/user.ex
@@ -0,0 +1,104 @@
+defmodule Couch.Test.Setup.Step.User do
+  @moduledoc """
+  Step to create user with given list of roles.
+  The :server_admin is a special role which is used to put user
+  into `admins` section of a config instead of a database.
+
+  Example
+    setup
+      |> Setup.Step.User.new(:admin, roles: [:server_admin])
+    ...
+      |> Setup.run
+    ...
+
+    user = setup |> Setup.get(:admin) |> Step.User.name()
+  """
+
+  alias Couch.Test.Setup
+  alias Couch.Test.Setup.Step
+  alias Couch.Test.Utils
+
+  import ExUnit.Callbacks, only: [on_exit: 1]
+
+  defstruct [:roles, :name, :password, :users_db]
+
+  import ExUnit.Assertions, only: [assert: 1, assert: 2]
+
+  import Utils
+
+  @admin {:user_ctx, user_ctx(roles: ["_admin"])}
+
+  def new(setup, id, roles: roles) do
+    setup |> Setup.step(id, %__MODULE__{roles: roles || []})
+  end
+
+  def setup(setup, %__MODULE__{roles: roles} = step) do
+    users_db = IO.chardata_to_string(
+      :config.get('chttpd_auth', 'authentication_db', '_users'))
+    if not Utils.db_exists?(users_db) do
+      on_exit fn ->
+        :fabric.delete_db(users_db, [@admin])
+      end
+      res = :fabric.create_db(users_db, [@admin])
+      assert res in [:ok, :accepted], "Cannot create `users` database #{users_db}"
+    end
+
+    if :server_admin in roles do
+      name = Utils.random_name("admin")
+      pass = Utils.random_password()
+      :config.set(
+        'admins', String.to_charlist(name), String.to_charlist(pass), false)
+      %{step |
+        name: name,
+        password: pass,
+        users_db: users_db
+      }
+    else
+      name = Utils.random_name("admin")
+      pass = Utils.random_password()
+      doc_id = "org.couchdb.user:#{name}"
+      user_doc = :couch_doc.from_json_obj(%{
+        _id: doc_id,
+        name: name,
+        type: "user",
+        roles: roles,
+        password: pass
+      })
+      res = :fabric.update_doc(users_db, user_doc, [@admin])
+      assert res in [:ok, :accepted], "Cannot create user document"
+      %{step |
+        name: name,
+        password: pass,
+        users_db: users_db,
+        roles: roles
+      }
+    end
+  end
+
+  def teardown(setup, %__MODULE__{name: name, users_db: users_db, roles: roles} = step) do
+    if :server_admin in roles do
+      :config.delete("admins", String.to_charlist(name), false)
+    else
+      doc_id = "org.couchdb.user:#{name}"
+      assert {:ok, doc_info(revs: [rev | _])} = :fabric.get_doc_info(users_db)
+      doc = :couch_doc.from_json_obj(%{
+        _id: doc_id,
+        _rev: rev,
+        _deleted: true
+      })
+      assert {:ok, _resp} = :fabric.update_doc(users_db, doc, [@admin])
+    end
+    :ok
+  end
+
+  def name(%__MODULE__{name: name}) do
+    name
+  end
+  def password(%__MODULE__{password: pass}) do
+    pass
+  end
+  def credentials(%__MODULE__{name: name, password: pass}) do
+    {name, pass}
+  end
+
+end
\ No newline at end of file
diff --git a/test/elixir/lib/utils.ex b/test/elixir/lib/utils.ex
new file mode 100644
index 0000000..4d3f33f
--- /dev/null
+++ b/test/elixir/lib/utils.ex
@@ -0,0 +1,60 @@
+defmodule Couch.Test.Utils do
+  require Record
+  @moduledoc "Helper functions for testing"
+  Record.defrecord :user_ctx, Record.extract(
+    :user_ctx, from_lib: "couch/include/couch_db.hrl")
+
+  Record.defrecord :doc_info, Record.extract(
+    :doc_info, from_lib: "couch/include/couch_db.hrl")
+
+  def random_name(prefix) do
+    time = :erlang.monotonic_time()
+    umi = :erlang.unique_integer([:monotonic])
+    "#{prefix}-#{time}-#{umi}"
+  end
+
+  def random_password() do
+    rand_bytes = :crypto.strong_rand_bytes(16)
+    rand_bytes
+      |> :base64.encode()
+      |> String.slice(0..16)
+  end
+
+  def db_exists?(db_name) do
+    try do
+      :fabric.get_db_info(db_name)
+    catch
+      :error, :database_does_not_exist -> false
+    end
+  end
+
+  @doc """
+  In some cases we need to access record definition at compile time.
+  We cannot use Record.defrecord in such cases. This helper function
+  can be used instead. Use it as follows:
+  ```
+  defmodule Foo do
+    admin_ctx = {:user_ctx, Utils.erlang_record(
+      :user_ctx, "couch/include/couch_db.hrl", roles: ["_admin"])}
+  end
+  ```
+
+  Longer term we should wrap erlang records as it is done for user_ctx
+  see beginning of the Utils.ex. In this case we would be able to use
+  them at compile time in other modules.
+  ```
+  Record.defrecord :user_ctx, Record.extract(
+    :user_ctx, from_lib: "couch/include/couch_db.hrl")
+  ```
+  """
+  def erlang_record(name, from_lib, opts \\ []) do
+    record_info = Record.extract(name, from_lib: from_lib)
+    index = [name | Keyword.keys(record_info)] |> Enum.with_index
+    draft = [name | Keyword.values(record_info)] |> List.to_tuple
+    opts
+      |> Enum.reduce(draft, fn
+        {k, v}, acc -> put_elem(acc, index[k], v)
+      end)
+  end
+
+end
\ No newline at end of file