Przeglądaj źródła

refactoring interfaces.

Tomi Cvetic 6 lat temu
rodzic
commit
ee266529ba

+ 52 - 43
backend/__tests__/src/interfaces.spec.js

@@ -1,3 +1,4 @@
+const rewire = require('rewire')
 const now = new Date('1980-03-18T04:15:00')
 
 const util = require('util')
@@ -10,6 +11,15 @@ 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 interfaces = rewire('../../src/interfaces')
+interfaces.__set__('PythonWorker', PythonWorker)
+
 /**
  * Asynchronous mock function that returns a promise witch
  * resolves or rejects based on the arguments given.
@@ -22,26 +32,7 @@ function AsyncMock ({ result, error }) {
   })
 }
 
-const mockSpawn = jest.fn()
-const mockSend = jest.fn()
-
-const PythonWorker = require('../../src/pythonWorker')
-// Mock constructor for the PythonWorker
-jest.mock('../../src/pythonWorker', () => {
-  return function (args) {
-    this.spawn = mockSpawn
-    this.send = mockSend
-  }
-})
-
-const md5 = require('md5')
-const interfaces = require('../../src/interfaces')
 const { resolvers, typeDefs, __test__ } = interfaces
-// const child_process = require('child_process')
-// child_process.addCRPair({
-//   input: '{"type":"options"}\n',
-//   output: "[{'option1':'value1'},{'option2':'value2'}]"
-// })
 
 describe('interfaces module', () => {
   it('exports resolvers and typeDefs', () => {
@@ -53,7 +44,7 @@ describe('interfaces module', () => {
 
   it('finds workers', async () => {
     // Check that all possible return values of fs.readdir are correctly handled.
-    const { findWorkers } = __test__
+    const findWorkers = interfaces.__get__('findWorkers')
 
     // 1. Check use case
     const files = ['test1_worker.py', 'test2_worker.py']
@@ -81,7 +72,7 @@ describe('interfaces module', () => {
   })
 
   it('finds options', async () => {
-    const { getOptions } = __test__
+    const getOptions = interfaces.__get__('getOptions')
     const options = [{ option1: 'value1' }]
     const workerProcess = new PythonWorker()
 
@@ -106,10 +97,7 @@ describe('interfaces module', () => {
   })
 
   it('creates an interface', async () => {
-    const { createInterface } = __test__
-    const testInterfaces = []
-    mockSpawn.mockReset()
-    mockSend.mockReset()
+    const createInterface = interfaces.__get__('createInterface')
 
     // 1. Check the use case
     mockSpawn.mockReturnValueOnce(AsyncMock({ result: { data: 'ready!' } }))
@@ -117,17 +105,19 @@ describe('interfaces module', () => {
     fs.stat.mockReturnValueOnce({ mtime: now, size: 1234 })
     await expect(
       createInterface('python_workers', 'test1_worker.py', [])
-    ).resolves.toContainEqual(
-      expect.objectContaining({
-        id: md5(`test1python_workers/test1_worker.py1980-03-18T03:15:00.000Z`),
-        interfaceName: 'test1',
-        options: [],
-        workerProcess: expect.objectContaining({
-          send: expect.anything(),
-          spawn: expect.anything()
-        })
+    ).resolves.toMatchObject({
+      interfaceName: 'test1',
+      options: [],
+      workerProcess: expect.objectContaining({
+        send: expect.anything(),
+        spawn: expect.anything()
+      }),
+      workerScript: expect.objectContaining({
+        path: 'python_workers/test1_worker.py',
+        mtime: now,
+        updated: null
       })
-    )
+    })
     expect(mockSpawn).toHaveBeenCalled()
     expect(mockSend).toHaveBeenCalledWith({ type: 'options' })
 
@@ -154,13 +144,32 @@ describe('interfaces module', () => {
   })
 
   it('finds interfaces', async () => {
-    const { findInterfaces } = __test__
-    interfaces.findWorkers = jest.fn()
-    interfaces.findWorkers.mockReturnValueOnce([])
-    interfaces.createInterface = jest.fn()
-    interfaces.createInterface.mockReturnValueOnce([])
-    const resp = await findInterfaces()
-    expect(resp).toBe([])
+    const findInterfaces = interfaces.__get__('findInterfaces')
+
+    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__({
+      findWorkers: mockFindWorkers,
+      createInterface: mockCreateInterface
+    })
+
+    // 1. Check use case
+    mockFindWorkers.mockReturnValueOnce(mockWorkerList)
+    mockCreateInterface.mockReturnValueOnce(mockInterfaceList[0])
+    mockCreateInterface.mockReturnValueOnce(mockInterfaceList[1])
+    await expect(findInterfaces()).resolves.toEqual(mockInterfaceList)
+    expect(mockFindWorkers).toHaveBeenCalledWith(moduleWorkerdir)
+    expect(mockCreateInterface).toHaveBeenCalledWith(
+      moduleWorkerdir,
+      'test1_worker.py',
+      expect.anything()
+    )
+    revert()
   })
-  const { Query, Mutation, Interface, Connection } = resolvers
 })

+ 200 - 0
backend/package-lock.json

@@ -1761,6 +1761,12 @@
         "@types/babel__traverse": "^7.0.6"
       }
     },
+    "babel-plugin-rewire": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/babel-plugin-rewire/-/babel-plugin-rewire-1.2.0.tgz",
+      "integrity": "sha512-JBZxczHw3tScS+djy6JPLMjblchGhLI89ep15H3SyjujIzlxo5nr6Yjo7AXotdeVczeBmWs0tF8PgJWDdgzAkQ==",
+      "dev": true
+    },
     "babel-preset-jest": {
       "version": "24.6.0",
       "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.6.0.tgz",
@@ -9347,6 +9353,187 @@
       "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
       "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
     },
+    "rewire": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/rewire/-/rewire-4.0.1.tgz",
+      "integrity": "sha512-+7RQ/BYwTieHVXetpKhT11UbfF6v1kGhKFrtZN7UDL2PybMsSt/rpLWeEUGF5Ndsl1D5BxiCB14VDJyoX+noYw==",
+      "requires": {
+        "eslint": "^4.19.1"
+      },
+      "dependencies": {
+        "acorn": {
+          "version": "5.7.3",
+          "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
+          "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw=="
+        },
+        "acorn-jsx": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
+          "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=",
+          "requires": {
+            "acorn": "^3.0.4"
+          },
+          "dependencies": {
+            "acorn": {
+              "version": "3.3.0",
+              "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
+              "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo="
+            }
+          }
+        },
+        "ajv": {
+          "version": "5.5.2",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+          "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
+          "requires": {
+            "co": "^4.6.0",
+            "fast-deep-equal": "^1.0.0",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.3.0"
+          }
+        },
+        "ajv-keywords": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz",
+          "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I="
+        },
+        "chardet": {
+          "version": "0.4.2",
+          "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
+          "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I="
+        },
+        "debug": {
+          "version": "3.2.6",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+          "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+          "requires": {
+            "ms": "^2.1.1"
+          }
+        },
+        "eslint": {
+          "version": "4.19.1",
+          "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz",
+          "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==",
+          "requires": {
+            "ajv": "^5.3.0",
+            "babel-code-frame": "^6.22.0",
+            "chalk": "^2.1.0",
+            "concat-stream": "^1.6.0",
+            "cross-spawn": "^5.1.0",
+            "debug": "^3.1.0",
+            "doctrine": "^2.1.0",
+            "eslint-scope": "^3.7.1",
+            "eslint-visitor-keys": "^1.0.0",
+            "espree": "^3.5.4",
+            "esquery": "^1.0.0",
+            "esutils": "^2.0.2",
+            "file-entry-cache": "^2.0.0",
+            "functional-red-black-tree": "^1.0.1",
+            "glob": "^7.1.2",
+            "globals": "^11.0.1",
+            "ignore": "^3.3.3",
+            "imurmurhash": "^0.1.4",
+            "inquirer": "^3.0.6",
+            "is-resolvable": "^1.0.0",
+            "js-yaml": "^3.9.1",
+            "json-stable-stringify-without-jsonify": "^1.0.1",
+            "levn": "^0.3.0",
+            "lodash": "^4.17.4",
+            "minimatch": "^3.0.2",
+            "mkdirp": "^0.5.1",
+            "natural-compare": "^1.4.0",
+            "optionator": "^0.8.2",
+            "path-is-inside": "^1.0.2",
+            "pluralize": "^7.0.0",
+            "progress": "^2.0.0",
+            "regexpp": "^1.0.1",
+            "require-uncached": "^1.0.3",
+            "semver": "^5.3.0",
+            "strip-ansi": "^4.0.0",
+            "strip-json-comments": "~2.0.1",
+            "table": "4.0.2",
+            "text-table": "~0.2.0"
+          }
+        },
+        "eslint-scope": {
+          "version": "3.7.3",
+          "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz",
+          "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==",
+          "requires": {
+            "esrecurse": "^4.1.0",
+            "estraverse": "^4.1.1"
+          }
+        },
+        "espree": {
+          "version": "3.5.4",
+          "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz",
+          "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==",
+          "requires": {
+            "acorn": "^5.5.0",
+            "acorn-jsx": "^3.0.0"
+          }
+        },
+        "external-editor": {
+          "version": "2.2.0",
+          "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
+          "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==",
+          "requires": {
+            "chardet": "^0.4.0",
+            "iconv-lite": "^0.4.17",
+            "tmp": "^0.0.33"
+          }
+        },
+        "fast-deep-equal": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+          "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ="
+        },
+        "inquirer": {
+          "version": "3.3.0",
+          "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz",
+          "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==",
+          "requires": {
+            "ansi-escapes": "^3.0.0",
+            "chalk": "^2.0.0",
+            "cli-cursor": "^2.1.0",
+            "cli-width": "^2.0.0",
+            "external-editor": "^2.0.4",
+            "figures": "^2.0.0",
+            "lodash": "^4.3.0",
+            "mute-stream": "0.0.7",
+            "run-async": "^2.2.0",
+            "rx-lite": "^4.0.8",
+            "rx-lite-aggregates": "^4.0.8",
+            "string-width": "^2.1.0",
+            "strip-ansi": "^4.0.0",
+            "through": "^2.3.6"
+          }
+        },
+        "json-schema-traverse": {
+          "version": "0.3.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
+          "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A="
+        },
+        "regexpp": {
+          "version": "1.1.0",
+          "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz",
+          "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw=="
+        },
+        "table": {
+          "version": "4.0.2",
+          "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz",
+          "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==",
+          "requires": {
+            "ajv": "^5.2.3",
+            "ajv-keywords": "^2.1.0",
+            "chalk": "^2.1.0",
+            "lodash": "^4.17.4",
+            "slice-ansi": "1.0.0",
+            "string-width": "^2.1.1"
+          }
+        }
+      }
+    },
     "rimraf": {
       "version": "2.6.3",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
@@ -9394,6 +9581,19 @@
         }
       }
     },
+    "rx-lite": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz",
+      "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ="
+    },
+    "rx-lite-aggregates": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz",
+      "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=",
+      "requires": {
+        "rx-lite": "*"
+      }
+    },
     "rxjs": {
       "version": "6.4.0",
       "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",

+ 6 - 3
backend/package.json

@@ -21,22 +21,25 @@
     "prisma-binding": "^2.3.10",
     "prisma-client-lib": "^1.30.1",
     "python-shell": "^1.0.7",
+    "rewire": "^4.0.1",
     "semaphore-async-await": "^1.5.1",
     "shortid": "^2.2.14",
     "standard": "^12.0.1"
   },
   "devDependencies": {
     "babel-jest": "^24.8.0",
+    "babel-plugin-rewire": "^1.2.0",
+    "graphql-cli": "^3.0.11",
     "jest": "^24.8.0",
     "jest-transform-graphql": "^2.1.0",
-    "graphql-cli": "^3.0.11",
     "nodemon": "^1.18.11"
   },
   "scripts": {
     "start": "nodemon -e js,graphql -x node index.js",
     "dev": "nodemon -e js,graphql -x node --inspect index.js",
     "test": "NODE_ENV=test jest --watch",
-    "deploy": "prisma deploy"
+    "deploy": "prisma deploy",
+    "pretest": "sed -e 's/:__/:_/' -i node_modules/babel-plugin-jest-hoist/build/index.js"
   },
   "jest": {
     "testPathIgnorePatterns": [
@@ -51,4 +54,4 @@
   "keywords": [],
   "author": "",
   "license": "ISC"
-}
+}

+ 91 - 73
backend/src/interfaces.js

@@ -18,7 +18,7 @@ const fs = require('fs')
 const os = require('os')
 const md5 = require('md5')
 const { promisify } = require('util')
-const PythonWorker = require('./pythonWorker')
+let PythonWorker = require('./pythonWorker')
 
 const readdir = promisify(fs.readdir)
 const stat = promisify(fs.stat)
@@ -127,6 +127,7 @@ const typeDefs = `
  * Find worker files in a directory based on filename convention
  * (ending in _worker.py).
  * @param {string} directory - Python worker directory
+ * @return {string[]} Array with found worker scripts
  */
 async function findWorkers (directory) {
   // Only accept strings
@@ -157,17 +158,28 @@ async function getOptions (workerProcess) {
  * Takes a Python worker script and generates an interface for it.
  * You need to call the spawn() function to launch the Python worker
  * process.
+ * @typedef {object} WorkerScript
+ * @property {string} path - Path of the worker script
+ * @property {number} mtime - Last modification time
+ * @property {number} updated - Last modification time, if changed after spawn
+ *
+ * @typedef {object} Interface
+ * @property {string} id - ID of the interface
+ * @property {string} interfaceName - Name of the interface
+ * @property {WorkerScript} workerScript - Information about the worker script
+ * @property {boolean} active - Is the worker still available
+ *
  * @param {string} directory - Python worker directory
  * @param {string} workerFile - Python worker file
- * @param {Object} state - State with already connected interfaces
- * @return {Object} Updated state
+ * @param {Interface[]} interfaces - State with already connected interfaces
+ * @return {Object} created interface
  */
 async function createInterface (directory, workerFile, interfaces) {
   // Assert that arguments are strings.
-  if (!(typeof directory === 'string')) {
+  if (!(typeof directory === 'string' || directory instanceof String)) {
     throw new Error('Directory argument must be a string.')
   }
-  if (!(typeof workerFile === 'string')) {
+  if (!(typeof workerFile === 'string' || workerFile instanceof String)) {
     throw new Error('workerFile argument must be a string.')
   }
   const interfaceName = workerFile.replace(/_worker\.py/, '')
@@ -176,45 +188,39 @@ async function createInterface (directory, workerFile, interfaces) {
   const { mtime } = await stat(path)
   const workerScript = { path, mtime, updated: null }
   // 2. Check if the interface already exists
-  const ifaceIndex = interfaces.findIndex(
-    iface => iface.interfaceName === interfaceName
-  )
-  if (ifaceIndex >= 0) {
+  const iface = interfaces.find(iface => iface.interfaceName === interfaceName)
+  if (iface) {
     // b. If it was modified, save the modification time.
-    if (interfaces[ifaceIndex].workerScript.mtime < mtime) {
-      const iface = interfaces[ifaceIndex]
-      const newWorkerScript = { ...iface.workerScript, updated: mtime }
-      return [
-        ...interfaces.slice(0, ifaceIndex),
-        {
-          ...iface,
-          workerScript: newWorkerScript
-        },
-        ...interfaces.slice(ifaceIndex + 1)
-      ]
-    } else {
-      return interfaces
+    if (iface.workerScript.mtime < mtime) {
+      iface.workerScript.update = mtime
     }
+    return iface
   }
   // c. Spawn a new worker connection.
   const workerProcess = new PythonWorker(workerScript)
   const { data, error } = await workerProcess.spawn()
   if (error) throw new Error(error)
-  const id = md5(`${interfaceName}${workerScript.path}${mtime.toISOString()}`)
+  const id = md5(
+    `${HOST}${interfaceName}${workerScript.path}${mtime.toISOString()}`
+  )
   const options = await getOptions(workerProcess)
-  // d. Save the worker in the state.
-  return [
-    ...interfaces,
-    { id, interfaceName, workerScript, workerProcess, options }
-  ]
+  // d. Return the interface.
+  return {
+    id,
+    interfaceName,
+    workerScript,
+    workerProcess,
+    options,
+    active: true
+  }
 }
 
 /**
- * INTERFACE SECTION
+ * Generate interface list
  */
-async function findInterfaces () {
+async function generateInterfaceList () {
   // 1. Don't check more frequently than once per second.
-  // if (state.lastScan.workers + 1000 > Date.now()) return null
+  // 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
@@ -222,23 +228,32 @@ async function findInterfaces () {
 
   // 3. For every worker script
   const workerPromises = workerFiles.map(workerFile =>
-    createInterface(WORKER_DIR, workerFile, state)
+    createInterface(WORKER_DIR, workerFile, state.interfaces)
   )
-  await Promise.all(workerPromises).then(results => console.log(results))
+  return Promise.all(workerPromises)
 }
 
 async function interfaces (parent, args, context, info) {
-  await findInterfaces()
+  const incomingInterfaces = await generateInterfaceList()
+  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
+    }
+  })
+  state.interfaces = [...state.interfaces, ...newInterfaces]
   return state.interfaces
 }
 
 async function iface (parent, { id }, context, info) {
-  await findInterfaces()
+  await generateInterfaceList()
   const iface = state.interfaces.find(
     iface => iface.id === id || iface.interfaceName === interfaceName
   )
   if (!iface) {
-    throw new Error(`Worker id=${id}, interfaceName=${interfaceName} not found`)
+    throw new Error(`Worker id=${id} not found`)
   }
   return iface
 }
@@ -273,43 +288,55 @@ function serializeWorkerProcess (parent, args, context, info) {
   }
 }
 
+async function createInterfacePorts (iface, ports) {
+  // a) Ask interface for ports.
+  const { data, error } = await iface.workerProcess.send({
+    type: 'ports'
+  })
+  if (error) throw new Error(error)
+  // b) Add all ports that are not in the list.
+  return data.map(port => {
+    const id = port.name || port.device
+    return {
+      id,
+      interface: iface,
+      interfaceName: iface.interfaceName,
+      host: HOST,
+      ...port
+    }
+  })
+}
+
 /**
- * PORTS SECTION
+ * Search for available ports on all interfaces
+ * @typedef {object} Port
+ * @property {string} id - Port ID
+ * @property {string} interfaceName - Interface name
+ * @property {host} host - Hostname
+ * @property {string} device - Physical device name
+ * @property {string} name - System name for the port
+ * @property {string} description - Description if available by system
+ *
+ * @param {Port[]} ports - Array of already found ports
+ * @return {Promise<Port[]>} Promise to return found ports
  */
-async function findPorts () {
+async function generatePortList (ports) {
   // 1. Make sure, workers are updated.
-  await findInterfaces()
+  const incomingPorts = 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 = state.interfaces.map(async iface => {
-    // a) Ask interface for ports.
-    const { data, error } = await iface.workerProcess.send({
-      type: 'ports'
-    })
-    if (error) throw new Error(error)
-    // b) Add all ports that are not in the list.
-    data.forEach(port => {
-      const id = port.name || port.device
-      if (state.ports.find(port => port.id === id)) return null
-      const newPort = {
-        id,
-        interface: iface,
-        interfaceName: iface.interfaceName,
-        host: HOST,
-        ...port
-      }
-      state.ports.push(newPort)
-    })
+  const portsPromises = interfaces.map(async iface => {
+    createInterfacePorts(iface, state.ports)
   })
   await Promise.all(portsPromises)
 }
 
 async function ports (parent, args, context, info) {
-  await findPorts()
+  await generatePortList()
 
   if (parent) {
     return state.ports.filter(
@@ -321,7 +348,7 @@ async function ports (parent, args, context, info) {
 }
 
 async function port (parent, { id }, context, info) {
-  await findPorts()
+  await generatePortList()
   const port = state.ports.find(port => port.id === id)
   return port
 }
@@ -330,7 +357,7 @@ async function port (parent, { id }, context, info) {
  * CONNECTION SECTION
  */
 async function connect (parent, { portId }, ctx, info) {
-  await findPorts()
+  await generatePortList()
   const port = state.ports.find(port => port.id === portId)
   if (!port) throw new Error(`Port ${portId} not found`)
 
@@ -431,20 +458,11 @@ const resolvers = {
   Interface: {
     ports,
     connections,
-    serializeWorkerProcess
+    workerProcess: serializeWorkerProcess
   },
   Connection: {
-    serializeWorkerProcess
+    workerProcess: serializeWorkerProcess
   }
 }
 
-const __test__ = {
-  findWorkers,
-  createInterface,
-  getOptions,
-  findInterfaces,
-  serializeWorkerProcess,
-  state
-}
-
-module.exports = { typeDefs, resolvers, __test__ }
+module.exports = { typeDefs, resolvers }