summaryrefslogtreecommitdiff
path: root/tests/test_main.py
blob: da91fdd845c9f3207851f6103e39a7951fcce31d (plain)
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
213
214
215
216
217
218
219
220
221
222
# -*- coding: utf-8 -*-
"""\
Test the natsort command-line tool functions.
"""

import re
import sys

import pytest
from hypothesis import given
from hypothesis.strategies import data, floats, integers, lists
from natsort.__main__ import (
    check_filters,
    keep_entry_range,
    keep_entry_value,
    main,
    range_check,
    sort_and_print_entries,
)


def test_main_passes_default_arguments_with_no_command_line_options(mocker):
    p = mocker.patch("natsort.__main__.sort_and_print_entries")
    main("num-2", "num-6", "num-1")
    args = p.call_args[0][1]
    assert not args.paths
    assert args.filter is None
    assert args.reverse_filter is None
    assert args.exclude is None
    assert not args.reverse
    assert args.number_type == "int"
    assert not args.signed
    assert args.exp
    assert not args.locale


def test_main_passes_arguments_with_all_command_line_options(mocker):
    arguments = ["--paths", "--reverse", "--locale"]
    arguments.extend(["--filter", "4", "10"])
    arguments.extend(["--reverse-filter", "100", "110"])
    arguments.extend(["--number-type", "float"])
    arguments.extend(["--noexp", "--sign"])
    arguments.extend(["--exclude", "34"])
    arguments.extend(["--exclude", "35"])
    arguments.extend(["num-2", "num-6", "num-1"])
    p = mocker.patch("natsort.__main__.sort_and_print_entries")
    main(*arguments)
    args = p.call_args[0][1]
    assert args.paths
    assert args.filter == [(4.0, 10.0)]
    assert args.reverse_filter == [(100.0, 110.0)]
    assert args.exclude == [34, 35]
    assert args.reverse
    assert args.number_type == "float"
    assert args.signed
    assert not args.exp
    assert args.locale


class Args:
    """A dummy class to simulate the argparse Namespace object"""

    def __init__(self, filt, reverse_filter, exclude, as_path, reverse):
        self.filter = filt
        self.reverse_filter = reverse_filter
        self.exclude = exclude
        self.reverse = reverse
        self.number_type = "float"
        self.signed = True
        self.exp = True
        self.paths = as_path
        self.locale = 0


mock_print = "__builtin__.print" if sys.version[0] == "2" else "builtins.print"

entries = [
    "tmp/a57/path2",
    "tmp/a23/path1",
    "tmp/a1/path1",
    "tmp/a1 (1)/path1",
    "tmp/a130/path1",
    "tmp/a64/path1",
    "tmp/a64/path2",
]


@pytest.mark.parametrize(
    "options, order",
    [
        # Defaults, all options false
        # tmp/a1 (1)/path1
        # tmp/a1/path1
        # tmp/a23/path1
        # tmp/a57/path2
        # tmp/a64/path1
        # tmp/a64/path2
        # tmp/a130/path1
        ([None, None, False, False, False], [3, 2, 1, 0, 5, 6, 4]),
        # Path option True
        # tmp/a1/path1
        # tmp/a1 (1)/path1
        # tmp/a23/path1
        # tmp/a57/path2
        # tmp/a64/path1
        # tmp/a64/path2
        # tmp/a130/path1
        ([None, None, False, True, False], [2, 3, 1, 0, 5, 6, 4]),
        # Filter option keeps only within range
        # tmp/a23/path1
        # tmp/a57/path2
        # tmp/a64/path1
        # tmp/a64/path2
        ([[(20, 100)], None, False, False, False], [1, 0, 5, 6]),
        # Reverse filter, exclude in range
        # tmp/a1/path1
        # tmp/a1 (1)/path1
        # tmp/a130/path1
        ([None, [(20, 100)], False, True, False], [2, 3, 4]),
        # Exclude given values with exclude list
        # tmp/a1/path1
        # tmp/a1 (1)/path1
        # tmp/a57/path2
        # tmp/a64/path1
        # tmp/a64/path2
        ([None, None, [23, 130], True, False], [2, 3, 0, 5, 6]),
        # Reverse order
        # tmp/a130/path1
        # tmp/a64/path2
        # tmp/a64/path1
        # tmp/a57/path2
        # tmp/a23/path1
        # tmp/a1 (1)/path1
        # tmp/a1/path1
        ([None, None, False, True, True], reversed([2, 3, 1, 0, 5, 6, 4])),
    ],
)
def test_sort_and_print_entries(options, order, mocker):
    p = mocker.patch(mock_print)
    sort_and_print_entries(entries, Args(*options))
    e = [mocker.call(entries[i]) for i in order]
    p.assert_has_calls(e)


# Each test has an "example" version for demonstrative purposes,
# and a test that uses the hypothesis module.


def test_range_check_returns_range_as_is_but_with_floats_example():
    assert range_check(10, 11) == (10.0, 11.0)
    assert range_check(6.4, 30) == (6.4, 30.0)


@given(x=floats(allow_nan=False, min_value=-1e8, max_value=1e8) | integers(), d=data())
def test_range_check_returns_range_as_is_if_first_is_less_than_second(x, d):
    # Pull data such that the first is less than the second.
    if isinstance(x, float):
        y = d.draw(floats(min_value=x + 1.0, max_value=1e9, allow_nan=False))
    else:
        y = d.draw(integers(min_value=x + 1))
    assert range_check(x, y) == (x, y)


def test_range_check_raises_value_error_if_second_is_less_than_first_example():
    with pytest.raises(ValueError, match="low >= high"):
        range_check(7, 2)


@given(x=floats(allow_nan=False), d=data())
def test_range_check_raises_value_error_if_second_is_less_than_first(x, d):
    # Pull data such that the first is greater than or equal to the second.
    y = d.draw(floats(max_value=x, allow_nan=False))
    with pytest.raises(ValueError, match="low >= high"):
        range_check(x, y)


def test_check_filters_returns_none_if_filter_evaluates_to_false():
    assert check_filters(()) is None
    assert check_filters(False) is None
    assert check_filters(None) is None


def test_check_filters_returns_input_as_is_if_filter_is_valid_example():
    assert check_filters([(6, 7)]) == [(6, 7)]
    assert check_filters([(6, 7), (2, 8)]) == [(6, 7), (2, 8)]


@given(x=lists(integers(), min_size=1), d=data())
def test_check_filters_returns_input_as_is_if_filter_is_valid(x, d):
    # ensure y is element-wise greater than x
    y = [d.draw(integers(min_value=val + 1)) for val in x]
    assert check_filters(list(zip(x, y))) == [(i, j) for i, j in zip(x, y)]


def test_check_filters_raises_value_error_if_filter_is_invalid_example():
    with pytest.raises(ValueError, match="Error in --filter: low >= high"):
        check_filters([(7, 2)])


@given(x=lists(integers(), min_size=1), d=data())
def test_check_filters_raises_value_error_if_filter_is_invalid(x, d):
    # ensure y is element-wise less than or equal to x
    y = [d.draw(integers(max_value=val)) for val in x]
    with pytest.raises(ValueError, match="Error in --filter: low >= high"):
        check_filters(list(zip(x, y)))


@pytest.mark.parametrize(
    "lows, highs, truth",
    # 1. Any portion is between the bounds => True.
    # 2. Any portion is between any bounds => True.
    # 3. No portion is between the bounds => False.
    [([0], [100], True), ([1, 88], [20, 90], True), ([1], [20], False)],
)
def test_keep_entry_range(lows, highs, truth):
    assert keep_entry_range("a56b23c89", lows, highs, int, re.compile(r"\d+")) is truth


# 1. Values not in entry => True. 2. Values in entry => False.
@pytest.mark.parametrize("values, truth", [([100, 45], True), ([23], False)])
def test_keep_entry_value(values, truth):
    assert keep_entry_value("a56b23c89", values, int, re.compile(r"\d+")) is truth