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

geo-engine / geoengine / 19679817551

25 Nov 2025 06:19PM UTC coverage: 88.463%. First build
19679817551

Pull #1096

github

web-flow
Merge 54b608b6c into 5dc8f9b67
Pull Request #1096: fix ogc open api spec

225 of 240 new or added lines in 5 files covered. (93.75%)

116710 of 131931 relevant lines covered (88.46%)

492112.57 hits per line

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

23.44
/services/src/error.rs
1
use crate::api::model::datatypes::{
2
    DatasetId, SpatialReference, SpatialReferenceOption, TimeInstance, TimeInterval,
3
};
4
use crate::api::model::responses::ErrorResponse;
5
use crate::datasets::external::aruna::error::ArunaProviderError;
6
use crate::datasets::external::netcdfcf::NetCdfCf4DProviderError;
7
use crate::{layers::listing::LayerCollectionId, workflows::workflow::WorkflowId};
8
use actix_web::HttpResponse;
9
use actix_web::http::StatusCode;
10
use geoengine_datatypes::dataset::{DataProviderId, LayerId};
11
use geoengine_datatypes::error::ErrorSource;
12
use geoengine_datatypes::primitives::RegularTimeDimension;
13
use geoengine_datatypes::util::helpers::ge_report;
14
use ordered_float::FloatIsNan;
15
use snafu::prelude::*;
16
use std::fmt;
17
use std::path::PathBuf;
18
use strum::IntoStaticStr;
19

20
pub type Result<T, E = Error> = std::result::Result<T, E>;
21

22
#[derive(Snafu, IntoStaticStr)]
23
#[snafu(visibility(pub(crate)))]
24
#[snafu(context(suffix(false)))] // disables default `Snafu` suffix
25
pub enum Error {
26
    #[snafu(transparent)]
27
    DataType {
28
        source: geoengine_datatypes::error::Error,
29
    },
30
    #[snafu(transparent)]
31
    Operator {
32
        source: geoengine_operators::error::Error,
33
    },
34
    #[snafu(display("Uuid error: {source}"))]
35
    Uuid {
36
        source: uuid::Error,
37
    },
38
    #[snafu(display("Serde json error: {source}"))]
39
    SerdeJson {
40
        source: serde_json::Error,
41
    },
42
    Io {
43
        source: std::io::Error,
44
    },
45
    TokioJoin {
46
        source: tokio::task::JoinError,
47
    },
48

49
    TokioSignal {
50
        source: std::io::Error,
51
    },
52

53
    Reqwest {
54
        source: reqwest::Error,
55
    },
56
    #[snafu(display("Unable to parse url: {source}"))]
57
    Url {
58
        source: url::ParseError,
59
    },
60
    Proj {
61
        source: proj::ProjError,
62
    },
63

64
    #[snafu(context(false))]
65
    Trace {
66
        source: opentelemetry_sdk::trace::TraceError,
67
    },
68
    #[snafu(context(false))]
69
    TraceOltp {
70
        source: opentelemetry_otlp::ExporterBuildError,
71
    },
72

73
    TokioChannelSend,
74

75
    #[snafu(display("Unable to parse query string: {}", source))]
76
    UnableToParseQueryString {
77
        source: serde_urlencoded::de::Error,
78
    },
79

80
    #[snafu(display("Unable to serialize query string: {}", source))]
81
    UnableToSerializeQueryString {
82
        source: serde_urlencoded::ser::Error,
83
    },
84

85
    ServerStartup,
86

87
    #[snafu(display("Registration failed: {}", reason))]
88
    RegistrationFailed {
89
        reason: String,
90
    },
91
    #[snafu(display("Tried to create duplicate: {}", reason))]
92
    Duplicate {
93
        reason: String,
94
    },
95
    #[snafu(display("User does not exist or password is wrong."))]
96
    LoginFailed,
97
    LogoutFailed,
98
    #[snafu(display("The session id is invalid."))]
99
    InvalidSession,
100
    #[snafu(display("Invalid admin token"))]
101
    InvalidAdminToken,
102
    #[snafu(display("Header with authorization token not provided."))]
103
    MissingAuthorizationHeader,
104
    #[snafu(display("Authentication scheme must be Bearer."))]
105
    InvalidAuthorizationScheme,
106

107
    #[snafu(display("Authorization error: {}", source))]
108
    Unauthorized {
109
        source: Box<Error>,
110
    },
111
    AccessDenied,
112
    #[snafu(display("Failed to create the project."))]
113
    ProjectCreateFailed,
114
    #[snafu(display("Failed to list projects."))]
115
    ProjectListFailed,
116
    #[snafu(display("The project failed to load."))]
117
    ProjectLoadFailed,
118
    #[snafu(display("Failed to update the project."))]
119
    ProjectUpdateFailed,
120
    #[snafu(display("Failed to delete the project."))]
121
    ProjectDeleteFailed,
122
    PermissionFailed,
123
    #[snafu(display("A permission error occurred: {source}."))]
124
    PermissionDb {
125
        source: Box<dyn ErrorSource>,
126
    },
127
    #[snafu(display("A role error occurred: {source}."))]
128
    RoleDb {
129
        source: Box<dyn ErrorSource>,
130
    },
131
    ProjectDbUnauthorized,
132

133
    InvalidNamespace,
134

135
    InvalidSpatialReference,
136
    #[snafu(display("SpatialReferenceMissmatch: Found {}, expected: {}", found, expected))]
137
    SpatialReferenceMissmatch {
138
        found: SpatialReferenceOption,
139
        expected: SpatialReferenceOption,
140
    },
141

142
    InvalidWfsTypeNames,
143

144
    NoWorkflowForGivenId,
145

146
    TokioPostgres {
147
        source: bb8_postgres::tokio_postgres::Error,
148
    },
149

150
    TokioPostgresTimeout,
151

152
    #[snafu(display(
153
        "Database cannot be cleared on startup because it was started without that setting before."
154
    ))]
