• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

paulmthompson / WhiskerToolbox / 18840223125

27 Oct 2025 12:01PM UTC coverage: 73.058% (+0.2%) from 72.822%
18840223125

push

github

paulmthompson
fix failing tests from table designer redesign

69 of 74 new or added lines in 2 files covered. (93.24%)

669 existing lines in 10 files now uncovered.

56029 of 76691 relevant lines covered (73.06%)

45039.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

59.73
/src/DataManager/transforms/TransformPipeline.cpp
1
#include "TransformPipeline.hpp"
2

3
#include "DataManager.hpp"
4
#include "ParameterFactory.hpp"
5
#include "TransformRegistry.hpp"
6
#include "transforms/Lines/Line_Proximity_Grouping/line_proximity_grouping.hpp"
7
#include "transforms/grouping_transforms.hpp"
8

9
#include <algorithm>
10
#include <chrono>
11
#include <fstream>
12
#include <iostream>
13
#include <thread>
14
#include <future>
15
#include <type_traits>
16

17
TransformPipeline::TransformPipeline(DataManager* data_manager, TransformRegistry* registry)
79✔
18
    : data_manager_(data_manager), registry_(registry) {
79✔
19
    if (!data_manager_) {
79✔
20
        throw std::invalid_argument("DataManager cannot be null");
×
21
    }
22
    if (!registry_) {
79✔
23
        throw std::invalid_argument("TransformRegistry cannot be null");
×
24
    }
25
    auto& factory = ParameterFactory::getInstance();
79✔
26
    factory.initializeDefaultSetters();
79✔
27

28
}
79✔
29

30
bool TransformPipeline::loadFromJson(nlohmann::json const& json_config) {
79✔
31
    try {
32
        clear();
79✔
33
        
34
        // Load metadata
35
        if (json_config.contains("metadata")) {
79✔
36
            metadata_ = json_config["metadata"];
60✔
37
        }
38
        
39
        // Extract variables from metadata for substitution
40
        auto variables = extractVariables();
79✔
41
        
42
        // Create a mutable copy of the config for variable substitution
43
        nlohmann::json processed_config = json_config;
79✔
44
        
45
        // Perform variable substitution on the entire config
46
        // (but skip the metadata.variables section itself to avoid circular substitution)
47
        if (processed_config.contains("steps")) {
79✔
48
            substituteVariables(processed_config["steps"], variables);
79✔
49
        }
50
        
51
        // Load steps
52
        if (!processed_config.contains("steps") || !processed_config["steps"].is_array()) {
79✔
UNCOV
53
            std::cerr << "Pipeline JSON must contain a 'steps' array" << std::endl;
×
UNCOV
54
            return false;
×
55
        }
56
        
57
        auto const& steps_json = processed_config["steps"];
79✔
58
        steps_.reserve(steps_json.size());
79✔
59
        
60
        for (size_t i = 0; i < steps_json.size(); ++i) {
162✔
61
            auto [success, step] = parseStep(steps_json[i], static_cast<int>(i));
84✔
62
            if (!success) {
84✔
63
                std::cerr << "Failed to parse step " << i << std::endl;
1✔
64
                return false;
1✔
65
            }
66
            steps_.push_back(std::move(step));
83✔
67
        }
84✔
68
        
69
        // Validate the loaded pipeline
70
        auto validation_errors = validate();
78✔
71
        if (!validation_errors.empty()) {
78✔
UNCOV
72
            std::cerr << "Pipeline validation failed:" << std::endl;
×
UNCOV
73
            for (auto const& error : validation_errors) {
×
74
                std::cerr << "  - " << error << std::endl;
×
75
            }
76
            return false;
×
77
        }
78
        
79
        return true;
78✔
80
    } catch (std::exception const& e) {
79✔
UNCOV
81
        std::cerr << "Error loading pipeline from JSON: " << e.what() << std::endl;
×
82
        return false;
×
83
    }
×
84
}
85

