refactor and test sequence farm events

pull/579/head
gabrielburnworth 2017-12-15 02:28:08 -08:00
parent 582beaf940
commit 7b96ddd896
5 changed files with 150 additions and 63 deletions

View File

@ -134,7 +134,8 @@
"html",
"json",
"lcov"
]
],
"setupTestFrameworkScriptFile": "<rootDir>/webpack/__test_support__/customMatchers.js"
},
"engines": {
"node": ">=8.9.0"

View File

@ -0,0 +1,31 @@
const diff = require('jest-diff');
expect.extend({
toBeSameTimeAs(received, expected) {
const pass = received.isSame(expected);
const message = pass
? () =>
this.utils.matcherHint('.not.toBeSameTimeAs') +
'\n\n' +
`Expected time to not be (using moment.isSame):\n` +
` ${this.utils.printExpected(expected)}\n` +
`Received:\n` +
` ${this.utils.printReceived(received)}`
: () => {
const diffString = diff(expected, received, {
expand: this.expand,
});
return (
this.utils.matcherHint('.toBeSameTimeAs') +
'\n\n' +
`Expected time to be (using moment.isSame):\n` +
` ${this.utils.printExpected(expected)}\n` +
`Received:\n` +
` ${this.utils.printReceived(received)}` +
(diffString ? `\n\nDifference:\n\n${diffString}` : '')
);
};
return { actual: received, message, pass };
},
});

View File

@ -1,6 +1,7 @@
import { scheduler, scheduleForFarmEvent, TimeLine, farmEventIntervalSeconds } from "../scheduler";
import * as moment from "moment";
import { TimeUnit } from "../../../interfaces";
import { Moment } from "moment";
describe("scheduler", () => {
it("runs every 4 hours, starting Tu, until Th w/ origin of Mo", () => {
@ -10,16 +11,16 @@ describe("scheduler", () => {
.startOf("isoWeek")
.startOf("day")
.add(8, "hours");
// 3am Tuesday
const tuesday = monday.clone().add(19, "hours");
// 4am Tuesday
const tuesday = monday.clone().add(20, "hours");
// 18pm Thursday
const thursday = monday.clone().add(3, "days").add(10, "hours");
const interval = moment.duration(4, "hours").asSeconds();
const result1 = scheduler({
originTime: monday,
currentTime: monday,
intervalSeconds: interval,
lowerBound: tuesday,
upperBound: thursday
startTime: tuesday,
endTime: thursday
});
expect(result1[0].format("dd")).toEqual("Tu");
expect(result1[0].hour()).toEqual(4);
@ -46,26 +47,77 @@ describe("scheduler", () => {
EXPECTED.map(x => expect(REALITY).toContain(x));
});
it("handles 0 as a repeat value? What happens?");
});
function testSchedule(
description: string, fakeEvent: TimeLine, expected: Moment[]) {
it(description, () => {
const result = scheduleForFarmEvent(fakeEvent);
expect(result.length).toEqual(expected.length);
expected.map((expectation, index) => {
expect(result[index]).toBeSameTimeAs(expectation);
});
});
}
it("schedules a FarmEvent", () => {
const fakeEvent: TimeLine = {
"start_time": "2017-08-01T17:30:00.000Z",
"end_time": "2017-08-07T05:00:00.000Z",
"repeat": 2,
"time_unit": "daily",
};
const EXPECTED = [
moment("2017-08-01T17:30:00.000Z"),
moment("2017-08-03T17:30:00.000Z"),
moment("2017-08-05T17:30:00.000Z")
];
const result = scheduleForFarmEvent(fakeEvent);
expect(result.length).toEqual(3);
EXPECTED.map((expectation, index) => {
expect(expectation.isSame(result[index])).toBeTruthy();
});
testSchedule("schedules a FarmEvent",
{
"start_time": "2017-08-01T17:30:00.000Z",
"end_time": "2017-08-07T05:00:00.000Z",
"repeat": 2,
"time_unit": "daily",
current_time: "2017-08-01T16:30:00.000Z"
},
[
moment("2017-08-01T17:30:00.000Z"),
moment("2017-08-03T17:30:00.000Z"),
moment("2017-08-05T17:30:00.000Z")
]);
testSchedule("handles 0 as a repeat value",
{
"start_time": "2017-08-01T17:30:00.000Z",
"end_time": "2017-08-07T05:00:00.000Z",
"repeat": 0,
"time_unit": "daily",
current_time: "2017-08-01T16:30:00.000Z"
},
[moment("2017-08-01T17:30:00.000Z")]);
testSchedule("handles start_time in the past",
{
"start_time": "2017-08-01T17:30:00.000Z",
"end_time": "2017-08-09T05:00:00.000Z",
"repeat": 2,
"time_unit": "daily",
current_time: "2017-08-03T18:30:00.000Z"
},
[
moment("2017-08-05T17:30:00.000Z"),
moment("2017-08-07T17:30:00.000Z")
]);
testSchedule("uses grace period",
{
"start_time": "2017-08-01T17:30:00.000Z",
"end_time": "2017-08-02T05:00:00.000Z",
"repeat": 4,
"time_unit": "hourly",
current_time: "2017-08-01T17:30:30.000Z"
},
[
moment("2017-08-01T17:30:00.000Z"),
moment("2017-08-01T21:30:00.000Z"),
moment("2017-08-02T01:30:00.000Z")
]);
testSchedule("farm event over",
{
"start_time": "2017-08-01T17:30:00.000Z",
"end_time": "2017-08-02T05:00:00.000Z",
"repeat": 4,
"time_unit": "hourly",
current_time: "2017-08-03T17:30:30.000Z"
},
[]);
});
describe("farmEventIntervalSeconds", () => {

View File

@ -5,41 +5,43 @@ import { TimeUnit } from "../../interfaces";
import { NEVER } from "../edit_fe_form";
interface SchedulerProps {
originTime: Moment;
startTime: Moment;
currentTime: Moment;
endTime: Moment;
intervalSeconds: number;
lowerBound: Moment;
upperBound?: Moment;
}
const nextYear = () => moment(moment().add(1, "year"));
export function scheduler({ originTime,
intervalSeconds,
lowerBound,
upperBound }: SchedulerProps): Moment[] {
if (!intervalSeconds) { // 0, NaN and friends
return [originTime];
export function scheduler({
startTime,
currentTime,
endTime,
intervalSeconds
}: SchedulerProps): Moment[] {
if (currentTime > endTime) {
return []; // Farm event is over
}
upperBound = upperBound || nextYear();
// # How many items must we skip to get to the first occurence?
const skip_intervals =
Math.ceil((lowerBound.unix() - originTime.unix()) / intervalSeconds);
// # At what time does the first event occur?
const first_item = originTime
.clone()
.add((skip_intervals * intervalSeconds), "seconds");
const list = [first_item];
times(60, () => {
const x = last(list);
if (x) {
const item = x.clone().add(intervalSeconds, "seconds");
if (item.isBefore(upperBound)) {
list.push(item);
const timeSinceStart = currentTime.unix() - startTime.unix();
const itemsMissed = Math.floor(timeSinceStart / intervalSeconds);
const nextItemTime = startTime.clone()
.add((itemsMissed * intervalSeconds), "seconds");
const scheduledItems = [
timeSinceStart > 0
? nextItemTime // start time in the past
: startTime]; // start time in the future
times(60, () => { // get next 60 or so calendar items
const previousItem = last(scheduledItems);
if (previousItem) {
const nextItem = previousItem.clone().add(intervalSeconds, "seconds");
if (nextItem.isBefore(endTime)) {
scheduledItems.push(nextItem);
}
}
});
return list;
// Match FarmBot OS calendar item execution grace period
const gracePeriodCutoffTime = currentTime.subtract(1, "minutes");
return scheduledItems.filter(m => m.isAfter(gracePeriodCutoffTime));
}
/** Translate farmbot interval names to momentjs interval names */
@ -55,7 +57,7 @@ const LOOKUP: Record<TimeUnit, unitOfTime.Base> = {
/** GIVEN: A time unit (hourly, weekly, etc) and a repeat (number)
* RETURNS: Number of seconds for interval.
* EXAMPLE: f(2, "minutely") => 120;
* EXAMPLE: f(2, "minutely") => 120; // "Every two minutes"
*/
export function farmEventIntervalSeconds(repeat: number, unit: TimeUnit) {
const momentUnit = LOOKUP[unit];
@ -75,19 +77,21 @@ export interface TimeLine {
start_time: string;
/** ISO string */
end_time?: string | undefined;
current_time?: string;
}
/** Takes a subset of FarmEvent<Sequence> data and generates a list of dates. */
export function scheduleForFarmEvent({ start_time, end_time, repeat, time_unit }:
TimeLine): Moment[] {
const i = repeat && farmEventIntervalSeconds(repeat, time_unit);
if (i && (time_unit !== NEVER)) {
const hmm = scheduler({
originTime: moment(start_time),
lowerBound: moment(start_time),
upperBound: end_time ? moment(end_time) : nextYear(),
intervalSeconds: i
export function scheduleForFarmEvent(
{ start_time, end_time, repeat, time_unit, current_time = moment() }: TimeLine
): Moment[] {
const interval = repeat && farmEventIntervalSeconds(repeat, time_unit);
if (interval && (time_unit !== NEVER)) {
const schedule = scheduler({
startTime: moment(start_time),
currentTime: moment(current_time),
endTime: end_time ? moment(end_time) : nextYear(),
intervalSeconds: interval
});
return hmm;
return schedule;
} else {
return [moment(start_time)];
}

View File

@ -59,6 +59,5 @@ export let regimenCalendarAdder = (index: ResourceIndex) =>
export let addSequenceToCalendar =
(f: FarmEventWithSequence, c: Calendar, now = moment()) => {
scheduleForFarmEvent(f)
.filter(m => m.isAfter(now))
.map(m => c.insert(occurrence(m, f)));
};