From d98d23f8b5d61bb9d20e743a3c44bf04b6b2330a Mon Sep 17 00:00:00 2001 From: Hamilton Kibbe Date: Mon, 2 Feb 2015 00:43:08 -0500 Subject: More tests and bugfixes --- gerber/__main__.py | 10 ++-- gerber/cam.py | 16 +++-- gerber/common.py | 3 +- gerber/primitives.py | 23 ++++--- gerber/render/render.py | 130 +++++++++++++++++++--------------------- gerber/tests/test_cam.py | 30 +++++++++- gerber/tests/test_common.py | 11 +++- gerber/tests/test_primitives.py | 79 ++++++++++++++++++++++-- gerber/tests/test_utils.py | 43 +++++++++---- 9 files changed, 235 insertions(+), 110 deletions(-) (limited to 'gerber') diff --git a/gerber/__main__.py b/gerber/__main__.py index 71e3bfc..8f20212 100644 --- a/gerber/__main__.py +++ b/gerber/__main__.py @@ -25,15 +25,15 @@ if __name__ == '__main__': sys.exit(1) ctx = GerberSvgContext() - ctx.set_alpha(0.95) + ctx.alpha = 0.95 for filename in sys.argv[1:]: print "parsing %s" % filename if 'GTO' in filename or 'GBO' in filename: - ctx.set_color((1, 1, 1)) - ctx.set_alpha(0.8) + ctx.color = (1, 1, 1) + ctx.alpha = 0.8 elif 'GTS' in filename or 'GBS' in filename: - ctx.set_color((0.2, 0.2, 0.75)) - ctx.set_alpha(0.8) + ctx.color = (0.2, 0.2, 0.75) + ctx.alpha = 0.8 gerberfile = read(filename) gerberfile.render(ctx) diff --git a/gerber/cam.py b/gerber/cam.py index a4057bc..9c731aa 100644 --- a/gerber/cam.py +++ b/gerber/cam.py @@ -62,29 +62,24 @@ class FileSettings(object): if units not in ['inch', 'metric']: raise ValueError('Units must be either inch or metric') self.units = units - - + if zero_suppression is None and zeros is None: self.zero_suppression = 'trailing' - + elif zero_suppression == zeros: raise ValueError('Zeros and Zero Suppression must be different. \ Best practice is to specify only one.') - + elif zero_suppression is not None: if zero_suppression not in ['leading', 'trailing']: raise ValueError('Zero suppression must be either leading or \ trailling') self.zero_suppression = zero_suppression - elif zeros is not None: if zeros not in ['leading', 'trailing']: raise ValueError('Zeros must be either leading or trailling') self.zeros = zeros - else: - self.zeros = 'leading' - if len(format) != 2: raise ValueError('Format must be a tuple(n=2) of integers') @@ -150,6 +145,9 @@ class FileSettings(object): raise ValueError('Format must be a tuple(n=2) of integers') self.format = value + else: + raise KeyError('%s is not a valid key' % key) + def __eq__(self, other): return (self.notation == other.notation and self.units == other.units and @@ -230,7 +228,7 @@ class CamFile(object): def bounds(self): """ File baundaries """ - pass + raise NotImplementedError('bounds must be implemented in a subclass') def render(self, ctx, filename=None): """ Generate image of layer. diff --git a/gerber/common.py b/gerber/common.py index 6e8c862..83e3cb0 100644 --- a/gerber/common.py +++ b/gerber/common.py @@ -39,4 +39,5 @@ def read(filename): elif fmt == 'excellon': return excellon.read(filename) else: - return None + raise TypeError('Unable to detect file format') + diff --git a/gerber/primitives.py b/gerber/primitives.py index 61df7c1..da05127 100644 --- a/gerber/primitives.py +++ b/gerber/primitives.py @@ -345,20 +345,25 @@ class Obround(Primitive): self.position = position self.width = width self.height = height - - @property - def orientation(self): - return 'vertical' if self.height > self.width else 'horizontal' + # Axis-aligned width and height + self._abs_width = (math.cos(math.radians(self.rotation)) * self.width + + math.sin(math.radians(self.rotation)) * self.height) + self._abs_height = (math.cos(math.radians(self.rotation)) * self.height + + math.sin(math.radians(self.rotation)) * self.width) @property def lower_left(self): - return (self.position[0] - (self.width / 2.), - self.position[1] - (self.height / 2.)) + return (self.position[0] - (self._abs_width / 2.), + self.position[1] - (self._abs_height / 2.)) @property def upper_right(self): - return (self.position[0] + (self.width / 2.), - self.position[1] + (self.height / 2.)) + return (self.position[0] + (self._abs_width / 2.), + self.position[1] + (self._abs_height / 2.)) + + @property + def orientation(self): + return 'vertical' if self.height > self.width else 'horizontal' @property def bounding_box(self): @@ -380,7 +385,7 @@ class Obround(Primitive): else: circle1 = Circle((self.position[0] - (self.height - self.width) / 2., self.position[1]), self.height) - circle2 = Circle((self.position[0] - (self.height - self.width) / 2., + circle2 = Circle((self.position[0] + (self.height - self.width) / 2., self.position[1]), self.height) rect = Rectangle(self.position, (self.width - self.height), self.height) diff --git a/gerber/render/render.py b/gerber/render/render.py index f5c58d8..2e4abfa 100644 --- a/gerber/render/render.py +++ b/gerber/render/render.py @@ -41,7 +41,7 @@ class GerberContext(object): Attributes ---------- units : string - Measurement units + Measurement units. 'inch' or 'metric' color : tuple (, , ) Color used for rendering as a tuple of normalized (red, green, blue) values. @@ -57,74 +57,70 @@ class GerberContext(object): Rendering opacity. Between 0.0 (transparent) and 1.0 (opaque.) """ def __init__(self, units='inch'): - self.units = units - self.color = (0.7215, 0.451, 0.200) - self.drill_color = (0.25, 0.25, 0.25) - self.background_color = (0.0, 0.0, 0.0) - self.alpha = 1.0 - - def set_units(self, units): - """ Set context measurement units - - Parameters - ---------- - unit : string - Measurement units. may be 'inch' or 'metric' - - Raises - ------ - ValueError - If `unit` is not 'inch' or 'metric' - """ + self._units = units + self._color = (0.7215, 0.451, 0.200) + self._drill_color = (0.25, 0.25, 0.25) + self._background_color = (0.0, 0.0, 0.0) + self._alpha = 1.0 + + @property + def units(self): + return self._units + + @units.setter + def units(self, units): if units not in ('inch', 'metric'): raise ValueError('Units may be "inch" or "metric"') - self.units = units - - def set_color(self, color): - """ Set rendering color. - - Parameters - ---------- - color : tuple (, , ) - Color as a tuple of (red, green, blue) values. Each channel is - represented as a float value in (0, 1) - """ - self.color = color - - def set_drill_color(self, color): - """ Set color used for rendering drill hits. - - Parameters - ---------- - color : tuple (, , ) - Color as a tuple of (red, green, blue) values. Each channel is - represented as a float value in (0, 1) - """ - self.drill_color = color - - def set_background_color(self, color): - """ Set rendering background color - - Parameters - ---------- - color : tuple (, , ) - Color as a tuple of (red, green, blue) values. Each channel is - represented as a float value in (0, 1) - """ - self.background_color = color - - def set_alpha(self, alpha): - """ Set layer rendering opacity - - .. note:: - Not all backends/rendering devices support this parameter. - - Parameters - ---------- - alpha : float - Rendering opacity. must be between 0.0 (transparent) and 1.0 (opaque) - """ - self.alpha = alpha + self._units = units + + @property + def color(self): + return self._color + + @color.setter + def color(self, color): + if len(color) != 3: + raise TypeError('Color must be a tuple of R, G, and B values') + for c in color: + if c < 0 or c > 1: + raise ValueError('Channel values must be between 0.0 and 1.0') + self._color = color + + @property + def drill_color(self): + return self._drill_color + + @drill_color.setter + def drill_color(self, color): + if len(color) != 3: + raise TypeError('Drill color must be a tuple of R, G, and B values') + for c in color: + if c < 0 or c > 1: + raise ValueError('Channel values must be between 0.0 and 1.0') + self._drill_color = color + + @property + def background_color(self): + return self._background_color + + @background_color.setter + def background_color(self, color): + if len(color) != 3: + raise TypeError('Background color must be a tuple of R, G, and B values') + for c in color: + if c < 0 or c > 1: + raise ValueError('Channel values must be between 0.0 and 1.0') + self._background_color = color + + @property + def alpha(self): + return self._alpha + + @alpha.setter + def alpha(self, alpha): + if alpha < 0 or alpha > 1: + raise ValueError('Alpha must be between 0.0 and 1.0') + self._alpha = alpha def render(self, primitive): color = (self.color if primitive.level_polarity == 'dark' diff --git a/gerber/tests/test_cam.py b/gerber/tests/test_cam.py index 1aeb18c..8e0270c 100644 --- a/gerber/tests/test_cam.py +++ b/gerber/tests/test_cam.py @@ -65,8 +65,14 @@ def test_camfile_settings(): cf = CamFile() assert_equal(cf.settings, FileSettings()) -def test_zeros(): +#def test_bounds_override(): +# cf = CamFile() +# assert_raises(NotImplementedError, cf.bounds) + +def test_zeros(): + """ Test zero/zero_suppression interaction + """ fs = FileSettings() assert_equal(fs.zero_suppression, 'trailing') assert_equal(fs.zeros, 'leading') @@ -89,3 +95,25 @@ def test_zeros(): assert_equal(fs.zeros, 'leading') assert_equal(fs.zero_suppression, 'trailing') + +def test_filesettings_validation(): + """ Test FileSettings constructor argument validation + """ + assert_raises(ValueError, FileSettings, 'absolute-ish', 'inch', None, (2, 5), None) + assert_raises(ValueError, FileSettings, 'absolute', 'degrees kelvin', None, (2, 5), None) + assert_raises(ValueError, FileSettings, 'absolute', 'inch', 'leading', (2, 5), 'leading') + assert_raises(ValueError, FileSettings, 'absolute', 'inch', 'following', (2, 5), None) + assert_raises(ValueError, FileSettings, 'absolute', 'inch', None, (2, 5), 'following') + assert_raises(ValueError, FileSettings, 'absolute', 'inch', None, (2, 5, 6), None) + +def test_key_validation(): + fs = FileSettings() + assert_raises(KeyError, fs.__getitem__, 'octopus') + assert_raises(KeyError, fs.__setitem__, 'octopus', 'do not care') + assert_raises(ValueError, fs.__setitem__, 'notation', 'absolute-ish') + assert_raises(ValueError, fs.__setitem__, 'units', 'degrees kelvin') + assert_raises(ValueError, fs.__setitem__, 'zero_suppression', 'following') + assert_raises(ValueError, fs.__setitem__, 'zeros', 'following') + assert_raises(ValueError, fs.__setitem__, 'format', (2, 5, 6)) + + diff --git a/gerber/tests/test_common.py b/gerber/tests/test_common.py index 1e1efe5..bf9760a 100644 --- a/gerber/tests/test_common.py +++ b/gerber/tests/test_common.py @@ -20,5 +20,12 @@ def test_file_type_detection(): """ ncdrill = read(NCDRILL_FILE) top_copper = read(TOP_COPPER_FILE) - assert(isinstance(ncdrill, ExcellonFile)) - assert(isinstance(top_copper, GerberFile)) + assert_true(isinstance(ncdrill, ExcellonFile)) + assert_true(isinstance(top_copper, GerberFile)) + +def test_file_type_validation(): + """ Test file format validation + """ + assert_raises(TypeError, read, 'LICENSE') + + diff --git a/gerber/tests/test_primitives.py b/gerber/tests/test_primitives.py index 4534484..e5ae770 100644 --- a/gerber/tests/test_primitives.py +++ b/gerber/tests/test_primitives.py @@ -165,6 +165,7 @@ def test_chamfer_rectangle_ctor(): assert_equal(r.chamfer, chamfer) assert_array_almost_equal(r.corners, corners) + def test_chamfer_rectangle_bounds(): """ Test chamfer rectangle bounding box calculation """ @@ -191,7 +192,8 @@ def test_round_rectangle_ctor(): assert_equal(r.height, height) assert_equal(r.radius, radius) assert_array_almost_equal(r.corners, corners) - + + def test_round_rectangle_bounds(): """ Test round rectangle bounding box calculation """ @@ -203,7 +205,76 @@ def test_round_rectangle_bounds(): xbounds, ybounds = r.bounding_box assert_array_almost_equal(xbounds, (-math.sqrt(2), math.sqrt(2))) assert_array_almost_equal(ybounds, (-math.sqrt(2), math.sqrt(2))) + + +def test_obround_ctor(): + """ Test obround creation + """ + test_cases = (((0,0), 1, 1), + ((0, 0), 1, 2), + ((1,1), 1, 2)) + for pos, width, height in test_cases: + o = Obround(pos, width, height) + assert_equal(o.position, pos) + assert_equal(o.width, width) + assert_equal(o.height, height) + + +def test_obround_bounds(): + """ Test obround bounding box calculation + """ + o = Obround((2,2),2,4) + xbounds, ybounds = o.bounding_box + assert_array_almost_equal(xbounds, (1, 3)) + assert_array_almost_equal(ybounds, (0, 4)) + o = Obround((2,2),4,2) + xbounds, ybounds = o.bounding_box + assert_array_almost_equal(xbounds, (0, 4)) + assert_array_almost_equal(ybounds, (1, 3)) + + +def test_obround_orientation(): + o = Obround((0, 0), 2, 1) + assert_equal(o.orientation, 'horizontal') + o = Obround((0, 0), 1, 2) + assert_equal(o.orientation, 'vertical') + + +def test_obround_subshapes(): + o = Obround((0,0), 1, 4) + ss = o.subshapes + assert_array_almost_equal(ss['rectangle'].position, (0, 0)) + assert_array_almost_equal(ss['circle1'].position, (0, 1.5)) + assert_array_almost_equal(ss['circle2'].position, (0, -1.5)) + o = Obround((0,0), 4, 1) + ss = o.subshapes + assert_array_almost_equal(ss['rectangle'].position, (0, 0)) + assert_array_almost_equal(ss['circle1'].position, (1.5, 0)) + assert_array_almost_equal(ss['circle2'].position, (-1.5, 0)) - - - \ No newline at end of file +def test_polygon_ctor(): + """ Test polygon creation + """ + test_cases = (((0,0), 3, 5), + ((0, 0), 5, 6), + ((1,1), 7, 7)) + for pos, sides, radius in test_cases: + p = Polygon(pos, sides, radius) + assert_equal(p.position, pos) + assert_equal(p.sides, sides) + assert_equal(p.radius, radius) + +def test_polygon_bounds(): + """ Test polygon bounding box calculation + """ + p = Polygon((2,2), 3, 2) + xbounds, ybounds = p.bounding_box + assert_array_almost_equal(xbounds, (0, 4)) + assert_array_almost_equal(ybounds, (0, 4)) + p = Polygon((2,2),3, 4) + xbounds, ybounds = p.bounding_box + assert_array_almost_equal(xbounds, (-2, 6)) + assert_array_almost_equal(ybounds, (-2, 6)) + + + diff --git a/gerber/tests/test_utils.py b/gerber/tests/test_utils.py index 706fa65..1077022 100644 --- a/gerber/tests/test_utils.py +++ b/gerber/tests/test_utils.py @@ -3,8 +3,8 @@ # Author: Hamilton Kibbe -from .tests import assert_equal -from ..utils import decimal_string, parse_gerber_value, write_gerber_value +from .tests import assert_equal, assert_raises +from ..utils import decimal_string, parse_gerber_value, write_gerber_value, detect_file_format def test_zero_suppression(): @@ -22,8 +22,8 @@ def test_zero_suppression(): ('-100000', -1.0), ('-1000000', -10.0), ('0', 0.0)] for string, value in test_cases: - assert(value == parse_gerber_value(string, fmt, zero_suppression)) - assert(string == write_gerber_value(value, fmt, zero_suppression)) + assert_equal(value, parse_gerber_value(string, fmt, zero_suppression)) + assert_equal(string, write_gerber_value(value, fmt, zero_suppression)) # Test trailing zero suppression zero_suppression = 'trailing' @@ -34,8 +34,8 @@ def test_zero_suppression(): ('-000001', -0.0001), ('-0000001', -0.00001), ('0', 0.0)] for string, value in test_cases: - assert(value == parse_gerber_value(string, fmt, zero_suppression)) - assert(string == write_gerber_value(value, fmt, zero_suppression)) + assert_equal(value, parse_gerber_value(string, fmt, zero_suppression)) + assert_equal(string, write_gerber_value(value, fmt, zero_suppression)) def test_format(): @@ -51,8 +51,8 @@ def test_format(): ((2, 2), '-1', -0.01), ((2, 1), '-1', -0.1), ((2, 6), '0', 0) ] for fmt, string, value in test_cases: - assert(value == parse_gerber_value(string, fmt, zero_suppression)) - assert(string == write_gerber_value(value, fmt, zero_suppression)) + assert_equal(value, parse_gerber_value(string, fmt, zero_suppression)) + assert_equal(string, write_gerber_value(value, fmt, zero_suppression)) zero_suppression = 'trailing' test_cases = [((6, 5), '1', 100000.0), ((5, 5), '1', 10000.0), @@ -63,8 +63,8 @@ def test_format(): ((2, 5), '-1', -10.0), ((1, 5), '-1', -1.0), ((2, 5), '0', 0)] for fmt, string, value in test_cases: - assert(value == parse_gerber_value(string, fmt, zero_suppression)) - assert(string == write_gerber_value(value, fmt, zero_suppression)) + assert_equal(value, parse_gerber_value(string, fmt, zero_suppression)) + assert_equal(string, write_gerber_value(value, fmt, zero_suppression)) def test_decimal_truncation(): @@ -74,7 +74,7 @@ def test_decimal_truncation(): for x in range(10): result = decimal_string(value, precision=x) calculated = '1.' + ''.join(str(y) for y in range(1,x+1)) - assert(result == calculated) + assert_equal(result, calculated) def test_decimal_padding(): @@ -85,6 +85,25 @@ def test_decimal_padding(): assert_equal(decimal_string(value, precision=4, padding=True), '1.1230') assert_equal(decimal_string(value, precision=5, padding=True), '1.12300') assert_equal(decimal_string(value, precision=6, padding=True), '1.123000') - assert_equal(decimal_string(0, precision=6, padding=True), '0.000000') + +def test_parse_format_validation(): + """ Test parse_gerber_value() format validation + """ + assert_raises(ValueError, parse_gerber_value, '00001111', (7, 5)) + assert_raises(ValueError, parse_gerber_value, '00001111', (5, 8)) + assert_raises(ValueError, parse_gerber_value, '00001111', (13,1)) + +def test_write_format_validation(): + """ Test write_gerber_value() format validation + """ + assert_raises(ValueError, write_gerber_value, 69.0, (7, 5)) + assert_raises(ValueError, write_gerber_value, 69.0, (5, 8)) + assert_raises(ValueError, write_gerber_value, 69.0, (13,1)) + + +def test_detect_format_with_short_file(): + """ Verify file format detection works with short files + """ + assert_equal('unknown', detect_file_format('gerber/tests/__init__.py')) -- cgit