/
klischa
/
AstraScanner2
Обзор
Документация
Войти
/
klischa
/
AstraScanner2
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
tests/test_project_manager.cpp
449 строк
15 KB
Astra2
fix: harden project state and tests
20 июн 2026, 08:27
20 июн 2026, 08:27
4fd91fd
Код
Авторство
О чём код?
#include <gtest/gtest.h> #include <QCoreApplication> #include <QDir> #include <QFile> #include <QFileInfo> #include <QJsonArray> #include <QJsonDocument> #include <QJsonObject> #include <QDateTime> #include <QTemporaryDir> #include <QThread> #include <pcl/point_cloud.h> #include <pcl/point_types.h> #include "../src/project/ProjectManager.h" using CloudPtr = pcl::PointCloud<pcl::PointXYZRGB>::Ptr; class ProjectManagerTest : public ::testing::Test { protected: void SetUp() override { if (!QCoreApplication::instance()) { static int argc = 1; static char arg0[] = "test_project_manager"; static char* argv[] = { arg0 }; new QCoreApplication(argc, argv); } // Создаем временную директорию для тестов tempDir.reset(new QTemporaryDir()); ASSERT_TRUE(tempDir->isValid()); projectDir = tempDir->path(); } void TearDown() override { tempDir.reset(); } // Создает облако с заданным количеством точек CloudPtr createTestCloud(int numPoints = 100) { CloudPtr cloud(new pcl::PointCloud<pcl::PointXYZRGB>); for (int i = 0; i < numPoints; ++i) { pcl::PointXYZRGB pt; pt.x = 0.1 * (i % 10) * 0.01f; pt.y = 0.1 * (i / 10) * 0.01f; pt.z = 0.01 * (i % 5) * 0.01f; pt.r = 255; pt.g = 255; pt.b = 255; cloud->push_back(pt); } return cloud; } std::unique_ptr<QTemporaryDir> tempDir; QString projectDir; }; // Тест: Создание нового проекта TEST_F(ProjectManagerTest, NewProject) { ProjectManager pm; QString projectPath = projectDir + "/test_project"; bool result = pm.newProject(projectPath, "Test Project"); ASSERT_TRUE(result); EXPECT_TRUE(pm.isOpen()); EXPECT_EQ(pm.projectName(), "Test Project"); EXPECT_EQ(pm.projectDir(), projectPath); EXPECT_EQ(pm.scanCount(), 0); // Проверяем структуру директорий QDir dir(projectPath); EXPECT_TRUE(dir.exists()); EXPECT_TRUE(dir.exists("scans")); EXPECT_TRUE(dir.exists("ai_temp")); EXPECT_TRUE(dir.exists("project.json")); } // Тест: Открытие проекта TEST_F(ProjectManagerTest, OpenProject) { // Сначала создаем проект ProjectManager pm1; QString projectPath = projectDir + "/open_test_project"; pm1.newProject(projectPath, "Open Test"); // Добавляем скан CloudPtr cloud = createTestCloud(50); pm1.addScan(cloud, "Scan 1"); // Сохраняем pm1.saveProject(); // Теперь открываем проект ProjectManager pm2; bool result = pm2.openProject(projectPath); ASSERT_TRUE(result); EXPECT_TRUE(pm2.isOpen()); EXPECT_EQ(pm2.projectName(), "Open Test"); EXPECT_EQ(pm2.scanCount(), 1); // Проверяем, что скан загружен CloudPtr loadedCloud = pm2.scanCloud(0); EXPECT_EQ(loadedCloud->size(), 50); } // Тест: Сохранение проекта TEST_F(ProjectManagerTest, SaveProject) { ProjectManager pm; QString projectPath = projectDir + "/save_test_project"; pm.newProject(projectPath, "Save Test"); // Добавляем сканы CloudPtr cloud1 = createTestCloud(30); CloudPtr cloud2 = createTestCloud(40); int idx1 = pm.addScan(cloud1, "Scan 1"); int idx2 = pm.addScan(cloud2, "Scan 2"); EXPECT_EQ(idx1, 0); EXPECT_EQ(idx2, 1); EXPECT_EQ(pm.scanCount(), 2); // Сохраняем bool result = pm.saveProject(); ASSERT_TRUE(result); EXPECT_FALSE(pm.isDirty()); // Проверяем наличие файлов QDir dir(projectPath + "/scans"); QStringList plyFiles = dir.entryList(QStringList() << "*.ply", QDir::Files); EXPECT_EQ(plyFiles.size(), 2) << "Ожидались 2 PLY файла, найдено: " << plyFiles.size(); // Проверяем, что файлы действительно существуют for (const QString& file : plyFiles) { QFileInfo fileInfo(dir.filePath(file)); EXPECT_TRUE(fileInfo.exists()) << "Файл не найден: " << fileInfo.absoluteFilePath().toStdString(); } EXPECT_TRUE(dir.exists("../project.json")); } // Тест: Добавление и получение сканов TEST_F(ProjectManagerTest, ScanManagement) { ProjectManager pm; QString projectPath = projectDir + "/scan_mgmt_project"; pm.newProject(projectPath, "Scan Management"); // Добавляем сканы CloudPtr cloud1 = createTestCloud(20); CloudPtr cloud2 = createTestCloud(25); CloudPtr cloud3 = createTestCloud(30); int idx1 = pm.addScan(cloud1, "First"); int idx2 = pm.addScan(cloud2, "Second"); int idx3 = pm.addScan(cloud3, "Third"); EXPECT_EQ(pm.scanCount(), 3); EXPECT_EQ(pm.scanCloud(idx1)->size(), 20); EXPECT_EQ(pm.scanCloud(idx2)->size(), 25); EXPECT_EQ(pm.scanCloud(idx3)->size(), 30); // Получаем по индексу CloudPtr retrieved = pm.scanCloud(1); EXPECT_EQ(retrieved->size(), 25); // Проверяем имена EXPECT_EQ(pm.scans()[0].name, "First"); EXPECT_EQ(pm.scans()[1].name, "Second"); EXPECT_EQ(pm.scans()[2].name, "Third"); } // Тест: Удаление скана TEST_F(ProjectManagerTest, RemoveScan) { ProjectManager pm; QString projectPath = projectDir + "/remove_scan_project"; pm.newProject(projectPath, "Remove Scan"); CloudPtr cloud1 = createTestCloud(10); CloudPtr cloud2 = createTestCloud(15); CloudPtr cloud3 = createTestCloud(20); pm.addScan(cloud1, "First"); pm.addScan(cloud2, "Second"); pm.addScan(cloud3, "Third"); EXPECT_EQ(pm.scanCount(), 3); // Удаляем средний скан bool result = pm.removeScan(1); ASSERT_TRUE(result); EXPECT_EQ(pm.scanCount(), 2); // Проверяем, что осталось EXPECT_EQ(pm.scanCloud(0)->size(), 10); EXPECT_EQ(pm.scanCloud(1)->size(), 20); // Проверяем имена EXPECT_EQ(pm.scans()[0].name, "First"); EXPECT_EQ(pm.scans()[1].name, "Third"); } // Тест: Переименование скана TEST_F(ProjectManagerTest, RenameScan) { ProjectManager pm; QString projectPath = projectDir + "/rename_scan_project"; pm.newProject(projectPath, "Rename Scan"); CloudPtr cloud = createTestCloud(10); pm.addScan(cloud, "Original Name"); EXPECT_EQ(pm.scans()[0].name, "Original Name"); bool result = pm.renameScan(0, "New Name"); ASSERT_TRUE(result); EXPECT_EQ(pm.scans()[0].name, "New Name"); } // Тест: Ошибки при работе с невалидными путями TEST_F(ProjectManagerTest, ErrorHandling) { ProjectManager pm; // Попытка открыть несуществующую директорию bool result = pm.openProject("/nonexistent/path/project"); ASSERT_FALSE(result); EXPECT_FALSE(pm.isOpen()); // Попытка сохранить без открытого проекта result = pm.saveProject(); ASSERT_FALSE(result); EXPECT_TRUE(pm.lastError().contains("Проект не открыт")); } // Тест: path traversal в metadata должен отбрасываться при открытии проекта TEST_F(ProjectManagerTest, IgnoresPathTraversalEntriesInMetadata) { ProjectManager pm; QString projectPath = projectDir + "/path_traversal_project"; ASSERT_TRUE(pm.newProject(projectPath, "Traversal Test")); // Создаём валидный скан, чтобы проект имел нормальную структуру. CloudPtr cloud = createTestCloud(10); ASSERT_GE(pm.addScan(cloud, "Valid"), 0); ASSERT_TRUE(pm.saveProject()); pm.closeProject(); // Подменяем metadata и добавляем вредоносную запись вне директории проекта. QFile metaFile(QDir(projectPath).filePath("project.json")); ASSERT_TRUE(metaFile.open(QIODevice::ReadOnly)); QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(metaFile.readAll(), &err); metaFile.close(); ASSERT_EQ(err.error, QJsonParseError::NoError); QJsonObject root = doc.object(); QJsonArray scans = root.value("scans").toArray(); QJsonObject injected; injected["name"] = "Traversal"; injected["file"] = "../outside.ply"; injected["createdAt"] = QDateTime::currentDateTime().toString(Qt::ISODate); injected["pointCount"] = 123; scans.append(injected); root["scans"] = scans; ASSERT_TRUE(metaFile.open(QIODevice::WriteOnly | QIODevice::Truncate)); metaFile.write(QJsonDocument(root).toJson(QJsonDocument::Indented)); metaFile.close(); ProjectManager reopened; ASSERT_TRUE(reopened.openProject(projectPath)); EXPECT_EQ(reopened.scanCount(), 1) << "Traversal entry should be ignored"; EXPECT_EQ(reopened.scans()[0].name, "Valid"); } // Тест: Жизненный цикл проекта (полный цикл) TEST_F(ProjectManagerTest, FullLifecycle) { ProjectManager pm1; QString projectPath = projectDir + "/lifecycle_project"; // 1. Создание bool result = pm1.newProject(projectPath, "Lifecycle"); ASSERT_TRUE(result); EXPECT_TRUE(pm1.isOpen()); EXPECT_EQ(pm1.scanCount(), 0); // 2. Добавление сканов CloudPtr cloud1 = createTestCloud(100); CloudPtr cloud2 = createTestCloud(150); pm1.addScan(cloud1, "Scan A"); pm1.addScan(cloud2, "Scan B"); EXPECT_EQ(pm1.scanCount(), 2); // После addScan проект сохраняется автоматически, поэтому isDirty() == false EXPECT_FALSE(pm1.isDirty()); // 3. Сохранение (дублируется, но проверяет, что сохранение работает) result = pm1.saveProject(); ASSERT_TRUE(result); EXPECT_FALSE(pm1.isDirty()); // 4. Закрытие pm1.closeProject(); EXPECT_FALSE(pm1.isOpen()); // 5. Повторное открытие ProjectManager pm2; result = pm2.openProject(projectPath); ASSERT_TRUE(result); EXPECT_TRUE(pm2.isOpen()); EXPECT_EQ(pm2.projectName(), "Lifecycle"); EXPECT_EQ(pm2.scanCount(), 2); // 6. Проверка данных CloudPtr loaded1 = pm2.scanCloud(0); CloudPtr loaded2 = pm2.scanCloud(1); // Временно: проверяем, что файлы существуют QString scan1Path = QDir(projectPath).filePath("scans"); QDir dir(scan1Path); QStringList files = dir.entryList(QStringList() << "*.ply", QDir::Files); EXPECT_EQ(loaded1->size(), 100) << "Файлов найдено: " << files.size(); EXPECT_EQ(loaded2->size(), 150); EXPECT_EQ(pm2.scans()[0].name, "Scan A"); EXPECT_EQ(pm2.scans()[1].name, "Scan B"); // 7. Закрытие второго менеджера pm2.closeProject(); EXPECT_FALSE(pm2.isOpen()); } // Тест: сериализация режима трекинга и позы сканера TEST_F(ProjectManagerTest, PersistsTrackingModeAndPose) { ProjectManager pm; QString projectPath = projectDir + "/tracking_pose_project"; ASSERT_TRUE(pm.newProject(projectPath, "Tracking Pose")); pm.setTrackingMode(TrackingMode::NeuralBased); Eigen::Affine3f pose = Eigen::Affine3f::Identity(); pose.translation() = Eigen::Vector3f(1.25f, -2.5f, 0.75f); pose.linear() = Eigen::AngleAxisf(0.5f, Eigen::Vector3f::UnitZ()).toRotationMatrix(); pm.setScannerPose(pose); ASSERT_TRUE(pm.saveProject()); ProjectManager reopened; ASSERT_TRUE(reopened.openProject(projectPath)); EXPECT_EQ(reopened.getTrackingMode(), TrackingMode::NeuralBased); Eigen::Affine3f loaded = reopened.getScannerPose(); EXPECT_NEAR((loaded.translation() - pose.translation()).norm(), 0.0f, 1e-5f); EXPECT_NEAR((loaded.linear() - pose.linear()).norm(), 0.0f, 1e-5f); } // Тест: SaveProjectAs должен копировать сканы и метаданные в новую директорию TEST_F(ProjectManagerTest, SaveProjectAsCopiesScans) { ProjectManager pm; QString srcProjectPath = projectDir + "/save_as_src"; QString dstProjectPath = projectDir + "/save_as_dst"; ASSERT_TRUE(pm.newProject(srcProjectPath, "SaveAs Source")); CloudPtr cloud = createTestCloud(42); ASSERT_GE(pm.addScan(cloud, "Original Scan"), 0); ASSERT_TRUE(pm.saveProjectAs(dstProjectPath)); EXPECT_EQ(pm.projectDir(), dstProjectPath); ProjectManager reopened; ASSERT_TRUE(reopened.openProject(dstProjectPath)); ASSERT_EQ(reopened.scanCount(), 1); EXPECT_EQ(reopened.scans()[0].name, "Original Scan"); EXPECT_EQ(reopened.scanCloud(0)->size(), 42); } // Тест: SaveProjectAs должен работать после повторного открытия проекта, // когда облака есть только на диске и ещё не загружены в память. TEST_F(ProjectManagerTest, SaveProjectAsCopiesUnloadedScansAfterReopen) { const QString srcProjectPath = projectDir + "/save_as_unloaded_src"; const QString dstProjectPath = projectDir + "/save_as_unloaded_dst"; { ProjectManager creator; ASSERT_TRUE(creator.newProject(srcProjectPath, "SaveAs Unloaded Source")); ASSERT_GE(creator.addScan(createTestCloud(37), "Disk Only Scan"), 0); ASSERT_TRUE(creator.saveProject()); } ProjectManager reopenedSource; ASSERT_TRUE(reopenedSource.openProject(srcProjectPath)); ASSERT_EQ(reopenedSource.scanCount(), 1); // Не вызываем scanCloud(0): ScanItem::cloud остаётся null. ASSERT_TRUE(reopenedSource.saveProjectAs(dstProjectPath)) << reopenedSource.lastError().toStdString(); EXPECT_EQ(reopenedSource.projectDir(), dstProjectPath); ProjectManager reopenedDst; ASSERT_TRUE(reopenedDst.openProject(dstProjectPath)); ASSERT_EQ(reopenedDst.scanCount(), 1); EXPECT_EQ(reopenedDst.scans()[0].name, "Disk Only Scan"); auto copied = reopenedDst.scanCloud(0); ASSERT_TRUE(copied != nullptr); EXPECT_EQ(copied->size(), 37); } // Тест: PCL облако без цвета (PointNormal) TEST_F(ProjectManagerTest, PointCloudWithoutColor) { ProjectManager pm; QString projectPath = projectDir + "/no_color_project"; pm.newProject(projectPath, "No Color"); // Создаем облако PointXYZ (без цвета) pcl::PointCloud<pcl::PointXYZ>::Ptr cloudXYZ(new pcl::PointCloud<pcl::PointXYZ>); for (int i = 0; i < 10; ++i) { pcl::PointXYZ pt; pt.x = 0.1 * i; pt.y = 0.1 * i; pt.z = 0.01 * i; cloudXYZ->push_back(pt); } // Конвертируем в PointXYZRGB pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloudRGB(new pcl::PointCloud<pcl::PointXYZRGB>); for (const auto& pt : cloudXYZ->points) { pcl::PointXYZRGB rgbPt; rgbPt.x = pt.x; rgbPt.y = pt.y; rgbPt.z = pt.z; rgbPt.r = 255; rgbPt.g = 255; rgbPt.b = 255; cloudRGB->push_back(rgbPt); } int idx = pm.addScan(cloudRGB, "XYZRGB Scan"); EXPECT_EQ(idx, 0); EXPECT_EQ(pm.scanCloud(0)->size(), 10); }