test_handle_table

The host end of the handle table, against a transport that models the shell’s.

dau_build.tests.test_handle_table_sim asserts these same four claims from inside the RTL, driving the table’s ports directly. This asserts them from the other end of the wire: the only thing the host may do is write the aperture, and a live handle has to resolve, a freed one has to be refused, a recycled slot has to refuse the previous generation, and an unarmed job has to be refused – all of it produced by the register sequence the driver actually emits, not by a host-side check standing in for the device.

Attributes

CAPACITY

ROW_BYTES

ROWS

Classes

HandleTableTransport

dau_shell_handle_table plus the shell's job close-out, in Python.

OverlappingHandleTransport

JOB_STATUS reports DONE only once the NEXT batch's H2C write has

Functions

_table(tmp_path[, transport, capacity, tier_bytes])

_run(table, handle, **kwargs)

test_a_live_handle_resolves_to_its_region(tmp_path) → None

The baseline the refusals are measured against: the allocator's grant

test_a_freed_handle_is_refused(tmp_path) → None

Freeing the slot stops it resolving even though the memory is untouched

test_a_recycled_slot_refuses_the_previous_generation(...) → None

THE CASE THE TABLE EXISTS FOR. The allocator recycles the id, so the

test_an_unarmed_job_handle_is_refused(tmp_path) → None

Generation zero is one the allocator never issues, so a host that

test_a_job_longer_than_the_grant_is_refused(tmp_path) → None

The grant bounds the read: asking past it would walk into whatever

test_an_id_past_the_table_is_refused_before_the_device_sees_it(...) → None

A slot the built table does not have is a fault on silicon (BAD_ID) and

test_installing_a_stale_handle_is_a_host_bug_not_a_device_refusal(...) → None

The host allocator's generation is the authority for what a slot is

test_freeing_a_slot_works_after_the_allocator_released_the_span(...) → None

Freeing a slot can only make the table refuse more, so it must not

test_the_install_sequence_stages_the_whole_grant_before_committing(...) → None

A commit is a pulse over whatever is staged, so every field has to be

test_a_datapath_error_is_not_reported_as_a_handle_fault(...) → None

The refusal type is only worth having if it means what it says: a code

test_repeats_amortize_one_staging_over_many_jobs(tmp_path) → None

The residency claim itself, through a handle: the rows are staged once

test_an_off_grid_length_is_refused_before_it_becomes_a_length_gate_fault(...) → None

The shell's 0xFE length gate fires before the table is asked, so an

test_an_unaligned_grant_never_becomes_a_slot(tmp_path) → None

A finer-grained allocator can hand out a base the DMA path cannot read

test_a_capacity_of_zero_is_not_a_table(tmp_path) → None

_batches(rows_per_batch[, row_bytes])

_pair(table, allocator[, grant_rows])

test_batches_ping_pong_between_two_grants_and_overlap_staging(...) → None

Each batch is staged into its grant's region through the allocator,

test_a_lazy_batch_iterator_is_consumed_one_ahead(tmp_path) → None

test_a_batch_the_shell_would_refuse_is_refused_before_it_moves(...) → None

test_one_grant_is_not_a_pair(tmp_path) → None

test_a_batch_past_its_grant_is_refused_by_the_allocator_before_the_dma(...) → None

test_a_grant_released_mid_stream_stops_the_stream(tmp_path) → None

The host frees the second grant while the first job runs. Staging

test_a_staging_failure_waits_for_the_running_job_before_it_escapes(...) → None

The failure is raised from the running job's poll, but only once

test_a_slot_freed_on_the_device_refuses_its_batch(tmp_path) → None

The device side of the same story: the slot is freed on the table but

test_a_yielding_poll_sleeps_between_status_reads_instead_of_spinning(...) → None

A spinning poll holds a core the host arm of a split is using; with

test_a_resident_scan_whose_rows_run_past_memory_is_refused(...) → None

Module Contents

test_handle_table.CAPACITY = 4
test_handle_table.ROW_BYTES = 8
test_handle_table.ROWS = 8
class test_handle_table.HandleTableTransport(capacity: int = CAPACITY, *, datapath_error: int = 0)

Bases: dau_driver.testing.FakeTransport

dau_shell_handle_table plus the shell’s job close-out, in Python.

Entries live exactly as the RTL’s do – a live bit, a base, a length, a generation – and a job start resolves JOB_HANDLE_* against them before anything else happens. A refused job sets ERROR with the fault code in LAST_ERROR and records NO run, which is how a test can tell a refusal from a job that ran and produced nothing.

capacity = 4
entries: dict[int, tuple[int, int, int]]
runs: list[tuple[int, int]] = []
datapath_error = 0
write_u32(offset: int, value: int) → None
read_u32(offset: int) → int
_staged() → tuple[int, int, int, int]
_commit(control: int) → None
_start() → None
test_handle_table._table(tmp_path, transport=None, *, capacity=CAPACITY, tier_bytes=1 << 20)
test_handle_table._run(table, handle, **kwargs)
test_handle_table.test_a_live_handle_resolves_to_its_region(tmp_path) → None

The baseline the refusals are measured against: the allocator’s grant is programmed into the slot, the job names the handle instead of an address, and the read lands on the region the allocator handed out.

test_handle_table.test_a_freed_handle_is_refused(tmp_path) → None

Freeing the slot stops it resolving even though the memory is untouched and the base the handle used to mean still reads fine.

test_handle_table.test_a_recycled_slot_refuses_the_previous_generation(tmp_path) → None

