Skip to content

Commit

Permalink
Add unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
nflaig committed Jan 28, 2023
1 parent f6f18b2 commit 7a53a3e
Show file tree
Hide file tree
Showing 5 changed files with 670 additions and 0 deletions.
38 changes: 38 additions & 0 deletions packages/beacon-node/test/unit/monitoring/clientStats.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {expect} from "chai";
import {ClientStats} from "../../../src/monitoring/types.js";
import {createClientStats} from "../../../src/monitoring/clientStats.js";
import {beaconNodeStatsSchema, ClientStatsSchema, systemStatsSchema, validatorStatsSchema} from "./schemas.js";

describe("monitoring / clientStats", () => {
describe("BeaconNodeStats", () => {
it("should contain all required keys", () => {
const beaconNodeStats = createClientStats("beacon")[0];

expect(getJsonKeys(beaconNodeStats)).to.have.all.members(getSchemaKeys(beaconNodeStatsSchema));
});
});

describe("ValidatorStats", () => {
it("should contain all required keys", () => {
const validatorNodeStats = createClientStats("validator")[0];

expect(getJsonKeys(validatorNodeStats)).to.have.all.members(getSchemaKeys(validatorStatsSchema));
});
});

describe("SystemStats", () => {
it("should contain all required keys", () => {
const systemStats = createClientStats("beacon", true)[1];

expect(getJsonKeys(systemStats)).to.have.all.members(getSchemaKeys(systemStatsSchema));
});
});
});

function getJsonKeys(stats: ClientStats): string[] {
return Object.values(stats).map((property) => property.definition.jsonKey);
}

function getSchemaKeys(schema: ClientStatsSchema): string[] {
return schema.map((s) => s.key);
}
247 changes: 247 additions & 0 deletions packages/beacon-node/test/unit/monitoring/properties.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import {expect} from "chai";
import {IMetrics} from "../../../src/metrics/index.js";
import {DynamicProperty, MetricProperty, StaticProperty} from "../../../src/monitoring/properties.js";
import {JsonType} from "../../../src/monitoring/types.js";
import {createMetricsTest} from "../metrics/utils.js";

describe("monitoring / properties", () => {
const jsonKey = "test_key";
const value = 1;

describe("StaticProperty", () => {
it("should return a json record with the configured key and value", () => {
const staticProperty = new StaticProperty({jsonKey, value});

const jsonRecord = staticProperty.getRecord();

expect(jsonRecord.key).to.equal(jsonKey);
expect(jsonRecord.value).to.equal(value);
});
});

describe("DynamicProperty", () => {
it("should return a json record with the configured key and return value of provider", async () => {
const dynamicProperty = new DynamicProperty({jsonKey, provider: () => value});

const jsonRecord = await dynamicProperty.getRecord();

expect(jsonRecord.key).to.equal(jsonKey);
expect(jsonRecord.value).to.equal(value);
});

it("should return the same value on consecutive calls if cacheResult is set to true", async () => {
const initialValue = 1;
let updatedValue = initialValue;

const provider = (): number => {
const value = updatedValue;
updatedValue++;
return value;
};

const dynamicProperty = new DynamicProperty({jsonKey, provider, cacheResult: true});

// ensure consecutive calls still return initial provider value
expect((await dynamicProperty.getRecord()).value).to.equal(initialValue);
expect((await dynamicProperty.getRecord()).value).to.equal(initialValue);
expect((await dynamicProperty.getRecord()).value).to.equal(initialValue);
});
});

describe("MetricProperty", () => {
let metrics: IMetrics;

before(() => {
metrics = createMetricsTest();
});

it("should return a json record with the configured key and metric value", async () => {
const peerCount = 50;
metrics.peers.set(peerCount);

const metricProperty = new MetricProperty({
jsonKey,
metricName: "libp2p_peers",
jsonType: JsonType.Number,
defaultValue: 0,
});

const jsonRecord = await metricProperty.getRecord(metrics.register);

expect(jsonRecord.key).to.equal(jsonKey);
expect(jsonRecord.value).to.equal(peerCount);
});

it("should return the default value if metric with name does not exist", async () => {
const defaultValue = 10;

const metricProperty = new MetricProperty({
jsonKey,
metricName: "does_not_exist",
jsonType: JsonType.Number,
defaultValue,
});

expect((await metricProperty.getRecord(metrics.register)).value).to.equal(defaultValue);
});

it("should get the value from label instead of metric value if fromLabel is defined", async () => {
const metricName = "static_metric";
const labelName = "test_label";
const labelValue = "test_value";

metrics.register.static({name: metricName, help: "fromLabel test", value: {[labelName]: labelValue}});

const metricProperty = new MetricProperty({
jsonKey,
metricName,
fromLabel: labelName,
jsonType: JsonType.String,
defaultValue: "",
});

expect((await metricProperty.getRecord(metrics.register)).value).to.equal(labelValue);
});

it("should get the value from metric with label if withLabel is defined", async () => {
const metricName = "metric_with_labels";
const labelName = "test_label_name";
const labelValue = "test_label_value";
const metricValue = 10;

const metric = metrics.register.gauge({name: metricName, help: "withLabel test", labelNames: [labelName]});
metric.set({[labelName]: "different_value"}, metricValue + 1);
metric.set({[labelName]: labelValue}, metricValue);

const metricProperty = new MetricProperty({
jsonKey,
metricName,
withLabel: {name: labelName, value: labelValue},
jsonType: JsonType.Number,
defaultValue: 0,
});

expect((await metricProperty.getRecord(metrics.register)).value).to.equal(metricValue);
});

it("should return the same value on consecutive calls if cacheResult is set to true", async () => {
const metricName = "metric_test_caching";
const initialValue = 10;

const metric = metrics.register.gauge({name: metricName, help: "cacheResult test"});
metric.set(initialValue);

const metricProperty = new MetricProperty({
jsonKey,
metricName,
jsonType: JsonType.Number,
defaultValue: 0,
cacheResult: true,
});

// initial call which will cache the result
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(initialValue);

// set different value
metric.set(initialValue + 1);

// ensure consecutive calls still return initial value
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(initialValue);
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(initialValue);
});

it("should convert the metric value to a string if jsonType is JsonType.String", async () => {
const metricName = "metric_test_string";

const metric = metrics.register.gauge({name: metricName, help: "JsonType.String test"});

const metricProperty = new MetricProperty({
jsonKey,
metricName,
jsonType: JsonType.String,
defaultValue: "",
});

metric.set(10);
expect((await metricProperty.getRecord(metrics.register)).value).to.equal("10");
});

it("should round the metric value to the nearest integer if jsonType is JsonType.Number", async () => {
const metricName = "metric_test_number";

const metric = metrics.register.gauge({name: metricName, help: "JsonType.Number test"});

const metricProperty = new MetricProperty({
jsonKey,
metricName,
jsonType: JsonType.Number,
defaultValue: 0,
});

metric.set(1.49);
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(1);
});

it("should convert the metric value to a boolean if jsonType is JsonType.Boolean", async () => {
const metricName = "metric_test_boolean";

const metric = metrics.register.gauge({name: metricName, help: "JsonType.Boolean test"});

const metricProperty = new MetricProperty({
jsonKey,
metricName,
jsonType: JsonType.Boolean,
defaultValue: false,
});

metric.set(0);
// metric value of 0 should be converted to false
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(false);

metric.set(1);
// metric value > 0 should be converted to true
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(true);
});

it("should convert the metric value to true if the specified rangeValue is matched", async () => {
const metricName = "metric_test_range_value";
const rangeValue = 3;

const metric = metrics.register.gauge({name: metricName, help: "rangeValue test"});

const metricProperty = new MetricProperty({
jsonKey,
metricName,
rangeValue,
jsonType: JsonType.Boolean,
defaultValue: false,
});

metric.set(rangeValue + 1);
// value does not match range value and should be converted to false
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(false);

metric.set(rangeValue);
// value matches range value and should be converted to true
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(true);
});

it("should apply the defined formatter to the metric value", async () => {
const metricName = "metric_test_formatting";
const metricValue = 10;

const metric = metrics.register.gauge({name: metricName, help: "formatter test"});

const metricProperty = new MetricProperty({
jsonKey,
metricName,
jsonType: JsonType.String,
formatter: (value) => `prefix_${value}`,
defaultValue: "",
});

metric.set(metricValue);
expect((await metricProperty.getRecord(metrics.register)).value).to.equal(`prefix_${metricValue}`);
});
});
});
86 changes: 86 additions & 0 deletions packages/beacon-node/test/unit/monitoring/remoteService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {expect} from "chai";
import fastify from "fastify";
import {RemoteServiceError} from "../../../src/monitoring/service.js";
import {ProcessType} from "../../../src/monitoring/types.js";
import {beaconNodeStatsSchema, ClientStatsSchema, systemStatsSchema, validatorStatsSchema} from "./schemas.js";

