feat(v11): CI/CD + regression tests + fuzzing + benchmarks + code quality
v11.1 — Test Infrastructure (14 files): - .github/workflows/ci.yml: Ubuntu 22.04 + GCC 11, auto ctest on push - tests/regression/: degenerate geometry, extreme coords, thin wall, large models - tests/fuzz/: SDF random CSG, random booleans, format round-trip, continuous mode - tests/benchmark/: boolean perf, MC perf, IO perf benchmarks - docs/benchmark-report.md: benchmark template v11.2 — Code Quality + Docs: - .clang-tidy: 15 check categories, 22 exclusions - .github/workflows/static-analysis.yml: clang-tidy CI scan - CODEOWNERS, SECURITY.md - docs: API overview, testing guide, contributing guide - README: module capability overview table Pending: binary formats + curve projection (retrying)
This commit is contained in:
+200
-37
@@ -1,59 +1,222 @@
|
||||
/// @file fuzz_boolean.cpp
|
||||
/// @brief B-Rep 布尔随机参数模糊测试 + 持续运行模式
|
||||
///
|
||||
/// 测试策略:
|
||||
/// 1. 随机尺寸的盒子/圆柱/球体对做布尔运算
|
||||
/// 2. 随机偏移后的布尔运算
|
||||
/// 3. 连续布尔链式模糊测试
|
||||
/// 4. 验证结果的合法性
|
||||
/// 5. 持续运行模式
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/brep/brep.h"
|
||||
#include "vde/brep/brep_boolean.h"
|
||||
#include "vde/brep/modeling.h"
|
||||
#include "vde/brep/brep_validate.h"
|
||||
#include "vde/brep/brep_heal.h"
|
||||
#include "vde/core/point.h"
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <cstdlib>
|
||||
|
||||
using namespace vde::brep;
|
||||
using namespace vde::core;
|
||||
|
||||
// ── B-Rep fuzz: random sized boxes with boolean operations ──
|
||||
static std::mt19937 s_rng(42);
|
||||
|
||||
TEST(FuzzBoolean, RandomBoxBooleans) {
|
||||
std::mt19937 rng(42);
|
||||
std::uniform_real_distribution<double> size_dist(1.0, 5.0);
|
||||
std::uniform_real_distribution<double> offset_dist(-2.0, 2.0);
|
||||
// ── 随机参数生成器 ──
|
||||
|
||||
for (int i = 0; i < 30; ++i) {
|
||||
double s1 = size_dist(rng);
|
||||
double s2 = size_dist(rng);
|
||||
double ox = offset_dist(rng);
|
||||
double oy = offset_dist(rng);
|
||||
double oz = offset_dist(rng);
|
||||
// NOTE: ox, oy, oz computed for future translate API use
|
||||
(void)ox; (void)oy; (void)oz;
|
||||
namespace {
|
||||
|
||||
auto box1 = make_box(s1, s1, s1);
|
||||
auto box2 = make_box(s2, s2, s2);
|
||||
double rand_range(double lo, double hi) {
|
||||
return std::uniform_real_distribution<double>(lo, hi)(s_rng);
|
||||
}
|
||||
|
||||
// Test that operations don't crash
|
||||
auto u = brep_union(box1, box2);
|
||||
auto inter = brep_intersection(box1, box2);
|
||||
auto diff = brep_difference(box1, box2);
|
||||
BrepModel rand_box() {
|
||||
return make_box(rand_range(0.5, 10.0),
|
||||
rand_range(0.5, 10.0),
|
||||
rand_range(0.5, 10.0));
|
||||
}
|
||||
|
||||
// Basic sanity: results should have finite bounds
|
||||
EXPECT_TRUE(std::isfinite(u.bounds().min().x()));
|
||||
EXPECT_TRUE(std::isfinite(inter.bounds().min().x()));
|
||||
EXPECT_TRUE(std::isfinite(diff.bounds().min().x()));
|
||||
BrepModel rand_cylinder() {
|
||||
return make_cylinder(rand_range(0.2, 5.0),
|
||||
rand_range(0.5, 10.0),
|
||||
static_cast<int>(rand_range(8, 64)));
|
||||
}
|
||||
|
||||
BrepModel rand_sphere() {
|
||||
return make_sphere(rand_range(0.3, 5.0),
|
||||
static_cast<int>(rand_range(8, 32)),
|
||||
static_cast<int>(rand_range(4, 16)));
|
||||
}
|
||||
|
||||
BrepModel rand_primitive() {
|
||||
int choice = static_cast<int>(rand_range(0, 3));
|
||||
switch (choice) {
|
||||
case 0: return rand_box();
|
||||
case 1: return rand_cylinder();
|
||||
default: return rand_sphere();
|
||||
}
|
||||
}
|
||||
|
||||
// ── B-Rep fuzz: self-operations (A∪A, A∩A, A\A) ──
|
||||
} // anonymous
|
||||
|
||||
TEST(FuzzBoolean, SelfOperations) {
|
||||
// ═══════════════════════════════════════════════
|
||||
// 随机盒子布尔测试
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
TEST(FuzzBoolean, RandomBoxUnion) {
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
auto a = rand_box();
|
||||
auto b = rand_box();
|
||||
|
||||
EXPECT_NO_THROW({
|
||||
auto u = brep_union(a, b);
|
||||
EXPECT_TRUE(u.is_valid());
|
||||
|
||||
// 结果不应为空体
|
||||
EXPECT_GT(u.num_vertices(), 0u);
|
||||
|
||||
// 验证
|
||||
auto result = validate(u);
|
||||
(void)result;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FuzzBoolean, RandomBoxIntersection) {
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
auto a = rand_box();
|
||||
auto b = rand_box();
|
||||
|
||||
EXPECT_NO_THROW({
|
||||
auto inter = brep_intersection(a, b);
|
||||
EXPECT_TRUE(inter.is_valid());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FuzzBoolean, RandomBoxDifference) {
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
auto a = rand_box();
|
||||
auto b = rand_box();
|
||||
|
||||
EXPECT_NO_THROW({
|
||||
auto diff = brep_difference(a, b);
|
||||
EXPECT_TRUE(diff.is_valid());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 随机基本体布尔混合测试
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
TEST(FuzzBoolean, RandomMixedPrimitives) {
|
||||
for (int i = 0; i < 60; ++i) {
|
||||
auto a = rand_primitive();
|
||||
auto b = rand_primitive();
|
||||
|
||||
EXPECT_NO_THROW({
|
||||
auto u = brep_union(a, b);
|
||||
EXPECT_TRUE(u.is_valid()) << "union failed at iteration " << i;
|
||||
|
||||
auto inter = brep_intersection(a, b);
|
||||
EXPECT_TRUE(inter.is_valid()) << "intersection failed at iteration " << i;
|
||||
|
||||
auto diff = brep_difference(a, b);
|
||||
EXPECT_TRUE(diff.is_valid()) << "difference failed at iteration " << i;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 随机布尔链(连续多个布尔操作)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
TEST(FuzzBoolean, RandomBooleanChain) {
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
double s = 1.0 + i * 0.2;
|
||||
auto box = make_box(s, s, s);
|
||||
EXPECT_NO_THROW({
|
||||
auto result = rand_primitive();
|
||||
int chain_len = static_cast<int>(rand_range(2, 6));
|
||||
|
||||
// Self-union should be valid
|
||||
auto self_u = brep_union(box, box);
|
||||
EXPECT_TRUE(self_u.is_valid());
|
||||
for (int j = 0; j < chain_len; ++j) {
|
||||
auto other = rand_primitive();
|
||||
int op_choice = static_cast<int>(rand_range(0, 3));
|
||||
switch (op_choice) {
|
||||
case 0: result = brep_union(result, other); break;
|
||||
case 1: result = brep_intersection(result, other); break;
|
||||
default: result = brep_difference(result, other); break;
|
||||
}
|
||||
ASSERT_TRUE(result.is_valid())
|
||||
<< "chain failed at step " << j << " of chain " << i;
|
||||
}
|
||||
|
||||
// Self-difference should be valid
|
||||
auto self_d = brep_difference(box, box);
|
||||
EXPECT_TRUE(self_d.is_valid());
|
||||
|
||||
// Self-intersection should be valid
|
||||
auto self_i = brep_intersection(box, box);
|
||||
EXPECT_TRUE(self_i.is_valid());
|
||||
// 最终结果应仍有效
|
||||
EXPECT_TRUE(result.is_valid());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 自交操作模糊测试
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
TEST(FuzzBoolean, RandomSelfOperations) {
|
||||
for (int i = 0; i < 30; ++i) {
|
||||
auto body = rand_primitive();
|
||||
|
||||
EXPECT_NO_THROW({
|
||||
auto u = brep_union(body, body);
|
||||
EXPECT_TRUE(u.is_valid());
|
||||
|
||||
auto inter = brep_intersection(body, body);
|
||||
EXPECT_TRUE(inter.is_valid());
|
||||
|
||||
auto diff = brep_difference(body, body);
|
||||
EXPECT_TRUE(diff.is_valid());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 持续运行模式
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
TEST(FuzzBoolean, ContinuousFuzz) {
|
||||
const char* forever = std::getenv("VDE_FUZZ_FOREVER");
|
||||
if (!forever || std::string(forever) != "1") {
|
||||
GTEST_SKIP() << "设置 VDE_FUZZ_FOREVER=1 启用持续模糊测试";
|
||||
}
|
||||
|
||||
int iteration = 0;
|
||||
while (true) {
|
||||
++iteration;
|
||||
auto a = rand_primitive();
|
||||
auto b = rand_primitive();
|
||||
|
||||
// 三项布尔操作都必须成功
|
||||
auto u = brep_union(a, b);
|
||||
ASSERT_TRUE(u.is_valid()) << "union failed at iteration " << iteration;
|
||||
|
||||
auto inter = brep_intersection(a, b);
|
||||
ASSERT_TRUE(inter.is_valid()) << "intersection failed at iteration " << iteration;
|
||||
|
||||
auto diff = brep_difference(a, b);
|
||||
ASSERT_TRUE(diff.is_valid()) << "difference failed at iteration " << iteration;
|
||||
|
||||
// 验证
|
||||
auto vr_u = validate(u);
|
||||
(void)vr_u;
|
||||
auto vr_i = validate(inter);
|
||||
(void)vr_i;
|
||||
|
||||
if (iteration % 1000 == 0) {
|
||||
std::cout << "[FuzzBoolean] iteration=" << iteration
|
||||
<< " union_faces=" << u.num_faces()
|
||||
<< " intersection_faces=" << inter.num_faces()
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+276
-17
@@ -1,40 +1,299 @@
|
||||
/// @file fuzz_format.cpp
|
||||
/// @brief 格式往返模糊测试(IGES/STEP)+ 持续运行模式
|
||||
///
|
||||
/// 测试策略:
|
||||
/// 1. 随机生成 B-Rep 体 → IGES 导出 → IGES 导入 → 验证一致性
|
||||
/// 2. 随机生成 B-Rep 体 → STEP 导出 → STEP 导入 → 验证一致性
|
||||
/// 3. 极端尺寸的格式往返
|
||||
/// 4. 多体格式往返
|
||||
/// 5. 持续运行模式
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/brep/brep.h"
|
||||
#include "vde/brep/modeling.h"
|
||||
#include "vde/brep/iges_export.h"
|
||||
#include "vde/brep/iges_import.h"
|
||||
#include "vde/brep/step_export.h"
|
||||
#include "vde/brep/step_import.h"
|
||||
#include "vde/brep/modeling.h"
|
||||
#include "vde/core/point.h"
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <cstdlib>
|
||||
|
||||
using namespace vde::brep;
|
||||
using namespace vde::core;
|
||||
|
||||
// ── Format fuzz: IGES round-trip ──
|
||||
static std::mt19937 s_rng(42);
|
||||
|
||||
TEST(FuzzFormat, IgesRoundTrip) {
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
double s = 0.5 + i * 0.5;
|
||||
auto box = make_box(s, s, s);
|
||||
namespace {
|
||||
|
||||
std::string iges = export_iges({box});
|
||||
double rand_range(double lo, double hi) {
|
||||
return std::uniform_real_distribution<double>(lo, hi)(s_rng);
|
||||
}
|
||||
|
||||
BrepModel rand_body() {
|
||||
int choice = static_cast<int>(rand_range(0, 3));
|
||||
switch (choice) {
|
||||
case 0:
|
||||
return make_box(rand_range(0.5, 10.0),
|
||||
rand_range(0.5, 10.0),
|
||||
rand_range(0.5, 10.0));
|
||||
case 1:
|
||||
return make_cylinder(rand_range(0.3, 5.0),
|
||||
rand_range(0.5, 10.0),
|
||||
static_cast<int>(rand_range(8, 32)));
|
||||
default:
|
||||
return make_sphere(rand_range(0.3, 5.0),
|
||||
static_cast<int>(rand_range(8, 24)),
|
||||
static_cast<int>(rand_range(4, 12)));
|
||||
}
|
||||
}
|
||||
|
||||
} // anonymous
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// IGES 随机往返
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
TEST(FuzzFormat, Iges_RandomRoundTrip) {
|
||||
for (int i = 0; i < 30; ++i) {
|
||||
auto body = rand_body();
|
||||
EXPECT_TRUE(body.is_valid());
|
||||
|
||||
// 导出 IGES
|
||||
std::string iges;
|
||||
EXPECT_NO_THROW({
|
||||
iges = export_iges({body});
|
||||
});
|
||||
EXPECT_FALSE(iges.empty()) << "IGES export failed at iteration " << i;
|
||||
|
||||
// 重新导入
|
||||
std::vector<BrepModel> loaded;
|
||||
EXPECT_NO_THROW({
|
||||
loaded = import_iges_from_string(iges);
|
||||
});
|
||||
EXPECT_FALSE(loaded.empty()) << "IGES import failed at iteration " << i;
|
||||
|
||||
// 验证导入体
|
||||
for (auto& lb : loaded) {
|
||||
EXPECT_TRUE(lb.is_valid()) << "imported body invalid at iteration " << i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FuzzFormat, Iges_MultiBodyRoundTrip) {
|
||||
for (int i = 0; i < 15; ++i) {
|
||||
int count = static_cast<int>(rand_range(2, 5));
|
||||
std::vector<BrepModel> bodies;
|
||||
for (int j = 0; j < count; ++j) {
|
||||
bodies.push_back(rand_body());
|
||||
}
|
||||
|
||||
std::string iges;
|
||||
EXPECT_NO_THROW({ iges = export_iges(bodies); });
|
||||
EXPECT_FALSE(iges.empty());
|
||||
|
||||
auto loaded = import_iges_from_string(iges);
|
||||
std::vector<BrepModel> loaded;
|
||||
EXPECT_NO_THROW({ loaded = import_iges_from_string(iges); });
|
||||
EXPECT_FALSE(loaded.empty());
|
||||
EXPECT_TRUE(loaded[0].is_valid());
|
||||
EXPECT_GE(loaded.size(), 1u); // 可能合并或保留多个体
|
||||
|
||||
for (auto& lb : loaded) {
|
||||
EXPECT_TRUE(lb.is_valid());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Format fuzz: STEP round-trip ──
|
||||
TEST(FuzzFormat, Iges_ExtremeSizes) {
|
||||
// 极端尺寸的 IGES 往返
|
||||
double sizes[] = {1e-3, 1e-1, 1.0, 10.0, 100.0, 1000.0};
|
||||
for (double s : sizes) {
|
||||
auto body = make_box(s, s * 0.5, s * 2.0);
|
||||
|
||||
TEST(FuzzFormat, StepRoundTrip) {
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
double s = 0.5 + i * 0.5;
|
||||
auto box = make_box(s, s, s);
|
||||
EXPECT_NO_THROW({
|
||||
std::string iges = export_iges({body});
|
||||
EXPECT_FALSE(iges.empty());
|
||||
|
||||
std::string step = export_step({box});
|
||||
auto loaded = import_iges_from_string(iges);
|
||||
EXPECT_FALSE(loaded.empty());
|
||||
EXPECT_TRUE(loaded[0].is_valid());
|
||||
}) << "failed at size=" << s;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// STEP 随机往返
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
TEST(FuzzFormat, Step_RandomRoundTrip) {
|
||||
for (int i = 0; i < 30; ++i) {
|
||||
auto body = rand_body();
|
||||
EXPECT_TRUE(body.is_valid());
|
||||
|
||||
std::string step;
|
||||
EXPECT_NO_THROW({
|
||||
step = export_step({body});
|
||||
});
|
||||
EXPECT_FALSE(step.empty()) << "STEP export failed at iteration " << i;
|
||||
|
||||
std::vector<BrepModel> loaded;
|
||||
EXPECT_NO_THROW({
|
||||
loaded = import_step_from_string(step);
|
||||
});
|
||||
EXPECT_FALSE(loaded.empty()) << "STEP import failed at iteration " << i;
|
||||
|
||||
for (auto& lb : loaded) {
|
||||
EXPECT_TRUE(lb.is_valid()) << "imported body invalid at iteration " << i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FuzzFormat, Step_MultiBodyRoundTrip) {
|
||||
for (int i = 0; i < 15; ++i) {
|
||||
int count = static_cast<int>(rand_range(2, 5));
|
||||
std::vector<BrepModel> bodies;
|
||||
for (int j = 0; j < count; ++j) {
|
||||
bodies.push_back(rand_body());
|
||||
}
|
||||
|
||||
std::string step;
|
||||
EXPECT_NO_THROW({ step = export_step(bodies); });
|
||||
EXPECT_FALSE(step.empty());
|
||||
|
||||
auto loaded = import_step_from_string(step);
|
||||
std::vector<BrepModel> loaded;
|
||||
EXPECT_NO_THROW({ loaded = import_step_from_string(step); });
|
||||
EXPECT_FALSE(loaded.empty());
|
||||
EXPECT_TRUE(loaded[0].is_valid());
|
||||
|
||||
for (auto& lb : loaded) {
|
||||
EXPECT_TRUE(lb.is_valid());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FuzzFormat, Step_ExtremeSizes) {
|
||||
double sizes[] = {1e-3, 1e-1, 1.0, 10.0, 100.0, 1000.0};
|
||||
for (double s : sizes) {
|
||||
auto body = make_box(s, s * 0.5, s * 2.0);
|
||||
|
||||
EXPECT_NO_THROW({
|
||||
std::string step = export_step({body});
|
||||
EXPECT_FALSE(step.empty());
|
||||
|
||||
auto loaded = import_step_from_string(step);
|
||||
EXPECT_FALSE(loaded.empty());
|
||||
EXPECT_TRUE(loaded[0].is_valid());
|
||||
}) << "failed at size=" << s;
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 交叉格式往返(IGES → STEP → IGES)
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
TEST(FuzzFormat, CrossFormat_IGES_to_STEP) {
|
||||
for (int i = 0; i < 15; ++i) {
|
||||
auto body = rand_body();
|
||||
|
||||
EXPECT_NO_THROW({
|
||||
// IGES export → STEP import
|
||||
std::string iges = export_iges({body});
|
||||
auto from_iges = import_iges_from_string(iges);
|
||||
EXPECT_FALSE(from_iges.empty());
|
||||
|
||||
// STEP export → IGES import
|
||||
std::string step = export_step({body});
|
||||
auto from_step = import_step_from_string(step);
|
||||
EXPECT_FALSE(from_step.empty());
|
||||
|
||||
// Both should be valid
|
||||
EXPECT_TRUE(from_iges[0].is_valid());
|
||||
EXPECT_TRUE(from_step[0].is_valid());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FuzzFormat, CrossFormat_STEP_to_IGES) {
|
||||
for (int i = 0; i < 15; ++i) {
|
||||
auto body = rand_body();
|
||||
|
||||
EXPECT_NO_THROW({
|
||||
// STEP → IGES
|
||||
std::string step = export_step({body});
|
||||
auto from_step = import_step_from_string(step);
|
||||
ASSERT_FALSE(from_step.empty());
|
||||
|
||||
std::string iges = export_iges(from_step);
|
||||
auto from_iges = import_iges_from_string(iges);
|
||||
EXPECT_FALSE(from_iges.empty());
|
||||
EXPECT_TRUE(from_iges[0].is_valid());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 布尔运算后的格式往返
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
TEST(FuzzFormat, BooleanThenRoundTrip) {
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
auto a = rand_body();
|
||||
auto b = rand_body();
|
||||
|
||||
EXPECT_NO_THROW({
|
||||
// 先做布尔运算
|
||||
auto u = brep_union(a, b);
|
||||
|
||||
// IGES 往返
|
||||
std::string iges = export_iges({u});
|
||||
auto from_iges = import_iges_from_string(iges);
|
||||
EXPECT_FALSE(from_iges.empty());
|
||||
EXPECT_TRUE(from_iges[0].is_valid());
|
||||
|
||||
// STEP 往返
|
||||
std::string step = export_step({u});
|
||||
auto from_step = import_step_from_string(step);
|
||||
EXPECT_FALSE(from_step.empty());
|
||||
EXPECT_TRUE(from_step[0].is_valid());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 持续运行模式
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
TEST(FuzzFormat, ContinuousFuzz) {
|
||||
const char* forever = std::getenv("VDE_FUZZ_FOREVER");
|
||||
if (!forever || std::string(forever) != "1") {
|
||||
GTEST_SKIP() << "设置 VDE_FUZZ_FOREVER=1 启用持续模糊测试";
|
||||
}
|
||||
|
||||
int iteration = 0;
|
||||
while (true) {
|
||||
++iteration;
|
||||
auto body = rand_body();
|
||||
ASSERT_TRUE(body.is_valid());
|
||||
|
||||
// IGES 往返
|
||||
std::string iges = export_iges({body});
|
||||
ASSERT_FALSE(iges.empty());
|
||||
auto from_iges = import_iges_from_string(iges);
|
||||
ASSERT_FALSE(from_iges.empty()) << "IGES import failed at iteration " << iteration;
|
||||
ASSERT_TRUE(from_iges[0].is_valid());
|
||||
|
||||
// STEP 往返
|
||||
std::string step = export_step({body});
|
||||
ASSERT_FALSE(step.empty());
|
||||
auto from_step = import_step_from_string(step);
|
||||
ASSERT_FALSE(from_step.empty()) << "STEP import failed at iteration " << iteration;
|
||||
ASSERT_TRUE(from_step[0].is_valid());
|
||||
|
||||
if (iteration % 1000 == 0) {
|
||||
std::cout << "[FuzzFormat] iteration=" << iteration
|
||||
<< " iges_len=" << iges.size()
|
||||
<< " step_len=" << step.size()
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+247
-57
@@ -1,81 +1,271 @@
|
||||
/// @file fuzz_sdf.cpp
|
||||
/// @brief SDF 随机CSG树模糊测试 + 持续运行模式
|
||||
///
|
||||
/// 测试策略:
|
||||
/// 1. 随机生成深度 CSG 树(最大深度 6)
|
||||
/// 2. 随机选择操作符(union/intersection/difference/smooth variants)
|
||||
/// 3. 随机选择图元(sphere/box/cylinder/torus/capsule)
|
||||
/// 4. 随机应用域变形(translate/rotate/scale/twist/bend)
|
||||
/// 5. 随机采样点评估并验证结果
|
||||
/// 6. 持续运行模式(命令行参数 -1 表示无限循环)
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "vde/sdf/sdf_primitives.h"
|
||||
#include "vde/sdf/sdf_operations.h"
|
||||
#include "vde/sdf/sdf_tree.h"
|
||||
#include <cmath>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <cstdlib>
|
||||
|
||||
using namespace vde::sdf;
|
||||
using namespace vde::core;
|
||||
|
||||
// ── SDF fuzz: random sphere evaluation ──
|
||||
// ═══════════════════════════════════════════════
|
||||
// SDF 随机种子
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
TEST(FuzzSdf, RandomSpheres) {
|
||||
std::mt19937 rng(42);
|
||||
std::uniform_real_distribution<double> pos(-100, 100);
|
||||
std::uniform_real_distribution<double> rad(0.1, 10.0);
|
||||
static std::mt19937 s_rng(42);
|
||||
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
double r = rad(rng);
|
||||
double x = pos(rng), y = pos(rng), z = pos(rng);
|
||||
Point3D p(x, y, z);
|
||||
// ═══════════════════════════════════════════════
|
||||
// 随机参数生成器
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
double d = sphere(p, r);
|
||||
// Basic sanity: SDF should be finite
|
||||
EXPECT_TRUE(std::isfinite(d));
|
||||
// On surface, SDF should be 0
|
||||
// Point on sphere in X direction
|
||||
Point3D on_surf(r, 0, 0);
|
||||
EXPECT_NEAR(sphere(on_surf, r), 0.0, 1e-6);
|
||||
}
|
||||
namespace {
|
||||
|
||||
/// 随机双精度 [lo, hi]
|
||||
double rand_range(double lo, double hi) {
|
||||
std::uniform_real_distribution<double> dist(lo, hi);
|
||||
return dist(s_rng);
|
||||
}
|
||||
|
||||
// ── SDF fuzz: random CSG boolean combinations ──
|
||||
|
||||
TEST(FuzzSdf, RandomCSG) {
|
||||
std::mt19937 rng(42);
|
||||
std::uniform_real_distribution<double> pos(-10, 10);
|
||||
std::uniform_real_distribution<double> rad(0.5, 5.0);
|
||||
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
double r1 = rad(rng), r2 = rad(rng);
|
||||
double x = pos(rng), y = pos(rng), z = pos(rng);
|
||||
Point3D p(x, y, z);
|
||||
|
||||
double d1 = sphere(Point3D(x, y, z), r1);
|
||||
double d2 = box(Point3D(x, y, z), Point3D(r2, r2, r2));
|
||||
|
||||
double du = op_union(d1, d2);
|
||||
double di = op_intersection(d1, d2);
|
||||
double dd = op_difference(d1, d2);
|
||||
|
||||
EXPECT_TRUE(std::isfinite(du));
|
||||
EXPECT_TRUE(std::isfinite(di));
|
||||
EXPECT_TRUE(std::isfinite(dd));
|
||||
|
||||
// Union of A and B should be <= max(A,B) at any point
|
||||
EXPECT_LE(du, std::max(d1, d2) + 1e-9);
|
||||
}
|
||||
/// 随机 Point3D,范围 [lo, hi]
|
||||
Point3D rand_point(double lo, double hi) {
|
||||
return Point3D(rand_range(lo, hi), rand_range(lo, hi), rand_range(lo, hi));
|
||||
}
|
||||
|
||||
// ── SDF fuzz: random SDF tree construction and evaluation ──
|
||||
/// 随机选择操作类型(CSG 布尔)
|
||||
SdfOp rand_csg_op() {
|
||||
static const SdfOp ops[] = {
|
||||
SdfOp::Union, SdfOp::Intersection, SdfOp::Difference,
|
||||
SdfOp::SmoothUnion, SdfOp::SmoothIntersection, SdfOp::SmoothDifference
|
||||
};
|
||||
return ops[std::uniform_int_distribution<int>(0, 5)(s_rng)];
|
||||
}
|
||||
|
||||
TEST(FuzzSdf, RandomTreeEvaluate) {
|
||||
std::mt19937 rng(42);
|
||||
/// 随机选择图元类型
|
||||
SdfOp rand_primitive_op() {
|
||||
static const SdfOp ops[] = {
|
||||
SdfOp::Sphere, SdfOp::Box, SdfOp::Cylinder,
|
||||
SdfOp::Torus, SdfOp::Capsule
|
||||
};
|
||||
return ops[std::uniform_int_distribution<int>(0, 4)(s_rng)];
|
||||
}
|
||||
|
||||
/// 随机选择域变形类型
|
||||
SdfOp rand_transform_op() {
|
||||
static const SdfOp ops[] = {
|
||||
SdfOp::Translate, SdfOp::Rotate, SdfOp::Scale,
|
||||
SdfOp::Twist, SdfOp::Bend
|
||||
};
|
||||
return ops[std::uniform_int_distribution<int>(0, 4)(s_rng)];
|
||||
}
|
||||
|
||||
} // anonymous
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 随机 CSG 树构造
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
/// 创建随机图元节点
|
||||
SdfNodePtr rand_primitive() {
|
||||
SdfOp op = rand_primitive_op();
|
||||
auto node = std::make_shared<SdfNode>(op);
|
||||
|
||||
switch (op) {
|
||||
case SdfOp::Sphere:
|
||||
node->params.radius = rand_range(0.1, 5.0);
|
||||
break;
|
||||
case SdfOp::Box:
|
||||
node->params.extents = rand_point(0.1, 3.0);
|
||||
break;
|
||||
case SdfOp::Cylinder:
|
||||
node->params.radius = rand_range(0.1, 3.0);
|
||||
node->params.height = rand_range(0.2, 6.0);
|
||||
break;
|
||||
case SdfOp::Torus:
|
||||
node->params.major_radius = rand_range(0.5, 4.0);
|
||||
node->params.minor_radius = rand_range(0.1, 1.5);
|
||||
break;
|
||||
case SdfOp::Capsule:
|
||||
node->params.pt_a = Point3D(0, -rand_range(0.5, 3.0), 0);
|
||||
node->params.pt_b = Point3D(0, rand_range(0.5, 3.0), 0);
|
||||
node->params.radius = rand_range(0.1, 1.0);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
/// 创建随机 CSG 布尔节点(递归)
|
||||
SdfNodePtr rand_csg_tree(int depth, int max_depth) {
|
||||
if (depth >= max_depth) {
|
||||
return rand_primitive();
|
||||
}
|
||||
|
||||
SdfOp op = rand_csg_op();
|
||||
auto left = rand_csg_tree(depth + 1, max_depth);
|
||||
auto right = rand_csg_tree(depth + 1, max_depth);
|
||||
|
||||
auto node = std::make_shared<SdfNode>(op);
|
||||
node->children.push_back(left);
|
||||
node->children.push_back(right);
|
||||
|
||||
if (op == SdfOp::SmoothUnion || op == SdfOp::SmoothIntersection ||
|
||||
op == SdfOp::SmoothDifference) {
|
||||
node->params.blend_k = rand_range(0.05, 0.5);
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
/// 随机应用域变形包装
|
||||
SdfNodePtr wrap_transform(SdfNodePtr child) {
|
||||
if (rand_range(0.0, 1.0) < 0.3) {
|
||||
SdfOp op = rand_transform_op();
|
||||
auto node = std::make_shared<SdfNode>(op);
|
||||
node->children.push_back(child);
|
||||
|
||||
switch (op) {
|
||||
case SdfOp::Translate:
|
||||
node->params.translate_offset = rand_point(-2.0, 2.0);
|
||||
break;
|
||||
case SdfOp::Rotate:
|
||||
node->params.amount = rand_range(-M_PI, M_PI);
|
||||
break;
|
||||
case SdfOp::Scale:
|
||||
node->params.scale_factors = rand_point(0.5, 2.0);
|
||||
break;
|
||||
case SdfOp::Twist:
|
||||
node->params.amount = rand_range(-0.5, 0.5);
|
||||
break;
|
||||
case SdfOp::Bend:
|
||||
node->params.amount = rand_range(-0.3, 0.3);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return node;
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 模糊测试用例
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
TEST(FuzzSdf, RandomCSGTree_Evaluate) {
|
||||
// 生成多个随机树并在多个点处求值
|
||||
for (int i = 0; i < 50; ++i) {
|
||||
auto tree = SdfNode::smooth_union(
|
||||
SdfNode::sphere(1.0 + (i % 5) * 0.5),
|
||||
SdfNode::box(Point3D(1, 1, 1)),
|
||||
0.1 + (i % 3) * 0.2
|
||||
);
|
||||
auto tree = rand_csg_tree(0, 4);
|
||||
// 可选变形包装
|
||||
tree = wrap_transform(tree);
|
||||
|
||||
for (int j = 0; j < 20; ++j) {
|
||||
double x = (j % 7 - 3) * 1.0;
|
||||
double y = (j % 5 - 2) * 1.0;
|
||||
double z = (j % 3 - 1) * 1.0;
|
||||
double v = evaluate(tree, Point3D(x, y, z));
|
||||
EXPECT_TRUE(std::isfinite(v));
|
||||
for (int j = 0; j < 30; ++j) {
|
||||
Point3D p = rand_point(-5.0, 5.0);
|
||||
EXPECT_NO_THROW({
|
||||
double v = evaluate(tree, p);
|
||||
// 求值结果必须是有限数(不应是 NaN 或 Inf)
|
||||
EXPECT_TRUE(std::isfinite(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FuzzSdf, RandomCSGTree_DeepTree) {
|
||||
// 深度 CSG 树(depth 6 → 最多 2^6 = 64 个叶子节点)
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
auto tree = rand_csg_tree(0, 6);
|
||||
|
||||
EXPECT_NO_THROW({
|
||||
auto bbox = estimate_bbox(tree, 0.5);
|
||||
EXPECT_TRUE(std::isfinite(bbox.min.x()));
|
||||
EXPECT_TRUE(std::isfinite(bbox.max.x()));
|
||||
|
||||
Point3D p = rand_point(-10.0, 10.0);
|
||||
double v = evaluate(tree, p);
|
||||
EXPECT_TRUE(std::isfinite(v));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FuzzSdf, RandomCSGTree_Bounds) {
|
||||
// 验证 estimate_bounds 对所有随机树返回合理的值
|
||||
for (int i = 0; i < 30; ++i) {
|
||||
auto tree = rand_csg_tree(0, 3);
|
||||
tree = wrap_transform(tree);
|
||||
|
||||
EXPECT_NO_THROW({
|
||||
auto half = estimate_bounds(tree, 1.0);
|
||||
EXPECT_GE(half.x(), 0.0);
|
||||
EXPECT_GE(half.y(), 0.0);
|
||||
EXPECT_GE(half.z(), 0.0);
|
||||
EXPECT_TRUE(std::isfinite(half.x()));
|
||||
EXPECT_TRUE(std::isfinite(half.y()));
|
||||
EXPECT_TRUE(std::isfinite(half.z()));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
TEST(FuzzSdf, RandomTree_Visit) {
|
||||
// 验证 visit() 对所有随机树都不崩溃
|
||||
for (int i = 0; i < 30; ++i) {
|
||||
auto tree = rand_csg_tree(0, 4);
|
||||
tree = wrap_transform(tree);
|
||||
|
||||
EXPECT_NO_THROW({
|
||||
int count = 0;
|
||||
tree->visit([&](const SdfNode& /*n*/) { ++count; });
|
||||
EXPECT_GT(count, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
// 持续运行模式(-1 = 无限循环)
|
||||
// 用法: ./fuzz_sdf --fuzz_forever
|
||||
// ═══════════════════════════════════════════════
|
||||
|
||||
TEST(FuzzSdf, ContinuousFuzz) {
|
||||
// 检查环境变量 VDE_FUZZ_FOREVER=1 启用持续模式
|
||||
const char* forever = std::getenv("VDE_FUZZ_FOREVER");
|
||||
if (!forever || std::string(forever) != "1") {
|
||||
GTEST_SKIP() << "设置 VDE_FUZZ_FOREVER=1 启用持续模糊测试";
|
||||
}
|
||||
|
||||
// 持续运行直到手动停止
|
||||
int iteration = 0;
|
||||
while (true) {
|
||||
++iteration;
|
||||
auto tree = rand_csg_tree(0, std::uniform_int_distribution<int>(2, 5)(s_rng));
|
||||
tree = wrap_transform(tree);
|
||||
|
||||
Point3D p = rand_point(-10.0, 10.0);
|
||||
double v = evaluate(tree, p);
|
||||
|
||||
ASSERT_TRUE(std::isfinite(v))
|
||||
<< "iteration=" << iteration
|
||||
<< " 非有限 SDF 值: " << v;
|
||||
|
||||
// 每 10000 次打印一次进度
|
||||
if (iteration % 10000 == 0) {
|
||||
auto bbox = estimate_bbox(tree, 1.0);
|
||||
std::cout << "[FuzzSdf] iteration=" << iteration
|
||||
<< " value=" << v
|
||||
<< " bbox=[" << bbox.min.x() << "," << bbox.max.x() << "]"
|
||||
<< std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user