86
bool TransformPipeline::loadFromJsonFile(std::string const& json_file_path) {
×
87
    try {
88
        std::ifstream file(json_file_path);
×
UNCOV
89
        if (!file.is_open()) {
×
UNCOV
90
            std::cerr << "Cannot open pipeline file: " << json_file_path << std::endl;
×
UNCOV
91
            return false;
×
92
        }
93
        
UNCOV
94
        nlohmann::json json_config;
×
UNCOV
95
        file >> json_config;
×
UNCOV
96
        return loadFromJson(json_config);
×
UNCOV
97
    } catch (std::exception const& e) {
×
UNCOV
98
        std::cerr << "Error reading pipeline file '" << json_file_path << "': " << e.what() << std::endl;
×
UNCOV
99
        return false;
×
UNCOV
100
    }
×
101
}
102

103
PipelineResult TransformPipeline::execute(PipelineProgressCallback progress_callback) {
78✔
104
    auto start_time = std::chrono::high_resolution_clock::now();
78✔
105
    
106
    PipelineResult result;
78✔
107
    result.total_steps = static_cast<int>(steps_.size());
78✔
108
    result.step_results.reserve(steps_.size());
78✔
109
    
110
    try {
111
        // Clear temporary data from previous executions
112
        temporary_data_.clear();
78✔
113
        
114
        // Group steps by phase
115
        auto phase_groups = groupStepsByPhase();
78✔
116
        
117
        int completed_steps = 0;
78✔
118
        
119
        // Execute each phase
120
        for (auto const& [phase_number, step_indices] : phase_groups) {
157✔
121
            if (progress_callback) {
79✔
122
                progress_callback(-1, "Starting phase " + std::to_string(phase_number), 0, 
60✔
123
                                (completed_steps * 100) / result.total_steps);
60✔
124
            }
125
            
126
            auto phase_results = executePhase(step_indices, phase_number, progress_callback);
79✔
127
            
128
            // Check if any step in this phase failed
129
            bool phase_failed = false;
79✔
130
            for (auto const& step_result : phase_results) {
162✔
131
                result.step_results.push_back(step_result);
83✔
132
                if (!step_result.success) {
83✔
UNCOV
133
                    phase_failed = true;
×
UNCOV
134
                    result.error_message = "Step failed: " + step_result.error_message;
×
UNCOV
135
                    break;
×
136
                }
137
                completed_steps++;
83✔
138
            }
139
            
140
            if (phase_failed) {
79✔
UNCOV
141
                result.success = false;
×
UNCOV
142
                result.steps_completed = completed_steps;
×
UNCOV
143
                break;
×
144
            }
145
        }
79✔
146
        
147
        if (result.step_results.size() == steps_.size()) {
78✔
148
            result.success = true;
78✔
149
            result.steps_completed = result.total_steps;
78✔
150
            
151
            if (progress_callback) {
78✔
152
                progress_callback(-1, "Pipeline completed", 100, 100);
180✔
153
            }
154
        }
155
        
156
    } catch (std::exception const& e) {
78✔
UNCOV
157
        result.success = false;
×
UNCOV
158
        result.error_message = "Pipeline execution error: " + std::string(e.what());
×
UNCOV
159
    }
×
160
    
161
    auto end_time = std::chrono::high_resolution_clock::now();
78✔
162
    result.total_execution_time_ms = std::chrono::duration<double, std::milli>(end_time - start_time).count();
78✔
163
    
164
    return result;
156✔
165
}
×
166

