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

taosdata / TDengine / #4696

29 Aug 2025 06:36AM UTC coverage: 58.25% (+0.2%) from 58.041%
#4696

push

travis-ci

web-flow
fix(gpt): fix race-condition in preparing tmp files (#32800)

133424 of 291873 branches covered (45.71%)

Branch coverage included in aggregate %.

5 of 34 new or added lines in 6 files covered. (14.71%)

444 existing lines in 69 files now uncovered.

201767 of 283561 relevant lines covered (71.15%)

17907122.76 hits per line

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

7.2
/source/common/src/tanalytics.c
1
/*
2
 * Copyright (c) 2019 TAOS Data, Inc. <jhtao@taosdata.com>
3
 *
4
 * This program is free software: you can use, redistribute, and/or modify
5
 * it under the terms of the GNU Affero General Public License, version 3
6
 * or later ("AGPL"), as published by the Free Software Foundation.
7
 *
8
 * This program is distributed in the hope that it will be useful, but WITHOUT
9
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
10
 * FITNESS FOR A PARTICULAR PURPOSE.
11
 *
12
 * You should have received a copy of the GNU Affero General Public License
13
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
14
 */
15

16
#define _DEFAULT_SOURCE
17
#include "tanalytics.h"
18
#include "ttypes.h"
19
#include "tutil.h"
20
#include "osTime.h"
21

22
#ifdef USE_ANALYTICS
23
#include <curl/curl.h>
24

25
#define ANALYTICS_ALOG_SPLIT_CHAR ","
26

27
typedef struct {
28
  int64_t       ver;
29
  SHashObj     *hash;  // algoname:algotype -> SAnalyticsUrl
30
  TdThreadMutex lock;
31
} SAlgoMgmt;
32

33
typedef struct {
34
  char   *data;
35
  int64_t dataLen;
36
} SCurlResp;
37

38
static SAlgoMgmt tsAlgos = {0};
39
static int32_t   taosAnalyBufGetCont(SAnalyticBuf *pBuf, char **ppCont, int64_t *pContLen);
40

UNCOV
41
const char *taosAnalysisAlgoType(EAnalAlgoType type) {
×
42
  switch (type) {
×
43
    case ANALY_ALGO_TYPE_ANOMALY_DETECT:
×
44
      return "anomaly-detection";
×
45
    case ANALY_ALGO_TYPE_FORECAST:
×
46
      return "forecast";
×
47
    default:
×
48
      return "unknown";
×
49
  }
50
}
51

UNCOV
52
const char *taosAnalyAlgoUrlStr(EAnalAlgoType type) {
×
53
  switch (type) {
×
54
    case ANALY_ALGO_TYPE_ANOMALY_DETECT:
×
55
      return "anomaly-detect";
×
56
    case ANALY_ALGO_TYPE_FORECAST:
×
57
      return "forecast";
×
58
    default:
×
59
      return "unknown";
×
60
  }
61
}
62

UNCOV
63
EAnalAlgoType taosAnalyAlgoInt(const char *name) {
×
64
  for (EAnalAlgoType i = 0; i < ANALY_ALGO_TYPE_END; ++i) {
×
65
    if (strcasecmp(name, taosAnalysisAlgoType(i)) == 0) {
×
66
      return i;
×
67
    }
68
  }
69

UNCOV
70
  return ANALY_ALGO_TYPE_END;
×
71
}
72

73
int32_t taosAnalyticsInit() {
2,413✔
74
  if (curl_global_init(CURL_GLOBAL_ALL) != 0) {
2,413!
UNCOV
75
    uError("failed to init curl");
×
76
    return -1;
×
77
  }
78

79
  tsAlgos.ver = 0;
2,413✔
80
  if (taosThreadMutexInit(&tsAlgos.lock, NULL) != 0) {
2,413!
UNCOV
81
    uError("failed to init algo mutex");
×
82
    return -1;
×
83
  }
84

85
  tsAlgos.hash = taosHashInit(64, MurmurHash3_32, true, HASH_ENTRY_LOCK);
2,413✔
86
  if (tsAlgos.hash == NULL) {
2,413!
UNCOV
87
    uError("failed to init algo hash");
×
88
    return -1;
×
89
  }
90

91
  uInfo("analysis env is initialized");
2,413!
92
  return 0;
2,413✔
93
}
94

95
static void taosAnalyFreeHash(SHashObj *hash) {
2,413✔
96
  void *pIter = taosHashIterate(hash, NULL);
2,413✔
97
  while (pIter != NULL) {
2,413!
UNCOV
98
    SAnalyticsUrl *pUrl = (SAnalyticsUrl *)pIter;
×
99
    taosMemoryFree(pUrl->url);
×
100
    pIter = taosHashIterate(hash, pIter);
×
101
  }
102
  taosHashCleanup(hash);
2,413✔
103
}
2,413✔
104

105
void taosAnalyticsCleanup() {
2,413✔
106
  curl_global_cleanup();
2,413✔
107
  if (taosThreadMutexDestroy(&tsAlgos.lock) != 0) {
2,413!
UNCOV
108
    uError("failed to destroy anal lock");
×
109
  }
110
  taosAnalyFreeHash(tsAlgos.hash);
2,413✔
111
  tsAlgos.hash = NULL;
2,413✔
112
  uInfo("analysis env is cleaned up");
2,413!
113
}
2,413✔
114

UNCOV
115
void taosAnalyUpdate(int64_t newVer, SHashObj *pHash) {
×
116
  if (newVer > tsAlgos.ver) {
×
117
    if (taosThreadMutexLock(&tsAlgos.lock) == 0) {
×
118
      SHashObj *hash = tsAlgos.hash;
×
119
      tsAlgos.ver = newVer;
×
120
      tsAlgos.hash = pHash;
×
121
      if (taosThreadMutexUnlock(&tsAlgos.lock) != 0) {
×
122
        uError("failed to unlock hash")
×
123
      }
UNCOV
124
      taosAnalyFreeHash(hash);
×
125
    }
126
  } else {
UNCOV
127
    taosAnalyFreeHash(pHash);
×
128
  }
UNCOV
129
}
×
130

131
int32_t taosAnalyGetOpts(const char *pOption, SHashObj **pOptHash) {
18✔
132
  int32_t num = 0;
18✔
133
  int32_t code = 0;
18✔
134
  char   *pTmp = NULL;
18✔
135

136
  if (pOptHash != NULL) {
18!
137
    (*pOptHash) = NULL;
18✔
138
  } else {
UNCOV
139
    return TSDB_CODE_INVALID_PARA;
×
140
  }
141

142
  pTmp = taosStrdup(pOption);
18!
143
  if (pTmp == NULL) {
18!
UNCOV
144
    return terrno;
×
145
  }
146

147
  int32_t unused = strdequote(pTmp);
18✔
148
  char  **pList = strsplit(pTmp, ANALYTICS_ALOG_SPLIT_CHAR, &num);
18✔
149

150
  (*pOptHash) = taosHashInit(20, taosGetDefaultHashFunction(TSDB_DATA_TYPE_VARCHAR), 1, HASH_NO_LOCK);
18✔
151
  if ((*pOptHash) == NULL) {
18!
UNCOV
152
    taosMemoryFree(pTmp);
×
153
    taosMemoryFree(pList);
×
154
    return terrno;
×
155
  }
156

157
  for (int32_t i = 0; i < num; ++i) {
72✔
158
    int32_t parts = 0;
54✔
159
    char  **pParts = strsplit(pList[i], "=", &parts);
54✔
160

161
    if (parts < 2) {  // invalid parameters, ignore and continue
54✔
162
      taosMemoryFree(pParts);
39!
163
      continue;
39✔
164
    }
165

166
    size_t keyLen = strtrim(pParts[0]);
15✔
167
    size_t valLen = strtrim(pParts[1]);
15✔
168

169
    code = taosHashPut(*pOptHash, pParts[0], keyLen, pParts[1], valLen);
15✔
170
    if (code != TSDB_CODE_SUCCESS && code != TSDB_CODE_DUP_KEY) {
15!
171
      // return error
UNCOV
172
      taosMemoryFree(pTmp);
×
173
      taosMemoryFree(pList);
×
174
      taosMemoryFree(pParts);
×
175
      return code;
×
176
    } else {
177
      code = 0;
15✔
178
    }
179

180
    taosMemoryFree(pParts);
15!
181
  }
182

183
  taosMemoryFree(pTmp);
18!
184
  taosMemoryFree(pList);
18!
185
  return code;
18✔
186
}
187

UNCOV
188
bool taosAnalyGetOptStr(const char *option, const char *optName, char *optValue, int32_t optMaxLen) {
×
189
  SHashObj* p = NULL;
×
190
  int32_t code = taosAnalyGetOpts(option, &p);
×
191
  if (code != TSDB_CODE_SUCCESS) {
×
192
    if (p != NULL) {
×
193
      taosHashCleanup(p);
×
194
      p = NULL;
×
195
    }
UNCOV
196
    return false;
×
197
  }
198

UNCOV
199
  void* pVal = taosHashGet(p, optName, strlen(optName));
×
200
  if (pVal == NULL) {
×
201
    taosHashCleanup(p);
×
202
    return false;
×
203
  }
204

UNCOV
205
  int32_t valLen = taosHashGetValueSize(pVal);
×
206

UNCOV
207
  if (optValue != NULL && optMaxLen >= 1) {
×
208
    int32_t len = MIN(valLen + 1, optMaxLen);
×
209
    tstrncpy(optValue, (char *)pVal, len);
×
210
  }
211

UNCOV
212
  taosHashCleanup(p);
×
213
  return true;
×
214
}
215

UNCOV
216
int32_t taosAnalyGetAlgoUrl(const char *algoName, EAnalAlgoType type, char *url, int32_t urlLen) {
×
217
  int32_t code = 0;
×
218
  char    name[TSDB_ANALYTIC_ALGO_KEY_LEN] = {0};
×
219
  int32_t nameLen = 1 + tsnprintf(name, sizeof(name) - 1, "%d:%s", type, algoName);
×
220

UNCOV
221
  char *unused = strntolower(name, name, nameLen);
×
222

UNCOV
223
  if (taosThreadMutexLock(&tsAlgos.lock) == 0) {
×
224
    SAnalyticsUrl *pUrl = taosHashAcquire(tsAlgos.hash, name, nameLen);
×
225
    if (pUrl != NULL) {
×
226
      tstrncpy(url, pUrl->url, urlLen);
×
227
      uDebug("algo:%s, type:%s, url:%s", algoName, taosAnalysisAlgoType(type), url);
×
228
    } else {
UNCOV
229
      url[0] = 0;
×
230
      code = TSDB_CODE_ANA_ALGO_NOT_FOUND;
×
231
      uError("algo:%s, type:%s, url not found", algoName, taosAnalysisAlgoType(type));
×
232
    }
233

UNCOV
234
    if (taosThreadMutexUnlock(&tsAlgos.lock) != 0) {
×
235
      uError("failed to unlock hash");
×
236
      return TSDB_CODE_OUT_OF_MEMORY;
×
237
    }
238
  }
239

UNCOV
240
  return code;
×
241
}
242

243
int64_t taosAnalyGetVersion() { return tsAlgos.ver; }
169,515✔
244

UNCOV
245
static size_t taosCurlWriteData(char *pCont, size_t contLen, size_t nmemb, void *userdata) {
×
246
  SCurlResp *pRsp = userdata;
×
247
  if (contLen == 0 || nmemb == 0 || pCont == NULL) {
×
248
    pRsp->dataLen = 0;
×
249
    pRsp->data = NULL;
×
250
    uError("curl response is received, len:%" PRId64, pRsp->dataLen);
×
251
    return 0;
×
252
  }
253

UNCOV
254
  int64_t newDataSize = (int64_t)contLen * nmemb;
×
255
  int64_t size = pRsp->dataLen + newDataSize;
×
256

UNCOV
257
  if (pRsp->data == NULL) {
×
258
    pRsp->data = taosMemoryMalloc(size + 1);
×
259
    if (pRsp->data == NULL) {
×
260
      uError("failed to prepare recv buffer for post rsp, len:%d, code:%s", (int32_t)size + 1, tstrerror(terrno));
×
261
      return 0;  // return the recv length, if failed, return 0
×
262
    }
263
  } else {
UNCOV
264
    char *p = taosMemoryRealloc(pRsp->data, size + 1);
×
265
    if (p == NULL) {
×
266
      uError("failed to prepare recv buffer for post rsp, len:%d, code:%s", (int32_t)size + 1, tstrerror(terrno));
×
267
      return 0;  // return the recv length, if failed, return 0
×
268
    }
269

UNCOV
270
    pRsp->data = p;
×
271
  }
272

UNCOV
273
  if (pRsp->data != NULL) {
×
274
    (void)memcpy(pRsp->data + pRsp->dataLen, pCont, newDataSize);
×
275

UNCOV
276
    pRsp->dataLen = size;
×
277
    pRsp->data[size] = 0;
×
278

UNCOV
279
    uDebugL("curl response is received, len:%" PRId64 ", content:%s", size, pRsp->data);
×
280
    return newDataSize;
×
281
  } else {
UNCOV
282
    pRsp->dataLen = 0;
×
283
    uError("failed to malloc curl response");
×
284
    return 0;
×
285
  }
286
}
287

UNCOV
288
static int32_t taosCurlGetRequest(const char *url, SCurlResp *pRsp) {
×
289
  CURL    *curl = NULL;
×
290
  CURLcode code = 0;
×
291

UNCOV
292
  curl = curl_easy_init();
×
293
  if (curl == NULL) {
×
294
    uError("failed to create curl handle");
×
295
    return -1;
×
296
  }
297

UNCOV
298
  if (curl_easy_setopt(curl, CURLOPT_URL, url) != 0) goto _OVER;
×
299
  if (curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, taosCurlWriteData) != 0) goto _OVER;
×
300
  if (curl_easy_setopt(curl, CURLOPT_WRITEDATA, pRsp) != 0) goto _OVER;
×
301
  if (curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 100) != 0) goto _OVER;
