Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 45 additions & 10 deletions fixtures/dart_async/test/futures_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,26 @@ Future<Duration> measureTime(Future<void> Function() action) async {
return end.difference(start);
}

// The DELAY-based timing assertions below check a LOWER bound only: that an
// async operation waited at least its expected delay (proving the async
// plumbing actually suspends). They deliberately do not assert an upper bound —
// wall-clock upper bounds measure host/CI scheduling speed, not binding
// correctness, and are the source of the flaky failures tracked in #139. This
// mirrors uniffi-rs's own futures fixture (`test_futures.py`), which uses
// `assertGreater` with no upper bound. (Illustrative, not enforced: locally the
// whole suite runs in a few seconds with correct results; on a heavily loaded
// CI runner it can take vastly longer, which is what tripped the old two-sided
// per-operation bounds like `< 300ms`.)
//
// Two groups are intentionally different:
// - `concurrent_future` keeps a concurrency check, but as a *relative*
// comparison (concurrent run < sequential run) rather than a fragile
// absolute ceiling — load dilates both sides, so the inequality holds.
// - The immediate-operation checks (`always_ready`, `void`, sync methods,
// constructors) still assert an upper bound only. They have no lower bound
// to fall back on and share the same latent wall-clock fragility; tightening
// those is deliberately left for a separate change.

class ErroringAsyncParser extends AsyncParser {
@override
Future<String> asString(int delayMs, int value) async => value.toString();
Expand Down Expand Up @@ -73,7 +93,7 @@ void main() {
await sleep(ms: 200);
});

expect(time.inMilliseconds > 200 && time.inMilliseconds < 300, true);
expect(time.inMilliseconds > 200, true);
});

test('sequential_future', () async {
Expand All @@ -83,11 +103,16 @@ void main() {
expect(resultAlice, 'Hello, Alice!');
expect(resultBob, 'Hello, Bob!');
});
expect(time.inMilliseconds > 300 && time.inMilliseconds < 400, true);
expect(time.inMilliseconds > 300, true);
});

test('concurrent_future', () async {
final time = await measureTime(() async {
// Run the same two delays concurrently and sequentially, then compare.
// A relative check (concurrent < sequential) verifies the futures actually
// overlap without a fragile absolute wall-clock ceiling: CI load dilates
// both measurements, so the inequality survives while an absolute `<= 300`
// would not. (Concurrent ≈ max(100, 200) = 200ms; sequential ≈ 300ms.)
final concurrentTime = await measureTime(() async {
final results = await Future.wait([
sayAfter(ms: 100, who: 'Alice'),
sayAfter(ms: 200, who: 'Bob'),
Expand All @@ -97,15 +122,25 @@ void main() {
expect(results[1], 'Hello, Bob!');
});

expect(time.inMilliseconds >= 200 && time.inMilliseconds <= 300, true);
final sequentialTime = await measureTime(() async {
await sayAfter(ms: 100, who: 'Alice');
await sayAfter(ms: 200, who: 'Bob');
});

// Lower bound: the longer of the two delays actually elapsed.
expect(concurrentTime.inMilliseconds >= 200, true);
// Concurrency: overlapping must be faster than summing the delays. If the
// binding regressed to serializing `Future.wait`, concurrentTime would rise
// to ~sequentialTime and this would fail.
expect(concurrentTime < sequentialTime, true);
});

test('with_tokio_runtime', () async {
final time = await measureTime(() async {
final resultAlice = await sayAfterWithTokio(ms: 200, who: 'Alice');
expect(resultAlice, 'Hello, Alice (with Tokio)!');
});
expect(time.inMilliseconds > 200 && time.inMilliseconds < 300, true);
expect(time.inMilliseconds > 200, true);
});

test('fallible_function_and_method', () async {
Expand Down Expand Up @@ -155,7 +190,7 @@ void main() {
); // calls the waker a second time after 1s
await sleep(ms: 200); // wait for possible failure
});
expect(time.inMilliseconds >= 400 && time.inMilliseconds <= 600, true);
expect(time.inMilliseconds >= 400, true);
});

test('udl_async_function', () async {
Expand Down Expand Up @@ -190,7 +225,7 @@ void main() {
final result = await megaphone.sayAfter(ms: 100, who: 'Alice');
expect(result, 'HELLO, ALICE!');
});
expect(time.inMilliseconds >= 100 && time.inMilliseconds < 200, true);
expect(time.inMilliseconds >= 100, true);

// Test async silence method
final silenceTime = await measureTime(() async {
Expand Down Expand Up @@ -218,7 +253,7 @@ void main() {
final result = await megaphone.sayAfterWithTokio(ms: 100, who: 'Charlie');
expect(result, 'HELLO, CHARLIE (WITH TOKIO)!');
});
expect(time.inMilliseconds >= 100 && time.inMilliseconds < 200, true);
expect(time.inMilliseconds >= 100, true);
});

test('proc_macro_megaphone_fallible_method', () async {
Expand Down Expand Up @@ -260,7 +295,7 @@ void main() {
final result = await udlMegaphone.sayAfter(ms: 100, who: 'Dave');
expect(result, 'HELLO, DAVE (FROM UDL MEGAPHONE)!');
});
expect(time.inMilliseconds >= 100 && time.inMilliseconds < 200, true);
expect(time.inMilliseconds >= 100, true);
});

test('async_object_creation_functions', () async {
Expand Down Expand Up @@ -291,7 +326,7 @@ void main() {
);
expect(result, 'HELLO, EVE!');
});
expect(time.inMilliseconds >= 100 && time.inMilliseconds < 200, true);
expect(time.inMilliseconds >= 100, true);
});

test('fallible_struct_creation', () async {
Expand Down
2 changes: 1 addition & 1 deletion src/gen/callback_interface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ pub fn generate_callback_functions(

// Generate the function body
let callback_method_name =
&format!("{}{}", &DartCodeOracle::fn_name(callback_name), &DartCodeOracle::class_name(m.name()));
&format!("{}{}", DartCodeOracle::fn_name(callback_name), DartCodeOracle::class_name(m.name()));

if m.is_async() {
let completion_base = foreign_future_completion_name(m);
Expand Down
2 changes: 1 addition & 1 deletion src/gen/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl CodeType for EnumCodeType {
}

fn ffi_converter_name(&self) -> String {
format!("FfiConverter{}", &DartCodeOracle::class_name(&self.id))
format!("FfiConverter{}", DartCodeOracle::class_name(&self.id))
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/gen/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub trait Renderable {
};

if !type_helper.include_once_check(&ty.as_codetype().canonical_name(), ty) {
println!("{} Added", &ty.as_codetype().canonical_name());
println!("{} Added", ty.as_codetype().canonical_name());
}

type_name
Expand Down
2 changes: 1 addition & 1 deletion src/gen/stream/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub fn generate_stream(obj: &Object, _type_helper: &dyn TypeHelperRenderer) -> d
let obj_name = obj.name();
let fn_name = DartCodeOracle::fn_name(&obj_name.replace("StreamExt", ""));
let obj_var_name = &DartCodeOracle::var_name(&fn_name);
let create_obj_fn_name = format!("createStream{}", &obj_name.replace("StreamExt", ""));
let create_obj_fn_name = format!("createStream{}", obj_name.replace("StreamExt", ""));

quote! {
$fn_name() async* {
Expand Down
Loading