farmbot_os/farmbot_celery_script/test/farmbot_celery_script/compiler_test.exs

486 lines
13 KiB
Elixir

defmodule FarmbotCeleryScript.CompilerTest do
use ExUnit.Case, async: true
alias FarmbotCeleryScript.{AST, Compiler}
# Only required to compile
alias FarmbotCeleryScript.SysCalls, warn: false
alias FarmbotCeleryScript.Compiler.IdentifierSanitizer
test "compiles a sequence with unbound variables" do
sequence = %AST{
kind: :sequence,
args: %{
locals: %AST{
kind: :scope_declaration,
args: %{},
body: [
%AST{
kind: :parameter_declaration,
args: %{
label: "provided_by_caller",
default_value: 100
}
}
]
}
},
body: [
%AST{kind: :identifier, args: %{label: "provided_by_caller"}}
]
}
[body_item] = Compiler.compile(sequence)
assert body_item.() == 100
# The compiler expects the `env` argument to be already sanatized.
# When supplying the env for this test, we need to make sure the
# `provided_by_caller` variable name is sanatized
sanatized_env = [
{IdentifierSanitizer.to_variable("provided_by_caller"), 900}
]
[body_item] = Compiler.compile(sequence, sanatized_env)
assert body_item.() == 900
celery_env = [
%AST{
kind: :parameter_application,
args: %{
label: "provided_by_caller",
data_value: 600
}
}
]
compiled_celery_env =
Compiler.Utils.compile_params_to_function_args(celery_env, [])
[body_item] = Compiler.compile(sequence, compiled_celery_env)
assert body_item.() == 600
end
test "compiles a sequence with no body" do
sequence = %AST{
args: %{
locals: %AST{
args: %{},
body: [],
comment: nil,
kind: :scope_declaration
},
version: 20_180_209
},
body: [],
comment: "This is the root",
kind: :sequence
}
body = Compiler.compile(sequence)
assert body == []
end
test "identifier sanitization" do
label = "System.cmd(\"echo\", [\"lol\"])"
value_ast = AST.Factory.new("coordinate", x: 1, y: 1, z: 1)
identifier_ast = AST.Factory.new("identifier", label: label)
parameter_application_ast =
AST.Factory.new("parameter_application",
label: label,
data_value: value_ast
)
celery_ast = %AST{
kind: :sequence,
args: %{
locals: %{
kind: :scope_declaration,
args: %{},
body: [
parameter_application_ast
]
}
},
body: [
identifier_ast
]
}
elixir_ast = Compiler.compile_ast(celery_ast, [])
elixir_code =
elixir_ast
|> Macro.to_string()
|> Code.format_string!()
|> IO.iodata_to_binary()
var_name = Compiler.IdentifierSanitizer.to_variable(label)
assert elixir_code =~
strip_nl("""
[
fn params ->
_ = inspect(params)
unsafe_U3lzdGVtLmNtZCgiZWNobyIsIFsibG9sIl0p = FarmbotCeleryScript.SysCalls.coordinate(1, 1, 1)
better_params = %{
"System.cmd(\\"echo\\", [\\"lol\\"])" => %FarmbotCeleryScript.AST{
args: %{x: 1, y: 1, z: 1},
body: [],
comment: nil,
kind: :coordinate,
meta: nil
}
}
[fn -> unsafe_U3lzdGVtLmNtZCgiZWNobyIsIFsibG9sIl0p end]
end
]
""")
refute String.contains?(elixir_code, label)
{[fun], _} = Code.eval_string(elixir_code, [], __ENV__)
assert is_function(fun, 1)
end
test "compiles execute" do
compiled =
compile(%AST{
kind: :execute,
args: %{sequence_id: 100},
body: []
})
assert compiled ==
strip_nl("""
case(FarmbotCeleryScript.SysCalls.get_sequence(100)) do
%FarmbotCeleryScript.AST{} = ast ->
env = []
FarmbotCeleryScript.Compiler.compile(ast, env)
error ->
error
end
""")
end
test "compiles execute_script" do
compiled =
compile(%AST{
kind: :execute_script,
args: %{label: "take-photo"},
body: [
%AST{kind: :pair, args: %{label: "a", value: "123"}}
]
})
assert compiled ==
strip_nl("""
package = "take-photo"
env = %{"a" => "123"}
FarmbotCeleryScript.SysCalls.log("Executing Farmware: \#{package}", true)
FarmbotCeleryScript.SysCalls.execute_script(package, env)
""")
end
test "compiles set_user_env" do
compiled =
compile(%AST{
kind: :set_user_env,
args: %{},
body: [
%AST{kind: :pair, args: %{label: "a", value: "123"}},
%AST{kind: :pair, args: %{label: "b", value: "345"}}
]
})
assert compiled ==
strip_nl("""
FarmbotCeleryScript.SysCalls.set_user_env("a", "123")
FarmbotCeleryScript.SysCalls.set_user_env("b", "345")
""")
end
test "install_first_party_farmware" do
compiled =
compile(%AST{
kind: :install_first_party_farmware,
args: %{},
body: []
})
assert compiled ==
strip_nl("""
FarmbotCeleryScript.SysCalls.log("Installing first party Farmware")
FarmbotCeleryScript.SysCalls.install_first_party_farmware()
""")
end
test "compiles nothing" do
compiled =
compile(%AST{
kind: :nothing,
args: %{},
body: []
})
assert compiled ==
strip_nl("""
FarmbotCeleryScript.SysCalls.nothing()
""")
end
test "compiles move_absolute no variables" do
compiled =
compile(%AST{
kind: :move_absolute,
args: %{
speed: 100,
location: %AST{
kind: :coordinate,
args: %{x: 100, y: 100, z: 100}
},
offset: %AST{
kind: :coordinate,
args: %{x: -20, y: -20, z: -20}
}
},
body: []
})
assert compiled ==
strip_nl("""
with(
%{x: locx, y: locy, z: locz} = FarmbotCeleryScript.SysCalls.coordinate(100, 100, 100),
%{x: offx, y: offy, z: offz} = FarmbotCeleryScript.SysCalls.coordinate(-20, -20, -20)
) do
[x, y, z] = [locx + offx, locy + offy, locz + offz]
x_str = FarmbotCeleryScript.FormatUtil.format_float(x)
y_str = FarmbotCeleryScript.FormatUtil.format_float(y)
z_str = FarmbotCeleryScript.FormatUtil.format_float(z)
FarmbotCeleryScript.SysCalls.log("Moving to (\#{x_str}, \#{y_str}, \#{z_str})", true)
FarmbotCeleryScript.SysCalls.move_absolute(x, y, z, 100)
end
""")
end
test "compiles move_relative" do
compiled =
compile(%AST{
kind: :move_relative,
args: %{
x: 100.4,
y: 90,
z: 50,
speed: 100
}
})
assert compiled ==
strip_nl("""
with(
locx when is_number(locx) <- 100.4,
locy when is_number(locy) <- 90,
locz when is_number(locz) <- 50,
curx when is_number(curx) <- FarmbotCeleryScript.SysCalls.get_current_x(),
cury when is_number(cury) <- FarmbotCeleryScript.SysCalls.get_current_y(),
curz when is_number(curz) <- FarmbotCeleryScript.SysCalls.get_current_z()
) do
x = locx + curx
y = locy + cury
z = locz + curz
x_str = FarmbotCeleryScript.FormatUtil.format_float(x)
y_str = FarmbotCeleryScript.FormatUtil.format_float(y)
z_str = FarmbotCeleryScript.FormatUtil.format_float(z)
FarmbotCeleryScript.SysCalls.log("Moving relative to (\#{x_str}, \#{y_str}, \#{z_str})", true)
FarmbotCeleryScript.SysCalls.move_absolute(x, y, z, 100)
end
""")
end
test "compiles write_pin" do
compiled =
compile(%AST{
kind: :write_pin,
args: %{pin_number: 17, pin_mode: 0, pin_value: 1}
})
assert compiled ==
strip_nl("""
pin = 17
mode = 0
value = 1
with(:ok <- FarmbotCeleryScript.SysCalls.write_pin(pin, mode, value)) do
FarmbotCeleryScript.SysCalls.read_pin(pin, mode)
end
""")
end
test "compiles read pin" do
compiled =
compile(%AST{
kind: :read_pin,
args: %{pin_number: 23, pin_mode: 0}
})
assert compiled ==
strip_nl("""
pin = 23
mode = 0
FarmbotCeleryScript.SysCalls.read_pin(pin, mode)
""")
end
test "compiles set_servo_angle" do
compiled =
compile(%AST{
kind: :set_servo_angle,
args: %{pin_number: 23, pin_value: 90}
})
assert compiled ==
strip_nl("""
pin = 23
angle = 90
FarmbotCeleryScript.SysCalls.log("Writing servo: \#{pin}: \#{angle}")
FarmbotCeleryScript.SysCalls.set_servo_angle(pin, angle)
""")
end
test "compiles set_pin_io_mode" do
compiled =
compile(%AST{
kind: :set_pin_io_mode,
args: %{pin_number: 23, pin_io_mode: "input"}
})
assert compiled ==
strip_nl("""
pin = 23
mode = "input"
FarmbotCeleryScript.SysCalls.log("Setting pin mode: \#{pin}: \#{mode}")
FarmbotCeleryScript.SysCalls.set_pin_io_mode(pin, mode)
""")
end
test "`update_resource`: " do
compiled =
"test/fixtures/mark_variable_removed.json"
|> File.read!()
|> Jason.decode!()
|> AST.decode()
|> compile()
assert compiled ==
strip_nl("""
[
fn params ->
_ = inspect(params)
unsafe_cGFyZW50 =
Keyword.get(params, :unsafe_cGFyZW50, FarmbotCeleryScript.SysCalls.coordinate(1, 2, 3))
better_params = %{}
[
fn ->
me = FarmbotCeleryScript.Compiler.UpdateResource
variable = %FarmbotCeleryScript.AST{
args: %{label: "parent"},
body: [],
comment: nil,
kind: :identifier,
meta: nil
}
update = %{"plant_stage" => "removed"}
case(variable) do
%AST{kind: :identifier} ->
args = Map.fetch!(variable, :args)
label = Map.fetch!(args, :label)
resource = Map.fetch!(better_params, label)
me.do_update(resource, update)
%AST{kind: :point} ->
me.do_update(variable.args(), update)
%AST{kind: :resource} ->
me.do_update(variable.args(), update)
res ->
raise("Resource error. Please notfiy support: \#{inspect(res)}")
end
end
]
end
]
""")
end
test "`update_resource`: Multiple fields of `resource` type." do
compiled =
"test/fixtures/update_resource_multi.json"
|> File.read!()
|> Jason.decode!()
|> AST.decode()
|> compile()
assert compiled ==
strip_nl("""
[
fn params ->
_ = inspect(params)
better_params = %{}
[
fn ->
me = FarmbotCeleryScript.Compiler.UpdateResource
variable = %FarmbotCeleryScript.AST{
args: %{resource_id: 23, resource_type: "Plant"},
body: [],
comment: nil,
kind: :resource,
meta: nil
}
update = %{"plant_stage" => "planted", "r" => 23}
case(variable) do
%AST{kind: :identifier} ->
args = Map.fetch!(variable, :args)
label = Map.fetch!(args, :label)
resource = Map.fetch!(better_params, label)
me.do_update(resource, update)
%AST{kind: :point} ->
me.do_update(variable.args(), update)
%AST{kind: :resource} ->
me.do_update(variable.args(), update)
res ->
raise("Resource error. Please notfiy support: \#{inspect(res)}")
end
end
]
end
]
""")
end
defp compile(ast) do
ast
|> Compiler.compile_ast([])
|> Macro.to_string()
|> Code.format_string!()
|> IO.iodata_to_binary()
end
defp strip_nl(text) do
String.trim_trailing(text, "\n")
end
end