From 358f33b18af94608cf7d3d5254a9bb11dac3098c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Fra=C3=9F?= Date: Fri, 27 Jun 2025 20:07:18 +0000 Subject: [PATCH 1/4] [task-340] [int] --- ivaldi.json | 2 + source/logic.ts | 300 +++++++++++++++++++++++++++++++++++++ source/main.ts | 258 ++++++++------------------------ source/test.ts | 384 ++++++++++++++++++++++++++++++++++++++++++++++++ source/types.ts | 32 +++- 5 files changed, 779 insertions(+), 197 deletions(-) create mode 100644 source/logic.ts create mode 100644 source/test.ts diff --git a/ivaldi.json b/ivaldi.json index bcedb2f..b3a009a 100644 --- a/ivaldi.json +++ b/ivaldi.json @@ -27,6 +27,8 @@ "targets/email.ts", "targets/_functions.ts", "conf.ts", + "logic.ts", + "test.ts", "main.ts" ] } diff --git a/source/logic.ts b/source/logic.ts new file mode 100644 index 0000000..693811f --- /dev/null +++ b/source/logic.ts @@ -0,0 +1,300 @@ +/* +This file is part of »munin«. + +Copyright 2025 'Fenris Wolf' + +»munin« is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +»munin« is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with »munin«. If not, see . + */ + + +namespace _munin.logic +{ + + /** + * @todo Tests schreiben + */ + function shall_remind( + event : _munin.type_event, + reminder : type_reminder, + { + "pit": pit = lib_plankton.pit.now(), + "interval": interval = 1, + } : { + pit ?: lib_plankton.pit.type_pit; + interval ?: int; + } = { + } + ) : boolean + { + const window_from : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( + pit, + 0 + ); + const window_to : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( + pit, + interval + ); + const event_begin : lib_plankton.pit.type_pit = lib_plankton.pit.from_datetime( + event.begin + ); + const reminder_time : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( + event_begin, + (-reminder) + ); + return lib_plankton.pit.is_between( + reminder_time, + window_from, + window_to + ); + } + + + /** + */ + function frequency_anchor( + frequency : _munin.enum_frequency, + { + "pit": pit = lib_plankton.pit.now(), + } : { + pit ?: lib_plankton.pit.type_pit, + } = { + } + ) : lib_plankton.pit.type_pit + { + switch (frequency) + { + case _munin.enum_frequency.hourly: return lib_plankton.pit.trunc_hour(pit); + case _munin.enum_frequency.daily: return lib_plankton.pit.trunc_day(pit); + case _munin.enum_frequency.weekly: return lib_plankton.pit.trunc_week(pit); + case _munin.enum_frequency.monthly: return lib_plankton.pit.trunc_month(pit); + default: + { + throw (new Error("unhandled frequency: " + frequency)); + break; + } + } + } + + + /** + */ + export function reminder_due( + reminder : type_reminder_new, + { + "pit": pit = lib_plankton.pit.now(), + "interval": interval = 1, + } : { + pit ?: lib_plankton.pit.type_pit; + interval ?: int; + } = { + } + ) : boolean + { + const anchor : lib_plankton.pit.type_pit = frequency_anchor( + reminder.frequency, + {"pit": pit} + ); + // 0 ≤ ((p - a(p)) - o) < i + const x : float = ( + ( + ( + lib_plankton.pit.to_unix_timestamp(pit) + - + lib_plankton.pit.to_unix_timestamp(anchor) + ) + / + (60 * 60) + ) + - + reminder.offset + ); + return ((0 <= x) && (x < interval)); + } + + + /** + */ + export function reminder_event_in_window( + reminder : type_reminder_new, + event : _munin.type_event, + { + "pit": pit = lib_plankton.pit.now(), + "interval": interval = 1, + } : { + pit ?: lib_plankton.pit.type_pit; + interval ?: int; + } = { + } + ) : boolean + { + const anchor : lib_plankton.pit.type_pit = frequency_anchor( + reminder.frequency, + {"pit": pit} + ); + const window_from : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( + anchor, + reminder.from + ); + const window_to : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( + anchor, + reminder.to + ); + const event_begin : lib_plankton.pit.type_pit = lib_plankton.pit.from_datetime( + event.begin + ); + return lib_plankton.pit.is_between( + event_begin, + window_from, + window_to + ); + } + + + /** + */ + function shall_remind_new( + reminder : type_reminder_new, + event : _munin.type_event, + { + "pit": pit = lib_plankton.pit.now(), + "interval": interval = 1, + } : { + pit ?: lib_plankton.pit.type_pit; + interval ?: int; + } = { + } + ) : boolean + { + return ( + reminder_due( + reminder, + {"pit": pit, "interval": interval} + ) + && + reminder_event_in_window( + reminder, + event, + {"pit": pit, "interval": interval} + ) + ); + } + + + /** + */ + async function run_iteration( + conf : _munin.conf.type_conf, + sources : Array<_munin.type_source>, + targets : Array<_munin.type_target>, + { + "dry_run": dry_run = false, + } : { + dry_run ?: boolean; + } = { + } + ) : Promise + { + const now : lib_plankton.pit.type_pit = lib_plankton.pit.now(); + const events : Array<_munin.type_event> = ( + (await Promise.all(sources.map(source => source.fetch()))) + .reduce((x, y) => x.concat(y), []) + ); + for (const target of targets) { + for (const reminder_hours of target.reminders) { + for (const event of events) { + const remind : boolean = shall_remind( + event, + reminder_hours, + { + "pit": now, + "interval": conf.settings.interval, + } + ); + if (! remind) { + // do nothing + } + else { + lib_plankton.log._info( + "munin.remind.do", + { + "details": { + "event": event, + "target": target.show(), + } + } + ); + if (dry_run) { + // do nothing + } + else { + try { + await target.send(conf.labels, event); + } + catch (error) { + lib_plankton.log.error( + "munin.remind.error", + { + "details": { + "message": String(error), + } + } + ); + } + } + } + } + } + } + } + + + /** + */ + export async function run( + conf : _munin.conf.type_conf, + { + "single_run": single_run = false, + "dry_run": dry_run = false, + } : { + single_run ?: boolean; + dry_run ?: boolean; + } = { + } + ) : Promise + { + const sources : Array<_munin.type_source> = conf.sources.map( + source_raw => _munin.sources.factory(source_raw) + ); + const targets : Array<_munin.type_target> = conf.targets.map( + target_raw => _munin.targets.factory(target_raw) + ); + lib_plankton.log._info( + "munin.run.start", + { + "details": { + } + } + ); + if (single_run) { + await run_iteration(conf, sources, targets, {"dry_run": dry_run}); + } + else { + while (true) { + await run_iteration(conf, sources, targets, {"dry_run": dry_run}); + await lib_plankton.call.sleep(conf.settings.interval * 60 * 60); + } + } + } + +} diff --git a/source/main.ts b/source/main.ts index f100ac2..9fb52fa 100644 --- a/source/main.ts +++ b/source/main.ts @@ -20,146 +20,7 @@ along with »munin«. If not, see . namespace _munin { - - /** - */ - async function run_iteration( - conf : _munin.conf.type_conf, - sources : Array<_munin.type_source>, - targets : Array<_munin.type_target>, - { - "dry_run": dry_run = false, - } : { - dry_run ?: boolean; - } = { - } - ) : Promise - { - const now : lib_plankton.pit.type_pit = lib_plankton.pit.now(); - const events : Array<_munin.type_event> = ( - (await Promise.all(sources.map(source => source.fetch()))) - .reduce((x, y) => x.concat(y), []) - ); - for (const target of targets) { - const window_from : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( - now, - 0 - ); - const window_to : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( - now, - conf.settings.interval - ); - lib_plankton.log._info( - "munin.run.iteration", - { - "details": { - "target": target.show(), - "window_from": lib_plankton.pit.to_date_object(window_from).toISOString(), - "window_to": lib_plankton.pit.to_date_object(window_to).toISOString(), - } - } - ); - for (const reminder_hours of target.reminders) { - for (const event of events) { - const event_begin : lib_plankton.pit.type_pit = lib_plankton.pit.from_datetime( - event.begin - ); - const reminder_time : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( - event_begin, - (-reminder_hours) - ); - lib_plankton.log._info( - "munin.run.check_dueness", - { - "details": { - "event_begin": lib_plankton.pit.to_date_object(event_begin).toISOString(), - "reminder_hours": reminder_hours, - "reminder_time": lib_plankton.pit.to_date_object(reminder_time).toISOString(), - } - } - ); - const remind : boolean = lib_plankton.pit.is_between( - reminder_time, - window_from, - window_to - ); - if (! remind) { - // do nothing - } - else { - lib_plankton.log._info( - "munin.remind.do", - { - "details": { - "event": event, - "target": target.show(), - } - } - ); - if (dry_run) { - // do nothing - } - else { - try { - await target.send(conf.labels, event); - } - catch (error) { - lib_plankton.log.error( - "munin.remind.error", - { - "details": { - "message": String(error), - } - } - ); - } - } - } - } - } - } - } - - - /** - */ - async function run( - conf : _munin.conf.type_conf, - { - "single_run": single_run = false, - "dry_run": dry_run = false, - } : { - single_run ?: boolean; - dry_run ?: boolean; - } = { - } - ) : Promise - { - const sources : Array<_munin.type_source> = conf.sources.map( - source_raw => _munin.sources.factory(source_raw) - ); - const targets : Array<_munin.type_target> = conf.targets.map( - target_raw => _munin.targets.factory(target_raw) - ); - lib_plankton.log._info( - "munin.run.start", - { - "details": { - } - } - ); - if (single_run) { - await run_iteration(conf, sources, targets, {"dry_run": dry_run}); - } - else { - while (true) { - await run_iteration(conf, sources, targets, {"dry_run": dry_run}); - await lib_plankton.call.sleep(conf.settings.interval * 60 * 60); - } - } - } - - + /** */ export async function main( @@ -169,16 +30,14 @@ namespace _munin // args const arg_handler : lib_plankton.args.class_handler = new lib_plankton.args.class_handler( { - /* "action": lib_plankton.args.class_argument.positional({ "index": 0, "type": lib_plankton.args.enum_type.string, "mode": lib_plankton.args.enum_mode.replace, "default": "run", - "info": "what to do : help | run", + "info": "what to do : test | run", "name": "action", }), - */ "conf_path": lib_plankton.args.class_argument.volatile({ "indicators_long": ["conf-path"], "indicators_short": ["c"], @@ -274,67 +133,76 @@ namespace _munin ); } else { - // init - const conf : _munin.conf.type_conf = await _munin.conf.load(args.conf_path); - lib_plankton.log.set_main_logger( - [ - { - "kind": "filtered", - "data": { - "core": { - "kind": "std", - "data": { - "target": "stdout", - "format": { - "kind": "human_readable", - "data": { - } - } - } - }, - "predicate": [ - [ - { - "item": { - "kind": "level", - "data": { - "threshold": args.verbosity, - } - }, - }, - ] - ], - } - }, - ] - ); - - // exec - if (args.conf_expose) { - process.stdout.write( - lib_plankton.json.encode( - conf, - { - "formatted": true, - } - ) - + - "\n" - ); - } - switch (/*args.action*/"run") { - default: { + switch (args.action) { + default: + { throw (new Error("unhandled action: " + args.action)); break; } - case "run": { - run( + case "test": + { + _munin.test.all(); + break; + } + case "run": + { + // init + const conf : _munin.conf.type_conf = await _munin.conf.load(args.conf_path); + lib_plankton.log.set_main_logger( + [ + { + "kind": "filtered", + "data": { + "core": { + "kind": "std", + "data": { + "target": "stdout", + "format": { + "kind": "human_readable", + "data": { + } + } + } + }, + "predicate": [ + [ + { + "item": { + "kind": "level", + "data": { + "threshold": args.verbosity, + } + }, + }, + ] + ], + } + }, + ] + ); + + // exec + if (args.conf_expose) { + process.stdout.write( + lib_plankton.json.encode( + conf, + { + "formatted": true, + } + ) + + + "\n" + ); + } + + _munin.logic.run( conf, { "single_run": args.single_run, "dry_run": args.dry_run, } ); + break; } } diff --git a/source/test.ts b/source/test.ts new file mode 100644 index 0000000..91b0842 --- /dev/null +++ b/source/test.ts @@ -0,0 +1,384 @@ +/* +This file is part of »munin«. + +Copyright 2025 'Fenris Wolf' + +»munin« is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +»munin« is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with »munin«. If not, see . + */ + + +namespace _munin.test +{ + + /** + */ + type type_datetime = { + year : int; + month : int; + day : int; + hour : int; + minute : int; + second : int; + }; + + + /** + */ + function datetime_to_pit( + datetime : type_datetime + ) : lib_plankton.pit.type_pit + { + return lib_plankton.pit.from_datetime( + { + "date": { + "year": datetime.year, + "month": datetime.month, + "day": datetime.day, + }, + "time": { + "hour": datetime.hour, + "minute": datetime.minute, + "second": datetime.second, + }, + "timezone_shift": 0, + } + ); + } + + + /** + */ + function start( + name : string + ) : void + { + lib_plankton.log._info( + "test.run", + { + "details": { + "name": name, + } + } + ); + } + + + /** + */ + function fail( + name : string + ) : void + { + lib_plankton.log._error( + "test.failed", + { + "details": { + "name": name, + } + } + ); + } + + + /** + */ + function frequency_decode( + frequency_encoded : string + ) : _munin.enum_frequency + { + switch (frequency_encoded) + { + case "hourly": return _munin.enum_frequency.hourly; + case "daily": return _munin.enum_frequency.daily; + case "weekly": return _munin.enum_frequency.weekly; + case "monthly": return _munin.enum_frequency.monthly; + default: {throw new Error("unhandled");} + } + } + + + /** + */ + export function reminder_due( + ) : void + { + type type_testcase = { + name : string; + input : { + reminder : { + frequency : string; + offset : int; + from : int; + to : int; + }; + datetime : type_datetime; + interval : int; + }; + output : boolean; + }; + const testcases : Array = [ + { + "name": "reminder_due.test1", + "input": { + "reminder": { + "frequency": "daily", + "offset": 16, + "from": 24, + "to": 48 + }, + "datetime": { + "year": 2025, + "month": 6, + "day": 27, + "hour": 15, + "minute": 30, + "second": 0 + }, + "interval": 1 + }, + "output": false + }, + { + "name": "reminder_due.test2", + "input": { + "reminder": { + "frequency": "daily", + "offset": 16, + "from": 24, + "to": 48 + }, + "datetime": { + "year": 2025, + "month": 6, + "day": 27, + "hour": 16, + "minute": 30, + "second": 0, + }, + "interval": 1 + }, + "output": true + }, + { + "name": "reminder_due.test3", + "input": { + "reminder": { + "frequency": "daily", + "offset": 16, + "from": 24, + "to": 48 + }, + "datetime": { + "year": 2025, + "month": 6, + "day": 27, + "hour": 17, + "minute": 30, + "second": 0 + }, + "interval": 1 + }, + "output": false + }, + ]; + for (const testcase of testcases) + { + start(testcase.name); + + // execution + const result : boolean = _munin.logic.reminder_due( + { + "frequency": frequency_decode(testcase.input.reminder.frequency), + "offset": testcase.input.reminder.offset, + "from": testcase.input.reminder.from, + "to": testcase.input.reminder.to, + }, + { + "pit": datetime_to_pit(testcase.input.datetime), + "interval": testcase.input.interval, + } + ); + + // assertions + if (result !== testcase.output) + { + fail(testcase.name); + } + } + } + + + /** + */ + export function reminder_event_in_window( + ) : void + { + type type_testcase = { + name : string; + input : { + reminder : { + frequency : string; + offset : int; + from : int; + to : int; + }; + event : { + begin : type_datetime; + }; + datetime : type_datetime; + interval : int; + }; + output : boolean; + }; + const testcases : Array = [ + { + "name": "reminder_event_in_window.test1", + "input": { + "reminder": { + "frequency": "daily", + "offset": 0, + "from": 24, + "to": 48 + }, + "event": { + "begin": { + "year": 2025, + "month": 6, + "day": 25, + "hour": 12, + "minute": 0, + "second": 0 + } + }, + "datetime": { + "year": 2025, + "month": 6, + "day": 23, + "hour": 10, + "minute": 0, + "second": 0 + }, + "interval": 1 + }, + "output": false + }, + { + "name": "reminder_event_in_window.test2", + "input": { + "reminder": { + "frequency": "daily", + "offset": 0, + "from": 24, + "to": 48 + }, + "event": { + "begin": { + "year": 2025, + "month": 6, + "day": 25, + "hour": 12, + "minute": 0, + "second": 0 + } + }, + "datetime": { + "year": 2025, + "month": 6, + "day": 24, + "hour": 10, + "minute": 0, + "second": 0 + }, + "interval": 1 + }, + "output": true + }, + { + "name": "reminder_event_in_window.test3", + "input": { + "reminder": { + "frequency": "daily", + "offset": 0, + "from": 24, + "to": 48 + }, + "event": { + "begin": { + "year": 2025, + "month": 6, + "day": 25, + "hour": 12, + "minute": 0, + "second": 0 + } + }, + "datetime": { + "year": 2025, + "month": 6, + "day": 25, + "hour": 10, + "minute": 0, + "second": 0 + }, + "interval": 1 + }, + "output": false + }, + ]; + + for (const testcase of testcases) + { + start(testcase.name); + + // execution + const result : boolean = _munin.logic.reminder_event_in_window( + { + "frequency": frequency_decode(testcase.input.reminder.frequency), + "offset": testcase.input.reminder.offset, + "from": testcase.input.reminder.from, + "to": testcase.input.reminder.to, + }, + { + "title": "test", + "begin": lib_plankton.pit.to_datetime(datetime_to_pit(testcase.input.event.begin)), + "end": null, + "description": null, + "location": null, + "tags": null, + }, + { + "pit": datetime_to_pit(testcase.input.datetime), + "interval": testcase.input.interval, + } + ); + + // assertions + if (result !== testcase.output) + { + fail(testcase.name); + } + } + } + + + /** + */ + export function all( + ) : void + { + reminder_due(); + reminder_event_in_window(); + } + +} diff --git a/source/types.ts b/source/types.ts index fae9db8..e29db21 100644 --- a/source/types.ts +++ b/source/types.ts @@ -20,7 +20,17 @@ along with »munin«. If not, see . namespace _munin { - + + /** + */ + export enum enum_frequency { + hourly = "hourly", + daily = "daily", + weekly = "weekly", + monthly = "monthly", + } + + /** */ export type type_labels = { @@ -55,10 +65,28 @@ namespace _munin }; + /** + * @todo rename + * @todo extend + */ + export type type_reminder = int; + + + /** + * @todo rename + */ + export type type_reminder_new = { + frequency : enum_frequency; + offset : int; + from : int; + to : int; + }; + + /** */ export type type_target = { - reminders : Array; + reminders : Array; show : (() => string); send : ( ( -- 2.39.5 From 1ade4dec2a44a74229f8cb1c2ecadd603a3ae87f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20Fra=C3=9F?= Date: Sat, 28 Jun 2025 04:27:46 +0000 Subject: [PATCH 2/4] [task-340] [int] --- source/conf.ts | 345 +++++++++++++++++++++++++++------ source/logic.ts | 194 ++++++++---------- source/main.ts | 3 +- source/targets/email.ts | 5 +- source/targets/telegram_bot.ts | 2 +- source/test.ts | 1 - source/types.ts | 9 +- 7 files changed, 380 insertions(+), 179 deletions(-) diff --git a/source/conf.ts b/source/conf.ts index cd440d7..f492559 100644 --- a/source/conf.ts +++ b/source/conf.ts @@ -24,6 +24,24 @@ along with »munin«. If not, see . namespace _munin.conf { + /** + */ + type type_reminder_raw = { + frequency : ( + "hourly" + | + "daily" + | + "weekly" + | + "monthly" + ); + offset : int; + from : int; + to : int; + }; + + /** */ type type_conf_v1 = { @@ -236,7 +254,71 @@ namespace _munin.conf /** */ - export type type_conf = type_conf_v4; + type type_conf_v5 = { + sources : Array< + ( + { + kind : "ical_feed"; + data : { + url : string; + filtration : { + category_whitelist : (null | Array); + category_blacklist : (null | Array); + title_whitelist : (null | Array); + title_blacklist : (null | Array); + } + } + } + ) + >; + targets : Array< + ( + { + kind : "email"; + data : { + smtp_host : string; + smtp_port : int; + smtp_username : string; + smtp_password : string; + sender : string; + receivers : Array; + hide_tags : boolean; + /** + * in hours + */ + reminders : Array; + } + } + | + { + kind : "telegram_bot"; + data : { + bot_token : string; + chat_id : int; + hide_tags : boolean; + /** + * in hours + */ + reminders : Array; + } + } + ) + >; + settings : { + interval : float; + }; + labels : { + head : string; + title : string; + time : string; + location : string; + }; + }; + + + /** + */ + export type type_conf = type_conf_v5; /** @@ -396,6 +478,65 @@ namespace _munin.conf } + /** + */ + function convert_from_v4( + conf_v4 : type_conf_v4 + ) : type_conf_v5 + { + const map_reminder = hours => ({ + "frequency": "hourly", + "offset": 0, + "from": hours, + "to": (hours + 1), + } as type_reminder_raw); + return { + "sources": conf_v4.sources, + "targets": conf_v4.targets.map( + target => { + switch (target.kind) { + case "email": { + return { + "kind": "email", + "data": { + "smtp_host": target.data.smtp_host, + "smtp_port": target.data.smtp_port, + "smtp_username": target.data.smtp_username, + "smtp_password": target.data.smtp_password, + "sender": target.data.sender, + "receivers": target.data.receivers, + "hide_tags": target.data.hide_tags, + "reminders": target.data.reminders.map(map_reminder), + }, + }; + break; + } + case "telegram_bot": { + return { + "kind": "telegram_bot", + "data": { + "bot_token": target.data.bot_token, + "chat_id": target.data.chat_id, + "hide_tags": target.data.hide_tags, + "reminders": target.data.reminders.map(map_reminder), + }, + }; + break; + } + default: { + // return target; + throw (new Error("unhandled target kind: " + String(target))); + break; + } + } + } + ), + "settings": conf_v4.settings, + "labels": conf_v4.labels, + }; + } + + /** */ function schema_source_kalender_digital( @@ -546,6 +687,136 @@ namespace _munin.conf } + /** + */ + function schema_sources( + version : string + ) : lib_plankton.conf.type_schema + { + switch (version) { + case "1": { + return { + "nullable": false, + "type": "array", + "items": { + "nullable": false, + "anyOf": [ + schema_source_kalender_digital(version), + ], + } + }; + break; + } + default: + case "2": { + return { + "nullable": false, + "type": "array", + "items": { + "nullable": false, + "anyOf": [ + schema_source_ical_feed(version), + ], + } + }; + break; + } + } + } + + + /** + */ + function schema_reminder( + version : string + ) : lib_plankton.conf.type_schema + { + switch (version) + { + case "1": + case "2": + case "3": + case "4": + { + return { + "type": "integer", + }; + } + case "5": + default: + { + return { + "nullable": false, + "type": "object", + "properties": { + "frequency": { + "nullable": false, + "type": "string", + "enum": [ + "hourly", + "daily", + "weekly", + "monthly", + ] + }, + "offset": { + "nullable": false, + "type": "integer", + "default": 0 + }, + "from": { + "nullable": false, + "type": "integer" + }, + "to": { + "nullable": false, + "type": "integer" + }, + }, + "additionalProperties": false, + "required": [ + "frequency", + "from", + "to", + ] + }; + break; + } + } + } + + + /** + */ + function default_reminder( + version : string + ) : any + { + switch (version) + { + case "1": + case "2": + case "3": + case "4": + { + return [24]; + } + case "5": + default: + { + return [ + { + "frequency": "hourly", + "from": 24, + "to": 25 + } + ]; + break; + } + } + } + + /** */ function schema_target_email( @@ -602,11 +873,8 @@ namespace _munin.conf "reminders": { "nullable": false, "type": "array", - "items": { - "nullable": false, - "type": "integer" - }, - "default": [24.0], + "items": schema_reminder(version), + "default": default_reminder(version), }, }, "additionalProperties": false, @@ -628,44 +896,6 @@ namespace _munin.conf } - /** - */ - function schema_sources( - version : string - ) : lib_plankton.conf.type_schema - { - switch (version) { - case "1": { - return { - "nullable": false, - "type": "array", - "items": { - "nullable": false, - "anyOf": [ - schema_source_kalender_digital(version), - ], - } - }; - break; - } - default: - case "2": { - return { - "nullable": false, - "type": "array", - "items": { - "nullable": false, - "anyOf": [ - schema_source_ical_feed(version), - ], - } - }; - break; - } - } - } - - /** */ function schema_target_telegram_bot( @@ -701,11 +931,8 @@ namespace _munin.conf "reminders": { "nullable": false, "type": "array", - "items": { - "nullable": false, - "type": "integer" - }, - "default": [24.0], + "items": schema_reminder(version), + "default": default_reminder(version), }, }, "additionalProperties": false, @@ -737,13 +964,18 @@ namespace _munin.conf "nullable": false, "anyOf": (() => { switch (version) { - default: { + case "1": + case "2": + { return [ schema_target_telegram_bot(version), ]; break; } - case "3": { + case "4": + case "5": + default: + { return [ schema_target_email(version), schema_target_telegram_bot(version), @@ -824,7 +1056,7 @@ namespace _munin.conf /** */ export function schema( - version : string = "4" + version : string = "5" ) : lib_plankton.conf.type_schema { switch (version) { @@ -852,7 +1084,9 @@ namespace _munin.conf } case "2": case "3": - case "4": { + case "4": + case "5": + { return { "nullable": false, "type": "object", @@ -887,7 +1121,8 @@ namespace _munin.conf "1": {"target": "2", "function": convert_from_v1}, "2": {"target": "3", "function": convert_from_v2}, "3": {"target": "4", "function": convert_from_v3}, - "4": null, + "4": {"target": "5", "function": convert_from_v4}, + "5": null, } ); @@ -925,7 +1160,7 @@ namespace _munin.conf } } */ - return (conf_raw.content as type_conf_v4); + return (conf_raw.content as type_conf_v5); } } diff --git a/source/logic.ts b/source/logic.ts index 693811f..a9efba2 100644 --- a/source/logic.ts +++ b/source/logic.ts @@ -21,45 +21,6 @@ along with »munin«. If not, see . namespace _munin.logic { - /** - * @todo Tests schreiben - */ - function shall_remind( - event : _munin.type_event, - reminder : type_reminder, - { - "pit": pit = lib_plankton.pit.now(), - "interval": interval = 1, - } : { - pit ?: lib_plankton.pit.type_pit; - interval ?: int; - } = { - } - ) : boolean - { - const window_from : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( - pit, - 0 - ); - const window_to : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( - pit, - interval - ); - const event_begin : lib_plankton.pit.type_pit = lib_plankton.pit.from_datetime( - event.begin - ); - const reminder_time : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( - event_begin, - (-reminder) - ); - return lib_plankton.pit.is_between( - reminder_time, - window_from, - window_to - ); - } - - /** */ function frequency_anchor( @@ -90,7 +51,7 @@ namespace _munin.logic /** */ export function reminder_due( - reminder : type_reminder_new, + reminder : type_reminder, { "pit": pit = lib_plankton.pit.now(), "interval": interval = 1, @@ -105,35 +66,46 @@ namespace _munin.logic reminder.frequency, {"pit": pit} ); - // 0 ≤ ((p - a(p)) - o) < i - const x : float = ( - ( - ( - lib_plankton.pit.to_unix_timestamp(pit) - - - lib_plankton.pit.to_unix_timestamp(anchor) - ) - / - (60 * 60) - ) - - - reminder.offset + const window_from : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( + anchor, + (reminder.offset + 0) ); - return ((0 <= x) && (x < interval)); + const window_to : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( + anchor, + (reminder.offset + interval) + ); + const result : boolean = lib_plankton.pit.is_between( + pit, + window_from, + window_to + ); + lib_plankton.log._info( + "munin.logic.reminder_due", + { + "details": { + "reminder": reminder, + "datetime": lib_plankton.pit.to_datetime(pit), + "interval": interval, + "anchor": lib_plankton.pit.to_datetime(anchor), + "window_from": lib_plankton.pit.to_datetime(window_from), + "window_to": lib_plankton.pit.to_datetime(window_to), + "result": result, + } + } + ); + return result; } /** */ export function reminder_event_in_window( - reminder : type_reminder_new, + reminder : type_reminder, event : _munin.type_event, { "pit": pit = lib_plankton.pit.now(), - "interval": interval = 1, } : { pit ?: lib_plankton.pit.type_pit; - interval ?: int; } = { } ) : boolean @@ -153,41 +125,26 @@ namespace _munin.logic const event_begin : lib_plankton.pit.type_pit = lib_plankton.pit.from_datetime( event.begin ); - return lib_plankton.pit.is_between( + const result : boolean = lib_plankton.pit.is_between( event_begin, window_from, window_to ); - } - - - /** - */ - function shall_remind_new( - reminder : type_reminder_new, - event : _munin.type_event, - { - "pit": pit = lib_plankton.pit.now(), - "interval": interval = 1, - } : { - pit ?: lib_plankton.pit.type_pit; - interval ?: int; - } = { - } - ) : boolean - { - return ( - reminder_due( - reminder, - {"pit": pit, "interval": interval} - ) - && - reminder_event_in_window( - reminder, - event, - {"pit": pit, "interval": interval} - ) + lib_plankton.log._info( + "munin.logic.reminder_event_in_window", + { + "details": { + "reminder": reminder, + "event": event, + "datetime": lib_plankton.pit.to_datetime(pit), + "anchor": lib_plankton.pit.to_datetime(anchor), + "window_from": lib_plankton.pit.to_datetime(window_from), + "window_to": lib_plankton.pit.to_datetime(window_to), + "result": result, + } + } ); + return result; } @@ -211,17 +168,30 @@ namespace _munin.logic .reduce((x, y) => x.concat(y), []) ); for (const target of targets) { - for (const reminder_hours of target.reminders) { - for (const event of events) { - const remind : boolean = shall_remind( - event, - reminder_hours, - { - "pit": now, - "interval": conf.settings.interval, - } + for (const reminder of target.reminders) { + const due : boolean = reminder_due( + reminder, + { + "pit": now, + "interval": conf.settings.interval, + } + ); + if (! due) + { + // do nothing + } + else + { + const events_matching : Array<_munin.type_event> = events.filter( + event => reminder_event_in_window( + reminder, + event, + { + "pit": now, + } + ) ); - if (! remind) { + if (events_matching.length <= 0) { // do nothing } else { @@ -229,7 +199,7 @@ namespace _munin.logic "munin.remind.do", { "details": { - "event": event, + "events": events, "target": target.show(), } } @@ -238,18 +208,24 @@ namespace _munin.logic // do nothing } else { - try { - await target.send(conf.labels, event); - } - catch (error) { - lib_plankton.log.error( - "munin.remind.error", - { - "details": { - "message": String(error), + /** + * @todo bundle? + */ + for (const event of events_matching) + { + try { + await target.send(conf.labels, event); + } + catch (error) { + lib_plankton.log.error( + "munin.remind.error", + { + "details": { + "message": String(error), + } } - } - ); + ); + } } } } diff --git a/source/main.ts b/source/main.ts index 9fb52fa..af737a4 100644 --- a/source/main.ts +++ b/source/main.ts @@ -158,8 +158,9 @@ namespace _munin "data": { "target": "stdout", "format": { - "kind": "human_readable", + "kind": "jsonl", "data": { + "structured": true } } } diff --git a/source/targets/email.ts b/source/targets/email.ts index b9d33fa..358ebd6 100644 --- a/source/targets/email.ts +++ b/source/targets/email.ts @@ -31,10 +31,7 @@ namespace _munin.targets.email sender : string; receivers : Array; hide_tags : boolean; - /** - * in hours - */ - reminders : Array; + reminders : Array<_munin.type_reminder>; }; diff --git a/source/targets/telegram_bot.ts b/source/targets/telegram_bot.ts index df24da4..44f0dd7 100644 --- a/source/targets/telegram_bot.ts +++ b/source/targets/telegram_bot.ts @@ -27,7 +27,7 @@ namespace _munin.targets.telegram_bot bot_token : string; chat_id : int; hide_tags : boolean; - reminders : Array; + reminders : Array<_munin.type_reminder>; }; diff --git a/source/test.ts b/source/test.ts index 91b0842..91bed40 100644 --- a/source/test.ts +++ b/source/test.ts @@ -359,7 +359,6 @@ namespace _munin.test }, { "pit": datetime_to_pit(testcase.input.datetime), - "interval": testcase.input.interval, } ); diff --git a/source/types.ts b/source/types.ts index e29db21..9c56ef0 100644 --- a/source/types.ts +++ b/source/types.ts @@ -67,15 +67,8 @@ namespace _munin /** * @todo rename - * @todo extend */ - export type type_reminder = int; - - - /** - * @todo rename - */ - export type type_reminder_new = { + export type type_reminder = { frequency : enum_frequency; offset : int; from : int; -- 2.39.5 From 37f7a27e9546ddf8c9685182f36b3009eee013d5 Mon Sep 17 00:00:00 2001 From: Fenris Wolf Date: Sat, 28 Jun 2025 08:34:57 +0200 Subject: [PATCH 3/4] [task-340] [mod] testing --- data/reminder_covers_event.testdata.json | 122 ++++++++ data/reminder_due.testdata.json | 77 +++++ ivaldi.json | 3 + source/helpers/test.ts | 124 ++++++++ source/logic.ts | 23 +- source/test.ts | 351 ++++------------------- tools/build | 13 +- 7 files changed, 416 insertions(+), 297 deletions(-) create mode 100644 data/reminder_covers_event.testdata.json create mode 100644 data/reminder_due.testdata.json create mode 100644 source/helpers/test.ts diff --git a/data/reminder_covers_event.testdata.json b/data/reminder_covers_event.testdata.json new file mode 100644 index 0000000..8fca1b6 --- /dev/null +++ b/data/reminder_covers_event.testdata.json @@ -0,0 +1,122 @@ +[ + { + "input": { + "reminder": { + "frequency": "daily", + "offset": 0, + "from": 24, + "to": 48 + }, + "event": { + "begin": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 25 + }, + "time": { + "hour": 12, + "minute": 0, + "second": 0 + } + } + }, + "datetime": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 23 + }, + "time": { + "hour": 10, + "minute": 0, + "second": 0 + } + }, + "interval": 1 + }, + "output": false + }, + { + "input": { + "reminder": { + "frequency": "daily", + "offset": 0, + "from": 24, + "to": 48 + }, + "event": { + "begin": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 25 + }, + "time": { + "hour": 12, + "minute": 0, + "second": 0 + } + } + }, + "datetime": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 24 + }, + "time": { + "hour": 10, + "minute": 0, + "second": 0 + } + }, + "interval": 1 + }, + "output": true + }, + { + "input": { + "reminder": { + "frequency": "daily", + "offset": 0, + "from": 24, + "to": 48 + }, + "event": { + "begin": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 25 + }, + "time": { + "hour": 12, + "minute": 0, + "second": 0 + } + } + }, + "datetime": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 25 + }, + "time": { + "hour": 10, + "minute": 0, + "second": 0 + } + }, + "interval": 1 + }, + "output": false + } +] diff --git a/data/reminder_due.testdata.json b/data/reminder_due.testdata.json new file mode 100644 index 0000000..1d7211c --- /dev/null +++ b/data/reminder_due.testdata.json @@ -0,0 +1,77 @@ +[ + { + "input": { + "reminder": { + "frequency": "daily", + "offset": 16, + "from": 24, + "to": 48 + }, + "datetime": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 27 + }, + "time": { + "hour": 15, + "minute": 30, + "second": 0 + } + }, + "interval": 1 + }, + "output": false + }, + { + "input": { + "reminder": { + "frequency": "daily", + "offset": 16, + "from": 24, + "to": 48 + }, + "datetime": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 27 + }, + "time": { + "hour": 16, + "minute": 30, + "second": 0 + } + }, + "interval": 1 + }, + "output": true + }, + { + "input": { + "reminder": { + "frequency": "daily", + "offset": 16, + "from": 24, + "to": 48 + }, + "datetime": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 27 + }, + "time": { + "hour": 17, + "minute": 30, + "second": 0 + } + }, + "interval": 1 + }, + "output": false + } +] diff --git a/ivaldi.json b/ivaldi.json index b3a009a..c3c033f 100644 --- a/ivaldi.json +++ b/ivaldi.json @@ -12,6 +12,8 @@ "email", "telegram", "url", + "json", + "file", "conf", "log", "args" @@ -20,6 +22,7 @@ } ], "sources": [ + "helpers/test.ts", "types.ts", "sources/ical_feed.ts", "sources/_functions.ts", diff --git a/source/helpers/test.ts b/source/helpers/test.ts new file mode 100644 index 0000000..5c87834 --- /dev/null +++ b/source/helpers/test.ts @@ -0,0 +1,124 @@ +/* +This file is part of »munin«. + +Copyright 2025 'Fenris Wolf' + +»munin« is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +»munin« is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with »munin«. If not, see . + */ + + +namespace _munin.helpers.test +{ + + /** + * @todo outsource + */ + type type_testcase_raw = { + active ?: boolean; + name ?: string; + input : type_input; + output : type_output; + }; + + + /** + * @todo outsource + */ + export type type_testcase = { + name : string; + input : type_input; + output : type_output; + }; + + + /** + * @todo outsource + */ + export async function get_data( + path : string, + { + "default_active": default_active = true, + } : { + default_active ?: boolean; + } = { + } + ) : Promise>> + { + const content : string = await lib_plankton.file.read(path); + const testcases_raw : Array> = ( + lib_plankton.json.decode(content) as Array> + ); + const testcases : Array> = ( + testcases_raw + .filter( + testcase_raw => (testcase_raw.active ?? default_active), + ) + .map( + (testcase_raw, index) => ({ + "name": ( + testcase_raw.name + ?? + lib_plankton.string.coin( + "{{path}} | #{{index}}", + { + "path": path, + "index": (index + 1).toFixed(0), + } + ) + ), + "input": testcase_raw.input, + "output": testcase_raw.output, + }) + ) + ); + return testcases; + } + + + /** + * @todo outsource + */ + export function start( + name : string + ) : void + { + lib_plankton.log._info( + "test.run", + { + "details": { + "name": name, + } + } + ); + } + + + /** + * @todo outsource + */ + export function fail( + name : string + ) : void + { + lib_plankton.log._error( + "test.failed", + { + "details": { + "name": name, + } + } + ); + } + +} diff --git a/source/logic.ts b/source/logic.ts index a9efba2..9a76d62 100644 --- a/source/logic.ts +++ b/source/logic.ts @@ -21,6 +21,23 @@ along with »munin«. If not, see . namespace _munin.logic { + /** + */ + export function frequency_decode( + frequency_encoded : string + ) : _munin.enum_frequency + { + switch (frequency_encoded) + { + case "hourly": return _munin.enum_frequency.hourly; + case "daily": return _munin.enum_frequency.daily; + case "weekly": return _munin.enum_frequency.weekly; + case "monthly": return _munin.enum_frequency.monthly; + default: {throw new Error("unhandled");} + } + } + + /** */ function frequency_anchor( @@ -99,7 +116,7 @@ namespace _munin.logic /** */ - export function reminder_event_in_window( + export function reminder_covers_event( reminder : type_reminder, event : _munin.type_event, { @@ -131,7 +148,7 @@ namespace _munin.logic window_to ); lib_plankton.log._info( - "munin.logic.reminder_event_in_window", + "munin.logic.reminder_covers_event", { "details": { "reminder": reminder, @@ -183,7 +200,7 @@ namespace _munin.logic else { const events_matching : Array<_munin.type_event> = events.filter( - event => reminder_event_in_window( + event => reminder_covers_event( reminder, event, { diff --git a/source/test.ts b/source/test.ts index 91bed40..b570f3f 100644 --- a/source/test.ts +++ b/source/test.ts @@ -20,192 +20,47 @@ along with »munin«. If not, see . namespace _munin.test { - + /** */ - type type_datetime = { - year : int; - month : int; - day : int; - hour : int; - minute : int; - second : int; - }; - - - /** - */ - function datetime_to_pit( - datetime : type_datetime - ) : lib_plankton.pit.type_pit + async function reminder_due( + ) : Promise { - return lib_plankton.pit.from_datetime( - { - "date": { - "year": datetime.year, - "month": datetime.month, - "day": datetime.day, - }, - "time": { - "hour": datetime.hour, - "minute": datetime.minute, - "second": datetime.second, - }, - "timezone_shift": 0, - } - ); - } - - - /** - */ - function start( - name : string - ) : void - { - lib_plankton.log._info( - "test.run", - { - "details": { - "name": name, - } - } - ); - } - - - /** - */ - function fail( - name : string - ) : void - { - lib_plankton.log._error( - "test.failed", - { - "details": { - "name": name, - } - } - ); - } - - - /** - */ - function frequency_decode( - frequency_encoded : string - ) : _munin.enum_frequency - { - switch (frequency_encoded) - { - case "hourly": return _munin.enum_frequency.hourly; - case "daily": return _munin.enum_frequency.daily; - case "weekly": return _munin.enum_frequency.weekly; - case "monthly": return _munin.enum_frequency.monthly; - default: {throw new Error("unhandled");} - } - } - - - /** - */ - export function reminder_due( - ) : void - { - type type_testcase = { - name : string; - input : { - reminder : { - frequency : string; - offset : int; - from : int; - to : int; - }; - datetime : type_datetime; - interval : int; + type type_input = { + reminder : { + frequency : string; + offset : int; + from : int; + to : int; }; - output : boolean; + datetime : lib_plankton.pit.type_datetime; + interval : int; }; - const testcases : Array = [ + type type_output = boolean; + const testcases : Array< + _munin.helpers.test.type_testcase< + type_input, + type_output + > + > = await _munin.helpers.test.get_data( + "data/reminder_due.testdata.json", { - "name": "reminder_due.test1", - "input": { - "reminder": { - "frequency": "daily", - "offset": 16, - "from": 24, - "to": 48 - }, - "datetime": { - "year": 2025, - "month": 6, - "day": 27, - "hour": 15, - "minute": 30, - "second": 0 - }, - "interval": 1 - }, - "output": false - }, - { - "name": "reminder_due.test2", - "input": { - "reminder": { - "frequency": "daily", - "offset": 16, - "from": 24, - "to": 48 - }, - "datetime": { - "year": 2025, - "month": 6, - "day": 27, - "hour": 16, - "minute": 30, - "second": 0, - }, - "interval": 1 - }, - "output": true - }, - { - "name": "reminder_due.test3", - "input": { - "reminder": { - "frequency": "daily", - "offset": 16, - "from": 24, - "to": 48 - }, - "datetime": { - "year": 2025, - "month": 6, - "day": 27, - "hour": 17, - "minute": 30, - "second": 0 - }, - "interval": 1 - }, - "output": false - }, - ]; + } + ); for (const testcase of testcases) { - start(testcase.name); + _munin.helpers.test.start(testcase.name); // execution const result : boolean = _munin.logic.reminder_due( { - "frequency": frequency_decode(testcase.input.reminder.frequency), + "frequency": _munin.logic.frequency_decode(testcase.input.reminder.frequency), "offset": testcase.input.reminder.offset, "from": testcase.input.reminder.from, "to": testcase.input.reminder.to, }, { - "pit": datetime_to_pit(testcase.input.datetime), + "pit": lib_plankton.pit.from_datetime(testcase.input.datetime), "interval": testcase.input.interval, } ); @@ -213,7 +68,7 @@ namespace _munin.test // assertions if (result !== testcase.output) { - fail(testcase.name); + _munin.helpers.test.fail(testcase.name); } } } @@ -221,163 +76,75 @@ namespace _munin.test /** */ - export function reminder_event_in_window( - ) : void + async function reminder_covers_event( + ) : Promise { - type type_testcase = { - name : string; - input : { - reminder : { - frequency : string; - offset : int; - from : int; - to : int; - }; - event : { - begin : type_datetime; - }; - datetime : type_datetime; - interval : int; + type type_input = { + reminder : { + frequency : string; + offset : int; + from : int; + to : int; }; - output : boolean; + event : { + begin : lib_plankton.pit.type_datetime; + }; + datetime : lib_plankton.pit.type_datetime; + interval : int; }; - const testcases : Array = [ + type type_output = boolean; + const testcases : Array< + _munin.helpers.test.type_testcase< + type_input, + type_output + > + > = await _munin.helpers.test.get_data( + "data/reminder_covers_event.testdata.json", { - "name": "reminder_event_in_window.test1", - "input": { - "reminder": { - "frequency": "daily", - "offset": 0, - "from": 24, - "to": 48 - }, - "event": { - "begin": { - "year": 2025, - "month": 6, - "day": 25, - "hour": 12, - "minute": 0, - "second": 0 - } - }, - "datetime": { - "year": 2025, - "month": 6, - "day": 23, - "hour": 10, - "minute": 0, - "second": 0 - }, - "interval": 1 - }, - "output": false - }, - { - "name": "reminder_event_in_window.test2", - "input": { - "reminder": { - "frequency": "daily", - "offset": 0, - "from": 24, - "to": 48 - }, - "event": { - "begin": { - "year": 2025, - "month": 6, - "day": 25, - "hour": 12, - "minute": 0, - "second": 0 - } - }, - "datetime": { - "year": 2025, - "month": 6, - "day": 24, - "hour": 10, - "minute": 0, - "second": 0 - }, - "interval": 1 - }, - "output": true - }, - { - "name": "reminder_event_in_window.test3", - "input": { - "reminder": { - "frequency": "daily", - "offset": 0, - "from": 24, - "to": 48 - }, - "event": { - "begin": { - "year": 2025, - "month": 6, - "day": 25, - "hour": 12, - "minute": 0, - "second": 0 - } - }, - "datetime": { - "year": 2025, - "month": 6, - "day": 25, - "hour": 10, - "minute": 0, - "second": 0 - }, - "interval": 1 - }, - "output": false - }, - ]; + } + ); for (const testcase of testcases) { - start(testcase.name); + _munin.helpers.test.start(testcase.name); // execution - const result : boolean = _munin.logic.reminder_event_in_window( + const result : boolean = _munin.logic.reminder_covers_event( { - "frequency": frequency_decode(testcase.input.reminder.frequency), + "frequency": _munin.logic.frequency_decode(testcase.input.reminder.frequency), "offset": testcase.input.reminder.offset, "from": testcase.input.reminder.from, "to": testcase.input.reminder.to, }, { "title": "test", - "begin": lib_plankton.pit.to_datetime(datetime_to_pit(testcase.input.event.begin)), + "begin": testcase.input.event.begin, "end": null, "description": null, "location": null, "tags": null, }, { - "pit": datetime_to_pit(testcase.input.datetime), + "pit": lib_plankton.pit.from_datetime(testcase.input.datetime), } ); // assertions if (result !== testcase.output) { - fail(testcase.name); + _munin.helpers.test.fail(testcase.name); } } } - + /** */ - export function all( - ) : void + export async function all( + ) : Promise { - reminder_due(); - reminder_event_in_window(); + await reminder_due(); + await reminder_covers_event(); } } diff --git a/tools/build b/tools/build index 155b24a..f85b346 100755 --- a/tools/build +++ b/tools/build @@ -1,4 +1,13 @@ -#!/usr/bin/env sh +#!/usr/bin/env bash +## core tools/ivaldi build -cd build && npm install nodemailer ; cd - + +## data +mkdir -p build/data +cp -r -u -v data/* build/data/ + +## node modules +node_modules="" +node_modules="${node_modules} nodemailer" +cd build && npm install ${node_modules} ; cd - -- 2.39.5 From 21c2d9945c58eaf6745a0b176f88925ac1486669 Mon Sep 17 00:00:00 2001 From: Fenris Wolf Date: Mon, 30 Jun 2025 13:02:06 +0200 Subject: [PATCH 4/4] [task-340] soweit fertig --- data/reminder_check.testdata.json | 218 +++++++++++++++++++++++ data/reminder_covers_event.testdata.json | 122 ------------- data/reminder_due.testdata.json | 77 -------- source/conf.ts | 8 +- source/logic.ts | 196 +++++++++----------- source/targets/email.ts | 170 +++++++++++------- source/targets/telegram_bot.ts | 108 +++++++---- source/test.ts | 127 ++++++------- source/types.ts | 3 +- 9 files changed, 546 insertions(+), 483 deletions(-) create mode 100644 data/reminder_check.testdata.json delete mode 100644 data/reminder_covers_event.testdata.json delete mode 100644 data/reminder_due.testdata.json diff --git a/data/reminder_check.testdata.json b/data/reminder_check.testdata.json new file mode 100644 index 0000000..0d93f3d --- /dev/null +++ b/data/reminder_check.testdata.json @@ -0,0 +1,218 @@ +[ + { + "name": "reminder_check.dueness-1", + "input": { + "reminder": { + "frequency": "daily", + "offset": 16, + "from": 24, + "to": 48 + }, + "events": [ + ], + "datetime": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 27 + }, + "time": { + "hour": 15, + "minute": 30, + "second": 0 + } + }, + "interval": 1 + }, + "output": null + }, + { + "name": "reminder_check.dueness-2", + "input": { + "reminder": { + "frequency": "daily", + "offset": 16, + "from": 24, + "to": 48 + }, + "events": [ + ], + "datetime": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 27 + }, + "time": { + "hour": 16, + "minute": 30, + "second": 0 + } + }, + "interval": 1 + }, + "output": [] + }, + { + "name": "reminder_check.dueness-3", + "input": { + "reminder": { + "frequency": "daily", + "offset": 16, + "from": 24, + "to": 48 + }, + "events": [ + ], + "datetime": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 27 + }, + "time": { + "hour": 17, + "minute": 30, + "second": 0 + } + }, + "interval": 1 + }, + "output": null + }, + { + "name": "reminder_check.events-1", + "input": { + "reminder": { + "frequency": "daily", + "offset": 16, + "from": 24, + "to": 48 + }, + "events": [ + { + "title": "e1", + "begin": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 27 + }, + "time": { + "hour": 12, + "minute": 0, + "second": 0 + } + } + } + ], + "datetime": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 27 + }, + "time": { + "hour": 16, + "minute": 30, + "second": 0 + } + }, + "interval": 1 + }, + "output": [] + }, + { + "name": "reminder_check.events-2", + "input": { + "reminder": { + "frequency": "daily", + "offset": 16, + "from": 24, + "to": 48 + }, + "events": [ + { + "title": "e1", + "begin": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 28 + }, + "time": { + "hour": 12, + "minute": 0, + "second": 0 + } + } + } + ], + "datetime": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 27 + }, + "time": { + "hour": 16, + "minute": 30, + "second": 0 + } + }, + "interval": 1 + }, + "output": ["e1"] + }, + { + "name": "reminder_check.events-3", + "input": { + "reminder": { + "frequency": "daily", + "offset": 16, + "from": 24, + "to": 48 + }, + "events": [ + { + "title": "e1", + "begin": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 29 + }, + "time": { + "hour": 12, + "minute": 0, + "second": 0 + } + } + } + ], + "datetime": { + "timezone_shift": 0, + "date": { + "year": 2025, + "month": 6, + "day": 27 + }, + "time": { + "hour": 16, + "minute": 30, + "second": 0 + } + }, + "interval": 1 + }, + "output": [] + } +] diff --git a/data/reminder_covers_event.testdata.json b/data/reminder_covers_event.testdata.json deleted file mode 100644 index 8fca1b6..0000000 --- a/data/reminder_covers_event.testdata.json +++ /dev/null @@ -1,122 +0,0 @@ -[ - { - "input": { - "reminder": { - "frequency": "daily", - "offset": 0, - "from": 24, - "to": 48 - }, - "event": { - "begin": { - "timezone_shift": 0, - "date": { - "year": 2025, - "month": 6, - "day": 25 - }, - "time": { - "hour": 12, - "minute": 0, - "second": 0 - } - } - }, - "datetime": { - "timezone_shift": 0, - "date": { - "year": 2025, - "month": 6, - "day": 23 - }, - "time": { - "hour": 10, - "minute": 0, - "second": 0 - } - }, - "interval": 1 - }, - "output": false - }, - { - "input": { - "reminder": { - "frequency": "daily", - "offset": 0, - "from": 24, - "to": 48 - }, - "event": { - "begin": { - "timezone_shift": 0, - "date": { - "year": 2025, - "month": 6, - "day": 25 - }, - "time": { - "hour": 12, - "minute": 0, - "second": 0 - } - } - }, - "datetime": { - "timezone_shift": 0, - "date": { - "year": 2025, - "month": 6, - "day": 24 - }, - "time": { - "hour": 10, - "minute": 0, - "second": 0 - } - }, - "interval": 1 - }, - "output": true - }, - { - "input": { - "reminder": { - "frequency": "daily", - "offset": 0, - "from": 24, - "to": 48 - }, - "event": { - "begin": { - "timezone_shift": 0, - "date": { - "year": 2025, - "month": 6, - "day": 25 - }, - "time": { - "hour": 12, - "minute": 0, - "second": 0 - } - } - }, - "datetime": { - "timezone_shift": 0, - "date": { - "year": 2025, - "month": 6, - "day": 25 - }, - "time": { - "hour": 10, - "minute": 0, - "second": 0 - } - }, - "interval": 1 - }, - "output": false - } -] diff --git a/data/reminder_due.testdata.json b/data/reminder_due.testdata.json deleted file mode 100644 index 1d7211c..0000000 --- a/data/reminder_due.testdata.json +++ /dev/null @@ -1,77 +0,0 @@ -[ - { - "input": { - "reminder": { - "frequency": "daily", - "offset": 16, - "from": 24, - "to": 48 - }, - "datetime": { - "timezone_shift": 0, - "date": { - "year": 2025, - "month": 6, - "day": 27 - }, - "time": { - "hour": 15, - "minute": 30, - "second": 0 - } - }, - "interval": 1 - }, - "output": false - }, - { - "input": { - "reminder": { - "frequency": "daily", - "offset": 16, - "from": 24, - "to": 48 - }, - "datetime": { - "timezone_shift": 0, - "date": { - "year": 2025, - "month": 6, - "day": 27 - }, - "time": { - "hour": 16, - "minute": 30, - "second": 0 - } - }, - "interval": 1 - }, - "output": true - }, - { - "input": { - "reminder": { - "frequency": "daily", - "offset": 16, - "from": 24, - "to": 48 - }, - "datetime": { - "timezone_shift": 0, - "date": { - "year": 2025, - "month": 6, - "day": 27 - }, - "time": { - "hour": 17, - "minute": 30, - "second": 0 - } - }, - "interval": 1 - }, - "output": false - } -] diff --git a/source/conf.ts b/source/conf.ts index f492559..fdb581f 100644 --- a/source/conf.ts +++ b/source/conf.ts @@ -312,6 +312,7 @@ namespace _munin.conf title : string; time : string; location : string; + events : string; }; }; @@ -532,7 +533,7 @@ namespace _munin.conf } ), "settings": conf_v4.settings, - "labels": conf_v4.labels, + "labels": Object.assign({"events": "Termine"}, conf_v4.labels), }; } @@ -1044,6 +1045,11 @@ namespace _munin.conf "type": "string", "default": "wo" }, + "events": { + "nullable": false, + "type": "string", + "default": "Termine" + }, }, "additionalProperties": false, "required": [ diff --git a/source/logic.ts b/source/logic.ts index 9a76d62..2f5c874 100644 --- a/source/logic.ts +++ b/source/logic.ts @@ -67,101 +67,64 @@ namespace _munin.logic /** */ - export function reminder_due( + export function reminder_check( reminder : type_reminder, + events_all : Array<_munin.type_event>, { "pit": pit = lib_plankton.pit.now(), "interval": interval = 1, - } : { + } + : { pit ?: lib_plankton.pit.type_pit; interval ?: int; - } = { } - ) : boolean + = { + } + ) : (null | Array<_munin.type_event>) { const anchor : lib_plankton.pit.type_pit = frequency_anchor( reminder.frequency, {"pit": pit} ); - const window_from : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( + const dueness_window_from : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( anchor, (reminder.offset + 0) ); - const window_to : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( + const dueness_window_to : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( anchor, (reminder.offset + interval) ); - const result : boolean = lib_plankton.pit.is_between( + const due : boolean = lib_plankton.pit.is_between( pit, - window_from, - window_to + dueness_window_from, + dueness_window_to ); - lib_plankton.log._info( - "munin.logic.reminder_due", - { - "details": { - "reminder": reminder, - "datetime": lib_plankton.pit.to_datetime(pit), - "interval": interval, - "anchor": lib_plankton.pit.to_datetime(anchor), - "window_from": lib_plankton.pit.to_datetime(window_from), - "window_to": lib_plankton.pit.to_datetime(window_to), - "result": result, - } - } - ); - return result; - } - - - /** - */ - export function reminder_covers_event( - reminder : type_reminder, - event : _munin.type_event, + if (! due) { - "pit": pit = lib_plankton.pit.now(), - } : { - pit ?: lib_plankton.pit.type_pit; - } = { + return null; + } + else + { + const events_window_from : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( + anchor, + reminder.from + ); + const events_window_to : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( + anchor, + reminder.to + ); + const events : Array<_munin.type_event> = ( + events_all + .filter( + (event) => lib_plankton.pit.is_between( + lib_plankton.pit.from_datetime(event.begin), + events_window_from, + events_window_to + ) + ) + ); + return events; } - ) : boolean - { - const anchor : lib_plankton.pit.type_pit = frequency_anchor( - reminder.frequency, - {"pit": pit} - ); - const window_from : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( - anchor, - reminder.from - ); - const window_to : lib_plankton.pit.type_pit = lib_plankton.pit.shift_hour( - anchor, - reminder.to - ); - const event_begin : lib_plankton.pit.type_pit = lib_plankton.pit.from_datetime( - event.begin - ); - const result : boolean = lib_plankton.pit.is_between( - event_begin, - window_from, - window_to - ); - lib_plankton.log._info( - "munin.logic.reminder_covers_event", - { - "details": { - "reminder": reminder, - "event": event, - "datetime": lib_plankton.pit.to_datetime(pit), - "anchor": lib_plankton.pit.to_datetime(anchor), - "window_from": lib_plankton.pit.to_datetime(window_from), - "window_to": lib_plankton.pit.to_datetime(window_to), - "result": result, - } - } - ); - return result; } @@ -173,76 +136,85 @@ namespace _munin.logic targets : Array<_munin.type_target>, { "dry_run": dry_run = false, - } : { + } + : { dry_run ?: boolean; - } = { + } + = { } ) : Promise { const now : lib_plankton.pit.type_pit = lib_plankton.pit.now(); - const events : Array<_munin.type_event> = ( + const events_all : Array<_munin.type_event> = ( (await Promise.all(sources.map(source => source.fetch()))) .reduce((x, y) => x.concat(y), []) ); - for (const target of targets) { - for (const reminder of target.reminders) { - const due : boolean = reminder_due( + for (const target of targets) + { + for (const reminder of target.reminders) + { + const events : Array<_munin.type_event> = reminder_check( reminder, + events_all, { "pit": now, "interval": conf.settings.interval, } ); - if (! due) + if (events === null) { - // do nothing - } - else - { - const events_matching : Array<_munin.type_event> = events.filter( - event => reminder_covers_event( - reminder, - event, - { - "pit": now, + lib_plankton.log._info( + "munin.run_iteration.reminder_not_due", + { + "details": { + "reminder": reminder, } - ) + } + ); + } + else { + if (events.length <= 0) + { + lib_plankton.log._info( + "munin.run_iteration.no_matching_events", + { + "details": { + "reminder": reminder, + } + } ); - if (events_matching.length <= 0) { - // do nothing } else { lib_plankton.log._info( - "munin.remind.do", + "munin.run_iteration.remind", { "details": { + "reminder": reminder, "events": events, "target": target.show(), } } ); - if (dry_run) { + if (dry_run) + { // do nothing } - else { - /** - * @todo bundle? - */ - for (const event of events_matching) + else + { + try { - try { - await target.send(conf.labels, event); - } - catch (error) { - lib_plankton.log.error( - "munin.remind.error", - { - "details": { - "message": String(error), - } + await target.send(conf.labels, events); + } + catch (error) + { + lib_plankton.log.error( + "munin.remind.error", + { + "details": { + "message": String(error), } - ); - } + } + ); } } } diff --git a/source/targets/email.ts b/source/targets/email.ts index 358ebd6..4e29c7c 100644 --- a/source/targets/email.ts +++ b/source/targets/email.ts @@ -37,10 +37,97 @@ namespace _munin.targets.email /** */ - async function send( + function summarize_event( parameters : type_parameters, labels : _munin.type_labels, event : _munin.type_event + ) : string + { + return lib_plankton.string.coin( + "[{{head}}] {{date}} : {{macro_tags}}{{title}}", + { + "head": labels.head, + "date": lib_plankton.pit.date_format( + event.begin.date + ), + "macro_tags": ( + (event.tags === null) + ? + "" + : + (event.tags.map(tag => ("{" + tag + "}")).join(" ") + " ") + ), + "title": event.title, + } + ); + } + + + /** + */ + function render_event( + parameters : type_parameters, + labels : _munin.type_labels, + event : _munin.type_event + ) : string + { + return lib_plankton.string.coin( + "{{title_label}} | {{macro_tags}}{{title_value}}\n{{time_label}} | {{time_value}}{{macro_location}}{{macro_description}}", + { + "title_label": labels.title.toUpperCase(), + "macro_tags": ( + (parameters.hide_tags || (event.tags === null)) + ? + "" + : + (event.tags.map(tag => ("{" + tag + "}")).join(" ") + " ") + ), + "title_value": event.title, + "time_label": labels.time.toUpperCase(), + "time_value": lib_plankton.pit.timespan_format( + event.begin, + event.end, + { + "adjust_to_ce": true, + } + ), + "macro_location": ( + (event.location === null) + ? + "" + : + lib_plankton.string.coin( + "\n{{location_label}} | {{location_value}}", + { + "location_label": labels.location.toUpperCase(), + "location_value": event.location, + } + ) + ), + "macro_description": ( + (event.description === null) + ? + "" + : + lib_plankton.string.coin( + "\n\n{{description_value}}", + { + "description_value": event.description, + } + ) + ), + } + ); + } + + + + /** + */ + async function send( + parameters : type_parameters, + labels : _munin.type_labels, + events : Array<_munin.type_event> ) : Promise { await lib_plankton.email.send( @@ -52,69 +139,24 @@ namespace _munin.targets.email }, parameters.sender, parameters.receivers, - lib_plankton.string.coin( - "[{{head}}] {{date}} : {{macro_tags}}{{title}}", - { - "head": labels.head, - "date": lib_plankton.pit.date_format( - event.begin.date - ), - "macro_tags": ( - (event.tags === null) - ? - "" - : - (event.tags.map(tag => ("{" + tag + "}")).join(" ") + " ") - ), - "title": event.title, - } + ( + (events.length === 1) + ? + summarize_event(parameters, labels, events[0]) + : + lib_plankton.string.coin( + "[{{head}}] {{count}} {{events}}", + { + "head": labels.head, + "count": events.length.toFixed(0), + "events": labels.events, + } + ) ), - lib_plankton.string.coin( - "{{title_label}} | {{macro_tags}}{{title_value}}\n{{time_label}} | {{time_value}}{{macro_location}}{{macro_description}}", - { - "title_label": labels.title.toUpperCase(), - "macro_tags": ( - (parameters.hide_tags || (event.tags === null)) - ? - "" - : - (event.tags.map(tag => ("{" + tag + "}")).join(" ") + " ") - ), - "title_value": event.title, - "time_label": labels.time.toUpperCase(), - "time_value": lib_plankton.pit.timespan_format( - event.begin, - event.end, - { - "adjust_to_ce": true, - } - ), - "macro_location": ( - (event.location === null) - ? - "" - : - lib_plankton.string.coin( - "\n{{location_label}} | {{location_value}}", - { - "location_label": labels.location.toUpperCase(), - "location_value": event.location, - } - ) - ), - "macro_description": ( - (event.description === null) - ? - "" - : - lib_plankton.string.coin( - "\n\n{{description_value}}", - { - "description_value": event.description, - } - ) - ), - } + ( + events + .map(event => render_event(parameters, labels, event)) + .join("\n\n---\n\n") ) ); } @@ -134,7 +176,7 @@ namespace _munin.targets.email "receivers": parameters.receivers.join(","), } ), - "send": (labels, event) => send(parameters, labels, event), + "send": (labels, events) => send(parameters, labels, events), }; } diff --git a/source/targets/telegram_bot.ts b/source/targets/telegram_bot.ts index 44f0dd7..f46c164 100644 --- a/source/targets/telegram_bot.ts +++ b/source/targets/telegram_bot.ts @@ -33,60 +33,94 @@ namespace _munin.targets.telegram_bot /** */ - async function send( + function render_event( parameters : type_parameters, labels : _munin.type_labels, event : _munin.type_event + ) : string + { + return lib_plankton.string.coin( + "{{title_label}} | {{macro_tags}}{{title_value}}\n{{time_label}} | {{time_value}}{{macro_location}}{{macro_description}}", + { + "macro_tags": ( + (parameters.hide_tags || (event.tags === null)) + ? + "" + : + (event.tags.map(tag => ("{" + tag + "}")).join(" ") + " ") + ), + "title_label": labels.title.toUpperCase(), + "title_value": event.title, + "time_label": labels.time.toUpperCase(), + "time_value": lib_plankton.pit.timespan_format( + event.begin, + event.end, + { + "adjust_to_ce": true, + } + ), + "macro_location": ( + (event.location === null) + ? + "" + : + lib_plankton.string.coin( + "\n{{location_label}} | {{location_value}}", + { + "location_label": labels.location.toUpperCase(), + "location_value": event.location, + } + ) + ), + "macro_description": ( + (event.description === null) + ? + "" + : + lib_plankton.string.coin( + "\n\n{{description_value}}", + { + "description_value": event.description, + } + ) + ), + } + ); + } + + + /** + */ + async function send( + parameters : type_parameters, + labels : _munin.type_labels, + events : Array<_munin.type_event> ) : Promise { await lib_plankton.telegram.bot_call_send_message( parameters.bot_token, parameters.chat_id, lib_plankton.string.coin( - "*{{head}}*\n\n\{{title_label}} | {{macro_tags}}{{title_value}}\n{{time_label}} | {{time_value}}{{macro_location}}{{macro_description}}", + "*{{head_core}}{{head_extra}}*\n\n{{events}}", { - "head": labels.head, - "macro_tags": ( - (parameters.hide_tags || (event.tags === null)) - ? - "" - : - (event.tags.map(tag => ("{" + tag + "}")).join(" ") + " ") - ), - "title_label": labels.title.toUpperCase(), - "title_value": event.title, - "time_label": labels.time.toUpperCase(), - "time_value": lib_plankton.pit.timespan_format( - event.begin, - event.end, - { - "adjust_to_ce": true, - } - ), - "macro_location": ( - (event.location === null) + "head_core": labels.head, + "head_extra": ( + (events.length <= 1) ? "" : lib_plankton.string.coin( - "\n{{location_label}} | {{location_value}}", + " ({{count}} {{events}})", { - "location_label": labels.location.toUpperCase(), - "location_value": event.location, + "count": events.length.toFixed(0), + "events": labels.events, } ) ), - "macro_description": ( - (event.description === null) - ? - "" - : - lib_plankton.string.coin( - "\n\n{{description_value}}", - { - "description_value": event.description, - } - ) + "events": ( + events + .map(event => render_event(parameters, labels, event)) + .join("\n\n---\n\n") ), } ), @@ -111,7 +145,7 @@ namespace _munin.targets.telegram_bot "chat_id": parameters.chat_id.toFixed(0), } ), - "send": (labels, event) => send(parameters, labels, event), + "send": (labels, events) => send(parameters, labels, events), }; } diff --git a/source/test.ts b/source/test.ts index b570f3f..06a79de 100644 --- a/source/test.ts +++ b/source/test.ts @@ -20,10 +20,28 @@ along with »munin«. If not, see . namespace _munin.test { + + /** + * @todo outsource? + */ + function lists_equal( + list1 : Array, + list2 : Array + ) : boolean + { + return ( + (list1.length === list2.length) + && + list1.every( + (element, index) => (element === list2[index]) + ) + ); + } + /** */ - async function reminder_due( + async function reminder_check( ) : Promise { type type_input = { @@ -33,73 +51,23 @@ namespace _munin.test from : int; to : int; }; - datetime : lib_plankton.pit.type_datetime; - interval : int; - }; - type type_output = boolean; - const testcases : Array< - _munin.helpers.test.type_testcase< - type_input, - type_output - > - > = await _munin.helpers.test.get_data( - "data/reminder_due.testdata.json", - { - } - ); - for (const testcase of testcases) - { - _munin.helpers.test.start(testcase.name); - - // execution - const result : boolean = _munin.logic.reminder_due( + events : Array< { - "frequency": _munin.logic.frequency_decode(testcase.input.reminder.frequency), - "offset": testcase.input.reminder.offset, - "from": testcase.input.reminder.from, - "to": testcase.input.reminder.to, - }, - { - "pit": lib_plankton.pit.from_datetime(testcase.input.datetime), - "interval": testcase.input.interval, + title : string; + begin : lib_plankton.pit.type_datetime; } - ); - - // assertions - if (result !== testcase.output) - { - _munin.helpers.test.fail(testcase.name); - } - } - } - - - /** - */ - async function reminder_covers_event( - ) : Promise - { - type type_input = { - reminder : { - frequency : string; - offset : int; - from : int; - to : int; - }; - event : { - begin : lib_plankton.pit.type_datetime; - }; + >; datetime : lib_plankton.pit.type_datetime; interval : int; }; - type type_output = boolean; + type type_output = (null | Array); const testcases : Array< _munin.helpers.test.type_testcase< type_input, type_output > > = await _munin.helpers.test.get_data( - "data/reminder_covers_event.testdata.json", + "data/reminder_check.testdata.json", { } ); @@ -109,28 +77,50 @@ namespace _munin.test _munin.helpers.test.start(testcase.name); // execution - const result : boolean = _munin.logic.reminder_covers_event( + const result : (null | Array<_munin.type_event>) = _munin.logic.reminder_check( { "frequency": _munin.logic.frequency_decode(testcase.input.reminder.frequency), "offset": testcase.input.reminder.offset, "from": testcase.input.reminder.from, "to": testcase.input.reminder.to, }, - { - "title": "test", - "begin": testcase.input.event.begin, - "end": null, - "description": null, - "location": null, - "tags": null, - }, + testcase.input.events.map( + event_raw => ({ + "title": event_raw.title, + "begin": event_raw.begin, + "end": null, + "description": null, + "location": null, + "tags": null, + }) + ), { "pit": lib_plankton.pit.from_datetime(testcase.input.datetime), + "interval": testcase.input.interval, } ); // assertions - if (result !== testcase.output) + if ( + ( + (testcase.output === null) + && + (result === null) + ) + || + ( + (testcase.output !== null) + && + lists_equal( + result.map(event => event.title), + testcase.output + ) + ) + ) + { + // success + } + else { _munin.helpers.test.fail(testcase.name); } @@ -143,8 +133,7 @@ namespace _munin.test export async function all( ) : Promise { - await reminder_due(); - await reminder_covers_event(); + await reminder_check(); } } diff --git a/source/types.ts b/source/types.ts index 9c56ef0..14b2cb6 100644 --- a/source/types.ts +++ b/source/types.ts @@ -38,6 +38,7 @@ namespace _munin title : string; time : string; location : string; + events : string; }; @@ -84,7 +85,7 @@ namespace _munin send : ( ( labels : type_labels, - event : type_event + events : Array ) => Promise -- 2.39.5