155
    ClearDatabaseOnStartupNotAllowed,
156

157
    #[snafu(display("Database schema must not be `public`."))]
158
    InvalidDatabaseSchema,
159

160
    #[snafu(display("Identifier does not have the right format."))]
161
    InvalidUuid,
162
    SessionNotInitialized,
163

164
    ConfigLockFailed,
165

166
    Config {
167
        source: config::ConfigError,
168
    },
169

170
    #[snafu(display("Unable to parse IP address: {source}"))]
171
    AddrParse {
172
        source: std::net::AddrParseError,
173
    },
174

175
    #[snafu(display("Missing working directory"))]
176
    MissingWorkingDirectory {
177
        source: std::io::Error,
178
    },
179

180
    MissingSettingsDirectory,
181

182
    DataIdTypeMissMatch,
183
    UnknownDataId,
184
    UnknownProviderId,
185
    MissingDatasetId,
186

187
    UnknownDatasetId,
188

189
    OperationRequiresAdminPrivilige,
190
    OperationRequiresOwnerPermission,
191

192
    #[snafu(display("Permission denied for dataset with id {:?}", dataset))]
193
    DatasetPermissionDenied {
194
        dataset: DatasetId,
195
    },
196

197
    #[snafu(display("Updating permission ({}, {:?}, {}) denied", role, dataset, permission))]
198
    UpdateDatasetPermission {
199
        role: String,
200
        dataset: DatasetId,
201
        permission: String,
202
    },
203

204
    #[snafu(display("Permission ({}, {:?}, {}) already exists", role, dataset, permission))]
205
    DuplicateDatasetPermission {
206
        role: String,
207
        dataset: DatasetId,
208
        permission: String,
209
    },
210

211
    // TODO: move to pro folder, because permissions are pro only
212
    #[snafu(display("Permission denied"))]
213
    PermissionDenied,
214

215
    #[snafu(display("Parameter {} must have length between {} and {}", parameter, min, max))]
216
    InvalidStringLength {
217
        parameter: String,
218
        min: usize,
219
        max: usize,
220
    },
221

222
    #[snafu(display("Limit must be <= {}", limit))]
223
    InvalidListLimit {
224
        limit: usize,
225
    },
226

227
    UploadFieldMissingFileName,
228
    UnknownUploadId,
229
    PathIsNotAFile,
230
    #[snafu(display("Failed loading multipart body: {reason}"))]
231
    Multipart {
232
        // TODO: this error is not send, so this does not work
233
        // source: actix_multipart::MultipartError,
234
        reason: String,
235
    },
