File: test_scope_parser.py

package info (click to toggle)
python-globus-sdk 4.3.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky
  • size: 5,172 kB
  • sloc: python: 35,227; sh: 44; makefile: 35
file content (265 lines) | stat: -rw-r--r-- 7,752 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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
import time

import pytest

from globus_sdk import exc
from globus_sdk.scopes import Scope, ScopeCycleError, ScopeParseError, ScopeParser


def test_scope_str_and_repr_simple():
    s = Scope("simple")
    assert str(s) == "simple"
    assert repr(s) == "Scope('simple')"


def test_scope_str_and_repr_optional():
    s = Scope("simple", optional=True)
    assert str(s) == "*simple"
    assert repr(s) == "Scope('simple', optional=True)"


def test_scope_str_and_repr_with_dependencies():
    s = Scope("top")
    s = s.with_dependency(Scope("foo"))
    assert str(s) == "top[foo]"
    s = s.with_dependency(Scope("bar"))
    assert str(s) == "top[foo bar]"
    assert repr(s) == "Scope('top', dependencies=(Scope('foo'), Scope('bar')))"


def test_scope_str_nested():
    bottom = Scope("bottom")
    mid = Scope("mid", dependencies=(bottom,))
    top = Scope("top", dependencies=(mid,))
    assert str(bottom) == "bottom"
    assert str(mid) == "mid[bottom]"
    assert str(top) == "top[mid[bottom]]"


def test_scope_with_optional_dependency_stringifies():
    s = Scope("top")
    s = s.with_dependency(Scope("subscope", optional=True))
    assert str(s) == "top[*subscope]"
    subscope_repr = "Scope('subscope', optional=True)"
    assert repr(s) == f"Scope('top', dependencies=({subscope_repr},))"


def test_scope_parsing_allows_empty_string():
    scopes = ScopeParser.parse("")
    assert scopes == []


@pytest.mark.parametrize(
    "scope_string1,scope_string2",
    [
        ("foo ", "foo"),
        (" foo", "foo"),
        ("foo[ bar]", "foo[bar]"),
    ],
)
def test_scope_parsing_ignores_non_semantic_whitespace(scope_string1, scope_string2):
    list1 = ScopeParser.parse(scope_string1)
    list2 = ScopeParser.parse(scope_string2)
    assert len(list1) == len(list2) == 1
    s1, s2 = list1[0], list2[0]
    # Scope.__eq__ is not defined, so equivalence checking is manual (and somewhat error
    # prone) for now
    assert s1.scope_string == s2.scope_string
    assert s1.optional == s2.optional
    for i in range(len(s1.dependencies)):
        assert s1.dependencies[i].scope_string == s2.dependencies[i].scope_string
        assert s1.dependencies[i].optional == s2.dependencies[i].optional


@pytest.mark.parametrize(
    "scopestring",
    [
        # ending in '*'
        "foo*",
        "foo *",
        # '*' followed by '[] '
        "foo*[bar]",
        "foo *[bar]",
        "foo [bar*]",
        "foo * ",
        "* foo",
        # empty brackets
        "foo[]",
        # starting with open bracket
        "[foo]",
        # double brackets
        "foo[[bar]]",
        # unbalanced open brackets
        "foo[",
        "foo[bar",
        # unbalanced close brackets
        "foo]",
        "foo bar]",
        "foo[bar]]",
        "foo[bar] baz]",
        # space before brackets
        "foo [bar]",
        # missing space before next scope string after ']'
        "foo[bar]baz",
    ],
)
def test_scope_parsing_rejects_bad_inputs(scopestring):
    with pytest.raises(ScopeParseError):
        ScopeParser.parse(scopestring)


@pytest.mark.parametrize(
    "scopestring",
    [
        "foo[foo]",
        "foo[*foo]",
        "foo[bar[foo]]",
        "foo[bar[baz[bar]]]",
        "foo[bar[*baz[bar]]]",
        "foo[bar[*baz[*bar]]]",
    ],
)
def test_scope_parsing_catches_and_rejects_cycles(scopestring):
    with pytest.raises(ScopeCycleError):
        ScopeParser.parse(scopestring)


@pytest.mark.flaky
def test_scope_parsing_catches_and_rejects_very_large_cycles_quickly():
    """
    WARNING: this test is hardware speed dependent and could fail on slow systems.

    This test creates a very long cycle and validates that it can be caught in a
    small timeframe of < 100ms.
    Observed times on a test system were <20ms, and in CI were <60ms.

    Although checking the speed in this way is not ideal, we want to avoid high
    time-complexity in the cycle detection. This test offers good protection against any
    major performance regression.
    """
    scope_string = ""
    for i in range(1000):
        scope_string += f"foo{i}["
    scope_string += " foo10"
    for _ in range(1000):
        scope_string += "]"

    t0 = time.time()
    with pytest.raises(ScopeCycleError):
        ScopeParser.parse(scope_string)
    t1 = time.time()
    assert t1 - t0 < 0.1


@pytest.mark.parametrize(
    "scopestring",
    ("foo", "*foo", "foo[bar]", "foo[*bar]", "foo bar", "foo[bar[baz]]"),
)
def test_scope_parsing_accepts_valid_inputs(scopestring):
    # test *only* that parsing does not error and returns a non-empty list of scopes
    scopes = ScopeParser.parse(scopestring)
    assert isinstance(scopes, list)
    assert len(scopes) > 0
    assert isinstance(scopes[0], Scope)


def test_scope_deserialize_simple():
    scope = Scope.parse("foo")
    assert str(scope) == "foo"


def test_scope_deserialize_with_dependencies():
    # oh, while we're here, let's also check that our whitespace insensitivity works
    scope = Scope.parse("foo[ bar   *baz  ]")
    assert str(scope) in ("foo[bar *baz]", "foo[*baz bar]")


def test_scope_deserialize_fails_on_empty():
    with pytest.raises(ValueError):
        Scope.parse("  ")


def test_scope_deserialize_fails_on_multiple_top_level_scopes():
    with pytest.raises(ValueError):
        Scope.parse("foo bar")


@pytest.mark.parametrize("scope_str", ("*foo", "foo[bar]", "foo[", "foo]", "foo bar"))
def test_scope_init_forbids_special_chars(scope_str):
    with pytest.raises(ValueError):
        Scope(scope_str)


@pytest.mark.parametrize(
    "original, reserialized",
    [
        ("foo[bar *bar]", {"foo[bar]"}),
        ("foo[*bar bar]", {"foo[bar]"}),
        ("foo[bar[baz]] bar[*baz]", {"foo[bar[baz]]", "bar[baz]"}),
        ("foo[bar[*baz]] bar[baz]", {"foo[bar[baz]]", "bar[baz]"}),
    ],
)
def test_scope_parsing_normalizes_optionals(original, reserialized):
    assert {str(s) for s in ScopeParser.parse(original)} == reserialized


@pytest.mark.parametrize(
    "scope_str",
    (
        "foo",
        "*foo",
        "foo[bar] baz",
        " foo  ",
        "foo[bar] bar[foo]",
    ),
)
def test_serialize_of_scope_string_is_exact(scope_str):
    assert ScopeParser.serialize(scope_str) == scope_str


def test_serialize_of_scope_object():
    assert ScopeParser.serialize(Scope("scope1")) == "scope1"


@pytest.mark.parametrize(
    "scope_collection",
    (
        ("scope1",),
        ["scope1"],
        {"scope1"},
        (s for s in ["scope1"]),
    ),
)
def test_serialize_of_simple_collection_of_strings(scope_collection):
    assert ScopeParser.serialize(scope_collection) == "scope1"


@pytest.mark.parametrize(
    "scope_collection, expect_str",
    (
        (("scope1", Scope("scope2")), "scope1 scope2"),
        ((Scope("scope1"), Scope("scope2")), "scope1 scope2"),
        ((Scope("scope1"), Scope("scope2"), "scope3"), "scope1 scope2 scope3"),
        (
            (Scope("scope1"), Scope("scope2"), "scope3 scope4"),
            "scope1 scope2 scope3 scope4",
        ),
        ([Scope("scope1"), "scope2", "scope3 scope4"], "scope1 scope2 scope3 scope4"),
    ),
)
def test_serialize_handles_mixed_data(scope_collection, expect_str):
    assert ScopeParser.serialize(scope_collection) == expect_str


@pytest.mark.parametrize("input_obj", ("", [], set(), ()))
def test_serialize_rejects_empty_by_default(input_obj):
    with pytest.raises(
        exc.GlobusSDKUsageError,
        match="'scopes' cannot be the empty string or empty collection",
    ):
        ScopeParser.serialize(input_obj)


@pytest.mark.parametrize("input_obj", ("", [], set(), ()))
def test_serialize_allows_empty_string_with_flag(input_obj):
    assert ScopeParser.serialize(input_obj, reject_empty=False) == ""