PEBL 2.2
Psychology Experiment Building Language - Cross-platform psychological experiment development system
SnapshotManager Class Reference

#include <SnapshotManager.h>

Classes

struct  SnapshotInfo
 
struct  ValidationResult
 

Public Member Functions

 SnapshotManager ()
 
 ~SnapshotManager ()
 
std::string CreateSnapshot (const std::string &studyPath, const std::string &snapshotsDir)
 
ValidationResult ValidateSnapshot (const std::string &snapshotPath)
 
bool ImportSnapshot (const std::string &snapshotPath, const std::string &studiesDir, const std::string &newStudyName)
 
SnapshotInfo GetSnapshotInfo (const std::string &snapshotPath)
 
bool ConvertSnapshotFormat (const std::string &studyPath)
 

Static Public Member Functions

static std::string GenerateSnapshotName (const std::string &studyName, int version)
 

Detailed Description

Definition at line 15 of file SnapshotManager.h.

Constructor & Destructor Documentation

◆ SnapshotManager()

SnapshotManager::SnapshotManager ( )

Definition at line 43 of file SnapshotManager.cpp.

43 {
44}

◆ ~SnapshotManager()

SnapshotManager::~SnapshotManager ( )

Definition at line 46 of file SnapshotManager.cpp.

46 {
47}

Member Function Documentation

◆ ConvertSnapshotFormat()

bool SnapshotManager::ConvertSnapshotFormat ( const std::string &  studyPath)

Convert PEBLOnlinePlatform snapshot format to launcher format Platform format has:

  • version: string (e.g., "Version 3")
  • test_id instead of test_name in tests array
  • test_name as display name Launcher format needs:
  • version: integer
  • test_name as identifier
  • display_name as readable name

Definition at line 321 of file SnapshotManager.cpp.

321 {
333 try {
334 std::string jsonPath = studyPath + "/study-info.json";
335
336 // Read existing study-info.json
337 std::ifstream file(jsonPath);
338 if (!file.is_open()) {
339 return false;
340 }
341
342 json platformJson;
343 file >> platformJson;
344 file.close();
345
346 // Create launcher-compatible JSON
347 json launcherJson;
348
349 // Convert metadata
350 launcherJson["study_name"] = platformJson.value("study_name", "Imported Study");
351 launcherJson["description"] = platformJson.value("description", "");
352 launcherJson["author"] = platformJson.value("created_by", "");
353
354 // Convert version from string to integer
355 std::string versionStr = platformJson.value("version", "1");
356 int versionInt = 1;
357 // Try to extract number from strings like "Version 3"
358 size_t lastSpace = versionStr.rfind(' ');
359 if (lastSpace != std::string::npos) {
360 std::string numPart = versionStr.substr(lastSpace + 1);
361 versionInt = std::atoi(numPart.c_str());
362 if (versionInt == 0) versionInt = 1;
363 }
364 launcherJson["version"] = versionInt;
365
366 // Convert tests array
367 json testsArray = json::array();
368 if (platformJson.contains("tests") && platformJson["tests"].is_array()) {
369 for (const auto& platformTest : platformJson["tests"]) {
370 json launcherTest;
371
372 // In platform format: test_id is the identifier, test_name is display name
373 std::string testId = platformTest.value("test_id", "");
374 std::string testDisplayName = platformTest.value("test_name", testId);
375
376 launcherTest["test_name"] = testId; // Launcher uses test_name as identifier
377 launcherTest["display_name"] = testDisplayName;
378 launcherTest["test_path"] = testId; // Path is same as identifier
379 launcherTest["included"] = true;
380
381 // Build parameter_variants by scanning params/ directory for .par.json files
382 json paramVariants = json::object();
383 std::string paramsDir = studyPath + "/tests/" + testId + "/params";
384 if (DirectoryExists(paramsDir)) {
385 // Always add a "default" variant
386 json defaultVariant;
387 defaultVariant["description"] = "Default parameters";
388 defaultVariant["file"] = nullptr;
389 paramVariants["default"] = defaultVariant;
390
391 // Scan for .par.json files
392 for (const auto& entry : fs::directory_iterator(paramsDir)) {
393 if (entry.is_regular_file()) {
394 std::string filename = entry.path().filename().string();
395 // Look for .par.json files
396 if (filename.size() > 9 && filename.substr(filename.size() - 9) == ".par.json") {
397 // Extract variant name (remove .par.json extension)
398 std::string variantName = filename.substr(0, filename.size() - 9);
399 json variant;
400 variant["description"] = variantName;
401 variant["file"] = filename;
402 paramVariants[variantName] = variant;
403 printf("Found parameter variant: %s -> %s\n", variantName.c_str(), filename.c_str());
404 }
405 }
406 }
407 }
408 launcherTest["parameter_variants"] = paramVariants;
409
410 testsArray.push_back(launcherTest);
411 }
412 }
413 launcherJson["tests"] = testsArray;
414
415 // Upload config must be set intentionally via Study Settings;
416 // do not auto-populate from upload.json files in test directories
417 launcherJson["study_token"] = "";
418 launcherJson["upload_server_url"] = "";
419 launcherJson["created_date"] = platformJson.value("created_at", "");
420 launcherJson["modified_date"] = platformJson.value("created_at", "");
421
422 // Write converted JSON back to file
423 std::ofstream outFile(jsonPath);
424 if (!outFile.is_open()) {
425 return false;
426 }
427
428 outFile << launcherJson.dump(2); // 2-space indentation
429 outFile.close();
430
431 printf("Converted snapshot format to launcher format\n");
432 return true;
433
434 } catch (const std::exception& e) {
435 printf("Error converting snapshot format: %s\n", e.what());
436 return false;
437 }
438}
nlohmann::json json
Definition Chain.cpp:14

