blob: a83503937cd42ea7a41f52b0ce24dc6a0721bcd5 [file] [edit]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// Objective C support is only available on mac.
@TestOn('mac-os')
library;
import 'dart:ffi';
import 'package:ffi/ffi.dart';
import 'package:objective_c/objective_c.dart';
import 'package:test/test.dart';
import 'util.dart';
void main() {
group('Observer', () {
test('receive updates', () {
// Using NSProgress here because it's already part of our generated
// bindings and has a property with a getter and setter.
final observed = NSProgress();
observed.totalUnitCount = 123;
expect(observed.totalUnitCount, 123);
final values = <dynamic>[];
final observer = Observer$Builder.implement(
observeValueForKeyPath_ofObject_change_context_:
(
NSString keyPath,
ObjCObject object,
NSDictionary change,
Pointer<Void> context,
) {
expect(keyPath.toDartString(), 'totalUnitCount');
expect(object, observed);
expect(context.address, 0x1234);
values.add(
toDartObject(change.asDart()[NSKeyValueChangeNewKey]!),
);
},
);
final observation = observed.addObserver(
observer,
forKeyPath: 'totalUnitCount'.toNSString(),
context: Pointer<Void>.fromAddress(0x1234),
);
observed.totalUnitCount = 456;
expect(values, [456]);
observed.totalUnitCount = 789;
expect(values, [456, 789]);
observation.remove();
observed.totalUnitCount = 246;
expect(values, [456, 789]);
observation.remove();
observed.totalUnitCount = 999;
expect(values, [456, 789]);
});
test('cancel observation due to GC', () {
final observed = NSProgress();
final values = <dynamic>[];
final observer = Observer$Builder.implement(
observeValueForKeyPath_ofObject_change_context_:
(
NSString keyPath,
ObjCObject object,
NSDictionary change,
Pointer<Void> context,
) {
values.add(
toDartObject(change.asDart()[NSKeyValueChangeNewKey]!),
);
},
);
() {
final observation = observed.addObserver(
observer,
forKeyPath: 'totalUnitCount'.toNSString(),
);
observed.totalUnitCount = 123;
expect(values, [123]);
// Force observation to stay in scope.
expect(observation, isNotNull);
}();
doGC();
observed.totalUnitCount = 456;
expect(values, [123]);
});
test('observer and observed kept alive by observation', () async {
await using((arena) async {
final observedTracker = ReferenceTracker(arena);
final observerTracker = ReferenceTracker(arena);
final values = <dynamic>[];
NSProgress? observed;
Observer? observer;
Observation? observation;
autoReleasePool(() {
observed = NSProgress();
observer = Observer$Builder.implement(
observeValueForKeyPath_ofObject_change_context_:
(
NSString keyPath,
ObjCObject object,
NSDictionary change,
Pointer<Void> context,
) {
values.add(
toDartObject(change.asDart()[NSKeyValueChangeNewKey]!),
);
// This is testing that a captured reference from the observer
// to the observed object does not cause leak.
expect(object, observed);
},
);
observedTracker.track(observed!);
observerTracker.track(observer!);
observation = observed!.addObserver(
observer!,
forKeyPath: 'totalUnitCount'.toNSString(),
);
});
observed!.totalUnitCount = 123;
expect(values, [123]);
final observedRaw = observed!.ref.pointer;
observed = null;
observer = null;
expect(observedTracker.isAlive, true);
expect(observerTracker.isAlive, true);
NSProgress.fromPointer(observedRaw).totalUnitCount = 456;
expect(values, [123, 456]);
// Force observation to stay in scope.
expect(observation, isNotNull);
observation = null;
doGC();
await Future<void>.delayed(Duration.zero);
doGC();
expect(observedTracker.isAlive, false);
expect(observerTracker.isAlive, false);
});
});
test('remove method drops references', () async {
await using((arena) async {
final observedTracker = ReferenceTracker(arena);
final observerTracker = ReferenceTracker(arena);
NSProgress? observed;
Observer? observer;
Observation? observation;
autoReleasePool(() {
observed = NSProgress();
observer = Observer$Builder.implement(
observeValueForKeyPath_ofObject_change_context_:
(
NSString keyPath,
ObjCObject object,
NSDictionary change,
Pointer<Void> context,
) {},
);
observedTracker.track(observed!);
observerTracker.track(observer!);
observation = observed!.addObserver(
observer!,
forKeyPath: 'totalUnitCount'.toNSString(),
);
});
observed = null;
observer = null;
doGC();
await Future<void>.delayed(Duration.zero);
doGC();
// Still holding a reference to observation.
expect(observedTracker.isAlive, true);
expect(observerTracker.isAlive, true);
observation!.remove();
doGC();
await Future<void>.delayed(Duration.zero);
doGC();
// Still holding a reference to observation, but we've called remove.
expect(observedTracker.isAlive, false);
expect(observerTracker.isAlive, false);
// Force observation to stay in scope.
expect(observation, isNotNull);
});
});
});
}