Skip to content

Commit

Permalink
Merge pull request #1515 from bendk/async-refactor
Browse files Browse the repository at this point in the history
Async refactor
  • Loading branch information
bendk authored May 11, 2023
2 parents a93713a + 2e5084c commit 323a497
Show file tree
Hide file tree
Showing 69 changed files with 2,435 additions and 1,329 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ members = [
"fixtures/external-types/crate-two",
"fixtures/external-types/lib",

"fixtures/foreign-executor",
"fixtures/keywords/kotlin",
"fixtures/keywords/rust",
"fixtures/keywords/swift",
Expand Down
19 changes: 19 additions & 0 deletions fixtures/foreign-executor/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "uniffi-fixture-foreign-executor"
version = "0.23.0"
edition = "2021"
license = "MPL-2.0"
publish = false

[lib]
crate-type = ["lib", "cdylib"]
name = "uniffi_fixture_foreign_executor"

[dependencies]
uniffi = { path = "../../uniffi" }

[build-dependencies]
uniffi = { path = "../../uniffi", features = ["build"] }

[dev-dependencies]
uniffi = { path = "../../uniffi", features = ["bindgen-tests"] }
7 changes: 7 additions & 0 deletions fixtures/foreign-executor/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

fn main() {
uniffi::generate_scaffolding("./src/foreign_executor.udl").unwrap();
}
7 changes: 7 additions & 0 deletions fixtures/foreign-executor/src/foreign_executor.udl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace fixture_foreign_executor { };

interface ForeignExecutorTester {
constructor(ForeignExecutor executor);
[Name=new_from_sequence]
constructor(sequence<ForeignExecutor> executors);
};
71 changes: 71 additions & 0 deletions fixtures/foreign-executor/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Instant;
use uniffi::ForeignExecutor;

pub struct ForeignExecutorTester {
executor: ForeignExecutor,
last_result: Arc<Mutex<Option<TestResult>>>,
}

// All constructor have to be defined in UDL for now
impl ForeignExecutorTester {
fn new(executor: ForeignExecutor) -> Self {
Self {
executor,
last_result: Arc::new(Mutex::new(None)),
}
}

// Test inputting the ForeignExecutor from a Vec. This tests that they can be written to a
// `RustBuffer`
fn new_from_sequence(executors: Vec<ForeignExecutor>) -> Self {
assert_eq!(executors.len(), 1);
Self::new(executors.into_iter().next().unwrap())
}
}

#[uniffi::export]
impl ForeignExecutorTester {
/// Schedule a fire-and-forget task to run the test
fn schedule_test(&self, delay: u32) {
let last_result = self.last_result.clone();
*last_result.lock().unwrap() = None;
// Start a thread to schedule the call. This tests if the foreign bindings can handle
// schedule callbacks from a thread that they don't manage.
thread::scope(move |scope| {
scope.spawn(move || {
let start_time = Instant::now();
let initial_thread_id = thread::current().id();
// Schedule a call with the foreign executor
self.executor.schedule(delay, move || {
// Return data on when/where the call happened. We check that this matches the
// expectations in the foreign bindings tests
let call_happened_in_different_thread =
thread::current().id() != initial_thread_id;
let delay_ms = start_time.elapsed().as_millis() as u32;
*last_result.lock().unwrap() = Some(TestResult {
call_happened_in_different_thread,
delay_ms,
});
});
});
});
}

fn get_last_result(&self) -> Option<TestResult> {
self.last_result.lock().unwrap().take()
}
}

#[derive(uniffi::Record)]
pub struct TestResult {
pub call_happened_in_different_thread: bool,
pub delay_ms: u32,
}

include!(concat!(env!("OUT_DIR"), "/foreign_executor.uniffi.rs"));
47 changes: 47 additions & 0 deletions fixtures/foreign-executor/tests/bindings/test_foreign_executor.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import uniffi.fixture_foreign_executor.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking


val coroutineScope = CoroutineScope(Dispatchers.IO)
// Test scheduling calls with no delay
runBlocking {
val tester = ForeignExecutorTester(coroutineScope)
launch {
tester.scheduleTest(0U)
}
delay(100L)
val result = tester.getLastResult() ?: throw RuntimeException("ForeignExecutorTester.getLastResult() returned null")
assert(result.callHappenedInDifferentThread)
assert(result.delayMs <= 100U)
tester.close()
}

// Test scheduling calls with a delay and using the newFromSequence constructor
runBlocking {
val tester = ForeignExecutorTester.newFromSequence(listOf(coroutineScope))
launch {
tester.scheduleTest(100U)
}
delay(200L)
val result = tester.getLastResult() ?: throw RuntimeException("ForeignExecutorTester.getLastResult() returned null")
assert(result.callHappenedInDifferentThread)
assert(result.delayMs >= 100U)
assert(result.delayMs <= 200U)
tester.close()
}

// Test that we cleanup when dropping a ForeignExecutor handles
assert(FfiConverterForeignExecutor.handleCount() == 0)
val tester = ForeignExecutorTester(coroutineScope)
val tester2 = ForeignExecutorTester.newFromSequence(listOf(coroutineScope))
tester.close()
tester2.close()
assert(FfiConverterForeignExecutor.handleCount() == 0)
50 changes: 50 additions & 0 deletions fixtures/foreign-executor/tests/bindings/test_foreign_executor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import asyncio
import unittest
import weakref
from fixture_foreign_executor import ForeignExecutorTester

class TestForeignExecutor(unittest.TestCase):
def test_schedule(self):
async def run_test(constructor, delay):
if constructor == "primary":
tester = ForeignExecutorTester(asyncio.get_running_loop())
elif constructor == "new_from_sequence":
tester = ForeignExecutorTester.new_from_sequence([asyncio.get_running_loop()])
else:
raise AssertionError(f"Unknown constructor: {constructor}")
tester.schedule_test(delay)
await asyncio.sleep((delay / 1000) + 0.1)
return tester.get_last_result()

# Test no delay and lifting the foreign executor directly
result = asyncio.run(run_test("primary", 0))
self.assertTrue(result.call_happened_in_different_thread)
self.assertTrue(result.delay_ms <= 1)

# Test no delay and reading the foreign executor from a list
result = asyncio.run(run_test("new_from_sequence", 10))
self.assertTrue(result.call_happened_in_different_thread)
self.assertTrue(9 <= result.delay_ms <= 11)

def test_reference_counts(self):
# Create an event loop
loop = asyncio.new_event_loop()
loop_ref = weakref.ref(loop)
# Create ForeignExecutorTester that stores the loop
tester = ForeignExecutorTester(loop)
tester2 = ForeignExecutorTester.new_from_sequence([loop]),
# Test that testers hold a reference to the loop. After deleting the loop, the weakref should still be alive
loop.close()
del loop
self.assertNotEqual(loop_ref(), None, "ForeignExecutor didn't take a reference to the event loop")
# Deleting testers should cause the loop to be destroyed
del tester
del tester2
self.assertEqual(loop_ref(), None, "ForeignExecutor didn't release a reference to the event loop")

if __name__=='__main__':
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import Foundation
import fixture_foreign_executor

func runTest(tester: ForeignExecutorTester, delay: UInt32) async -> TestResult {
let handle = Task { () -> TestResult in
tester.scheduleTest(delay: delay)
try! await Task.sleep(nanoseconds: numericCast((delay + 10) * 1000000))
return tester.getLastResult()!
}
return await handle.value
}

Task {
// Test scheduling with no delay
let result = await runTest(
tester: ForeignExecutorTester(
executor: UniFfiForeignExecutor(priority: TaskPriority.background)
),
delay: 0
)
assert(result.callHappenedInDifferentThread)
assert(result.delayMs <= 1)

// Test scheduling with delay and an executor created from a list
let result2 = await runTest(
tester: ForeignExecutorTester.newFromSequence(
executors: [UniFfiForeignExecutor(priority: TaskPriority.background)]
),
delay: 1000
)
assert(result2.callHappenedInDifferentThread)
assert(result2.delayMs >= 90)
assert(result2.delayMs <= 110)
}



// No need to test reference counting, since `UniFfiForeignExecutor` on Swift is just a value type
5 changes: 5 additions & 0 deletions fixtures/foreign-executor/tests/test_generated_bindings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
uniffi::build_foreign_language_testcases!(
"tests/bindings/test_foreign_executor.py",
"tests/bindings/test_foreign_executor.kts",
"tests/bindings/test_foreign_executor.swift",
);
Loading

0 comments on commit 323a497

Please sign in to comment.