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

nasa / trick / 21849135008

10 Feb 2026 02:21AM UTC coverage: 55.709% (+0.09%) from 55.624%
21849135008

Pull #2016

github

web-flow
Merge c90850b1a into d49130d35
Pull Request #2016: better support set_cycle in drgroup

75 of 93 new or added lines in 1 file covered. (80.65%)

4 existing lines in 2 files now uncovered.

12573 of 22569 relevant lines covered (55.71%)

321147.65 hits per line

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

82.86
/trick_source/sim_services/DataRecord/DataRecordGroup.cpp
1

2
#include <algorithm>
3
#include <string>
4
#include <iostream>
5
#include <sstream>
6
#include <string.h>
7
#include <stdlib.h>
8
#include <iomanip>
9
#include <math.h>
10

11
#ifdef __GNUC__
12
#include <cxxabi.h>
13
#endif
14

15
#include "trick/DataRecordGroup.hh"
16
#include "trick/command_line_protos.h"
17
#include "trick/exec_proto.h"
18
#include "trick/reference.h"
19
#include "trick/memorymanager_c_intf.h"
20
#include "trick/message_proto.h"
21
#include "trick/message_type.h"
22

23
/**
24
@details
25
-# The recording group is enabled
26
-# The recording group is set to always record data (not changes only)
27
-# The recording group is set to allocate a maximum of 100,000 records in memory
28
-# The recording group is set to record at a cycle period of 0.1 seconds.
29
-# The first recording parameter is assigned to simulation time.  The name of
30
   the parmeter is named "sys.exec.out.time" to preserve backwards compatibility
31
   with older %Trick releases.
32
-# The call_function and call_function_double functions inherited from the SimObject
33
   are populated.  The data_record function is added as a job for this SimObject.
34

35
A sim object is created named @c data_record_group<n> where <n> is a unique group number. A
36
data_record class job within this sim object is also created that will write the group's
37
recorded data to disk. The job's name is <tt> data_record_group<n>.<in_name></tt>.
38

39
*/
40
Trick::DataRecordBuffer::DataRecordBuffer() {
5,692✔
41
    buffer = last_value = NULL ;
5,692✔
42
    ref = NULL ;
5,692✔
43
    ref_searched = false ;
5,692✔
44
}
5,692✔
45

46
Trick::DataRecordBuffer::~DataRecordBuffer() {
636✔
47
    if ( buffer ) {
636✔
48
        free(buffer) ;
621✔
49
    }
50
    if ( last_value ) {
636✔
51
        free(last_value) ;
624✔
52
    }
53

54
    ref_free(ref) ;
636✔
55
    free(ref) ;
636✔
56
}
636✔
57

58

59
Trick::LoggingCycle::LoggingCycle(double rate_in)
622✔
60
{
61
    set_rate(rate_in);
622✔
62
}
622✔
63

64
void Trick::LoggingCycle::set_rate(double rate_in)
681✔
65
{
66
    rate_in_seconds = rate_in;
681✔
67
    rate_in_tics = (long long)round(rate_in * Trick::JobData::time_tic_value);
681✔
68
}
681✔
69

70
long long Trick::LoggingCycle::calc_next_tics_on_or_after_input_tic(long long input_tic, long long cycle_tic)
22,035,578✔
71
{
72
    long long next_tic;
73
    if((input_tic % cycle_tic) != 0)
22,035,578✔
74
    {
75
        next_tic = (input_tic/cycle_tic) * cycle_tic + cycle_tic;
15,115,302✔
76
    } else 
77
    {
78
        next_tic = input_tic;
6,920,276✔
79
    }
80
    return next_tic;
22,035,578✔
81
}
82

NEW
83
Trick::DataRecordGroupJobData::DataRecordGroupJobData(Trick::DataRecordGroup &owner_in)
×
NEW
84
    : owner(owner_in)
×
85
{
NEW
86
}
×
87

88
Trick::DataRecordGroupJobData::DataRecordGroupJobData(Trick::DataRecordGroup &owner_in, int in_thread, int in_id, std::string in_job_class_name, void *in_sup_class_data,
616✔
89
                                                      double in_cycle, std::string in_name, std::string in_tag, int in_phase,
90
                                                      double in_start, double in_stop)
616✔
91
    : Trick::JobData(in_thread, in_id, in_job_class_name, in_sup_class_data, in_cycle, in_name, in_tag, in_phase, in_start, in_stop),
92
      owner(owner_in)
616✔
93
{
94
}
616✔
95

96
int Trick::DataRecordGroupJobData::set_cycle(double rate)
1✔
97
{
98
    owner.set_cycle(rate);
1✔
99
    return 0;
1✔
100
}
101

102
Trick::DataRecordGroup::DataRecordGroup( std::string in_name, Trick::DR_Type dr_type ) :
616✔
103
 record(true) ,
104
 inited(false) ,
105
 group_name(in_name) ,
106
 freq(DR_Always),
107
 start(0.0) ,
108
 cycle(0.1) ,
109
 time_value_attr() ,
110
 num_variable_names(0),
111
 variable_names(NULL),
112
 variable_alias(NULL),
113
 num_change_variable_names(0),
114
 change_variable_names(NULL),
115
 change_variable_alias(NULL),
116
 max_num(100000),
117
 buffer_num(0),
118
 writer_num(0),
119
 max_file_size(1<<30), // 1 GB
120
 total_bytes_written(0),
121
 max_size_warning(false),
122
 writer_buff(NULL),
123
 single_prec_only(false),
124
 buffer_type(DR_Buffer),
125
 job_class("data_record"),
126
 curr_time(0.0),
127
 curr_time_dr_job(0.0)
616✔
128
{
129

130
    union {
131
        long l;
132
        char c[sizeof(long)];
133
    } byte_order_union;
134

135
    byte_order_union.l = 1;
616✔
136
    if (byte_order_union.c[sizeof(long) - 1] != 1) {
616✔
137
        byte_order = "little_endian" ;
616✔
138
    } else {
139
        byte_order = "big_endian" ;
×
140
    }
141

142
    // sim object name
143
    name = std::string("trick_data_record_group_") + in_name ;
616✔
144

145
    configure_jobs(dr_type) ;
616✔
146

147
    add_time_variable() ;
616✔
148

149
    logging_rates.emplace_back(cycle);
616✔
150
}
616✔
151