236
    InvalidUploadFileName,
237
    InvalidDatasetIdNamespace,
238
    DuplicateDatasetId,
239
    #[snafu(display("Dataset name '{}' already exists", dataset_name))]
240
    DatasetNameAlreadyExists {
241
        dataset_name: String,
242
        dataset_id: DatasetId,
243
    },
244
    #[snafu(display("Dataset name '{}' does not exist", dataset_name))]
245
    UnknownDatasetName {
246
        dataset_name: String,
247
    },
248
    InvalidDatasetName,
249
    #[snafu(display("Layer name '{layer_name}' is invalid"))]
250
    DatasetInvalidLayerName {
251
        layer_name: String,
252
    },
253
    DatasetHasNoAutoImportableLayer,
254
    #[snafu(display("GdalError: {}", source))]
255
    Gdal {
256
        source: gdal::errors::GdalError,
257
    },
258
    EmptyDatasetCannotBeImported,
259
    NoMainFileCandidateFound,
260
    NoFeatureDataTypeForColumnDataType,
261

262
    #[snafu(display("Spatial reference '{srs_string}' is unknown"))]
263
    UnknownSpatialReference {
264
        srs_string: String,
265
    },
266

267
    NotYetImplemented,
268

269
    #[snafu(display("Band '{band_name}' does not exist"))]
270
    StacNoSuchBand {
271
        band_name: String,
272
    },
273
    StacInvalidGeoTransform,
274
    StacInvalidBbox,
275
    #[snafu(display(
276
        "Failed to parse stac response from '{url}'. Error: {error}\nOriginal Response: {response}"
277
    ))]
278
    StacJsonResponse {
279
        url: String,
280
        response: String,
281
        error: serde_json::Error,
282
    },
283
    RasterDataTypeNotSupportByGdal,
284

285
    MissingSpatialReference,
286

287
    WcsVersionNotSupported,
288
    WcsGridOriginMustEqualBoundingboxUpperLeft,
289
    WcsBoundingboxCrsMustEqualGridBaseCrs,
290
    WcsInvalidGridOffsets,
291

292
    InvalidDatasetId,
293

294
    PangaeaNoTsv,
295
    GfbioMissingAbcdField,
296
    #[snafu(display("The response from the EDR server does not match the expected format."))]
297
    EdrInvalidMetadataFormat,
298
    ExpectedExternalDataId,
299
    InvalidDataId,
300

301
    Nature40UnknownRasterDbname,
302
    Nature40WcsDatasetMissingLabelInMetadata,
303

304
    #[snafu(display("FlexiLogger initialization error"))]
305
    Logger {
306
        source: flexi_logger::FlexiLoggerError,
307
    },
308

309
    #[snafu(display("Spatial reference system '{srs_string}' is unknown"))]
310
    UnknownSrsString {
311
        srs_string: String,
312
    },
313

314
    #[snafu(display("Axis ordering is unknown for SRS '{srs_string}'"))]
315
    AxisOrderingNotKnownForSrs {
316
        srs_string: String,
317
    },
318

319
    #[snafu(display("Anonymous access is disabled, please log in"))]
320
    AnonymousAccessDisabled,
321

322
    #[snafu(display("User registration is disabled"))]
323
    UserRegistrationDisabled,
324

325
    UserDoesNotExist,
326
    RoleDoesNotExist,
327
    RoleWithNameAlreadyExists,
328
    RoleAlreadyAssigned,
329
    RoleNotAssigned,
330

331
    #[snafu(display(
332
        "WCS request endpoint {} must match identifier {}",
333
        endpoint,
334
        identifier
335
    ))]
336
    WCSEndpointIdentifierMissmatch {
337
        endpoint: WorkflowId,
338
        identifier: WorkflowId,
339
    },
340
    #[snafu(display(
341
        "WCS request endpoint {} must match identifiers {}",
342
        endpoint,
343
        identifiers
344
    ))]
345
    WCSEndpointIdentifiersMissmatch {
346
        endpoint: WorkflowId,
347
        identifiers: WorkflowId,
348
    },
349
    #[snafu(display("WMS request endpoint {} must match layer {}", endpoint, layer))]
350
    WMSEndpointLayerMissmatch {
351
        endpoint: WorkflowId,
352
        layer: WorkflowId,
353
    },
