unit tests

This commit is contained in:
Nikolay Khabarov
2017-05-13 21:02:27 +03:00
parent bb0f74c6d9
commit dc90aab445
12 changed files with 512 additions and 6 deletions

View File

@@ -1,3 +1,4 @@
from __future__ import division
import math

View File

@@ -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.
"""

View File

@@ -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

View File

@@ -1,3 +1,4 @@
from __future__ import division
import logging
import time

13
runtests.sh Executable file
View File

@@ -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"

0
tests/__init__.py Normal file
View File

123
tests/test_coordinates.py Normal file
View File

@@ -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()

114
tests/test_gcode.py Normal file
View File

@@ -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()

127
tests/test_gmachine.py Normal file
View File

@@ -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()

117
tests/test_pulses.py Normal file
View File

@@ -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()