Skip to content

Commit

Permalink
test: increase unit test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
uniqueg committed Feb 12, 2023
1 parent a90cd3e commit 93dc6ab
Show file tree
Hide file tree
Showing 8 changed files with 451 additions and 101 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,9 @@ jobs:

- name: Run unit tests
run: coverage run --source tes -m pytest -W ignore::DeprecationWarning
run: |
pytest \
--cov=tes \
--cov-branch \
--cov-report=term-missing \
--cov-fail-under=99
10 changes: 5 additions & 5 deletions tes/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,17 +120,17 @@ def check_success(data: Task) -> bool:
time.sleep(0.5)

def _request_params(
self, data: Optional[str] = None,
params: Optional[Dict] = None
self, data: Optional[str] = None, params: Optional[Dict] = None
) -> Dict[str, Any]:
kwargs: Dict[str, Any] = {}
kwargs['timeout'] = self.timeout
kwargs['headers'] = {}
kwargs['headers']['Content-type'] = 'application/json'
kwargs['auth'] = (self.user, self.password)
if data:
if self.user is not None and self.password is not None:
kwargs['auth'] = (self.user, self.password)
if data is not None:
kwargs['data'] = data
if params:
if params is not None:
kwargs['params'] = params
if self.token:
kwargs['headers']['Authorization'] = f"Bearer {self.token}"
Expand Down
32 changes: 14 additions & 18 deletions tes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,18 @@
from typing import Any, Dict, List, Optional, Tuple, Type, Union


@attrs
@attrs(repr=False)
class _ListOfValidator(object):
type: Type = attrib()

def __call__(self, inst, attr, value):
"""
We use a callable class to be able to change the ``__repr__``.
"""
if not all([isinstance(n, self.type) for n in value]):
raise TypeError(
"'{attr.name}' must be a list of {self.type!r} (got {value!r} "
"that is a list of {values[0].__class__!r}).",
attr, self.type, value,
f"'{attr.name}' must be a list of {self.type!r} (got "
f"{value!r}", attr
)

def __repr__(self) -> str:
def __repr__(self):
return f"<instance_of validator for type {self.type!r}>"


Expand Down Expand Up @@ -60,15 +56,15 @@ def strconv(value: Any) -> Any:
# since an int64 value is encoded as a string in json we need to handle
# conversion
def int64conv(value: Optional[str]) -> Optional[int]:
if value is not None:
return int(value)
return value
if value is None:
return value
return int(value)


def timestampconv(value: Optional[str]) -> Optional[datetime]:
if value is not None:
return dateutil.parser.parse(value)
return value
if value is None:
return value
return dateutil.parser.parse(value)


def datetime_json_handler(x: Any) -> str:
Expand Down Expand Up @@ -294,7 +290,7 @@ def is_valid(self) -> Tuple[bool, Union[None, TypeError]]:
for e in self.executors:
if e.image is None:
errs.append("Executor image must be provided")
if len(e.command) == 0:
if e.command is None or len(e.command) == 0:
errs.append("Executor command must be provided")
if e.stdin is not None:
if not os.path.isabs(e.stdin):
Expand All @@ -306,8 +302,8 @@ def is_valid(self) -> Tuple[bool, Union[None, TypeError]]:
if not os.path.isabs(e.stderr):
errs.append("Executor stderr must be an absolute path")
if e.env is not None:
for k, v in e.env:
if not isinstance(k, str) and not isinstance(k, str):
for k, v in e.env.items():
if not isinstance(k, str) and not isinstance(v, str):
errs.append(
"Executor env keys and values must be StrType"
)
Expand Down Expand Up @@ -339,7 +335,7 @@ def is_valid(self) -> Tuple[bool, Union[None, TypeError]]:
errs.append("Volume paths must be absolute")

