160 lines
5.7 KiB
C++
160 lines
5.7 KiB
C++
#include "data/store/ChunkedVolumeStore.hpp"
|
||
#include "core/algo/GprVolumeBuilder.hpp"
|
||
#include <gtest/gtest.h>
|
||
#include <filesystem>
|
||
#include <cstdint>
|
||
#include <vector>
|
||
using namespace geopro::data;
|
||
|
||
namespace {
|
||
// 非常量体:体素随 (i,j,k) 变化,确保逐块对拍真正区分块内容(非全相同)。
|
||
geopro::core::BuiltI16 makeBuilt(int nx, int ny, int nz) {
|
||
geopro::core::BuiltI16 b;
|
||
b.vol = geopro::core::ScalarVolumeI16(nx, ny, nz);
|
||
for (int k = 0; k < nz; ++k)
|
||
for (int j = 0; j < ny; ++j)
|
||
for (int i = 0; i < nx; ++i)
|
||
b.vol.at(i, j, k) =
|
||
static_cast<std::int16_t>((i * 131 + j * 17 + k * 7) % 251);
|
||
b.quant = {0.5, -3.0};
|
||
b.origin = {{10.0, 20.0, 30.0}};
|
||
b.spacing = {{2.0, 3.0, 4.0}};
|
||
b.vminPhys = -3.0;
|
||
b.vmaxPhys = 122.0;
|
||
return b;
|
||
}
|
||
|
||
int ceilDiv(int n, int brick) { return (n + brick - 1) / brick; }
|
||
int extent(int n, int b, int brick) {
|
||
const int got = n - b * brick;
|
||
return got < brick ? got : brick;
|
||
}
|
||
|
||
// 从整卷切出 (bx,by,bz) 块体素(块内 i 最快、k 最慢,与 write 同布局)。
|
||
std::vector<std::int16_t> sliceBrickFrom(const geopro::core::BuiltI16& b, int bx,
|
||
int by, int bz, int brick) {
|
||
const auto& vol = b.vol;
|
||
const int bw = extent(vol.nx(), bx, brick);
|
||
const int bh = extent(vol.ny(), by, brick);
|
||
const int bd = extent(vol.nz(), bz, brick);
|
||
std::vector<std::int16_t> out(static_cast<std::size_t>(bw) * bh * bd);
|
||
const int i0 = bx * brick, j0 = by * brick, k0 = bz * brick;
|
||
std::size_t w = 0;
|
||
for (int kk = 0; kk < bd; ++kk)
|
||
for (int jj = 0; jj < bh; ++jj)
|
||
for (int ii = 0; ii < bw; ++ii)
|
||
out[w++] = vol.at(i0 + ii, j0 + jj, k0 + kk);
|
||
return out;
|
||
}
|
||
|
||
std::string tmp(const char* name) {
|
||
return (std::filesystem::temp_directory_path() / name).string();
|
||
}
|
||
} // namespace
|
||
|
||
// 核心验收:流式逐块写 vs 非流式整卷 write,逐块 readBrick + meta 完全一致。
|
||
TEST(StreamingVolumeWriter, MatchesNonStreamingWrite) {
|
||
auto b = makeBuilt(100, 40, 30); // 100/40/30 均非整除 64 → 含边缘块
|
||
const int brick = 64;
|
||
auto dirA = tmp("swA"), dirB = tmp("swB");
|
||
std::filesystem::remove_all(dirA);
|
||
std::filesystem::remove_all(dirB);
|
||
|
||
ChunkedVolumeStore::write(dirA, b, brick); // 金标准
|
||
StoreMeta m = ChunkedVolumeStore::readMeta(dirA); // 复用同一 meta 元信息
|
||
|
||
const int bX = ceilDiv(m.nx, brick);
|
||
const int bY = ceilDiv(m.ny, brick);
|
||
const int bZ = ceilDiv(m.nz, brick);
|
||
|
||
{
|
||
StreamingVolumeWriter w(dirB, m);
|
||
// 故意打乱写入顺序(与 write 的固定遍历顺序不同),验证顺序无关。
|
||
for (int bx = 0; bx < bX; ++bx)
|
||
for (int by = 0; by < bY; ++by)
|
||
for (int bz = 0; bz < bZ; ++bz)
|
||
w.writeBrick(bx, by, bz, sliceBrickFrom(b, bx, by, bz, brick));
|
||
w.finalize();
|
||
}
|
||
|
||
ChunkedVolumeStore A(dirA), B(dirB);
|
||
for (int bz = 0; bz < bZ; ++bz)
|
||
for (int by = 0; by < bY; ++by)
|
||
for (int bx = 0; bx < bX; ++bx)
|
||
EXPECT_EQ(A.readBrick(bx, by, bz), B.readBrick(bx, by, bz))
|
||
<< "brick mismatch at " << bx << "," << by << "," << bz;
|
||
|
||
EXPECT_EQ(B.meta().nx, A.meta().nx);
|
||
EXPECT_EQ(B.meta().ny, A.meta().ny);
|
||
EXPECT_EQ(B.meta().nz, A.meta().nz);
|
||
EXPECT_EQ(B.meta().brick, A.meta().brick);
|
||
EXPECT_EQ(B.meta().origin, A.meta().origin);
|
||
EXPECT_EQ(B.meta().spacing, A.meta().spacing);
|
||
EXPECT_EQ(B.meta().quant.scale, A.meta().quant.scale);
|
||
EXPECT_EQ(B.meta().quant.offset, A.meta().quant.offset);
|
||
EXPECT_EQ(B.meta().vminPhys, A.meta().vminPhys);
|
||
EXPECT_EQ(B.meta().vmaxPhys, A.meta().vmaxPhys);
|
||
|
||
std::filesystem::remove_all(dirA);
|
||
std::filesystem::remove_all(dirB);
|
||
}
|
||
|
||
// 同一块重复写 → 抛异常(约定:每块只写一次)。
|
||
TEST(StreamingVolumeWriter, DuplicateBrickThrows) {
|
||
auto b = makeBuilt(70, 30, 20);
|
||
auto dirA = tmp("swDup");
|
||
std::filesystem::remove_all(dirA);
|
||
ChunkedVolumeStore::write(dirA, b, 64);
|
||
StoreMeta m = ChunkedVolumeStore::readMeta(dirA);
|
||
|
||
auto dirB = tmp("swDupB");
|
||
std::filesystem::remove_all(dirB);
|
||
{
|
||
// 持久 data.bin 句柄随 writer 生命周期持有,需先析构 writer 再删目录(Windows 文件锁)。
|
||
StreamingVolumeWriter w(dirB, m);
|
||
w.writeBrick(0, 0, 0, sliceBrickFrom(b, 0, 0, 0, 64));
|
||
EXPECT_THROW(w.writeBrick(0, 0, 0, sliceBrickFrom(b, 0, 0, 0, 64)),
|
||
std::runtime_error);
|
||
}
|
||
std::filesystem::remove_all(dirA);
|
||
std::filesystem::remove_all(dirB);
|
||
}
|
||
|
||
// 缺块 finalize → 抛异常(约定:所有块必须写齐)。
|
||
TEST(StreamingVolumeWriter, MissingBrickFinalizeThrows) {
|
||
auto b = makeBuilt(70, 30, 20); // bX=2,bY=1,bZ=1 → 共 2 块
|
||
auto dirA = tmp("swMiss");
|
||
std::filesystem::remove_all(dirA);
|
||
ChunkedVolumeStore::write(dirA, b, 64);
|
||
StoreMeta m = ChunkedVolumeStore::readMeta(dirA);
|
||
|
||
auto dirB = tmp("swMissB");
|
||
std::filesystem::remove_all(dirB);
|
||
{
|
||
StreamingVolumeWriter w(dirB, m);
|
||
w.writeBrick(0, 0, 0, sliceBrickFrom(b, 0, 0, 0, 64)); // 只写 1 块,缺 (1,0,0)
|
||
EXPECT_THROW(w.finalize(), std::runtime_error);
|
||
}
|
||
std::filesystem::remove_all(dirA);
|
||
std::filesystem::remove_all(dirB);
|
||
}
|
||
|
||
// 块体素大小不符(bw*bh*bd 不匹配)→ 抛异常。
|
||
TEST(StreamingVolumeWriter, WrongVoxelCountThrows) {
|
||
auto b = makeBuilt(70, 30, 20);
|
||
auto dirA = tmp("swSize");
|
||
std::filesystem::remove_all(dirA);
|
||
ChunkedVolumeStore::write(dirA, b, 64);
|
||
StoreMeta m = ChunkedVolumeStore::readMeta(dirA);
|
||
|
||
auto dirB = tmp("swSizeB");
|
||
std::filesystem::remove_all(dirB);
|
||
{
|
||
StreamingVolumeWriter w(dirB, m);
|
||
std::vector<std::int16_t> bad(10); // 远小于 64*30*20
|
||
EXPECT_THROW(w.writeBrick(0, 0, 0, bad), std::runtime_error);
|
||
}
|
||
std::filesystem::remove_all(dirA);
|
||
std::filesystem::remove_all(dirB);
|
||
}
|