[task-340] [int]

This commit is contained in:
Christian Fraß 2025-06-27 20:07:18 +00:00
parent 9e698560fd
commit 358f33b18a
5 changed files with 779 additions and 197 deletions

View file

@ -27,6 +27,8 @@
"targets/email.ts",
"targets/_functions.ts",
"conf.ts",
"logic.ts",
"test.ts",
"main.ts"
]
}

300
source/logic.ts Normal file
View file

@ -0,0 +1,300 @@
/*
This file is part of »munin«.
Copyright 2025 'Fenris Wolf' <fenris@folksprak.org>
»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 <http://www.gnu.org/licenses/>.
*/
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<void>
{
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<void>
{
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);
}
}
}
}

View file

@ -20,146 +20,7 @@ along with »munin«. If not, see <http://www.gnu.org/licenses/>.
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<void>
{
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<void>
{
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;
}
}

384
source/test.ts Normal file
View file

@ -0,0 +1,384 @@
/*
This file is part of »munin«.
Copyright 2025 'Fenris Wolf' <fenris@folksprak.org>
»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 <http://www.gnu.org/licenses/>.
*/
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<type_testcase> = [
{
"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<type_testcase> = [
{
"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();
}
}

View file

@ -20,7 +20,17 @@ along with »munin«. If not, see <http://www.gnu.org/licenses/>.
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<int>;
reminders : Array<type_reminder>;
show : (() => string);
send : (
(