pdtest: add support for input format specs in test.conf files master
authorGerhard Sittig <gerhard.sittig@gmx.net>
Sat, 23 Apr 2022 06:32:30 +0000 (08:32 +0200)
committerGerhard Sittig <gerhard.sittig@gmx.net>
Sat, 23 Apr 2022 14:58:46 +0000 (16:58 +0200)
The pdtest(1) utility interprets test.conf files. Extend the syntax of
'input' lines. These used to accept one filename only. Add support for
'format' and 'option' keywords. These translate to runtc(1) -I options.

configure.ac
decoder/pdtest
decoder/runtc.c

index e87ca0c89447509d505ff6320df1bcf794ee5355..f0c6753f4a115536dffbaf8959871b2c6fb79798 100644 (file)
@@ -59,7 +59,10 @@ SR_PKG_CHECK_SUMMARY([st_pkglibs_summary])
 # first, since usually only that variant will add "-lpython3.8".
 # https://docs.python.org/3/whatsnew/3.8.html#debug-build-uses-the-same-abi-as-release-build
 SR_PKG_CHECK([python3], [ST_PKGLIBS],
-       [python-3.8-embed], [python-3.8 >= 3.8], [python-3.7 >= 3.7], [python-3.6 >= 3.6], [python-3.5 >= 3.5], [python-3.4 >= 3.4], [python-3.3 >= 3.3], [python-3.2 >= 3.2], [python3 >= 3.2])
+       [python-3.8-embed], [python3-embed],
+       [python-3.8 >= 3.8], [python-3.7 >= 3.7], [python-3.6 >= 3.6],
+       [python-3.5 >= 3.5], [python-3.4 >= 3.4], [python-3.3 >= 3.3],
+       [python-3.2 >= 3.2], [python3 >= 3.2])
 AS_IF([test "x$sr_have_python3" = xno],
        [AC_MSG_ERROR([Cannot find Python 3 development headers.])])
 
index 59483253c86eab67bf6f5322c088827bb492e5cd..a2a33fc9b66260f609f32a98db067656c61231e2 100755 (executable)
@@ -146,9 +146,26 @@ def parse_testfile(path, pd, tc, op_type, op_class):
                     raise E_syntax
                 tclist[-1]['stack'] = f
             elif key == 'input':
-                if len(f) != 1:
+                if len(f) < 1:
                     raise E_syntax
