potluck.explain

Functions for explaining test results.

explain.py

  1"""
  2Functions for explaining test results.
  3
  4explain.py
  5"""
  6
  7import re
  8import textwrap
  9
 10from . import mast
 11from . import harness
 12from . import html_tools
 13from . import phrasing
 14
 15
 16def summarize_parse_error(e):
 17    """
 18    Creates an HTML summary of a parsing-stage error with line number
 19    info if available.
 20    """
 21    # TODO: Line numbers should be links...
 22    line_info = ''
 23    if isinstance(e, (SyntaxError, IndentationError)):
 24        line_info = " (on line {})".format(e.lineno)
 25    elif isinstance(e, mast.MastParseError):
 26        if isinstance(e.trigger, (SyntaxError, IndentationError)):
 27            line_info = " (on line {})".format(e.trigger.lineno)
 28
 29    estring = str(type(e).__name__) + line_info
 30    return (
 31        f"<details>\n<summary>{estring}</summary>\n"
 32        f"<pre>{str(e)}</pre>\n</details>"
 33    )
 34    # TODO: Any file locations to obfuscate in str(e)?
 35
 36
 37def direct_args_repr(*posargs, **kwargs):
 38    """
 39    Returns a string representing the arguments provided, such that if
 40    put in parentheses after a function name, the resulting string could
 41    be evaluated to reproduce a function call (modulo inexact reprs).
 42
 43    Note that with big argument values, this string will be prohibitively
 44    long.
 45    """
 46    return (
 47        ', '.join(repr(arg) for arg in posargs)
 48      + ', '.join(f"{key}={kwargs[key]!r}" for key in kwargs)
 49    )
 50
 51
 52#------------------------#
 53# Docstring manipulation #
 54#------------------------#
 55
 56def grab_docstring_paragraphs(function):
 57    """
 58    Given a function, grab its docstring and split it into paragraphs,
 59    cleaning up indentation. Returns a list of strings. Returns an empty
 60    list if the target function doesn't have a docstring.
 61    """
 62    full_doc = function.__doc__
 63    if full_doc is None:
 64        return []
 65    trimmed = re.sub("\n[ \t]*", "\n", full_doc) # trim indentation
 66    collapsed = re.sub("\n\n+", "\n\n", trimmed) # multiple blanks -> one
 67    return [par.strip() for par in collapsed.split("\n\n")]
 68
 69
 70DOC_WRAP_WIDTH = 73
 71"""
 72The width in characters to wrap docstrings to.
 73"""
 74
 75
 76def add_docstring_paragraphs(base_docstring, paragraphs):
 77    """
 78    Takes a base docstring and a list of strings representing paragraphs
 79    to be added to the target docstring, and returns a new docstring
 80    value with the given paragraphs added that has consistent indentation
 81    and mostly-consistent wrapping.
 82
 83    Indentation is measured from the first line of the given docstring.
 84    """
 85    indent = ''
 86    for char in base_docstring:
 87        if char not in ' \t':
 88            break
 89        indent += char
 90
 91    result = base_docstring
 92    for graf in paragraphs:
 93        wrapped = textwrap.fill(graf, width=DOC_WRAP_WIDTH)
 94        indented = textwrap.indent(wrapped, indent)
 95        result += '\n\n' + indented
 96
 97    return result
 98
 99