×
302

UNCOV
303
  uDebug("curl get request will sent, url:%s", url);
×
304
  code = curl_easy_perform(curl);
×
305
  if (code != CURLE_OK) {
×
306
    uError("failed to perform curl action, code:%d", code);
×
307
  }
308

UNCOV
309
_OVER:
×
310
  if (curl != NULL) curl_easy_cleanup(curl);
×
311
  return code;
×
312
}
313

NEW
314
static int32_t taosCurlPostRequest(const char *url, SCurlResp *pRsp, const char *buf, int32_t bufLen, int32_t timeout,
×
315
                                   const char *id) {
UNCOV
316
  struct curl_slist *headers = NULL;
×
UNCOV
317
  CURL              *curl = NULL;
×
318
  CURLcode           code = 0;
×
319

320
  curl = curl_easy_init();
×
UNCOV
321
  if (curl == NULL) {
×
NEW
322
    uError("%s failed to create curl handle", id);
×
323
    return -1;
×
324
  }
325

UNCOV
326
  headers = curl_slist_append(headers, "Content-Type:application/json;charset=UTF-8");
×
UNCOV
327
  if (curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers) != 0) goto _OVER;
×
328
  if (curl_easy_setopt(curl, CURLOPT_URL, url) != 0) goto _OVER;
