blob: 0404b5a0eaf29b1f63a15e2fa59aa5a8834cc91a [file] [log] [blame]
#
# Copyright (c) Memfault, Inc.
# See License.txt for details
#
import json
import os
import sys
from io import BufferedIOBase, BytesIO
from json import dumps
from typing import Any, List, Tuple
from unittest import mock
from unittest.mock import ANY, MagicMock
import pytest
from . import gdb_fake
gdb_fake.install_fake()
tests_dir = os.path.dirname(os.path.realpath(__file__))
scripts_dir = os.path.dirname(tests_dir)
sys.path.insert(0, scripts_dir)
from memfault_gdb import ( # noqa: E402 # pyright: ignore[reportArgumentType]
ArmCortexMCoredumpArch,
MemfaultConfig,
MemfaultCoredump,
MemfaultCoredumpWriter,
MemfaultLogin,
Section,
_post_chunk_data,
add_basic_auth,
has_uploaded_symbols,
lookup_registers_from_list,
parse_maintenance_info_sections,
read_memory_until_error,
settings_save,
should_capture_section,
)
# We save the time in the coredump generated so each run of "memfault coredump" looks unique
# Let's monkeypatch time so we get a reproducible time in out unit tests
@pytest.fixture(autouse=True)
def _patch_datetime_now(mocker):
mocker.patch("memfault_gdb.time", return_value=float(1565034658.6))
@pytest.fixture()
def http_expect_request():
connections: List[MagicMock] = []
expected_requests: List[Tuple[Any, ...]] = []
actual_request_count = 0
def _http_expect_request(url, method, assert_body_fn, req_headers, resp_status, resp_body):
expected_requests.append((url, method, assert_body_fn, req_headers, resp_status, resp_body))
def _create_connection_constructor(scheme):
def _connection_constructor(hostname, port=0):
connection = MagicMock()
try:
(
exp_url,
exp_method,
assert_body_fn,
exp_req_headers,
resp_status,
resp_body,
) = expected_requests[len(connections)]
except IndexError as e:
raise AssertionError("Unexpected mock HTTPConnection created!") from e
def _request(method, path, body=None, headers=None):
nonlocal actual_request_count
actual_request_count += 1
standard_port = (scheme == "http" and port != 80) or (
scheme == "https" and port != 443
)
port_str = ":{}".format(port) if standard_port else ""
assert "{}://{}{}{}".format(scheme, hostname, port_str, path) == exp_url
assert method == exp_method
if assert_body_fn and assert_body_fn is not ANY:
body_bytes = body.read() if isinstance(body, BufferedIOBase) else body
assert_body_fn(body_bytes)
assert headers == exp_req_headers
connection.request = _request
response = MagicMock()
response.status = resp_status
response.reason = ""
response.read.return_value = (
None if resp_body is None else dumps(resp_body).encode("utf8")
)
connection.getresponse.return_value = response
connections.append(connection)
return connection
return _connection_constructor
with (
mock.patch("memfault_gdb.HTTPSConnection", _create_connection_constructor("https")),
mock.patch("memfault_gdb.HTTPConnection", _create_connection_constructor("http")),
):
yield _http_expect_request
assert actual_request_count == len(expected_requests)
TEST_EMAIL = "[email protected]"
TEST_PASSWORD = "open_sesame"
TEST_AUTH_HEADERS = {"Authorization": "Basic bWFydGlqbkBtZW1mYXVsdC5jb206b3Blbl9zZXNhbWU="}
TEST_ORG = "acme-inc"
TEST_PROJECT = "smart-sink"
TEST_HW_VERSION = "DEVBOARD"
TEST_PROJECT_KEY = "7f083342d30444b2b3bed65357f24a19"
@pytest.fixture(autouse=True)
def test_config(mocker, tmp_path):
"""Updates MEMFAULT_CONFIG to contain TEST_... values"""
rv = MemfaultConfig()
mocker.patch("memfault_gdb.MEMFAULT_CONFIG", rv)
rv.prompt = lambda prompt: "Y"
rv.json_path = str(tmp_path / "settings.yml")
return rv
@pytest.fixture()
def test_config_with_login(test_config):
test_config.email = TEST_EMAIL
test_config.password = TEST_PASSWORD
test_config.organization = TEST_ORG
test_config.project = TEST_PROJECT
return test_config
TEST_MAINTENANCE_INFO_SECTIONS_FIXTURE_FMT = """Exec file:
`{}', file type elf32-littlearm.
[0] 0x26000->0x6b784 at 0x00006000: .text ALLOC LOAD READONLY CODE HAS_CONTENTS
[1] 0x2003d800->0x2003e878 at 0x0005d800: .rtt ALLOC
[2] 0x6b784->0x6b7a8 at 0x0004b784: .gnu_build_id ALLOC LOAD READONLY DATA HAS_CONTENTS
[3] 0x2003fe00->0x2003fe14 at 0x0005fe00: .fw_install_info ALLOC
[4] 0x2003ff00->0x20040000 at 0x0005ff00: .noinit ALLOC
[5] 0x6b7a8->0x6b7c8 at 0x0004b7a8: .sdh_soc_observers ALLOC LOAD READONLY DATA HAS_CONTENTS
[6] 0x6b7c8->0x6b850 at 0x0004b7c8: .sdh_ble_observers ALLOC LOAD READONLY DATA HAS_CONTENTS
[7] 0x6b850->0x6b860 at 0x0004b850: .sdh_stack_observers ALLOC LOAD READONLY DATA HAS_CONTENTS
[8] 0x6b860->0x6b868 at 0x0004b860: .sdh_req_observers ALLOC LOAD READONLY DATA HAS_CONTENTS
[9] 0x6b868->0x6b880 at 0x0004b868: .sdh_state_observers ALLOC LOAD READONLY DATA HAS_CONTENTS
[10] 0x6b880->0x6b8a8 at 0x0004b880: .nrf_queue ALLOC LOAD READONLY DATA HAS_CONTENTS
[11] 0x6b8a8->0x6b8d0 at 0x0004b8a8: .nrf_balloc ALLOC LOAD READONLY DATA HAS_CONTENTS
[12] 0x6b8d0->0x6b8f8 at 0x0004b8d0: .cli_command ALLOC LOAD READONLY DATA HAS_CONTENTS
[13] 0x6b8f8->0x6b908 at 0x0004b8f8: .crypto_data ALLOC LOAD READONLY DATA HAS_CONTENTS
[14] 0x6b908->0x6b9e0 at 0x0004b908: .log_const_data ALLOC LOAD READONLY DATA HAS_CONTENTS
[15] 0x6b9e0->0x6ba00 at 0x0004b9e0: .log_backends ALLOC LOAD READONLY DATA HAS_CONTENTS
[16] 0x6ba00->0x6ba08 at 0x0004ba00: .ARM.exidx ALLOC LOAD READONLY DATA HAS_CONTENTS
[17] 0x200057b8->0x20005bf0 at 0x000557b8: .data ALLOC LOAD DATA HAS_CONTENTS
[18] 0x20005bf0->0x20005c04 at 0x00055bf0: .cli_sorted_cmd_ptrs ALLOC LOAD DATA HAS_CONTENTS
[19] 0x20005c04->0x20005c2c at 0x00055c04: .fs_data ALLOC LOAD DATA HAS_CONTENTS
[20] 0x20005c30->0x2000b850 at 0x00055c2c: .bss ALLOC
[21] 0x2000b850->0x2000d850 at 0x00055c30: .heap READONLY HAS_CONTENTS
[22] 0x2000b850->0x2000d850 at 0x00057c30: .stack_dummy READONLY HAS_CONTENTS
[23] 0x0000->0x0030 at 0x00059c30: .ARM.attributes READONLY HAS_CONTENTS
[24] 0x0000->0x00f4 at 0x00059c60: .comment READONLY HAS_CONTENTS
[25] 0x0000->0xf3d30 at 0x00059d54: .debug_info READONLY HAS_CONTENTS
[26] 0x0000->0x1eeec at 0x0014da84: .debug_abbrev READONLY HAS_CONTENTS
[27] 0x0000->0x59416 at 0x0016c970: .debug_loc READONLY HAS_CONTENTS
[28] 0x0000->0x2c10 at 0x001c5d88: .debug_aranges READONLY HAS_CONTENTS
[29] 0x0000->0x121a8 at 0x001c8998: .debug_ranges READONLY HAS_CONTENTS
[30] 0x0000->0x37f3a at 0x001dab40: .debug_macro READONLY HAS_CONTENTS
[31] 0x0000->0x708cb at 0x00212a7a: .debug_line READONLY HAS_CONTENTS
[32] 0x0000->0xcee28 at 0x00283345: .debug_str READONLY HAS_CONTENTS
[33] 0x0000->0x91f0 at 0x00352170: .debug_frame READONLY HAS_CONTENTS
[34] 0x0000->0x00df at 0x0035b360: .stabstr READONLY HAS_CONTENTS
"""
@pytest.fixture()
def fake_elf(tmp_path):
file = tmp_path / "fake.elf"
file.write_bytes(b"ELF")
return file
@pytest.fixture()
def maintenance_info_sections_fixture(fake_elf):
return TEST_MAINTENANCE_INFO_SECTIONS_FIXTURE_FMT.format(fake_elf)
# Data from a GDB Server with register names that differ from the final names we use
@pytest.fixture()
def info_reg_all_fixture(fake_elf):
return """
r0 0x0 0
r1 0x1 1
r2 0xe000ed00 -536810240
r3 0x20003fe8 536887272
r4 0x20000294 536871572
r5 0x0 0
r6 0x0 0
r7 0x0 0
r8 0x0 0
r9 0x0 0
r10 0x0 0
r11 0x0 0
r12 0x0 0
sp 0x200046c8 0x200046c8 <os_idle_thread_stack+480>
lr 0x8002649 134227529
pc 0x8007122 0x8007122 <hal_sleep+18>
cpsr 0x61000000 1627389952
PRIMASK 0x0 0
BASExPRI 0x0 0
FAULTMASK 0x0 0
CONTROL 0x0 0
MSP 0x2001ffc0 537001920
PSP 0x200046c8 536889032
"""
@pytest.fixture()
def _settings_coredump_allowed(test_config):
settings_save({"coredump.allow": True})
def test_parse_maintenance_info_sections_no_file():
fn, sections = parse_maintenance_info_sections("""Remote serial target in gdb-specific protocol:
Debugging a target over a serial line.
""")
assert fn is None
assert sections is None
def test_parse_maintenance_info_sections_with_file(maintenance_info_sections_fixture, fake_elf):
fn, sections = parse_maintenance_info_sections(maintenance_info_sections_fixture)
assert fn == str(fake_elf)
assert sections
assert len(sections) == 35
assert sections[0] == Section(0x26000, 0x6B784 - 0x26000, ".text", read_only=True)
assert sections[20] == Section(0x20005C30, 0x2000B850 - 0x20005C30, ".bss", read_only=False)
def test_read_current_registers(mocker, info_reg_all_fixture):
frame = MagicMock()
frame.read_register.return_value = gdb_fake.Value(1, type=gdb_fake.Type(sizeof=4))
mocker.patch.object(gdb_fake, "newest_frame", return_value=frame)
dummy_dict: dict = {}
arch = ArmCortexMCoredumpArch()
register_list = lookup_registers_from_list(arch, info_reg_all_fixture, dummy_dict)
for reg in arch.register_collection_list:
# A mock read_register should have taken place so the value should be non-zero
assert len(register_list[reg]) == 4
assert register_list[reg][0] != 0, reg
def test_should_capture_section():
# Never capture .text, even when NOT marked READONLY:
assert not should_capture_section(Section(0, 10, ".text", read_only=False))
# Never capture .debug_...
assert not should_capture_section(Section(0, 10, ".debug_info", read_only=True))
# Never capture 0 length sections
assert not should_capture_section(Section(0, 0, ".dram0.data", read_only=False))
# Always capture sections with .bss, .heap, .data or .stack in the name, even when marked READONLY:
assert should_capture_section(Section(0, 10, ".foo.bss", read_only=True))
assert should_capture_section(Section(0, 10, ".stack_dummy", read_only=True))
assert should_capture_section(Section(0, 10, ".heap_psram", read_only=True))
assert should_capture_section(Section(0, 10, ".dram0.data", read_only=True))
def test_armv7_get_used_ram_base_addresses():
sections = (
# Sections must meet the guess criteria:
# 1. within an allowed region
# 2. not read-only OR contain `.data`/`.bss` etc in the name
Section(0x21000000, 10, "", read_only=False),
Section(0x30000000, 10, "", read_only=True),
Section(0x3F000000, 10, ".data.etc", read_only=True),
Section(0x40000000, 10, "", read_only=True),
Section(0x70000000, 10, "", read_only=True),
Section(0x90000000, 10, "", read_only=True),
Section(0x00000000, 10, "", read_only=True),
Section(0xE0000000, 10, "", read_only=True),
)
collection_size_arm = 1 * 1024 * 1024 # 1MB
assert [
(0x21000000, collection_size_arm),
(0x3F000000, collection_size_arm),
] == ArmCortexMCoredumpArch().guess_ram_regions(sections)
def test_read_memory_until_error_no_error():
read_size = 1024
size = 1024 * 1024
inferior = MagicMock()
inferior.read_memory.return_value = b"A" * read_size
data = read_memory_until_error(inferior, 0x20000000, size, read_size=read_size)
assert data == b"A" * size
def test_read_memory_until_error_after_10k():
read_size = 1024
size = 1024 * 1024
inferior = MagicMock()
total_size = 0
def _read_memory_raise_after_10k(_addr, _size):
nonlocal total_size
total_size += _size
if total_size > _size * 10:
raise ValueError
return b"A" * _size
inferior.read_memory.side_effect = _read_memory_raise_after_10k
data = read_memory_until_error(inferior, 0x20000000, size, read_size=read_size)
assert data == b"A" * read_size * 10
def test_coredump_writer(snapshot):
arch = ArmCortexMCoredumpArch()
device_serial = "device_serial"
software_type = "main"
software_version = "1.2.3"
hardware_revision = "gdb-proto"
cd_writer = MemfaultCoredumpWriter(
arch, device_serial, software_type, software_version, hardware_revision
)
cd_writer.trace_reason = 5
cd_writer.regs = [ # pyright: ignore[reportAttributeAccessIssue]
{
"r0": 4 * b"\x00",
"r1": 4 * b"\x01",
"r2": 4 * b"\x02",
"r3": 4 * b"\x03",
"r4": 4 * b"\x04",
"r5": 4 * b"\x05",
"r6": 4 * b"\x06",
"r7": 4 * b"\x07",
"r8": 4 * b"\x08",
"r9": 4 * b"\x09",
"r10": 4 * b"\x0a",
"r11": 4 * b"\x0b",
"r12": 4 * b"\x0c",
"sp": 4 * b"\x0d",
"lr": 4 * b"\x0e",
"pc": 4 * b"\x0f",
"xpsr": 4 * b"\x10",
"msp": 4 * b"\x11",
"psp": 4 * b"\x12",
"primask": 4 * b"\x13",
"control": 4 * b"\x14",
},
]
section = Section(4, 32, ".test", "") # pyright: ignore[reportArgumentType]
section.data = b"hello world"
cd_writer.add_section(section)
f_out = BytesIO()
cd_writer.write(f_out)
f_out.seek(0)
snapshot.assert_match(f_out.read().hex())
def test_http_basic_auth():
headers = add_basic_auth("[email protected]", "open_sesame", {"Foo": "Bar"})
assert headers == {
"Foo": "Bar",
"Authorization": "Basic bWFydGlqbkBtZW1mYXVsdC5jb206b3Blbl9zZXNhbWU=",
}
@pytest.fixture()
def _gdb_for_coredump(mocker, maintenance_info_sections_fixture, _settings_coredump_allowed):
frame = MagicMock()
mocker.patch.object(gdb_fake, "newest_frame", return_value=frame)
def _gdb_execute(cmd, to_string):
if cmd == "maintenance info sections":
return maintenance_info_sections_fixture
if cmd == "info all-registers":
return "r0\t0\n"
if cmd == "info threads":
return ""
if cmd.startswith("show arch"):
return "The target architecture is set automatically (currently armv7m)"
raise NotImplementedError(f"Command: {cmd}")
mocker.patch.object(gdb_fake, "execute", side_effect=_gdb_execute)
selected_thread = MagicMock()
mocker.patch.object(gdb_fake, "selected_thread", return_value=selected_thread)
inferior = MagicMock()
inferior.threads.return_value = [selected_thread]
mocker.patch.object(gdb_fake, "selected_inferior", return_value=inferior)
def _read_memory(addr, size):
return b"A" * size # Write 41 'A' for reads to memory regions
inferior.read_memory = _read_memory
@pytest.mark.usefixtures("_gdb_for_coredump")
def test_coredump_command_no_target(capsys):
"""Test coredump command shows error when no target is connected"""
inferior = gdb_fake.selected_inferior()
assert inferior
inferior.threads.return_value = []
cmd = MemfaultCoredump()
cmd.invoke("--project-key {}".format(TEST_PROJECT_KEY), True)
stdout = capsys.readouterr().out
assert "No target" in stdout
def test_coredump_command_non_arm(mocker, capsys):
"""Test coredump command shows error when no ARM or XTENSA target"""
def _gdb_execute(cmd, to_string):
if cmd.startswith("show arch"):
return "The target architecture is set automatically (currently riscv)"
return ""
mocker.patch.object(gdb_fake, "execute", side_effect=_gdb_execute)
cmd = MemfaultCoredump()
cmd.invoke("--project-key {}".format(TEST_PROJECT_KEY), True)
stdout = capsys.readouterr().out
assert "This command is currently only supported for ARM and XTENSA targets!" in stdout
@pytest.mark.usefixtures("_gdb_for_coredump")
def test_coredump_command_with_project_key(http_expect_request, test_config):
"""Test coredump command project key"""
# Expect to use ingress_uri from config if not provided explicitely to the command invocation
test_config.ingress_uri = "https://custom-ingress.memfault.com"
http_expect_request(
"https://custom-ingress.memfault.com/api/v0/upload/coredump",
"POST",
ANY, # Not checking the request body at the moment
{"Content-Type": "application/octet-stream", "Memfault-Project-Key": TEST_PROJECT_KEY},
200,
{},
)
cmd = MemfaultCoredump()
cmd.invoke("--project-key {}".format(TEST_PROJECT_KEY), True)
def test_coredump_command_not_allowing(
http_expect_request,
capsys,
test_config,
):
"""Test coredump command and not accepting prompt"""
test_config.prompt = lambda prompt: "N"
cmd = MemfaultCoredump()
cmd.invoke("--project-key {}".format(TEST_PROJECT_KEY), True)
stdout = capsys.readouterr().out
assert "Aborting" in stdout
@pytest.mark.usefixtures("_gdb_for_coredump")
def test_coredump_all_overrides(http_expect_request, test_config, mocker):
"""Test coredump command with all overrides set"""
# Verify coredump is uploaded
test_config.ingress_uri = "https://custom-ingress.memfault.com"
http_expect_request(
"https://custom-ingress.memfault.com/api/v0/upload/coredump",
"POST",
ANY,
{"Content-Type": "application/octet-stream", "Memfault-Project-Key": TEST_PROJECT_KEY},
200,
{},
)
writer = MagicMock()
mocker.patch("memfault_gdb.MemfaultCoredumpWriter", writer)
hardware_revision = "TESTREVISION"
software_version = "TESTVERSION"
software_type = "TESTTYPE"
device_serial = "TESTSERIAL"
cmd = MemfaultCoredump()
cmd.invoke(
"--project-key {} --hardware-revision {} --software-version {} --software-type {}"
" --device-serial {}".format(
TEST_PROJECT_KEY, hardware_revision, software_version, software_type, device_serial
),
True,
)
# Verify that all args were successfully extracted and passed to writer
writer.assert_called_once_with(
ANY, device_serial, software_type, software_version, hardware_revision
)
@pytest.mark.parametrize(
("cmd_option", "cmd_type"),
[
("--hardware-revision", "hardware revision"),
("--software-version", "software version"),
("--software-type", "software type"),
("--device-serial", "device serial"),
],
)
def test_coredump_override_invalid_input(capsys, cmd_option, cmd_type):
"""Test coredump command with invalid input for each override"""
invalid_input = 'inv"lid'
cmd = MemfaultCoredump()
cmd.invoke("--project-key {} {} {}".format(TEST_PROJECT_KEY, cmd_option, invalid_input), True)
# Verify an error is thrown for the invalid input
stderr = capsys.readouterr().err
assert "Invalid characters in {}: {}".format(cmd_type, invalid_input) in stderr
@pytest.mark.parametrize(
("expected_result", "extra_cmd_args", "expected_coredump_fn"),
[
(None, "", "fixture2.bin"),
(None, "-r 0x600000 8 -r 0x800000 4", "fixture3.bin"),
],
)
@pytest.mark.usefixtures("_gdb_for_coredump")
def test_coredump_command_with_login_no_existing_release_or_symbols(
test_config_with_login,
http_expect_request,
test_config,
expected_result,
extra_cmd_args,
expected_coredump_fn,
capsys,
snapshot,
):
"""Test coredump command without project key, but with logged in user and default org & project"""
# Check whether Release and symbols exists -- No (404)
http_expect_request(
"https://api.memfault.com/api/v0/organizations/acme-inc/projects/smart-sink/software_types/main/software_versions/1.0.0-md5+b61b2d6c",
"GET",
ANY,
ANY,
404,
None,
)
# Upload Symbols
token = "token-foo"
upload_url = "https://memfault-test-east1.s3.amazonaws.com/symbols/foo"
http_expect_request(
"https://api.memfault.com/api/v0/organizations/acme-inc/projects/smart-sink/upload",
"POST",
ANY,
ANY,
200,
{
"data": {
"token": token,
"upload_url": upload_url,
}
},
)
http_expect_request(
upload_url,
"PUT",
ANY,
ANY,
200,
None,
)
http_expect_request(
"https://api.memfault.com/api/v0/organizations/acme-inc/projects/smart-sink/symbols",
"POST",
lambda body_bytes: json.loads(body_bytes)
== {
"file": {"token": token, "name": "symbols.elf"},
"software_version": {
"version": "1.0.0-md5+b61b2d6c",
"software_type": "main",
},
},
ANY,
200,
{"data": {}},
)
# Get Project-Key:
http_expect_request(
"https://api.memfault.com/api/v0/organizations/acme-inc/projects/smart-sink/api_key",
"GET",
ANY,
ANY,
200,
{"data": {"api_key": TEST_PROJECT_KEY}},
)
# Upload coredump:
def _check_request_body(body_bytes):
# truncated in the interest of avoiding bloat in snapshot:
snapshot.assert_match(body_bytes[:500].hex())
http_expect_request(
"https://ingress.memfault.com/api/v0/upload/coredump",
"POST",
_check_request_body,
{"Content-Type": "application/octet-stream", "Memfault-Project-Key": TEST_PROJECT_KEY},
200,
{},
)
cmd = MemfaultCoredump()
cmd.invoke(extra_cmd_args, True)
stdout = capsys.readouterr().out
assert (
"""Coredump uploaded successfully!
Once it has been processed, it will appear here:
https://app.memfault.com/organizations/acme-inc/projects/smart-sink/issues?live"""
in stdout
)
def test_login_command_simple(http_expect_request, test_config):
http_expect_request(
"https://api.memfault.com/auth/me", "GET", None, TEST_AUTH_HEADERS, 200, {"id": 123}
)
login = MemfaultLogin()
login.invoke("{} {}".format(TEST_EMAIL, TEST_PASSWORD), True)
assert test_config.email == TEST_EMAIL
assert test_config.password == TEST_PASSWORD
assert test_config.organization is None
assert test_config.project is None
assert test_config.user_id == 123
def test_login_command_with_all_options(http_expect_request, test_config):
test_api_uri = "http://dev-api.memfault.com:8000"
test_ingress_uri = "http://dev-ingress.memfault.com"
http_expect_request(
"http://dev-api.memfault.com:8000/auth/me", "GET", None, TEST_AUTH_HEADERS, 200, {"id": 123}
)
login = MemfaultLogin()
login.invoke(
"{} {} -o {} -p {} --api-uri {} --ingress-uri {}".format(
TEST_EMAIL,
TEST_PASSWORD,
TEST_ORG,
TEST_PROJECT,
test_api_uri,
test_ingress_uri,
),
True,
)
assert test_config.email == TEST_EMAIL
assert test_config.password == TEST_PASSWORD
assert test_config.organization == TEST_ORG
assert test_config.project == TEST_PROJECT
assert test_config.api_uri == test_api_uri
assert test_config.ingress_uri == test_ingress_uri
@pytest.mark.parametrize(
("expected_result", "resp_status", "resp_body"),
[
(
True,
200,
{"data": {"symbol_file": {"downloadable": True}}},
),
(
False,
200,
{"data": {}},
),
(False, 404, None),
],
)
def test_has_uploaded_symbols(
http_expect_request, expected_result, resp_status, resp_body, test_config_with_login
):
test_software_type = "main"
test_software_version = "1.0.0"
http_expect_request(
"https://api.memfault.com/api/v0/organizations/{}/projects/{}/software_types/{}/software_versions/{}".format(
test_config_with_login.organization,
test_config_with_login.project,
test_software_type,
test_software_version,
),
"GET",
None,
TEST_AUTH_HEADERS,
resp_status,
resp_body,
)
assert (
has_uploaded_symbols(test_config_with_login, test_software_type, test_software_version)
== expected_result
)
def test_post_chunk_data(http_expect_request):
base_uri = "https://example.chunks.memfault.com"
chunk_data = bytearray([1, 2, 3, 4])
device_serial = "GDB_TESTSERIAL"
def _check_request_body(body_bytes):
assert body_bytes.hex() == chunk_data.hex(), body_bytes
http_expect_request(
"{}/api/v0/chunks/GDB_TESTSERIAL".format(base_uri),
"POST",
_check_request_body,
{"Content-Type": "application/octet-stream", "Memfault-Project-Key": TEST_PROJECT_KEY},
202,
None,
)
_post_chunk_data(
chunk_data, project_key=TEST_PROJECT_KEY, chunks_uri=base_uri, device_serial=device_serial
)