354
    #[snafu(display(
355
        "WFS request endpoint {} must match type_names {}",
356
        endpoint,
357
        type_names
358
    ))]
359
    WFSEndpointTypeNamesMissmatch {
360
        endpoint: WorkflowId,
361
        type_names: WorkflowId,
362
    },
363

364
    #[snafu(context(false))]
365
    ArunaProvider {
366
        source: ArunaProviderError,
367
    },
368

369
    #[snafu(context(false))]
370
    NetCdfCf4DProvider {
371
        source: NetCdfCf4DProviderError,
372
    },
373

374
    AbcdUnitIdColumnMissingInDatabase,
375

376
    BaseUrlMustEndWithSlash,
377

378
    #[snafu(context(false))]
379
    LayerDb {
380
        source: crate::layers::LayerDbError,
381
    },
382

383
    #[snafu(display("Operator '{operator}' is unknown"))]
384
    UnknownOperator {
385
        operator: String,
386
    },
387

388
    #[snafu(display("The id is expected to be an uuid, but it is '{found}'."))]
389
    IdStringMustBeUuid {
390
        found: String,
391
    },
392

393
    #[snafu(context(false), display("TaskError: {}", source))]
394
    Task {
395
        source: crate::tasks::TaskError,
396
    },
397

398
    #[snafu(display("'{id}' is not a known layer collection id"))]
399
    UnknownLayerCollectionId {
400
        id: LayerCollectionId,
401
    },
402
    #[snafu(display("'{id}' is not a known layer id"))]
403
    UnknownLayerId {
404
        id: LayerId,
405
    },
406
    InvalidLayerCollectionId,
407
    InvalidLayerId,
408

409
    #[snafu(context(false))]
410
    WorkflowApi {
411
        source: crate::api::handlers::workflows::WorkflowApiError,
412
    },
413

414
    #[snafu(display("The sub path '{}' escapes the base path '{}'", sub_path.display(), base.display()))]
415
    SubPathMustNotEscapeBasePath {
416
        base: PathBuf,
417
        sub_path: PathBuf,
418
    },
419

420
    #[snafu(display("The sub path '{}' contains references to the parent '{}'", sub_path.display(), base.display()))]
421
    PathMustNotContainParentReferences {
422
        base: PathBuf,
423
        sub_path: PathBuf,
424
    },
425

426
    #[snafu(display("Time instance must be between {} and {}, but is {}", min.inner(), max.inner(), is))]
427
    InvalidTimeInstance {
428
        min: TimeInstance,
429
        max: TimeInstance,
430
        is: i64,
431
    },
432

433
    #[snafu(display("ParseU32: {}", source))]
434
    ParseU32 {
435
        source: <u32 as std::str::FromStr>::Err,
436
    },
437
    #[snafu(display("InvalidSpatialReferenceString: {}", spatial_reference_string))]
438
    InvalidSpatialReferenceString {
439
        spatial_reference_string: String,
440
    },
441

442
    #[snafu(context(false), display("OidcError: {}", source))]
443
    Oidc {
444
        source: crate::users::OidcError,
445
    },
446

447
    #[snafu(display(
448
        "Could not resolve a Proj string for this SpatialReference: {}",
449
        spatial_ref
450
    ))]
451
    ProjStringUnresolvable {
452
        spatial_ref: SpatialReference,
453
    },
454

455
    #[snafu(display(
456
        "Cannot resolve the query's BBOX ({:?}) in the selected SRS ({})",
457
        query_bbox,
458
        query_srs
459
    ))]
460
    UnresolvableQueryBoundingBox2DInSrs {
461
        query_srs: SpatialReference,
462
        query_bbox: crate::api::model::datatypes::BoundingBox2D,
463
    },
464

465
    #[snafu(display("Result Descriptor field '{}' {}", field, cause))]
466
    LayerResultDescriptorMissingFields {
467
        field: String,
468
        cause: String,
469
    },
470

471
    ProviderDoesNotSupportBrowsing,
472

473
    InvalidPath,
474

475
    InvalidWorkflowOutputType,
476

477
    #[snafu(display("Functionality is not implemented: '{}'", message))]
478
    NotImplemented {
479
        message: String,
480
    },
481

482
    #[snafu(display("NotNan error: {}", source))]