167
StepResult TransformPipeline::executeStep(PipelineStep const& step, ProgressCallback progress_callback) {
83✔
168
    auto start_time = std::chrono::high_resolution_clock::now();
83✔
169
    
170
    StepResult result;
83✔
171
    result.output_key = step.output_key;
83✔
172
    
173
    try {
174
        // Check if step is enabled
175
        if (!step.enabled) {
83✔
UNCOV
176
            result.success = true; // Disabled steps are considered successful
×
UNCOV
177
            return result;
×
178
        }
179
        
180
        // Get the transform operation
181
        auto* operation = registry_->findOperationByName(step.transform_name);
83✔
182
        if (!operation) {
83✔
UNCOV
183
            result.error_message = "Transform '" + step.transform_name + "' not found in registry";
×
184
            return result;
×
185
        }
186
        
187
        // Get input data
188
        auto [input_success, input_data] = getInputData(step.input_key);
83✔
189
        if (!input_success) {
83✔
UNCOV
190
            result.error_message = "Failed to get input data for key '" + step.input_key + "'";
×
UNCOV
191
            return result;
×
192
        }
193
        
194
        // Check if operation can apply to input data
195
        if (!operation->canApply(input_data)) {
83✔
UNCOV
196
            result.error_message = "Transform '" + step.transform_name + "' cannot be applied to input data";
×
UNCOV
197
            return result;
×
198
        }
199
        
200
        // Create parameters
201
        auto parameters = createParametersFromJson(step.transform_name, step.parameters);
83✔
202
        
203
        // Execute the transform
204
        DataTypeVariant output_data;
83✔
205
        if (progress_callback) {
83✔
206
            output_data = operation->execute(input_data, parameters.get(), progress_callback);
64✔
207
        } else {
208
            output_data = operation->execute(input_data, parameters.get());
19✔
209
        }
210
        
211
        // Check if execution was successful (assuming empty variant means failure)
212
        if (std::visit([](auto const& ptr) { return ptr == nullptr; }, output_data)) {
166✔
213
            result.error_message = "Transform execution returned null result";
×
UNCOV
214
            return result;
×
215
        }
216
        
217
        // Store output data
218
        auto time_key = data_manager_->getTimeKey(step.input_key);
83✔
219
        storeOutputData(step.output_key, output_data, step.step_id, time_key);
83✔
220
        result.result_data = output_data;
83✔
221
        result.success = true;
83✔
222
        
223
    } catch (std::exception const& e) {
83✔
UNCOV
224
        result.error_message = "Step execution error: " + std::string(e.what());
×
UNCOV
225
    }
×
226
    
227
    auto end_time = std::chrono::high_resolution_clock::now();
83✔
228
    result.execution_time_ms = std::chrono::duration<double, std::milli>(end_time - start_time).count();
83✔
229
    
230
    return result;
83✔
231
}
×
232

233
std::vector<std::string> TransformPipeline::validate() const {
78✔
234
    std::vector<std::string> errors;
78✔
235
    
236
    // Check for duplicate step IDs
237
    std::unordered_map<std::string, int> step_id_counts;
78✔
238
    for (auto const& step : steps_) {
161✔
239
        step_id_counts[step.step_id]++;
83✔
240
    }
241
    for (auto const& [step_id, count] : step_id_counts) {
161✔
242
        if (count > 1) {
83✔
UNCOV
243
            errors.push_back("Duplicate step ID: " + step_id);
×
244
        }
245
    }
246
    
247
    // Validate each step
248
    for (size_t i = 0; i < steps_.size(); ++i) {
161✔
249
        auto const& step = steps_[i];
83✔
250
        std::string step_prefix = "Step " + std::to_string(i) + " (" + step.step_id + "): ";
83✔
251
        
252
        // Check if transform exists
253
        if (!registry_->findOperationByName(step.transform_name)) {
83✔
UNCOV
254
            errors.push_back(step_prefix + "Transform '" + step.transform_name + "' not found in registry");
×
255
        }
256
        
257
        // Check input key
258
        if (step.input_key.empty()) {
83✔
UNCOV
259
            errors.push_back(step_prefix + "Input key cannot be empty");
×
260
        }
261
        
262
        // Check step ID
263
        if (step.step_id.empty()) {
83✔
UNCOV
264
            errors.push_back(step_prefix + "Step ID cannot be empty");
×
265
        }
266
        
267
        // Check phase number
268
        if (step.phase < 0) {
83✔
UNCOV
269
            errors.push_back(step_prefix + "Phase number cannot be negative");
×
270
        }
271
    }
83✔
272
    
273
    return errors;
156✔
274
}
78✔
275

