В предыдущей статье мы формулировали цели и составляли список тестов для автоматизации. Далее в работе QA обычно следует разработка базовых тестов и обвязки для них, чем мы в этой статье и займёмся. Полный код проекта лежит на гитхаб и приводить его в статье мы не будем.
Непосредственно тесты
Ну что же, пришло время написать немного кода. Здесь я опишу основной подход на примере тестов для FTP. И так - вот код теста:
class TestFTP:
def test_ftp_10MB_file_upload(self, ftp, perfmeter, test_name, ssh, target_dir):
file_data = get_file_with_size_mb(10)
md5sum_source = get_md5sum(file_data)
file_name = PurePath('test_ftp_file_10MB.bin')
source_file = PurePath('/tmp') / file_name
with open(source_file, 'wb') as fh:
fh.write(file_data)
fh.close()
with open(source_file, 'rb') as fh:
ftp.cwd(CFG['FTP']['PATH'])
logging.info(f"Current path: {ftp.pwd()}")
logging.info(f"List before operation: {list(ftp.mlsd())}")
perfmeter.estimate_func(test_name, ftp.storbinary, f"STOR {file_name}", fh)
logging.info(f"List after operation: {list(ftp.mlsd())}")
md5sum_target = ssh.md5sum(target_dir / file_name)
assert md5sum_source == md5sum_target
Останавливаться подробно на объявлении теста и вызовах фикстур мы не будем, разберём их по мере использования в тесте.
Сперва мы генерируем файл заданного размера:
file_data = get_file_with_size_mb(10)
md5sum_source = get_md5sum(file_data)
Для этого используем “os.urandom(size*1024*1024)”, и тут же считаем md5 сумму, которую используем позже для проверки корректности копирования.
Для упрощения работы через стандартную библиотеку ftplib сохраняем файл локально:
file_name = PurePath('test_ftp_file_10MB.bin')
source_file = PurePath('/tmp') / file_name
with open(source_file, 'wb') as fh:
fh.write(file_data)
fh.close()
И затем кладём этот файл на NAS по FTP:
with open(source_file, 'rb') as fh:
ftp.cwd(CFG['FTP']['PATH'])
logging.info(f"Current path: {ftp.pwd()}")
logging.info(f"List before operation: {list(ftp.mlsd())}")
perfmeter.estimate_func(test_name, ftp.storbinary, f"STOR {file_name}", fh)
logging.info(f"List after operation: {list(ftp.mlsd())}")
И это, по сути, главная часть теста.
Объект ftp из “ftp.cwd(CFG['FTP']['PATH'])” создаётся фикстурой ftp:
@pytest.fixture(scope="session")
def ftp():
ftpcon = FTP(CFG['FTP']['ADDRESS'])
ftpcon.login(user=CFG['FTP']['LOGIN'], passwd=CFG['FTP']['PASSWD'])
logging.info("FTP Welcome message:" + ftpcon.getwelcome())
yield ftpcon
ftpcon.quit()
Благодаря scope="session" ftp подключение будет жить весь прогон тестов, а по завершении тестов корректно закроет соединение.
Для измерения времени копирования мы используем perfmeter в строке “perfmeter.estimate_func(test_name, ftp.storbinary, f"STOR {file_name}", fh)”.
class PerformaceMeter:
def __init__(self):
self._init_db_connection(CFG['DIAGNOSTIC']['PERF_STAT_DB'])
…
def estimate_func(self, test_name, func, *args):
start_time = time.time()
func(*args)
end_time = time.time()
func_time = end_time - start_time
logging.info(f"Test {test_name} - {func_time}")
self._db_cur.execute("insert into OperationTime values (?, ?, ?, ?)",
(test_name, start_time, end_time, 0))
self._db_con.commit()
Объект perfmeter класса PerformasnceMeter создаётся тоже фикстурой для скоупа session. Время выполнения операции мы вычисляем как разницу между end_time и start_time, так как высокой точности нам тут и не нужно. Результат сразу пишем в базу.
Следующим шагом мы проверяем корректность выполнения копирования.
md5sum_target = ssh.md5sum(target_dir / file_name)
assert md5sum_source == md5sum_target
Здесь объект ssh класса SSH_Client - это обёртка над paramiko.SSHClient. В нём хранится ssh сессия:
class SSH_Client:
def __init__(self):
self._client = paramiko.SSHClient()
self._client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
def connect(self, hostname, username, password=None):
if password:
logging.info(f"ssh {username}@{hostname} with password {password}") # security :)
self._client.connect(
hostname=hostname, username=username, password=password)
else:
logging.info(f"ssh {username}@{hostname} without password")
self._client.connect(hostname=hostname, username=username)
self._sftp = self._client.open_sftp()
и сделаны методы для выполнения определённых команд и парсинга результата:
def exec(self, cmd):
logging.debug(f"exec$ {cmd}")
stdin, stdout, stderr = self._client.exec_command(cmd, get_pty=True)
stdin.close()
out = stdout.readlines()
out_text = ''.join(out)
logging.debug(f"STDOUT:\n{out_text}")
err = stderr.readlines()
err_text = ''.join(err)
logging.debug(f"STDERR:\n{err_text}")
return out, err
def md5sum(self, path):
out, err = self.exec(f"md5sum {path}")
for l in out:
if re.match(r"\S{32} ", l):
md5sum = l[:32]
break
return md5sum
Так что, упрощённо говоря, мы в начале теста генерируем случайный файл заданного размера и снимаем с него md5 сумму, копируем файл на удалённый сервер, засекая время копирования, подключаемся туда по ssh, снимаем md5 сумму для файла и сравниваем их.
Подобным образом мы работаем и с другими протоколами. По итогу мы сохраняем время выполнения операции в базу вне зависимости от корректности операции (и это допустимо), но в случае возникновения проблем с копированием мы сможем провести диагностику.
Дополнительные тесты
При разработке тестов пришлось пересмотреть некоторые приоритеты. А именно - мы приоритизировали тестирование производительности различных способов подключения дисков, а также добавили проверку WebDAV.
Тестирование различных методов подключения дисков
Про тестирование дисков есть отличная статья на Хабре: https://habr.com/ru/articles/154235/. Мы же, проанализировав задачу, решили вынести данный вид тестов в отдельный проектик. Строго говоря, это будет набор конфигураций для fio. Результаты тестов будут обрабатываться вручную. Причин для такого решения несколько:
Менять методы подключения дисков мы будем не часто. А значит и fio гонять часто не придётся.
Сами тесты идут длительное время. Мешать их с нашими относительно шустрыми тестами не хотелось бы. Да и смысла в этом не много.
Подробнее эту тему постараемся раскрыть в следующих статьях.
Настройка WebDAV
По заявкам в комментах к прошлой статьи решили добавить в список тестов и WebDAV. В конце концов это довольно популярное решение, хотя и не среди пользователей OMV. Ведь плагина для него нету даже в omv-extras (точнее был, но протух). Почитав форум приняли решение не связываться с плагинами. У нас остаётся 2 пути:
Запустить WebDAV в Docker, что и рекомендуют на форуме. Решение выглядит разумным и удобным, но “загрязняет” результаты тестов. Ведь помимо сервера систему будет нагружать ещё и Docker. Да, у него оверхед не очень большой, но присутствует.
Сконфигурировать WebDAV нативно в системе. Этот путь нравится мне ещё меньше, так как придётся вмешиваться в конфиги, отлаженные разработчиками OMV. Последствия такого вмешательства для меня лично не очень очевидны. Да и поддерживать это решение будет сложнее.
Из двух зол выбрали меньшее и более стабильное. То есть Докер.
Как мы не поставили докер на OMV через плагины
Попытка установить новый плагин для докера по инструкции с форума увенчалась полным провалом:
Failed to execute command 'export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin; export LANG=C.UTF-8; export LANGUAGE=; export DEBIAN_FRONTEND=noninteractive; apt-get --yes --allow-downgrades --allow-change-held-packages --fix-missing --allow-unauthenticated --reinstall install openmediavault-compose 2>&1' with exit code '100': Reading package lists...
Building dependency tree...
Reading state information...
Some packages could not be installed. This may mean that you have
requested an impossible situation or if you are using the unstable
distribution that some required packages have not yet been created
or been moved out of Incoming.
The following information may help to resolve the situation:
The following packages have unmet dependencies:
openmediavault-compose : Depends: openmediavault-sharerootfs but it is not installable
E: Unable to correct problems, you have held broken packages.
OMV\ExecException: Failed to execute command 'export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin; export LANG=C.UTF-8; export LANGUAGE=; export DEBIAN_FRONTEND=noninteractive; apt-get --yes --allow-downgrades --allow-change-held-packages --fix-missing --allow-unauthenticated --reinstall install openmediavault-compose 2>&1' with exit code '100': Reading package lists...
Building dependency tree...
Reading state information...
Some packages could not be installed. This may mean that you have
requested an impossible situation or if you are using the unstable
distribution that some required packages have not yet been created
or been moved out of Incoming.
The following information may help to resolve the situation:
The following packages have unmet dependencies:
openmediavault-compose : Depends: openmediavault-sharerootfs but it is not installable
E: Unable to correct problems, you have held broken packages. in /usr/share/openmediavault/engined/rpc/pluginmgmt.inc:247
Stack trace:
#0 /usr/share/php/openmediavault/rpc/serviceabstract.inc(619): Engined\Rpc\PluginMgmt->Engined\Rpc\{closure}('/tmp/bgstatusfo...', '/tmp/bgoutputNb...')
#1 /usr/share/openmediavault/engined/rpc/pluginmgmt.inc(251): OMV\Rpc\ServiceAbstract->execBgProc(Object(Closure))
#2 [internal function]: Engined\Rpc\PluginMgmt->install(Array, Array)
#3 /usr/share/php/openmediavault/rpc/serviceabstract.inc(123): call_user_func_array(Array, Array)
#4 /usr/share/php/openmediavault/rpc/rpc.inc(86): OMV\Rpc\ServiceAbstract->callMethod('install', Array, Array)
#5 /usr/sbin/omv-engined(537): OMV\Rpc\Rpc::call('Plugin', 'install', Array, Array, 1)
#6 {main}
Какой-то бардак с зависимостями. На форуме про такое тоже слышали и решили “переустановкой системы”. Вообще чем плотнее работаю с OMV, тем больше понимаю позицию тех, кто хочет купить условный Synology, и чтобы у них всё “просто работало”.
Наша цель - сделать максимально простое, стабильное и легко воспроизводимое решение. Взяли уже готовый образ с Docker Hub: https://hub.docker.com/r/bytemark/webdav.
Качаем и запускаем его:
sudo docker run --restart always \
-v /srv/dev-disk-by-uuid-5c2aebc7-baad-45ca-a418-703fda90f8db/webdav:/var/lib/dav \
-e AUTH_TYPE=Digest -e USERNAME=test -e PASSWORD=test \
--publish 8079:80 -d bytemark/webdav
И проверяем, подключив его как сетевой диск в Проводник. Создать папку я в нём могу:
Папка появляется на сервере:
test@ubernas-x86:/srv/dev-disk-by-uuid-5c2aebc7-baad-45ca-a418-703fda90f8db/webdav/data$ ls -l
total 4
drwxrwsr-x+ 3 82 82 4096 Jul 20 05:12 webdavtestvk
Переходим к автоматизации!
Чтобы не тратить силы на исследования питоновских библиотек для WebDAV, воспользуемся утилитой mount, и будем работать с удалённой директорией, как с локальной. Монтирование можно осуществить по команде:
sudo mount -t davfs http://192.168.10.20:8079 /home/vkovalev/webdavtest
Либо прописать соответствующую инструкцию в fstab и положить креды в ~/.davfs2/secrets.
Код теста получился простым и лаконичным. Сперва.
def test_webdav_10MB_file_upload(self, perfmeter, test_name, target_dir_webdav, file_path_webdav, ssh):
file_data = get_file_with_size_mb(10)
md5sum_source = get_md5sum(file_data)
put_file_to_directory(file_data, file_path_webdav)
perfmeter.estimate_func(test_name, put_file_to_directory, file_data, file_path_webdav)
file_name = file_path_webdav.name
target_file = target_dir_webdav / file_name
md5sum_target = ssh.md5sum(target_file)
assert md5sum_source == md5sum_target
Берём файл, кладём его в примонтированную директорию. Как только операция на стороне тестового клиента выполнена - проверяем корректность копирования на стороне NAS. Но, для меня внезапно, тест падает. Файла на том конце не найдено. Отключил удаление временных файлов после теста и убедился, что дело во времени копирования. В отличии от NFS, процесс записи файла в директорию завершается гораздо раньше сохранения этого файла на диск на стороне NAS. Интересно, а где именно этот файл “задерживается”? На клиенте, или на сервере? А ещё возникает неудобный вопрос - а что именно мы проверяем подобными тестами?
С неудобными вопросами позже, а пока вкарячим костыль, позволяющий точнее определять момент появления файла на NAS.
Костылестроение
Суть проблемы - мы должны убедиться, что файл сохранился на NAS, прежде чем останавливать секундомер. А значит в estimate_func будем проверять наличие файла на NAS через ssh подключение. Да, это, во-первых, добавляет заметный оверхед, а во-вторых портит концептуальность метода estimate_func. Но на то он и костыль.
Проверку существования файла сделал так:
def is_file_exists(self, path):
cmd = f"if [ -f {path} ]; then echo 'True'; fi"
out, err = self.exec(cmd)
return len(out) > 0 and out[0].strip() == "True"
Мы просто в цикле проверяем наличие файла, пока он не появится, или пока не прервётся по таймауту:
def wait_for_file(ssh_client, file_path, timeout=150):
logging.debug(f"Wait for file {file_path} with timeout {timeout} seconds.")
time_start = time.time()
while not ssh_client.is_file_exists(file_path):
if time.time() - time_start > timeout:
return False
time.sleep(0.05)
logging.debug(f"File {file_path} is appears in {time.time() - time_start} seconds.")
return True
При встраивании костыля пришлось произвести небольшой рефакторинг остальных тестов. Но в результате мы получили более честное определение момента, когда файл действительно появился на NAS. Хотя до идеала тут ещё далеко.
Выполнение тестов
Ну и пример тестового прогона:
(venv) vkovalev@VK-UBER-LAPTOP:~/dev/regular_tests$ NAS_TEST_CONFIG=x86nas_conf.ini pytest src/test_simple.py --log-cli-level=INFO --count=50 -k ftp
======================================== test session starts =========================================
platform linux -- Python 3.10.6, pytest-7.3.1, pluggy-1.2.0
rootdir: /home/vkovalev/dev/regular_tests
plugins: repeat-0.9.1
collected 400 items / 300 deselected / 100 selected
src/test_simple.py::TestFTP::test_ftp_10MB_file_upload[1-50]
------------------------------------------- live log setup -------------------------------------------
INFO root:ssh_client.py:15 ssh test@192.168.10.20 with password test
INFO paramiko.transport:transport.py:1873 Connected (version 2.0, client OpenSSH_8.4p1)
INFO paramiko.transport:transport.py:1873 Authentication (publickey) failed.
INFO paramiko.transport:transport.py:1873 Authentication (password) successful!
INFO paramiko.transport.sftp:sftp.py:169 [chan 0] Opened sftp connection (server version 3)
INFO root:conftest.py:58 FTP Welcome message:220 ProFTPD Server (Debian) [::ffff:192.168.10.20]
------------------------------------------- live log call --------------------------------------------
INFO root:test_simple.py:74 Current path: /home/test
INFO root:test_simple.py:75 List before operation: [('.', {'modify': '20230724121427', 'perm': 'flcdmpe', 'type': 'cdir', 'unique': '10302U20008', 'unix.group': '0', 'unix.groupname': 'users', 'unix.mode': '0755', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('.bash_history', {'modify': '20230724142057', 'perm': 'adfrw', 'size': '1056', 'type': 'file', 'unique': '10302U20009', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0600', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('findmevk', {'modify': '20230724120730', 'perm': 'flcdmpe', 'type': 'dir', 'unique': '10302U2000A', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0755', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('test_ftp_file_500MB.bin', {'modify': '20230724121441', 'perm': 'adfrw', 'size': '524288000', 'type': 'file', 'unique': '10302U2000C', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0644', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('..', {'modify': '20230721085135', 'perm': 'fle', 'type': 'pdir', 'unique': '10302U20001', 'unix.group': '0', 'unix.groupname': 'users', 'unix.mode': '0755', 'unix.owner': '0', 'unix.ownername': 'test'}), ('test_ftp_file_10MB.bin', {'modify': '20230724121206', 'perm': 'adfrw', 'size': '10485760', 'type': 'file', 'unique': '10302U2000B', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0644', 'unix.owner': '1001', 'unix.ownername': 'test'})]
INFO root:utils.py:89 Test test_ftp_10MB_file_upload - 7.1594085693359375
INFO root:test_simple.py:77 List after operation: [('.', {'modify': '20230724121427', 'perm': 'flcdmpe', 'type': 'cdir', 'unique': '10302U20008', 'unix.group': '0', 'unix.groupname': 'users', 'unix.mode': '0755', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('.bash_history', {'modify': '20230724142057', 'perm': 'adfrw', 'size': '1056', 'type': 'file', 'unique': '10302U20009', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0600', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('findmevk', {'modify': '20230724120730', 'perm': 'flcdmpe', 'type': 'dir', 'unique': '10302U2000A', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0755', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('test_ftp_file_500MB.bin', {'modify': '20230724121441', 'perm': 'adfrw', 'size': '524288000', 'type': 'file', 'unique': '10302U2000C', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0644', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('..', {'modify': '20230721085135', 'perm': 'fle', 'type': 'pdir', 'unique': '10302U20001', 'unix.group': '0', 'unix.groupname': 'users', 'unix.mode': '0755', 'unix.owner': '0', 'unix.ownername': 'test'}), ('test_ftp_file_10MB.bin', {'modify': '20230727080632', 'perm': 'adfrw', 'size': '10485760', 'type': 'file', 'unique': '10302U2000B', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0644', 'unix.owner': '1001', 'unix.ownername': 'test'})]
PASSED [ 1%]
src/test_simple.py::TestFTP::test_ftp_10MB_file_upload[2-50]
...
src/test_simple.py::TestFTP::test_ftp_500MB_file_upload[50-50]
------------------------------------------- live log call --------------------------------------------
INFO root:test_simple.py:96 Current path: /home/test
INFO root:test_simple.py:97 List before operation: [('.', {'modify': '20230724121427', 'perm': 'flcdmpe', 'type': 'cdir', 'unique': '10302U20008', 'unix.group': '0', 'unix.groupname': 'users', 'unix.mode': '0755', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('.bash_history', {'modify': '20230724142057', 'perm': 'adfrw', 'size': '1056', 'type': 'file', 'unique': '10302U20009', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0600', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('findmevk', {'modify': '20230724120730', 'perm': 'flcdmpe', 'type': 'dir', 'unique': '10302U2000A', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0755', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('test_ftp_file_500MB.bin', {'modify': '20230727091558', 'perm': 'adfrw', 'size': '524288000', 'type': 'file', 'unique': '10302U2000C', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0644', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('..', {'modify': '20230721085135', 'perm': 'fle', 'type': 'pdir', 'unique': '10302U20001', 'unix.group': '0', 'unix.groupname': 'users', 'unix.mode': '0755', 'unix.owner': '0', 'unix.ownername': 'test'}), ('test_ftp_file_10MB.bin', {'modify': '20230727080940', 'perm': 'adfrw', 'size': '10485760', 'type': 'file', 'unique': '10302U2000B', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0644', 'unix.owner': '1001', 'unix.ownername': 'test'})]
INFO root:utils.py:89 Test test_ftp_500MB_file_upload - 82.15386939048767
INFO root:test_simple.py:99 List after operation: [('.', {'modify': '20230724121427', 'perm': 'flcdmpe', 'type': 'cdir', 'unique': '10302U20008', 'unix.group': '0', 'unix.groupname': 'users', 'unix.mode': '0755', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('.bash_history', {'modify': '20230724142057', 'perm': 'adfrw', 'size': '1056', 'type': 'file', 'unique': '10302U20009', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0600', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('findmevk', {'modify': '20230724120730', 'perm': 'flcdmpe', 'type': 'dir', 'unique': '10302U2000A', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0755', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('test_ftp_file_500MB.bin', {'modify': '20230727091719', 'perm': 'adfrw', 'size': '524288000', 'type': 'file', 'unique': '10302U2000C', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0644', 'unix.owner': '1001', 'unix.ownername': 'test'}), ('..', {'modify': '20230721085135', 'perm': 'fle', 'type': 'pdir', 'unique': '10302U20001', 'unix.group': '0', 'unix.groupname': 'users', 'unix.mode': '0755', 'unix.owner': '0', 'unix.ownername': 'test'}), ('test_ftp_file_10MB.bin', {'modify': '20230727080940', 'perm': 'adfrw', 'size': '10485760', 'type': 'file', 'unique': '10302U2000B', 'unix.group': '100', 'unix.groupname': 'users', 'unix.mode': '0644', 'unix.owner': '1001', 'unix.ownername': 'test'})]
PASSED [100%]
----------------------------------------- live log teardown ------------------------------------------
INFO paramiko.transport.sftp:sftp.py:169 [chan 0] sftp session closed.
Проверяем, что статистика в базу сохраняется:
(venv) vkovalev@VK-UBER-LAPTOP:~/dev/regular_tests/test_logs$ sqlite3 perf_stat.db
SQLite version 3.37.2 2022-01-06 13:25:41
Enter ".help" for usage hints.
sqlite> SELECT * FROM OperationTime;
test_ftp_10MB_file_upload|1690408346.56921|1690408348.23682|0
test_ftp_10MB_file_upload|1690408348.45588|1690408350.45754|0
test_ftp_10MB_file_upload|1690408350.6777|1690408352.87904|0
...
test_ftp_500MB_file_upload|1690412608.31712|1690412712.00417|0
test_ftp_500MB_file_upload|1690412718.02048|1690412804.01584|0
test_ftp_500MB_file_upload|1690412809.94036|1690412892.09423|0
Отлично, значит подход рабочий, и на этой базе можно продолжать дальнейшую разработку.
Зачем это написано?
Под конец хотелось бы объяснить, зачем подобный текст вообще написан и что он делает на Хабре. Это не новость, не туториал и не энциклопедическая заметка, не аккуратная статья со схемами, графиками и выверенной структурой. Дело в том, что проект NAS мы делаем по фану и в свободное от основной работы время. Как следствие, ресурсов на разработку не много, а энтузиазм и внутренняя дисциплина не всесильны. Ответственность перед аудиторией Хабра подталкивает что-то делать, перепроверять свои идеи, избегать халтуры, проводить ревью результатов работы, да и просто глубже вникать в суть.
По этим причинам мы попробуем вести на Хабре дневник разработки. Может кто-то почерпнёт тут идеи для своих проектов, а мы в свою очередь сумеем раньше прислушаться к мнению аудитории и улучшим свои решения.
Например здесь мне особенно интересно, как можно улучшить работу в консоле тестируемого устройство? Есть ли более практичные решения, чем парсинг вывода консоли линукс по ssh? Буду крайне признателен за советы!
В следующих статьях опишем наши тестовые стенды и схемы прототипов, подробнее разберёмся с корректностью наших тестов, продемонстрируем результаты измерений.