◆ CreateSnapshot()

std::string SnapshotManager::CreateSnapshot ( const std::string &  studyPath,
const std::string &  snapshotsDir 
)

Definition at line 49 of file SnapshotManager.cpp.

50 {
51 // Load study to get metadata
52 auto study = Study::LoadFromDirectory(studyPath);
53 if (!study) {
54 return "";
55 }
56
57 // Generate snapshot name: studyname_vN_YYYY-MM-DD
58 std::string snapshotName = GenerateSnapshotName(study->GetName(), study->GetVersion());
59 std::string snapshotPath = snapshotsDir + "/" + snapshotName;
60
61 // Check if snapshot already exists
62 if (DirectoryExists(snapshotPath)) {
63 // Append timestamp to make unique
64 auto now = std::time(nullptr);
65 std::ostringstream oss;
66 oss << snapshotPath << "_" << now;
67 snapshotPath = oss.str();
68 }
69
70 // Copy study directory, excluding data/ directories
71 if (!CopyDirectory(studyPath, snapshotPath, true)) {
72 return "";
73 }
74
75 return snapshotName;
76}
static std::string GenerateSnapshotName(const std::string &studyName, int version)
static std::shared_ptr< Study > LoadFromDirectory(const std::string &path)
Definition Study.cpp:100

References GenerateSnapshotName(), and Study::LoadFromDirectory().

◆ GenerateSnapshotName()

std::string SnapshotManager::GenerateSnapshotName ( const std::string &  studyName,
int  version 
)
static

Definition at line 166 of file SnapshotManager.cpp.

166 {
167 // Replace spaces with hyphens, convert to lowercase
168 std::string safeName = studyName;
169 for (char& c : safeName) {
170 if (c == ' ') c = '-';
171 else c = std::tolower(c);
172 }
173
174 // Get current date
175 auto now = std::time(nullptr);
176 auto tm = *std::localtime(&now);
177
178 std::ostringstream oss;
179 oss << safeName << "_v" << version << "_"
180 << std::put_time(&tm, "%Y-%m-%d");
181
182 return oss.str();
183}

Referenced by CreateSnapshot().

◆ GetSnapshotInfo()

SnapshotManager::SnapshotInfo SnapshotManager::GetSnapshotInfo ( const std::string &  snapshotPath)

Definition at line 185 of file SnapshotManager.cpp.

185 {
186 SnapshotInfo info;
187 info.version = 0;
188 info.testCount = 0;
189 info.chainCount = 0;
190
191 auto study = Study::LoadFromDirectory(snapshotPath);
192 if (!study) {
193 return info;
194 }
195
196 info.studyName = study->GetName();
197 info.version = study->GetVersion();
198 info.description = study->GetDescription();
199 info.author = study->GetAuthor();
200 info.createdDate = study->GetCreatedDate();
201 info.testCount = static_cast<int>(study->GetTests().size());
202 info.chainCount = study->GetChainCount();
203
204 return info;
205}

