From dc90aab4459d4f111f62f6abd5f32966bebded14 Mon Sep 17 00:00:00 2001 From: Nikolay Khabarov <2xl@mail.ru> Date: Sat, 13 May 2017 21:02:27 +0300 Subject: [PATCH] unit tests --- cnc/coordinates.py | 1 + cnc/gcode.py | 4 +- cnc/gmachine.py | 18 ++- cnc/hal_virtual.py | 1 + runtests.sh | 13 ++ tests/__init__.py | 0 tests/{rpgpio-test.sh => rpgpio_test.sh} | 0 tests/test_coordinates.py | 123 +++++++++++++++++ tests/test_gcode.py | 114 ++++++++++++++++ tests/test_gmachine.py | 127 ++++++++++++++++++ .../{test-parser.gcode => test_parser.gcode} | 0 tests/test_pulses.py | 117 ++++++++++++++++ 12 files changed, 512 insertions(+), 6 deletions(-) create mode 100755 runtests.sh create mode 100644 tests/__init__.py rename tests/{rpgpio-test.sh => rpgpio_test.sh} (100%) create mode 100644 tests/test_coordinates.py create mode 100644 tests/test_gcode.py create mode 100644 tests/test_gmachine.py rename tests/{test-parser.gcode => test_parser.gcode} (100%) create mode 100644 tests/test_pulses.py diff --git a/cnc/coordinates.py b/cnc/coordinates.py index d325af9..e662df6 100644 --- a/cnc/coordinates.py +++ b/cnc/coordinates.py @@ -1,3 +1,4 @@ +from __future__ import division import math diff --git a/cnc/gcode.py b/cnc/gcode.py index 6f9e897..83d37db 100644 --- a/cnc/gcode.py +++ b/cnc/gcode.py @@ -33,7 +33,7 @@ class GCode(object): return default return float(self.params[argname]) * multiply - def getXYZ(self, default, multiply): + def coordinates(self, default, multiply): """ Get X, Y and Z values as Coord object. :param default: Default values, if any of coords is not specified. :param multiply: If value exist, multiply it by this value. @@ -44,7 +44,7 @@ class GCode(object): z = self.get('Z', default.z, multiply) return Coordinates(x, y, z) - def isXYZ(self): + def has_coordinates(self): """ Check if at least one of the coordinates is present. :return: Boolean value. """ diff --git a/cnc/gmachine.py b/cnc/gmachine.py index 23563e5..084032d 100644 --- a/cnc/gmachine.py +++ b/cnc/gmachine.py @@ -1,3 +1,4 @@ +from __future__ import division import time import logging @@ -69,6 +70,15 @@ class GMachine(object): d = Coordinates(-self._position.x, -self._position.y, 0) self._move(d, STEPPER_MAX_VELOCITY_MM_PER_MIN) + def position(self): + """ Return current machine position (after the latest command) + Note that hal might still be moving motors and in this case + function will block until motors stops. + This function for tests only. + """ + hal.join() + return self._position + def do_command(self, gcode): """ Perform action. :param gcode: GCode object which represent one gcode line @@ -78,15 +88,15 @@ class GMachine(object): logging.debug("got command " + str(gcode.params)) # read command c = gcode.command() - if c is None and gcode.isXYZ(): + if c is None and gcode.has_coordinates(): c = 'G1' # read parameters if self._absoluteCoordinates: - coord = gcode.getXYZ(self._position, self._convertCoordinates) + coord = gcode.coordinates(self._position, self._convertCoordinates) coord = coord + self._local delta = coord - self._position else: - delta = gcode.getXYZ(Coordinates(0.0, 0.0, 0.0), self._convertCoordinates) + delta = gcode.coordinates(Coordinates(0.0, 0.0, 0.0), self._convertCoordinates) coord = self._position + delta velocity = gcode.get('F', self._velocity) spindle_rpm = gcode.get('S', self._spindle_rpm) @@ -118,7 +128,7 @@ class GMachine(object): self._absoluteCoordinates = False elif c == 'G92': # switch to local coords self._local = self._position - \ - gcode.getXYZ(Coordinates(0.0, 0.0, 0.0), self._convertCoordinates) + gcode.coordinates(Coordinates(0.0, 0.0, 0.0), self._convertCoordinates) elif c == 'M3': # spinle on self._spindle(spindle_rpm) elif c == 'M5': # spindle off diff --git a/cnc/hal_virtual.py b/cnc/hal_virtual.py index 0aa6b62..2140825 100644 --- a/cnc/hal_virtual.py +++ b/cnc/hal_virtual.py @@ -1,3 +1,4 @@ +from __future__ import division import logging import time diff --git a/runtests.sh b/runtests.sh new file mode 100755 index 0000000..f33db86 --- /dev/null +++ b/runtests.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -e + +echo '**********************************************************************' +echo '* Testing PyCNC modules. *' +echo '* Hint: pass -v to this script arguments to see more verbose output. *' +echo '* Note: HAL tests should be run manually on corresponding board. For *' +echo '* example Raspberry Pi tests is tests/rpgpio_test.sh which should be *' +echo '* run with RPi board with connected to pin GPIO21 LED. LED should *' +echo '* light up on pullup, set, and DMA test events. *' +echo '**********************************************************************' +python -m unittest discover "$@" --pattern="test_*.py" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/rpgpio-test.sh b/tests/rpgpio_test.sh similarity index 100% rename from tests/rpgpio-test.sh rename to tests/rpgpio_test.sh diff --git a/tests/test_coordinates.py b/tests/test_coordinates.py new file mode 100644 index 0000000..8220310 --- /dev/null +++ b/tests/test_coordinates.py @@ -0,0 +1,123 @@ +import unittest + +from cnc.coordinates import * + + +class TestCoordinates(unittest.TestCase): + def setUp(self): + self.default = Coordinates(96, 102, 150) + + def tearDown(self): + pass + + def test_constructor(self): + # constructor rounds values to 10 digits after the point + self.assertRaises(TypeError, Coordinates) + c = Coordinates(1.00000000005, 2.00000000004, -3.5000000009) + self.assertEquals(c.x, 1.0000000001) + self.assertEquals(c.y, 2.0) + self.assertEquals(c.z, -3.5000000009) + + def test_zero(self): + c = Coordinates(0, 0, 0) + self.assertTrue(c.is_zero()) + + def test_aabb(self): + # aabb - Axis Aligned Bounded Box. + # original method checks if point belongs aabb. + p1 = Coordinates(0, 0, 0) + p2 = Coordinates(2, 2, 2) + c = Coordinates(1, 1, 1) + self.assertTrue(c.is_in_aabb(p1, p2)) + self.assertTrue(c.is_in_aabb(p2, p1)) + c = Coordinates(0, 0, 0) + self.assertTrue(c.is_in_aabb(p1, p2)) + c = Coordinates(2, 2, 2) + self.assertTrue(c.is_in_aabb(p1, p2)) + c = Coordinates(2, 3, 2) + self.assertFalse(c.is_in_aabb(p1, p2)) + c = Coordinates(-1, 1, 1) + self.assertFalse(c.is_in_aabb(p1, p2)) + c = Coordinates(1, 1, 3) + self.assertFalse(c.is_in_aabb(p1, p2)) + + def test_length(self): + c = Coordinates(-1, 0, 0) + self.assertEquals(c.length(), 1) + c = Coordinates(0, 3, -4) + self.assertEquals(c.length(), 5) + c = Coordinates(3, 4, 12) + self.assertEquals(c.length(), 13) + + def test_round(self): + # round works in another way then Python's round. + # This round() rounds digits with specified step. + c = Coordinates(1.5, -1.4, 3.05) + r = c.round(1) + self.assertEquals(r.x, 2.0) + self.assertEquals(r.y, -1.0) + self.assertEquals(r.z, 3.0) + r = c.round(0.25) + self.assertEquals(r.x, 1.5) + self.assertEquals(r.y, -1.5) + self.assertEquals(r.z, 3.0) + + def test_max(self): + self.assertEquals(self.default.find_max(), max(self.default.x, + self.default.y, + self.default.z)) + + # build-in function overriding tests + def test_add(self): + r = self.default + Coordinates(1, 2, 3) + self.assertEquals(r.x, self.default.x + 1) + self.assertEquals(r.y, self.default.y + 2) + self.assertEquals(r.z, self.default.z + 3) + + def test_sub(self): + r = self.default - Coordinates(1, 2, 3) + self.assertEquals(r.x, self.default.x - 1) + self.assertEquals(r.y, self.default.y - 2) + self.assertEquals(r.z, self.default.z - 3) + + def test_mul(self): + r = self.default * 2 + self.assertEquals(r.x, self.default.x * 2) + self.assertEquals(r.y, self.default.y * 2) + self.assertEquals(r.z, self.default.z * 2) + + def test_div(self): + r = self.default / 2 + self.assertEquals(r.x, self.default.x / 2) + self.assertEquals(r.y, self.default.y / 2) + self.assertEquals(r.z, self.default.z / 2) + + def test_truediv(self): + r = self.default / 3.0 + self.assertEquals(r.x, self.default.x / 3.0) + self.assertEquals(r.y, self.default.y / 3.0) + self.assertEquals(r.z, self.default.z / 3.0) + + def test_eq(self): + a = Coordinates(self.default.x, self.default.y, self.default.z) + self.assertTrue(a == self.default) + a = Coordinates(-self.default.x, self.default.y, self.default.z) + self.assertFalse(a == self.default) + a = Coordinates(self.default.x, -self.default.y, self.default.z) + self.assertFalse(a == self.default) + a = Coordinates(self.default.x, self.default.y, -self.default.z) + self.assertFalse(a == self.default) + + def test_str(self): + self.assertTrue(isinstance(str(self.default), str)) + + def test_abs(self): + c = Coordinates(-1, -2.5, -99) + r = abs(c) + self.assertEquals(r.x, 1.0) + self.assertEquals(r.y, 2.5) + self.assertEquals(r.z, 99.0) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_gcode.py b/tests/test_gcode.py new file mode 100644 index 0000000..b78c65b --- /dev/null +++ b/tests/test_gcode.py @@ -0,0 +1,114 @@ +import unittest + +from cnc.coordinates import * +from cnc.gcode import * + + +class TestGCode(unittest.TestCase): + def setUp(self): + self.default = Coordinates(-7, 8, 9) + + def tearDown(self): + pass + + def test_constructor(self): + # GCode shouldn't be created with constructor, but since it uses + # internally, let's check it. + self.assertRaises(TypeError, GCode) + gc = GCode({"X": "1", "Y": "-2", "Z":"0", "G": "1"}) + self.assertEquals(gc.coordinates(self.default, 1).x, 1.0) + self.assertEquals(gc.coordinates(self.default, 1).y, -2.0) + self.assertEquals(gc.coordinates(self.default, 1).z, 0.0) + + def test_parser(self): + gc = GCode.parse_line("G1X2Y-3Z4") + self.assertEquals(gc.command(), "G1") + self.assertEquals(gc.coordinates(self.default, 1).x, 2.0) + self.assertEquals(gc.coordinates(self.default, 1).y, -3.0) + self.assertEquals(gc.coordinates(self.default, 1).z, 4.0) + gc = GCode.parse_line("") + self.assertIsNone(gc) + + def test_defaults(self): + # defaults are values which should be returned if corresponding + # value doesn't exist in gcode. + default = Coordinates(11, -12, 14) + gc = GCode.parse_line("G1") + self.assertEquals(gc.coordinates(default, 1).x, 11.0) + self.assertEquals(gc.coordinates(default, 1).y, -12.0) + self.assertEquals(gc.coordinates(default, 1).z, 14.0) + + def test_commands(self): + gc = GCode({"G": "1"}) + self.assertEquals(gc.command(), "G1") + gc = GCode.parse_line("M99") + self.assertEquals(gc.command(), "M99") + + def test_case_sensitivity(self): + gc = GCode.parse_line("m111") + self.assertEquals(gc.command(), "M111") + gc = GCode.parse_line("g2X3y-4Z5") + self.assertEquals(gc.command(), "G2") + self.assertEquals(gc.coordinates(self.default, 1).x, 3.0) + self.assertEquals(gc.coordinates(self.default, 1).y, -4.0) + self.assertEquals(gc.coordinates(self.default, 1).z, 5.0) + + def test_has_coordinates(self): + gc = GCode.parse_line("X2Y-3Z4") + self.assertTrue(gc.has_coordinates()) + gc = GCode.parse_line("G1") + self.assertFalse(gc.has_coordinates()) + gc = GCode.parse_line("X1") + self.assertTrue(gc.has_coordinates()) + gc = GCode.parse_line("Y1") + self.assertTrue(gc.has_coordinates()) + gc = GCode.parse_line("Z1") + self.assertTrue(gc.has_coordinates()) + + def test_multiply(self): + # getting coordinates could modify value be specified multiplier. + gc = GCode.parse_line("X2 Y-3 Z4") + self.assertEquals(gc.coordinates(self.default, 25.4).x, 50.8) + self.assertEquals(gc.coordinates(self.default, 2).y, -6) + self.assertEquals(gc.coordinates(self.default, 0).y, 0) + + def test_whitespaces(self): + gc = GCode.parse_line("X1 Y2") + self.assertEquals(gc.coordinates(self.default, 1).x, 1.0) + self.assertEquals(gc.coordinates(self.default, 1).y, 2.0) + gc = GCode.parse_line("X 3 Y4") + self.assertEquals(gc.coordinates(self.default, 1).x, 3.0) + self.assertEquals(gc.coordinates(self.default, 1).y, 4.0) + gc = GCode.parse_line("X 5 Y\t 6") + self.assertEquals(gc.coordinates(self.default, 1).x, 5.0) + self.assertEquals(gc.coordinates(self.default, 1).y, 6.0) + gc = GCode.parse_line(" \tX\t\t \t\t7\t ") + self.assertEquals(gc.coordinates(self.default, 1).x, 7.0) + + def test_errors(self): + self.assertRaises(GCodeException, GCode.parse_line, "X1X1") + self.assertRaises(GCodeException, GCode.parse_line, "X1+Y1") + self.assertRaises(GCodeException, GCode.parse_line, "X1-Y1") + self.assertRaises(GCodeException, GCode.parse_line, "~Y1") + self.assertRaises(GCodeException, GCode.parse_line, "Y") + self.assertRaises(GCodeException, GCode.parse_line, "abracadabra") + self.assertRaises(GCodeException, GCode.parse_line, "G1M1") + self.assertRaises(GCodeException, GCode.parse_line, "x 1 y 1 z 1 X 1") + + def test_comments(self): + self.assertIsNone(GCode.parse_line("; some text")) + self.assertIsNone(GCode.parse_line(" \t \t ; some text")) + self.assertIsNone(GCode.parse_line("(another comment)")) + gc = GCode.parse_line("X2.5 ; end of line comment") + self.assertEquals(gc.coordinates(self.default, 1).x, 2.5) + gc = GCode.parse_line("X2 Y(inline comment)7") + self.assertEquals(gc.coordinates(self.default, 1).x, 2.0) + self.assertEquals(gc.coordinates(self.default, 1).y, 7.0) + gc = GCode.parse_line("X2 Y(inline comment)3 \t(one more comment) \tz4 ; multi comment test") + self.assertEquals(gc.coordinates(self.default, 1).x, 2.0) + self.assertEquals(gc.coordinates(self.default, 1).y, 3.0) + self.assertEquals(gc.coordinates(self.default, 1).z, 4.0) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test_gmachine.py b/tests/test_gmachine.py new file mode 100644 index 0000000..93448f2 --- /dev/null +++ b/tests/test_gmachine.py @@ -0,0 +1,127 @@ +import unittest +import time + +from cnc.coordinates import * +from cnc.gcode import * +from cnc.gmachine import * + + +class TestGMachine(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_reset(self): + # reset() resets all configurable from gcode things. + m = GMachine() + m.do_command(GCode.parse_line("G20")) + m.do_command(GCode.parse_line("G91")) + m.do_command(GCode.parse_line("X1Y1Z1")) + m.reset() + m.do_command(GCode.parse_line("X3Y4Z5")) + self.assertEquals(m.position(), Coordinates(3, 4, 5)) + + def test_release(self): + # release homes head. + m = GMachine() + m.do_command(GCode.parse_line("X1Y2Z3")) + m.release() + self.assertEquals(m.position(), Coordinates(0, 0, 0)) + + def test_home(self): + m = GMachine() + m.do_command(GCode.parse_line("X1Y2Z3")) + m.home() + self.assertEquals(m.position(), Coordinates(0, 0, 0)) + + def test_none(self): + # GMachine must ignore None commands, since GCode.parse_line() + # returns None if no gcode found in line. + m = GMachine() + m.do_command(None) + self.assertEquals(m.position(), Coordinates(0, 0, 0)) + + def test_unknown(self): + # Test commands which doesn't exists + m = GMachine() + self.assertRaises(GMachineException, + m.do_command, GCode.parse_line("G99699X1Y2Z3")) + self.assertRaises(GMachineException, + m.do_command, GCode.parse_line("M99699")) + + # Test gcode commands. + def test_g0_g1(self): + m = GMachine() + m.do_command(GCode.parse_line("G0X3Y2Z1")) + self.assertEquals(m.position(), Coordinates(3, 2, 1)) + m.do_command(GCode.parse_line("G1X1Y2Z3")) + self.assertEquals(m.position(), Coordinates(1, 2, 3)) + self.assertRaises(GMachineException, + m.do_command, GCode.parse_line("G1F-1")) + self.assertRaises(GMachineException, + m.do_command, GCode.parse_line("G1F999999")) + self.assertRaises(GMachineException, + m.do_command, GCode.parse_line("G1X-1Y0Z0")) + self.assertRaises(GMachineException, + m.do_command, GCode.parse_line("G1X0Y-1Z0")) + self.assertRaises(GMachineException, + m.do_command, GCode.parse_line("G1X0Y0Z-1")) + + def test_g4(self): + m = GMachine() + st = time.time() + m.do_command(GCode.parse_line("G4P0.5")) + self.assertLess(0.5, time.time() - st) + self.assertRaises(GMachineException, + m.do_command, GCode.parse_line("G4P-0.5")) + + def test_g20_g21(self): + m = GMachine() + m.do_command(GCode.parse_line("G20")) + m.do_command(GCode.parse_line("X3Y2Z1")) + self.assertEquals(m.position(), Coordinates(76.2, 50.8, 25.4)) + m.do_command(GCode.parse_line("G21")) + m.do_command(GCode.parse_line("X3Y2Z1")) + self.assertEquals(m.position(), Coordinates(3, 2, 1)) + + def test_g90_g91(self): + m = GMachine() + m.do_command(GCode.parse_line("G91")) + m.do_command(GCode.parse_line("X1Y1Z1")) + m.do_command(GCode.parse_line("X1Y1")) + m.do_command(GCode.parse_line("X1")) + self.assertEquals(m.position(), Coordinates(3, 2, 1)) + m.do_command(GCode.parse_line("X-1Y-1Z-1")) + m.do_command(GCode.parse_line("G90")) + m.do_command(GCode.parse_line("X1Y1Z1")) + self.assertEquals(m.position(), Coordinates(1, 1, 1)) + + def test_g90_g92(self): + m = GMachine() + m.do_command(GCode.parse_line("G92X100Y100Z100")) + m.do_command(GCode.parse_line("X101Y102Z103")) + self.assertEquals(m.position(), Coordinates(1, 2, 3)) + m.do_command(GCode.parse_line("G92X-1Y-1Z-1")) + m.do_command(GCode.parse_line("X1Y1Z1")) + self.assertEquals(m.position(), Coordinates(3, 4, 5)) + m.do_command(GCode.parse_line("G92X3Y4Z5")) + m.do_command(GCode.parse_line("X0Y0Z0")) + self.assertEquals(m.position(), Coordinates(0, 0, 0)) + m.do_command(GCode.parse_line("G90")) + m.do_command(GCode.parse_line("X6Y7Z8")) + self.assertEquals(m.position(), Coordinates(6, 7, 8)) + + def test_m3_m5(self): + m = GMachine() + m.do_command(GCode.parse_line("M3S" + str(SPINDLE_MAX_RPM))) + self.assertRaises(GMachineException, + m.do_command, GCode.parse_line("M3S-10")) + self.assertRaises(GMachineException, + m.do_command, GCode.parse_line("M3S999999999")) + m.do_command(GCode.parse_line("M5")) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file diff --git a/tests/test-parser.gcode b/tests/test_parser.gcode similarity index 100% rename from tests/test-parser.gcode rename to tests/test_parser.gcode diff --git a/tests/test_pulses.py b/tests/test_pulses.py new file mode 100644 index 0000000..3082a97 --- /dev/null +++ b/tests/test_pulses.py @@ -0,0 +1,117 @@ +import unittest +import time + +from cnc.coordinates import * +from cnc.pulses import * +from cnc.config import * +from cnc import hal_virtual + + +class TestPulses(unittest.TestCase): + def setUp(self): + self.v = STEPPER_MAX_VELOCITY_MM_PER_MIN + + def tearDown(self): + pass + + def test_zero(self): + # PulseGenerator should never receive empty movement. + self.assertRaises(ZeroDivisionError, + PulseGeneratorLinear, + Coordinates(0, 0, 0), self.v) + + def test_step(self): + # Check if PulseGenerator returns correctly single step movement. + g = PulseGeneratorLinear(Coordinates(1.0 / STEPPER_PULSES_PER_MM, 0, 0), + self.v) + i = 0 + for px, py, pz in g: + i += 1 + self.assertEquals(px, 0) + self.assertEquals(py, None) + self.assertEquals(pz, None) + self.assertEquals(i, 1) + g = PulseGeneratorLinear(Coordinates( + 1.0 / STEPPER_PULSES_PER_MM, + 1.0 / STEPPER_PULSES_PER_MM, + 1.0 / STEPPER_PULSES_PER_MM), + self.v) + i = 0 + for px, py, pz in g: + i += 1 + self.assertEquals(px, 0) + self.assertEquals(py, 0) + self.assertEquals(pz, 0) + self.assertEquals(i, 1) + + def test_linear_with_hal_virtual(self): + # Using hal_virtual module for this test, it already contains plenty + # of asserts for wrong number of pulses, pulse timing issues etc + hal_virtual.move_linear(Coordinates(1, 0, 0), self.v) + hal_virtual.move_linear(Coordinates(25.4, 0, 0), self.v) + hal_virtual.move_linear(Coordinates(25.4, 0, 0), self.v) + hal_virtual.move_linear(Coordinates(25.4, 0, 0), self.v) + hal_virtual.move_linear(Coordinates(TABLE_SIZE_X_MM, + TABLE_SIZE_Y_MM, + TABLE_SIZE_Z_MM), self.v) + + def test_twice_faster(self): + # Checks if one axis moves exactly twice faster, pulses are correct. + m = Coordinates(2, 4, 0) + g = PulseGeneratorLinear(m, self.v) + i = 0 + for px, py, pz in g: + if i % 2 == 0: + self.assertNotEquals(px, None) + else: + self.assertEquals(px, None) + self.assertNotEquals(py, None) + self.assertEquals(pz, None) + i += 1 + self.assertEquals(m.find_max() * STEPPER_PULSES_PER_MM, i) + + def test_pulses_count_and_timings(self): + # Check if number of pulses is equal to specified distance. + m = Coordinates(TABLE_SIZE_X_MM, TABLE_SIZE_Y_MM, TABLE_SIZE_Z_MM) + g = PulseGeneratorLinear(m, self.v) + ix = 0 + iy = 0 + iz = 0 + t = -1 + for px, py, pz in g: + if px is not None: + ix += 1 + if py is not None: + iy += 1 + if pz is not None: + iz += 1 + v = list(x for x in (px, py, pz) if x is not None) + self.assertEquals(min(v), max(v)) + self.assertLess(t, min(v)) + t = max(v) + self.assertEquals(m.x * STEPPER_PULSES_PER_MM, ix) + self.assertEquals(m.y * STEPPER_PULSES_PER_MM, iy) + self.assertEquals(m.z * STEPPER_PULSES_PER_MM, iz) + self.assertLessEqual(t, g.total_time_s()) + + def test_acceleration_velocity(self): + # Check if acceleration present in pulses sequence and if velocity + # is correct + m = Coordinates(TABLE_SIZE_X_MM, 0, 0) + g = PulseGeneratorLinear(m, self.v) + i = 0 + lx = 0 + for px, py, pz in g: + if i == 2: + at = px - lx + if i == TABLE_SIZE_X_MM * STEPPER_PULSES_PER_MM / 2: + lt = px - lx + bt = px - lx + lx = px + i += 1 + self.assertEquals(round(60.0 / lt / STEPPER_PULSES_PER_MM), round(self.v)) + self.assertGreater(at, lt) + self.assertGreater(bt, lt) + +if __name__ == '__main__': + unittest.main() \ No newline at end of file