Termine gebündelt ausgeben #1

Merged
fenris merged 4 commits from task-340 into main 2025-06-30 13:06:51 +02:00
7 changed files with 416 additions and 297 deletions
Showing only changes of commit 37f7a27e95 - Show all commits

View file

@ -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
}
]

View file

@ -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
}
]

View file

@ -12,6 +12,8 @@
"email", "email",
"telegram", "telegram",
"url", "url",
"json",
"file",
"conf", "conf",
"log", "log",
"args" "args"
@ -20,6 +22,7 @@
} }
], ],
"sources": [ "sources": [
"helpers/test.ts",
"types.ts", "types.ts",
"sources/ical_feed.ts", "sources/ical_feed.ts",
"sources/_functions.ts", "sources/_functions.ts",

124
source/helpers/test.ts Normal file
View file

@ -0,0 +1,124 @@
/*
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.helpers.test
{
/**
* @todo outsource
*/
type type_testcase_raw<type_input, type_output> = {
active ?: boolean;
name ?: string;
input : type_input;
output : type_output;
};
/**
* @todo outsource
*/
export type type_testcase<type_input, type_output> = {
name : string;
input : type_input;
output : type_output;
};
/**
* @todo outsource
*/
export async function get_data<type_input, type_output>(
path : string,
{
"default_active": default_active = true,
} : {
default_active ?: boolean;
} = {
}
) : Promise<Array<type_testcase<type_input, type_output>>>
{
const content : string = await lib_plankton.file.read(path);
const testcases_raw : Array<type_testcase_raw<type_input, type_output>> = (
lib_plankton.json.decode(content) as Array<type_testcase_raw<type_input, type_output>>
);
const testcases : Array<type_testcase<type_input, type_output>> = (
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,
}
}
);
}
}

View file

@ -21,6 +21,23 @@ along with »munin«. If not, see <http://www.gnu.org/licenses/>.
namespace _munin.logic 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( function frequency_anchor(
@ -99,7 +116,7 @@ namespace _munin.logic
/** /**
*/ */
export function reminder_event_in_window( export function reminder_covers_event(
reminder : type_reminder, reminder : type_reminder,
event : _munin.type_event, event : _munin.type_event,
{ {
@ -131,7 +148,7 @@ namespace _munin.logic
window_to window_to
); );
lib_plankton.log._info( lib_plankton.log._info(
"munin.logic.reminder_event_in_window", "munin.logic.reminder_covers_event",
{ {
"details": { "details": {
"reminder": reminder, "reminder": reminder,
@ -183,7 +200,7 @@ namespace _munin.logic
else else
{ {
const events_matching : Array<_munin.type_event> = events.filter( const events_matching : Array<_munin.type_event> = events.filter(
event => reminder_event_in_window( event => reminder_covers_event(
reminder, reminder,
event, event,
{ {

View file

@ -23,189 +23,44 @@ namespace _munin.test
/** /**
*/ */
type type_datetime = { async function reminder_due(
year : int; ) : Promise<void>
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( type type_input = {
{ reminder : {
"date": { frequency : string;
"year": datetime.year, offset : int;
"month": datetime.month, from : int;
"day": datetime.day, to : int;
},
"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; datetime : lib_plankton.pit.type_datetime;
interval : int;
}; };
const testcases : Array<type_testcase> = [ type type_output = boolean;
const testcases : Array<
_munin.helpers.test.type_testcase<
type_input,
type_output
>
> = await _munin.helpers.test.get_data<type_input, type_output>(
"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) for (const testcase of testcases)
{ {
start(testcase.name); _munin.helpers.test.start(testcase.name);
// execution // execution
const result : boolean = _munin.logic.reminder_due( 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, "offset": testcase.input.reminder.offset,
"from": testcase.input.reminder.from, "from": testcase.input.reminder.from,
"to": testcase.input.reminder.to, "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, "interval": testcase.input.interval,
} }
); );
@ -213,7 +68,7 @@ namespace _munin.test
// assertions // assertions
if (result !== testcase.output) if (result !== testcase.output)
{ {
fail(testcase.name); _munin.helpers.test.fail(testcase.name);
} }
} }
} }
@ -221,151 +76,63 @@ namespace _munin.test
/** /**
*/ */
export function reminder_event_in_window( async function reminder_covers_event(
) : void ) : Promise<void>
{ {
type type_testcase = { type type_input = {
name : string; reminder : {
input : { frequency : string;
reminder : { offset : int;
frequency : string; from : int;
offset : int; to : int;
from : int;
to : int;
};
event : {
begin : type_datetime;
};
datetime : type_datetime;
interval : int;
}; };
output : boolean; event : {
begin : lib_plankton.pit.type_datetime;
};
datetime : lib_plankton.pit.type_datetime;
interval : int;
}; };
const testcases : Array<type_testcase> = [ type type_output = boolean;
const testcases : Array<
_munin.helpers.test.type_testcase<
type_input,
type_output
>
> = await _munin.helpers.test.get_data<type_input, type_output>(
"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) for (const testcase of testcases)
{ {
start(testcase.name); _munin.helpers.test.start(testcase.name);
// execution // 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, "offset": testcase.input.reminder.offset,
"from": testcase.input.reminder.from, "from": testcase.input.reminder.from,
"to": testcase.input.reminder.to, "to": testcase.input.reminder.to,
}, },
{ {
"title": "test", "title": "test",
"begin": lib_plankton.pit.to_datetime(datetime_to_pit(testcase.input.event.begin)), "begin": testcase.input.event.begin,
"end": null, "end": null,
"description": null, "description": null,
"location": null, "location": null,
"tags": null, "tags": null,
}, },
{ {
"pit": datetime_to_pit(testcase.input.datetime), "pit": lib_plankton.pit.from_datetime(testcase.input.datetime),
} }
); );
// assertions // assertions
if (result !== testcase.output) if (result !== testcase.output)
{ {
fail(testcase.name); _munin.helpers.test.fail(testcase.name);
} }
} }
} }
@ -373,11 +140,11 @@ namespace _munin.test
/** /**
*/ */
export function all( export async function all(
) : void ) : Promise<void>
{ {
reminder_due(); await reminder_due();
reminder_event_in_window(); await reminder_covers_event();
} }
} }

View file

@ -1,4 +1,13 @@
#!/usr/bin/env sh #!/usr/bin/env bash
## core
tools/ivaldi build 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 -