483
    InvalidNotNanFloatKey {
484
        source: ordered_float::FloatIsNan,
485
    },
486

487
    UnexpectedInvalidDbTypeConversion,
488

489
    #[snafu(display(
490
        "Unexpected database version during migration, expected `{}` but found `{}`",
491
        expected,
492
        found
493
    ))]
494
    UnexpectedDatabaseVersionDuringMigration {
495
        expected: String,
496
        found: String,
497
    },
498

499
    #[snafu(display("Raster band names must be unique. Found {duplicate_key} more than once."))]
500
    RasterBandNamesMustBeUnique {
501
        duplicate_key: String,
502
    },
503
    #[snafu(display("Raster band names must not be empty"))]
504
    RasterBandNameMustNotBeEmpty,
505
    #[snafu(display("Raster band names must not be longer than 256 bytes"))]
506
    RasterBandNameTooLong,
507

508
    #[snafu(display("Resource id is invalid: type: {}, id: {}", resource_type, resource_id))]
509
    InvalidResourceId {
510
        resource_type: String,
511
        resource_id: String,
512
    },
513

514
    #[snafu(display("Unknown volume name: {}", volume_name))]
515
    UnknownVolumeName {
516
        volume_name: String,
517
    },
518

519
    #[snafu(display("Cannot access path of volume with name {}", volume_name))]
520
    CannotAccessVolumePath {
521
        volume_name: String,
522
    },
523

524
    #[snafu(display("A provider with id '{}' already exists", provider_id))]
525
    ProviderIdAlreadyExists {
526
        provider_id: DataProviderId,
527
    },
528

529
    #[snafu(display("An existing provider's type cannot be modified"))]
530
    ProviderTypeUnmodifiable,
531

532
    #[snafu(display("An existing provider's id cannot be modified"))]
533
    ProviderIdUnmodifiable,
534

535
    #[snafu(display("Unknown resource name {} of kind {}", name, kind))]
536
    UnknownResource {
537
        kind: String,
538
        name: String,
539
    },
540

541
    #[snafu(display("MachineLearning error: {}", source))]
542
    MachineLearning {
543
        // TODO: make `source: MachineLearningError`, once pro features is removed
544
        source: Box<dyn ErrorSource>,
545
    },
546

547
    DatasetName {
548
        source: crate::datasets::DatasetNameError,
549
    },
550

551
    MlModelName {
552
        source: geoengine_datatypes::machine_learning::MlModelNameError,
553
    },
554

555
    #[snafu(display("WildLIVE connector error: {source}"), context(false))]
556
    Wildlive {
557
        source: crate::datasets::external::WildliveError,
558
    },
559
    #[snafu(display(
560
        "Dataset tile time `{times:?}` conflict with existing times `{existing_times:?}`"
561
    ))]
562
    DatasetTileTimeConflict {
563
        times: Vec<TimeInterval>,
564
        existing_times: Vec<TimeInterval>,
565
    },
566
    #[snafu(display(
567
        "Dataset tile times `{times:?}` conflict with dataset regularity {time_dim:?}"
568
    ))]
569
    DatasetTileRegularTimeConflict {
570
        times: Vec<TimeInterval>,
571
        time_dim: RegularTimeDimension,
572
    },
573
    #[snafu(display(
574
        "Dataset tile z-index of files `{files:?}` conflict with existing tiles with the same z-indexes"
575
    ))]
576
    DatasetTileZIndexConflict {
577
        files: Vec<String>,
578
    },
579
}
580

581
impl actix_web::error::ResponseError for Error {
582
    fn error_response(&self) -> HttpResponse {
38✔
583
        HttpResponse::build(self.status_code()).json(ErrorResponse::from_service_error(self))
38✔
584
    }
38✔
585

586
    fn status_code(&self) -> StatusCode {
38✔
587
        match self {
38✔
588
            Error::Unauthorized { source: _ } => StatusCode::UNAUTHORIZED,
15✔
589
            Error::Duplicate { reason: _ } => StatusCode::CONFLICT,
1✔
590
            _ => StatusCode::BAD_REQUEST,
22✔
591
        }
592
    }
38✔
593
}
594

595
impl fmt::Debug for Error {
596
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38✔
597
        write!(f, "{}", ge_report(self))
38✔
598
    }