100def description_templates_from_docstring(function):
101    """
102    Given a function, inspects its docstring for a paragraph that just
103    says exactly "Description:" and grabs the next 2-4 paragraphs
104    (however many are available) returning them as description
105    templates. Always returns 4 strings, duplicating the 1st and 2nd
106    strings to fill in for missing 3rd and 4th strings, since in 4-part
107    descriptions it's okay for the second two to be copies of the first
108    two.
109
110    If there is no "Description:" paragraph in the target function's
111    docstring, the function's name is used as the title, and the string
112    "Details not provided." is used as the description.
113
114    The rest of this docstring provides an example of expected
115    formatting:
116
117    Description:
118
119    This paragraph will become the description title template.
120
121    This paragraph will become the description details template.
122
123    This paragraph will become the feedback-level description title
124    template.
125
126    This paragraph will become the feedback-level description details
127    template.
128
129    Any further paragraphs, like this one, are not relevant, although
130    putting description stuff last in the docstring is generally a good
131    idea.
132    """
133    paragraphs = grab_docstring_paragraphs(function)
134    if "Description:" in paragraphs:
135        where = paragraphs.index("Description:")
136        parts = paragraphs[where:where + 4]
137    else:
138        parts = []
139
140    if len(parts) == 0:
141        return (
142            function.__name__,
143            "Details not provided.",
144            function.__name__,
145            "Details not provided."
146        )
147    elif len(parts) == 1:
148        return (
149            parts[0],
150            "Details not provided.",
151            parts[0],
152            "Details not provided."
153        )
154    elif len(parts) == 2:
155        return ( parts[0], parts[1], parts[0], parts[1] )
156    elif len(parts) == 3:
157        return ( parts[0], parts[1], parts[2], parts[1] )
158    else: # length is >= 4
159        return ( parts[0], parts[1], parts[2], parts[3] )
160
161
162#------------------------#
163# Automatic descriptions #
164#------------------------#
165
166def code_check_description(
167    limits,
168    short_desc,
169    long_desc,
170    details_prefix=None,
171    verb="use",
172    helper="of"
173):
174    """
175    Creates and returns a 2-item description tuple describing a
176    requirement based on an ImplementationCheck with the given limits.
177    The short and long description arguments should be strings, and
178    should describe what is being looked for. As an example, if a rule
179    looks for a for loop and has a sub-rule that looks for an
180    accumulation pattern, the arguments could be:
181
182    `"a for loop", "a for loop with an accumulator"`
183
184    The details_prefix will be prepended to the details part of the
185    description that is created.
186
187    The verb will be used to talk about the rule and should be in
188    imperative form. For example, for a function call, 'call' could be
189    used instead of 'use'. It will be used along with the helper to
190    describe instances of the pattern being looked for.
191    """
192    Verb = verb.capitalize()
193
194    # Decide topic and details
195    topic = f"{Verb} {short_desc}"
196    if limits[0] in (0, None):
197        if limits[1] is None:
198            # Note: in this case, probably will be a sub-goal?
199            details = f"Each {verb} {helper} {long_desc} will be checked."
200        elif limits[1] == 0:
201            # Change topic:
202            topic = f"Do not {verb} {short_desc}"
203            details = f"Do not {verb} {long_desc}."
204        elif limits[1] == 1:
205            details = f"{Verb} {long_desc} in at most one place."
206        else:
207            details = f"{Verb} {long_desc} in at most {limits[1]} places."
208    elif limits[0] == 1:
209        if limits[1] is None:
210            details = f"{Verb} {long_desc} in at least one place."
211        elif limits[1] == 1:
212            details = f"{Verb} {long_desc} in exactly one place."
213        else:
214            details = (
215                f"{Verb} {long_desc} in at least one and at most "
216                f"{limits[1]} places."
217            )
218    else:
219        if limits[1] is None:
220            details = f"{Verb} {long_desc} in at least {limits[0]} places."
221        elif limits[0] == limits[1]:
222            details = f"{Verb} {long_desc} in exactly {limits[0]} places."
223        else:
224            details = (
225                f"{Verb} {long_desc} in at least {limits[0]} and at "
226                f"most {limits[1]} places."
227            )
228
229    if details_prefix is not None:
230        details = details_prefix + " " + details[0].lower() + details[1:]
231
232    return topic, details
233
234
235def function_call_description(
236    code_tag,
237    details_code,
238    limits,
239    details_prefix=None
240):
241    """
242    Returns a 2-item description tuple describing what is required for
243    an ImplementationCheck with the given limits that matches a function
244    call. The code_tag and details_code arguments should be strings
245    returned from function_call_code_tags. The given details prefix will
246    be prepended to the description details returned.
247    """
248    return code_check_description(
249        limits,
250        code_tag,
251        details_code,
252        details_prefix=details_prefix,
253        verb="call",
254        helper="to"
255    )
256
257
258def payload_description(
259    base_constructor,
260    constructor_args,
261    augmentations,
262    obfuscated=False,
263    topic_repr_limit=80,
264    details_repr_limit=80
265):
266    """
267    Returns an pair of HTML topic and details strings for the test
268    payload that would be constructed using the provided payload
269    constructor, arguments to that constructor (as a dictionary) and
270    augmentations dictionary (mapping augmentation function names to
271    argument dictionaries).
272
273    If `obfuscated` is set to True, an obfuscated description will
274    be returned which avoids key details like which specific
275    arguments are used or what inputs are provided. This also puts
276    the description into the future tense instead of the past tense.
277
278    `topic_repr_limit` and  `details_repr_limit` controls the character
279    counts at which direct representations of arguments are considered
280    too long for the topic (alternative is to omit them) or for the
281    details (alternative is to use a bulleted list of smart reprs).
282    """
283    # Prefix that describes what value(s) are created
284    products = [ "result" ]
285    if "capturing_printed_output" in augmentations:
286        if base_constructor == harness.create_module_import_payload:
287            products = [ "output" ]
288        else:
289            products = [ "result", "output" ]
290
291    if "tracing_function_calls" in augmentations:
292        products.append("process trace")
293
294    what = phrasing.comma_list(products)
295
296    # Tense management
297    was = "was"
298    were = "were"
299    if obfuscated:
300        was = "will be"
301        were = "will be"
302
303    # Base topic/details based on constructor type
304    if base_constructor == harness.create_run_function_payload:
305        fname = constructor_args["fname"]
306        posargs = constructor_args["posargs"]
307        kwargs = constructor_args["kwargs"]
308        # Note: copy_args does not show up in descriptions
309
310        if not posargs and not kwargs: # if there are no arguments
311            topic_repr = f"<code>{fname}</code>"
312            details_repr = topic_repr
313        elif obfuscated:
314            topic_repr = f"<code>{fname}(...)</code>"
315            details_repr = f"<code>{fname}</code> with some arguments"
316        else:
317            direct = html_tools.escape(direct_args_repr(*posargs, **kwargs))
318            if len(fname + direct) > topic_repr_limit:
319                topic_repr = f"<code>{fname}(...)</code>"
320            else:
321                topic_repr = f"<code>{fname}({direct})</code>"
322
323            if len(direct) > details_repr_limit:
324                details_repr = (
325                    f"<code>{fname}</code> with the following"
326                    f" arguments:\n"
327                ) + html_tools.args_repr_list(posargs, kwargs)
328            else:
329                details_repr = f"<code>{fname}({direct})</code>"
330
331        topic = f"The {what} of {topic_repr}"
332        if obfuscated:
333            details = f"We will run {details_repr} and record the {what}."
334        else:
335            details = f"We ran {details_repr} and recorded the {what}."
336
337    elif base_constructor == harness.create_run_harness_payload:
338        test_harness = constructor_args["harness"]
339        fname = constructor_args["fname"]
340        posargs = constructor_args["posargs"]
341        kwargs = constructor_args["kwargs"]
342        # Note: copy_args does not show up in descriptions
343
344        if not posargs and not kwargs: # if there are no arguments
345            topic_args_repr = ""
346            details_args_repr = ""
347        elif obfuscated:
348            topic_args_repr = "(...)"
349            details_args_repr = " with some arguments"
350        else:
351            direct = html_tools.escape(direct_args_repr(*posargs, **kwargs))
352            if len(fname + direct) > topic_repr_limit:
353                topic_args_repr = "(...)"
354            else:
355                topic_args_repr = "(" + direct + ")"
356
357            if len(direct) > details_repr_limit:
358                details_args_repr = (
359                    " with the following arguments:\n"
360                  + html_tools.args_repr_list(posargs, kwargs)
361                )
362            else:
363                details_args_repr = (
364                    f" with arguments: <code>({direct})</code>"
365                )
366
367        (
368            obf_topic, obfuscated,
369            clear_topic, clear
370        ) = harness_descriptions(
371            test_harness,
372            fname,
373            topic_args_repr,
374            details_args_repr,
375            what
376        )
377
378        if obfuscated:
379            topic = obf_topic
380            details = obfuscated
381        else:
382            topic = clear_topic
383            details = clear
384
385    elif base_constructor == harness.create_module_import_payload:
386        prep = constructor_args["prep"]
387        wrap = constructor_args["wrap"]
388        if prep or wrap:
389            mod = " with some modifications"
390        else:
391            mod = ""
392
393        if obfuscated:
394            topic = f"The {what} of your program"
395            details = (
396                f"We will run your submitted code{mod} and record"
397                f" the {what}."
398            )
399        else:
400            topic = f"The {what} of your program"
401            details = (
402                f"We ran your submitted code{mod} and recorded"
403                f" the {what}."
404            )
405
406    elif base_constructor == harness.create_read_variable_payload:
407        varname = constructor_args["varname"]
408        if obfuscated:
409            topic = f"The value of <code>{varname}</code>"
410            details = (
411                f"We will inspect the value of"
412                f" <code>{varname}</code>."
413            )
414        else:
415            topic = f"The value of <code>{varname}</code>"
416            details = (
417                f"We inspected the value of <code>{varname}</code>."
418            )
419
420    else: # unsure what our payload is (this shouldn't happen)...
421        if obfuscated:
422            topic = "A test of your code"
423            details = "We will test your submission."
424        else:
425            topic = "A test of your code"
426            details = "We tested your submission."
427
428    # Assemble details from augmentations
429    testing_details = []
430    if "with_timeout" in augmentations:
431        limit = augmentations["with_timeout"]["time_limit"]
432        if obfuscated:
433            testing_details.append(
434                f"Will be terminated if it takes longer than {limit}s."
435            )
436        else:
437            testing_details.append(f"Ran with a {limit}s time limit.")
438
439    if "capturing_printed_output" in augmentations:
440        errors_too = augmentations["capturing_printed_output"]\
441            .get("capture_errors")
442
443        if errors_too:
444            testing_details.append(
445                f"Printed output and error messages {were} recorded."
446            )
447        else:
448            testing_details.append(f"Printed output {was} recorded.")
449
450    if "with_fake_input" in augmentations:
451        inputs = augmentations["with_fake_input"]["inputs"]
452        policy = augmentations["with_fake_input"]["extra_policy"]
453
454        policy_note = " in a loop" if policy == "loop" else ""
455
456        if obfuscated:
457            topic += " with inputs"
458            testing_details.append("Inputs will be provided")
459        else:
460            listing = ', '.join(
461                f"<code>{html_tools.escape(repr(inp))}</code>"
462                for inp in inputs
463            )
464            inputs_pl = phrasing.plural(len(inputs), "input")
465            was_were = phrasing.plural(len(inputs), "was", "were")
466            proposed = f"{topic} with {inputs_pl}: {listing}"
467            if html_tools.len_as_text(topic + listing) < topic_repr_limit:
468                topic = proposed
469            else:
470                topic += f" with {inputs_pl}"
471            testing_details.append(
472                (
473                    f"The following {inputs_pl} {was_were}"
474                    f" provided{policy_note}:\n"
475                )
476              + html_tools.build_list(
477                    html_tools.dynamic_html_repr(text)
478                    for text in inputs
479                )
480            )
481
482    if "with_module_decorations" in augmentations:
483        args = augmentations["with_module_decorations"]
484        decmap = args["decorations"]
485        testing_details.append(
486            f"Adjustments {were} made to the following functions:\n"
487          + html_tools.build_list(
488                f"<pre>{fn}</pre>"
489                for fn in decmap
490            )
491        )
492
493    if "tracing_function_calls" in augmentations:
494        args = augmentations["tracing_function_calls"]
495        tracing = args["trace_targets"]
496        state_function = args["state_function"]
497        sfdesc = description_templates_from_docstring(state_function)
498        if sfdesc[0] == state_function.__name__:
499            # No custom description provided
500            tracking = ""
501        else:
502            tracking = f" ({sfdesc[0]})"
503
504        testing_details.append(
505            (
506                f"Calls to the following function(s) {were}"
507                f" monitored{tracking}:\n"
508            )
509          + html_tools.build_list(
510              f"<pre>{fn}</pre>"
511              for fn in tracing
512            )
513        )
514
515    if "sampling_distribution_of_results" in augmentations:
516        args = augmentations["sampling_distribution_of_results"]
517        trials = args["trials"]
518        testing_details.append(
519            f"The distribution of results {was} measured across"
520            f" {trials} trials."
521        )
522
523    # Note that we don't need to mention
524    # run_for_base_and_ref_values, as comparing to the solution
525    # value is implied.
526
527    details += (
528        "<br>\nTesting details:"
529      + html_tools.build_list(testing_details)
530    )
531
532    return (topic, details)
533
534
535def harness_descriptions(
536    test_harness,
537    fname,
538    topic_args_repr,
539    details_args_repr,
540    what_is_captured
541):
542    """
543    Extracts descriptions of a test harness from its docstring and
544    formats them using the given function name, topic arguments
545    representation, details arguments representation, and description of
546    what is captured by the test. Returns a description 4-tuple of
547    strings.
548    """
549    hdesc = description_templates_from_docstring(test_harness)
550
551    # Create default description if there is no custom description
552    if hdesc[0] == test_harness.__name__:
553        hdesc = (
554            "Specialized test of <code>{fname}{args}</code>",
555            (
556                "We will test your <code>{fname}</code>{args} using"
557              + " <code>" + hdesc[0] + "</code>, recording the"
558              + " {captured}."
559            ),
560            "Specialized test of <code>{fname}{args}</code>",
561            (
562                "We tested your <code>{fname}</code>{args} using"
563              + " <code>" + hdesc[0] + "</code>, recording the"
564              + " {captured}."
565            )
566        )
567
568    args_for_parts = [
569        topic_args_repr,
570        details_args_repr,
571        topic_args_repr,
572        details_args_repr,
573    ]
574    result = tuple(
575        part.format(fname=fname, args=args_repr, captured=what_is_captured)
576        for part, args_repr in zip(hdesc, args_for_parts)
577    )
578
579    return result
def summarize_parse_error(e):
17def summarize_parse_error(e):
18    """
19    Creates an HTML summary of a parsing-stage error with line number
20    info if available.
21    """
22    # TODO: Line numbers should be links...
23    line_info = ''
24    if isinstance(e, (SyntaxError, IndentationError)):
25        line_info = " (on line {})".format(e.lineno)
26    elif isinstance(e, mast.MastParseError):
27        if isinstance(e.trigger, (SyntaxError, IndentationError)):
28            line_info = " (on line {})".format(e.trigger.lineno)
29
30    estring = str(type(e).__name__) + line_info
31    return (
32        f"<details>\n<summary>{estring}</summary>\n"
33        f"<pre>{str(e)}</pre>\n</details>"
34    )
35    # TODO: Any file locations to obfuscate in str(e)?

