Browse Source

Improve unit tests to succeed without hash tweak

PYTHONHASHSEED was set to 0 to disable hash randomization, because some
tests assertions were order sensitive. This commit is to improve the
assertions, so the PYTHONHASHSEED hack in tox.ini is not needed.

Change-Id: I4ff09d202af818d27321e8e83718e82d0c48e3d2
Closes-Bug: 1348818
changes/39/214539/2
Wang Muyu 7 years ago
parent
commit
b070ed0286
  1. 11
      contrib/rackspace/rackspace/tests/test_cloud_loadbalancer.py
  2. 10
      heat/tests/api/aws/test_api_ec2token.py
  3. 12
      heat/tests/api/cfn/test_api_cfn_v1.py
  4. 24
      heat/tests/api/cloudwatch/test_api_cloudwatch.py
  5. 13
      heat/tests/api/openstack_v1/test_views_common.py
  6. 3
      heat/tests/aws/test_loadbalancer.py
  7. 8
      heat/tests/ceilometer/test_ceilometer_alarm.py
  8. 7
      heat/tests/clients/test_nova_client.py
  9. 9
      heat/tests/engine/test_software_config.py
  10. 6
      heat/tests/generic_resource.py
  11. 3
      heat/tests/openstack/test_waitcondition.py
  12. 34
      heat/tests/test_common_serializers.py
  13. 10
      heat/tests/test_metadata_refresh.py
  14. 10
      heat/tests/test_parameters.py
  15. 13
      heat/tests/test_provider_template.py
  16. 27
      heat/tests/test_signal.py
  17. 18
      heat/tests/test_stack_resource.py
  18. 31
      heat/tests/utils.py
  19. 3
      tox.ini

11
contrib/rackspace/rackspace/tests/test_cloud_loadbalancer.py

@ -17,6 +17,7 @@ import json
import uuid
import mock
import mox
import six
from heat.common import exception
@ -1229,11 +1230,11 @@ class LoadBalancerTest(common.HeatTestCase):
template['Resources'][lb_name]['Properties']['metadata'] = {
'a': 1, 'b': 2}
expected_body = copy.deepcopy(self.expected_body)
expected_body['metadata'] = [{'key': 'a', 'value': 1},
{'key': 'b', 'value': 2}]
rsrc, fake_loadbalancer = self._mock_loadbalancer(template,
self.lb_name,
expected_body)
expected_body['metadata'] = mox.SameElementsAs(
[{'key': 'a', 'value': 1},
{'key': 'b', 'value': 2}])
rsrc, fake_loadbalancer = self._mock_loadbalancer(
template, self.lb_name, expected_body)
self.m.ReplayAll()
scheduler.TaskRunner(rsrc.create)()

10
heat/tests/api/aws/test_api_ec2token.py

