-
Notifications
You must be signed in to change notification settings - Fork 23
Pytest tests and CI #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
53a3358
0093076
d23c946
3b23acc
8d31993
86dea61
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
| *~ | ||
| /*.o | ||
| /microcom | ||
| *.pyc | ||
|
|
||
| # autotools stuff | ||
| /.deps | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ static struct termios sots; /* old stdout/in termios settings to restore */ | |
|
|
||
| struct ios_ops *ios; | ||
| int debug; | ||
| int quiet; | ||
|
|
||
| void init_terminal(void) | ||
| { | ||
|
|
@@ -98,6 +99,7 @@ void main_usage(int exitcode, char *str, char *dev) | |
| " default: (%s:%x:%x)\n" | ||
| " -f, --force ignore existing lock file\n" | ||
| " -d, --debug output debugging info\n" | ||
| " -q, --quiet do not print status information to stdout\n" | ||
| " -l, --logfile=<logfile> log output to <logfile>\n" | ||
| " -o, --listenonly Do not modify local terminal, do not send input\n" | ||
| " from stdin\n" | ||
|
|
@@ -136,6 +138,7 @@ int main(int argc, char *argv[]) | |
| { "telnet", required_argument, NULL, 't'}, | ||
| { "can", required_argument, NULL, 'c'}, | ||
| { "debug", no_argument, NULL, 'd' }, | ||
| { "quiet", no_argument, NULL, 'q' }, | ||
| { "force", no_argument, NULL, 'f' }, | ||
| { "logfile", required_argument, NULL, 'l'}, | ||
| { "listenonly", no_argument, NULL, 'o'}, | ||
|
|
@@ -144,54 +147,57 @@ int main(int argc, char *argv[]) | |
| { }, | ||
| }; | ||
|
|
||
| while ((opt = getopt_long(argc, argv, "hp:s:t:c:dfl:oi:a:e:v", long_options, NULL)) != -1) { | ||
| while ((opt = getopt_long(argc, argv, "hp:s:t:c:dqfl:oi:a:e:v", long_options, NULL)) != -1) { | ||
| switch (opt) { | ||
| case '?': | ||
| main_usage(1, "", ""); | ||
| break; | ||
| case 'h': | ||
| main_usage(0, "", ""); | ||
| break; | ||
| case 'v': | ||
| printf("%s\n", PACKAGE_VERSION); | ||
| exit(EXIT_SUCCESS); | ||
| break; | ||
| case 'p': | ||
| device = optarg; | ||
| break; | ||
| case 's': | ||
| current_speed = strtoul(optarg, NULL, 0); | ||
| break; | ||
| case 't': | ||
| telnet = 1; | ||
| hostport = optarg; | ||
| break; | ||
| case 'c': | ||
| can = 1; | ||
| interfaceid = optarg; | ||
| break; | ||
| case 'f': | ||
| opt_force = 1; | ||
| break; | ||
| case 'd': | ||
| debug = 1; | ||
| break; | ||
| case 'l': | ||
| logfile = optarg; | ||
| break; | ||
| case 'o': | ||
| listenonly = 1; | ||
| break; | ||
| case 'a': | ||
| answerback = optarg; | ||
| break; | ||
| case 'e': | ||
| if (strlen(optarg) != 1) { | ||
| fprintf(stderr, "Option -e requires a single character argument.\n"); | ||
| exit(EXIT_FAILURE); | ||
| } | ||
| escape_char = *optarg; | ||
| break; | ||
| case '?': | ||
| main_usage(1, "", ""); | ||
| break; | ||
| case 'h': | ||
| main_usage(0, "", ""); | ||
| break; | ||
| case 'v': | ||
| printf("%s\n", PACKAGE_VERSION); | ||
| exit(EXIT_SUCCESS); | ||
| break; | ||
| case 'p': | ||
| device = optarg; | ||
| break; | ||
| case 's': | ||
| current_speed = strtoul(optarg, NULL, 0); | ||
| break; | ||
| case 't': | ||
| telnet = 1; | ||
| hostport = optarg; | ||
| break; | ||
| case 'c': | ||
| can = 1; | ||
| interfaceid = optarg; | ||
| break; | ||
| case 'f': | ||
| opt_force = 1; | ||
| break; | ||
| case 'd': | ||
| debug = 1; | ||
| break; | ||
| case 'q': | ||
| quiet = 1; | ||
| break; | ||
| case 'l': | ||
| logfile = optarg; | ||
| break; | ||
| case 'o': | ||
| listenonly = 1; | ||
| break; | ||
| case 'a': | ||
| answerback = optarg; | ||
| break; | ||
| case 'e': | ||
| if (strlen(optarg) != 1) { | ||
| fprintf(stderr, "Option -e requires a single character argument.\n"); | ||
| exit(EXIT_FAILURE); | ||
| } | ||
| escape_char = *optarg; | ||
| break; | ||
|
Comment on lines
+152
to
+200
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This mixes a formatting change with a code change. It would be great to only have the + case 'q':
+ quiet = 1;
+ break;In this commit.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I originally based this on all the three dependent branches and only haphazardly rebased to main to avoid making too large a pull request. |
||
| } | ||
| } | ||
|
|
||
|
|
@@ -239,8 +245,8 @@ int main(int argc, char *argv[]) | |
| ios->set_flow(ios, current_flow); | ||
|
|
||
| if (!listenonly) { | ||
| printf("Escape character: Ctrl-%c\n", escape_char); | ||
| printf("Type the escape character to get to the prompt.\n"); | ||
| msg_printf("Escape character: Ctrl-%c\n", escape_char); | ||
| msg_printf("Type the escape character to get to the prompt.\n"); | ||
|
|
||
| /* Now deal with the local terminal side */ | ||
| tcgetattr(STDIN_FILENO, &sots); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| import os | ||
| from pathlib import Path | ||
| import shlex | ||
| import pytest | ||
|
|
||
|
|
||
| def pytest_sessionstart(session): | ||
| # run tests in toplevel directory always, not in test/ | ||
| os.chdir(Path(__file__).resolve().parent.parent) | ||
|
|
||
|
|
||
| def pytest_addoption(parser): | ||
| parser.addoption( | ||
| "--cmd", | ||
| action="store", | ||
| default="./microcom", | ||
| help="Command used to invoke microcom" | ||
| ) | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def cmd(pytestconfig): | ||
| cmd = pytestconfig.getoption("--cmd") | ||
| return shlex.split(cmd) |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,101 @@ | ||||||||||||||
| import socket | ||||||||||||||
| import threading | ||||||||||||||
| import time | ||||||||||||||
| import pytest | ||||||||||||||
| import subprocess | ||||||||||||||
| import os | ||||||||||||||
| import pty | ||||||||||||||
|
|
||||||||||||||
| RFC2217_CMD = bytes([255, 254, 44]) # RFC2217 IAC DONT COM-PORT-OPTION | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def make_pattern(length): | ||||||||||||||
| ascii_range = list(range(32, 127)) | ||||||||||||||
| return bytes(ascii_range[i % len(ascii_range)] for i in range(length)) | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| @pytest.fixture | ||||||||||||||
| def telnet_recv(cmd): | ||||||||||||||
| def _recv(buf, timeout=1): | ||||||||||||||
|
Comment on lines
+17
to
+19
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Define helper functions directly; there is no need to use fixtures for that.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason that this is a fixture and not a helper function is that it directly depends on the As there's little reason why a test that uses def test_a(telnet_recv):
assert telnet_recv("abc") == "abc"Over this: def test_a(cmd):
assert telnet_recv(cmd, "abc") == "abc"The difference is negligible in this case but adding more dependent fixtures to telnet_recv in the latter solution would be suboptimal, because they'd all need to be added to each of the tests even if the test code itself isn't touched
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using a fixture for helper functions such as these hides that |
||||||||||||||
| sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||||||||||||
| sock.bind(("127.0.0.1", 0)) | ||||||||||||||
| sock.listen(1) | ||||||||||||||
| host, port = sock.getsockname() | ||||||||||||||
|
|
||||||||||||||
| def run(): | ||||||||||||||
| conn, _ = sock.accept() | ||||||||||||||
| with conn: | ||||||||||||||
| conn.sendall(buf) | ||||||||||||||
| time.sleep(0.01) # sadly without this, microcom may miss the transmission | ||||||||||||||
| sock.close() | ||||||||||||||
|
Comment on lines
+28
to
+30
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this hardcoded sleep result in flaky tests in the future?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, definitely. I'm not sure why this is even needed. I'd expect that expect blocks until microcom has connected via TCP and that from this point whatever the server sends should definitely reach microcom. I suspect this is actually a workaround for a microcom bug. But yes bad bad hack! |
||||||||||||||
|
|
||||||||||||||
| thread = threading.Thread(target=run, daemon=True) | ||||||||||||||
| thread.start() | ||||||||||||||
|
|
||||||||||||||
| telnet_cmd = cmd + [f"--telnet={host}:{port}", "--quiet"] | ||||||||||||||
| master_fd, slave_fd = pty.openpty() | ||||||||||||||
| proc = subprocess.Popen( | ||||||||||||||
| telnet_cmd, | ||||||||||||||
| stdin=slave_fd, | ||||||||||||||
| stdout=slave_fd, | ||||||||||||||
| stderr=os.dup(2), | ||||||||||||||
| close_fds=True, | ||||||||||||||
| ) | ||||||||||||||
| os.close(slave_fd) | ||||||||||||||
| output = bytearray() | ||||||||||||||
| start_time = time.time() | ||||||||||||||
| while (time.time() - start_time) < timeout: | ||||||||||||||
| try: | ||||||||||||||
| chunk = os.read(master_fd, 1024) | ||||||||||||||
| if not chunk: | ||||||||||||||
| break | ||||||||||||||
| output.extend(chunk) | ||||||||||||||
| except OSError: | ||||||||||||||
| break | ||||||||||||||
| os.close(master_fd) | ||||||||||||||
| proc.wait() | ||||||||||||||
| assert proc.returncode in (0, 1), f"Exit code must be 0 or 1, got {proc.returncode}" | ||||||||||||||
|
|
||||||||||||||
| return bytes(output) | ||||||||||||||
| return _recv | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| @pytest.mark.parametrize("buf", [10, 1023, 1024, 1025, 4000]) | ||||||||||||||
| def test_no_cmd(telnet_recv, buf): | ||||||||||||||
| payload = make_pattern(buf) | ||||||||||||||
|
Comment on lines
+63
to
+65
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||
|
|
||||||||||||||
| assert telnet_recv(payload) == payload | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def test_cmd_across_buffers(telnet_recv): | ||||||||||||||
| before_pattern = make_pattern(1023) | ||||||||||||||
| after_pattern = make_pattern(20) | ||||||||||||||
| payload = before_pattern + RFC2217_CMD + after_pattern | ||||||||||||||
| expected_output = before_pattern + after_pattern | ||||||||||||||
|
|
||||||||||||||
| assert telnet_recv(payload) == expected_output | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def test_cmd_buffer_end(telnet_recv): | ||||||||||||||
| pattern = make_pattern(1023) | ||||||||||||||
| payload = pattern + RFC2217_CMD | ||||||||||||||
|
|
||||||||||||||
| assert telnet_recv(payload) == pattern | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def test_cmd_within_buffer(telnet_recv): | ||||||||||||||
| before_pattern = make_pattern(345) | ||||||||||||||
| after_pattern = make_pattern(890) | ||||||||||||||
| payload = before_pattern + RFC2217_CMD + after_pattern | ||||||||||||||
| expected_output = before_pattern + after_pattern | ||||||||||||||
|
|
||||||||||||||
| assert telnet_recv(payload) == expected_output | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| def test_iac_escape(telnet_recv): | ||||||||||||||
| before_pattern = make_pattern(42) | ||||||||||||||
| after_pattern = make_pattern(42) | ||||||||||||||
| payload = before_pattern + bytes([255, 255]) + after_pattern | ||||||||||||||
| expected_output = before_pattern + bytes([255]) + after_pattern | ||||||||||||||
|
|
||||||||||||||
| assert telnet_recv(payload) == expected_output | ||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#55 explicitly zeros
debug. Not zeroingquietnow aligns it with the code as-is, but since #55 should be merged first I think this should be:int quiet = 0;There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes it's an artifact from my attempt to split up what was originally a single series.
quietis zero-initialized on the "single series" branch to which I can update this pr once the dependent PRs are merged.I suppose unitialized globals are more a bad style practice than actual reliance on unitialized memory, at least on linux where they're zero-initialized as part of the bss (in contrast to locals which are stack-allocated and not automatically initialized).
Anyway, yes this will be taken care of :)