Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/tests/unit/test_shorthand.py
1566 views
1
# Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License"). You
4
# may not use this file except in compliance with the License. A copy of
5
# the License is located at
6
#
7
# http://aws.amazon.com/apache2.0/
8
#
9
# or in the "license" file accompanying this file. This file is
10
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
# ANY KIND, either express or implied. See the License for the specific
12
# language governing permissions and limitations under the License.
13
from unittest.mock import patch
14
15
import pytest
16
import signal
17
18
import awscli.paramfile
19
from awscli import shorthand
20
from awscli.testutils import skip_if_windows, unittest
21
22
from botocore import model
23
24
PARSING_TEST_CASES = (
25
# Key val pairs with scalar value.
26
('foo=bar', {'foo': 'bar'}),
27
('foo=bar', {'foo': 'bar'}),
28
('foo=bar,baz=qux', {'foo': 'bar', 'baz': 'qux'}),
29
('a=b,c=d,e=f', {'a': 'b', 'c': 'd', 'e': 'f'}),
30
# Empty values are allowed.
31
('foo=', {'foo': ''}),
32
('foo=,bar=', {'foo': '', 'bar': ''}),
33
# Unicode is allowed.
34
(u'foo=\u2713', {'foo': u'\u2713'}),
35
(u'foo=\u2713,\u2713', {'foo': [u'\u2713', u'\u2713']}),
36
# Key val pairs with csv values.
37
('foo=a,b', {'foo': ['a', 'b']}),
38
('foo=a,b,c', {'foo': ['a', 'b', 'c']}),
39
('foo=a,b,bar=c,d', {'foo': ['a', 'b'], 'bar': ['c', 'd']}),
40
('foo=a,b,c,bar=d,e,f', {'foo': ['a', 'b', 'c'], 'bar': ['d', 'e', 'f']}),
41
# Spaces in values are allowed.
42
('foo=a,b=with space', {'foo': 'a', 'b': 'with space'}),
43
# Trailing spaces are still ignored.
44
('foo=a,b=with trailing space ', {'foo': 'a', 'b': 'with trailing space'}),
45
('foo=first space', {'foo': 'first space'}),
46
(
47
'foo=a space,bar=a space,baz=a space',
48
{'foo': 'a space', 'bar': 'a space', 'baz': 'a space'}
49
),
50
# Dashes are allowed in key names.
51
('with-dash=bar', {'with-dash': 'bar'}),
52
# Underscore are also allowed.
53
('with_underscore=bar', {'with_underscore': 'bar'}),
54
# Dots are allowed.
55
('with.dot=bar', {'with.dot': 'bar'}),
56
# Pound signs are allowed.
57
('#key=value', {'#key': 'value'}),
58
# Forward slashes are allowed in keys.
59
('some/thing=value', {'some/thing': 'value'}),
60
# Colon chars are allowed in keys:
61
('aws:service:region:124:foo/bar=baz', {'aws:service:region:124:foo/bar': 'baz'}),
62
# Explicit lists.
63
('foo=[]', {'foo': []}),
64
('foo=[a]', {'foo': ['a']}),
65
('foo=[a,b]', {'foo': ['a', 'b']}),
66
('foo=[a,b,c]', {'foo': ['a', 'b', 'c']}),
67
('foo=[a,b],bar=c,d', {'foo': ['a', 'b'], 'bar': ['c', 'd']}),
68
('foo=[a,b],bar=[c,d]', {'foo': ['a', 'b'], 'bar': ['c', 'd']}),
69
('foo=a,b,bar=[c,d]', {'foo': ['a', 'b'], 'bar': ['c', 'd']}),
70
('foo=[a=b,c=d]', {'foo': ['a=b', 'c=d']}),
71
('foo=[a=b,c=d]', {'foo': ['a=b', 'c=d']}),
72
# Lists with whitespace.
73
('foo=[ a , b , c ]', {'foo': ['a', 'b', 'c']}),
74
('foo = [ a , b , c ]', {'foo': ['a', 'b', 'c']}),
75
('foo=[,,]', {'foo': ['', '']}),
76
# Single quoted strings.
77
("foo='bar'", {"foo": "bar"}),
78
("foo='bar,baz'", {"foo": "bar,baz"}),
79
# Single quoted strings for each value in a CSV list.
80
("foo='bar','baz'", {"foo": ['bar', 'baz']}),
81
# Can mix single quoted and non quoted values.
82
("foo=bar,'baz'", {"foo": ['bar', 'baz']}),
83
# Quoted strings can include chars not allowed in unquoted strings.
84
("foo=bar,'baz=qux'", {"foo": ['bar', 'baz=qux']}),
85
("foo=bar,'--option=bar space'", {"foo": ['bar', '--option=bar space']}),
86
# Can escape the single quote.
87
("foo='bar\\'baz'", {"foo": "bar'baz"}),
88
("foo='bar\\\\baz'", {"foo": "bar\\baz"}),
89
# Double quoted strings.
90
('foo="bar"', {'foo': 'bar'}),
91
('foo="bar,baz"', {'foo': 'bar,baz'}),
92
('foo="bar","baz"', {'foo': ['bar', 'baz']}),
93
('foo=bar,"baz=qux"', {'foo': ['bar', 'baz=qux']}),
94
('foo=bar,"--option=bar space"', {'foo': ['bar', '--option=bar space']}),
95
('foo="bar\\"baz"', {'foo': 'bar"baz'}),
96
('foo="bar\\\\baz"', {'foo': 'bar\\baz'}),
97
# Can escape comma in CSV list.
98
('foo=a\\,b', {"foo": "a,b"}),
99
('foo=a\\,b', {"foo": "a,b"}),
100
('foo=a\\,', {"foo": "a,"}),
101
('foo=\\,', {"foo": ","}),
102
('foo=a,b\\,c', {"foo": ['a', 'b,c']}),
103
('foo=a,b\\,', {"foo": ['a', 'b,']}),
104
('foo=a,\\,bc', {"foo": ['a', ',bc']}),
105
# Ignores whitespace around '=' and ','
106
('foo= bar', {'foo': 'bar'}),
107
('foo =bar', {'foo': 'bar'}),
108
('foo = bar', {'foo': 'bar'}),
109
('foo = bar', {'foo': 'bar'}),
110
('foo = bar,baz = qux', {'foo': 'bar', 'baz': 'qux'}),
111
('a = b, c = d , e = f', {'a': 'b', 'c': 'd', 'e': 'f'}),
112
('foo = ', {'foo': ''}),
113
('a=b,c= d, e, f', {'a': 'b', 'c': ['d', 'e', 'f']}),
114
('Name=foo,Values= a , b , c ', {'Name': 'foo', 'Values': ['a', 'b', 'c']}),
115
('Name=foo,Values= a, b , c', {'Name': 'foo', 'Values': ['a', 'b', 'c']}),
116
# Can handle newlines between values.
117
('Name=foo,\nValues=a,b,c', {'Name': 'foo', 'Values': ['a', 'b', 'c']}),
118
('A=b,\nC=d,\nE=f\n', {'A': 'b', 'C': 'd', 'E': 'f'}),
119
# Hashes
120
('Name={foo=bar,baz=qux}', {'Name': {'foo': 'bar', 'baz': 'qux'}}),
121
('Name={foo=[a,b,c],bar=baz}', {'Name': {'foo': ['a', 'b', 'c'], 'bar': 'baz'}}),
122
('Name={foo=bar},Bar=baz', {'Name': {'foo': 'bar'}, 'Bar': 'baz'}),
123
('Bar=baz,Name={foo=bar}', {'Bar': 'baz', 'Name': {'foo': 'bar'}}),
124
('a={b={c=d}}', {'a': {'b': {'c': 'd'}}}),
125
('a={b={c=d,e=f},g=h}', {'a': {'b': {'c': 'd', 'e': 'f'}, 'g': 'h'}}),
126
# Combining lists and hashes.
127
('Name=[{foo=bar}, {baz=qux}]', {'Name': [{'foo': 'bar'}, {'baz': 'qux'}]}),
128
# Combining hashes and lists.
129
(
130
'Name=[{foo=[a,b]}, {bar=[c,d]}]',
131
{'Name': [{'foo': ['a', 'b']}, {'bar': ['c', 'd']}]}
132
),
133
# key-value pairs using @= syntax
134
('foo@=bar', {'foo': 'bar'}),
135
('foo@=bar,baz@=qux', {'foo': 'bar', 'baz': 'qux'}),
136
('foo@=,bar@=', {'foo': '', 'bar': ''}),
137
(u'foo@=\u2713,\u2713', {'foo': [u'\u2713', u'\u2713']}),
138
('foo@=a,b,bar=c,d', {'foo': ['a', 'b'], 'bar': ['c', 'd']}),
139
('foo=a,b@=with space', {'foo': 'a', 'b': 'with space'}),
140
('foo=a,b@=with trailing space ', {'foo': 'a', 'b': 'with trailing space'}),
141
('aws:service:region:124:foo/bar@=baz', {'aws:service:region:124:foo/bar': 'baz'}),
142
('foo=[a,b],bar@=[c,d]', {'foo': ['a', 'b'], 'bar': ['c', 'd']}),
143
('foo @= [ a , b , c ]', {'foo': ['a', 'b', 'c']}),
144
('A=b,\nC@=d,\nE@=f\n', {'A': 'b', 'C': 'd', 'E': 'f'}),
145
('Bar@=baz,Name={foo@=bar}', {'Bar': 'baz', 'Name': {'foo': 'bar'}}),
146
('Name=[{foo@=bar}, {baz=qux}]', {'Name': [{'foo': 'bar'}, {'baz': 'qux'}]}),
147
(
148
'Name=[{foo@=[a,b]}, {bar=[c,d]}]',
149
{'Name': [{'foo': ['a', 'b']}, {'bar': ['c', 'd']}]}
150
),
151
)
152
153
154
@pytest.mark.parametrize(
155
"expr", (
156
'foo',
157
# Missing closing quotes
158
'foo="bar',
159
'"foo=bar',
160
"foo='bar",
161
"foo=[bar",
162
"foo={bar",
163
"foo={bar}",
164
"foo={bar=bar",
165
"foo=bar,",
166
"foo==bar,\nbar=baz",
167
# Duplicate keys should error otherwise they silently
168
# set only one of the values.
169
'foo=bar,foo=qux'
170
)
171
)
172
def test_error_parsing(expr):
173
with pytest.raises(shorthand.ShorthandParseError):
174
shorthand.ShorthandParser().parse(expr)
175
176
177
@pytest.mark.parametrize(
178
"expr", (
179
# starting with " but unclosed, then repeated \
180
f'foo="' + '\\' * 100,
181
# starting with ' but unclosed, then repeated \
182
f'foo=\'' + '\\' * 100,
183
)
184
)
185
@skip_if_windows("Windows does not support signal.SIGALRM.")
186
def test_error_with_backtracking(expr):
187
signal.signal(signal.SIGALRM, handle_timeout)
188
# Ensure we don't spend more than 5 seconds backtracking
189
signal.alarm(5)
190
with pytest.raises(shorthand.ShorthandParseError):
191
shorthand.ShorthandParser().parse(expr)
192
signal.alarm(0)
193
194
195
def handle_timeout(signum, frame):
196
raise TimeoutError('Shorthand parsing timed out')
197
198
@pytest.mark.parametrize(
199
'data, expected',
200
PARSING_TEST_CASES
201
)
202
def test_parse(data, expected):
203
actual = shorthand.ShorthandParser().parse(data)
204
assert actual == expected
205
206
class TestShorthandParserParamFile:
207
@patch('awscli.paramfile.compat_open')
208
@pytest.mark.parametrize(
209
'file_contents, data, expected',
210
(
211
('file-contents123', 'Foo@=file://foo,Bar={Baz@=file://foo}', {'Foo': 'file-contents123', 'Bar': {'Baz': 'file-contents123'}}),
212
(b'file-contents123', 'Foo@=fileb://foo,Bar={Baz@=fileb://foo}', {'Foo': b'file-contents123', 'Bar': {'Baz': b'file-contents123'}}),
213
('file-contents123', 'Bar@={Baz=file://foo}', {'Bar': {'Baz': 'file://foo'}}),
214
('file-contents123', 'Foo@=foo,Bar={Baz@=foo}', {'Foo': 'foo', 'Bar': {'Baz': 'foo'}})
215
)
216
)
217
def test_paramfile(self, mock_compat_open, file_contents, data, expected):
218
mock_compat_open.return_value.__enter__.return_value.read.return_value = file_contents
219
result = shorthand.ShorthandParser().parse(data)
220
assert result == expected
221
222
@patch('awscli.paramfile.compat_open')
223
def test_paramfile_list(self, mock_compat_open):
224
f1_contents = 'file-contents123'
225
f2_contents = 'contents2'
226
mock_compat_open.return_value.__enter__.return_value.read.side_effect = [f1_contents, f2_contents]
227
result = shorthand.ShorthandParser().parse(
228
f'Foo@=[a, file://foo1, file://foo2]'
229
)
230
assert result == {'Foo': ['a', f1_contents, f2_contents]}
231
232
def test_paramfile_does_not_exist_error(self, capsys):
233
with pytest.raises(awscli.paramfile.ResourceLoadingError):
234
shorthand.ShorthandParser().parse('Foo@=file://fakefile.txt')
235
captured = capsys.readouterr()
236
assert "No such file or directory: 'fakefile.txt" in captured.err
237
238
239
class TestModelVisitor(unittest.TestCase):
240
def test_promote_to_list_of_ints(self):
241
m = model.DenormalizedStructureBuilder().with_members({
242
'A': {
243
'type': 'list',
244
'member': {'type': 'string'}
245
},
246
}).build_model()
247
b = shorthand.BackCompatVisitor()
248
249
params = {'A': 'foo'}
250
b.visit(params, m)
251
self.assertEqual(params, {'A': ['foo']})
252
253
def test_dont_promote_list_if_none_value(self):
254
m = model.DenormalizedStructureBuilder().with_members({
255
'A': {
256
'type': 'list',
257
'member': {
258
'type': 'structure',
259
'members': {
260
'Single': {'type': 'string'}
261
},
262
},
263
},
264
}).build_model()
265
b = shorthand.BackCompatVisitor()
266
params = {}
267
b.visit(params, m)
268
self.assertEqual(params, {})
269
270
def test_can_convert_scalar_types_from_string(self):
271
m = model.DenormalizedStructureBuilder().with_members({
272
'A': {'type': 'integer'},
273
'B': {'type': 'string'},
274
'C': {'type': 'float'},
275
'D': {'type': 'boolean'},
276
'E': {'type': 'boolean'},
277
}).build_model()
278
b = shorthand.BackCompatVisitor()
279
280
params = {'A': '24', 'B': '24', 'C': '24.12345',
281
'D': 'true', 'E': 'false'}
282
b.visit(params, m)
283
self.assertEqual(
284
params,
285
{'A': 24, 'B': '24', 'C': float('24.12345'),
286
'D': True, 'E': False})
287
288
def test_empty_values_not_added(self):
289
m = model.DenormalizedStructureBuilder().with_members({
290
'A': {'type': 'boolean'},
291
}).build_model()
292
b = shorthand.BackCompatVisitor()
293
294
params = {}
295
b.visit(params, m)
296
self.assertEqual(params, {})
297
298
def test_can_convert_list_of_integers(self):
299
m = model.DenormalizedStructureBuilder().with_members({
300
'A': {
301
'type': 'list',
302
'member': {
303
'type': 'integer',
304
},
305
},
306
}).build_model()
307
b = shorthand.BackCompatVisitor()
308
params = {'A': ['1', '2']}
309
b.visit(params, m)
310
# We should have converted each list element to an integer
311
# because the type of the list member is integer.
312
self.assertEqual(params, {'A': [1, 2]})
313
314