blob: 6bf3d5176a0f16c359b9135cad8be328989142c5 [file] [edit]
/*****************************************************************************\
* Copyright (C) SchedMD LLC.
*
* This file is part of Slurm, a resource management program.
* For details, see <https://slurm.schedmd.com/>.
* Please also read the included file: DISCLAIMER.
*
* Slurm is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* In addition, as a special exception, the copyright holders give permission
* to link the code of portions of this program with the OpenSSL library under
* certain conditions as described in each individual source file, and
* distribute linked combinations including the two. You must obey the GNU
* General Public License in all respects for all of the code used other than
* OpenSSL. If you modify file(s) with this exception, you may extend this
* exception to your version of the file(s), but you are not obligated to do
* so. If you do not wish to do so, delete this exception statement from your
* version. If you delete this exception statement from all source files in
* the program, then also delete it here.
*
* Slurm is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along
* with Slurm; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
\*****************************************************************************/
/*
* This is a port of the old slurm_unit serializer-test.c. It runs under the
* test_100_1 pytest meta-runner (compiled and run by atf.run_check_test() as a
* single translation unit, so the large JSON data blobs are #include'd rather
* than linked).
*
* It tests every plugin that serves application/json for round-trip
* correctness. The suite runs once per implementation: serializer/json, when
* it was built, and serializer/xjson. Cases that need constructs only JSON6
* provides gate themselves on _active_grammar().
*/
/* _GNU_SOURCE required for HAVE_MALLINFO2 for better logging */
#define _GNU_SOURCE
#include "config.h"
#include <limits.h>
#if defined(__GLIBC__) && !defined(__UCLIBC__) && !defined(__MUSL__)
#include <features.h>
#if defined(__GLIBC_PREREQ)
#if __GLIBC_PREREQ(2, 33) && defined(__SIZEOF_INT128__)
#define HAVE_MALLINFO2
#include <malloc.h>
#endif
#endif
#endif
#include <check.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "slurm/slurm_errno.h"
#include "src/common/data.h"
#include "src/common/log.h"
#include "src/common/macros.h"
#include "src/common/read_config.h"
#include "src/common/slurm_protocol_defs.h"
#include "src/common/slurm_time.h"
#include "src/common/timers.h"
#include "src/common/xmalloc.h"
#include "src/common/xstring.h"
#include "src/common/xutf.h"
#include "src/interfaces/serializer.h"
#include "public_datasets/twitter.json.c"
#include "public_datasets/noaa_global_temps.json.c"
#define BYTES_IN_MiB (1024 * 1024)
#ifdef HAVE_MALLINFO2
typedef unsigned __int128 uint128_t;
typedef struct {
uint128_t arena; /* non-mmapped space allocated from system */
uint128_t ordblks; /* number of free chunks */
uint128_t smblks; /* number of fastbin blocks */
uint128_t hblks; /* number of mmapped regions */
uint128_t hblkhd; /* space in mmapped regions */
uint128_t usmblks; /* always 0, preserved for backwards compatibility */
uint128_t fsmblks; /* space available in freed fastbin blocks */
uint128_t uordblks; /* total allocated space */
uint128_t fordblks; /* total free space */
uint128_t keepcost; /* top-most, releasable (via malloc_trim) space */
} mallinfo2_128_t;
typedef struct {
struct mallinfo2 peak;
mallinfo2_128_t total;
int count;
} mem_track_t;
#endif /* HAVE_MALLINFO2 */
static const char *mime_types[] = {
MIME_TYPE_YAML,
MIME_TYPE_JSON,
};
static const serializer_flags_t flag_combinations[] = {
SER_FLAGS_COMPACT,
SER_FLAGS_PRETTY,
(SER_FLAGS_COMPACT | SER_FLAGS_COMPLEX),
(SER_FLAGS_PRETTY | SER_FLAGS_COMPLEX),
};
/*
* Run both layouts over the large datasets: the pretty printer is the path most
* likely to have indentation or newline defects and these are the only
* multi-megabyte documents that exercise it. SER_FLAGS_COMPLEX is deliberately
* absent -- these datasets hold no infinities, so it would only duplicate a run
* for no added signal.
*/
static const serializer_flags_t flag_combinations_large[] = {
SER_FLAGS_COMPACT,
SER_FLAGS_PRETTY,
};
static const struct {
const char *source;
const char *tag;
const int run_count; /* diff count to avoid test running too long */
} test_json[] = { { test_json1, "twitter-dataset", 25 },
{ test_json2, "NOAA-ocean-temps", 50 } };
/*
* Grammar served by the plugin bound to MIME_TYPE_JSON.
*
* serializer/json implements RFC 8259 only. serializer/xjson implements JSON6,
* a superset, so it accepts everything RFC 8259 does plus comments, single
* quoted and unquoted strings, line continuations and a wider escape set.
* Documents are classified by the grammar they require rather than by plugin
* name so that a second JSON6 implementation would need no changes here.
*/
typedef enum {
GRAMMAR_JSON = SLURM_BIT(0), /* RFC 8259 */
GRAMMAR_JSON6 = SLURM_BIT(1), /* JSON6 superset of RFC 8259 */
} grammar_t;
/* Override which plugin serves MIME_TYPE_JSON: see _json_plugin() */
#define SERIALIZER_ENV "SLURM_TESTSUITE_SERIALIZER"
#define SERIALIZER_PREFIX "serializer/"
/* Strip the optional "serializer/" prefix that load_plugins() also accepts */
static const char *_bare_plugin(const char *plugin)
{
if (!xstrncmp(plugin, SERIALIZER_PREFIX, strlen(SERIALIZER_PREFIX)))
return (plugin + strlen(SERIALIZER_PREFIX));
return plugin;
}
/*
* Is plugin named in SerializerPlugins?
*
* load_plugins() takes a comma separated list and accepts each entry with or
* without the "serializer/" prefix, so match the same way.
*/
static bool _conf_selects(const char *plugin)
{
const char *bare = _bare_plugin(plugin);
char *list = NULL, *type = NULL, *last = NULL;
bool found = false;
if (!slurm_conf.serializer_plugins)
return false;
list = xstrdup(slurm_conf.serializer_plugins);
for (type = strtok_r(list, ",", &last); type;
type = strtok_r(NULL, ",", &last)) {
if (!xstrcmp(_bare_plugin(type), bare)) {
found = true;
break;
}
}
xfree(list);
return found;
}
/*
* Plugin that must serve MIME_TYPE_JSON for the tcase being run.
*
* Set by the tcase fixture before any test runs, so that the same tests run
* once per implementation. Always one of the MIME_TYPE_*_PLUGIN constants, so
* it is already in the "serializer/<name>" form load_plugins() normalizes to.
*/
static const char *json_plugin = NULL;
static const char *_json_plugin(void)
{
ck_assert_msg(json_plugin, "tcase fixture must select a plugin");
return json_plugin;
}
/* Grammar the configured JSON plugin is required to implement */
static grammar_t _active_grammar(void)
{
bool json, xjson;
/*
* An unset SerializerPlugins loads every plugin found in PluginDir and
* the binding for MIME_TYPE_JSON then depends on readdir() order, so
* the grammar under test would be undefined.
*/
ck_assert_msg(slurm_conf.serializer_plugins,
"SerializerPlugins must be set to bind " MIME_TYPE_JSON);
json = _conf_selects(MIME_TYPE_JSON_PLUGIN);
xjson = _conf_selects(MIME_TYPE_XJSON_PLUGIN);
ck_assert_msg((json != xjson),
"exactly one of %s or %s must be configured, got \"%s\"",
MIME_TYPE_JSON_PLUGIN, MIME_TYPE_XJSON_PLUGIN,
slurm_conf.serializer_plugins);
if (xjson)
return (GRAMMAR_JSON | GRAMMAR_JSON6);
return GRAMMAR_JSON;
}
static void _setup(void)
{
log_options_t log_opts = LOG_OPTS_INITIALIZER;
const char *debug_env = getenv("SLURM_DEBUG");
const char *debug_flags_env = getenv("SLURM_DEBUG_FLAGS");
if (debug_env)
log_opts.stderr_level = log_string2num(debug_env);
if (debug_flags_env)
debug_str2flags(debug_flags_env, &slurm_conf.debug_flags);
log_init("serializer-test", log_opts, 0, NULL);
ck_assert(!slurm_conf_init(NULL));
/*
* Pin which plugins load. Leaving SerializerPlugins unset loads every
* plugin in PluginDir, and serializer/json and serializer/xjson declare
* the same mime_types[], so only the first one registered would serve
* application/json. Naming the plugin makes the implementation under
* test explicit rather than discovered.
*/
xfree(slurm_conf.serializer_plugins);
slurm_conf.serializer_plugins =
xstrdup_printf("%s,%s,%s", _json_plugin(),
MIME_TYPE_YAML_PLUGIN,
MIME_TYPE_URL_ENCODED_PLUGIN);
/*
* Pin the flags too. Every case below asserts against the plugin's
* default layout and substitution behavior, so an inherited
* SerializerParameters or SLURM_JSON/SLURM_YAML from the environment
* would change what the dumper emits and fail cases that are correct.
* json6 is the sharpest example: it turns the U+FFFD substitution that
* _test_utf8_case() expects into \xNN escapes.
*/
xfree(slurm_conf.serializer_params);
unsetenv(ENV_CONFIG_JSON);
unsetenv(ENV_CONFIG_YAML);
serializer_required(MIME_TYPE_JSON);
serializer_required(MIME_TYPE_YAML);
}
/*
* One fixture per implementation. tcase fixtures take no arguments, so each
* selects its plugin before the shared setup runs.
*/
static void _setup_json(void)
{
json_plugin = MIME_TYPE_JSON_PLUGIN;
_setup();
}
static void _setup_xjson(void)
{
json_plugin = MIME_TYPE_XJSON_PLUGIN;
_setup();
}
static void teardown(void)
{
serializer_g_fini();
slurm_conf_destroy();
log_fini();
}
/* serialize src to a string, re-parse it, and verify the round-trip matches */
static void _test_run(const char *tag, data_t *src, const char *mime_type,
const serializer_flags_t flags)
{
char *output = NULL;
size_t output_len = -1;
data_t *verify_src = NULL;
int rc;
rc = serialize_g_data_to_string(&output, &output_len, src, mime_type,
flags);
ck_assert_int_eq(rc, 0);
debug("dumped %s with %s:\n%s\n\n\n\n", tag, mime_type, output);
rc = serialize_g_string_to_data(&verify_src, output, output_len,
mime_type);
ck_assert_int_eq(rc, 0);
ck_assert_msg(data_check_match(src, verify_src, false),
"match verification failed");
xfree(output);
FREE_NULL_DATA(verify_src);
}
START_TEST(test_mimetype)
{
const char *expected = _json_plugin();
const grammar_t grammar = _active_grammar();
const char *ptr = NULL;
/*
* The plugin named in SerializerPlugins must be the one that actually
* won the mime type. They differ if the plugin failed to load or lost
* the type to another plugin, which would silently test the wrong
* implementation.
*/
ck_assert(resolve_mime_type(MIME_TYPE_JSON, &ptr) != NULL);
ck_assert(ptr != NULL);
ck_assert_str_eq(ptr, expected);
/* Grammar must follow the bound plugin */
if (!xstrcmp(ptr, MIME_TYPE_XJSON_PLUGIN))
ck_assert(grammar & GRAMMAR_JSON6);
else
ck_assert(!(grammar & GRAMMAR_JSON6));
ck_assert(grammar & GRAMMAR_JSON);
ptr = NULL;
ck_assert(resolve_mime_type("application/jsonrequest", &ptr) != NULL);
ck_assert(ptr != NULL);
ck_assert_str_eq(ptr, expected);
}
END_TEST
START_TEST(test_mimetype_arrays)
{
const char **all = get_mime_type_array(false);
const char **openapi = get_mime_type_array(true);
const char *expected[] = {
MIME_TYPE_JSON,
MIME_TYPE_YAML,
MIME_TYPE_URL_ENCODED,
};
for (int i = 0; i < ARRAY_SIZE(expected); i++) {
ck_assert_ptr_nonnull(all[i]);
ck_assert_str_eq(all[i], expected[i]);
ck_assert_ptr_nonnull(resolve_mime_type(expected[i], NULL));
}
ck_assert_ptr_null(all[ARRAY_SIZE(expected)]);
ck_assert_ptr_nonnull(openapi[0]);
ck_assert_str_eq(openapi[0], MIME_TYPE_JSON);
ck_assert_ptr_nonnull(openapi[1]);
ck_assert_str_eq(openapi[1], MIME_TYPE_YAML);
ck_assert_ptr_null(openapi[2]);
}
END_TEST
START_TEST(test_parse_invalid)
{
/* malformed JSON that the parser must reject */
static const char
*sf[] = {
"\"taco",
"taco\"",
"[",
"]",
"{",
"}",
"[{",
"{[",
"{[}",
"[{}",
"[\"taco",
"{\"taco",
"{\"taco:",
"{taco:",
"{\"taco\":",
"[taco:",
"[\"taco\":",
"[\"taco\",:",
",,,,]",
",:,,]",
"\\,",
":",
",:,",
"\"\\\"",
"[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[",
"{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:{test:test}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}",
"{\"taco\"::taco}",
"{::taco}",
"\xFE",
"\xFF",
"\xFE\xFF",
"\xFF\xFE",
"\xFE\xFF\x00\x00",
"\xFEtaco",
"\xFFtaco",
"\xFE\xFFtaco",
"\xFF\xFEtaco",
"\xFE\xFF\x00\x00taco",
"\x01",
"\x02",
"\x03",
"\x04",
"\x05",
"\x06",
"\x07",
"\x08",
"\\u0",
"\\uTACOS",
"\\u000TACOS",
"tacos\"tacos\"taco\"\"",
"*\"tacos\"taco\"\"",
"*,0",
};
for (int i = 0; i < ARRAY_SIZE(sf); i++) {
int rc;
data_t *d = NULL;
rc = serialize_g_string_to_data(&d, sf[i], strlen(sf[i]),
MIME_TYPE_JSON);
debug("expected fail source %d=%d -> %pD\n%s\n\n\n\n", i, rc, d,
sf[i]);
ck_assert_ptr_null(d);
FREE_NULL_DATA(d);
}
}
END_TEST
START_TEST(test_parse_valid)
{
/* valid JSON that must parse, match, and round-trip */
static const char *s[] = {
"\"taco\"",
"\"\\\"taco\\\"\"",
"[ 100 ]",
"[ 100.389 ]",
"[ -100.389 ]",
"[ 1.1238e10 ]",
"[ -1.1238e10 ]",
"{ \"taco\": \"tacos\" }",
"[ \"taco1\", \"taco2\", ]",
"[ \"taco1\", \"taco2\", \"taco3\" ]",
"[ true, false ]",
"{\t\t\t\n}",
"{ }",
"[ \"\\u0024\", \"\\u00a3\", \"\\u00c0\", \"\\u0418\", \"\\u0939\", \"\\u20ac\", \"\\ud55c\" ]",
"[]",
"[]",
"{}",
"[]",
"[[] \t]",
"[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]",
"[{\"test\":\"test\"}]",
"{\"test\":[]}",
"{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":{\"test\":\"test\"}}}}}}}}}}}}}}}}}}}}}}}}}}",
"\"\\u10FFFF\"",
"\"\\u10FFFFFFFFFFFFFFFFFFFFFFF\"",
};
data_t *c[] = {
data_set_string(data_new(), "taco"),
data_set_string(data_new(), "\"taco\""),
data_set_list(data_new()),
data_set_list(data_new()),
data_set_list(data_new()),
data_set_list(data_new()),
data_set_list(data_new()),
data_set_dict(data_new()),
data_set_list(data_new()),
data_set_list(data_new()),
data_set_list(data_new()),
data_set_dict(data_new()),
data_set_dict(data_new()),
data_set_list(data_new()),
data_set_list(data_new()),
data_set_list(data_new()),
data_set_dict(data_new()),
data_set_list(data_new()),
data_set_list(data_new()),
data_set_list(data_new()),
data_set_list(data_new()),
data_set_dict(data_new()),
data_set_dict(data_new()),
data_set_string(data_new(), "\u10FFFF"),
data_set_string(data_new(), "\u10FFFFFFFFFFFFFFFFFFFFFFF"),
};
data_set_int(data_list_append(c[2]), 100);
data_set_float(data_list_append(c[3]), 100.389);
data_set_float(data_list_append(c[4]), -100.389);
data_set_float(data_list_append(c[5]), 1.1238e10);
data_set_float(data_list_append(c[6]), -1.1238e10);
data_set_string(data_key_set(c[7], "taco"), "tacos");
data_set_string(data_list_append(c[8]), "taco1");
data_set_string(data_list_append(c[8]), "taco2");
data_set_string(data_list_append(c[9]), "taco1");
data_set_string(data_list_append(c[9]), "taco2");
data_set_string(data_list_append(c[9]), "taco3");
data_set_bool(data_list_append(c[10]), true);
data_set_bool(data_list_append(c[10]), false);
data_set_string(data_list_append(c[13]), "\U00000024");
data_set_string(data_list_append(c[13]), "\U000000a3");
data_set_string(data_list_append(c[13]), "\U000000c0");
data_set_string(data_list_append(c[13]), "\U00000418");
data_set_string(data_list_append(c[13]), "\U00000939");
data_set_string(data_list_append(c[13]), "\U000020ac");
data_set_string(data_list_append(c[13]), "\U0000d55c");
data_set_list(data_list_append(c[18]));
{
data_t *t = c[19];
for (int i = 0; i < 19; i++)
t = data_set_list(data_list_append(t));
}
data_set_string(data_key_set(data_set_dict(data_list_append(c[20])),
"test"),
"test");
data_set_list(data_key_set(c[21], "test"));
{
data_t *t = c[22];
for (int i = 0; i < 26; i++)
t = data_set_dict(data_key_set(t, "test"));
data_set_string(t, "test");
}
for (int i = 0; i < ARRAY_SIZE(s); i++) {
int rc;
data_t *d = NULL;
rc = serialize_g_string_to_data(&d, s[i], strlen(s[i]),
MIME_TYPE_JSON);
debug("expected pass source %d=%d -> %pD\n%s\n\n\n\n", i, rc, d,
s[i]);
ck_assert_int_eq(rc, 0);
data_convert_tree(d, DATA_TYPE_NONE);
ck_assert_msg(data_check_match(c[i], d, false),
"verify failed: %s", s[i]);
for (int f = 0; f < ARRAY_SIZE(flag_combinations); f++) {
for (int m = 0; m < ARRAY_SIZE(mime_types); m++) {
const char *mptr = NULL;
const char *mime_type =
resolve_mime_type(mime_types[m], &mptr);
if (mime_type)
_test_run(s[i], d, mime_type,
flag_combinations[f]);
else
debug("skipping test with %s",
mime_types[m]);
}
}
FREE_NULL_DATA(d);
}
for (int i = 0; i < ARRAY_SIZE(c); i++)
FREE_NULL_DATA(c[i]);
}
END_TEST
START_TEST(test_compliance_large)
{
/*
* Verify that the same JSON file can be parsed and dumped with same
* contents.
*/
for (int i = 0; i < ARRAY_SIZE(test_json); i++) {
int rc;
data_t *data = NULL;
const int len = strlen(test_json[i].source);
debug("source %s:\n%s\n\n\n\n", test_json[i].tag,
test_json[i].source);
rc = serialize_g_string_to_data(&data, test_json[i].source, len,
MIME_TYPE_JSON);
ck_assert_int_eq(rc, 0);
for (int f = 0; f < ARRAY_SIZE(flag_combinations_large); f++) {
for (int m = 0; m < ARRAY_SIZE(mime_types); m++) {
const char *mptr = NULL;
const char *mime_type =
resolve_mime_type(mime_types[m], &mptr);
if (mime_type)
_test_run(test_json[i].tag, data,
mime_type,
flag_combinations_large[f]);
else
debug("skipping test with %s",
mime_types[m]);
}
}
FREE_NULL_DATA(data);
}
}
END_TEST
#ifdef HAVE_MALLINFO2
static void _track_mem(mem_track_t *track)
{
struct mallinfo2 mi = mallinfo2();
track->peak.arena = MAX(mi.arena, track->peak.arena);
track->peak.ordblks = MAX(mi.ordblks, track->peak.ordblks);
track->peak.hblks = MAX(mi.hblks, track->peak.hblks);
track->peak.hblkhd = MAX(mi.hblkhd, track->peak.hblkhd);
track->peak.usmblks = MAX(mi.usmblks, track->peak.usmblks);
track->peak.fsmblks = MAX(mi.fsmblks, track->peak.fsmblks);
track->peak.uordblks = MAX(mi.uordblks, track->peak.uordblks);
track->peak.fordblks = MAX(mi.fordblks, track->peak.fordblks);
track->peak.keepcost = MAX(mi.keepcost, track->peak.keepcost);
track->total.arena += mi.arena;
track->total.ordblks += mi.ordblks;
track->total.hblks += mi.hblks;
track->total.hblkhd += mi.hblkhd;
track->total.usmblks += mi.usmblks;
track->total.fsmblks += mi.fsmblks;
track->total.uordblks += mi.uordblks;
track->total.fordblks += mi.fordblks;
track->total.keepcost += mi.keepcost;
track->count++;
}
static void _print_tracked_mem(mem_track_t *track, const char *type)
{
struct mallinfo2 avg = {
.arena = track->total.arena / track->count,
.ordblks = track->total.ordblks / track->count,
.hblks = track->total.hblks / track->count,
.hblkhd = track->total.hblkhd / track->count,
.usmblks = track->total.usmblks / track->count,
.fsmblks = track->total.fsmblks / track->count,
.uordblks = track->total.uordblks / track->count,
.fordblks = track->total.fordblks / track->count,
.keepcost = track->total.keepcost / track->count,
};
printf("\t%s Total non-mmapped bytes (arena): %zu/%zuB %zu/%zuMiB\n",
type, avg.arena, track->peak.arena, (avg.arena / 1048576),
(track->peak.arena / 1048576));
printf("\t%s # of free chunks (ordblks): %zu/%zu\n", type,
avg.ordblks, track->peak.ordblks);
printf("\t%s # of free fastbin blocks (smblks): %zu/%zu\n", type,
avg.smblks, track->peak.smblks);
printf("\t%s # of mapped regions (hblks): %zu/%zu\n", type,
avg.hblks, track->peak.hblks);
printf("\t%s Bytes in mapped regions (hblkhd): %zu/%zu\n", type,
avg.hblkhd, track->peak.hblkhd);
printf("\t%s Max. total allocated space (usmblks): %zu/%zu\n", type,
avg.usmblks, track->peak.usmblks);
printf("\t%s Free bytes held in fastbins (fsmblks): %zu/%zu\n", type,
avg.fsmblks, track->peak.fsmblks);
printf("\t%s Total allocated space (uordblks): %zu/%zu\n", type,
avg.uordblks, track->peak.uordblks);
printf("\t%s Total free space (fordblks): %zu/%zu\n", type,
avg.fordblks, track->peak.fordblks);
printf("\t%s Topmost releasable block (keepcost): %zu/%zu\n\n", type,
avg.keepcost, track->peak.keepcost);
}
#else /* !HAVE_MALLINFO2 */
/* avoid compile errors for undefined/unused variables */
typedef struct {
int a;
int b;
} mem_track_t;
#define _track_mem(track) ((void) (track))
#define _print_tracked_mem(track, type) ((void) (track))
#endif /* !HAVE_MALLINFO2 */
static void _test_bandwidth_str(const char *tag, const char *source,
const int run_count)
{
int rc;
data_t *data = NULL;
const int test_json_len = strlen(source);
char *output = NULL;
size_t output_len = 0;
timespec_t read_times = { 0, 0 }, write_times = { 0, 0 };
uint64_t total_written = 0, total_read = 0;
timespec_t fastest_write = { UINT_MAX, 0 };
timespec_t fastest_read = { UINT_MAX, 0 };
double read_avg, write_avg;
double read_diff, write_diff, read_rate, write_rate;
double read_rate_bytes, write_rate_bytes;
double fastest_read_rate_bytes, fastest_write_rate_bytes;
double fastest_read_rate, fastest_write_rate;
mem_track_t read_mem = { 0 }, write_mem = { 0 };
for (int i = 0; i < run_count; i++) {
DEF_TIMERS;
timespec_t duration = { 0, 0 };
FREE_NULL_DATA(data);
_track_mem(&read_mem);
START_TIMER;
rc = serialize_g_string_to_data(&data, source, test_json_len,
MIME_TYPE_JSON);
END_TIMER3(__func__, INFINITE);
duration = timespec_diff_ns(TIMER_END_TS, TIMER_START_TS).diff;
_track_mem(&read_mem);
total_read += test_json_len;
read_times = timespec_add(read_times, duration);
if (timespec_is_after(fastest_read, duration))
fastest_read = duration;
ck_assert_int_eq(rc, 0);
}
for (int i = 0; i < run_count; i++) {
DEF_TIMERS;
timespec_t duration = { 0, 0 };
_track_mem(&write_mem);
START_TIMER;
rc = serialize_g_data_to_string(&output, &output_len, data,
MIME_TYPE_JSON,
SER_FLAGS_PRETTY);
END_TIMER3(__func__, INFINITE);
duration = timespec_diff_ns(TIMER_END_TS, TIMER_START_TS).diff;
_track_mem(&write_mem);
total_written += output_len;
write_times = timespec_add(write_times, duration);
if (timespec_is_after(fastest_write, duration))
fastest_write = duration;
xfree(output);
output_len = 0;
ck_assert_int_eq(rc, 0);
}
FREE_NULL_DATA(data);
read_diff = timespec_to_secs(read_times) / run_count;
write_diff = timespec_to_secs(write_times) / run_count;
read_avg = total_read / run_count;
write_avg = total_written / run_count;
/* (bytes / sec) * (1 MiB / 1024*1024 bytes) */
read_rate_bytes = (read_avg / read_diff);
write_rate_bytes = (write_avg / write_diff);
fastest_read_rate_bytes = read_avg / timespec_to_secs(fastest_read);
fastest_write_rate_bytes = write_avg / timespec_to_secs(fastest_write);
read_rate = read_rate_bytes / BYTES_IN_MiB;
write_rate = write_rate_bytes / BYTES_IN_MiB;
fastest_read_rate = fastest_read_rate_bytes / BYTES_IN_MiB;
fastest_write_rate = fastest_write_rate_bytes / BYTES_IN_MiB;
printf("%s: %u runs:\n", tag, run_count);
printf("\tfastest read=%lf sec\n\tfastest write=%lf sec\n\n",
timespec_to_secs(fastest_read), timespec_to_secs(fastest_write));
printf("\tfastest read=%f MiB/sec \n\tfastest write=%f MiB/sec\n\n",
fastest_read_rate, fastest_write_rate);
printf("\tavg read=%lf sec\n\tavg write=%lf sec\n\n", read_diff,
write_diff);
printf("\tavg read=%f MiB/sec \n\tavg write=%f MiB/sec\n\n", read_rate,
write_rate);
_print_tracked_mem(&read_mem, "read");
_print_tracked_mem(&write_mem, "write");
}
START_TEST(test_bandwidth)
{
for (int i = 0; i < ARRAY_SIZE(test_json); i++)
_test_bandwidth_str(test_json[i].tag, test_json[i].source,
test_json[i].run_count);
}
END_TEST
/*
* Strings that can not be dumped as conformant JSON.
*
* Values are built from explicit byte arrays and never from C string literals:
* a literal such as "\u10FFFF" is decoded by the C compiler with its own
* escape rules, which are not JSON's, so what reaches the serializer would not
* be what the test appears to say.
*
* SER_FLAGS_JSON6 dumps the source bytes as \xNN and must round trip
* exactly. Without it every byte sequence below is replaced with U+FFFD, which
* is lossy but leaves valid JSON.
*/
static const struct {
const char *tag;
/*
* Grammar whose dumper this row describes. The substitution and \xNN
* behaviour below is serializer/xjson's; serializer/json passes bytes
* that are not valid UTF-8 straight through instead, so those rows are
* skipped unless a JSON6 plugin is bound. Pinning serializer/json's
* passthrough here would cement it, and emitting invalid UTF-8 is
* itself a violation of RFC 8259 section 8.1.
*/
const grammar_t requires;
const utf8_t in[8];
const size_t in_bytes;
/* expected value after dump+parse without SER_FLAGS_COMPLEX */
const utf8_t expect[8];
const size_t expect_bytes;
} utf8_cases[] = {
{ "private-use-U+E000",
GRAMMAR_JSON6,
{ 'a', 0xee, 0x80, 0x80, 'z' },
5,
{ 'a', 0xef, 0xbf, 0xbd, 'z' },
5 },
{ "private-use-U+F8FF",
GRAMMAR_JSON6,
{ 'a', 0xef, 0xa3, 0xbf, 'z' },
5,
{ 'a', 0xef, 0xbf, 0xbd, 'z' },
5 },
{ "astral-PUA-U+F0000",
GRAMMAR_JSON6,
{ 'a', 0xf3, 0xb0, 0x80, 0x80, 'z' },
6,
{ 'a', 0xef, 0xbf, 0xbd, 'z' },
5 },
{ "noncharacter-U+FFFE",
GRAMMAR_JSON6,
{ 'a', 0xef, 0xbf, 0xbe, 'z' },
5,
{ 'a', 0xef, 0xbf, 0xbd, 'z' },
5 },
{ "cesu8-surrogate",
GRAMMAR_JSON6,
{ 'a', 0xed, 0xa0, 0x80, 'z' },
5,
{ 'a', 0xef, 0xbf, 0xbd, 'z' },
5 },
{ "beyond-U+10FFFF",
GRAMMAR_JSON6,
{ 'a', 0xf4, 0x90, 0x80, 0x80, 'z' },
6,
{ 'a', 0xef, 0xbf, 0xbd, 'z' },
5 },
{ "overlong",
GRAMMAR_JSON6,
{ 'a', 0xc0, 0xb3, 'z' },
4,
{ 'a', 0xef, 0xbf, 0xbd, 'z' },
5 },
{ "bad-continuation",
GRAMMAR_JSON6,
{ 'a', 0xe0, 0xa0, 'A', 'z' },
5,
{ 'a', 0xef, 0xbf, 0xbd, 'A', 'z' },
6 },
{ "lone-continuation",
GRAMMAR_JSON6,
{ 'a', 0xff, 'z' },
3,
{ 'a', 0xef, 0xbf, 0xbd, 'z' },
5 },
/* valid UTF-8 must be untouched by every grammar and both modes */
{ "ascii", GRAMMAR_JSON, { 'h', 'i' }, 2, { 'h', 'i' }, 2 },
{ "emoji",
GRAMMAR_JSON,
{ 'a', 0xf0, 0x9f, 0x8c, 0xae, 'z' },
6,
{ 'a', 0xf0, 0x9f, 0x8c, 0xae, 'z' },
6 },
};
/*
* Dump src, verify the JSON, parse it back and compare against want[].
* Dumps the parsed value a second time to verify the output is idempotent: a
* substitution that drifted would produce different JSON on the second pass.
*/
static void _test_utf8_case(const char *tag, const utf8_t *in, size_t in_bytes,
const utf8_t *want, size_t want_bytes,
serializer_flags_t flags)
{
data_t *src = data_set_dict(data_new()), *back = NULL;
char *out = NULL, *again = NULL;
char *value = xstrndup((const char *) in, in_bytes);
size_t out_bytes = 0, again_bytes = 0;
const char *parsed = NULL;
const bool json6 = (flags & SER_FLAGS_JSON6);
data_set_string_own(data_key_set(src, "k"), value);
ck_assert_int_eq(serialize_g_data_to_string(&out, &out_bytes, src,
MIME_TYPE_JSON, flags),
0);
/*
* \u0000 is never a valid escape to emit: the parser rejects it and
* conformant parsers decode it to a NUL that truncates the value.
*/
ck_assert_msg(!xstrstr(out, "\\u0000"), "%s: dumped \\u0000 escape: %s",
tag, out);
if (!json6)
ck_assert_msg(
!xstrstr(out, "\\x"),
"%s: dumped \\x escape without SER_FLAGS_JSON6: %s",
tag, out);
ck_assert_int_eq(serialize_g_string_to_data(&back, out, out_bytes,
MIME_TYPE_JSON),
0);
parsed = data_get_string(data_key_get(back, "k"));
ck_assert_ptr_nonnull(parsed);
if (json6) {
/* source bytes are escaped verbatim and must survive exactly */
ck_assert_msg((strlen(parsed) == in_bytes) &&
!memcmp(parsed, in, in_bytes),
"%s: SER_FLAGS_JSON6 did not round trip: %s", tag,
out);
} else {
ck_assert_msg((strlen(parsed) == want_bytes) &&
!memcmp(parsed, want, want_bytes),
"%s: unexpected substitution: %s", tag, out);
}
ck_assert_int_eq(serialize_g_data_to_string(&again, &again_bytes, back,
MIME_TYPE_JSON, flags),
0);
ck_assert_msg(!xstrcmp(out, again),
"%s: dump is not idempotent: %s then %s", tag, out,
again);
xfree(out);
xfree(again);
FREE_NULL_DATA(src);
FREE_NULL_DATA(back);
}
/*
* RFC 8259 documents exercising every legal comma position.
*
* JSON6 grammar extensions must never change how a conformant document parses.
* Array elision is the one with teeth: it appends an element when a comma
* arrives with no value to its left, which RFC 8259 section 5 makes impossible
* for a conformant array, since every comma there sits between two values. If
* an extension ever fires on conformant input the element count moves, so
* pinning the exact lengths here is what catches it.
*
* nulls is tracked separately because a leaked elision shows up as an extra
* DATA_TYPE_NULL element rather than as a wrong value.
*/
static const struct {
const char *tag;
const char *doc;
const bool is_list;
const size_t length; /* list or dict entries */
const size_t nulls; /* DATA_TYPE_NULL elements, lists only */
} rfc_comma_cases[] = {
{ "empty-list", "[]", true, 0, 0 },
{ "one", "[1]", true, 1, 0 },
{ "two", "[1,2]", true, 2, 0 },
{ "three", "[1,2,3]", true, 3, 0 },
{ "five", "[1,2,3,4,5]", true, 5, 0 },
{ "spaced", "[ 1 , 2 , 3 ]", true, 3, 0 },
{ "newlines", "[1,\n2,\n3]", true, 3, 0 },
{ "strings", "[\"a\",\"b\",\"c\"]", true, 3, 0 },
{ "mixed-types", "[0,-1,1.5,true,false,null]", true, 6, 1 },
{ "explicit-nulls", "[null,null]", true, 2, 2 },
{ "nested-lists", "[[1,2],[3,4]]", true, 2, 0 },
{ "empty-nested", "[[],[]]", true, 2, 0 },
{ "list-of-dicts", "[{\"a\":1},{\"b\":2}]", true, 2, 0 },
{ "dict-two-keys", "{\"a\":1,\"b\":2}", false, 2, 0 },
{ "dict-three-keys", "{\"a\":1,\"b\":2,\"c\":3}", false, 3, 0 },
{ "dict-of-lists", "{\"a\":[1,2],\"b\":[3]}", false, 2, 0 },
{ "deep", "{\"a\":{\"b\":[1,2,{\"c\":[3,4]}]}}", false, 1, 0 },
};
static data_for_each_cmd_t _count_nulls(const data_t *d, void *arg)
{
size_t *nulls = arg;
if (data_get_type(d) == DATA_TYPE_NULL)
(*nulls)++;
return DATA_FOR_EACH_CONT;
}
START_TEST(test_rfc_comma_positions)
{
/*
* Every case here is strictly RFC 8259 conformant, so every plugin
* that serves application/json must accept it and produce the same
* structure. Running this for the RFC 8259 only grammar too is what
* makes the JSON6 result meaningful: it establishes the conformant
* baseline the extended grammar must not deviate from.
*/
for (int i = 0; i < ARRAY_SIZE(rfc_comma_cases); i++) {
const char *tag = rfc_comma_cases[i].tag;
const char *doc = rfc_comma_cases[i].doc;
data_t *d = NULL;
size_t nulls = 0;
ck_assert_msg(!serialize_g_string_to_data(&d, doc, strlen(doc),
MIME_TYPE_JSON),
"%s: conformant document rejected: %s", tag, doc);
if (rfc_comma_cases[i].is_list) {
ck_assert_msg(data_get_type(d) == DATA_TYPE_LIST,
"%s: expected a list: %s", tag, doc);
ck_assert_msg(data_get_list_length(d) ==
rfc_comma_cases[i].length,
"%s: expected %zu entries, got %zu: %s",
tag, rfc_comma_cases[i].length,
data_get_list_length(d), doc);
data_list_for_each_const(d, _count_nulls, &nulls);
ck_assert_msg(
nulls == rfc_comma_cases[i].nulls,
"%s: expected %zu null entries, got %zu: %s",
tag, rfc_comma_cases[i].nulls, nulls, doc);
} else {
ck_assert_msg(data_get_type(d) == DATA_TYPE_DICT,
"%s: expected a dictionary: %s", tag,
doc);
ck_assert_msg(data_get_dict_length(d) ==
rfc_comma_cases[i].length,
"%s: expected %zu keys, got %zu: %s", tag,
rfc_comma_cases[i].length,
data_get_dict_length(d), doc);
}
/* the document must also survive a dump and re-parse intact */
_test_run(tag, d, MIME_TYPE_JSON, SER_FLAGS_COMPACT);
FREE_NULL_DATA(d);
}
}
END_TEST
START_TEST(test_utf8_roundtrip)
{
const grammar_t grammar = _active_grammar();
for (int i = 0; i < ARRAY_SIZE(utf8_cases); i++) {
if ((utf8_cases[i].requires & grammar) !=
utf8_cases[i].requires) {
debug("skipping %s: needs a grammar the bound plugin does not implement",
utf8_cases[i].tag);
continue;
}
_test_utf8_case(utf8_cases[i].tag, utf8_cases[i].in,
utf8_cases[i].in_bytes, utf8_cases[i].expect,
utf8_cases[i].expect_bytes, SER_FLAGS_COMPACT);
_test_utf8_case(utf8_cases[i].tag, utf8_cases[i].in,
utf8_cases[i].in_bytes, utf8_cases[i].expect,
utf8_cases[i].expect_bytes,
(SER_FLAGS_COMPACT | SER_FLAGS_JSON6));
}
}
END_TEST
/*
* How a string's bytes survive parsing.
*
* serializer/xjson accepts everything RFC 8259 does and more: the JSON6 escape
* set, and any line ending or control byte written literally between the
* quotes. That is a superset of the parsing features rather than a defect, so
* these cases pin the value each form decodes to instead of arguing about
* which ones should be accepted.
*
* Dumping is not a superset. Whatever was parsed, the output must be strict
* RFC 8259, so every accepted case also asserts that re-dumping emits no raw
* control byte: each one comes back as a clean escaped character code.
* SER_FLAGS_COMPACT is used for that because the pretty printer emits newlines
* and tabs as layout.
*
* accepted_by is checked before rejected_by: JSON6 is a superset, so a JSON6
* plugin's grammar also has GRAMMAR_JSON set.
*
* A row with neither mask set for the bound grammar is skipped. serializer/json
* accepts several of these too -- raw control bytes, a lone surrogate, and an
* escaped NUL, which truncates its string -- but that is libjson-c's behavior
* rather than a grammar this suite can pin, and a stricter release of it must
* not turn this test red.
*/
static const struct {
const char *tag;
const char *doc;
/* grammar that must accept doc and parse it to want */
const grammar_t accepted_by;
const char *want;
/* grammar that must reject doc, or 0 to assert nothing */
const grammar_t rejected_by;
} string_cases[] = {
/* RFC 8259 two character escapes: both grammars, same value */
{ "esc-quote", "{\"k\":\"a\\\"b\"}", GRAMMAR_JSON, "a\"b", 0 },
{ "esc-backslash", "{\"k\":\"a\\\\b\"}", GRAMMAR_JSON, "a\\b", 0 },
{ "esc-solidus", "{\"k\":\"a\\/b\"}", GRAMMAR_JSON, "a/b", 0 },
{ "esc-backspace", "{\"k\":\"a\\bb\"}", GRAMMAR_JSON, "a\bb", 0 },
{ "esc-formfeed", "{\"k\":\"a\\fb\"}", GRAMMAR_JSON, "a\fb", 0 },
{ "esc-newline", "{\"k\":\"a\\nb\"}", GRAMMAR_JSON, "a\nb", 0 },
{ "esc-return", "{\"k\":\"a\\rb\"}", GRAMMAR_JSON, "a\rb", 0 },
{ "esc-tab", "{\"k\":\"a\\tb\"}", GRAMMAR_JSON, "a\tb", 0 },
/* RFC 8259 \uXXXX, decoded to UTF-8 */
{ "esc-u-ascii", "{\"k\":\"a\\u0041b\"}", GRAMMAR_JSON, "aAb", 0 },
{ "esc-u-latin1", "{\"k\":\"a\\u00e9b\"}", GRAMMAR_JSON,
"a\xc3\xa9"
"b",
0 },
{ "esc-u-bmp", "{\"k\":\"a\\u20acb\"}", GRAMMAR_JSON,
"a\xe2\x82\xac"
"b",
0 },
{ "esc-u-pair", "{\"k\":\"a\\ud83d\\ude00b\"}", GRAMMAR_JSON,
"a\xf0\x9f\x98\x80"
"b",
0 },
/* JSON6 adds these escapes; RFC 8259 has no such sequence */
{ "esc-vtab", "{\"k\":\"a\\vb\"}", GRAMMAR_JSON6, "a\vb",
GRAMMAR_JSON },
{ "esc-single-quote", "{\"k\":\"a\\'b\"}", GRAMMAR_JSON6, "a'b",
GRAMMAR_JSON },
{ "esc-hex", "{\"k\":\"a\\x41b\"}", GRAMMAR_JSON6, "aAb",
GRAMMAR_JSON },
{ "esc-hex-utf8", "{\"k\":\"a\\xc3\\xa9b\"}", GRAMMAR_JSON6,
"a\xc3\xa9"
"b",
GRAMMAR_JSON },
/* code points Slurm rejects by policy, in escaped form */
{ "esc-u-lone-surrogate", "{\"k\":\"a\\ud800b\"}", 0, NULL,
GRAMMAR_JSON6 },
{ "esc-u-nul", "{\"k\":\"a\\u0000b\"}", 0, NULL, GRAMMAR_JSON6 },
/* not an escape in either grammar */
{ "esc-unknown", "{\"k\":\"a\\qb\"}", 0, NULL,
(GRAMMAR_JSON | GRAMMAR_JSON6) },
/* JSON6 keeps a control byte that is written literally */
{ "raw-tab", "{\"k\":\"a\tb\"}", GRAMMAR_JSON6, "a\tb", 0 },
{ "raw-backspace", "{\"k\":\"a\bb\"}", GRAMMAR_JSON6, "a\bb", 0 },
{ "raw-vtab", "{\"k\":\"a\vb\"}", GRAMMAR_JSON6, "a\vb", 0 },
{ "raw-escape", "{\"k\":\"a\033b\"}", GRAMMAR_JSON6, "a\033b", 0 },
{ "raw-0x01", "{\"k\":\"a\001b\"}", GRAMMAR_JSON6, "a\001b", 0 },
{ "raw-0x1f", "{\"k\":\"a\037b\"}", GRAMMAR_JSON6, "a\037b", 0 },
/* a line ending is just another literal control byte */
{ "literal-crlf", "{\"k\":\"a\r\nb\"}", GRAMMAR_JSON6, "a\r\nb", 0 },
{ "literal-lf", "{\"k\":\"a\nb\"}", GRAMMAR_JSON6, "a\nb", 0 },
{ "literal-cr", "{\"k\":\"a\rb\"}", GRAMMAR_JSON6, "a\rb", 0 },
/*
* unless a backslash precedes it, which removes it. JSON6 treats CR,
* LF and CR LF alike, so a document written on Windows has to lose both
* bytes: keeping the LF would leave it in the value.
*/
{ "continuation-crlf", "{\"k\":\"a\\\r\nb\"}", GRAMMAR_JSON6, "ab",
GRAMMAR_JSON },
{ "continuation-lf", "{\"k\":\"a\\\nb\"}", GRAMMAR_JSON6, "ab",
GRAMMAR_JSON },
{ "continuation-cr", "{\"k\":\"a\\\rb\"}", GRAMMAR_JSON6, "ab",
GRAMMAR_JSON },
/* the terminator is the last byte: the parser must not read past it */
{ "continuation-cr-eof", "{\"k\":\"a\\\r", 0, NULL,
(GRAMMAR_JSON | GRAMMAR_JSON6) },
{ "continuation-crlf-eof", "{\"k\":\"a\\\r\n", 0, NULL,
(GRAMMAR_JSON | GRAMMAR_JSON6) },
{ "continuation-lf-eof", "{\"k\":\"a\\\n", 0, NULL,
(GRAMMAR_JSON | GRAMMAR_JSON6) },
};
START_TEST(test_string_escapes)
{
const grammar_t grammar = _active_grammar();
for (int i = 0; i < ARRAY_SIZE(string_cases); i++) {
const char *tag = string_cases[i].tag;
const char *doc = string_cases[i].doc;
const char *want = string_cases[i].want;
const grammar_t accepted_by = string_cases[i].accepted_by;
const grammar_t rejected_by = string_cases[i].rejected_by;
const char *parsed = NULL;
char *out = NULL;
size_t out_bytes = 0;
data_t *d = NULL;
int rc;
if (!(accepted_by & grammar)) {
if (!(rejected_by & grammar)) {
debug("skipping %s: not this grammar's to pin",
tag);
continue;
}
rc = serialize_g_string_to_data(&d, doc, strlen(doc),
MIME_TYPE_JSON);
ck_assert_msg(rc,
"%s: accepted \"%s\", wanted a rejection",
tag, doc);
ck_assert_ptr_null(d);
continue;
}
rc = serialize_g_string_to_data(&d, doc, strlen(doc),
MIME_TYPE_JSON);
ck_assert_msg(!rc, "%s: rejected with rc %d", tag, rc);
parsed = data_get_string(data_key_get(d, "k"));
ck_assert_ptr_nonnull(parsed);
ck_assert_msg(!xstrcmp(parsed, want),
"%s: parsed %zu bytes, wanted %zu", tag,
strlen(parsed), strlen(want));
/*
* Parsing is a superset, dumping is not: every control byte
* has to come back out as an escaped character code.
*/
ck_assert_int_eq(serialize_g_data_to_string(&out, &out_bytes, d,
MIME_TYPE_JSON,
SER_FLAGS_COMPACT),
0);
for (size_t j = 0; j < strlen(out); j++)
ck_assert_msg(((unsigned char) out[j]) >= 0x20,
"%s: dumped raw control byte 0x%02x", tag,
((unsigned char) out[j]));
xfree(out);
FREE_NULL_DATA(d);
}
}
END_TEST
/* Run every test against the one implementation selected by setup_fn */
static void _add_tcase(Suite *suite, const char *name, SFun setup_fn)
{
TCase *tcase = tcase_create(name);
/*
* Checked, not unchecked: the fixture has to run inside the forked
* child of each test. serializer_g_init() is one shot per process --
* serializer_g_fini() only releases the plugins under
* MEMORY_LEAK_DEBUG, and it sets a flag that makes a later
* serializer_g_init() assert. An unchecked fixture runs in the parent,
* so the second tcase would abort the runner before it could bind its
* plugin.
*/
tcase_add_checked_fixture(tcase, setup_fn, teardown);
/* generous timeout: the bandwidth and large-dataset cases run a while */
tcase_set_timeout(tcase, 3000);
tcase_add_test(tcase, test_mimetype);
tcase_add_test(tcase, test_mimetype_arrays);
tcase_add_test(tcase, test_parse_invalid);
tcase_add_test(tcase, test_parse_valid);
tcase_add_test(tcase, test_rfc_comma_positions);
tcase_add_test(tcase, test_utf8_roundtrip);
tcase_add_test(tcase, test_string_escapes);
tcase_add_test(tcase, test_compliance_large);
tcase_add_test(tcase, test_bandwidth);
suite_add_tcase(suite, tcase);
}
/* True if plugin should run: no filter set, or the filter names it */
static bool _selected(const char *only, const char *plugin)
{
if (!only || !only[0])
return true;
return !xstrcmp(_bare_plugin(only), _bare_plugin(plugin));
}
extern int main(int argc, char **argv)
{
int failures, added = 0;
Suite *suite = suite_create("serializer");
SRunner *sr = NULL;
const char *only = getenv(SERIALIZER_ENV);
/*
* Run every test once per implementation that serves
* application/json. Both must satisfy the RFC 8259 cases; the cases
* that need JSON6 gate themselves on _active_grammar(), so they run
* only under serializer/xjson.
*
* A single suite holds both tcases because the runner writes one XML
* document and the harness reads a single <suite> from it.
*
* SLURM_TESTSUITE_SERIALIZER limits the run to one implementation,
* accepting it with or without the "serializer/" prefix the same way
* load_plugins() does.
*/
#ifdef HAVE_JSON
/* serializer/json is only built when a JSON parser library was found */
if (_selected(only, MIME_TYPE_JSON_PLUGIN)) {
_add_tcase(suite, MIME_TYPE_JSON_PLUGIN, _setup_json);
added++;
}
#endif
if (_selected(only, MIME_TYPE_XJSON_PLUGIN)) {
_add_tcase(suite, MIME_TYPE_XJSON_PLUGIN, _setup_xjson);
added++;
}
if (!added) {
/*
* Running nothing would report success, so refuse instead of
* silently proving nothing.
*/
fprintf(stderr, "%s=\"%s\" selected no serializer plugin\n",
SERIALIZER_ENV, (only ? only : ""));
return 1;
}
sr = srunner_create(suite);
/*
* Each tcase binds a different plugin, and serializer_g_init() is one
* shot per process, so every test must run in its own fork. CK_FORK=no
* would leave the second tcase silently bound to the first tcase's
* plugin. Force forking rather than testing the wrong implementation.
*/
srunner_set_fork_status(sr, CK_FORK);
srunner_run_all(sr, CK_VERBOSE);
failures = srunner_ntests_failed(sr);
srunner_free(sr);
return failures;
}