Creates an HTML summary of a parsing-stage error with line number info if available.

def direct_args_repr(*posargs, **kwargs):
38def direct_args_repr(*posargs, **kwargs):
39    """
40    Returns a string representing the arguments provided, such that if
41    put in parentheses after a function name, the resulting string could
42    be evaluated to reproduce a function call (modulo inexact reprs).
43
44    Note that with big argument values, this string will be prohibitively
45    long.
46    """
47    return (
48        ', '.join(repr(arg) for arg in posargs)
49      + ', '.join(f"{key}={kwargs[key]!r}" for key in kwargs)
50    )

Returns a string representing the arguments provided, such that if put in parentheses after a function name, the resulting string could be evaluated to reproduce a function call (modulo inexact reprs).

Note that with big argument values, this string will be prohibitively long.

def grab_docstring_paragraphs(function):
57def grab_docstring_paragraphs(function):
58    """
59    Given a function, grab its docstring and split it into paragraphs,
60    cleaning up indentation. Returns a list of strings. Returns an empty
61    list if the target function doesn't have a docstring.
62    """
63    full_doc = function.__doc__
64    if full_doc is None:
65        return []
66    trimmed = re.sub("\n[ \t]*", "\n", full_doc) # trim indentation
67    collapsed = re.sub("\n\n+", "\n\n", trimmed) # multiple blanks -> one
68    return [par.strip() for par in collapsed.split("\n\n")]

