Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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 }}"

32 changes: 0 additions & 32 deletions .github/workflows/test.yml

This file was deleted.

64 changes: 64 additions & 0 deletions .github/workflows/tests-end-to-end.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# 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: E2E Test Suite

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
test:

strategy:
matrix:
os: [ubuntu-latest]
node-version: [20.x]
arch: [x64]

runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} / Node ${{ matrix.node-version }} ${{ matrix.arch }}

steps:

- 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 }}

- 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-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-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

- 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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
34 changes: 34 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

# 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.

## 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. Build
the image as follows before running the E2E test suite:

```sh
docker build -t bacnet-stack-server --platform linux/amd64 docker/bacnet-stack-server
```

## Running the E2E test suite

```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
12 changes: 12 additions & 0 deletions docker/bacnet-stack-server/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
30 changes: 30 additions & 0 deletions docker/bacnet-stack-server/server.js
Original file line number Diff line number Diff line change
@@ -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.trim());
}
});
});
});

server.listen(3000, '0.0.0.0');
3 changes: 3 additions & 0 deletions e2e-tests-clean.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

docker stop bacnet-stack-server && docker rm bacnet-stack-server
docker network rm bacnet-js
5 changes: 5 additions & 0 deletions e2e-tests-prep.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@


docker network create bacnet-js

docker run -d --platform linux/amd64 --network bacnet-js --name bacnet-stack-server bacnet-stack-server
8 changes: 8 additions & 0 deletions e2e-tests-run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@

docker run \
--rm \
--volume ./:/app \
--workdir /app \
--network bacnet-js \
node:20-alpine \
node --enable-source-maps --test dist
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 <jacopo@scazzosi.com>",
"license": "MIT",
Expand All @@ -39,6 +39,7 @@
"dist/**/*.js",
"dist/**/*.d.ts",
"!dist/examples/**/*",
"!dist/tests/**/*",
"CONFORMANCE.md"
],
"keywords": [
Expand Down
6 changes: 4 additions & 2 deletions src/objects/device/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,8 +402,10 @@ export class BDDevice extends BDObject implements AsyncEventEmitter<BDDeviceEven
this.#covqueue.kill();
this.removeListener('aftercov', this.#onChildAfterCov);
for (const object of this.#objects.values()) {
object.destroy();
object.removeListener('aftercov', this.#onChildAfterCov);
if (object !== this) {
object.destroy();
object.removeListener('aftercov', this.#onChildAfterCov);
}
}
this.#client
.removeListener('whoHas', this.#onBacnetWhoHas)
Expand Down
39 changes: 39 additions & 0 deletions src/tests/analogvalue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@

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, {
name: 'Test Device',
});
device.on('error', console.error);
device.addObject(new BDAnalogValue({
name: 'Test Value',
unit: EngineeringUnits.AMPERES,
presentValue: 0,
}));
});

afterEach(async () => {
device.destroy();
})

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');
});

});
19 changes: 19 additions & 0 deletions src/tests/bacnet-stack-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@

import { ObjectType, PropertyIdentifier } from '@bacnet-js/client';

const bsExec = async (bin: string, args: string[]) => {
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}`]);
};