152
Trick::DataRecordGroup::~DataRecordGroup() {
44✔
153
    free((void *)time_value_attr.units) ;
44✔
154
}
44✔
155

156
int Trick::DataRecordGroup::call_function( Trick::JobData * curr_job ) {
5,192,013✔
157

158
    int ret = 0 ;
5,192,013✔
159

160
    switch (curr_job->id) {
5,192,013✔
161
        case 1:
46✔
162
            ret = init() ;
46✔
163
            break ;
46✔
164
        case 2:
437,050✔
165
            ret = write_data(false) ;
437,050✔
166
            break ;
437,050✔
167
        case 3:
13✔
168
            ret = checkpoint() ;
13✔
169
            break ;
13✔
170
        case 4:
13✔
171
            clear_checkpoint_vars() ;
13✔
172
            break ;
13✔
173
        case 5:
9✔
174
            ret = restart() ;
9✔
175
            break ;
9✔
176
        case 6:
46✔
177
            ret = shutdown() ;
46✔
178
            break ;
46✔
179
        default:
4,754,836✔
180
            ret = data_record(exec_get_sim_time()) ;
4,754,836✔
181
            break ;
4,754,836✔
182
    }
183

184
    return(ret) ;
5,192,013✔
185

186
}
187

188
double Trick::DataRecordGroup::call_function_double( Trick::JobData * curr_job ) {
×
189
    (void) curr_job ;
190
    return(0.0) ;
×
191
}
192

193
void Trick::DataRecordGroup::register_group_with_mm(void * address , const char * type) {
44✔
194
    // Only add to the memory manager if it has not already been added
195
    if ( TMM_var_exists(name.c_str()) == 0 ) {
44✔
196
        // Declare this to the memory manager.  Must be done here to get the correct type name
197
        TMM_declare_ext_var(address , TRICK_STRUCTURED, type , 0 , name.c_str() , 0 , NULL ) ;
44✔
198
        ALLOC_INFO * alloc_info = get_alloc_info_at(address) ;
44✔
199
        alloc_info->stcl = TRICK_LOCAL ;
44✔
200
        alloc_info->alloc_type = TRICK_ALLOC_NEW ;
44✔
201
    }
202
}
44✔
203

204
const std::string & Trick::DataRecordGroup::get_group_name() {
43✔
205
    return group_name ;
43✔
206
}
207

NEW
208
size_t Trick::DataRecordGroup::get_num_rates(){
×
NEW
209
    return logging_rates.size();
×
210
}
211

NEW
212
double Trick::DataRecordGroup::get_rate(const size_t rateIdx)
×
213
{
NEW
214
    if(rateIdx < logging_rates.size())
×
215
    {
NEW
216
        return logging_rates[rateIdx].rate_in_seconds;
×
217
    }
NEW
218
    return -1.0;
×
219
}
220

221
int Trick::DataRecordGroup::set_cycle( double in_cycle ) {
52✔
222
    return set_rate(0, in_cycle);    
52✔
223
}
224

225
int Trick::DataRecordGroup::add_cycle(double in_cycle)
6✔
226
{
227
    logging_rates.emplace_back(in_cycle);
6✔
228
    set_rate(logging_rates.size()-1, in_cycle);
6✔
229
    return (int)logging_rates.size()-1;
6✔
230
}
231

232
int Trick::DataRecordGroup::set_rate(const size_t rate_idx, const double rate_in)
62✔
233
{
234
    if(rate_idx >= logging_rates.size())
62✔
235
    {
NEW
236
        message_publish(MSG_ERROR, "DataRecordGroup ERROR: DR Group \"%s\" : set_rate: invalid rate idx %lu\n", group_name.c_str(), rate_idx);
×
NEW
237
        return 1;
×
238
    }
239
    if(inited) {
62✔
240
        int ret = check_if_rate_is_valid(rate_in);
12✔
241
        if(ret)
12✔
242
        {
243
            emit_rate_error(ret, rate_idx, rate_in);
3✔
244
            message_publish(MSG_ERROR, "DataRecordGroup ERROR: DR Group \"%s\" : Rejecting runtime set_rate(%lu, %.16g)\n", group_name.c_str(), rate_idx, rate_in);
3✔
245
            return 1;
3✔
246
        }
247
        long long prev_log_tics = (long long)round(curr_time_dr_job * Trick::JobData::time_tic_value);
9✔
248
        long long curr_time_tic = exec_get_time_tics();
9✔
249
        LoggingCycle & curLog = logging_rates[rate_idx];
9✔
250
        curLog.set_rate(rate_in);
9✔
251
        curLog.next_cycle_in_tics = LoggingCycle::calc_next_tics_on_or_after_input_tic(curr_time_tic, curLog.rate_in_tics);
9✔
252
        if(curLog.next_cycle_in_tics == curr_time_tic && curLog.next_cycle_in_tics == prev_log_tics)
9✔
253
        {
NEW
254
            curLog.next_cycle_in_tics += curLog.rate_in_tics;
×
255
        }
256
        for(auto & logCycle : logging_rates)
27✔
257
        {
258
            logCycle.next_cycle_in_tics = 0;
18✔
259
        }
260
        advance_log_tics_given_curr_tic(curr_time_tic-1);
9✔
261
        write_job->next_tics = calculate_next_logging_tic(curr_time_tic-1);
9✔
262

263
        long long next_next_tics = calculate_next_logging_tic(write_job->next_tics);
9✔
264
        write_job->cycle_tics = next_next_tics - write_job->next_tics;
9✔
265
        write_job->cycle = (double)write_job->cycle_tics / Trick::JobData::time_tic_value;
9✔
266
    } else {
267
        logging_rates[rate_idx].set_rate(rate_in);
50✔
268
    }
269
    return 0;
59✔
270
}
271

272
int Trick::DataRecordGroup::set_phase( unsigned short in_phase ) {
596✔
273
    write_job->phase = in_phase ;
596✔
274
    return(0) ;
596✔
275
}
276

277
int Trick::DataRecordGroup::set_freq( DR_Freq in_freq ) {
38✔
278
    freq = in_freq ;
38✔
279
    return(0) ;
38✔
280
}
281

282
int Trick::DataRecordGroup::set_max_buffer_size( int num ) {
×
283
    max_num = num ;
×
284
    return(0) ;
×
285
}
286

