refactor and test sequence farm events
parent
582beaf940
commit
7b96ddd896
|
@ -134,7 +134,8 @@
|
|||
"html",
|
||||
"json",
|
||||
"lcov"
|
||||
]
|
||||
],
|
||||
"setupTestFrameworkScriptFile": "<rootDir>/webpack/__test_support__/customMatchers.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
|
|
|
@ -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 };
|
||||
},
|
||||
});
|
|
@ -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", () => {
|
||||
|
|
|
@ -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)];
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue