Merge pull request #552 from RickCarlino/variables

Variables
pull/553/head
Rick Carlino 2017-12-05 08:54:11 -06:00 committed by GitHub
commit 694bcddce3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 512 additions and 72 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

66
idea.json 100644
View File

@ -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
}
}
}
}
]
}

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -42,7 +42,8 @@ export let bot: Everything["bot"] = {
locked: false,
commit: "---",
target: "---",
env: "---"
env: "---",
node_name: "afluent avocado"
},
"user_env": {},
"process_info": {

View File

@ -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",

View File

@ -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: {} }, }
}
};
};

View File

@ -281,7 +281,8 @@ const tr13: TaggedResource = {
"args": {
"is_outdated": false,
"version": 4,
"label": "foo"
"label": "foo",
"locals": { kind: "nothing", args: {} },
},
"kind": "sequence"
},

View File

@ -64,7 +64,8 @@ export let initialState: BotState = {
locked: false,
commit: "---",
target: "---",
env: "---"
env: "---",
node_name: "affluent_avocado"
},
user_env: {},
process_info: {

View File

@ -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"
},

View File

@ -45,6 +45,7 @@ describe("commitBulkEditor()", () => {
"color": "gray",
"body": [{ kind: "wait", args: { milliseconds: 100 } }],
"args": {
locals: { kind: "nothing", args: {} },
"version": 4,
"label": "WIP"
},

View File

@ -23,6 +23,7 @@ describe("maybeTagSteps()", () => {
}
],
"args": {
"locals": { kind: "nothing", args: {} },
"version": 4
},
"kind": "sequence"

View File

@ -41,6 +41,7 @@ describe("<AllSteps/>", () => {
}
],
"args": {
"locals": { kind: "nothing", args: {} },
"is_outdated": false,
"version": 4,
"label": "WIP"

View File

@ -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"

View File

@ -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"

View File

@ -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: []

View File

@ -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);
});
});

View File

@ -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");
}
}

View File

@ -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();
}

View File

@ -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"