From: Alicia Boya García Date: Sun, 28 Oct 2018 17:27:22 +0000 (+0000) Subject: New validate plugin: validateflow X-Git-Tag: 1.19.3~491^2~425 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=e96f2ca7140e895021b4b848d87f974255a5da7e;p=platform%2Fupstream%2Fgstreamer.git New validate plugin: validateflow validateflow can be used to check the buffers and events flowing through a custom pipeline match an expectation file. This can be used to test non-regular-playback use cases like demuxers handling adaptive streaming fragment pushing. This patch includes also new actions used for these cases: `appsrc-push`, `appsrc-eos` and `flush` (plus `checkpoint`, which is only available with validateflow). --- diff --git a/validate/gst/validate/gst-validate-override.h b/validate/gst/validate/gst-validate-override.h index a5edabc..a3454a6 100644 --- a/validate/gst/validate/gst-validate-override.h +++ b/validate/gst/validate/gst-validate-override.h @@ -90,6 +90,9 @@ GST_VALIDATE_API GstValidateOverride * gst_validate_override_new (void); void gst_validate_override_free (GstValidateOverride * override); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GstValidateOverride, gst_validate_override_free) + GST_VALIDATE_API void gst_validate_override_change_severity (GstValidateOverride * override, GstValidateIssueId issue_id, GstValidateReportLevel new_level); GST_VALIDATE_API diff --git a/validate/gst/validate/gst-validate-scenario.c b/validate/gst/validate/gst-validate-scenario.c index f6d9dee..9008dc1 100644 --- a/validate/gst/validate/gst-validate-scenario.c +++ b/validate/gst/validate/gst-validate-scenario.c @@ -2507,6 +2507,248 @@ _execute_emit_signal (GstValidateScenario * scenario, return TRUE; } +typedef GstFlowReturn (*ChainWrapperFunction) (GstPad * pad, GstObject * parent, + GstBuffer * buffer, gpointer * user_data, gboolean * remove_wrapper); + +typedef struct _ChainWrapperFunctionData +{ + GstPadChainFunction wrapped_chain_func; + gpointer wrapped_chain_data; + GDestroyNotify wrapped_chain_notify; + ChainWrapperFunction wrapper_function; + gpointer wrapper_function_user_data; +} ChainWrapperFunctionData; + +static GstFlowReturn +_pad_chain_wrapper (GstPad * pad, GstObject * parent, GstBuffer * buffer) +{ + ChainWrapperFunctionData *data = pad->chaindata; + GstFlowReturn ret; + gboolean remove_wrapper = FALSE; + + pad->chainfunc = data->wrapped_chain_func; + pad->chaindata = data->wrapped_chain_data; + pad->chainnotify = data->wrapped_chain_notify; + + ret = data->wrapper_function (pad, parent, buffer, + data->wrapper_function_user_data, &remove_wrapper); + + if (!remove_wrapper) { + /* The chain function may have changed during the calling (e.g. if it was + * a nested wrapper that decided to remove itself) so we need to update the + * wrapped function just in case. */ + data->wrapped_chain_func = pad->chainfunc; + data->wrapped_chain_data = pad->chaindata; + data->wrapped_chain_notify = pad->chainnotify; + + /* Restore the wrapper as chain function */ + pad->chainfunc = _pad_chain_wrapper; + pad->chaindata = data; + pad->chainnotify = g_free; + } else + g_free (data); + + return ret; +} + +static void +wrap_pad_chain_function (GstPad * pad, ChainWrapperFunction new_function, + gpointer user_data) +{ + ChainWrapperFunctionData *data = g_new (ChainWrapperFunctionData, 1); + data->wrapped_chain_func = pad->chainfunc; + data->wrapped_chain_data = pad->chaindata; + data->wrapped_chain_notify = pad->chainnotify; + data->wrapper_function = new_function; + data->wrapper_function_user_data = user_data; + + pad->chainfunc = _pad_chain_wrapper; + pad->chaindata = data; + pad->chainnotify = g_free; +} + +static GstFlowReturn +appsrc_push_chain_wrapper (GstPad * pad, GstObject * parent, GstBuffer * buffer, + gpointer * user_data, gboolean * remove_wrapper) +{ + GstValidateAction *action = (GstValidateAction *) user_data; + GstFlowReturn ret = pad->chainfunc (pad, parent, buffer); + gst_validate_action_set_done (action); + *remove_wrapper = TRUE; + return ret; +} + +static gboolean +structure_get_uint64_permissive (const GstStructure * structure, + const gchar * fieldname, guint64 * dest) +{ + const GValue *original; + GValue transformed = G_VALUE_INIT; + + original = gst_structure_get_value (structure, fieldname); + if (!original) + return FALSE; + + g_value_init (&transformed, G_TYPE_UINT64); + if (!g_value_transform (original, &transformed)) + return FALSE; + + *dest = g_value_get_uint64 (&transformed); + g_value_unset (&transformed); + return TRUE; +} + +static gint +_execute_appsrc_push (GstValidateScenario * scenario, + GstValidateAction * action) +{ + GstElement *target; + gchar *file_name; + gchar *file_contents; + gsize file_length; + GError *error = NULL; + GstBuffer *buffer; + guint64 offset = 0; + guint64 size = -1; + gint push_buffer_ret; + + target = _get_target_element (scenario, action); + if (target == NULL) { + gchar *structure_string = gst_structure_to_string (action->structure); + GST_VALIDATE_REPORT (scenario, SCENARIO_ACTION_EXECUTION_ERROR, + "No element found for action: %s", structure_string); + g_free (structure_string); + return GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED; + } + + file_name = + g_strdup (gst_structure_get_string (action->structure, "file-name")); + if (file_name == NULL) { + gchar *structure_string = gst_structure_to_string (action->structure); + GST_VALIDATE_REPORT (scenario, SCENARIO_ACTION_EXECUTION_ERROR, + "Missing file-name property: %s", structure_string); + g_free (structure_string); + return GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED; + } + + structure_get_uint64_permissive (action->structure, "offset", &offset); + structure_get_uint64_permissive (action->structure, "size", &size); + + g_file_get_contents (file_name, &file_contents, &file_length, &error); + if (error != NULL) { + gchar *structure_string = gst_structure_to_string (action->structure); + GST_VALIDATE_REPORT (scenario, SCENARIO_ACTION_EXECUTION_ERROR, + "Could not open file for action: %s. Error: %s", structure_string, + error->message); + g_free (structure_string); + return GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED; + } + buffer = gst_buffer_new_wrapped_full (GST_MEMORY_FLAG_READONLY, file_contents, + file_length, offset, (size == -1 ? file_length : size), NULL, g_free); + + { + const GValue *caps_value; + caps_value = gst_structure_get_value (action->structure, "caps"); + if (caps_value) + g_object_set (target, "caps", gst_value_get_caps (caps_value), NULL); + } + + /* We temporarily override the peer pad chain function to finish the action + * once the buffer chain actually ends. */ + { + GstPad *appsrc_pad = gst_element_get_static_pad (target, "src"); + GstPad *peer_pad = gst_pad_get_peer (appsrc_pad); + if (!peer_pad) { + gchar *structure_string = gst_structure_to_string (action->structure); + GST_VALIDATE_REPORT (scenario, SCENARIO_ACTION_EXECUTION_ERROR, + "Action failed, pad not linked: %s", structure_string); + g_free (structure_string); + return GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED; + } + + wrap_pad_chain_function (peer_pad, appsrc_push_chain_wrapper, action); + + gst_object_unref (appsrc_pad); + gst_object_unref (peer_pad); + } + + g_signal_emit_by_name (target, "push-buffer", buffer, &push_buffer_ret); + if (push_buffer_ret != GST_FLOW_OK) { + gchar *structure_string = gst_structure_to_string (action->structure); + GST_VALIDATE_REPORT (scenario, SCENARIO_ACTION_EXECUTION_ERROR, + "push-buffer signal failed in action: %s", structure_string); + g_free (structure_string); + return GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED; + } + + g_free (file_name); + gst_object_unref (target); + return GST_VALIDATE_EXECUTE_ACTION_ASYNC; +} + +static gint +_execute_appsrc_eos (GstValidateScenario * scenario, GstValidateAction * action) +{ + GstElement *target; + gint eos_ret; + + target = _get_target_element (scenario, action); + if (target == NULL) { + gchar *structure_string = gst_structure_to_string (action->structure); + GST_VALIDATE_REPORT (scenario, SCENARIO_ACTION_EXECUTION_ERROR, + "No element found for action: %s", structure_string); + g_free (structure_string); + return GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED; + } + + g_signal_emit_by_name (target, "end-of-stream", &eos_ret); + if (eos_ret != GST_FLOW_OK) { + gchar *structure_string = gst_structure_to_string (action->structure); + GST_VALIDATE_REPORT (scenario, SCENARIO_ACTION_EXECUTION_ERROR, + "Failed to emit end-of-stream signal for action: %s", structure_string); + g_free (structure_string); + return GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED; + } + + gst_object_unref (target); + return GST_VALIDATE_EXECUTE_ACTION_OK; +} + +static gint +_execute_flush (GstValidateScenario * scenario, GstValidateAction * action) +{ + GstElement *target; + GstEvent *event; + gboolean reset_time = TRUE; + + target = _get_target_element (scenario, action); + if (target == NULL) { + gchar *structure_string = gst_structure_to_string (action->structure); + GST_VALIDATE_REPORT (scenario, SCENARIO_ACTION_EXECUTION_ERROR, + "No element found for action: %s", structure_string); + g_free (structure_string); + return GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED; + } + + gst_structure_get_boolean (action->structure, "reset-time", &reset_time); + + event = gst_event_new_flush_start (); + if (!gst_element_send_event (target, event)) { + GST_VALIDATE_REPORT (scenario, SCENARIO_ACTION_EXECUTION_ERROR, + "FLUSH_START event was not handled"); + return GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED; + } + + event = gst_event_new_flush_stop (reset_time); + if (!gst_element_send_event (target, event)) { + GST_VALIDATE_REPORT (scenario, SCENARIO_ACTION_EXECUTION_ERROR, + "FLUSH_STOP event was not handled"); + return GST_VALIDATE_EXECUTE_ACTION_ERROR_REPORTED; + } + + return GST_VALIDATE_EXECUTE_ACTION_OK; +} + static GstValidateExecuteActionReturn _execute_disable_plugin (GstValidateScenario * scenario, GstValidateAction * action) @@ -4679,6 +4921,79 @@ init_scenarios (void) "Emits a signal to an element in the pipeline", GST_VALIDATE_ACTION_TYPE_NONE); + REGISTER_ACTION_TYPE ("appsrc-push", _execute_appsrc_push, + ((GstValidateActionParameter []) + { + { + .name = "target-element-name", + .description = "The name of the appsrc to push data on", + .mandatory = TRUE, + .types = "string" + }, + { + .name = "file-name", + .description = "Relative path to a file whose contents will be pushed as a buffer", + .mandatory = TRUE, + .types = "string" + }, + { + .name = "offset", + .description = "Offset within the file where the buffer will start", + .mandatory = FALSE, + .types = "uint64" + }, + { + .name = "size", + .description = "Number of bytes from the file that will be pushed as a buffer", + .mandatory = FALSE, + .types = "uint64" + }, + { + .name = "caps", + .description = "Caps for the buffer to be pushed", + .mandatory = FALSE, + .types = "caps" + }, + {NULL} + }), + "Queues a buffer from an appsrc and waits for it to be handled by downstream elements in the same streaming thread.", + GST_VALIDATE_ACTION_TYPE_NONE); + + REGISTER_ACTION_TYPE ("appsrc-eos", _execute_appsrc_eos, + ((GstValidateActionParameter []) + { + { + .name = "target-element-name", + .description = "The name of the appsrc to emit EOS on", + .mandatory = TRUE, + .types = "string" + }, + {NULL} + }), + "Queues a EOS event in an appsrc.", + GST_VALIDATE_ACTION_TYPE_NONE); + + REGISTER_ACTION_TYPE ("flush", _execute_flush, + ((GstValidateActionParameter []) + { + { + .name = "target-element-name", + .description = "The name of the appsrc to flush on", + .mandatory = TRUE, + .types = "string" + }, + { + .name = "reset-time", + .description = "Whether the flush should reset running time", + .mandatory = FALSE, + .types = "boolean", + .def = "TRUE" + }, + {NULL} + }), + "Sends FLUSH_START and FLUSH_STOP events.", + GST_VALIDATE_ACTION_TYPE_NONE); + REGISTER_ACTION_TYPE ("disable-plugin", _execute_disable_plugin, ((GstValidateActionParameter []) { diff --git a/validate/launcher/apps/gstvalidate.py b/validate/launcher/apps/gstvalidate.py index e71c394..75b0587 100644 --- a/validate/launcher/apps/gstvalidate.py +++ b/validate/launcher/apps/gstvalidate.py @@ -156,7 +156,7 @@ class FakeMediaDescriptor(MediaDescriptor): class GstValidatePipelineTestsGenerator(GstValidateTestsGenerator): def __init__(self, name, test_manager, pipeline_template=None, - pipelines_descriptions=None, valid_scenarios=[]): + pipelines_descriptions=None, valid_scenarios=None): """ @name: The name of the generator @pipeline_template: A template pipeline to be used to generate actual pipelines @@ -170,6 +170,7 @@ class GstValidatePipelineTestsGenerator(GstValidateTestsGenerator): @valid_scenarios: A list of scenario name that can be used with that generator """ + valid_scenarios = valid_scenarios or [] GstValidateTestsGenerator.__init__(self, name, test_manager) self._pipeline_template = pipeline_template self._pipelines_descriptions = [] @@ -185,7 +186,14 @@ class GstValidatePipelineTestsGenerator(GstValidateTestsGenerator): self._valid_scenarios = valid_scenarios @classmethod - def from_json(self, test_manager, json_file): + def from_json(cls, test_manager, json_file, extra_data=None): + """ + :param json_file: Path to a JSON file containing pipeline tests. + :param extra_data: Variables available for interpolation in validate + configs and scenario actions. + """ + if extra_data is None: + extra_data = {} with open(json_file, 'r') as f: descriptions = json.load(f) @@ -193,28 +201,56 @@ class GstValidatePipelineTestsGenerator(GstValidateTestsGenerator): pipelines_descriptions = [] for test_name, defs in descriptions.items(): tests_definition = {'name': test_name, 'pipeline': defs['pipeline']} + test_private_dir = os.path.join(test_manager.options.privatedir, + name, test_name) + + config_file = None + if 'config' in defs: + os.makedirs(test_private_dir, exist_ok=True) + config_file = os.path.join(test_private_dir, + test_name + '.config') + with open(config_file, 'w') as f: + f.write(cls._format_config_template(extra_data, + '\n'.join(defs['config']) + '\n', test_name)) + scenarios = [] - for scenario in defs['scenarios']: + for scenario in defs.get('scenarios', []): if isinstance(scenario, str): + # Path to a scenario file scenarios.append(scenario) else: + # Dictionary defining a new scenario in-line scenario_name = scenario_file = scenario['name'] actions = scenario.get('actions') if actions: - scenario_dir = os.path.join( - test_manager.options.privatedir, name, test_name) + os.makedirs(test_private_dir, exist_ok=True) scenario_file = os.path.join( - scenario_dir, scenario_name + '.scenario') - os.makedirs(scenario_dir, exist_ok=True) + test_private_dir, scenario_name + '.scenario') with open(scenario_file, 'w') as f: - f.write('\n'.join(actions) + '\n') + f.write('\n'.join(action % extra_data for action in actions) + '\n') scenarios.append(scenario_file) - tests_definition['extra_data'] = {'scenarios': scenarios} + tests_definition['extra_data'] = {'scenarios': scenarios, 'config_file': config_file} tests_definition['pipeline_data'] = {"config_path": os.path.dirname(json_file)} pipelines_descriptions.append(tests_definition) return GstValidatePipelineTestsGenerator(name, test_manager, pipelines_descriptions=pipelines_descriptions) + @classmethod + def _format_config_template(cls, extra_data, config_text, test_name): + # Variables available for interpolation inside config blocks. + + extra_vars = extra_data.copy() + + if 'validate-flow-expectations-dir' in extra_vars and \ + 'validate-flow-actual-results-dir' in extra_vars: + expectations_dir = os.path.join(extra_vars['validate-flow-expectations-dir'], + test_name.replace('.', os.sep)) + actual_results_dir = os.path.join(extra_vars['validate-flow-actual-results-dir'], + test_name.replace('.', os.sep)) + extra_vars['validateflow'] = "validateflow, expectations-dir=\"%s\", actual-results-dir=\"%s\"" % (expectations_dir, actual_results_dir) + + return config_text % extra_vars + def get_fname(self, scenario, protocol=None, name=None): if name is None: name = self.name @@ -245,7 +281,16 @@ class GstValidatePipelineTestsGenerator(GstValidateTestsGenerator): extra_data = description.get('extra_data', {}) pipeline_data = description.get('pipeline_data', {}) - for scenario in extra_data.get('scenarios', scenarios): + if 'scenarios' in extra_data: + # A pipeline description can override the default scenario set. + # The pipeline description may specify an empty list of + # scenarios, in which case one test will be generated with no + # scenario. + scenarios_to_iterate = extra_data['scenarios'] or [None] + else: + scenarios_to_iterate = scenarios + + for scenario in scenarios_to_iterate: if isinstance(scenario, str): scenario = self.test_manager.scenarios_manager.get_scenario( scenario) @@ -255,7 +300,8 @@ class GstValidatePipelineTestsGenerator(GstValidateTestsGenerator): continue if self.test_manager.options.mute: - needs_clock = scenario.needs_clock_sync() + needs_clock = scenario.needs_clock_sync() \ + if scenario else False audiosink = self.get_fakesink_for_media_type( "audio", needs_clock) videosink = self.get_fakesink_for_media_type( @@ -272,15 +318,17 @@ class GstValidatePipelineTestsGenerator(GstValidateTestsGenerator): expected_failures = extra_data.get("expected-failures") extra_env_vars = extra_data.get("extra_env_vars") - self.add_test(GstValidateLaunchTest(fname, - self.test_manager.options, - self.test_manager.reporter, - pipeline_desc, - scenario=scenario, - media_descriptor=mediainfo, - expected_failures=expected_failures, - extra_env_variables=extra_env_vars) - ) + test = GstValidateLaunchTest(fname, + self.test_manager.options, + self.test_manager.reporter, + pipeline_desc, + scenario=scenario, + media_descriptor=mediainfo, + expected_failures=expected_failures, + extra_env_variables=extra_env_vars) + if extra_data.get('config_file'): + test.add_validate_config(extra_data['config_file']) + self.add_test(test) class GstValidatePlaybinTestsGenerator(GstValidatePipelineTestsGenerator): @@ -377,7 +425,10 @@ class GstValidatePlaybinTestsGenerator(GstValidatePipelineTestsGenerator): class GstValidateMixerTestsGenerator(GstValidatePipelineTestsGenerator): def __init__(self, name, test_manager, mixer, media_type, converter="", - num_sources=3, mixed_srcs={}, valid_scenarios=[]): + num_sources=3, mixed_srcs=None, valid_scenarios=None): + mixed_srcs = mixed_srcs or {} + valid_scenarios = valid_scenarios or [] + pipe_template = "%(mixer)s name=_mixer ! " + \ converter + " ! %(sink)s " self.converter = converter diff --git a/validate/launcher/baseclasses.py b/validate/launcher/baseclasses.py index 21d418d..419b025 100644 --- a/validate/launcher/baseclasses.py +++ b/validate/launcher/baseclasses.py @@ -1520,10 +1520,11 @@ class _TestsLauncher(Loggable): for testsuite in self.options.testsuites: loaded = False wanted_test_manager = None - if hasattr(testsuite, "TEST_MANAGER"): - wanted_test_manager = testsuite.TEST_MANAGER - if not isinstance(wanted_test_manager, list): - wanted_test_manager = [wanted_test_manager] + # TEST_MANAGER has been set in _load_testsuites() + assert hasattr(testsuite, "TEST_MANAGER") + wanted_test_manager = testsuite.TEST_MANAGER + if not isinstance(wanted_test_manager, list): + wanted_test_manager = [wanted_test_manager] for tester in self.testers: if wanted_test_manager is not None and \ @@ -1763,11 +1764,8 @@ class _TestsLauncher(Loggable): return True def _run_tests(self): - cur_test_num = 0 - if not self.all_tests: - all_tests = self.list_tests() - self.all_tests = all_tests + self.all_tests = self.list_tests() self.total_num_tests = len(self.all_tests) if not sys.stdout.isatty(): printc("\nRunning %d tests..." % self.total_num_tests, color=Colors.HEADER) @@ -1806,8 +1804,8 @@ class _TestsLauncher(Loggable): current_test_num += 1 res = test.test_end() self.reporter.after_test(test) - if res != Result.PASSED and (self.options.forever or - self.options.fatal_error): + if res != Result.PASSED and (self.options.forever + or self.options.fatal_error): return False if self.start_new_job(tests_left): jobs_running += 1 diff --git a/validate/plugins/flow/formatting.c b/validate/plugins/flow/formatting.c new file mode 100644 index 0000000..96e28ca --- /dev/null +++ b/validate/plugins/flow/formatting.c @@ -0,0 +1,275 @@ +/* GStreamer + * + * Copyright (C) 2018-2019 Igalia S.L. + * Copyright (C) 2018 Metrological Group B.V. + * Author: Alicia Boya García + * + * formatting.c: Functions used by validateflow to get string + * representations of buffers. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include "formatting.h" + +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +typedef void (*Uint64Formatter) (gchar * dest, guint64 time); + +void +format_time (gchar * dest_str, guint64 time) +{ + if (GST_CLOCK_TIME_IS_VALID (time)) { + sprintf (dest_str, "%" GST_TIME_FORMAT, GST_TIME_ARGS (time)); + } else { + strcpy (dest_str, "none"); + } +} + +static void +format_number (gchar * dest_str, guint64 number) +{ + sprintf (dest_str, "%" G_GUINT64_FORMAT, number); +} + +gchar * +validate_flow_format_segment (const GstSegment * segment) +{ + Uint64Formatter uint64_format; + gchar *segment_str; + gchar *parts[7]; + GString *format; + gchar start_str[32], offset_str[32], stop_str[32], time_str[32], base_str[32], + position_str[32], duration_str[32]; + int parts_index = 0; + + uint64_format = + segment->format == GST_FORMAT_TIME ? format_time : format_number; + uint64_format (start_str, segment->start); + uint64_format (offset_str, segment->offset); + uint64_format (stop_str, segment->stop); + uint64_format (time_str, segment->time); + uint64_format (base_str, segment->base); + uint64_format (position_str, segment->position); + uint64_format (duration_str, segment->duration); + + format = g_string_new (gst_format_get_name (segment->format)); + format = g_string_ascii_up (format); + parts[parts_index++] = + g_strdup_printf ("format=%s, start=%s, offset=%s, stop=%s", format->str, + start_str, offset_str, stop_str); + if (segment->rate != 1.0) + parts[parts_index++] = g_strdup_printf ("rate=%f", segment->rate); + if (segment->applied_rate != 1.0) + parts[parts_index++] = + g_strdup_printf ("applied_rate=%f", segment->applied_rate); + if (segment->flags) + parts[parts_index++] = g_strdup_printf ("flags=0x%02x", segment->flags); + parts[parts_index++] = + g_strdup_printf ("time=%s, base=%s, position=%s", time_str, base_str, + position_str); + if (GST_CLOCK_TIME_IS_VALID (segment->duration)) + parts[parts_index++] = g_strdup_printf ("duration=%s", duration_str); + parts[parts_index] = NULL; + + segment_str = g_strjoinv (", ", parts); + + while (parts_index > 0) + g_free (parts[--parts_index]); + g_string_free (format, TRUE); + + return segment_str; +} + +static gboolean +structure_only_given_keys (GQuark field_id, GValue * value, + gpointer _keys_to_print) +{ + const gchar *const *keys_to_print = (const gchar * const *) _keys_to_print; + return (!keys_to_print + || g_strv_contains (keys_to_print, g_quark_to_string (field_id))); +} + +static void +gpointer_free (gpointer pointer_location) +{ + g_free (*(void **) pointer_location); +} + +gchar * +validate_flow_format_caps (const GstCaps * caps, + const gchar * const *keys_to_print) +{ + guint i; + GArray *structures_strv = g_array_new (TRUE, FALSE, sizeof (gchar *)); + gchar *caps_str; + + g_array_set_clear_func (structures_strv, gpointer_free); + + /* A single GstCaps can contain several caps structures (although only one is + * used in most cases). We will print them separated with spaces. */ + for (i = 0; i < gst_caps_get_size (caps); i++) { + GstStructure *structure = + gst_structure_copy (gst_caps_get_structure (caps, i)); + gchar *structure_str; + gst_structure_filter_and_map_in_place (structure, structure_only_given_keys, + (gpointer) keys_to_print); + structure_str = gst_structure_to_string (structure); + g_array_append_val (structures_strv, structure_str); + } + + caps_str = g_strjoinv (" ", (gchar **) structures_strv->data); + g_array_free (structures_strv, TRUE); + return caps_str; +} + + +static gchar * +buffer_get_flags_string (GstBuffer * buffer) +{ + GFlagsClass *flags_class = + G_FLAGS_CLASS (g_type_class_ref (gst_buffer_flags_get_type ())); + GstBufferFlags flags = GST_BUFFER_FLAGS (buffer); + GString *string = NULL; + + while (1) { + GFlagsValue *value = g_flags_get_first_value (flags_class, flags); + if (!value) + break; + + if (string == NULL) + string = g_string_new (NULL); + else + g_string_append (string, " "); + + g_string_append (string, value->value_nick); + flags &= ~value->value; + } + + return (string != NULL) ? g_string_free (string, FALSE) : NULL; +} + +/* Returns a newly-allocated string describing the metas on this buffer, or NULL */ +static gchar * +buffer_get_meta_string (GstBuffer * buffer) +{ + gpointer state = NULL; + GstMeta *meta; + GString *s = NULL; + + while ((meta = gst_buffer_iterate_meta (buffer, &state))) { + const gchar *desc = g_type_name (meta->info->type); + + if (s == NULL) + s = g_string_new (NULL); + else + g_string_append (s, ", "); + + g_string_append (s, desc); + } + + return (s != NULL) ? g_string_free (s, FALSE) : NULL; +} + +gchar * +validate_flow_format_buffer (GstBuffer * buffer) +{ + gchar *flags_str, *meta_str, *buffer_str; + gchar *buffer_parts[6]; + int buffer_parts_index = 0; + + if (GST_CLOCK_TIME_IS_VALID (buffer->dts)) { + gchar time_str[32]; + format_time (time_str, buffer->dts); + buffer_parts[buffer_parts_index++] = g_strdup_printf ("dts=%s", time_str); + } + + if (GST_CLOCK_TIME_IS_VALID (buffer->pts)) { + gchar time_str[32]; + format_time (time_str, buffer->pts); + buffer_parts[buffer_parts_index++] = g_strdup_printf ("pts=%s", time_str); + } + + if (GST_CLOCK_TIME_IS_VALID (buffer->duration)) { + gchar time_str[32]; + format_time (time_str, buffer->duration); + buffer_parts[buffer_parts_index++] = g_strdup_printf ("dur=%s", time_str); + } + + flags_str = buffer_get_flags_string (buffer); + if (flags_str) { + buffer_parts[buffer_parts_index++] = + g_strdup_printf ("flags=%s", flags_str); + } + + meta_str = buffer_get_meta_string (buffer); + if (meta_str) + buffer_parts[buffer_parts_index++] = g_strdup_printf ("meta=%s", meta_str); + + buffer_parts[buffer_parts_index] = NULL; + buffer_str = + buffer_parts_index > 0 ? g_strjoinv (", ", + buffer_parts) : g_strdup ("(empty)"); + + g_free (meta_str); + g_free (flags_str); + while (buffer_parts_index > 0) + g_free (buffer_parts[--buffer_parts_index]); + + return buffer_str; +} + +gchar * +validate_flow_format_event (GstEvent * event, gboolean allow_stream_id, + const gchar * const *caps_properties) +{ + const gchar *event_type; + gchar *structure_string; + gchar *event_string; + + event_type = gst_event_type_get_name (GST_EVENT_TYPE (event)); + + if (GST_EVENT_TYPE (event) == GST_EVENT_SEGMENT) { + const GstSegment *segment; + gst_event_parse_segment (event, &segment); + structure_string = validate_flow_format_segment (segment); + } else if (GST_EVENT_TYPE (event) == GST_EVENT_CAPS) { + GstCaps *caps; + gst_event_parse_caps (event, &caps); + structure_string = validate_flow_format_caps (caps, caps_properties); + } else if (!gst_event_get_structure (event)) { + structure_string = g_strdup ("(no structure)"); + } else { + GstStructure *printable = + gst_structure_copy (gst_event_get_structure (event)); + + if (GST_EVENT_TYPE (event) == GST_EVENT_STREAM_START && !allow_stream_id) + gst_structure_remove_fields (printable, "stream-id", NULL); + + structure_string = gst_structure_to_string (printable); + gst_structure_free (printable); + } + + event_string = g_strdup_printf ("%s: %s", event_type, structure_string); + g_free (structure_string); + return event_string; +} diff --git a/validate/plugins/flow/formatting.h b/validate/plugins/flow/formatting.h new file mode 100644 index 0000000..ea644c7 --- /dev/null +++ b/validate/plugins/flow/formatting.h @@ -0,0 +1,38 @@ +/* GStreamer + * + * Copyright (C) 2018-2019 Igalia S.L. + * Copyright (C) 2018 Metrological Group B.V. + * Author: Alicia Boya García + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef __GST_VALIDATE_FLOW_FORMATTING_H__ +#define __GST_VALIDATE_FLOW_FORMATTING_H__ + +#include + +void format_time(gchar* dest_str, guint64 time); + +gchar* validate_flow_format_segment (const GstSegment *segment); + +gchar* validate_flow_format_caps (const GstCaps* caps, const gchar * const *keys_to_print); + +gchar* validate_flow_format_buffer (GstBuffer *buffer); + +gchar* validate_flow_format_event (GstEvent *event, gboolean allow_stream_id, const gchar * const *caps_properties); + +#endif // __GST_VALIDATE_FLOW_FORMATTING_H__ diff --git a/validate/plugins/flow/gstvalidateflow.c b/validate/plugins/flow/gstvalidateflow.c new file mode 100644 index 0000000..b5dc99f --- /dev/null +++ b/validate/plugins/flow/gstvalidateflow.c @@ -0,0 +1,485 @@ +/* GStreamer + * + * Copyright (C) 2018-2019 Igalia S.L. + * Copyright (C) 2018 Metrological Group B.V. + * Author: Alicia Boya García + * + * gstvalidateflow.c: A plugin to record streams and match them to + * expectation files. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include +#include "../../gst/validate/validate.h" +#include "../../gst/validate/gst-validate-utils.h" +#include "../../gst/validate/gst-validate-report.h" +#include "formatting.h" +#include +#include +#include +#include + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#define VALIDATE_FLOW_MISMATCH g_quark_from_static_string ("validateflow::mismatch") + +typedef enum _ValidateFlowMode +{ + VALIDATE_FLOW_MODE_WRITING_EXPECTATIONS, + VALIDATE_FLOW_MODE_WRITING_ACTUAL_RESULTS +} ValidateFlowMode; + +typedef struct _ValidateFlowOverride +{ + GstValidateOverride parent; + + const gchar *pad_name; + gboolean record_buffers; + gchar *expectations_dir; + gchar *actual_results_dir; + gboolean error_writing_file; + gchar **caps_properties; + gboolean record_stream_id; + + gchar *expectations_file_path; + gchar *actual_results_file_path; + ValidateFlowMode mode; + + /* output_file will refer to the expectations file if it did not exist, + * or to the actual results file otherwise. */ + gchar *output_file_path; + FILE *output_file; + GMutex output_file_mutex; + +} ValidateFlowOverride; + +GList *all_overrides = NULL; + +static void validate_flow_override_finalize (GObject * object); +static void _runner_set (GObject * object, GParamSpec * pspec, + gpointer user_data); +static void runner_stopping (GstValidateRunner * runner, + ValidateFlowOverride * flow); + +#define VALIDATE_TYPE_FLOW_OVERRIDE validate_flow_override_get_type () +G_DECLARE_FINAL_TYPE (ValidateFlowOverride, validate_flow_override, + VALIDATE, FLOW_OVERRIDE, GstValidateOverride); +G_DEFINE_TYPE (ValidateFlowOverride, validate_flow_override, + GST_TYPE_VALIDATE_OVERRIDE); + +void +validate_flow_override_init (ValidateFlowOverride * self) +{ +} + +void +validate_flow_override_class_init (ValidateFlowOverrideClass * klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = validate_flow_override_finalize; + + g_assert (gst_validate_is_initialized ()); + + gst_validate_issue_register (gst_validate_issue_new + (VALIDATE_FLOW_MISMATCH, + "The recorded log does not match the expectation file.", + "The recorded log does not match the expectation file.", + GST_VALIDATE_REPORT_LEVEL_CRITICAL)); +} + +static void +validate_flow_override_vprintf (ValidateFlowOverride * flow, const char *format, + va_list ap) +{ + g_mutex_lock (&flow->output_file_mutex); + if (!flow->error_writing_file && vfprintf (flow->output_file, format, ap) < 0) { + GST_ERROR_OBJECT (flow, "Writing to file %s failed", + flow->output_file_path); + flow->error_writing_file = TRUE; + } + g_mutex_unlock (&flow->output_file_mutex); +} + +static void +validate_flow_override_printf (ValidateFlowOverride * flow, const char *format, + ...) +{ + va_list ap; + va_start (ap, format); + validate_flow_override_vprintf (flow, format, ap); + va_end (ap); +} + +static void +validate_flow_override_event_handler (GstValidateOverride * override, + GstValidateMonitor * pad_monitor, GstEvent * event) +{ + ValidateFlowOverride *flow = VALIDATE_FLOW_OVERRIDE (override); + gchar *event_string; + + if (flow->error_writing_file) + return; + + event_string = validate_flow_format_event (event, flow->record_stream_id, + (const gchar * const *) flow->caps_properties); + validate_flow_override_printf (flow, "event %s\n", event_string); + g_free (event_string); +} + +static void +validate_flow_override_buffer_handler (GstValidateOverride * override, + GstValidateMonitor * pad_monitor, GstBuffer * buffer) +{ + ValidateFlowOverride *flow = VALIDATE_FLOW_OVERRIDE (override); + gchar *buffer_str; + + if (flow->error_writing_file || !flow->record_buffers) + return; + + buffer_str = validate_flow_format_buffer (buffer); + validate_flow_override_printf (flow, "buffer: %s\n", buffer_str); + g_free (buffer_str); +} + +static gchar ** +parse_caps_properties_setting (const ValidateFlowOverride * flow, + GstStructure * config) +{ + const GValue *list; + gchar **parsed_list; + guint i, size; + + list = gst_structure_get_value (config, "caps-properties"); + if (!list) + return NULL; + + if (!GST_VALUE_HOLDS_LIST (list)) { + GST_ERROR_OBJECT (flow, + "caps-properties must have type list of string, e.g. caps-properties={ width, height };"); + return NULL; + } + + size = gst_value_list_get_size (list); + parsed_list = g_malloc_n (size + 1, sizeof (gchar *)); + for (i = 0; i < size; i++) + parsed_list[i] = g_value_dup_string (gst_value_list_get_value (list, i)); + parsed_list[i] = NULL; + return parsed_list; +} + +static ValidateFlowOverride * +validate_flow_override_new (GstStructure * config) +{ + ValidateFlowOverride *flow; + GstValidateOverride *override; + + flow = g_object_new (VALIDATE_TYPE_FLOW_OVERRIDE, NULL); + override = GST_VALIDATE_OVERRIDE (flow); + + /* pad: Name of the pad where flowing buffers and events will be monitorized. */ + flow->pad_name = gst_structure_get_string (config, "pad"); + if (!flow->pad_name) { + g_error ("pad property is mandatory, not found in %s", + gst_structure_to_string (config)); + } + + /* record-buffers: Whether buffers will be written to the expectation log. */ + flow->record_buffers = FALSE; + gst_structure_get_boolean (config, "record-buffers", &flow->record_buffers); + + /* caps-properties: Caps events can include many dfferent properties, but + * many of these may be irrelevant for some tests. If this option is set, + * only the listed properties will be written to the expectation log. */ + flow->caps_properties = parse_caps_properties_setting (flow, config); + + /* record-stream-id: stream-id's are often non reproducible (this is the case + * for basesrc, for instance). For this reason, they are omitted by default + * when recording a stream-start event. This setting allows to override that + * behavior. */ + flow->record_stream_id = FALSE; + gst_structure_get_boolean (config, "record-stream-id", + &flow->record_stream_id); + + /* expectations-dir: Path to the directory where the expectations will be + * written if they don't exist, relative to the current working directory. + * By default the current working directory is used. */ + flow->expectations_dir = + g_strdup (gst_structure_get_string (config, "expectations-dir")); + if (!flow->expectations_dir) + flow->expectations_dir = g_strdup ("."); + + /* actual-results-dir: Path to the directory where the events will be + * recorded. The expectation file will be compared to this. */ + flow->actual_results_dir = + g_strdup (gst_structure_get_string (config, "actual-results-dir")); + if (!flow->actual_results_dir) + flow->actual_results_dir = g_strdup ("."); + + { + gchar *expectations_file_name = + g_strdup_printf ("log-%s-expected", flow->pad_name); + gchar *actual_results_file_name = + g_strdup_printf ("log-%s-actual", flow->pad_name); + flow->expectations_file_path = + g_build_path (G_DIR_SEPARATOR_S, flow->expectations_dir, + expectations_file_name, NULL); + flow->actual_results_file_path = + g_build_path (G_DIR_SEPARATOR_S, flow->actual_results_dir, + actual_results_file_name, NULL); + g_free (expectations_file_name); + g_free (actual_results_file_name); + } + + if (g_file_test (flow->expectations_file_path, G_FILE_TEST_EXISTS)) { + flow->mode = VALIDATE_FLOW_MODE_WRITING_ACTUAL_RESULTS; + flow->output_file_path = g_strdup (flow->actual_results_file_path); + } else { + flow->mode = VALIDATE_FLOW_MODE_WRITING_EXPECTATIONS; + flow->output_file_path = g_strdup (flow->expectations_file_path); + gst_validate_printf (NULL, "Writing expectations file: %s\n", + flow->expectations_file_path); + } + + { + gchar *directory_path = g_path_get_dirname (flow->output_file_path); + if (g_mkdir_with_parents (directory_path, 0755) < 0) { + g_error ("Could not create directory tree: %s Reason: %s", + directory_path, g_strerror (errno)); + } + g_free (directory_path); + } + + flow->output_file = fopen (flow->output_file_path, "w"); + if (!flow->output_file) + g_error ("Could not open for writing: %s", flow->output_file_path); + + gst_validate_override_register_by_name (flow->pad_name, override); + + override->buffer_handler = validate_flow_override_buffer_handler; + override->event_handler = validate_flow_override_event_handler; + + g_signal_connect (flow, "notify::validate-runner", + G_CALLBACK (_runner_set), NULL); + + return flow; +} + +static void +_runner_set (GObject * object, GParamSpec * pspec, gpointer user_data) +{ + ValidateFlowOverride *flow = VALIDATE_FLOW_OVERRIDE (object); + GstValidateRunner *runner = + gst_validate_reporter_get_runner (GST_VALIDATE_REPORTER (flow)); + + g_signal_connect (runner, "stopping", G_CALLBACK (runner_stopping), flow); + gst_object_unref (runner); +} + +static void +run_diff (const gchar * expected_file, const gchar * actual_file) +{ + GError *error = NULL; + GSubprocess *process = + g_subprocess_new (G_SUBPROCESS_FLAGS_STDOUT_PIPE, &error, "diff", "-u", + "--", expected_file, actual_file, NULL); + gchar *stdout_text = NULL; + + g_subprocess_communicate_utf8 (process, NULL, NULL, &stdout_text, NULL, + &error); + if (!error) { + fprintf (stderr, "%s\n", stdout_text); + } else { + fprintf (stderr, "Cannot show more details, failed to run diff: %s", + error->message); + g_error_free (error); + } + + g_object_unref (process); + g_free (stdout); +} + +static const gchar * +_line_to_show (gchar ** lines, gsize i) +{ + if (lines[i] == NULL) { + return ""; + } else if (*lines[i] == '\0') { + if (lines[i + 1] != NULL) + /* skip blank lines for reporting purposes (e.g. before CHECKPOINT) */ + return lines[i + 1]; + else + /* last blank line in the file */ + return ""; + } else { + return lines[i]; + } +} + +static void +show_mismatch_error (ValidateFlowOverride * flow, gchar ** lines_expected, + gchar ** lines_actual, gsize line_index) +{ + const gchar *line_expected = _line_to_show (lines_expected, line_index); + const gchar *line_actual = _line_to_show (lines_actual, line_index); + + GST_VALIDATE_REPORT (flow, VALIDATE_FLOW_MISMATCH, + "Mismatch error in pad %s, line %" G_GSIZE_FORMAT + ". Expected:\n%s\nActual:\n%s\n", flow->pad_name, line_index + 1, + line_expected, line_actual); + + run_diff (flow->expectations_file_path, flow->actual_results_file_path); +} + +static void +runner_stopping (GstValidateRunner * runner, ValidateFlowOverride * flow) +{ + gchar **lines_expected, **lines_actual; + gsize i = 0; + + fclose (flow->output_file); + flow->output_file = NULL; + if (flow->mode == VALIDATE_FLOW_MODE_WRITING_EXPECTATIONS) + return; + + { + gchar *contents; + GError *error = NULL; + g_file_get_contents (flow->expectations_file_path, &contents, NULL, &error); + if (error) { + g_error ("Failed to open expectations file: %s Reason: %s", + flow->expectations_file_path, error->message); + } + lines_expected = g_strsplit (contents, "\n", 0); + } + + { + gchar *contents; + GError *error = NULL; + g_file_get_contents (flow->actual_results_file_path, &contents, NULL, + &error); + if (error) { + g_error ("Failed to open actual results file: %s Reason: %s", + flow->actual_results_file_path, error->message); + } + lines_actual = g_strsplit (contents, "\n", 0); + } + + for (i = 0; lines_expected[i] && lines_actual[i]; i++) { + if (strcmp (lines_expected[i], lines_actual[i])) { + show_mismatch_error (flow, lines_expected, lines_actual, i); + goto stop; + } + } + + if (!lines_expected[i] && lines_actual[i]) { + show_mismatch_error (flow, lines_expected, lines_actual, i); + } else if (lines_expected[i] && !lines_actual[i]) { + show_mismatch_error (flow, lines_expected, lines_actual, i); + } + +stop: + g_strfreev (lines_expected); + g_strfreev (lines_actual); +} + +static void +validate_flow_override_finalize (GObject * object) +{ + ValidateFlowOverride *flow = VALIDATE_FLOW_OVERRIDE (object); + + all_overrides = g_list_remove (all_overrides, flow); + g_free (flow->actual_results_dir); + g_free (flow->actual_results_file_path); + g_free (flow->expectations_dir); + g_free (flow->expectations_file_path); + g_free (flow->output_file_path); + if (flow->output_file) + fclose (flow->output_file); + if (flow->caps_properties) { + gchar **str_pointer; + for (str_pointer = flow->caps_properties; *str_pointer != NULL; + str_pointer++) + g_free (*str_pointer); + g_free (flow->caps_properties); + } + + G_OBJECT_CLASS (validate_flow_override_parent_class)->finalize (object); +} + +static gboolean +_execute_checkpoint (GstValidateScenario * scenario, GstValidateAction * action) +{ + GList *i; + gchar *checkpoint_name = + g_strdup (gst_structure_get_string (action->structure, "text")); + + for (i = all_overrides; i; i = i->next) { + ValidateFlowOverride *flow = (ValidateFlowOverride *) i->data; + + if (checkpoint_name) + validate_flow_override_printf (flow, "\nCHECKPOINT: %s\n\n", + checkpoint_name); + else + validate_flow_override_printf (flow, "\nCHECKPOINT\n\n"); + } + + g_free (checkpoint_name); + return TRUE; +} + +static gboolean +gst_validate_flow_init (GstPlugin * plugin) +{ + GList *tmp; + GList *config_list = gst_validate_plugin_get_config (plugin); + + if (!config_list) + return TRUE; + + for (tmp = config_list; tmp; tmp = tmp->next) { + GstStructure *config = tmp->data; + ValidateFlowOverride *flow = validate_flow_override_new (config); + all_overrides = g_list_append (all_overrides, flow); + } + +/* *INDENT-OFF* */ + gst_validate_register_action_type_dynamic (plugin, "checkpoint", + GST_RANK_PRIMARY, _execute_checkpoint, ((GstValidateActionParameter []) + { + { + .name = "text", + .description = "Text that will be logged in validateflow", + .mandatory = FALSE, + .types = "string" + }, + {NULL} + }), + "Prints a line of text in validateflow logs so that it's easy to distinguish buffers and events ocurring before or after a given action.", + GST_VALIDATE_ACTION_TYPE_NONE); +/* *INDENT-ON* */ + + return TRUE; +} + +GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, + GST_VERSION_MINOR, + validateflow, + "GstValidate plugin that records buffers and events on specified pads and matches the log with expectation files.", + gst_validate_flow_init, VERSION, "LGPL", GST_PACKAGE_NAME, + GST_PACKAGE_ORIGIN) diff --git a/validate/plugins/flow/meson.build b/validate/plugins/flow/meson.build new file mode 100644 index 0000000..701e9b2 --- /dev/null +++ b/validate/plugins/flow/meson.build @@ -0,0 +1,9 @@ +shared_library('gstvalidateflow', + 'gstvalidateflow.c', 'formatting.c', + include_directories : inc_dirs, + c_args: ['-DHAVE_CONFIG_H'], + install: true, + install_dir: validate_plugins_install_dir, + dependencies : [gst_dep, gst_pbutils_dep, gio_dep], + link_with : [gstvalidate] + ) diff --git a/validate/plugins/meson.build b/validate/plugins/meson.build index 7294686..488a1e2 100644 --- a/validate/plugins/meson.build +++ b/validate/plugins/meson.build @@ -2,6 +2,7 @@ subdir('fault_injection') subdir('gapplication') subdir('ssim') subdir('extra_checks') +subdir('flow') if gtk_dep.found() subdir('gtk')