8afad88241
Базовый класс IntegrationTestBase (IAsyncLifetime) закрывает все открытые документы КОМПАС после КАЖДОГО теста (DocumentService.CloseAllAsync) — чтобы не копились вкладки-пустышки и упавший Assert не оставлял открытый документ. Все интеграционные тест-классы переведены на него (: base(fx)); индивидуальные finally CloseAsync сохранены как первичная очистка. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
250 lines
11 KiB
C#
250 lines
11 KiB
C#
using Kompas.Mcp.Core.Documents;
|
||
using Kompas.Mcp.Core.Modeling;
|
||
using Kompas.Mcp.Core.Query;
|
||
using Kompas.Mcp.Core.Vision;
|
||
|
||
namespace Kompas.Mcp.Tests.Integration;
|
||
|
||
/// <summary>Интеграция: построение 3D (эскиз → выдавливание) и снимок.</summary>
|
||
[Trait("Category", "Integration")]
|
||
[Collection(KompasCollection.Name)]
|
||
public sealed class ModelingTests : IntegrationTestBase
|
||
{
|
||
private readonly DocumentService _docs;
|
||
private readonly PartModeler _modeler;
|
||
private readonly SnapshotService _snap;
|
||
private readonly QueryService _query;
|
||
|
||
public ModelingTests(KompasFixture fx) : base(fx)
|
||
{
|
||
_docs = new DocumentService(fx.Session, fx.Dispatcher);
|
||
_modeler = new PartModeler(fx.Session, fx.Dispatcher);
|
||
_snap = new SnapshotService(fx.Session, fx.Dispatcher);
|
||
_query = new QueryService(fx.Session, fx.Dispatcher);
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Build_cylinder_then_cut_hole_and_snapshot()
|
||
{
|
||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||
try
|
||
{
|
||
// Эскиз 1: окружность R20 на XOY → выдавить boss на 40 (цилиндр).
|
||
var s1 = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||
await _modeler.AddCircleAsync(s1, 0, 0, 20);
|
||
await _modeler.CloseSketchAsync(s1);
|
||
var boss = await _modeler.ExtrudeAsync(s1, depth: 40);
|
||
Assert.True(boss > 0);
|
||
|
||
// Эскиз 2: окружность R8 на XOY → вырезать насквозь (отверстие).
|
||
var s2 = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||
await _modeler.AddCircleAsync(s2, 0, 0, 8);
|
||
await _modeler.CloseSketchAsync(s2);
|
||
var cut = await _modeler.ExtrudeAsync(s2, depth: 40, cut: true, throughAll: true);
|
||
Assert.True(cut > 0);
|
||
|
||
await _modeler.RebuildAsync();
|
||
|
||
var path = TestPaths.NewFile(".png");
|
||
var snap = await _snap.CaptureActiveModelAsync(saveToFile: path);
|
||
Assert.True(snap.Bytes.Length > 1000, "Снимок цилиндра подозрительно мал.");
|
||
Assert.True(File.Exists(path));
|
||
}
|
||
finally
|
||
{
|
||
await _docs.CloseAsync(save: false);
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Sketch_on_face_adds_boss_on_top()
|
||
{
|
||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||
try
|
||
{
|
||
// Цилиндр R10×H20 на XOY → верхняя грань на Z=20.
|
||
var s1 = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||
await _modeler.AddCircleAsync(s1, 0, 0, 10);
|
||
await _modeler.CloseSketchAsync(s1);
|
||
await _modeler.ExtrudeAsync(s1, depth: 20);
|
||
|
||
// Запрос ДО достройки — проверяем, что повторный запрос увидит изменения
|
||
// (МЦХ не должна «залипнуть» на устаревшем значении).
|
||
var before = await _query.GetPartInfoAsync();
|
||
|
||
// Эскиз на верхней грани (точка (0,0,20)) → штифт R5×H10 наружу.
|
||
var s2 = await _modeler.OpenSketchOnFaceAsync(0, 0, 20);
|
||
await _modeler.AddCircleAsync(s2, 0, 0, 5);
|
||
await _modeler.CloseSketchAsync(s2);
|
||
var boss = await _modeler.ExtrudeAsync(s2, depth: 10);
|
||
Assert.True(boss > 0);
|
||
await _modeler.RebuildAsync();
|
||
|
||
var after = await _query.GetPartInfoAsync();
|
||
|
||
// Прирост объёма ≈ объём штифта π·5²·10 ≈ 785.4 мм³.
|
||
var deltaExpected = Math.PI * 25 * 10;
|
||
Assert.InRange(after.Volume - before.Volume, deltaExpected * 0.9, deltaExpected * 1.1);
|
||
}
|
||
finally
|
||
{
|
||
await _docs.CloseAsync(save: false);
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Revolve_rectangle_makes_tube()
|
||
{
|
||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||
try
|
||
{
|
||
// Профиль-прямоугольник [10..15]×[0..20] + осевая по локальной оси Y (x=0).
|
||
// Вращение 360° вокруг оси → труба: внеш. R15, внутр. R10, высота 20.
|
||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||
await _modeler.AddAxisAsync(s, 0, 0, 0, 20);
|
||
await _modeler.AddRectangleAsync(s, 10, 0, 15, 20);
|
||
await _modeler.CloseSketchAsync(s);
|
||
var rev = await _modeler.RevolveAsync(s, angle: 360);
|
||
Assert.True(rev > 0);
|
||
await _modeler.RebuildAsync();
|
||
|
||
var info = await _query.GetPartInfoAsync();
|
||
|
||
// Объём трубы = π·(15² − 10²)·20 = π·2500 ≈ 7853.98 мм³.
|
||
var expected = Math.PI * (15 * 15 - 10 * 10) * 20;
|
||
Assert.InRange(info.Volume, expected * 0.97, expected * 1.03);
|
||
}
|
||
finally
|
||
{
|
||
await _docs.CloseAsync(save: false);
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Fillet_and_chamfer_edges()
|
||
{
|
||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||
try
|
||
{
|
||
// Цилиндр R10×H20 → верхнее ребро на (10,0,20), нижнее на (10,0,0).
|
||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||
await _modeler.AddCircleAsync(s, 0, 0, 10);
|
||
await _modeler.CloseSketchAsync(s);
|
||
await _modeler.ExtrudeAsync(s, depth: 20);
|
||
var cylinderVolume = (await _query.GetPartInfoAsync()).Volume;
|
||
|
||
// Скругление верхнего ребра R3 и фаска нижнего 2×2.
|
||
var fil = await _modeler.FilletEdgeAsync(10, 0, 20, 3);
|
||
Assert.True(fil > 0);
|
||
var cha = await _modeler.ChamferEdgeAsync(10, 0, 0, 2);
|
||
Assert.True(cha > 0);
|
||
await _modeler.RebuildAsync();
|
||
|
||
var path = TestPaths.NewFile("_fillet.png");
|
||
var snap = await _snap.CaptureActiveModelAsync(saveToFile: path);
|
||
Assert.True(snap.Bytes.Length > 1000);
|
||
|
||
// Скругление и фаска снимают материал с рёбер → объём уменьшается, но незначительно.
|
||
var after = (await _query.GetPartInfoAsync()).Volume;
|
||
Assert.True(after < cylinderVolume, "После скругления/фаски объём должен уменьшиться.");
|
||
Assert.InRange(after, cylinderVolume * 0.9, cylinderVolume);
|
||
}
|
||
finally
|
||
{
|
||
await _docs.CloseAsync(save: false);
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public async Task List_faces_and_sketch_by_index()
|
||
{
|
||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||
try
|
||
{
|
||
// Цилиндр R10×H20: 3 грани — 2 плоские (торцы) + 1 цилиндрическая (бок).
|
||
var s1 = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||
await _modeler.AddCircleAsync(s1, 0, 0, 10);
|
||
await _modeler.CloseSketchAsync(s1);
|
||
await _modeler.ExtrudeAsync(s1, depth: 20);
|
||
|
||
var faces = await _query.ListFacesAsync();
|
||
Assert.Equal(3, faces.Count);
|
||
Assert.Equal(2, faces.Count(f => f.Type == "plane"));
|
||
Assert.Equal(1, faces.Count(f => f.Type == "cylinder"));
|
||
|
||
// Площадь торца ≈ π·10² ≈ 314 мм²; боковой ≈ 2π·10·20 ≈ 1257 мм².
|
||
var planar = faces.First(f => f.Type == "plane");
|
||
Assert.InRange(planar.Area, Math.PI * 100 * 0.95, Math.PI * 100 * 1.05);
|
||
var side = faces.First(f => f.Type == "cylinder");
|
||
Assert.InRange(side.Area, 2 * Math.PI * 10 * 20 * 0.95, 2 * Math.PI * 10 * 20 * 1.05);
|
||
|
||
// Эскиз на плоской грани по индексу → бобышка (проверяем, что выбор по индексу работает).
|
||
var s2 = await _modeler.OpenSketchOnFaceIndexAsync(planar.Index);
|
||
await _modeler.AddCircleAsync(s2, 0, 0, 4);
|
||
await _modeler.CloseSketchAsync(s2);
|
||
var boss = await _modeler.ExtrudeAsync(s2, depth: 8, forward: false);
|
||
Assert.True(boss > 0);
|
||
await _modeler.RebuildAsync();
|
||
|
||
// Модель осталась валидной и опрашиваемой.
|
||
Assert.True((await _query.GetPartInfoAsync()).Volume > 0);
|
||
}
|
||
finally
|
||
{
|
||
await _docs.CloseAsync(save: false);
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public async Task List_edges_and_fillet_by_index()
|
||
{
|
||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||
try
|
||
{
|
||
// Цилиндр R10×H20: торцевые рёбра — две окружности длиной 2π·10 ≈ 62.83 мм.
|
||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||
await _modeler.AddCircleAsync(s, 0, 0, 10);
|
||
await _modeler.CloseSketchAsync(s);
|
||
await _modeler.ExtrudeAsync(s, depth: 20);
|
||
|
||
var edges = await _query.ListEdgesAsync();
|
||
var circles = edges.Where(e => e.Type == "circle").ToList();
|
||
Assert.True(circles.Count >= 2, "У цилиндра ожидались минимум два круговых ребра (торцы).");
|
||
foreach (var c in circles)
|
||
Assert.InRange(c.Length, 2 * Math.PI * 10 * 0.95, 2 * Math.PI * 10 * 1.05);
|
||
|
||
// Скругление кругового ребра по индексу → объём уменьшается.
|
||
var before = (await _query.GetPartInfoAsync()).Volume;
|
||
var fil = await _modeler.FilletEdgeIndexAsync(circles[0].Index, 3);
|
||
Assert.True(fil > 0);
|
||
await _modeler.RebuildAsync();
|
||
|
||
var after = (await _query.GetPartInfoAsync()).Volume;
|
||
Assert.True(after < before, "После скругления ребра объём должен уменьшиться.");
|
||
Assert.InRange(after, before * 0.9, before);
|
||
}
|
||
finally
|
||
{
|
||
await _docs.CloseAsync(save: false);
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public async Task Reset_invalidates_sketch_ids()
|
||
{
|
||
await _docs.CreateAsync(KompasDocumentType.Part);
|
||
try
|
||
{
|
||
var s = await _modeler.OpenSketchAsync(BasePlane.XOY);
|
||
await _modeler.ResetAsync();
|
||
// После сброса реестра старый id недействителен.
|
||
await Assert.ThrowsAsync<KeyNotFoundException>(
|
||
() => _modeler.AddCircleAsync(s, 0, 0, 5));
|
||
}
|
||
finally
|
||
{
|
||
await _docs.CloseAsync(save: false);
|
||
}
|
||
}
|
||
}
|