From 6bafb036eeda1e4111e39bde7ec9f9c6303fe2e0 Mon Sep 17 00:00:00 2001 From: linsui Date: Wed, 9 Jun 2021 15:46:52 +0800 Subject: [PATCH] lint.py: use pathlib and support Windows --- fdroidserver/common.py | 8 +- fdroidserver/lint.py | 464 ++++++++++++++++++++++++++--------------- tests/lint.TestCase | 64 +++--- 3 files changed, 334 insertions(+), 202 deletions(-) diff --git a/fdroidserver/common.py b/fdroidserver/common.py index 8d1a81c7..a832a7de 100644 --- a/fdroidserver/common.py +++ b/fdroidserver/common.py @@ -3983,9 +3983,7 @@ YAML_LINT_CONFIG = {'extends': 'default', def run_yamllint(path, indent=0): - - # TODO: Remove this - path = str(path) + path = Path(path) try: import yamllint.config import yamllint.linter @@ -3993,10 +3991,10 @@ def run_yamllint(path, indent=0): return '' result = [] - with open(path, 'r', encoding='utf-8') as f: + with path.open('r', encoding='utf-8') as f: problems = yamllint.linter.run(f, yamllint.config.YamlLintConfig(json.dumps(YAML_LINT_CONFIG))) for problem in problems: - result.append(' ' * indent + path + ':' + str(problem.line) + ': ' + problem.message) + result.append(' ' * indent + str(path) + ':' + str(problem.line) + ': ' + problem.message) return '\n'.join(result) diff --git a/fdroidserver/lint.py b/fdroidserver/lint.py index 82f3b84c..04b9fc0b 100644 --- a/fdroidserver/lint.py +++ b/fdroidserver/lint.py @@ -17,11 +17,11 @@ # along with this program. If not, see . from argparse import ArgumentParser -import glob -import os import re import sys +import platform import urllib.parse +from pathlib import Path from . import _ from . import common @@ -33,8 +33,12 @@ options = None def enforce_https(domain): - return (re.compile(r'^http://([^/]*\.)?' + re.escape(domain) + r'(/.*)?', re.IGNORECASE), - domain + " URLs should always use https://") + return ( + re.compile( + r'^http://([^/]*\.)?' + re.escape(domain) + r'(/.*)?', re.IGNORECASE + ), + domain + " URLs should always use https://", + ) https_enforcings = [ @@ -59,8 +63,10 @@ https_enforcings = [ def forbid_shortener(domain): - return (re.compile(r'https?://[^/]*' + re.escape(domain) + r'/.*'), - _("URL shorteners should not be used")) + return ( + re.compile(r'https?://[^/]*' + re.escape(domain) + r'/.*'), + _("URL shorteners should not be used"), + ) http_url_shorteners = [ @@ -119,70 +125,98 @@ http_url_shorteners = [ forbid_shortener('➡.ws'), ] -http_checks = https_enforcings + http_url_shorteners + [ - (re.compile(r'^(?!https?://)[^/]+'), - _("URL must start with https:// or http://")), - (re.compile(r'^https://(github|gitlab)\.com(/[^/]+){2,3}\.git'), - _("Appending .git is not necessary")), - (re.compile(r'^https://[^/]*(github|gitlab|bitbucket|rawgit|githubusercontent)\.[a-zA-Z]+/([^/]+/){2,3}master/'), - _("Use /HEAD instead of /master to point at a file in the default branch")), -] +http_checks = ( + https_enforcings + + http_url_shorteners + + [ + ( + re.compile(r'^(?!https?://)[^/]+'), + _("URL must start with https:// or http://"), + ), + ( + re.compile(r'^https://(github|gitlab)\.com(/[^/]+){2,3}\.git'), + _("Appending .git is not necessary"), + ), + ( + re.compile( + r'^https://[^/]*(github|gitlab|bitbucket|rawgit|githubusercontent)\.[a-zA-Z]+/([^/]+/){2,3}master/' + ), + _("Use /HEAD instead of /master to point at a file in the default branch"), + ), + ] +) regex_checks = { 'WebSite': http_checks, 'SourceCode': http_checks, 'Repo': https_enforcings, 'UpdateCheckMode': https_enforcings, - 'IssueTracker': http_checks + [ - (re.compile(r'.*github\.com/[^/]+/[^/]+/*$'), - _("/issues is missing")), - (re.compile(r'.*gitlab\.com/[^/]+/[^/]+/*$'), - _("/issues is missing")), + 'IssueTracker': http_checks + + [ + (re.compile(r'.*github\.com/[^/]+/[^/]+/*$'), _("/issues is missing")), + (re.compile(r'.*gitlab\.com/[^/]+/[^/]+/*$'), _("/issues is missing")), ], - 'Donate': http_checks + [ - (re.compile(r'.*flattr\.com'), - _("Flattr donation methods belong in the FlattrID: field")), - (re.compile(r'.*liberapay\.com'), - _("Liberapay donation methods belong in the Liberapay: field")), - (re.compile(r'.*opencollective\.com'), - _("OpenCollective donation methods belong in the OpenCollective: field")), + 'Donate': http_checks + + [ + ( + re.compile(r'.*flattr\.com'), + _("Flattr donation methods belong in the FlattrID: field"), + ), + ( + re.compile(r'.*liberapay\.com'), + _("Liberapay donation methods belong in the Liberapay: field"), + ), + ( + re.compile(r'.*opencollective\.com'), + _("OpenCollective donation methods belong in the OpenCollective: field"), + ), ], 'Changelog': http_checks, 'Author Name': [ - (re.compile(r'^\s'), - _("Unnecessary leading space")), - (re.compile(r'.*\s$'), - _("Unnecessary trailing space")), + (re.compile(r'^\s'), _("Unnecessary leading space")), + (re.compile(r'.*\s$'), _("Unnecessary trailing space")), ], 'Summary': [ - (re.compile(r'.*\b(free software|open source)\b.*', re.IGNORECASE), - _("No need to specify that the app is Free Software")), - (re.compile(r'.*((your|for).*android|android.*(app|device|client|port|version))', re.IGNORECASE), - _("No need to specify that the app is for Android")), - (re.compile(r'.*[a-z0-9][.!?]( |$)'), - _("Punctuation should be avoided")), - (re.compile(r'^\s'), - _("Unnecessary leading space")), - (re.compile(r'.*\s$'), - _("Unnecessary trailing space")), + ( + re.compile(r'.*\b(free software|open source)\b.*', re.IGNORECASE), + _("No need to specify that the app is Free Software"), + ), + ( + re.compile( + r'.*((your|for).*android|android.*(app|device|client|port|version))', + re.IGNORECASE, + ), + _("No need to specify that the app is for Android"), + ), + (re.compile(r'.*[a-z0-9][.!?]( |$)'), _("Punctuation should be avoided")), + (re.compile(r'^\s'), _("Unnecessary leading space")), + (re.compile(r'.*\s$'), _("Unnecessary trailing space")), ], - 'Description': https_enforcings + http_url_shorteners + [ - (re.compile(r'\s*[*#][^ .]'), - _("Invalid bulleted list")), - (re.compile(r'https://f-droid.org/[a-z][a-z](_[A-Za-z]{2,4})?/'), - _("Locale included in f-droid.org URL")), - (re.compile(r'^\s'), - _("Unnecessary leading space")), - (re.compile(r'.*\s$'), - _("Unnecessary trailing space")), - (re.compile(r'.*<(applet|base|body|button|embed|form|head|html|iframe|img|input|link|object|picture|script|source|style|svg|video).*', re.IGNORECASE), - _("Forbidden HTML tags")), - (re.compile(r'''.*\s+src=["']javascript:.*'''), - _("Javascript in HTML src attributes")), + 'Description': https_enforcings + + http_url_shorteners + + [ + (re.compile(r'\s*[*#][^ .]'), _("Invalid bulleted list")), + ( + re.compile(r'https://f-droid.org/[a-z][a-z](_[A-Za-z]{2,4})?/'), + _("Locale included in f-droid.org URL"), + ), + (re.compile(r'^\s'), _("Unnecessary leading space")), + (re.compile(r'.*\s$'), _("Unnecessary trailing space")), + ( + re.compile( + r'.*<(applet|base|body|button|embed|form|head|html|iframe|img|input|link|object|picture|script|source|style|svg|video).*', + re.IGNORECASE, + ), + _("Forbidden HTML tags"), + ), + ( + re.compile(r'''.*\s+src=["']javascript:.*'''), + _("Javascript in HTML src attributes"), + ), ], } -locale_pattern = re.compile(r'^[a-z]{2,3}(-[A-Z][A-Z])?$') +locale_pattern = re.compile(r"[a-z]{2,3}(-([A-Z][a-zA-Z]+|\d+|[a-z]+))*") def check_regexes(app): @@ -215,8 +249,7 @@ def get_lastbuild(builds): def check_update_check_data_url(app): - """UpdateCheckData must have a valid HTTPS URL to protect checkupdates runs - """ + """UpdateCheckData must have a valid HTTPS URL to protect checkupdates runs""" if app.UpdateCheckData and app.UpdateCheckMode == 'HTTP': urlcode, codeex, urlver, verex = app.UpdateCheckData.split('|') for url in (urlcode, urlver): @@ -229,33 +262,40 @@ def check_update_check_data_url(app): def check_vercode_operation(app): - if app.VercodeOperation and not common.VERCODE_OPERATION_RE.match(app.VercodeOperation): + if app.VercodeOperation and not common.VERCODE_OPERATION_RE.match( + app.VercodeOperation + ): yield _('Invalid VercodeOperation: {field}').format(field=app.VercodeOperation) def check_ucm_tags(app): lastbuild = get_lastbuild(app.get('Builds', [])) - if (lastbuild is not None - and lastbuild.commit - and app.UpdateCheckMode == 'RepoManifest' - and not lastbuild.commit.startswith('unknown') - and lastbuild.versionCode == app.CurrentVersionCode - and not lastbuild.forcevercode - and any(s in lastbuild.commit for s in '.,_-/')): - yield _("Last used commit '{commit}' looks like a tag, but UpdateCheckMode is '{ucm}'")\ - .format(commit=lastbuild.commit, ucm=app.UpdateCheckMode) + if ( + lastbuild is not None + and lastbuild.commit + and app.UpdateCheckMode == 'RepoManifest' + and not lastbuild.commit.startswith('unknown') + and lastbuild.versionCode == app.CurrentVersionCode + and not lastbuild.forcevercode + and any(s in lastbuild.commit for s in '.,_-/') + ): + yield _( + "Last used commit '{commit}' looks like a tag, but UpdateCheckMode is '{ucm}'" + ).format(commit=lastbuild.commit, ucm=app.UpdateCheckMode) def check_char_limits(app): limits = config['char_limits'] if len(app.Summary) > limits['summary']: - yield _("Summary of length {length} is over the {limit} char limit")\ - .format(length=len(app.Summary), limit=limits['summary']) + yield _("Summary of length {length} is over the {limit} char limit").format( + length=len(app.Summary), limit=limits['summary'] + ) if len(app.Description) > limits['description']: - yield _("Description of length {length} is over the {limit} char limit")\ - .format(length=len(app.Description), limit=limits['description']) + yield _("Description of length {length} is over the {limit} char limit").format( + length=len(app.Description), limit=limits['description'] + ) def check_old_links(app): @@ -272,8 +312,9 @@ def check_old_links(app): for f in ['WebSite', 'SourceCode', 'IssueTracker', 'Changelog']: v = app.get(f) if any(s in v for s in old_sites): - yield _("App is in '{repo}' but has a link to {url}")\ - .format(repo=app.Repo, url=v) + yield _("App is in '{repo}' but has a link to {url}").format( + repo=app.Repo, url=v + ) def check_useless_fields(app): @@ -286,8 +327,14 @@ filling_ucms = re.compile(r'^(Tags.*|RepoManifest.*)') def check_checkupdates_ran(app): if filling_ucms.match(app.UpdateCheckMode): - if not app.AutoName and not app.CurrentVersion and app.CurrentVersionCode == '0': - yield _("UpdateCheckMode is set but it looks like checkupdates hasn't been run yet") + if ( + not app.AutoName + and not app.CurrentVersion + and app.CurrentVersionCode == '0' + ): + yield _( + "UpdateCheckMode is set but it looks like checkupdates hasn't been run yet" + ) def check_empty_fields(app): @@ -295,25 +342,27 @@ def check_empty_fields(app): yield _("Categories are not set") -all_categories = set([ - "Connectivity", - "Development", - "Games", - "Graphics", - "Internet", - "Money", - "Multimedia", - "Navigation", - "Phone & SMS", - "Reading", - "Science & Education", - "Security", - "Sports & Health", - "System", - "Theming", - "Time", - "Writing", -]) +all_categories = set( + [ + "Connectivity", + "Development", + "Games", + "Graphics", + "Internet", + "Money", + "Multimedia", + "Navigation", + "Phone & SMS", + "Reading", + "Science & Education", + "Security", + "Sports & Health", + "System", + "Theming", + "Time", + "Writing", + ] +) def check_categories(app): @@ -376,7 +425,9 @@ def check_bulleted_lists(app): if line[0] == lchar and line[1] == ' ': lcount += 1 if lcount > 2 and lchar not in validchars: - yield _("Description has a list (%s) but it isn't bulleted (*) nor numbered (#)") % lchar + yield _( + "Description has a list (%s) but it isn't bulleted (*) nor numbered (#)" + ) % lchar break else: lchar = line[0] @@ -389,50 +440,61 @@ def check_builds(app): for build in app.get('Builds', []): if build.disable: if build.disable.startswith('Generated by import.py'): - yield _("Build generated by `fdroid import` - remove disable line once ready") + yield _( + "Build generated by `fdroid import` - remove disable line once ready" + ) continue for s in ['master', 'origin', 'HEAD', 'default', 'trunk']: if build.commit and build.commit.startswith(s): - yield _("Branch '{branch}' used as commit in build '{versionName}'")\ - .format(branch=s, versionName=build.versionName) + yield _( + "Branch '{branch}' used as commit in build '{versionName}'" + ).format(branch=s, versionName=build.versionName) for srclib in build.srclibs: if '@' in srclib: ref = srclib.split('@')[1].split('/')[0] if ref.startswith(s): - yield _("Branch '{branch}' used as commit in srclib '{srclib}'")\ - .format(branch=s, srclib=srclib) + yield _( + "Branch '{branch}' used as commit in srclib '{srclib}'" + ).format(branch=s, srclib=srclib) else: - yield _('srclibs missing name and/or @') + ' (srclibs: ' + srclib + ')' + yield _( + 'srclibs missing name and/or @' + ) + ' (srclibs: ' + srclib + ')' for key in build.keys(): if key not in supported_flags: yield _('%s is not an accepted build field') % key def check_files_dir(app): - dir_path = os.path.join('metadata', app.id) - if not os.path.isdir(dir_path): + dir_path = Path('metadata') / app.id + if not dir_path.is_dir(): return files = set() - for name in os.listdir(dir_path): - path = os.path.join(dir_path, name) - if not (os.path.isfile(path) or name == 'signatures' or locale_pattern.match(name)): + for path in dir_path.iterdir(): + name = path.name + if not ( + path.is_file() or name == 'signatures' or locale_pattern.fullmatch(name) + ): yield _("Found non-file at %s") % path continue files.add(name) - used = {'signatures', } + used = { + 'signatures', + } for build in app.get('Builds', []): for fname in build.patch: if fname not in files: - yield _("Unknown file '{filename}' in build '{versionName}'")\ - .format(filename=fname, versionName=build.versionName) + yield _("Unknown file '{filename}' in build '{versionName}'").format( + filename=fname, versionName=build.versionName + ) else: used.add(fname) for name in files.difference(used): - if locale_pattern.match(name): + if locale_pattern.fullmatch(name): continue - yield _("Unused file at %s") % os.path.join(dir_path, name) + yield _("Unused file at %s") % (dir_path / name) def check_format(app): @@ -446,41 +508,49 @@ def check_license_tag(app): return if app.License not in config['lint_licenses']: if config['lint_licenses'] == APPROVED_LICENSES: - yield _('Unexpected license tag "{}"! Only use FSF or OSI ' - 'approved tags from https://spdx.org/license-list') \ - .format(app.License) + yield _( + 'Unexpected license tag "{}"! Only use FSF or OSI ' + 'approved tags from https://spdx.org/license-list' + ).format(app.License) else: - yield _('Unexpected license tag "{}"! Only use license tags ' - 'configured in your config file').format(app.License) + yield _( + 'Unexpected license tag "{}"! Only use license tags ' + 'configured in your config file' + ).format(app.License) def check_extlib_dir(apps): - dir_path = os.path.join('build', 'extlib') - unused_extlib_files = set() - for root, dirs, files in os.walk(dir_path): - for name in files: - unused_extlib_files.add(os.path.join(root, name)[len(dir_path) + 1:]) + dir_path = Path('build/extlib') + extlib_files = set() + for path in dir_path.glob('**/*'): + if path.is_file(): + extlib_files.add(path.relative_to(dir_path)) used = set() for app in apps: for build in app.get('Builds', []): for path in build.extlibs: - if path not in unused_extlib_files: - yield _("{appid}: Unknown extlib {path} in build '{versionName}'")\ - .format(appid=app.id, path=path, versionName=build.versionName) + if path not in extlib_files: + yield _( + "{appid}: Unknown extlib {path} in build '{versionName}'" + ).format(appid=app.id, path=path, versionName=build.versionName) else: used.add(path) - for path in unused_extlib_files.difference(used): - if any(path.endswith(s) for s in [ - '.gitignore', - 'source.txt', 'origin.txt', 'md5.txt', - 'LICENSE', 'LICENSE.txt', - 'COPYING', 'COPYING.txt', - 'NOTICE', 'NOTICE.txt', - ]): - continue - yield _("Unused extlib at %s") % os.path.join(dir_path, path) + for path in extlib_files.difference(used): + if path.name not in [ + '.gitignore', + 'source.txt', + 'origin.txt', + 'md5.txt', + 'LICENSE', + 'LICENSE.txt', + 'COPYING', + 'COPYING.txt', + 'NOTICE', + 'NOTICE.txt', + ]: + yield _("Unused extlib at %s") % (dir_path / path) def check_app_field_types(app): @@ -493,39 +563,69 @@ def check_app_field_types(app): continue elif field == 'Builds': if not isinstance(v, list): - yield(_("{appid}: {field} must be a '{type}', but it is a '{fieldtype}'!") - .format(appid=app.id, field=field, - type='list', fieldtype=v.__class__.__name__)) + yield ( + _( + "{appid}: {field} must be a '{type}', but it is a '{fieldtype}'!" + ).format( + appid=app.id, + field=field, + type='list', + fieldtype=v.__class__.__name__, + ) + ) elif t == metadata.TYPE_LIST and not isinstance(v, list): - yield(_("{appid}: {field} must be a '{type}', but it is a '{fieldtype}!'") - .format(appid=app.id, field=field, - type='list', fieldtype=v.__class__.__name__)) + yield ( + _( + "{appid}: {field} must be a '{type}', but it is a '{fieldtype}!'" + ).format( + appid=app.id, + field=field, + type='list', + fieldtype=v.__class__.__name__, + ) + ) elif t == metadata.TYPE_STRING and not type(v) in (str, bool, dict): - yield(_("{appid}: {field} must be a '{type}', but it is a '{fieldtype}'!") - .format(appid=app.id, field=field, - type='str', fieldtype=v.__class__.__name__)) + yield ( + _( + "{appid}: {field} must be a '{type}', but it is a '{fieldtype}'!" + ).format( + appid=app.id, + field=field, + type='str', + fieldtype=v.__class__.__name__, + ) + ) def check_for_unsupported_metadata_files(basedir=""): """Checks whether any non-metadata files are in metadata/""" - + basedir = Path(basedir) global config + if not (basedir / 'metadata').exists(): + return False return_value = False - for f in glob.glob(basedir + 'metadata/*') + glob.glob(basedir + 'metadata/.*'): - if os.path.isdir(f): - if not os.path.exists(f + '.yml'): + for f in (basedir / 'metadata').iterdir(): + if f.is_dir(): + if not Path(str(f) + '.yml').exists(): print(_('"%s/" has no matching metadata file!') % f) return_value = True - elif f.endswith('.yml'): - packageName = os.path.splitext(os.path.basename(f))[0] + elif f.suffix == '.yml': + packageName = f.stem if not common.is_valid_package_name(packageName): - print('"' + packageName + '" is an invalid package name!\n' - + 'https://developer.android.com/studio/build/application-id') + print( + '"' + + packageName + + '" is an invalid package name!\n' + + 'https://developer.android.com/studio/build/application-id' + ) return_value = True else: - print(_('"{path}" is not a supported file format (use: metadata/*.yml)') - .format(path=f.replace(basedir, ''))) + print( + _( + '"{path}" is not a supported file format (use: metadata/*.yml)' + ).format(path=f.relative_to(basedir)) + ) return_value = True return return_value @@ -556,8 +656,11 @@ def check_current_version_code(app): if active_builds == 0: return # all builds are disabled if cv is not None and int(cv) < min_versionCode: - yield(_('CurrentVersionCode {cv} is less than oldest build entry {versionCode}') - .format(cv=cv, versionCode=min_versionCode)) + yield ( + _( + 'CurrentVersionCode {cv} is less than oldest build entry {versionCode}' + ).format(cv=cv, versionCode=min_versionCode) + ) def main(): @@ -567,12 +670,25 @@ def main(): # Parse command line... parser = ArgumentParser() common.setup_global_opts(parser) - parser.add_argument("-f", "--format", action="store_true", default=False, - help=_("Also warn about formatting issues, like rewritemeta -l")) - parser.add_argument('--force-yamllint', action="store_true", default=False, - help=_("When linting the entire repository yamllint is disabled by default. " - "This option forces yamllint regardless.")) - parser.add_argument("appid", nargs='*', help=_("application ID of file to operate on")) + parser.add_argument( + "-f", + "--format", + action="store_true", + default=False, + help=_("Also warn about formatting issues, like rewritemeta -l"), + ) + parser.add_argument( + '--force-yamllint', + action="store_true", + default=False, + help=_( + "When linting the entire repository yamllint is disabled by default. " + "This option forces yamllint regardless." + ), + ) + parser.add_argument( + "appid", nargs='*', help=_("application ID of file to operate on") + ) metadata.add_metadata_arguments(parser) options = parser.parse_args() metadata.warnings_action = options.W @@ -586,7 +702,7 @@ def main(): anywarns = check_for_unsupported_metadata_files() apps_check_funcs = [] - if len(options.appid) == 0: + if not options.appid: # otherwise it finds tons of unused extlibs apps_check_funcs.append(check_extlib_dir) for check_func in apps_check_funcs: @@ -600,29 +716,37 @@ def main(): if options.force_yamllint: import yamllint # throw error if it is not installed + yamllint # make pyflakes ignore this # only run yamllint when linting individual apps. - if len(options.appid) > 0 or options.force_yamllint: + if options.appid or options.force_yamllint: # run yamllint on app metadata - ymlpath = os.path.join('metadata', appid + '.yml') - if os.path.isfile(ymlpath): + ymlpath = Path('metadata') / (appid + '.yml') + if ymlpath.is_file(): yamllintresult = common.run_yamllint(ymlpath) - if yamllintresult != '': + if yamllintresult: print(yamllintresult) # run yamllint on srclib metadata srclibs = set() for build in app.get('Builds', []): for srclib in build.srclibs: - srclibs.add(srclib) + name, _ref, _number, _subdir = common.parse_srclib_spec(srclib) + srclibs.add(name + '.yml') for srclib in srclibs: - name, ref, number, subdir = common.parse_srclib_spec(srclib) - srclibpath = os.path.join('srclibs', name + '.yml') - if os.path.isfile(srclibpath): + srclibpath = Path('srclibs') / srclib + if srclibpath.is_file(): + if platform.system() == 'Windows': + # Handle symlink on Windows + symlink = srclibpath.read_text() + if symlink in srclibs: + continue + elif (srclibpath.parent / symlink).is_file(): + srclibpath = srclibpath.parent / symlink yamllintresult = common.run_yamllint(srclibpath) - if yamllintresult != '': + if yamllintresult: print(yamllintresult) app_check_funcs = [ diff --git a/tests/lint.TestCase b/tests/lint.TestCase index 49ba9b6f..3cf6b857 100755 --- a/tests/lint.TestCase +++ b/tests/lint.TestCase @@ -2,7 +2,6 @@ # http://www.drdobbs.com/testing/unit-testing-with-python/240165163 -import inspect import logging import optparse import os @@ -10,13 +9,12 @@ import shutil import sys import tempfile import unittest +from pathlib import Path -localmodule = os.path.realpath( - os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..') -) -print('localmodule: ' + localmodule) +localmodule = Path(__file__).resolve().parent.parent +print('localmodule: ' + str(localmodule)) if localmodule not in sys.path: - sys.path.insert(0, localmodule) + sys.path.insert(0, str(localmodule)) import fdroidserver.common import fdroidserver.lint @@ -27,26 +25,34 @@ class LintTest(unittest.TestCase): '''fdroidserver/lint.py''' def setUp(self): - logging.basicConfig(level=logging.INFO) - self.basedir = os.path.join(localmodule, 'tests') - self.tmpdir = os.path.abspath(os.path.join(self.basedir, '..', '.testfiles')) - if not os.path.exists(self.tmpdir): - os.makedirs(self.tmpdir) - os.chdir(self.basedir) + logging.basicConfig(level=logging.DEBUG) + self.basedir = localmodule / 'tests' + self.tmpdir = localmodule / '.testfiles' + self.tmpdir.mkdir(exist_ok=True) + # TODO: Python3.6: Accepts a path-like object. + os.chdir(str(self.basedir)) def test_check_for_unsupported_metadata_files(self): self.assertTrue(fdroidserver.lint.check_for_unsupported_metadata_files()) - tmptestsdir = tempfile.mkdtemp(prefix=inspect.currentframe().f_code.co_name, - dir=self.tmpdir) - self.assertFalse(fdroidserver.lint.check_for_unsupported_metadata_files(tmptestsdir + '/')) - shutil.copytree(os.path.join(localmodule, 'tests', 'metadata'), - os.path.join(tmptestsdir, 'metadata'), - ignore=shutil.ignore_patterns('apk', 'dump', '*.json')) - self.assertFalse(fdroidserver.lint.check_for_unsupported_metadata_files(tmptestsdir + '/')) - with open(os.path.join(tmptestsdir, 'metadata', 'org.adaway.json'), 'w') as fp: - fp.write('placeholder') - self.assertTrue(fdroidserver.lint.check_for_unsupported_metadata_files(tmptestsdir + '/')) + with tempfile.TemporaryDirectory(dir=str(self.tmpdir)) as testdir: + testdir = Path(testdir) + self.assertFalse( + fdroidserver.lint.check_for_unsupported_metadata_files(testdir) + ) + # TODO: Python3.6: Accepts a path-like object. + shutil.copytree( + str(self.basedir / 'metadata'), + str(testdir / 'metadata'), + ignore=shutil.ignore_patterns('apk', 'dump', '*.json'), + ) + self.assertFalse( + fdroidserver.lint.check_for_unsupported_metadata_files(testdir) + ) + (testdir / 'metadata/org.adaway.json').write_text('placeholder') + self.assertTrue( + fdroidserver.lint.check_for_unsupported_metadata_files(testdir) + ) def test_forbidden_html_tags(self): config = dict() @@ -130,7 +136,9 @@ class LintTest(unittest.TestCase): fields = { 'AntiFeatures': { 'good': [ - ['KnownVuln', ], + [ + 'KnownVuln', + ], ['NonFreeNet', 'KnownVuln'], ], 'bad': [ @@ -140,7 +148,9 @@ class LintTest(unittest.TestCase): }, 'Categories': { 'good': [ - ['Sports & Health', ], + [ + 'Sports & Health', + ], ['Multimedia', 'Graphics'], ], 'bad': [ @@ -154,7 +164,9 @@ class LintTest(unittest.TestCase): ], 'bad': [ [], - ['nope', ], + [ + 'nope', + ], 29, ], }, @@ -320,8 +332,6 @@ class LintTest(unittest.TestCase): if __name__ == "__main__": - os.chdir(os.path.dirname(__file__)) - parser = optparse.OptionParser() parser.add_option( "-v",