Initial commit
This commit is contained in:
commit
6b1729f4a7
11 changed files with 749 additions and 0 deletions
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
|
@ -0,0 +1,20 @@
|
|||
# The directory Mix will write compiled artifacts to.
|
||||
/_build
|
||||
|
||||
# If you run "mix test --cover", coverage assets end up here.
|
||||
/cover
|
||||
|
||||
# The directory Mix downloads your dependencies sources to.
|
||||
/deps
|
||||
|
||||
# Where 3rd-party dependencies like ExDoc output generated docs.
|
||||
/doc
|
||||
|
||||
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||
/.fetch
|
||||
|
||||
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||
erl_crash.dump
|
||||
|
||||
# Also ignore archive artifacts (built via "mix archive.build").
|
||||
*.ez
|
27
LICENSE
Normal file
27
LICENSE
Normal file
|
@ -0,0 +1,27 @@
|
|||
Copyright © 2017, Mikko Ahlroth
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
18
README.md
Normal file
18
README.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# FBU: Frontend Build Utilities
|
||||
|
||||
**TODO: Add description**
|
||||
|
||||
## Installation
|
||||
|
||||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||
by adding `fbu` to your list of dependencies in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[{:fbu, "~> 0.1.0"}]
|
||||
end
|
||||
```
|
||||
|
||||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
|
||||
be found at [https://hexdocs.pm/fbu](https://hexdocs.pm/fbu).
|
30
config/config.exs
Normal file
30
config/config.exs
Normal file
|
@ -0,0 +1,30 @@
|
|||
# This file is responsible for configuring your application
|
||||
# and its dependencies with the aid of the Mix.Config module.
|
||||
use Mix.Config
|
||||
|
||||
# This configuration is loaded before any dependency and is restricted
|
||||
# to this project. If another project depends on this project, this
|
||||
# file won't be loaded nor affect the parent project. For this reason,
|
||||
# if you want to provide default values for your application for
|
||||
# 3rd-party users, it should be done in your "mix.exs" file.
|
||||
|
||||
# You can configure for your application as:
|
||||
#
|
||||
# config :fbu, key: :value
|
||||
#
|
||||
# And access this configuration in your application as:
|
||||
#
|
||||
# Application.get_env(:fbu, :key)
|
||||
#
|
||||
# Or configure a 3rd-party app:
|
||||
#
|
||||
# config :logger, level: :info
|
||||
#
|
||||
|
||||
# It is also possible to import configuration files, relative to this
|
||||
# directory. For example, you can emulate configuration per environment
|
||||
# by uncommenting the line below and defining dev.exs, test.exs and such.
|
||||
# Configuration from the imported file will override the ones defined
|
||||
# here (which is why it is important to import them last).
|
||||
#
|
||||
# import_config "#{Mix.env}.exs"
|
86
lib/apprunner.exs
Normal file
86
lib/apprunner.exs
Normal file
|
@ -0,0 +1,86 @@
|
|||
defmodule FBU.AppRunner do
|
||||
@moduledoc """
|
||||
AppRunner is a script for ensuring that external apps are terminated
|
||||
properly when executed from BEAM.
|
||||
|
||||
If you start an application that does not listen to STDIN from BEAM
|
||||
and BEAM is terminated, the application's STDIN is closed but it may
|
||||
never close. AppRunner is meant to sit in between, monitoring the
|
||||
STDIN pipe and killing the application if STDIN closes. This ensures
|
||||
no zombie processes are left behind if the host BEAM dies.
|
||||
|
||||
Output from the application will be sent back to the host BEAM. If the
|
||||
application dies for any reason, AppRunner is also closed.
|
||||
"""
|
||||
|
||||
def wait_for_eof() do
|
||||
case IO.getn("", 1024) do
|
||||
:eof -> nil
|
||||
_ -> wait_for_eof()
|
||||
end
|
||||
end
|
||||
|
||||
def exec(program, args) do
|
||||
Port.open(
|
||||
{:spawn_executable, program},
|
||||
[
|
||||
:exit_status,
|
||||
:stderr_to_stdout, # Redirect stderr to stdout to log properly
|
||||
args: args,
|
||||
line: 1024
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def get_pid(port) do
|
||||
case Port.info(port) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
info when is_list(info) ->
|
||||
case Keyword.get(info, :os_pid) do
|
||||
nil -> nil
|
||||
pid -> Integer.to_string(pid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def kill(pid) do
|
||||
System.find_executable("kill") |> System.cmd([pid])
|
||||
end
|
||||
|
||||
def wait_loop(port) do
|
||||
receive do
|
||||
{_, {:data, {:eol, msg}}} ->
|
||||
msg |> :unicode.characters_to_binary(:unicode) |> IO.puts()
|
||||
|
||||
{_, {:data, {:noeol, msg}}} ->
|
||||
msg |> :unicode.characters_to_binary(:unicode) |> IO.write()
|
||||
|
||||
{_, :eof_received} ->
|
||||
get_pid(port) |> kill()
|
||||
:erlang.halt(0)
|
||||
|
||||
{_, :closed} ->
|
||||
:erlang.halt(0)
|
||||
|
||||
{_, {:exit_status, status}} ->
|
||||
:erlang.halt(status)
|
||||
|
||||
{:EXIT, _, _} ->
|
||||
:erlang.halt(1)
|
||||
end
|
||||
|
||||
wait_loop(port)
|
||||
end
|
||||
end
|
||||
|
||||
[program | args] = System.argv()
|
||||
port = FBU.AppRunner.exec(program, args)
|
||||
|
||||
Task.async(fn ->
|
||||
FBU.AppRunner.wait_for_eof()
|
||||
:eof_received
|
||||
end)
|
||||
|
||||
FBU.AppRunner.wait_loop(port)
|
44
lib/buildtask.ex
Normal file
44
lib/buildtask.ex
Normal file
|
@ -0,0 +1,44 @@
|
|||
defmodule FBU.BuildTask do
|
||||
@moduledoc """
|
||||
BuildTask contains the macros that are used for making the tasks look nicer.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Sets up the necessary things for a build task.
|
||||
|
||||
Each build task should have `use FBU.BuildTask` at its beginning. BuildTask automatically
|
||||
requires Logger so that doesn't need to be added into the task itself.
|
||||
"""
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
use Mix.Task
|
||||
require Logger
|
||||
|
||||
import FBU.BuildTask
|
||||
|
||||
# Dependencies of the task that will be automatically run before it
|
||||
@deps []
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Replacement for Mix.Task's run/1, used similarly. Code inside will be run when the task
|
||||
is called, with @deps being run first unless `deps: false` is given in the arguments.
|
||||
"""
|
||||
defmacro task(args, do: block) do
|
||||
quote do
|
||||
def run(unquote(args) = fbu_buildtask_args) do
|
||||
task = Mix.Task.task_name(__MODULE__)
|
||||
Logger.info("[Started] #{task}")
|
||||
|
||||
if Keyword.get(fbu_buildtask_args, :deps, true) and not Enum.empty?(@deps) do
|
||||
FBU.TaskUtils.run_tasks(@deps)
|
||||
end
|
||||
|
||||
unquote(block)
|
||||
|
||||
Logger.info("[Finished] #{task}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
407
lib/utils.ex
Normal file
407
lib/utils.ex
Normal file
|
@ -0,0 +1,407 @@
|
|||
defmodule FBU.TaskUtils do
|
||||
@moduledoc """
|
||||
Utilities for project build tasks.
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
@elixir System.find_executable("elixir")
|
||||
|
||||
@default_task_timeout 60000
|
||||
|
||||
defmodule ProgramSpec do
|
||||
@moduledoc """
|
||||
Program that is executed with arguments. Name is used for prefixing logs.
|
||||
"""
|
||||
defstruct [
|
||||
name: "",
|
||||
port: nil,
|
||||
pending_output: ""
|
||||
]
|
||||
end
|
||||
|
||||
defmodule WatchSpec do
|
||||
@moduledoc """
|
||||
Watch specification, target to watch and callback to execute on events.
|
||||
Name is used for prefixing logs.
|
||||
|
||||
Callback must have arity 2, gets filename/path and list of events as arguments.
|
||||
"""
|
||||
defstruct [
|
||||
name: "",
|
||||
path: "",
|
||||
callback: nil,
|
||||
pid: nil,
|
||||
name_atom: nil
|
||||
]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get configuration value.
|
||||
"""
|
||||
def conf(key) when is_atom(key) do
|
||||
Application.get_env(:code_stats, key)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Get absolute path to a program in $PATH.
|
||||
"""
|
||||
def exec_path(program) do
|
||||
System.find_executable(program)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Run the given Mix task and wait for it to stop before returning.
|
||||
|
||||
See run_tasks/2 for the argument description.
|
||||
"""
|
||||
def run_task(task, args \\ []) do
|
||||
run_tasks([{task, args}])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Run the given tasks in parallel and wait for them all to stop
|
||||
before returning.
|
||||
|
||||
Valid task types:
|
||||
|
||||
* `{task_name, args}`, where `task_name` is a Mix task name or module,
|
||||
* `task_name`, where `task_name` is a Mix task name or module to call without
|
||||
arguments, or
|
||||
* `task_function` where `task_function` is a function to call without arguments.
|
||||
"""
|
||||
def run_tasks(tasks) do
|
||||
tasks
|
||||
|
||||
|> Enum.map(fn
|
||||
task_name when is_atom(task_name) or is_binary(task_name) ->
|
||||
{task_name, []}
|
||||
|
||||
{task_name, args} when is_atom(task_name) or is_binary(task_name) ->
|
||||
{task_name, args}
|
||||
|
||||
fun when is_function(fun) ->
|
||||
fun
|
||||
end)
|
||||
|
||||
# Convert module references to string task names
|
||||
|> Enum.map(fn
|
||||
{task_module, args} when is_atom(task_module) ->
|
||||
{Mix.Task.task_name(task_module), args}
|
||||
|
||||
{task_name, args} when is_binary(task_name) ->
|
||||
{task_name, args}
|
||||
|
||||
fun when is_function(fun) ->
|
||||
fun
|
||||
end)
|
||||
|
||||
|> Enum.map(fn
|
||||
{task, args} ->
|
||||
# Rerun is used here to enable tasks being run again in watches
|
||||
fn -> Mix.Task.rerun(task, args) end
|
||||
|
||||
fun when is_function(fun) ->
|
||||
fun
|
||||
end)
|
||||
|
||||
|> run_funs(@default_task_timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Run the given functions in parallel and wait for them all to stop
|
||||
before returning.
|
||||
|
||||
Functions can either be anonymous functions or tuples of
|
||||
{module, fun, args}.
|
||||
"""
|
||||
def run_funs(funs, timeout \\ @default_task_timeout) when is_list(funs) do
|
||||
funs
|
||||
|> Enum.map(fn
|
||||
fun when is_function(fun) ->
|
||||
Task.async(fun)
|
||||
{module, fun, args} ->
|
||||
Task.async(module, fun, args)
|
||||
end)
|
||||
|> Enum.map(fn task ->
|
||||
Task.await(task, timeout)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Start an external program with apprunner.
|
||||
|
||||
Apprunner handles killing the program if BEAM is abruptly shut down.
|
||||
Name is used as a prefix for logging output.
|
||||
|
||||
Options that can be given:
|
||||
|
||||
- name: Use as name for logging, otherwise name of binary is used.
|
||||
- cd: Directory to change to before executing.
|
||||
|
||||
Returns ProgramSpec for the started program.
|
||||
"""
|
||||
def exec(executable, args, opts \\ []) do
|
||||
name = Keyword.get(
|
||||
opts,
|
||||
:name,
|
||||
(executable |> Path.rootname() |> Path.basename())
|
||||
)
|
||||
|
||||
options = [
|
||||
:exit_status, # Send msg with status when command stops
|
||||
args: [Path.join(__DIR__, "apprunner.exs") | [executable | args]],
|
||||
line: 1024 # Send command output as lines of 1k length
|
||||
]
|
||||
|
||||
options = case Keyword.get(opts, :cd) do
|
||||
nil -> options
|
||||
cd -> Keyword.put(options, :cd, cd)
|
||||
end
|
||||
|
||||
Logger.debug("[Spawned] Program #{name}")
|
||||
|
||||
%ProgramSpec{
|
||||
name: name,
|
||||
pending_output: "",
|
||||
port: Port.open(
|
||||
{:spawn_executable, @elixir},
|
||||
options
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Start watching a path. Name is used for prefixing logs.
|
||||
|
||||
Path can point to a file or a directory in which case all subdirs will be watched.
|
||||
|
||||
Returns a WatchSpec.
|
||||
"""
|
||||
def watch(name, path, fun) do
|
||||
name_atom = String.to_atom(name)
|
||||
{:ok, pid} = :fs.start_link(name_atom, String.to_charlist(path))
|
||||
:fs.subscribe(name_atom)
|
||||
|
||||
Logger.debug("[Spawned] Watch #{name}")
|
||||
|
||||
%WatchSpec{
|
||||
name: name,
|
||||
path: path,
|
||||
callback: fun,
|
||||
pid: pid,
|
||||
name_atom: name_atom
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Listen to messages from specs and print them to the screen.
|
||||
|
||||
If watch: true is given in the options, will listen for user's enter key and
|
||||
kill programs if enter is pressed.
|
||||
"""
|
||||
def listen(specs, opts \\ [])
|
||||
|
||||
# If there are no specs, stop running
|
||||
def listen([], _), do: :ok
|
||||
|
||||
def listen(spec, opts) when not is_list(spec) do
|
||||
listen([spec], opts)
|
||||
end
|
||||
|
||||
def listen(specs, opts) when is_list(specs) do
|
||||
# Start another task to ask for user input if we are in watch mode
|
||||
task = with \
|
||||
true <- Keyword.get(opts, :watch, false),
|
||||
nil <- Keyword.get(opts, :task),
|
||||
{:ok, task} <- Task.start_link(__MODULE__, :wait_for_input, [self()])
|
||||
do
|
||||
Logger.info("Programs/watches started, press ENTER to exit.")
|
||||
task
|
||||
end
|
||||
|
||||
specs = receive do
|
||||
# User pressed enter
|
||||
:user_input_received ->
|
||||
Logger.info("ENTER received, killing tasks.")
|
||||
Enum.each(specs, &kill/1)
|
||||
[]
|
||||
|
||||
# Program sent output with end of line
|
||||
{port, {:data, {:eol, msg}}} ->
|
||||
program = Enum.find(specs, program_checker(port))
|
||||
msg = :unicode.characters_to_binary(msg, :unicode)
|
||||
|
||||
prefix = "[#{program.name}] #{program.pending_output}"
|
||||
|
||||
Logger.debug(prefix <> msg)
|
||||
|
||||
specs
|
||||
|> Enum.reject(program_checker(port))
|
||||
|> Enum.concat([
|
||||
%{program | pending_output: ""}
|
||||
])
|
||||
|
||||
# Program sent output without end of line
|
||||
{port, {:data, {:noeol, msg}}} ->
|
||||
program = Enum.find(specs, program_checker(port))
|
||||
msg = :unicode.characters_to_binary(msg, :unicode)
|
||||
|
||||
specs
|
||||
|> Enum.reject(program_checker(port))
|
||||
|> Enum.concat([
|
||||
%{program | pending_output: "#{program.pending_output}#{msg}"}
|
||||
])
|
||||
|
||||
# Port was closed normally after being told to close
|
||||
{port, :closed} ->
|
||||
handle_closed(specs, port)
|
||||
|
||||
# Port closed because the program closed by itself
|
||||
{port, {:exit_status, 0}} ->
|
||||
handle_closed(specs, port)
|
||||
|
||||
# Program closed with error status
|
||||
{port, {:exit_status, status}} ->
|
||||
program = Enum.find(specs, program_checker(port))
|
||||
Logger.error("Program #{program.name} returned status #{status}.")
|
||||
raise "Failed status #{status} from #{program.name}!"
|
||||
|
||||
# Port crashed
|
||||
{:EXIT, port, _} ->
|
||||
handle_closed(specs, port)
|
||||
|
||||
# FS watch sent file event
|
||||
{_, {:fs, :file_event}, {file, events}} ->
|
||||
handle_events(specs, file, events)
|
||||
end
|
||||
|
||||
listen(specs, Keyword.put(opts, :task, task))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Kill a running program returned by exec().
|
||||
"""
|
||||
def kill(%ProgramSpec{name: name, port: port}) do
|
||||
if name != nil do
|
||||
Logger.debug("[Killing] #{name}")
|
||||
end
|
||||
|
||||
send(port, {self(), :close})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Kill a running watch.
|
||||
"""
|
||||
def kill(%WatchSpec{name: name, pid: pid}) do
|
||||
Logger.debug("[Killing] #{name}")
|
||||
Process.exit(pid, :kill)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Print file size of given file in human readable form.
|
||||
|
||||
If old file is given as second argument, print the old file's size
|
||||
and the diff also.
|
||||
"""
|
||||
def print_size(new_file, old_file \\ nil) do
|
||||
new_size = get_size(new_file)
|
||||
|
||||
{prefix, postfix} = if old_file != nil do
|
||||
old_size = get_size(old_file)
|
||||
|
||||
{
|
||||
"#{human_size(old_size)} -> ",
|
||||
" Diff: #{human_size(old_size - new_size)}"
|
||||
}
|
||||
else
|
||||
{"", ""}
|
||||
end
|
||||
|
||||
Logger.debug("#{Path.basename(new_file)}: #{prefix}#{human_size(new_size)}.#{postfix}")
|
||||
end
|
||||
|
||||
def wait_for_input(target) do
|
||||
IO.gets("")
|
||||
send(target, :user_input_received)
|
||||
end
|
||||
|
||||
defp get_size(file) do
|
||||
%File.Stat{size: size} = File.stat!(file)
|
||||
size
|
||||
end
|
||||
|
||||
defp human_size(size) do
|
||||
size_units = ["B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"] # You never know
|
||||
|
||||
human_size(size, size_units)
|
||||
end
|
||||
|
||||
defp human_size(size, [unit | []]), do: size_with_unit(size, unit)
|
||||
|
||||
defp human_size(size, [unit | rest]) do
|
||||
if size > 1024 do
|
||||
human_size(size / 1024, rest)
|
||||
else
|
||||
size_with_unit(size, unit)
|
||||
end
|
||||
end
|
||||
|
||||
defp size_with_unit(size, unit) when is_float(size), do: "#{Float.round(size, 2)} #{unit}"
|
||||
|
||||
defp size_with_unit(size, unit), do: "#{size} #{unit}"
|
||||
|
||||
# Utility to find program in spec list based on port
|
||||
defp program_checker(port) do
|
||||
fn
|
||||
%ProgramSpec{port: program_port} ->
|
||||
program_port == port
|
||||
|
||||
%WatchSpec{} ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
# Utility to find watch in spec list based on path
|
||||
defp watch_checker(path) do
|
||||
fn
|
||||
%ProgramSpec{} ->
|
||||
false
|
||||
|
||||
%WatchSpec{path: watch_path} ->
|
||||
# If given path is relative to (under) the watch path or is the same
|
||||
# path completely, it's a match.
|
||||
path != watch_path and Path.relative_to(path, watch_path) != path
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_closed(specs, port) do
|
||||
case Enum.find(specs, program_checker(port)) do
|
||||
%ProgramSpec{} = program ->
|
||||
Logger.debug("[Stopped] #{program.name}")
|
||||
|
||||
specs
|
||||
|> Enum.reject(program_checker(port))
|
||||
|
||||
nil ->
|
||||
# Program was already removed
|
||||
specs
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_events(specs, file, events) do
|
||||
file = to_string(file)
|
||||
case Enum.find(specs, watch_checker(file)) do
|
||||
%WatchSpec{name: name, callback: callback} ->
|
||||
Logger.debug("[#{name}] Changed #{inspect(events)}: #{file}")
|
||||
|
||||
callback.(file, events)
|
||||
|
||||
nil ->
|
||||
# Watch was maybe removed for some reason
|
||||
Logger.debug("[Error] Watch sent event but path was not in specs list: #{inspect(events)} #{file}")
|
||||
end
|
||||
|
||||
specs
|
||||
end
|
||||
end
|
36
mix.exs
Normal file
36
mix.exs
Normal file
|
@ -0,0 +1,36 @@
|
|||
defmodule FBU.Mixfile do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[app: :fbu,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.4",
|
||||
build_embedded: Mix.env == :prod,
|
||||
start_permanent: Mix.env == :prod,
|
||||
deps: deps()]
|
||||
end
|
||||
|
||||
# Configuration for the OTP application
|
||||
#
|
||||
# Type "mix help compile.app" for more information
|
||||
def application do
|
||||
# Specify extra applications you'll use from Erlang/Elixir
|
||||
[extra_applications: [:logger]]
|
||||
end
|
||||
|
||||
# Dependencies can be Hex packages:
|
||||
#
|
||||
# {:my_dep, "~> 0.3.0"}
|
||||
#
|
||||
# Or git/path repositories:
|
||||
#
|
||||
# {:my_dep, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
|
||||
#
|
||||
# Type "mix help deps" for more examples and options
|
||||
defp deps do
|
||||
[
|
||||
{:ex_doc, "~> 0.15.0", only: :dev},
|
||||
{:fs, "~> 2.12.0"}
|
||||
]
|
||||
end
|
||||
end
|
3
mix.lock
Normal file
3
mix.lock
Normal file
|
@ -0,0 +1,3 @@
|
|||
%{"earmark": {:hex, :earmark, "1.2.0", "bf1ce17aea43ab62f6943b97bd6e3dc032ce45d4f787504e3adf738e54b42f3a", [:mix], []},
|
||||
"ex_doc": {:hex, :ex_doc, "0.15.0", "e73333785eef3488cf9144a6e847d3d647e67d02bd6fdac500687854dd5c599f", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, optional: false]}]},
|
||||
"fs": {:hex, :fs, "2.12.0", "ad631efacc9a5683c8eaa1b274e24fa64a1b8eb30747e9595b93bec7e492e25e", [:rebar3], []}}
|
77
test/fbu_test.exs
Normal file
77
test/fbu_test.exs
Normal file
|
@ -0,0 +1,77 @@
|
|||
defmodule FBUTest do
|
||||
use ExUnit.Case
|
||||
|
||||
# Dependency functions to call in tasks
|
||||
defmodule Depfuns do
|
||||
def foo(), do: send(Process.whereis(:deps_test_runner), :foo)
|
||||
def bar(), do: send(Process.whereis(:deps_test_runner), :bar)
|
||||
def foo_no(), do: send(Process.whereis(:no_deps_test_runner), :foo)
|
||||
def bar_no(), do: send(Process.whereis(:no_deps_test_runner), :bar)
|
||||
end
|
||||
|
||||
test "that a module using the task macro has a run function that takes one argument" do
|
||||
defmodule Mix.Task.Foo1 do
|
||||
use FBU.BuildTask
|
||||
|
||||
task args do
|
||||
assert args == [1]
|
||||
end
|
||||
end
|
||||
|
||||
assert Mix.Task.Foo1.run([1]) == :ok
|
||||
end
|
||||
|
||||
test "that the macro works when given _ to denote ignored args" do
|
||||
defmodule Mix.Task.Foo2 do
|
||||
use FBU.BuildTask
|
||||
|
||||
task _ do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
assert Mix.Task.Foo2.run([]) == :ok
|
||||
end
|
||||
|
||||
test "that deps are run before the task" do
|
||||
defmodule Mix.Task.Foo3 do
|
||||
use FBU.BuildTask
|
||||
|
||||
@deps [
|
||||
&Depfuns.foo/0,
|
||||
&Depfuns.bar/0
|
||||
]
|
||||
|
||||
task _ do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
Process.register(self(), :deps_test_runner)
|
||||
|
||||
Mix.Task.Foo3.run([])
|
||||
assert_receive :foo
|
||||
assert_receive :bar
|
||||
end
|
||||
|
||||
test "that deps are not run if deps: false is given" do
|
||||
defmodule Mix.Task.Foo4 do
|
||||
use FBU.BuildTask
|
||||
|
||||
@deps [
|
||||
&Depfuns.foo_no/0,
|
||||
&Depfuns.bar_no/0
|
||||
]
|
||||
|
||||
task _ do
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
Process.register(self(), :no_deps_test_runner)
|
||||
|
||||
Mix.Task.Foo4.run([1, 2, 3, 4, 5, deps: false])
|
||||
refute_receive :foo
|
||||
refute_receive :bar
|
||||
end
|
||||
end
|
1
test/test_helper.exs
Normal file
1
test/test_helper.exs
Normal file
|
@ -0,0 +1 @@
|
|||
ExUnit.start()
|
Loading…
Reference in a new issue