38✔
599
}
600

601
impl From<bb8_postgres::bb8::RunError<<bb8_postgres::PostgresConnectionManager<bb8_postgres::tokio_postgres::NoTls> as bb8_postgres::bb8::ManageConnection>::Error>> for Error {
602
    fn from(e: bb8_postgres::bb8::RunError<<bb8_postgres::PostgresConnectionManager<bb8_postgres::tokio_postgres::NoTls> as bb8_postgres::bb8::ManageConnection>::Error>) -> Self {
×
603
        match e {
×
604
            bb8_postgres::bb8::RunError::User(e) => Self::TokioPostgres { source: e },
×
605
            bb8_postgres::bb8::RunError::TimedOut => Self::TokioPostgresTimeout,
×
606
        }
607
    }
×
608
}
609

610
// TODO: remove automatic conversion to our Error because we do not want to leak database internals in the API
611

612
impl From<bb8_postgres::tokio_postgres::error::Error> for Error {
613
    fn from(e: bb8_postgres::tokio_postgres::error::Error) -> Self {
10✔
614
        Self::TokioPostgres { source: e }
10✔
615
    }
10✔
616
}
617

618
impl From<serde_json::Error> for Error {
619
    fn from(e: serde_json::Error) -> Self {
×
620
        Self::SerdeJson { source: e }
×
621
    }
×
622
}
623

624
impl From<serde_urlencoded::de::Error> for Error {
NEW
625
    fn from(e: serde_urlencoded::de::Error) -> Self {
×
NEW
626
        Self::UnableToParseQueryString { source: e }
×
NEW
627
    }
×
628
}
629

630
impl From<serde_urlencoded::ser::Error> for Error {
NEW
631
    fn from(e: serde_urlencoded::ser::Error) -> Self {
×
NEW
632
        Self::UnableToSerializeQueryString { source: e }
×
NEW
633
    }
×
634
}
635

636
impl From<std::io::Error> for Error {
637
    fn from(e: std::io::Error) -> Self {
×
638
        Self::Io { source: e }
×
639
    }
×
640
}
641

642
impl From<gdal::errors::GdalError> for Error {
643
    fn from(gdal_error: gdal::errors::GdalError) -> Self {
×
644
        Self::Gdal { source: gdal_error }
×
645
    }
×
646
}
647

648
impl From<reqwest::Error> for Error {
649
    fn from(source: reqwest::Error) -> Self {
×
650
        Self::Reqwest { source }
×
651
    }
×
652
}
653

654
impl From<actix_multipart::MultipartError> for Error {
655
    fn from(source: actix_multipart::MultipartError) -> Self {
×
656
        Self::Multipart {
×
657
            reason: source.to_string(),
×
658
        }
×
659
    }
×
660
}
661

662
impl From<url::ParseError> for Error {
663
    fn from(source: url::ParseError) -> Self {
×
664
        Self::Url { source }
×
665
    }
×
666
}
667

668
impl From<flexi_logger::FlexiLoggerError> for Error {
669
    fn from(source: flexi_logger::FlexiLoggerError) -> Self {
×
670
        Self::Logger { source }
×
671
    }
×
672
}
673

674
impl From<proj::ProjError> for Error {
675
    fn from(source: proj::ProjError) -> Self {
×
676
        Self::Proj { source }
×
677
    }
×
678
}
679

680
impl From<tokio::task::JoinError> for Error {
681
    fn from(source: tokio::task::JoinError) -> Self {
×
682
        Error::TokioJoin { source }
×
683
    }
×
684
}
685

686
impl From<ordered_float::FloatIsNan> for Error {
687
    fn from(source: FloatIsNan) -> Self {
×
688
        Error::InvalidNotNanFloatKey { source }
×
689
    }
×
690
}
691

692
impl From<crate::datasets::DatasetNameError> for Error {
693
    fn from(source: crate::datasets::DatasetNameError) -> Self {
×
694
        Error::DatasetName { source }
×
695
    }
×
696
}
697

698
impl From<geoengine_datatypes::machine_learning::MlModelNameError> for Error {
699
    fn from(source: geoengine_datatypes::machine_learning::MlModelNameError) -> Self {
×
700
        Error::MlModelName { source }
×
701
    }
×
702
}
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