From 2cb12f959446b6164bf31e67cc15f07365916faa Mon Sep 17 00:00:00 2001 From: Hans-Christoph Steiner Date: Tue, 9 May 2023 18:15:10 +0200 Subject: [PATCH] metadata: break out write_yaml to standalone function and add unit tests --- fdroidserver/metadata.py | 164 ++++++++++++++++++++------------------- tests/metadata.TestCase | 43 ++++++++++ 2 files changed, 127 insertions(+), 80 deletions(-) diff --git a/fdroidserver/metadata.py b/fdroidserver/metadata.py index b86dd004..6a25a5cc 100644 --- a/fdroidserver/metadata.py +++ b/fdroidserver/metadata.py @@ -1044,6 +1044,89 @@ def _del_duplicated_NoSourceSince(app): del app['AntiFeatures'][key] +def _field_to_yaml(typ, value): + """Convert data to YAML 1.2 format that keeps the right TYPE_*.""" + if typ == TYPE_STRING: + return str(value) + elif typ == TYPE_INT: + return int(value) + elif typ == TYPE_MULTILINE: + if '\n' in value: + return ruamel.yaml.scalarstring.preserve_literal(str(value)) + else: + return str(value) + elif typ == TYPE_SCRIPT: + if type(value) == list: + if len(value) == 1: + return value[0] + else: + return value + else: + return value + + +def _builds_to_yaml(app): + builds = ruamel.yaml.comments.CommentedSeq() + for build in app.get('Builds', []): + if not isinstance(build, Build): + build = Build(build) + b = ruamel.yaml.comments.CommentedMap() + for field in build_flags: + if hasattr(build, field): + value = getattr(build, field) + if field == 'gradle' and value == ['off']: + value = [ + ruamel.yaml.scalarstring.SingleQuotedScalarString('off') + ] + typ = flagtype(field) + # don't check value == True for TYPE_INT as it could be 0 + if value is not None and (typ == TYPE_INT or value): + b.update({field: _field_to_yaml(typ, value)}) + builds.append(b) + + # insert extra empty lines between build entries + for i in range(1, len(builds)): + builds.yaml_set_comment_before_after_key(i, 'bogus') + builds.ca.items[i][1][-1].value = '\n' + + return builds + + +def _app_to_yaml(app): + cm = ruamel.yaml.comments.CommentedMap() + insert_newline = False + for field in yaml_app_field_order: + if field == '\n': + # next iteration will need to insert a newline + insert_newline = True + else: + if app.get(field) or field == 'Builds': + if field == 'Builds': + if app.get('Builds'): + cm.update({field: _builds_to_yaml(app)}) + elif field == 'CurrentVersionCode': + cm.update({field: _field_to_yaml(TYPE_INT, getattr(app, field))}) + elif field == 'AllowedAPKSigningKeys': + value = getattr(app, field) + if value: + value = [str(i).lower() for i in value] + if len(value) == 1: + cm.update({field: _field_to_yaml(TYPE_STRING, value[0])}) + else: + cm.update({field: _field_to_yaml(TYPE_LIST, value)}) + else: + cm.update({field: _field_to_yaml(fieldtype(field), getattr(app, field))}) + + if insert_newline: + # we need to prepend a newline in front of this field + insert_newline = False + # inserting empty lines is not supported so we add a + # bogus comment and over-write its value + cm.yaml_set_comment_before_after_key(field, 'bogus') + cm.ca.items[field][1][-1].value = '\n' + return cm + + def write_yaml(mf, app): """Write metadata in yaml format. @@ -1053,87 +1136,8 @@ def write_yaml(mf, app): active file discriptor for writing app app metadata to written to the yaml file + """ - def _field_to_yaml(typ, value): - """Convert data to YAML 1.2 format that keeps the right TYPE_*.""" - if typ == TYPE_STRING: - return str(value) - elif typ == TYPE_INT: - return int(value) - elif typ == TYPE_MULTILINE: - if '\n' in value: - return ruamel.yaml.scalarstring.preserve_literal(str(value)) - else: - return str(value) - elif typ == TYPE_SCRIPT: - if type(value) == list: - if len(value) == 1: - return value[0] - else: - return value - else: - return value - - def _app_to_yaml(app): - cm = ruamel.yaml.comments.CommentedMap() - insert_newline = False - for field in yaml_app_field_order: - if field == '\n': - # next iteration will need to insert a newline - insert_newline = True - else: - if app.get(field) or field == 'Builds': - if field == 'Builds': - if app.get('Builds'): - cm.update({field: _builds_to_yaml(app)}) - elif field == 'CurrentVersionCode': - cm.update({field: _field_to_yaml(TYPE_INT, getattr(app, field))}) - elif field == 'AllowedAPKSigningKeys': - value = getattr(app, field) - if value: - value = [str(i).lower() for i in value] - if len(value) == 1: - cm.update({field: _field_to_yaml(TYPE_STRING, value[0])}) - else: - cm.update({field: _field_to_yaml(TYPE_LIST, value)}) - else: - cm.update({field: _field_to_yaml(fieldtype(field), getattr(app, field))}) - - if insert_newline: - # we need to prepend a newline in front of this field - insert_newline = False - # inserting empty lines is not supported so we add a - # bogus comment and over-write its value - cm.yaml_set_comment_before_after_key(field, 'bogus') - cm.ca.items[field][1][-1].value = '\n' - return cm - - def _builds_to_yaml(app): - builds = ruamel.yaml.comments.CommentedSeq() - for build in app.get('Builds', []): - if not isinstance(build, Build): - build = Build(build) - b = ruamel.yaml.comments.CommentedMap() - for field in build_flags: - if hasattr(build, field): - value = getattr(build, field) - if field == 'gradle' and value == ['off']: - value = [ - ruamel.yaml.scalarstring.SingleQuotedScalarString('off') - ] - typ = flagtype(field) - # don't check value == True for TYPE_INT as it could be 0 - if value is not None and (typ == TYPE_INT or value): - b.update({field: _field_to_yaml(typ, value)}) - builds.append(b) - - # insert extra empty lines between build entries - for i in range(1, len(builds)): - builds.yaml_set_comment_before_after_key(i, 'bogus') - builds.ca.items[i][1][-1].value = '\n' - - return builds - _del_duplicated_NoSourceSince(app) yaml_app = _app_to_yaml(app) yaml = ruamel.yaml.YAML() diff --git a/tests/metadata.TestCase b/tests/metadata.TestCase index a687714f..bfad7d28 100755 --- a/tests/metadata.TestCase +++ b/tests/metadata.TestCase @@ -1684,6 +1684,49 @@ class MetadataTest(unittest.TestCase): ), ) + def test_app_to_yaml_smokecheck(self): + self.assertTrue( + isinstance(metadata._app_to_yaml(dict()), ruamel.yaml.comments.CommentedMap) + ) + + def test_app_to_yaml_build_list_empty(self): + app = metadata.App({'Builds': [metadata.Build({'rm': []})]}) + self.assertEqual(dict(), metadata._app_to_yaml(app)['Builds'][0]) + + def test_app_to_yaml_build_list_string(self): + app = metadata.App({'Builds': [metadata.Build({'rm': 'one'})]}) + self.assertEqual({'rm': 'one'}, metadata._app_to_yaml(app)['Builds'][0]) + + def test_app_to_yaml_build_list_one(self): + app = metadata.App({'Builds': [metadata.Build({'rm': ['one']})]}) + self.assertEqual({'rm': ['one']}, metadata._app_to_yaml(app)['Builds'][0]) + + def test_app_to_yaml_build_list(self): + app = metadata.App({'Builds': [metadata.Build({'rm': ['b2', 'NO1']})]}) + self.assertEqual({'rm': ['b2', 'NO1']}, metadata._app_to_yaml(app)['Builds'][0]) + + def test_app_to_yaml_AllowedAPKSigningKeys_two(self): + cm = metadata._app_to_yaml(metadata.App({'AllowedAPKSigningKeys': ['b', 'A']})) + self.assertEqual(['b', 'a'], cm['AllowedAPKSigningKeys']) + + def test_app_to_yaml_AllowedAPKSigningKeys_one(self): + cm = metadata._app_to_yaml(metadata.App({'AllowedAPKSigningKeys': ['One']})) + self.assertEqual('one', cm['AllowedAPKSigningKeys']) + + def test_app_to_yaml_int_hex(self): + cm = metadata._app_to_yaml(metadata.App({'CurrentVersionCode': 0xFF})) + self.assertEqual(255, cm['CurrentVersionCode']) + + def test_app_to_yaml_int_underscore(self): + cm = metadata._app_to_yaml(metadata.App({'CurrentVersionCode': 1_2_3})) + self.assertEqual(123, cm['CurrentVersionCode']) + + def test_app_to_yaml_int_0(self): + """Document that 0 values fail to make it through.""" + # TODO it should be possible to use `CurrentVersionCode: 0` + cm = metadata._app_to_yaml(metadata.App({'CurrentVersionCode': 0})) + self.assertFalse('CurrentVersionCode' in cm) + class PostMetadataParseTest(unittest.TestCase): """Test the functions that post process the YAML input.