276
void TransformPipeline::clear() {
79✔
277
    steps_.clear();
79✔
278
    metadata_ = nlohmann::json::object();
79✔
279
    temporary_data_.clear();
79✔
280
}
79✔
281

282
nlohmann::json TransformPipeline::exportToJson() const {
×
283
    nlohmann::json result;
×
284
    
285
    // Export metadata
286
    result["metadata"] = metadata_;
×
287
    
288
    // Export steps
289
    result["steps"] = nlohmann::json::array();
×
UNCOV
290
    for (auto const& step : steps_) {
×
291
        nlohmann::json step_json;
×
292
        step_json["step_id"] = step.step_id;
×
UNCOV
293
        step_json["transform_name"] = step.transform_name;
×
UNCOV
294
        step_json["input_key"] = step.input_key;
×
295
        step_json["output_key"] = step.output_key;
×
296
        step_json["parameters"] = step.parameters;
×
UNCOV
297
        step_json["phase"] = step.phase;
×
298
        step_json["enabled"] = step.enabled;
×
299
        
UNCOV
300
        if (!step.description.empty()) {
×
301
            step_json["description"] = step.description;
×
302
        }
303
        if (!step.tags.empty()) {
×
304
            step_json["tags"] = step.tags;
×
305
        }
306
        
UNCOV
307
        result["steps"].push_back(step_json);
×
UNCOV
308
    }
×
309
    
310
    return result;
×
311
}
×
312

313
bool TransformPipeline::saveToJsonFile(std::string const& json_file_path) const {
×
314
    try {
315
        std::ofstream file(json_file_path);
×
UNCOV
316
        if (!file.is_open()) {
×
UNCOV
317
            std::cerr << "Cannot create pipeline file: " << json_file_path << std::endl;
×
UNCOV
318
            return false;
×
319
        }
320
        
UNCOV
321
        auto json_data = exportToJson();
×
UNCOV
322
        file << json_data.dump(2); // Pretty print with 2-space indentation
×
UNCOV
323
        return true;
×
UNCOV
324
    } catch (std::exception const& e) {
×
UNCOV
325
        std::cerr << "Error saving pipeline file '" << json_file_path << "': " << e.what() << std::endl;
×
326
        return false;
×
327
    }
×
328
}
329

330
// Private implementation methods
331