×
329
  if (curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, taosCurlWriteData) != 0) goto _OVER;
×
330
  if (curl_easy_setopt(curl, CURLOPT_WRITEDATA, pRsp) != 0) goto _OVER;
×
331
  if (curl_easy_setopt(curl, CURLOPT_TIMEOUT, timeout) != 0) goto _OVER;
×
332
  if (curl_easy_setopt(curl, CURLOPT_POST, 1) != 0) goto _OVER;
×
333
  if (curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, bufLen) != 0) goto _OVER;
×
334
  if (curl_easy_setopt(curl, CURLOPT_POSTFIELDS, buf) != 0) goto _OVER;
×
335
  if (curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L) != 0) goto _OVER;
×
336
  if (curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L) != 0) goto _OVER;
×
337

NEW
338
  uDebugL("%s curl post request will sent, url:%s len:%d content:%s", id, url, bufLen, buf);
×
UNCOV
339
  code = curl_easy_perform(curl);
×
340
  if (code != CURLE_OK) {
×
NEW
341
    uError("%s failed to perform curl action, code:%d", id, code);
×
342
  }
343

UNCOV
344
_OVER:
×
UNCOV
345
  if (curl != NULL) {
×
346
    curl_slist_free_all(headers);
×
347
    curl_easy_cleanup(curl);
×
348
  }
349
  return code;
×
350
}
351

NEW
352
SJson *taosAnalySendReqRetJson(const char *url, EAnalyHttpType type, SAnalyticBuf *pBuf, int64_t timeout, const char* id) {
×
UNCOV
353
  int32_t   code = -1;
×
354
  char     *pCont = NULL;
×
355
  int64_t   contentLen;
356
  SJson    *pJson = NULL;
×
UNCOV
357
  SCurlResp curlRsp = {0};
×
358

359
  if (type == ANALYTICS_HTTP_TYPE_GET) {
×
UNCOV
360
    if (taosCurlGetRequest(url, &curlRsp) != 0) {
×
361
      terrno = TSDB_CODE_ANA_URL_CANT_ACCESS;
×
362
      goto _OVER;
×
363
    }
364
  } else {
UNCOV
365
    code = taosAnalyBufGetCont(pBuf, &pCont, &contentLen);
×
UNCOV
366
    if (code != 0) {
×
367
      terrno = code;
×
368
      goto _OVER;
×
369
    }
NEW
370
    if (taosCurlPostRequest(url, &curlRsp, pCont, contentLen, timeout, id) != 0) {
×
UNCOV
371
      terrno = TSDB_CODE_ANA_URL_CANT_ACCESS;
×
372
      goto _OVER;
×
373
    }
374
  }
375

UNCOV
376
  if (curlRsp.data == NULL || curlRsp.dataLen == 0) {
×
UNCOV
377
    terrno = TSDB_CODE_ANA_URL_RSP_IS_NULL;
×
378
    goto _OVER;
×
379
  }
380

UNCOV
381
  pJson = tjsonParse(curlRsp.data);
×
UNCOV
382
  if (pJson == NULL) {
×
383
    if (curlRsp.data[0] == '<') {
×
384
      terrno = TSDB_CODE_ANA_ANODE_RETURN_ERROR;
×
385
    } else {
386
      terrno = TSDB_CODE_INVALID_JSON_FORMAT;
×
387
    }
388
    goto _OVER;
×
389
  }
390

UNCOV
391
_OVER:
×
UNCOV
392
  if (curlRsp.data != NULL) taosMemoryFreeClear(curlRsp.data);
×
393
  if (pCont != NULL) taosMemoryFree(pCont);
×
394
  return pJson;
×
395
}
396

