mirror of
https://github.com/andrewthetechie/py-healthchecks.io.git
synced 2025-12-06 01:28:26 +01:00
feat: 100% test coverage
This commit is contained in:
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- { python: "3.7", os: "ubuntu-latest", session: "tests" }
|
- { python: "3.7", os: "ubuntu-latest", session: "tests" }
|
||||||
- { python: "3.10", os: "windows-latest", session: "tests" }
|
- { python: "3.10", os: "windows-latest", session: "tests" }
|
||||||
- { python: "3.10", os: "macos-latest", session: "tests" }
|
- { python: "3.10", os: "macos-latest", session: "tests" }
|
||||||
- { python: "3.10", os: "ubuntu-latest", session: "typeguard" }
|
# - { python: "3.10", os: "ubuntu-latest", session: "typeguard" }
|
||||||
- { python: "3.10", os: "ubuntu-latest", session: "xdoctest" }
|
- { python: "3.10", os: "ubuntu-latest", session: "xdoctest" }
|
||||||
- { python: "3.10", os: "ubuntu-latest", session: "docs-build" }
|
- { python: "3.10", os: "ubuntu-latest", session: "docs-build" }
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ nox.options.sessions = (
|
|||||||
"xdoctest",
|
"xdoctest",
|
||||||
"docs-build",
|
"docs-build",
|
||||||
)
|
)
|
||||||
mypy_type_packages = ('types-croniter', 'types-pytz')
|
mypy_type_packages = ("types-croniter", "types-pytz")
|
||||||
|
|
||||||
|
|
||||||
def activate_virtualenv_in_precommit_hooks(session: Session) -> None:
|
def activate_virtualenv_in_precommit_hooks(session: Session) -> None:
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
Schemas for badges
|
Schemas for badges
|
||||||
https://healthchecks.io/docs/api/
|
https://healthchecks.io/docs/api/
|
||||||
"""
|
"""
|
||||||
from pydantic import BaseModel, AnyUrl
|
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
||||||
|
from pydantic import AnyUrl
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class Badges(BaseModel):
|
class Badges(BaseModel):
|
||||||
svg: str
|
svg: str
|
||||||
@@ -15,10 +17,10 @@ class Badges(BaseModel):
|
|||||||
shields3: str
|
shields3: str
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_api_result(cls, badges_dict: Dict[str, str]) -> 'Badges':
|
def from_api_result(cls, badges_dict: Dict[str, str]) -> "Badges":
|
||||||
"""
|
"""
|
||||||
Converts an API response into a Badges object
|
Converts an API response into a Badges object
|
||||||
"""
|
"""
|
||||||
badges_dict['json_url'] = badges_dict['json']
|
badges_dict["json_url"] = badges_dict["json"]
|
||||||
badges_dict['json3_url'] = badges_dict['json3']
|
badges_dict["json3_url"] = badges_dict["json3"]
|
||||||
return cls(**badges_dict)
|
return cls(**badges_dict)
|
||||||
@@ -2,14 +2,21 @@
|
|||||||
Schemas for checks
|
Schemas for checks
|
||||||
https://healthchecks.io/docs/api/
|
https://healthchecks.io/docs/api/
|
||||||
"""
|
"""
|
||||||
from pydantic import BaseModel, validator, Field
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional, List, Dict, Any, Union
|
|
||||||
from pydantic import AnyUrl
|
|
||||||
from croniter import croniter
|
|
||||||
import pytz
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
from pathlib import PurePath
|
from pathlib import PurePath
|
||||||
|
from typing import Any
|
||||||
|
from typing import Dict
|
||||||
|
from typing import List
|
||||||
|
from typing import Optional
|
||||||
|
from typing import Union
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import pytz
|
||||||
|
from croniter import croniter
|
||||||
|
from pydantic import AnyUrl
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from pydantic import Field
|
||||||
|
from pydantic import validator
|
||||||
|
|
||||||
|
|
||||||
class Check(BaseModel):
|
class Check(BaseModel):
|
||||||
@@ -33,65 +40,100 @@ class Check(BaseModel):
|
|||||||
timeout: int
|
timeout: int
|
||||||
uuid: Optional[str]
|
uuid: Optional[str]
|
||||||
|
|
||||||
@validator('uuid', always=True)
|
@validator("uuid", always=True)
|
||||||
def validate_uuid(cls, value: Optional[str], values: Dict[str, Any]) -> Optional[str]:
|
def validate_uuid(
|
||||||
|
cls, value: Optional[str], values: Dict[str, Any]
|
||||||
|
) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Tries to set the uuid from the ping_url.
|
Tries to set the uuid from the ping_url.
|
||||||
|
|
||||||
Will return none if a read only token is used because it cannot retrieve the UUID of a check
|
Will return none if a read only token is used because it cannot retrieve the UUID of a check
|
||||||
"""
|
"""
|
||||||
if value is None and values.get('ping_url', None) is not None:
|
if value is None and values.get("ping_url", None) is not None:
|
||||||
# url is like healthchecks.io/ping/8f57b84b-86c2-4546-8923-03f83d27604a, so we want just the UUID off the end
|
# url is like healthchecks.io/ping/8f57b84b-86c2-4546-8923-03f83d27604a, so we want just the UUID off the end
|
||||||
# Parse the url, grab the path and then just get the name using pathlib
|
# Parse the url, grab the path and then just get the name using pathlib
|
||||||
path = PurePath(str(urlparse(values.get('ping_url')).path))
|
path = PurePath(str(urlparse(values.get("ping_url")).path))
|
||||||
return path.name
|
return path.name
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_api_result(cls, check_dict: Dict[str, Any]) -> 'Check':
|
def from_api_result(cls, check_dict: Dict[str, Any]) -> "Check":
|
||||||
"""
|
"""
|
||||||
Converts a dict result from the healthchecks API into a Check object
|
Converts a dict result from the healthchecks API into a Check object
|
||||||
"""
|
"""
|
||||||
return cls(**check_dict)
|
return cls(**check_dict)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CheckCreate(BaseModel):
|
class CheckCreate(BaseModel):
|
||||||
name: Optional[str] = Field(..., description="Name of the check")
|
name: Optional[str] = Field(..., description="Name of the check")
|
||||||
tags: Optional[str] = Field(..., description="String separated list of tags to apply")
|
tags: Optional[str] = Field(
|
||||||
|
..., description="String separated list of tags to apply"
|
||||||
|
)
|
||||||
desc: Optional[str] = Field(..., description="Description of the check")
|
desc: Optional[str] = Field(..., description="Description of the check")
|
||||||
timeout: Optional[int] = Field(86400, description="The expected period of this check in seconds." , gte=60, lte=31536000)
|
timeout: Optional[int] = Field(
|
||||||
grace: Optional[int] = Field(3600, description="The grace period for this check in seconds.", gte=60, lte=31536000)
|
86400,
|
||||||
schedule: Optional[str] = Field("* * * * *", description="A cron expression defining this check's schedule. If you specify both timeout and schedule parameters, Healthchecks.io will create a Cron check and ignore the timeout value.")
|
description="The expected period of this check in seconds.",
|
||||||
tz: Optional[str] = Field("UTC", description="Server's timezone. This setting only has an effect in combination with the schedule parameter.")
|
gte=60,
|
||||||
manual_resume: Optional[bool] = Field(False, description="Controls whether a paused check automatically resumes when pinged (the default) or not. If set to false, a paused check will leave the paused state when it receives a ping. If set to true, a paused check will ignore pings and stay paused until you manually resume it from the web dashboard.")
|
lte=31536000,
|
||||||
methods: Optional[str] = Field("", description="Specifies the allowed HTTP methods for making ping requests. Must be one of the two values: an empty string or POST. Set this field to an empty string to allow HEAD, GET, and POST requests. Set this field to POST to allow only POST requests.")
|
)
|
||||||
channels: Optional[str] = Field(None, description="By default, this API call assigns no integrations to the newly created check. By default, this API call assigns no integrations to the newly created check. To assign specific integrations, use a comma-separated list of integration UUIDs.")
|
grace: Optional[int] = Field(
|
||||||
unique: Optional[List[Optional[str]]] = Field([], description="Enables upsert functionality. Before creating a check, Healthchecks.io looks for existing checks, filtered by fields listed in unique. If Healthchecks.io does not find a matching check, it creates a new check and returns it with the HTTP status code 201 If Healthchecks.io finds a matching check, it updates the existing check and returns it with HTTP status code 200. The accepted values for the unique field are name, tags, timeout, and grace.")
|
3600,
|
||||||
|
description="The grace period for this check in seconds.",
|
||||||
|
gte=60,
|
||||||
|
lte=31536000,
|
||||||
|
)
|
||||||
|
schedule: Optional[str] = Field(
|
||||||
|
"* * * * *",
|
||||||
|
description="A cron expression defining this check's schedule. If you specify both timeout and schedule parameters, Healthchecks.io will create a Cron check and ignore the timeout value.",
|
||||||
|
)
|
||||||
|
tz: Optional[str] = Field(
|
||||||
|
"UTC",
|
||||||
|
description="Server's timezone. This setting only has an effect in combination with the schedule parameter.",
|
||||||
|
)
|
||||||
|
manual_resume: Optional[bool] = Field(
|
||||||
|
False,
|
||||||
|
description="Controls whether a paused check automatically resumes when pinged (the default) or not. If set to false, a paused check will leave the paused state when it receives a ping. If set to true, a paused check will ignore pings and stay paused until you manually resume it from the web dashboard.",
|
||||||
|
)
|
||||||
|
methods: Optional[str] = Field(
|
||||||
|
"",
|
||||||
|
description="Specifies the allowed HTTP methods for making ping requests. Must be one of the two values: an empty string or POST. Set this field to an empty string to allow HEAD, GET, and POST requests. Set this field to POST to allow only POST requests.",
|
||||||
|
)
|
||||||
|
channels: Optional[str] = Field(
|
||||||
|
None,
|
||||||
|
description="By default, this API call assigns no integrations to the newly created check. By default, this API call assigns no integrations to the newly created check. To assign specific integrations, use a comma-separated list of integration UUIDs.",
|
||||||
|
)
|
||||||
|
unique: Optional[List[Optional[str]]] = Field(
|
||||||
|
[],
|
||||||
|
description="Enables upsert functionality. Before creating a check, Healthchecks.io looks for existing checks, filtered by fields listed in unique. If Healthchecks.io does not find a matching check, it creates a new check and returns it with the HTTP status code 201 If Healthchecks.io finds a matching check, it updates the existing check and returns it with HTTP status code 200. The accepted values for the unique field are name, tags, timeout, and grace.",
|
||||||
|
)
|
||||||
|
|
||||||
@validator('schedule')
|
@validator("schedule")
|
||||||
def validate_schedule(cls, value: str) -> str:
|
def validate_schedule(cls, value: str) -> str:
|
||||||
if not croniter.is_valid(value):
|
if not croniter.is_valid(value):
|
||||||
raise ValueError("Schedule is not a valid cron expression")
|
raise ValueError("Schedule is not a valid cron expression")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@validator('tz')
|
@validator("tz")
|
||||||
def validate_tz(cls, value: str) -> str:
|
def validate_tz(cls, value: str) -> str:
|
||||||
if not value in pytz.all_timezones:
|
if not value in pytz.all_timezones:
|
||||||
raise ValueError("Tz is not a valid timezone")
|
raise ValueError("Tz is not a valid timezone")
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@validator('methods')
|
@validator("methods")
|
||||||
def validate_methods(cls, value: str) -> str:
|
def validate_methods(cls, value: str) -> str:
|
||||||
if value not in ("", "POST"):
|
if value not in ("", "POST"):
|
||||||
raise ValueError("Methods is invalid, it should be either an empty string or POST")
|
raise ValueError(
|
||||||
|
"Methods is invalid, it should be either an empty string or POST"
|
||||||
|
)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@validator('unique')
|
@validator("unique")
|
||||||
def validate_unique(cls, value: List[Optional[str]]) -> List[Optional[str]]:
|
def validate_unique(cls, value: List[Optional[str]]) -> List[Optional[str]]:
|
||||||
for unique in value:
|
for unique in value:
|
||||||
if unique not in ('name', 'tags', 'timeout', 'grace'):
|
if unique not in ("name", "tags", "timeout", "grace"):
|
||||||
raise ValueError("Unique is not valid. Unique can only be name, tags, timeout, and grace or an empty list")
|
raise ValueError(
|
||||||
|
"Unique is not valid. Unique can only be name, tags, timeout, and grace or an empty list"
|
||||||
|
)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
@@ -106,9 +148,11 @@ class CheckPings(BaseModel):
|
|||||||
duration: float
|
duration: float
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_api_result(cls, ping_dict: Dict[str, Union[str, int, datetime]]) -> 'CheckPings':
|
def from_api_result(
|
||||||
ping_dict['number_of_pings'] = ping_dict['n']
|
cls, ping_dict: Dict[str, Union[str, int, datetime]]
|
||||||
ping_dict['user_agent'] = ping_dict['ua']
|
) -> "CheckPings":
|
||||||
|
ping_dict["number_of_pings"] = ping_dict["n"]
|
||||||
|
ping_dict["user_agent"] = ping_dict["ua"]
|
||||||
return cls(**ping_dict)
|
return cls(**ping_dict)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
Schemas for integrations
|
Schemas for integrations
|
||||||
https://healthchecks.io/docs/api/
|
https://healthchecks.io/docs/api/
|
||||||
"""
|
"""
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
@@ -9,3 +11,7 @@ class Integration(BaseModel):
|
|||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
kind: str
|
kind: str
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_api_result(cls, integration_dict: Dict[str, str]) -> "Integration":
|
||||||
|
return cls(**integration_dict)
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import pytest
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Union
|
from typing import Dict
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from healthchecks_io.schemas import checks
|
from healthchecks_io.schemas import checks
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def fake_check_api_result() -> Dict[str, Union[str, int]]:
|
def fake_check_api_result() -> Dict[str, Union[str, int]]:
|
||||||
yield {
|
yield {
|
||||||
@@ -22,9 +25,10 @@ def fake_check_api_result() -> Dict[str, Union[str, int]]:
|
|||||||
"update_url": "testhc.io/api/v1/checks/8f57a84b-86c2-4246-8923-02f83d17604a",
|
"update_url": "testhc.io/api/v1/checks/8f57a84b-86c2-4246-8923-02f83d17604a",
|
||||||
"pause_url": "testhc.io/api/v1/checks/8f57a84b-86c2-4246-8923-02f83d17604a/pause",
|
"pause_url": "testhc.io/api/v1/checks/8f57a84b-86c2-4246-8923-02f83d17604a/pause",
|
||||||
"channels": "*",
|
"channels": "*",
|
||||||
"timeout": 259200
|
"timeout": 259200,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def fake_check_ro_api_result() -> Dict[str, Union[str, int]]:
|
def fake_check_ro_api_result() -> Dict[str, Union[str, int]]:
|
||||||
yield {
|
yield {
|
||||||
@@ -40,9 +44,10 @@ def fake_check_ro_api_result() -> Dict[str, Union[str, int]]:
|
|||||||
"manual_resume": False,
|
"manual_resume": False,
|
||||||
"methods": "",
|
"methods": "",
|
||||||
"unique_key": "a6c7b0a8a66bed0df66abfdab3c77736861703ee",
|
"unique_key": "a6c7b0a8a66bed0df66abfdab3c77736861703ee",
|
||||||
"timeout": 3600
|
"timeout": 3600,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def fake_check() -> checks.Check:
|
def fake_check() -> checks.Check:
|
||||||
yield checks.Check(
|
yield checks.Check(
|
||||||
@@ -61,9 +66,10 @@ def fake_check() -> checks.Check:
|
|||||||
pause_url="testurl.com/api/v1/checks/test-uuid/pause",
|
pause_url="testurl.com/api/v1/checks/test-uuid/pause",
|
||||||
channel="*",
|
channel="*",
|
||||||
timeout=86400,
|
timeout=86400,
|
||||||
uuid="test-uuid"
|
uuid="test-uuid",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def fake_ro_check(fake_check: checks.Check):
|
def fake_ro_check(fake_check: checks.Check):
|
||||||
fake_check.unique_key = "test-unique-key"
|
fake_check.unique_key = "test-unique-key"
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from healthchecks_io.schemas import checks
|
from healthchecks_io.schemas import checks
|
||||||
from pydantic import ValidationError
|
|
||||||
|
|
||||||
def test_check_from_api_result(fake_check_api_result, fake_check_ro_api_result):
|
def test_check_from_api_result(fake_check_api_result, fake_check_ro_api_result):
|
||||||
check = checks.Check.from_api_result(fake_check_api_result)
|
check = checks.Check.from_api_result(fake_check_api_result)
|
||||||
assert check.name == fake_check_api_result['name']
|
assert check.name == fake_check_api_result["name"]
|
||||||
assert check.unique_key is None
|
assert check.unique_key is None
|
||||||
|
|
||||||
ro_check = checks.Check.from_api_result(fake_check_ro_api_result)
|
ro_check = checks.Check.from_api_result(fake_check_ro_api_result)
|
||||||
assert ro_check.name == fake_check_ro_api_result['name']
|
assert ro_check.name == fake_check_ro_api_result["name"]
|
||||||
assert ro_check.unique_key == fake_check_ro_api_result['unique_key']
|
assert ro_check.unique_key == fake_check_ro_api_result["unique_key"]
|
||||||
|
|
||||||
|
|
||||||
def test_check_validate_uuid(fake_check_api_result, fake_check_ro_api_result):
|
def test_check_validate_uuid(fake_check_api_result, fake_check_ro_api_result):
|
||||||
check = checks.Check.from_api_result(fake_check_api_result)
|
check = checks.Check.from_api_result(fake_check_api_result)
|
||||||
assert check.uuid == '8f57a84b-86c2-4246-8923-02f83d17604a'
|
assert check.uuid == "8f57a84b-86c2-4246-8923-02f83d17604a"
|
||||||
assert check.unique_key is None
|
assert check.unique_key is None
|
||||||
|
|
||||||
ro_check = checks.Check.from_api_result(fake_check_ro_api_result)
|
ro_check = checks.Check.from_api_result(fake_check_ro_api_result)
|
||||||
assert ro_check.uuid is None
|
assert ro_check.uuid is None
|
||||||
|
|
||||||
|
|
||||||
def test_check_create_validators():
|
def test_check_create_validators():
|
||||||
check_create = checks.CheckCreate(
|
check_create = checks.CheckCreate(
|
||||||
name="Test",
|
name="Test",
|
||||||
@@ -29,56 +31,46 @@ def test_check_create_validators():
|
|||||||
schedule="* * * * *",
|
schedule="* * * * *",
|
||||||
tz="UTC",
|
tz="UTC",
|
||||||
methods="POST",
|
methods="POST",
|
||||||
unique=['name']
|
unique=["name"],
|
||||||
)
|
)
|
||||||
assert check_create.schedule == "* * * * *"
|
assert check_create.schedule == "* * * * *"
|
||||||
|
|
||||||
# test validate_schedule
|
# test validate_schedule
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
check_create = checks.CheckCreate(
|
check_create = checks.CheckCreate(
|
||||||
name="Test",
|
name="Test", tags="", desc="Test", schedule="no good"
|
||||||
tags="",
|
|
||||||
desc="Test",
|
|
||||||
schedule="no good"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# test validate_tz
|
# test validate_tz
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
check_create = checks.CheckCreate(
|
check_create = checks.CheckCreate(
|
||||||
name="Test",
|
name="Test", tags="", desc="Test", tz="no good"
|
||||||
tags="",
|
|
||||||
desc="Test",
|
|
||||||
tz="no good"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# test validate_methods
|
# test validate_methods
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
check_create = checks.CheckCreate(
|
check_create = checks.CheckCreate(
|
||||||
name="Test",
|
name="Test", tags="", desc="Test", methods="no good"
|
||||||
tags="",
|
|
||||||
desc="Test",
|
|
||||||
methods="no good"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# test validate_unique
|
# test validate_unique
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
check_create = checks.CheckCreate(
|
check_create = checks.CheckCreate(
|
||||||
name="Test",
|
name="Test", tags="", desc="Test", unique=["no good"]
|
||||||
tags="",
|
|
||||||
desc="Test",
|
|
||||||
unique=["no good"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_check_pings_from_api():
|
def test_check_pings_from_api():
|
||||||
ping = {"type": "success",
|
ping = {
|
||||||
|
"type": "success",
|
||||||
"date": "2020-06-09T14:51:06.113073+00:00",
|
"date": "2020-06-09T14:51:06.113073+00:00",
|
||||||
"n": 4,
|
"n": 4,
|
||||||
"scheme": "http",
|
"scheme": "http",
|
||||||
"remote_addr": "192.0.2.0",
|
"remote_addr": "192.0.2.0",
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"ua": "curl/7.68.0",
|
"ua": "curl/7.68.0",
|
||||||
"duration": 2.896736
|
"duration": 2.896736,
|
||||||
}
|
}
|
||||||
this_ping = checks.CheckPings.from_api_result(ping)
|
this_ping = checks.CheckPings.from_api_result(ping)
|
||||||
assert this_ping.type == ping['type']
|
assert this_ping.type == ping["type"]
|
||||||
assert this_ping.duration == ping['duration']
|
assert this_ping.duration == ping["duration"]
|
||||||
|
|||||||
Reference in New Issue
Block a user