blob: 19bb539902f5d360a5f1b0a6d184cf9b455f5e83 [file] [log] [blame] [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:objective_c/objective_c.dart';
import 'package:test/test.dart';
import 'util.dart';
void main() {
group('Observer', () {
setUpAll(() {
// TODO(https://github.com/dart-lang/native/issues/1068): Remove this.
DynamicLibrary.open(testDylib);
});
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.implement(
observeValueForKeyPath_ofObject_change_context_:
(
NSString keyPath,
ObjCObjectBase object,
NSDictionary change,
Pointer<Void> context,
) {
expect(keyPath.toDartString(), 'totalUnitCount');
expect(object, observed);
expect(context.address, 0x1234);
values.add(toDartObject(change[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.implement(
observeValueForKeyPath_ofObject_change_context_:
(
NSString keyPath,
ObjCObjectBase object,
NSDictionary change,
Pointer<Void> context,
) {
values.add(toDartObject(change[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 {
final values = <dynamic>[];
NSProgress? observed;
Observer? observer;
Observation? observation;
autoReleasePool(() {
observed = NSProgress();
observer = Observer.implement(
observeValueForKeyPath_ofObject_change_context_:
(
NSString keyPath,
ObjCObjectBase object,
NSDictionary change,
Pointer<Void> context,
) {
values.add(toDartObject(change[NSKeyValueChangeNewKey]!));
// This is testing that a captured reference from the observer
// to the observed object does not cause leak.
expect(object, observed);
},
);
observation = observed!.addObserver(
observer!,
forKeyPath: 'totalUnitCount'.toNSString(),
);
});
observed!.totalUnitCount = 123;
expect(values, [123]);
final observedRaw = observed!.ref.pointer;
final observerRaw = observer!.ref.pointer;
observed = null;
observer = null;
// TODO(https://github.com/dart-lang/native/issues/2352): Reenable.
// expect(objectRetainCount(observedRaw), greaterThan(0));
// expect(objectRetainCount(observerRaw), greaterThan(0));
NSProgress.castFromPointer(observedRaw).totalUnitCount = 456;
expect(values, [123, 456]);
// Force observation to stay in scope.
expect(observation, isNotNull);
observation = null;
expect(observation, isNull);
doGC();
await Future<void>.delayed(Duration.zero);
doGC();
expect(objectRetainCount(observedRaw), 0);
expect(objectRetainCount(observerRaw), 0);
});
test('remove method drops references', () async {
NSProgress? observed;
Observer? observer;
Observation? observation;
autoReleasePool(() {
observed = NSProgress();
observer = Observer.implement(
observeValueForKeyPath_ofObject_change_context_:
(
NSString keyPath,
ObjCObjectBase object,
NSDictionary change,
Pointer<Void> context,
) {},
);
observation = observed!.addObserver(
observer!,
forKeyPath: 'totalUnitCount'.toNSString(),
);
});
final observedRaw = observed!.ref.pointer;
final observerRaw = observer!.ref.pointer;
observed = null;
observer = null;
doGC();
await Future<void>.delayed(Duration.zero);
doGC();
// Still holding a reference to observation.
// TODO(https://github.com/dart-lang/native/issues/2352): Reenable.
// expect(objectRetainCount(observedRaw), greaterThan(0));
// expect(objectRetainCount(observerRaw), greaterThan(0));
observation!.remove();
doGC();
await Future<void>.delayed(Duration.zero);
doGC();
// Still holding a reference to observation, but we've called remove.
expect(objectRetainCount(observedRaw), 0);
expect(objectRetainCount(observerRaw), 0);
// Force observation to stay in scope.
expect(observation, isNotNull);
});
});
}