UNCOV
397
static int32_t taosAnalyJsonBufGetCont(const char *fileName, char **ppCont, int64_t *pContLen) {
×
UNCOV
398
  int32_t   code = 0;
×
399
  int64_t   contLen;
400
  char     *pCont = NULL;
×
UNCOV
401
  TdFilePtr pFile = NULL;
×
402

403
  pFile = taosOpenFile(fileName, TD_FILE_READ);
×
UNCOV
404
  if (pFile == NULL) {
×
405
    code = terrno;
×
406
    goto _OVER;
×
407
  }
408

UNCOV
409
  code = taosFStatFile(pFile, &contLen, NULL);
×
UNCOV
410
  if (code != 0) goto _OVER;
×
411

412
  pCont = taosMemoryMalloc(contLen + 1);
×
UNCOV
413
  if (pCont == NULL) {
×
414
    code = TSDB_CODE_OUT_OF_MEMORY;
×
415
    goto _OVER;
×
416
  }
417

UNCOV
418
  if (taosReadFile(pFile, pCont, contLen) != contLen) {
×
UNCOV
419
    code = terrno;
×
420
    goto _OVER;
×
421
  }
422

UNCOV
423
  pCont[contLen] = '\0';
×
424

425
_OVER:
×
UNCOV
426
  if (code == 0) {
×
427
    *ppCont = pCont;
×
428
    *pContLen = contLen;
×
429
  } else {
430
    if (pCont != NULL) taosMemoryFree(pCont);
×
431
  }
432
  if (pFile != NULL) taosCloseFile(&pFile);
×
UNCOV
433
  return code;
×
434
}
435

UNCOV
436
static int32_t taosAnalyJsonBufWriteOptInt(SAnalyticBuf *pBuf, const char *optName, int64_t optVal) {
×
UNCOV
437
  char    buf[64] = {0};
×
438
  int32_t bufLen = tsnprintf(buf, sizeof(buf), "\"%s\": %" PRId64 ",\n", optName, optVal);
×
439
  if (taosWriteFile(pBuf->filePtr, buf, bufLen) != bufLen) {
×
440
    return terrno;
×
441
  }
442
  return 0;
×
443
}
444

UNCOV
445
static int32_t taosAnalyJsonBufWriteOptStr(SAnalyticBuf *pBuf, const char *optName, const char *optVal) {
×
UNCOV
446
  int32_t code = 0;
×
447
  int32_t keyLen = strlen(optName);
×
448
  int32_t valLen = strlen(optVal);
×
449

450
  int32_t totalLen = keyLen + valLen + 20;
×
UNCOV
451
  char *  buf = taosMemoryMalloc(totalLen);
×
452
  if (buf == NULL) {
×
453
    uError("failed to prepare the buffer for serializing the key/value info for analysis, len:%d, code:%s", totalLen,
×
454
           tstrerror(terrno));
455
    return terrno;
×
456
  }
457

UNCOV
458
  int32_t bufLen = tsnprintf(buf, totalLen, "\"%s\": \"%s\",\n", optName, optVal);
×
UNCOV
459
  if (taosWriteFile(pBuf->filePtr, buf, bufLen) != bufLen) {
×
460
    code = terrno;
×
461
  }
462

UNCOV
463
  taosMemoryFree(buf);
×
UNCOV
464
  return code;
×
465
}
466

UNCOV
467
static int32_t taosAnalyJsonBufWriteOptFloat(SAnalyticBuf *pBuf, const char *optName, float optVal) {
×
UNCOV
468
  char    buf[128] = {0};
×
469
  int32_t bufLen = tsnprintf(buf, sizeof(buf), "\"%s\": %f,\n", optName, optVal);
×
470
  if (taosWriteFile(pBuf->filePtr, buf, bufLen) != bufLen) {
×
471
    return terrno;
×
472
  }
473
  return 0;
×
474
}
475

UNCOV
476
static int32_t taosAnalyJsonBufWriteStr(SAnalyticBuf *pBuf, const char *buf, int32_t bufLen) {
×
UNCOV
477
  if (bufLen <= 0) {
×
478
    bufLen = strlen(buf);
×
479
  }
480
  if (taosWriteFile(pBuf->filePtr, buf, bufLen) != bufLen) {
×
UNCOV
481
    return terrno;
×
482
  }
483
  return 0;
×
484
}
485

UNCOV
486
static int32_t taosAnalyJsonBufWriteStart(SAnalyticBuf *pBuf) { return taosAnalyJsonBufWriteStr(pBuf, "{\n", 0); }
×
487

NEW
488
static int32_t tsosAnalyJsonBufOpen(SAnalyticBuf *pBuf, int32_t numOfCols, const char* pId) {
×
UNCOV
489
  pBuf->filePtr = taosOpenFile(pBuf->fileName, TD_FILE_CREATE | TD_FILE_WRITE | TD_FILE_TRUNC | TD_FILE_WRITE_THROUGH);
×
490
  if (pBuf->filePtr == NULL) {
×
491
    return terrno;
×
492
  }
493

UNCOV
494
  pBuf->pCols = taosMemoryCalloc(numOfCols, sizeof(SAnalyticsColBuf));
×
UNCOV
495
  if (pBuf->pCols == NULL) return TSDB_CODE_OUT_OF_MEMORY;
×
496
  pBuf->numOfCols = numOfCols;
×
497

498
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON) {
×
UNCOV
499
    return taosAnalyJsonBufWriteStart(pBuf);
×
500
  }
