Перейти к содержимому

Наблюдение и тестирование

Исход описывает результат одного Job. JobObserver получает ещё и события жизненного цикла, сообщения и ошибки работы вне тела:

final class Log extends JobObserver {
@override
void onFinish(Job<Object?> job) => print('$job: ${job.outcome}');
@override
void onLog(Job<Object?> job, Object? message) {
final data = message is Object? Function() ? message() : message;
print('$job: $data');
}
}
final job = Job<int>(
key: 'load',
observer: Log(),
(ctx) => ctx.wait(load),
); // Job(load): Done(3)

У него четыре метода: onStart, onFinish, onError и onLog. Первые три Job вызывает автоматически, а сообщения для onLog тело отправляет через ctx.log(message).

Все четыре по умолчанию пустые, поэтому можно переопределить только нужные. Если класс уже наследует другой класс, можно использовать implements JobObserver. Наблюдатель передаётся при создании задачи; дочерние задачи наследуют его, если у них нет своего. Если метод бросит исключение, оно попадёт в текущую зону, не меняя поведения задачи.

Строковое представление задачи имеет вид Job($key). Используйте key для обозначения задачи в журнале или в правилах планирования библиотеки, например в политиках очереди solo. describe добавляет подробности к описанию в журнале.

Сообщения, переданные в ctx.log, остаются объектами, пока слушатель не отформатирует их. Без наблюдателя ctx.log ничего не делает, но Dart всё равно вычисляет его аргумент. Например, ctx.log('migration failed: $error') создаёт строку и без наблюдателя. Передавайте объект напрямую, чтобы форматированием занимался слушатель, или передайте колбэк, чтобы отложить само построение сообщения:

ctx.log(() => 'migration failed: $error');

Наблюдатель выше вызывает колбэк и форматирует полученные данные. Без наблюдателя колбэк не вызывается и интерполяция не выполняется. Это соглашение конкретного наблюдателя: сам ctx.log передаёт колбэк без изменений, как любой другой объект.

Ошибка тела передаётся наблюдателю. Если задача завершается этой ошибкой, она сохраняется в Failed. Если этот исход остаётся ненаблюдаемым, ошибка также передаётся в зону создания задачи.

Ошибки вне тела не могут стать его исходом. К ним относятся поздние ошибки действия, ожидание которого прервал wait, ошибки уборки, колбэков отмены (ctx.onCancel или job.whenCancelled), работы ctx.unattended и построения описания отмены дочерней задачи. Они передаются наблюдателю, а без него сразу в зону создания задачи. Наблюдатель сам решает, как их обработать. Cancelled, попавший в этот механизм, передаётся только наблюдателю и никогда не уходит в зону как необработанная ошибка.

Чтобы проверить запуск, отмену и уборку, нужно управлять микротасками и таймерами. package:fake_async позволяет делать это без ожидания в реальном времени. Этот пакет использует его в собственных тестах; здесь проверяется, что отмена во время открытия базы всё равно приводит к её закрытию, когда открытие завершится:

test('a cancelled open still closes what it opened', () {
fakeAsync((async) {
var closed = 0;
final job = Job<Database>(
(ctx) => ctx.join(
Database.open,
discard: (db) {
closed++;
return db.close();
},
),
);
async.elapse(const Duration(milliseconds: 10));
job.cancel().ignore(); // внутри `fakeAsync` никто ничего не ждёт
async.flushTimers();
expect(job.outcome, isA<Cancelled>());
expect(closed, 1); // ради чего тест и назван
});
});

Для запуска задачи достаточно flushMicrotasks(). Операции, запланированные через Future(...) или Future.delayed(...), используют таймеры: для них нужны flushTimers() или elapse(...).