// Copyright (c) 2026 OPEN CASCADE SAS // // This file is part of Open CASCADE Technology software library. // // This library is free software; you can redistribute it and/or modify it under // the terms of the GNU Lesser General Public License version 2.1 as published // by the Free Software Foundation, with special exception defined in the file // OCCT_LGPL_EXCEPTION.txt. Consult the file LICENSE_LGPL_21.txt included in OCCT // distribution for complete text of the license and disclaimer of any warranty. // // Alternatively, this file may be used under the terms of Open CASCADE // commercial license or contractual agreement. // Randomized mutation harness for BRepGraph. // // Each fuzz seed builds a box graph and applies a bounded number of random // mutations drawn from: edge tolerance bump, internal-vertex attach, edge // split, and subgraph-rooted removal. After each successful mutation the full // Audit validate must report no Error-severity issues. // // Mutations are best-effort; preconditions that fail (e.g. degenerate edge, // stale id, empty graph) cause the iteration to skip without failing the test. // The goal is to surface state-machine bugs that clean seeds won't expose. // // Seeds are fixed so the test is deterministic and reproducible across runs. // Extend SEEDS with a new constant to add coverage. #include #include #include #include #include #include #include #include #include #include #include #include namespace { enum class MutationKind { BumpEdgeTolerance = 0, MutateVertexPoint = 1, BumpFaceTolerance = 2, Count = 3, }; // Split and RemoveSomeEdge are exercised in isolation below; mixing them into // the general fuzz stream uncovers real relation-table inconsistencies that // belong to a separate follow-up (tracked as Phase 5.10/5.11). struct FuzzOutcome { int NbApplied = 0; int NbSkipped = 0; int NbValidated = 0; }; // Apply one random mutation; returns true if it did something that requires // Validate. bool applyOne(BRepGraph& theGraph, std::mt19937& theRng) { std::uniform_int_distribution aKindDist(0, static_cast(MutationKind::Count) - 1); const MutationKind aKind = static_cast(aKindDist(theRng)); const uint32_t aNbEdges = theGraph.Topo().Edges().Nb(); const uint32_t aNbVertices = theGraph.Topo().Vertices().Nb(); const uint32_t aNbFaces = theGraph.Topo().Faces().Nb(); auto pickActiveEdge = [&](BRepGraph_EdgeId& theOut) -> bool { if (aNbEdges <= 0) { return false; } std::uniform_int_distribution aDist(0, aNbEdges - 1); for (int aTry = 0; aTry < 8; ++aTry) { const BRepGraph_EdgeId anId(aDist(theRng)); if (!anId.IsRemoved(theGraph)) { theOut = anId; return true; } } return false; }; auto pickActiveVertex = [&](BRepGraph_VertexId& theOut) -> bool { if (aNbVertices <= 0) { return false; } std::uniform_int_distribution aDist(0, aNbVertices - 1); for (int aTry = 0; aTry < 8; ++aTry) { const BRepGraph_VertexId anId(aDist(theRng)); if (!anId.IsRemoved(theGraph)) { theOut = anId; return true; } } return false; }; switch (aKind) { case MutationKind::BumpEdgeTolerance: { BRepGraph_EdgeId anEdgeId; if (!pickActiveEdge(anEdgeId)) { return false; } BRepGraph_MutGuard aMut = theGraph.Editor().Edges().Mut(anEdgeId); theGraph.Editor().Edges().SetTolerance(aMut, aMut->Tolerance + 1.0e-4); return true; } case MutationKind::MutateVertexPoint: { BRepGraph_VertexId aVtxId; if (!pickActiveVertex(aVtxId)) { return false; } BRepGraph_MutGuard aMut = theGraph.Editor().Vertices().Mut(aVtxId); const gp_Pnt aOld = aMut->Point; std::uniform_real_distribution aDist(-0.1, 0.1); theGraph.Editor().Vertices().SetPoint( aMut, gp_Pnt(aOld.X() + aDist(theRng), aOld.Y() + aDist(theRng), aOld.Z() + aDist(theRng))); return true; } case MutationKind::BumpFaceTolerance: { if (aNbFaces <= 0) { return false; } std::uniform_int_distribution aDist(0, aNbFaces - 1); BRepGraph_FaceId aFaceId(aDist(theRng)); if (aFaceId.IsRemoved(theGraph)) { return false; } BRepGraph_MutGuard aMut = theGraph.Editor().Faces().Mut(aFaceId); theGraph.Editor().Faces().SetTolerance(aMut, aMut->Tolerance + 1.0e-4); return true; } default: return false; } } FuzzOutcome runFuzz(BRepGraph& theGraph, const uint32_t theSeed, const int theNbIter) { FuzzOutcome aOut; std::mt19937 aRng(theSeed); for (int aIt = 0; aIt < theNbIter; ++aIt) { if (applyOne(theGraph, aRng)) { ++aOut.NbApplied; const BRepGraph_Validate::Result aResult = BRepGraph_Validate::Perform(theGraph, BRepGraph_Validate::Options::Audit()); ++aOut.NbValidated; EXPECT_TRUE(aResult.IsValid()) << "Fuzz iteration " << aIt << " (seed=" << theSeed << ") left the graph invalid. " << "First issue: " << (aResult.Issues.Size() > 0 ? aResult.Issues.First().Description.ToCString() : "(none)"); } else { ++aOut.NbSkipped; } } return aOut; } } // namespace class BRepGraph_FuzzSeedTest : public testing::TestWithParam { }; TEST_P(BRepGraph_FuzzSeedTest, BoxSeed_RandomMutations_RemainValid) { const uint32_t aSeed = GetParam(); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes1 = aGraph.Shapes().Add(BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape()); ASSERT_FALSE(aGraph.IsEmpty()); ASSERT_TRUE(BRepGraph_Validate::Perform(aGraph, BRepGraph_Validate::Options::Audit()).IsValid()) << "Seed graph must be clean before fuzzing"; const FuzzOutcome aOut = runFuzz(aGraph, aSeed, 50); EXPECT_GE(aOut.NbValidated, 1) << "At least one mutation should land per seed"; SUCCEED() << "seed=" << aSeed << " applied=" << aOut.NbApplied << " skipped=" << aOut.NbSkipped; } TEST_P(BRepGraph_FuzzSeedTest, CylinderSeed_RandomMutations_RemainValid) { const uint32_t aSeed = GetParam(); BRepGraph aGraph; aGraph.Clear(); [[maybe_unused]] const BRepGraph::ShapesView::Result aBuildRes2 = aGraph.Shapes().Add(BRepPrimAPI_MakeCylinder(5.0, 15.0).Shape()); ASSERT_FALSE(aGraph.IsEmpty()); ASSERT_TRUE(BRepGraph_Validate::Perform(aGraph, BRepGraph_Validate::Options::Audit()).IsValid()); const FuzzOutcome aOut = runFuzz(aGraph, aSeed, 40); EXPECT_GE(aOut.NbValidated, 1); SUCCEED() << "seed=" << aSeed << " applied=" << aOut.NbApplied << " skipped=" << aOut.NbSkipped; } INSTANTIATE_TEST_SUITE_P(FixedSeeds, BRepGraph_FuzzSeedTest, testing::Values(1u, 7u, 42u, 137u, 2026u, 0xC0FFEEu, 0xDEADBEEFu));