/* eslint-disable no-console */

type ReceivedData = Record<string, unknown>;

export const remoteServiceRoutes = {
success: "/success",
error: "/error",
pending: "/pending",
};

export const remoteServiceError: RemoteServiceError = {status: "error", data: null};

/**
* Starts mocked remote service to receive and validate client stats
*/
export async function startRemoteService(): Promise<{baseUrl: URL}> {
const server = fastify();

server.post(remoteServiceRoutes.success, {}, async function (request, reply) {
if (Array.isArray(request.body)) {
request.body.forEach(validateRequestData);
} else {
validateRequestData(request.body as ReceivedData);
}

return reply.status(200).send();
});

server.post(remoteServiceRoutes.error, {}, async function (_request, reply) {
return reply.status(400).send(remoteServiceError);
});

server.post(remoteServiceRoutes.pending, {}, function () {
// keep request pending until timeout is reached or aborted
});

server.addHook("onError", (_request, _reply, error, done) => {
console.log(`Error: ${error.message}`);
done();
});

// ask the operating system to assign a free (ephemeral) port
// and use IPv4 localhost "127.0.0.1" to avoid known IPv6 issues
const baseUrl = await server.listen({host: "127.0.0.1", port: 0});

after(() => {
// there is no need to wait for server to be closed
server.close().catch(console.log);
});

return {baseUrl: new URL(baseUrl)};
}

function validateRequestData(data: ReceivedData): void {
switch (data.process) {
case ProcessType.BeaconNode:
validateClientStats(data, beaconNodeStatsSchema);
break;
case ProcessType.Validator:
validateClientStats(data, validatorStatsSchema);
break;
case ProcessType.System:
validateClientStats(data, systemStatsSchema);
break;
default:
throw new Error(`Invalid process type "${data.process}"`);
}
}

function validateClientStats(data: ReceivedData, schema: ClientStatsSchema): void {
schema.forEach((s) => {
try {
expect(data[s.key]).to.be.a(s.type);
} catch {
throw new Error(
`Validation of property "${s.key}" failed. Expected type "${s.type}" but received "${typeof data[s.key]}".`
);
}
});
}
Loading

0 comments on commit 7a53a3e

Please sign in to comment.