Friendlier write_pin errors for CeleryScript(TM) invalidation.

pull/1235/head
Rick Carlino 2019-06-13 14:02:31 -05:00
parent 0346fe9adb
commit cc7402f9d6
2 changed files with 158 additions and 135 deletions

View File

@ -3,17 +3,18 @@
# this part last
module CeleryScript
UNBOUND_VAR = "Unbound variable: %s"
class TypeCheckError < StandardError; end
class Checker
MISSING_ARG = "Expected node '%s' to have a '%s', but got: %s."
EXTRA_ARGS = "'%s' has unexpected arguments: %s. Allowed arguments: %s"
BAD_LEAF = "Expected leaf '%{kind}' within '%{parent_kind}'"\
" to be one of: %{allowed} but got %{actual}"
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"
T_MISMATCH = "Type mismatch. %s must be one of: %s. Got: %s"
EXTRA_ARGS = "'%s' has unexpected arguments: %s. Allowed arguments: %s"
BAD_LEAF = "Expected leaf '%{kind}' within '%{parent_kind}'" \
" to be one of: %{allowed} but got %{actual}"
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"
T_MISMATCH = "Type mismatch. %s must be one of: %s. Got: %s"
# Certain CeleryScript pairing errors are more than just a syntax error.
# For instance, A `nothing` node in a `parameter_declaration` is often an
@ -22,6 +23,7 @@ module CeleryScript
# BAD_LEAF template.
FRIENDLY_ERRORS = {
nothing: {
write_pin: "You must select a Peripheral in the Control Peripheral step.",
variable_declaration: "You must provide a value for all parameters",
parameter_declaration: "You must provide a value for all parameters",
},
@ -89,7 +91,7 @@ module CeleryScript
unless has_key
msgs = node.args.keys.join(", ")
msgs = "nothing" if msgs.length < 1
msg = MISSING_ARG % [node.kind, arg, msgs]
msg = MISSING_ARG % [node.kind, arg, msgs]
raise TypeCheckError, msg
end
end

View File

@ -1,51 +1,72 @@
require 'spec_helper'
require "spec_helper"
HAS_POINTS = JSON.parse(File.read("spec/lib/celery_script/ast_has_points.json"))
describe Api::SequencesController do
before :each do
request.headers["accept"] = 'application/json'
request.headers["accept"] = "application/json"
end
include Devise::Test::ControllerHelpers
describe '#create' do
describe "#create" do
let(:user) { FactoryBot.create(:user) }
let(:nodes) { sequence_body_for(user) }
it 'handles a well formed AST in the body attribute' do
it "provides human readable errors for empty write_pin nodes" do
sign_in user
body = [
{
kind: "write_pin",
args: {
pin_number: { kind: "nothing", args: {} },
pin_value: 0,
pin_mode: 0,
},
},
]
input = { name: "Scare Birds", body: body }
sequence_body_for(user)
post :create, body: input.to_json, params: { format: :json }
expect(response.status).to eq(422)
err = json.fetch(:body)
expected = "You must select a Peripheral in the Control Peripheral step."
expect(err).to eq(expected)
end
it "handles a well formed AST in the body attribute" do
sign_in user
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)
expect(json[:body].length).to eq(nodes.length)
end
it 'disregards extra attrs (like `uuid`) on sequence body nodes' do
it "disregards extra attrs (like `uuid`) on sequence body nodes" do
sign_in user
input = { name: "Scare Birds",
body: nodes }
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)
expect(json[:body].length).to eq(nodes.length)
end
it 'creates a new sequences for a user' do
it "creates a new sequences for a user" do
sign_in user
input = { name: "Scare Birds", body: [] }
post :create, body: input.to_json, format: :json
expect(response.status).to eq(200)
end
it 'handles invalid params' do
it "handles invalid params" do
# Needed to test the `else` branch of mutate() somewhere
sign_in user
input = {}
@ -55,32 +76,31 @@ describe Api::SequencesController do
expect(json[:name]).to eq("Name is required")
end
it 'doesnt allow nonsense in `sequence.args.locals`' do
it "doesnt allow nonsense in `sequence.args.locals`" do
PinBinding.destroy_all
Sequence.destroy_all
input = { name: "Scare Birds",
body: [],
# Intentional nonsense to check validation logic.
args: { locals: { kind: "wait", args: { milliseconds: 5000 } } }
}
args: { locals: { kind: "wait", args: { milliseconds: 5000 } } } }
sign_in user
post :create, body: input.to_json, params: {format: :json}
post :create, body: input.to_json, params: { format: :json }
expect(response.status).to eq(422)
expect(Sequence.last).to_not be
xpectd = "Expected leaf 'wait' within 'sequence' to be one of: "\
xpectd = "Expected leaf 'wait' within 'sequence' to be one of: " \
"[\"scope_declaration\"] but got wait"
expect(json.fetch(:body)).to eq(xpectd)
end
it 'strips excess `args`' do
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}
post :create, body: input.to_json, params: { format: :json }
expect(response.status).to eq(200)
expect(json[:args][:foo]).to eq(nil)
generated_result = CeleryScript::FetchCelery
@ -89,7 +109,7 @@ describe Api::SequencesController do
expect(generated_result.dig(:args, :foo)).to eq(nil)
end
it 'disallows bad default_values' do
it "disallows bad default_values" do
input = {
name: "Scare Birds",
body: [],
@ -105,24 +125,24 @@ describe Api::SequencesController do
label: "parent",
default_value: {
kind: "wait",
args: { milliseconds: 12 }
}
}
}
]
}
}
args: { milliseconds: 12 },
},
},
},
],
},
},
}
sign_in user
post :create, body: input.to_json, params: {format: :json}
post :create, body: input.to_json, params: { format: :json }
expect(response.status).to eq(422)
expect(json.fetch(:body)).to include('"tool"')
expect(json[:body]).to include("Expected leaf 'wait' within "\
expect(json[:body]).to include("Expected leaf 'wait' within " \
"'parameter_declaration' to be one of: [")
end
it 'disallows erroneous `locals` declaration' do
it "disallows erroneous `locals` declaration" do
input = {
name: "Scare Birds",
body: [],
@ -132,21 +152,21 @@ describe Api::SequencesController do
kind: "scope_declaration",
args: {},
body: [
{ kind: "wait", args: { milliseconds: 5000 } }
]
}
}
{ kind: "wait", args: { milliseconds: 5000 } },
],
},
},
}
sign_in user
post :create, body: input.to_json, params: {format: :json}
post :create, body: input.to_json, params: { format: :json }
expect(response.status).to eq(422)
expctd =
"Expected one of: [:variable_declaration, :parameter_declaration]"
expect(json[:body]).to include(expctd)
end
it 'allows declaration of a variable named `parent`' do
it "allows declaration of a variable named `parent`" do
input = {
name: "Scare Birds",
args: {
@ -163,10 +183,10 @@ describe Api::SequencesController do
args: {
x: 0,
y: 0,
z: 0
}
}
}
z: 0,
},
},
},
},
{
kind: "variable_declaration",
@ -174,12 +194,12 @@ describe Api::SequencesController do
label: "parent2",
data_value: {
kind: "coordinate",
args: { x: 9, y: 9, z: 9, }
}
}
}
]
}
args: { x: 9, y: 9, z: 9 },
},
},
},
],
},
},
body: [
{
@ -187,30 +207,34 @@ describe Api::SequencesController do
args: {
location: {
kind: "identifier",
args: { label: "parent" } },
offset: {
args: { label: "parent" },
},
offset: {
kind: "coordinate",
args: { x: 0, y: 0, z: 0 } },
speed: 100,
}
args: { x: 0, y: 0, z: 0 },
},
speed: 100,
},
},
{
kind: "move_absolute",
args: {
location: {
kind: "identifier",
args: { label: "parent2" } },
offset: {
args: { label: "parent2" },
},
offset: {
kind: "coordinate",
args: { x: 0, y: 0, z: 0 } },
speed: 100,
}
}
]
args: { x: 0, y: 0, z: 0 },
},
speed: 100,
},
},
],
}
sign_in 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)
dig_path = [:args, :locals, :body, 0, :args, :label]
generated_result = CeleryScript::FetchCelery
@ -220,7 +244,7 @@ describe Api::SequencesController do
expect(json.dig(*dig_path)).to eq("parent")
end
it 'tracks Points' do
it "tracks Points" do
point = FactoryBot.create(:generic_pointer, device: user.device)
PinBinding.destroy_all
Sequence.destroy_all
@ -231,14 +255,14 @@ describe Api::SequencesController do
sign_in user
input = { name: "Scare Birds", body: HAS_POINTS["body"] }
sequence_body_for(user)
before = EdgeNode.where(kind: "pointer_id").count
post :create, body: input.to_json, params: {format: :json}
before = EdgeNode.where(kind: "pointer_id").count
post :create, body: input.to_json, params: { format: :json }
expect(response.status).to eq(200)
now = EdgeNode.where(kind: "pointer_id").count
expect(now).to be > before
end
it 'prevents unbound variables' do
it "prevents unbound variables" do
sign_in user
input = {
name: "Unbound Variable Exception",
@ -249,23 +273,23 @@ describe Api::SequencesController do
args: {
location: {
kind: "identifier",
args: { label: "parent" }
args: { label: "parent" },
},
offset: {
offset: {
kind: "coordinate",
args: { x: 0, y: 0, z: 0 }
args: { x: 0, y: 0, z: 0 },
},
speed: 100,
}
}
]
speed: 100,
},
},
],
}
post :create, body: input.to_json, params: {format: :json}
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 'does not let you use other peoples point resources' do
it "does not let you use other peoples point resources" do
sign_in user
not_yours = FactoryBot.create(:plant)
expect(not_yours.device_id).to_not eq(user.device_id)
@ -277,52 +301,50 @@ describe Api::SequencesController do
kind: "move_absolute",
args: {
location: {
kind: "point",
args: { pointer_type: "Plant", pointer_id: not_yours.id }
},
speed: 100,
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } }
}
}
kind: "point",
args: { pointer_type: "Plant", pointer_id: not_yours.id },
},
speed: 100,
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } },
},
},
],
}
post :create, body: input.to_json, params: {format: :json}
post :create, body: input.to_json, params: { format: :json }
expect(response.status).to eq(422)
expect(json[:body]).to include("Bad point ID")
end
it 'prevents type errors from bad identifier / binding combos' do
it "prevents type errors from bad identifier / binding combos" do
sign_in user
input = { name: "type mismatch",
args: {
locals: {
kind: "scope_declaration",
args: {},
body: [
{
kind: "parameter_declaration",
args: {
label: "parent",
default_value: { kind: "sync", 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}
args: {
locals: {
kind: "scope_declaration",
args: {},
body: [
{
kind: "parameter_declaration",
args: {
label: "parent",
default_value: { kind: "sync", 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.fetch(:body)).to include('"point"')
expect(json[:body]).to include("but got sync")
@ -331,27 +353,26 @@ describe Api::SequencesController do
it 'provides human readable errors for "nothing" mismatches' do
sign_in user
input = { name: "type mismatch",
args: {
locals: {
kind: "scope_declaration",
args: { },
body: [
{
kind: "parameter_declaration",
args: {
label: "x",
default_value: {
kind: "nothing",
args: {}
}
}
}
]
}
args: {
locals: {
kind: "scope_declaration",
args: {},
body: [
{
kind: "parameter_declaration",
args: {
label: "x",
default_value: {
kind: "nothing",
args: {},
},
body: [ ]
}
post :create, body: input.to_json, params: {format: :json}
},
},
],
},
},
body: [] }
post :create, body: input.to_json, params: { format: :json }
expect(response.status).to eq(422)
expect(json[:body]).to include("must provide a value for all parameters")
end