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

FormulasQuestion / moodle-qtype_formulas / 13217446514

08 Feb 2025 04:37PM UTC coverage: 76.899% (+1.9%) from 75.045%
13217446514

Pull #62

github

web-flow
Merge b36f9931f into acd272945
Pull Request #62: Rewrite the parser

2547 of 3139 new or added lines in 22 files covered. (81.14%)

146 existing lines in 6 files now uncovered.

3006 of 3909 relevant lines covered (76.9%)

438.31 hits per line

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

0.0
/edit_formulas_form.php
1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <https://www.gnu.org/licenses/>.
16

17
/**
18
 * Defines the editing form for the formulas question type.
19
 *
20
 * @copyright 2010-2011 Hon Wai, Lau
21
 * @author Hon Wai, Lau <lau65536@gmail.com>
22
 * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 * @package qtype_formulas
24
 */
25

26
use qtype_formulas\unit_conversion_rules;
27

28
defined('MOODLE_INTERNAL') || die();
29

NEW
30
require_once($CFG->dirroot . '/question/type/edit_question_form.php');
×
NEW
31
require_once($CFG->dirroot . '/question/type/multichoice/questiontype.php');
×
32

33
/**
34
 * coodinate question type editing form definition.
35
 */
36
class qtype_formulas_edit_form extends question_edit_form {
37

38
    /**
39
     * Add question-type specific form fields.
40
     *
41
     * @param MoodleQuickForm $mform the form being built.
42
     */
43
    protected function definition_inner($mform) {
44
        global $PAGE;
UNCOV
45
        $config = get_config('qtype_formulas');
×
UNCOV
46
        $PAGE->requires->js_call_amd('qtype_formulas/editform', 'init', [get_config('qtype_formulas')->defaultcorrectness]);
×
UNCOV
47
        $PAGE->requires->js('/question/type/formulas/script/formatcheck.js');
×
UNCOV
48
        $PAGE->requires->css('/question/type/formulas/styles.css');
×
UNCOV
49
        $PAGE->requires->css('/question/type/formulas/tabulator.css');
×
50
        // Legacy, needed until formatcheck.js is refactored.
UNCOV
51
        $PAGE->requires->string_for_js('unit', 'qtype_formulas');
×
52
        // Hide the unused form fields.
UNCOV
53
        $mform->removeElement('defaultmark');
×
UNCOV
54
        $mform->addElement('hidden', 'defaultmark');
×
UNCOV
55
        $mform->setType('defaultmark', PARAM_RAW);
×
56

UNCOV
57
        $mform->addHelpButton('questiontext', 'questiontext', 'qtype_formulas');
×
58

59
        // Random and global variables and main question.
UNCOV
60
        $mform->insertElementBefore($mform->createElement('header', 'globalvarshdr', get_string('globalvarshdr', 'qtype_formulas'),
×
UNCOV
61
            ''), 'questiontext');
×
UNCOV
62
        $mform->insertElementBefore($mform->createElement('textarea', 'varsrandom', get_string('varsrandom', 'qtype_formulas'),
×
NEW
63
            ['cols' => 80, 'rows' => 1]) , 'questiontext');
×
UNCOV
64
        $mform->addHelpButton('varsrandom', 'varsrandom', 'qtype_formulas');
×
UNCOV
65
        $mform->insertElementBefore($mform->createElement('textarea', 'varsglobal', get_string('varsglobal', 'qtype_formulas'),
×
NEW
66
            ['cols' => 80, 'rows'  => 1]) , 'questiontext');
×
UNCOV
67
        $mform->addHelpButton('varsglobal', 'varsglobal', 'qtype_formulas');
×
UNCOV
68
        $mform->insertElementBefore($mform->createElement('header', 'mainq', get_string('mainq', 'qtype_formulas'),
×
UNCOV
69
            ''), 'questiontext');
×
NEW
70
        $numberingoptions = qtype_multichoice::get_numbering_styles();
×
UNCOV
71
        $mform->addElement('select', 'answernumbering',
×
UNCOV
72
                get_string('answernumbering', 'qtype_multichoice'), $numberingoptions);
×
UNCOV
73
        $mform->setDefault('answernumbering', get_config('qtype_multichoice', 'answernumbering'));
×
74

75
        // Part's answers.
UNCOV
76
        $this->add_per_answer_fields($mform, get_string('answerno', 'qtype_formulas', '{no}'),
×
UNCOV
77
            question_bank::fraction_options(), 1, 2);
×
78

79
        // Display options, flow options and global part's options.
UNCOV
80
        $mform->addElement('header', 'subqoptions', get_string('subqoptions', 'qtype_formulas'));
×
UNCOV
81
        $mform->addElement('text', 'globalunitpenalty',
×
UNCOV
82
            get_string('unitpenalty', 'qtype_formulas'),
×
NEW
83
            ['size' => 3]
×
UNCOV
84
        );
×
UNCOV
85
        $mform->addHelpButton('globalunitpenalty', 'unitpenalty', 'qtype_formulas');
×
UNCOV
86
        $mform->setDefault('globalunitpenalty', $config->defaultunitpenalty);
×
UNCOV
87
        $mform->setType('globalunitpenalty', PARAM_FLOAT);
×
88

NEW
89
        $conversionrules = new unit_conversion_rules();
×
UNCOV
90
        $allrules = $conversionrules->allrules();
×
UNCOV
91
        foreach ($allrules as $id => $entry) {
×
UNCOV
92
            $defaultrulechoice[$id] = $entry[0];
×
93
        }
UNCOV
94
        $mform->addElement('select', 'globalruleid', get_string('ruleid', 'qtype_formulas'), $defaultrulechoice);
×
UNCOV
95
        $mform->setDefault('globalruleid', 1);
×
UNCOV
96
        $mform->addHelpButton('globalruleid', 'ruleid', 'qtype_formulas');
×
97

98
        // Allow instantiate random variables and display the data for instantiated variables.
UNCOV
99
        $mform->addElement('header', 'checkvarshdr', get_string('checkvarshdr', 'qtype_formulas'));
×
NEW
100
        $numdatasetgroup = [];
×
UNCOV
101
        $numdatasetgroup[] = $mform->createElement('select', 'numdataset', '',
×
NEW
102
            [
×
UNCOV
103
                '1' => '1', '5' => '5', '10' => '10', '25' => '25', '50' => '50', '100' => '100', '250' => '250',
×
NEW
104
                '500' => '500', '1000' => '1000', '-1' => '*',
×
NEW
105
            ]
×
UNCOV
106
        );
×
UNCOV
107
        $numdatasetgroup[] = $mform->createElement('button', 'instantiatebtn', get_string('instantiate', 'qtype_formulas'));
×
UNCOV
108
        $mform->addElement('group', 'instantiationctrl', get_string('numdataset', 'qtype_formulas'), $numdatasetgroup, null, false);
×
UNCOV
109
        $mform->addElement('static', 'varsdata', get_string('varsdata', 'qtype_formulas'), '<div id="varsdata_display"></div>');
×
UNCOV
110
        $mform->addElement('static', 'qtextpreview', '', '<div id="qtextpreview_display"></div>');
×
111

UNCOV
112
        $this->add_combined_feedback_fields(true);
×
UNCOV
113
        $this->add_interactive_settings(true, true);
×
114
    }
115

116
    /**
117
     * Add the answer field for a particular part labelled by placeholder.
118
     *
119
     * @param MoodleQuickForm $mform the form being built
120
     * @param string $label label to use for each option
121
     * @param $gradeoptions the possible grades for each answer.
122
     * @param array $repeatedoptions reference to array of repeated options to fill
123
     * @param array $answersoption reference to return the name of $question->options field holding an array of answers
124
     * @return array of form fields.
125
     */
126
    protected function get_per_answer_fields($mform, $label, $gradeoptions,
127
            &$repeatedoptions, &$answersoption) {
UNCOV
128
        $config = get_config('qtype_formulas');
×
NEW
129
        $repeated = [];
×
UNCOV
130
        $repeated[] = $mform->createElement('header', 'answerhdr', $label);
×
131
        // Part's mark.
UNCOV
132
        $repeated[] = $mform->createElement('text', 'answermark', get_string('answermark', 'qtype_formulas'),
×
NEW
133
            ['size' => 3]);
×
NEW
134
        $repeatedoptions['answermark']['helpbutton'] = ['answermark', 'qtype_formulas'];
×
UNCOV
135
        $repeatedoptions['answermark']['default'] = $config->defaultanswermark;
×
UNCOV
136
        $repeatedoptions['answermark']['type'] = PARAM_FLOAT;
×
137
        // Part's number of coordinates.
UNCOV
138
        $repeated[] = $mform->createElement('hidden', 'numbox', '', '');   // Exact value will be computed during validation.
×
UNCOV
139
        $repeatedoptions['numbox']['type'] = PARAM_INT;
×
140
        // Part's placeholder.
UNCOV
141
        $repeated[] = $mform->createElement('text', 'placeholder', get_string('placeholder', 'qtype_formulas'),
×
NEW
142
            ['size' => 20]);
×
NEW
143
        $repeatedoptions['placeholder']['helpbutton'] = ['placeholder', 'qtype_formulas'];
×
UNCOV
144
        $repeatedoptions['placeholder']['type'] = PARAM_RAW;
×
145
        // Part's text.
UNCOV
146
        $repeated[] = $mform->createElement('editor', 'subqtext', get_string('subqtext', 'qtype_formulas'),
×
NEW
147
            ['rows' => 3], $this->editoroptions);
×
NEW
148
        $repeatedoptions['subqtext']['helpbutton'] = ['subqtext', 'qtype_formulas'];
×
149
        // Part's answer type (0, 10, 100, 1000).
UNCOV
150
        $repeated[] = $mform->createElement('select', 'answertype', get_string('answertype', 'qtype_formulas'),
×
NEW
151
                [0 => get_string('number', 'qtype_formulas'), 10 => get_string('numeric', 'qtype_formulas'),
×
UNCOV
152
                        100 => get_string('numerical_formula', 'qtype_formulas'),
×
NEW
153
                        1000 => get_string('algebraic_formula', 'qtype_formulas')]);;
×
UNCOV
154
        $repeatedoptions['answertype']['default'] = $config->defaultanswertype;
×
UNCOV
155
        $repeatedoptions['answertype']['type'] = PARAM_INT;
×
NEW
156
        $repeatedoptions['answertype']['helpbutton'] = ['answertype', 'qtype_formulas'];
×
157
        // Part's answer.
UNCOV
158
        $repeated[] = $mform->createElement('text', 'answer', get_string('answer', 'qtype_formulas'),
×
NEW
159
            ['size' => 80]);
×
NEW
160
        $repeatedoptions['answer']['helpbutton'] = ['answer', 'qtype_formulas'];
×
NEW
161
        $repeatedoptions['answer']['type'] = PARAM_RAW_TRIMMED;
×
162
        // Whether the question has multiple answers.
UNCOV
163
        $repeated[] = $mform->createElement(
×
UNCOV
164
            'advcheckbox',
×
UNCOV
165
            'answernotunique',
×
UNCOV
166
            get_string('answernotunique', 'qtype_formulas')
×
UNCOV
167
        );
×
NEW
168
        $repeatedoptions['answernotunique']['helpbutton'] = ['answernotunique', 'qtype_formulas'];
×
169
        // Part's unit.
UNCOV
170
        $repeated[] = $mform->createElement('text', 'postunit', get_string('postunit', 'qtype_formulas'),
×
NEW
171
            ['size' => 60, 'class' => 'formulas_editing_unit']);
×
NEW
172
        $repeatedoptions['postunit']['helpbutton'] = ['postunit', 'qtype_formulas'];
×
UNCOV
173
        $repeatedoptions['postunit']['type'] = PARAM_RAW;
×
174
        // Part's grading criteria.
NEW
175
        $gradinggroup = [];
×
UNCOV
176
        $gradinggroup[] = $mform->createElement('select', 'correctness_simple_type', null,
×
NEW
177
            [
×
UNCOV
178
                get_string('relerror', 'qtype_formulas'),
×
NEW
179
                get_string('abserror', 'qtype_formulas'),
×
NEW
180
            ], ['aria-label' => 'type'] // ARIA label needed as workaround for accessibility.
×
UNCOV
181
        );
×
UNCOV
182
        $gradinggroup[] = $mform->createElement(
×
UNCOV
183
            'select',
×
UNCOV
184
            'correctness_simple_comp',
×
UNCOV
185
            null,
×
NEW
186
            ['==', '<'],
×
NEW
187
            ['aria-label' => 'comparison'],
×
UNCOV
188
            false
×
UNCOV
189
        );
×
NEW
190
        $gradinggroup[] = $mform->createElement('text', 'correctness_simple_tol', null, ['aria-label' => 'tolerance']);
×
UNCOV
191
        $repeated[] = $mform->createElement(
×
UNCOV
192
            'group',
×
UNCOV
193
            'correctness_simple',
×
UNCOV
194
            get_string('correctness', 'qtype_formulas'),
×
UNCOV
195
            $gradinggroup,
×
UNCOV
196
            null,
×
UNCOV
197
            false
×
UNCOV
198
        );
×
UNCOV
199
        $repeated[] = $mform->createElement('text', 'correctness', get_string('correctness', 'qtype_formulas'),
×
NEW
200
        ['size' => 60]);
×
UNCOV
201
        $repeated[] = $mform->createElement(
×
UNCOV
202
            'checkbox',
×
UNCOV
203
            'correctness_simple_mode',
×
UNCOV
204
            get_string('correctnesssimple', 'qtype_formulas')
×
UNCOV
205
        );
×
206

UNCOV
207
        $repeatedoptions['correctness_simple_mode']['default'] = 0;
×
NEW
208
        $repeatedoptions['correctness']['hideif'] = ['correctness_simple_mode', 'checked'];
×
UNCOV
209
        $repeatedoptions['correctness']['default'] = $config->defaultcorrectness;
×
NEW
210
        $repeatedoptions['correctness']['helpbutton'] = ['correctness', 'qtype_formulas'];
×
NEW
211
        $repeatedoptions['correctness']['type'] = PARAM_RAW_TRIMMED;
×
NEW
212
        $repeatedoptions['correctness_simple']['hideif'] = ['correctness_simple_mode', 'notchecked'];
×
NEW
213
        $repeatedoptions['correctness_simple']['helpbutton'] = ['correctness', 'qtype_formulas'];
×
UNCOV
214
        $repeatedoptions['correctness_simple_tol']['type'] = PARAM_FLOAT;
×
UNCOV
215
        $repeatedoptions['correctness_simple_tol']['default'] = '0.01';
×
216

217
        // Part's local variables.
UNCOV
218
        $repeated[] = $mform->createElement('textarea', 'vars1', get_string('vars1', 'qtype_formulas'),
×
NEW
219
            ['cols' => 80, 'rows' => 1]);
×
NEW
220
        $repeatedoptions['vars1']['type'] = PARAM_RAW_TRIMMED;
×
NEW
221
        $repeatedoptions['vars1']['helpbutton'] = ['vars1', 'qtype_formulas'];
×
UNCOV
222
        $repeatedoptions['vars1']['advanced'] = true;
×
223
        // Part's grading variables.
UNCOV
224
        $repeated[] = $mform->createElement('textarea', 'vars2', get_string('vars2', 'qtype_formulas'),
×
NEW
225
            ['cols' => 80, 'rows' => 1]);
×
NEW
226
        $repeatedoptions['vars2']['type'] = PARAM_RAW_TRIMMED;
×
NEW
227
        $repeatedoptions['vars2']['helpbutton'] = ['vars2', 'qtype_formulas'];
×
UNCOV
228
        $repeatedoptions['vars2']['advanced'] = true;
×
229
        // Part's other rules.
UNCOV
230
        $repeated[] = $mform->createElement('textarea', 'otherrule', get_string('otherrule', 'qtype_formulas'),
×
NEW
231
            ['cols' => 80, 'rows' => 1]);
×
NEW
232
        $repeatedoptions['otherrule']['helpbutton'] = ['otherrule', 'qtype_formulas'];
×
NEW
233
        $repeatedoptions['otherrule']['type'] = PARAM_RAW_TRIMMED;
×
UNCOV
234
        $repeatedoptions['otherrule']['advanced'] = true;
×
235
        // Part's feedback.
UNCOV
236
        $repeated[] = $mform->createElement('editor', 'feedback', get_string('feedback', 'qtype_formulas'),
×
NEW
237
            ['rows' => 3], $this->editoroptions);
×
NEW
238
        $repeatedoptions['feedback']['helpbutton'] = ['feedback', 'qtype_formulas'];
×
UNCOV
239
        $repeatedoptions['feedback']['advanced'] = true;
×
240
        // Part's combined feedback.
UNCOV
241
        $repeated[] = $mform->createElement('editor', 'partcorrectfb', get_string('correctfeedback', 'qtype_formulas'),
×
NEW
242
            ['rows' => 3], $this->editoroptions);
×
NEW
243
        $repeatedoptions['partcorrectfb']['helpbutton'] = ['correctfeedback', 'qtype_formulas'];
×
UNCOV
244
        $repeatedoptions['partcorrectfb']['advanced'] = true;
×
UNCOV
245
        $repeated[] = $mform->createElement(
×
UNCOV
246
          'editor',
×
UNCOV
247
          'partpartiallycorrectfb',
×
UNCOV
248
          get_string('partiallycorrectfeedback', 'qtype_formulas'),
×
NEW
249
          ['rows' => 3],
×
UNCOV
250
          $this->editoroptions
×
UNCOV
251
        );
×
NEW
252
        $repeatedoptions['partpartiallycorrectfb']['helpbutton'] = ['partiallycorrectfeedback', 'qtype_formulas'];
×
UNCOV
253
        $repeatedoptions['partpartiallycorrectfb']['advanced'] = true;
×
UNCOV
254
        $repeated[] = $mform->createElement('editor', 'partincorrectfb', get_string('incorrectfeedback', 'qtype_formulas'),
×
NEW
255
            ['rows' => 3], $this->editoroptions);
×
NEW
256
        $repeatedoptions['partincorrectfb']['helpbutton'] = ['incorrectfeedback', 'qtype_formulas'];
×
UNCOV
257
        $repeatedoptions['partincorrectfb']['advanced'] = true;
×
UNCOV
258
        $answersoption = 'answers';
×
UNCOV
259
        return $repeated;
×
260
    }
261

262
    /**
263
     * Add a set of form fields, obtained from get_per_answer_fields, to the form,
264
     * one for each existing answer, with some blanks for some new ones.
265
     * @param object $mform the form being built.
266
     * @param $label the label to use for each option.
267
     * @param $gradeoptions the possible grades for each answer.
268
     * @param $minoptions the minimum number of answer blanks to display.
269
     *      Default QUESTION_NUMANS_START.
270
     * @param $addoptions the number of answer blanks to add. Default QUESTION_NUMANS_ADD.
271
     */
272
    protected function add_per_answer_fields(&$mform, $label, $gradeoptions,
273
            $minoptions = QUESTION_NUMANS_START, $addoptions = QUESTION_NUMANS_ADD) {
UNCOV
274
        $answersoption = '';
×
NEW
275
        $repeatedoptions = [];
×
UNCOV
276
        $repeated = $this->get_per_answer_fields($mform, $label, $gradeoptions,
×
UNCOV
277
                $repeatedoptions, $answersoption);
×
278

279
        // If we are editing an existing question and the user inadvertently cleared all parts,
280
        // we still want to show the fields for one part in the form. If we are creating a new
281
        // question, we show $minoptions part(s), the default is 3.
282
        if (isset($this->question->options)) {
×
NEW
283
            $repeatsatstart = max(1, count($this->question->options->$answersoption));
×
284
        } else {
UNCOV
285
            $repeatsatstart = $minoptions;
×
286
        }
287

UNCOV
288
        $this->repeat_elements($repeated, $repeatsatstart, $repeatedoptions,
×
UNCOV
289
                'noanswers', 'addanswers', $addoptions,
×
UNCOV
290
                $this->get_more_choices_string(), false);
×
291
    }
292

293
    /**
294
     * Language string to use for 'Add {no} more blanks'. Override from parent for
295
     * appropriate text.
296
     *
297
     * @return void
298
     */
299
    protected function get_more_choices_string() {
UNCOV
300
        return get_string('addmorepartsblanks', 'qtype_formulas');
×
301
    }
302

303
    /**
304
     * Perform any preprocessing needed on the data passed to {@link set_data()}
305
     * before it is used to initialise the form.
306
     *
307
     * @param object $question the data being passed to the form
308
     * @return object the modified data
309
     */
310
    protected function data_preprocessing($question) {
311
        $question = parent::data_preprocessing($question);
×
312
        $question = $this->data_preprocessing_combined_feedback($question, true);
×
313
        $question = $this->data_preprocessing_hints($question, true, true);
×
314
        if (isset($question->options)) {
×
NEW
315
            $defaultvalues = [];
×
316
            if (count($question->options->answers)) {
×
NEW
317
                $tags = qtype_formulas::PART_BASIC_FIELDS;
×
318
                foreach ($question->options->answers as $key => $answer) {
×
319

320
                    foreach ($tags as $tag) {
×
321
                        if ($tag === 'unitpenalty' || $tag === 'ruleid') {
×
322
                            $defaultvalues['global' . $tag] = $answer->$tag;
×
323
                        } else {
324
                            $defaultvalues[$tag.'['.$key.']'] = $answer->$tag;
×
325
                        }
326
                    }
327

NEW
328
                    $fields = ['subqtext', 'feedback'];
×
329
                    foreach ($fields as $field) {
×
330
                        $fieldformat = $field . 'format';
×
331
                        $itemid = file_get_submitted_draft_itemid($field . '[' . $key . ']');
×
332
                        $fieldtxt = file_prepare_draft_area($itemid, $this->context->id, 'qtype_formulas',
×
333
                                'answer' . $field, empty($answer->id) ? null : (int)$answer->id,
×
334
                                $this->fileoptions, $answer->$field);
×
NEW
335
                        $defaultvalues[$field . '[' . $key . ']'] = ['text' => $fieldtxt,
×
NEW
336
                            'format' => $answer->$fieldformat, 'itemid' => $itemid];
×
337
                    }
NEW
338
                    $fields = ['partcorrectfb', 'partpartiallycorrectfb', 'partincorrectfb'];
×
339
                    foreach ($fields as $field) {
×
340
                        $fieldformat = $field . 'format';
×
341
                        $itemid = file_get_submitted_draft_itemid($field . '[' . $key . ']');
×
342
                        $fieldtxt = file_prepare_draft_area($itemid, $this->context->id, 'qtype_formulas',
×
343
                                $field, empty($answer->id) ? null : (int)$answer->id,
×
344
                                $this->fileoptions, $answer->$field);
×
NEW
345
                        $defaultvalues[$field . '[' . $key . ']'] = ['text' => $fieldtxt,
×
NEW
346
                            'format' => $answer->$fieldformat, 'itemid' => $itemid];
×
347
                    }
348
                }
349
            }
350

351
            $question = (object)((array)$question + $defaultvalues);
×
352
        }
353
        return $question;
×
354
    }
355

356
    /**
357
     * Validating the data returning from the form.
358
     *
359
     * The check the basic error as well as the formula error by evaluating one instantiation.
360
     */
361
    public function validation($fromform, $files) {
UNCOV
362
        $errors = parent::validation($fromform, $files);
×
363
        // Use the validation defined in the question type, check by instantiating one variable set.
UNCOV
364
        $data = (object)$fromform;
×
NEW
365
        $qtype = new qtype_formulas();
×
NEW
366
        $instantiationresult = $qtype->validate($data);
×
UNCOV
367
        if (isset($instantiationresult->errors)) {
×
UNCOV
368
            $errors = array_merge($errors, $instantiationresult->errors);
×
369
        }
UNCOV
370
        return $errors;
×
371
    }
372

373
    /**
374
     * Overriding abstract parent method to return the question type name.
375
     *
376
     * @return string
377
     */
378
    public function qtype() {
UNCOV
379
        return 'formulas';
×
380
    }
381
}
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

© 2025 Coveralls, Inc