Merge pull request #193 from marosluuce/convert-to-application
Convert to application
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
* kamidev
|
||||
* Mahmut Surekci
|
||||
* Makis Otman
|
||||
* Nathan Walker
|
||||
* Rabea Gleissner
|
||||
* Ria Cataquian
|
||||
* Simone D'Amico
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
defmodule Display do
|
||||
use GenServer
|
||||
|
||||
alias IO.ANSI
|
||||
alias Display.ProgressBar
|
||||
alias Display.Intro
|
||||
alias Display.Failure
|
||||
alias Display.Notifications
|
||||
alias Display.{ProgressBar, Intro, Failure, Notifications}
|
||||
|
||||
def start_link do
|
||||
GenServer.start_link(__MODULE__, %{clear_screen: true}, name: __MODULE__)
|
||||
end
|
||||
|
||||
def disable_clear do
|
||||
GenServer.cast(__MODULE__, :disable_clear)
|
||||
end
|
||||
|
||||
def handle_cast(:disable_clear, state) do
|
||||
{:noreply, %{state | clear_screen: false}}
|
||||
end
|
||||
|
||||
def handle_cast(:clear_screen, state = %{clear_screen: true}) do
|
||||
IO.puts(ANSI.clear)
|
||||
IO.puts(ANSI.home)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
def handle_cast(:clear_screen, state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def invalid_koan(koan, modules) do
|
||||
Notifications.invalid_koan(koan, modules)
|
||||
@@ -15,17 +36,6 @@ defmodule Display do
|
||||
|> IO.puts
|
||||
end
|
||||
|
||||
def format(failure, module, name) do
|
||||
"""
|
||||
#{Intro.intro(module, Tracker.visited)}
|
||||
Now meditate upon #{format_module(module)}
|
||||
#{ProgressBar.progress_bar(Tracker.summarize)}
|
||||
----------------------------------------
|
||||
#{name}
|
||||
#{Failure.format_failure(failure)}
|
||||
"""
|
||||
end
|
||||
|
||||
def show_compile_error(error) do
|
||||
Failure.show_compile_error(error)
|
||||
|> IO.puts
|
||||
@@ -37,10 +47,18 @@ defmodule Display do
|
||||
end
|
||||
|
||||
def clear_screen do
|
||||
if Options.clear_screen? do
|
||||
IO.puts(ANSI.clear)
|
||||
IO.puts(ANSI.home)
|
||||
end
|
||||
GenServer.cast(__MODULE__, :clear_screen)
|
||||
end
|
||||
|
||||
defp format(failure, module, name) do
|
||||
"""
|
||||
#{Intro.intro(module, Tracker.visited)}
|
||||
Now meditate upon #{format_module(module)}
|
||||
#{ProgressBar.progress_bar(Tracker.summarize)}
|
||||
----------------------------------------
|
||||
#{name}
|
||||
#{Failure.format_failure(failure)}
|
||||
"""
|
||||
end
|
||||
|
||||
defp format_module(module) do
|
||||
|
||||
17
lib/elixir_koans.ex
Normal file
17
lib/elixir_koans.ex
Normal file
@@ -0,0 +1,17 @@
|
||||
defmodule ElixirKoans do
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
import Supervisor.Spec
|
||||
|
||||
children = [
|
||||
worker(Display, []),
|
||||
worker(Tracker, []),
|
||||
worker(Runner, []),
|
||||
worker(Watcher, [])
|
||||
]
|
||||
|
||||
opts = [strategy: :one_for_one, name: ElixirKoans.Supervisor]
|
||||
Supervisor.start_link(children, opts)
|
||||
end
|
||||
end
|
||||
@@ -1,30 +1,37 @@
|
||||
defmodule Mix.Tasks.Meditate do
|
||||
use Mix.Task
|
||||
alias Options
|
||||
|
||||
@shortdoc "Start the koans"
|
||||
|
||||
def run(args) do
|
||||
Application.ensure_all_started(:elixir_koans)
|
||||
Code.compiler_options(ignore_module_conflict: true)
|
||||
{:ok, watcher} = Watcher.start
|
||||
Process.monitor(watcher)
|
||||
|
||||
Options.start(args)
|
||||
{parsed, _, _} = OptionParser.parse(args)
|
||||
|
||||
Options.initial_koan
|
||||
modules = parsed
|
||||
|> initial_module
|
||||
|> ok?
|
||||
|> Runner.modules_to_run
|
||||
|> Tracker.start
|
||||
|> Runner.run
|
||||
|
||||
if Tracker.complete? do
|
||||
Display.congratulate
|
||||
exit(:normal)
|
||||
end
|
||||
Tracker.set_total(modules)
|
||||
Tracker.notify_on_complete(self())
|
||||
|
||||
receive do
|
||||
{:DOWN, _references, :process, ^watcher, _reason} -> nil
|
||||
set_clear_screen(parsed)
|
||||
Runner.run(modules)
|
||||
|
||||
Tracker.wait_until_complete()
|
||||
Display.congratulate()
|
||||
end
|
||||
|
||||
defp initial_module(parsed) do
|
||||
name = Keyword.get(parsed, :koan, "Equalities")
|
||||
String.to_atom("Elixir."<> name)
|
||||
end
|
||||
|
||||
defp set_clear_screen(parsed) do
|
||||
if Keyword.has_key?(parsed, :no_clear_screen) do
|
||||
Display.disable_clear()
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
defmodule Options do
|
||||
@defaults %{
|
||||
clear_screen: true,
|
||||
initial_koan: Equalities,
|
||||
}
|
||||
|
||||
def start(args) do
|
||||
Agent.start_link(fn -> parse(args) end, name: __MODULE__)
|
||||
end
|
||||
|
||||
def clear_screen? do
|
||||
Agent.get(__MODULE__, fn(options) ->
|
||||
Map.fetch!(options, :clear_screen)
|
||||
end)
|
||||
end
|
||||
|
||||
def initial_koan() do
|
||||
Agent.get(__MODULE__, fn(options) ->
|
||||
Map.fetch!(options, :initial_koan)
|
||||
end)
|
||||
end
|
||||
|
||||
defp parse(args) do
|
||||
Enum.reduce(args, @defaults, fn(arg, acc) ->
|
||||
Map.merge(acc, parse_argument(arg))
|
||||
end)
|
||||
end
|
||||
|
||||
def parse_argument("--koan="<>module), do: %{ initial_koan: String.to_atom("Elixir."<> module)}
|
||||
def parse_argument("--no-clear-screen"), do: %{ clear_screen: false}
|
||||
def parse_argument(_), do: %{}
|
||||
end
|
||||
@@ -1,4 +1,5 @@
|
||||
defmodule Runner do
|
||||
use GenServer
|
||||
|
||||
def koan?(koan) do
|
||||
Keyword.has_key?(koan.__info__(:exports), :all_koans)
|
||||
@@ -24,17 +25,40 @@ defmodule Runner do
|
||||
String.to_integer(number)
|
||||
end
|
||||
|
||||
def modules_to_run, do: Options.initial_koan |> modules_to_run
|
||||
def modules_to_run(start_module), do: Enum.drop_while(modules(), &(&1 != start_module))
|
||||
|
||||
def run(modules) do
|
||||
Display.clear_screen()
|
||||
|
||||
modules
|
||||
|> Enum.take_while( &(run_module(&1) == :passed))
|
||||
def start_link do
|
||||
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
||||
end
|
||||
|
||||
def run_module(module) do
|
||||
def handle_cast({:run, modules}, _) do
|
||||
flush()
|
||||
send(self(), :run_modules)
|
||||
{:noreply, modules}
|
||||
end
|
||||
|
||||
def handle_info(:run_modules, []) do
|
||||
{:noreply, []}
|
||||
end
|
||||
|
||||
def handle_info(:run_modules, [module | rest]) do
|
||||
Display.clear_screen()
|
||||
|
||||
case run_module(module) do
|
||||
:passed ->
|
||||
send(self(), :run_modules)
|
||||
{:noreply, rest}
|
||||
|
||||
_ ->
|
||||
{:noreply, []}
|
||||
end
|
||||
end
|
||||
|
||||
def run(modules) do
|
||||
GenServer.cast(__MODULE__, {:run, modules})
|
||||
end
|
||||
|
||||
defp run_module(module) do
|
||||
module
|
||||
|> Execute.run_module(&track/3)
|
||||
|> display
|
||||
@@ -49,4 +73,11 @@ defmodule Runner do
|
||||
end
|
||||
defp display(_), do: :passed
|
||||
|
||||
defp flush do
|
||||
receive do
|
||||
_ -> flush()
|
||||
after
|
||||
0 -> :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,22 +1,47 @@
|
||||
defmodule Tracker do
|
||||
def start(modules) do
|
||||
total = modules
|
||||
|> Enum.flat_map(&(&1.all_koans))
|
||||
|> Enum.count
|
||||
alias __MODULE__
|
||||
|
||||
Agent.start_link(fn -> %{total: total,
|
||||
koans: MapSet.new(),
|
||||
visited_modules: MapSet.new()} end, name: __MODULE__)
|
||||
modules
|
||||
defstruct total: 0,
|
||||
koans: MapSet.new(),
|
||||
visited_modules: MapSet.new(),
|
||||
on_complete: :noop
|
||||
|
||||
def start_link do
|
||||
Agent.start_link(fn -> %Tracker{} end, name: __MODULE__)
|
||||
end
|
||||
|
||||
defp get(), do: Agent.get(__MODULE__, &(&1))
|
||||
def notify_on_complete(pid) do
|
||||
Agent.update(__MODULE__, fn state -> %{state | on_complete: pid} end)
|
||||
end
|
||||
|
||||
def set_total(modules) do
|
||||
total = modules
|
||||
|> Enum.flat_map(&(&1.all_koans))
|
||||
|> Enum.count
|
||||
|
||||
Agent.update(__MODULE__, fn _ -> %Tracker{total: total} end)
|
||||
end
|
||||
|
||||
def completed(module, koan) do
|
||||
Agent.update(__MODULE__, fn(%{koans: completed, visited_modules: modules} = all) ->
|
||||
%{ all | koans: MapSet.put(completed, koan),
|
||||
visited_modules: MapSet.put(modules, module)}
|
||||
end)
|
||||
Agent.update(__MODULE__, &mark_koan_completed(&1, module, koan))
|
||||
if complete?() do
|
||||
Agent.cast(__MODULE__, fn state ->
|
||||
send(state.on_complete, {self(), :complete})
|
||||
state
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
def wait_until_complete() do
|
||||
pid = Process.whereis(Tracker)
|
||||
receive do
|
||||
{^pid, :complete} -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp mark_koan_completed(state, module, koan) do
|
||||
%{state | koans: MapSet.put(state.koans, koan),
|
||||
visited_modules: MapSet.put(state.visited_modules, module)}
|
||||
end
|
||||
|
||||
def visited do
|
||||
@@ -28,14 +53,13 @@ defmodule Tracker do
|
||||
total == completed
|
||||
end
|
||||
|
||||
def summarize, do: get() |> summarize()
|
||||
defp summarize(%{total: total,
|
||||
koans: completed,
|
||||
visited_modules: modules}) do
|
||||
def summarize do
|
||||
state = Agent.get(__MODULE__, &(&1))
|
||||
|
||||
%{
|
||||
total: total,
|
||||
current: MapSet.size(completed),
|
||||
visited_modules: MapSet.to_list(modules)
|
||||
total: state.total,
|
||||
current: MapSet.size(state.koans),
|
||||
visited_modules: MapSet.to_list(state.visited_modules)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
defmodule Watcher do
|
||||
use ExFSWatch, dirs: ["lib/koans"]
|
||||
|
||||
def start_link do
|
||||
start()
|
||||
end
|
||||
|
||||
def callback(file, events) do
|
||||
if Enum.member?(events, :modified) do
|
||||
file |> normalize |> reload
|
||||
|
||||
if Tracker.complete? do
|
||||
Display.congratulate
|
||||
exit(:normal)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
3
mix.exs
3
mix.exs
@@ -10,7 +10,8 @@ defmodule Koans.Mixfile do
|
||||
end
|
||||
|
||||
def application do
|
||||
[applications: [:exfswatch, :logger]]
|
||||
[mod: {ElixirKoans, []},
|
||||
applications: [:exfswatch, :logger]]
|
||||
end
|
||||
|
||||
defp deps do
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
defmodule OptionTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
test "has default options" do
|
||||
Options.start([])
|
||||
assert Options.clear_screen?
|
||||
end
|
||||
|
||||
test "override clearing of screen" do
|
||||
Options.start(["--no-clear-screen"])
|
||||
refute Options.clear_screen?
|
||||
end
|
||||
|
||||
test "can target specifc koans" do
|
||||
Options.start(["--koan=Strings"])
|
||||
assert Options.initial_koan() == Strings
|
||||
end
|
||||
|
||||
test "ignores unknown options" do
|
||||
Options.start(["--foo"])
|
||||
assert Options.clear_screen?
|
||||
end
|
||||
end
|
||||
@@ -4,25 +4,25 @@ defmodule TrackerTest do
|
||||
@sample_modules [SampleKoan, PassingKoan]
|
||||
|
||||
test "can start" do
|
||||
Tracker.start(@sample_modules)
|
||||
Tracker.set_total(@sample_modules)
|
||||
assert Tracker.summarize == %{total: 2, current: 0, visited_modules: []}
|
||||
end
|
||||
|
||||
test "can be notified of completed koans" do
|
||||
Tracker.start(@sample_modules)
|
||||
Tracker.set_total(@sample_modules)
|
||||
Tracker.completed(SampleKoan, :"Hi there")
|
||||
assert Tracker.summarize == %{total: 2, current: 1, visited_modules: [SampleKoan]}
|
||||
end
|
||||
|
||||
test "multiple comletions of the same koan count only once" do
|
||||
Tracker.start(@sample_modules)
|
||||
Tracker.set_total(@sample_modules)
|
||||
Tracker.completed(SampleKoan, :"Hi there")
|
||||
Tracker.completed(SampleKoan, :"Hi there")
|
||||
assert Tracker.summarize == %{total: 2, current: 1, visited_modules: [SampleKoan]}
|
||||
end
|
||||
|
||||
test "knows when koans are not complete" do
|
||||
Tracker.start(@sample_modules)
|
||||
Tracker.set_total(@sample_modules)
|
||||
refute Tracker.complete?
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user