332
std::pair<bool, PipelineStep> TransformPipeline::parseStep(nlohmann::json const& step_json, int step_index) {
84✔
333
    PipelineStep step;
84✔
334
    
335
    try {
336
        // Required fields
337
        if (!step_json.contains("step_id") || !step_json["step_id"].is_string()) {
84✔
338
            std::cerr << "Step " << step_index << ": 'step_id' is required and must be a string" << std::endl;
×
339
            return {false, step};
×
340
        }
341
        step.step_id = step_json["step_id"];
84✔
342
        
343
        if (!step_json.contains("transform_name") || !step_json["transform_name"].is_string()) {
84✔
UNCOV
344
            std::cerr << "Step " << step_index << ": 'transform_name' is required and must be a string" << std::endl;
×
UNCOV
345
            return {false, step};
×
346
        }
347
        step.transform_name = step_json["transform_name"];
84✔
348
        
349
        if (!step_json.contains("input_key") || !step_json["input_key"].is_string()) {
84✔
UNCOV
350
            std::cerr << "Step " << step_index << ": 'input_key' is required and must be a string" << std::endl;
×
UNCOV
351
            return {false, step};
×
352
        }
353
        step.input_key = step_json["input_key"];
84✔
354
        
355
        // Optional fields
356
        if (step_json.contains("output_key")) {
84✔
357
            if (step_json["output_key"].is_string()) {
84✔
358
                step.output_key = step_json["output_key"];
84✔
359
            }
360
        }
361
        
362
        if (step_json.contains("parameters")) {
84✔
363
            step.parameters = step_json["parameters"];
84✔
364
        } else {
UNCOV
365
            step.parameters = nlohmann::json::object();
×
366
        }
367
        
368
        if (step_json.contains("phase")) {
84✔
369
            if (step_json["phase"].is_number_integer()) {
66✔
370
                step.phase = step_json["phase"];
2✔
371
            }
372
        }
373
        
374
        if (step_json.contains("enabled")) {
84✔
375
            if (step_json["enabled"].is_boolean()) {
×
376
                step.enabled = step_json["enabled"];
×
377
            }
378
        }
379
        
380
        if (step_json.contains("description")) {
84✔
UNCOV
381
            if (step_json["description"].is_string()) {
×
UNCOV
382
                step.description = step_json["description"];
×
383
            }
384
        }
385
        
386
        if (step_json.contains("tags")) {
84✔
387
            if (step_json["tags"].is_array()) {
×
388
                for (auto const& tag : step_json["tags"]) {
×
UNCOV
389
                    if (tag.is_string()) {
×
UNCOV
390
                        step.tags.push_back(tag);
×
391
                    }
392
                }
393
            }
394
        }
395
        
396
        // Validate parameters by attempting to create them (only if operation exists)
397
        if (!step.parameters.empty()) {
84✔
398
            auto* operation = registry_->findOperationByName(step.transform_name);
72✔
399
            if (operation) {
72✔
400
                // Only validate if operation is registered
401
                auto test_params = createParametersFromJson(step.transform_name, step.parameters);
72✔
402
                if (!test_params) {
72✔
403
                    std::cerr << "Step " << step_index << " (" << step.step_id 
1✔
404
                              << "): Failed to create valid parameters" << std::endl;
1✔
405
                    return {false, step};
1✔
406
                }
407
            }
72✔
408
            // If operation not found, validation will happen later in the validate() method
409
        }
410
        
411
        return {true, step};
83✔
412
    } catch (std::exception const& e) {
×
413
        std::cerr << "Error parsing step " << step_index << ": " << e.what() << std::endl;
×
UNCOV
414
        return {false, step};
×
415
    }
×
416
}
84✔
417

418
std::unique_ptr<TransformParametersBase> TransformPipeline::createParametersFromJson(
155✔
419
    std::string const& transform_name, 
420
    nlohmann::json const& param_json) {
421
    
422
    // Get the operation to get default parameters
423
    auto* operation = registry_->findOperationByName(transform_name);
155✔
424
    if (!operation) {
155✔
425
        return nullptr;
×
426
    }
427

428
    // Special handling for grouping operations that need EntityGroupManager
429
    if (transform_name == "Group Lines by Proximity") {
155✔
UNCOV
430
        auto* group_manager = data_manager_->getEntityGroupManager();
×
431
        if (!group_manager) {
×
432
            std::cerr << "Error: EntityGroupManager not available for grouping operation" << std::endl;
×
UNCOV
433
            return nullptr;
×
434
        }
435
        
UNCOV
436
        auto parameters = std::make_unique<LineProximityGroupingParameters>(group_manager);
×
437
        
438
        // Set parameters from JSON
439
        for (auto const& [param_name, param_value] : param_json.items()) {
×
UNCOV
440
            if (!setParameterValue(parameters.get(), param_name, param_value, transform_name)) {
×
441
                std::cerr << "Warning: Failed to set parameter '" << param_name 
UNCOV
442
                          << "' for transform '" << transform_name << "'" << std::endl;
×
443
            }
UNCOV
444
        }
×
445
        
UNCOV
446
        return std::move(parameters);
×
UNCOV
447
    }
×
448
    
449
    // Get default parameters first
450
    auto parameters = operation->getDefaultParameters();
155✔
451
    if (!parameters) {
155✔
UNCOV
452
        return nullptr;
×
453
    }
454
    
455
    // Check if this is a grouping operation that needs EntityGroupManager
456
    auto* grouping_params = dynamic_cast<GroupingTransformParametersBase*>(parameters.get());
155✔
457
    if (grouping_params) {
155✔
UNCOV
458
        auto* group_manager = data_manager_->getEntityGroupManager();
×
UNCOV
459
        if (!group_manager) {
×
460
            std::cerr << "Error: EntityGroupManager not available for grouping operation '" 
UNCOV
461
                      << transform_name << "'" << std::endl;
×
UNCOV
462
            return nullptr;
×
463
        }
464
        
465
        // Set the group manager
UNCOV
466
        grouping_params->setGroupManager(group_manager);
×
467
    }
468
    
469
    // Set parameters from JSON
470
    for (auto const& [param_name, param_value] : param_json.items()) {
647✔
471
        if (!setParameterValue(parameters.get(), param_name, param_value, transform_name)) {
493✔
472
            std::cerr << "Error: Failed to set parameter '" << param_name 
473
                      << "' for transform '" << transform_name << "'" << std::endl;
1✔
474
            return nullptr;  // Fail the pipeline loading if parameter setting fails
1✔
475
        }
476
    }
156✔
477
    
478
    return parameters;
154✔
479
}
155✔
480

