test: remove infrastructure-dependent integration suites (#1573)

This commit is contained in:
Hampus authored and GitHub committed 2026-08-12 16:56:25 +02:00
1 parent f88b0f69b2
commit 10fc79ab37
199 files changed
+688 -46963

No files matched your search

@@ -525,671 +525,3 @@ pub fn try_acquire_device() -> Option<(wgpu::Device, wgpu::Queue, wgpu::Instance
Err(_) => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use fluxer_gpu_rebuild::{GpuLossRegistry, RebuildOutcome};
struct GpuCtx {
device: wgpu::Device,
queue: wgpu::Queue,
_instance: wgpu::Instance,
}
fn gpu_ctx() -> Option<GpuCtx> {
let (device, queue, instance) = try_acquire_device()?;
Some(GpuCtx {
device,
queue,
_instance: instance,
})
}
fn make_source_texture(
device: &wgpu::Device,
queue: &wgpu::Queue,
width: u32,
height: u32,
pixels: &[u8],
) -> wgpu::Texture {
assert_eq!(
pixels.len() as u64,
u64::from(width) * u64::from(height) * 4
);
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("nv12_gpu_pack.test_source"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(width * 4),
rows_per_image: Some(height),
},
wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
);
texture
}
fn make_storage_buffer(device: &wgpu::Device, size: u64, label: &str) -> wgpu::Buffer {
device.create_buffer(&wgpu::BufferDescriptor {
label: Some(label),
size,
usage: wgpu::BufferUsages::STORAGE
| wgpu::BufferUsages::COPY_SRC
| wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
})
}
fn read_buffer(device: &wgpu::Device, queue: &wgpu::Queue, src: &wgpu::Buffer) -> Vec<u8> {
let size = src.size();
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("nv12_gpu_pack.test_staging"),
size,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("nv12_gpu_pack.test_readback"),
});
encoder.copy_buffer_to_buffer(src, 0, &staging, 0, size);
queue.submit(std::iter::once(encoder.finish()));
let slice = staging.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
let _ = device.poll(wgpu::PollType::Wait {
submission_index: None,
timeout: None,
});
let recv = rx.recv();
assert!(recv.is_ok(), "channel must deliver map result");
let map_result = recv.expect("map result channel");
assert!(map_result.is_ok(), "map_async must succeed");
let data = {
let range = slice.get_mapped_range();
range.to_vec()
};
staging.unmap();
data
}
fn pack_and_readback(
ctx: &GpuCtx,
packer: &Nv12Packer,
texture: &wgpu::Texture,
width: u32,
height: u32,
) -> (Vec<u8>, Vec<u8>) {
let y_size = Nv12Packer::y_plane_size(width, height);
let uv_size = Nv12Packer::uv_plane_size(width, height);
let y_buf = make_storage_buffer(&ctx.device, y_size, "y");
let uv_buf = make_storage_buffer(&ctx.device, uv_size, "uv");
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("nv12_gpu_pack.test_encoder"),
});
let result = packer.pack(PackJob {
device: &ctx.device,
queue: &ctx.queue,
encoder: &mut encoder,
source: &view,
y_out: &y_buf,
uv_out: &uv_buf,
dims: (width, height),
});
assert!(result.is_ok(), "pack must succeed: {result:?}");
ctx.queue.submit(std::iter::once(encoder.finish()));
let y = read_buffer(&ctx.device, &ctx.queue, &y_buf);
let uv = read_buffer(&ctx.device, &ctx.queue, &uv_buf);
(y, uv)
}
#[test]
fn solid_gray_packs_to_expected_bt709_limited() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let width: u32 = 16;
let height: u32 = 8;
let mut pixels = vec![0u8; (width * height * 4) as usize];
for chunk in pixels.chunks_exact_mut(4) {
chunk[0] = 128;
chunk[1] = 128;
chunk[2] = 128;
chunk[3] = 255;
}
let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels);
let packer = Nv12Packer::new(&ctx.device, width, height);
let (y, uv) = pack_and_readback(&ctx, &packer, &tex, width, height);
for value in &y {
assert!(
(124..=128).contains(value),
"Y value {value} out of expected range for gray 128"
);
}
for value in &uv {
assert!(
(126..=130).contains(value),
"UV value {value} out of expected range for neutral gray"
);
}
}
#[test]
fn solid_red_v_is_above_neutral() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let width: u32 = 16;
let height: u32 = 8;
let mut pixels = vec![0u8; (width * height * 4) as usize];
for chunk in pixels.chunks_exact_mut(4) {
chunk[0] = 255;
chunk[1] = 0;
chunk[2] = 0;
chunk[3] = 255;
}
let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels);
let packer = Nv12Packer::new(&ctx.device, width, height);
let (_y, uv) = pack_and_readback(&ctx, &packer, &tex, width, height);
for pair in uv.chunks_exact(2) {
let u = pair[0];
let v = pair[1];
assert!(u < 128, "expected U below neutral for pure red, got {u}");
assert!(
v > 200,
"expected V well above neutral for pure red, got {v}"
);
}
}
#[test]
fn rejects_dims_exceeding_max() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let max_w: u32 = 8;
let max_h: u32 = 8;
let packer = Nv12Packer::new(&ctx.device, max_w, max_h);
let y_size = Nv12Packer::y_plane_size(16, 16);
let uv_size = Nv12Packer::uv_plane_size(16, 16);
let y_buf = make_storage_buffer(&ctx.device, y_size, "y");
let uv_buf = make_storage_buffer(&ctx.device, uv_size, "uv");
let dummy_pixels = vec![0u8; (max_w * max_h * 4) as usize];
let tex = make_source_texture(&ctx.device, &ctx.queue, max_w, max_h, &dummy_pixels);
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
let result = packer.pack(PackJob {
device: &ctx.device,
queue: &ctx.queue,
encoder: &mut encoder,
source: &view,
y_out: &y_buf,
uv_out: &uv_buf,
dims: (16, 16),
});
assert!(matches!(
result,
Err(Nv12PackError::DimensionsExceedMax { .. })
));
}
#[test]
fn rejects_odd_dims() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let packer = Nv12Packer::new(&ctx.device, 32, 32);
let y_buf = make_storage_buffer(&ctx.device, 64, "y");
let uv_buf = make_storage_buffer(&ctx.device, 64, "uv");
let dummy_pixels = vec![0u8; (8 * 8 * 4) as usize];
let tex = make_source_texture(&ctx.device, &ctx.queue, 8, 8, &dummy_pixels);
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
let result = packer.pack(PackJob {
device: &ctx.device,
queue: &ctx.queue,
encoder: &mut encoder,
source: &view,
y_out: &y_buf,
uv_out: &uv_buf,
dims: (7, 8),
});
assert!(matches!(
result,
Err(Nv12PackError::DimensionsNotEven { .. })
));
}
#[test]
fn determinism_two_runs_same_input_same_output() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let width: u32 = 32;
let height: u32 = 16;
let mut pixels = vec![0u8; (width * height * 4) as usize];
for (i, chunk) in pixels.chunks_exact_mut(4).enumerate() {
let v = (i % 251) as u8;
chunk[0] = v;
chunk[1] = v.wrapping_mul(2);
chunk[2] = v.wrapping_add(31);
chunk[3] = 255;
}
let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels);
let packer = Nv12Packer::new(&ctx.device, width, height);
let (y1, uv1) = pack_and_readback(&ctx, &packer, &tex, width, height);
let (y2, uv2) = pack_and_readback(&ctx, &packer, &tex, width, height);
assert_eq!(y1, y2, "Y plane must be deterministic");
assert_eq!(uv1, uv2, "UV plane must be deterministic");
}
#[test]
fn buffer_too_small_is_rejected() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let packer = Nv12Packer::new(&ctx.device, 64, 64);
let y_buf = make_storage_buffer(&ctx.device, 16, "y");
let uv_buf = make_storage_buffer(&ctx.device, 16, "uv");
let pixels = vec![0u8; (16 * 16 * 4) as usize];
let tex = make_source_texture(&ctx.device, &ctx.queue, 16, 16, &pixels);
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
let result = packer.pack(PackJob {
device: &ctx.device,
queue: &ctx.queue,
encoder: &mut encoder,
source: &view,
y_out: &y_buf,
uv_out: &uv_buf,
dims: (16, 16),
});
assert!(matches!(result, Err(Nv12PackError::YBufferTooSmall { .. })));
}
#[test]
fn packer_is_ready_only_when_built() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let mut packer = Nv12Packer::new(&ctx.device, 32, 32);
assert!(
packer.is_ready(),
"freshly constructed packer must be ready"
);
packer.release();
assert!(!packer.is_ready(), "released packer must not be ready");
let rebuilt = packer.rebuild(&ctx.device, &ctx.queue);
assert!(rebuilt.is_ok(), "rebuild on a fresh device must succeed");
assert!(packer.is_ready(), "rebuilt packer must be ready");
}
#[test]
fn pack_between_release_and_rebuild_returns_not_ready() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let mut packer = Nv12Packer::new(&ctx.device, 16, 16);
packer.release();
let y_buf = make_storage_buffer(&ctx.device, 256, "y");
let uv_buf = make_storage_buffer(&ctx.device, 256, "uv");
let dummy_pixels = vec![0u8; (16 * 16 * 4) as usize];
let tex = make_source_texture(&ctx.device, &ctx.queue, 16, 16, &dummy_pixels);
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut encoder = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
let result = packer.pack(PackJob {
device: &ctx.device,
queue: &ctx.queue,
encoder: &mut encoder,
source: &view,
y_out: &y_buf,
uv_out: &uv_buf,
dims: (16, 16),
});
assert!(matches!(result, Err(Nv12PackError::NotReady)));
}
#[test]
fn registry_release_then_rebuild_invokes_callback_in_order() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let registry = GpuLossRegistry::new();
let packer = Box::new(Nv12Packer::new(&ctx.device, 16, 16));
let _guard = registry.register(packer);
let report = registry.handle_device_lost(&ctx.device, &ctx.queue);
assert_eq!(report.released_count, 1);
assert_eq!(report.rebuilt_count, 1);
assert_eq!(report.failed_count, 0);
assert!(report.is_total_success());
}
#[test]
fn after_loss_handling_packer_can_pack_again() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let width: u32 = 16;
let height: u32 = 8;
let pixels = vec![128u8; (width * height * 4) as usize];
let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels);
let mut packer = Nv12Packer::new(&ctx.device, width, height);
packer.release();
packer
.rebuild(&ctx.device, &ctx.queue)
.expect("rebuild must succeed after release");
let (y, _uv) = pack_and_readback(&ctx, &packer, &tex, width, height);
assert!(!y.is_empty(), "Y plane must be non-empty after rebuild");
assert!(
(124..=128).contains(&y[0]),
"Y value out of range for gray 128 after rebuild: {}",
y[0]
);
}
#[test]
fn determinism_across_rebuild_round_trip() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let width: u32 = 32;
let height: u32 = 16;
let mut pixels = vec![0u8; (width * height * 4) as usize];
for (i, chunk) in pixels.chunks_exact_mut(4).enumerate() {
let v = (i % 251) as u8;
chunk[0] = v;
chunk[1] = v.wrapping_mul(2);
chunk[2] = v.wrapping_add(31);
chunk[3] = 255;
}
let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels);
let mut packer = Nv12Packer::new(&ctx.device, width, height);
let (y1, uv1) = pack_and_readback(&ctx, &packer, &tex, width, height);
packer.release();
packer
.rebuild(&ctx.device, &ctx.queue)
.expect("rebuild must succeed");
let (y2, uv2) = pack_and_readback(&ctx, &packer, &tex, width, height);
assert_eq!(y1, y2, "Y plane must match across rebuild round-trip");
assert_eq!(uv1, uv2, "UV plane must match across rebuild round-trip");
}
#[test]
fn double_rebuild_without_release_is_owner_invariant_error() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let mut packer = Nv12Packer::new(&ctx.device, 16, 16);
let outcome = packer.rebuild(&ctx.device, &ctx.queue);
assert!(matches!(
outcome,
Err(GpuRebuildError::OwnerInvariantBroken { .. })
));
}
struct FailingRebuildOwner {
ready: bool,
}
impl FailingRebuildOwner {
fn new() -> Self {
Self { ready: true }
}
}
impl GpuLossCallback for FailingRebuildOwner {
fn release(&mut self) {
let was_ready = self.ready;
assert!(
was_ready,
"FailingRebuildOwner release must observe ready state",
);
self.ready = false;
assert!(!self.ready, "FailingRebuildOwner postcondition");
}
fn rebuild(
&mut self,
_device: &wgpu::Device,
_queue: &wgpu::Queue,
) -> Result<(), GpuRebuildError> {
assert!(
!self.ready,
"rebuild must run after release in FailingRebuildOwner"
);
Err(GpuRebuildError::DeviceRejected {
reason: "synthetic second-device failure",
})
}
fn is_ready(&self) -> bool {
self.ready
}
fn debug_label(&self) -> &'static str {
"test.failing_rebuild_owner"
}
}
#[test]
fn failed_rebuild_surfaces_in_report_and_owner_reports_not_ready() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let registry = GpuLossRegistry::new();
let _guard = registry.register(Box::new(FailingRebuildOwner::new()));
let report = registry.handle_device_lost(&ctx.device, &ctx.queue);
assert_eq!(report.released_count, 1);
assert_eq!(report.failed_count, 1);
assert_eq!(report.rebuilt_count, 0);
assert!(!report.is_total_success());
let failed = report
.outcomes
.iter()
.filter(|o| matches!(o, RebuildOutcome::Failed { .. }))
.count();
assert_eq!(failed, 1);
}
#[test]
fn multiple_packers_release_lifo_rebuild_fifo() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let registry = GpuLossRegistry::new();
let mut guards = Vec::new();
for _ in 0..3u32 {
let packer = Box::new(Nv12Packer::new(&ctx.device, 16, 16));
guards.push(registry.register(packer));
}
assert_eq!(registry.len(), 3);
let report = registry.handle_device_lost(&ctx.device, &ctx.queue);
assert_eq!(report.released_count, 3);
assert_eq!(report.rebuilt_count, 3);
assert_eq!(report.failed_count, 0);
for outcome in &report.outcomes {
assert!(matches!(outcome, RebuildOutcome::Rebuilt { .. }));
}
drop(guards);
}
fn pack_one_pass(
ctx: &GpuCtx,
packer: &Nv12Packer,
view: &wgpu::TextureView,
y_buf: &wgpu::Buffer,
uv_buf: &wgpu::Buffer,
width: u32,
height: u32,
) {
let mut encoder = ctx
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("nv12_gpu_pack.cache_test_encoder"),
});
let result = packer.pack(PackJob {
device: &ctx.device,
queue: &ctx.queue,
encoder: &mut encoder,
source: view,
y_out: y_buf,
uv_out: uv_buf,
dims: (width, height),
});
assert!(result.is_ok(), "pack must succeed: {result:?}");
ctx.queue.submit(std::iter::once(encoder.finish()));
}
#[test]
fn cache_hit_when_pack_called_twice_with_same_inputs() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let width: u32 = 32;
let height: u32 = 16;
let pixels = vec![64u8; (width * height * 4) as usize];
let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels);
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let packer = Nv12Packer::new(&ctx.device, width, height);
let y_buf = make_storage_buffer(&ctx.device, Nv12Packer::y_plane_size(width, height), "y");
let uv_buf =
make_storage_buffer(&ctx.device, Nv12Packer::uv_plane_size(width, height), "uv");
assert_eq!(packer.cached_bind_group_count(), 0);
pack_one_pass(&ctx, &packer, &view, &y_buf, &uv_buf, width, height);
assert_eq!(packer.cached_bind_group_count(), 1);
pack_one_pass(&ctx, &packer, &view, &y_buf, &uv_buf, width, height);
assert_eq!(packer.cached_bind_group_count(), 1);
}
#[test]
fn cache_miss_when_buffers_change() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let width: u32 = 32;
let height: u32 = 16;
let pixels = vec![64u8; (width * height * 4) as usize];
let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels);
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let packer = Nv12Packer::new(&ctx.device, width, height);
let y_buf_a =
make_storage_buffer(&ctx.device, Nv12Packer::y_plane_size(width, height), "y_a");
let uv_buf_a = make_storage_buffer(
&ctx.device,
Nv12Packer::uv_plane_size(width, height),
"uv_a",
);
pack_one_pass(&ctx, &packer, &view, &y_buf_a, &uv_buf_a, width, height);
assert_eq!(packer.cached_bind_group_count(), 1);
let y_buf_b =
make_storage_buffer(&ctx.device, Nv12Packer::y_plane_size(width, height), "y_b");
let uv_buf_b = make_storage_buffer(
&ctx.device,
Nv12Packer::uv_plane_size(width, height),
"uv_b",
);
pack_one_pass(&ctx, &packer, &view, &y_buf_b, &uv_buf_b, width, height);
assert_eq!(packer.cached_bind_group_count(), 1);
pack_one_pass(&ctx, &packer, &view, &y_buf_a, &uv_buf_a, width, height);
assert_eq!(packer.cached_bind_group_count(), 1);
}
#[test]
fn cache_invalidated_on_release() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let width: u32 = 32;
let height: u32 = 16;
let pixels = vec![64u8; (width * height * 4) as usize];
let tex = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels);
let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
let mut packer = Nv12Packer::new(&ctx.device, width, height);
let y_buf = make_storage_buffer(&ctx.device, Nv12Packer::y_plane_size(width, height), "y");
let uv_buf =
make_storage_buffer(&ctx.device, Nv12Packer::uv_plane_size(width, height), "uv");
pack_one_pass(&ctx, &packer, &view, &y_buf, &uv_buf, width, height);
assert_eq!(packer.cached_bind_group_count(), 1);
packer.release();
assert_eq!(packer.cached_bind_group_count(), 0);
packer
.rebuild(&ctx.device, &ctx.queue)
.expect("rebuild must succeed");
assert_eq!(packer.cached_bind_group_count(), 0);
pack_one_pass(&ctx, &packer, &view, &y_buf, &uv_buf, width, height);
assert_eq!(packer.cached_bind_group_count(), 1);
}
#[test]
fn cache_replaces_when_source_view_changes() {
let Some(ctx) = gpu_ctx() else {
eprintln!("nv12-gpu-pack: no wgpu adapter, skipping");
return;
};
let width: u32 = 32;
let height: u32 = 16;
let pixels = vec![64u8; (width * height * 4) as usize];
let tex_a = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels);
let tex_b = make_source_texture(&ctx.device, &ctx.queue, width, height, &pixels);
let view_a = tex_a.create_view(&wgpu::TextureViewDescriptor::default());
let view_b = tex_b.create_view(&wgpu::TextureViewDescriptor::default());
let packer = Nv12Packer::new(&ctx.device, width, height);
let y_buf = make_storage_buffer(&ctx.device, Nv12Packer::y_plane_size(width, height), "y");
let uv_buf =
make_storage_buffer(&ctx.device, Nv12Packer::uv_plane_size(width, height), "uv");
pack_one_pass(&ctx, &packer, &view_a, &y_buf, &uv_buf, width, height);
assert_eq!(packer.cached_bind_group_count(), 1);
pack_one_pass(&ctx, &packer, &view_b, &y_buf, &uv_buf, width, height);
assert_eq!(packer.cached_bind_group_count(), 1);
}
}