#!/usr/bin/env python3 """One interpreter start for the whole of 99-python3-pathtools3. Emits `key=value' lines, one per line, in file order; the suite reads them back with grep. A python3 start on this arm is tens of seconds and every measurement here is a walk over a fixture tree of fourteen entries in a tmpfs directory, so the start is the entire cost of the suite and taking it once is the difference between a 30-second run and a fifteen-minute one. TWO RULES THE VALUES OBEY. The runner parses its input line by line, so no value may contain a newline. Every value below is a single line. And an EMPTY RESULT MUST BE DISTINGUISHABLE FROM AN ABSENT KEY. Half of this library's answers are empty -- an empty listing, a non-matching filter, the parent of "/" -- and `grep' followed by `cut' cannot tell "" from a key the probe never reached. So every string result is emitted WRAPPED IN ANGLE BRACKETS: `', and the empty string is `<>'. A refusal is `!' instead, and a case the probe skipped is simply missing, which reads as neither. THE FIXTURE IS BUILT BY THIS PROBE, in a fresh directory under $TMPDIR, and torn down at the end. Nothing here reads the rootfs, reaches a device, a peer or a terminal, so the suite is non-interactive by construction. ONE THING THIS PROBE MUST NEVER DO: call any walker with `followlinks=True' over the fixture, which contains a DELIBERATE symlink cycle. That is the one input that turns a bounded walk into an unbounded one, and a hang costs the whole batch its verify rather than turning one case red. Every call below leaves `followlinks' at its default False, and `cycle-not-followed' is the case that proves the default is what protects us. """ import os import sys emitted = [] def emit(key, value): emitted.append(key) print('%s=%s' % (key, value)) def call(fn, *a, **kw): """`' on success, `!' on a refusal. The exception NAME rather than its message: the messages are asserted separately, by the two cases that are ABOUT the message, and a release that reworded the rest would otherwise read as a behavioural change when nothing about the refusal had moved.""" try: return '<%s>' % (fn(*a, **kw),) except Exception as exc: return '!<%s>' % type(exc).__name__ def msg(fn, *a, **kw): """The exception's `args[0]', which is where both messages here land.""" try: fn(*a, **kw) return '' except Exception as exc: return '<%s>' % (exc.args[0] if exc.args else '',) # ------------------------------------------------------- the parent package -- # ORDER MATTERS IN THIS BLOCK. Importing `pathtools.path' BINDS `path' as an # attribute of the parent package, so the two "the parent exports nothing" # measurements below are only true before the submodule imports, and taking them # afterwards would quietly measure the opposite. try: import pathtools except Exception as exc: # pragma: no cover print('import_error=%s: %s' % (type(exc).__name__, exc)) sys.exit(1) emit('is_package', 'yes' if hasattr(pathtools, '__path__') else 'no') # `__init__.py' IS 1209 BYTES OF LICENCE HEADER AND NOTHING ELSE. It imports no # submodule and defines no name, so the package's own namespace is bare -- which # is why `import pathtools' alone is useless and every caller must name a # submodule. Both halves are asserted, because they fail differently: a release # that added `from .path import *' would flip the first, and one that merely set # `__version__' would flip only the second. emit('parent_has_version', 'yes' if hasattr(pathtools, '__version__') else 'no') emit('parent_has_path_attr', 'yes' if hasattr(pathtools, 'path') else 'no') emit('parent_has_patterns_attr', 'yes' if hasattr(pathtools, 'patterns') else 'no') emit('parent_public_names', '<%s>' % ','.join(sorted(n for n in dir(pathtools) if not n.startswith('_')))) from pathtools import path as ptpath, patterns as ptpat, version as ptver # ...and the attribute appears the moment the submodule is imported, which is # what makes the measurement above a statement about `__init__.py' rather than # about the import system. emit('parent_path_attr_after', 'yes' if hasattr(pathtools, 'path') else 'no') # ------------------------------------------------------------- identity ------ emit('path_module_file', '<%s>' % os.path.realpath(ptpath.__file__)) emit('patterns_module_file', '<%s>' % os.path.realpath(ptpat.__file__)) # `__version__' IS A TUPLE, NOT A STRING, and it lives on `pathtools.version' # rather than on the package. `"0.2.1" == pathtools.version.__version__' is # False and always was; the string lives beside it under a different name. emit('version_attr', '<%s>' % (ptver.__version__,)) emit('version_type', '<%s>' % type(ptver.__version__).__name__) emit('version_string', '<%s>' % ptver.VERSION_STRING) emit('version_info', '<%s>' % (ptver.VERSION_INFO,)) # THE IMPORT NAME IS NOT THE DISTRIBUTION NAME. The package installs as # `pathtools' and the distribution is `pathtools3', so a caller who asks # importlib.metadata for the name it imported gets a refusal. This is the fork's # whole packaging story -- `pathtools3' is the maintained fork of the abandoned # `pathtools', and it deliberately keeps the old import name so it can be dropped # in -- and it is why the two cannot be co-installed. try: from importlib.metadata import version as _dv emit('dist_version_pathtools3', '<%s>' % _dv('pathtools3')) except Exception as exc: emit('dist_version_pathtools3', '!<%s>' % type(exc).__name__) try: from importlib.metadata import version as _dv2 emit('dist_version_pathtools', '<%s>' % _dv2('pathtools')) except Exception as exc: emit('dist_version_pathtools', '!<%s>' % type(exc).__name__) # ------------------------------------------------------ the exported surface - # The two `__all__' lists are the contract, so they are asserted IN ORDER and by # count. `path' exports eight names and `patterns' four. emit('path_all', '<%s>' % ','.join(ptpath.__all__)) emit('path_all_count', len(ptpath.__all__)) emit('patterns_all', '<%s>' % ','.join(ptpat.__all__)) emit('patterns_all_count', len(ptpat.__all__)) for _n in ptpath.__all__: emit('path_has_%s' % _n, 'yes' if hasattr(ptpath, _n) else 'no') for _n in ptpat.__all__: emit('patterns_has_%s' % _n, 'yes' if hasattr(ptpat, _n) else 'no') # THERE IS NO `list_symlinks', AND THE TICKET THAT ORDERED THIS PORT NAMED ONE. # The surface this version ships lists symlinks through `list_files' (a broken # one) and `list_directories' (a symlink to a directory) rather than through a # function of its own. Asserted so that the gap is a measurement in the record # rather than a note somebody has to remember. emit('has_list_symlinks', 'yes' if hasattr(ptpath, 'list_symlinks') else 'no') # ============================================================================ # pathtools.patterns # ============================================================================ mp = ptpat.match_path mpa = ptpat.match_path_against map_ = ptpat.match_any_paths fp = ptpat.filter_paths PY = '/users/gorakhargosh/foobar.py' UP = '/users/gorakhargosh/FOOBAR.PY' # ---------------------------------------------------------- the defaults ----- # `included_patterns=None' becomes `["*"]' and `excluded_patterns=None' becomes # `[]', so a bare `match_path(p)' is True for EVERY path, including one that # names nothing on disk -- these functions never touch the filesystem. emit('mp_default', call(mp, PY)) emit('mp_default_insensitive', call(mp, PY, case_sensitive=False)) emit('mp_default_nonexistent', call(mp, '/no/such/thing.zzz')) emit('mp_default_empty_string', call(mp, '')) # ------------------------------------------------------ include / exclude ---- emit('mp_include_hit', call(mp, PY, ['*.py'])) emit('mp_include_miss', call(mp, PY, ['*.txt'])) emit('mp_include_one_of_many', call(mp, PY, ['*.txt', '*.py', '*.md'])) # THE EXCLUDE WINS. `_match_path' is `matches-included AND NOT matches-excluded', # so an exclude overrides an include that also matches -- and it does so even # when the two patterns are different spellings of the same set. emit('mp_exclude_overrides', call(mp, PY, ['*.py'], ['*foobar*'])) emit('mp_exclude_miss', call(mp, PY, ['*.py'], ['*.txt'])) emit('mp_exclude_only', call(mp, PY, None, ['*.py'])) # An empty INCLUDE list is not the same as None: `[]' matches nothing, so the # whole expression is False. A caller who passes a filtered-empty list expecting # "no restriction" gets the opposite of what they wanted. emit('mp_include_empty_list', call(mp, PY, [])) # ------------------------------------------------------- case sensitivity ---- emit('mp_case_sensitive_hit', call(mp, PY, ['*.py'], None, True)) emit('mp_case_sensitive_miss', call(mp, UP, ['*.py'], None, True)) emit('mp_case_insensitive_hit', call(mp, UP, ['*.py'], None, False)) emit('mp_case_insensitive_pattern', call(mp, PY, ['*.PY'], None, False)) emit('mpa_case_sensitive_miss', call(mpa, PY, ['*.PY'], True)) emit('mpa_case_insensitive_hit', call(mpa, PY, ['*.PY'], False)) # --------------------------------------------------- the conflict ValueError - # `_match_path' intersects the include and exclude SETS and refuses before it # matches anything. The refusal is therefore about the PATTERNS ALONE: it fires # for a pathname that matches neither, and for one that is not a path at all. emit('mp_conflict', call(mp, PY, ['*.py'], ['*.py'])) emit('mp_conflict_msg', msg(mp, PY, ['*.py'], ['*.py'])) emit('mp_conflict_irrelevant_path', call(mp, '/nothing/here.zzz', ['*.py'], ['*.py'])) # ...and under `case_sensitive=False' BOTH LISTS ARE LOWERCASED FIRST, so two # patterns that differ only in case become the same pattern and collide. This is # the library's own doctest, and it is the trap: the identical call is fine when # case-sensitive and raises when not, so turning the switch off on a working # call can break it. emit('mp_conflict_case_sensitive_ok', call(mp, UP, ['*.py'], ['*.PY'], True)) emit('mp_conflict_case_insensitive', call(mp, UP, ['*.py'], ['*.PY'], False)) emit('mp_conflict_case_msg', msg(mp, UP, ['*.py'], ['*.PY'], False)) # THE MESSAGE IS BUILT FROM `list(common_patterns)', WHICH IS A LIST OF A SET. # With ONE conflicting pattern that is deterministic and asserted verbatim above. # With TWO it is not: set iteration order for strings depends on PYTHONHASHSEED, # so the message can name them in either order from one interpreter start to the # next. What IS stable is the SET of names in it, so that is what is measured -- # and a suite that asserted the two-pattern message verbatim would be flaky by # construction, which is the point of measuring it this way instead. _m2 = msg(mp, PY, ['*.py', '*.md'], ['*.py', '*.md']) emit('mp_conflict2_names', '<%s>' % ','.join(sorted(x for x in ('*.py', '*.md') if x in _m2))) emit('mp_conflict2_is_valueerror', call(mp, PY, ['*.py', '*.md'], ['*.py', '*.md'])) # -------------------------------------- a bare STRING where a list belongs ---- # `match_path_against' iterates `set(patterns)'. Handed a STRING it iterates the # string's CHARACTERS, so `"*.txt"' becomes the four patterns `*', `.', `t', `x' # -- and `*' matches everything. A caller who forgets the brackets therefore # gets True for a path the pattern does not match, silently. emit('str_pattern_false_hit', call(mp, PY, '*.txt')) emit('str_pattern_no_star', call(mp, PY, 'zq')) # Worse: two DIFFERENT string patterns share the characters `*' and `.', so the # intersection is non-empty and the call raises "conflicting patterns" about # patterns the caller never wrote. emit('str_pattern_spurious_conflict', call(mp, PY, '*.py', '*.txt')) # ------------------------------------------------- filter_paths / any_paths -- PATHS = ['/z/last.py', '/a/first.py', '/m/mid.conf', '/q/skip.status', '/w/no.bin'] # `filter_paths' IS A GENERATOR and `match_any_paths' IS NOT. That single # difference decides WHEN the conflict ValueError above is raised: eagerly at the # call for `match_any_paths' and `match_path', and only at the first `next()' for # `filter_paths'. A caller who builds the generator inside a `try' and iterates # it outside catches nothing. emit('fp_is_generator', 'yes' if hasattr(fp(PATHS), '__next__') else 'no') emit('map_is_generator', 'yes' if hasattr(map_(PATHS), '__next__') else 'no') _g = fp(PATHS, ['*.py'], ['*.py']) # constructed, not iterated emit('fp_conflict_deferred', 'yes') # reaching this line IS the result emit('fp_conflict_on_iter', call(list, _g)) emit('map_conflict_eager', call(map_, PATHS, ['*.py'], ['*.py'])) # ORDER IS THE INPUT'S. `filter_paths' iterates `pathnames' in sequence, so a # list comes back in list order -- unlike the patterns, which go through a set. emit('fp_order', '<%s>' % ','.join(fp(PATHS, ['*.py', '*.conf']))) emit('fp_all_default', '<%s>' % ','.join(fp(PATHS))) emit('fp_exclude', '<%s>' % ','.join(fp(PATHS, ['*.py', '*.conf'], ['*first*']))) emit('fp_none_match', '<%s>' % ','.join(fp(PATHS, ['*.nope']))) emit('fp_empty_input', '<%s>' % ','.join(fp([], ['*.py']))) emit('map_hit', call(map_, PATHS, ['*.py'])) emit('map_miss', call(map_, PATHS, ['*.nope'])) emit('map_empty_input', call(map_, [], ['*'])) emit('map_default', call(map_, PATHS)) # DUPLICATE PATTERNS ARE DEDUPED, because the patterns go through `set()'. The # answer is unchanged; what is lost is the order, which is why no case here # asserts anything about which pattern matched. emit('mp_duplicate_patterns', call(mp, PY, ['*.py', '*.py', '*.py'])) # ============================================================================ # pathtools.path -- over a fixture this probe builds and tears down itself # ============================================================================ # # THE TREE. Fourteen entries, chosen so that every classification os.walk can # make is represented exactly once: # # a/ a directory with children # a/b/ a nested directory # a/b/deep.txt the only file two levels down # a/one.py } two extensions in one directory, so a pattern # a/two.TXT } filter has something to discriminate on # c/ an EMPTY directory -- walked, contributes no entry # top.py } two files at the top level, which is what the # top.md } non-recursive listings must return and no more # link_to_a -> a a symlink TO A DIRECTORY # broken -> ... a BROKEN symlink, pointing at a name that never existed # cyc/ a directory holding... # cyc/self -> ../cyc ...a symlink back to itself: a genuine CYCLE # # `os.walk' classifies by `is_dir()', which FOLLOWS symlinks, so `link_to_a' and # `cyc/self' land in `dirnames' while `broken' -- whose target cannot be # stat'ed -- lands in `filenames'. That is the whole of the symlink behaviour # here, and each half is asserted separately below. # # The cycle is safe ONLY because `followlinks' defaults to False. Nothing in # this probe passes True, and `walk_roots_recursive' is what proves the default # holds: a walker that followed links would recurse through `cyc/self' forever # and hang the guest. import shutil import tempfile T = os.path.abspath(tempfile.mkdtemp(prefix='pt3-')) K = os.path.join(T, '..', 'pt3-case-%d' % os.getpid()) K = os.path.abspath(K) def rel(p, base=None): return os.path.relpath(p, base or T) def rels(seq, base=None): """Sorted, comma-joined relative paths. SORTED because `os.walk' hands back whatever `scandir' yields, which is filesystem order and is not a promise on any of the three filesystems this image can present. The one place order IS asserted is the walk's own top-down/bottom-up sequence, and those two cases build their value without going through here.""" return '<%s>' % ','.join(sorted(rel(p, base) for p in seq)) try: os.makedirs(os.path.join(T, 'a', 'b')) os.makedirs(os.path.join(T, 'c')) os.makedirs(os.path.join(T, 'cyc')) for name, body in (('top.py', 'top\n'), ('top.md', '# top\n'), (os.path.join('a', 'one.py'), 'one\n'), (os.path.join('a', 'two.TXT'), 'two\n'), (os.path.join('a', 'b', 'deep.txt'), 'deep\n')): with open(os.path.join(T, name), 'w') as fh: fh.write(body) os.symlink('a', os.path.join(T, 'link_to_a')) os.symlink('no-such-target', os.path.join(T, 'broken')) os.symlink(os.path.join('..', 'cyc'), os.path.join(T, 'cyc', 'self')) emit('fixture_built', 'yes') except Exception as exc: emit('fixture_built', '!<%s>' % type(exc).__name__) # The fixture's own shape, measured with os.lstat rather than with the library -- # so that a red below can be told apart from a fixture that was never built the # way this probe believes. A guest whose filesystem refused a symlink would turn # these three red and every symlink case with them, which names the cause. emit('fx_link_is_symlink', 'yes' if os.path.islink(os.path.join(T, 'link_to_a')) else 'no') emit('fx_broken_is_symlink', 'yes' if os.path.islink(os.path.join(T, 'broken')) else 'no') emit('fx_broken_is_dangling', 'yes' if not os.path.exists(os.path.join(T, 'broken')) else 'no') emit('fx_cycle_is_symlink', 'yes' if os.path.islink(os.path.join(T, 'cyc', 'self')) else 'no') # --------------------------------------------------------------- walk -------- # EVERY DIRECTORY EXACTLY ONCE, AND THE CYCLE NOT ENTERED. Five roots: the top, # `a', `a/b', `c' and `cyc'. `link_to_a' and `cyc/self' are NOT among them -- # they are symlinks, and `followlinks' is False -- which is the same fact stated # twice because it is the fact the cycle depends on. _roots = [r for (r, d, f) in ptpath.walk(T)] emit('walk_roots_recursive', rels(_roots)) emit('walk_roots_count', len(_roots)) emit('walk_roots_unique', 'yes' if len(_roots) == len(set(_roots)) else 'no') emit('walk_entered_cycle', 'yes' if any(os.path.join('cyc', 'self') in rel(r) for r in _roots) else 'no') emit('walk_entered_link', 'yes' if any(rel(r).startswith('link_to_a') for r in _roots) else 'no') # TOP-DOWN AND BOTTOM-UP DIFFER ONLY IN ORDER, and the top is the marker: first # when topdown, last when not. Asserted as the FIRST and LAST root rather than # the whole sequence, because the order among siblings is still scandir's. emit('walk_topdown_first', '<%s>' % rel([r for (r, d, f) in ptpath.walk(T)][0])) emit('walk_bottomup_last', '<%s>' % rel([r for (r, d, f) in ptpath.walk(T, topdown=False)][-1])) emit('walk_bottomup_count', len([r for (r, d, f) in ptpath.walk(T, topdown=False)])) # NON-RECURSIVE IS EXACTLY ONE ROOT -- `get_dir_walker' yields `next(os.walk())' # and stops. _nr = [r for (r, d, f) in ptpath.walk(T, recursive=False)] emit('walk_nonrecursive_count', len(_nr)) emit('walk_nonrecursive_root', '<%s>' % rel(_nr[0]) if _nr else '<>') # An EMPTY directory is still walked: one root, no children either way. _ec = list(ptpath.walk(os.path.join(T, 'c'))) emit('walk_empty_dir_count', len(_ec)) emit('walk_empty_dir_entries', '<%s>' % ','.join(sorted(_ec[0][1] + _ec[0][2])) if _ec else '') # ------------------------------------------------------------- listings ------ emit('list_files_recursive', rels(ptpath.list_files(T))) emit('list_files_nonrecursive', rels(ptpath.list_files(T, recursive=False))) emit('list_dirs_recursive', rels(ptpath.list_directories(T))) emit('list_dirs_nonrecursive', rels(ptpath.list_directories(T, recursive=False))) emit('listdir_recursive', rels(ptpath.listdir(T))) emit('listdir_nonrecursive', rels(ptpath.listdir(T, recursive=False))) # THE SYMLINKS, STATED AS THE TWO CASES THEY ACTUALLY ARE. A broken symlink is # a FILE to this library and a symlink-to-directory is a DIRECTORY, because both # answers come from `is_dir()'. Neither is what a caller looking for "the # symlinks" would guess, and there is no `list_symlinks' to ask instead. _lf = set(rel(p) for p in ptpath.list_files(T)) _ld = set(rel(p) for p in ptpath.list_directories(T)) emit('broken_link_is_file', 'yes' if 'broken' in _lf else 'no') emit('broken_link_not_dir', 'yes' if 'broken' not in _ld else 'no') emit('dir_link_is_dir', 'yes' if 'link_to_a' in _ld else 'no') emit('dir_link_not_file', 'yes' if 'link_to_a' not in _lf else 'no') emit('dir_link_not_descended', 'yes' if not any(p.startswith('link_to_a' + os.sep) for p in _lf) else 'no') emit('cycle_link_is_dir', 'yes' if os.path.join('cyc', 'self') in _ld else 'no') # EVERY LISTING IS A GENERATOR, which is why a caller who wants a count must # realise it -- and why a caller who iterates twice gets nothing the second time. emit('list_files_is_generator', 'yes' if hasattr(ptpath.list_files(T), '__next__') else 'no') emit('walk_is_generator', 'yes' if hasattr(ptpath.walk(T), '__next__') else 'no') _once = ptpath.list_files(T) emit('generator_exhausts', '%d,%d' % (len(list(_once)), len(list(_once)))) # The listings return ABSOLUTE paths regardless of what they were handed, which # is the one thing `absolute_path' is used for internally. emit('listing_absolute', 'yes' if all(os.path.isabs(p) for p in ptpath.list_files(T)) else 'no') # THE LISTINGS DO NOT FILTER. There is no pattern argument anywhere in # `pathtools.path' -- combining the two halves of this package is the CALLER's # job, and `filter_paths' over `list_files' is how it is meant to be done. emit('listings_take_no_pattern', 'yes' if 'pattern' not in ptpath.list_files.__code__.co_varnames else 'no') emit('combined_filter', rels(ptpat.filter_paths(ptpath.list_files(T), ['*.py']))) # ============================================================================ # THE HEADLINE: A MISSING DIRECTORY IS SILENT WHEN RECURSIVE AND RAISES WHEN NOT # ============================================================================ # # `os.walk' swallows the error from a path it cannot list -- its `onerror' is # None -- and yields NOTHING. The recursive walker is `os.walk' itself, so it # comes back empty. The non-recursive walker is a GENERATOR that does # `yield next(os.walk(path))', and `next()' on an empty iterator raises # StopIteration INSIDE a generator, which PEP 479 turns into # # RuntimeError: generator raised StopIteration # # So the same missing path is an empty result one way and a RuntimeError the # other, and the RuntimeError names neither the path nor the real cause. A # caller who flips `recursive=False' to make a listing cheaper turns a silent # empty into a crash. # # The same divergence fires for any path `os.walk' declines to enumerate, and the # second instance below -- a walk rooted at a FILE -- is the one a caller is more # likely to reach by accident. MISSING = os.path.join(T, 'no-such-directory') AFILE = os.path.join(T, 'top.py') emit('missing_recursive', call(lambda: len(list(ptpath.walk(MISSING))))) emit('missing_nonrecursive', call(lambda: len(list(ptpath.walk(MISSING, recursive=False))))) emit('missing_nonrecursive_msg', msg(lambda: list(ptpath.walk(MISSING, recursive=False)))) emit('missing_files_recursive', call(lambda: len(list(ptpath.list_files(MISSING))))) emit('missing_files_nonrecursive', call(lambda: len(list(ptpath.list_files(MISSING, recursive=False))))) emit('file_root_recursive', call(lambda: len(list(ptpath.walk(AFILE))))) emit('file_root_nonrecursive', call(lambda: len(list(ptpath.walk(AFILE, recursive=False))))) # `get_dir_walker' IS PUBLIC -- it is in `__all__' -- so the divergence is # reachable directly, and it is asserted at that level too: the recursive walker # is a `functools.partial' over os.walk and the non-recursive one is a generator # function this module defines. emit('walker_recursive_type', '<%s>' % type(ptpath.get_dir_walker(True)).__name__) emit('walker_nonrecursive_type', '<%s>' % type(ptpath.get_dir_walker(False)).__name__) # ============================================================================ # CASE SENSITIVITY OVER A FIXTURE WHOSE NAMES DIFFER ONLY IN CASE # ============================================================================ # # Built in its OWN directory so it cannot perturb the listings above. The two # names differ only in case, so a case-INSENSITIVE filesystem would collapse them # into one file -- `case_fixture_count' is what says which kind of filesystem # this is, and it is measured rather than assumed because the guest's root and # its tmpfs need not agree. try: os.makedirs(K) for name in ('Case.TXT', 'case.txt'): with open(os.path.join(K, name), 'w') as fh: fh.write(name + '\n') emit('case_fixture_built', 'yes') except Exception as exc: emit('case_fixture_built', '!<%s>' % type(exc).__name__) emit('case_fixture_count', len(os.listdir(K)) if os.path.isdir(K) else -1) _kf = list(ptpath.list_files(K)) emit('case_listed', rels(_kf, K)) # CASE-SENSITIVE: `*.txt' matches only the lowercase one. CASE-INSENSITIVE: both. emit('case_sensitive_filter', '<%s>' % ','.join(sorted(rel(p, K) for p in ptpat.filter_paths(_kf, ['*.txt'], None, True)))) emit('case_insensitive_filter', '<%s>' % ','.join(sorted(rel(p, K) for p in ptpat.filter_paths(_kf, ['*.txt'], None, False)))) emit('case_sensitive_upper', '<%s>' % ','.join(sorted(rel(p, K) for p in ptpat.filter_paths(_kf, ['*.TXT'], None, True)))) # ============================================================================ # absolute_path / real_absolute_path / parent_dir_path # ============================================================================ # # `absolute_path' is `abspath(normpath(p))', and `abspath' already normpaths, so # the inner call is redundant -- harmless, and worth knowing when reading a # profile. What matters to a caller is that it is CWD-RELATIVE: a relative name # is resolved against wherever the process happens to be. os.chdir(T) emit('abs_of_relative', '<%s>' % rel(ptpath.absolute_path('a/b'))) emit('abs_of_dotted', '<%s>' % rel(ptpath.absolute_path('a/./b/../b'))) emit('abs_of_empty', 'yes' if ptpath.absolute_path('') == T else 'no') emit('abs_of_dot', 'yes' if ptpath.absolute_path('.') == T else 'no') emit('abs_is_idempotent', 'yes' if ptpath.absolute_path(ptpath.absolute_path('a')) == ptpath.absolute_path('a') else 'no') # `absolute_path' does NOT resolve symlinks and `real_absolute_path' does. That # is the only difference between them, and it is what a caller comparing two # paths for equality has to know: `link_to_a' and `a' are different strings from # one and the same string from the other. emit('abs_keeps_symlink', 'yes' if ptpath.absolute_path('link_to_a') != ptpath.absolute_path('a') else 'no') emit('real_resolves_symlink', 'yes' if ptpath.real_absolute_path('link_to_a') == ptpath.real_absolute_path('a') else 'no') # `parent_dir_path' IS `dirname' WITH A TRAILING-SLASH TRAP. `dirname("/a/b/")' # is "/a/b", so the "parent" of a directory written with a trailing slash is that # directory itself -- and the "parent" of a BARE RELATIVE NAME is the current # directory, not the name's parent, because `dirname("foo")' is "". emit('parent_of_file', '<%s>' % rel(ptpath.parent_dir_path('a/one.py'))) emit('parent_of_dir', '<%s>' % rel(ptpath.parent_dir_path('a/b'))) emit('parent_of_trailing_slash', '<%s>' % rel(ptpath.parent_dir_path('a/b/'))) emit('parent_trailing_slash_is_self', 'yes' if ptpath.parent_dir_path('a/b/') == ptpath.absolute_path('a/b') else 'no') emit('parent_of_bare_name_is_cwd', 'yes' if ptpath.parent_dir_path('top.py') == T else 'no') emit('parent_of_root', '<%s>' % ptpath.parent_dir_path('/')) emit('parent_of_empty', 'yes' if ptpath.parent_dir_path('') == T else 'no') # --------------------------------------------------------------- teardown ---- os.chdir('/') for _d in (T, K): try: shutil.rmtree(_d) except Exception: pass emit('torn_down', 'yes' if not os.path.isdir(T) and not os.path.isdir(K) else 'no') # The trailer. `emitted_count' counts every key above INCLUDING `probe_done' and # EXCLUDING itself, so a probe that died half way through is caught by the suite # even though every line it did emit was correct. emit('probe_done', 'yes') print('emitted_count=%d' % len(emitted))