Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

unittest assertSequenceEqual can lead to Difflib.compare() crashing on mostly different sequences #65452

Open
nnja opened this issue Apr 16, 2014 · 8 comments
Labels
3.7 only security fixes 3.8 only security fixes stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error

Comments

@nnja
Copy link
Contributor

nnja commented Apr 16, 2014

BPO 21253
Nosy @tim-one, @terryjreedy, @gpshead, @bitdancer, @jftuga, @nnja, @ilevkivskyi
Files
  • diff_bug34.py
  • Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

    Show more details

    GitHub fields:

    assignee = None
    closed_at = None
    created_at = <Date 2014-04-16.15:54:46.162>
    labels = ['3.7', '3.8', 'type-bug', 'library']
    title = 'unittest assertSequenceEqual can lead to Difflib.compare() crashing on mostly different sequences'
    updated_at = <Date 2019-03-08.15:56:27.847>
    user = 'https://github.com/nnja'

    bugs.python.org fields:

    activity = <Date 2019-03-08.15:56:27.847>
    actor = 'Ernesto Eduardo Medina N\xc3\xba\xc3\xb1ez'
    assignee = 'none'
    closed = False
    closed_date = None
    closer = None
    components = ['Library (Lib)']
    creation = <Date 2014-04-16.15:54:46.162>
    creator = 'nnja'
    dependencies = []
    files = ['34911']
    hgrepos = []
    issue_num = 21253
    keywords = []
    message_count = 7.0
    messages = ['216479', '216809', '216816', '216830', '216835', '248905', '337498']
    nosy_count = 8.0
    nosy_names = ['tim.peters', 'terry.reedy', 'gregory.p.smith', 'r.david.murray', 'jftuga', 'nnja', 'levkivskyi', 'Ernesto Eduardo Medina N\xc3\xba\xc3\xb1ez']
    pr_nums = []
    priority = 'normal'
    resolution = None
    stage = None
    status = 'open'
    superseder = None
    type = 'behavior'
    url = 'https://bugs.python.org/issue21253'
    versions = ['Python 2.7', 'Python 3.6', 'Python 3.7', 'Python 3.8']

    @nnja
    Copy link
    Contributor Author

    nnja commented Apr 16, 2014

    When difflib.compare() is used on two moderately large sequences with little or no common elements, a RuntimeError: maximum recursion depth exceeded occurs.

    This error became apparent when testing another bug (see: bpo-19217) in the AssertEquals() method of the unit test library.

    A sample program to reproduce this issue in 3.4 is attached. To repo in 2.7 remove the list() wrapper from the range call.

    @nnja nnja added type-crash A hard crash of the interpreter, possibly with a core dump stdlib Python modules in the Lib dir labels Apr 16, 2014
    @nnja nnja changed the title Difflib Difflib.compare() crashes when sequences contain little or no common elements Apr 16, 2014
    @terryjreedy
    Copy link
    Member

    An obvious fix for the recursion limit error is to convert the .compare recursion to iteration with a stack (which I could try to do), but I don't know if the deep recursion is expected in this case, or is a bug. Tim?

    @terryjreedy terryjreedy changed the title Difflib.compare() crashes when sequences contain little or no common elements Difflib.compare() crashes on mostly different sequences Apr 18, 2014
    @gpshead
    Copy link
    Member

    gpshead commented Apr 18, 2014

    It appears to devolve into linear recursion in this case, one per each item in one of the sequences being searched for a match, so even using a stack seems wrong as it'd still be linear (though it would prevent the recursion depth problem).

    The mutual _fancy_replace + _fancy_helper linear recursion comes from http://hg.python.org/cpython/file/604b74f9a07d/Lib/difflib.py#l1021

    @tim-one
    Copy link
    Member

    tim-one commented Apr 19, 2014

    Comparison can certainly trigger a recursion error if the sequences contain no (or few) matching non-junk elements, but contain many "almost matching" elements. If the sequences have lengths M and N, recursion can go as deep as 2*min(M, N) then.

    Now in the test case, we have two lists of integers. Difflib has no idea what "almost match" might mean for integers. But difflib isn't passed two lists of integers. Instead unittest appears to be converting the input lists to giant strings, then splitting the giant strings on whitespace (or just linefeeds?), and then feeding the resulting lists of substrings to difflib. That doesn't make much sense to me, but so it goes.

    There are no matches in the two lists of strings, so difflib starts looking for "close matches", and there are a lot of these.

    At first it decides "[1," and "[100," aren't close enough, but " 10," and " 101," are close enough. That's used as a synch point, and then there's recursion to match the sublists before and after the synch point. Then " 12," and " 102," are close enough, so that pair is used as the next synch point, and another layer of 2-sided recursion. Etc.

    Whether someone wants to rip the recursion out of _fancy_replace and _fancy_helper is up to them. I wouldn't bother, if this unittest-created problem is the only reported instance. Comparing strings seems a poor idea from the start (there's no guarantee in general, e.g., that A != B implies str(A) != str(B) or repr(A) != repr(B)), and difflib isn't good in any case at comparing sequences with few matching elements (e.g., remove the recursion and it will still take time at best cubic in the common length of the sequences - would it really help to change "a failing unittest bombs with RecursionError" to "a failing unittest seems to take forever"?).

    I'd suggest instead that unittest, say, locate the first pair of non-equal elements itself, and display that along with a few elements of context on either side. Or something ;-) Something worst-case linear-time, and using != directly on sequence elements (not on strings derived from the sequence elements).

    @gpshead
    Copy link
    Member

    gpshead commented Apr 19, 2014

    that seems reasonable. unittest's assertSequenceEqual is using this to attempt to display a useful error message as to what the delta was; it should try harder to avoid difflib corner cases.

    At the very least, unittest should recover from a difflib failure and report a test failure without the possibly nicer message.

    @gpshead gpshead changed the title Difflib.compare() crashes on mostly different sequences unittest assertSequenceEqual can lead to Difflib.compare() crashing on mostly different sequences Apr 19, 2014
    @jftuga
    Copy link
    Mannequin

    jftuga mannequin commented Aug 20, 2015

    I am seeing something similar in difflib.HtmlDiff.make_file() under Python 3.4.3 (windows 7). Do I need to file a separate bug report?

    File "H:\test\test.py", line 522, in print_differ
    diff = html.make_file(file1_data,file2_data,"dir 1","dir 2",True)
    File "C:\Python34\lib\difflib.py", line 1713, in make_file
    context=context,numlines=numlines))
    File "C:\Python34\lib\difflib.py", line 1962, in make_table
    fromlist,tolist,flaglist = self._collect_lines(diffs)
    File "C:\Python34\lib\difflib.py", line 1830, in _collect_lines
    for fromdata,todata,flag in diffs:
    File "C:\Python34\lib\difflib.py", line 1806, in _line_wrapper
    self._split_line(fromlist,fromline,fromtext)
    File "C:\Python34\lib\difflib.py", line 1791, in _split_line
    self._split_line(data_list,'>',line2)

    (repeated many times)

    File "C:\Python34\lib\difflib.py", line 1791, in _split_line
    self._split_line(data_list,'>',line2)
    File "C:\Python34\lib\difflib.py", line 1755, in _split_line
    if (size <= max) or ((size -(text.count('\0')*3)) <= max):
    RuntimeError: maximum recursion depth exceeded in comparison

    @ilevkivskyi ilevkivskyi added the 3.7 only security fixes label Mar 18, 2017
    @serhiy-storchaka serhiy-storchaka added 3.8 only security fixes type-bug An unexpected behavior, bug, or error and removed type-crash A hard crash of the interpreter, possibly with a core dump labels Jul 11, 2018
    @ErnestoEduardoMedinaNez
    Copy link
    Mannequin

    ErnestoEduardoMedinaNez mannequin commented Mar 8, 2019

    While this gets fixed, can you provide a workaround? or recommend another library?

    @ezio-melotti ezio-melotti transferred this issue from another repository Apr 10, 2022
    @gpshead
    Copy link
    Member

    gpshead commented Nov 7, 2022

    workaround idea? https://pypi.org/project/diff-match-patch/ exists and seems maybe maintained (updated in 2020), though I personally haven't used it.

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
    Labels
    3.7 only security fixes 3.8 only security fixes stdlib Python modules in the Lib dir type-bug An unexpected behavior, bug, or error
    Projects
    Status: No status
    Development

    No branches or pull requests

    6 participants