Observe rejection, borrow, cancellation and drain¶
Run this host tutorial to observe the real public SPI API against a Native provider. It demonstrates software ownership and failure paths, not physical DMA timing. The complete executable fixture is the maintained GoogleTest target; its model controls stand in for external IRQ and wire progress only. GoogleTest/GoogleMock and C++17 are host-only; production firmware remains C11.
Build and run the complete fixtures¶
From the repository root, with host tools and pinned dependencies available:
python tools/dev/dev.py configure --preset native-debug
python tools/dev/dev.py build --preset native-debug --parallel 4 --target nexus_native_async_test nexus_native_factory_test nexus_uart_owner_mock_test
Run each binary without a filter and retain fresh XML in a new directory:
mkdir -p /tmp/nexus-io-tutorial
build/native-debug/bin/nexus_native_async_test --gtest_output=xml:/tmp/nexus-io-tutorial/async.xml
build/native-debug/bin/nexus_native_factory_test --gtest_output=xml:/tmp/nexus-io-tutorial/factory.xml
build/native-debug/bin/nexus_uart_owner_mock_test --gtest_output=xml:/tmp/nexus-io-tutorial/owner.xml
All three commands must exit zero and report passing, nonzero test counts. These complete target executions are a tutorial scope, not the all-target commit TDD qualification. Remove the tutorial directory only after retaining needed reports.
Follow the normal and rejected admission path¶
TEST_F(NativeAsync, SPIBorrowWaitsForWireCompletionAndOtherEndpointRejects) {
nx_native_spi_state_t controller = {};
nx_native_spi_endpoint_state_t first = {};
nx_native_spi_endpoint_state_t second = {};
const nx_spi_port_t bus = {&nx_native_spi_ops, &controller};
const nx_spi_endpoint_t first_api = {&nx_native_spi_endpoint_ops, &first};
const nx_spi_endpoint_t second_api = {&nx_native_spi_endpoint_ops, &second};
ASSERT_EQ(nx_native_spi_port_configure_instance(&controller), NX_SUCCESS);
ASSERT_EQ(nx_native_spi_configure_instance(&first, &controller, nullptr, 0),
NX_SUCCESS);
ASSERT_EQ(
nx_native_spi_configure_instance(&second, &controller, nullptr, 0),
NX_SUCCESS);
nx_native_spi_model_async_configure(&bus, false);
WakeFacts facts;
const nx_irq_wake_t wake = {&facts, record_wake, false};
ASSERT_EQ(nx_spi_port_attach_wake(&bus, &wake, 5), NX_SUCCESS);
uint8_t tx[] = {17, 29};
uint8_t rx[2] = {};
nx_spi_request_t request = {};
nx_spi_request_t rejected = {};
nx_request_initialize(&request.base);
nx_request_initialize(&rejected.base);
ASSERT_EQ(nx_spi_request_prepare(&request, tx, rx, 2, 100), NX_SUCCESS);
ASSERT_EQ(nx_spi_request_prepare(&rejected, tx, nullptr, 2, 100),
NX_SUCCESS);
ASSERT_EQ(nx_spi_endpoint_submit(&first_api, &request), NX_SUCCESS);
EXPECT_TRUE(nx_native_spi_model_cs_active(&first_api));
EXPECT_EQ(facts.calls, 0u);
EXPECT_EQ(nx_spi_endpoint_submit(&second_api, &rejected), NX_ERROR_BUSY);
EXPECT_EQ(nx_request_state(&rejected.base), NX_REQUEST_READY);
nx_native_spi_model_irq_step(&bus);
nx_native_spi_model_irq_step(&bus);
nx_spi_port_service(&bus);
EXPECT_EQ(nx_request_state(&request.base), NX_REQUEST_ACTIVE);
EXPECT_EQ(facts.calls, 0u);
EXPECT_TRUE(nx_native_spi_model_cs_active(&first_api));
nx_native_spi_model_irq_step(&bus);
nx_spi_port_service(&bus);
EXPECT_EQ(nx_request_state(&request.base), NX_REQUEST_SETTLED);
EXPECT_FALSE(nx_native_spi_model_cs_active(&first_api));
EXPECT_EQ(facts.calls, 1u);
EXPECT_FALSE(facts.masked);
EXPECT_EQ(rx[0], 17);
EXPECT_EQ(rx[1], 29);
tx[0] = 99;
nx_native_spi_model_irq_step(&bus);
EXPECT_EQ(rx[0], 17);
EXPECT_EQ(nx_spi_port_stop(&bus), NX_SUCCESS);
nx_native_spi_model_irq_step(&bus);
EXPECT_EQ(facts.calls, 1u);
}
The first endpoint accepts its request. The caller keeps the request and buffers
alive. The second endpoint returns NX_ERROR_BUSY while its request stays READY;
that rejection retains no borrow. Stepping payload transfer does not publish
SETTLED before the simulated wire-completion step. Service and IRQ progress remain
explicit; a waiting client cannot assume a hidden worker.
Follow cancellation and failed drain¶
TEST_F(NativeAsync, SPIQuarantineRetainsBorrowAndStopUntilDrain) {
nx_native_spi_state_t controller = {};
nx_native_spi_endpoint_state_t endpoint = {};
const nx_spi_port_t bus = {&nx_native_spi_ops, &controller};
const nx_spi_endpoint_t endpoint_api = {&nx_native_spi_endpoint_ops,
&endpoint};
ASSERT_EQ(nx_native_spi_port_configure_instance(&controller), NX_SUCCESS);
ASSERT_EQ(
nx_native_spi_configure_instance(&endpoint, &controller, nullptr, 0),
NX_SUCCESS);
nx_native_spi_model_async_configure(&bus, false);
nx_native_spi_model_drain_hold(&bus, true);
uint8_t tx[] = {1, 2};
nx_spi_request_t request = {};
nx_request_initialize(&request.base);
ASSERT_EQ(nx_spi_request_prepare(&request, tx, nullptr, 2, 100),
NX_SUCCESS);
ASSERT_EQ(nx_spi_endpoint_submit(&endpoint_api, &request), NX_SUCCESS);
nx_native_spi_model_irq_step(&bus);
EXPECT_EQ(nx_spi_port_cancel(&bus, &request), NX_SUCCESS);
nx_spi_port_service(&bus);
EXPECT_EQ(nx_request_state(&request.base), NX_REQUEST_QUARANTINED);
EXPECT_EQ(nx_spi_port_stop(&bus), NX_ERROR_BUSY);
EXPECT_EQ(nx_spi_request_prepare(&request, tx, nullptr, 2, 100),
NX_ERROR_STATE);
nx_native_spi_model_drain_hold(&bus, false);
nx_spi_port_service(&bus);
EXPECT_EQ(nx_request_state(&request.base), NX_REQUEST_SETTLED);
nx_result_t result = NX_SUCCESS;
size_t count = 0;
ASSERT_EQ(nx_request_result(&request.base, &result, &count), NX_SUCCESS);
EXPECT_EQ(result, NX_ERROR_CANCELLED);
EXPECT_EQ(count, 1u);
EXPECT_EQ(
nx_spi_endpoint_transfer(&endpoint_api, tx, nullptr, 1, 100, &count),
NX_ERROR_STATE);
EXPECT_EQ(nx_spi_port_stop(&bus), NX_SUCCESS);
}
Cancel requests termination. With drain deliberately held, the request becomes QUARANTINED, stop returns BUSY and prepare rejects reuse. The model then supplies actual drain progress; service publishes SETTLED and result observation exposes the cancelled result and transferred prefix. Removing the model hold is a test control, not a generic production recovery API.
The generated factory fixture also tests platform stop with a quarantined UART borrow. The UART owner fixture checks rejected submissions and retained payload against GoogleMock dependency ports. Read ownership before copying request storage into an application.
FIG-06 expresses current Core helper transitions with provider proof obligations. The I/O lifecycle matrix limits what these models establish. IRQ hints, stable slot epochs, observer detach and join require the OS shutdown guide.