summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorILYA Khlopotov <iilyak@apache.org>2019-05-27 19:17:16 +0000
committerILYA Khlopotov <iilyak@apache.org>2019-07-29 11:24:37 +0000
commitd0ccfa2b463b7087c1c3a766ca3ae963c1d1738d (patch)
tree9a5f34affa5f647ecf0bdbe968276572387d6552
parentcf60cffb44bf18889aa8362b616bafb7bed87445 (diff)
downloadcouchdb-d0ccfa2b463b7087c1c3a766ca3ae963c1d1738d.tar.gz
Add chained setups
-rw-r--r--test/elixir/README.md143
-rw-r--r--test/elixir/lib/ex_unit.ex44
-rw-r--r--test/elixir/lib/setup.ex97
-rw-r--r--test/elixir/lib/setup/common.ex22
-rw-r--r--test/elixir/lib/step.ex44
-rw-r--r--test/elixir/lib/step/config.ex33
-rw-r--r--test/elixir/lib/step/create_db.ex53
-rw-r--r--test/elixir/lib/step/start.ex85
-rw-r--r--test/elixir/lib/step/user.ex104
-rw-r--r--test/elixir/lib/utils.ex60
10 files changed, 685 insertions, 0 deletions
diff --git a/test/elixir/README.md b/test/elixir/README.md
index a59b4df90..f7691ad3c 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 000000000..7abba07ef
--- /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 000000000..037988521
--- /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 000000000..3b59e9476
--- /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 000000000..316d765aa
--- /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 000000000..9d9ac8eab
--- /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 000000000..3cca3c55a
--- /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 000000000..ea7c70f5a
--- /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 000000000..5a1cab33c
--- /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 000000000..4d3f33f69
--- /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