File: helpers.py

package info (click to toggle)
diff-cover 10.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,256 kB
  • sloc: python: 6,452; xml: 218; cpp: 18; sh: 12; makefile: 10
file content (212 lines) | stat: -rw-r--r-- 5,799 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
"""
Test helper functions.
"""

import os.path
import random

HUNK_BUFFER = 2
MAX_LINE_LENGTH = 300
LINE_STRINGS = ["test", "+ has a plus sign", "- has a minus sign"]


def fixture_path(rel_path):
    """
    Returns the absolute path to a fixture file
    given `rel_path` relative to the fixture directory.
    """
    fixture_dir = os.path.join(os.path.dirname(__file__), "fixtures")
    return os.path.join(fixture_dir, rel_path)


def load_fixture(rel_path, encoding=None):
    """
    Return the contents of the file at `rel_path`
    (relative path to the "fixtures" directory).

    If `encoding` is not None, attempts to decode
    the contents as `encoding` (e.g. 'utf-8').
    """
    with open(fixture_path(rel_path), encoding=encoding or "utf-8") as fixture_file:
        contents = fixture_file.read()

    if encoding is not None and isinstance(contents, bytes):
        contents = contents.decode(encoding)

    return contents


def line_numbers(start, end):
    """
    Return a list of line numbers, in [start, end] (inclusive).
    """
    return list(range(start, end + 1))


def git_diff_output(diff_dict, deleted_files=None):
    """
    Construct fake output from `git diff` using the description
    defined by `diff_dict`, which is a dictionary of the form:

        {
            SRC_FILE_NAME: MODIFIED_LINES,
            ...
        }

    where `SRC_FILE_NAME` is the name of a source file in the diff,
    and `MODIFIED_LINES` is a list of lines added or changed in the
    source file.

    `deleted_files` is a list of files that have been deleted

    The content of the source files are randomly generated.

    Returns a byte string.
    """

    output = []

    # Entries for deleted files
    output.extend(_deleted_file_entries(deleted_files))

    # Entries for source files
    for src_file, modified_lines in diff_dict.items():
        output.extend(_source_file_entry(src_file, modified_lines))

    return "\n".join(output)


def _deleted_file_entries(deleted_files):
    """
    Create fake `git diff` output for files that have been
    deleted in this changeset.

    `deleted_files` is a list of files deleted in the changeset.

    Returns a list of lines in the diff output.
    """

    output = []

    if deleted_files is not None:
        for src_file in deleted_files:
            # File information
            output.append(f"diff --git a/{src_file} b/{src_file}")
            output.append("index 629e8ad..91b8c0a 100644")
            output.append(f"--- a/{src_file}")
            output.append("+++ b/dev/null")

            # Choose a random number of lines
            num_lines = random.randint(1, 30)

            # Hunk information
            output.append(f"@@ -0,{num_lines} +0,0 @@")
            output.extend(["-" + _random_string() for _ in range(num_lines)])

    return output


def _source_file_entry(src_file, modified_lines):
    """
    Create fake `git diff` output for added/modified lines.

    `src_file` is the source file with the changes;
    `modified_lines` is the list of modified line numbers.

    Returns a list of lines in the diff output.
    """

    output = []

    # Line for the file names
    output.append(f"diff --git a/{src_file} b/{src_file}")

    # Index line
    output.append("index 629e8ad..91b8c0a 100644")

    # Additions/deletions
    output.append(f"--- a/{src_file}")
    output.append(f"+++ b/{src_file}")

    # Hunk information
    for start, end in _hunks(modified_lines):
        output.extend(_hunk_entry(start, end, modified_lines))

    return output


def _hunk_entry(start, end, modified_lines):
    """
    Generates fake `git diff` output for a hunk,
    where `start` and `end` are the start/end lines of the hunk
    and `modified_lines` is a list of modified lines in the hunk.

    Just as `git diff` does, this will include a few lines before/after
    the changed lines in each hunk.
    """
    output = []

    # The actual hunk usually has a few lines before/after
    start -= HUNK_BUFFER
    end += HUNK_BUFFER

    start = max(start, 0)

    # Hunk definition line
    # Real `git diff` output would have different line numbers
    # for before/after the change, but since we're only interested
    # in after the change, we use the same numbers for both.
    length = end - start
    output.append(f"@@ -{start},{length} +{start},{length} @@")

    # Output line modifications
    for line_number in range(start, end + 1):
        # This is a changed line, so prepend a + sign
        if line_number in modified_lines:
            # Delete the old line
            output.append("-" + _random_string())

            # Include the changed line
            output.append("+" + _random_string())

        # This is a line we didn't modify, so no + or - signs
        # but prepend with a space.
        else:
            output.append(" " + _random_string())

    return output


def _hunks(modified_lines):
    """
    Given a list of line numbers, return a list of hunks represented
    as `(start, end)` tuples.
    """

    # Identify contiguous lines as hunks
    hunks = []
    last_line = None

    for line in sorted(modified_lines):
        # If this is contiguous with the last line, continue the hunk
        # We're guaranteed at this point to have at least one hunk
        if (line - 1) == last_line:
            start, _ = hunks[-1]
            hunks[-1] = (start, line)

        # If non-contiguous, start a new hunk with just the current line
        else:
            hunks.append((line, line))

        # Store the last line
        last_line = line

    return hunks


def _random_string():
    """
    Return a random byte string with length in the range
    [0, `MAX_LINE_LENGTH`] (inclusive).
    """
    return random.choice(LINE_STRINGS)