855 lines
22 KiB
ObjectPascal
855 lines
22 KiB
ObjectPascal
{ test_runner -- SQLite-backed end-to-end scheduler tests.
|
|
|
|
Each scenario gets its own temp DB so failures don't pollute
|
|
later runs. Timing-sensitive scenarios use 1-second intervals
|
|
and ~3-second sleeps -- the scheduler wakes once a second so a
|
|
3-second window catches >= 2 firings reliably. }
|
|
|
|
program test_runner;
|
|
|
|
{$mode objfpc}{$H+}
|
|
|
|
uses
|
|
{$IFDEF UNIX}cthreads,{$ENDIF}
|
|
Classes, SysUtils, fpjson, syncobjs, DateUtils,
|
|
log.types,
|
|
database.types, database.dialect, database.schema, database.pool,
|
|
cron.types, cron.events, cron.runner;
|
|
|
|
var
|
|
Total, Passed, Failed: Integer;
|
|
|
|
procedure Check(const Name: string; OK: Boolean; const Detail: string = '');
|
|
begin
|
|
Inc(Total);
|
|
if OK then
|
|
begin
|
|
Inc(Passed);
|
|
Writeln(' [PASS] ', Name);
|
|
end
|
|
else
|
|
begin
|
|
Inc(Failed);
|
|
if Detail <> '' then
|
|
Writeln(' [FAIL] ', Name, ' -- ', Detail)
|
|
else
|
|
Writeln(' [FAIL] ', Name);
|
|
end;
|
|
end;
|
|
|
|
function MakeTempDB(const Tag: string): string;
|
|
begin
|
|
Result := '/tmp/fpcsched_' + Tag + '_' +
|
|
IntToStr(GetProcessID) + '_' +
|
|
IntToStr(Random(99999)) + '.sqlite3';
|
|
if FileExists(Result) then DeleteFile(Result);
|
|
end;
|
|
|
|
function MakePool(const APath: string): TDBPool;
|
|
begin
|
|
Result := TDBPool.Create;
|
|
Result.Init(dbSQLite, APath);
|
|
end;
|
|
|
|
{ Bootstrap the schema independently of TCron.Create so tests
|
|
can seed rows before constructing a scheduler. }
|
|
procedure EnsureSchedulerSchema(APool: TDBPool);
|
|
begin
|
|
APool.DeclareTable(BuildSystemSchedulerSpec(APool.Dialect.NowExpr));
|
|
APool.DeclareTable(BuildSchedulerLogSpec);
|
|
end;
|
|
|
|
{ ---- Test recorders ---- }
|
|
|
|
type
|
|
TRecorder = class
|
|
public
|
|
Lock: TCriticalSection;
|
|
PluginCalls: TStringList;
|
|
SystemCalls: TStringList;
|
|
Events: TStringList;
|
|
FailNext: Boolean;
|
|
constructor Create;
|
|
destructor Destroy; override;
|
|
procedure HandlePluginTask(const APluginName, ATaskName: string);
|
|
procedure HandleSystemTask(const ATaskName: string);
|
|
procedure HandleSystemTaskFailing(const ATaskName: string);
|
|
procedure HandleTaskStart(const APluginName, ATaskName: string);
|
|
procedure HandleTaskComplete(const APluginName, ATaskName: string;
|
|
ASuccess: Boolean; ADurationMs: Integer;
|
|
const AError: string);
|
|
procedure HandleLog(Level: TLogLevel; const Category, Msg: string);
|
|
end;
|
|
|
|
constructor TRecorder.Create;
|
|
begin
|
|
inherited Create;
|
|
Lock := TCriticalSection.Create;
|
|
PluginCalls := TStringList.Create;
|
|
SystemCalls := TStringList.Create;
|
|
Events := TStringList.Create;
|
|
end;
|
|
|
|
destructor TRecorder.Destroy;
|
|
begin
|
|
PluginCalls.Free;
|
|
SystemCalls.Free;
|
|
Events.Free;
|
|
Lock.Free;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TRecorder.HandlePluginTask(const APluginName, ATaskName: string);
|
|
begin
|
|
Lock.Enter;
|
|
try
|
|
PluginCalls.Add(APluginName + '/' + ATaskName);
|
|
finally
|
|
Lock.Leave;
|
|
end;
|
|
end;
|
|
|
|
procedure TRecorder.HandleSystemTask(const ATaskName: string);
|
|
begin
|
|
Lock.Enter;
|
|
try
|
|
SystemCalls.Add(ATaskName);
|
|
finally
|
|
Lock.Leave;
|
|
end;
|
|
end;
|
|
|
|
procedure TRecorder.HandleSystemTaskFailing(const ATaskName: string);
|
|
begin
|
|
Lock.Enter;
|
|
try
|
|
SystemCalls.Add(ATaskName);
|
|
finally
|
|
Lock.Leave;
|
|
end;
|
|
raise Exception.Create('synthetic system task failure');
|
|
end;
|
|
|
|
procedure TRecorder.HandleTaskStart(const APluginName, ATaskName: string);
|
|
begin
|
|
Lock.Enter;
|
|
try
|
|
Events.Add('start:' + APluginName + '/' + ATaskName);
|
|
finally
|
|
Lock.Leave;
|
|
end;
|
|
end;
|
|
|
|
procedure TRecorder.HandleTaskComplete(const APluginName, ATaskName: string;
|
|
ASuccess: Boolean; ADurationMs: Integer; const AError: string);
|
|
var
|
|
S: string;
|
|
begin
|
|
if ASuccess then S := 'ok' else S := 'err';
|
|
Lock.Enter;
|
|
try
|
|
Events.Add('complete:' + APluginName + '/' + ATaskName + '=' + S);
|
|
finally
|
|
Lock.Leave;
|
|
end;
|
|
end;
|
|
|
|
procedure TRecorder.HandleLog(Level: TLogLevel; const Category, Msg: string);
|
|
begin
|
|
{ Tests don't assert on log output but the handler is wired up
|
|
so the runner exercises its log path during a test run. }
|
|
end;
|
|
|
|
{ ---- helpers ---- }
|
|
|
|
procedure InsertTaskRow(Pool: TDBPool; const PluginName, TaskName: string;
|
|
IntervalSec: Integer; const Category: string;
|
|
Enabled: Boolean; FireImmediately: Boolean);
|
|
var
|
|
Params: TJSONObject;
|
|
En: Integer;
|
|
begin
|
|
if Enabled then En := 1 else En := 0;
|
|
Params := TJSONObject.Create;
|
|
try
|
|
Params.Add('pn', PluginName);
|
|
Params.Add('tn', TaskName);
|
|
Params.Add('iv', IntervalSec);
|
|
Params.Add('cat', Category);
|
|
Params.Add('en', En);
|
|
if FireImmediately then
|
|
Params.Add('nr', FormatDateTime('yyyy-mm-dd hh:nn:ss',
|
|
LocalTimeToUniversal(Now) - 1))
|
|
else
|
|
Params.Add('nr', FormatDateTime('yyyy-mm-dd hh:nn:ss',
|
|
LocalTimeToUniversal(Now) + 0.001));
|
|
Pool.ExecSQL(
|
|
'INSERT INTO system_scheduler ' +
|
|
'(task_name, plugin_name, description, category, ' +
|
|
' schedule_type, interval_seconds, enabled, next_run) VALUES ' +
|
|
'(:tn, :pn, '''', :cat, ''interval'', :iv, :en, :nr)',
|
|
Params);
|
|
finally
|
|
Params.Free;
|
|
end;
|
|
end;
|
|
|
|
function CountSchedulerLogRows(Pool: TDBPool;
|
|
const Plugin, Task: string): Integer;
|
|
var
|
|
P: TJSONObject;
|
|
begin
|
|
P := TJSONObject.Create;
|
|
try
|
|
P.Add('pn', Plugin);
|
|
P.Add('tn', Task);
|
|
Result := Pool.QueryInt(
|
|
'SELECT COUNT(*) FROM scheduler_log WHERE plugin_name = :pn ' +
|
|
'AND task_name = :tn', P);
|
|
finally
|
|
P.Free;
|
|
end;
|
|
end;
|
|
|
|
{ ---- Scenarios ---- }
|
|
|
|
procedure TestInit;
|
|
var
|
|
DBFile: string;
|
|
Pool: TDBPool;
|
|
Sched: TCron;
|
|
Tasks: TJSONArray;
|
|
begin
|
|
Writeln('-- init: Create declares tables');
|
|
DBFile := MakeTempDB('init');
|
|
Pool := MakePool(DBFile);
|
|
try
|
|
Sched := TCron.Create(Pool);
|
|
try
|
|
Check('system_scheduler table created',
|
|
Pool.TableExists('system_scheduler'));
|
|
Check('scheduler_log table created',
|
|
Pool.TableExists('scheduler_log'));
|
|
Tasks := Sched.GetTasksJSON;
|
|
try
|
|
Check('GetTasksJSON returns 0 tasks initially',
|
|
Tasks.Count = 0);
|
|
finally
|
|
Tasks.Free;
|
|
end;
|
|
finally
|
|
Sched.Free;
|
|
end;
|
|
finally
|
|
Pool.Free;
|
|
DeleteFile(DBFile);
|
|
end;
|
|
end;
|
|
|
|
procedure TestIntervalFires;
|
|
var
|
|
DBFile: string;
|
|
Pool: TDBPool;
|
|
Sched: TCron;
|
|
Rec: TRecorder;
|
|
begin
|
|
Writeln('-- interval task fires repeatedly');
|
|
DBFile := MakeTempDB('interval');
|
|
Pool := MakePool(DBFile);
|
|
Rec := TRecorder.Create;
|
|
try
|
|
EnsureSchedulerSchema(Pool);
|
|
InsertTaskRow(Pool, 'plug1', 'task1', 1, 'plugin', True, True);
|
|
|
|
Sched := TCron.Create(Pool, @Rec.HandlePluginTask, nil,
|
|
@Rec.HandleLog);
|
|
try
|
|
Sched.Start;
|
|
Sleep(3500);
|
|
Sched.Terminate;
|
|
finally
|
|
Sched.Free;
|
|
end;
|
|
|
|
Check(Format('plugin task fired at least 2 times (got %d)',
|
|
[Rec.PluginCalls.Count]),
|
|
Rec.PluginCalls.Count >= 2);
|
|
Check('scheduler_log has matching rows',
|
|
CountSchedulerLogRows(Pool, 'plug1', 'task1') >= 2);
|
|
finally
|
|
Rec.Free;
|
|
Pool.Free;
|
|
DeleteFile(DBFile);
|
|
end;
|
|
end;
|
|
|
|
procedure TestSystemTaskFires;
|
|
var
|
|
DBFile: string;
|
|
Pool: TDBPool;
|
|
Sched: TCron;
|
|
Rec: TRecorder;
|
|
begin
|
|
Writeln('-- system task dispatch via RegisterSystemTask');
|
|
DBFile := MakeTempDB('system');
|
|
Pool := MakePool(DBFile);
|
|
Rec := TRecorder.Create;
|
|
try
|
|
EnsureSchedulerSchema(Pool);
|
|
InsertTaskRow(Pool, 'system', 'cleanup', 1, 'general', True, True);
|
|
|
|
Sched := TCron.Create(Pool, nil, nil, @Rec.HandleLog);
|
|
try
|
|
Sched.RegisterSystemTask('cleanup', @Rec.HandleSystemTask);
|
|
Sched.Start;
|
|
Sleep(2500);
|
|
Sched.Terminate;
|
|
finally
|
|
Sched.Free;
|
|
end;
|
|
|
|
Check(Format('system task fired at least once (got %d)',
|
|
[Rec.SystemCalls.Count]),
|
|
Rec.SystemCalls.Count >= 1);
|
|
Check('scheduler_log records system success',
|
|
Pool.QueryInt(
|
|
'SELECT COUNT(*) FROM scheduler_log WHERE plugin_name = ''system''' +
|
|
' AND task_name = ''cleanup'' AND result = ''success''') >= 1);
|
|
finally
|
|
Rec.Free;
|
|
Pool.Free;
|
|
DeleteFile(DBFile);
|
|
end;
|
|
end;
|
|
|
|
procedure TestSystemTaskFailureRecorded;
|
|
var
|
|
DBFile: string;
|
|
Pool: TDBPool;
|
|
Sched: TCron;
|
|
Rec: TRecorder;
|
|
begin
|
|
Writeln('-- system task that raises is recorded as error');
|
|
DBFile := MakeTempDB('failing');
|
|
Pool := MakePool(DBFile);
|
|
Rec := TRecorder.Create;
|
|
try
|
|
EnsureSchedulerSchema(Pool);
|
|
InsertTaskRow(Pool, 'system', 'fails', 1, 'general', True, True);
|
|
|
|
Sched := TCron.Create(Pool, nil, nil, @Rec.HandleLog);
|
|
try
|
|
Sched.RegisterSystemTask('fails', @Rec.HandleSystemTaskFailing);
|
|
Sched.Start;
|
|
Sleep(2500);
|
|
Sched.Terminate;
|
|
finally
|
|
Sched.Free;
|
|
end;
|
|
|
|
Check('failing system task entered',
|
|
Rec.SystemCalls.Count >= 1);
|
|
Check('scheduler_log records error',
|
|
Pool.QueryInt(
|
|
'SELECT COUNT(*) FROM scheduler_log WHERE plugin_name = ''system''' +
|
|
' AND task_name = ''fails'' AND result = ''error''') >= 1);
|
|
Check('system_scheduler fail_count incremented',
|
|
Pool.QueryInt(
|
|
'SELECT fail_count FROM system_scheduler ' +
|
|
'WHERE plugin_name = ''system'' AND task_name = ''fails''') >= 1);
|
|
finally
|
|
Rec.Free;
|
|
Pool.Free;
|
|
DeleteFile(DBFile);
|
|
end;
|
|
end;
|
|
|
|
procedure TestUpdateTaskDisables;
|
|
var
|
|
DBFile: string;
|
|
Pool: TDBPool;
|
|
Sched: TCron;
|
|
Rec: TRecorder;
|
|
TaskID: Int64;
|
|
Updates: TJSONObject;
|
|
CountAtDisable, CountAfterWait: Integer;
|
|
begin
|
|
Writeln('-- UpdateTask({enabled:false}) stops further firings');
|
|
DBFile := MakeTempDB('disable');
|
|
Pool := MakePool(DBFile);
|
|
Rec := TRecorder.Create;
|
|
try
|
|
EnsureSchedulerSchema(Pool);
|
|
InsertTaskRow(Pool, 'plug', 'taskd', 1, 'plugin', True, True);
|
|
TaskID := Pool.QueryInt(
|
|
'SELECT id FROM system_scheduler WHERE task_name = ''taskd''');
|
|
|
|
Sched := TCron.Create(Pool, @Rec.HandlePluginTask, nil,
|
|
@Rec.HandleLog);
|
|
try
|
|
Sched.Start;
|
|
Sleep(2500);
|
|
CountAtDisable := Rec.PluginCalls.Count;
|
|
|
|
Updates := TJSONObject.Create;
|
|
try
|
|
Updates.Add('enabled', False);
|
|
Check('UpdateTask returns True',
|
|
Sched.UpdateTask(TaskID, Updates));
|
|
finally
|
|
Updates.Free;
|
|
end;
|
|
|
|
Sleep(2500);
|
|
CountAfterWait := Rec.PluginCalls.Count;
|
|
Sched.Terminate;
|
|
finally
|
|
Sched.Free;
|
|
end;
|
|
|
|
Check(Format('saw firings before disable (got %d)',
|
|
[CountAtDisable]),
|
|
CountAtDisable >= 1);
|
|
Check(Format('no further firings after disable (was %d, now %d)',
|
|
[CountAtDisable, CountAfterWait]),
|
|
CountAfterWait = CountAtDisable);
|
|
finally
|
|
Rec.Free;
|
|
Pool.Free;
|
|
DeleteFile(DBFile);
|
|
end;
|
|
end;
|
|
|
|
procedure TestRefreshTasks;
|
|
var
|
|
DBFile: string;
|
|
Pool: TDBPool;
|
|
Sched: TCron;
|
|
Rec: TRecorder;
|
|
begin
|
|
Writeln('-- RefreshTasks pulls a row added externally and runs it');
|
|
DBFile := MakeTempDB('refresh');
|
|
Pool := MakePool(DBFile);
|
|
Rec := TRecorder.Create;
|
|
try
|
|
Sched := TCron.Create(Pool, @Rec.HandlePluginTask, nil,
|
|
@Rec.HandleLog);
|
|
try
|
|
Sched.Start;
|
|
Sleep(1200);
|
|
Check('no fires before task is added',
|
|
Rec.PluginCalls.Count = 0);
|
|
|
|
InsertTaskRow(Pool, 'late', 'incoming', 1, 'plugin', True, True);
|
|
Sched.RefreshTasks;
|
|
Sleep(2500);
|
|
Sched.Terminate;
|
|
finally
|
|
Sched.Free;
|
|
end;
|
|
|
|
Check(Format('post-refresh task fired (got %d)',
|
|
[Rec.PluginCalls.Count]),
|
|
Rec.PluginCalls.Count >= 1);
|
|
finally
|
|
Rec.Free;
|
|
Pool.Free;
|
|
DeleteFile(DBFile);
|
|
end;
|
|
end;
|
|
|
|
procedure TestRunTaskNow;
|
|
var
|
|
DBFile: string;
|
|
Pool: TDBPool;
|
|
Sched: TCron;
|
|
Rec: TRecorder;
|
|
TaskID: Int64;
|
|
begin
|
|
Writeln('-- RunTaskNow fires synchronously without starting the thread');
|
|
DBFile := MakeTempDB('runnow');
|
|
Pool := MakePool(DBFile);
|
|
Rec := TRecorder.Create;
|
|
try
|
|
EnsureSchedulerSchema(Pool);
|
|
InsertTaskRow(Pool, 'plug', 'now', 60, 'plugin', True, False);
|
|
TaskID := Pool.QueryInt(
|
|
'SELECT id FROM system_scheduler WHERE task_name = ''now''');
|
|
Sched := TCron.Create(Pool, @Rec.HandlePluginTask, nil,
|
|
@Rec.HandleLog);
|
|
try
|
|
Check('task is registered',
|
|
TaskID > 0);
|
|
Sched.RunTaskNow(TaskID);
|
|
Check('RunTaskNow fired the callback',
|
|
Rec.PluginCalls.Count = 1);
|
|
Check('scheduler_log row written',
|
|
CountSchedulerLogRows(Pool, 'plug', 'now') = 1);
|
|
finally
|
|
Sched.Free;
|
|
end;
|
|
finally
|
|
Rec.Free;
|
|
Pool.Free;
|
|
DeleteFile(DBFile);
|
|
end;
|
|
end;
|
|
|
|
{ ---- SyncPluginTasks via callback ---- }
|
|
|
|
type
|
|
TPluginTaskSupplier = class
|
|
public
|
|
Tasks: TJSONArray;
|
|
constructor Create;
|
|
destructor Destroy; override;
|
|
procedure SetTasks(ATasks: TJSONArray);
|
|
function GetTasks: TJSONArray;
|
|
end;
|
|
|
|
constructor TPluginTaskSupplier.Create;
|
|
begin
|
|
inherited Create;
|
|
Tasks := nil;
|
|
end;
|
|
|
|
destructor TPluginTaskSupplier.Destroy;
|
|
begin
|
|
Tasks := nil;
|
|
inherited Destroy;
|
|
end;
|
|
|
|
procedure TPluginTaskSupplier.SetTasks(ATasks: TJSONArray);
|
|
begin
|
|
Tasks := ATasks;
|
|
end;
|
|
|
|
function TPluginTaskSupplier.GetTasks: TJSONArray;
|
|
var
|
|
Cloned: TJSONArray;
|
|
begin
|
|
{ The scheduler frees the returned array. Hand back a clone so
|
|
the caller can keep referring to its template. }
|
|
if Tasks = nil then
|
|
Cloned := TJSONArray.Create
|
|
else
|
|
Cloned := TJSONArray(Tasks.Clone);
|
|
Result := Cloned;
|
|
end;
|
|
|
|
procedure TestSyncPluginTasksRegisters;
|
|
var
|
|
DBFile: string;
|
|
Pool: TDBPool;
|
|
Sched: TCron;
|
|
Rec: TRecorder;
|
|
Sup: TPluginTaskSupplier;
|
|
Tasks: TJSONArray;
|
|
Obj: TJSONObject;
|
|
begin
|
|
Writeln('-- SyncPluginTasks inserts a row from the callback');
|
|
DBFile := MakeTempDB('syncadd');
|
|
Pool := MakePool(DBFile);
|
|
Rec := TRecorder.Create;
|
|
Sup := TPluginTaskSupplier.Create;
|
|
Tasks := TJSONArray.Create;
|
|
try
|
|
Obj := TJSONObject.Create;
|
|
Obj.Add('plugin_name', 'discovered');
|
|
Obj.Add('task_name', 'periodic');
|
|
Obj.Add('description', 'demo');
|
|
Obj.Add('interval_seconds', 60);
|
|
Tasks.Add(Obj);
|
|
Sup.SetTasks(Tasks);
|
|
|
|
Sched := TCron.Create(Pool, @Rec.HandlePluginTask, @Sup.GetTasks,
|
|
@Rec.HandleLog);
|
|
try
|
|
Check('row inserted into system_scheduler',
|
|
Pool.QueryInt(
|
|
'SELECT COUNT(*) FROM system_scheduler ' +
|
|
'WHERE plugin_name = ''discovered'' AND task_name = ''periodic''') = 1);
|
|
Check('description carried through',
|
|
Pool.QueryStr(
|
|
'SELECT description FROM system_scheduler ' +
|
|
'WHERE plugin_name = ''discovered''') = 'demo');
|
|
finally
|
|
Sched.Free;
|
|
end;
|
|
finally
|
|
Tasks.Free;
|
|
Sup.Free;
|
|
Rec.Free;
|
|
Pool.Free;
|
|
DeleteFile(DBFile);
|
|
end;
|
|
end;
|
|
|
|
procedure TestSyncPluginTasksRemovesOrphans;
|
|
var
|
|
DBFile: string;
|
|
Pool: TDBPool;
|
|
Sched: TCron;
|
|
Rec: TRecorder;
|
|
Sup: TPluginTaskSupplier;
|
|
Tasks: TJSONArray;
|
|
Obj: TJSONObject;
|
|
begin
|
|
Writeln('-- SyncPluginTasks removes rows whose plugin no longer reports them');
|
|
DBFile := MakeTempDB('syncorph');
|
|
Pool := MakePool(DBFile);
|
|
Rec := TRecorder.Create;
|
|
Sup := TPluginTaskSupplier.Create;
|
|
|
|
{ Pre-populate two plugin rows. The supplier will only re-report
|
|
one of them, so the other should be deleted. }
|
|
EnsureSchedulerSchema(Pool);
|
|
InsertTaskRow(Pool, 'kept', 'a', 60, 'plugin', True, False);
|
|
InsertTaskRow(Pool, 'removed', 'b', 60, 'plugin', True, False);
|
|
|
|
Tasks := TJSONArray.Create;
|
|
try
|
|
Obj := TJSONObject.Create;
|
|
Obj.Add('plugin_name', 'kept');
|
|
Obj.Add('task_name', 'a');
|
|
Obj.Add('description', 'still here');
|
|
Obj.Add('interval_seconds', 60);
|
|
Tasks.Add(Obj);
|
|
Sup.SetTasks(Tasks);
|
|
|
|
Sched := TCron.Create(Pool, @Rec.HandlePluginTask, @Sup.GetTasks,
|
|
@Rec.HandleLog);
|
|
try
|
|
Check('kept row survived',
|
|
Pool.QueryInt(
|
|
'SELECT COUNT(*) FROM system_scheduler ' +
|
|
'WHERE plugin_name = ''kept''') = 1);
|
|
Check('removed row was deleted',
|
|
Pool.QueryInt(
|
|
'SELECT COUNT(*) FROM system_scheduler ' +
|
|
'WHERE plugin_name = ''removed''') = 0);
|
|
finally
|
|
Sched.Free;
|
|
end;
|
|
finally
|
|
Tasks.Free;
|
|
Sup.Free;
|
|
Rec.Free;
|
|
Pool.Free;
|
|
DeleteFile(DBFile);
|
|
end;
|
|
end;
|
|
|
|
procedure TestTypedCallbacksFire;
|
|
var
|
|
DBFile: string;
|
|
Pool: TDBPool;
|
|
Sched: TCron;
|
|
Rec: TRecorder;
|
|
I: Integer;
|
|
SawStart, SawComplete: Boolean;
|
|
begin
|
|
Writeln('-- typed observers OnTaskStart + OnTaskComplete fire');
|
|
DBFile := MakeTempDB('evbus');
|
|
Pool := MakePool(DBFile);
|
|
Rec := TRecorder.Create;
|
|
try
|
|
EnsureSchedulerSchema(Pool);
|
|
InsertTaskRow(Pool, 'plug', 'event_check', 1, 'plugin', True, True);
|
|
|
|
Sched := TCron.Create(Pool, @Rec.HandlePluginTask, nil,
|
|
@Rec.HandleLog);
|
|
try
|
|
Sched.OnTaskStart := @Rec.HandleTaskStart;
|
|
Sched.OnTaskComplete := @Rec.HandleTaskComplete;
|
|
Sched.Start;
|
|
Sleep(2200);
|
|
Sched.Terminate;
|
|
finally
|
|
Sched.Free;
|
|
end;
|
|
|
|
SawStart := False;
|
|
SawComplete := False;
|
|
for I := 0 to Rec.Events.Count - 1 do
|
|
begin
|
|
if Pos('start:plug/event_check', Rec.Events[I]) = 1 then SawStart := True;
|
|
if Pos('complete:plug/event_check=ok', Rec.Events[I]) = 1 then SawComplete := True;
|
|
end;
|
|
Check('OnTaskStart fired at least once', SawStart);
|
|
Check('OnTaskComplete fired at least once', SawComplete);
|
|
finally
|
|
Rec.Free;
|
|
Pool.Free;
|
|
DeleteFile(DBFile);
|
|
end;
|
|
end;
|
|
|
|
procedure TestNextRunUTCRoundtrip;
|
|
var
|
|
DBFile: string;
|
|
Pool: TDBPool;
|
|
Sched: TCron;
|
|
Rec: TRecorder;
|
|
Tasks: TJSONArray;
|
|
Obj: TJSONObject;
|
|
NextRunStr: string;
|
|
begin
|
|
Writeln('-- NextRun is recalculated and persisted on Create');
|
|
DBFile := MakeTempDB('nextrun');
|
|
Pool := MakePool(DBFile);
|
|
Rec := TRecorder.Create;
|
|
try
|
|
EnsureSchedulerSchema(Pool);
|
|
InsertTaskRow(Pool, 'plug', 'r', 60, 'plugin', True, False);
|
|
|
|
Sched := TCron.Create(Pool, @Rec.HandlePluginTask, nil,
|
|
@Rec.HandleLog);
|
|
try
|
|
Tasks := Sched.GetTasksJSON;
|
|
try
|
|
Check('exactly one task', Tasks.Count = 1);
|
|
Obj := TJSONObject(Tasks.Items[0]);
|
|
NextRunStr := Obj.Get('next_run', '');
|
|
Check('next_run is set after Create',
|
|
NextRunStr <> '');
|
|
finally
|
|
Tasks.Free;
|
|
end;
|
|
finally
|
|
Sched.Free;
|
|
end;
|
|
finally
|
|
Rec.Free;
|
|
Pool.Free;
|
|
DeleteFile(DBFile);
|
|
end;
|
|
end;
|
|
|
|
procedure TestCronTaskExecutes;
|
|
var
|
|
DBFile: string;
|
|
Pool: TDBPool;
|
|
Sched: TCron;
|
|
Rec: TRecorder;
|
|
TaskID: Int64;
|
|
NextRunBefore, NextRunAfter: string;
|
|
begin
|
|
Writeln('-- cron task fires through ExecuteTask and NextRun moves forward');
|
|
DBFile := MakeTempDB('cron');
|
|
Pool := MakePool(DBFile);
|
|
Rec := TRecorder.Create;
|
|
try
|
|
EnsureSchedulerSchema(Pool);
|
|
Pool.ExecSQL(
|
|
'INSERT INTO system_scheduler ' +
|
|
'(task_name, plugin_name, schedule_type, cron_expr, enabled) VALUES ' +
|
|
'(''nightly'', ''demo'', ''cron'', ''* * * * *'', 1)');
|
|
TaskID := Pool.QueryInt(
|
|
'SELECT id FROM system_scheduler WHERE task_name = ''nightly''');
|
|
|
|
Sched := TCron.Create(Pool, @Rec.HandlePluginTask, nil,
|
|
@Rec.HandleLog);
|
|
try
|
|
NextRunBefore := Pool.QueryStr(
|
|
'SELECT next_run FROM system_scheduler WHERE id = ' +
|
|
IntToStr(TaskID));
|
|
Check('NextRun seeded after Create',
|
|
NextRunBefore <> '');
|
|
|
|
Sched.RunTaskNow(TaskID);
|
|
|
|
Check('cron task fired exactly once',
|
|
Rec.PluginCalls.Count = 1);
|
|
Check('scheduler_log records the cron fire',
|
|
CountSchedulerLogRows(Pool, 'demo', 'nightly') = 1);
|
|
|
|
NextRunAfter := Pool.QueryStr(
|
|
'SELECT next_run FROM system_scheduler WHERE id = ' +
|
|
IntToStr(TaskID));
|
|
{ CalcNextRunCron is deterministic-by-minute, so the post-fire
|
|
NextRun may equal the pre-fire one if both evaluations land
|
|
in the same minute. Verify the cron path ran by checking
|
|
run_count + last_run instead. }
|
|
Check('NextRun is still set after fire',
|
|
NextRunAfter <> '');
|
|
Check('run_count incremented to 1',
|
|
Pool.QueryInt(
|
|
'SELECT run_count FROM system_scheduler WHERE id = ' +
|
|
IntToStr(TaskID)) = 1);
|
|
Check('last_run populated after fire',
|
|
Pool.QueryStr(
|
|
'SELECT last_run FROM system_scheduler WHERE id = ' +
|
|
IntToStr(TaskID)) <> '');
|
|
finally
|
|
Sched.Free;
|
|
end;
|
|
finally
|
|
Rec.Free;
|
|
Pool.Free;
|
|
DeleteFile(DBFile);
|
|
end;
|
|
end;
|
|
|
|
procedure TestTwoSchedulersOnePool;
|
|
var
|
|
DBFile: string;
|
|
Pool: TDBPool;
|
|
A, B: TCron;
|
|
Rec: TRecorder;
|
|
begin
|
|
Writeln('-- two TCron instances on one pool: second DeclareTable is a no-op');
|
|
DBFile := MakeTempDB('twosched');
|
|
Pool := MakePool(DBFile);
|
|
Rec := TRecorder.Create;
|
|
try
|
|
A := TCron.Create(Pool, nil, nil, @Rec.HandleLog);
|
|
Check('first scheduler created without error',
|
|
Assigned(A));
|
|
Check('first Create declared system_scheduler',
|
|
Pool.TableExists('system_scheduler'));
|
|
|
|
try
|
|
B := TCron.Create(Pool, nil, nil, @Rec.HandleLog);
|
|
try
|
|
Check('second scheduler created without error (idempotent DeclareTable)',
|
|
Assigned(B));
|
|
Check('table still present after second Create',
|
|
Pool.TableExists('system_scheduler'));
|
|
finally
|
|
B.Free;
|
|
end;
|
|
finally
|
|
A.Free;
|
|
end;
|
|
finally
|
|
Rec.Free;
|
|
Pool.Free;
|
|
DeleteFile(DBFile);
|
|
end;
|
|
end;
|
|
|
|
begin
|
|
Total := 0; Passed := 0; Failed := 0;
|
|
Randomize;
|
|
|
|
TestInit;
|
|
TestNextRunUTCRoundtrip;
|
|
TestRunTaskNow;
|
|
TestSyncPluginTasksRegisters;
|
|
TestSyncPluginTasksRemovesOrphans;
|
|
TestIntervalFires;
|
|
TestSystemTaskFires;
|
|
TestSystemTaskFailureRecorded;
|
|
TestUpdateTaskDisables;
|
|
TestRefreshTasks;
|
|
TestTypedCallbacksFire;
|
|
TestCronTaskExecutes;
|
|
TestTwoSchedulersOnePool;
|
|
|
|
Writeln;
|
|
Writeln(Format('Total: %d Passed: %d Failed: %d',
|
|
[Total, Passed, Failed]));
|
|
if Failed > 0 then Halt(1);
|
|
end.
|