Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions source/MRTest/MRTest.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
<ClCompile Include="MRMeshVoxelsConverter.cpp" />
<ClCompile Include="MRTriMathTests.cpp" />
<ClCompile Include="MRVolumeToMeshByPartsTests.cpp" />
<ClCompile Include="MRZipCompressTests.cpp" />
<ClCompile Include="MRZlibTests.cpp" />
<ClCompile Include="MRProgressCallback.cpp" />
<ClCompile Include="MRConvexHull.cpp" />
Expand Down
3 changes: 3 additions & 0 deletions source/MRTest/MRTest.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@
<ClCompile Include="MRFillHoleTests.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="MRZipCompressTests.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="..\.editorconfig" />
Expand Down
191 changes: 191 additions & 0 deletions source/MRTest/MRZipCompressTests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#include <MRMesh/MRGTest.h>
#include <MRMesh/MRMakeSphereMesh.h>
#include <MRMesh/MRMesh.h>
#include <MRMesh/MRMeshSave.h>
#include <MRMesh/MRUniqueTemporaryFolder.h>
#include <MRMesh/MRZip.h>
#include <MRPch/MRSpdlog.h>

#include <cstdint>
#include <cstdio>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>
#include <vector>

namespace MR
{

// Writes a sphere to a .mrmesh file in a temporary folder, then
// compresses that folder to a .zip and verifies the archive was created and
// is non-empty. Serves as a realistic end-to-end exercise of MeshLib's zip
// write path (libzip + deflate) on mesh-sized data.
TEST( MRMesh, CompressSphereToZip )
{
UniqueTemporaryFolder srcFolder;
ASSERT_TRUE( bool( srcFolder ) );

constexpr int targetVerts = 1000; // increase it to make the file being compressed larger, 100'000 vertices -> 12M bytes
SphereParams params;
params.radius = 1.0f;
params.numMeshVertices = targetVerts;
const Mesh sphere = makeSphere( params );
EXPECT_EQ( (int)sphere.topology.numValidVerts(), targetVerts );

// Save mesh as a .mrmesh file in the temp folder.
const std::filesystem::path meshPath = srcFolder / "sphere.mrmesh";
const auto saveRes = MeshSave::toMrmesh( sphere, meshPath );
ASSERT_TRUE( saveRes.has_value() ) << saveRes.error();
std::error_code ec;
ASSERT_TRUE( std::filesystem::exists( meshPath, ec ) );
const auto meshSize = std::filesystem::file_size( meshPath, ec );
EXPECT_GT( meshSize, 0u );
spdlog::info( "sphere.mrmesh size: {} bytes", meshSize );

// Compress the temp folder into a .zip located in a second temp folder
// (so the zip isn't inside the folder being compressed).
UniqueTemporaryFolder dstFolder;
ASSERT_TRUE( bool( dstFolder ) );
const std::filesystem::path zipPath = dstFolder / "sphere.zip";

const auto compressRes = compressZip( zipPath, srcFolder );
ASSERT_TRUE( compressRes.has_value() ) << compressRes.error();
ASSERT_TRUE( std::filesystem::exists( zipPath, ec ) );
const auto zipSize = std::filesystem::file_size( zipPath, ec );
EXPECT_GT( zipSize, 0u );
spdlog::info( "sphere.zip size: {} bytes", zipSize );

// Sanity: the zip should not be absurdly larger than the source
// (that would indicate something is wrong with the envelope); and
// since .mrmesh is a raw binary dump of topology plus coordinate
// floats, deflate typically produces a modestly smaller archive.
EXPECT_LT( zipSize, meshSize * 2u );
}

// Writes many binary files and same number JSON files to a temporary folder, then
// compresses the folder to a .zip. Pairs with CompressSphereToZip to compare
// compression of one large binary vs many small mixed-type entries.
//
// libzip compresses each entry independently, so per-entry overhead (local
// file header, CRC32 pass, separate deflate session) can dominate when the
// archive is made of many small files. This test makes that cost visible.
TEST( MRMesh, CompressManySmallFilesToZip )
{
UniqueTemporaryFolder srcFolder;
ASSERT_TRUE( bool( srcFolder ) );

// increase both below numbers to make the files being compressed larger, 200 * 2 files * 60'000 bytes -> 24M bytes
constexpr int numBinaryFiles = 20;
constexpr int numJsonFiles = numBinaryFiles;
constexpr size_t bytesPerFile = 6000;

// Simple LCG used to produce deterministic pseudo-random bytes.
// Keeps the test reproducible across runs and platforms while avoiding
// trivially-compressible input (an all-zeros buffer would make deflate
// look unrealistically good).
auto nextLcg = []( uint64_t & state ) -> uint64_t
{
state = state * 6364136223846793005ULL + 1442695040888963407ULL;
return state;
};

auto makeName = []( const char * prefix, int i, const char * ext )
{
char buf[64];
std::snprintf( buf, sizeof( buf ), "%s_%03d.%s", prefix, i, ext );
return std::string( buf );
};

// 100 binary files of pseudo-random bytes. Poor compressibility on
// purpose — representative of mesh coordinate floats, compressed-texture
// blobs, and other near-incompressible payloads that often live in a
// MeshLib scene save.
std::size_t totalBinaryBytes = 0;
std::vector<char> binBuf( bytesPerFile );
for ( int i = 0; i < numBinaryFiles; ++i )
{
uint64_t state = 0x1234567890ABCDEFULL ^ ( (uint64_t)i << 1 );
for ( size_t j = 0; j < bytesPerFile; ++j )
binBuf[j] = (char)( nextLcg( state ) >> 56 );

const std::filesystem::path p = srcFolder / makeName( "data", i, "bin" );
std::ofstream out( p, std::ios::binary );
ASSERT_TRUE( out.is_open() );
out.write( binBuf.data(), (std::streamsize)binBuf.size() );
ASSERT_TRUE( out.good() );
out.close();
totalBinaryBytes += bytesPerFile;
}

// JSON files of deterministic structured-looking text. Highly
// compressible — representative of scene-description metadata, logs,
// shader source, and other textual payloads.
std::size_t totalJsonBytes = 0;
for ( int i = 0; i < numJsonFiles; ++i )
{
uint64_t state = 0xDEADBEEFCAFEBABEULL ^ ( (uint64_t)i << 1 );

std::string text;
text.reserve( bytesPerFile + 256 );
text += "[\n";
int idx = 0;
while ( text.size() + 96 < bytesPerFile )
{
if ( idx > 0 )
text += ",\n";
const uint32_t rx = (uint32_t)( nextLcg( state ) >> 32 );
const uint32_t ry = (uint32_t)( nextLcg( state ) >> 32 );
const uint32_t rz = (uint32_t)( nextLcg( state ) >> 32 );
char line[128];
const int n = std::snprintf( line, sizeof( line ),
" {\"id\": %d, \"x\": %.6f, \"y\": %.6f, \"z\": %.6f}",
idx,
(double)rx / 4294967296.0,
(double)ry / 4294967296.0,
(double)rz / 4294967296.0 );
ASSERT_GT( n, 0 );
text.append( line, (size_t)n );
++idx;
}
text += "\n]\n";
// Pad to exactly bytesPerFile with trailing spaces so the per-file
// size — and therefore the total — is deterministic across runs.
// The file is never parsed, so trailing whitespace past the final
// ']' is harmless.
if ( text.size() < bytesPerFile )
text.append( bytesPerFile - text.size(), ' ' );
else if ( text.size() > bytesPerFile )
text.resize( bytesPerFile );

const std::filesystem::path p = srcFolder / makeName( "meta", i, "json" );
std::ofstream out( p, std::ios::binary );
ASSERT_TRUE( out.is_open() );
out.write( text.data(), (std::streamsize)text.size() );
ASSERT_TRUE( out.good() );
out.close();
totalJsonBytes += text.size();
}

const std::size_t totalInput = totalBinaryBytes + totalJsonBytes;
spdlog::info( "many-files input: {} binary + {} json = {} bytes",
totalBinaryBytes, totalJsonBytes, totalInput );

// Compress to a zip in a separate temp folder.
UniqueTemporaryFolder dstFolder;
ASSERT_TRUE( bool( dstFolder ) );
const std::filesystem::path zipPath = dstFolder / "many.zip";

const auto compressRes = compressZip( zipPath, srcFolder );
ASSERT_TRUE( compressRes.has_value() ) << compressRes.error();
std::error_code ec;
ASSERT_TRUE( std::filesystem::exists( zipPath, ec ) );
const auto zipSize = std::filesystem::file_size( zipPath, ec );
EXPECT_GT( zipSize, 0u );
spdlog::info( "many.zip size: {} bytes", zipSize );

// Sanity envelope: same bound as the sphere test.
EXPECT_LT( zipSize, totalInput * 2u );
}

} // namespace MR
Loading