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.
First obtain the fixed source and recursive dependencies. Keep the source revision with your fresh execution reports.
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.