From a5de664ff0fbf3d94c9b18c4319960248605173e Mon Sep 17 00:00:00 2001 From: Jacopo Scazzosi Date: Sun, 28 Sep 2025 23:25:30 +0200 Subject: [PATCH 1/9] chore: first pass at automating e2e tests with bacnet-stack --- .github/workflows/test.yml | 6 ++--- TESTING.md | 25 +++++++++++++++++++ docker/bacnet-stack-server/Dockerfile | 12 +++++++++ docker/bacnet-stack-server/server.js | 30 ++++++++++++++++++++++ e2e-tests-clean.sh | 3 +++ e2e-tests-prep.sh | 8 ++++++ e2e-tests-run.sh | 8 ++++++ package.json | 3 ++- src/objects/device/device.ts | 6 +++-- src/tests/bacnet-stack-client.ts | 19 ++++++++++++++ src/tests/object.analogvalue.test.ts | 36 +++++++++++++++++++++++++++ 11 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 TESTING.md create mode 100644 docker/bacnet-stack-server/Dockerfile create mode 100644 docker/bacnet-stack-server/server.js create mode 100644 e2e-tests-clean.sh create mode 100644 e2e-tests-prep.sh create mode 100644 e2e-tests-run.sh create mode 100644 src/tests/bacnet-stack-client.ts create mode 100644 src/tests/object.analogvalue.test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 43bf485..fe1571a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,6 +27,6 @@ jobs: uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - - run: npm ci - - run: npm run build - - run: npm test + - run: sh e2e-tests-prep.sh + - run: sh e2e-tests-run.sh + - run: sh e2e-tests-clean.sh diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..2d0ca57 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,25 @@ + +# Testing + +## End-to-end tests + +E2E tests use [bacnet-stack] as a third-party implementation of the BACnet +protocol. + +The suite depends on Docker to run [bacnet-stack]'s CLI programs in a separate +container from that of the BACnet device implemented through this library. This +is necessary because, in practice, it's very hard to get different BACnet +devices to share the same network interface. Docker offers us an easy way to +ensure that each device runs on its dedicated (virtual) interface. + +The image used for the [bacnet-stack] container, whose Dockerfile is located at +`./docker/docker-stack-server`, includes a simple HTTP server through which the +other container (which runs the test suite) can trigger BACnet queries. + +```sh +sh e2e-tests-prep.sh +sh e2e-tests-run.sh +sh e2e-tests-clean.sh +``` + +[bacnet-stack]: https://github.com/bacnet-stack/bacnet-stack diff --git a/docker/bacnet-stack-server/Dockerfile b/docker/bacnet-stack-server/Dockerfile new file mode 100644 index 0000000..f2c42d6 --- /dev/null +++ b/docker/bacnet-stack-server/Dockerfile @@ -0,0 +1,12 @@ + +FROM node:20-alpine AS build-env +RUN apk add --no-cache build-base curl git linux-headers +WORKDIR / +RUN git clone https://github.com/bacnet-stack/bacnet-stack.git --depth 1 --branch bacnet-stack-1.4.1 +RUN cd bacnet-stack && make clean all + +FROM node:20-alpine +COPY --from=build-env /bacnet-stack /bacnet-stack +WORKDIR /bacnet-stack +COPY ./server.js /server.js +CMD ["node", "/server.js"] diff --git a/docker/bacnet-stack-server/server.js b/docker/bacnet-stack-server/server.js new file mode 100644 index 0000000..989e943 --- /dev/null +++ b/docker/bacnet-stack-server/server.js @@ -0,0 +1,30 @@ + +import { createServer } from 'node:http'; +import { exec } from 'node:child_process'; + +const server = createServer((req, res) => { + let buf = Buffer.alloc(0); + req.on('data', (chunk) => { + buf = Buffer.concat([buf, chunk]); + }); + req.on('end', () => { + let cmd = buf.toString(); + if (cmd.length === 0 || !cmd.startsWith('bac')) { + res.statusCode = 400; + res.end('Invalid command'); + return; + } + cmd = `/bacnet-stack/bin/${cmd}`; + exec(cmd, (err, stdout, stderr) => { + if (err) { + res.statusCode = 500; + res.end(err.stack ?? err); + } else { + res.statusCode = 200; + res.end(stdout); + } + }); + }); +}); + +server.listen(3000, '0.0.0.0'); diff --git a/e2e-tests-clean.sh b/e2e-tests-clean.sh new file mode 100644 index 0000000..7dea138 --- /dev/null +++ b/e2e-tests-clean.sh @@ -0,0 +1,3 @@ + +docker stop bacnet-stack-server && docker rm bacnet-stack-server +docker network rm bacnet-js diff --git a/e2e-tests-prep.sh b/e2e-tests-prep.sh new file mode 100644 index 0000000..829b349 --- /dev/null +++ b/e2e-tests-prep.sh @@ -0,0 +1,8 @@ + +cd docker/bacnet-stack-server +docker build -t bacnet-stack-server --platform linux/amd64 . +cd ../.. + +docker network create bacnet-js + +docker run -d --platform linux/amd64 --network bacnet-js --name bacnet-stack-server bacnet-stack-server diff --git a/e2e-tests-run.sh b/e2e-tests-run.sh new file mode 100644 index 0000000..baf9c15 --- /dev/null +++ b/e2e-tests-run.sh @@ -0,0 +1,8 @@ + +docker run \ + --rm \ + --volume ./:/app \ + --workdir /app \ + --network bacnet-js \ + node:20-alpine \ + node --enable-source-maps --test dist diff --git a/package.json b/package.json index 42f6b2b..85d4518 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "clean": "rm -rf ./dist", "build": "npm run clean && tsc -p .", "hooks": "cog install-hook --all", - "test": "echo \"done\"" + "test": "echo \"see TESTING.md\"" }, "author": "Jacopo Scazzosi ", "license": "MIT", @@ -39,6 +39,7 @@ "dist/**/*.js", "dist/**/*.d.ts", "!dist/examples/**/*", + "!dist/tests/**/*", "CONFORMANCE.md" ], "keywords": [ diff --git a/src/objects/device/device.ts b/src/objects/device/device.ts index 5ef9804..353c1b6 100644 --- a/src/objects/device/device.ts +++ b/src/objects/device/device.ts @@ -402,8 +402,10 @@ export class BDDevice extends BDObject implements AsyncEventEmitter { + const res = await fetch('http://bacnet-stack-server:3000', { + method: 'POST', + body: `${bin} ${args.map(a => `"${a}"`).join(' ')}`, + }); + if (res.ok) { + const stdout = await res.text(); + return stdout; + } + const err = await res.text(); + throw new Error(err); +}; + +export const bsReadProperty = async (devIn: number, objType: ObjectType, objIn: number, propId: PropertyIdentifier) => { + return await bsExec('bacrp', [`${devIn}`, `${objType}`, `${objIn}`, `${propId}`]); +}; diff --git a/src/tests/object.analogvalue.test.ts b/src/tests/object.analogvalue.test.ts new file mode 100644 index 0000000..9088c33 --- /dev/null +++ b/src/tests/object.analogvalue.test.ts @@ -0,0 +1,36 @@ + +import { it, describe, beforeEach, afterEach } from 'node:test'; +import { deepStrictEqual } from 'node:assert'; +import { BDDevice } from '../objects/device/device.js'; +import { bsReadProperty } from './bacnet-stack-client.js'; +import { BDAnalogValue } from '../objects/numeric/analogvalue.js'; +import { EngineeringUnits, ObjectType, PropertyIdentifier } from '@bacnet-js/client'; + +describe('AnalogValue', () => { + + let device: BDDevice; + + beforeEach(async () => { + device = new BDDevice(1, { + // interface: '0.0.0.0', + name: 'Test Device', + }); + device.on('error', console.error); + }); + + afterEach(async () => { + device.destroy(); + await new Promise(resolve => setTimeout(resolve, 10)); + }) + + it('should read presentvalue', async () => { + device.addObject(new BDAnalogValue({ + name: 'Test Value', + unit: EngineeringUnits.AMPERES, + presentValue: 0, + })); + const value = await bsReadProperty(1, ObjectType.ANALOG_VALUE, 1, PropertyIdentifier.PRESENT_VALUE); + deepStrictEqual(parseInt(value), 0); + }); + +}); From 0233ed1250c805ebe864d43587e21d25cdc4bfd3 Mon Sep 17 00:00:00 2001 From: Jacopo Scazzosi Date: Sun, 28 Sep 2025 23:28:55 +0200 Subject: [PATCH 2/9] chore: update config for github actions --- .github/workflows/test.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fe1571a..272de55 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - node-version: [20.x, 22.x, 24.x] + node-version: [20.x] arch: [x64] runs-on: ${{ matrix.os }} @@ -27,6 +27,8 @@ jobs: uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} + - run: npm ci + - run: npm run build - run: sh e2e-tests-prep.sh - run: sh e2e-tests-run.sh - run: sh e2e-tests-clean.sh From 9460ccfae52a3609343a676b12c244f2a501c898 Mon Sep 17 00:00:00 2001 From: Jacopo Scazzosi Date: Mon, 29 Sep 2025 10:43:49 +0200 Subject: [PATCH 3/9] chore: minor tweaks --- docker/bacnet-stack-server/server.js | 2 +- ...nalogvalue.test.ts => analogvalue.test.ts} | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) rename src/tests/{object.analogvalue.test.ts => analogvalue.test.ts} (76%) diff --git a/docker/bacnet-stack-server/server.js b/docker/bacnet-stack-server/server.js index 989e943..56ac967 100644 --- a/docker/bacnet-stack-server/server.js +++ b/docker/bacnet-stack-server/server.js @@ -21,7 +21,7 @@ const server = createServer((req, res) => { res.end(err.stack ?? err); } else { res.statusCode = 200; - res.end(stdout); + res.end(stdout.trim()); } }); }); diff --git a/src/tests/object.analogvalue.test.ts b/src/tests/analogvalue.test.ts similarity index 76% rename from src/tests/object.analogvalue.test.ts rename to src/tests/analogvalue.test.ts index 9088c33..d1033ea 100644 --- a/src/tests/object.analogvalue.test.ts +++ b/src/tests/analogvalue.test.ts @@ -12,25 +12,28 @@ describe('AnalogValue', () => { beforeEach(async () => { device = new BDDevice(1, { - // interface: '0.0.0.0', name: 'Test Device', }); device.on('error', console.error); + device.addObject(new BDAnalogValue({ + name: 'Test Value', + unit: EngineeringUnits.AMPERES, + presentValue: 0, + })); }); afterEach(async () => { device.destroy(); - await new Promise(resolve => setTimeout(resolve, 10)); }) - it('should read presentvalue', async () => { - device.addObject(new BDAnalogValue({ - name: 'Test Value', - unit: EngineeringUnits.AMPERES, - presentValue: 0, - })); + it('should read the object\'s Present_Value property', async () => { const value = await bsReadProperty(1, ObjectType.ANALOG_VALUE, 1, PropertyIdentifier.PRESENT_VALUE); deepStrictEqual(parseInt(value), 0); }); + it('should read the object\'s Units property', async () => { + const value = await bsReadProperty(1, ObjectType.ANALOG_VALUE, 1, PropertyIdentifier.UNITS); + deepStrictEqual(value, 'amperes'); + }); + }); From 7371bdeefcd54fbd7124c88de387d7a2691954ba Mon Sep 17 00:00:00 2001 From: Jacopo Scazzosi Date: Mon, 29 Sep 2025 10:48:00 +0200 Subject: [PATCH 4/9] chore: adds testing section to readme --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 69de3bb..9e875b7 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,10 @@ able to run locally and use with any BACnet client of your choice. For local testing I tend to use [YABE], which is native to Windows but can be made to work reasonably well on macOS and Linux via [Wine]. +## Testing + +See [TESTING.md]. + [device]: https://github.com/bacnet-js/device [apidocs]: https://bacnet-js.github.io/device @@ -68,6 +72,7 @@ work reasonably well on macOS and Linux via [Wine]. [@bacnet-js/client]: https://github.com/bacnet-js/node-bacnet [LICENSE]: https://github.com/bacnet-js/device/blob/main/LICENSE [CONFORMANCE.md]: https://github.com/bacnet-js/device/blob/main/CONFORMANCE.md +[TESTING.md]: https://github.com/bacnet-js/device/blob/main/TESTING.md [examples]: https://github.com/bacnet-js/device/tree/main/src/examples [YABE]: https://sourceforge.net/projects/yetanotherbacnetexplorer/ [Wine]: https://www.winehq.org From 6e8deb9f54ad12bb95a8bf13e88dd2cbe57871e9 Mon Sep 17 00:00:00 2001 From: Jacopo Scazzosi Date: Mon, 29 Sep 2025 10:51:04 +0200 Subject: [PATCH 5/9] chore: better naming of github workflows --- .github/workflows/{cocogitto.yml => check-commit-pr.yml} | 3 +-- .github/workflows/{test.yml => tests-end-to-end.yml} | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) rename .github/workflows/{cocogitto.yml => check-commit-pr.yml} (95%) rename .github/workflows/{test.yml => tests-end-to-end.yml} (97%) diff --git a/.github/workflows/cocogitto.yml b/.github/workflows/check-commit-pr.yml similarity index 95% rename from .github/workflows/cocogitto.yml rename to .github/workflows/check-commit-pr.yml index b546705..75b72ef 100644 --- a/.github/workflows/cocogitto.yml +++ b/.github/workflows/check-commit-pr.yml @@ -3,7 +3,7 @@ # to keep things as portable as possible and to easily replicate the # workflow locally. -name: check commit messages +name: Check commit messages and PR titles on: push: @@ -27,4 +27,3 @@ jobs: - name: Check PR title using cocogitto if: github.event_name == 'pull_request' run: docker run -t ghcr.io/cocogitto/cog:latest verify "${{ github.event.pull_request.title }}" - \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/tests-end-to-end.yml similarity index 97% rename from .github/workflows/test.yml rename to .github/workflows/tests-end-to-end.yml index 272de55..c140a9a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/tests-end-to-end.yml @@ -1,7 +1,7 @@ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions -name: Node.js CI +name: E2E Test Suite on: push: From 79dbd2965dab958d2af8ef6458d85c7944c942fc Mon Sep 17 00:00:00 2001 From: Jacopo Scazzosi Date: Mon, 29 Sep 2025 11:12:14 +0200 Subject: [PATCH 6/9] chore: caching bacnet-stack docker image in github workflows --- .github/workflows/tests-end-to-end.yml | 41 ++++++++++++++++++++++---- TESTING.md | 11 ++++++- e2e-tests-prep.sh | 3 -- 3 files changed, 45 insertions(+), 10 deletions(-) diff --git a/.github/workflows/tests-end-to-end.yml b/.github/workflows/tests-end-to-end.yml index c140a9a..4eca441 100644 --- a/.github/workflows/tests-end-to-end.yml +++ b/.github/workflows/tests-end-to-end.yml @@ -22,13 +22,42 @@ jobs: name: ${{ matrix.os }} / Node ${{ matrix.node-version }} ${{ matrix.arch }} steps: - - uses: actions/checkout@v2 + + - name: Checkout code + uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} - - run: npm ci - - run: npm run build - - run: sh e2e-tests-prep.sh - - run: sh e2e-tests-run.sh - - run: sh e2e-tests-clean.sh + + - name: Cache bacnet-stack-server image + uses: actions/cache@v4 + with: + path: bacnet-stack-server.tar + key: ${{ runner.os }}-${{ hashFiles('docker/bacnet-stack-server/*') }} + + - name: Load cached bacnet-stack-server image + if: ${{ steps.cache-npm.outputs.cache-hit == 'true' }} + continue-on-error: false + run: docker load -i bacnet-stack-server.tar + + - name: Build bacnet-stack-server image + if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} + continue-on-error: false + run: docker build -t bacnet-stack-server --platform linux/amd64 . && docker save -o bacnet-stack-server.tar bacnet-stack-server + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Prepare environment for E2E tests + run: sh e2e-tests-prep.sh + + - name: Run E2E tests + run: sh e2e-tests-run.sh + + - name: Clean up environment after E2E tests + run: sh e2e-tests-clean.sh diff --git a/TESTING.md b/TESTING.md index 2d0ca57..578ce07 100644 --- a/TESTING.md +++ b/TESTING.md @@ -12,9 +12,18 @@ is necessary because, in practice, it's very hard to get different BACnet devices to share the same network interface. Docker offers us an easy way to ensure that each device runs on its dedicated (virtual) interface. +## Building the `bacnet-stack` image + The image used for the [bacnet-stack] container, whose Dockerfile is located at `./docker/docker-stack-server`, includes a simple HTTP server through which the -other container (which runs the test suite) can trigger BACnet queries. +other container (which runs the test suite) can trigger BACnet queries. Build +the image as follows before running the E2E test suite: + +```sh +docker build -t bacnet-stack-server --platform linux/amd64 . +``` + +## Running the E2E test suite ```sh sh e2e-tests-prep.sh diff --git a/e2e-tests-prep.sh b/e2e-tests-prep.sh index 829b349..2f65417 100644 --- a/e2e-tests-prep.sh +++ b/e2e-tests-prep.sh @@ -1,7 +1,4 @@ -cd docker/bacnet-stack-server -docker build -t bacnet-stack-server --platform linux/amd64 . -cd ../.. docker network create bacnet-js From 3f78d7348d8eb05a80faedb3d4be116e7884fa3a Mon Sep 17 00:00:00 2001 From: Jacopo Scazzosi Date: Mon, 29 Sep 2025 11:13:47 +0200 Subject: [PATCH 7/9] chore: fixes broken path in image build step --- .github/workflows/tests-end-to-end.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests-end-to-end.yml b/.github/workflows/tests-end-to-end.yml index 4eca441..b5db72b 100644 --- a/.github/workflows/tests-end-to-end.yml +++ b/.github/workflows/tests-end-to-end.yml @@ -45,7 +45,7 @@ jobs: - name: Build bacnet-stack-server image if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} continue-on-error: false - run: docker build -t bacnet-stack-server --platform linux/amd64 . && docker save -o bacnet-stack-server.tar bacnet-stack-server + run: docker build -t bacnet-stack-server --platform linux/amd64 docker/bacnet-stack-server && docker save -o bacnet-stack-server.tar bacnet-stack-server - name: Install dependencies run: npm ci From ab36adca0767080ca0fa13be50c64f96e808e265 Mon Sep 17 00:00:00 2001 From: Jacopo Scazzosi Date: Mon, 29 Sep 2025 11:17:22 +0200 Subject: [PATCH 8/9] chore: updates testing.md --- TESTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TESTING.md b/TESTING.md index 578ce07..621d209 100644 --- a/TESTING.md +++ b/TESTING.md @@ -20,7 +20,7 @@ other container (which runs the test suite) can trigger BACnet queries. Build the image as follows before running the E2E test suite: ```sh -docker build -t bacnet-stack-server --platform linux/amd64 . +docker build -t bacnet-stack-server --platform linux/amd64 docker/bacnet-stack-server ``` ## Running the E2E test suite From 4272565970a0a2a936238e0aafa6a80bbcd761a9 Mon Sep 17 00:00:00 2001 From: Jacopo Scazzosi Date: Mon, 29 Sep 2025 11:22:01 +0200 Subject: [PATCH 9/9] chore: fixes cache references in github actions --- .github/workflows/tests-end-to-end.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests-end-to-end.yml b/.github/workflows/tests-end-to-end.yml index b5db72b..ed3088f 100644 --- a/.github/workflows/tests-end-to-end.yml +++ b/.github/workflows/tests-end-to-end.yml @@ -33,17 +33,18 @@ jobs: - name: Cache bacnet-stack-server image uses: actions/cache@v4 + id: cache-bacnet-stack-server-docker-image with: path: bacnet-stack-server.tar key: ${{ runner.os }}-${{ hashFiles('docker/bacnet-stack-server/*') }} - name: Load cached bacnet-stack-server image - if: ${{ steps.cache-npm.outputs.cache-hit == 'true' }} + if: ${{ steps.cache-bacnet-stack-server-docker-image.outputs.cache-hit == 'true' }} continue-on-error: false run: docker load -i bacnet-stack-server.tar - name: Build bacnet-stack-server image - if: ${{ steps.cache-npm.outputs.cache-hit != 'true' }} + if: ${{ steps.cache-bacnet-stack-server-docker-image.outputs.cache-hit != 'true' }} continue-on-error: false run: docker build -t bacnet-stack-server --platform linux/amd64 docker/bacnet-stack-server && docker save -o bacnet-stack-server.tar bacnet-stack-server