#!/usr/bin/env python3 """One interpreter start for the whole of 99-python3-jsonrpcclient. 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 per measurement would turn a one-minute suite into a half-hour one. WHAT THIS PACKAGE IS, AND THEREFORE WHAT IS MEASURED. jsonrpcclient BUILDS JSON-RPC 2.0 REQUEST OBJECTS AND TAKES RESPONSE OBJECTS APART. It owns no transport: there is no socket, no http client and no session anywhere in the seven modules, so every case here is a string in and a string out and NOTHING IN THIS FILE REACHES A DEVICE, A PEER OR A TERMINAL. That is what lets the port close on its suite. FOUR OF THE TICKET'S EXPECTATIONS ARE WRONG, AND THIS PROBE IS WHERE THAT IS ESTABLISHED. `to_response' looks at exactly three keys -- `result', `error' and `id' -- so a response missing `jsonrpc' parses happily, a response carrying BOTH `result' and `error' silently becomes an Ok with the error discarded, and an `id' matching no request is returned unremarked because the library keeps no registry of outstanding requests to match it against. Each is emitted as the value it actually produces rather than as the rejection the ticket predicted. THE ID COUNTER IS PROCESS-WIDE AND SHARED, WHICH ORDERS THIS FILE. `request = request_natural = partial(request_impure, id_generators.decimal())' binds ONE itertools.count at import, and `request_json = compose(json.dumps, request_natural)' closes over THAT SAME PARTIAL -- so `request' and `request_json' draw from one counter, and every call either function makes advances it. Every natural-id call this probe makes is therefore confined to the one section marked below, in a fixed order, and the ids are emitted as one literal sequence. Adding a `request(...)' anywhere else in this file renumbers that sequence and turns the suite red for a reason that has nothing to do with the package. 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 no value may depend on the encoding stdout happens to be in: every one is ASCII. """ import json import os import sys emitted = [] def emit(key, value): emitted.append(key) print('%s=%s' % (key, value)) def call(fn, *a, **kw): """Return fn(*a) as a string, or `!ExceptionName' if it raised. Asserted by exception NAME rather than by message: the message is CPython's and has moved between releases, while the name is the contract a caller has to code against. Where the MESSAGE is the package's own -- and so is part of what it promises -- it is emitted separately by callmsg().""" try: return fn(*a, **kw) except Exception as exc: return '!' + type(exc).__name__ def callmsg(fn, *a, **kw): """`!ExceptionName:message' -- for the messages jsonrpcclient itself writes.""" try: return 'no-raise:%r' % (fn(*a, **kw),) except Exception as exc: return '!%s:%s' % (type(exc).__name__, exc) def j(obj): """The object as JSON with its keys in INSERTION order. Insertion order is the thing under test here: request_pure builds the dict literal `jsonrpc, method, params, id' and json.dumps walks it as built, so the wire form is fixed and can be asserted as a literal string rather than only as a parsed structure.""" return json.dumps(obj) def keys(obj): return ','.join(obj.keys()) # ===================================================================== import # Emitted FIRST and on its own. Every case below is unscorable if the import # failed, and the reason it might is not hypothetical on this arm: the package # reaches `string' (id_generators.py line 4) which OE splits out as # python3-stringold, and it reaches `uuid' and `random', whose C accelerators # live in a lib-dynload that is EMPTY on this image (BRINTOS-1476). emit('import_error', call(lambda: (__import__('jsonrpcclient'), 'none')[1])) import jsonrpcclient as J from jsonrpcclient import (Error, Ok, notification, notification_json, parse, parse_json, request, request_hex, request_json, request_json_hex, request_json_random, request_json_uuid, request_random, request_uuid) from jsonrpcclient import id_generators, requests, responses, sentinels, utils emit('module_file', J.__file__) emit('is_package', 'yes' if hasattr(J, '__path__') else 'no') emit('module_dir', os.path.dirname(J.__file__)) # The submodule set, as shipped. A release that grew a transport would show up # here first. emit('submodules', ','.join(sorted( f[:-3] for f in os.listdir(os.path.dirname(J.__file__)) if f.endswith('.py')))) emit('py_typed', 'yes' if os.path.exists( os.path.join(os.path.dirname(J.__file__), 'py.typed')) else 'no') # MANIFEST.in names `jsonrpcclient/response-schema.json' and the file does not # exist in the tree the SRCREV pins, so it cannot ship. Asserted rather than # assumed: a release that restored it would change what a caller may rely on. emit('response_schema', 'yes' if os.path.exists( os.path.join(os.path.dirname(J.__file__), 'response-schema.json')) else 'no') # ================================================================ the API set # `__all__' NAMES "request" TWICE -- once beside the notification helpers and # again beside Ok/Error. Harmless to `from ... import *', and a fact about the # shipped file, so it is asserted as the literal it is rather than de-duplicated. emit('dunder_all_len', len(J.__all__)) emit('dunder_all', ','.join(J.__all__)) emit('dunder_all_unique', ','.join(sorted(set(J.__all__)))) emit('dunder_all_has_dup', 'yes' if len(J.__all__) != len(set(J.__all__)) else 'no') # Every name in __all__ must actually be bound -- a `from jsonrpcclient import *' # that raises is the failure this catches. emit('all_names_bound', ','.join(sorted( n for n in set(J.__all__) if not hasattr(J, n))) or 'none-missing') # NO TRANSPORT. The ticket's premise, asserted as an absence: the names every # JSON-RPC client library that DOES own a transport exposes. emit('no_transport_names', ','.join(sorted( n for n in ('send', 'Client', 'client', 'HTTPClient', 'request_and_send', 'session', 'Session', 'connect', 'socket') if hasattr(J, n))) or 'none') # NO BATCH HELPER EITHER. The specification's batch is a JSON array, and # building one is the CALLER's job here -- `json.dumps([request(...), ...])'. emit('no_batch_names', ','.join(sorted( n for n in ('batch', 'batch_json', 'requests_json', 'notifications') if hasattr(J, n))) or 'none') # ============================================================ NATURAL IDS ==== # EVERY `request' AND `request_json' CALL IN THIS FILE IS IN THIS SECTION, in # this order. See the module docstring: they share one process-wide counter # that starts at 1, so the sequence below is a literal the suite pins. An # explicit `id=' does NOT draw from it -- request_pure evaluates `next()' only # in the branch it does not take -- and `req_explicit_no_draw' is the case that # says so. _ids = [] def rid(obj): _ids.append(obj['id']) return obj # 1. A method with no parameters. The wire form is asserted as a literal # because the key order is fixed by the dict literal request_pure builds. emit('req_bare', j(rid(request('ping')))) # 2. Positional parameters. A TUPLE is converted to a list by request_pure # (`list(params) if isinstance(params, tuple)'), so the JSON is an array # either way -- but the DICT a caller inspects differs, which is # `req_params_tuple_type' below. emit('req_pos_list', j(rid(request('sum', [1, 2, 3])))) emit('req_pos_tuple', j(rid(request('sum', (1, 2, 3))))) _t = rid(request('sum', (1, 2))) emit('req_params_tuple_type', type(_t['params']).__name__) _l = rid(request('sum', [1, 2])) emit('req_params_list_type', type(_l['params']).__name__) # 3. Named parameters. A dict is passed through untouched. emit('req_named', j(rid(request('subtract', {'minuend': 42, 'subtrahend': 23})))) # 4. FALSY PARAMS ARE DROPPED, AND SILENTLY. `request_impure' does # `params or ()' and `request_pure' then tests `if params', so an empty # tuple, an empty list, an empty dict and a 0 all produce a request with NO # `params' key at all. A caller who meant to send `params: []' -- which the # specification permits -- cannot say so through this API. emit('req_params_empty_tuple', j(rid(request('m', ())))) emit('req_params_empty_list', j(rid(request('m', [])))) emit('req_params_empty_dict', j(rid(request('m', {})))) emit('req_params_zero', j(rid(request('m', 0)))) # A list holding a falsy element is itself truthy and IS kept -- the test # is on the container, not its contents. emit('req_params_list_of_zero', j(rid(request('m', [0])))) # 5. An explicit id, and the three shapes of it that matter. `id is not NOID' # is an IDENTITY test, so 0, None and '' are all kept as given rather than # replaced by a generated one. emit('req_explicit_int', j(request('m', id=99))) emit('req_explicit_zero', j(request('m', id=0))) emit('req_explicit_none', j(request('m', id=None))) emit('req_explicit_str', j(request('m', id='abc'))) emit('req_explicit_with_params', j(request('m', [1], id='x'))) # THE EXPLICIT-ID CALLS ABOVE DREW NOTHING FROM THE COUNTER. The next natural # id continues the sequence, which is what this pair establishes. emit('req_explicit_no_draw', j(rid(request('after-explicit')))) # 6. request_json is the SAME function composed with json.dumps, drawing from # the SAME counter -- so its id continues the sequence rather than starting # its own. This is the sharing the docstring warns about, asserted. _rj = request_json('rj') _ids.append(json.loads(_rj)['id']) emit('req_json_str', _rj) emit('req_json_type', type(_rj).__name__) emit('req_json_separators', 'yes' if '", "' in _rj else 'no') # 7. The whole sequence, in call order. 1..N with no gaps and no repeats is # the ticket's "auto-generated id is unique across calls", stated as the # stronger property it actually has. emit('natural_ids', ','.join(str(i) for i in _ids)) emit('natural_ids_unique', 'yes' if len(set(_ids)) == len(_ids) else 'no') emit('natural_ids_are_int', 'yes' if all( isinstance(i, int) for i in _ids) else 'no') # ========================================================== the other id arms # Each of `request_hex', `request_random' and `request_uuid' binds its OWN # generator, so their counters are independent of the natural one and of each # other. request_json_hex composes over request_hex and therefore SHARES its # counter, exactly as request_json shares request's. emit('hex_1', request_hex('m')['id']) emit('hex_2', request_hex('m')['id']) _hex_json = json.loads(request_json_hex('m')) emit('hex_via_json', _hex_json['id']) # Nine more take it past 9 into the letters -- the point of a hex counter # and the only place its formatting is visible. for _ in range(6): request_hex('m') emit('hex_tenth', request_hex('m')['id']) _rand = request_random('m')['id'] emit('random_len', len(_rand)) emit('random_is_str', 'yes' if isinstance(_rand, str) else 'no') emit('random_charset', 'yes' if all( c in '0123456789abcdefghijklmnopqrstuvwxyz' for c in _rand) else 'no') emit('random_distinct', 'yes' if len({request_random('m')['id'] for _ in range(20)}) == 20 else 'no') emit('random_via_json_len', len(json.loads(request_json_random('m'))['id'])) _uu = request_uuid('m')['id'] emit('uuid_len', len(_uu)) emit('uuid_dashes', _uu.count('-')) emit('uuid_version', _uu[14]) emit('uuid_variant', 'yes' if _uu[19] in '89ab' else 'no') emit('uuid_distinct', 'yes' if len({request_uuid('m')['id'] for _ in range(20)}) == 20 else 'no') emit('uuid_via_json_len', len(json.loads(request_json_uuid('m'))['id'])) # EVERY REQUEST FORM CARRIES jsonrpc: "2.0". Asserted over all eight of them at # once rather than one case at a time, so a release that dropped it from one # arm is caught wherever it dropped it. _forms = [request('m', id=1), request_hex('m'), request_random('m'), request_uuid('m'), json.loads(request_json('m')), json.loads(request_json_hex('m')), json.loads(request_json_random('m')), json.loads(request_json_uuid('m')), notification('m'), json.loads(notification_json('m'))] emit('jsonrpc_everywhere', 'yes' if all( f.get('jsonrpc') == '2.0' for f in _forms) else 'no') emit('jsonrpc_form_count', len(_forms)) # ============================================================== notifications # A NOTIFICATION IS A REQUEST WITHOUT AN id, and that is the whole of the # difference: `notification_pure' builds `jsonrpc, method[, params]' and never # reaches the generator, so a notification draws no id and cannot be correlated # with a response. The specification says a server must not answer one. emit('note_bare', j(notification('ping'))) emit('note_bare_keys', keys(notification('ping'))) emit('note_has_id', 'yes' if 'id' in notification('ping') else 'no') emit('note_named', j(notification('subtract', {'a': 1}))) # A NOTIFICATION DOES NOT CONVERT ITS TUPLE, AND A REQUEST DOES. # `notification_pure' stores `params' as handed over -- there is no # `list(params) if isinstance(params, tuple)' on this path -- so a caller who # inspects the returned dict finds a tuple here and a list from `request'. # json.dumps hides the difference on the wire (both are arrays); nothing hides # it from the caller. _nt = notification('sum', (1, 2)) emit('note_params_tuple_type', type(_nt['params']).__name__) emit('note_pos_tuple_json', j(_nt)) emit('note_pos_list_json', j(notification('sum', [1, 2]))) emit('note_params_empty', j(notification('m', ()))) emit('note_json_str', notification_json('ping')) emit('note_json_type', type(notification_json('ping')).__name__) # A BATCH IS THE CALLER'S JSON ARRAY. There is no helper (`no_batch_names' # above), so this is the documented spelling -- and it is why the shared # counter matters: the two members here are numbered by one counter, in the # order json.dumps walks the list. _b1 = request('one', id='b1') _b2 = notification('two') _b3 = request('three', [1], id='b3') emit('batch_request_json', json.dumps([_b1, _b2, _b3])) emit('batch_len', len(json.loads(json.dumps([_b1, _b2, _b3])))) emit('batch_mixed_ids', ','.join( str(m.get('id', '')) for m in [_b1, _b2, _b3])) # ============================================================ parsing: Ok ==== # `to_response' tests `"result" in response' and returns Ok(result, id). Ok is # a NamedTuple, so it is a tuple as well as an object with fields -- both # accesses are asserted because callers use both. _ok = parse({'jsonrpc': '2.0', 'result': 19, 'id': 1}) emit('ok_type', type(_ok).__name__) emit('ok_fields', ','.join(_ok._fields)) emit('ok_result', _ok.result) emit('ok_id', _ok.id) emit('ok_index_0', _ok[0]) emit('ok_index_1', _ok[1]) emit('ok_len', len(_ok)) emit('ok_is_tuple', 'yes' if isinstance(_ok, tuple) else 'no') emit('ok_repr', repr(_ok)) emit('ok_asdict', json.dumps(dict(_ok._asdict()))) # EVERY JSON TYPE AS A RESULT, one case each. `result: null' is the one that # matters: `"result" in response' is a KEY test, so a null result is an Ok # carrying None and NOT an error -- which a caller testing `if response.result' # gets wrong. for _name, _val in (('null', None), ('true', True), ('false', False), ('int', 7), ('negint', -7), ('float', 1.5), ('str', 'hello'), ('emptystr', ''), ('zero', 0), ('array', [1, 'a', None]), ('object', {'k': [1, 2]}), ('emptyarray', []), ('emptyobject', {})): _r = parse({'jsonrpc': '2.0', 'result': _val, 'id': 1}) emit('ok_result_' + _name, '%s:%s' % (type(_r).__name__, json.dumps(_r.result))) # ========================================================= parsing: Error ==== # The `error' arm reads `code' and `message' with [] -- REQUIRED -- and `data' # with .get(), so `data' is None when the server omitted it and there is no way # to tell that from a server that sent `"data": null'. _err = parse({'jsonrpc': '2.0', 'error': {'code': -32601, 'message': 'Method not found'}, 'id': 1}) emit('err_type', type(_err).__name__) emit('err_fields', ','.join(_err._fields)) emit('err_code', _err.code) emit('err_message', _err.message) emit('err_data_absent', json.dumps(_err.data)) emit('err_id', _err.id) emit('err_len', len(_err)) emit('err_is_tuple', 'yes' if isinstance(_err, tuple) else 'no') emit('err_repr', repr(_err)) emit('err_index', '%s|%s|%s|%s' % (_err[0], _err[1], json.dumps(_err[2]), _err[3])) _errd = parse({'jsonrpc': '2.0', 'error': {'code': -32602, 'message': 'Invalid params', 'data': {'detail': 'x'}}, 'id': 2}) emit('err_data_present', json.dumps(_errd.data)) _errn = parse({'jsonrpc': '2.0', 'error': {'code': -32603, 'message': 'Internal error', 'data': None}, 'id': 3}) emit('err_data_explicit_null', json.dumps(_errn.data)) emit('err_data_null_indistinguishable', 'yes' if _errn.data == _err.data else 'no') # THE SPECIFICATION'S RESERVED CODES, each parsed and each field read back. # The point is not that the library validates them -- it does not, and # `err_code_unreserved' says so -- but that every one survives the trip. for _code, _msg in ((-32700, 'Parse error'), (-32600, 'Invalid Request'), (-32601, 'Method not found'), (-32602, 'Invalid params'), (-32603, 'Internal error'), (-32000, 'Server error'), (-32099, 'Server error')): _e = parse({'jsonrpc': '2.0', 'error': {'code': _code, 'message': _msg, 'data': _code}, 'id': 'c'}) emit('err_code_%d' % -_code, '%s|%s|%s|%s' % ( type(_e).__name__, _e.code, _e.message, json.dumps(_e.data))) # NOTHING IS VALIDATED. A code outside every reserved range, and a code that # is not even an integer, are both returned as given -- the NamedTuple's `code: # int' annotation is a hint and is not enforced at runtime. _eu = parse({'jsonrpc': '2.0', 'error': {'code': 1234, 'message': 'x'}, 'id': 1}) emit('err_code_unreserved', '%s|%s' % (type(_eu).__name__, _eu.code)) _es = parse({'jsonrpc': '2.0', 'error': {'code': 'oops', 'message': 'x'}, 'id': 1}) emit('err_code_not_int', '%s|%s|%s' % (type(_es).__name__, _es.code, type(_es.code).__name__)) # ========================================================== parsing: batch === # `parse' OF A LIST RETURNS A LAZY `map', NOT A LIST. Two consequences a # caller will meet: `len()' raises, and the sequence is CONSUMED ONCE -- a # second pass over the same object yields nothing at all, silently. This is # the single most surprising thing in the package. _batch = parse([{'jsonrpc': '2.0', 'result': 1, 'id': 1}, {'jsonrpc': '2.0', 'error': {'code': -32601, 'message': 'm'}, 'id': 2}, {'jsonrpc': '2.0', 'result': None, 'id': 3}]) emit('batch_parse_type', type(_batch).__name__) emit('batch_is_list', 'yes' if isinstance(_batch, list) else 'no') emit('batch_len_raises', call(len, _batch)) _first = list(_batch) emit('batch_first_pass', ','.join(type(r).__name__ for r in _first)) emit('batch_first_ids', ','.join(str(r.id) for r in _first)) emit('batch_second_pass_len', len(list(_batch))) emit('batch_empty', len(list(parse([])))) # parse_json over a batch does the same thing through json.loads first. _bj = parse_json('[{"jsonrpc":"2.0","result":1,"id":1},' '{"jsonrpc":"2.0","result":2,"id":2}]') emit('batch_json_type', type(_bj).__name__) emit('batch_json_results', ','.join(str(r.result) for r in _bj)) # ========================================================== parse_json ======= emit('pj_ok', repr(parse_json('{"jsonrpc":"2.0","result":"x","id":1}'))) emit('pj_err', repr(parse_json( '{"jsonrpc":"2.0","error":{"code":-32601,"message":"nope"},"id":1}'))) # WHITESPACE AND KEY ORDER ON THE WIRE ARE IRRELEVANT -- json.loads has already # made them so by the time to_response sees anything. emit('pj_reordered', repr(parse_json( '{ "id" : 5 , "result" : [1,2] , "jsonrpc" : "2.0" }'))) # ================================================= malformed input, and the # ================================================= four predictions it refutes # # THE TICKET EXPECTS `a response missing jsonrpc', `one carrying both result # and error' and `one whose id does not match any request' each to produce "the # documented failure rather than a partly-populated result". NONE OF THEM DOES. # `to_response' reads three keys and validates nothing, and there is no # registry of outstanding requests anywhere in the package for an id to be # matched against. What the values below record is what the code does. # 1. Text that is not JSON. This one DOES fail, out of json.loads -- # JSONDecodeError, which is a ValueError, so a caller catching ValueError # catches it. emit('bad_not_json', call(parse_json, 'not json at all')) try: parse_json('nope') _v = 'no-raise' except ValueError: _v = 'yes' except Exception: _v = 'not-a-valueerror' emit('bad_not_json_is_valueerror', _v) emit('bad_truncated', call(parse_json, '{oops')) emit('bad_empty_string', call(parse_json, '')) # 2. A response with NO `jsonrpc' member. PARSES, as an ordinary Ok. emit('bad_no_jsonrpc', repr(parse({'result': 1, 'id': 1}))) # ... and so does one whose `jsonrpc' says something else entirely. emit('bad_wrong_version', repr(parse({'jsonrpc': '1.0', 'result': 1, 'id': 1}))) # 3. A response carrying BOTH `result' and `error'. `"result" in response' is # tested FIRST, so it is an Ok and THE ERROR IS DISCARDED WITHOUT A WORD. emit('bad_both', repr(parse({'jsonrpc': '2.0', 'result': 1, 'error': {'code': -1, 'message': 'ignored'}, 'id': 1}))) # The mirror image -- a null result beside a real error -- goes the same way, # because the test is on the KEY. emit('bad_both_null_result', repr(parse({'jsonrpc': '2.0', 'result': None, 'error': {'code': -1, 'message': 'ignored'}, 'id': 1}))) # 4. An `id' that matches no request. There is nothing to match it against: # the package holds no state between a request and a response, so this is # an ordinary Ok and correlation is entirely the caller's job. emit('bad_unknown_id', repr(parse({'jsonrpc': '2.0', 'result': 1, 'id': 'never-sent'}))) emit('bad_null_id', repr(parse({'jsonrpc': '2.0', 'result': 1, 'id': None}))) # WHAT DOES FAIL, and it is all KeyError out of the three [] lookups. emit('bad_no_id', call(parse, {'jsonrpc': '2.0', 'result': 1})) emit('bad_neither', call(parse, {'jsonrpc': '2.0', 'id': 1})) emit('bad_err_no_code', call(parse, {'error': {'message': 'm'}, 'id': 1})) emit('bad_err_no_message', call(parse, {'error': {'code': -1}, 'id': 1})) emit('bad_err_not_dict', call(parse, {'error': 'boom', 'id': 1})) emit('bad_err_null', call(parse, {'error': None, 'id': 1})) emit('bad_empty_object', call(parse, {})) emit('bad_scalar', call(parse, 7)) emit('bad_none', call(parse, None)) # A STRING HANDED TO `parse' IS THE ONE CASE THE PACKAGE ITSELF DIAGNOSES, and # the message is its own rather than CPython's, so it is asserted verbatim. emit('bad_str_to_parse', callmsg(parse, '{"result":1,"id":1}')) # The guard is `isinstance(deserialized, str)' at the TOP of parse, so it fires # on any string -- including one that is not JSON at all. emit('bad_str_not_json', callmsg(parse, 'hello')) # And it fires through parse_json too, when the JSON document IS a string: # json.loads('"hello"') returns str, which parse then refuses. emit('bad_json_is_string', callmsg(parse_json, '"hello"')) # A JSON document that is a bare number reaches to_response and fails there # instead, with CPython's message. emit('bad_json_is_number', call(parse_json, '42')) # ======================================================== the sentinel ======= # `NOID' is not exported from the top level, so a caller who needs it reaches # into `jsonrpcclient.sentinels'. Its repr is the reason the class exists. emit('noid_repr', repr(sentinels.NOID)) emit('noid_name', sentinels.NOID.name) emit('noid_at_toplevel', 'yes' if hasattr(J, 'NOID') else 'no') emit('noid_identity', 'yes' if sentinels.NOID is sentinels.NOID else 'no') emit('sentinel_distinct', 'yes' if sentinels.Sentinel('NoId') is not sentinels.NOID else 'no') # Passed explicitly, NOID means exactly what the default means: draw one. # (This DOES advance the natural counter -- deliberately placed after # `natural_ids' has been emitted, so it cannot renumber it.) emit('noid_explicit_draws', 'yes' if isinstance( request('m', id=sentinels.NOID)['id'], int) else 'no') # ========================================================== utils.compose ==== emit('compose_two', utils.compose(str, len)([1, 2, 3])) emit('compose_three', utils.compose(str, len, list)('abcd')) emit('compose_one', utils.compose(str)(5)) emit('compose_order', utils.compose(lambda s: s + '!', str.upper)('hi')) emit('compose_kwargs', utils.compose(str, dict)(a=1)) # ====================================================== id_generators ======== # The four generators on their own, away from the request helpers. emit('gen_decimal', ','.join(str(next(_g)) for _g in [id_generators.decimal()] for _ in '123')) _d5 = id_generators.decimal(5) emit('gen_decimal_start', ','.join(str(next(_d5)) for _ in '123')) _h = id_generators.hexadecimal(8) emit('gen_hex_rollover', ','.join(next(_h) for _ in '1234')) _h2 = id_generators.hexadecimal(255) emit('gen_hex_wide', next(_h2)) emit('gen_random_len', len(next(id_generators.random(4)))) _gr = id_generators.random(6, chars='ab') emit('gen_random_chars', 'yes' if all( set(next(_gr)) <= set('ab') for _ in range(20)) else 'no') emit('gen_uuid_shape', '%d/%d' % (len(next(id_generators.uuid())), next(id_generators.uuid()).count('-'))) # decimal() is itertools.count, so it is an iterator and not a list -- the same # laziness `parse' has, in the place it is expected. emit('gen_decimal_type', type(id_generators.decimal()).__name__) # ================================================= the stdlib this reaches === # The RDEPENDS the recipe declares are python3-json, python3-math and # python3-netclient, and each is here because of a MODULE-SCOPE import: # json requests.py:2, responses.py:3 -> python3-json # random id_generators.py:3 (choice) -> python3-math # uuid id_generators.py:6 (uuid4) -> python3-netclient # `string' (id_generators.py:4) is in python3-stringold, which the recipe does # NOT name -- it arrives because python3-netclient rdepends on it. That chain # is asserted on the rootfs by the suite; here we assert only that the modules # import at all, which on an arm with an EMPTY lib-dynload is not free: both # `random' and `uuid' reach C accelerators (_random unguarded, _uuid in a # try/except), and `json' falls back to its pure-Python scanner. for _m in ('json', 'random', 'string', 'uuid', 'itertools', 'functools', 'typing', 'sys'): emit('imports_' + _m, call(lambda n=_m: (__import__(n), 'yes')[1])) emit('json_c_scanner', 'yes' if getattr( __import__('json').decoder, 'c_scanstring', None) is not None else 'no') # =============================================================== hygiene ===== # The package must not have pulled a transport in behind our backs. Measured as # the sys.modules delta rather than read off the imports. # Scored against a FIXED list rather than as `everything outside the standard # library', which on a host control also picks up whatever sitecustomize dragged # in and would make the case say more about the machine than about the package. emit('transport_modules', ','.join(sorted( n for n in ('socket', 'ssl', 'http', 'http.client', 'urllib', 'urllib.request', 'asyncio', 'selectors', 'requests', 'httpx', 'aiohttp', 'websockets') if n in sys.modules)) or 'none') emit('socket_imported', 'yes' if 'socket' in sys.modules else 'no') # The package's own module set, as sys.modules saw it -- six submodules and the # package, and nothing else under that name. emit('own_modules', ','.join(sorted( n for n in sys.modules if n.split('.')[0] == 'jsonrpcclient'))) emit('emitted_count', len(emitted)) print('probe_done=yes')