node-gerber-parser
Version: 
Streaming Gerber/drill file parser
955 lines (817 loc) • 28.3 kB
JavaScript
// test suite for the top level gerber parser class
// test subset - parsing gerber files
var expect = require('chai').expect
var partial = require('lodash/partial')
var parser = require('..')
describe('gerber parser with gerber files', function() {
  var p
  var pFactory = partial(parser, {filetype: 'gerber'})
  // convenience function to expect an array of results
  var expectResults = function(expected, done) {
    var handleData = function(res) {
      expect(res).to.eql(expected.shift())
      if (!expected.length) {
        return done()
      }
    }
    p.on('data', handleData)
  }
  beforeEach(function() {
    p = pFactory()
  })
  afterEach(function() {
    p.removeAllListeners('data')
    p.removeAllListeners('warning')
    p.removeAllListeners('error')
  })
  it('should do nothing with comments', function(done) {
    p.once('data', function() {
      p.removeAllListeners('warning').removeAllListeners('error')
      throw new Error('should not have emitted from comments')
    })
    p.once('warning', function() {
      p.removeAllListeners('data').removeAllListeners('error')
      throw new Error('should not have warned from comments')
    })
    p.once('error', function() {
      p.removeAllListeners('warning').removeAllListeners('data')
      throw new Error('should not have errored from comments')
    })
    p.write('G04 MOIN*')
    p.write('G04 this is a comment*')
    p.write('G04 D03*')
    p.write('G04 D02*')
    p.write('G04 G36*')
    p.write('G04 M02*')
    setTimeout(done, 1)
  })
  it('should do nothing with "empty" blocks', function(done) {
    p.once('data', function() {
      p.removeAllListeners('warning').removeAllListeners('error')
      throw new Error('should not have emitted from empty block')
    })
    p.once('warning', function() {
      p.removeAllListeners('data').removeAllListeners('error')
      throw new Error('should not have warned from empty block')
    })
    p.once('error', function() {
      p.removeAllListeners('warning').removeAllListeners('data')
      throw new Error('should not have errored from empty block')
    })
    p.write('*\n')
    p.write('\n')
    p.write('')
    p.write('\n\n\n')
    setTimeout(done, 1)
  })
  it('should warn if a block is unhandled', function(done) {
    p.once('warning', function(w) {
      expect(w.line).to.equal(0)
      expect(w.message).to.match(/not recognized/)
      done()
    })
    p.write('foobarbaz*\n')
  })
  it('should handle split blocks', function(done) {
    p.once('data', function() {
      p.removeAllListeners('warning').removeAllListeners('error')
      throw new Error('should not have emitted from split comment')
    })
    p.once('warning', function() {
      p.removeAllListeners('data').removeAllListeners('error')
      throw new Error('should not have warned from split comment')
    })
    p.once('error', function() {
      p.removeAllListeners('warning').removeAllListeners('data')
      throw new Error('should not have errored from split comment')
    })
    p.write('G04 thi')
    p.write('s is a comment*\n')
    setTimeout(done, 1)
  })
  it('should end the file with a M02', function(done) {
    var expected = [{type: 'done', line: 0}]
    expectResults(expected, done)
    p.write('M02*\n')
  })
  describe('general set commands (G-codes)', function() {
    it('should set region mode on/off with G36/7', function(done) {
      var expected = [
        {type: 'set', prop: 'region', value: true, line: 0},
        {type: 'set', prop: 'region', value: false, line: 1},
      ]
      expectResults(expected, done)
      p.write('G36*\nG37*\n')
    })
    it('should set interpolation mode with G01/2/3', function(done) {
      var expected = [
        {type: 'set', prop: 'mode', value: 'i', line: 0},
        {type: 'set', prop: 'mode', value: 'cw', line: 1},
        {type: 'set', prop: 'mode', value: 'ccw', line: 2},
        {type: 'set', prop: 'mode', value: 'i', line: 3},
        {type: 'set', prop: 'mode', value: 'cw', line: 4},
        {type: 'set', prop: 'mode', value: 'ccw', line: 5},
      ]
      expectResults(expected, done)
      p.write('G01*\nG02*\nG03*\n')
      p.write('G1*\nG2*\nG3*\n')
    })
    it('should set the arc mode with G74/5', function(done) {
      var expected = [
        {type: 'set', prop: 'arc', value: 's', line: 0},
        {type: 'set', prop: 'arc', value: 'm', line: 1},
      ]
      expectResults(expected, done)
      p.write('G74*\nG75*\n')
    })
  })
  describe('unit set commands (MO parameter)', function() {
    it('should set units with %MOIN*% and %MOMM*%', function(done) {
      var expected = [
        {type: 'set', prop: 'units', value: 'in', line: 0},
        {type: 'set', prop: 'units', value: 'mm', line: 1},
      ]
      expectResults(expected, done)
      p.write('%MOIN*%\n')
      p.write('%MOMM*%\n')
    })
    // Special Case: Cadence Allegro
    // The gerber spec says
    //   "There can be only one extended code command between each pair of ‘%’ delimiters"
    // Cadence Allegro violates this rule by including the units after the format command
    //
    // Sample:
    //   G04 File Origin:  Cadence Allegro 17.2-S032*
    //   ...
    //   %FSLAX25Y25*MOMM*%
    //   ...
    //
    describe('should set units if found in same line as format', function() {
      it('should output notation and epsilion then units', function(done) {
        var epsilon = 1.5 * Math.pow(10, -5)
        var expected = [
          {type: 'set', line: 0, prop: 'nota', value: 'A'},
          {type: 'set', line: 0, prop: 'epsilon', value: epsilon},
          {type: 'set', line: 0, prop: 'units', value: 'in'},
          {type: 'set', line: 1, prop: 'nota', value: 'A'},
          {type: 'set', line: 1, prop: 'epsilon', value: epsilon},
          {type: 'set', line: 1, prop: 'units', value: 'mm'},
        ]
        expectResults(expected, done)
        p.write('%FSLAX25Y25*MOIN*%\n') // inches
        p.write('%FSTAX25Y25*MOMM*%\n') // millimeters
      })
      it('should still parse format', function() {
        // Inches
        p.write('%FSLAX25Y25*MOIN*%\n')
        expect(p.format.zero).to.equal('L')
        expect(p.format.places).to.eql([2, 5])
        // Millimeters
        p.format.zero = null
        p.format.places = null
        p.write('%FSTAX37Y37*MOMM*%\n')
        expect(p.format.zero).to.equal('T')
        expect(p.format.places).to.eql([3, 7])
      })
    })
    it('should set backup units with G70/1', function(done) {
      var expected = [
        {type: 'set', prop: 'backupUnits', value: 'in', line: 0},
        {type: 'set', prop: 'backupUnits', value: 'mm', line: 1},
      ]
      expectResults(expected, done)
      p.write('G70*\nG71*\n')
    })
  })
  describe('format block', function() {
    it('should parse zero suppression', function() {
      var format = '%FSLAX34Y34*%'
      p.write(format)
      expect(p.format.zero).to.equal('L')
      p = pFactory()
      format = '%FSTAX34Y34*%'
      p.write(format)
      expect(p.format.zero).to.equal('T')
    })
    it('should warn trailing suppression is deprected', function(done) {
      p.once('warning', function(w) {
        expect(w.line).to.equal(0)
        expect(w.message).to.match(/trailing zero suppression/)
        done()
      })
      p.write('%FSTAX34Y34*%\n')
    })
    it('should parse places format', function() {
      var format = '%FSLAX34Y34*%'
      p.write(format)
      expect(p.format.places).to.eql([3, 4])
      p = pFactory()
      format = '%FSLAX77Y77*%'
      p.write(format)
      expect(p.format.places).to.eql([7, 7])
    })
    it('should not override user-set places or suppression', function() {
      var format = '%FSLAX34Y34*%'
      p.format.zero = 'T'
      p.format.places = [7, 7]
      p.write(format)
      expect(p.format.zero).to.equal('T')
      expect(p.format.places).to.eql([7, 7])
    })
    it('should set notation and epsilon', function(done) {
      var format1 = '%FSLAX34Y34*%\n'
      var format2 = '%FSLIX77Y77*%\n'
      // ensure it parses if suppression is missing
      var format3 = '%FSAX66Y66*%\n'
      var expected = [
        {type: 'set', line: 0, prop: 'nota', value: 'A'},
        {type: 'set', line: 0, prop: 'epsilon', value: 1.5 * Math.pow(10, -4)},
        {type: 'set', line: 1, prop: 'nota', value: 'I'},
        {type: 'set', line: 1, prop: 'epsilon', value: 1.5 * Math.pow(10, -7)},
        {type: 'set', line: 2, prop: 'nota', value: 'A'},
        {type: 'set', line: 2, prop: 'epsilon', value: 1.5 * Math.pow(10, -6)},
      ]
      expectResults(expected, done)
      // clear places format between writes to simulate new parsers
      p.write(format1)
      p.format.places = null
      p.write(format2)
      p.format.places = null
      p.write(format3)
    })
    it('should warn and set leading if suppression missing', function(done) {
      var format = '%FSAX34Y34*%\n'
      p.once('warning', function(w) {
        expect(w.line).to.equal(0)
        expect(w.message).to.match(/suppression missing/)
        expect(p.format.zero).to.equal('L')
        done()
      })
      p.write(format)
    })
    it('should be able to set backup notation with G90/1', function(done) {
      var expected = [
        {type: 'set', line: 0, prop: 'backupNota', value: 'A'},
        {type: 'set', line: 1, prop: 'backupNota', value: 'I'},
      ]
      expectResults(expected, done)
      p.write('G90*\n')
      p.write('G91*\n')
    })
    it('should warn but still set format if there are extra characters', function(done) {
      var format = '%FSLAN2X34Y34*%\n'
      p.once('warning', function(w) {
        expect(w.line).to.equal(0)
        expect(w.message).to.match(/unknown characters/)
        expect(p.format.zero).to.equal('L')
        expect(p.format.places).to.eql([3, 4])
        done()
      })
      p.write(format)
    })
  })
  describe('new level commands (SR/LP parameters)', function() {
    it('should parse a new level polarity', function(done) {
      var expected = [
        {type: 'level', line: 0, level: 'polarity', value: 'D'},
        {type: 'level', line: 1, level: 'polarity', value: 'C'},
      ]
      expectResults(expected, done)
      p.write('%LPD*%\n%LPC*%')
    })
    it('should parse a new step-repeat level', function(done) {
      var expected = [
        {
          type: 'level',
          line: 0,
          level: 'stepRep',
          value: {x: 1, y: 1, i: 0, j: 0},
        },
        {
          type: 'level',
          line: 1,
          level: 'stepRep',
          value: {x: 2, y: 3, i: 2, j: 3},
        },
        {
          type: 'level',
          line: 2,
          level: 'stepRep',
          value: {x: 1, y: 1, i: 0, j: 0},
        },
      ]
      expectResults(expected, done)
      p.format.places = [2, 2]
      p.write('%SRX1Y1I0J0*%\n')
      p.write('%SRX2Y3I2.0J3.0*%\n')
      p.write('%SR*%\n')
    })
  })
  describe('tool changes and definitions', function() {
    beforeEach(function() {
      p.format.zero = 'L'
      p.format.places = [2, 2]
    })
    it('should parse a tool change block', function(done) {
      var expected = [
        {type: 'set', line: 0, prop: 'tool', value: '10'},
        {type: 'set', line: 1, prop: 'tool', value: '11'},
        {type: 'set', line: 2, prop: 'tool', value: '12'},
      ]
      expectResults(expected, done)
      p.write('D10*\n')
      p.write('G54D11*\n')
      p.write('D00012*\n')
    })
    it('should handle standard circles', function(done) {
      var expectedTools = [
        {shape: 'circle', params: [1], hole: []},
        {shape: 'circle', params: [1], hole: [0.1]},
        {shape: 'circle', params: [1], hole: [0.2, 0.3]},
      ]
      var expected = [
        {type: 'tool', line: 0, code: '10', tool: expectedTools[0]},
        {type: 'tool', line: 1, code: '11', tool: expectedTools[1]},
        {type: 'tool', line: 2, code: '12', tool: expectedTools[2]},
      ]
      expectResults(expected, done)
      p.write('%ADD10C,1*%\n')
      p.write('%ADD11C,1X0.1*%\n')
      p.write('%ADD12C,1X0.2X0.3*%\n')
    })
    it('should handle standard rectangles/obrounds', function(done) {
      var expectedTools = [
        {shape: 'rect', params: [1, 2], hole: []},
        {shape: 'obround', params: [3, 4], hole: [0.1]},
        {shape: 'rect', params: [5, 6], hole: [0.2, 0.3]},
      ]
      var expected = [
        {type: 'tool', line: 0, code: '10', tool: expectedTools[0]},
        {type: 'tool', line: 1, code: '11', tool: expectedTools[1]},
        {type: 'tool', line: 2, code: '12', tool: expectedTools[2]},
      ]
      expectResults(expected, done)
      p.write('%ADD10R,1X2*%\n')
      p.write('%ADD11O,3.0X4.0X0.1*%\n')
      p.write('%ADD12R,5X6X0.2X0.3*%\n')
    })
    it('should handle standard polygons', function(done) {
      var expectedTools = [
        {shape: 'poly', params: [1, 5, 0], hole: []},
        {shape: 'poly', params: [2, 6, 45], hole: []},
        {shape: 'poly', params: [3, 7, 0], hole: [0.1]},
        {shape: 'poly', params: [4, 8, 0], hole: [0.2, 0.3]},
      ]
      var expected = [
        {type: 'tool', line: 0, code: '10', tool: expectedTools[0]},
        {type: 'tool', line: 1, code: '11', tool: expectedTools[1]},
        {type: 'tool', line: 2, code: '12', tool: expectedTools[2]},
        {type: 'tool', line: 3, code: '13', tool: expectedTools[3]},
      ]
      expectResults(expected, done)
      p.write('%ADD10P,1X5*%\n')
      p.write('%ADD11P,2X6X45*%\n')
      p.write('%ADD12P,3X7X0X0.1*%\n')
      p.write('%ADD13P,4X8X0X0.2X0.3*%\n')
    })
    it('should handle aperture macro tools', function(done) {
      var expectedTools = [
        {shape: 'CIRC', params: [1, 0.5], hole: []},
        {shape: 'RECT', params: [], hole: []},
      ]
      var expected = [
        {type: 'tool', line: 0, code: '10', tool: expectedTools[0]},
        {type: 'tool', line: 1, code: '11', tool: expectedTools[1]},
      ]
      expectResults(expected, done)
      p.write('%ADD10CIRC,1X0.5*%\n')
      p.write('%ADD11RECT*%\n')
    })
    it('should handle tool defs with leading zeros', function(done) {
      var expectedTools = [{shape: 'circle', params: [1], hole: []}]
      var expected = [
        {type: 'tool', line: 0, code: '10', tool: expectedTools[0]},
      ]
      expectResults(expected, done)
      p.write('%ADD0010C,1*%\n')
    })
    it('should handle tool defs with negative modifiers', function(done) {
      var expectedTools = [{shape: 'CIRC', params: [1, -0.5, 3], hole: []}]
      var expected = [
        {type: 'tool', line: 0, code: '10', tool: expectedTools[0]},
      ]
      expectResults(expected, done)
      p.write('%ADD10CIRC,1X-0.5X3*%\n')
    })
  })
  describe('aperture macros', function() {
    it('should parse the name of the macro properly', function(done) {
      var expected = [
        {type: 'macro', line: 0, name: 'NAME1', blocks: []},
        {type: 'macro', line: 1, name: 'CRAZY8', blocks: []},
        {type: 'macro', line: 2, name: 'NAME-1', blocks: []},
        {type: 'macro', line: 3, name: 'Name1.0', blocks: []},
        {type: 'macro', line: 4, name: '$Name1', blocks: []},
      ]
      expectResults(expected, done)
      p.write('%AMNAME1*%\n')
      p.write('%AMCRAZY8*%\n')
      p.write('%AMNAME-1*%\n')
      p.write('%AMName1.0*%\n')
      p.write('%AM$Name1*%\n')
    })
    it('should warn that hyphens in macro names are invalid', function(done) {
      p.once('warning', function(w) {
        expect(w.line).to.equal(0)
        expect(w.message).to.match(/hyphen/)
        done()
      })
      p.write('%AMNAME-1*%\n')
    })
    describe('primitive blocks', function() {
      var exp = 1
      it('should parse comments', function(done) {
        var expectedBlocks = [{type: 'comment'}]
        var expected = [
          {type: 'macro', line: 1, name: 'NAME1', blocks: expectedBlocks},
        ]
        expectResults(expected, done)
        p.write('%AMNAME1*\n')
        p.write('0 a comment*%\n')
      })
      it('should parse circle primitives', function(done) {
        var expectedBlocks = [
          {type: 'circle', exp: exp, dia: 5, cx: 1, cy: 2, rot: 0},
          {type: 'circle', exp: exp, dia: 3, cx: 4, cy: 5, rot: 20},
        ]
        var expected = [
          {type: 'macro', line: 2, name: 'CIRC1', blocks: expectedBlocks},
        ]
        expectResults(expected, done)
        p.write('%AMCIRC1*\n')
        p.write('1,1,5,1,2*\n')
        p.write('1,1,3,4,5,20*%\n')
      })
      it('should parse vector primitives', function(done) {
        var expectedBlocks = [
          {
            type: 'vect',
            exp: exp,
            width: 2,
            x1: 3,
            y1: 4,
            x2: 5,
            y2: 6,
            rot: 7,
          },
          {
            type: 'vect',
            exp: exp,
            width: 2,
            x1: 3,
            y1: 4,
            x2: 5,
            y2: 6,
            rot: 7,
          },
        ]
        var expected = [
          {type: 'macro', line: 2, name: 'VECT1', blocks: expectedBlocks},
        ]
        expectResults(expected, done)
        p.write('%AMVECT1*\n')
        p.write('2,1,2,3,4,5,6,7*\n')
        p.write('20,1,2,3,4,5,6,7*%\n')
      })
      it('should warn that primitive code 2 is deprecated', function(done) {
        p.once('warning', function(w) {
          expect(w.line).to.equal(1)
          expect(w.message).to.match(/vector.*deprecated/)
          done()
        })
        p.write('%AMVECT1*\n')
        p.write('2,1,2,3,4,5,6,7*%\n')
      })
      it('should parse rectangle primitives', function(done) {
        var expectedBlocks = [
          {type: 'rect', exp: exp, width: 2, height: 3, cx: 4, cy: 5, rot: 6},
        ]
        var expected = [
          {type: 'macro', line: 1, name: 'RECT1', blocks: expectedBlocks},
        ]
        expectResults(expected, done)
        p.write('%AMRECT1*\n')
        p.write('21,1,2,3,4,5,6*%\n')
      })
      it('should parse a lower left rectangle primitive', function(done) {
        var expectedBlocks = [
          {type: 'rectLL', exp: exp, width: 2, height: 3, x: 4, y: 5, rot: 6},
        ]
        var expected = [
          {type: 'macro', line: 1, name: 'RECTLL1', blocks: expectedBlocks},
        ]
        expectResults(expected, done)
        p.write('%AMRECTLL1*\n')
        p.write('22,1,2,3,4,5,6*%\n')
      })
      it('should warn that primitive code 22 is deprecated', function(done) {
        p.once('warning', function(w) {
          expect(w.line).to.equal(1)
          expect(w.message).to.match(/lower-left.*deprecated/)
          done()
        })
        p.write('%AMRECTLL1*\n')
        p.write('22,1,2,3,4,5,6*%\n')
      })
      it('should parse an outline polygon primitive', function(done) {
        var expectedBlocks = [
          {type: 'outline', exp: exp, points: [3, 4, 5, 6, 7, 8], rot: 9},
        ]
        var expected = [
          {type: 'macro', line: 1, name: 'OUT1', blocks: expectedBlocks},
        ]
        expectResults(expected, done)
        p.write('%AMOUT1*\n')
        p.write('4,1,2,3,4,5,6,7,8,9*%\n')
      })
      it('should parse a regular polygon primitive', function(done) {
        var expectedBlocks = [
          {type: 'poly', exp: exp, vertices: 3, cx: 4, cy: 5, dia: 6, rot: 7},
        ]
        var expected = [
          {type: 'macro', line: 1, name: 'POLY1', blocks: expectedBlocks},
        ]
        expectResults(expected, done)
        p.write('%AMPOLY1*\n')
        p.write('5,1,3,4,5,6,7*%\n')
      })
      it('should parse a moire primitive', function(done) {
        var expectedBlocks = [
          {
            type: 'moire',
            exp: 1,
            cx: 1,
            cy: 2,
            dia: 3,
            ringThx: 4,
            ringGap: 5,
            maxRings: 6,
            crossThx: 7,
            crossLen: 8,
            rot: 9,
          },
        ]
        var expected = [
          {type: 'macro', line: 1, name: 'MOIRE1', blocks: expectedBlocks},
        ]
        expectResults(expected, done)
        p.write('%AMMOIRE1*\n')
        p.write('6,1,2,3,4,5,6,7,8,9*%\n')
      })
      it('should parse a thermal primitive', function(done) {
        var expectedBlocks = [
          {
            type: 'thermal',
            exp: 1,
            cx: 1,
            cy: 2,
            outerDia: 3,
            innerDia: 4,
            gap: 5,
            rot: 6,
          },
        ]
        var expected = [
          {type: 'macro', line: 1, name: 'THERMAL1', blocks: expectedBlocks},
        ]
        expectResults(expected, done)
        p.write('%AMTHERMAL1*\n')
        p.write('7,1,2,3,4,5,6*%\n')
      })
      it('should warn if the primitive is unrecognized', function(done) {
        p.once('warning', function(w) {
          expect(w.line).to.equal(1)
          expect(w.message).to.match(/unrecognized primitive/)
          done()
        })
        p.write('%AMNOTAREALPRIMITIVE*\n')
        p.write('8,1,2,3,4,5,6,7*%\n')
      })
      it('should parse primitives with negative parameters', function(done) {
        var expectedBlocks = [
          {type: 'circle', exp: exp, dia: 5, cx: -1, cy: -2.5, rot: 0},
        ]
        var expected = [
          {type: 'macro', line: 1, name: 'CIRC1', blocks: expectedBlocks},
        ]
        expectResults(expected, done)
        p.write('%AMCIRC1*\n')
        p.write('1,1,5,-1,-2.5*%\n')
      })
      it('should ignore empty blocks', function(done) {
        var expectedBlocks = [
          {type: 'circle', exp: exp, dia: 5, cx: 1, cy: 2, rot: 0},
        ]
        var expected = [
          {type: 'macro', line: 2, name: 'CIRC1', blocks: expectedBlocks},
        ]
        expectResults(expected, done)
        p.write('%AMCIRC1*\n')
        p.write('1,1,5,1,2*\n')
        p.write('*%\n')
      })
    })
    describe('variable set blocks', function() {
      var mods
      beforeEach(function() {
        mods = {$1: 42}
      })
      var expectExprResults = function(expected, done) {
        p.once('data', function(res) {
          expect(res.type).to.equal('macro')
          expect(res.name).to.equal('MODS1')
          res.blocks.forEach(function(v) {
            var newMods = v.set(mods)
            expect(v.type).to.equal('variable')
            expect(newMods).to.eql(expected.shift())
          })
          done()
        })
      }
      it('should return function that takes / returns mods', function(done) {
        var expected = [{$1: 42, $2: 1}]
        expectExprResults(expected, done)
        p.write('%AMMODS1*\n')
        p.write('$2=1*%\n')
      })
      it('should parse addition', function(done) {
        var expected = [
          {$1: 42, $2: 3},
          {$1: 42, $2: 56},
        ]
        expectExprResults(expected, done)
        p.write('%AMMODS1*\n')
        p.write('$2=1+2*\n')
        p.write('$2=$1+14*%\n')
      })
      it('should parse subtraction', function(done) {
        var expected = [
          {$1: 42, $2: 3},
          {$1: 42, $2: 21},
        ]
        expectExprResults(expected, done)
        p.write('%AMMODS1*\n')
        p.write('$2=5-2*\n')
        p.write('$2=63-$1*%\n')
      })
      it('should parse multiplication with x and X', function(done) {
        var expected = [
          {$1: 42, $2: 21},
          {$1: 42, $2: 6},
        ]
        expectExprResults(expected, done)
        p.write('%AMMODS1*\n')
        p.write('$2=$1x0.5*\n')
        p.write('$2=2X3*%\n')
      })
      it('should warn that mult with X is incorrect', function(done) {
        p.once('warning', function(w) {
          expect(w.message).to.match(/multiplication/)
          done()
        })
        p.write('%AMMODS1*\n')
        p.write('$2=$1X1*%\n')
      })
      it('should parse division with /', function(done) {
        var expected = [
          {$1: 42, $2: 4},
          {$1: 42, $2: 21},
        ]
        expectExprResults(expected, done)
        p.write('%AMMODS1*\n')
        p.write('$2=12/3*\n')
        p.write('$2=$1/2*%\n')
      })
      it('should handle expressions with parentheses', function(done) {
        var expected = [{$1: 42, $2: 3}]
        expectExprResults(expected, done)
        p.write('%AMMODS1*\n')
        p.write('$2=($1-30)x(2/(3+12-7))*%\n')
      })
    })
    it('should parse params in circle primitives as expressions', function(done) {
      p.once('data', function(d) {
        expect(d.blocks[0].dia({$1: 4})).to.equal(5)
        done()
      })
      p.write('%AMCIRC1*\n')
      p.write('1,1,$1+1,1,2*%\n')
    })
    it('should parse params in outline primitives as expressions', function(done) {
      p.once('data', function(d) {
        var block = d.blocks[0]
        var points = block.points
        var rotation = block.rot
        var mods = {
          $1: 0,
          $2: 1,
          $3: 2,
          $4: 3,
          $5: 4,
          $6: 5,
          $7: 6,
          $8: 7,
          $9: 8,
        }
        points.forEach(function(p, i) {
          expect(p(mods)).to.equal(i)
        })
        expect(rotation(mods)).to.equal(8)
        done()
      })
      p.write('%AMOUT1*\n')
      p.write('4,1,3,$1,$2,$3,$4,$5,$6,$7,$8,$9**%\n')
    })
  })
  describe('operations', function() {
    beforeEach(function() {
      p.format.zero = 'L'
      p.format.places = [2, 3]
    })
    it('should parse an interpolation command', function(done) {
      var expected = [
        {
          type: 'op',
          line: 0,
          op: 'int',
          coord: {x: 0.1, y: 0.2, i: 0.3, j: 0.4},
        },
        {type: 'op', line: 1, op: 'int', coord: {x: 0.11, y: 0}},
        {type: 'op', line: 2, op: 'int', coord: {x: 0.22}},
        {type: 'op', line: 3, op: 'int', coord: {y: 0.33}},
        {type: 'op', line: 4, op: 'int', coord: {}},
      ]
      expectResults(expected, done)
      p.write('X100Y200I300J400D01*\n')
      p.write('X110Y0D01*\n')
      p.write('X220D1*\n')
      p.write('Y330D1*\n')
      p.write('D01*\n')
    })
    it('should parse a move command', function(done) {
      var expected = [
        {type: 'op', line: 0, op: 'move', coord: {x: 0.3, y: 0.001}},
        {type: 'op', line: 1, op: 'move', coord: {x: -0.1}},
        {type: 'op', line: 2, op: 'move', coord: {}},
      ]
      expectResults(expected, done)
      p.write('X300Y1D02*\n')
      p.write('X-100D2*\n')
      p.write('D02*\n')
    })
    it('should parse a flash command', function(done) {
      var expected = [
        {type: 'op', line: 0, op: 'flash', coord: {x: 0.3, y: 0.001}},
        {type: 'op', line: 1, op: 'flash', coord: {x: -0.1}},
        {type: 'op', line: 2, op: 'flash', coord: {}},
      ]
      expectResults(expected, done)
      p.write('X300Y1D03*\n')
      p.write('X-100D3*\n')
      p.write('D03*\n')
    })
    it('should send "last" operation if op code is missing', function(done) {
      var expected = [
        {type: 'op', line: 0, op: 'last', coord: {x: 0.3, y: 0.001}},
        {type: 'op', line: 1, op: 'last', coord: {x: -0.1}},
      ]
      expectResults(expected, done)
      p.write('X300Y1*\n')
      p.write('X-100*\n')
    })
    it('should interpolate with inline mode set', function(done) {
      var expected = [
        {type: 'set', line: 0, prop: 'mode', value: 'i'},
        {type: 'op', line: 0, op: 'int', coord: {x: 0.001, y: 0.001}},
        {type: 'set', line: 1, prop: 'mode', value: 'cw'},
        {type: 'op', line: 1, op: 'int', coord: {x: 0.001, y: 0.001}},
        {type: 'set', line: 2, prop: 'mode', value: 'ccw'},
        {type: 'op', line: 2, op: 'int', coord: {x: 0.001, y: 0.001}},
      ]
      expectResults(expected, done)
      p.write('G01X01Y01D01*\n')
      p.write('G02X01Y01D01*\n')
      p.write('G03X01Y01D01*\n')
    })
    it('should be strict about what counts as a operation', function(done) {
      p.once('readable', function() {
        throw new Error('should not have operated')
      })
      p.write('somestuffX01Y01*\n')
      p.write('X01Y01somestuff*\n')
      p.write('%TF.GerberVersion,J1*%\n')
      setTimeout(done, 10)
    })
  })
})