481
bool TransformPipeline::setParameterValue(TransformParametersBase* param_obj, 
493✔
482
                                         std::string const& param_name, 
483
                                         nlohmann::json const& json_value,
484
                                         std::string const& transform_name) {
485
    
486
    // Use the parameter factory for type conversion
487
    auto& factory = ParameterFactory::getInstance();
493✔
488
    
489
    return factory.setParameter(transform_name, param_obj, param_name, json_value, data_manager_);
493✔
490
}
491

492
std::pair<bool, DataTypeVariant> TransformPipeline::getInputData(std::string const& input_key) {
83✔
493
    // First check temporary data
494
    auto temp_it = temporary_data_.find(input_key);
83✔
495
    if (temp_it != temporary_data_.end()) {
83✔
UNCOV
496
        return {true, temp_it->second};
×
497
    }
498
    
499
    // Then check data manager
500
    auto data_variant = data_manager_->getDataVariant(input_key);
83✔
501
    if (data_variant.has_value()) {
83✔
502
        return {true, data_variant.value()};
83✔
503
    }
504
    
UNCOV
505
    return {false, DataTypeVariant{}};
×
506
}
83✔
507

508
void TransformPipeline::storeOutputData(std::string const& output_key, 
83✔
509
                                       DataTypeVariant const& data, 
510
                                       std::string const& step_id,
511
                                       TimeKey const& time_key) {
512
    if (output_key.empty()) {
83✔
513
        // Store as temporary data using step_id
UNCOV
514
        temporary_data_[step_id + "_output"] = data;
×
515
    } else {
516
        // Store in data manager
517
        data_manager_->setData(output_key, data, time_key);
83✔
518
    }
519
}
83✔
520

521
std::map<int, std::vector<int>> TransformPipeline::groupStepsByPhase() const {
78✔
522
    std::map<int, std::vector<int>> phase_groups;
78✔
523
    
524
    for (size_t i = 0; i < steps_.size(); ++i) {
161✔
525
        phase_groups[steps_[i].phase].push_back(static_cast<int>(i));
83✔
526
    }
527
    
528
    return phase_groups;
78✔
UNCOV
529
}
×
530