if self.tags is not None:
for k, v in self.tags:
for k, v in self.tags.items():
if not isinstance(k, str) and not isinstance(k, str):
errs.append(
"Tag keys and values must be StrType"
Expand Down
39 changes: 25 additions & 14 deletions tes/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,30 @@ def __init__(self, *args, **kwargs):


def unmarshal(j: Any, o: Type, convert_camel_case=True) -> Any:
print(f"{j=}")
m: Any = None
if isinstance(j, str):
m = json.loads(j)
elif isinstance(j, dict):
m = j
try:
m = json.loads(j)
except json.decoder.JSONDecodeError:
pass
elif j is None:
return None
else:
raise TypeError("j must be a str, a dict or None")
m = j
print(f"{m=}")

if not isinstance(m, dict):
raise TypeError("j must be a dictionary, a JSON string evaluation to "
"a dictionary, or None")

d: Dict[str, Any] = {}
if convert_camel_case:
for k, v in m.items():
d[camel_to_snake(k)] = v
else:
d = m
print(f"{d=}")

fullOmap = {
"Executor": {
Expand All @@ -64,6 +73,7 @@ def unmarshal(j: Any, o: Type, convert_camel_case=True) -> Any:
}

def _unmarshal(v: Any, obj: Type) -> Any:
print(f"v_private={v}")
if isinstance(v, list):
field = []
for item in v:
Expand All @@ -74,23 +84,24 @@ def _unmarshal(v: Any, obj: Type) -> Any:

r = {}
for k, v in d.items():
print(f"{k=}")
print(f"{v=}")
field = v
print(f"{field=}")
omap = fullOmap.get(o.__name__, {})
print(f"{omap=}")
if k in omap:
if isinstance(omap[k], tuple):
try:
obj = omap[k][0]
field = _unmarshal(v, obj)
except Exception:
obj = omap[k][1]
field = _unmarshal(v, obj)
else:
obj = omap[k]
field = _unmarshal(v, obj)
print(f"k_omap={k}")
obj = omap[k]
print(f"{obj=}")
field = _unmarshal(v, obj)
print(f"field_omap={field}")
r[k] = field
print(f"{r[k]=}")

try:
output = o(**r)
print(f"{output=}")
except Exception as e:
msg = "%s could not be unmarshalled to type: %s" % (j, o.__name__) + \
"\n" + \
Expand Down
1 change: 1 addition & 0 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ coverage>=6.5.0
coveralls>=3.3.1
flake8>=5.0.4
pytest>=7.2.1
pytest-cov>=4.0.0
requests_mock>=1.10.0
49 changes: 37 additions & 12 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ class TestHTTPClient(unittest.TestCase):
def test_cli(self):
cli = HTTPClient(url="http://fakehost:8000//", timeout=5)
self.assertEqual(cli.url, "http://fakehost:8000")
self.assertEqual(cli.timeout, 5)
self.assertAlmostEqual(cli.timeout, 5)

with self.assertRaises(ValueError):
HTTPClient(url="fakehost:8000", timeout=5)

with self.assertRaises(ValueError):
HTTPClient(url="htpp://fakehost:8000", timeout="5")
HTTPClient(url="htpp://fakehost:8000", timeout="5") # type: ignore

def test_create_task(self):
with requests_mock.Mocker() as m:
Expand All @@ -41,7 +41,7 @@ def test_create_task(self):
)
self.cli.create_task(self.task)
self.assertEqual(m.last_request.text, self.task.as_json())
self.assertEqual(m.last_request.timeout, self.cli.timeout)
self.assertAlmostEqual(m.last_request.timeout, self.cli.timeout)

m.post(
"%s/v1/tasks" % (self.mock_url),
Expand All @@ -50,6 +50,9 @@ def test_create_task(self):
with self.assertRaises(requests.HTTPError):
self.cli.create_task(self.task)

with self.assertRaises(TypeError):
self.cli.create_task('not_a_task_object') # type: ignore

def test_get_task(self):
with requests_mock.Mocker() as m:
m.get(
Expand All @@ -61,11 +64,11 @@ def test_get_task(self):
}
)
self.cli.get_task(self.mock_id, "MINIMAL")
self.assertEqual(
self.assertAlmostEqual(
m.last_request.url,
"%s/v1/tasks/%s?view=MINIMAL" % (self.mock_url, self.mock_id)
)
self.assertEqual(m.last_request.timeout, self.cli.timeout)
self.assertAlmostEqual(m.last_request.timeout, self.cli.timeout)

m.get(
"%s/v1/tasks/%s" % (self.mock_url, self.mock_id),
Expand All @@ -84,11 +87,11 @@ def test_list_tasks(self):
}
)
self.cli.list_tasks()
self.assertEqual(
self.assertAlmostEqual(
m.last_request.url,
"%s/v1/tasks?view=MINIMAL" % (self.mock_url)
)
self.assertEqual(m.last_request.timeout, self.cli.timeout)
self.assertAlmostEqual(m.last_request.timeout, self.cli.timeout)

# empty response
m.get(
Expand All @@ -97,7 +100,7 @@ def test_list_tasks(self):
json={}
)
self.cli.list_tasks()
self.assertEqual(
self.assertAlmostEqual(
m.last_request.url,
"%s/v1/tasks?view=MINIMAL" % (self.mock_url)
)
Expand All @@ -117,11 +120,11 @@ def test_cancel_task(self):
json={}
)
self.cli.cancel_task(self.mock_id)
self.assertEqual(
self.assertAlmostEqual(
m.last_request.url,
"%s/v1/tasks/%s:cancel" % (self.mock_url, self.mock_id)
)
self.assertEqual(m.last_request.timeout, self.cli.timeout)
self.assertAlmostEqual(m.last_request.timeout, self.cli.timeout)

m.post(
"%s/v1/tasks/%s:cancel" % (self.mock_url, self.mock_id),
Expand All @@ -138,11 +141,11 @@ def test_get_service_info(self):
json={}
)
self.cli.get_service_info()
self.assertEqual(
self.assertAlmostEqual(
m.last_request.url,
"%s/v1/tasks/service-info" % (self.mock_url)
)
self.assertEqual(m.last_request.timeout, self.cli.timeout)
self.assertAlmostEqual(m.last_request.timeout, self.cli.timeout)

m.get(
"%s/v1/tasks/service-info" % (self.mock_url),
Expand Down Expand Up @@ -177,3 +180,25 @@ def test_wait(self):
]
)
self.cli.wait(self.mock_id, timeout=2)

def test_request_params(self):

cli = HTTPClient(url="http://fakehost:8000", timeout=5)
vals = cli._request_params()
self.assertAlmostEqual(vals["timeout"], 5)
self.assertEqual(vals["headers"]["Content-type"], "application/json")
self.assertRaises(KeyError, lambda: vals["headers"]["Authorization"])
self.assertRaises(KeyError, lambda: vals["auth"])
self.assertRaises(KeyError, lambda: vals["data"])
self.assertRaises(KeyError, lambda: vals["params"])

cli = HTTPClient(url="http://fakehost:8000", user="user",
password="password", token="token")
vals = cli._request_params(data='{"json": "string"}',
params={"query_param": "value"})
self.assertAlmostEqual(vals["timeout"], 10)
self.assertEqual(vals["headers"]["Content-type"], "application/json")
self.assertEqual(vals["headers"]["Authorization"], "Bearer token")
self.assertAlmostEqual(vals["auth"], ("user", "password"))
self.assertAlmostEqual(vals["data"], '{"json": "string"}')
self.assertAlmostEqual(vals["params"], {"query_param": "value"})
Loading

0 comments on commit 93dc6ab

Please sign in to comment.