Browse Source

worked some more on tests.

Tomi Cvetic 6 năm trước cách đây
mục cha
commit
5fb81d5182
2 tập tin đã thay đổi với 230 bổ sung53 xóa
  1. 195 32
      backend/__tests__/src/interfaces.spec.js
  2. 35 21
      backend/src/interfaces.js

+ 195 - 32
backend/__tests__/src/interfaces.spec.js

@@ -11,13 +11,14 @@ fs.stat = jest.fn()
 const os = require('os')
 os.hostname = jest.fn().mockImplementation(() => 'fakehost')
 
-const mockSpawn = jest.fn()
 const mockSend = jest.fn()
-function PythonWorker (args) {
-  this.spawn = mockSpawn
-  this.send = mockSend
+const mockSpawn = jest.fn()
+const PythonWorker = function () {
+  return { send: mockSend, spawn: mockSpawn }
 }
+
 const interfaces = rewire('../../src/interfaces')
+const { resolvers, typeDefs } = interfaces
 interfaces.__set__('PythonWorker', PythonWorker)
 
 /**
@@ -32,8 +33,6 @@ function AsyncMock ({ result, error }) {
   })
 }
 
-const { resolvers, typeDefs, __test__ } = interfaces
-
 describe('interfaces module', () => {
   it('exports resolvers and typeDefs', () => {
     expect(resolvers).toBeDefined()
@@ -42,23 +41,73 @@ describe('interfaces module', () => {
     expect(resolvers).toHaveProperty('Mutation')
   })
 
-  it('finds workers', async () => {
-    // Check that all possible return values of fs.readdir are correctly handled.
+  it('has initialized a state', () => {
+    const modState = interfaces.__get__('state')
+    expect(modState).toMatchObject({
+      interfaces: expect.arrayContaining([]),
+      ports: expect.arrayContaining([]),
+      connections: expect.arrayContaining([])
+    })
+  })
+
+  it('handles state correctly', async () => {
+    const setState = interfaces.__get__('setState')
+    const revert = interfaces.__set__({
+      state: { test1: 'test1' }
+    })
+
+    // 1. Add a new property
+    await expect(setState({ test2: 'test2' })).resolves
+    expect(interfaces.__get__('state')).toEqual({
+      test1: 'test1',
+      test2: 'test2'
+    })
+
+    // 2. Update an existing property
+    await expect(setState({ test2: 'TEST2' })).resolves
+    expect(interfaces.__get__('state')).toEqual({
+      test1: 'test1',
+      test2: 'TEST2'
+    })
+
+    // 3. Let a function change the state
+    const mockStateChange = jest.fn().mockReturnValueOnce({
+      test: 'from function'
+    })
+    await expect(setState(mockStateChange)).resolves
+    expect(mockStateChange).toHaveBeenCalledWith({
+      test1: 'test1',
+      test2: 'TEST2'
+    })
+    expect(interfaces.__get__('state')).toEqual({
+      test: 'from function'
+    })
+    revert()
+  })
+
+  it('finds workers in a directory', async () => {
     const findWorkers = interfaces.__get__('findWorkers')
+    fs.readdir.mockReset()
+
+    // Check that all possible return values of fs.readdir are correctly handled.
 
     // 1. Check use case
     const files = ['test1_worker.py', 'test2_worker.py']
     fs.readdir.mockReturnValueOnce(files)
     await expect(findWorkers('python_workers')).resolves.toEqual(files)
+    expect(fs.readdir).toHaveBeenCalledWith('python_workers')
 
     // 2. Check empty directory
     fs.readdir.mockReturnValueOnce([])
     await expect(findWorkers('/')).resolves.toEqual([])
+    expect(fs.readdir).toHaveBeenCalledWith('/')
 
     // 3. Check argument error
     await expect(findWorkers()).rejects.toThrow(
       'Directory argument must be a string.'
     )
+    fs.readdir.mockReset()
+    expect(fs.readdir).not.toHaveBeenCalled()
 
     // 4. Check non-existing path
     fs.readdir.mockImplementation(() => {
@@ -69,42 +118,56 @@ describe('interfaces module', () => {
     await expect(findWorkers('/DOESNTEXIST')).rejects.toThrow(
       `ENOENT: no such file or directory, scandir '/DOESNTEXIST'`
     )
+    expect(fs.readdir).toHaveBeenCalledWith('/DOESNTEXIST')
+    fs.readdir.mockReset()
   })
 
-  it('finds options', async () => {
+  it('gets options from an interface', async () => {
     const getOptions = interfaces.__get__('getOptions')
-    const options = [{ option1: 'value1' }]
-    const workerProcess = new PythonWorker()
+    const options = [{ option1: 'value1', option2: 'value2' }]
+    const mockWorker = new PythonWorker()
 
     // 1. Check the use case
-    mockSend.mockReturnValueOnce(AsyncMock({ result: { data: options } }))
-    await expect(getOptions(workerProcess)).resolves.toEqual(options)
-    expect(mockSend).toHaveBeenCalledWith({ type: 'options' })
+    mockWorker.send.mockReturnValueOnce(
+      AsyncMock({ result: { data: options } })
+    )
+    await expect(getOptions(mockWorker)).resolves.toEqual(options)
+    expect(mockWorker.send).toHaveBeenCalledWith({ type: 'options' })
 
     // 2. Check empty option case
-    mockSend.mockReturnValueOnce(AsyncMock({ result: { data: [] } }))
-    await expect(getOptions(workerProcess)).resolves.toEqual([])
-    expect(mockSend).toHaveBeenCalledWith({ type: 'options' })
+    mockWorker.send.mockReturnValueOnce(AsyncMock({ result: { data: [] } }))
+    await expect(getOptions(mockWorker)).resolves.toEqual([])
+    expect(mockWorker.send).toHaveBeenCalledWith({ type: 'options' })
 
     // 3. Check error case
-    mockSend.mockReturnValueOnce(AsyncMock({ result: { error: 'Timeout' } }))
-    await expect(getOptions(workerProcess)).rejects.toThrow('Timeout')
+    mockWorker.send.mockReturnValueOnce(
+      AsyncMock({ result: { error: 'Timeout' } })
+    )
+    await expect(getOptions(mockWorker)).rejects.toThrow('Timeout')
 
-    // 4. Throw error if workerProcess doesn't have a "send" property.
-    expect(getOptions()).rejects.toThrow(
+    // 4. Throw error if mockWorker doesn't have a "send" property.
+    await expect(getOptions()).rejects.toThrow(
       'workerProcess not configured properly.'
     )
   })
 
-  it('creates an interface', async () => {
+  it('creates an interface from a worker script', async () => {
+    fs.stat.mockReset()
+    mockSend.mockReset()
+    mockSpawn.mockReset()
+    const mockFile = {
+      name: 'test1_worker.py',
+      path: 'python_workers/test1_worker.py',
+      stat: { mtime: now }
+    }
     const createInterface = interfaces.__get__('createInterface')
 
     // 1. Check the use case
     mockSpawn.mockReturnValueOnce(AsyncMock({ result: { data: 'ready!' } }))
     mockSend.mockReturnValueOnce(AsyncMock({ result: { data: [] } }))
-    fs.stat.mockReturnValueOnce({ mtime: now, size: 1234 })
+    fs.stat.mockReturnValueOnce(mockFile.stat)
     await expect(
-      createInterface('python_workers', 'test1_worker.py', [])
+      createInterface('python_workers', mockFile.name, [])
     ).resolves.toMatchObject({
       interfaceName: 'test1',
       options: [],
@@ -113,13 +176,14 @@ describe('interfaces module', () => {
         spawn: expect.anything()
       }),
       workerScript: expect.objectContaining({
-        path: 'python_workers/test1_worker.py',
-        mtime: now,
+        path: mockFile.path,
+        ...mockFile.stat,
         updated: null
       })
     })
     expect(mockSpawn).toHaveBeenCalled()
     expect(mockSend).toHaveBeenCalledWith({ type: 'options' })
+    expect(fs.stat).toHaveBeenCalledWith(mockFile.path)
 
     // 2. Check that the update property is set for existing interfaces
 
@@ -143,15 +207,14 @@ describe('interfaces module', () => {
     )
   })
 
-  it('finds interfaces', async () => {
-    const findInterfaces = interfaces.__get__('findInterfaces')
+  it('generates the interface list', async () => {
+    const generateInterfaceList = interfaces.__get__('generateInterfaceList')
 
     const mockWorkerList = ['test1_worker.py', 'test2_worker.py']
     const mockInterfaceList = [
       { interfaceName: 'test1' },
       { interfaceName: 'test2' }
     ]
-    const moduleWorkerdir = interfaces.__get__('WORKER_DIR')
     const mockFindWorkers = jest.fn()
     const mockCreateInterface = jest.fn()
     const revert = interfaces.__set__({
@@ -163,13 +226,113 @@ describe('interfaces module', () => {
     mockFindWorkers.mockReturnValueOnce(mockWorkerList)
     mockCreateInterface.mockReturnValueOnce(mockInterfaceList[0])
     mockCreateInterface.mockReturnValueOnce(mockInterfaceList[1])
-    await expect(findInterfaces()).resolves.toEqual(mockInterfaceList)
-    expect(mockFindWorkers).toHaveBeenCalledWith(moduleWorkerdir)
+    await expect(generateInterfaceList([])).resolves.toEqual(mockInterfaceList)
+    expect(mockFindWorkers).toHaveBeenCalledWith(process.env.WORKER_DIR)
     expect(mockCreateInterface).toHaveBeenCalledWith(
-      moduleWorkerdir,
+      process.env.WORKER_DIR,
       'test1_worker.py',
       expect.anything()
     )
+    expect(mockCreateInterface).toHaveBeenCalledWith(
+      process.env.WORKER_DIR,
+      'test2_worker.py',
+      expect.anything()
+    )
     revert()
   })
+
+  it('resolves interfaces for GraphQL queries', async () => {
+    // 1. calls to generate the interface list.
+    const mockPrevInterfaceList = [
+      { id: 1, interfaceName: 'test1', active: true },
+      { id: 2, interfaceName: 'test2', active: true },
+      { id: 3, interfaceName: 'test3', active: true }
+    ]
+    const mockNextInterfaceList = [
+      { id: 2, interfaceName: 'test2', active: true },
+      { id: 3, interfaceName: 'test3', active: true },
+      { id: 4, interfaceName: 'test4', active: true }
+    ]
+    const mockExpInterfaceList = [
+      { id: 1, interfaceName: 'test1', active: false },
+      { id: 2, interfaceName: 'test2', active: true },
+      { id: 3, interfaceName: 'test3', active: true },
+      { id: 4, interfaceName: 'test4', active: true }
+    ]
+    const mockGenerateInterfaceList = jest.fn()
+    const revert = interfaces.__set__({
+      state: { interfaces: mockPrevInterfaceList },
+      generateInterfaceList: mockGenerateInterfaceList
+    })
+
+    mockGenerateInterfaceList.mockReturnValueOnce(mockNextInterfaceList)
+
+    // 2. returns the updated interface list
+    await expect(resolvers.Query.interfaces()).resolves.toEqual(
+      mockExpInterfaceList
+    )
+    revert()
+  })
+
+  it('resolves a single interface', async () => {
+    await expect(resolvers.Query.interface()).rejects.toThrow(
+      "Cannot destructure property `id` of 'undefined' or 'null'."
+    )
+
+    const mockInterface = { id: 3, interfaceName: 'test3' }
+    const revert = interfaces.__set__({
+      state: { interfaces: [mockInterface] }
+    })
+    await expect(resolvers.Query.interface(null, { id: 3 })).resolves.toEqual(
+      mockInterface
+    )
+    revert()
+  })
+
+  it('creates interface ports', async () => {
+    const createInterfacePorts = interfaces.__get__('createInterfacePorts')
+    const iface = {
+      id: 3,
+      interfaceName: 'test1',
+      workerProcess: new PythonWorker()
+    }
+    const ports = [{ id: 1, device: 'COM1' }, { id: 2, device: 'COM2' }]
+
+    mockSend.mockReturnValueOnce(
+      AsyncMock({
+        result: {
+          data: ports
+        }
+      })
+    )
+    await expect(createInterfacePorts(iface)).resolves.toEqual([
+      expect.objectContaining(ports[0]),
+      expect.objectContaining(ports[1])
+    ])
+  })
+
+  it('generates the port list', async () => {
+    const generatePortList = interfaces.__get__('generatePortList')
+    const revert = interfaces.__set__({
+      generateInterfaceList: jest
+        .fn()
+        .mockReturnValueOnce([
+          { id: 1, interfaceName: 'test1' },
+          { id: 2, interfaceName: 'test2' }
+        ]),
+      createInterfacePorts: jest
+        .fn()
+        .mockReturnValueOnce([1, 2])
+        .mockReturnValueOnce([3, 4])
+    })
+
+    // const mockPortList = [{ id: 1, device: 'COM1' }, { id: 2, device: 'COM2' }]
+
+    await expect(generatePortList([])).resolves.toEqual([1, 2, 3, 4])
+    revert()
+  })
+
+  it('resolves ports', async () => {
+    expect(1).toEqual(2)
+  })
 })

+ 35 - 21
backend/src/interfaces.js

@@ -1,6 +1,5 @@
 /**
  * @fileOverview INTERFACE BACKEND
- * @author <a href="tcve@u-blox.com">Tomi Cvetic</a>
  *
  * Communication with workers (for now only Python)
  * The workers are implemented as REPL clients that
@@ -23,9 +22,6 @@ let PythonWorker = require('./pythonWorker')
 const readdir = promisify(fs.readdir)
 const stat = promisify(fs.stat)
 
-/** @type {string} Directory with worker files */
-const WORKER_DIR = `${__dirname}/python_workers`
-
 /** @type {string} Hostname is used to identify ports across machines */
 const HOST = os.hostname()
 
@@ -38,7 +34,7 @@ const HOST = os.hostname()
  * @property {object} lastScan - Time of the last scan (per property)
  */
 /** @type {State} Local state of the modules. */
-const state = {
+let state = {
   interfaces: [], // @type {array}
   ports: [],
   connections: [],
@@ -49,7 +45,16 @@ const state = {
   }
 }
 
-async function setState (content) {}
+async function setState (arg) {
+  if (typeof arg === 'function') {
+    state = await arg(state)
+  } else {
+    state = {
+      ...state,
+      ...arg
+    }
+  }
+}
 
 /** @type {string} GraphQL types for the interface module */
 const typeDefs = `
@@ -218,26 +223,27 @@ async function createInterface (directory, workerFile, interfaces) {
 /**
  * Generate interface list
  */
-async function generateInterfaceList () {
+async function generateInterfaceList (interfaces) {
   // 1. Don't check more frequently than once per second.
   // if (state.lastScan.workers + 1000 > Date.now()) return state.interfaces
   // state.lastScan.workers = Date.now()
 
   // 2. Find all files in ./python_workers that end in _worker.py
-  const workerFiles = await findWorkers(WORKER_DIR)
+  const workerFiles = await findWorkers(process.env.WORKER_DIR)
 
   // 3. For every worker script
   const workerPromises = workerFiles.map(workerFile =>
-    createInterface(WORKER_DIR, workerFile, state.interfaces)
+    createInterface(process.env.WORKER_DIR, workerFile, interfaces)
   )
   return Promise.all(workerPromises)
 }
 
 async function interfaces (parent, args, context, info) {
-  const incomingInterfaces = await generateInterfaceList()
+  const incomingInterfaces = await generateInterfaceList(state.interfaces)
   const newInterfaces = incomingInterfaces.filter(
     inIface => !state.interfaces.find(stIface => stIface.id === inIface.id)
   )
+
   state.interfaces.forEach(stIface => {
     if (!incomingInterfaces.find(inIface => inIface.id === stIface.id)) {
       stIface.active = false
@@ -248,10 +254,7 @@ async function interfaces (parent, args, context, info) {
 }
 
 async function iface (parent, { id }, context, info) {
-  await generateInterfaceList()
-  const iface = state.interfaces.find(
-    iface => iface.id === id || iface.interfaceName === interfaceName
-  )
+  const iface = state.interfaces.find(iface => iface.id === id)
   if (!iface) {
     throw new Error(`Worker id=${id} not found`)
   }
@@ -288,7 +291,7 @@ function serializeWorkerProcess (parent, args, context, info) {
   }
 }
 
-async function createInterfacePorts (iface, ports) {
+async function createInterfacePorts (iface) {
   // a) Ask interface for ports.
   const { data, error } = await iface.workerProcess.send({
     type: 'ports'
@@ -302,6 +305,7 @@ async function createInterfacePorts (iface, ports) {
       interface: iface,
       interfaceName: iface.interfaceName,
       host: HOST,
+      active: true,
       ...port
     }
   })
@@ -322,21 +326,31 @@ async function createInterfacePorts (iface, ports) {
  */
 async function generatePortList (ports) {
   // 1. Make sure, workers are updated.
-  const incomingPorts = await generateInterfaceList()
+  const incomingInterfaces = await generateInterfaceList()
 
   // 2. Don't check more frequently than once per second.
   // if (state.lastScan.ports + 1000 > Date.now()) return null
   // state.lastScan.ports = Date.now()
 
   // 3. Loop through all workers to find available ports.
-  const portsPromises = interfaces.map(async iface => {
-    createInterfacePorts(iface, state.ports)
-  })
-  await Promise.all(portsPromises)
+  const portsPromises = incomingInterfaces.map(async iface =>
+    createInterfacePorts(iface, ports)
+  )
+  const portList = await Promise.all(portsPromises)
+  return portList.flat()
 }
 
 async function ports (parent, args, context, info) {
-  await generatePortList()
+  const incomingPorts = await generatePortList(state.ports)
+  const newPorts = incomingPorts.filter(
+    inPort => !state.ports.find(stPort => stPort.id === inPort.id)
+  )
+  state.ports.forEach(stPort => {
+    if (!incomingPorts.find(inPort => inPort.id === stPort.id)) {
+      stPort.active = false
+    }
+  })
+  state.ports = [...state.ports, ...newPorts]
 
   if (parent) {
     return state.ports.filter(