#!/usr/bin/env python3 """One interpreter start for the whole of 99-python3-reedsolo. 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, so taking one start for every measurement here is the difference between a one-minute run and a half-hour one. WHAT THIS PACKAGE DOES, AND THEREFORE WHAT IS MEASURED. `reedsolo' is a Reed-Solomon error-correcting encoder and decoder over GF(2^c_exp). It is PURE ARITHMETIC -- no socket, no subprocess, no device, no terminal, no file -- so every fixture below is built by this probe and every answer is EXACT. That is what makes this port close on its suite with no BRINTOS-780 follow-on, and it is also what makes the assertions unusually sharp: an error-correcting code either returns the original message or it does not, and the set of positions it says it repaired either is the set that was corrupted or it is not. THE CORRUPTION IS DETERMINISTIC. Nothing here is random. Every corrupted position is a literal in this file and every corrupting byte is chosen with `^ 0xFF' so it cannot accidentally equal the byte it replaces -- a random corruption that happened to be a no-op would turn a decoder failure into a green case, which is the one way a suite like this lies. WHY THE POSITIONS ARE COMPARED AS A SORTED SET *AND* RAW. `decode' returns the errata positions as `erase_pos + err_pos`: the erasures the caller declared, in the caller's order, followed by the errors the Chien search found, which `rs_find_errors' appends in DESCENDING position order. So the raw sequence is neither sorted nor in corruption order. `*-positions' below sorts before comparing, which is the property a caller actually depends on; `*-positions-raw' asserts the order the library really returns, so that a future version that starts sorting them is a visible change here rather than a silent one. FOUR THINGS A CALLER WILL GET WRONG. Each has its own case below. `decode' RETURNS THREE VALUES, NOT TWO. Its own docstring says `rmes, rmesecc = RSCodec.decode(data)'. The code returns `(msg, msg+ecc, errata_positions)'. A caller who believes the docstring gets a ValueError on unpacking. `decode-arity' is that fact. THERE IS NO `reedsolo.__version__'. The version exists only in the dist-info METADATA. `has-version-attr' says so, and the suite reads the real version from the dist-info instead. CONSTRUCTING AN `RSCodec' MUTATES MODULE-LEVEL STATE. `__init__' calls `init_tables', which assigns the module globals `gf_exp', `gf_log' and `field_charac'. So the bare `gf_mul' family operates on whichever codec was built LAST, not on any field the caller names. `ctor-sets-module-globals' asserts the side effect, because a library that reconfigures its own module state from a constructor is worth a caller knowing about -- and `gf-*-before-init' asserts what the same functions answer before any codec exists, which is garbage from the placeholder tables, not an exception. THE PACKAGE SHIPS A TOP-LEVEL `tests' MODULE. `setup.py' declares only `py_modules=["reedsolo"]', but the sdist carries a stale `reedsolo.egg-info/top_level.txt' listing `tests', and that is what the wheel was built from. So `/usr/lib/python3.12/ site-packages/tests/' is on this image and `import tests' resolves to this package's own test suite. Measured, not assumed: `tests-shadow-module' below imports it and reports where it landed. No other package in the image owns that path, so there is no dpkg conflict -- it is a namespace hazard, not a build failure. 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 byte strings are emitted as hex. And no value may depend on the encoding stdout happens to be in: every one is ASCII. """ import sys emitted = [] def emit(key, value): emitted.append(key) print('%s=%s' % (key, value)) def hx(b): """A byte string as lowercase hex -- one line, ASCII, whatever the bytes.""" return bytes(bytearray(b)).hex() def call(fn, *a, **kw): """Run fn and render its outcome as a single ASCII token. An exception becomes `!ClassName' so that a case asserting a raise reads the same way as a case asserting a value, and an unexpected exception type is a visible red rather than a crashed probe.""" try: return fn(*a, **kw) except Exception as exc: # noqa: BLE001 - that is the point return '!%s' % type(exc).__name__ # --------------------------------------------------------------------------- # The bare module functions BEFORE any codec exists. # # reedsolo defines gf_exp = bytearray([1]*512) and gf_log = bytearray(256) at # import time -- placeholder tables, not a real field. So gf_mul answers, and # answers WRONGLY, rather than raising. Asserted first, because building any # codec below overwrites these globals for good. # --------------------------------------------------------------------------- import reedsolo # noqa: E402 emit('module_file', getattr(reedsolo, '__file__', '')) emit('is_package', 'yes' if hasattr(reedsolo, '__path__') else 'no') emit('has_version_attr', 'yes' if hasattr(reedsolo, '__version__') else 'no') emit('gf_field_charac_before_init', reedsolo.field_charac) emit('gf_exp_len_before_init', len(reedsolo.gf_exp)) emit('gf_log_len_before_init', len(reedsolo.gf_log)) # 1*1 under the placeholder table. Not a real field answer -- that is the point. emit('gf_mul_before_init', call(reedsolo.gf_mul, 2, 2)) # --------------------------------------------------------------------------- # The public names. # --------------------------------------------------------------------------- for _n in ('RSCodec', 'ReedSolomonError', 'init_tables', 'rs_encode_msg', 'rs_correct_msg', 'rs_check', 'rs_generator_poly', 'gf_add', 'gf_sub', 'gf_mul', 'gf_div', 'gf_pow', 'gf_inverse', 'gf_poly_mul', 'gf_poly_add', 'gf_poly_div', 'gf_poly_eval', 'find_prime_polys'): emit('has_%s' % _n.lower(), 'yes' if hasattr(reedsolo, _n) else 'no') emit('error_is_exception', 'yes' if issubclass(reedsolo.ReedSolomonError, Exception) else 'no') # --------------------------------------------------------------------------- # init_tables and the Galois field itself. # # GF(2^8) with the QR-code / conventional primitive polynomial 0x11d and # generator 2. The literals below are what that polynomial DEFINES, computed # from it rather than copied from a table: # # alpha^0..alpha^7 are 1,2,4,8,16,32,64,128 -- plain doubling, no reduction. # alpha^8 overflows 8 bits and is reduced by 0x11d: 0x100 ^ 0x11d = 0x1d = 29. # alpha^255 == alpha^0 == 1, which is what makes the field cyclic of order 255. # --------------------------------------------------------------------------- gf_log, gf_exp, field_charac = reedsolo.init_tables(prim=0x11d, generator=2, c_exp=8) emit('init_tables_field_charac', field_charac) emit('init_tables_exp_len', len(gf_exp)) emit('init_tables_log_len', len(gf_log)) emit('gf_exp_0', gf_exp[0]) emit('gf_exp_1', gf_exp[1]) emit('gf_exp_7', gf_exp[7]) # The reduction step: 2^8 = 256 is outside the field, 256 ^ 0x11d = 29. emit('gf_exp_8', gf_exp[8]) emit('gf_exp_9', gf_exp[9]) emit('gf_log_1', gf_log[1]) emit('gf_log_2', gf_log[2]) emit('gf_log_29', gf_log[29]) # Addition in a characteristic-2 field IS xor, and subtraction is the same # operation -- the two cases below are what "characteristic 2" means in code. emit('gf_add_5_3', reedsolo.gf_add(5, 3)) emit('gf_sub_5_3', reedsolo.gf_sub(5, 3)) emit('gf_add_is_sub', 'yes' if reedsolo.gf_add(200, 37) == reedsolo.gf_sub(200, 37) else 'no') emit('gf_neg_7', reedsolo.gf_neg(7)) # Multiplication against the polynomial definition. 2*2=4 and 2*4=8 need no # reduction; 128*2 does, and is the alpha^8 case again from the other side. emit('gf_mul_2_2', reedsolo.gf_mul(2, 2)) emit('gf_mul_2_4', reedsolo.gf_mul(2, 4)) emit('gf_mul_128_2', reedsolo.gf_mul(128, 2)) emit('gf_mul_0_5', reedsolo.gf_mul(0, 5)) emit('gf_mul_5_0', reedsolo.gf_mul(5, 0)) emit('gf_mul_1_37', reedsolo.gf_mul(1, 37)) emit('gf_div_8_2', reedsolo.gf_div(8, 2)) emit('gf_div_0_7', reedsolo.gf_div(0, 7)) emit('gf_div_by_zero', call(reedsolo.gf_div, 7, 0)) emit('gf_pow_2_8', reedsolo.gf_pow(2, 8)) emit('gf_pow_2_0', reedsolo.gf_pow(2, 0)) emit('gf_inverse_2', reedsolo.gf_inverse(2)) # x * x^-1 == 1 is the property, asserted over a value that is not a power of 2. emit('gf_inverse_roundtrip', reedsolo.gf_mul(37, reedsolo.gf_inverse(37))) # Every non-zero element has an inverse and every product stays in the field: # 255 * 255 multiplications, reported as one number a reader can check. _closed = 1 for _a in range(1, 256): if reedsolo.gf_mul(_a, reedsolo.gf_inverse(_a)) != 1: _closed = 0 break emit('gf_all_inverses_ok', _closed) # --------------------------------------------------------------------------- # Polynomials over the field. # --------------------------------------------------------------------------- emit('gf_poly_add', hx(reedsolo.gf_poly_add(bytearray([1, 2]), bytearray([3])))) emit('gf_poly_mul', hx(reedsolo.gf_poly_mul(bytearray([1, 2]), bytearray([1, 3])))) # Horner over GF(2^8): at x==1 every multiply is the identity and the result # is the xor of the coefficients, 1^2^3 == 0. At x==0 only the constant term # survives, so it is the last coefficient, 3. emit('gf_poly_eval_at_1', reedsolo.gf_poly_eval(bytearray([1, 2, 3]), 1)) emit('gf_poly_eval_at_0', reedsolo.gf_poly_eval(bytearray([1, 2, 3]), 0)) emit('gf_poly_eval_at_4', reedsolo.gf_poly_eval(bytearray([1, 2, 3]), 4)) emit('gf_poly_scale', hx(reedsolo.gf_poly_scale(bytearray([1, 2, 3]), 2))) # The generator polynomial of an nsym=2 code is (x-alpha^0)(x-alpha^1), which # over GF(2^8) with alpha=2 is x^2 + 3x + 2 -> [1, 3, 2]. emit('rs_generator_poly_2', hx(reedsolo.rs_generator_poly(2))) emit('rs_generator_poly_2_len', len(reedsolo.rs_generator_poly(2))) emit('rs_generator_poly_10_len', len(reedsolo.rs_generator_poly(10))) # --------------------------------------------------------------------------- # The codec's own parameters: what it reports against what it was built with. # --------------------------------------------------------------------------- rs = reedsolo.RSCodec(10) emit('ctor_nsym', rs.nsym) emit('ctor_nsize', rs.nsize) emit('ctor_fcr', rs.fcr) emit('ctor_prim', hex(rs.prim)) emit('ctor_generator', rs.generator) emit('ctor_c_exp', rs.c_exp) emit('ctor_field_charac', rs.field_charac) # The constructor ran init_tables, which assigns the module globals. This is # the side effect a caller holding two codecs has to know about. emit('ctor_sets_module_globals', 'yes' if reedsolo.field_charac == rs.field_charac else 'no') rs12 = reedsolo.RSCodec(nsym=12, nsize=63, c_exp=6, prim=0x43) emit('ctor6_c_exp', rs12.c_exp) emit('ctor6_field_charac', rs12.field_charac) emit('ctor6_nsize', rs12.nsize) # nsym must be strictly less than nsize; the boundary is a ValueError, not a # clamp, and not a wrong answer at decode time. emit('ctor_nsym_eq_nsize', call(reedsolo.RSCodec, 10, 10)) emit('ctor_nsym_gt_nsize', call(reedsolo.RSCodec, 11, 10)) # maxerrata is the Singleton bound: nsym/2 errors OR nsym erasures. emit('maxerrata_10', repr(rs.maxerrata()).replace(' ', '')) emit('maxerrata_10_given_4_erasures', repr(rs.maxerrata(erasures=4)).replace(' ', '')) emit('maxerrata_10_given_2_errors', repr(rs.maxerrata(errors=2)).replace(' ', '')) emit('maxerrata_over_bound', call(rs.maxerrata, erasures=11)) # --------------------------------------------------------------------------- # THE ROUND TRIP. This is the backbone of the suite. # # For each parity count: encode, corrupt exactly nsym//2 symbols at positions # chosen here, and assert that decode returns the ORIGINAL message and names # the EXACT set of positions that were corrupted. # --------------------------------------------------------------------------- MSG = bytearray(b'brintOS reed-solomon round trip fixture 0123456789') def corrupt(codeword, positions): """Flip every byte at `positions' so none can accidentally stay itself.""" out = bytearray(codeword) for p in positions: out[p] ^= 0xFF return out def roundtrip(tag, nsym, positions, msg=MSG): codec = reedsolo.RSCodec(nsym) enc = codec.encode(msg) emit('%s_enc_len' % tag, len(enc)) # The parity is appended: the encoded message still starts with the message. emit('%s_enc_prefix' % tag, 'yes' if enc[:len(msg)] == msg else 'no') emit('%s_check_clean' % tag, repr(codec.check(enc)).replace(' ', '')) bad = corrupt(enc, positions) emit('%s_check_corrupt' % tag, repr(codec.check(bad)).replace(' ', '')) got = call(codec.decode, bad) if isinstance(got, str): # an exception token emit('%s_msg' % tag, got) emit('%s_positions' % tag, got) return dec, dec_full, errata = got emit('%s_msg' % tag, 'yes' if bytearray(dec) == msg else 'no') emit('%s_full_is_msg_plus_ecc' % tag, 'yes' if bytearray(dec_full) == enc else 'no') emit('%s_positions' % tag, ','.join(str(p) for p in sorted(errata))) # nsym=4 corrects 2 errors; nsym=10 corrects 5; nsym=20 corrects 10. roundtrip('rt4', 4, [0, 7]) roundtrip('rt10', 10, [0, 3, 17, 40, 52]) roundtrip('rt20', 20, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) # The raw order the library returns, asserted once. erase_pos is empty here, so # this is rs_find_errors' own output: DESCENDING, not sorted, not input order. _c = reedsolo.RSCodec(10) _e = _c.encode(MSG) _d = _c.decode(corrupt(_e, [0, 3, 17, 40, 52])) emit('positions_raw_order', ','.join(str(p) for p in _d[2])) emit('positions_raw_is_sorted', 'yes' if list(_d[2]) == sorted(_d[2]) else 'no') # decode returns THREE values. The docstring says two. emit('decode_arity', len(_d)) emit('decode_msg_type', type(_d[0]).__name__) emit('decode_errata_type', type(_d[2]).__name__) # A codeword with no corruption at all: zero errata, message unchanged. _d0 = _c.decode(_e) emit('clean_decode_msg', 'yes' if bytearray(_d0[0]) == MSG else 'no') emit('clean_decode_positions', ','.join(str(p) for p in _d0[2])) # --------------------------------------------------------------------------- # ONE CORRUPTION TOO MANY. # # The case that separates a working decoder from one that appears to work: the # library must RAISE rather than hand back a wrong message quietly. # --------------------------------------------------------------------------- def over_capacity(tag, nsym, positions): codec = reedsolo.RSCodec(nsym) enc = codec.encode(MSG) bad = corrupt(enc, positions) got = call(codec.decode, bad) if isinstance(got, str): emit('%s_outcome' % tag, got) return # It did not raise. Say whether it at least got the answer right, because # "wrong message returned silently" and "right message beyond the bound" # are very different results and a lone `no' would hide which happened. emit('%s_outcome' % tag, 'returned-correct' if bytearray(got[0]) == MSG else 'returned-wrong') # nsym=4 corrects 2; three errors is one too many. over_capacity('over4', 4, [0, 7, 19]) # nsym=10 corrects 5; six is one too many. over_capacity('over10', 10, [0, 3, 17, 40, 52, 55]) # check() must report the corruption as corruption. _cc = reedsolo.RSCodec(4) emit('check_over_capacity', repr(_cc.check(corrupt(_cc.encode(MSG), [0, 7, 19]))).replace(' ', '')) # --------------------------------------------------------------------------- # ERASURES. # # A known-bad position costs one parity symbol instead of two, so an nsym=10 # code repairs 10 erasures where it repairs only 5 unlocalized errors. Both # halves are asserted: the higher capacity works, and one erasure past it does # not. # --------------------------------------------------------------------------- def erasures(tag, nsym, positions, only_erasures=True): codec = reedsolo.RSCodec(nsym) enc = codec.encode(MSG) bad = corrupt(enc, positions) got = call(codec.decode, bad, erase_pos=list(positions), only_erasures=only_erasures) if isinstance(got, str): emit('%s_msg' % tag, got) emit('%s_positions' % tag, got) return emit('%s_msg' % tag, 'yes' if bytearray(got[0]) == MSG else 'no') emit('%s_positions' % tag, ','.join(str(p) for p in sorted(got[2]))) # Ten erasures under an nsym=10 code: exactly the bound, and twice what the # same code can do without being told where the damage is. erasures('era10', 10, [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) # Six erasures under nsym=10 -- comfortably inside the bound. erasures('era6', 10, [2, 9, 20, 31, 44, 57]) # Eleven erasures under nsym=10 is past the Singleton bound. erasures('era11', 10, list(range(11))) # The same six positions as ERRORS rather than erasures is past the nsym=10 # error bound of five. Asserted beside era6 on purpose: the pair is what shows # that declaring the positions is what buys the extra capacity, not luck. over_capacity('era6_as_errors', 10, [2, 9, 20, 31, 44, 57]) # Erasures WITHOUT only_erasures: the library computes Forney syndromes and # still repairs, and the reported positions still name the corrupted set. erasures('era6_forney', 10, [2, 9, 20, 31, 44, 57], only_erasures=False) # --------------------------------------------------------------------------- # THE EDGE CASES THE TICKET NAMES. # --------------------------------------------------------------------------- # --- a single-symbol code ------------------------------------------------- # nsym=1 corrects zero errors -- int(1/2) == 0 -- but one ERASURE, because a # located symbol costs one parity symbol. Both halves asserted. rs1 = reedsolo.RSCodec(1) _e1 = rs1.encode(MSG) emit('nsym1_enc_len', len(_e1)) emit('nsym1_maxerrata', repr(rs1.maxerrata()).replace(' ', '')) emit('nsym1_clean_msg', 'yes' if bytearray(rs1.decode(_e1)[0]) == MSG else 'no') _g1 = call(rs1.decode, corrupt(_e1, [5]), erase_pos=[5], only_erasures=True) emit('nsym1_one_erasure', _g1 if isinstance(_g1, str) else ('yes' if bytearray(_g1[0]) == MSG else 'no')) emit('nsym1_one_error', call(rs1.decode, corrupt(_e1, [5]))) # --- an empty message ------------------------------------------------------ # ceil(0/chunk) is zero chunks, so encode returns an EMPTY bytearray: no parity # at all, not a bare parity block. A caller who expects nsym bytes back is # wrong, and this is where that shows. _ee = rs.encode(bytearray(b'')) emit('empty_enc_len', len(_ee)) emit('empty_enc_hex', hx(_ee)) _ed = call(rs.decode, bytearray(b'')) emit('empty_decode', _ed if isinstance(_ed, str) else 'arity-%d-lens-%s' % (len(_ed), ','.join(str(len(x)) for x in _ed))) emit('empty_check', repr(rs.check(bytearray(b''))).replace(' ', '')) # --- a message longer than one block --------------------------------------- # nsize=20 with nsym=6 gives a 14-byte payload per chunk, so 35 bytes is three # chunks and the chunking loop is genuinely exercised: 3*20 == 60 bytes out. LONG = bytearray(bytes(range(35))) rsL = reedsolo.RSCodec(nsym=6, nsize=20) _eL = rsL.encode(LONG) # THE LAST CHUNK IS SHORT, AND THE ENCODED LENGTH SHOWS IT. `encode' # preallocates total_chunks*nsize == 60 bytes, then slice-assigns each encoded # chunk into it. The final chunk carries only 35-28 == 7 message bytes, so it # encodes to 7+6 == 13 rather than 20, and the slice assignment RESIZES the # bytearray down: 40+13 == 53. A caller who sized the output as # total_chunks*nsize is wrong, which is why the length is asserted and not the # chunk count derived from it. emit('long_enc_len', len(_eL)) emit('long_last_chunk_len', len(_eL) - 40) emit('long_check_clean', repr(rsL.check(_eL)).replace(' ', '')) emit('long_check_len', len(rsL.check(_eL))) # One error in each of the three chunks: chunk 0 at 3, chunk 1 at 25, chunk 2 # at 41. Well inside the 3-error-per-chunk bound of an nsym=6 code. _bL = corrupt(_eL, [3, 25, 41]) _dL = call(rsL.decode, _bL) if isinstance(_dL, str): emit('long_msg', _dL) emit('long_positions', _dL) emit('long_dec_len', _dL) else: # decode returns total_chunks * (nsize - nsym) = 3*14 = 42 bytes: the 35 # message bytes plus the 7 zero pad bytes of the last chunk's payload. # The same resize runs on the way back, so what comes out is exactly the # 35 message bytes with no padding -- decode does NOT return # total_chunks*(nsize-nsym) == 42 despite preallocating that much. emit('long_dec_len', len(_dL[0])) emit('long_msg', 'yes' if bytearray(_dL[0]) == LONG else 'no') # Positions are CHUNK-RELATIVE and are not offset by the chunk index, so # the three corruptions at 3, 25 and 41 are reported as 3, 5 and 1. A # caller who reads them as absolute offsets into the codeword is wrong. emit('long_positions', ','.join(str(p) for p in sorted(_dL[2]))) emit('long_check_corrupt', repr(rsL.check(_bL)).replace(' ', '')) # --------------------------------------------------------------------------- # INPUT TYPES. # # encode accepts str as latin-1 and bytes/bytearray as themselves, and returns # a bytearray in every case. A str carrying a non-latin-1 character is a # UnicodeEncodeError rather than a mangled codeword. # --------------------------------------------------------------------------- emit('encode_bytes_type', type(rs.encode(b'hello')).__name__) emit('encode_bytearray_type', type(rs.encode(bytearray(b'hello'))).__name__) emit('encode_hello_hex', hx(rs.encode(b'hello'))) # THE DOCUMENTED str SUPPORT DOES NOT WORK IN THIS FIELD, and this is the third # caller trap. `encode' opens with `if isinstance(data, str): data = # _bytearray(data)', and `init_tables' binds `_bytearray' to the BUILTIN # bytearray whenever c_exp <= 8. `bytearray('hello')` with no encoding is a # TypeError, so a str reaches the trap and dies there. Only a field with # c_exp > 8 -- where init_tables installs the array.array shim that latin-1 # encodes -- accepts one. Asserted through call() as the TypeError it is, and # then asserted to WORK on a c_exp=12 codec, because the pair is what shows # this is a field-dependent trap rather than a module that never took str. emit('encode_str_default_field', call(rs.encode, 'hello')) emit('decode_str_default_field', call(rs.decode, 'hello')) rs12b = reedsolo.RSCodec(nsym=10, nsize=4095, c_exp=12) _s12 = call(rs12b.encode, 'hello') emit('encode_str_c_exp_12', _s12 if isinstance(_s12, str) else 'ok-len-%d' % len(_s12)) # A codeword shorter than the parity block is not a codeword. emit('decode_too_short', call(rs.decode, bytearray(b'ab'))) # A message longer than the field is refused rather than silently mis-located. emit('decode_too_long', call(reedsolo.rs_correct_msg, bytearray(300), 10)) # --------------------------------------------------------------------------- # THE BARE FUNCTIONAL API, used without an RSCodec. # --------------------------------------------------------------------------- reedsolo.init_tables(0x11d, 2, 8) _enc = reedsolo.rs_encode_msg(bytearray(b'hello'), 10) emit('bare_encode_len', len(_enc)) emit('bare_encode_matches_codec', 'yes' if bytearray(_enc) == bytearray(rs.encode(b'hello')) else 'no') emit('bare_check_clean', reedsolo.rs_check(_enc, 10)) _bad = corrupt(_enc, [0, 2]) emit('bare_check_corrupt', reedsolo.rs_check(_bad, 10)) _bare = call(reedsolo.rs_correct_msg, _bad, 10) if isinstance(_bare, str): emit('bare_correct_msg', _bare) emit('bare_correct_positions', _bare) else: emit('bare_correct_msg', hx(_bare[0])) emit('bare_correct_positions', ','.join(str(p) for p in sorted(_bare[2]))) # --------------------------------------------------------------------------- # THE ACCELERATOR THAT IS NOT HERE, AND THE MODULE THAT IS. # --------------------------------------------------------------------------- # The recipe inherits setuptools3 with no Cython dependency and setup.py only # builds the extension under an explicit `--cythonize', so the pure-Python # implementation is what shipped. This image's python3 is one static binary # with an empty lib-dynload and no dynamic loader (BRINTOS-1476), so a compiled # creedsolo could not have been imported here anyway. try: import creedsolo # noqa: F401 emit('creedsolo_importable', 'yes') except ImportError: emit('creedsolo_importable', 'no') # The stale egg-info top_level.txt put `tests' in the wheel, so this package # owns the top-level name `tests' on the image. Measured, not assumed. # It imports, and it is a NAMESPACE package: the wheel ships tests/perf.py, # tests/test_creedsolo.py and tests/test_reedsolo.py with no __init__.py, so # `tests.__file__' is None and the evidence is __path__ instead. Reported as # the path so the case names the directory a reader can go and look at. try: import tests emit('tests_shadow_importable', 'yes') emit('tests_shadow_file', repr(getattr(tests, '__file__', 'missing'))) emit('tests_shadow_path', ';'.join(list(getattr(tests, '__path__', [])))) emit('tests_shadow_submodule', call(__import__, 'tests.test_reedsolo') and 'yes') except ImportError: emit('tests_shadow_importable', 'no') emit('tests_shadow_file', 'no') emit('tests_shadow_path', 'no') emit('tests_shadow_submodule', 'no') emit('emitted_count', len(emitted)) emit('probe_done', 'yes')