287
int Trick::DataRecordGroup::set_buffer_type( int in_buffer_type ) {
46✔
288
    buffer_type = (DR_Buffering)in_buffer_type ;
46✔
289
    return(0) ;
46✔
290
}
291

292
int Trick::DataRecordGroup::set_max_file_size( uint64_t bytes ) {
18✔
293
    if(bytes == 0) {
18✔
294
        max_file_size = UINT64_MAX ;
×
295
    } else {
296
    max_file_size = bytes ; 
18✔
297
    }
298
    return(0) ;
18✔
299
}
300

301
int Trick::DataRecordGroup::set_single_prec_only( bool in_single_prec_only ) {
26✔
302
    single_prec_only = in_single_prec_only ;
26✔
303
    return(0) ;
26✔
304
}
305

306
int Trick::DataRecordGroup::set_thread( unsigned int in_thread_id ) {
202✔
307

308
    unsigned int jj ;
309
    Trick::JobData * temp_job  ;
310

311
    /* make all data_record_groups have same sim object id as data_record */
312
    for ( jj = 0 ; jj < jobs.size() ; jj++ ) {
1,828✔
313
        temp_job = jobs[jj] ;
1,626✔
314
        temp_job->thread = in_thread_id ;
1,626✔
315
    }
316
    return 0 ;
202✔
317
}
318

319
int Trick::DataRecordGroup::set_job_class( std::string in_class ) {
596✔
320
    write_job->job_class_name = job_class = in_class ;
596✔
321
    return(0) ;
596✔
322
}
323

324
int Trick::DataRecordGroup::add_time_variable() {
658✔
325
    REF2 * new_ref ;
326

327
    // Create attributes for time recorded as a double
328
    time_value_attr.type = TRICK_DOUBLE ;
658✔
329
    time_value_attr.size = sizeof(double) ;
658✔
330
    time_value_attr.units = strdup("s") ;
658✔
331

332
    // Create a reference that records time as sys.exec.out.time
333
    new_ref = (REF2 *)calloc( 1 , sizeof(REF2));
658✔
334
    new_ref->reference = strdup("sys.exec.out.time") ;
658✔
335
    new_ref->address = &curr_time ;
658✔
336
    new_ref->attr = &time_value_attr ;
658✔
337
    add_variable(new_ref) ;
658✔
338

339
    return 0 ;
658✔
340
}
341

342
int Trick::DataRecordGroup::add_variable( std::string in_name , std::string alias ) {
964✔
343

344
    Trick::DataRecordBuffer * new_var = new Trick::DataRecordBuffer ;
964✔
345
    // Trim leading spaces
346
    in_name.erase( 0, in_name.find_first_not_of( " \t" ) );
964✔
347
    // Trim trailing spaces
348
    in_name.erase( in_name.find_last_not_of( " \t" ) + 1);
964✔
349
    new_var->name = in_name ;
964✔
350
    new_var->alias = alias ;
964✔
351
    rec_buffer.push_back(new_var) ;
964✔
352
    return 0 ;
964✔
353
}
354

355
void Trick::DataRecordGroup::remove_variable( std::string in_name ) {
×
356
    // Trim leading spaces++ 
357
    in_name.erase( 0, in_name.find_first_not_of( " \t" ) );
×
358
    // Trim trailing spaces
359
    in_name.erase( in_name.find_last_not_of( " \t" ) + 1);
×
360

361
    if (!in_name.compare("sys.exec.out.time")) {
×
362
        // This class assumes sim time is always the first variable.
363
        // Removing it results in errors.
364
        return;
×
365
    }
366

367
    auto remove_from = [&](std::vector<DataRecordBuffer*>& buffer) {
×
368
        for (auto i = buffer.begin(); i != buffer.end(); ++i) {
×
369
            if (!(*i)->name.compare(in_name)) {
×
370
                delete *i;
×
371
                buffer.erase(i);
×
372
                break;
×
373
            }
374
        }
375
    };
×
376

377
    remove_from(rec_buffer);
×
378
    remove_from(change_buffer);
×
379
}
380

381
void Trick::DataRecordGroup::remove_all_variables() {
55✔
382
    // remove all but the first variable, which is sim time
383
    if(!rec_buffer.empty()) {
55✔
384
        for (auto i = rec_buffer.begin() + 1; i != rec_buffer.end(); ++i) {
615✔
385
            delete *i;
560✔
386
        }
387
        rec_buffer.erase(rec_buffer.begin() + 1, rec_buffer.end());
55✔
388
    }
389

390
    // remove everything
391
    for (auto variable : change_buffer) {
61✔
392
        delete variable;
6✔
393
    }
394

395
    change_buffer.clear();
55✔
396
}
55✔
397

398
int Trick::DataRecordGroup::add_variable( REF2 * ref2 ) {
4,722✔
399

400
    if ( ref2 == NULL || ref2->attr == NULL ) {
4,722✔
401
        return(-1) ;
×
402
    }
403

404
    Trick::DataRecordBuffer * new_var = new Trick::DataRecordBuffer ;
4,722✔
405
    new_var->name = std::string(ref2->reference) ;
4,722✔
406
    new_var->ref_searched = true ;
4,722✔
407
    new_var->ref = ref2 ;
4,722✔
408
    new_var->last_value = (char *)calloc(1 , new_var->ref->attr->size) ;
4,722✔
409
    // Don't allocate space for the temp storage buffer until "init"
410
    rec_buffer.push_back(new_var) ;
4,722✔
411

412
    return(0) ;
4,722✔
413

414
}
415

416
int Trick::DataRecordGroup::add_change_variable( std::string in_name ) {
6✔
417

418
    REF2 * ref2 ;
419

420
    ref2 = ref_attributes(in_name.c_str()) ;
6✔
421

422
    if ( ref2 == NULL || ref2->attr == NULL ) {
6✔
NEW
423
        message_publish(MSG_WARNING, "DR Group \"%s\" : Could not find Data Record change variable %s.\n", group_name.c_str(), in_name.c_str()) ;
×
424
        return(-1) ;
×
425
    }
426

427
    Trick::DataRecordBuffer * new_var = new Trick::DataRecordBuffer ;
6✔
428
    new_var->ref = ref2 ;
6✔
429
    new_var->name = in_name;
6✔
430
    new_var->buffer = (char *)malloc(ref2->attr->size) ;
6✔
431
    new_var->last_value =  NULL ;
6✔
432
    memcpy(new_var->buffer , ref2->address , ref2->attr->size) ;
6✔
433
    change_buffer.push_back(new_var) ;
6✔
434

435
    return(0) ;
6✔
436

437
}
438

439
bool Trick::DataRecordGroup::isSupportedType(REF2 * ref2, std::string& message) {
548✔
440
    if (ref2->attr->type == TRICK_STRING || ref2->attr->type == TRICK_STL || ref2->attr->type == TRICK_STRUCTURED) {
548✔
441
        message = "Cannot Data Record variable " + std::string(ref2->reference) + " of unsupported type " + std::to_string(ref2->attr->type);
2✔
442
        return false;
2✔
443
    }
444
    
445
    // If this is an array and not a single value, don't record it
446
    if (ref2->num_index != ref2->attr->num_index) {
546✔
447
        message = "Cannot Data Record arrayed variable " + std::string(ref2->reference);
4✔
448
        return false;
4✔
449
    }
450

451
    return true;
542✔
452
}
453

454
/**
455
@details
456
-# The simulation output directory is retrieved from the CommandLineArguments
457
-# The log header file is created
458
   -# The endianness of the log file is written to the log header.
459
   -# The names of the parameters contained in the log file are written to the header.
460
-# Memory buffers are allocated to store simulation data
461
-# The DataRecordGroupObject (a derived SimObject) is added to the Scheduler.
462
*/
463
int Trick::DataRecordGroup::init(bool is_restart) {
58✔
464

465
    unsigned int jj ;
466
    int ret ;
467

468
    // reset counter here so we can "re-init" our recording
469
    buffer_num = writer_num = total_bytes_written = 0 ;
58✔
470

471
    output_dir = command_line_args_get_output_dir() ;
58✔
472
    /* this is the common part of the record file name, the format specific will add the correct suffix */
473
    file_name = output_dir + "/log_" + group_name ;
58✔
474

475
    pthread_mutex_init(&buffer_mutex, NULL);
58✔
476

477
    // Allocate recording space for time.
478
    rec_buffer[0]->buffer = (char *)calloc(max_num , rec_buffer[0]->ref->attr->size) ;
58✔
479
    rec_buffer[0]->last_value = (char *)calloc(1 , rec_buffer[0]->ref->attr->size) ;
58✔
480

481
    /* Loop through all variables looking up names.  Allocate recording space
482
       according to size of the variable */
483
    for (jj = 1; jj < rec_buffer.size() ; jj++) {
644✔
484
        Trick::DataRecordBuffer * drb = rec_buffer[jj] ;
586✔
485
        if ( drb->ref_searched == false ) {
586✔
486
            REF2 * ref2 ;
487

488
            ref2 = ref_attributes(drb->name.c_str()) ;
548✔
489
            if ( ref2 == NULL || ref2->attr == NULL ) {
548✔
NEW
490
                message_publish(MSG_WARNING, "DR Group \"%s\" : Could not find Data Record variable %s.\n", group_name.c_str(), drb->name.c_str()) ;
×
491
                rec_buffer.erase(rec_buffer.begin() + jj--) ;
×
492
                delete drb ;
×
493
                continue ;
×
494
            } else {
495
                std::string message;
548✔
496
                if (!isSupportedType(ref2, message)) {
548✔
497
                    message_publish(MSG_WARNING, "DR Group \"%s\" : %s\n", group_name.c_str(), message.c_str()) ;
6✔
498
                    rec_buffer.erase(rec_buffer.begin() + jj--) ;
6✔
499
                    delete drb ;
6✔
500
                    continue ;
6✔
501
                } else {
502
                    drb->ref = ref2 ;
542✔
503
                }
504
            }
505
        }
506
        if ( drb->alias.compare("") ) {
580✔
507
            drb->ref->reference = strdup(drb->alias.c_str()) ;
16✔
508
        }
509
        drb->last_value = (char *)calloc(1 , drb->ref->attr->size) ;
580✔
510
        drb->buffer = (char *)calloc(max_num , drb->ref->attr->size) ;
580✔
511
        drb->ref_searched = true ;
580✔
512
    }
513

514
    write_header() ;
58✔
515

516
    // call format specific initialization to open destination and write header
517
    ret = format_specific_init() ;
58✔
518

519
    if(!is_restart)
58✔
520
    {
521
        if(!check_if_rates_are_valid())
49✔
522
        {
NEW
523
            disable();
×
NEW
524
            return (1);
×
525
        }
526
        long long curr_tics = exec_get_time_tics();
49✔
527
        write_job->next_tics = curr_tics;
49✔
528

529
        long long next_next_tics = calculate_next_logging_tic(write_job->next_tics);
49✔
530
        write_job->cycle_tics = next_next_tics - curr_tics;
49✔
531
        write_job->cycle = (double)write_job->cycle_tics / Trick::JobData::time_tic_value;
49✔
532
    }
533

534
    // set the inited flag to true when all initialization is done
535
    if ( ret == 0 ) {
58✔
536
        inited = true ;
58✔
537
    }
538

539
    return(0) ;
58✔
540

541
}
542

543
void Trick::DataRecordGroup::configure_jobs(DR_Type type) {
616✔
544
    switch(type) {
616✔
545
    default:
53✔
546
        // run the restart job in phase 60001
547
        add_job(0, 5, (char *)"restart", NULL, 1.0, (char *)"restart", (char *)"TRK", 60001) ;
53✔
548

549
    case DR_Type::DR_Type_FrameLogDataRecord:
616✔
550
        // add_jobs_to_queue will fill in job_id later
551
        // make the init job run after all other initialization jobs but before the post init checkpoint
552
        // job so users can allocate memory in initialization jobs and checkpointing data rec groups will work
553
        add_job(0, 1, (char *)"initialization", NULL, cycle, (char *)"init", (char *)"TRK", 65534) ;
616✔
554
        add_job(0, 2, (char *)"end_of_frame", NULL, 1.0, (char *)"write_data", (char *)"TRK") ;
616✔
555
        add_job(0, 3, (char *)"checkpoint", NULL, 1.0, (char *)"checkpoint", (char *)"TRK") ;
616✔
556
        add_job(0, 4, (char *)"post_checkpoint", NULL, 1.0, (char *)"clear_checkpoint_vars", (char *)"TRK") ;
616✔
557
        add_job(0, 6, (char *)"shutdown", NULL, 1.0, (char *)"shutdown", (char *)"TRK") ;
616✔
558

559
        write_job = new Trick::DataRecordGroupJobData(*this, 0, 99, (char *)job_class.c_str(), NULL, cycle, (char *)"data_record" , (char *)"TRK") ;
616✔
560
        jobs.push_back(write_job) ;
616✔
561
        break ;
616✔
562
    }
563
    write_job->set_system_job_class(true);
616✔
564
}
616✔
565

566
int Trick::DataRecordGroup::checkpoint() {
13✔
567
    unsigned int jj ;
568

569
    /*
570
       Save the names of the variables and the aliases to the checkpoint,
571
       the rest of the DataRecordBuffer will be reconstructed during restart.
572
       The first variable is time which we do not have to save.
573
     */
574
    if ( rec_buffer.size() > 1 ) {
13✔
575
        num_variable_names = rec_buffer.size() - 1 ;
13✔
576
        variable_names = (char **)TMM_declare_var_1d("char *", (int)rec_buffer.size() - 1) ;
13✔
577
        variable_alias = (char **)TMM_declare_var_1d("char *", (int)rec_buffer.size() - 1) ;
13✔
578

579
        for (jj = 1; jj < rec_buffer.size() ; jj++) {
60✔
580
            Trick::DataRecordBuffer * drb = rec_buffer[jj] ;
47✔
581

582
            variable_names[jj-1] = TMM_strdup((char *)drb->name.c_str()) ;
47✔
583
            variable_alias[jj-1] = TMM_strdup((char *)drb->alias.c_str()) ;
47✔
584
        }
585
    }
586

587
    /*
588
       Save the names of the change variables and the aliases to the checkpoint,
589
       the rest of the DataRecordBuffer will be reconstructed during restart
590
     */
591
    if ( change_buffer.size() > 0 ) {
13✔
592
        num_change_variable_names = change_buffer.size() ;
2✔
593
        change_variable_names = (char **)TMM_declare_var_1d("char *", (int)change_buffer.size()) ;
2✔
594
        change_variable_alias = (char **)TMM_declare_var_1d("char *", (int)change_buffer.size()) ;
2✔
595

596
        for (jj = 0; jj < change_buffer.size() ; jj++) {
4✔
597
            Trick::DataRecordBuffer * drb = change_buffer[jj] ;
2✔
598

599
            change_variable_names[jj] = TMM_strdup((char *)drb->name.c_str()) ;
2✔
600
            change_variable_alias[jj] = TMM_strdup((char *)drb->alias.c_str()) ;
2✔
601
        }
602
    }
603

604
    return 0 ;
13✔
605
}
606

607
void Trick::DataRecordGroup::clear_checkpoint_vars() {
55✔
608
    
609
    if ( variable_names ) {
55✔
610
        for(unsigned int jj = 0; jj < num_variable_names; jj++) {
84✔
611
            TMM_delete_var_a(variable_names[jj]);
62✔
612
        }
613
        TMM_delete_var_a(variable_names) ;
22✔
614
    }
615

616
    if ( variable_alias ) {
55✔
617
        for(unsigned int jj = 0; jj < num_variable_names; jj++) {
84✔
618
            TMM_delete_var_a(variable_alias[jj]);
62✔
619
        }
620
        TMM_delete_var_a(variable_alias) ;
22✔
621
    }
622

623
    if ( change_variable_names ) {
55✔
624
        for(unsigned int jj = 0; jj < num_change_variable_names; jj++) {
8✔
625
            TMM_delete_var_a(change_variable_names[jj]);
4✔
626
        }
627
        TMM_delete_var_a(change_variable_names) ;
4✔
628
    }
629

630
    if ( change_variable_alias ) {
55✔
631
        for(unsigned int jj = 0; jj < num_change_variable_names; jj++) {
8✔
632
            TMM_delete_var_a(change_variable_alias[jj]);
4✔
633
        }
634
        TMM_delete_var_a(change_variable_alias) ;
4✔
635
    }
636

637
    variable_names = NULL ;
55✔
638
    variable_alias = NULL ;
55✔
639
    change_variable_names = NULL ;
55✔
640
    change_variable_alias = NULL ;
55✔
641
    num_variable_names = 0 ;
55✔
642
    num_change_variable_names = 0 ;
55✔
643
}
55✔
644

645
int Trick::DataRecordGroup::restart() {
9✔
646
    std::vector <Trick::DataRecordBuffer *>::iterator drb_it ;
9✔
647

648
    /* delete the current rec_buffer */
649
    for ( drb_it = rec_buffer.begin() ; drb_it != rec_buffer.end() ; ++drb_it ) {
18✔
650
        delete *drb_it ;
9✔
651
    }
652
    rec_buffer.clear() ;
9✔
653
    /* Add back the time variable */
654
    add_time_variable() ;
9✔
655

656
    /* delete the current change_buffer contents */
657
    for ( drb_it = change_buffer.begin() ; drb_it != change_buffer.end() ; ++drb_it ) {
9✔
658
        delete *drb_it ;
×
659
    }
660
    change_buffer.clear() ;
9✔
661

662
    unsigned int jj ;
663
    /* add the variable names listed in the checkpoint file */
664
    for ( jj = 0 ; jj < num_variable_names ; jj++ ) {
24✔
665
        add_variable( variable_names[jj] , variable_alias[jj] ) ;
15✔
666
    }
667
    for ( jj = 0 ; jj < num_change_variable_names ; jj++ ) {
11✔
668
        add_change_variable( change_variable_names[jj] ) ;
2✔
669
    }
670

671
    clear_checkpoint_vars() ;
9✔
672

673
    // set the write job class to what is in the checkpoint file.
674
    write_job->job_class_name = job_class ;
9✔
675

676
    // reset the sim_object name.
677
    name = std::string("data_record_group_") + group_name ;
9✔
678

679
    /* call init to open the recording file and look up variable name addresses */
680
    init(true) ;
9✔
681

682
    return 0 ;
9✔
683
}
684

685
int Trick::DataRecordGroup::write_header() {
58✔
686

687
    unsigned int jj ;
688
    std::string header_name ;
116✔
689
    std::fstream out_stream ;
116✔
690

691
    /*! create the header file used by the GUIs */
692
    header_name = output_dir + "/log_" + group_name + ".header" ;
58✔
693

694
    out_stream.open(header_name.c_str(), std::fstream::out ) ;
58✔
695
    if ( ! out_stream  ||  ! out_stream.good() ) {
58✔
696
        return -1;
×
697
    }
698

699
    /* Header file first line is created in format specific header */
700
    out_stream << "log_" << group_name ;
58✔
701

702
    format_specific_header(out_stream) ;
58✔
703

704
    /* Output the file name, variable size, units, and variable name
705
     * to the rest of recorded data header file.
706
     * (e.g. file_name  C_type  units  sim_name)
707
     * Note: "sys.exec.out.time" should be the first variable in the buffer.
708
     */
709
    for (jj = 0; jj < rec_buffer.size() ; jj++) {
696✔
710
        /*! recording single data item */
711
        out_stream << "log_" << group_name << "\t"
638✔
712
            << type_string(rec_buffer[jj]->ref->attr->type,
1,276✔
713
                           rec_buffer[jj]->ref->attr->size) << "\t"
638✔
714
            << std::setw(6) ;
1,276✔
715

716
        if ( rec_buffer[jj]->ref->attr->mods & TRICK_MODS_UNITSDASHDASH ) {
638✔
717
            out_stream << "--" ;
4✔
718
        } else {
719
            out_stream << rec_buffer[jj]->ref->attr->units ;
634✔
720
        }
721
        out_stream << "\t" << rec_buffer[jj]->ref->reference << std::endl ;
638✔
722
    }
723

724
    // Send all unwritten characters in the buffer to its output/file.
725
    out_stream.flush() ;
58✔
726
    out_stream.close() ;
58✔
727

728
    return(0) ;
58✔
729

730
}
731

732
int Trick::DataRecordGroup::data_record(double in_time) {
4,754,836✔
733

734
    unsigned int jj ;
735
    unsigned int buffer_offset ;
736
    Trick::DataRecordBuffer * drb ;
737
    bool change_detected = false ;
4,754,836✔
738

739
    curr_time_dr_job = in_time;
4,754,836✔
740

741
    //TODO: does not handle bitfields correctly!
742
    if ( record == true ) {
4,754,836✔
743
        if ( freq != DR_Always ) {
4,754,836✔
744
            for (jj = 0; jj < change_buffer.size() ; jj++) {
1,644✔
745
                drb = change_buffer[jj] ;
822✔
746
                REF2 * ref = drb->ref ;
822✔
747
                if ( ref->pointer_present == 1 ) {
822✔
748
                    ref->address = follow_address_path(ref) ;
×
749
                }
750
                if ( memcmp( drb->buffer , drb->ref->address , drb->ref->attr->size) ) {
822✔
751
                    change_detected = true ;
28✔
752
                    memcpy( drb->buffer , drb->ref->address , drb->ref->attr->size) ;
28✔
753
                }
754
            }
755

756
        }
757

758
        if ( freq == DR_Always || change_detected == true ) {
4,754,836✔
759

760
            // If this is not the ring buffer and
761
            // we are going to have trouble fitting 2 data sets then write the data now.
762
            if ( buffer_type != DR_Ring_Buffer ) {
4,754,042✔
763
                if ( buffer_num - writer_num >= (max_num - 2) ) {
4,494,842✔
764
                    write_data(true) ;
×
765
                }
766
            }
767

768
            curr_time = in_time ;
4,754,042✔
769

770
            if ( freq == DR_Changes_Step ) {
4,754,042✔
771
                buffer_offset = buffer_num % max_num ;
×
772
                *((double *)(rec_buffer[0]->last_value)) = in_time ;
×
773
                for (jj = 0; jj < rec_buffer.size() ; jj++) {
×
774
                    drb = rec_buffer[jj] ;
×
775
                    REF2 * ref = drb->ref ;
×
776
                    int param_size = ref->attr->size ;
×
777
                    if ( buffer_offset == 0 ) {
×
778
                       drb->curr_buffer = drb->buffer ;
×
779
                    } else {
780
                       drb->curr_buffer += param_size ;
×
781
                    }
782
                    switch ( param_size ) {
×
783
                        case 8:
×
784
                            *(int64_t *)drb->curr_buffer = *(int64_t *)drb->last_value ;
×
785
                            break ;
×
786
                        case 4:
×
787
                            *(int32_t *)drb->curr_buffer = *(int32_t *)drb->last_value ;
×
788
                            break ;
×
789
                        case 2:
×
790
                            *(int16_t *)drb->curr_buffer = *(int16_t *)drb->last_value ;
×
791
                            break ;
×
792
                        case 1:
×
793
                            *(int8_t *)drb->curr_buffer = *(int8_t *)drb->last_value ;
×
794
                            break ;
×
795
                        default:
×
796
                            memcpy( drb->curr_buffer , drb->last_value , param_size ) ;
×
797
                            break ;
×
798
                    }
799
                }
800
                buffer_num++ ;
×
801
            }
802

803
            buffer_offset = buffer_num % max_num ;
4,754,042✔
804
            for (jj = 0; jj < rec_buffer.size() ; jj++) {
39,655,449✔
805
                drb = rec_buffer[jj] ;
34,901,407✔
806
                REF2 * ref = drb->ref ;
34,901,407✔
807
                if ( ref->pointer_present == 1 ) {
34,901,407✔
808
                    ref->address = follow_address_path(ref) ;
×
809
                }
810
                int param_size = ref->attr->size ;
34,901,407✔
811
                if ( buffer_offset == 0 ) {
34,901,407✔
812
                   drb->curr_buffer = drb->buffer ;
916✔
813
                } else {
814
                   drb->curr_buffer += param_size ;
34,900,491✔
815
                }
816
                /**
817
                 * While the typical idiom is something like:
818
                 * 1. previous_value = current_value
819
                 * 2. current_value = new_value
820
                 * That is incorrect here, as curr_buffer is a pointer that has already been
821
                 * incremented to the next value's location. We therefore set *curr_buffer and
822
                 * *last_value to the same value, which results in the DR_Changes_Step loop above
823
                 * correctly using this value as the first point of the step change on the next
824
                 * call to this function.
825
                 */
826
                switch ( param_size ) {
34,901,407✔
827
                    case 8:
33,945,747✔
828
                        *(int64_t *)drb->last_value = *(int64_t *)drb->curr_buffer = *(int64_t *)ref->address ;
33,945,747✔
829
                        break ;
33,945,747✔
830
                    case 4:
954,940✔
831
                        *(int32_t *)drb->last_value = *(int32_t *)drb->curr_buffer = *(int32_t *)ref->address ;
954,940✔
832
                        break ;
954,940✔
833
                    case 2:
650✔
834
                        *(int16_t *)drb->last_value = *(int16_t *)drb->curr_buffer = *(int16_t *)ref->address ;
650✔
835
                        break ;
650✔
836
                    case 1:
70✔
837
                        *(int8_t *)drb->last_value = *(int8_t *)drb->curr_buffer = *(int8_t *)ref->address ;
70✔
838
                        break ;
70✔
839
                    default:
×
840
                        memcpy( drb->curr_buffer , ref->address , param_size ) ;
×
841
                        memcpy( drb->last_value , drb->curr_buffer , param_size ) ;
×
842
                        break ;
×
843
                }
844
            }
845
            buffer_num++ ;
4,754,042✔
846
        }
847
    }
848

849
    long long curr_tics = (long long)round(in_time * Trick::JobData::time_tic_value);
4,754,836✔
850
    advance_log_tics_given_curr_tic(curr_tics);
4,754,836✔
851

852
    write_job->next_tics = calculate_next_logging_tic(curr_tics);
4,754,836✔
853
    write_job->cycle_tics = write_job->next_tics - curr_tics;
4,754,836✔
854
    write_job->cycle = (double)write_job->cycle_tics / Trick::JobData::time_tic_value;
4,754,836✔
855

856
    return(0) ;
4,754,836✔
857
}
858

859
bool Trick::DataRecordGroup::check_if_rates_are_valid()
49✔
860
{
861
    // long long curr_tics = exec_get_time_tics();
862
    long long tic_value = Trick::JobData::time_tic_value;
49✔
863
    bool areValid = true;
49✔
864

865
    for(size_t ii = 0; ii < logging_rates.size(); ++ii)
104✔
866
    {
867
        double logging_rate = logging_rates[ii].rate_in_seconds;
55✔
868
        int ret = check_if_rate_is_valid(logging_rate);
55✔
869
        if(ret != 0){
55✔
NEW
870
            emit_rate_error(ret, ii, logging_rate);
×
NEW
871
            areValid = false;
×
872
        }
873
    }
874
    return areValid;
49✔
875
}
876

877
void Trick::DataRecordGroup::emit_rate_error(int rate_err_code, size_t log_idx, double err_rate)
3✔
878
{
879
    long long tic_value = Trick::JobData::time_tic_value;
3✔
880
    if(rate_err_code == 1) {
3✔
881
        message_publish(
1✔
882
            MSG_ERROR,
883
            "DataRecordGroup ERROR: DR Group \"%s\" : Cycle for %lu logging rate idx is less than time tic value. cycle = "
884
            "%16.12f, time_tic = %16.12f\n",
885
            group_name.c_str(),
886
            log_idx,
887
            err_rate,
888
            tic_value);
889
    } else if(rate_err_code == 2)
2✔
890
    {
891
        long long cycle_tics = (long long)round(err_rate * tic_value);
2✔
892
        message_publish(MSG_ERROR,
2✔
893
                        "DataRecordGroup ERROR: DR Group \"%s\" : Cycle for %lu logging rate idx cannot be exactly scheduled "
894
                        "with time tic value. "
895
                        "cycle = %16.12f, cycle_tics = %lld , time_tic = %16.12f\n",
896
                        group_name.c_str(),
897
                        log_idx,
898
                        err_rate,
899
                        cycle_tics,
900
                        1.0 / tic_value);
901
    }
902
}
3✔
903

904
 int Trick::DataRecordGroup::check_if_rate_is_valid(double test_rate)
67✔
905
 {
906
    long long tic_value = Trick::JobData::time_tic_value;
67✔
907
    int ret = 0;
67✔
908

909
    double logging_rate = test_rate;
67✔
910
    if(logging_rate < (1.0 / tic_value))
67✔
911
    {
912
        ret = 1;        
1✔
913
    } else {        
914
        /* Calculate the if the cycle_tics would be a whole number  */
915
        double test_rem = fmod(logging_rate * (double)tic_value, 1.0);
66✔
916

917
        if(test_rem > 0.001)
66✔
918
        {
919
            ret = 2;
2✔
920
        }
921
    }
922
    
923
    return ret;
67✔
924
 }
925

926

927
/**
928
 * Loop through the required logging rates and calculate the
929
 * next logging time in tics.
930
 * @return Next logging time in tics,
931
 */
932
long long Trick::DataRecordGroup::calculate_next_logging_tic(long long min_tic)
4,754,903✔
933
{
934
    long long ticOfCycleToProcess = std::numeric_limits<long long>::max();
4,754,903✔
935

936
    // Loop over all the logging rates. If the logging rate's next tic is equal to the min_tic, test against
937
    // that rate's next cycle from min. Find the smallest next tic 
938
    for(size_t cycleIndex = 0; cycleIndex < logging_rates.size(); ++cycleIndex)
26,790,545✔
939
    {
940
        long long logNextTic = logging_rates[cycleIndex].next_cycle_in_tics;
22,035,642✔
941

942
        if(logNextTic == min_tic)
22,035,642✔
943
        {
944
            logNextTic += logging_rates[cycleIndex].rate_in_tics;
64✔
945
        }
946

947
        if(logNextTic < ticOfCycleToProcess)
22,035,642✔
948
        {
949
            ticOfCycleToProcess = logNextTic;
15,512,125✔
950
        }
951
    }
952

953
    return ticOfCycleToProcess;
4,754,903✔
954
}
955

956
/**
957
 * Loop through the required logging rates and advance the next cycle tics of matching rates
958
 * @param curr_tic_in - time in tics to match and advance the next cycle tic
959
 */
960
void Trick::DataRecordGroup::advance_log_tics_given_curr_tic(long long curr_tic_in)
4,754,845✔
961
{
962
    for(size_t cycleIndex = 0; cycleIndex < logging_rates.size(); ++cycleIndex)
26,790,414✔
963
    {
964
        long long & logNextTic = logging_rates[cycleIndex].next_cycle_in_tics;
22,035,569✔
965
        logNextTic = LoggingCycle::calc_next_tics_on_or_after_input_tic(curr_tic_in, logging_rates[cycleIndex].rate_in_tics);
22,035,569✔
966
        if(logNextTic <= curr_tic_in)
22,035,569✔
967
        {
968
            logNextTic += logging_rates[cycleIndex].rate_in_tics;
6,920,272✔
969
        }
970
    }
971
}
4,754,845✔
972

973
int Trick::DataRecordGroup::write_data(bool must_write) {
512,462✔
974

975
    unsigned int local_buffer_num ;
976
    unsigned int num_to_write ;
977
    unsigned int writer_offset ;
978

979
    if ( record and inited and (buffer_type == DR_No_Buffer or must_write) and (total_bytes_written <= max_file_size)) {
512,462✔
980

981
        // buffer_mutex is used in this one place to prevent forced calls of write_data
982
        // to not overwrite data being written by the asynchronous thread.
983
        pthread_mutex_lock(&buffer_mutex) ;
76,129✔
984

985
        local_buffer_num = buffer_num ;
76,129✔
986
        if ( (local_buffer_num - writer_num) > max_num ) {
76,129✔
987
            num_to_write = max_num ;
×
988
        } else {
989
            num_to_write = (local_buffer_num - writer_num) ;
76,129✔
990
        }
991
        writer_num = local_buffer_num - num_to_write ;
76,129✔
992

993
        //! This loop pulls a "row" of time homogeneous data and writes it to the file
994
        while ( writer_num != local_buffer_num ) {
4,830,171✔
995

996
            writer_offset = writer_num % max_num ;
4,754,042✔
997
            //! keep record of bytes written to file. Default max is 1GB
998
            total_bytes_written += format_specific_write_data(writer_offset) ;
4,754,042✔
999
            writer_num++ ;
4,754,042✔
1000

1001
        }
1002

1003
        if(!max_size_warning && (total_bytes_written > max_file_size)) {
76,129✔
1004
            std::cerr << "WARNING: Data record max file size " << (static_cast<double>(max_file_size))/(1<<20) << "MB reached.\n"
×
1005
            "https://nasa.github.io/trick/documentation/simulation_capabilities/Data-Record#changing-the-max-file-size-of-a-data-record-group-ascii-and-binary-only" 
×
1006
            << std::endl;
×
1007
            max_size_warning = true;
×
1008
        }
1009

1010
        pthread_mutex_unlock(&buffer_mutex) ;
76,129✔
1011

1012
    }
1013

1014
    return 0 ;
512,460✔
1015
}
1016

1017
int Trick::DataRecordGroup::enable() {
38✔
1018
    record = true ;
38✔
1019
    return(0) ;
38✔
1020
}
1021

1022
int Trick::DataRecordGroup::disable() {
×
1023
    record = false ;
×
1024
    return(0) ;
×
1025
}
1026

1027
int Trick::DataRecordGroup::shutdown() {
55✔
1028

1029
    // Force write out all data
1030
    record = true ; // If user disabled group, make sure any recorded data gets written out
55✔
1031
    write_data(true) ;
55✔
1032
    format_specific_shutdown() ;
55✔
1033

1034
    remove_all_variables();
55✔
1035

1036
    // remove_all_variables does not remove sim time
1037
    if(!rec_buffer.empty()){
55✔
1038
        delete rec_buffer[0];
55✔
1039
        rec_buffer.clear();
55✔
1040
    }
1041

1042
    if ( writer_buff ) {
55✔
1043
        free(writer_buff) ;
55✔
1044
        writer_buff = NULL ;
55✔
1045
    }
1046

1047
    return 0 ;
55✔
1048
}
1049

1050
std::string Trick::DataRecordGroup::type_string( int item_type, int item_size ) {
638✔
1051
    switch (item_type) {
638✔
1052
        case TRICK_CHARACTER:
2✔
1053
                return "char";
2✔
1054
                break;
1055
        case TRICK_UNSIGNED_CHARACTER:
2✔
1056
                return "unsigned_char";
2✔
1057
                break;
1058
        case TRICK_STRING:
×
1059
                return "string";
×
1060
                break;
1061
        case TRICK_SHORT:
5✔
1062
                return "short";
5✔
1063
                break;
1064
        case TRICK_UNSIGNED_SHORT:
5✔
1065
                return "unsigned_short";
5✔
1066
                break;
1067
        case TRICK_ENUMERATED:
77✔
1068
        case TRICK_INTEGER:
1069
                return "int";
77✔
1070
                break;
1071
        case TRICK_UNSIGNED_INTEGER:
5✔
1072
                return "unsigned_int";
5✔
1073
                break;
1074
        case TRICK_LONG:
5✔
1075
                return "long";
5✔
1076
                break;
1077
        case TRICK_UNSIGNED_LONG:
5✔
1078
                return "unsigned_long";
5✔
1079
                break;
1080
        case TRICK_FLOAT:
5✔
1081
                return "float";
5✔
1082
                break;
1083
        case TRICK_DOUBLE:
460✔
1084
                if ( single_prec_only ) {
460✔
1085
                        return "float";
×
1086
                }
1087
                else {
1088
                        return "double";
460✔
1089
                }
1090
                break;
1091
        case TRICK_BITFIELD:
28✔
1092
                if (item_size == sizeof(int)) {
28✔
1093
                        return "int";
28✔
1094
                } else if (item_size == sizeof(short)) {
×
1095
                        return "short";
×
1096
                } else {
1097
                        return "char";
×
1098
                }
1099
                break;
1100
        case TRICK_UNSIGNED_BITFIELD:
28✔
1101
                if (item_size == sizeof(int)) {
28✔
1102
                        return "unsigned_int";
28✔
1103
                } else if (item_size == sizeof(short)) {
×
1104
                        return "unsigned_short";
×
1105
                } else {
1106
                        return "unsigned_char";
×
1107
                }
1108
                break;
1109
        case TRICK_LONG_LONG:
5✔
1110
                return "long_long";
5✔
1111
                break;
1112
        case TRICK_UNSIGNED_LONG_LONG:
2✔
1113
                return "unsigned_long_long";
2✔
1114
                break;
1115
        case TRICK_BOOLEAN:
4✔
1116
#if ( __sun | __APPLE__ )
1117
                return "int";
1118
#else
1119
                return "unsigned_char";
4✔
1120
#endif
1121
                break;
1122
    }
1123
    return "";
×
1124
}
1125

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