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

openmrs / openmrs-core / 17208175973

25 Aug 2025 12:00PM UTC coverage: 63.742% (+0.07%) from 63.671%
17208175973

push

github

ibacher
TRUNK-6395: Saner scheme for copying properties from the installation script (#5260)

0 of 2 new or added lines in 1 file covered. (0.0%)

697 existing lines in 13 files now uncovered.

22147 of 34745 relevant lines covered (63.74%)

0.64 hits per line

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

84.31
/api/src/main/java/org/openmrs/api/db/hibernate/HibernateAdministrationDAO.java
1
/**
2
 * This Source Code Form is subject to the terms of the Mozilla Public License,
3
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
4
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6
 *
7
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8
 * graphic logo is a trademark of OpenMRS Inc.
9
 */
10
package org.openmrs.api.db.hibernate;
11

12
import java.sql.Statement;
13
import java.sql.Connection;
14
import java.sql.SQLException;
15
import java.util.ArrayList;
16
import java.util.List;
17

18
import org.hibernate.Criteria;
19
import org.hibernate.FlushMode;
20
import org.hibernate.MappingException;
21
import org.hibernate.Session;
22
import org.hibernate.SessionFactory;
23
import org.hibernate.boot.Metadata;
24
import org.hibernate.criterion.MatchMode;
25
import org.hibernate.criterion.Order;
26
import org.hibernate.criterion.Restrictions;
27
import org.hibernate.engine.spi.SessionImplementor;
28
import org.hibernate.jdbc.Work;
29
import org.hibernate.mapping.Column;
30
import org.hibernate.mapping.PersistentClass;
31
import org.hibernate.metadata.ClassMetadata;
32
import org.hibernate.type.StringType;
33
import org.hibernate.type.TextType;
34
import org.hibernate.type.Type;
35
import org.openmrs.GlobalProperty;
36
import org.openmrs.OpenmrsObject;
37
import org.openmrs.api.APIException;
38
import org.openmrs.api.db.AdministrationDAO;
39
import org.openmrs.api.db.DAOException;
40
import org.openmrs.util.DatabaseUtil;
41
import org.openmrs.util.HandlerUtil;
42
import org.openmrs.util.OpenmrsConstants;
43
import org.slf4j.Logger;
44
import org.slf4j.LoggerFactory;
45
import org.springframework.beans.BeansException;
46
import org.springframework.context.ApplicationContext;
47
import org.springframework.context.ApplicationContextAware;
48
import org.springframework.validation.Errors;
49
import org.springframework.validation.Validator;
50

51
/**
52
 * Hibernate specific database methods for the AdministrationService
53
 *
54
 * @see org.openmrs.api.context.Context
55
 * @see org.openmrs.api.db.AdministrationDAO
56
 * @see org.openmrs.api.AdministrationService
57
 */
58
public class HibernateAdministrationDAO implements AdministrationDAO, ApplicationContextAware {
59
        
60
        private static final Logger log = LoggerFactory.getLogger(HibernateAdministrationDAO.class);
1✔
61
        private static final String PROPERTY = "property";
62
        
63
        /**
64
         * Hibernate session factory
65
         */
66
        private SessionFactory sessionFactory;
67

68
        private Metadata metadata;
69
        
70
        public HibernateAdministrationDAO() {
1✔
71
        }
1✔
72
        
73
        /**
74
         * Set session factory
75
         *
76
         * @param sessionFactory
77
         */
78
        public void setSessionFactory(SessionFactory sessionFactory) {
79
                this.sessionFactory = sessionFactory;
1✔
80
        }
1✔
81
        
82
        /**
83
         * @see org.openmrs.api.db.AdministrationDAO#getGlobalProperty(java.lang.String)
84
         */
85
        @Override
86
        public String getGlobalProperty(String propertyName) throws DAOException {
87
                GlobalProperty gp = getGlobalPropertyObject(propertyName);
1✔
88
                
89
                // if no gp exists, return a null value
90
                if (gp == null) {
1✔
91
                        return null;
1✔
92
                }
93
                
94
                return gp.getPropertyValue();
1✔
95
        }
96
        
97
        /**
98
         * @see org.openmrs.api.db.AdministrationDAO#getGlobalPropertyObject(java.lang.String)
99
         */
100
        @Override
101
        public GlobalProperty getGlobalPropertyObject(String propertyName) {
102
                if (isDatabaseStringComparisonCaseSensitive()) {
1✔
103
                        Criteria criteria = sessionFactory.getCurrentSession().createCriteria(GlobalProperty.class);
1✔
104
                        return (GlobalProperty) criteria.add(Restrictions.eq(PROPERTY, propertyName).ignoreCase())
1✔
105
                                .uniqueResult();
1✔
106
                } else {
107
                        return (GlobalProperty) sessionFactory.getCurrentSession().get(GlobalProperty.class, propertyName);
×
108
                }
109
        }
110
        
111
        @Override
112
        public GlobalProperty getGlobalPropertyByUuid(String uuid) throws DAOException {
113

114
                return (GlobalProperty) sessionFactory.getCurrentSession()
1✔
115
                        .createQuery("from GlobalProperty t where t.uuid = :uuid").setString("uuid", uuid).uniqueResult();
1✔
116
        }
117
        
118
        /**
119
         * @see org.openmrs.api.db.AdministrationDAO#getAllGlobalProperties()
120
         */
121
        @Override
122
        @SuppressWarnings("unchecked")
123
        public List<GlobalProperty> getAllGlobalProperties() throws DAOException {
124
                Criteria criteria = sessionFactory.getCurrentSession().createCriteria(GlobalProperty.class);
1✔
125
                return criteria.addOrder(Order.asc(PROPERTY)).list();
1✔
126
        }
127
        
128
        /**
129
         * @see org.openmrs.api.db.AdministrationDAO#getGlobalPropertiesByPrefix(java.lang.String)
130
         */
131
        @Override
132
        @SuppressWarnings("unchecked")
133
        public List<GlobalProperty> getGlobalPropertiesByPrefix(String prefix) {
134
                return sessionFactory.getCurrentSession().createCriteria(GlobalProperty.class)
1✔
135
                        .add(Restrictions.ilike(PROPERTY, prefix, MatchMode.START)).list();
1✔
136
        }
137
        
138
        /**
139
         * @see org.openmrs.api.db.AdministrationDAO#getGlobalPropertiesBySuffix(java.lang.String)
140
         */
141
        @Override
142
        @SuppressWarnings("unchecked")
143
        public List<GlobalProperty> getGlobalPropertiesBySuffix(String suffix) {
144
                return sessionFactory.getCurrentSession().createCriteria(GlobalProperty.class)
1✔
145
                        .add(Restrictions.ilike(PROPERTY, suffix, MatchMode.END)).list();
1✔
146
        }
147
        
148
        /**
149
         * @see org.openmrs.api.db.AdministrationDAO#deleteGlobalProperty(GlobalProperty)
150
         */
151
        @Override
152
        public void deleteGlobalProperty(GlobalProperty property) throws DAOException {
153
                sessionFactory.getCurrentSession().delete(property);
1✔
154
        }
1✔
155
        
156
        /**
157
         * @see org.openmrs.api.db.AdministrationDAO#saveGlobalProperty(org.openmrs.GlobalProperty)
158
         */
159
        @Override
160
        public GlobalProperty saveGlobalProperty(GlobalProperty gp) throws DAOException {
161
                GlobalProperty gpObject = getGlobalPropertyObject(gp.getProperty());
1✔
162
                if (gpObject != null) {
1✔
163
                        gpObject.setPropertyValue(gp.getPropertyValue());
1✔
164
                        gpObject.setDescription(gp.getDescription());
1✔
165
                        sessionFactory.getCurrentSession().update(gpObject);
1✔
166
                        return gpObject;
1✔
167
                } else {
168
                        sessionFactory.getCurrentSession().save(gp);
1✔
169
                        return gp;
1✔
170
                }
171
        }
172
        
173
        /**
174
         * @see org.openmrs.api.db.AdministrationDAO#executeSQL(java.lang.String, boolean)
175
         */
176
        @Override
177
        public List<List<Object>> executeSQL(String sql, boolean selectOnly) throws DAOException {
178
                
179
                // (solution for junit tests that usually use hsql
180
                // hsql does not like the backtick.  Replace the backtick with the hsql
181
                // escape character: the double quote (or nothing).
182
                if (HibernateUtil.isHSQLDialect(sessionFactory)) {
1✔
183
                        sql = sql.replace("`", "");
×
184
                }
185
                return DatabaseUtil.executeSQL(sessionFactory.getCurrentSession(), sql, selectOnly);
1✔
186
        }
187
        
188
        @Override
189
        public int getMaximumPropertyLength(Class<? extends OpenmrsObject> aClass, String fieldName) {
190
                PersistentClass persistentClass = metadata.getEntityBinding(aClass.getName().split("_")[0]);
1✔
191
                if (persistentClass == null) {
1✔
192
                        throw new APIException("Couldn't find a class in the hibernate configuration named: " + aClass.getName());
×
193
                } else {
194
                        int fieldLength;
195
                        try {
196
                                fieldLength = ((Column) persistentClass.getProperty(fieldName).getColumnIterator().next()).getLength();
1✔
197
                        }
198
                        catch (Exception e) {
×
199
                                log.debug("Could not determine maximum length", e);
×
200
                                return -1;
×
201
                        }
1✔
202
                        return fieldLength;
1✔
203
                }
204
        }
205
        
206
        @Override
207
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
208
                HibernateSessionFactoryBean sessionFactoryBean = (HibernateSessionFactoryBean) applicationContext
1✔
209
                        .getBean("&sessionFactory");
1✔
210
                metadata = sessionFactoryBean.getMetadata();
1✔
211
        }
1✔
212
        
213
        /**
214
         * @see org.openmrs.api.db.AdministrationDAO#validate(java.lang.Object, Errors)
215
         * <strong>Should</strong> Pass validation if field lengths are correct
216
         * <strong>Should</strong> Fail validation if field lengths are not correct
217
         * <strong>Should</strong> Fail validation for location class if field lengths are not correct
218
         * <strong>Should</strong> Pass validation for location class if field lengths are correct
219
         */
220
        
221
        //@SuppressWarnings({ "deprecation", "unchecked", "rawtypes" })
222
        @Override
223
        public void validate(Object object, Errors errors) throws DAOException {
224
                Class entityClass = object.getClass();
1✔
225
                ClassMetadata metadata = null;
1✔
226
                try {
227
                        metadata = sessionFactory.getClassMetadata(entityClass);
1✔
228
                }
229
                catch (MappingException ex) {
1✔
230
                        log.debug(entityClass + " is not a hibernate mapped entity", ex);
1✔
231
                }
1✔
232
                if (metadata != null) {
1✔
233
                        String[] propNames = metadata.getPropertyNames();
1✔
234
                        Object identifierType = metadata.getIdentifierType();
1✔
235
                        String identifierName = metadata.getIdentifierPropertyName();
1✔
236
                        if (identifierType instanceof StringType || identifierType instanceof TextType) {
1✔
237
                                int maxLength = getMaximumPropertyLength(entityClass, identifierName);
1✔
238
                                String identifierValue = (String) metadata.getIdentifier(object,
1✔
239
                                    (SessionImplementor) sessionFactory.getCurrentSession());
1✔
240
                                if (identifierValue != null) {
1✔
241
                                        int identifierLength = identifierValue.length();
1✔
242
                                        if (identifierLength > maxLength) {
1✔
243
                                                
244
                                                errors.rejectValue(identifierName, "error.exceededMaxLengthOfField", new Object[] { maxLength },
1✔
245
                                                    null);
246
                                        }
247
                                }
248
                        }
249
                        for (String propName : propNames) {
1✔
250
                                Type propType = metadata.getPropertyType(propName);
1✔
251
                                if (propType instanceof StringType || propType instanceof TextType) {
1✔
252
                                        String propertyValue = (String) metadata.getPropertyValue(object, propName);
1✔
253
                                        if (propertyValue != null) {
1✔
254
                                                int maxLength = getMaximumPropertyLength(entityClass, propName);
1✔
255
                                                int propertyValueLength = propertyValue.length();
1✔
256
                                                if (propertyValueLength > maxLength) {
1✔
257
                                                        errors.rejectValue(propName, "error.exceededMaxLengthOfField", new Object[] { maxLength },
1✔
258
                                                                        null);
259
                                                }
260
                                        }
261
                                }
262
                        }
263
                }
264
                FlushMode previousFlushMode = sessionFactory.getCurrentSession().getHibernateFlushMode();
1✔
265
                sessionFactory.getCurrentSession().setHibernateFlushMode(FlushMode.MANUAL);
1✔
266
                try {
267
                        for (Validator validator : getValidators(object)) {
1✔
268
                                validator.validate(object, errors);
1✔
269
                        }
1✔
270
                        
271
                }
272
                
273
                finally {
274
                        sessionFactory.getCurrentSession().setHibernateFlushMode(previousFlushMode);
1✔
275
                }
276
                
277
        }
1✔
278
        
279
        /**
280
         * Fetches all validators that are registered
281
         *
282
         * @param obj the object that will be validated
283
         * @return list of compatible validators
284
         */
285
        protected List<Validator> getValidators(Object obj) {
286
                List<Validator> matchingValidators = new ArrayList<>();
1✔
287

288
                List<Validator> validators = HandlerUtil.getHandlersForType(Validator.class, obj.getClass());
1✔
289
                
290
                for (Validator validator : validators) {
1✔
291
                        if (validator.supports(obj.getClass())) {
1✔
292
                                matchingValidators.add(validator);
1✔
293
                        }
294
                }
1✔
295
                
296
                return matchingValidators;
1✔
297
        }
298
        
299
        @Override
300
        public boolean isDatabaseStringComparisonCaseSensitive() {
301
                GlobalProperty gp = (GlobalProperty) sessionFactory.getCurrentSession().get(GlobalProperty.class,
1✔
302
                    OpenmrsConstants.GP_CASE_SENSITIVE_DATABASE_STRING_COMPARISON);
303
                if (gp != null) {
1✔
304
                        return Boolean.valueOf(gp.getPropertyValue());
1✔
305
                } else {
UNCOV
306
                        return true;
×
307
                }
308
        }
309
        
310
        /**
311
         * Updates PostgreSQL Sequences after core data insertion
312
         * 
313
         * @see org.openmrs.api.db.AdministrationDAO#updatePostgresSequence()
314
         */
315
        @Override
316
        public void updatePostgresSequence() throws DAOException {
317
                
318
                if (HibernateUtil.isPostgreSQLDialect(sessionFactory)) {
×
319
                        
320
                        // All the required PostgreSQL sequences that need to be updated
321
                        String postgresSequences = "SELECT setval('person_person_id_seq', (SELECT MAX(person_id) FROM person)+1);"
×
322
                                + "SELECT setval('person_name_person_name_id_seq', (SELECT MAX(person_name_id) FROM person_name)+1);"
323
                                + "SELECT setval('person_attribute_type_person_attribute_type_id_seq', (SELECT MAX(person_attribute_type_id) FROM person_attribute_type)+1);"
324
                                + "SELECT setval('relationship_type_relationship_type_id_seq', (SELECT MAX(relationship_type_id) FROM relationship_type)+1);"
325
                                + "SELECT setval('users_user_id_seq', (SELECT MAX(user_id) FROM users)+1);"
326
                                + "SELECT setval('care_setting_care_setting_id_seq', (SELECT MAX(care_setting_id) FROM care_setting)+1);"
327
                                + "SELECT setval('concept_datatype_concept_datatype_id_seq', (SELECT MAX(concept_datatype_id) FROM concept_datatype)+1);"
328
                                + "SELECT setval('concept_map_type_concept_map_type_id_seq', (SELECT MAX(concept_map_type_id) FROM concept_map_type)+1);"
329
                                + "SELECT setval('concept_stop_word_concept_stop_word_id_seq', (SELECT MAX(concept_stop_word_id) FROM concept_stop_word)+1);"
330
                                + "SELECT setval('concept_concept_id_seq', (SELECT MAX(concept_id) FROM concept)+1);"
331
                                + "SELECT setval('concept_name_concept_name_id_seq', (SELECT MAX(concept_name_id) FROM concept_name)+1);"
332
                                + "SELECT setval('concept_class_concept_class_id_seq', (SELECT MAX(concept_class_id) FROM concept_class)+1);"
333
                                + "SELECT setval('concept_reference_source_concept_source_id_seq', (SELECT MAX(concept_source_id) FROM concept_reference_source)+1);"
334
                                + "SELECT setval('encounter_role_encounter_role_id_seq', (SELECT MAX(encounter_role_id) FROM encounter_role)+1);"
335
                                + "SELECT setval('field_type_field_type_id_seq', (SELECT MAX(field_type_id) FROM field_type)+1);"
336
                                + "SELECT setval('hl7_source_hl7_source_id_seq', (SELECT MAX(hl7_source_id) FROM hl7_source)+1);"
337
                                + "SELECT setval('location_location_id_seq', (SELECT MAX(location_id) FROM location)+1);"
338
                                + "SELECT setval('encounter_encounter_id_seq', (SELECT MAX(encounter_id) FROM encounter)+1);"
339
                                + "SELECT setval('concept_description_concept_description_id_seq', (SELECT MAX(concept_description_id) FROM concept_description)+1);"
340
                                + "SELECT setval('conditions_condition_id_seq', (SELECT MAX(condition_id) FROM conditions)+1);"
341
                                + "SELECT setval('encounter_diagnosis_diagnosis_id_seq', (SELECT MAX(diagnosis_id) FROM encounter_diagnosis)+1);"
342
                                + "SELECT setval('diagnosis_attribute_type_diagnosis_attribute_type_id_seq', (SELECT MAX(diagnosis_attribute_type_id) FROM diagnosis_attribute_type)+1);"
343
                                + "SELECT setval('visit_visit_id_seq', (SELECT MAX(visit_id) FROM visit)+1);"
344
                                + "SELECT setval('concept_reference_term_concept_reference_term_id_seq', (SELECT MAX(concept_reference_term_id) FROM concept_reference_term)+1);"
345
                                + "SELECT setval('orders_order_id_seq', (SELECT MAX(order_id) FROM orders)+1);"
346
                                + "SELECT setval('order_group_order_group_id_seq', (SELECT MAX(order_group_id) FROM order_group)+1);"
347
                                + "SELECT setval('concept_reference_map_concept_map_id_seq', (SELECT MAX(concept_map_id) FROM concept_reference_map)+1);"
348
                                + "SELECT setval('order_group_attribute_type_order_group_attribute_type_id_seq', (SELECT MAX(order_group_attribute_type_id) FROM order_group_attribute_type)+1);"
349
                                + "SELECT setval('encounter_provider_encounter_provider_id_seq', (SELECT MAX(encounter_provider_id) FROM encounter_provider)+1);"
350
                                + "SELECT setval('provider_attribute_type_provider_attribute_type_id_seq', (SELECT MAX(provider_attribute_type_id) FROM provider_attribute_type)+1);"
351
                                + "SELECT setval('program_attribute_type_program_attribute_type_id_seq', (SELECT MAX(program_attribute_type_id) FROM program_attribute_type)+1);"
352
                                + "SELECT setval('concept_state_conversion_concept_state_conversion_id_seq', (SELECT MAX(concept_state_conversion_id) FROM concept_state_conversion)+1);"
353
                                + "SELECT setval('program_program_id_seq', (SELECT MAX(program_id) FROM program)+1);"
354
                                + "SELECT setval('concept_attribute_type_concept_attribute_type_id_seq', (SELECT MAX(concept_attribute_type_id) FROM concept_attribute_type)+1);"
355
                                + "SELECT setval('concept_name_tag_concept_name_tag_id_seq', (SELECT MAX(concept_name_tag_id) FROM concept_name_tag)+1);"
356
                                + "SELECT setval('allergy_reaction_allergy_reaction_id_seq', (SELECT MAX(allergy_reaction_id) FROM allergy_reaction)+1);"
357
                                + "SELECT setval('cohort_cohort_id_seq', (SELECT MAX(cohort_id) FROM cohort)+1);"
358
                                + "SELECT setval('cohort_member_cohort_member_id_seq', (SELECT MAX(cohort_member_id) FROM cohort_member)+1);"
359
                                + "SELECT setval('visit_type_visit_type_id_seq', (SELECT MAX(visit_type_id) FROM visit_type)+1);"
360
                                + "SELECT setval('visit_attribute_type_visit_attribute_type_id_seq', (SELECT MAX(visit_attribute_type_id) FROM visit_attribute_type)+1);"
361
                                + "SELECT setval('order_attribute_type_order_attribute_type_id_seq', (SELECT MAX(order_attribute_type_id) FROM order_attribute_type)+1);"
362
                                + "SELECT setval('medication_dispense_medication_dispense_id_seq', (SELECT MAX(medication_dispense_id) FROM medication_dispense)+1);"
363
                                + "SELECT setval('order_set_attribute_type_order_set_attribute_type_id_seq', (SELECT MAX(order_set_attribute_type_id) FROM order_set_attribute_type)+1);"
364
                                + "SELECT setval('person_address_person_address_id_seq', (SELECT MAX(person_address_id) FROM person_address)+1);"
365
                                + "SELECT setval('patient_identifier_patient_identifier_id_seq', (SELECT MAX(patient_identifier_id) FROM patient_identifier)+1);"
366
                                + "SELECT setval('relationship_relationship_id_seq', (SELECT MAX(relationship_id) FROM relationship)+1);"
367
                                + "SELECT setval('provider_provider_id_seq', (SELECT MAX(provider_id) FROM provider)+1);"
368
                                + "SELECT setval('encounter_type_encounter_type_id_seq', (SELECT MAX(encounter_type_id) FROM encounter_type)+1);"
369
                                + "SELECT setval('person_attribute_person_attribute_id_seq', (SELECT MAX(person_attribute_id) FROM person_attribute)+1);"
370
                                + "SELECT setval('allergy_allergy_id_seq', (SELECT MAX(allergy_id) FROM allergy)+1);"
371
                                + "SELECT setval('location_attribute_type_location_attribute_type_id_seq', (SELECT MAX(location_attribute_type_id) FROM location_attribute_type)+1);"
372
                                + "SELECT setval('order_frequency_order_frequency_id_seq', (SELECT MAX(order_frequency_id) FROM order_frequency)+1);"
373
                                + "SELECT setval('patient_program_patient_program_id_seq', (SELECT MAX(patient_program_id) FROM patient_program)+1);"
374
                                + "SELECT setval('form_field_form_field_id_seq', (SELECT MAX(form_field_id) FROM form_field)+1);"
375
                                + "SELECT setval('concept_proposal_concept_proposal_id_seq', (SELECT MAX(concept_proposal_id) FROM concept_proposal)+1);"
376
                                + "SELECT setval('program_workflow_program_workflow_id_seq', (SELECT MAX(program_workflow_id) FROM program_workflow)+1);"
377
                                + "SELECT setval('program_workflow_state_program_workflow_state_id_seq', (SELECT MAX(program_workflow_state_id) FROM program_workflow_state)+1);"
378
                                + "SELECT setval('patient_state_patient_state_id_seq', (SELECT MAX(patient_state_id) FROM patient_state)+1);"
379
                                + "SELECT setval('obs_obs_id_seq', (SELECT MAX(obs_id) FROM obs)+1);"
380
                                + "SELECT setval('order_type_order_type_id_seq', (SELECT MAX(order_type_id) FROM order_type)+1);"
381
                                + "SELECT setval('patient_identifier_type_patient_identifier_type_id_seq', (SELECT MAX(patient_identifier_type_id) FROM patient_identifier_type)+1);"
382
                                + "SELECT setval('scheduler_task_config_task_config_id_seq', (SELECT MAX(task_config_id) FROM scheduler_task_config)+1);"
383
                                + "SELECT setval('scheduler_task_config_property_task_config_property_id_seq', (SELECT MAX(task_config_property_id) FROM scheduler_task_config_property)+1)"
384
                                + "";
385
                        Session session = sessionFactory.getCurrentSession();
×
386
                        
387
                        session.doWork(new Work() {
×
388
                                
389
                                @Override
390
                                public void execute(Connection con) throws SQLException {
391
                                        Statement stmt = con.createStatement();
×
392
                                        stmt.addBatch(postgresSequences);
×
393
                                        stmt.executeBatch();
×
394
                                }
×
395
                        });
396
                }
397
        }
×
398
}
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