@ -24,12 +24,13 @@ from heat.api.aws import ec2token
from heat.api.aws import exception
from heat.common import wsgi
from heat.tests import common
from heat.tests import utils
class Ec2TokenTest(common.HeatTestCase):
'''
"""
Tests the Ec2Token middleware
'''
"""
def setUp(self):
super(Ec2TokenTest, self).setUp()
@ -253,8 +254,9 @@ class Ec2TokenTest(common.HeatTestCase):
"path": "/v1",
"body_hash": body_hash}})
req_headers = {'Content-Type': 'application/json'}
requests.post(req_url, data=req_creds, verify=verify, cert=cert,
headers=req_headers).AndReturn(DummyHTTPResponse())
requests.post(
req_url, data=utils.JsonEquals(req_creds), verify=verify,
cert=cert, headers=req_headers).AndReturn(DummyHTTPResponse())
def test_call_ok(self):
dummy_conf = {'auth_uri': 'http://123:5000/v2.0'}

12
heat/tests/api/cfn/test_api_cfn_v1.py

@ -33,10 +33,10 @@ policy_path = os.path.dirname(os.path.realpath(__file__)) + "/../../policy/"
class CfnStackControllerTest(common.HeatTestCase):
'''
"""
Tests the API class which acts as the WSGI controller,
the endpoint processing API requests after they are routed
'''
"""
def setUp(self):
super(CfnStackControllerTest, self).setUp()
@ -333,7 +333,8 @@ class CfnStackControllerTest(common.HeatTestCase):
'DisableRollback': 'true',
'LastUpdatedTime': u'2012-07-09T09:13:11Z'}]}}}
self.assertEqual(expected, response)
self.assertEqual(utils.recursive_sort(expected),
utils.recursive_sort(response))
def test_describe_arn(self):
# Format a dummy GET request to pass into the WSGI handler
@ -417,7 +418,8 @@ class CfnStackControllerTest(common.HeatTestCase):
'DisableRollback': 'true',
'LastUpdatedTime': u'2012-07-09T09:13:11Z'}]}}}
self.assertEqual(expected, response)
self.assertEqual(utils.recursive_sort(expected),
utils.recursive_sort(response))
def test_describe_arn_invalidtenant(self):
# Format a dummy GET request to pass into the WSGI handler
@ -481,7 +483,7 @@ class CfnStackControllerTest(common.HeatTestCase):
self.assertIsInstance(result, exception.HeatInvalidParameterValueError)
def test_get_template_int_body(self):
'''Test the internal _get_template function.'''
"""Test the internal _get_template function."""
params = {'TemplateBody': "abcdef"}
dummy_req = self._dummy_GET_request(params)
result = self.controller._get_template(dummy_req)

24
heat/tests/api/cloudwatch/test_api_cloudwatch.py

@ -73,11 +73,13 @@ class WatchControllerTest(common.HeatTestCase):
dims = [{'StackId': u'21617058-781e-4262-97ab-5f9df371ee52',
'Foo': 'bar'}]
self.assertEqual([{'Name': 'StackId',
'Value': u'21617058-781e-4262-97ab-5f9df371ee52'},
{'Name': 'Foo', 'Value': 'bar'}],
self.controller._reformat_dimensions(dims)
)
self.assertEqual(
utils.recursive_sort(
[{'Name': 'StackId',
'Value': u'21617058-781e-4262-97ab-5f9df371ee52'},
{'Name': 'Foo',
'Value': 'bar'}]),
utils.recursive_sort(self.controller._reformat_dimensions(dims)))
def test_enforce_default(self):
self.m.ReplayAll()
@ -301,7 +303,9 @@ class WatchControllerTest(common.HeatTestCase):
'MetricName': u'ServiceFailure3'}]}}}
# First pass no query paramters filtering, should get all three
self.assertEqual(expected, self.controller.list_metrics(dummy_req))
self.assertEqual(
utils.recursive_sort(expected),
utils.recursive_sort(self.controller.list_metrics(dummy_req)))
def test_list_metrics_filter_name(self):
@ -358,7 +362,9 @@ class WatchControllerTest(common.HeatTestCase):
'Value': 1}],
'MetricName': u'ServiceFailure'}]}}}
# First pass no query paramters filtering, should get all three
self.assertEqual(expected, self.controller.list_metrics(dummy_req))
self.assertEqual(
utils.recursive_sort(expected),
utils.recursive_sort(self.controller.list_metrics(dummy_req)))
def test_list_metrics_filter_namespace(self):
@ -426,7 +432,9 @@ class WatchControllerTest(common.HeatTestCase):
{'Name': u'Value',
'Value': 1}],
'MetricName': u'ServiceFailure2'}]}}}
self.assertEqual(expected, self.controller.list_metrics(dummy_req))
self.assertEqual(
utils.recursive_sort(expected),
utils.recursive_sort(self.controller.list_metrics(dummy_req)))
def test_put_metric_alarm(self):
# Not yet implemented, should raise HeatAPINotImplementedError

13
heat/tests/api/openstack_v1/test_views_common.py

@ -38,10 +38,12 @@ class TestViewsCommon(common.HeatTestCase):
self.setUpGetCollectionLinks()
links = views_common.get_collection_links(self.request, self.items)
expected = 'http://example.com/fake/path?marker=id2&limit=2'
expected_params = {'marker': ['id2'], 'limit': ['2']}
next_link = filter(lambda link: link['rel'] == 'next', links).pop()
self.assertEqual('next', next_link['rel'])
self.assertEqual(expected, next_link['href'])
url_path, url_params = next_link['href'].split('?', 1)
self.assertEqual(url_path, self.request.path_url)
self.assertEqual(expected_params, urlparse.parse_qs(url_params))
def test_get_collection_links_doesnt_create_next_if_no_limit(self):
self.setUpGetCollectionLinks()
@ -62,9 +64,12 @@ class TestViewsCommon(common.HeatTestCase):
self.request.params = {'limit': '2', 'marker': 'some_marker'}
links = views_common.get_collection_links(self.request, self.items)
expected = 'http://example.com/fake/path?marker=id2&limit=2'
expected_params = {'marker': ['id2'], 'limit': ['2']}
next_link = filter(lambda link: link['rel'] == 'next', links).pop()
self.assertEqual(expected, next_link['href'])
self.assertEqual('next', next_link['rel'])
url_path, url_params = next_link['href'].split('?', 1)
self.assertEqual(url_path, self.request.path_url)
self.assertEqual(expected_params, urlparse.parse_qs(url_params))
def test_get_collection_links_does_not_overwrite_other_params(self):
self.setUpGetCollectionLinks()

3
heat/tests/aws/test_loadbalancer.py

@ -88,7 +88,8 @@ class LoadBalancerTest(common.HeatTestCase):
lb_defn = s.t.resource_definitions(s)[resource_name]
rsrc = lb.LoadBalancer(resource_name, lb_defn, s)
nova.NovaClientPlugin._create = mock.Mock(return_value=self.fc)
self.patchobject(nova.NovaClientPlugin, '_create',
return_value=self.fc)
initial_md = {'AWS::CloudFormation::Init':
{'config':

8
heat/tests/ceilometer/test_ceilometer_alarm.py

@ -198,7 +198,7 @@ class CeilometerAlarmTest(common.HeatTestCase):
if 'matching_metadata' in al:
del al['matching_metadata']
if query:
rule['query'] = query
rule['query'] = mox.SameElementsAs(query)
al['threshold_rule'] = rule
al['type'] = 'threshold'
self.m.StubOutWithMock(self.fa.alarms, 'create')
@ -396,8 +396,9 @@ class CeilometerAlarmTest(common.HeatTestCase):
self.m.VerifyAll()
def test_mem_alarm_high_not_correct_string_parameters(self):
snippet = template_format.parse(not_string_alarm_template)
orig_snippet = template_format.parse(not_string_alarm_template)
for p in ('period', 'evaluation_periods'):
snippet = copy.deepcopy(orig_snippet)
snippet['Resources']['MEMAlarmHigh']['Properties'][p] = '60a'
stack = utils.parse_stack(snippet)
@ -411,8 +412,9 @@ class CeilometerAlarmTest(common.HeatTestCase):
"Value '60a' is not an integer" % p, six.text_type(error))
def test_mem_alarm_high_not_integer_parameters(self):
snippet = template_format.parse(not_string_alarm_template)
orig_snippet = template_format.parse(not_string_alarm_template)
for p in ('period', 'evaluation_periods'):
snippet = copy.deepcopy(orig_snippet)
snippet['Resources']['MEMAlarmHigh']['Properties'][p] = [60]
stack = utils.parse_stack(snippet)

7
heat/tests/clients/test_nova_client.py

@ -374,7 +374,12 @@ class NovaClientPluginMetadataTests(NovaClientPluginTestCase):
self.assertEqual(expected, self.nova_plugin.meta_serialize(original))
def test_serialize_dict(self):
original = {'test_key': {'a': 'b', 'c': 'd'}}
original = collections.OrderedDict([
('test_key', collections.OrderedDict([
('a', 'b'),
('c', 'd'),
]))
])
expected = {'test_key': '{"a": "b", "c": "d"}'}
actual = self.nova_plugin.meta_serialize(original)
self.assertEqual(json.loads(expected['test_key']),

9
heat/tests/engine/test_software_config.py

@ -85,6 +85,11 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
return self.engine.create_software_config(
self.ctx, group, name, config, inputs, outputs, options)
def assert_status_reason(self, expected, actual):
expected_dict = dict((i.split(' : ') for i in expected.split(', ')))
actual_dict = dict((i.split(' : ') for i in actual.split(', ')))
self.assertEqual(expected_dict, actual_dict)
def test_list_software_configs(self):
config = self._create_software_config()
config_id = config['id']
@ -362,7 +367,7 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
sd = software_deployment_object.SoftwareDeployment.get_by_id(
self.ctx, deployment_id)
self.assertEqual('FAILED', sd.status)
self.assertEqual(
self.assert_status_reason(
('deploy_status_code : Deployment exited with non-zero '
'status code: -1'),
sd.status_reason)
@ -394,7 +399,7 @@ class SoftwareConfigServiceTest(common.HeatTestCase):
sd = software_deployment_object.SoftwareDeployment.get_by_id(
self.ctx, deployment_id)
self.assertEqual('FAILED', sd.status)
self.assertEqual(
self.assert_status_reason(
('foo : bar, deploy_status_code : Deployment exited with '
'non-zero status code: -1'),
sd.status_reason)

6
heat/tests/generic_resource.py

@ -11,6 +11,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import collections
from oslo_log import log as logging
import six
@ -31,8 +32,9 @@ class GenericResource(resource.Resource):
Dummy resource for use in tests
'''
properties_schema = {}
attributes_schema = {'foo': attributes.Schema('A generic attribute'),
'Foo': attributes.Schema('Another generic attribute')}
attributes_schema = collections.OrderedDict([
('foo', attributes.Schema('A generic attribute')),
('Foo', attributes.Schema('Another generic attribute'))])
@classmethod
def is_service_available(cls, context):