Given a function, grab its docstring and split it into paragraphs, cleaning up indentation. Returns a list of strings. Returns an empty list if the target function doesn't have a docstring.

DOC_WRAP_WIDTH = 73

The width in characters to wrap docstrings to.

def add_docstring_paragraphs(base_docstring, paragraphs):
77def add_docstring_paragraphs(base_docstring, paragraphs):
78    """
79    Takes a base docstring and a list of strings representing paragraphs
80    to be added to the target docstring, and returns a new docstring
81    value with the given paragraphs added that has consistent indentation
82    and mostly-consistent wrapping.
83
84    Indentation is measured from the first line of the given docstring.
85    """
86    indent = ''
87    for char in base_docstring:
88        if char not in ' \t':
89            break
90        indent += char
91
92    result = base_docstring
93    for graf in paragraphs:
94        wrapped = textwrap.fill(graf, width=DOC_WRAP_WIDTH)
95        indented = textwrap.indent(wrapped, indent)
96        result += '\n\n' + indented
97
98    return result

Takes a base docstring and a list of strings representing paragraphs to be added to the target docstring, and returns a new docstring value with the given paragraphs added that has consistent indentation and mostly-consistent wrapping.

Indentation is measured from the first line of the given docstring.

def description_templates_from_docstring(function):
101def description_templates_from_docstring(function):
102    """
103    Given a function, inspects its docstring for a paragraph that just
104    says exactly "Description:" and grabs the next 2-4 paragraphs
105    (however many are available) returning them as description
106    templates. Always returns 4 strings, duplicating the 1st and 2nd
107    strings to fill in for missing 3rd and 4th strings, since in 4-part
108    descriptions it's okay for the second two to be copies of the first
109    two.
110
111    If there is no "Description:" paragraph in the target function's
112    docstring, the function's name is used as the title, and the string
113    "Details not provided." is used as the description.
114
115    The rest of this docstring provides an example of expected
116    formatting:
117
118    Description:
119
120    This paragraph will become the description title template.
121
122    This paragraph will become the description details template.
123
124    This paragraph will become the feedback-level description title
125    template.
126
127    This paragraph will become the feedback-level description details
128    template.
129
130    Any further paragraphs, like this one, are not relevant, although
131    putting description stuff last in the docstring is generally a good
132    idea.
133    """
134    paragraphs = grab_docstring_paragraphs(function)
135    if "Description:" in paragraphs:
136        where = paragraphs.index("Description:")
137        parts = paragraphs[where:where + 4]
138    else:
139        parts = []
140
141    if len(parts) == 0:
142        return (
143            function.__name__,
144            "Details not provided.",
145            function.__name__,
146            "Details not provided."
147        )
148    elif len(parts) == 1:
149        return (
150            parts[0],
151            "Details not provided.",
152            parts[0],
153            "Details not provided."
154        )
155    elif len(parts) == 2:
156        return ( parts[0], parts[1], parts[0], parts[1] )
157    elif len(parts) == 3:
158        return ( parts[0], parts[1], parts[2], parts[1] )
159    else: # length is >= 4
160        return ( parts[0], parts[1], parts[2], parts[3] )

