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

daisytuner / sdfglib / 17907460602

22 Sep 2025 07:08AM UTC coverage: 60.192% (-0.5%) from 60.653%
17907460602

Pull #233

github

web-flow
Merge 07c7b1d2f into c3f4f7063
Pull Request #233: adds constant returns with type and extends API

60 of 184 new or added lines in 10 files covered. (32.61%)

19 existing lines in 5 files now uncovered.

9514 of 15806 relevant lines covered (60.19%)

105.75 hits per line

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

58.65
/src/builder/sdfg_builder.cpp
1
#include "sdfg/builder/sdfg_builder.h"
2

3
#include "sdfg/types/utils.h"
4

5
namespace sdfg {
6
namespace builder {
7

8
Function& SDFGBuilder::function() const { return static_cast<Function&>(*this->sdfg_); };
226✔
9

10
SDFGBuilder::SDFGBuilder(std::unique_ptr<SDFG>& sdfg)
×
11
    : FunctionBuilder(), sdfg_(std::move(sdfg)) {
×
12

13
      };
×
14

15
SDFGBuilder::SDFGBuilder(const std::string& name, FunctionType type)
50✔
16
    : FunctionBuilder(), sdfg_(new SDFG(name, type)) {
50✔
17

18
      };
50✔
19

20
SDFGBuilder::SDFGBuilder(const std::string& name, FunctionType type, const types::IType& return_type)
×
21
    : FunctionBuilder(), sdfg_(new SDFG(name, type, return_type)) {
×
22

23
      };
×
24

25
SDFG& SDFGBuilder::subject() const { return *this->sdfg_; };
33✔
26

27
std::unique_ptr<SDFG> SDFGBuilder::move() {
13✔
28
#ifndef NDEBUG
29
    this->sdfg_->validate();
13✔
30
#endif
31

32
    return std::move(this->sdfg_);
13✔
33
};
34

35
void SDFGBuilder::rename_container(const std::string& old_name, const std::string& new_name) const {
×
36
    FunctionBuilder::rename_container(old_name, new_name);
×
37

38
    for (auto& entry : this->sdfg_->states_) {
×
39
        entry.second->replace(symbolic::symbol(old_name), symbolic::symbol(new_name));
×
40
    }
41
    for (auto& entry : this->sdfg_->edges_) {
×
42
        entry.second->replace(symbolic::symbol(old_name), symbolic::symbol(new_name));
×
43
    }
44
}
×
45

46
/***** Section: Control-Flow Graph *****/
47

48
control_flow::State& SDFGBuilder::add_state(bool is_start_state, const DebugInfo& debug_info) {
60✔
49
    auto vertex = boost::add_vertex(this->sdfg_->graph_);
60✔
50
    auto res = this->sdfg_->states_.insert(
120✔
51
        {vertex,
60✔
52
         std::unique_ptr<control_flow::State>(new control_flow::State(this->new_element_id(), debug_info, vertex))}
60✔
53
    );
54

55
    assert(res.second);
60✔
56
    (*res.first).second->dataflow_->parent_ = (*res.first).second.get();
60✔
57

58
    if (is_start_state) {
60✔
59
        this->sdfg_->start_state_ = (*res.first).second.get();
12✔
60
    }
12✔
61

62
    return *(*res.first).second;
60✔
63
};
×
64

65
control_flow::State& SDFGBuilder::
66
    add_state_before(const control_flow::State& state, bool is_start_state, const DebugInfo& debug_info) {
1✔
67
    auto& new_state = this->add_state(false, debug_info);
1✔
68

69
    std::vector<const control_flow::InterstateEdge*> to_redirect;
1✔
70
    for (auto& e : this->sdfg_->in_edges(state)) to_redirect.push_back(&e);
2✔
71

72
    // Redirect control-flow
73
    for (auto edge : to_redirect) {
2✔
74
        this->add_edge(edge->src(), new_state, edge->condition());
1✔
75

76
        auto desc = edge->edge();
1✔
77
        this->sdfg_->edges_.erase(desc);
1✔
78
        boost::remove_edge(desc, this->sdfg_->graph_);
1✔
79
    }
80
    this->add_edge(new_state, state);
1✔
81

82
    if (is_start_state) {
1✔
83
        this->sdfg_->start_state_ = &new_state;
×
84
    }
×
85

86
    return new_state;
1✔
87
};
1✔
88

89
control_flow::State& SDFGBuilder::
90
    add_state_after(const control_flow::State& state, bool connect_states, const DebugInfo& debug_info) {
4✔
91
    auto& new_state = this->add_state(false, debug_info);
4✔
92

93
    std::vector<const control_flow::InterstateEdge*> to_redirect;
4✔
94
    for (auto& e : this->sdfg_->out_edges(state)) to_redirect.push_back(&e);
5✔
95

96
    // Redirect control-flow
97
    for (auto& edge : to_redirect) {
5✔
98
        this->add_edge(new_state, edge->dst(), edge->condition());
1✔
99

100
        auto desc = edge->edge();
1✔
101
        this->sdfg_->edges_.erase(desc);
1✔
102
        boost::remove_edge(desc, this->sdfg_->graph_);
1✔
103
    }
104
    if (connect_states) {
4✔
105
        this->add_edge(state, new_state);
3✔
106
    }
3✔
107

108
    return new_state;
4✔
109
};
4✔
110

111
control_flow::ReturnState& SDFGBuilder::add_return_state(const std::string& data, const DebugInfo& debug_info) {
4✔
112
    auto vertex = boost::add_vertex(this->sdfg_->graph_);
4✔
113
    auto res = this->sdfg_->states_.insert(
8✔
114
        {vertex,
4✔
115
         std::unique_ptr<
4✔
116
             control_flow::State>(new control_flow::ReturnState(this->new_element_id(), debug_info, vertex, data))}
4✔
117
    );
118

119
    assert(res.second);
4✔
120
    (*res.first).second->dataflow_->parent_ = (*res.first).second.get();
4✔
121

122
    return static_cast<control_flow::ReturnState&>(*(*res.first).second);
4✔
NEW
123
};
×
124

125
control_flow::ReturnState& SDFGBuilder::
126
    add_return_state_after(const control_flow::State& state, const std::string& data, const DebugInfo& debug_info) {
3✔
127
    auto& new_state = this->add_return_state(data, debug_info);
3✔
128

129
    std::vector<const control_flow::InterstateEdge*> to_redirect;
3✔
130
    for (auto& e : this->sdfg_->out_edges(state)) to_redirect.push_back(&e);
3✔
131

132
    // Redirect control-flow
133
    for (auto& edge : to_redirect) {
3✔
NEW
134
        this->add_edge(new_state, edge->dst(), edge->condition());
×
135

NEW
136
        auto desc = edge->edge();
×
NEW
137
        this->sdfg_->edges_.erase(desc);
×
NEW
138
        boost::remove_edge(desc, this->sdfg_->graph_);
×
139
    }
140
    this->add_edge(state, new_state);
3✔
141

142
    return new_state;
3✔
143
};
3✔
144

NEW
145
control_flow::ReturnState& SDFGBuilder::add_unreachable_state(const DebugInfo& debug_info) {
×
UNCOV
146
    auto vertex = boost::add_vertex(this->sdfg_->graph_);
×
UNCOV
147
    auto res = this->sdfg_->states_.insert(
×
UNCOV
148
        {vertex,
×
NEW
149
         std::unique_ptr<control_flow::State>(new control_flow::ReturnState(this->new_element_id(), debug_info, vertex))
×
150
        }
151
    );
152

NEW
153
    assert(res.second);
×
NEW
154
    (*res.first).second->dataflow_->parent_ = (*res.first).second.get();
×
155

NEW
156
    return static_cast<control_flow::ReturnState&>(*(*res.first).second);
×
NEW
157
};
×
158

159
control_flow::ReturnState& SDFGBuilder::
NEW
160
    add_unreachable_state_after(const control_flow::State& state, const DebugInfo& debug_info) {
×
NEW
161
    auto& new_state = this->add_unreachable_state(debug_info);
×
162

NEW
163
    std::vector<const control_flow::InterstateEdge*> to_redirect;
×
NEW
164
    for (auto& e : this->sdfg_->out_edges(state)) to_redirect.push_back(&e);
×
165

166
    // Redirect control-flow
NEW
167
    for (auto& edge : to_redirect) {
×
NEW
168
        this->add_edge(new_state, edge->dst(), edge->condition());
×
169

NEW
170
        auto desc = edge->edge();
×
NEW
171
        this->sdfg_->edges_.erase(desc);
×
NEW
172
        boost::remove_edge(desc, this->sdfg_->graph_);
×
173
    }
NEW
174
    this->add_edge(state, new_state);
×
175

NEW
176
    return new_state;
×
NEW
177
};
×
178

179
control_flow::ReturnState& SDFGBuilder::
NEW
180
    add_constant_return_state(const std::string& data, const types::IType& type, const DebugInfo& debug_info) {
×
NEW
181
    auto vertex = boost::add_vertex(this->sdfg_->graph_);
×
NEW
182
    auto res = this->sdfg_->states_.insert(
×
NEW
183
        {vertex,
×
NEW
184
         std::unique_ptr<
×
NEW
185
             control_flow::State>(new control_flow::ReturnState(this->new_element_id(), debug_info, vertex, data, type))
×
186
        }
187
    );
188

UNCOV
189
    assert(res.second);
×
UNCOV
190
    (*res.first).second->dataflow_->parent_ = (*res.first).second.get();
×
191

UNCOV
192
    return static_cast<control_flow::ReturnState&>(*(*res.first).second);
×
193
};
×
194

NEW
195
control_flow::ReturnState& SDFGBuilder::add_constant_return_state_after(
×
196
    const control_flow::State& state, const std::string& data, const types::IType& type, const DebugInfo& debug_info
197
) {
NEW
198
    auto& new_state = this->add_constant_return_state(data, type, debug_info);
×
199

UNCOV
200
    std::vector<const control_flow::InterstateEdge*> to_redirect;
×
UNCOV
201
    for (auto& e : this->sdfg_->out_edges(state)) to_redirect.push_back(&e);
×
202

203
    // Redirect control-flow
UNCOV
204
    for (auto& edge : to_redirect) {
×
205
        this->add_edge(new_state, edge->dst(), edge->condition());
×
206

207
        auto desc = edge->edge();
×
208
        this->sdfg_->edges_.erase(desc);
×
209
        boost::remove_edge(desc, this->sdfg_->graph_);
×
210
    }
UNCOV
211
    this->add_edge(state, new_state);
×
212

UNCOV
213
    return new_state;
×
UNCOV
214
};
×
215

216
control_flow::InterstateEdge& SDFGBuilder::
217
    add_edge(const control_flow::State& src, const control_flow::State& dst, const DebugInfo& debug_info) {
41✔
218
    return this->add_edge(src, dst, control_flow::Assignments{}, SymEngine::boolTrue, debug_info);
41✔
219
};
×
220

221
control_flow::InterstateEdge& SDFGBuilder::add_edge(
8✔
222
    const control_flow::State& src,
223
    const control_flow::State& dst,
224
    const symbolic::Condition condition,
225
    const DebugInfo& debug_info
226
) {
227
    return this->add_edge(src, dst, control_flow::Assignments{}, condition, debug_info);
8✔
228
};
×
229

230
control_flow::InterstateEdge& SDFGBuilder::add_edge(
2✔
231
    const control_flow::State& src,
232
    const control_flow::State& dst,
233
    const control_flow::Assignments& assignments,
234
    const DebugInfo& debug_info
235
) {
236
    return this->add_edge(src, dst, assignments, SymEngine::boolTrue, debug_info);
2✔
237
};
×
238

239
control_flow::InterstateEdge& SDFGBuilder::add_edge(
53✔
240
    const control_flow::State& src,
241
    const control_flow::State& dst,
242
    const control_flow::Assignments& assignments,
243
    const symbolic::Condition condition,
244
    const DebugInfo& debug_info
245
) {
246
    if (dynamic_cast<const control_flow::ReturnState*>(&src) != nullptr) {
53✔
247
        throw InvalidSDFGException("Cannot add edge from ReturnState");
×
248
    }
249

250
    for (auto& entry : assignments) {
57✔
251
        auto& lhs = entry.first;
4✔
252
        auto& type = this->function().type(lhs->get_name());
4✔
253
        if (type.type_id() != types::TypeID::Scalar) {
4✔
254
            throw InvalidSDFGException("Assignment - LHS: must be integer type");
×
255
        }
256

257
        auto& rhs = entry.second;
4✔
258
        for (auto& atom : symbolic::atoms(rhs)) {
5✔
259
            if (symbolic::is_nullptr(atom)) {
1✔
260
                throw InvalidSDFGException("Assignment - RHS: must be integer type, but is nullptr");
×
261
            }
262
            auto& atom_type = this->function().type(atom->get_name());
1✔
263
            if (atom_type.type_id() != types::TypeID::Scalar) {
1✔
264
                throw InvalidSDFGException("Assignment - RHS: must be integer type");
×
265
            }
266
        }
267
    }
268

269
    for (auto& atom : symbolic::atoms(condition)) {
61✔
270
        if (symbolic::is_nullptr(atom)) {
8✔
271
            continue;
×
272
        }
273
        auto& atom_type = this->function().type(atom->get_name());
8✔
274
        if (atom_type.type_id() != types::TypeID::Scalar && atom_type.type_id() != types::TypeID::Pointer) {
8✔
275
            throw InvalidSDFGException("Condition: must be integer type or pointer type");
×
276
        }
277
    }
278

279
    auto edge = boost::add_edge(src.vertex_, dst.vertex_, this->sdfg_->graph_);
53✔
280
    assert(edge.second);
53✔
281

282
    auto res = this->sdfg_->edges_.insert(
106✔
283
        {edge.first,
106✔
284
         std::unique_ptr<control_flow::InterstateEdge>(new control_flow::InterstateEdge(
106✔
285
             this->new_element_id(), debug_info, edge.first, src, dst, condition, assignments
53✔
286
         ))}
287
    );
288

289
    assert(res.second);
53✔
290

291
    return *(*res.first).second;
53✔
292
};
×
293

294
void SDFGBuilder::remove_edge(const control_flow::InterstateEdge& edge) {
×
295
    auto desc = edge.edge();
×
296
    this->sdfg_->edges_.erase(desc);
×
297

298
    boost::remove_edge(desc, this->sdfg_->graph_);
×
299
};
×
300

301
std::tuple<control_flow::State&, control_flow::State&, control_flow::State&> SDFGBuilder::add_loop(
1✔
302
    const control_flow::State& state,
303
    sdfg::symbolic::Symbol iterator,
304
    sdfg::symbolic::Expression init,
305
    sdfg::symbolic::Condition cond,
306
    sdfg::symbolic::Expression update,
307
    const DebugInfo& debug_info
308
) {
309
    // Init: iterator = init
310
    auto& init_state = this->add_state_after(state, true, debug_info);
1✔
311
    const graph::Edge init_edge_desc = (*this->sdfg_->in_edges(init_state).begin()).edge_;
1✔
312
    auto& init_edge = this->sdfg_->edges_[init_edge_desc];
1✔
313
    init_edge->assignments_.insert({iterator, init});
1✔
314

315
    // Final state
316
    auto& final_state = this->add_state_after(init_state, false, debug_info);
1✔
317

318
    // Init -> early_exit -> final
319
    auto& early_exit_state = this->add_state(false, debug_info);
1✔
320
    this->add_edge(init_state, early_exit_state, symbolic::Not(cond));
1✔
321
    this->add_edge(early_exit_state, final_state);
1✔
322

323
    // Init -> header -> body
324
    auto& header_state = this->add_state(false, debug_info);
1✔
325
    this->add_edge(init_state, header_state, cond);
1✔
326

327
    auto& body_state = this->add_state(false, debug_info);
1✔
328
    this->add_edge(header_state, body_state);
1✔
329

330
    auto& update_state = this->add_state(false, debug_info);
1✔
331
    this->add_edge(body_state, update_state, {{iterator, update}});
1✔
332

333
    // Back edge and exit edge
334
    this->add_edge(update_state, header_state, cond);
1✔
335
    this->add_edge(update_state, final_state, symbolic::Not(cond));
1✔
336

337
    return {init_state, body_state, final_state};
1✔
338
};
×
339

340
/***** Section: Dataflow Graph *****/
341

342
data_flow::AccessNode& SDFGBuilder::
343
    add_access(control_flow::State& state, const std::string& data, const DebugInfo& debug_info) {
6✔
344
    auto& dataflow = state.dataflow();
6✔
345
    auto vertex = boost::add_vertex(dataflow.graph_);
6✔
346
    auto res = dataflow.nodes_.insert(
12✔
347
        {vertex,
6✔
348
         std::unique_ptr<
6✔
349
             data_flow::AccessNode>(new data_flow::AccessNode(this->new_element_id(), debug_info, vertex, dataflow, data)
6✔
350
         )}
351
    );
352

353
    return static_cast<data_flow::AccessNode&>(*(res.first->second));
6✔
354
};
×
355

356
data_flow::ConstantNode& SDFGBuilder::add_constant(
1✔
357
    control_flow::State& state, const std::string& data, const types::IType& type, const DebugInfo& debug_info
358
) {
359
    auto& dataflow = state.dataflow();
1✔
360
    auto vertex = boost::add_vertex(dataflow.graph_);
1✔
361
    auto res = dataflow.nodes_.insert(
2✔
362
        {vertex,
1✔
363
         std::unique_ptr<data_flow::ConstantNode>(
1✔
364
             new data_flow::ConstantNode(this->new_element_id(), debug_info, vertex, dataflow, data, type)
1✔
365
         )}
366
    );
367

368
    return static_cast<data_flow::ConstantNode&>(*(res.first->second));
1✔
369
};
×
370

371
data_flow::Tasklet& SDFGBuilder::add_tasklet(
3✔
372
    control_flow::State& state,
373
    const data_flow::TaskletCode code,
374
    const std::string& output,
375
    const std::vector<std::string>& inputs,
376
    const DebugInfo& debug_info
377
) {
378
    auto& dataflow = state.dataflow();
3✔
379
    auto vertex = boost::add_vertex(dataflow.graph_);
3✔
380
    auto res = dataflow.nodes_.insert(
6✔
381
        {vertex,
3✔
382
         std::unique_ptr<data_flow::Tasklet>(
3✔
383
             new data_flow::Tasklet(this->new_element_id(), debug_info, vertex, dataflow, code, output, inputs)
3✔
384
         )}
385
    );
386

387
    return static_cast<data_flow::Tasklet&>(*(res.first->second));
3✔
388
};
×
389

390
data_flow::Memlet& SDFGBuilder::add_memlet(
6✔
391
    control_flow::State& state,
392
    data_flow::DataFlowNode& src,
393
    const std::string& src_conn,
394
    data_flow::DataFlowNode& dst,
395
    const std::string& dst_conn,
396
    const data_flow::Subset& subset,
397
    const types::IType& base_type,
398
    const DebugInfo& debug_info
399
) {
400
    auto& dataflow = state.dataflow();
6✔
401
    auto edge = boost::add_edge(src.vertex_, dst.vertex_, dataflow.graph_);
6✔
402
    auto res = dataflow.edges_.insert(
12✔
403
        {edge.first,
12✔
404
         std::unique_ptr<data_flow::Memlet>(new data_flow::Memlet(
6✔
405
             this->new_element_id(), debug_info, edge.first, dataflow, src, src_conn, dst, dst_conn, subset, base_type
6✔
406
         ))}
407
    );
408

409
    auto& memlet = static_cast<data_flow::Memlet&>(*(res.first->second));
6✔
410
#ifndef NDEBUG
411
    memlet.validate(*this->sdfg_);
6✔
412
#endif
413

414
    return memlet;
6✔
415
};
×
416

417
data_flow::Memlet& SDFGBuilder::add_computational_memlet(
×
418
    control_flow::State& state,
419
    data_flow::AccessNode& src,
420
    data_flow::Tasklet& dst,
421
    const std::string& dst_conn,
422
    const data_flow::Subset& subset,
423
    const types::IType& base_type,
424
    const DebugInfo& debug_info
425
) {
426
    return this->add_memlet(state, src, "void", dst, dst_conn, subset, base_type, debug_info);
×
427
};
×
428

429
data_flow::Memlet& SDFGBuilder::add_computational_memlet(
×
430
    control_flow::State& state,
431
    data_flow::Tasklet& src,
432
    const std::string& src_conn,
433
    data_flow::AccessNode& dst,
434
    const data_flow::Subset& subset,
435
    const types::IType& base_type,
436
    const DebugInfo& debug_info
437
) {
438
    return this->add_memlet(state, src, src_conn, dst, "void", subset, base_type, debug_info);
×
439
};
×
440

441
data_flow::Memlet& SDFGBuilder::add_computational_memlet(
3✔
442
    control_flow::State& state,
443
    data_flow::AccessNode& src,
444
    data_flow::Tasklet& dst,
445
    const std::string& dst_conn,
446
    const data_flow::Subset& subset,
447
    const DebugInfo& debug_info
448
) {
449
    const types::IType* src_type = nullptr;
3✔
450
    if (auto cnode = dynamic_cast<data_flow::ConstantNode*>(&src)) {
3✔
451
        src_type = &cnode->type();
×
452
    } else {
×
453
        src_type = &this->sdfg_->type(src.data());
3✔
454
    }
455
    auto& base_type = types::infer_type(*this->sdfg_, *src_type, subset);
3✔
456
    if (base_type.type_id() != types::TypeID::Scalar) {
3✔
457
        throw InvalidSDFGException("Computational memlet must have a scalar type");
×
458
    }
459
    return this->add_memlet(state, src, "void", dst, dst_conn, subset, *src_type, debug_info);
3✔
460
};
×
461

462
data_flow::Memlet& SDFGBuilder::add_computational_memlet(
3✔
463
    control_flow::State& state,
464
    data_flow::Tasklet& src,
465
    const std::string& src_conn,
466
    data_flow::AccessNode& dst,
467
    const data_flow::Subset& subset,
468
    const DebugInfo& debug_info
469
) {
470
    auto& dst_type = this->function().type(dst.data());
3✔
471
    auto& base_type = types::infer_type(this->function(), dst_type, subset);
3✔
472
    if (base_type.type_id() != types::TypeID::Scalar) {
3✔
473
        throw InvalidSDFGException("Computational memlet must have a scalar type");
×
474
    }
475
    return this->add_memlet(state, src, src_conn, dst, "void", subset, dst_type, debug_info);
3✔
476
};
×
477

478
data_flow::Memlet& SDFGBuilder::add_computational_memlet(
×
479
    control_flow::State& state,
480
    data_flow::AccessNode& src,
481
    data_flow::LibraryNode& dst,
482
    const std::string& dst_conn,
483
    const data_flow::Subset& subset,
484
    const types::IType& base_type,
485
    const DebugInfo& debug_info
486
) {
487
    return this->add_memlet(state, src, "void", dst, dst_conn, subset, base_type, debug_info);
×
488
};
×
489

490
data_flow::Memlet& SDFGBuilder::add_computational_memlet(
×
491
    control_flow::State& state,
492
    data_flow::LibraryNode& src,
493
    const std::string& src_conn,
494
    data_flow::AccessNode& dst,
495
    const data_flow::Subset& subset,
496
    const types::IType& base_type,
497
    const DebugInfo& debug_info
498
) {
499
    return this->add_memlet(state, src, src_conn, dst, "void", subset, base_type, debug_info);
×
500
};
×
501

502
data_flow::Memlet& SDFGBuilder::add_reference_memlet(
×
503
    control_flow::State& state,
504
    data_flow::AccessNode& src,
505
    data_flow::AccessNode& dst,
506
    const data_flow::Subset& subset,
507
    const types::IType& base_type,
508
    const DebugInfo& debug_info
509
) {
510
    return this->add_memlet(state, src, "void", dst, "ref", subset, base_type, debug_info);
×
511
};
×
512

513
data_flow::Memlet& SDFGBuilder::add_dereference_memlet(
×
514
    control_flow::State& state,
515
    data_flow::AccessNode& src,
516
    data_flow::AccessNode& dst,
517
    bool derefs_src,
518
    const types::IType& base_type,
519
    const DebugInfo& debug_info
520
) {
521
    if (derefs_src) {
×
522
        return this->add_memlet(state, src, "void", dst, "deref", {symbolic::zero()}, base_type, debug_info);
×
523
    } else {
524
        return this->add_memlet(state, src, "deref", dst, "void", {symbolic::zero()}, base_type, debug_info);
×
525
    }
526
};
×
527

528
} // namespace builder
529
} // namespace sdfg
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