3
heat/tests/openstack/test_waitcondition.py

@ -214,7 +214,8 @@ class HeatWaitConditionTest(common.HeatTestCase):
'status': 'SUCCESS', 'id': '456'}
ret = handle.handle_signal(details=test_metadata)
wc_att = rsrc.FnGetAtt('data')
self.assertEqual(u'{"123": "foo", "456": "dog"}', wc_att)
self.assertEqual(json.loads(u'{"123": "foo", "456": "dog"}'),
json.loads(wc_att))
self.assertEqual('status:SUCCESS reason:cat', ret)
self.m.VerifyAll()

34
heat/tests/test_common_serializers.py

@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import collections
import datetime
import webob
@ -37,13 +38,21 @@ class JSONResponseSerializerTest(common.HeatTestCase):
self.assertEqual(expected, actual)
def test_to_json_with_more_deep_format(self):
fixture = {"is_public": True, "name": [{"name1": "test"}]}
fixture = collections.OrderedDict([
('is_public', True),
('name', [collections.OrderedDict([
('name1', 'test'),
])])
])
expected = '{"is_public": true, "name": [{"name1": "test"}]}'
actual = serializers.JSONResponseSerializer().to_json(fixture)
self.assertEqual(expected, actual)
def test_to_json_with_objects(self):
fixture = {"is_public": True, "value": complex(1, 2)}
fixture = collections.OrderedDict([
('is_public', True),
('value', complex(1, 2)),
])
expected = '{"is_public": true, "value": "(1+2j)"}'
actual = serializers.JSONResponseSerializer().to_json(fixture)
self.assertEqual(expected, actual)
@ -83,8 +92,14 @@ class XMLResponseSerializerTest(common.HeatTestCase):
def test_to_xml_with_more_deep_format(self):
# Note we expect tree traversal from one root key, which is compatible
# with the AWS format responses we need to serialize
fixture = {"aresponse":
{"is_public": True, "name": [{"name1": "test"}]}}
fixture = collections.OrderedDict([
('aresponse', collections.OrderedDict([
('is_public', True),
('name', [collections.OrderedDict([
('name1', 'test'),
])])
]))
])
expected = ('<aresponse><is_public>True</is_public>'
'<name><member><name1>test</name1></member></name>'
'</aresponse>')
@ -94,10 +109,13 @@ class XMLResponseSerializerTest(common.HeatTestCase):
def test_to_xml_with_json_only_keys(self):
# Certain keys are excluded from serialization because CFN
# format demands a json blob in the XML body
fixture = {"aresponse":
{"is_public": True,
"TemplateBody": {"name1": "test"},
"Metadata": {"name2": "test2"}}}
fixture = collections.OrderedDict([
('aresponse', collections.OrderedDict([
('is_public', True),
('TemplateBody', {"name1": "test"}),
('Metadata', {"name2": "test2"}),
]))
])
expected = ('<aresponse><is_public>True</is_public>'
'<TemplateBody>{"name1": "test"}</TemplateBody>'
'<Metadata>{"name2": "test2"}</Metadata></aresponse>')

