Merge pull request #1030 from RickCarlino/index_experiment

Done with Index stuff
pull/1031/head
Rick Carlino 2018-11-02 14:40:42 -05:00 committed by GitHub
commit 6821070a24
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 280 additions and 327 deletions

View File

@ -30,7 +30,7 @@ GEM
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
active_model_serializers (0.10.7)
active_model_serializers (0.10.8)
actionpack (>= 4.1, < 6)
activemodel (>= 4.1, < 6)
case_transform (>= 0.2)
@ -63,13 +63,14 @@ GEM
builder (3.2.3)
bunny (2.12.0)
amq-protocol (~> 2.3, >= 2.3.0)
capybara (3.9.0)
capybara (3.10.0)
addressable
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
xpath (~> 3.1)
regexp_parser (~> 1.2)
xpath (~> 3.2)
case_transform (0.2)
activesupport
childprocess (0.9.0)
@ -174,7 +175,7 @@ GEM
actionpack (>= 3.0)
activerecord (>= 3.0)
railties (>= 3.0)
loofah (2.2.2)
loofah (2.2.3)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.7.1)
@ -224,7 +225,7 @@ GEM
hashie (~> 3.5)
multi_json (~> 1.12)
rack (2.0.5)
rack-attack (5.4.1)
rack-attack (5.4.2)
rack (>= 1.0, < 3)
rack-cors (1.0.2)
rack-test (1.1.0)
@ -264,7 +265,8 @@ GEM
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
rake (12.3.1)
redis (4.0.2)
redis (4.0.3)
regexp_parser (1.2.0)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
@ -304,7 +306,7 @@ GEM
activerecord (>= 4.0.0)
railties (>= 4.0.0)
secure_headers (6.0.0)
selenium-webdriver (3.14.1)
selenium-webdriver (3.141.0)
childprocess (~> 0.5)
rubyzip (~> 1.2, >= 1.2.2)
signet (0.11.0)
@ -317,9 +319,9 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
skylight (3.1.0)
skylight-core (= 3.1.0)
skylight-core (3.1.0)
skylight (3.1.1)
skylight-core (= 3.1.1)
skylight-core (3.1.1)
activesupport (>= 4.2.0)
sprockets (3.7.2)
concurrent-ruby (~> 1.0)

View File

@ -7,7 +7,12 @@ module CeleryScript
BODY_HAS_NON_NODES = "The `body` of a node can only contain nodes- " \
"no leaves here."
LEAVES_NEED_KEYS = "Tried to initialize a leaf without a key."
def initialize(parent = nil, args:, body: nil, comment: "", kind:)
def initialize(parent = nil,
args:,
body: nil,
comment: "",
kind:,
uuid: nil)
@comment, @kind, @parent = comment, kind, parent
@args = args.map do |key, value|

View File

@ -30,16 +30,16 @@
},
"dependencies": {
"@blueprintjs/core": "^3.7.0",
"@blueprintjs/datetime": "^3.3.0",
"@blueprintjs/select": "^3.2.0",
"@blueprintjs/datetime": "^3.3.1",
"@blueprintjs/select": "^3.2.1",
"@types/enzyme": "3.1.14",
"@types/fastclick": "^1.0.28",
"@types/i18next": "^11.9.3",
"@types/jest": "^23.3.5",
"@types/jest": "^23.3.9",
"@types/lodash": "^4.14.117",
"@types/markdown-it": "0.0.6",
"@types/markdown-it": "0.0.7",
"@types/moxios": "^0.4.5",
"@types/node": "^10.12.0",
"@types/node": "^10.12.2",
"@types/react": "^16.4.18",
"@types/react-color": "2.13.6",
"@types/react-dom": "^16.0.9",
@ -49,7 +49,7 @@
"boxed_value": "^1.0.0",
"browser-speech": "1.1.1",
"coveralls": "3.0.2",
"css-loader": "1.0.0",
"css-loader": "^1.0.1",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.6.0",
"farmbot": "^6.5.7",
@ -68,14 +68,14 @@
"moxios": "^0.4.0",
"node-sass": "^4.9.4",
"optimize-css-assets-webpack-plugin": "5.0.1",
"raf": "^3.4.0",
"react": "16.5.2",
"raf": "^3.4.1",
"react": "^16.6.0",
"react-addons-test-utils": "^15.6.2",
"react-color": "2.14.1",
"react-dom": "16.5.2",
"react-dom": "^16.6.0",
"react-joyride": "^2.0.0-15",
"react-redux": "^5.0.6",
"react-test-renderer": "16.5.2",
"react-redux": "^5.1.0",
"react-test-renderer": "^16.6.0",
"react-transition-group": "2.5.0",
"redux": "^4.0.1",
"redux-immutable-state-invariant": "^2.1.0",
@ -88,11 +88,11 @@
"takeme": "^0.11.1",
"ts-jest": "^23.10.4",
"ts-lint": "^4.5.1",
"ts-loader": "^5.2.2",
"ts-loader": "^5.3.0",
"tslint": "5.11.0",
"typescript": "^3.1.3",
"typescript": "^3.1.6",
"url-loader": "^1.1.2",
"webpack": "^4.22.0",
"webpack": "^4.24.0",
"webpack-uglify-js-plugin": "1.1.9",
"which": "1.3.1"
},

