···22 use GenServer
3344 alias Hobbes.Construct.SimLog
55- alias Hobbes.Construct.Scheduler.{ProcStore, ProcQueue, ProcRegistry}
55+ alias Hobbes.Construct.Scheduler.{ProcStore, ProcQueue, ProcRegistry, FileStore}
66 alias Hobbes.Construct.Scheduler.ProcStore.ProcState
7788 defmodule Spawn do
···3737 proc_store: ProcStore.t,
3838 proc_queue: ProcQueue.t,
3939 proc_registry: ProcRegistry.t,
4040+ file_stores: %{atom => FileStore.t},
4041 log_server_pid: pid,
4142 resumes_without_send: non_neg_integer,
4243 }
···4748 proc_store: nil,
4849 proc_queue: nil,
4950 proc_registry: nil,
5151+ file_stores: %{},
50525153 log_server_pid: nil,
5254 resumes_without_send: 0,
···131133 GenServer.call(scheduler, {:register_process, self(), pid, name})
132134 end
133135136136+ @spec get_file_store(pid) :: FileStore.t
137137+ def get_file_store(scheduler) do
138138+ GenServer.call(scheduler, {:get_file_store, self()})
139139+ end
140140+134141 @spec current_time(term, pid) :: non_neg_integer
135142 def current_time(scheduler, for_pid \\ self()) when is_pid(for_pid) do
136143 GenServer.call(scheduler, {:current_time, for_pid})
···161168 proc_store: ProcStore.new(),
162169 proc_queue: ProcQueue.new(),
163170 proc_registry: ProcRegistry.new(),
171171+ # TODO: spawn per node instead
172172+ file_stores: %{nonode: FileStore.new()},
164173 }
165174 ProcStore.add_process(state.proc_store, initial_pid)
166175···184193 {:error, _error} ->
185194 {:reply, :error, state}
186195 end
196196+ end
197197+198198+ def handle_call({:get_file_store, pid}, _from, %State{} = state) when is_pid(pid) do
199199+ # TODO: node for pid
200200+ node = :nonode
201201+202202+ file_store = Map.fetch!(state.file_stores, node)
203203+ {:reply, file_store, state}
187204 end
188205189206 def handle_call({:current_time, for_pid}, _from, %State{} = state) when is_pid(for_pid) do
+111
lib/construct/scheduler/file_store.ex
···11+defmodule Hobbes.Construct.Scheduler.FileStore do
22+ alias Hobbes.KV.FlatKV
33+44+ @type t :: FlatKV.t
55+ @type posix :: :enoent | :eexist | :enotdir | :eisdir
66+77+ @spec new :: t
88+ def new do
99+ kv = FlatKV.new(public: true)
1010+ FlatKV.put(kv, "/", "directory")
1111+1212+ kv
1313+ end
1414+1515+ @spec mkdir(t, binary) :: :ok | {:error, posix}
1616+ def mkdir(kv, path) do
1717+ path = normalize_directory(path)
1818+1919+ case all_components_dir?(kv, path) do
2020+ true ->
2121+ case exists?(kv, path) do
2222+ false ->
2323+ FlatKV.put(kv, path, "directory")
2424+ :ok
2525+2626+ true -> {:error, :eexist}
2727+ end
2828+2929+ false -> {:error, :enoent}
3030+ end
3131+ end
3232+3333+ @spec mkdir_p(t, binary) :: :ok | {:error, posix}
3434+ def mkdir_p(kv, path) do
3535+ path
3636+ |> components()
3737+ |> Enum.reduce_while(:ok, fn p, :ok ->
3838+ case exists?(kv, p) do
3939+ false ->
4040+ :ok = mkdir(kv, p)
4141+ {:cont, :ok}
4242+4343+ true ->
4444+ case dir?(kv, p) do
4545+ true -> {:cont, :ok}
4646+ false -> {:halt, {:error, :enotdir}}
4747+ end
4848+ end
4949+ end)
5050+ end
5151+5252+ @spec write(t, binary, binary) :: :ok | {:error, posix}
5353+ def write(kv, path, contents) do
5454+ case all_components_dir?(kv, path) do
5555+ true ->
5656+ FlatKV.put(kv, path, "file:" <> contents)
5757+ :ok
5858+ false ->
5959+ {:error, :enoent}
6060+ end
6161+ end
6262+6363+ @spec read(t, binary) :: {:ok, binary} | {:error, posix}
6464+ def read(kv, path) do
6565+ case FlatKV.get(kv, path) do
6666+ "file:" <> contents ->
6767+ {:ok, contents}
6868+6969+ "directory" ->
7070+ {:error, :eisdir}
7171+7272+ nil ->
7373+ {:error, :enoent}
7474+ end
7575+ end
7676+7777+ @spec dir?(t, binary) :: boolean
7878+ def dir?(kv, path) do
7979+ path = normalize_directory(path)
8080+ FlatKV.get(kv, path) == "directory"
8181+ end
8282+8383+ @spec exists?(t, binary) :: boolean
8484+ def exists?(kv, path) do
8585+ FlatKV.get(kv, path) != nil
8686+ end
8787+8888+ defp components(path) do
8989+ split = Path.split(path)
9090+9191+ Enum.map(1..length(split), fn count ->
9292+ Enum.take(split, count) |> Path.join()
9393+ end)
9494+ end
9595+9696+ defp all_components_dir?(kv, path) do
9797+ path
9898+ |> components()
9999+ |> case do
100100+ [] -> []
101101+ list -> List.delete_at(list, -1)
102102+ end
103103+ |> Enum.all?(&dir?(kv, &1))
104104+ end
105105+106106+ defp normalize_directory("/"), do: "/"
107107+ defp normalize_directory(path), do: String.trim_trailing(path, "/")
108108+109109+ @doc false
110110+ def dump(kv), do: FlatKV.dump(kv)
111111+end
+42
lib/construct/sim_file.ex
···11+defmodule Hobbes.Construct.SimFile do
22+ alias Hobbes.Construct.Scheduler
33+ alias Hobbes.Construct.Scheduler.FileStore
44+55+ import Hobbes.Construct.SimServer, only: [get_scheduler_pid: 0]
66+77+ @spec mkdir(binary) :: :ok | {:error, File.posix}
88+ def mkdir(path) when is_binary(path) do
99+ case get_scheduler_pid() do
1010+ spid when is_pid(spid) ->
1111+ fs = Scheduler.get_file_store(spid)
1212+ FileStore.mkdir(fs, path)
1313+ end
1414+ end
1515+1616+ @spec mkdir_p(binary) :: :ok | {:error, File.posix}
1717+ def mkdir_p(path) when is_binary(path) do
1818+ case get_scheduler_pid() do
1919+ spid when is_pid(spid) ->
2020+ fs = Scheduler.get_file_store(spid)
2121+ FileStore.mkdir_p(fs, path)
2222+ end
2323+ end
2424+2525+ @spec write(binary, binary) :: :ok | {:error, File.posix}
2626+ def write(path, contents) when is_binary(path) and is_binary(contents) do
2727+ case get_scheduler_pid() do
2828+ spid when is_pid(spid) ->
2929+ fs = Scheduler.get_file_store(spid)
3030+ FileStore.write(fs, path, contents)
3131+ end
3232+ end
3333+3434+ @spec read(binary) :: {:ok, binary} | {:error, File.posix}
3535+ def read(path) when is_binary(path) do
3636+ case get_scheduler_pid() do
3737+ spid when is_pid(spid) ->
3838+ fs = Scheduler.get_file_store(spid)
3939+ FileStore.read(fs, path)
4040+ end
4141+ end
4242+end
+5-2
lib/kv/flat_kv.ex
···5566 alias Hobbes.Structs.RangeResult
7788- def new do
99- :ets.new(:flat_kv, [:ordered_set, :private])
88+ @type t :: :ets.table
99+1010+ @spec new :: t
1111+ def new(opts \\ []) do
1212+ :ets.new(:flat_kv, [:ordered_set, if(opts[:public], do: :public, else: :private)])
1013 end
11141215 @spec load(:ets.table, [{binary, binary}]) :: :ok
+84
test/construct/scheduler/file_store_test.exs
···11+defmodule Hobbes.Construct.Scheduler.FileStoreTest do
22+ use ExUnit.Case, async: true
33+44+ alias Hobbes.Construct.Scheduler.FileStore
55+66+ @moduletag :file_store
77+88+ setup do
99+ %{fs: FileStore.new()}
1010+ end
1111+1212+ describe "mkdir/2" do
1313+ test "returns error if file in path", %{fs: fs} do
1414+ :ok = FileStore.mkdir(fs, "/foo")
1515+ :ok = FileStore.write(fs, "/foo/bar", "hello")
1616+1717+ assert {:error, :eexist} = FileStore.mkdir(fs, "/foo/bar")
1818+ assert {:error, :enoent} = FileStore.mkdir(fs, "/foo/bar/baz")
1919+ end
2020+2121+ test "returns error if directory exists", %{fs: fs} do
2222+ :ok = FileStore.mkdir(fs, "/foo")
2323+ assert {:error, :eexist} = FileStore.mkdir(fs, "/foo")
2424+ end
2525+ end
2626+2727+ describe "mkdir_p/2" do
2828+ test "returns error if file in path", %{fs: fs} do
2929+ assert :ok = FileStore.mkdir(fs, "/foo")
3030+ assert :ok = FileStore.write(fs, "/foo/file", "hello world")
3131+3232+ assert {:error, :enotdir} = FileStore.mkdir_p(fs, "/foo/file/bar")
3333+ end
3434+3535+ test "makes directories", %{fs: fs} do
3636+ assert :ok = FileStore.mkdir_p(fs, "/foo/bar/baz")
3737+ assert :ok = FileStore.mkdir_p(fs, "/foo/bar/baz/qux")
3838+3939+ assert FileStore.dir?(fs, "/")
4040+ assert FileStore.dir?(fs, "/foo")
4141+ assert FileStore.dir?(fs, "/foo/bar")
4242+ assert FileStore.dir?(fs, "/foo/bar/baz")
4343+ assert FileStore.dir?(fs, "/foo/bar/baz/qux")
4444+ assert FileStore.dir?(fs, "/foo/bar/baz/qux/")
4545+ end
4646+ end
4747+4848+ describe "write/3" do
4949+ test "returns error if no directory", %{fs: fs} do
5050+ assert {:error, :enoent} = FileStore.write(fs, "/foo/bar.txt", "hello")
5151+ end
5252+5353+ test "returns error if file in path", %{fs: fs} do
5454+ :ok = FileStore.write(fs, "/foo", "hello")
5555+ assert {:error, :enoent} = FileStore.write(fs, "/foo/bar.txt", "hello")
5656+ end
5757+5858+ test "writes file", %{fs: fs} do
5959+ :ok = FileStore.mkdir_p(fs, "/foo/bar")
6060+6161+ assert :ok = FileStore.write(fs, "/foo/bar/baz.txt", "hello world!")
6262+6363+ assert {:ok, "hello world!"} = FileStore.read(fs, "/foo/bar/baz.txt")
6464+ end
6565+ end
6666+6767+ describe "read/2" do
6868+ test "returns error if file does not exist", %{fs: fs} do
6969+ assert {:error, :enoent} = FileStore.read(fs, "/foo/bar.txt")
7070+ end
7171+7272+ test "returns error if file is a directory", %{fs: fs} do
7373+ :ok = FileStore.mkdir_p(fs, "/foo/bar")
7474+ assert {:error, :eisdir} = FileStore.read(fs, "/foo/bar")
7575+ end
7676+7777+ test "reads file", %{fs: fs} do
7878+ :ok = FileStore.mkdir(fs, "/foo")
7979+ :ok = FileStore.write(fs, "/foo/bar.txt", "hello world!")
8080+8181+ assert {:ok, "hello world!"} = FileStore.read(fs, "/foo/bar.txt")
8282+ end
8383+ end
8484+end
+14-1
test/construct_test.exs
···11defmodule Hobbes.ConstructTest do
22 use ExUnit.Case, async: false
33- alias Hobbes.Construct.SimServer
33+44+ alias Hobbes.Construct.{SimServer, SimFile}
4556 @moduletag :construct
67···50515152 assert_receive {:EXIT, ^pid, {%RuntimeError{message: "Some error"}, _}}
5253 Process.sleep(1)
5454+ end
5555+ end
5656+5757+ describe "SimFile" do
5858+ test "reads and writes files" do
5959+ SimServer.start_scheduler()
6060+6161+ assert :ok = SimFile.mkdir_p("/foo/bar/baz")
6262+ assert :ok = SimFile.write("/foo/bar/baz/qux.txt", "hello world!")
6363+6464+ assert {:ok, "hello world!"} = SimFile.read("/foo/bar/baz/qux.txt")
6565+ assert {:error, :enoent} = SimFile.read("/foo/qux.txt")
5366 end
5467 end
5568end