501

NEW
502
  pBuf->pid = pId;
×
503

UNCOV
504
  for (int32_t i = 0; i < numOfCols; ++i) {
×
UNCOV
505
    SAnalyticsColBuf *pCol = &pBuf->pCols[i];
×
NEW
506
    snprintf(pCol->fileName, sizeof(pCol->fileName), "%s-c%d-%p", pBuf->fileName, i, pBuf);
×
UNCOV
507
    pCol->filePtr =
×
508
        taosOpenFile(pCol->fileName, TD_FILE_CREATE | TD_FILE_WRITE | TD_FILE_TRUNC | TD_FILE_WRITE_THROUGH);
×
509
    if (pCol->filePtr == NULL) {
×
NEW
510
      uError("%s failed to open tmp file for keep forecast history data, code:%s", pId, tstrerror(terrno));
×
511
      return terrno;
×
512
    }
513
  }
514

515
  return taosAnalyJsonBufWriteStart(pBuf);
×
516
}
517

UNCOV
518
static int32_t taosAnalyJsonBufWriteColMeta(SAnalyticBuf *pBuf, int32_t colIndex, int32_t colType, const char *colName) {
×
UNCOV
519
  char buf[128] = {0};
×
520
  bool first = (colIndex == 0);
×
UNCOV
521
  bool last = (colIndex == pBuf->numOfCols - 1);
×
522

523
  if (first) {
×
524
    if (taosAnalyJsonBufWriteStr(pBuf, "\"schema\": [\n", 0) != 0) {
×
525
      return terrno;
×
526
    }
527
  }
528

529
  int32_t bufLen = tsnprintf(buf, sizeof(buf), "  [\"%s\", \"%s\", %d]%s\n", colName, tDataTypes[colType].name,
×
530
                             tDataTypes[colType].bytes, last ? "" : ",");
UNCOV
531
  if (taosWriteFile(pBuf->filePtr, buf, bufLen) != bufLen) {
×
UNCOV
532
    return terrno;
×
533
  }
534

UNCOV
535
  if (last) {
×
536
    if (taosAnalyJsonBufWriteStr(pBuf, "],\n", 0) != 0) {
×
537
      return terrno;
×
538
    }
539
  }
540

541
  return 0;
×
542
}
543

UNCOV
544
static int32_t taosAnalyJsonBufWriteDataBegin(SAnalyticBuf *pBuf) {
×
UNCOV
545
  return taosAnalyJsonBufWriteStr(pBuf, "\"data\": [\n", 0);
×
546
}
547

UNCOV
548
static int32_t taosAnalyJsonBufWriteStrUseCol(SAnalyticBuf *pBuf, const char *buf, int32_t bufLen, int32_t colIndex) {
×
549
  if (bufLen <= 0) {
×
550
    bufLen = strlen(buf);
×
551
  }
552

553
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON) {
×
554
    int32_t ret = taosWriteFile(pBuf->filePtr, buf, bufLen);
×
555
    if (ret != bufLen) {
×
UNCOV
556
      return terrno;
×
557
    }
558
  } else {
559
    int32_t ret = taosWriteFile(pBuf->pCols[colIndex].filePtr, buf, bufLen);
×
560
    if (ret != bufLen) {
×
561
      return terrno;
×
562
    }
563
  }
564

565
  return 0;
×
566
}
567

UNCOV
568
static int32_t taosAnalyJsonBufWriteColBegin(SAnalyticBuf *pBuf, int32_t colIndex) {
×
UNCOV
569
  return taosAnalyJsonBufWriteStrUseCol(pBuf, "[\n", 0, colIndex);
×
570
}
571

UNCOV
572
static int32_t taosAnalyJsonBufWriteColEnd(SAnalyticBuf *pBuf, int32_t colIndex) {
×
573
  if (colIndex == pBuf->numOfCols - 1) {
×
574
    return taosAnalyJsonBufWriteStrUseCol(pBuf, "\n]\n", 0, colIndex);
×
575

576
  } else {
577
    return taosAnalyJsonBufWriteStrUseCol(pBuf, "\n],\n", 0, colIndex);
×
578
  }
579
}
580