Given a function, inspects its docstring for a paragraph that just says exactly "Description:" and grabs the next 2-4 paragraphs (however many are available) returning them as description templates. Always returns 4 strings, duplicating the 1st and 2nd strings to fill in for missing 3rd and 4th strings, since in 4-part descriptions it's okay for the second two to be copies of the first two.

If there is no "Description:" paragraph in the target function's docstring, the function's name is used as the title, and the string "Details not provided." is used as the description.

The rest of this docstring provides an example of expected formatting:

Description:

This paragraph will become the description title template.

This paragraph will become the description details template.

This paragraph will become the feedback-level description title template.

This paragraph will become the feedback-level description details template.

Any further paragraphs, like this one, are not relevant, although putting description stuff last in the docstring is generally a good idea.

def code_check_description( limits, short_desc, long_desc, details_prefix=None, verb='use', helper='of'):
167def code_check_description(
168    limits,
169    short_desc,
170    long_desc,
171    details_prefix=None,
172    verb="use",
173    helper="of"
174):
175    """
176    Creates and returns a 2-item description tuple describing a
177    requirement based on an ImplementationCheck with the given limits.
178    The short and long description arguments should be strings, and
179    should describe what is being looked for. As an example, if a rule
180    looks for a for loop and has a sub-rule that looks for an
181    accumulation pattern, the arguments could be:
182
183    `"a for loop", "a for loop with an accumulator"`
184
185    The details_prefix will be prepended to the details part of the
186    description that is created.
187
188    The verb will be used to talk about the rule and should be in
189    imperative form. For example, for a function call, 'call' could be
190    used instead of 'use'. It will be used along with the helper to
191    describe instances of the pattern being looked for.
192    """
193    Verb = verb.capitalize()
194
195    # Decide topic and details
196    topic = f"{Verb} {short_desc}"
197    if limits[0] in (0, None):
198        if limits[1] is None:
199            # Note: in this case, probably will be a sub-goal?
200            details = f"Each {verb} {helper} {long_desc} will be checked."
201        elif limits[1] == 0:
202            # Change topic:
203            topic = f"Do not {verb} {short_desc}"
204            details = f"Do not {verb} {long_desc}."
205        elif limits[1] == 1:
206            details = f"{Verb} {long_desc} in at most one place."
207        else:
208            details = f"{Verb} {long_desc} in at most {limits[1]} places."
209    elif limits[0] == 1:
210        if limits[1] is None:
211            details = f"{Verb} {long_desc} in at least one place."
212        elif limits[1] == 1:
213            details = f"{Verb} {long_desc} in exactly one place."
214        else:
215            details = (
216                f"{Verb} {long_desc} in at least one and at most "
217                f"{limits[1]} places."
218            )
219    else:
220        if limits[1] is None:
221            details = f"{Verb} {long_desc} in at least {limits[0]} places."
222        elif limits[0] == limits[1]:
223            details = f"{Verb} {long_desc} in exactly {limits[0]} places."
224        else:
225            details = (
226                f"{Verb} {long_desc} in at least {limits[0]} and at "
227                f"most {limits[1]} places."
228            )
229
230    if details_prefix is not None:
231        details = details_prefix + " " + details[0].lower() + details[1:]
232
233    return topic, details