THE CASE THE TABLE EXISTS FOR. The allocator recycles the id, so the second allocation lands in the same SLOT over a different region. The first caller’s handle is byte-for-byte what it always was and its base still reads – the generation is the only thing that can refuse it. It does, and the reissued handle reaches the new tenant’s region, proving the slot was really reused underneath.

test_handle_table.test_an_unarmed_job_handle_is_refused(tmp_path) → None

Generation zero is one the allocator never issues, so a host that starts a job without arming a handle is refused rather than quietly reading slot zero – which is live here, and would otherwise answer.

test_handle_table.test_a_job_longer_than_the_grant_is_refused(tmp_path) → None

The grant bounds the read: asking past it would walk into whatever follows the region, the same wrong answer by a different route.

test_handle_table.test_an_id_past_the_table_is_refused_before_the_device_sees_it(tmp_path) → None

A slot the built table does not have is a fault on silicon (BAD_ID) and a refusal here, because capacity is a design fact the host was told, not one it discovers – and the cheapest place to learn a personality is too small is before the run.

test_handle_table.test_installing_a_stale_handle_is_a_host_bug_not_a_device_refusal(tmp_path) → None

The host allocator’s generation is the authority for what a slot is programmed from. Installing a handle it will not resolve would teach the device a grant the host does not believe in – the one direction the generation check cannot catch, since the device would then agree.

test_handle_table.test_freeing_a_slot_works_after_the_allocator_released_the_span(tmp_path) → None

Freeing a slot can only make the table refuse more, so it must not depend on the allocator still holding the handle: a caller who released host-side first would otherwise be unable to close the device-side hole at all, which is the exposure inverted.

test_handle_table.test_the_install_sequence_stages_the_whole_grant_before_committing(tmp_path) → None

A commit is a pulse over whatever is staged, so every field has to be written first. A half-staged entry commits a grant made of one allocation’s base and another’s length.

test_handle_table.test_a_datapath_error_is_not_reported_as_a_handle_fault(tmp_path) → None

The refusal type is only worth having if it means what it says: a code from a tile is a job that RAN, and calling that a stale handle would send a caller looking for an allocation bug that is not there.

test_handle_table.test_repeats_amortize_one_staging_over_many_jobs(tmp_path) → None

The residency claim itself, through a handle: the rows are staged once and the job runs against the same grant repeatedly, arming it once.

test_handle_table.test_an_off_grid_length_is_refused_before_it_becomes_a_length_gate_fault(tmp_path) → None

The shell’s 0xFE length gate fires before the table is asked, so an off-grid length would arrive as a fault that says nothing about the handle.

test_handle_table.test_an_unaligned_grant_never_becomes_a_slot(tmp_path) → None

A finer-grained allocator can hand out a base the DMA path cannot read from, and through a handle nobody ever writes that address down to notice. Refuse it where the mistake is still cheap, as the raw-address runner does.

test_handle_table.test_a_capacity_of_zero_is_not_a_table(tmp_path) → None
class test_handle_table.OverlappingHandleTransport(total_batches: int)

Bases: HandleTableTransport

JOB_STATUS reports DONE only once the NEXT batch’s H2C write has landed (the last job completes unconditionally), as the raw-address runner’s test does: a runner that staged only after the job finished would time out here, so a green run proves the two grants ping-pong.

total
finish_current = False
status_polls = 0
read_u32(offset: int) → int
test_handle_table._batches(rows_per_batch, row_bytes=ROW_BYTES)
test_handle_table._pair(table, allocator, grant_rows=64)
test_handle_table.test_batches_ping_pong_between_two_grants_and_overlap_staging(tmp_path) → None

Each batch is staged into its grant’s region through the allocator, the job names that grant (never an address), the next batch lands while the job runs, and the results come back in batch order.

test_handle_table.test_a_lazy_batch_iterator_is_consumed_one_ahead(tmp_path) → None
test_handle_table.test_a_batch_the_shell_would_refuse_is_refused_before_it_moves(tmp_path, batches, message) → None
test_handle_table.test_one_grant_is_not_a_pair(tmp_path) → None
test_handle_table.test_a_batch_past_its_grant_is_refused_by_the_allocator_before_the_dma(tmp_path) → None
test_handle_table.test_a_grant_released_mid_stream_stops_the_stream(tmp_path) → None

The host frees the second grant while the first job runs. Staging into it goes through the allocator, which no longer resolves the handle, and the failure surfaces from the running job’s wait rather than after a timeout – with nothing collected for the batches that did run.

test_handle_table.test_a_staging_failure_waits_for_the_running_job_before_it_escapes(tmp_path) → None

The failure is raised from the running job’s poll, but only once that job is DONE: the caller frees the grants on the way out, and a job still reading one would then read the next tenant’s bytes.

test_handle_table.test_a_slot_freed_on_the_device_refuses_its_batch(tmp_path) → None

The device side of the same story: the slot is freed on the table but the host still holds the handle, so the bytes land (the allocator still believes in the grant) and the JOB is refused, as HandleFaultError.

test_handle_table.test_a_yielding_poll_sleeps_between_status_reads_instead_of_spinning(tmp_path, monkeypatch) → None

A spinning poll holds a core the host arm of a split is using; with poll_yield_seconds the runner sleeps between JOB_STATUS reads. Both runners take it; the fake finishes the job after a few polls so the sleeps are counted, not just possible.

test_handle_table.test_a_resident_scan_whose_rows_run_past_memory_is_refused(tmp_path) → None