UNCOV
581
static int32_t taosAnalyJsonBufWriteColData(SAnalyticBuf *pBuf, int32_t colIndex, int32_t colType, void *colValue) {
×
582
  char    buf[64];
UNCOV
583
  int32_t bufLen = 0;
×
584

UNCOV
585
  if (pBuf->pCols[colIndex].numOfRows != 0) {
×
586
    buf[bufLen] = ',';
×
UNCOV
587
    buf[bufLen + 1] = '\n';
×
588
    buf[bufLen + 2] = 0;
×
UNCOV
589
    bufLen += 2;
×
590
  }
591

592
  switch (colType) {
×
593
    case TSDB_DATA_TYPE_BOOL:
×
594
      bufLen += tsnprintf(buf + bufLen, sizeof(buf) - bufLen, "%d", (*((int8_t *)colValue) == 1) ? 1 : 0);
×
UNCOV
595
      break;
×
UNCOV
596
    case TSDB_DATA_TYPE_TINYINT:
×
597
      bufLen += tsnprintf(buf + bufLen, sizeof(buf) - bufLen, "%d", *(int8_t *)colValue);
×
598
      break;
×
599
    case TSDB_DATA_TYPE_UTINYINT:
×
600
      bufLen += tsnprintf(buf + bufLen, sizeof(buf) - bufLen, "%u", *(uint8_t *)colValue);
×
601
      break;
×
602
    case TSDB_DATA_TYPE_SMALLINT:
×
603
      bufLen += tsnprintf(buf + bufLen, sizeof(buf) - bufLen, "%d", *(int16_t *)colValue);
×
604
      break;
×
605
    case TSDB_DATA_TYPE_USMALLINT:
×
606
      bufLen += tsnprintf(buf + bufLen, sizeof(buf) - bufLen, "%u", *(uint16_t *)colValue);
×
607
      break;
×
608
    case TSDB_DATA_TYPE_INT:
×
609
      bufLen += tsnprintf(buf + bufLen, sizeof(buf) - bufLen, "%d", *(int32_t *)colValue);
×
610
      break;
×
611
    case TSDB_DATA_TYPE_UINT:
×
612
      bufLen += tsnprintf(buf + bufLen, sizeof(buf) - bufLen, "%u", *(uint32_t *)colValue);
×
613
      break;
×
614
    case TSDB_DATA_TYPE_BIGINT:
×
615
    case TSDB_DATA_TYPE_TIMESTAMP:
616
      bufLen += tsnprintf(buf + bufLen, sizeof(buf) - bufLen, "%" PRId64, *(int64_t *)colValue);
×
617
      break;
×
618
    case TSDB_DATA_TYPE_UBIGINT:
×
619
      bufLen += tsnprintf(buf + bufLen, sizeof(buf) - bufLen, "%" PRIu64, *(uint64_t *)colValue);
×
UNCOV
620
      break;
×
621
    case TSDB_DATA_TYPE_FLOAT:
×
622
      bufLen += tsnprintf(buf + bufLen, sizeof(buf) - bufLen, "%f", GET_FLOAT_VAL(colValue));
×
623
      break;
×
624
    case TSDB_DATA_TYPE_DOUBLE:
×
625
      bufLen += tsnprintf(buf + bufLen, sizeof(buf) - bufLen, "%f", GET_DOUBLE_VAL(colValue));
×
626
      break;
×
627
    default:
×
628
      buf[bufLen] = '\0';
×
629
  }
630

631
  pBuf->pCols[colIndex].numOfRows++;
×
632
  return taosAnalyJsonBufWriteStrUseCol(pBuf, buf, bufLen, colIndex);
×
633
}
634

UNCOV
635
static int32_t taosAnalyJsonBufWriteDataEnd(SAnalyticBuf *pBuf) {
×
636
  int32_t code = 0;
×
637
  char   *pCont = NULL;
×
UNCOV
638
  int64_t contLen = 0;
×
639

640
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) {
×
641
    for (int32_t i = 0; i < pBuf->numOfCols; ++i) {
×
642
      SAnalyticsColBuf *pCol = &pBuf->pCols[i];
×
643

UNCOV
644
      code = taosFsyncFile(pCol->filePtr);
×
645
      if (code != 0) return code;
×
646

647
      code = taosCloseFile(&pCol->filePtr);
×
UNCOV
648
      if (code != 0) return code;
×
649

650
      code = taosAnalyJsonBufGetCont(pBuf->pCols[i].fileName, &pCont, &contLen);
×
UNCOV
651
      if (code != 0) return code;
×
652

653
      code = taosAnalyJsonBufWriteStr(pBuf, pCont, contLen);
×
UNCOV
654
      if (code != 0) return code;
×
655

656
      taosMemoryFreeClear(pCont);
×
UNCOV
657
      contLen = 0;
×
658
    }
659
  }
660

661
  return taosAnalyJsonBufWriteStr(pBuf, "],\n", 0);
×
662
}
663

UNCOV
664
static int32_t taosAnalyJsonBufWriteEnd(SAnalyticBuf *pBuf) {
×
UNCOV
665
  int32_t code = taosAnalyJsonBufWriteOptInt(pBuf, "rows", pBuf->pCols[0].numOfRows);
×
666
  if (code != 0) return code;
×
667

UNCOV
668
  return taosAnalyJsonBufWriteStr(pBuf, "\"protocol\": 1.0\n}", 0);
×
669
}
670

671
int32_t taosAnalJsonBufClose(SAnalyticBuf *pBuf) {
×
UNCOV
672
  int32_t code = taosAnalyJsonBufWriteEnd(pBuf);
×
673
  if (code != 0) return code;
×
674

UNCOV
675
  if (pBuf->filePtr != NULL) {
×
676
    code = taosFsyncFile(pBuf->filePtr);
×
677
    if (code != 0) return code;
×
678
    code = taosCloseFile(&pBuf->filePtr);
×
UNCOV
679
    if (code != 0) return code;
×
680
  }
681

682
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) {
×
683
    for (int32_t i = 0; i < pBuf->numOfCols; ++i) {
×
684
      SAnalyticsColBuf *pCol = &pBuf->pCols[i];
×
UNCOV
685
      if (pCol->filePtr != NULL) {
×
UNCOV
686
        code = taosFsyncFile(pCol->filePtr);
×
687
        if (code != 0) return code;
×
688
        code = taosCloseFile(&pCol->filePtr);
×
689
        if (code != 0) return code;
×
690
      }
691
    }
692
  }
693

694
  return 0;
×
695
}
696

UNCOV
697
void taosAnalyBufDestroy(SAnalyticBuf *pBuf) {
×
UNCOV
698
  if (pBuf->fileName[0] != 0) {
×
699
    if (pBuf->filePtr != NULL) (void)taosCloseFile(&pBuf->filePtr);
×
NEW
700
    if (taosRemoveFile(pBuf->fileName) != 0) {
×
NEW
701
      uError("%s failed to remove file:%s, code:%s", pBuf->pid, pBuf->fileName, tstrerror(terrno));
×
702
    }
703
    pBuf->fileName[0] = 0;
×
704
  }
705

706
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) {
×
707
    for (int32_t i = 0; i < pBuf->numOfCols; ++i) {
×
708
      SAnalyticsColBuf *pCol = &pBuf->pCols[i];
×
UNCOV
709
      if (pCol->fileName[0] != 0) {
×
710
        if (pCol->filePtr != NULL) (void)taosCloseFile(&pCol->filePtr);
×
UNCOV
711
        if (taosRemoveFile(pCol->fileName) != 0) {
×
NEW
712
          uError("%s failed to remove file:%s, code:%s", pBuf->pid, pCol->fileName, tstrerror(terrno));
×
713
        }
714
        pCol->fileName[0] = 0;
×
715
      }
716
    }
717
  }
718

719
  taosMemoryFreeClear(pBuf->pCols);
×
UNCOV
720
  pBuf->numOfCols = 0;
×
721
}
×
722

NEW
723
int32_t tsosAnalyBufOpen(SAnalyticBuf *pBuf, int32_t numOfCols, const char* pId) {
×
UNCOV
724
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON || pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) {
×
NEW
725
    return tsosAnalyJsonBufOpen(pBuf, numOfCols, pId);
×
726
  } else {
727
    return TSDB_CODE_ANA_BUF_INVALID_TYPE;
×
728
  }
729
}
730

731
int32_t taosAnalyBufWriteOptStr(SAnalyticBuf *pBuf, const char *optName, const char *optVal) {
×
732
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON || pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) {
×
UNCOV
733
    return taosAnalyJsonBufWriteOptStr(pBuf, optName, optVal);
×
734
  } else {
UNCOV
735
    return TSDB_CODE_ANA_BUF_INVALID_TYPE;
×
736
  }
737
}
738

739
int32_t taosAnalyBufWriteOptInt(SAnalyticBuf *pBuf, const char *optName, int64_t optVal) {
×
740
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON || pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) {
×
UNCOV
741
    return taosAnalyJsonBufWriteOptInt(pBuf, optName, optVal);
×
742
  } else {
UNCOV
743
    return TSDB_CODE_ANA_BUF_INVALID_TYPE;
×
744
  }
745
}
746

747
int32_t taosAnalyBufWriteOptFloat(SAnalyticBuf *pBuf, const char *optName, float optVal) {
×
748
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON || pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) {
×
UNCOV
749
    return taosAnalyJsonBufWriteOptFloat(pBuf, optName, optVal);
×
750
  } else {
UNCOV
751
    return TSDB_CODE_ANA_BUF_INVALID_TYPE;
×
752
  }
753
}
754

755
int32_t taosAnalyBufWriteColMeta(SAnalyticBuf *pBuf, int32_t colIndex, int32_t colType, const char *colName) {
×
756
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON || pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) {
×
UNCOV
757
    return taosAnalyJsonBufWriteColMeta(pBuf, colIndex, colType, colName);
×
758
  } else {
UNCOV
759
    return TSDB_CODE_ANA_BUF_INVALID_TYPE;
×
760
  }
761
}
762

763
int32_t taosAnalyBufWriteDataBegin(SAnalyticBuf *pBuf) {
×
764
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON || pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) {
×
UNCOV
765
    return taosAnalyJsonBufWriteDataBegin(pBuf);
×
766
  } else {
UNCOV
767
    return TSDB_CODE_ANA_BUF_INVALID_TYPE;
×
768
  }
769
}
770

771
int32_t taosAnalyBufWriteColBegin(SAnalyticBuf *pBuf, int32_t colIndex) {
×
772
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON || pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) {
×
UNCOV
773
    return taosAnalyJsonBufWriteColBegin(pBuf, colIndex);
×
774
  } else {
UNCOV
775
    return TSDB_CODE_ANA_BUF_INVALID_TYPE;
×
776
  }
777
}
778

779
int32_t taosAnalyBufWriteColData(SAnalyticBuf *pBuf, int32_t colIndex, int32_t colType, void *colValue) {
×
780
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON || pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) {
×
UNCOV
781
    return taosAnalyJsonBufWriteColData(pBuf, colIndex, colType, colValue);
×
782
  } else {
UNCOV
783
    return TSDB_CODE_ANA_BUF_INVALID_TYPE;
×
784
  }
785
}
786

787
int32_t taosAnalyBufWriteColEnd(SAnalyticBuf *pBuf, int32_t colIndex) {
×
788
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON || pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) {
×
UNCOV
789
    return taosAnalyJsonBufWriteColEnd(pBuf, colIndex);
×
790
  } else {
UNCOV
791
    return TSDB_CODE_ANA_BUF_INVALID_TYPE;
×
792
  }
793
}
794

795
int32_t taosAnalyBufWriteDataEnd(SAnalyticBuf *pBuf) {
×
796
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON || pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) {
×
UNCOV
797
    return taosAnalyJsonBufWriteDataEnd(pBuf);
×
798
  } else {
UNCOV
799
    return TSDB_CODE_ANA_BUF_INVALID_TYPE;
×
800
  }
801
}
802

803
int32_t taosAnalyBufClose(SAnalyticBuf *pBuf) {
×
804
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON || pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) {
×
UNCOV
805
    return taosAnalJsonBufClose(pBuf);
×
806
  } else {
UNCOV
807
    return TSDB_CODE_ANA_BUF_INVALID_TYPE;
×
808
  }
809
}
810

811
static int32_t taosAnalyBufGetCont(SAnalyticBuf *pBuf, char **ppCont, int64_t *pContLen) {
×
812
  *ppCont = NULL;
×
UNCOV
813
  *pContLen = 0;
×
814

UNCOV
815
  if (pBuf->bufType == ANALYTICS_BUF_TYPE_JSON || pBuf->bufType == ANALYTICS_BUF_TYPE_JSON_COL) {
×
UNCOV
816
    return taosAnalyJsonBufGetCont(pBuf->fileName, ppCont, pContLen);
×
817
  } else {
818
    return TSDB_CODE_ANA_BUF_INVALID_TYPE;
×
819
  }
820
}
821

822
// extract the timeout parameter
823
int64_t taosAnalysisParseTimout(SHashObj* pHashMap, const char* id) {
×
UNCOV
824
  int32_t code = 0;
×
825
  char* pTimeout = taosHashGet(pHashMap, ALGO_OPT_TIMEOUT_NAME, strlen(ALGO_OPT_TIMEOUT_NAME));
×
UNCOV
826
  if (pTimeout == NULL) {
×
UNCOV
827
    uDebug("%s not set the timeout val, set default:%d", id, ANALY_DEFAULT_TIMEOUT);
×
UNCOV
828
    return ANALY_DEFAULT_TIMEOUT;
×
829
  } else {
830
    int64_t t = taosStr2Int64(pTimeout, NULL, 10);
×
831
    if (t <= 0 || t > ANALY_MAX_TIMEOUT) {
×
832
      uDebug("%s timeout val:%" PRId64 "s is invalid (greater than 10min or less than 1s), use default:%dms", id, t,
×
833
             ANALY_DEFAULT_TIMEOUT);
834
      return ANALY_DEFAULT_TIMEOUT;
×
835
    }
836

837
    uDebug("%s timeout val is set to: %" PRId64 "s", id, t);
×
838
    return t;
×
839
  }
840
}
841

UNCOV
842
int32_t taosAnalysisParseAlgo(const char* pOpt, char* pAlgoName, char* pUrl, int32_t type, int32_t len, SHashObj* pHashMap, const char* id) {
×
UNCOV
843
  char* pAlgo = taosHashGet(pHashMap, ALGO_OPT_ALGO_NAME, strlen(ALGO_OPT_ALGO_NAME));
×
844
  if (pAlgo == NULL) {
×
845
    uError("%s failed to get analysis algorithm name from %s", id, pOpt);
×
UNCOV
846
    return TSDB_CODE_ANA_ALGO_NOT_FOUND;
×
847
  }
848

849
  tstrncpy(pAlgoName, pAlgo, taosHashGetValueSize(pAlgo) + 1);
×
850

851
  if (taosAnalyGetAlgoUrl(pAlgoName, type, pUrl, len) != 0) {
×
852
    uError("%s failed to get analysis algorithm url from %s", id, pAlgoName);
×
853
    return TSDB_CODE_ANA_ALGO_NOT_LOAD;
×
854
  }
855

856
  return 0;
×
857
}
858

859
int8_t taosAnalysisParseWncheck(SHashObj* pHashMap, const char* id) {
×
860
  char* pWncheck = taosHashGet(pHashMap, ALGO_OPT_WNCHECK_NAME, strlen(ALGO_OPT_WNCHECK_NAME));
×
UNCOV
861
  if (pWncheck != NULL) {
×
UNCOV
862
    int32_t v = (int32_t) taosStr2Int64(pWncheck, NULL, 10);
×
863
    uDebug("%s analysis wncheck:%d", id, v);
×
UNCOV
864
    return v;
×
865
  } else {
866
    uDebug("%s analysis wncheck not found, use default:%d", id, ANALY_FORECAST_DEFAULT_WNCHECK);
×
867
    return ANALY_FORECAST_DEFAULT_WNCHECK;
×
868
  }
869
}
870

871
#else
872

873
int32_t taosAnalyticsInit() { return 0; }
874
void    taosAnalyticsCleanup() {}
875
SJson  *taosAnalySendReqRetJson(const char *url, EAnalyHttpType type, SAnalyticBuf *pBuf, int64_t timeout, const char*id) {
876
  return NULL;
877
}
878

879
int32_t taosAnalyGetAlgoUrl(const char *algoName, EAnalAlgoType type, char *url, int32_t urlLen) { return 0; }
880
bool    taosAnalyGetOptStr(const char *option, const char *optName, char *optValue, int32_t optMaxLen) { return true; }
881
int64_t taosAnalyGetVersion() { return 0; }
882
void    taosAnalyUpdate(int64_t newVer, SHashObj *pHash) {}
883

884
int32_t tsosAnalyBufOpen(SAnalyticBuf *pBuf, int32_t numOfCols, const char* id) { return 0; }
885
int32_t taosAnalyBufWriteOptStr(SAnalyticBuf *pBuf, const char *optName, const char *optVal) { return 0; }
886
int32_t taosAnalyBufWriteOptInt(SAnalyticBuf *pBuf, const char *optName, int64_t optVal) { return 0; }
887
int32_t taosAnalyBufWriteOptFloat(SAnalyticBuf *pBuf, const char *optName, float optVal) { return 0; }
888
int32_t taosAnalyBufWriteColMeta(SAnalyticBuf *pBuf, int32_t colIndex, int32_t colType, const char *colName) {
889
  return 0;
890
}
891
int32_t taosAnalyBufWriteDataBegin(SAnalyticBuf *pBuf) { return 0; }
892
int32_t taosAnalyBufWriteColBegin(SAnalyticBuf *pBuf, int32_t colIndex) { return 0; }
893
int32_t taosAnalyBufWriteColData(SAnalyticBuf *pBuf, int32_t colIndex, int32_t colType, void *colValue) { return 0; }
894
int32_t taosAnalyBufWriteColEnd(SAnalyticBuf *pBuf, int32_t colIndex) { return 0; }
895
int32_t taosAnalyBufWriteDataEnd(SAnalyticBuf *pBuf) { return 0; }
896
int32_t taosAnalyBufClose(SAnalyticBuf *pBuf) { return 0; }
897
void    taosAnalyBufDestroy(SAnalyticBuf *pBuf) {}
898

899
const char   *taosAnalysisAlgoType(EAnalAlgoType algoType) { return 0; }
900
EAnalAlgoType taosAnalAlgoInt(const char *algoName) { return 0; }
901
const char   *taosAnalAlgoUrlStr(EAnalAlgoType algoType) { return 0; }
902

903
int64_t taosAnalysisParseTimout(SHashObj *pHashMap, const char *id) { return 0; }
904

905
int32_t taosAnalysisParseAlgo(const char *pOpt, char *pAlgoName, char *pUrl, int32_t type, int32_t len,
906
                              SHashObj *pHashMap, const char *id) {
907
  return 0;
908
}
909

910
int8_t taosAnalysisParseWncheck(SHashObj* pHashMap, const char* id) { return 0;}
911
int32_t taosAnalyGetOpts(const char *pOption, SHashObj **pOptHash) { return 0;}
912

913
#endif
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc