def verify_state_dict(self, state_dict, curr_state, state_name_list): """Verifies a state dictionary.""" STATE_DICT_SCHEMA = [ ('content', list), ('param_changes', list), ('widget', dict)] self.verify_dict_keys_and_types(state_dict, STATE_DICT_SCHEMA) # TODO(sll): Change the following verification once we move 'content' # to become a parameter. CONTENT_ITEM_SCHEMA = [('type', basestring), ('value', basestring)] for content_item in state_dict['content']: self.verify_dict_keys_and_types(content_item, CONTENT_ITEM_SCHEMA) self.assertIn(content_item['type'], ['text', 'image', 'video']) PARAM_CHANGES_SCHEMA = [ ('name', basestring), ('values', list), ('obj_type', basestring)] for param_change in state_dict['param_changes']: self.verify_dict_keys_and_types(param_change, PARAM_CHANGES_SCHEMA) # TODO(sll): Test that the elements of 'values' are of the correct # type. WIDGET_SCHEMA = [ ('widget_id', basestring), ('params', dict), ('handlers', list), ('sticky', bool)] self.verify_dict_keys_and_types(state_dict['widget'], WIDGET_SCHEMA) for handler in state_dict['widget']['handlers']: HANDLER_SCHEMA = [('name', basestring), ('rules', list)] self.verify_dict_keys_and_types(handler, HANDLER_SCHEMA) # Check that the list of rules is non-empty. self.assertTrue(handler['rules']) for rule in handler['rules']: RULE_SCHEMA = [ ('dest', basestring), ('feedback', list), ('inputs', dict), ('name', basestring), ('param_changes', list) ] self.verify_dict_keys_and_types(rule, RULE_SCHEMA) # Check that the destination is a valid one. if rule['dest'] != feconf.END_DEST: self.assertIn(rule['dest'], state_name_list) # Check that there are no feedback-less self-loops. self.assertFalse( rule['dest'] == curr_state and not rule['feedback'] and not state_dict['widget']['sticky'], msg='State %s has a self-loop with no feedback. This is ' 'likely to frustrate the reader.' % curr_state) # TODO(sll): Does 'inputs' need any tests? # TODO(sll): Check that the name corresponds to a valid one # from the relevant exploration -- maybe. (Need to decide if # we allow initialization of parameters midway through states.) for param_change in state_dict['param_changes']: self.verify_dict_keys_and_types( param_change, PARAM_CHANGES_SCHEMA) # TODO(sll): Test that the elements of 'values' are of the # correct type. for wp_name, wp_value in state_dict['widget']['params'].iteritems(): self.assertTrue(isinstance(wp_name, basestring)) # Check that the parameter name is valid. widget_params = Widget.get(state_dict['widget']['widget_id']).params widget_param_names = [wp.name for wp in widget_params] self.assertIn( wp_name, widget_param_names, msg='Parameter %s is not a valid parameter for widget %s' % ( wp_name, state_dict['widget']['widget_id'])) # Get the object class used to normalize the value for this param. for wp in widget_params: if wp.name == wp_name: obj_class = get_object_class(wp.obj_type) self.assertIsNotNone(obj_class) break # Check that the parameter value has the correct type. obj_class.normalize(wp_value)
def test_default_widgets_are_valid(self): """Test the default widgets.""" widget_ids = os.listdir(os.path.join(feconf.SAMPLE_WIDGETS_DIR)) for widget_id in widget_ids: # Check that the widget_id name is valid. self.assertTrue(self.is_camel_cased(widget_id)) # Check that the widget directory exists. widget_dir = os.path.join(feconf.SAMPLE_WIDGETS_DIR, widget_id) self.assertTrue(os.path.isdir(widget_dir)) # In this directory there should only be a config.yaml file, an html # entry-point file, and (optionally) a directory named 'static'. dir_contents = os.listdir(widget_dir) self.assertLessEqual(len(dir_contents), 3) if len(dir_contents) == 3: self.assertIn('static', dir_contents) static_dir = os.path.join(widget_dir, 'static') self.assertTrue(os.path.isdir(static_dir)) config_filepath = os.path.join( widget_dir, '%s.config.yaml' % widget_id) html_entry_point = os.path.join(widget_dir, '%s.html' % widget_id) self.assertTrue(os.path.isfile(config_filepath)) self.assertTrue(os.path.isfile(html_entry_point)) # Read the widget configuration from config.yaml. with open(config_filepath) as f: widget_config = utils.dict_from_yaml(f.read().decode('utf-8')) WIDGET_CONFIG_SCHEMA = [ ('id', basestring), ('name', basestring), ('category', basestring), ('description', basestring), ('handlers', list), ('params', list) ] # Check that the configuration file contains the correct top-level # keys, and that these keys have the correct types. self.verify_dict_keys_and_types(widget_config, WIDGET_CONFIG_SCHEMA) # Check that the specified widget id is the same as the directory # name. self.assertTrue(widget_config['id'], widget_id) for handler in widget_config['handlers']: HANDLER_KEYS = ['name', 'classifier'] self.assertItemsEqual(HANDLER_KEYS, handler.keys()) self.assertTrue(isinstance(handler['name'], basestring)) # The default classifier may be None (e.g. in the case of the # 'Continue' widget). if handler['classifier'] is not None: self.assertTrue(isinstance( handler['classifier'], basestring)) # Check that the classifier directory exists. classifier_dir = os.path.join( 'data/classifiers', handler['classifier']) self.assertTrue( os.path.isdir(classifier_dir), msg='Classifier %s does not exist' % classifier_dir) for param in widget_config['params']: PARAM_KEYS = ['name', 'description', 'obj_type', 'values'] self.assertItemsEqual(PARAM_KEYS, param.keys()) self.assertTrue(isinstance(param['name'], basestring)) self.assertTrue(self.is_alphanumeric_string(param['name'])) self.assertTrue(isinstance(param['description'], basestring)) # Check that the default values have the correct types. obj_class = get_object_class(param['obj_type']) self.assertIsNotNone(obj_class) self.assertTrue(isinstance(param['values'], list)) for value in param['values']: obj_class.normalize(value)