From 64440922f032bcd2ba647041934f4228e8651f08 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sat, 22 Feb 2025 20:12:06 +0100 Subject: [PATCH 01/35] Add basic implementation for can bridge --- can/bridge.py | 47 +++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 1 + 2 files changed, 48 insertions(+) create mode 100644 can/bridge.py diff --git a/can/bridge.py b/can/bridge.py new file mode 100644 index 000000000..0a5921e0b --- /dev/null +++ b/can/bridge.py @@ -0,0 +1,47 @@ +""" +Creates a bridge between two CAN busses. + +This will connect to two CAN busses. Messages received on one +bus will be sent to the other bus and vice versa. +""" + +import argparse +import errno +import sys +from datetime import datetime + +from .logger import _create_base_argument_parser, _create_bus, _parse_additional_config + + +def main() -> None: + parser = argparse.ArgumentParser(description="Bridge two CAN busses.") + + _create_base_argument_parser(parser) + + parser.add_argument( + "--error-frames", + help="Also send error frames to the interface.", + action="store_true", + ) + + # print help message when no arguments were given + if len(sys.argv) < 2: + parser.print_help(sys.stderr) + raise SystemExit(errno.EINVAL) + + results, unknown_args = parser.parse_known_args() + additional_config = _parse_additional_config([*results.extra_args, *unknown_args]) + + verbosity = results.verbosity + + error_frames = results.error_frames + + with _create_bus(results, **additional_config) as bus_a + with _create_bus(results, **additional_config) as bus_b + print(f"CAN Bridge (Started on {datetime.now()})") + + print(f"CAN Bridge (Stopped on {datetime.now()})") + + +if __name__ == "__main__": + main() diff --git a/pyproject.toml b/pyproject.toml index a6a7f38c4..18217ac4b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ can_logconvert = "can.logconvert:main" can_logger = "can.logger:main" can_player = "can.player:main" can_viewer = "can.viewer:main" +can_bridge = "can.bridge:main" [project.urls] homepage = "https://github.com/hardbyte/python-can" From 83083315269f062da10ede819557793108f1496d Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sat, 22 Feb 2025 21:16:54 +0100 Subject: [PATCH 02/35] Implement basic configuration parsing --- can/bridge.py | 80 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 14 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index 0a5921e0b..3c6709876 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -8,36 +8,88 @@ import argparse import errno import sys +import logging from datetime import datetime from .logger import _create_base_argument_parser, _create_bus, _parse_additional_config -def main() -> None: - parser = argparse.ArgumentParser(description="Bridge two CAN busses.") +LOG = logging.getLogger(__name__) + + +def get_config_list(it, separator, conf): + while True: + el = next(it) + if el == separator: + break + else: + conf.append(el) + + +def split_configurations(arg_list, separator='--'): + general = [] + conf_a = [] + conf_b = [] + + it = iter(arg_list) + try: + get_config_list(it, separator, conf_a) + get_config_list(it, separator, conf_b) + + # When we reached this point we found two separators so we have + # a general config. We will treate the first config as general + general = conf_a + conf_a = conf_b + get_config_list(it, separator, conf_b) - _create_base_argument_parser(parser) + # When we reached this point we found three separators so this is + # an error. + raise Exception("To many configurations") + except StopIteration: + LOG.debug("All configurations were split") - parser.add_argument( - "--error-frames", - help="Also send error frames to the interface.", - action="store_true", + return general, conf_a, conf_b + + +def main() -> None: + general_parser = argparse.ArgumentParser() + general_parser.add_argument( + "-v", + action="count", + dest="verbosity", + help="""How much information do you want to see at the command line? + You can add several of these e.g., -vv is DEBUG""", + default=2, ) + bus_parser = argparse.ArgumentParser(description="Bridge two CAN busses.") + + _create_base_argument_parser(bus_parser) + + parser = argparse.ArgumentParser(description="Bridge two CAN busses.") + parser.add_argument('configs', nargs=argparse.REMAINDER) + # print help message when no arguments were given if len(sys.argv) < 2: - parser.print_help(sys.stderr) + bus_parser.print_help(sys.stderr) raise SystemExit(errno.EINVAL) - results, unknown_args = parser.parse_known_args() - additional_config = _parse_additional_config([*results.extra_args, *unknown_args]) + args = sys.argv[1:] + general, conf_a, conf_b = split_configurations(args) + + g_results = general_parser.parse_args(general) + verbosity = g_results.verbosity - verbosity = results.verbosity + a_results, a_unknown_args = bus_parser.parse_known_args(conf_a) + a_additional_config = _parse_additional_config([*a_results.extra_args, *a_unknown_args]) + a_results.__dict__['verbosity'] = verbosity - error_frames = results.error_frames + b_results, b_unknown_args = bus_parser.parse_known_args(conf_b) + b_additional_config = _parse_additional_config([*b_results.extra_args, *b_unknown_args]) + b_results.__dict__['verbosity'] = verbosity - with _create_bus(results, **additional_config) as bus_a - with _create_bus(results, **additional_config) as bus_b + with _create_bus(a_results, **a_additional_config) as bus_a: + with _create_bus(b_results, **b_additional_config) as bus_b: print(f"CAN Bridge (Started on {datetime.now()})") print(f"CAN Bridge (Stopped on {datetime.now()})") From c2b7413b36fb8913f4c038ee0c338b935c897ada Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sat, 22 Feb 2025 21:25:11 +0100 Subject: [PATCH 03/35] Add implementation for bridge --- can/bridge.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/can/bridge.py b/can/bridge.py index 3c6709876..cb957f0db 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -8,11 +8,14 @@ import argparse import errno import sys +import time import logging from datetime import datetime from .logger import _create_base_argument_parser, _create_bus, _parse_additional_config +import can + LOG = logging.getLogger(__name__) @@ -90,7 +93,13 @@ def main() -> None: with _create_bus(a_results, **a_additional_config) as bus_a: with _create_bus(b_results, **b_additional_config) as bus_b: + reader_a = can.RedirectReader(bus_b) + reader_b = can.RedirectReader(bus_a) + notifier_a = can.Notifier(bus_a, [reader_a]) + notifier_b = can.Notifier(bus_b, [reader_b]) print(f"CAN Bridge (Started on {datetime.now()})") + while True: + time.sleep(1) print(f"CAN Bridge (Stopped on {datetime.now()})") From 12f037d181c8b19933c8fd5deab503de6fdb4db4 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sat, 22 Feb 2025 21:26:15 +0100 Subject: [PATCH 04/35] Improve exit handling --- can/bridge.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index cb957f0db..057813bef 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -98,8 +98,11 @@ def main() -> None: notifier_a = can.Notifier(bus_a, [reader_a]) notifier_b = can.Notifier(bus_b, [reader_b]) print(f"CAN Bridge (Started on {datetime.now()})") - while True: - time.sleep(1) + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + pass print(f"CAN Bridge (Stopped on {datetime.now()})") From 21ac3a0dae946fd35cb0e37db78a9c613897cda1 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sat, 22 Feb 2025 21:35:01 +0100 Subject: [PATCH 05/35] Add debug logging --- can/bridge.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/can/bridge.py b/can/bridge.py index 057813bef..24f4f0e20 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -80,6 +80,9 @@ def main() -> None: args = sys.argv[1:] general, conf_a, conf_b = split_configurations(args) + LOG.debug("General configuration: %s", general) + LOG.debug("Bus A configuration: %s", conf_a) + LOG.debug("Bus B configuration: %s", conf_b) g_results = general_parser.parse_args(general) verbosity = g_results.verbosity @@ -91,6 +94,11 @@ def main() -> None: b_additional_config = _parse_additional_config([*b_results.extra_args, *b_unknown_args]) b_results.__dict__['verbosity'] = verbosity + LOG.debug("General configuration results: %s", g_results) + LOG.debug("Bus A configuration results: %s", a_results) + LOG.debug("Bus A additional configuration results: %s", a_additional_config) + LOG.debug("Bus B configuration results: %s", b_results) + LOG.debug("Bus B additional configuration results: %s", b_additional_config) with _create_bus(a_results, **a_additional_config) as bus_a: with _create_bus(b_results, **b_additional_config) as bus_b: reader_a = can.RedirectReader(bus_b) From 59e64f6f3555a0fed2842009270f2f87f7e23209 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sat, 22 Feb 2025 21:50:12 +0100 Subject: [PATCH 06/35] Add error handling for wrong arguments --- can/bridge.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index 24f4f0e20..3d17faa2e 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -20,6 +20,10 @@ LOG = logging.getLogger(__name__) +class UserError(Exception): + pass + + def get_config_list(it, separator, conf): while True: el = next(it) @@ -34,9 +38,11 @@ def split_configurations(arg_list, separator='--'): conf_a = [] conf_b = [] + found_sep = False it = iter(arg_list) try: get_config_list(it, separator, conf_a) + found_sep = True get_config_list(it, separator, conf_b) # When we reached this point we found two separators so we have @@ -47,9 +53,11 @@ def split_configurations(arg_list, separator='--'): # When we reached this point we found three separators so this is # an error. - raise Exception("To many configurations") + raise UserError("To many configurations") except StopIteration: LOG.debug("All configurations were split") + if not found_sep: + raise UserError("Missing separator") return general, conf_a, conf_b @@ -78,7 +86,13 @@ def main() -> None: raise SystemExit(errno.EINVAL) args = sys.argv[1:] - general, conf_a, conf_b = split_configurations(args) + try: + general, conf_a, conf_b = split_configurations(args) + except UserError as exc: + print("Error while processing arguments:") + print(exc) + print("") + raise SystemExit(errno.EINVAL) LOG.debug("General configuration: %s", general) LOG.debug("Bus A configuration: %s", conf_a) From 07b6e37dd6c53380af4c1f658ab2915fd5e2c8f3 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sat, 22 Feb 2025 21:54:07 +0100 Subject: [PATCH 07/35] Use stderr --- can/bridge.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index 3d17faa2e..10ee9bd88 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -89,9 +89,8 @@ def main() -> None: try: general, conf_a, conf_b = split_configurations(args) except UserError as exc: - print("Error while processing arguments:") - print(exc) - print("") + print(f"Error while processing arguments: {exc}", + file=sys.stderr) raise SystemExit(errno.EINVAL) LOG.debug("General configuration: %s", general) From 3dce956cae3596ff6b5c78a7058788dc41767481 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sat, 22 Feb 2025 22:03:35 +0100 Subject: [PATCH 08/35] Add custom usage --- can/bridge.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/can/bridge.py b/can/bridge.py index 10ee9bd88..e40c3f0b3 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -17,6 +17,23 @@ import can +USAGE = """ +usage: can_bridge [{general config} --] {can A config} -- {can B config} + +Bridge two CAN busses. + +Both can busses will be connected so that messages from bus A will be sent on bus B and messages on bus B will be sent to bus A. The busses are separated by a `--` + +positional arguments: + {general config} The configuration for this program excluding the config for each bus. Can be omitted + {can A config} The configuration for the first bus + {can B config} The configuration for the second bus + +Example usage: + can_bridge -i socketcan -c can0 -- -i socketcan can1 + can_bridge -vvv -- -i socketcan -c can0 -- -i socketcan can1 +""" + LOG = logging.getLogger(__name__) @@ -82,7 +99,7 @@ def main() -> None: # print help message when no arguments were given if len(sys.argv) < 2: - bus_parser.print_help(sys.stderr) + print(USAGE, file=sys.stderr) raise SystemExit(errno.EINVAL) args = sys.argv[1:] From 6016ae7c3a6e3ec83b514bbc2fca393be4ff68e4 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sat, 22 Feb 2025 22:07:36 +0100 Subject: [PATCH 09/35] Add bus configuration info --- can/bridge.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index e40c3f0b3..8e0ee40e1 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -32,6 +32,8 @@ Example usage: can_bridge -i socketcan -c can0 -- -i socketcan can1 can_bridge -vvv -- -i socketcan -c can0 -- -i socketcan can1 + +Type `can_bridge help_bus` for information about single bus configuration. """ LOG = logging.getLogger(__name__) @@ -106,8 +108,11 @@ def main() -> None: try: general, conf_a, conf_b = split_configurations(args) except UserError as exc: - print(f"Error while processing arguments: {exc}", - file=sys.stderr) + if len(args) == 1 and args[0] == 'help_bus': + bus_parser.print_help(sys.stderr) + else: + print(f"Error while processing arguments: {exc}", + file=sys.stderr) raise SystemExit(errno.EINVAL) LOG.debug("General configuration: %s", general) From b0e75826b9c7071c27a1597cc92c6c5f11892722 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 10:54:47 +0100 Subject: [PATCH 10/35] Code format --- can/bridge.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index 8e0ee40e1..8a6b270c8 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -52,7 +52,7 @@ def get_config_list(it, separator, conf): conf.append(el) -def split_configurations(arg_list, separator='--'): +def split_configurations(arg_list, separator="--"): general = [] conf_a = [] conf_b = [] @@ -97,7 +97,7 @@ def main() -> None: _create_base_argument_parser(bus_parser) parser = argparse.ArgumentParser(description="Bridge two CAN busses.") - parser.add_argument('configs', nargs=argparse.REMAINDER) + parser.add_argument("configs", nargs=argparse.REMAINDER) # print help message when no arguments were given if len(sys.argv) < 2: @@ -108,11 +108,10 @@ def main() -> None: try: general, conf_a, conf_b = split_configurations(args) except UserError as exc: - if len(args) == 1 and args[0] == 'help_bus': + if len(args) == 1 and args[0] == "help_bus": bus_parser.print_help(sys.stderr) else: - print(f"Error while processing arguments: {exc}", - file=sys.stderr) + print(f"Error while processing arguments: {exc}", file=sys.stderr) raise SystemExit(errno.EINVAL) LOG.debug("General configuration: %s", general) @@ -122,12 +121,16 @@ def main() -> None: verbosity = g_results.verbosity a_results, a_unknown_args = bus_parser.parse_known_args(conf_a) - a_additional_config = _parse_additional_config([*a_results.extra_args, *a_unknown_args]) - a_results.__dict__['verbosity'] = verbosity + a_additional_config = _parse_additional_config( + [*a_results.extra_args, *a_unknown_args] + ) + a_results.__dict__["verbosity"] = verbosity b_results, b_unknown_args = bus_parser.parse_known_args(conf_b) - b_additional_config = _parse_additional_config([*b_results.extra_args, *b_unknown_args]) - b_results.__dict__['verbosity'] = verbosity + b_additional_config = _parse_additional_config( + [*b_results.extra_args, *b_unknown_args] + ) + b_results.__dict__["verbosity"] = verbosity LOG.debug("General configuration results: %s", g_results) LOG.debug("Bus A configuration results: %s", a_results) From 78787d9e1daa0dfa5a236a4de7b0d7ed33a04b0f Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 11:03:07 +0100 Subject: [PATCH 11/35] Add exception for prints for can_bridge --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 18217ac4b..8fd77de21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -187,6 +187,7 @@ ignore = [ "can/cli.py" = ["T20"] # flake8-print "can/logger.py" = ["T20"] # flake8-print "can/player.py" = ["T20"] # flake8-print +"can/bridge.py" = ["T20"] # flake8-print "can/viewer.py" = ["T20"] # flake8-print "examples/*" = ["T20"] # flake8-print From d9391f7eb1c98f594425adf456108205f4215ca0 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 11:05:02 +0100 Subject: [PATCH 12/35] Add from to exception in exception --- can/bridge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index 8a6b270c8..65bd25664 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -76,7 +76,7 @@ def split_configurations(arg_list, separator="--"): except StopIteration: LOG.debug("All configurations were split") if not found_sep: - raise UserError("Missing separator") + raise UserError("Missing separator") from None return general, conf_a, conf_b @@ -112,7 +112,7 @@ def main() -> None: bus_parser.print_help(sys.stderr) else: print(f"Error while processing arguments: {exc}", file=sys.stderr) - raise SystemExit(errno.EINVAL) + raise SystemExit(errno.EINVAL) from exc LOG.debug("General configuration: %s", general) LOG.debug("Bus A configuration: %s", conf_a) From aca4798c52be1417902ed722d63d41f8f5aa3908 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 11:05:19 +0100 Subject: [PATCH 13/35] Remove assignment to unused variable --- can/bridge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index 65bd25664..992c96ee5 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -141,8 +141,8 @@ def main() -> None: with _create_bus(b_results, **b_additional_config) as bus_b: reader_a = can.RedirectReader(bus_b) reader_b = can.RedirectReader(bus_a) - notifier_a = can.Notifier(bus_a, [reader_a]) - notifier_b = can.Notifier(bus_b, [reader_b]) + can.Notifier(bus_a, [reader_a]) + can.Notifier(bus_b, [reader_b]) print(f"CAN Bridge (Started on {datetime.now()})") try: while True: From 46434925b960b6e53fa9d44a9b81fdd671838ba1 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 11:06:45 +0100 Subject: [PATCH 14/35] Shorten line length --- can/bridge.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index 992c96ee5..5a4a16d21 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -22,10 +22,12 @@ Bridge two CAN busses. -Both can busses will be connected so that messages from bus A will be sent on bus B and messages on bus B will be sent to bus A. The busses are separated by a `--` +Both can busses will be connected so that messages from bus A will be sent on +bus B and messages on bus B will be sent to bus A. The busses are separated by a `--` positional arguments: - {general config} The configuration for this program excluding the config for each bus. Can be omitted + {general config} The configuration for this program excluding + the config for each bus. Can be omitted {can A config} The configuration for the first bus {can B config} The configuration for the second bus From e117b38c51d857e9352af5ca1d31acf23677aaeb Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 11:10:33 +0100 Subject: [PATCH 15/35] Organize imports --- can/bridge.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index 5a4a16d21..e9f51c1d4 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -7,15 +7,14 @@ import argparse import errno +import logging import sys import time -import logging from datetime import datetime -from .logger import _create_base_argument_parser, _create_bus, _parse_additional_config - import can +from .logger import _create_base_argument_parser, _create_bus, _parse_additional_config USAGE = """ usage: can_bridge [{general config} --] {can A config} -- {can B config} From e40ee3370f9c4b7de07d6365f5700d1c6c9231fc Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 11:12:35 +0100 Subject: [PATCH 16/35] Remove unnecessary else --- can/bridge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index e9f51c1d4..6833af8af 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -49,8 +49,8 @@ def get_config_list(it, separator, conf): el = next(it) if el == separator: break - else: - conf.append(el) + + conf.append(el) def split_configurations(arg_list, separator="--"): From 166b8826cf8a22535e2b70140e56091908206620 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 11:18:58 +0100 Subject: [PATCH 17/35] Add documentation for new script --- doc/scripts.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/doc/scripts.rst b/doc/scripts.rst index 2d59b7528..281391162 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -57,6 +57,21 @@ The full usage page can be seen below: :shell: +can.bridge +---------- + +A small application that can be used to connect two can busses: + +.. command-output:: python -m can.bridge -h + :shell: + + +Example call: +:: + + python -m can.bridge -i socketcan -c can0 -- -i socketcan -c can1 + + can.logconvert -------------- From 0ef6ca237b61c48afabfb7283f2d4e7cf455ef54 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 11:25:08 +0100 Subject: [PATCH 18/35] Add handling for -h and help sub command Necessary for generation of documentation --- can/bridge.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index 6833af8af..a2cbfd4aa 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -109,8 +109,12 @@ def main() -> None: try: general, conf_a, conf_b = split_configurations(args) except UserError as exc: - if len(args) == 1 and args[0] == "help_bus": - bus_parser.print_help(sys.stderr) + if len(args) >= 1: + if args[0] == "-h" or args[0] == "help": + print(USAGE) + raise SystemExit() + elif args[0] == "help_bus": + bus_parser.print_help(sys.stderr) else: print(f"Error while processing arguments: {exc}", file=sys.stderr) raise SystemExit(errno.EINVAL) from exc From f1401ab4b0ad4961e11e66b0194accff6f2d0d5d Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 11:27:24 +0100 Subject: [PATCH 19/35] Add from none to exception --- can/bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/bridge.py b/can/bridge.py index a2cbfd4aa..130ed46c6 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -112,7 +112,7 @@ def main() -> None: if len(args) >= 1: if args[0] == "-h" or args[0] == "help": print(USAGE) - raise SystemExit() + raise SystemExit() from None elif args[0] == "help_bus": bus_parser.print_help(sys.stderr) else: From 7057f69635490a47d6a75ce08306362a74dde30e Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 18:34:02 +0100 Subject: [PATCH 20/35] Fix typo busses to bus --- can/bridge.py | 14 +++++++------- doc/scripts.rst | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index 130ed46c6..0b42d525f 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -1,7 +1,7 @@ """ -Creates a bridge between two CAN busses. +Creates a bridge between two CAN buses. -This will connect to two CAN busses. Messages received on one +This will connect to two CAN buses. Messages received on one bus will be sent to the other bus and vice versa. """ @@ -19,10 +19,10 @@ USAGE = """ usage: can_bridge [{general config} --] {can A config} -- {can B config} -Bridge two CAN busses. +Bridge two CAN buses. -Both can busses will be connected so that messages from bus A will be sent on -bus B and messages on bus B will be sent to bus A. The busses are separated by a `--` +Both can buses will be connected so that messages from bus A will be sent on +bus B and messages on bus B will be sent to bus A. The buses are separated by a `--` positional arguments: {general config} The configuration for this program excluding @@ -93,11 +93,11 @@ def main() -> None: default=2, ) - bus_parser = argparse.ArgumentParser(description="Bridge two CAN busses.") + bus_parser = argparse.ArgumentParser(description="Bridge two CAN buses.") _create_base_argument_parser(bus_parser) - parser = argparse.ArgumentParser(description="Bridge two CAN busses.") + parser = argparse.ArgumentParser(description="Bridge two CAN buses.") parser.add_argument("configs", nargs=argparse.REMAINDER) # print help message when no arguments were given diff --git a/doc/scripts.rst b/doc/scripts.rst index 281391162..47555bf87 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -60,7 +60,7 @@ The full usage page can be seen below: can.bridge ---------- -A small application that can be used to connect two can busses: +A small application that can be used to connect two can buses: .. command-output:: python -m can.bridge -h :shell: From 91ffbdbf738b79249802be3e89992ed47fc44017 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 18:48:55 +0100 Subject: [PATCH 21/35] Add type annotations --- can/bridge.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index 0b42d525f..12d04ca9d 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -11,6 +11,9 @@ import sys import time from datetime import datetime +from typing import ( + Iterator, +) import can @@ -44,7 +47,7 @@ class UserError(Exception): pass -def get_config_list(it, separator, conf): +def get_config_list(it: Iterator[str], separator: str, conf) -> None: while True: el = next(it) if el == separator: @@ -53,7 +56,9 @@ def get_config_list(it, separator, conf): conf.append(el) -def split_configurations(arg_list, separator="--"): +def split_configurations( + arg_list: list[str], separator: str = "--" +) -> (list, list, list): general = [] conf_a = [] conf_b = [] From e0b536835bcfe4a706ceb598b4729f539258052c Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 18:52:36 +0100 Subject: [PATCH 22/35] Fix type annotations --- can/bridge.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index 12d04ca9d..3300a50a7 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -13,6 +13,7 @@ from datetime import datetime from typing import ( Iterator, + List, ) import can @@ -47,7 +48,7 @@ class UserError(Exception): pass -def get_config_list(it: Iterator[str], separator: str, conf) -> None: +def get_config_list(it: Iterator[str], separator: str, conf: list) -> None: while True: el = next(it) if el == separator: @@ -57,7 +58,7 @@ def get_config_list(it: Iterator[str], separator: str, conf) -> None: def split_configurations( - arg_list: list[str], separator: str = "--" + arg_list: List[str], separator: str = "--" ) -> (list, list, list): general = [] conf_a = [] From deb06fdae40775049eb496b47becb605a45796c8 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 18:55:41 +0100 Subject: [PATCH 23/35] Fix type annotations again --- can/bridge.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index 3300a50a7..50da299a3 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -14,6 +14,7 @@ from typing import ( Iterator, List, + Tuple, ) import can @@ -59,10 +60,10 @@ def get_config_list(it: Iterator[str], separator: str, conf: list) -> None: def split_configurations( arg_list: List[str], separator: str = "--" -) -> (list, list, list): +) -> Tuple[list, list, list]: general = [] - conf_a = [] - conf_b = [] + conf_a: List[str] = [] + conf_b: List[str] = [] found_sep = False it = iter(arg_list) From c7222b1a930d1b9b1dd01c402fc57a58220476b4 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 19:47:03 +0100 Subject: [PATCH 24/35] Add --help to get help --- can/bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can/bridge.py b/can/bridge.py index 50da299a3..bbae0a3e1 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -117,7 +117,7 @@ def main() -> None: general, conf_a, conf_b = split_configurations(args) except UserError as exc: if len(args) >= 1: - if args[0] == "-h" or args[0] == "help": + if args[0] == "-h" or args[0] == "--help" or args[0] == "help": print(USAGE) raise SystemExit() from None elif args[0] == "help_bus": From 69c039a63ebd82ba3eb0f3b7a6fcb1c8852fcbdb Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 19:47:17 +0100 Subject: [PATCH 25/35] Add basic print help test --- test/test_scripts.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/test_scripts.py b/test/test_scripts.py index 9d8c059cf..c1a6c082d 100644 --- a/test/test_scripts.py +++ b/test/test_scripts.py @@ -98,6 +98,20 @@ def _import(self): return module +class TestBridgeScript(CanScriptTest): + def _commands(self): + commands = [ + "python -m can.bridge --help", + "can_bridge --help", + ] + return commands + + def _import(self): + import can.bridge as module + + return module + + class TestLogconvertScript(CanScriptTest): def _commands(self): commands = [ From 037eb23c38c5472034ce647b0d4ebd129f69da92 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 19:52:55 +0100 Subject: [PATCH 26/35] Add basic test file for bridge script --- test/test_bridge.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 test/test_bridge.py diff --git a/test/test_bridge.py b/test/test_bridge.py new file mode 100644 index 000000000..43fb96e06 --- /dev/null +++ b/test/test_bridge.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python + +""" +This module tests the functions inside of bridge.py +""" + +import unittest +from unittest import mock +from unittest.mock import Mock + +import can +import can.bridge + + +class TestBridgeScriptModule(unittest.TestCase): + def setUp(self) -> None: + # Patch VirtualBus object + patcher_virtual_bus = mock.patch("can.interfaces.virtual.VirtualBus", spec=True) + self.MockVirtualBus = patcher_virtual_bus.start() + self.addCleanup(patcher_virtual_bus.stop) + self.mock_virtual_bus = self.MockVirtualBus.return_value + self.mock_virtual_bus.shutdown = Mock() + + self.testmsg = can.Message( + arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True + ) + + def assert_successfull_cleanup(self): + self.MockVirtualBus.assert_called_once() + self.mock_virtual_bus.shutdown.assert_called_once() + + +if __name__ == "__main__": + unittest.main() From 991aa05a1f556f2ddb7c6b1f58bb1f189a17f92a Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 20:16:02 +0100 Subject: [PATCH 27/35] Add very basic test --- test/test_bridge.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/test/test_bridge.py b/test/test_bridge.py index 43fb96e06..bd857b071 100644 --- a/test/test_bridge.py +++ b/test/test_bridge.py @@ -4,6 +4,7 @@ This module tests the functions inside of bridge.py """ +import sys import unittest from unittest import mock from unittest.mock import Mock @@ -21,13 +22,26 @@ def setUp(self) -> None: self.mock_virtual_bus = self.MockVirtualBus.return_value self.mock_virtual_bus.shutdown = Mock() + # Patch time sleep object + patcher_sleep = mock.patch("can.io.player.time.sleep", spec=True) + self.MockSleep = patcher_sleep.start() + self.addCleanup(patcher_sleep.stop) + self.testmsg = can.Message( arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True ) + self.busargs = ["-i", "virtual"] + def assert_successfull_cleanup(self): - self.MockVirtualBus.assert_called_once() - self.mock_virtual_bus.shutdown.assert_called_once() + self.MockVirtualBus.assert_called() + + def test_bridge_no_config(self): + self.MockSleep.side_effect = KeyboardInterrupt + sys.argv = [sys.argv[0], *self.busargs, "--", *self.busargs] + can.bridge.main() + + self.assert_successfull_cleanup() if __name__ == "__main__": From d2227d7519df846e75f2178317614547e332617f Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 20:20:56 +0100 Subject: [PATCH 28/35] Add different channels for virtual bus --- test/test_bridge.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/test_bridge.py b/test/test_bridge.py index bd857b071..3189d18b7 100644 --- a/test/test_bridge.py +++ b/test/test_bridge.py @@ -38,7 +38,16 @@ def assert_successfull_cleanup(self): def test_bridge_no_config(self): self.MockSleep.side_effect = KeyboardInterrupt - sys.argv = [sys.argv[0], *self.busargs, "--", *self.busargs] + sys.argv = [ + sys.argv[0], + *self.busargs, + "-c", + "can_a", + "--", + *self.busargs, + "-c", + "can_b", + ] can.bridge.main() self.assert_successfull_cleanup() From 3974c512c6465de24be150f67a364c6dd10284ff Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 20:28:39 +0100 Subject: [PATCH 29/35] Add assert for call to exit --- test/test_bridge.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_bridge.py b/test/test_bridge.py index 3189d18b7..b17030aff 100644 --- a/test/test_bridge.py +++ b/test/test_bridge.py @@ -20,7 +20,7 @@ def setUp(self) -> None: self.MockVirtualBus = patcher_virtual_bus.start() self.addCleanup(patcher_virtual_bus.stop) self.mock_virtual_bus = self.MockVirtualBus.return_value - self.mock_virtual_bus.shutdown = Mock() + self.mock_virtual_bus.__enter__ = Mock(return_value=self.mock_virtual_bus) # Patch time sleep object patcher_sleep = mock.patch("can.io.player.time.sleep", spec=True) @@ -35,6 +35,7 @@ def setUp(self) -> None: def assert_successfull_cleanup(self): self.MockVirtualBus.assert_called() + self.assertEqual(2, len(self.mock_virtual_bus.__exit__.mock_calls)) def test_bridge_no_config(self): self.MockSleep.side_effect = KeyboardInterrupt From 8f6ae52082b6d835210bd9c742722c3a8747f735 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Sun, 23 Feb 2025 20:33:11 +0100 Subject: [PATCH 30/35] Patch correct function --- test/test_bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_bridge.py b/test/test_bridge.py index b17030aff..c57b5768b 100644 --- a/test/test_bridge.py +++ b/test/test_bridge.py @@ -23,7 +23,7 @@ def setUp(self) -> None: self.mock_virtual_bus.__enter__ = Mock(return_value=self.mock_virtual_bus) # Patch time sleep object - patcher_sleep = mock.patch("can.io.player.time.sleep", spec=True) + patcher_sleep = mock.patch("can.bridge.time.sleep", spec=True) self.MockSleep = patcher_sleep.start() self.addCleanup(patcher_sleep.stop) From e86814f3b4f6935b14d2781b0370c3c37a2428f9 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Mon, 24 Feb 2025 21:20:16 +0100 Subject: [PATCH 31/35] test --- test/test_bridge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_bridge.py b/test/test_bridge.py index c57b5768b..973f98e93 100644 --- a/test/test_bridge.py +++ b/test/test_bridge.py @@ -49,9 +49,9 @@ def test_bridge_no_config(self): "-c", "can_b", ] - can.bridge.main() + #can.bridge.main() - self.assert_successfull_cleanup() + #self.assert_successfull_cleanup() if __name__ == "__main__": From 6dd6519d12c802421ac20361563665372810f001 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Mon, 24 Feb 2025 21:23:59 +0100 Subject: [PATCH 32/35] fjkdf --- test/test_bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_bridge.py b/test/test_bridge.py index 973f98e93..df5a10a94 100644 --- a/test/test_bridge.py +++ b/test/test_bridge.py @@ -49,7 +49,7 @@ def test_bridge_no_config(self): "-c", "can_b", ] - #can.bridge.main() + can.bridge.main() #self.assert_successfull_cleanup() From c3a69065f1d5db66549395ed89884de29a3dbfa6 Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Mon, 24 Feb 2025 21:27:42 +0100 Subject: [PATCH 33/35] once again -.- --- test/test_bridge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_bridge.py b/test/test_bridge.py index df5a10a94..c57b5768b 100644 --- a/test/test_bridge.py +++ b/test/test_bridge.py @@ -51,7 +51,7 @@ def test_bridge_no_config(self): ] can.bridge.main() - #self.assert_successfull_cleanup() + self.assert_successfull_cleanup() if __name__ == "__main__": From 9f0950ed446c2a4b4652a9118b5ac2123a853c4d Mon Sep 17 00:00:00 2001 From: Peter Kessen Date: Mon, 24 Feb 2025 21:32:40 +0100 Subject: [PATCH 34/35] Try snakecase --- test/test_bridge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/test_bridge.py b/test/test_bridge.py index c57b5768b..11e645ede 100644 --- a/test/test_bridge.py +++ b/test/test_bridge.py @@ -33,7 +33,7 @@ def setUp(self) -> None: self.busargs = ["-i", "virtual"] - def assert_successfull_cleanup(self): + def assertSuccessfullCleanup(self): self.MockVirtualBus.assert_called() self.assertEqual(2, len(self.mock_virtual_bus.__exit__.mock_calls)) @@ -51,7 +51,7 @@ def test_bridge_no_config(self): ] can.bridge.main() - self.assert_successfull_cleanup() + self.assertSuccessfullCleanup() if __name__ == "__main__": From 4e063c3877e5a24524babd01e92683a68ceb27ec Mon Sep 17 00:00:00 2001 From: zariiii9003 <52598363+zariiii9003@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:03:58 +0200 Subject: [PATCH 35/35] use new api to create cli args for bus1 and bus2 --- can/bridge.py | 161 ++++++++------------------------------------ doc/scripts.rst | 6 -- test/test_bridge.py | 138 +++++++++++++++++++++++++++---------- 3 files changed, 132 insertions(+), 173 deletions(-) diff --git a/can/bridge.py b/can/bridge.py index bbae0a3e1..57ebb368d 100644 --- a/can/bridge.py +++ b/can/bridge.py @@ -7,154 +7,51 @@ import argparse import errno -import logging import sys import time from datetime import datetime -from typing import ( - Iterator, - List, - Tuple, -) +from typing import Final -import can - -from .logger import _create_base_argument_parser, _create_bus, _parse_additional_config - -USAGE = """ -usage: can_bridge [{general config} --] {can A config} -- {can B config} +from can.cli import add_bus_arguments, create_bus_from_namespace +from can.listener import RedirectReader +from can.notifier import Notifier +BRIDGE_DESCRIPTION: Final = """\ Bridge two CAN buses. -Both can buses will be connected so that messages from bus A will be sent on -bus B and messages on bus B will be sent to bus A. The buses are separated by a `--` - -positional arguments: - {general config} The configuration for this program excluding - the config for each bus. Can be omitted - {can A config} The configuration for the first bus - {can B config} The configuration for the second bus - -Example usage: - can_bridge -i socketcan -c can0 -- -i socketcan can1 - can_bridge -vvv -- -i socketcan -c can0 -- -i socketcan can1 - -Type `can_bridge help_bus` for information about single bus configuration. +Both can buses will be connected so that messages from bus1 will be sent on +bus2 and messages from bus2 will be sent to bus1. """ - -LOG = logging.getLogger(__name__) - - -class UserError(Exception): - pass - - -def get_config_list(it: Iterator[str], separator: str, conf: list) -> None: - while True: - el = next(it) - if el == separator: - break - - conf.append(el) +BUS_1_PREFIX: Final = "bus1" +BUS_2_PREFIX: Final = "bus2" -def split_configurations( - arg_list: List[str], separator: str = "--" -) -> Tuple[list, list, list]: - general = [] - conf_a: List[str] = [] - conf_b: List[str] = [] +def _parse_bridge_args(args: list[str]) -> argparse.Namespace: + """Parse command line arguments for bridge script.""" - found_sep = False - it = iter(arg_list) - try: - get_config_list(it, separator, conf_a) - found_sep = True - get_config_list(it, separator, conf_b) + parser = argparse.ArgumentParser(description=BRIDGE_DESCRIPTION) + add_bus_arguments(parser, prefix=BUS_1_PREFIX, group_title="Bus 1 arguments") + add_bus_arguments(parser, prefix=BUS_2_PREFIX, group_title="Bus 2 arguments") - # When we reached this point we found two separators so we have - # a general config. We will treate the first config as general - general = conf_a - conf_a = conf_b - get_config_list(it, separator, conf_b) - - # When we reached this point we found three separators so this is - # an error. - raise UserError("To many configurations") - except StopIteration: - LOG.debug("All configurations were split") - if not found_sep: - raise UserError("Missing separator") from None + # print help message when no arguments were given + if not args: + parser.print_help(sys.stderr) + raise SystemExit(errno.EINVAL) - return general, conf_a, conf_b + results, _unknown_args = parser.parse_known_args(args) + return results def main() -> None: - general_parser = argparse.ArgumentParser() - general_parser.add_argument( - "-v", - action="count", - dest="verbosity", - help="""How much information do you want to see at the command line? - You can add several of these e.g., -vv is DEBUG""", - default=2, - ) - - bus_parser = argparse.ArgumentParser(description="Bridge two CAN buses.") - - _create_base_argument_parser(bus_parser) - - parser = argparse.ArgumentParser(description="Bridge two CAN buses.") - parser.add_argument("configs", nargs=argparse.REMAINDER) - - # print help message when no arguments were given - if len(sys.argv) < 2: - print(USAGE, file=sys.stderr) - raise SystemExit(errno.EINVAL) - - args = sys.argv[1:] - try: - general, conf_a, conf_b = split_configurations(args) - except UserError as exc: - if len(args) >= 1: - if args[0] == "-h" or args[0] == "--help" or args[0] == "help": - print(USAGE) - raise SystemExit() from None - elif args[0] == "help_bus": - bus_parser.print_help(sys.stderr) - else: - print(f"Error while processing arguments: {exc}", file=sys.stderr) - raise SystemExit(errno.EINVAL) from exc - - LOG.debug("General configuration: %s", general) - LOG.debug("Bus A configuration: %s", conf_a) - LOG.debug("Bus B configuration: %s", conf_b) - g_results = general_parser.parse_args(general) - verbosity = g_results.verbosity - - a_results, a_unknown_args = bus_parser.parse_known_args(conf_a) - a_additional_config = _parse_additional_config( - [*a_results.extra_args, *a_unknown_args] - ) - a_results.__dict__["verbosity"] = verbosity - - b_results, b_unknown_args = bus_parser.parse_known_args(conf_b) - b_additional_config = _parse_additional_config( - [*b_results.extra_args, *b_unknown_args] - ) - b_results.__dict__["verbosity"] = verbosity - - LOG.debug("General configuration results: %s", g_results) - LOG.debug("Bus A configuration results: %s", a_results) - LOG.debug("Bus A additional configuration results: %s", a_additional_config) - LOG.debug("Bus B configuration results: %s", b_results) - LOG.debug("Bus B additional configuration results: %s", b_additional_config) - with _create_bus(a_results, **a_additional_config) as bus_a: - with _create_bus(b_results, **b_additional_config) as bus_b: - reader_a = can.RedirectReader(bus_b) - reader_b = can.RedirectReader(bus_a) - can.Notifier(bus_a, [reader_a]) - can.Notifier(bus_b, [reader_b]) + results = _parse_bridge_args(sys.argv[1:]) + + with ( + create_bus_from_namespace(results, prefix=BUS_1_PREFIX) as bus1, + create_bus_from_namespace(results, prefix=BUS_2_PREFIX) as bus2, + ): + reader1_to_2 = RedirectReader(bus2) + reader2_to_1 = RedirectReader(bus1) + with Notifier(bus1, [reader1_to_2]), Notifier(bus2, [reader2_to_1]): print(f"CAN Bridge (Started on {datetime.now()})") try: while True: diff --git a/doc/scripts.rst b/doc/scripts.rst index 47555bf87..1d730a74b 100644 --- a/doc/scripts.rst +++ b/doc/scripts.rst @@ -66,12 +66,6 @@ A small application that can be used to connect two can buses: :shell: -Example call: -:: - - python -m can.bridge -i socketcan -c can0 -- -i socketcan -c can1 - - can.logconvert -------------- diff --git a/test/test_bridge.py b/test/test_bridge.py index 11e645ede..ee41bd949 100644 --- a/test/test_bridge.py +++ b/test/test_bridge.py @@ -4,54 +4,122 @@ This module tests the functions inside of bridge.py """ +import random +import string import sys -import unittest -from unittest import mock -from unittest.mock import Mock +import threading +import time +from time import sleep as real_sleep +import unittest.mock import can import can.bridge +from can.interfaces import virtual +from .message_helper import ComparingMessagesTestCase + + +class TestBridgeScriptModule(unittest.TestCase, ComparingMessagesTestCase): + + TIMEOUT = 3.0 + + def __init__(self, *args, **kwargs): + unittest.TestCase.__init__(self, *args, **kwargs) + ComparingMessagesTestCase.__init__( + self, + allowed_timestamp_delta=None, + preserves_channel=False, + ) -class TestBridgeScriptModule(unittest.TestCase): def setUp(self) -> None: - # Patch VirtualBus object - patcher_virtual_bus = mock.patch("can.interfaces.virtual.VirtualBus", spec=True) - self.MockVirtualBus = patcher_virtual_bus.start() - self.addCleanup(patcher_virtual_bus.stop) - self.mock_virtual_bus = self.MockVirtualBus.return_value - self.mock_virtual_bus.__enter__ = Mock(return_value=self.mock_virtual_bus) - - # Patch time sleep object - patcher_sleep = mock.patch("can.bridge.time.sleep", spec=True) - self.MockSleep = patcher_sleep.start() - self.addCleanup(patcher_sleep.stop) + self.stop_event = threading.Event() + + self.channel1 = "".join(random.choices(string.ascii_letters, k=8)) + self.channel2 = "".join(random.choices(string.ascii_letters, k=8)) + + self.cli_args = [ + "--bus1-interface", + "virtual", + "--bus1-channel", + self.channel1, + "--bus2-interface", + "virtual", + "--bus2-channel", + self.channel2, + ] self.testmsg = can.Message( arbitration_id=0xC0FFEE, data=[0, 25, 0, 1, 3, 1, 4, 1], is_extended_id=True ) - self.busargs = ["-i", "virtual"] - - def assertSuccessfullCleanup(self): - self.MockVirtualBus.assert_called() - self.assertEqual(2, len(self.mock_virtual_bus.__exit__.mock_calls)) - - def test_bridge_no_config(self): - self.MockSleep.side_effect = KeyboardInterrupt - sys.argv = [ - sys.argv[0], - *self.busargs, - "-c", - "can_a", - "--", - *self.busargs, - "-c", - "can_b", - ] - can.bridge.main() + def fake_sleep(self, duration): + """A fake replacement for time.sleep that checks periodically + whether self.stop_event is set, and raises KeyboardInterrupt + if so. + + This allows tests to simulate an interrupt (like Ctrl+C) + during long sleeps, in a controlled and responsive way. + """ + interval = 0.05 # Small interval for responsiveness + t_wakeup = time.perf_counter() + duration + while time.perf_counter() < t_wakeup: + if self.stop_event.is_set(): + raise KeyboardInterrupt("Simulated interrupt from fake_sleep") + real_sleep(interval) + + def test_bridge(self): + with ( + unittest.mock.patch("can.bridge.time.sleep", new=self.fake_sleep), + unittest.mock.patch("can.bridge.sys.argv", [sys.argv[0], *self.cli_args]), + ): + # start script + thread = threading.Thread(target=can.bridge.main) + thread.start() + + # wait until script instantiates virtual buses + t0 = time.perf_counter() + while True: + with virtual.channels_lock: + if ( + self.channel1 in virtual.channels + and self.channel2 in virtual.channels + ): + break + if time.perf_counter() > t0 + 2.0: + raise TimeoutError("Bridge script did not create virtual buses") + real_sleep(0.2) + + # create buses with the same channels as in scripts + with ( + can.interfaces.virtual.VirtualBus(self.channel1) as bus1, + can.interfaces.virtual.VirtualBus(self.channel2) as bus2, + ): + # send test message to bus1, it should be received on bus2 + bus1.send(self.testmsg) + recv_msg = bus2.recv(self.TIMEOUT) + self.assertMessageEqual(self.testmsg, recv_msg) + + # assert that both buses are empty + self.assertIsNone(bus1.recv(0)) + self.assertIsNone(bus2.recv(0)) + + # send test message to bus2, it should be received on bus1 + bus2.send(self.testmsg) + recv_msg = bus1.recv(self.TIMEOUT) + self.assertMessageEqual(self.testmsg, recv_msg) + + # assert that both buses are empty + self.assertIsNone(bus1.recv(0)) + self.assertIsNone(bus2.recv(0)) + + # stop the bridge script + self.stop_event.set() + thread.join() - self.assertSuccessfullCleanup() + # assert that the virtual buses were closed + with virtual.channels_lock: + self.assertNotIn(self.channel1, virtual.channels) + self.assertNotIn(self.channel2, virtual.channels) if __name__ == "__main__":