commit
694bcddce3
|
@ -21,8 +21,8 @@ module Api
|
|||
|
||||
def update
|
||||
mutate Sequences::Update.run(sequence_params, # params[:sequence].as_json,
|
||||
device: current_device,
|
||||
sequence: sequence)
|
||||
device: current_device,
|
||||
sequence: sequence)
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
|
|
@ -9,6 +9,8 @@ module CeleryScript
|
|||
MALFORMED = "Expected '%s' to be a node or leaf, but it was neither"
|
||||
BAD_BODY = "Body of '%s' node contains '%s' node. "\
|
||||
"Expected one of: %s"
|
||||
UNBOUND_VAR = "Unbound variable: %s"
|
||||
T_MISMATCH = "Type mismatch. %s must be one of: %s. Got: %s"
|
||||
|
||||
attr_reader :tree, :corpus
|
||||
|
||||
|
@ -37,9 +39,20 @@ module CeleryScript
|
|||
e
|
||||
end
|
||||
|
||||
def check_leaf(node)
|
||||
allowed = corpus.values(node)
|
||||
actual = node.value.class
|
||||
ok_leaf = allowed.include?(actual)
|
||||
raise TypeCheckError, (BAD_LEAF % [ node.kind,
|
||||
node.parent.kind,
|
||||
allowed.inspect,
|
||||
actual.inspect]) unless ok_leaf
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate(node)
|
||||
p = node.try(:parent).try(:kind) || "root"
|
||||
validate_body(node)
|
||||
validate_node(node)
|
||||
end
|
||||
|
@ -76,30 +89,61 @@ module CeleryScript
|
|||
end
|
||||
end
|
||||
|
||||
def check_arg_validity(expectation, node)
|
||||
case node
|
||||
def check_arg_validity(key, value)
|
||||
case value
|
||||
when AstNode
|
||||
validate_node_pairing(key, value)
|
||||
when AstLeaf
|
||||
check_leaf(node)
|
||||
validate_leaf_pairing(key, value)
|
||||
check_leaf(value)
|
||||
else
|
||||
malformed_node!(expectation)
|
||||
malformed_node!(key)
|
||||
end
|
||||
run_additional_validations(node, expectation)
|
||||
run_additional_validations(value, key)
|
||||
end
|
||||
|
||||
def validate_node_pairing(key, value)
|
||||
actual = value.kind
|
||||
allowed = corpus.fetchArg(key).allowed_values.map(&:to_s)
|
||||
# It would be safe to run type checking here.
|
||||
if (actual == "identifier")
|
||||
allowed_types = allowed.without("identifier")
|
||||
# Resolve the identifier.
|
||||
# Someday, we might need to use the return value to perform more
|
||||
# in depth type checking. We're not there yet, though.
|
||||
# Currently we just need `resolve_variable!` to
|
||||
# catch unbound identifiers
|
||||
# data_type =
|
||||
resolve_variable!(value).args[:data_type].value
|
||||
# if !allowed_types.include?(data_type)
|
||||
# # Did it reolve?
|
||||
# # YES: Make sure it resolves to a `kind` from the list above.
|
||||
# value.invalidate!(T_MISMATCH % [value.args["label"].value,
|
||||
# allowed_types,
|
||||
# data_type])
|
||||
# end
|
||||
end
|
||||
ok = allowed.include?(actual)
|
||||
raise TypeCheckError, (BAD_LEAF % [ value.kind,
|
||||
value.parent.kind,
|
||||
allowed.inspect,
|
||||
actual.inspect ]) unless ok
|
||||
end
|
||||
|
||||
def validate_leaf_pairing(key, value)
|
||||
actual = value.value.class
|
||||
allowed = corpus.fetchArg(key).allowed_values
|
||||
ok = allowed.include?(actual)
|
||||
raise TypeCheckError, (BAD_LEAF % [ value.kind,
|
||||
value.parent.kind,
|
||||
allowed.inspect,
|
||||
actual.inspect]) unless ok
|
||||
end
|
||||
|
||||
def bad_body_kind(prnt, child, i, ok)
|
||||
raise TypeCheckError, (BAD_BODY % [prnt.kind, child.kind, ok.inspect])
|
||||
end
|
||||
|
||||
def check_leaf(node)
|
||||
allowed = corpus.values(node)
|
||||
actual = node.value.class
|
||||
unless allowed.include?(actual)
|
||||
raise TypeCheckError, (BAD_LEAF % [node.kind, node.parent.kind,
|
||||
allowed.inspect, actual.inspect])
|
||||
end
|
||||
end
|
||||
|
||||
def malformed_node!(expectation)
|
||||
raise TypeCheckError, (MALFORMED % expectation)
|
||||
end
|
||||
|
@ -107,5 +151,28 @@ module CeleryScript
|
|||
def run_additional_validations(node, expectation)
|
||||
corpus.validator(expectation).call(node, TypeCheckError, corpus)
|
||||
end
|
||||
|
||||
# Calling this method with only one paramter
|
||||
# indicates a starting condition 🏁
|
||||
def resolve_variable!(node, origin = node)
|
||||
locals = (node.args["locals"] || node.args[:locals])
|
||||
|
||||
if locals&.kind === "scope_declaration"
|
||||
label = (origin.args["label"] || origin.args[:label])&.value
|
||||
result = locals
|
||||
.body
|
||||
.find{ |x| (x.args[:label] || x.args["label"])&.value == label }
|
||||
return result if result
|
||||
end
|
||||
|
||||
case node.parent
|
||||
when AstNode
|
||||
# sequence: Check the `scope` arg
|
||||
# Keep recursing if we can't find a scope on this node.
|
||||
resolve_variable!(node.parent, origin)
|
||||
when nil # We've got an unbound variable.
|
||||
origin.invalidate!(UNBOUND_VAR % origin.args["label"].value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -28,7 +28,7 @@ module SequenceMigration
|
|||
# it's acting weird with autoloading right now :shipit:. TODO: See if there
|
||||
# is a way to automatically infer all classes
|
||||
def self.descendants
|
||||
[SequenceSpeedChange]
|
||||
[SequenceSpeedChange, AddLocalsToSequenceArgs]
|
||||
end
|
||||
|
||||
def self.latest_version
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
# This migration:
|
||||
# Adds a `locals` arg to all v5 or better sequences.
|
||||
module SequenceMigration
|
||||
class AddLocalsToSequenceArgs < Base
|
||||
VERSION = 6
|
||||
CREATED_ON = "NOVEMBER 30 2017"
|
||||
|
||||
def up
|
||||
sequence.args["locals"] ||= Sequence::NOTHING
|
||||
end
|
||||
end
|
||||
end
|
|
@ -13,7 +13,8 @@ module CeleryScriptSettingsBag
|
|||
factory_reset execute_script set_user_env wait
|
||||
install_farmware update_farmware take_photo zero
|
||||
install_first_party_farmware remove_farmware
|
||||
find_home register_gpio unregister_gpio)
|
||||
find_home register_gpio unregister_gpio
|
||||
set_servo_angle)
|
||||
ALLOWED_PACKAGES = %w(farmbot_os arduino_firmware)
|
||||
ALLOWED_CHAGES = %w(add remove update)
|
||||
RESOURCE_NAME = %w(images plants regimens peripherals
|
||||
|
@ -21,14 +22,14 @@ module CeleryScriptSettingsBag
|
|||
tool_slots tools points tokens users device)
|
||||
ALLOWED_MESSAGE_TYPES = %w(success busy warn error info fun debug)
|
||||
ALLOWED_CHANNEL_NAMES = %w(ticker toast email)
|
||||
ALLOWED_DATA_TYPES = %w(string integer)
|
||||
ALLOWED_POINTER_TYPE = %w(GenericPointer ToolSlot Plant)
|
||||
ALLOWED_DATA_TYPES = %w(tool coordinate point)
|
||||
ALLOWED_OPS = %w(< > is not is_undefined)
|
||||
ALLOWED_AXIS = %w(x y z all)
|
||||
ALLOWED_LHS = [*(0..69)].map{|x| "pin#{x}"}.concat(%w(x y z))
|
||||
ALLOWED_POINTER_TYPE = %w(GenericPointer ToolSlot Plant)
|
||||
STEPS = %w(move_absolute move_relative write_pin read_pin wait
|
||||
send_message execute _if execute_script take_photo
|
||||
find_home)
|
||||
send_message execute _if execute_script take_photo
|
||||
find_home)
|
||||
BAD_ALLOWED_PIN_MODES = '"%s" is not a valid pin_mode. Allowed values: %s'
|
||||
BAD_LHS = 'Can not put "%s" into a left hand side (LHS) '\
|
||||
'argument. Allowed values: %s'
|
||||
|
@ -38,6 +39,7 @@ module CeleryScriptSettingsBag
|
|||
BAD_OP = 'Can not put "%s" into an operand (OP) argument. '\
|
||||
'Allowed values: %s'
|
||||
BAD_CHANNEL_NAME = '"%s" is not a valid channel_name. Allowed values: %s'
|
||||
BAD_DATA_TYPE = '"%s" is not a valid data_type. Allowed values: %s'
|
||||
BAD_MESSAGE_TYPE = '"%s" is not a valid message_type. Allowed values: %s'
|
||||
BAD_MESSAGE = "Messages must be between 1 and 300 characters"
|
||||
BAD_TOOL_ID = 'Tool #%s does not exist.'
|
||||
|
@ -131,11 +133,17 @@ module CeleryScriptSettingsBag
|
|||
tooLong = notString || node.value.length > 300
|
||||
node.invalidate! BAD_MESSAGE if (tooShort || tooLong)
|
||||
end
|
||||
.defineArg(:location, [:tool, :coordinate, :point])
|
||||
.defineArg(:location, [:tool, :coordinate, :point, :identifier])
|
||||
.defineArg(:offset, [:coordinate])
|
||||
.defineArg(:_then, [:execute, :nothing])
|
||||
.defineArg(:_else, [:execute, :nothing])
|
||||
.defineArg(:url, [String])
|
||||
.defineArg(:locals, [:scope_declaration, :nothing])
|
||||
.defineArg(:data_type, [String]) do |node|
|
||||
within(ALLOWED_DATA_TYPES, node) do |v|
|
||||
BAD_DATA_TYPE % [v.to_s, ALLOWED_DATA_TYPES.inspect]
|
||||
end
|
||||
end
|
||||
.defineNode(:nothing, [])
|
||||
.defineNode(:tool, [:tool_id])
|
||||
.defineNode(:coordinate, [:x, :y, :z])
|
||||
|
@ -148,7 +156,7 @@ module CeleryScriptSettingsBag
|
|||
.defineNode(:send_message, [:message, :message_type], [:channel])
|
||||
.defineNode(:execute, [:sequence_id])
|
||||
.defineNode(:_if, [:lhs, :op, :rhs, :_then, :_else], [:pair])
|
||||
.defineNode(:sequence, [:version], STEPS)
|
||||
.defineNode(:sequence, [:version, :locals], STEPS)
|
||||
.defineNode(:home, [:speed, :axis], [])
|
||||
.defineNode(:find_home, [:speed, :axis], [])
|
||||
.defineNode(:zero, [:axis], [])
|
||||
|
@ -177,6 +185,10 @@ module CeleryScriptSettingsBag
|
|||
.defineNode(:install_farmware, [:url])
|
||||
.defineNode(:update_farmware, [:package])
|
||||
.defineNode(:remove_farmware, [:package])
|
||||
.defineNode(:scope_declaration, [], [:parameter_declaration])
|
||||
.defineNode(:identifier, [:label])
|
||||
.defineNode(:parameter_declaration, [:label, :data_type], [])
|
||||
.defineNode(:set_servo_angle, [:pin_number, :pin_value], [])
|
||||
.defineNode(:install_first_party_farmware, [])
|
||||
# Given an array of allowed values and a CeleryScript AST node, will DETERMINE
|
||||
# if the node contains a legal value. Throws exception and invalidates if not.
|
||||
|
@ -185,3 +197,4 @@ module CeleryScriptSettingsBag
|
|||
node.invalidate!(yield(val)) if !array.include?(val)
|
||||
end
|
||||
end
|
||||
# {kind: "set_servo_angle", args: {pin_number: 4 | 5, pin_value: 0..360}}
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
# most of the functionality of a programming language such a variables and
|
||||
# conditional logic.
|
||||
class Sequence < ApplicationRecord
|
||||
NOTHING = { kind: "nothing", args: {} }
|
||||
DEFAULT_ARGS = {locals: NOTHING,
|
||||
version: SequenceMigration::Base.latest_version }
|
||||
# Does some extra magic for serialized columns for us, such as providing a
|
||||
# default value and making hashes have indifferent access.
|
||||
class CustomSerializer
|
||||
|
@ -46,10 +49,9 @@ class Sequence < ApplicationRecord
|
|||
before_validation :set_defaults
|
||||
|
||||
def set_defaults
|
||||
self.args ||= {}
|
||||
self.args["version"] ||= SequenceMigration::Base.latest_version
|
||||
self.color ||= "gray"
|
||||
self.kind ||= "sequence"
|
||||
self.args = {}.merge(DEFAULT_ARGS).merge(self.args)
|
||||
self.color ||= "gray"
|
||||
self.kind ||= "sequence"
|
||||
end
|
||||
|
||||
def maybe_migrate
|
||||
|
|
|
@ -2,9 +2,9 @@ module Sequences
|
|||
module CeleryScriptValidators
|
||||
|
||||
NO_TRANSACTION = "You need to do this in a transaction"
|
||||
ARGS_OF_INTEREST = { "tool_id" => Tool,
|
||||
"sequence_id" => Sequence,
|
||||
"pointer_id" => Point }
|
||||
ARGS_OF_INTEREST = {"tool_id" => Tool,
|
||||
"sequence_id" => Sequence,
|
||||
"pointer_id" => Point }
|
||||
ALLOWED_NODE_KEYS = [
|
||||
"body",
|
||||
"kind",
|
||||
|
@ -25,20 +25,13 @@ module Sequences
|
|||
add_error :body, :syntax_error, checker.error.message if !checker.valid?
|
||||
end
|
||||
|
||||
def seq
|
||||
@seq ||= {body: [],
|
||||
args: { version: SequenceMigration::Base.latest_version },
|
||||
kind: "sequence"}.merge(inputs.symbolize_keys.slice(:body,
|
||||
:kind,
|
||||
:args))
|
||||
def symbolized_input
|
||||
@symbolized_input ||= inputs.symbolize_keys.slice(:body, :kind, :args)
|
||||
end
|
||||
|
||||
def reload_dependencies(sequence)
|
||||
must_be_in_transaction
|
||||
SequenceDependency
|
||||
.where(sequence: sequence)
|
||||
.destroy_all
|
||||
|
||||
SequenceDependency.where(sequence: sequence).destroy_all
|
||||
SequenceDependency.create!(deps(sequence))
|
||||
end
|
||||
|
||||
|
@ -48,7 +41,17 @@ module Sequences
|
|||
end
|
||||
|
||||
def tree
|
||||
@tree = CeleryScript::AstNode.new(**seq)
|
||||
hmm = {
|
||||
kind: "sequence",
|
||||
body: symbolized_input[:body],
|
||||
args: {
|
||||
version: SequenceMigration::Base.latest_version,
|
||||
locals: symbolized_input
|
||||
.deep_symbolize_keys
|
||||
.dig(:args, :locals) || Sequence::NOTHING
|
||||
}
|
||||
}
|
||||
@tree = CeleryScript::AstNode.new(**hmm)
|
||||
end
|
||||
|
||||
def corpus
|
||||
|
|
|
@ -10,6 +10,15 @@ module Sequences
|
|||
|
||||
optional do
|
||||
string :color, in: Sequence::COLORS
|
||||
hash :args do
|
||||
optional do
|
||||
hash :locals do
|
||||
optional do
|
||||
duck :*, methods: [:[], :[]=], default: {}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def validate
|
||||
|
|
|
@ -11,6 +11,13 @@ module Sequences
|
|||
duck :body, methods: [:[], :[]=, :each, :map]
|
||||
string :name
|
||||
string :color, in: Sequence::COLORS
|
||||
hash :args do
|
||||
optional do
|
||||
hash :locals do
|
||||
duck :*, methods: [] # Let CeleryScript lib do the type checking...
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def validate
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
// STAGE 1: = = = = = = =
|
||||
// ARGS TO ADD:
|
||||
// X "locals" (sequence),
|
||||
// X "data_type" (parameter_declaration)
|
||||
//
|
||||
// NODES TO ADD:
|
||||
// X "identifier" (label),
|
||||
// X "parameter_declaration" (label, data_type)
|
||||
// X "scope_declaration"(contains parameter_declaration),
|
||||
//
|
||||
// Stage 2: = = = = = = = =
|
||||
// NODES TO ADD:
|
||||
// "local_declaration" (label, data_value),
|
||||
//
|
||||
// ARGS TO ADD:
|
||||
// "data_value" (local_declaration)
|
||||
//
|
||||
{
|
||||
"kind": "sequence",
|
||||
"args": {
|
||||
"is_outdated": false,
|
||||
"version": 4,
|
||||
"locals": { // <= New args
|
||||
"kind": "scope_declaration", // <= New node
|
||||
"args": {},
|
||||
"body": [
|
||||
{ // SCENARIO A: Setting the parent value to a 'literal''
|
||||
"kind": "local_declaration", // <= New node
|
||||
"args": {
|
||||
"label": "parent",
|
||||
"data_value": "POINT_LITERAL_OR_WHATEVER_GOES_HERE"
|
||||
}
|
||||
},
|
||||
{ // SCENARIO B: Defer assignment to parent, only set expected (eventual) type
|
||||
"kind": "parameter_declaration", // <= New node
|
||||
"args": {
|
||||
"label": "parent",
|
||||
"data_type": "EXPECTED_KIND_GOES_HERE"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"body": [
|
||||
{
|
||||
"kind": "move_absolute",
|
||||
"args": {
|
||||
"speed": 800,
|
||||
"location": {
|
||||
"kind": "identifier", // <== New Node- this is how you use variables.
|
||||
"args": {
|
||||
"label": "parent"
|
||||
}
|
||||
},
|
||||
"offset": {
|
||||
"kind": "coordinate",
|
||||
"args": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -52,7 +52,7 @@
|
|||
"enzyme": "^3.1.0",
|
||||
"enzyme-adapter-react-15": "^1.0.2",
|
||||
"extract-text-webpack-plugin": "^3.0.1",
|
||||
"farmbot": "5.1.1",
|
||||
"farmbot": "^5.2.0-rc4",
|
||||
"farmbot-toastr": "^1.0.3",
|
||||
"fastclick": "^1.0.6",
|
||||
"file-loader": "^1.1.5",
|
||||
|
|
|
@ -17,9 +17,7 @@ describe Api::SequencesController do
|
|||
input = { name: "Scare Birds",
|
||||
body: nodes }
|
||||
sequence_body_for(user)
|
||||
post :create,
|
||||
body: input.to_json,
|
||||
params: {format: :json}
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
expect(response.status).to eq(200)
|
||||
expect(json[:args]).to be_kind_of(Hash)
|
||||
expect(json[:body]).to be_kind_of(Array)
|
||||
|
@ -33,9 +31,7 @@ describe Api::SequencesController do
|
|||
input[:body].first[:uuid] = SecureRandom.uuid
|
||||
input[:body].first["uuid"] = SecureRandom.uuid
|
||||
sequence_body_for(user)
|
||||
post :create,
|
||||
body: input.to_json,
|
||||
params: {format: :json}
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
expect(response.status).to eq(200)
|
||||
expect(json[:args]).to be_kind_of(Hash)
|
||||
expect(json[:body]).to be_kind_of(Array)
|
||||
|
@ -76,6 +72,120 @@ describe Api::SequencesController do
|
|||
expect(validated_count).to eq(new_count)
|
||||
end
|
||||
|
||||
it 'doesnt allow nonsense in `sequence.args.locals`' do
|
||||
input = { name: "Scare Birds",
|
||||
body: [],
|
||||
# Intentional nonsense to check validation logic.
|
||||
args: { locals: { kind: "wait", args: { milliseconds: 5000 } } }
|
||||
}
|
||||
|
||||
sign_in user
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
expect(response.status).to eq(422)
|
||||
expect(Sequence.last.args["locals"]["kind"]).to_not be("wait")
|
||||
end
|
||||
|
||||
it 'strips excess `args`' do
|
||||
input = { name: "Scare Birds",
|
||||
body: [],
|
||||
# Intentional nonsense to check validation logic.
|
||||
args: { foo: "BAR" } }
|
||||
|
||||
sign_in user
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
expect(response.status).to eq(200)
|
||||
expect(json[:args][:foo]).to eq(nil)
|
||||
expect(Sequence.last.args[:foo]).to eq(nil)
|
||||
end
|
||||
|
||||
it 'disallows typo `data_types` in `locals` declaration' do
|
||||
input = {
|
||||
name: "Scare Birds",
|
||||
body: [],
|
||||
# Intentional nonsense to check validation logic.
|
||||
args: {
|
||||
locals: {
|
||||
kind: "scope_declaration",
|
||||
args: {},
|
||||
body: [
|
||||
{
|
||||
kind: "parameter_declaration",
|
||||
args: {
|
||||
label: "parent",
|
||||
data_type: "PlantSpelledBackwards"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sign_in user
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
expect(response.status).to eq(422)
|
||||
expect(json[:body]).to include("not a valid data_type")
|
||||
end
|
||||
|
||||
it 'disallows erroneous `data_types` in `locals` declaration' do
|
||||
input = {
|
||||
name: "Scare Birds",
|
||||
body: [],
|
||||
# Intentional nonsense to check validation logic.
|
||||
args: {
|
||||
locals: {
|
||||
kind: "scope_declaration",
|
||||
args: {},
|
||||
body: [
|
||||
{ kind: "wait", args: { milliseconds: 5000 } }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sign_in user
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
expect(response.status).to eq(422)
|
||||
expect(json[:body])
|
||||
.to include("Expected one of: [:parameter_declaration]")
|
||||
end
|
||||
|
||||
it 'allows declaration of a variable named `parent`' do
|
||||
input = {
|
||||
name: "Scare Birds",
|
||||
args: {
|
||||
locals: {
|
||||
kind: "scope_declaration",
|
||||
args: {},
|
||||
body: [
|
||||
{
|
||||
kind: "parameter_declaration",
|
||||
args: {
|
||||
label: "parent",
|
||||
data_type: "point"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
body: [
|
||||
{
|
||||
kind: "move_absolute",
|
||||
args: {
|
||||
location: { kind: "identifier", args: { label: "parent" } },
|
||||
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } },
|
||||
speed: 100,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
sign_in user
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
expect(response.status).to eq(200)
|
||||
expect(Sequence.last.args.dig("locals", "body", 0, "args", "label"))
|
||||
.to eq("parent")
|
||||
end
|
||||
|
||||
it 'tracks Points' do
|
||||
point = FactoryBot.create(:point, device: user.device)
|
||||
SequenceDependency.delete_all
|
||||
|
@ -88,8 +198,8 @@ describe Api::SequencesController do
|
|||
body: HAS_POINTS["body"] }
|
||||
sequence_body_for(user)
|
||||
post :create,
|
||||
body: input.to_json,
|
||||
params: {format: :json}
|
||||
body: input.to_json,
|
||||
params: {format: :json}
|
||||
expect(response.status).to eq(200)
|
||||
new_count = SequenceDependency.count
|
||||
validated_count = SequenceDependency.where(sequence_id: json[:id]).count
|
||||
|
@ -97,5 +207,66 @@ describe Api::SequencesController do
|
|||
expect(validated_count).to eq(new_count)
|
||||
expect(SequenceDependency.last.dependency.id).to eq(point.id)
|
||||
end
|
||||
|
||||
it 'prevents unbound variables' do
|
||||
sign_in user
|
||||
input = {
|
||||
name: "Unbound Variable Exception",
|
||||
args: { locals: { kind: "nothing", args: {} } },
|
||||
body: [
|
||||
{
|
||||
kind: "move_absolute",
|
||||
args: {
|
||||
location: {
|
||||
kind: "identifier",
|
||||
args: { label: "parent" }
|
||||
},
|
||||
offset: {
|
||||
kind: "coordinate",
|
||||
args: { x: 0, y: 0, z: 0 }
|
||||
},
|
||||
speed: 100,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
expect(response.status).to eq(422)
|
||||
expect(json[:body]).to eq("Unbound variable: parent")
|
||||
end
|
||||
|
||||
it 'prevents type errors from bad identifier / binding combos' do
|
||||
$lol = true
|
||||
sign_in user
|
||||
input = { name: "type mismatch",
|
||||
args: {
|
||||
locals: {
|
||||
kind: "scope_declaration",
|
||||
args: {},
|
||||
body: [
|
||||
{
|
||||
kind: "parameter_declaration",
|
||||
args: { label: "parent", data_type: "wait" }
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
body: [
|
||||
{ kind: "move_absolute",
|
||||
args: {
|
||||
location: { kind: "identifier", args: { label: "parent" } },
|
||||
offset: {
|
||||
kind: "coordinate",
|
||||
args: { x: 0, y: 0, z: 0 }
|
||||
},
|
||||
speed: 100,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
post :create, body: input.to_json, params: {format: :json}
|
||||
expect(response.status).to eq(422)
|
||||
expect(json[:body]).to include("not a valid data_type")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,19 @@ describe Api::SequencesController do
|
|||
|
||||
let(:user) { FactoryBot.create(:user) }
|
||||
|
||||
it 'doesnt allow nonsense in `sequence.args.locals`' do
|
||||
sign_in user
|
||||
sequence = FactoryBot.create(:sequence, device: user.device)
|
||||
input = { id: sequence.id,
|
||||
sequence: { name: "Wrong `locals` declaration",
|
||||
body: [],
|
||||
args: { locals: {} } } }
|
||||
patch :update, params: {id: sequence.id }, body: input.to_json, as: :json
|
||||
expect(response.status).to eq(422)
|
||||
expect(json[:body]).to include("leaf 'locals' within 'sequence'")
|
||||
expect(json[:body]).to include("but got Hash")
|
||||
end
|
||||
|
||||
it 'refreshes sequence dependencies on update' do
|
||||
SequenceDependency.destroy_all
|
||||
old_count = SequenceDependency.count
|
||||
|
|
|
@ -6,6 +6,7 @@ describe CeleryScript::Checker do
|
|||
{
|
||||
kind: "sequence",
|
||||
args: {
|
||||
locals: Sequence::NOTHING,
|
||||
version: 0
|
||||
},
|
||||
comment: "Properly formatted, syntactically valid sequence.",
|
||||
|
@ -60,6 +61,14 @@ describe CeleryScript::Checker do
|
|||
hash[:body][0][:args][:location][:args][:x] = "supposed to be an Integer"
|
||||
result = checker.run
|
||||
expect(result.message).to eq("Expected leaf 'x' within 'coordinate' to"\
|
||||
" be one of: [Integer] but got String")
|
||||
" be one of: [Integer] but got String")
|
||||
end
|
||||
|
||||
it "finds a bad leaf" do
|
||||
parent = CeleryScript::AstNode.new(parent = nil, args: {}, kind: "nothing")
|
||||
expect {
|
||||
checker.check_leaf CeleryScript::AstLeaf.new(parent, 6, :location)
|
||||
}.to raise_error(CeleryScript::TypeCheckError)
|
||||
|
||||
end
|
||||
end
|
||||
|
|
|
@ -101,7 +101,7 @@ describe CeleryScript::Corpus do
|
|||
it "serializes into JSON" do
|
||||
result = JSON.parse(corpus.to_json)
|
||||
|
||||
expect(result["tag"]).to eq(5)
|
||||
expect(result["tag"]).to eq(6)
|
||||
expect(result["args"]).to be_kind_of(Array)
|
||||
expect(result["nodes"]).to be_kind_of(Array)
|
||||
expect(result["nodes"].sample.keys.sort).to eq(["allowed_args",
|
||||
|
|
|
@ -4,7 +4,7 @@ describe "Celery Script `point` node" do
|
|||
let(:plant) { FactoryBot.create(:plant_point).pointer }
|
||||
let(:hash) do
|
||||
{ kind: "sequence",
|
||||
args: { version:4 },
|
||||
args: Sequence::DEFAULT_ARGS,
|
||||
body: [
|
||||
{
|
||||
kind:"move_absolute",
|
||||
|
|
|
@ -54,7 +54,7 @@ end
|
|||
|
||||
describe SequenceMigration do
|
||||
it 'has a latest version' do
|
||||
expect(SequenceMigration::Base.latest_version).to eq(5)
|
||||
expect(SequenceMigration::Base.latest_version).to eq(6)
|
||||
end
|
||||
|
||||
it 'updates speed on all the things < v5' do
|
||||
|
@ -70,6 +70,7 @@ describe SequenceMigration do
|
|||
expect(s.body[0]["args"]["speed"]).to eq(100)
|
||||
expect(s.body[1]["args"]["speed"]).to eq(100)
|
||||
expect(s.body[2]["args"]["speed"]).to eq(100)
|
||||
expect(s.args.dig("locals","kind")).to eq("nothing")
|
||||
end
|
||||
|
||||
it 'warns developers that `up()` is required' do
|
||||
|
|
|
@ -42,7 +42,8 @@ export let bot: Everything["bot"] = {
|
|||
locked: false,
|
||||
commit: "---",
|
||||
target: "---",
|
||||
env: "---"
|
||||
env: "---",
|
||||
node_name: "afluent avocado"
|
||||
},
|
||||
"user_env": {},
|
||||
"process_info": {
|
||||
|
|
|
@ -13,7 +13,11 @@ let idCounter = 1;
|
|||
|
||||
export function fakeSequence(): TaggedSequence {
|
||||
return fakeResource("Sequence", {
|
||||
args: { version: 4, label: "WIP" },
|
||||
args: {
|
||||
version: 4,
|
||||
label: "WIP",
|
||||
locals: { kind: "nothing", args: {} },
|
||||
},
|
||||
id: 12,
|
||||
color: "red",
|
||||
name: "fake",
|
||||
|
|
|
@ -24,7 +24,7 @@ export let fakeFarmEventWithExecutable = (): FarmEventWithExecutable => {
|
|||
color: "red",
|
||||
name: "faker",
|
||||
kind: "sequence",
|
||||
args: { version: 0, label: "WIP" }
|
||||
args: { version: 0, label: "WIP", locals: { kind: "nothing", args: {} }, }
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -281,7 +281,8 @@ const tr13: TaggedResource = {
|
|||
"args": {
|
||||
"is_outdated": false,
|
||||
"version": 4,
|
||||
"label": "foo"
|
||||
"label": "foo",
|
||||
"locals": { kind: "nothing", args: {} },
|
||||
},
|
||||
"kind": "sequence"
|
||||
},
|
||||
|
|
|
@ -64,7 +64,8 @@ export let initialState: BotState = {
|
|||
locked: false,
|
||||
commit: "---",
|
||||
target: "---",
|
||||
env: "---"
|
||||
env: "---",
|
||||
node_name: "affluent_avocado"
|
||||
},
|
||||
user_env: {},
|
||||
process_info: {
|
||||
|
|
|
@ -41,7 +41,8 @@ describe("mapStateToProps()", () => {
|
|||
"body": [{ kind: "wait", args: { milliseconds: 100 } }],
|
||||
"args": {
|
||||
"version": 4,
|
||||
"label": "WIP"
|
||||
"label": "WIP",
|
||||
locals: { kind: "nothing", args: {} },
|
||||
},
|
||||
"kind": "sequence"
|
||||
},
|
||||
|
|
|
@ -45,6 +45,7 @@ describe("commitBulkEditor()", () => {
|
|||
"color": "gray",
|
||||
"body": [{ kind: "wait", args: { milliseconds: 100 } }],
|
||||
"args": {
|
||||
locals: { kind: "nothing", args: {} },
|
||||
"version": 4,
|
||||
"label": "WIP"
|
||||
},
|
||||
|
|
|
@ -23,6 +23,7 @@ describe("maybeTagSteps()", () => {
|
|||
}
|
||||
],
|
||||
"args": {
|
||||
"locals": { kind: "nothing", args: {} },
|
||||
"version": 4
|
||||
},
|
||||
"kind": "sequence"
|
||||
|
|
|
@ -41,6 +41,7 @@ describe("<AllSteps/>", () => {
|
|||
}
|
||||
],
|
||||
"args": {
|
||||
"locals": { kind: "nothing", args: {} },
|
||||
"is_outdated": false,
|
||||
"version": 4,
|
||||
"label": "WIP"
|
||||
|
|
|
@ -12,7 +12,10 @@ describe("<TestButton/>", () => {
|
|||
"name": "Goto 0, 0, 0",
|
||||
"color": "gray",
|
||||
"body": [],
|
||||
"args": { "version": 4 },
|
||||
"args": {
|
||||
"version": 4,
|
||||
"locals": { kind: "nothing", args: {} },
|
||||
},
|
||||
"kind": "sequence"
|
||||
},
|
||||
"uuid": "Sequence.23.47"
|
||||
|
|
|
@ -40,7 +40,10 @@ describe("<InputDefault/>", () => {
|
|||
"name": "Goto 0, 0, 0",
|
||||
"color": "gray",
|
||||
"body": [step],
|
||||
"args": { "version": 4 },
|
||||
"args": {
|
||||
"version": 4,
|
||||
"locals": { kind: "nothing", args: {} },
|
||||
},
|
||||
"kind": "sequence"
|
||||
},
|
||||
"uuid": "Sequence.74.145"
|
||||
|
|
|
@ -80,7 +80,10 @@ export class SequencesList extends
|
|||
specialStatus: SpecialStatus.SAVED,
|
||||
body: {
|
||||
name: "new sequence " + (this.props.sequences.length),
|
||||
args: { version: -999 },
|
||||
args: {
|
||||
version: -999,
|
||||
locals: { kind: "nothing", args: {} },
|
||||
},
|
||||
color: "gray",
|
||||
kind: "sequence",
|
||||
body: []
|
||||
|
|
|
@ -2,8 +2,10 @@ import * as React from "react";
|
|||
import { TileMoveAbsolute } from "../tile_move_absolute";
|
||||
import { mount, ReactWrapper } from "enzyme";
|
||||
import { fakeSequence } from "../../../__test_support__/fake_state/resources";
|
||||
import { MoveAbsolute } from "farmbot/dist";
|
||||
import { MoveAbsolute, SequenceBodyItem } from "farmbot/dist";
|
||||
import { emptyState } from "../../../resources/reducer";
|
||||
import { buildResourceIndex } from "../../../__test_support__/resource_index_builder";
|
||||
import { SpecialStatus } from "../../../resources/tagged_resources";
|
||||
|
||||
describe("<TileMoveAbsolute/>", () => {
|
||||
function bootstrapTest() {
|
||||
|
@ -67,4 +69,38 @@ describe("<TileMoveAbsolute/>", () => {
|
|||
checkField(block, 6, "y-offset", "5");
|
||||
checkField(block, 7, "z-offset", "6");
|
||||
});
|
||||
|
||||
it("retrieves a tool", () => {
|
||||
const index = buildResourceIndex([
|
||||
{
|
||||
kind: "Tool",
|
||||
uuid: "Tool.4.4",
|
||||
specialStatus: SpecialStatus.SAVED,
|
||||
body: {
|
||||
id: 4,
|
||||
name: "tool123"
|
||||
}
|
||||
}
|
||||
]).index;
|
||||
const tool = index.references[index.byKind.Tool[0]];
|
||||
if (!tool) { throw new Error("Impossible"); }
|
||||
|
||||
const currentStep: SequenceBodyItem = {
|
||||
kind: "move_absolute",
|
||||
args: {
|
||||
location: { kind: "tool", args: { tool_id: tool.body.id || -1 } },
|
||||
speed: 100,
|
||||
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } }
|
||||
}
|
||||
};
|
||||
|
||||
const component = mount(<TileMoveAbsolute
|
||||
currentSequence={fakeSequence()}
|
||||
currentStep={currentStep}
|
||||
dispatch={jest.fn()}
|
||||
index={0}
|
||||
resources={index} />).instance() as TileMoveAbsolute;
|
||||
|
||||
expect(component.tool).toEqual(tool);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,7 +8,8 @@ import {
|
|||
Tool,
|
||||
Coordinate,
|
||||
LegalSequenceKind,
|
||||
Point
|
||||
Point,
|
||||
Identifier
|
||||
} from "farmbot";
|
||||
import {
|
||||
Row,
|
||||
|
@ -36,7 +37,7 @@ import { StepIconGroup } from "../step_icon_group";
|
|||
import { StepInputBox } from "../inputs/step_input_box";
|
||||
|
||||
interface Args {
|
||||
location: Tool | Coordinate | Point;
|
||||
location: Tool | Coordinate | Point | Identifier;
|
||||
speed: number;
|
||||
offset: Coordinate;
|
||||
}
|
||||
|
@ -65,11 +66,13 @@ export class TileMoveAbsolute extends Component<StepParams, MoveAbsState> {
|
|||
throw new Error("Impossible celery node detected.");
|
||||
}
|
||||
}
|
||||
|
||||
get location(): Tool | Coordinate {
|
||||
if (this.args.location.kind !== "point") {
|
||||
if (this.args.location.kind !== "point"
|
||||
&& this.args.location.kind !== "identifier") {
|
||||
return this.args.location;
|
||||
} else {
|
||||
throw new Error("A `point` node snuck in. Still WIP");
|
||||
throw new Error("A `point` or `identifier` node snuck in. Still WIP");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,13 @@ export function activeTools(resources: ResourceIndex) {
|
|||
.map(tool => (tool && tool.kind === "Tool") ? tool : undefined));
|
||||
}
|
||||
|
||||
// const PARENT: Identifier = { kind: "identifier", args: { label: "parent" } };
|
||||
// const PARENT_DDI: DropDownItem = {
|
||||
// label: "Parent",
|
||||
// value: "parent",
|
||||
// headingId: "identifier",
|
||||
// };
|
||||
|
||||
export function generateList(input: ResourceIndex): DropDownItem[] {
|
||||
const toolNameById = mapToolIdToName(input);
|
||||
const SORT_KEY: keyof DropDownItem = "headingId";
|
||||
|
@ -34,6 +41,7 @@ export function generateList(input: ResourceIndex): DropDownItem[] {
|
|||
.sortBy(SORT_KEY)
|
||||
.reverse()
|
||||
.concat(toolDDI)
|
||||
// .concat([PARENT_DDI])
|
||||
.filter(x => parseInt("" + x.value) > 0)
|
||||
.value();
|
||||
}
|
||||
|
|
|
@ -1996,9 +1996,9 @@ farmbot-toastr@^1.0.0, farmbot-toastr@^1.0.3:
|
|||
farmbot-toastr "^1.0.0"
|
||||
typescript "^2.3.4"
|
||||
|
||||
farmbot@5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/farmbot/-/farmbot-5.1.1.tgz#92b7618281385610c7b629dfb524b607d495d4dc"
|
||||
farmbot@^5.2.0-rc4:
|
||||
version "5.2.0-rc4"
|
||||
resolved "https://registry.yarnpkg.com/farmbot/-/farmbot-5.2.0-rc4.tgz#6817b00a043d8344be866514fe200998f45634f5"
|
||||
dependencies:
|
||||
mqtt "^2.13.1"
|
||||
typescript "^2.4.2"
|
||||
|
|
Loading…
Reference in New Issue