diff options
Diffstat (limited to 'test/elixir')
-rw-r--r-- | test/elixir/.credo.exs | 156 | ||||
-rw-r--r-- | test/elixir/Makefile | 4 | ||||
-rw-r--r-- | test/elixir/README.md | 143 | ||||
-rw-r--r-- | test/elixir/lib/ex_unit.ex | 44 | ||||
-rw-r--r-- | test/elixir/lib/setup.ex | 97 | ||||
-rw-r--r-- | test/elixir/lib/setup/common.ex | 22 | ||||
-rw-r--r-- | test/elixir/lib/step.ex | 44 | ||||
-rw-r--r-- | test/elixir/lib/step/config.ex | 33 | ||||
-rw-r--r-- | test/elixir/lib/step/create_db.ex | 53 | ||||
-rw-r--r-- | test/elixir/lib/step/start.ex | 85 | ||||
-rw-r--r-- | test/elixir/lib/step/user.ex | 104 | ||||
-rw-r--r-- | test/elixir/lib/utils.ex | 61 | ||||
-rw-r--r-- | test/elixir/mix.exs | 37 | ||||
-rw-r--r-- | test/elixir/mix.lock | 9 | ||||
-rwxr-xr-x | test/elixir/run | 6 | ||||
-rw-r--r-- | test/elixir/test/replication_test.exs | 4 | ||||
-rw-r--r-- | test/elixir/test/test_helper.exs | 8 |
17 files changed, 696 insertions, 214 deletions
diff --git a/test/elixir/.credo.exs b/test/elixir/.credo.exs deleted file mode 100644 index e24836c8f..000000000 --- a/test/elixir/.credo.exs +++ /dev/null @@ -1,156 +0,0 @@ -# This file contains the configuration for Credo and you are probably reading -# this after creating it with `mix credo.gen.config`. -# -# If you find anything wrong or unclear in this file, please report an -# issue on GitHub: https://github.com/rrrene/credo/issues -# -%{ - # - # You can have as many configs as you like in the `configs:` field. - configs: [ - %{ - # - # Run any exec using `mix credo -C <name>`. If no exec name is given - # "default" is used. - # - name: "default", - # - # These are the files included in the analysis: - files: %{ - # - # You can give explicit globs or simply directories. - # In the latter case `**/*.{ex,exs}` will be used. - # - included: ["lib/", "src/", "test/", "web/", "apps/"], - excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] - }, - # - # If you create your own checks, you must specify the source files for - # them here, so they can be loaded by Credo before running the analysis. - # - requires: [], - # - # If you want to enforce a style guide and need a more traditional linting - # experience, you can change `strict` to `true` below: - # - strict: false, - # - # If you want to use uncolored output by default, you can change `color` - # to `false` below: - # - color: true, - # - # You can customize the parameters of any check by adding a second element - # to the tuple. - # - # To disable a check put `false` as second element: - # - # {Credo.Check.Design.DuplicatedCode, false} - # - checks: [ - # - ## Consistency Checks - # - {Credo.Check.Consistency.ExceptionNames, []}, - {Credo.Check.Consistency.LineEndings, []}, - {Credo.Check.Consistency.ParameterPatternMatching, false}, - {Credo.Check.Consistency.SpaceAroundOperators, []}, - {Credo.Check.Consistency.SpaceInParentheses, []}, - {Credo.Check.Consistency.TabsOrSpaces, []}, - - # - ## Design Checks - # - # You can customize the priority of any check - # Priority values are: `low, normal, high, higher` - # - {Credo.Check.Design.AliasUsage, - [priority: :low, if_nested_deeper_than: 2, if_called_more_often_than: 0]}, - # You can also customize the exit_status of each check. - # If you don't want TODO comments to cause `mix credo` to fail, just - # set this value to 0 (zero). - # - {Credo.Check.Design.TagTODO, false}, - {Credo.Check.Design.TagFIXME, []}, - - # - ## Readability Checks - # - {Credo.Check.Readability.AliasOrder, []}, - {Credo.Check.Readability.FunctionNames, []}, - {Credo.Check.Readability.LargeNumbers, []}, - {Credo.Check.Readability.MaxLineLength, [priority: :normal, max_length: 90]}, - {Credo.Check.Readability.ModuleAttributeNames, []}, - {Credo.Check.Readability.ModuleDoc, []}, - {Credo.Check.Readability.ModuleNames, []}, - {Credo.Check.Readability.ParenthesesInCondition, []}, - {Credo.Check.Readability.ParenthesesOnZeroArityDefs, []}, - {Credo.Check.Readability.PredicateFunctionNames, []}, - {Credo.Check.Readability.PreferImplicitTry, []}, - {Credo.Check.Readability.RedundantBlankLines, []}, - {Credo.Check.Readability.Semicolons, []}, - {Credo.Check.Readability.SpaceAfterCommas, []}, - {Credo.Check.Readability.StringSigils, []}, - {Credo.Check.Readability.TrailingBlankLine, []}, - {Credo.Check.Readability.TrailingWhiteSpace, []}, - {Credo.Check.Readability.VariableNames, []}, - - # - ## Refactoring Opportunities - # - {Credo.Check.Refactor.CondStatements, []}, - {Credo.Check.Refactor.CyclomaticComplexity, false}, - {Credo.Check.Refactor.FunctionArity, []}, - {Credo.Check.Refactor.LongQuoteBlocks, false}, - {Credo.Check.Refactor.MapInto, []}, - {Credo.Check.Refactor.MatchInCondition, []}, - {Credo.Check.Refactor.NegatedConditionsInUnless, []}, - {Credo.Check.Refactor.NegatedConditionsWithElse, []}, - {Credo.Check.Refactor.Nesting, false}, - {Credo.Check.Refactor.PipeChainStart, - [ - excluded_argument_types: [:atom, :binary, :fn, :keyword], - excluded_functions: [] - ]}, - {Credo.Check.Refactor.UnlessWithElse, []}, - - # - ## Warnings - # - {Credo.Check.Warning.BoolOperationOnSameValues, []}, - {Credo.Check.Warning.ExpensiveEmptyEnumCheck, []}, - {Credo.Check.Warning.IExPry, []}, - {Credo.Check.Warning.IoInspect, []}, - {Credo.Check.Warning.LazyLogging, []}, - {Credo.Check.Warning.OperationOnSameValues, []}, - {Credo.Check.Warning.OperationWithConstantResult, []}, - {Credo.Check.Warning.RaiseInsideRescue, []}, - {Credo.Check.Warning.UnusedEnumOperation, []}, - {Credo.Check.Warning.UnusedFileOperation, []}, - {Credo.Check.Warning.UnusedKeywordOperation, []}, - {Credo.Check.Warning.UnusedListOperation, []}, - {Credo.Check.Warning.UnusedPathOperation, []}, - {Credo.Check.Warning.UnusedRegexOperation, []}, - {Credo.Check.Warning.UnusedStringOperation, []}, - {Credo.Check.Warning.UnusedTupleOperation, []}, - - # - # Controversial and experimental checks (opt-in, just remove `, false`) - # - {Credo.Check.Consistency.MultiAliasImportRequireUse, false}, - {Credo.Check.Design.DuplicatedCode, false}, - {Credo.Check.Readability.Specs, false}, - {Credo.Check.Refactor.ABCSize, false}, - {Credo.Check.Refactor.AppendSingleItem, false}, - {Credo.Check.Refactor.DoubleBooleanNegation, false}, - {Credo.Check.Refactor.VariableRebinding, false}, - {Credo.Check.Warning.MapGetUnsafePass, false}, - {Credo.Check.Warning.UnsafeToAtom, false} - - # - # Custom checks can be created using `mix credo.gen.check`. - # - ] - } - ] -} diff --git a/test/elixir/Makefile b/test/elixir/Makefile index bfcf017d5..67ce2b427 100644 --- a/test/elixir/Makefile +++ b/test/elixir/Makefile @@ -1,2 +1,4 @@ +SELF_DIR := $(dir $(lastword $(MAKEFILE_LIST))) + all: - mix test --trace + make -C ${SELF_DIR}../.. elixir 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..3ecf878e7 --- /dev/null +++ b/test/elixir/lib/utils.ex @@ -0,0 +1,61 @@ +defmodule Couch.Test.Utils do + require Record + @moduledoc "Helper functions for testing" + @project_root "#{__DIR__}/../../../" + Record.defrecord :user_ctx, Record.extract( + :user_ctx, from: "#{@project_root}/src/couch/include/couch_db.hrl") + + Record.defrecord :doc_info, Record.extract( + :doc_info, from: "#{@project_root}/src/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 diff --git a/test/elixir/mix.exs b/test/elixir/mix.exs deleted file mode 100644 index f04038ef3..000000000 --- a/test/elixir/mix.exs +++ /dev/null @@ -1,37 +0,0 @@ -defmodule Foo.Mixfile do - use Mix.Project - - def project do - [ - app: :foo, - version: "0.1.0", - elixir: "~> 1.6", - elixirc_paths: elixirc_paths(Mix.env()), - start_permanent: Mix.env() == :prod, - deps: deps() - ] - end - - # Run "mix help compile.app" to learn about applications. - def application do - [ - extra_applications: [:logger] - ] - end - - # Specifies which paths to compile per environment. - defp elixirc_paths(:test), do: ["lib", "test/support"] - defp elixirc_paths(_), do: ["lib"] - - # Run "mix help deps" to learn about dependencies. - defp deps do - [ - # {:dep_from_hexpm, "~> 0.3.0"}, - {:httpotion, "~> 3.0"}, - {:jiffy, "~> 0.15.2"}, - {:credo, "~> 1.0.0", only: [:dev, :test], runtime: false}, - {:junit_formatter, "~> 3.0", only: [:test]} - # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}, - ] - end -end diff --git a/test/elixir/mix.lock b/test/elixir/mix.lock deleted file mode 100644 index 0fc391a92..000000000 --- a/test/elixir/mix.lock +++ /dev/null @@ -1,9 +0,0 @@ -%{ - "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, - "credo": {:hex, :credo, "1.0.0", "aaa40fdd0543a0cf8080e8c5949d8c25f0a24e4fc8c1d83d06c388f5e5e0ea42", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, - "httpotion": {:hex, :httpotion, "3.1.0", "14d20d9b0ce4e86e253eb91e4af79e469ad949f57a5d23c0a51b2f86559f6589", [:mix], [{:ibrowse, "~> 4.4", [hex: :ibrowse, repo: "hexpm", optional: false]}], "hexpm"}, - "ibrowse": {:hex, :ibrowse, "4.4.1", "2b7d0637b0f8b9b4182de4bd0f2e826a4da2c9b04898b6e15659ba921a8d6ec2", [:rebar3], [], "hexpm"}, - "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, - "jiffy": {:hex, :jiffy, "0.15.2", "de266c390111fd4ea28b9302f0bc3d7472468f3b8e0aceabfbefa26d08cd73b7", [:rebar3], [], "hexpm"}, - "junit_formatter": {:hex, :junit_formatter, "3.0.0", "13950d944dbd295da7d8cc4798b8faee808a8bb9b637c88069954eac078ac9da", [:mix], [], "hexpm"}, -} diff --git a/test/elixir/run b/test/elixir/run deleted file mode 100755 index a9c2efa4d..000000000 --- a/test/elixir/run +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -e -cd "$(dirname "$0")" -mix local.hex --force -mix local.rebar --force -mix deps.get -mix test --trace "$@" diff --git a/test/elixir/test/replication_test.exs b/test/elixir/test/replication_test.exs index e98775fbd..a6e1303e6 100644 --- a/test/elixir/test/replication_test.exs +++ b/test/elixir/test/replication_test.exs @@ -1757,11 +1757,11 @@ defmodule ReplicationTest do end def get_att1_data do - File.read!("test/data/lorem.txt") + File.read!(Path.expand("data/lorem.txt", __DIR__)) end def get_att2_data do - File.read!("test/data/lorem_b64.txt") + File.read!(Path.expand("data/lorem_b64.txt", __DIR__)) end def cmp_json(lhs, rhs) when is_map(lhs) and is_map(rhs) do diff --git a/test/elixir/test/test_helper.exs b/test/elixir/test/test_helper.exs index ef71bbb1b..4bf65bcf6 100644 --- a/test/elixir/test/test_helper.exs +++ b/test/elixir/test/test_helper.exs @@ -2,12 +2,14 @@ # and skip certain tests that fail on jenkins. exclude = case System.get_env("BUILD_NUMBER") !== nil do - true -> [pending: true, skip_on_jenkins: true] - false -> [pending: true] + true -> [:pending, :skip_on_jenkins] + false -> [:pending] end +current_exclude = Keyword.get(ExUnit.configuration(), :exclude, []) + ExUnit.configure( - exclude: exclude, + exclude: Enum.uniq(exclude ++ current_exclude), formatters: [JUnitFormatter, ExUnit.CLIFormatter] ) |