10
heat/tests/test_metadata_refresh.py

@ -13,6 +13,7 @@
import mock
import mox
from oslo_serialization import jsonutils
from heat.common import identifier
from heat.common import template_format
@ -286,12 +287,13 @@ class WaitCondMetadataUpdateTest(common.HeatTestCase):
update_metadata('456', 'blarg', 'wibble')
self.assertEqual('{"123": "foo", "456": "blarg"}',
watch.FnGetAtt('Data'))
self.assertEqual({'123': 'foo', '456': 'blarg'},
jsonutils.loads(watch.FnGetAtt('Data')))
self.assertEqual('{"123": "foo"}',
inst.metadata_get()['test'])
self.assertEqual('{"123": "foo", "456": "blarg"}',
inst.metadata_get(refresh=True)['test'])
self.assertEqual(
{'123': 'foo', '456': 'blarg'},
jsonutils.loads(inst.metadata_get(refresh=True)['test']))
self.m.VerifyAll()

10
heat/tests/test_parameters.py

@ -75,7 +75,10 @@ class ParameterTestCommon(common.HeatTestCase):
def test_param_to_str(self):
p = new_parameter('p', {'Type': self.p_type}, self.value)
self.assertEqual(self.expected, str(p))
if self.p_type == 'Json':
self.assertEqual(json.loads(self.expected), json.loads(str(p)))
else:
self.assertEqual(self.expected, str(p))
def test_default_no_override(self):
p = new_parameter('defaulted', {'Type': self.p_type,
@ -137,7 +140,10 @@ class ParameterTestCommon(common.HeatTestCase):
'NoEcho': 'false'},
self.value)
self.assertFalse(p.hidden())
self.assertEqual(self.expected, str(p))
if self.p_type == 'Json':
self.assertEqual(json.loads(self.expected), json.loads(str(p)))
else:
self.assertEqual(self.expected, str(p))
def test_default_empty(self):
p = new_parameter('defaulted', {'Type': self.p_type,

13
heat/tests/test_provider_template.py

@ -11,6 +11,7 @@
# License for the specific language governing permissions and limitations
# under the License.
import collections
import json
import os
import uuid
@ -140,8 +141,13 @@ class ProviderTemplateTest(common.HeatTestCase):
prop_vals = {
"Foo": "Bar",
"AList": ["one", "two", "three"],
"MemList": [{"key": "name", "value": "three"},
{"key": "name", "value": "four"}],
"MemList": [collections.OrderedDict([
('key', 'name'),
('value', 'three'),
]), collections.OrderedDict([
('key', 'name'),
('value', 'four'),
])],
"ListEmpty": [],
"ANum": 5,
"AMap": map_prop_val,
@ -165,7 +171,8 @@ class ProviderTemplateTest(common.HeatTestCase):
'.member.0.value=three,'
'.member.1.key=name,'
'.member.1.value=four')
self.assertEqual(mem_exp, converted_params.get("MemList"))
self.assertEqual(sorted(mem_exp.split(',')),
sorted(converted_params.get("MemList").split(',')))
# verify Number conversion
self.assertEqual(5, converted_params.get("ANum"))
# verify Map conversion

27
heat/tests/test_signal.py

@ -17,6 +17,7 @@ import uuid
from keystoneclient import exceptions as kc_exceptions
import mox
import six
from six.moves.urllib import parse as urlparse
from heat.common import exception
from heat.common import template_format
@ -176,19 +177,25 @@ class SignalTest(common.HeatTestCase):
rsrc.created_time = created_time
self.assertEqual((rsrc.CREATE, rsrc.COMPLETE), rsrc.state)
expected_url = "".join([
# url parameters come in unexpected order, so the conversion has to be
# done for comparison
expected_url_path = "".join([
'http://server.test:8000/v1/signal/',
'arn%3Aopenstack%3Aheat%3A%3Atest_tenant%3Astacks%2F',
'test_stack%2FSTACKABCD1234%2Fresources%2F',
'signal_handler?',
'Timestamp=2012-11-29T13%3A49%3A37Z&',
'SignatureMethod=HmacSHA256&',
'AWSAccessKeyId=4567&',
'SignatureVersion=2&',
'Signature=',
'VW4NyvRO4WhQdsQ4rxl5JMUr0AlefHN6OLsRz9oZyls%3D'])
self.assertEqual(expected_url, rsrc.FnGetAtt('AlarmUrl'))
'signal_handler'])
expected_url_params = {
'Timestamp': ['2012-11-29T13:49:37Z'],
'SignatureMethod': ['HmacSHA256'],
'AWSAccessKeyId': ['4567'],
'SignatureVersion': ['2'],
'Signature': ['VW4NyvRO4WhQdsQ4rxl5JMUr0AlefHN6OLsRz9oZyls=']}
url = rsrc.FnGetAtt('AlarmUrl')
url_path, url_params = url.split('?', 1)
url_params = urlparse.parse_qs(url_params)
self.assertEqual(expected_url_path, url_path)
self.assertEqual(expected_url_params, url_params)
self.m.VerifyAll()
def test_FnGetAtt_Alarm_Url_is_cached(self):

18
heat/tests/test_stack_resource.py

@ -17,6 +17,7 @@ import uuid
import mock
from oslo_config import cfg
from oslo_messaging import exceptions as msg_exceptions
from oslo_serialization import jsonutils
import six
import testtools
@ -156,11 +157,24 @@ class StackResourceBaseTest(common.HeatTestCase):
class StackResourceTest(StackResourceBaseTest):
def setUp(self):
super(StackResourceTest, self).setUp()
self.templ = template_format.parse(param_template)
self.simple_template = template_format.parse(simple_template)
# to get same json string from a dict for comparison,
# make sort_keys True
orig_dumps = jsonutils.dumps
def sorted_dumps(*args, **kwargs):
kwargs.setdefault('sort_keys', True)
return orig_dumps(*args, **kwargs)
patched_dumps = mock.patch(
'oslo_serialization.jsonutils.dumps', sorted_dumps)
patched_dumps.start()
self.addCleanup(lambda: patched_dumps.stop())
def test_child_template_defaults_to_not_implemented(self):
self.assertRaises(NotImplementedError,
self.parent_resource.child_template)
@ -188,8 +202,8 @@ class StackResourceTest(StackResourceBaseTest):
sig1, sig2 = self.parent_resource.implementation_signature()
self.assertEqual('7b0eaabb5b82b9e90804d42e0bb739035588cb797'
'82427770646686ca2235028', sig1)
self.assertEqual('5a58b34cc3dd7f4e11fa35b63daad7b6b3aaa1744'
'19eb1c42b75d102bdda5fc9', sig2)
self.assertEqual('8fa647d036b8f36909386e1e1004539dfae7a8e88'
'c24aac0d85399e881421301', sig2)
self.parent_stack.t.files["foo"] = "bar"
sig1a, sig2a = self.parent_resource.implementation_signature()
self.assertEqual(sig1, sig1a)

31
heat/tests/utils.py

@ -15,8 +15,10 @@ import random
import string
import uuid
import mox
from oslo_config import cfg
from oslo_db import options
from oslo_serialization import jsonutils
import sqlalchemy
from heat.common import context
@ -143,3 +145,32 @@ class PhysName(object):
def __repr__(self):
return self._physname
def recursive_sort(obj):
"""Recursively sort list in iterables for comparison."""
if isinstance(obj, dict):
for v in obj.values():
recursive_sort(v)
elif isinstance(obj, list):
obj.sort()
for i in obj:
recursive_sort(i)
return obj
class JsonEquals(mox.Comparator):
"""Comparison class used to check if two json strings equal.
If a dict is dumped to json, the order is undecided, so load the string
back to an object for comparison
"""
def __init__(self, other_json):
self.other_json = other_json
def equals(self, rhs):
return jsonutils.loads(self.other_json) == jsonutils.loads(rhs)
def __repr__(self):
return "<equals to json '%s'>" % self.other_json

3
tox.ini

@ -4,10 +4,7 @@ minversion = 1.6
skipsdist = True
[testenv]
# Note the hash seed is set to 0 until heat can be tested with a
# random hash seed successfully.
setenv = VIRTUAL_ENV={envdir}
PYTHONHASHSEED=0
usedevelop = True
install_command = pip install {opts} {packages}
deps = -r{toxinidir}/requirements.txt

Loading…
Cancel
Save