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¶
Classes¶
|
|
JOB_STATUS reports DONE only once the NEXT batch's H2C write has |
Functions¶
|
|
|
|
|
The baseline the refusals are measured against: the allocator's grant |
|
Freeing the slot stops it resolving even though the memory is untouched |
|
THE CASE THE TABLE EXISTS FOR. The allocator recycles the id, so the |
|
Generation zero is one the allocator never issues, so a host that |
|
The grant bounds the read: asking past it would walk into whatever |
|
A slot the built table does not have is a fault on silicon (BAD_ID) and |
|
The host allocator's generation is the authority for what a slot is |
|
Freeing a slot can only make the table refuse more, so it must not |
|
A commit is a pulse over whatever is staged, so every field has to be |
|
The refusal type is only worth having if it means what it says: a code |
|
The residency claim itself, through a handle: the rows are staged once |
|
The shell's 0xFE length gate fires before the table is asked, so an |
|
A finer-grained allocator can hand out a base the DMA path cannot read |
|
|
|
|
|
|
|
Each batch is staged into its grant's region through the allocator, |
|
|
|
|
|
|
|
|
|
The host frees the second grant while the first job runs. Staging |
|
The failure is raised from the running job's poll, but only once |
|
The device side of the same story: the slot is freed on the table but |
|
A spinning poll holds a core the host arm of a split is using; with |
|
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.FakeTransportdau_shell_handle_tableplus 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:
HandleTableTransportJOB_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_secondsthe 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¶