Creates and returns a 2-item description tuple describing a requirement based on an ImplementationCheck with the given limits. The short and long description arguments should be strings, and should describe what is being looked for. As an example, if a rule looks for a for loop and has a sub-rule that looks for an accumulation pattern, the arguments could be:

"a for loop", "a for loop with an accumulator"

The details_prefix will be prepended to the details part of the description that is created.

The verb will be used to talk about the rule and should be in imperative form. For example, for a function call, 'call' could be used instead of 'use'. It will be used along with the helper to describe instances of the pattern being looked for.

def function_call_description(code_tag, details_code, limits, details_prefix=None):
236def function_call_description(
237    code_tag,
238    details_code,
239    limits,
240    details_prefix=None
241):
242    """
243    Returns a 2-item description tuple describing what is required for
244    an ImplementationCheck with the given limits that matches a function
245    call. The code_tag and details_code arguments should be strings
246    returned from function_call_code_tags. The given details prefix will
247    be prepended to the description details returned.
248    """
249    return code_check_description(
250        limits,
251        code_tag,
252        details_code,
253        details_prefix=details_prefix,
254        verb="call",
255        helper="to"
256    )

Returns a 2-item description tuple describing what is required for an ImplementationCheck with the given limits that matches a function call. The code_tag and details_code arguments should be strings returned from function_call_code_tags. The given details prefix will be prepended to the description details returned.

def payload_description( base_constructor, constructor_args, augmentations, obfuscated=False, topic_repr_limit=80, details_repr_limit=80):
259def payload_description(
260    base_constructor,
261    constructor_args,
262    augmentations,
263    obfuscated=False,
264    topic_repr_limit=80,
265    details_repr_limit=80
266):
267    """
268    Returns an pair of HTML topic and details strings for the test
269    payload that would be constructed using the provided payload
270    constructor, arguments to that constructor (as a dictionary) and
271    augmentations dictionary (mapping augmentation function names to
272    argument dictionaries).
273
274    If `obfuscated` is set to True, an obfuscated description will
275    be returned which avoids key details like which specific
276    arguments are used or what inputs are provided. This also puts
277    the description into the future tense instead of the past tense.
278
279    `topic_repr_limit` and  `details_repr_limit` controls the character
280    counts at which direct representations of arguments are considered
281    too long for the topic (alternative is to omit them) or for the
282    details (alternative is to use a bulleted list of smart reprs).
283    """
284    # Prefix that describes what value(s) are created
285    products = [ "result" ]
286    if "capturing_printed_output" in augmentations:
287        if base_constructor == harness.create_module_import_payload:
288            products = [ "output" ]
289        else:
290            products = [ "result", "output" ]
291
292    if "tracing_function_calls" in augmentations:
293        products.append("process trace")
294
295    what = phrasing.comma_list(products)
296
297    # Tense management
298    was = "was"
299    were = "were"
300    if obfuscated:
301        was = "will be"
302        were = "will be"
303
304    # Base topic/details based on constructor type
305    if base_constructor == harness.create_run_function_payload:
306        fname = constructor_args["fname"]
307        posargs = constructor_args["posargs"]
308        kwargs = constructor_args["kwargs"]
309        # Note: copy_args does not show up in descriptions
310
311        if not posargs and not kwargs: # if there are no arguments
312            topic_repr = f"<code>{fname}</code>"
313            details_repr = topic_repr
314        elif obfuscated:
315            topic_repr = f"<code>{fname}(...)</code>"
316            details_repr = f"<code>{fname}</code> with some arguments"
317        else:
318            direct = html_tools.escape(direct_args_repr(*posargs, **kwargs))
319            if len(fname + direct) > topic_repr_limit:
320                topic_repr = f"<code>{fname}(...)</code>"
321            else:
322                topic_repr = f"<code>{fname}({direct})</code>"
323
324            if len(direct) > details_repr_limit:
325                details_repr = (
326                    f"<code>{fname}</code> with the following"
327                    f" arguments:\n"
328                ) + html_tools.args_repr_list(posargs, kwargs)
329            else:
330                details_repr = f"<code>{fname}({direct})</code>"
331
332        topic = f"The {what} of {topic_repr}"
333        if obfuscated:
334            details = f"We will run {details_repr} and record the {what}."
335        else:
336            details = f"We ran {details_repr} and recorded the {what}."
337
338    elif base_constructor == harness.create_run_harness_payload:
339        test_harness = constructor_args["harness"]
340        fname = constructor_args["fname"]
341        posargs = constructor_args["posargs"]
342        kwargs = constructor_args["kwargs"]
343        # Note: copy_args does not show up in descriptions
344
345        if not posargs and not kwargs: # if there are no arguments
346            topic_args_repr = ""
347            details_args_repr = ""
348        elif obfuscated:
349            topic_args_repr = "(...)"
350            details_args_repr = " with some arguments"
351        else:
352            direct = html_tools.escape(direct_args_repr(*posargs, **kwargs))
353            if len(fname + direct) > topic_repr_limit:
354                topic_args_repr = "(...)"
355            else:
356                topic_args_repr = "(" + direct + ")"
357
358            if len(direct) > details_repr_limit:
359                details_args_repr = (
360                    " with the following arguments:\n"
361                  + html_tools.args_repr_list(posargs, kwargs)
362                )
363            else:
364                details_args_repr = (
365                    f" with arguments: <code>({direct})</code>"
366                )
367
368        (
369            obf_topic, obfuscated,
370            clear_topic, clear
371        ) = harness_descriptions(
372            test_harness,
373            fname,
374            topic_args_repr,
375            details_args_repr,
376            what
377        )
378
379        if obfuscated:
380            topic = obf_topic
381            details = obfuscated
382        else:
383            topic = clear_topic
384            details = clear
385
386    elif base_constructor == harness.create_module_import_payload:
387        prep = constructor_args["prep"]
388        wrap = constructor_args["wrap"]
389        if prep or wrap:
390            mod = " with some modifications"
391        else:
392            mod = ""
393
394        if obfuscated:
395            topic = f"The {what} of your program"
396            details = (
397                f"We will run your submitted code{mod} and record"
398                f" the {what}."
399            )
400        else:
401            topic = f"The {what} of your program"
402            details = (
403                f"We ran your submitted code{mod} and recorded"
404                f" the {what}."
405            )
406
407    elif base_constructor == harness.create_read_variable_payload:
408        varname = constructor_args["varname"]
409        if obfuscated:
410            topic = f"The value of <code>{varname}</code>"
411            details = (
412                f"We will inspect the value of"
413                f" <code>{varname}</code>."
414            )
415        else:
416            topic = f"The value of <code>{varname}</code>"
417            details = (
418                f"We inspected the value of <code>{varname}</code>."
419            )
420
421    else: # unsure what our payload is (this shouldn't happen)...
422        if obfuscated:
423            topic = "A test of your code"
424            details = "We will test your submission."
425        else:
426            topic = "A test of your code"
427            details = "We tested your submission."
428
429    # Assemble details from augmentations
430    testing_details = []
431    if "with_timeout" in augmentations:
432        limit = augmentations["with_timeout"]["time_limit"]
433        if obfuscated:
434            testing_details.append(
435                f"Will be terminated if it takes longer than {limit}s."
436            )
437        else:
438            testing_details.append(f"Ran with a {limit}s time limit.")
439
440    if "capturing_printed_output" in augmentations:
441        errors_too = augmentations["capturing_printed_output"]\
442            .get("capture_errors")
443
444        if errors_too:
445            testing_details.append(
446                f"Printed output and error messages {were} recorded."
447            )
448        else:
449            testing_details.append(f"Printed output {was} recorded.")
450
451    if "with_fake_input" in augmentations:
452        inputs = augmentations["with_fake_input"]["inputs"]
453        policy = augmentations["with_fake_input"]["extra_policy"]
454
455        policy_note = " in a loop" if policy == "loop" else ""
456
457        if obfuscated:
458            topic += " with inputs"
459            testing_details.append("Inputs will be provided")
460        else:
461            listing = ', '.join(
462                f"<code>{html_tools.escape(repr(inp))}</code>"
463                for inp in inputs
464            )
465            inputs_pl = phrasing.plural(len(inputs), "input")
466            was_were = phrasing.plural(len(inputs), "was", "were")
467            proposed = f"{topic} with {inputs_pl}: {listing}"
468            if html_tools.len_as_text(topic + listing) < topic_repr_limit:
469                topic = proposed
470            else:
471                topic += f" with {inputs_pl}"
472            testing_details.append(
473                (
474                    f"The following {inputs_pl} {was_were}"
475                    f" provided{policy_note}:\n"
476                )
477              + html_tools.build_list(
478                    html_tools.dynamic_html_repr(text)
479                    for text in inputs
480                )
481            )
482
483    if "with_module_decorations" in augmentations:
484        args = augmentations["with_module_decorations"]
485        decmap = args["decorations"]
486        testing_details.append(
487            f"Adjustments {were} made to the following functions:\n"
488          + html_tools.build_list(
489                f"<pre>{fn}</pre>"
490                for fn in decmap
491            )
492        )
493
494    if "tracing_function_calls" in augmentations:
495        args = augmentations["tracing_function_calls"]
496        tracing = args["trace_targets"]
497        state_function = args["state_function"]
498        sfdesc = description_templates_from_docstring(state_function)
499        if sfdesc[0] == state_function.__name__:
500            # No custom description provided
501            tracking = ""
502        else:
503            tracking = f" ({sfdesc[0]})"
504
505        testing_details.append(
506            (
507                f"Calls to the following function(s) {were}"
508                f" monitored{tracking}:\n"
509            )
510          + html_tools.build_list(
511              f"<pre>{fn}</pre>"
512              for fn in tracing
513            )
514        )
515
516    if "sampling_distribution_of_results" in augmentations:
517        args = augmentations["sampling_distribution_of_results"]
518        trials = args["trials"]
519        testing_details.append(
520            f"The distribution of results {was} measured across"
521            f" {trials} trials."
522        )
523
524    # Note that we don't need to mention
525    # run_for_base_and_ref_values, as comparing to the solution
526    # value is implied.
527
528    details += (
529        "<br>\nTesting details:"
530      + html_tools.build_list(testing_details)
531    )
532
533    return (topic, details)

Returns an pair of HTML topic and details strings for the test payload that would be constructed using the provided payload constructor, arguments to that constructor (as a dictionary) and augmentations dictionary (mapping augmentation function names to argument dictionaries).

If obfuscated is set to True, an obfuscated description will be returned which avoids key details like which specific arguments are used or what inputs are provided. This also puts the description into the future tense instead of the past tense.

topic_repr_limit and details_repr_limit controls the character counts at which direct representations of arguments are considered too long for the topic (alternative is to omit them) or for the details (alternative is to use a bulleted list of smart reprs).

def harness_descriptions( test_harness, fname, topic_args_repr, details_args_repr, what_is_captured):
536def harness_descriptions(
537    test_harness,
538    fname,
539    topic_args_repr,
540    details_args_repr,
541    what_is_captured
542):
543    """
544    Extracts descriptions of a test harness from its docstring and
545    formats them using the given function name, topic arguments
546    representation, details arguments representation, and description of
547    what is captured by the test. Returns a description 4-tuple of
548    strings.
549    """
550    hdesc = description_templates_from_docstring(test_harness)
551
552    # Create default description if there is no custom description
553    if hdesc[0] == test_harness.__name__:
554        hdesc = (
555            "Specialized test of <code>{fname}{args}</code>",
556            (
557                "We will test your <code>{fname}</code>{args} using"
558              + " <code>" + hdesc[0] + "</code>, recording the"
559              + " {captured}."
560            ),
561            "Specialized test of <code>{fname}{args}</code>",
562            (
563                "We tested your <code>{fname}</code>{args} using"
564              + " <code>" + hdesc[0] + "</code>, recording the"
565              + " {captured}."
566            )
567        )
568
569    args_for_parts = [
570        topic_args_repr,
571        details_args_repr,
572        topic_args_repr,
573        details_args_repr,
574    ]
575    result = tuple(
576        part.format(fname=fname, args=args_repr, captured=what_is_captured)
577        for part, args_repr in zip(hdesc, args_for_parts)
578    )
579
580    return result

Extracts descriptions of a test harness from its docstring and formats them using the given function name, topic arguments representation, details arguments representation, and description of what is captured by the test. Returns a description 4-tuple of strings.