View File

@ -0,0 +1,18 @@
import { Edge } from "./interfaces";
/** throttle calls to these functions to avoid unnecessary re-paints. */
const SLOWDOWN_TIME = 1500;
const lastCalledAt: Record<Edge, number> = {
"user.api": 0, "user.mqtt": 0, "bot.mqtt": 0
};
export function bumpThrottle(edge: Edge, now: number) {
lastCalledAt[edge] = now;
}
export function shouldThrottle(edge: Edge, now: number): boolean {
const then = lastCalledAt[edge];
const diff = then - now;
return diff > SLOWDOWN_TIME;
}

View File

@ -0,0 +1,18 @@
import { selectImage } from "../actions";
import { Actions } from "../../../constants";
describe("selectImage", () => {
it("selects one image", () => {
const payload = "my uuid";
const result = selectImage(payload);
expect(result.type).toEqual(Actions.SELECT_IMAGE);
expect(result.payload).toEqual(payload);
});
it("selects no image", () => {
const payload = undefined;
const result = selectImage(payload);
expect(result.type).toEqual(Actions.SELECT_IMAGE);
expect(result.payload).toEqual(payload);
});
});

View File

@ -6,8 +6,13 @@ jest.mock("../../util/stop_ie", () => {
return { stopIE: jest.fn() };
});
jest.mock("../../util",
() => ({ attachToRoot: jest.fn(), trim: (s: string) => s }));
import { detectLanguage } from "../../i18n";
import { stopIE } from "../../util/stop_ie";
import { attachToRoot } from "../../util";
import { FrontPage, attachFrontPage } from "../front_page";
describe("index", () => {
it("Attaches to the DOM", async () => {
@ -15,4 +20,9 @@ describe("index", () => {
expect(detectLanguage).toHaveBeenCalled();
expect(stopIE).toHaveBeenCalled();
});
it("attaches FrontPage to DOM specifically", () => {
attachFrontPage();
expect(attachToRoot).toHaveBeenCalledWith(FrontPage, {});
});
});

View File

@ -4,7 +4,7 @@ import { t } from "i18next";
import * as _ from "lodash";
import { error as log, success, init as logInit } from "farmbot-toastr";
import { AuthState } from "../auth/interfaces";
import { prettyPrintApiErrors } from "../util";
import { prettyPrintApiErrors, attachToRoot } from "../util";
import { API } from "../api";
import { Session } from "../session";
import { FrontPageState, SetterCB } from "./interfaces";
@ -15,6 +15,10 @@ import { ResendVerification } from "./resend_verification";
import { CreateAccount } from "./create_account";
import { Content } from "../constants";
export const attachFrontPage = () => {
attachToRoot(FrontPage, {});
};
const showFor = (size: string[], extraClass?: string): string => {
const ALL_SIZES = ["xs", "sm", "md", "lg", "xl"];
const HIDDEN_SIZES = ALL_SIZES.filter(x => !size.includes(x));

View File

@ -1,14 +1,9 @@
import { detectLanguage } from "../i18n";
import { FrontPage } from "./front_page";
import * as i18next from "i18next";
import "../css/_index.scss";
import { attachToRoot } from "../util";
import { stopIE } from "../util/stop_ie";
import { attachFrontPage } from "./front_page";
stopIE();
detectLanguage().then((config) => {
i18next.init(config, () => {
attachToRoot(FrontPage, {});
});
});
detectLanguage().then((config) => i18next.init(config, attachFrontPage));

View File

@ -1,13 +1,21 @@
import { findByUuid, joinKindAndId } from "../reducer_support";
import { fakeState } from "../../__test_support__/fake_state";
import { overwrite, refreshStart, refreshOK, refreshNO } from "../../api/crud";
import { SpecialStatus, TaggedSequence, TaggedDevice, ResourceName, TaggedResource } from "farmbot";
import {
SpecialStatus,
TaggedSequence,
TaggedDevice,
ResourceName,
TaggedResource,
TaggedTool
} from "farmbot";
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
import { GeneralizedError } from "../actions";
import { Actions } from "../../constants";
import { fakeResource } from "../../__test_support__/fake_resource";
import { resourceReducer } from "../reducer";
import { resourceReady } from "../../sync/actions";
import { EditResourceParams } from "../../api/interfaces";
describe("resource reducer", () => {
it("marks resources as DIRTY when reducing OVERWRITE_RESOURCE", () => {
@ -72,6 +80,36 @@ describe("resource reducer", () => {
TEST_RESOURCE_NAMES.map(kind => testResource(kind));
});
it("EDITs a _RESOURCE", () => {
const startingState = fakeState().resources;
const { index } = startingState;
const uuid = Object.keys(index.byKind.Tool)[0];
const update: Partial<TaggedTool["body"]> = { name: "after" };
const payload: EditResourceParams = {
uuid,
update,
specialStatus: SpecialStatus.SAVED
};
const action = { type: Actions.EDIT_RESOURCE, payload };
const newState = resourceReducer(startingState, action);
const oldTool = index.references[uuid] as TaggedTool;
const newTool = newState.index.references[uuid] as TaggedTool;
expect(oldTool.body.name).not.toEqual("after");
expect(newTool.body.name).toEqual("after");
});
it("handles resource failures", () => {
const startingState = fakeState().resources;
const uuid = Object.keys(startingState.index.byKind.Tool)[0];
const action = {
type: Actions._RESOURCE_NO,
payload: { uuid, err: "Whatever", statusBeforeError: SpecialStatus.DIRTY }
};
const newState = resourceReducer(startingState, action);
const tool = newState.index.references[uuid] as TaggedTool;
expect(tool.specialStatus).toBe(SpecialStatus.DIRTY);
});
it("covers destroy resource branches", () => {
const testResourceDestroy = (kind: ResourceName) => {

View File

@ -1,8 +1,8 @@
import { TaggedSequence, SpecialStatus } from "farmbot";
import { get } from "lodash";
import { maybeTagSteps, getStepTag } from "../sequence_tagging";
import { getStepTag, setStepTag } from "../sequence_tagging";
describe("maybeTagSteps()", () => {
describe("tagAllSteps()", () => {
const UNTAGGED_SEQUENCE: TaggedSequence = {
"kind": "Sequence",
"uuid": "whatever",
@ -36,7 +36,7 @@ describe("maybeTagSteps()", () => {
expect(() => {
getStepTag(body[0]);
}).toThrow();
maybeTagSteps(UNTAGGED_SEQUENCE);
setStepTag(body[0]);
expect(get(body[0], "uuid")).toBeDefined();
});
});

View File

@ -1,5 +1,5 @@
import { fakeTool } from "../../__test_support__/fake_state/resources";
import { getArrayStatus } from "../tagged_resources";
import { getArrayStatus, sanityCheck } from "../tagged_resources";
import { SpecialStatus } from "farmbot";
describe("getArrayStatus()", () => {
@ -28,3 +28,9 @@ describe("getArrayStatus()", () => {
expect(getArrayStatus(arr)).toBe(SpecialStatus.SAVED);
});
});
describe("sanityCheck", () => {
it("crashes on malformed TaggedResources", () => {
expect(() => sanityCheck({})).toThrow("Bad kind, uuid, or body: {}");
});
});

View File

@ -15,11 +15,17 @@ import { HelpState } from "../help/reducer";
type UUID = string;
export interface SequenceVariableMeta {
label: string;
kind: "Point"
}
export interface ResourceIndex {
all: Record<UUID, UUID>;
all: Record<UUID, true>;
byKind: Record<ResourceName, Record<UUID, UUID>>;
byKindAndId: CowardlyDictionary<UUID>;
references: Dictionary<TaggedResource | undefined>;
sequenceMeta: Record<UUID, SequenceVariableMeta[]>;
}
export interface RestResources {

View File

@ -76,7 +76,8 @@ export const emptyState = (): RestResources => {
DiagnosticDump: {}
},
byKindAndId: {},
references: {}
references: {},
sequenceMeta: {}
}
};
};

View File

@ -1,9 +1,8 @@
import { ResourceIndex } from "./interfaces";
import { TaggedResource } from "farmbot";
import { joinKindAndId } from "./reducer_support";
import { maybeTagSteps } from "./sequence_tagging";
import {
recomputeLocalVarDeclaration
performAllTransformsOnSequence
} from "../sequences/step_tiles/tile_move_absolute/variables_support";
type IndexDirection = "up" | "down";
@ -16,7 +15,7 @@ const REFERENCES: Indexer = {
};
const ALL: Indexer = {
up: (r, s) => s.all[r.uuid] = r.uuid,
up: (r, s) => s.all[r.uuid] = true,
down: (r, i) => delete i.all[r.uuid],
};
@ -37,15 +36,24 @@ const BY_KIND_AND_ID: Indexer = {
};
const SEQUENCE_STUFF: Indexer = {
up(r) {
up(r, _i) {
if (r.kind === "Sequence") {
const recomputed = recomputeLocalVarDeclaration(r.body);
r.body.args = recomputed.args;
r.body.body = recomputed.body;
maybeTagSteps(r);
performAllTransformsOnSequence(r.body);
// const locals = (r.body.args.locals.body || []);
// i.sequenceMeta[r.uuid] = locals.map((local): SequenceVariableMeta => {
// switch (local.kind) {
// case "parameter_declaration":
// case "variable_declaration":
// return {
// label: local.args.label,
// kind: local.args.data_type
// };
// }
// });
}
},
down(_r, _i) {
down(r, i) {
delete i.sequenceMeta[r.uuid];
},
};

View File

@ -1,11 +1,9 @@
import {
TaggedResource,
TaggedToolSlotPointer,
TaggedSequence,
TaggedRegimen,
TaggedFarmEvent,
TaggedTool,
TaggedPoint
} from "farmbot";
import { CowardlyDictionary } from "../util";
import {
@ -13,7 +11,6 @@ import {
SlotWithTool
} from "./interfaces";
import {
selectAllTools,
selectAllToolSlotPointers,
maybeFindToolById
} from "./selectors";
@ -28,8 +25,6 @@ interface Indexer<T extends TaggedResource> {
interface MapperFn<T extends TaggedResource> { (item: T): T | undefined; }
type StringMap = CowardlyDictionary<string>;
/** Build a function,
* that returns a function,
* that returns a dictionary,
@ -57,26 +52,11 @@ export const buildIndexer =
};
};
const slotMapper = (i: TaggedPoint): TaggedToolSlotPointer | undefined => {
if (i.kind == "Point" && (i.body.pointer_type === "ToolSlot")) {
return i as TaggedToolSlotPointer;
}
return undefined;
};
export const indexBySlotId =
buildIndexer<TaggedToolSlotPointer>("Point", slotMapper);
export const indexSequenceById = buildIndexer<TaggedSequence>("Sequence");
export const indexRegimenById = buildIndexer<TaggedRegimen>("Regimen");
export const indexFarmEventById = buildIndexer<TaggedFarmEvent>("FarmEvent");
export const indexByToolId = buildIndexer<TaggedTool>("Tool");
export function mapToolIdToName(input: ResourceIndex) {
return selectAllTools(input)
.map(x => ({ key: "" + x.body.id, val: x.body.name }))
.reduce((x, y) => ({ ...{ [y.key]: y.val, ...x } }), {} as StringMap);
}
/** For those times that you need to ref a tool and slot together. */
export function joinToolsAndSlot(index: ResourceIndex): SlotWithTool[] {
return selectAllToolSlotPointers(index)

View File

@ -1,6 +1,8 @@
import { get, set } from "lodash";
import { SequenceBodyItem, uuid } from "farmbot/dist";
import { TaggedResource } from "farmbot";
import {
Traversable
} from "../sequences/step_tiles/tile_move_absolute/variables_support";
/** HISTORICAL NOTES:
* This file is the result of some very subtle bugs relating to dynamic
@ -50,6 +52,9 @@ export type StepTag = string;
/** Property name where a unique ID is stored in a step. */
const TAG_PROP = "uuid";
export const setStepTag =
(node: Traversable) => !get(node, TAG_PROP) && set(node, TAG_PROP, uuid());
/** VERY IMPORTANT FUNCTION.
* SEE HEADER AT TOP OF FILE.
* Retrieves tag from a step object. Assumes that all steps have a tag.
@ -59,18 +64,3 @@ export function getStepTag(i: SequenceBodyItem): StepTag {
if (tag) { return tag; }
throw new Error("No tag on step: " + i.kind);
}
/** Idempotently add a `uuid` property to a step. */
export let setStepTag = (i: SequenceBodyItem) => {
set(i, TAG_PROP, uuid());
};
/** Idempotently add a `uuid` property to all steps in an array. */
export let tagAllSteps = (i: SequenceBodyItem[]) => i.map(setStepTag);
/** REALLY IMPORTANT SEE FILE HEADER FOR MORE INFO! -RC
* Used by Redux within the `resource` reducer. Given a TaggedResource,
* idempotently adds `UUID` property to all steps in all sequences. */
export function maybeTagSteps(x: TaggedResource) {
if (x && (x.kind === "Sequence")) { tagAllSteps(x.body.body || []); }
}

View File

@ -3,14 +3,12 @@ import { betterCompact } from "../util";
import * as _ from "lodash";
import { assertUuid } from "./util";
import {
TaggedCrop,
TaggedResource,
ResourceName,
TaggedRegimen,
TaggedSequence,
TaggedTool,
TaggedFarmEvent,
TaggedLog,
TaggedToolSlotPointer,
TaggedPlantPointer,
TaggedGenericPointer,
@ -87,12 +85,8 @@ export let isTaggedSequence =
(x: object): x is TaggedSequence => is("Sequence")(x);
export let isTaggedTool =
(x: object): x is TaggedTool => is("Tool")(x);
export let isTaggedCrop =
(x: object): x is TaggedCrop => is("Crop")(x);
export let isTaggedFarmEvent =
(x: object): x is TaggedFarmEvent => is("FarmEvent")(x);
export let isTaggedLog =
(x: object): x is TaggedLog => is("Log")(x);
export let isTaggedToolSlotPointer =
(x: object): x is TaggedToolSlotPointer => {
return isTaggedPoint(x) && (x.body.pointer_type === "ToolSlot");

View File

@ -3,16 +3,16 @@ import { AllSteps } from "../all_steps";
import { buildResourceIndex } from "../../__test_support__/resource_index_builder";
import { shallow } from "enzyme";
import { TaggedSequence, SpecialStatus } from "farmbot";
import { maybeTagSteps } from "../../resources/sequence_tagging";
import { TileMoveRelative } from "../step_tiles/tile_move_relative";
import { TileReadPin } from "../step_tiles/tile_read_pin";
import { TileWritePin } from "../step_tiles/tile_write_pin";
import { performAllTransformsOnSequence } from "../step_tiles/tile_move_absolute/variables_support";
describe("<AllSteps/>", () => {
const TEST_CASE = {
const TEST_CASE: TaggedSequence = {
"kind": "Sequence",
"specialStatus": SpecialStatus.SAVED,
"body": {
"body": performAllTransformsOnSequence({
"id": 8,
"name": "Goto 0, 0, 0",
"color": "gray",
@ -45,16 +45,12 @@ describe("<AllSteps/>", () => {
],
"args": {
"locals": { kind: "scope_declaration", args: {} },
"is_outdated": false,
"version": 4,
"label": "WIP"
},
"kind": "sequence"
},
}),
"uuid": "Sequence.8.52"
} as TaggedSequence;
maybeTagSteps(TEST_CASE);
};
it("uses index as a key", () => {
const el = shallow(<AllSteps

View File

@ -1,150 +1,35 @@
import { Sequence, MoveAbsolute } from "farmbot";
import { collectAllVariables, recomputeLocalVarDeclaration } from "../variables_support";
import { fakeSequence } from "../../../../__test_support__/fake_state/resources";
import { MoveAbsolute } from "farmbot";
import { performAllTransformsOnSequence } from "../variables_support";
import { get } from "lodash";
const PARENT: MoveAbsolute = {
kind: "move_absolute",
args: {
location: { kind: "identifier", args: { label: "parent" } },
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } },
speed: 100
}
};
describe("performAllIndexesOnSequence", () => {
const move_abs: MoveAbsolute = {
kind: "move_absolute",
args: {
location: { kind: "identifier", args: { label: "parent" } },
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } },
speed: 800
}
};
const PARENT2: MoveAbsolute = {
kind: "move_absolute",
args: {
location: { kind: "identifier", args: { label: "parent2" } },
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } },
speed: 100
}
};
/** Another random variable. */
const Q: MoveAbsolute = {
kind: "move_absolute",
args: {
location: { kind: "identifier", args: { label: "q" } },
offset: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } },
speed: 100
}
};
const EMPTY_SEQ_ARGS: Sequence["args"] = {
version: 6,
locals: { kind: "scope_declaration", args: {} }
};
// USE CASE 1: Empty / new sequence.
const sequence1: Sequence = { kind: "sequence", args: EMPTY_SEQ_ARGS, body: [] };
// USE CASE 2: Single variable
const sequence2: Sequence = { kind: "sequence", args: EMPTY_SEQ_ARGS, body: [PARENT] };
// USE CASE 3: Multiple unique variables
const sequence3: Sequence = {
kind: "sequence",
args: EMPTY_SEQ_ARGS,
body: [PARENT, PARENT2]
};
// USE CASE 4: Duplicate variables
const sequence4: Sequence = {
kind: "sequence",
args: EMPTY_SEQ_ARGS,
body: [PARENT, PARENT]
};
// USE CASE 5: preexisting variables
const sequence5: Sequence = {
kind: "sequence",
args: {
version: 6,
locals: {
it("fills in missing information related to variables", () => {
const missing_declaration = fakeSequence().body;
expect(missing_declaration.args.locals.body).toBeUndefined();
missing_declaration.body = [move_abs];
missing_declaration.args.locals = {
kind: "scope_declaration",
args: {},
body: [
{
kind: "parameter_declaration",
args: { label: "foo", data_type: "point" }
}
]
body: []
};
performAllTransformsOnSequence(missing_declaration);
const locals = missing_declaration.args.locals.body;
if (locals) {
expect(locals[0]).toBeDefined();
expect(get(locals[0], "uuid")).toBeDefined();
expect(locals[0].args.label).toEqual("parent");
} else {
fail("Expected performAllIndexesOnSequence to fill in missing declarations");
}
},
body: [PARENT, Q]
};
// USE CASE 6: Empty scope_declaration
const sequence6: Sequence = {
kind: "sequence",
args: { version: 6, locals: { kind: "scope_declaration", args: {} } },
body: [PARENT, Q]
};
// USE CASE 7:
const sequence7: Sequence = {
kind: "sequence",
args: { version: 6, locals: { kind: "scope_declaration", args: {} } },
body: []
};
describe("collectIdentifiers", () => {
it("doesnt blow up on empty sequences", () => {
const results = collectAllVariables(sequence1);
expect(results.length).toBe(0);
});
it("finds a single identifier", () => {
const results = collectAllVariables(sequence2);
expect(results.length).toBe(1);
expect(results[0].args.label).toBe("parent");
});
it("finds multiple identifiers", () => {
const results = collectAllVariables(sequence3);
expect(results.length).toBe(2);
expect(results[0].args.label).toBe("parent");
expect(results[1].args.label).toBe("parent2");
});
it("removes duplicates", () => {
const results = collectAllVariables(sequence4);
expect(results.length).toBe(1);
expect(results[0].args.label).toBe("parent");
});
});
describe("recomputeLocals", () => {
it("recomputes locals (multiple use of same var)", () => {
const result = recomputeLocalVarDeclaration(sequence4);
expect(result.args.locals.kind).toBe("scope_declaration");
expect((result.args.locals.body || []).length).toBe(1);
});
it("recomputes locals (multiple variables)", () => {
const result = recomputeLocalVarDeclaration(sequence3);
expect(result.args.locals.kind).toBe("scope_declaration");
expect((result.args.locals.body || []).length).toBe(2);
});
it("recomputes locals (squash old variables)", () => {
const result = recomputeLocalVarDeclaration(sequence5);
expect(result.args.locals.kind).toBe("scope_declaration");
expect((result.args.locals.body || []).length).toBe(2);
const labels = (result.args.locals.body || []).map(x => x.args.label);
expect(labels).toContain("q");
});
it("Doesn't crash on empty arrays in `locals`", () => {
const result = recomputeLocalVarDeclaration(sequence6);
expect(result.args.locals.kind).toBe("scope_declaration");
expect((result.args.locals.body || []).length).toBe(2);
const labels = (result.args.locals.body || []).map(x => x.args.label);
expect(labels).toContain("q");
});
it("Doesn't crash on variable-less sequences", () => {
const result = recomputeLocalVarDeclaration(sequence7);
expect(result.args.locals.kind).toBe("scope_declaration");
expect((result.args.locals.body || []).length).toBe(0);
});
});

View File

@ -1,105 +1,102 @@
import { get } from "lodash";
import {
Identifier,
Dictionary,
Sequence,
Nothing,
ScopeDeclarationBodyItem
Identifier,
ScopeDeclarationBodyItem,
VariableDeclaration
} from "farmbot";
import { get, uniqBy } from "lodash";
import { defensiveClone } from "../../../util";
import {
SequenceResource as Sequence
} from "farmbot/dist/resources/api_resources";
import { setStepTag } from "../../../resources/sequence_tagging";
/** A less strict version of a CeleryScript node used for
* the sake of recursion. */
interface Traversable {
kind: string;
args: Args;
body?: Body;
}
/** Junk that we don't care about on a celery script node. */
type Other = string | number | object;
/** Less strict version of CeleryScript `args`-
* It's either traversable, or we don't care. */
type Args = Dictionary<Traversable | Other>;
// ======= TYPE DECLARATIONS =======
/** Less strict version of CeleryScript args. It's traversable, or unknown. */
type Args = Dictionary<Traversable | unknown>;
type Body = Traversable[] | undefined;
/** Accumulator for collecting identifiers found while recursing. */
type Accum = Identifier[];
/** Less strict CeleryScript node used for the sake of recursion. */
export interface Traversable { kind: string; args: Args; body?: Body; }
type TreeClimberCB = (item: Traversable) => void;
// ======= END TYPE DECLARATIONS =======
export const NOTHING: Nothing = { kind: "nothing", args: {} };
/** Is it a variable (identifier)? */
const isIdentifier =
(x: Traversable): x is Identifier => (x.kind === "identifier");
// ======= CONST / LITERAL / DYNAMIC KEY DECLARATIONS =======
const ARGS = "args";
const IDENTIFIER = "identifier";
const KIND = "kind";
const OBJECT = "object";
const STRING = "string";
// ======= END CONST / LITERAL DECLARATIONS =======
/** Is it a fully-formed CeleryScript node? Can we continue recursing? */
const isTraversable = (x: unknown): x is Traversable => {
const kind: string | undefined = get(x, "kind");
const args: object | undefined = get(x, "args");
return !!((typeof kind == "string") && args && typeof args == "object");
const hasKind = typeof get(x, KIND, -1) == STRING;
const hasArgs = typeof get(x, ARGS, -1) == OBJECT;
return hasKind && hasArgs;
};
/** Is it an _identifier_ node? Put it in the array if so.
* If it is some other node type, continue recursion. */
const maybeCollect =
(x: Traversable, y: Accum) => isIdentifier(x) ? y.push(x) : traverse(y)(x);
/** Is it a variable (identifier)? */
const isIdentifier =
(x: Traversable): x is Identifier => (x.kind === IDENTIFIER);
/** Maybe recurse into each leg of node.args */
const traverseArgs = (input: Args, accumulator: Accum) => {
const keys = Object.keys(input);
keys.map(key => {
const value = input[key];
isTraversable(value) && maybeCollect(value, accumulator);
});
};
/** Iterate over node.body and perform recursion on each child node. */
const traverseBody = (input: Body, accumulator: Accum) => {
input && input.map(traverse(accumulator));
};
/** Recurse into every leg of node.args and node.body, pushing all `identifier`
* nodes into the `acc` array. */
const traverse = (acc: Accum = []) => (input: unknown): Accum => {
if (isTraversable(input)) {
traverseArgs(input.args, acc);
traverseBody(input.body, acc);
const newVar = (label: string): VariableDeclaration => ({
kind: "variable_declaration",
args: {
label,
data_value: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } }
}
});
return acc;
};
/** This function currently has two responsibilities:
* 1. Recursively tag all CeleryScript nodes with a `uuid` property to
* prevent subtle React issues.
* 2. "fill in the blanks" for variables. Example: If a move abs. step
* references `parent`, but sequence.args.locals.body does not contain a
* `parent` declaration, that could be very bad.
*
* SEE ALSO: Huge explanation in `sequence_tagging.ts` */
export const performAllTransformsOnSequence = (input: Sequence): Sequence => {
// Ideally, we want to be able to blindly insert identifiers into any part of
// a sequence and have said identifier show up in the `scope_declaration`.
/** Used to remove duplicates */
const iteratee = (x: Identifier) => x.args["label"];
const actuallyUsed: Dictionary<Identifier> = {};
const declared: Dictionary<ScopeDeclarationBodyItem> = {};
const body = (input.args.locals.body = input.args.locals.body || []);
input.args.locals.body.map(item => declared[item.args.label] = item);
const updateDeclarations = (node: Identifier) => {
const varName = node.args.label;
// STEP 1: Collect all the stuff thats been _declared_.
actuallyUsed[varName] = node;
/** Collect ever `identifier` CeleryScript node in a sequence. */
export const collectAllVariables =
(sequence: Sequence) => uniqBy(traverse([])(sequence), iteratee);
const generateDeclarationsFromIdentifiers = (s: Sequence) => {
const { locals } = s.args;
const lookup: Dictionary<ScopeDeclarationBodyItem | undefined> = {};
(locals.body || []).map(x => (lookup[x.args.label] = x));
return (identifier: Identifier): ScopeDeclarationBodyItem => {
return lookup[identifier.args.label] || {
kind: "variable_declaration",
args: {
label: identifier.args.label,
data_value: { kind: "coordinate", args: { x: 0, y: 0, z: 0 } }
}
};
/** Scenario: You referenced a variable, but it does not
* exist in `seq.args.locals`. */
if (!declared[varName]) {
// STEP 2: Collect all stuff that's been _referenced_.
// If it's not already in the sequence.args, declare it as an empty var.
const declaration = newVar(varName);
declared[varName] = declaration;
setStepTag(declaration);
body.push(declaration);
}
};
};
/** Calculate the next value of sequence.arg.locals given a *new* list of
* variables for a sequence. */
export const recomputeLocalVarDeclaration = (input: Sequence): Sequence => {
const output = defensiveClone(input);
const identifiers = collectAllVariables(output);
const body = identifiers.map(generateDeclarationsFromIdentifiers(input));
input.args.locals = { kind: "scope_declaration", args: {}, body };
climb(input, node => {
setStepTag(node);
isIdentifier(node) && updateDeclarations(node);
});
return input;
};
export function climb(t: Traversable | unknown, cb: TreeClimberCB) {
const climbArgs = /** RECURSION ALERT! */
(a: Args) => Object.keys(a).map(arg => climb(a[arg], cb));
const climbBody = /** WEE OOO WEE OO */
(body: Body = []) => body.map(item => climb(item, cb));
if (isTraversable(t)) {
t.body = t.body || [];
climbArgs(t.args);
climbBody(t.body);
cb(t);
}
}