-                tclist[-1]['input'] = f[0]
+                input_spec = {
+                    'name': f.pop(0),
+                    'format': None,
+                    'options': [],
+                }
+                while len(f):
+                    if len(f) < 2:
+                        # Always needs <key> <value>
+                        raise E_syntax
+                    a, b = f[:2]
+                    f = f[2:]
+                    if a == 'format':
+                        input_spec['format'] = b
+                    elif a == 'option':
+                        input_spec['options'].append(b)
+                    else:
+                        raise E_syntax
+                tclist[-1]['input'] = input_spec
             elif key == 'output':
                 op_spec = {
                     'pd': f.pop(0),
@@ -325,7 +342,15 @@ def run_tests(tests, fix=False):
                         args.extend(['-o', "%s=%s" % (option, value)])
                     for label, initial_pin in spd['initial_pins']:
                         args.extend(['-N', "%s=%d" % (label, initial_pin)])
-                args.extend(['-i', os.path.join(dumps_dir, tc['input'])])
+                # Setup input spec for this test (optional format spec).
+                in_spec = tc['input']
+                infile = os.path.join(dumps_dir, in_spec['name'])
+                args.extend(['-i', infile])
+                if in_spec['format']:
+                    args.extend(['-I', in_spec['format']])
+                    for opt in in_spec['options']:
+                        args.extend(['-I', opt])
+                # Setup output spec for this test.
                 for op in tc['output']:
                     name = "%s/%s/%s" % (pd, tc['name'], op['type'])
                     opargs = ['-O', "%s:%s" % (op['pd'], op['type'])]
index 90c37b64b76746ed7391b8ae20c90c0703b3310e..074882629350a5e28cb3f6c90ab1bb8f9ea34cdb 100644 (file)
@@ -17,6 +17,8 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include <config.h>
+
 #include <Python.h>
 #include <libsigrokdecode/libsigrokdecode.h>
 #include <libsigrok/libsigrok.h>
@@ -37,6 +39,8 @@
 #include <sched.h>
 #endif
 
+#define CHUNK_SIZE     (4 * 1024 * 1024)
+
 static int debug = FALSE;
 static int statistics = FALSE;
 static char *coverage_report;
@@ -64,11 +68,18 @@ struct pd {
        GSList *initial_pins;
 };
 
+struct input {
+       const char *filename;
+       const char *format;
+       GSList *opt_list;
+       GHashTable *opt_hash;
+};
+
 struct output {
        const char *pd;
        const char *pd_id;
        int type;
-       const char *class;
+       const char *class_;
        int class_idx;
        const char *outfile;
        int outfd;
@@ -150,6 +161,7 @@ static void usage(const char *msg)
        printf("  -o <channeloption=value> (optional)\n");
        printf("  -N <channelname=initial-pin-value> (optional)\n");
        printf("  -i <input file>\n");
+       printf("  -I <input format> (optional)\n");
        printf("  -O <output-pd:output-type[:output-class]>\n");
        printf("  -f <output file> (optional)\n");
        printf("  -c <coverage report> (optional)\n");
@@ -324,10 +336,16 @@ static void sr_cb(const struct sr_dev_inst *sdi,
                const struct sr_datafeed_packet *packet, void *cb_data)
 {
        static int samplecnt = 0;
+       static gboolean start_sent;
+
        const struct sr_datafeed_logic *logic;
        struct srd_session *sess;
+       const struct sr_datafeed_meta *meta;
+       struct sr_config *src;
+       GSList *l;
        GVariant *gvar;
        uint64_t samplerate;
+       int ret;
        int num_samples;
        struct sr_dev_driver *driver;
 
@@ -336,26 +354,49 @@ static void sr_cb(const struct sr_dev_inst *sdi,
        driver = sr_dev_inst_driver_get(sdi);
 
        switch (packet->type) {
+       case SR_DF_META:
+               DBG("Received SR_DF_META");
+               meta = packet->payload;
+               for (l = meta->config; l; l = l->next) {
+                       src = l->data;
+                       switch (src->key) {
+                       case SR_CONF_SAMPLERATE:
+                               samplerate = g_variant_get_uint64(src->data);
+                               ret = srd_session_metadata_set(sess,
+                                       SRD_CONF_SAMPLERATE,
+                                       g_variant_new_uint64(samplerate));
+                               if (ret != SRD_OK)
+                                       ERR("Setting samplerate failed (meta)");
+                               break;
+                       default:
+                               /* EMPTY */
+                               break;
+                       }
+               }
+               break;
        case SR_DF_HEADER:
                DBG("Received SR_DF_HEADER");
                if (sr_config_get(driver, sdi, NULL, SR_CONF_SAMPLERATE,
                                &gvar) != SR_OK) {
-                       ERR("Getting samplerate failed");
+                       DBG("Getting samplerate failed (SR_DF_HEADER)");
                        break;
                }
                samplerate = g_variant_get_uint64(gvar);
                g_variant_unref(gvar);
-               if (srd_session_metadata_set(sess, SRD_CONF_SAMPLERATE,
-                               g_variant_new_uint64(samplerate)) != SRD_OK) {
-                       ERR("Setting samplerate failed");
-                       break;
-               }
-               if (srd_session_start(sess) != SRD_OK) {
-                       ERR("Session start failed");
-                       break;
-               }
+               ret = srd_session_metadata_set(sess, SRD_CONF_SAMPLERATE,
+                       g_variant_new_uint64(samplerate));
+               if (ret != SRD_OK)
+                       ERR("Setting samplerate failed (header)");
                break;
        case SR_DF_LOGIC:
+               DBG("Received SR_DF_LOGIC");
+               if (!start_sent) {
+                       if (srd_session_start(sess) != SRD_OK) {
+                               ERR("Session start failed");
+                               break;
+                       }
+                       start_sent = TRUE;
+               }
                logic = packet->payload;
                num_samples = logic->length / logic->unitsize;
                DBG("Received SR_DF_LOGIC (%"PRIu64" bytes, unitsize = %d).",
@@ -366,12 +407,113 @@ static void sr_cb(const struct sr_dev_inst *sdi,
                break;
        case SR_DF_END:
                DBG("Received SR_DF_END");
+#if defined HAVE_SRD_SESSION_SEND_EOF && HAVE_SRD_SESSION_SEND_EOF
+               (void)srd_session_send_eof(sess);
+#endif
                break;
        }
 
 }
 
-static int run_testcase(const char *infile, GSList *pdlist, struct output *op)
+static GHashTable *parse_input_options(const struct sr_option **pd_opts,
+       GSList *user_specs)
+{
+       GHashTable *set_opts;
+       GVariant *pd_def, *gvar;
+       size_t idx;
+       GSList *l;
+       const char *pd_key;
+       const char *spec_text, *s;
+
+       set_opts = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
+               (GDestroyNotify)g_variant_unref);
+       for (idx = 0; pd_opts[idx]; idx++) {
+               pd_key = pd_opts[idx]->id;
+               /* Is the PD provided option in the set of input specs? */
+               s = NULL;
+               for (l = user_specs; l; l = l->next) {
+                       spec_text = l->data;
+                       if (strncmp(spec_text, pd_key, strlen(pd_key)) != 0)
+                               continue;
+                       spec_text += strlen(pd_key);
+                       if (!*spec_text) {
+                               /* Found 'key' up to end of text. */
+                               s = spec_text;
+                               break;
+                       }
+                       if (*spec_text == '=') {
+                               /* Found 'key=...', position to RHS value. */
+                               s = ++spec_text;
+                               break;
+                       }
+               }
+               if (!s)
+                       continue;
+               /*
+                * Normalize the input text for the user specified value.
+                * A key without an explicit value is useful for booleans.
+                */
+               if (!*s)
+                       s = "";
+               /*
+                * Convert the text to the PD default value's data type.
+                * Store the resulting variable in the hash which gets
+                * passed to the input module.
+                */
+               pd_def = pd_opts[idx]->def;
+               if (g_variant_is_of_type(pd_def, G_VARIANT_TYPE_UINT32)) {
+                       gvar = g_variant_new_uint32(strtoul(s, NULL, 10));
+                       g_hash_table_insert(set_opts, g_strdup(pd_key),
+                               g_variant_ref_sink(gvar));
+                       continue;
+               }
+               if (g_variant_is_of_type(pd_def, G_VARIANT_TYPE_INT32)) {
+                       gvar = g_variant_new_int32(strtol(s, NULL, 10));
+                       g_hash_table_insert(set_opts, g_strdup(pd_key),
+                               g_variant_ref_sink(gvar));
+                       continue;
+               }
+               if (g_variant_is_of_type(pd_def, G_VARIANT_TYPE_UINT64)) {
+                       gvar = g_variant_new_uint64(strtoull(s, NULL, 10));
+                       g_hash_table_insert(set_opts, g_strdup(pd_key),
+                               g_variant_ref_sink(gvar));
+                       continue;
+               }
+               if (g_variant_is_of_type(pd_def, G_VARIANT_TYPE_DOUBLE)) {
+                       gvar = g_variant_new_double(strtod(s, NULL));
+                       g_hash_table_insert(set_opts, g_strdup(pd_key),
+                               g_variant_ref_sink(gvar));
+                       continue;
+               }
+               if (g_variant_is_of_type(pd_def, G_VARIANT_TYPE_STRING)) {
+                       gvar = g_variant_new_string(s);
+                       g_hash_table_insert(set_opts, g_strdup(pd_key),
+                               g_variant_ref_sink(gvar));
+                       continue;
+               }
+               if (g_variant_is_of_type(pd_def, G_VARIANT_TYPE_BOOLEAN)) {
+                       gboolean b;
+                       if (strcmp(s, "false") == 0 || strcmp(s, "no") == 0) {
+                               b = FALSE;
+                       } else if (strcmp(s, "true") == 0 || strcmp(s, "yes") == 0) {
+                               b = TRUE;
+                       } else {
+                               ERR("Cannot convert '%s' to boolean", s);
+                               return NULL;
+                       }
+                       gvar = g_variant_new_boolean(b);
+                       g_hash_table_insert(set_opts, g_strdup(pd_key),
+                               g_variant_ref_sink(gvar));
+                       continue;
+               }
+               ERR("Unsupported data type for option '%s'", pd_key);
+               return NULL;
+       }
+
+       return set_opts;
+}
+
+static int run_testcase(struct input *inp, GSList *pdlist, struct output *op)
 {
        struct srd_session *sess;
        struct srd_decoder *dec;
@@ -383,10 +525,14 @@ static int run_testcase(const char *infile, GSList *pdlist, struct output *op)
        GVariant *gvar;
        GHashTable *channels, *opts;
        GSList *pdl, *l, *l2, *devices;
-       int idx, i;
+       int ret, idx, i;
        int max_channel;
        char **decoder_class;
        struct sr_session *sr_sess;
+       const struct sr_input *in;
+       const struct sr_input_module *imod;
+       const struct sr_option **options;
+       GHashTable *mod_opts;
        gboolean is_number;
        const char *s;
        GArray *initial_pins;
@@ -400,10 +546,74 @@ static int run_testcase(const char *infile, GSList *pdlist, struct output *op)
                }
        }
 
-       if (sr_session_load(ctx, infile, &sr_sess) != SR_OK){
-               ERR("sr_session_load() failed");
+       /* Tell "session files" (.sr format) from other input modules. */
+       sr_sess = NULL;
+       in = NULL;
+       if (inp->format && strcmp(inp->format, "match") == 0) {
+               /* Automatic format detection. */
+               if (inp->opt_list) {
+                       ERR("Automatic file format won't take options");
+                       return FALSE;
+               }
+               ret = sr_input_scan_file(inp->filename, &in);
+               if (ret != SR_OK || !in) {
+                       ERR("Cannot open input file (format match)");
+                       return FALSE;
+               }
+               sr_session_new(ctx, &sr_sess);
+               ret = SR_OK;
+       } else if (inp->format) {
+               /* Caller specified format, potentially with options. */
+               imod = sr_input_find(inp->format);
+               if (!imod) {
+                       ERR("Cannot find specified input module");
+                       return FALSE;
+               }
+               mod_opts = NULL;
+               options = sr_input_options_get(imod);
+               if (!options && inp->opt_list) {
+                       ERR("Input module does not support options");
+                       return FALSE;
+               }
+               if (inp->opt_list) {
+                       mod_opts = parse_input_options(options, inp->opt_list);
+                       if (!mod_opts) {
+                               ERR("Cannot process input module options");
+                               return FALSE;
+                       }
+               }
+               sr_input_options_free(options);
+               in = sr_input_new(imod, mod_opts);
+               if (!in) {
+                       ERR("Cannot create input module instance");
+                       return FALSE;
+               }
+               if (mod_opts)
+                       g_hash_table_destroy(mod_opts);
+               sr_session_new(ctx, &sr_sess);
+               ret = SR_OK;
+       } else {
+               /* No caller's format spec, assume .sr session file. */
+               ret = sr_session_load(ctx, inp->filename, &sr_sess);
+               if (ret != SR_OK || !sr_sess) {
+                       ERR("Cannot open session file");
+                       return FALSE;
+               }
+       }
+       if (ret != SR_OK || !sr_sess) {
+               ERR("Failed to open input file");
                return FALSE;
        }
+       if (in) {
+               /* Check file access early for non-session files. */
+               int fd;
+               fd = open(inp->filename, O_RDONLY);
+               if (fd < 0) {
+                       ERR("Cannot access input file (input module)");
+                       return FALSE;
+               }
+               close(fd);
+       }
 
        sr_session_dev_list(sr_sess, &devices);
 
@@ -544,7 +754,7 @@ static int run_testcase(const char *infile, GSList *pdlist, struct output *op)
 
        /* Resolve selected decoder's class index, so we can match. */
        dec = srd_decoder_get_by_id(pd->name);
-       if (op->class) {
+       if (op->class_) {
                if (op->type == SRD_OUTPUT_ANN)
                        l = dec->annotations;
                else if (op->type == SRD_OUTPUT_BINARY)
@@ -557,7 +767,7 @@ static int run_testcase(const char *infile, GSList *pdlist, struct output *op)
                idx = 0;
                while (l) {
                        decoder_class = l->data;
-                       if (!strcmp(decoder_class[0], op->class)) {
+                       if (!strcmp(decoder_class[0], op->class_)) {
                                op->class_idx = idx;
                                break;
                        }
@@ -566,15 +776,72 @@ static int run_testcase(const char *infile, GSList *pdlist, struct output *op)
                }
                if (op->class_idx == -1) {
                        ERR("Output class '%s' not found in decoder %s.",
-                                       op->class, pd->name);
+                                       op->class_, pd->name);
                        return FALSE;
                }
-               DBG("Class %s index is %d", op->class, op->class_idx);
+               DBG("Class %s index is %d", op->class_, op->class_idx);
        }
 
-       sr_session_start(sr_sess);
-       sr_session_run(sr_sess);
-       sr_session_stop(sr_sess);
+       /*
+        * Run a convenience sequence for session files. Run a custom
+        * read loop for non-native file formats (import modules).
+        *
+        * TODO Ideally the libsigrok library would provide transparent
+        * access to either kind of file. Either wrap an input module
+        * similar to "session files", or turn the currently "special"
+        * session file into a regular input module. It's unfortunate
+        * that multiple applications need to re-invent this logic.
+        */
+       if (!in) {
+               sr_session_start(sr_sess);
+               sr_session_run(sr_sess);
+               sr_session_stop(sr_sess);
+       } else if (in) {
+               int fd;
+               GString *buf;
+               gboolean got_sdi;
+               struct sr_dev_inst *sdi;
+               ssize_t len;
+
+               fd = open(inp->filename, O_RDONLY);
+               if (fd < 0) {
+                       ERR("Cannot open input file (read loop)");
+                       return FALSE;
+               }
+               buf = g_string_sized_new(CHUNK_SIZE);
+               got_sdi = FALSE;
+               while (TRUE) {
+                       g_string_truncate(buf, 0);
+                       len = read(fd, buf->str, buf->allocated_len);
+                       if (len < 0) {
+                               ERR("Cannot read from input file (read loop)");
+                               return FALSE;
+                       }
+                       if (len == 0)
+                               break;
+                       buf->len = len;
+                       ret = sr_input_send(in, buf);
+                       if (ret != SR_OK) {
+                               ERR("Cannot process input file content");
+                               break;
+                       }
+
+                       sdi = sr_input_dev_inst_get(in);
+                       if (!got_sdi && sdi) {
+                               /* First time we got a valid sdi. */
+                               if (sr_session_dev_add(sr_sess, sdi) != SR_OK) {
+                                       ERR("Cannot use device after sdi creation");
+                                       break;
+                               }
+                               got_sdi = TRUE;
+                       }
+               }
+               sr_input_end(in);
+               sr_input_free(in);
+               g_string_free(buf, TRUE);
+               close(fd);
+               sr_session_destroy(sr_sess);
+       }
 
        srd_session_destroy(sess);
 
@@ -813,24 +1080,30 @@ int main(int argc, char **argv)
        struct pd *pd;
        struct channel *channel;
        struct option *option;
+       struct input *inp;
        struct output *op;
        int ret, c;
-       char *opt_infile, **kv, **opstr;
+       char **kv, **opstr;
        struct initial_pin_info *initial_pin;
 
+       inp = malloc(sizeof(*inp));
+       inp->filename = NULL;
+       inp->format = NULL;
+       inp->opt_list = NULL;
+       inp->opt_hash = NULL;
+
        op = malloc(sizeof(struct output));
        op->pd = NULL;
        op->pd_id = NULL;
        op->type = -1;
-       op->class = NULL;
+       op->class_ = NULL;
        op->class_idx = -1;
        op->outfd = 1;
 
        pdlist = NULL;
-       opt_infile = NULL;
        pd = NULL;
        coverage = NULL;
-       while ((c = getopt(argc, argv, "dP:p:o:N:i:O:f:c:S")) != -1) {
+       while ((c = getopt(argc, argv, "dP:p:o:N:i:I:O:f:c:S")) != -1) {
                switch (c) {
                case 'd':
                        debug = TRUE;
@@ -878,7 +1151,15 @@ int main(int argc, char **argv)
                        }
                        break;
                case 'i':
-                       opt_infile = optarg;
+                       inp->filename = optarg;
+                       break;
+               case 'I':
+                       /* First arg is the format name, others are options. */
+                       if (!inp->format) {
+                               inp->format = optarg;
+                               break;
+                       }
+                       inp->opt_list = g_slist_append(inp->opt_list, optarg);
                        break;
                case 'O':
                        opstr = g_strsplit(optarg, ":", 0);
@@ -904,7 +1185,7 @@ int main(int argc, char **argv)
                                usage(NULL);
                        }
                        if (opstr[2])
-                               op->class = g_strdup(opstr[2]);
+                               op->class_ = g_strdup(opstr[2]);
                        g_strfreev(opstr);
                        break;
                case 'f':
@@ -925,7 +1206,7 @@ int main(int argc, char **argv)
                usage(NULL);
        if (g_slist_length(pdlist) == 0)
                usage(NULL);
-       if (!opt_infile)
+       if (!inp->filename)
                usage(NULL);
        if (!op->pd || op->type == -1)
                usage(NULL);
@@ -949,7 +1230,7 @@ int main(int argc, char **argv)
        }
 
        ret = 0;
-       if (!run_testcase(opt_infile, pdlist, op))
+       if (!run_testcase(inp, pdlist, op))
                ret = 1;
 
        if (coverage) {