master
c 827 lines 30.6 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "dyncfg-internals.h"
4 #include "dyncfg.h"
5
6 // ----------------------------------------------------------------------------
7 // unit test
8
9 #define LINE_FILE_STR TOSTRING(__LINE__) "@" __FILE__
10
11 struct dyncfg_unittest {
12 bool enabled;
13 size_t errors;
14
15 DICTIONARY *nodes;
16
17 SPINLOCK spinlock;
18 struct dyncfg_unittest_action *queue;
19 } dyncfg_unittest_data = { 0 };
20
21 typedef struct {
22 bool enabled;
23 bool removed;
24 struct {
25 double dbl;
26 bool bln;
27 } value;
28 } TEST_CFG;
29
30 typedef struct {
31 const char *id;
32 const char *source;
33 bool sync;
34 DYNCFG_TYPE type;
35 DYNCFG_CMDS cmds;
36 DYNCFG_SOURCE_TYPE source_type;
37
38 TEST_CFG current;
39 TEST_CFG expected;
40
41 bool received;
42 bool finished;
43
44 size_t last_saves;
45 bool needs_save;
46 } TEST;
47
48 struct dyncfg_unittest_action {
49 TEST *t;
50 BUFFER *result;
51 BUFFER *payload;
52 DYNCFG_CMDS cmd;
53 const char *add_name;
54 const char *source;
55
56 rrd_function_result_callback_t result_cb;
57 void *result_cb_data;
58
59 struct dyncfg_unittest_action *prev, *next;
60 };
61
62 static void dyncfg_unittest_register_error(const char *id, const char *msg) {
63 if(msg)
64 nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG UNITTEST: error on id '%s': %s", id ? id : "", msg);
65
66 __atomic_add_fetch(&dyncfg_unittest_data.errors, 1, __ATOMIC_RELAXED);
67 }
68
69 static int dyncfg_unittest_execute_cb(struct rrd_function_execute *rfe, void *data);
70
71 bool dyncfg_unittest_parse_payload(BUFFER *payload, TEST *t, DYNCFG_CMDS cmd, const char *add_name, const char *source) {
72 CLEAN_JSON_OBJECT *jobj = json_tokener_parse(buffer_tostring(payload));
73 if(!jobj) {
74 dyncfg_unittest_register_error(t->id, "cannot parse json payload");
75 return false;
76 }
77
78 struct json_object *json_double;
79 struct json_object *json_boolean;
80
81 json_object_object_get_ex(jobj, "double", &json_double);
82 double value_double = json_object_get_double(json_double);
83
84 json_object_object_get_ex(jobj, "boolean", &json_boolean);
85 int value_boolean = json_object_get_boolean(json_boolean);
86
87 if(cmd == DYNCFG_CMD_UPDATE) {
88 t->current.value.dbl = value_double;
89 t->current.value.bln = value_boolean;
90 }
91 else if(cmd == DYNCFG_CMD_ADD) {
92 size_t buf_size = strlen(t->id) + strlen(add_name) + 20;
93 CLEAN_CHAR_P *buf = mallocz(buf_size);
94 snprintfz(buf, buf_size, "%s:%s", t->id, add_name);
95 TEST tmp = {
96 .id = strdupz(buf),
97 .source = strdupz(source),
98 .cmds = (t->cmds & ~DYNCFG_CMD_ADD) | DYNCFG_CMD_GET | DYNCFG_CMD_REMOVE | DYNCFG_CMD_UPDATE | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE | DYNCFG_CMD_TEST,
99 .sync = t->sync,
100 .type = DYNCFG_TYPE_JOB,
101 .source_type = DYNCFG_SOURCE_TYPE_DYNCFG,
102 .received = true,
103 .finished = true,
104 .current =
105 {.enabled = true,
106 .removed = false,
107 .value =
108 {
109 .dbl = value_double,
110 .bln = value_boolean,
111 }},
112 .expected = {
113 .enabled = true,
114 .removed = false,
115 .value = {
116 .dbl = 3.14,
117 .bln = true,
118 }
119 },
120 .needs_save = true,
121 };
122 const DICTIONARY_ITEM *item = dictionary_set_and_acquire_item(dyncfg_unittest_data.nodes, buf, &tmp, sizeof(tmp));
123 TEST *t2 = dictionary_acquired_item_value(item);
124 dictionary_acquired_item_release(dyncfg_unittest_data.nodes, item);
125
126 dyncfg_add_low_level(localhost, t2->id, "/unittests",
127 DYNCFG_STATUS_RUNNING, t2->type, t2->source_type, t2->source,
128 t2->cmds, 0, 0, t2->sync,
129 HTTP_ACCESS_NONE, HTTP_ACCESS_NONE,
130 dyncfg_unittest_execute_cb, t2);
131 }
132 else {
133 dyncfg_unittest_register_error(t->id, "invalid command received to parse payload");
134 return false;
135 }
136
137 return true;
138 }
139
140 static int dyncfg_unittest_action(struct dyncfg_unittest_action *a) {
141 TEST *t = a->t;
142
143 int rc = HTTP_RESP_OK;
144
145 if(a->cmd == DYNCFG_CMD_ENABLE)
146 t->current.enabled = true;
147 else if(a->cmd == DYNCFG_CMD_DISABLE)
148 t->current.enabled = false;
149 else if(a->cmd == DYNCFG_CMD_ADD || a->cmd == DYNCFG_CMD_UPDATE)
150 rc = dyncfg_unittest_parse_payload(a->payload, a->t, a->cmd, a->add_name, a->source) ? HTTP_RESP_OK : HTTP_RESP_BAD_REQUEST;
151 else if(a->cmd == DYNCFG_CMD_REMOVE)
152 t->current.removed = true;
153 else
154 rc = HTTP_RESP_BAD_REQUEST;
155
156 dyncfg_default_response(a->result, rc, NULL);
157
158 a->result_cb(a->result, rc, a->result_cb_data);
159
160 buffer_free(a->payload);
161 freez((void *)a->add_name);
162 freez(a);
163
164 __atomic_store_n(&t->finished, true, __ATOMIC_RELAXED);
165
166 return rc;
167 }
168
169 static void dyncfg_unittest_thread_action(void *ptr __maybe_unused) {
170 while(!nd_thread_signaled_to_cancel()) {
171 struct dyncfg_unittest_action *a = NULL;
172 spinlock_lock(&dyncfg_unittest_data.spinlock);
173 a = dyncfg_unittest_data.queue;
174 if(a)
175 DOUBLE_LINKED_LIST_REMOVE_ITEM_UNSAFE(dyncfg_unittest_data.queue, a, prev, next);
176 spinlock_unlock(&dyncfg_unittest_data.spinlock);
177
178 if(a)
179 dyncfg_unittest_action(a);
180 else
181 sleep_usec(10 * USEC_PER_MS);
182 }
183 }
184
185 static int dyncfg_unittest_execute_cb(struct rrd_function_execute *rfe, void *data) {
186
187 int rc;
188 bool run_the_callback = true;
189 TEST *t = data;
190
191 t->received = true;
192
193 CLEAN_CHAR_P *buf = strdupz(rfe->function);
194
195 char *words[MAX_FUNCTION_PARAMETERS]; // an array of pointers for the words in this line
196 size_t num_words = quoted_strings_splitter_whitespace(buf, words, MAX_FUNCTION_PARAMETERS);
197
198 const char *config = get_word(words, num_words, 0);
199 const char *id = get_word(words, num_words, 1);
200 const char *action = get_word(words, num_words, 2);
201 const char *add_name = get_word(words, num_words, 3);
202
203 if(!config || !*config || strcmp(config, PLUGINSD_FUNCTION_CONFIG) != 0) {
204 char *msg = "did not receive a config call";
205 dyncfg_unittest_register_error(id, msg);
206 rc = dyncfg_default_response(rfe->result.wb, HTTP_RESP_BAD_REQUEST, msg);
207 goto cleanup;
208 }
209
210 if(!id || !*id) {
211 char *msg = "did not receive an id";
212 dyncfg_unittest_register_error(id, msg);
213 rc = dyncfg_default_response(rfe->result.wb, HTTP_RESP_BAD_REQUEST, msg);
214 goto cleanup;
215 }
216
217 if(t->type != DYNCFG_TYPE_TEMPLATE && strcmp(t->id, id) != 0) {
218 char *msg = "id received is not the expected";
219 dyncfg_unittest_register_error(id, msg);
220 rc = dyncfg_default_response(rfe->result.wb, HTTP_RESP_BAD_REQUEST, msg);
221 goto cleanup;
222 }
223
224 if(!action || !*action) {
225 char *msg = "did not receive an action";
226 dyncfg_unittest_register_error(id, msg);
227 rc = dyncfg_default_response(rfe->result.wb, HTTP_RESP_BAD_REQUEST, msg);
228 goto cleanup;
229 }
230
231 DYNCFG_CMDS cmd = dyncfg_cmds2id(action);
232 if(cmd == DYNCFG_CMD_NONE) {
233 char *msg = "action received is not known";
234 dyncfg_unittest_register_error(id, msg);
235 rc = dyncfg_default_response(rfe->result.wb, HTTP_RESP_BAD_REQUEST, msg);
236 goto cleanup;
237 }
238
239 if(!(t->cmds & cmd)) {
240 char *msg = "received a command that is not supported";
241 dyncfg_unittest_register_error(id, msg);
242 rc = dyncfg_default_response(rfe->result.wb, HTTP_RESP_BAD_REQUEST, msg);
243 goto cleanup;
244 }
245
246 if(t->current.removed && cmd != DYNCFG_CMD_ADD) {
247 char *msg = "received a command for a removed entry";
248 dyncfg_unittest_register_error(id, msg);
249 rc = dyncfg_default_response(rfe->result.wb, HTTP_RESP_BAD_REQUEST, msg);
250 goto cleanup;
251 }
252
253 struct dyncfg_unittest_action *a = callocz(1, sizeof(*a));
254 a->t = t;
255 a->add_name = add_name ? strdupz(add_name) : NULL;
256 a->source = rfe->source,
257 a->result = rfe->result.wb;
258 a->payload = buffer_dup(rfe->payload);
259 a->cmd = cmd;
260 a->result_cb = rfe->result.cb;
261 a->result_cb_data = rfe->result.data;
262
263 run_the_callback = false;
264
265 if(t->sync)
266 rc = dyncfg_unittest_action(a);
267 else {
268 spinlock_lock(&dyncfg_unittest_data.spinlock);
269 DOUBLE_LINKED_LIST_APPEND_ITEM_UNSAFE(dyncfg_unittest_data.queue, a, prev, next);
270 spinlock_unlock(&dyncfg_unittest_data.spinlock);
271 rc = HTTP_RESP_OK;
272 }
273
274 cleanup:
275 if(run_the_callback) {
276 __atomic_store_n(&t->finished, true, __ATOMIC_RELAXED);
277
278 if (rfe->result.cb)
279 rfe->result.cb(rfe->result.wb, rc, rfe->result.data);
280 }
281
282 return rc;
283 }
284
285 static bool dyncfg_unittest_check(TEST *t, DYNCFG_CMDS c, const char *cmd, bool received) {
286 size_t errors = 0;
287
288 fprintf(stderr, "CHECK '%s' after cmd '%s'...", t->id, cmd);
289
290 if(t->received != received) {
291 fprintf(stderr, "\n - received flag found '%s', expected '%s'",
292 t->received?"true":"false",
293 received?"true":"false");
294 errors++;
295 goto cleanup;
296 }
297
298 if(!received)
299 goto cleanup;
300
301 usec_t give_up_ut = now_monotonic_usec() + 2 * USEC_PER_SEC;
302 while(!__atomic_load_n(&t->finished, __ATOMIC_RELAXED)) {
303 tinysleep();
304
305 if(now_monotonic_usec() > give_up_ut) {
306 fprintf(stderr, "\n - gave up waiting for the plugin to process this!");
307 errors++;
308 goto cleanup;
309 }
310 }
311
312 if(t->type != DYNCFG_TYPE_TEMPLATE && t->current.enabled != t->expected.enabled) {
313 fprintf(stderr, "\n - enabled flag found '%s', expected '%s'",
314 t->current.enabled?"true":"false",
315 t->expected.enabled?"true":"false");
316 errors++;
317 }
318 if(t->current.removed != t->expected.removed) {
319 fprintf(stderr, "\n - removed flag found '%s', expected '%s'",
320 t->current.removed?"true":"false",
321 t->expected.removed?"true":"false");
322 errors++;
323 }
324 if(t->current.value.bln != t->expected.value.bln) {
325 fprintf(stderr, "\n - boolean value found '%s', expected '%s'",
326 t->current.value.bln?"true":"false",
327 t->expected.value.bln?"true":"false");
328 errors++;
329 }
330 if(t->current.value.dbl != t->expected.value.dbl) {
331 fprintf(stderr, "\n - double value found '%f', expected '%f'",
332 t->current.value.dbl, t->expected.value.dbl);
333 errors++;
334 }
335
336 DYNCFG *df = dictionary_get(dyncfg_globals.nodes, t->id);
337 if(!df) {
338 fprintf(stderr, "\n - not found in DYNCFG nodes dictionary!");
339 errors++;
340 }
341 else if(df->cmds != t->cmds) {
342 fprintf(stderr, "\n - has different cmds in DYNCFG nodes dictionary; found: ");
343 dyncfg_cmds2fp(df->cmds, stderr);
344 fprintf(stderr, ", expected: ");
345 dyncfg_cmds2fp(t->cmds, stderr);
346 fprintf(stderr, "\n");
347 errors++;
348 }
349 else if(df->type == DYNCFG_TYPE_JOB && df->current.source_type == DYNCFG_SOURCE_TYPE_DYNCFG && !df->dyncfg.saves) {
350 fprintf(stderr, "\n - DYNCFG job has no saves!");
351 errors++;
352 }
353 else if(df->type == DYNCFG_TYPE_JOB && df->current.source_type == DYNCFG_SOURCE_TYPE_DYNCFG && (!df->dyncfg.payload || !buffer_strlen(df->dyncfg.payload))) {
354 fprintf(stderr, "\n - DYNCFG job has no payload!");
355 errors++;
356 }
357 else if(df->dyncfg.user_disabled && !df->dyncfg.saves) {
358 fprintf(stderr, "\n - DYNCFG disabled config has no saves!");
359 errors++;
360 }
361 else if((c & (DYNCFG_CMD_ADD | DYNCFG_CMD_UPDATE)) && t->source && string_strcmp(df->current.source, t->source) != 0) {
362 fprintf(stderr, "\n - source does not match!");
363 errors++;
364 }
365 else if((c & (DYNCFG_CMD_ADD | DYNCFG_CMD_UPDATE)) && df->current.source && !t->source) {
366 fprintf(stderr, "\n - there is a source but it shouldn't be any!");
367 errors++;
368 }
369 else if(t->needs_save && df->dyncfg.saves <= t->last_saves) {
370 fprintf(stderr, "\n - should be saved, but it is not saved!");
371 errors++;
372 }
373 else if(!t->needs_save && df->dyncfg.saves > t->last_saves) {
374 fprintf(stderr, "\n - should be not be saved, but it saved!");
375 errors++;
376 }
377
378 cleanup:
379 if(errors) {
380 fprintf(stderr, "\n >>> FAILED\n\n");
381 dyncfg_unittest_register_error(NULL, NULL);
382 return false;
383 }
384
385 fprintf(stderr, " OK\n");
386 return true;
387 }
388
389 static void dyncfg_unittest_reset(void) {
390 TEST *t;
391 dfe_start_read(dyncfg_unittest_data.nodes, t) {
392 t->received = t->finished = false;
393 t->needs_save = false;
394
395 DYNCFG *df = dictionary_get(dyncfg_globals.nodes, t->id);
396 if(!df) {
397 nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG UNITTEST: cannot find id '%s'", t->id);
398 dyncfg_unittest_register_error(NULL, NULL);
399 }
400 else
401 t->last_saves = df->dyncfg.saves;
402 }
403 dfe_done(t);
404 }
405
406 void should_be_saved(TEST *t, DYNCFG_CMDS c) {
407 DYNCFG *df;
408
409 if(t->type == DYNCFG_TYPE_TEMPLATE) {
410 df = dictionary_get(dyncfg_globals.nodes, t->id);
411 t->current.enabled = !df->dyncfg.user_disabled;
412 }
413
414 t->needs_save =
415 c == DYNCFG_CMD_UPDATE ||
416 (t->current.enabled && c == DYNCFG_CMD_DISABLE) ||
417 (!t->current.enabled && c == DYNCFG_CMD_ENABLE);
418 }
419
420 static int dyncfg_unittest_run(const char *cmd, BUFFER *wb, const char *payload, const char *source) {
421 dyncfg_unittest_reset();
422
423 CLEAN_CHAR_P *buf = strdupz(cmd);
424
425 char *words[MAX_FUNCTION_PARAMETERS]; // an array of pointers for the words in this line
426 size_t num_words = quoted_strings_splitter_whitespace(buf, words, MAX_FUNCTION_PARAMETERS);
427
428 // const char *config = get_word(words, num_words, 0);
429 const char *id = get_word(words, num_words, 1);
430 char *action = get_word(words, num_words, 2);
431 const char *add_name = get_word(words, num_words, 3);
432
433 DYNCFG_CMDS c = dyncfg_cmds2id(action);
434
435 TEST *t = dictionary_get(dyncfg_unittest_data.nodes, id);
436 if(!t) {
437 nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG UNITTEST: cannot find id '%s' from cmd: %s", id, cmd);
438 dyncfg_unittest_register_error(NULL, NULL);
439 return HTTP_RESP_NOT_FOUND;
440 }
441
442 if(t->type == DYNCFG_TYPE_TEMPLATE)
443 t->received = t->finished = true;
444
445 if(c == DYNCFG_CMD_DISABLE)
446 t->expected.enabled = false;
447 if(c == DYNCFG_CMD_ENABLE)
448 t->expected.enabled = true;
449 if(c == DYNCFG_CMD_UPDATE) {
450 memset(&t->current.value, 0, sizeof(t->current.value));
451 if(t->type == DYNCFG_TYPE_JOB) {
452 // a successful update on a job flips ownership to DYNCFG, so the
453 // node must advertise REMOVE -- we hardcode the expected change
454 // here (rather than calling dyncfg_sanitize_cmds()) so a regression
455 // in the SUT cannot mask itself by also breaking the oracle.
456 t->cmds |= DYNCFG_CMD_REMOVE;
457 }
458 }
459
460 if(c & (DYNCFG_CMD_UPDATE) || (c & (DYNCFG_CMD_DISABLE|DYNCFG_CMD_ENABLE) && t->type != DYNCFG_TYPE_TEMPLATE)) {
461 freez((void *)t->source);
462 t->source = strdupz(source);
463 }
464
465 buffer_flush(wb);
466
467 CLEAN_BUFFER *pld = NULL;
468
469 if(payload) {
470 pld = buffer_create(1024, NULL);
471 buffer_strcat(pld, payload);
472 }
473
474 should_be_saved(t, c);
475
476 int rc = rrd_function_run(localhost, wb, 10, HTTP_ACCESS_ALL, cmd,
477 true, NULL,
478 NULL, NULL,
479 NULL, NULL,
480 NULL, NULL,
481 pld, source, false);
482 if(!DYNCFG_RESP_SUCCESS(rc)) {
483 nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG UNITTEST: failed to run: %s; returned code %d", cmd, rc);
484 dyncfg_unittest_register_error(NULL, NULL);
485 }
486
487 dyncfg_unittest_check(t, c, cmd, true);
488
489 if(rc == HTTP_RESP_OK && t->type == DYNCFG_TYPE_TEMPLATE) {
490 if(c == DYNCFG_CMD_ADD) {
491 size_t buf2_size = strlen(id) + strlen(add_name) + 2;
492 CLEAN_CHAR_P *buf2 = mallocz(buf2_size);
493 snprintfz(buf2, buf2_size, "%s:%s", id, add_name);
494 TEST *tt = dictionary_get(dyncfg_unittest_data.nodes, buf2);
495 if (!tt) {
496 nd_log(NDLS_DAEMON, NDLP_ERR,
497 "DYNCFG UNITTEST: failed to find newly added id '%s' of command: %s",
498 id, cmd);
499 dyncfg_unittest_register_error(NULL, NULL);
500 }
501 dyncfg_unittest_check(tt, c, cmd, true);
502 }
503 else {
504 STRING *template = string_strdupz(t->id);
505 DYNCFG *df;
506 dfe_start_read(dyncfg_globals.nodes, df) {
507 if(df->type == DYNCFG_TYPE_JOB && df->template == template) {
508 TEST *tt = dictionary_get(dyncfg_unittest_data.nodes, df_dfe.name);
509 if (!tt) {
510 nd_log(NDLS_DAEMON, NDLP_ERR,
511 "DYNCFG UNITTEST: failed to find id '%s' while running command: %s", df_dfe.name, cmd);
512 dyncfg_unittest_register_error(NULL, NULL);
513 }
514 else {
515 if(c == DYNCFG_CMD_DISABLE)
516 tt->expected.enabled = false;
517 if(c == DYNCFG_CMD_ENABLE)
518 tt->expected.enabled = true;
519 dyncfg_unittest_check(tt, c, cmd, true);
520 }
521 }
522 }
523 dfe_done(df);
524 string_freez(template);
525 }
526 }
527
528 return rc;
529 }
530
531 static void dyncfg_unittest_cleanup_files(void) {
532 char path[FILENAME_MAX];
533 snprintfz(path, sizeof(path) - 1, "%s/%s", netdata_configured_varlib_dir, "config");
534
535 DIR *dir = opendir(path);
536 if (!dir) {
537 nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG UNITTEST: cannot open directory '%s'", path);
538 return;
539 }
540
541 struct dirent *entry;
542 char filename[FILENAME_MAX + sizeof(entry->d_name)];
543 while ((entry = readdir(dir)) != NULL) {
544 if ((entry->d_type == DT_REG || entry->d_type == DT_LNK) && strstartswith(entry->d_name, "unittest:") && strendswith(entry->d_name, ".dyncfg")) {
545 snprintf(filename, sizeof(filename), "%s/%s", path, entry->d_name);
546 nd_log(NDLS_DAEMON, NDLP_INFO, "DYNCFG UNITTEST: deleting file '%s'", filename);
547 unlink(filename);
548 }
549 }
550
551 closedir(dir);
552 }
553
554 static TEST *dyncfg_unittest_add(TEST t) {
555 dyncfg_unittest_reset();
556
557 TEST *ret = dictionary_set(dyncfg_unittest_data.nodes, t.id, &t, sizeof(t));
558
559 if(!dyncfg_add_low_level(localhost, t.id, "/unittests", DYNCFG_STATUS_RUNNING, t.type,
560 t.source_type, t.source,
561 t.cmds, 0, 0, t.sync,
562 HTTP_ACCESS_NONE, HTTP_ACCESS_NONE,
563 dyncfg_unittest_execute_cb, ret)) {
564 dyncfg_unittest_register_error(t.id, "addition of job failed");
565 }
566
567 dyncfg_unittest_check(ret, DYNCFG_CMD_NONE, "plugin create", t.type != DYNCFG_TYPE_TEMPLATE);
568
569 return ret;
570 }
571
572 void dyncfg_unittest_delete_cb(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data __maybe_unused) {
573 TEST *v = value;
574 freez((void *)v->id);
575 freez((void *)v->source);
576 }
577
578 int dyncfg_unittest(void) {
579 dyncfg_unittest_data.nodes = dictionary_create(DICT_OPTION_NONE);
580 dictionary_register_delete_callback(dyncfg_unittest_data.nodes, dyncfg_unittest_delete_cb, NULL);
581
582 dyncfg_unittest_cleanup_files();
583 rrd_functions_inflight_init();
584 dyncfg_init(false);
585
586 // ------------------------------------------------------------------------
587 // create the thread for testing async communication
588
589 ND_THREAD *thread = nd_thread_create("unittest", NETDATA_THREAD_OPTION_DEFAULT, dyncfg_unittest_thread_action, NULL);
590
591 // ------------------------------------------------------------------------
592 // single
593
594 TEST *single1 = dyncfg_unittest_add((TEST){
595 .id = strdupz("unittest:sync:single1"),
596 .source = strdupz(LINE_FILE_STR),
597 .type = DYNCFG_TYPE_SINGLE,
598 .cmds = DYNCFG_CMD_GET | DYNCFG_CMD_SCHEMA | DYNCFG_CMD_UPDATE | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE,
599 .source_type = DYNCFG_SOURCE_TYPE_INTERNAL,
600 .sync = true,
601 .current = {
602 .enabled = true,
603 },
604 .expected = {
605 .enabled = true,
606 }
607 }); (void)single1;
608
609 TEST *single2 = dyncfg_unittest_add((TEST){
610 .id = strdupz("unittest:async:single2"),
611 .source = strdupz(LINE_FILE_STR),
612 .type = DYNCFG_TYPE_SINGLE,
613 .cmds = DYNCFG_CMD_GET | DYNCFG_CMD_SCHEMA | DYNCFG_CMD_UPDATE | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE,
614 .source_type = DYNCFG_SOURCE_TYPE_INTERNAL,
615 .sync = false,
616 .current = {
617 .enabled = true,
618 },
619 .expected = {
620 .enabled = true,
621 }
622 }); (void)single2;
623
624 // ------------------------------------------------------------------------
625 // template
626
627 TEST *template1 = dyncfg_unittest_add((TEST){
628 .id = strdupz("unittest:sync:template1"),
629 .source = strdupz(LINE_FILE_STR),
630 .type = DYNCFG_TYPE_TEMPLATE,
631 .cmds = DYNCFG_CMD_SCHEMA | DYNCFG_CMD_ADD | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE,
632 .source_type = DYNCFG_SOURCE_TYPE_INTERNAL,
633 .sync = true,
634 }); (void)template1;
635
636 TEST *template2 = dyncfg_unittest_add((TEST){
637 .id = strdupz("unittest:async:template2"),
638 .source = strdupz(LINE_FILE_STR),
639 .type = DYNCFG_TYPE_TEMPLATE,
640 .cmds = DYNCFG_CMD_SCHEMA | DYNCFG_CMD_ADD | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE,
641 .source_type = DYNCFG_SOURCE_TYPE_INTERNAL,
642 .sync = false,
643 }); (void)template2;
644
645 // ------------------------------------------------------------------------
646 // job
647
648 TEST *user1 = dyncfg_unittest_add((TEST){
649 .id = strdupz("unittest:sync:template1:user1"),
650 .source = strdupz(LINE_FILE_STR),
651 .type = DYNCFG_TYPE_JOB,
652 .cmds = DYNCFG_CMD_SCHEMA | DYNCFG_CMD_UPDATE | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE,
653 .source_type = DYNCFG_SOURCE_TYPE_USER,
654 .sync = true,
655 .current = {
656 .enabled = true,
657 },
658 .expected = {
659 .enabled = true,
660 }
661 }); (void)user1;
662
663 TEST *user2 = dyncfg_unittest_add((TEST){
664 .id = strdupz("unittest:async:template2:user2"),
665 .source = strdupz(LINE_FILE_STR),
666 .type = DYNCFG_TYPE_JOB,
667 .cmds = DYNCFG_CMD_SCHEMA | DYNCFG_CMD_UPDATE | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE,
668 .source_type = DYNCFG_SOURCE_TYPE_USER,
669 .sync = false,
670 .expected = {
671 .enabled = true,
672 }
673 }); (void)user2;
674
675 // ------------------------------------------------------------------------
676
677 int rc; (void)rc;
678 BUFFER *wb = buffer_create(0, NULL);
679
680 // ------------------------------------------------------------------------
681 // dynamic job
682
683 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:template1 add dyn1", wb, "{\"double\":3.14,\"boolean\":true}", LINE_FILE_STR);
684 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:template1 add dyn2", wb, "{\"double\":3.14,\"boolean\":true}", LINE_FILE_STR);
685 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:async:template2 add dyn3", wb, "{\"double\":3.14,\"boolean\":true}", LINE_FILE_STR);
686 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:async:template2 add dyn4", wb, "{\"double\":3.14,\"boolean\":true}", LINE_FILE_STR);
687
688 // ------------------------------------------------------------------------
689 // updating an existing user/stock-style job makes it dyncfg-owned and removable
690
691 user1->expected.value.dbl = 3.14;
692 user1->expected.value.bln = true;
693 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:template1:user1 update", wb, "{\"double\":3.14,\"boolean\":true}", LINE_FILE_STR);
694
695 // direct, helper-free assertion that the production node really exposes
696 // REMOVE after the ownership flip -- catches regressions in dyncfg_sanitize_cmds()
697 // or the intercept-on-successful-update path independently of the harness oracle.
698 {
699 DYNCFG *df = dictionary_get(dyncfg_globals.nodes, user1->id);
700 if(!df)
701 dyncfg_unittest_register_error(user1->id, "node missing after update");
702 else {
703 if(df->current.source_type != DYNCFG_SOURCE_TYPE_DYNCFG)
704 dyncfg_unittest_register_error(user1->id, "after update, current.source_type should be DYNCFG");
705 if(!(df->cmds & DYNCFG_CMD_REMOVE))
706 dyncfg_unittest_register_error(user1->id, "after update flipping ownership to DYNCFG, cmds must include REMOVE");
707 }
708 }
709
710 // ------------------------------------------------------------------------
711 // saving of user_disabled
712
713 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:single1 disable", wb, NULL, LINE_FILE_STR);
714 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:async:single2 disable", wb, NULL, LINE_FILE_STR);
715 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:template1:user1 disable", wb, NULL, LINE_FILE_STR);
716 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:async:template2:user2 disable", wb, NULL, LINE_FILE_STR);
717 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:template1:dyn1 disable", wb, NULL, LINE_FILE_STR);
718 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:template1:dyn2 disable", wb, NULL, LINE_FILE_STR);
719 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:async:template2:dyn3 disable", wb, NULL, LINE_FILE_STR);
720 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:async:template2:dyn4 disable", wb, NULL, LINE_FILE_STR);
721
722 // ------------------------------------------------------------------------
723 // enabling
724
725 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:single1 enable", wb, NULL, LINE_FILE_STR);
726 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:async:single2 enable", wb, NULL, LINE_FILE_STR);
727 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:template1:user1 enable", wb, NULL, LINE_FILE_STR);
728 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:async:template2:user2 enable", wb, NULL, LINE_FILE_STR);
729 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:template1:dyn1 enable", wb, NULL, LINE_FILE_STR);
730 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:template1:dyn2 enable", wb, NULL, LINE_FILE_STR);
731 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:async:template2:dyn3 enable", wb, NULL, LINE_FILE_STR);
732 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:async:template2:dyn4 enable", wb, NULL, LINE_FILE_STR);
733
734 // ------------------------------------------------------------------------
735 // disabling template
736
737 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:template1 disable", wb, NULL, LINE_FILE_STR);
738 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:async:template2 disable", wb, NULL, LINE_FILE_STR);
739
740 // ------------------------------------------------------------------------
741 // enabling template
742
743 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:template1 enable", wb, NULL, LINE_FILE_STR);
744 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:async:template2 enable", wb, NULL, LINE_FILE_STR);
745
746 // ------------------------------------------------------------------------
747 // adding job on disabled template
748
749 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:template1 disable", wb, NULL, LINE_FILE_STR);
750 dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:async:template2 disable", wb, NULL, LINE_FILE_STR);
751
752 TEST *user3 = dyncfg_unittest_add((TEST){
753 .id = strdupz("unittest:sync:template1:user3"),
754 .source = strdupz(LINE_FILE_STR),
755 .type = DYNCFG_TYPE_JOB,
756 .cmds = DYNCFG_CMD_SCHEMA | DYNCFG_CMD_UPDATE | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE,
757 .source_type = DYNCFG_SOURCE_TYPE_USER,
758 .sync = true,
759 .expected = {
760 .enabled = false,
761 }
762 }); (void)user3;
763
764 TEST *user4 = dyncfg_unittest_add((TEST){
765 .id = strdupz("unittest:async:template2:user4"),
766 .source = strdupz(LINE_FILE_STR),
767 .type = DYNCFG_TYPE_JOB,
768 .cmds = DYNCFG_CMD_SCHEMA | DYNCFG_CMD_UPDATE | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE,
769 .source_type = DYNCFG_SOURCE_TYPE_USER,
770 .sync = false,
771 .expected = {
772 .enabled = false,
773 }
774 }); (void)user4;
775
776 TEST *user5 = dyncfg_unittest_add((TEST){
777 .id = strdupz("unittest:sync:template1:user5"),
778 .source = strdupz(LINE_FILE_STR),
779 .type = DYNCFG_TYPE_JOB,
780 .cmds = DYNCFG_CMD_SCHEMA | DYNCFG_CMD_UPDATE | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE,
781 .source_type = DYNCFG_SOURCE_TYPE_USER,
782 .sync = true,
783 .expected = {
784 .enabled = false,
785 }
786 }); (void)user5;
787
788 TEST *user6 = dyncfg_unittest_add((TEST){
789 .id = strdupz("unittest:async:template2:user6"),
790 .source = strdupz(LINE_FILE_STR),
791 .type = DYNCFG_TYPE_JOB,
792 .cmds = DYNCFG_CMD_SCHEMA | DYNCFG_CMD_UPDATE | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE,
793 .source_type = DYNCFG_SOURCE_TYPE_USER,
794 .sync = false,
795 .expected = {
796 .enabled = false,
797 }
798 }); (void)user6;
799
800 // dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:template1:user5 disable", wb, NULL, LINE_FILE_STR);
801 // dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:async:template2:user6 disable", wb, NULL, LINE_FILE_STR);
802
803 // // ------------------------------------------------------------------------
804 // // enable template with disabled jobs
805 //
806 // user3->expected.enabled = true;
807 // user5->expected.enabled = false;
808 // dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:sync:template1 enable", wb, NULL, LINE_FILE_STR);
809 //
810 // user4->expected.enabled = true;
811 // user6->expected.enabled = false;
812 // dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " unittest:async:template2 enable", wb, NULL, LINE_FILE_STR);
813
814
815 // // ------------------------------------------------------------------------
816 //
817 // rc = dyncfg_unittest_run(PLUGINSD_FUNCTION_CONFIG " tree", wb, NULL);
818 // if(rc == HTTP_RESP_OK)
819 // fprintf(stderr, "%s\n", buffer_tostring(wb));
820
821 nd_thread_signal_cancel(thread);
822 nd_thread_join(thread);
823 dyncfg_unittest_cleanup_files();
824 dictionary_destroy(dyncfg_unittest_data.nodes);
825 buffer_free(wb);
826 return __atomic_load_n(&dyncfg_unittest_data.errors, __ATOMIC_RELAXED) > 0 ? 1 : 0;
827 }