From 33e3ee3b0c80e0897816ee0ad4e787d0249e6fe4 Mon Sep 17 00:00:00 2001 From: Derek Maggio Date: Tue, 20 Jul 2021 11:35:11 -0700 Subject: [PATCH] feat(api): G-Code Diffing (#8135) * refactor(api): Refactor supported text modes * supported_text_modes: Provide lookup function inside of Enum so text mode can be looked up by "nice" value * Add tests for it * refactor(api): Refactor supported_text_modes * Won't be useful because response is needed. But if the response was added it is a single line difference from default and seems kinda useless * Add InvalidTextModeError * Refactor(api): Expand test coverage to 100% * Refactor(api): Fix linting * Refactor(api): Remove enum value * feat(api): G-Code Parsing * feat(api): Adding pipfile * feat(api): Move package to dev-packages --- api/Pipfile | 1 + api/Pipfile.lock | 8 + .../g_code_parsing/g_code_differ.py | 53 ++++ .../g_code_parsing/test_g_code_differ.py | 275 ++++++++++++++++++ 4 files changed, 337 insertions(+) create mode 100644 api/src/opentrons/hardware_control/g_code_parsing/g_code_differ.py create mode 100644 api/tests/opentrons/hardware_control/g_code_parsing/test_g_code_differ.py diff --git a/api/Pipfile b/api/Pipfile index 7ab528d46b2..be639aea967 100755 --- a/api/Pipfile +++ b/api/Pipfile @@ -32,6 +32,7 @@ flake8 = "~=3.9.0" flake8-annotations = "~=2.6.2" flake8-docstrings = "~=1.6.0" flake8-noqa = "~=1.1.0" +diff-match-patch = "*" decoy = "~=1.6.2" [packages] diff --git a/api/Pipfile.lock b/api/Pipfile.lock index e69aaa401a3..b33f93a696d 100644 --- a/api/Pipfile.lock +++ b/api/Pipfile.lock @@ -366,6 +366,14 @@ "index": "pypi", "version": "==1.6.2" }, + "diff-match-patch": { + "hashes": [ + "sha256:8bf9d9c4e059d917b5c6312bac0c137971a32815ddbda9c682b949f2986b4d34", + "sha256:da6f5a01aa586df23dfc89f3827e1cafbb5420be9d87769eeb079ddfd9477a18" + ], + "index": "pypi", + "version": "==20200713" + }, "dill": { "hashes": [ "sha256:97fd758f5fe742d42b11ec8318ecfcff8776bccacbfcec05dfd6276f5d450f73" diff --git a/api/src/opentrons/hardware_control/g_code_parsing/g_code_differ.py b/api/src/opentrons/hardware_control/g_code_parsing/g_code_differ.py new file mode 100644 index 00000000000..429f8740a6b --- /dev/null +++ b/api/src/opentrons/hardware_control/g_code_parsing/g_code_differ.py @@ -0,0 +1,53 @@ +from __future__ import annotations + +from typing import Tuple +from diff_match_patch import diff_match_patch as dmp # type: ignore +from opentrons.hardware_control.g_code_parsing.g_code_program.g_code_program import \ + GCodeProgram +from opentrons.hardware_control.g_code_parsing.g_code_program.supported_text_modes \ + import SupportedTextModes + + +class GCodeDiffer: + INSERTION_TYPE = 'Insertion' + EQUALITY_TYPE = 'Equality' + DELETION_TYPE = 'Deletion' + + DIFF_TYPE_LOOKUP = { + -1: DELETION_TYPE, + 0: EQUALITY_TYPE, + 1: INSERTION_TYPE + } + + @classmethod + def from_g_code_program( + cls, + program_1: GCodeProgram, + program_2: GCodeProgram + ) -> GCodeDiffer: + string_1 = program_1.get_text_explanation(SupportedTextModes.CONCISE.value) + string_2 = program_2.get_text_explanation(SupportedTextModes.CONCISE.value) + return cls(string_1, string_2) + + def __init__(self, string_1: str, string_2: str): + self._string_1 = string_1 + self._string_2 = string_2 + + def get_diff(self, timeout_secs=10): + differ = dmp() + differ.Diff_Timeout = timeout_secs + return differ.diff_main( + text1=self._string_1, + text2=self._string_2 + ) + + def get_html_diff(self): + return dmp().diff_prettyHtml(self.get_diff()) + + @classmethod + def get_diff_type(cls, diff_tuple: Tuple[int, str]): + return cls.DIFF_TYPE_LOOKUP[diff_tuple[0]] + + @classmethod + def get_diff_content(cls, diff_tuple: Tuple[int, str]): + return diff_tuple[1] diff --git a/api/tests/opentrons/hardware_control/g_code_parsing/test_g_code_differ.py b/api/tests/opentrons/hardware_control/g_code_parsing/test_g_code_differ.py new file mode 100644 index 00000000000..170fe4e0929 --- /dev/null +++ b/api/tests/opentrons/hardware_control/g_code_parsing/test_g_code_differ.py @@ -0,0 +1,275 @@ +import pytest +from textwrap import dedent +from opentrons.hardware_control.g_code_parsing.g_code_differ import GCodeDiffer +from opentrons.hardware_control.g_code_parsing.g_code import GCode +from opentrons.hardware_control.g_code_parsing.g_code_program.g_code_program import \ + GCodeProgram +from opentrons.hardware_control.g_code_parsing.g_code_program.supported_text_modes \ + import SupportedTextModes + +# Naive Strings +HELLO = 'Hello' +HELLO_WORLD = 'Hello World' + +# G-Codes Only +G_CODE_1 = dedent( + """ + G28.2 ABC + G0 F5.005 + """ +).strip() +G_CODE_2 = dedent( + """ + G28.2 ABC + M400 + G0 F5.005 + """ +).strip() + + +# Concise G-Code Text Explanations +@pytest.fixture +def first_g_code_explanation() -> str: + g_code_line_1 = GCode.from_raw_code( + 'G28.2 ABCXYZ', + GCode.SMOOTHIE_IDENT, + 'ok\r\nok\r\n' + )[0] + g_code_line_2 = GCode.from_raw_code( + 'G38.2 F420Y-40.0', + GCode.SMOOTHIE_IDENT, + 'G38.2 F420Y-40.0\r\n\r\n[PRB:296.825,292.663,218.000:1]\nok\r\nok\r\n' + )[0] + g_code_line_3 = GCode.from_raw_code( + 'M203.1 A125 B40 C40 X600 Y400 Z125\r\n\r\n', + GCode.SMOOTHIE_IDENT, + '' + )[0] + mode = SupportedTextModes.get_text_mode(SupportedTextModes.CONCISE.value) + return '\n'.join( + [ + mode.builder(line) + for line in [g_code_line_1, g_code_line_2, g_code_line_3] + ] + ) + + +@pytest.fixture +def second_g_code_explanation() -> str: + g_code_line_1 = GCode.from_raw_code( + 'G28.2 ABCXYZ', + GCode.SMOOTHIE_IDENT, + 'ok\r\nok\r\n' + )[0] + g_code_line_2 = GCode.from_raw_code( + 'M203.1 A125 B40 C40 X600 Y400 Z125\r\n\r\n', + GCode.SMOOTHIE_IDENT, + '' + )[0] + + mode = SupportedTextModes.get_text_mode(SupportedTextModes.CONCISE.value) + return '\n'.join( + [ + mode.builder(line) + for line in [g_code_line_1, g_code_line_2] + ] + ) + + +@pytest.fixture +def first_g_code_program() -> GCodeProgram: + raw_codes = [ + ['G28.2 ABCXYZ', 'ok\r\nok\r\n'], + ['G0 X113.38 Y11.24', 'ok\r\nok\r\n'], + ['G4 P555', 'ok\r\nok\r\n'], + [ + 'M114.2', + 'M114.2\r\n\r\nok MCS: A:218.0 B:0.0 C:0.0 X:418.0 Y:-3.0 Z:218.0' + ], + ] + + g_code_list = [ + GCode.from_raw_code(code, 'smoothie', response) + for code, response in raw_codes + ] + + return GCodeProgram([code[0] for code in g_code_list]) + + +@pytest.fixture +def second_g_code_program() -> GCodeProgram: + raw_codes = [ + ['G28.2 ABCXYZ', 'ok\r\nok\r\n'], + ['G0 X113.01 Y11.24', 'ok\r\nok\r\n'], + ['G4 P555', 'ok\r\nok\r\n'], + ] + + g_code_list = [ + GCode.from_raw_code(code, 'smoothie', response) + for code, response in raw_codes + ] + + return GCodeProgram([code[0] for code in g_code_list]) + + +def test_naive_insertion(): + diff = GCodeDiffer(HELLO, HELLO_WORLD).get_diff() + hello = diff[0] + world = diff[1] + assert GCodeDiffer.get_diff_type(hello) == GCodeDiffer.EQUALITY_TYPE + assert GCodeDiffer.get_diff_content(hello) == 'Hello' + assert GCodeDiffer.get_diff_type(world) == GCodeDiffer.INSERTION_TYPE + assert GCodeDiffer.get_diff_content(world) == ' World' + + +def test_naive_deletion(): + diff = GCodeDiffer(HELLO_WORLD, HELLO).get_diff() + hello = diff[0] + world = diff[1] + assert GCodeDiffer.get_diff_type(hello) == GCodeDiffer.EQUALITY_TYPE + assert GCodeDiffer.get_diff_content(hello) == 'Hello' + assert GCodeDiffer.get_diff_type(world) == GCodeDiffer.DELETION_TYPE + assert GCodeDiffer.get_diff_content(world) == ' World' + + +def test_g_code_insertion(): + diff = GCodeDiffer(G_CODE_1, G_CODE_2).get_diff() + line_1 = diff[0] + line_2 = diff[1] + line_3 = diff[2] + + assert GCodeDiffer.get_diff_type(line_1) == GCodeDiffer.EQUALITY_TYPE + assert GCodeDiffer.get_diff_content(line_1) == 'G28.2 ABC\n' + assert GCodeDiffer.get_diff_type(line_2) == GCodeDiffer.INSERTION_TYPE + assert GCodeDiffer.get_diff_content(line_2) == 'M400\n' + assert GCodeDiffer.get_diff_type(line_3) == GCodeDiffer.EQUALITY_TYPE + assert GCodeDiffer.get_diff_content(line_3) == 'G0 F5.005' + + +def test_g_code_deletion(): + diff = GCodeDiffer(G_CODE_2, G_CODE_1).get_diff() + line_1 = diff[0] + line_2 = diff[1] + line_3 = diff[2] + + assert GCodeDiffer.get_diff_type(line_1) == GCodeDiffer.EQUALITY_TYPE + assert GCodeDiffer.get_diff_content(line_1) == 'G28.2 ABC\n' + assert GCodeDiffer.get_diff_type(line_2) == GCodeDiffer.DELETION_TYPE + assert GCodeDiffer.get_diff_content(line_2) == 'M400\n' + assert GCodeDiffer.get_diff_type(line_3) == GCodeDiffer.EQUALITY_TYPE + assert GCodeDiffer.get_diff_content(line_3) == 'G0 F5.005' + + +def test_g_code_explanation_insertion( + first_g_code_explanation, + second_g_code_explanation +): + diff = GCodeDiffer(second_g_code_explanation, first_g_code_explanation).get_diff() + line_1 = diff[0] + line_2 = diff[1] + line_3 = diff[2] + + assert GCodeDiffer.get_diff_type(line_1) == GCodeDiffer.EQUALITY_TYPE + assert GCodeDiffer.get_diff_content(line_1) == \ + 'G28.2 A B C X Y Z -> Homing the following axes: X, Y, Z, A, B, C ->\n' + + assert GCodeDiffer.get_diff_type(line_2) == GCodeDiffer.INSERTION_TYPE + assert GCodeDiffer.get_diff_content(line_2) == \ + 'G38.2 F420.0 Y-40.0 -> Probing -40.0 on the Y axis, at a speed of 420.0 ' \ + '-> Probed to : X Axis: 296.825 Y Axis: 292.663 Z Axis: 218.000\n' + assert GCodeDiffer.get_diff_type(line_3) == GCodeDiffer.EQUALITY_TYPE + assert GCodeDiffer.get_diff_content(line_3) == \ + 'M203.1 A125.0 B40.0 C40.0 X600.0 Y400.0 Z125.0 -> Setting the max speed ' \ + 'for the following axes: X-Axis: 600.0 Y-Axis: 400.0 Z-Axis: 125.0 ' \ + 'A-Axis: 125.0 B-Axis: 40.0 C-Axis: 40.0 ->' + + +def test_g_code_explanation_deletion( + first_g_code_explanation, + second_g_code_explanation +): + diff = GCodeDiffer(first_g_code_explanation, second_g_code_explanation).get_diff() + line_1 = diff[0] + line_2 = diff[1] + line_3 = diff[2] + + assert GCodeDiffer.get_diff_type(line_1) == GCodeDiffer.EQUALITY_TYPE + assert GCodeDiffer.get_diff_content(line_1) == \ + 'G28.2 A B C X Y Z -> Homing the following axes: X, Y, Z, A, B, C ->\n' + + assert GCodeDiffer.get_diff_type(line_2) == GCodeDiffer.DELETION_TYPE + assert GCodeDiffer.get_diff_content(line_2) == \ + 'G38.2 F420.0 Y-40.0 -> Probing -40.0 on the Y axis, at a speed of 420.0 ' \ + '-> Probed to : X Axis: 296.825 Y Axis: 292.663 Z Axis: 218.000\n' + assert GCodeDiffer.get_diff_type(line_3) == GCodeDiffer.EQUALITY_TYPE + assert GCodeDiffer.get_diff_content(line_3) == \ + 'M203.1 A125.0 B40.0 C40.0 X600.0 Y400.0 Z125.0 -> Setting the max speed ' \ + 'for the following axes: X-Axis: 600.0 Y-Axis: 400.0 Z-Axis: 125.0 ' \ + 'A-Axis: 125.0 B-Axis: 40.0 C-Axis: 40.0 ->' + + +def test_g_code_program_diff(first_g_code_program, second_g_code_program): + diff = GCodeDiffer.from_g_code_program( + first_g_code_program, second_g_code_program + ).get_diff() + line_1 = diff[0] + remove_38 = diff[1] + add_01 = diff[2] + beginning_partial_response = diff[3] + remove_response_38 = diff[4] + add_response_01 = diff[5] + remaining_partial_response = diff[6] + remove_last_line = diff[7] + + assert GCodeDiffer.get_diff_type(line_1) == GCodeDiffer.EQUALITY_TYPE + assert GCodeDiffer.get_diff_content( + line_1 + ) == 'G28.2 A B C X Y Z -> Homing the following axes: X, Y, Z, A, B, C ->\nG0 X113.' + + assert GCodeDiffer.get_diff_type(remove_38) == GCodeDiffer.DELETION_TYPE + assert GCodeDiffer.get_diff_content(remove_38) == '38' + + assert GCodeDiffer.get_diff_type(add_01) == GCodeDiffer.INSERTION_TYPE + assert GCodeDiffer.get_diff_content(add_01) == '01' + + assert GCodeDiffer.get_diff_type(beginning_partial_response) == \ + GCodeDiffer.EQUALITY_TYPE + assert GCodeDiffer.get_diff_content( + beginning_partial_response + ) == ' Y11.24 -> Moving the robot as follows: The gantry to 113.' + + assert GCodeDiffer.get_diff_type(remove_response_38) == GCodeDiffer.DELETION_TYPE + assert GCodeDiffer.get_diff_content(remove_response_38) == '38' + + assert GCodeDiffer.get_diff_type(add_response_01) == GCodeDiffer.INSERTION_TYPE + assert GCodeDiffer.get_diff_content(add_response_01) == '01' + + assert GCodeDiffer.get_diff_type(remaining_partial_response) == \ + GCodeDiffer.EQUALITY_TYPE + assert GCodeDiffer.get_diff_content( + remaining_partial_response + ) == ' on the X-Axis The gantry to 11.24 on the Y-Axis ->\nG4 P555.0 -> Pausing ' \ + 'movement for 555.0ms ->' + + assert GCodeDiffer.get_diff_type(remove_last_line) == GCodeDiffer.DELETION_TYPE + assert GCodeDiffer.get_diff_content(remove_last_line) == \ + '\nM114.2 -> Getting current position for all axes -> The current ' \ + 'position of the robot is: A Axis: 218.0 B Axis: 0.0 C Axis: 0.0 ' \ + 'X Axis: 418.0 Y Axis: -3.0 Z Axis: 218.0' + + +def test_html_diff( + first_g_code_explanation, + second_g_code_explanation +): + expected_html = 'G28.2 A B C X Y Z -> Homing the following axes: ' \ + 'X, Y, Z, A, B, C ->¶
G38.2 F420.0 Y-40.0 -> Probing -40.0 on the Y ' \ + 'axis, at a speed of 420.0 -> Probed to : X Axis: 296.825 Y ' \ + 'Axis: 292.663 Z Axis: 218.000¶
M203.1 ' \ + 'A125.0 B40.0 C40.0 X600.0 Y400.0 Z125.0 -> Setting the max ' \ + 'speed for the following axes: X-Axis: 600.0 Y-Axis: 400.0 ' \ + 'Z-Axis: 125.0 A-Axis: 125.0 B-Axis: 40.0 C-Axis: 40.0 ->' + diff = GCodeDiffer(first_g_code_explanation, second_g_code_explanation) + html = diff.get_html_diff() + assert html == expected_html