531
std::vector<StepResult> TransformPipeline::executePhase(
79✔
532
    std::vector<int> const& phase_steps, 
533
    int,
534
    PipelineProgressCallback progress_callback) {
535
    
536
    std::vector<StepResult> results;
79✔
537
    results.reserve(phase_steps.size());
79✔
538
    
539
    if (phase_steps.size() == 1) {
79✔
540
        // Single step - execute directly
541
        int step_index = phase_steps[0];
78✔
542
        auto const& step = steps_[static_cast<size_t>(step_index)];
78✔
543
        
544
        ProgressCallback step_progress_callback;
78✔
545
        if (progress_callback) {
78✔
546
            step_progress_callback = [this, progress_callback, step_index, step](int step_progress) {
118✔
547
                int overall_progress = (step_index * 100) / static_cast<int>(steps_.size());
206✔
548
                progress_callback(step_index, step.step_id, step_progress, overall_progress);
206✔
549
            };
59✔
550
        }
551
        
552
        results.push_back(executeStep(step, step_progress_callback));
78✔
553
    } else {
78✔
554
        // Multiple steps - execute in parallel
555
        std::vector<std::future<StepResult>> futures;
1✔
556
        futures.reserve(phase_steps.size());
1✔
557
        
558
        for (int step_index : phase_steps) {
6✔
559
            auto const& step = steps_[static_cast<size_t>(step_index)];
5✔
560
            
561
            ProgressCallback step_progress_callback;
5✔
562
            if (progress_callback) {
5✔
563
                step_progress_callback = [this, progress_callback, step_index, step](int step_progress) {
10✔
564
                    int overall_progress = (step_index * 100) / static_cast<int>(steps_.size());
35✔
565
                    progress_callback(step_index, step.step_id, step_progress, overall_progress);
35✔
566
                };
5✔
567
            }
568
            
569
            futures.push_back(std::async(std::launch::async, 
15✔
570
                [this, step, step_progress_callback]() {
10✔
571
                    return executeStep(step, step_progress_callback);
5✔
572
                }));
573
        }
5✔
574
        
575
        // Collect results
576
        for (auto& future : futures) {
6✔
577
            results.push_back(future.get());
5✔
578
        }
579
    }
1✔
580
    
581
    return results;
79✔
UNCOV
582
}
×
583

584
std::unordered_map<std::string, std::string> TransformPipeline::extractVariables() const {
79✔
585
    std::unordered_map<std::string, std::string> variables;
79✔
586
    
587
    if (metadata_.contains("variables") && metadata_["variables"].is_object()) {
79✔
UNCOV
588
        for (auto const& [key, value] : metadata_["variables"].items()) {
×
UNCOV
589
            if (value.is_string()) {
×
UNCOV
590
                variables[key] = value.get<std::string>();
×
UNCOV
591
            } else if (value.is_number()) {
×
UNCOV
592
                variables[key] = std::to_string(value.get<double>());
×
UNCOV
593
            } else if (value.is_boolean()) {
×
UNCOV
594
                variables[key] = value.get<bool>() ? "true" : "false";
×
595
            }
UNCOV
596
        }
×
597
    }
598
    
599
    return variables;
79✔
UNCOV
600
}
×
601

602
void TransformPipeline::substituteVariables(nlohmann::json& json, 
919✔
603
                                            std::unordered_map<std::string, std::string> const& variables) const {
604
    if (json.is_string()) {
919✔
605
        std::string str = json.get<std::string>();
481✔
606
        
607
        // Replace all ${variable_name} patterns
608
        size_t pos = 0;
481✔
609
        while ((pos = str.find("${", pos)) != std::string::npos) {
481✔
UNCOV
610
            size_t end_pos = str.find("}", pos + 2);
×
UNCOV
611
            if (end_pos == std::string::npos) {
×
612
                // Malformed variable reference, skip it
UNCOV
613
                pos += 2;
×
UNCOV
614
                continue;
×
615
            }
616
            
UNCOV
617
            std::string var_name = str.substr(pos + 2, end_pos - pos - 2);
×
UNCOV
618
            auto it = variables.find(var_name);
×
619
            
UNCOV
620
            if (it != variables.end()) {
×
621
                // Replace the ${var_name} with the value
UNCOV
622
                str.replace(pos, end_pos - pos + 1, it->second);
×
UNCOV
623
                pos += it->second.length();
×
624
            } else {
625
                // Variable not found, leave it as is and move past it
UNCOV
626
                std::cerr << "Warning: Variable '${" << var_name << "}' not found in metadata.variables" << std::endl;
×
UNCOV
627
                pos = end_pos + 1;
×
628
            }
UNCOV
629
        }
×
630
        
631
        json = str;
481✔
632
    } else if (json.is_array()) {
919✔
633
        for (auto& element : json) {
247✔
634
            substituteVariables(element, variables);
84✔
635
        }
636
    } else if (json.is_object()) {
359✔
637
        for (auto& [key, value] : json.items()) {
1,682✔
638
            substituteVariables(value, variables);
756✔
639
        }
170✔
640
    }
641
}
919✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc