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

polserver / polserver / 13378462461

17 Feb 2025 08:59PM UTC coverage: 58.754% (+0.3%) from 58.475%
13378462461

push

github

web-flow
Add support for sequence and index bindings (#760)

* update grammar

* add ast nodes; ast building

* Rename to unpacking

* semantic analysis

* executor work part 1, unpacking indices

* renamings; implement index binding

* small cleanup

* use multi_index; more tests

* use multi_index only for rest, otherwise list

* initial formatting

* add missing token decoding

* address self-review comments

* formatting tweaks

* add test for var binding in classes

* add StringIterator

* fix spread tests

* add cfgfile iterator; add cfgelem opersubscript; tests

* add iterator for SQLResultSet and SQLRow; tests

* Copy value in take global/local

* Allow any iterable can index rest unpacking
Always use dictionary as rest object in index unpacking

* add docs, core-changes, doc tests

* formatting changes...

* address self-review comments

* update formatter, format all binding test srcs

* address review comments
- unset var scope

* add cfgfile/cfgelem docs

* reformat objref svg

501 of 550 new or added lines in 19 files covered. (91.09%)

14 existing lines in 3 files now uncovered.

42235 of 71885 relevant lines covered (58.75%)

377989.47 hits per line

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

55.52
/pol-core/pol/sqlscrobj.cpp
1
/** @file
2
 *
3
 * @par History
4
 */
5

6

7
#include "sqlscrobj.h"
8

9
#include <exception>
10
#include <regex>
11
#include <string.h>
12

13
#include "../bscript/bobject.h"
14

15

16
#ifdef HAVE_MYSQL
17

18
#include "../bscript/berror.h"
19
#include "../bscript/contiter.h"
20
#include "../bscript/impstr.h"
21
#include "../bscript/objmembers.h"
22
#include "../bscript/objmethods.h"
23
#include "../clib/esignal.h"
24
#include "../clib/logfacility.h"
25
#include "../clib/threadhelp.h"
26
#include "globals/network.h"
27

28
namespace Pol
29
{
30
namespace Core
31
{
32
using namespace Bscript;
33

34
BSQLRow::BSQLRow( BSQLResultSet* resultset ) : PolObjectImp( OTSQLRow )
10✔
35
{
36
  _result = resultset->_result;
10✔
37
  _row = mysql_fetch_row( _result->ptr() );
10✔
38
  _fields = mysql_fetch_fields( _result->ptr() );
10✔
39
}
10✔
40
BSQLRow::BSQLRow( RES_WRAPPER result ) : PolObjectImp( OTSQLRow )
×
41
{
42
  _result = result;
×
43
  _row = mysql_fetch_row( _result->ptr() );
×
44
  _fields = mysql_fetch_fields( _result->ptr() );
×
45
}
×
46
BSQLRow::BSQLRow( RES_WRAPPER result, MYSQL_ROW row, MYSQL_FIELD* fields )
×
47
    : PolObjectImp( OTSQLRow ), _row( row ), _result( result ), _fields( fields )
×
48
{
49
}
×
50

51
class SQLRowIterator final : public Bscript::ContIterator
52
{
53
public:
54
  SQLRowIterator( BSQLRow* node, Bscript::BObject* pIter );
55
  virtual Bscript::BObject* step() override;
56

57
private:
58
  Bscript::BObject m_RowObj;
59
  BSQLRow* rowimp;
60
  Bscript::BObjectRef m_IterVal;
61
  unsigned int index;
62
};
63

64
SQLRowIterator::SQLRowIterator( BSQLRow* row, Bscript::BObject* pIter )
4✔
65
    : ContIterator(), m_RowObj( row ), rowimp( row ), m_IterVal( pIter ), index( 0 )
4✔
66
{
67
}
4✔
68

69
Bscript::BObject* SQLRowIterator::step()
10✔
70
{
71
  unsigned int num_fields = mysql_num_fields( rowimp->_result->ptr() );
10✔
72
  const auto& fields = rowimp->_fields;
10✔
73
  const auto& row = rowimp->_row;
10✔
74

75
  if ( index >= num_fields )
10✔
76
    return nullptr;
2✔
77

78
  m_IterVal->setimp( new String( fields[index].name ) );
8✔
79

80
  if ( IS_NUM( fields[index].type ) && fields[index].type != MYSQL_TYPE_TIMESTAMP )
8✔
81
  {
82
    if ( fields[index].type == MYSQL_TYPE_DECIMAL || fields[index].type == MYSQL_TYPE_NEWDECIMAL ||
4✔
83
         fields[index].type == MYSQL_TYPE_FLOAT || fields[index].type == MYSQL_TYPE_DOUBLE )
4✔
NEW
84
      return new BObject( new Double( strtod( row[index++], nullptr ) ) );
×
85
    return new BObject( new BLong( strtoul( row[index++], nullptr, 0 ) ) );
4✔
86
  }
87

88
  return new BObject( new String( row[index++], String::Tainted::YES ) );
4✔
89
}
90

91
ContIterator* BSQLRow::createIterator( Bscript::BObject* pIterVal )
4✔
92
{
93
  if ( !_result )
4✔
94
  {
NEW
95
    return BObjectImp::createIterator( pIterVal );
×
96
  }
97
  return new SQLRowIterator( this, pIterVal );
4✔
98
}
99

100
BObjectRef BSQLRow::OperSubscript( const BObject& obj )
6✔
101
{
102
  const Bscript::BObjectImp& right = obj.impref();
6✔
103
  if ( _result == nullptr )
6✔
104
    return BObjectRef( new BError( "No result" ) );
×
105
  unsigned int num_fields = mysql_num_fields( _result->ptr() );
6✔
106
  if ( right.isa( OTLong ) )  // vector
6✔
107
  {
108
    BLong& lng = (BLong&)right;
6✔
109

110
    unsigned index = (unsigned)lng.value();
6✔
111
    if ( index > num_fields || index <= 0 )
6✔
112
    {
113
      return BObjectRef( new BError( "Index out of bounds" ) );
×
114
    }
115
    else if ( _row[index - 1] == nullptr )
6✔
116
    {
117
      return BObjectRef( UninitObject::create() );
×
118
    }
119
    else if ( IS_NUM( _fields[index - 1].type ) && _fields[index - 1].type != MYSQL_TYPE_TIMESTAMP )
6✔
120
    {
121
      if ( _fields[index - 1].type == MYSQL_TYPE_DECIMAL ||
×
122
           _fields[index - 1].type == MYSQL_TYPE_NEWDECIMAL ||
×
123
           _fields[index - 1].type == MYSQL_TYPE_FLOAT ||
×
124
           _fields[index - 1].type == MYSQL_TYPE_DOUBLE )
×
125
        return BObjectRef( new Double( strtod( _row[index - 1], nullptr ) ) );
×
126
      return BObjectRef( new BLong( strtoul( _row[index - 1], nullptr, 0 ) ) );
×
127
    }
128
    return BObjectRef( new String( _row[index - 1], String::Tainted::YES ) );
6✔
129
  }
130
  else if ( right.isa( OTString ) )
×
131
  {
132
    String& string = (String&)right;
×
133
    for ( unsigned int i = 0; i < num_fields; i++ )
×
134
    {
135
      if ( !strncmp( _fields[i].name, string.data(), _fields[i].name_length ) )
×
136
      {
137
        if ( _row[i] == nullptr )
×
138
        {
139
          return BObjectRef( UninitObject::create() );
×
140
        }
141
        else if ( IS_NUM( _fields[i].type ) && _fields[i].type != MYSQL_TYPE_TIMESTAMP )
×
142
        {
143
          if ( _fields[i].type == MYSQL_TYPE_DECIMAL || _fields[i].type == MYSQL_TYPE_NEWDECIMAL ||
×
144
               _fields[i].type == MYSQL_TYPE_FLOAT || _fields[i].type == MYSQL_TYPE_DOUBLE )
×
145
            return BObjectRef( new Double( strtod( _row[i], nullptr ) ) );
×
146
          return BObjectRef( new BLong( strtoul( _row[i], nullptr, 0 ) ) );
×
147
        }
148
        return BObjectRef( new String( _row[i], String::Tainted::YES ) );
×
149
      }
150
    }
151
    return BObjectRef( new BError( "Column does not exist" ) );
×
152
  }
153
  else
154
  {
155
    return BObjectRef( new BError( "SQLRow keys must be integer" ) );
×
156
  }
157
}
158
BSQLRow::~BSQLRow() {}
20✔
159
Bscript::BObjectImp* BSQLRow::copy() const
×
160
{
161
  return new BSQLRow( _result, _row, _fields );
×
162
}
163

164
BSQLResultSet::BSQLResultSet( RES_WRAPPER result )
4✔
165
    : Bscript::BObjectImp( OTSQLResultSet ),
166
      _result( result ),
4✔
167
      _fields( nullptr ),
4✔
168
      _affected_rows( 0 )
4✔
169
{
170
  if ( result->ptr() != nullptr )
4✔
171
    _fields = mysql_fetch_fields( result->ptr() );
4✔
172
}
4✔
173
BSQLResultSet::BSQLResultSet( RES_WRAPPER result, MYSQL_FIELD* fields )
×
174
    : Bscript::BObjectImp( OTSQLResultSet ),
175
      _result( result ),
×
176
      _fields( fields ),
×
177
      _affected_rows( 0 )
×
178
{
179
}
×
180
BSQLResultSet::BSQLResultSet( int affected_rows )
4✔
181
    : Bscript::BObjectImp( OTSQLResultSet ),
182
      _result( nullptr ),
4✔
183
      _fields( nullptr ),
4✔
184
      _affected_rows( affected_rows )
4✔
185
{
186
}
4✔
187

188
class SQLResultSetIterator final : public Bscript::ContIterator
189
{
190
public:
191
  SQLResultSetIterator( BSQLResultSet* node, Bscript::BObject* pIter );
192
  virtual Bscript::BObject* step() override;
193

194
private:
195
  Bscript::BObject m_ResultsObj;
196
  BSQLResultSet* results;
197
  Bscript::BObjectRef m_IterVal;
198
  BLong* m_pIterVal;
199
};
200

201
SQLResultSetIterator::SQLResultSetIterator( BSQLResultSet* results, Bscript::BObject* pIterVal )
2✔
202
    : ContIterator(),
203
      m_ResultsObj( results ),
2✔
204
      results( results ),
2✔
205
      m_IterVal( pIterVal ),
2✔
206
      m_pIterVal( new BLong( 0 ) )
4✔
207
{
208
  m_IterVal.get()->setimp( m_pIterVal );
2✔
209
}
2✔
210

211
Bscript::BObject* SQLResultSetIterator::step()
6✔
212
{
213
  if ( m_pIterVal->value() >= mysql_num_rows( results->_result->ptr() ) )
6✔
214
    return nullptr;
2✔
215

216
  m_pIterVal->increment();
4✔
217
  return new BObject( new BSQLRow( results ) );
4✔
218
}
219

220
ContIterator* BSQLResultSet::createIterator( Bscript::BObject* pIterVal )
2✔
221
{
222
  if ( !_result )
2✔
223
  {
NEW
224
    return BObjectImp::createIterator( pIterVal );
×
225
  }
226
  return new SQLResultSetIterator( this, pIterVal );
2✔
227
}
228

UNCOV
229
const char* BSQLResultSet::field_name( unsigned int index ) const
×
230
{
231
  if ( !_result || _result->ptr() == nullptr )
×
232
    return nullptr;
×
233
  if ( index <= 0 || index > mysql_num_fields( _result->ptr() ) )
×
234
  {
235
    return nullptr;
×
236
  }
237
  return _fields[index - 1].name;
×
238
}
239
int BSQLResultSet::num_rows() const
6✔
240
{
241
  if ( !_result )
6✔
242
    return 0;
×
243
  return static_cast<int>( mysql_num_rows( _result->ptr() ) );
6✔
244
};
245

246
bool BSQLResultSet::has_result() const
6✔
247
{
248
  return _result && _result->ptr();
6✔
249
}
250

251
Bscript::BObjectImp* BSQLResultSet::copy() const
×
252
{
253
  if ( _affected_rows )
×
254
    return new BSQLResultSet( _affected_rows );
×
255
  else
256
    return new BSQLResultSet( _result, _fields );
×
257
};
258

259
int BSQLResultSet::num_fields() const
×
260
{
261
  if ( _result && _result->ptr() != nullptr )
×
262
    return mysql_num_fields( _result->ptr() );
×
263
  return 0;
×
264
}
265
int BSQLResultSet::affected_rows() const
×
266
{
267
  return _affected_rows;
×
268
}
269
BSQLResultSet::~BSQLResultSet() {}
16✔
270
bool BSQLResultSet::isTrue() const
4✔
271
{
272
  return true;
4✔
273
}
274
std::string BSQLResultSet::getStringRep() const
×
275
{
276
  return "SQLResultSet";
×
277
}
278

279
bool BSQLConnection::close()
×
280
{
281
  _conn->set( nullptr );
×
282
  return true;
×
283
}
284
Bscript::BObjectImp* BSQLConnection::getResultSet() const
8✔
285
{
286
  if ( _errno )
8✔
287
    return new BError( _error );
×
288
  RES_WRAPPER result = std::make_shared<ResultWrapper>( mysql_store_result( _conn->ptr() ) );
8✔
289
  if ( result && result->ptr() != nullptr )  // there are rows
8✔
290
  {
291
    return new BSQLResultSet( result );
4✔
292
    // retrieve rows, then call mysql_free_result(result)
293
  }
294
  else  // mysql_store_result() returned nothing; should it have?
295
  {
296
    /*  if (mysql_errno(_conn))
297
        {
298
          _error = mysql_error(_conn);
299
          _errno = mysql_errno(_conn);
300
          return new BError(_error);
301
        }
302
        else */
303
    if ( mysql_field_count( _conn->ptr() ) == 0 )
4✔
304
    {
305
      return new BSQLResultSet( static_cast<int>( mysql_affected_rows( _conn->ptr() ) ) );
4✔
306
    }
307
  }
308
  return new BError( "Unknown error getting ResultSet" );
×
309
}
8✔
310
BSQLConnection::BSQLConnection()
1✔
311
    : PolObjectImp( OTSQLConnection ), _conn( new ConnectionWrapper ), _errno( 0 )
1✔
312
{
313
  _conn->set( mysql_init( nullptr ) );
1✔
314
  if ( !_conn->ptr() )
1✔
315
  {
316
    _error = "Insufficient memory";
×
317
    _errno = 1;
×
318
  }
319
}
1✔
320

321
BSQLConnection::BSQLConnection( std::shared_ptr<ConnectionWrapper> conn )
×
322
    : PolObjectImp( OTSQLConnection ), _conn( conn ), _errno( 0 )
×
323
{
324
}
×
325

326
BSQLConnection::~BSQLConnection() {}
2✔
327
std::string BSQLConnection::getStringRep() const
×
328
{
329
  return "SQLConnection";
×
330
}
331
bool BSQLConnection::isTrue() const
1✔
332
{
333
  if ( !_conn->ptr() )
1✔
334
    return false;  // closed by hand
×
335
  if ( !mysql_ping( _conn->ptr() ) )
1✔
336
    return true;
1✔
337
  return false;
×
338
}
339
bool BSQLConnection::connect( const char* host, const char* user, const char* passwd, int port )
1✔
340
{
341
  if ( !_conn->ptr() )
1✔
342
  {
343
    _errno = -1;
×
344
    _error = "No active MYSQL object instance.";
×
345
    return false;
×
346
  }
347
  // port == 0 means default sql port
348
  if ( !mysql_real_connect( _conn->ptr(), host, user, passwd, nullptr, port, nullptr, 0 ) )
1✔
349
  {
350
    _errno = mysql_errno( _conn->ptr() );
×
351
    _error = mysql_error( _conn->ptr() );
×
352
    return false;
×
353
  }
354
  return true;
1✔
355
}
356
bool BSQLConnection::select_db( const char* db )
1✔
357
{
358
  if ( !_conn->ptr() )
1✔
359
  {
360
    _errno = -1;
×
361
    _error = "No active MYSQL object instance.";
×
362
    return false;
×
363
  }
364
  else if ( mysql_select_db( _conn->ptr(), db ) )
1✔
365
  {
366
    _errno = mysql_errno( _conn->ptr() );
×
367
    _error = mysql_error( _conn->ptr() );
×
368
    return false;
×
369
  }
370
  return true;
1✔
371
}
372

373
bool BSQLConnection::query( const std::string query )
8✔
374
{
375
  if ( !_conn->ptr() )
8✔
376
  {
377
    _errno = -1;
×
378
    _error = "No active MYSQL object instance.";
×
379
    return false;
×
380
  }
381

382
  if ( mysql_query( _conn->ptr(), query.c_str() ) )
8✔
383
  {
384
    _errno = mysql_errno( _conn->ptr() );
×
385
    _error = mysql_error( _conn->ptr() );
×
386
    return false;
×
387
  }
388

389
  return true;
8✔
390
}
391

392
/*
393
 * Allows binding parameters to the query
394
 * Every occurrence of "?" is replaced with a single parameter
395
 */
396
bool BSQLConnection::query( const std::string query, QueryParams params )
8✔
397
{
398
  if ( params == nullptr || !params->size() )
8✔
399
    return this->query( query );
7✔
400

401
  if ( !_conn->ptr() )
1✔
402
  {
403
    _errno = -1;
×
404
    _error = "No active MYSQL object instance.";
×
405
    return false;
×
406
  }
407

408
  std::string replaced = query;
1✔
409
  std::regex re( "^((?:[^']|'[^']*')*?)(\\?)" );
1✔
410
  for ( auto it = params->begin(); it != params->end(); ++it )
3✔
411
  {
412
    if ( !std::regex_search( replaced, re ) )
2✔
413
    {
414
      _errno = -2;
×
415
      _error = "Could not replace parameters.";
×
416
      return false;
×
417
    }
418

419
    if ( it->size() > ( std::numeric_limits<size_t>::max() - 5 ) / 2 )
2✔
420
    {
421
      _errno = -3;
×
422
      _error = "Parameter is too long.";
×
423
    }
424

425
    // Escape the string and add quoting. A bit tricky, but effective.
426
    size_t escaped_max_size =
427
        it->size() * 2 + 5;  // max is +1, using +5 to leave space for quoting and "$1"
2✔
428
    std::unique_ptr<char[]> escptr(
429
        new char[escaped_max_size] );  // will contain the escaped string
2✔
430
    // use +3 to leave space for quoting
431
    unsigned long esclen = mysql_real_escape_string( _conn->ptr(), escptr.get() + 3, it->c_str(),
2✔
432
                                                     static_cast<unsigned long>( it->size() ) );
2✔
433
    // Now add quoting, equivalent to escptr = "$1'" + escptr + "'"
434
    esclen += 4;
2✔
435
    escptr[0] = '$';
2✔
436
    escptr[1] = '1';
2✔
437
    escptr[2] = '\'';
2✔
438
    escptr[esclen - 1] = '\'';
2✔
439
    escptr[esclen] = '\0';
2✔
440

441
    replaced =
442
        std::regex_replace( replaced, re, escptr.get(), std::regex_constants::format_first_only );
2✔
443
  }
2✔
444

445
  return this->query( replaced );
1✔
446
}
1✔
447

448
bool BSQLConnection::escape_string( const std::string& text, std::string* escaped ) const
1✔
449
{
450
  if ( !_conn->ptr() )
1✔
451
    return false;
×
452
  *escaped = std::string( text.size() * 2 + 1, '\0' );
1✔
453
  if ( mysql_real_escape_string( _conn->ptr(), escaped->data(), text.data(),
1✔
454
                                 (unsigned long)text.size() ) == (unsigned long)-1 )
2✔
455
    return false;
×
456
  escaped->resize( escaped->find_first_of( '\0' ) );
1✔
457
  return true;
1✔
458
}
459

460
std::string BSQLConnection::getLastError() const
×
461
{
462
  return _error;
×
463
}
464
int BSQLConnection::getLastErrNo() const
1✔
465
{
466
  return _errno;
1✔
467
}
468
std::shared_ptr<BSQLConnection::ConnectionWrapper> BSQLConnection::getConnection() const
×
469
{
470
  return _conn;
×
471
}
472

473

474
BObjectRef BSQLConnection::get_member_id( const int /*id*/ )  // id test
×
475
{
476
  return BObjectRef( UninitObject::create() );
×
477
  // switch(id)
478
  //{
479

480
  //  default: return BObjectRef(UninitObject::create());
481
  //}
482
}
483
BObjectRef BSQLConnection::get_member( const char* membername )
×
484
{
485
  ObjMember* objmember = getKnownObjMember( membername );
×
486
  if ( objmember != nullptr )
×
487
    return this->get_member_id( objmember->id );
×
488
  else
489
    return BObjectRef( UninitObject::create() );
×
490
}
491

492
Bscript::BObjectImp* BSQLConnection::call_polmethod( const char* methodname, UOExecutor& ex )
×
493
{
494
  ObjMethod* objmethod = getKnownObjMethod( methodname );
×
495
  if ( objmethod != nullptr )
×
496
    return this->call_polmethod_id( objmethod->id, ex );
×
497
  else
498
    return nullptr;
×
499
}
500

501
Bscript::BObjectImp* BSQLConnection::call_polmethod_id( const int /*id*/, UOExecutor& /*ex*/,
×
502
                                                        bool /*forcebuiltin*/ )
503
{
504
  return new BLong( 0 );
×
505
}
506

507
Bscript::BObjectImp* BSQLConnection::copy() const
×
508
{
509
  return new BSQLConnection( _conn );
×
510
}
511

512
BSQLConnection::ConnectionWrapper::ConnectionWrapper() : _conn( nullptr ) {}
1✔
513
BSQLConnection::ConnectionWrapper::~ConnectionWrapper()
1✔
514
{
515
  if ( _conn )
1✔
516
    mysql_close( _conn );
1✔
517
  _conn = nullptr;
1✔
518
}
1✔
519
void BSQLConnection::ConnectionWrapper::set( MYSQL* conn )
1✔
520
{
521
  if ( _conn )
1✔
522
    mysql_close( _conn );
×
523
  _conn = conn;
1✔
524
}
1✔
525
MYSQL* BSQLConnection::ConnectionWrapper::ptr()
44✔
526
{
527
  return _conn;
44✔
528
};
529

530
ResultWrapper::ResultWrapper( MYSQL_RES* res ) : _result( res ) {}
8✔
531
ResultWrapper::ResultWrapper() : _result( nullptr ) {}
×
532
ResultWrapper::~ResultWrapper()
8✔
533
{
534
  if ( _result )
8✔
535
    mysql_free_result( _result );
4✔
536
  _result = nullptr;
8✔
537
}
8✔
538
void ResultWrapper::set( MYSQL_RES* result )
×
539
{
540
  if ( _result )
×
541
    mysql_free_result( _result );
×
542
  _result = result;
×
543
}
×
544
MYSQL_RES* ResultWrapper::ptr()
70✔
545
{
546
  return _result;
70✔
547
}
548

549

550
void sql_service_thread_stub()
2✔
551
{
552
  try
553
  {
554
    networkManager.sql_service->start();
2✔
555
  }
556
  catch ( const char* msg )
×
557
  {
558
    POLLOGLN( "SQL Thread exits due to exception: {}", msg );
×
559
    throw;
×
560
  }
×
561
  catch ( std::string& str )
×
562
  {
563
    POLLOGLN( "SQL Thread exits due to exception: {}", str );
×
564
    throw;
×
565
  }
×
566
  catch ( std::exception& ex )
×
567
  {
568
    POLLOGLN( "SQL Thread exits due to exception: {}", ex.what() );
×
569
    throw;
×
570
  }
×
571
}
2✔
572

573
SQLService::SQLService() {}
3✔
574
SQLService::~SQLService() {}
3✔
575
void SQLService::stop()
2✔
576
{
577
  _msgs.cancel();
2✔
578
}
2✔
579
void SQLService::push( msg&& msg_ )
10✔
580
{
581
  _msgs.push_move( std::move( msg_ ) );
10✔
582
}
10✔
583
void SQLService::start()  // executed inside a extra thread
2✔
584
{
585
  while ( !Clib::exit_signalled )
12✔
586
  {
587
    try
588
    {
589
      msg task;
12✔
590
      _msgs.pop_wait( &task );
12✔
591
      task();
10✔
592
    }
12✔
593
    catch ( msg_queue::Canceled& )
2✔
594
    {
595
      break;
2✔
596
    }
2✔
597
    // ignore remaining tasks
598
  }
599
}
2✔
600

601

602
void start_sql_service()
2✔
603
{
604
  threadhelp::start_thread( sql_service_thread_stub, "SQLService" );
2✔
605
}
2✔
606
}  // namespace Core
607
}  // namespace Pol
608
#endif
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