References SnapshotManager::SnapshotInfo::author, SnapshotManager::SnapshotInfo::chainCount, SnapshotManager::SnapshotInfo::createdDate, SnapshotManager::SnapshotInfo::description, Study::LoadFromDirectory(), SnapshotManager::SnapshotInfo::studyName, SnapshotManager::SnapshotInfo::testCount, and SnapshotManager::SnapshotInfo::version.

◆ ImportSnapshot()

bool SnapshotManager::ImportSnapshot ( const std::string &  snapshotPath,
const std::string &  studiesDir,
const std::string &  newStudyName 
)

Definition at line 140 of file SnapshotManager.cpp.

142 {
143 std::string studyPath = studiesDir + "/" + newStudyName;
144
145 // Check if study already exists
146 if (DirectoryExists(studyPath)) {
147 return false;
148 }
149
150 // Copy snapshot to studies directory
151 // Note: For ZIP imports, format conversion happens before this is called
152 // For directory imports, we need to convert after copying
153 if (!CopyDirectory(snapshotPath, studyPath, false)) {
154 return false;
155 }
156
157 // Create data/ directory (snapshots don't have it)
158 std::string dataDir = studyPath + "/data";
159 if (!DirectoryExists(dataDir)) {
160 mkdir(dataDir.c_str(), 0755);
161 }
162
163 return true;
164}

◆ ValidateSnapshot()

SnapshotManager::ValidationResult SnapshotManager::ValidateSnapshot ( const std::string &  snapshotPath)

Definition at line 78 of file SnapshotManager.cpp.

78 {
79 ValidationResult result;
80 result.isValid = true;
81
82 // Check if directory exists
83 if (!DirectoryExists(snapshotPath)) {
84 result.isValid = false;
85 result.errors.push_back("Snapshot directory does not exist");
86 return result;
87 }
88
89 // Check for study-info.json
90 std::string studyInfoPath = snapshotPath + "/study-info.json";
91 if (!FileExists(studyInfoPath)) {
92 result.isValid = false;
93 result.errors.push_back("study-info.json not found");
94 return result;
95 }
96
97 // Try to load study
98 auto study = Study::LoadFromDirectory(snapshotPath);
99 if (!study) {
100 result.isValid = false;
101 result.errors.push_back("Failed to parse study-info.json");
102 return result;
103 }
104
105 // Validate study structure
106 auto studyValidation = study->Validate();
107 result.errors.insert(result.errors.end(), studyValidation.errors.begin(), studyValidation.errors.end());
108 result.warnings.insert(result.warnings.end(), studyValidation.warnings.begin(), studyValidation.warnings.end());
109
110 // Check for chains/ directory
111 std::string chainsDir = snapshotPath + "/chains";
112 if (!DirectoryExists(chainsDir)) {
113 result.warnings.push_back("chains/ directory not found");
114 }
115
116 // Check for tests/ directory
117 std::string testsDir = snapshotPath + "/tests";
118 if (!DirectoryExists(testsDir)) {
119 result.warnings.push_back("tests/ directory not found");
120 }
121
122 // Verify no data/ directories (snapshots shouldn't have data)
123 std::string dataDir = snapshotPath + "/data";
124 if (DirectoryExists(dataDir)) {
125 result.warnings.push_back("Snapshot contains data/ directory (should be excluded)");
126 }
127
128 // Check each test for data/ subdirectories
129 for (const auto& test : study->GetTests()) {
130 std::string testDataDir = snapshotPath + "/tests/" + test.testPath + "/data";
131 if (DirectoryExists(testDataDir)) {
132 result.warnings.push_back("Test " + test.testName + " contains data/ directory");
133 }
134 }
135
136 result.isValid = result.errors.empty();
137 return result;
138}

References SnapshotManager::ValidationResult::errors, SnapshotManager::ValidationResult::isValid, Study::LoadFromDirectory(), and SnapshotManager::ValidationResult::warnings.


The documentation for this class was generated from the following files: