from unittest.mock import patch
import pytest
import signal
import awscli.paramfile
from awscli import shorthand
from awscli.testutils import skip_if_windows, unittest
from botocore import model
PARSING_TEST_CASES = (
('foo=bar', {'foo': 'bar'}),
('foo=bar', {'foo': 'bar'}),
('foo=bar,baz=qux', {'foo': 'bar', 'baz': 'qux'}),
('a=b,c=d,e=f', {'a': 'b', 'c': 'd', 'e': 'f'}),
('foo=', {'foo': ''}),
('foo=,bar=', {'foo': '', 'bar': ''}),
(u'foo=\u2713', {'foo': u'\u2713'}),
(u'foo=\u2713,\u2713', {'foo': [u'\u2713', u'\u2713']}),
('foo=a,b', {'foo': ['a', 'b']}),
('foo=a,b,c', {'foo': ['a', 'b', 'c']}),
('foo=a,b,bar=c,d', {'foo': ['a', 'b'], 'bar': ['c', 'd']}),
('foo=a,b,c,bar=d,e,f', {'foo': ['a', 'b', 'c'], 'bar': ['d', 'e', 'f']}),
('foo=a,b=with space', {'foo': 'a', 'b': 'with space'}),
('foo=a,b=with trailing space ', {'foo': 'a', 'b': 'with trailing space'}),
('foo=first space', {'foo': 'first space'}),
(
'foo=a space,bar=a space,baz=a space',
{'foo': 'a space', 'bar': 'a space', 'baz': 'a space'}
),
('with-dash=bar', {'with-dash': 'bar'}),
('with_underscore=bar', {'with_underscore': 'bar'}),
('with.dot=bar', {'with.dot': 'bar'}),
('#key=value', {'#key': 'value'}),
('some/thing=value', {'some/thing': 'value'}),
('aws:service:region:124:foo/bar=baz', {'aws:service:region:124:foo/bar': 'baz'}),
('foo=[]', {'foo': []}),
('foo=[a]', {'foo': ['a']}),
('foo=[a,b]', {'foo': ['a', 'b']}),
('foo=[a,b,c]', {'foo': ['a', 'b', 'c']}),
('foo=[a,b],bar=c,d', {'foo': ['a', 'b'], 'bar': ['c', 'd']}),
('foo=[a,b],bar=[c,d]', {'foo': ['a', 'b'], 'bar': ['c', 'd']}),
('foo=a,b,bar=[c,d]', {'foo': ['a', 'b'], 'bar': ['c', 'd']}),
('foo=[a=b,c=d]', {'foo': ['a=b', 'c=d']}),
('foo=[a=b,c=d]', {'foo': ['a=b', 'c=d']}),
('foo=[ a , b , c ]', {'foo': ['a', 'b', 'c']}),
('foo = [ a , b , c ]', {'foo': ['a', 'b', 'c']}),
('foo=[,,]', {'foo': ['', '']}),
("foo='bar'", {"foo": "bar"}),
("foo='bar,baz'", {"foo": "bar,baz"}),
("foo='bar','baz'", {"foo": ['bar', 'baz']}),
("foo=bar,'baz'", {"foo": ['bar', 'baz']}),
("foo=bar,'baz=qux'", {"foo": ['bar', 'baz=qux']}),
("foo=bar,'--option=bar space'", {"foo": ['bar', '--option=bar space']}),
("foo='bar\\'baz'", {"foo": "bar'baz"}),
("foo='bar\\\\baz'", {"foo": "bar\\baz"}),
('foo="bar"', {'foo': 'bar'}),
('foo="bar,baz"', {'foo': 'bar,baz'}),
('foo="bar","baz"', {'foo': ['bar', 'baz']}),
('foo=bar,"baz=qux"', {'foo': ['bar', 'baz=qux']}),
('foo=bar,"--option=bar space"', {'foo': ['bar', '--option=bar space']}),
('foo="bar\\"baz"', {'foo': 'bar"baz'}),
('foo="bar\\\\baz"', {'foo': 'bar\\baz'}),
('foo=a\\,b', {"foo": "a,b"}),
('foo=a\\,b', {"foo": "a,b"}),
('foo=a\\,', {"foo": "a,"}),
('foo=\\,', {"foo": ","}),
('foo=a,b\\,c', {"foo": ['a', 'b,c']}),
('foo=a,b\\,', {"foo": ['a', 'b,']}),
('foo=a,\\,bc', {"foo": ['a', ',bc']}),
('foo= bar', {'foo': 'bar'}),
('foo =bar', {'foo': 'bar'}),
('foo = bar', {'foo': 'bar'}),
('foo = bar', {'foo': 'bar'}),
('foo = bar,baz = qux', {'foo': 'bar', 'baz': 'qux'}),
('a = b, c = d , e = f', {'a': 'b', 'c': 'd', 'e': 'f'}),
('foo = ', {'foo': ''}),
('a=b,c= d, e, f', {'a': 'b', 'c': ['d', 'e', 'f']}),
('Name=foo,Values= a , b , c ', {'Name': 'foo', 'Values': ['a', 'b', 'c']}),
('Name=foo,Values= a, b , c', {'Name': 'foo', 'Values': ['a', 'b', 'c']}),
('Name=foo,\nValues=a,b,c', {'Name': 'foo', 'Values': ['a', 'b', 'c']}),
('A=b,\nC=d,\nE=f\n', {'A': 'b', 'C': 'd', 'E': 'f'}),
('Name={foo=bar,baz=qux}', {'Name': {'foo': 'bar', 'baz': 'qux'}}),
('Name={foo=[a,b,c],bar=baz}', {'Name': {'foo': ['a', 'b', 'c'], 'bar': 'baz'}}),
('Name={foo=bar},Bar=baz', {'Name': {'foo': 'bar'}, 'Bar': 'baz'}),
('Bar=baz,Name={foo=bar}', {'Bar': 'baz', 'Name': {'foo': 'bar'}}),
('a={b={c=d}}', {'a': {'b': {'c': 'd'}}}),
('a={b={c=d,e=f},g=h}', {'a': {'b': {'c': 'd', 'e': 'f'}, 'g': 'h'}}),
('Name=[{foo=bar}, {baz=qux}]', {'Name': [{'foo': 'bar'}, {'baz': 'qux'}]}),
(
'Name=[{foo=[a,b]}, {bar=[c,d]}]',
{'Name': [{'foo': ['a', 'b']}, {'bar': ['c', 'd']}]}
),
('foo@=bar', {'foo': 'bar'}),
('foo@=bar,baz@=qux', {'foo': 'bar', 'baz': 'qux'}),
('foo@=,bar@=', {'foo': '', 'bar': ''}),
(u'foo@=\u2713,\u2713', {'foo': [u'\u2713', u'\u2713']}),
('foo@=a,b,bar=c,d', {'foo': ['a', 'b'], 'bar': ['c', 'd']}),
('foo=a,b@=with space', {'foo': 'a', 'b': 'with space'}),
('foo=a,b@=with trailing space ', {'foo': 'a', 'b': 'with trailing space'}),
('aws:service:region:124:foo/bar@=baz', {'aws:service:region:124:foo/bar': 'baz'}),
('foo=[a,b],bar@=[c,d]', {'foo': ['a', 'b'], 'bar': ['c', 'd']}),
('foo @= [ a , b , c ]', {'foo': ['a', 'b', 'c']}),
('A=b,\nC@=d,\nE@=f\n', {'A': 'b', 'C': 'd', 'E': 'f'}),
('Bar@=baz,Name={foo@=bar}', {'Bar': 'baz', 'Name': {'foo': 'bar'}}),
('Name=[{foo@=bar}, {baz=qux}]', {'Name': [{'foo': 'bar'}, {'baz': 'qux'}]}),
(
'Name=[{foo@=[a,b]}, {bar=[c,d]}]',
{'Name': [{'foo': ['a', 'b']}, {'bar': ['c', 'd']}]}
),
)
@pytest.mark.parametrize(
"expr", (
'foo',
'foo="bar',
'"foo=bar',
"foo='bar",
"foo=[bar",
"foo={bar",
"foo={bar}",
"foo={bar=bar",
"foo=bar,",
"foo==bar,\nbar=baz",
'foo=bar,foo=qux'
)
)
def test_error_parsing(expr):
with pytest.raises(shorthand.ShorthandParseError):
shorthand.ShorthandParser().parse(expr)
@pytest.mark.parametrize(
"expr", (
f'foo="' + '\\' * 100,
f'foo=\'' + '\\' * 100,
)
)
@skip_if_windows("Windows does not support signal.SIGALRM.")
def test_error_with_backtracking(expr):
signal.signal(signal.SIGALRM, handle_timeout)
signal.alarm(5)
with pytest.raises(shorthand.ShorthandParseError):
shorthand.ShorthandParser().parse(expr)
signal.alarm(0)
def handle_timeout(signum, frame):
raise TimeoutError('Shorthand parsing timed out')
@pytest.mark.parametrize(
'data, expected',
PARSING_TEST_CASES
)
def test_parse(data, expected):
actual = shorthand.ShorthandParser().parse(data)
assert actual == expected
class TestShorthandParserParamFile:
@patch('awscli.paramfile.compat_open')
@pytest.mark.parametrize(
'file_contents, data, expected',
(
('file-contents123', 'Foo@=file://foo,Bar={Baz@=file://foo}', {'Foo': 'file-contents123', 'Bar': {'Baz': 'file-contents123'}}),
(b'file-contents123', 'Foo@=fileb://foo,Bar={Baz@=fileb://foo}', {'Foo': b'file-contents123', 'Bar': {'Baz': b'file-contents123'}}),
('file-contents123', 'Bar@={Baz=file://foo}', {'Bar': {'Baz': 'file://foo'}}),
('file-contents123', 'Foo@=foo,Bar={Baz@=foo}', {'Foo': 'foo', 'Bar': {'Baz': 'foo'}})
)
)
def test_paramfile(self, mock_compat_open, file_contents, data, expected):
mock_compat_open.return_value.__enter__.return_value.read.return_value = file_contents
result = shorthand.ShorthandParser().parse(data)
assert result == expected
@patch('awscli.paramfile.compat_open')
def test_paramfile_list(self, mock_compat_open):
f1_contents = 'file-contents123'
f2_contents = 'contents2'
mock_compat_open.return_value.__enter__.return_value.read.side_effect = [f1_contents, f2_contents]
result = shorthand.ShorthandParser().parse(
f'Foo@=[a, file://foo1, file://foo2]'
)
assert result == {'Foo': ['a', f1_contents, f2_contents]}
def test_paramfile_does_not_exist_error(self, capsys):
with pytest.raises(awscli.paramfile.ResourceLoadingError):
shorthand.ShorthandParser().parse('Foo@=file://fakefile.txt')
captured = capsys.readouterr()
assert "No such file or directory: 'fakefile.txt" in captured.err
class TestModelVisitor(unittest.TestCase):
def test_promote_to_list_of_ints(self):
m = model.DenormalizedStructureBuilder().with_members({
'A': {
'type': 'list',
'member': {'type': 'string'}
},
}).build_model()
b = shorthand.BackCompatVisitor()
params = {'A': 'foo'}
b.visit(params, m)
self.assertEqual(params, {'A': ['foo']})
def test_dont_promote_list_if_none_value(self):
m = model.DenormalizedStructureBuilder().with_members({
'A': {
'type': 'list',
'member': {
'type': 'structure',
'members': {
'Single': {'type': 'string'}
},
},
},
}).build_model()
b = shorthand.BackCompatVisitor()
params = {}
b.visit(params, m)
self.assertEqual(params, {})
def test_can_convert_scalar_types_from_string(self):
m = model.DenormalizedStructureBuilder().with_members({
'A': {'type': 'integer'},
'B': {'type': 'string'},
'C': {'type': 'float'},
'D': {'type': 'boolean'},
'E': {'type': 'boolean'},
}).build_model()
b = shorthand.BackCompatVisitor()
params = {'A': '24', 'B': '24', 'C': '24.12345',
'D': 'true', 'E': 'false'}
b.visit(params, m)
self.assertEqual(
params,
{'A': 24, 'B': '24', 'C': float('24.12345'),
'D': True, 'E': False})
def test_empty_values_not_added(self):
m = model.DenormalizedStructureBuilder().with_members({
'A': {'type': 'boolean'},
}).build_model()
b = shorthand.BackCompatVisitor()
params = {}
b.visit(params, m)
self.assertEqual(params, {})
def test_can_convert_list_of_integers(self):
m = model.DenormalizedStructureBuilder().with_members({
'A': {
'type': 'list',
'member': {
'type': 'integer',
},
},
}).build_model()
b = shorthand.BackCompatVisitor()
params = {'A': ['1', '2']}
b.visit(params, m)
self.assertEqual(params, {'A': [1, 2]})