Files
factorio-clusterio/test/lib/prometheus.js
T
Hornwitser 8229d1955c Skip broken Prometheus client tests
Skip Prometheus client tests that assume the environment is isolated
from the rest of the tests.
2019-12-20 19:14:35 +01:00

570 lines
17 KiB
JavaScript

const assert = require('assert').strict;
const prometheus = require('lib/prometheus');
describe("lib/prometheus", function() {
describe("class CollectorRegistry", function() {
let registry;
beforeEach(function() {
registry = new prometheus.CollectorRegistry();
})
describe(".register()", function() {
it("should register a collector", function() {
let collector = new prometheus.Collector(false);
registry.register(collector);
assert(
registry.collectors.includes(collector),
"Collector was not registered"
);
});
it("should throw when already registered", function() {
let collector = new prometheus.Collector(false);
registry.register(collector);
assert.throws(
() => registry.register(collector),
new Error("Collector is already registered in this registry.")
);
});
it("should allow being regitstered in another register", function() {
let otherCollectorRegistry = new prometheus.CollectorRegistry();
let collector = new prometheus.Collector(false);
registry.register(collector);
otherCollectorRegistry.register(collector);
assert(
registry.collectors.includes(collector),
"Collector was not registered in first register"
);
assert(
otherCollectorRegistry.collectors.includes(collector),
"Collector was not registered in second register"
);
});
});
describe(".unregister()", function() {
it("should removed a registered collector", function() {
let collector = new prometheus.Collector(false);
registry.register(collector);
registry.unregister(collector);
assert(
!registry.collectors.includes(collector),
"Collector was not removed"
);
});
it("should throw when not registered", function() {
let collector = new prometheus.Collector(false);
assert.throws(
() => registry.unregister(collector),
new Error("Collector is not registered in this registry.")
);
});
});
describe("defaultRegistry", function() {
it("should have collectors registered to it by default", function() {
let collector = new prometheus.Collector();
assert(
prometheus.defaultRegistry.collectors.includes(collector),
"collector was not registered to defaultRegistry"
);
prometheus.defaultRegistry.unregister(collector);
});
it("should have the default collectors registered to it", function() {
let collectors = Object.entries(prometheus.defaultCollectors);
for (let [name, collector] of collectors) {
assert(
prometheus.defaultRegistry.collectors.includes(collector),
`${name} is not registered`
);
}
});
it("should allow default collectors to be unregistered", function() {
this.skip(); // XXX Doesn't work in Clusterio's shared testing env
let collectors = Object.values(prometheus.defaultCollectors);
for (let collector of collectors) {
prometheus.defaultRegistry.unregister(collector);
}
assert(
prometheus.defaultRegistry.collectors.length == 0,
"Collectors left over after removing the default ones"
);
});
});
});
describe("class Collector", function() {
it("should implement .collect()", async function() {
let collector = new prometheus.Collector(false);
for await (let result of collector.collect()) {
assert.fail("Should not return any results");
}
});
});
describe("class Counter", function() {
let counter;
beforeEach(function() {
counter = new prometheus.Counter('test', "Help", { register: false });
});
describe("constructor", function() {
it("should be initialized to 0", function() {
assert.equal(counter._values.get(''), 0);
});
});
describe(".inc()", function() {
it("should increment the counter by 1 by default", function() {
counter.inc();
assert.equal(counter._values.get(''), 1);
});
it("should increment the counter by specified value", function() {
counter.inc(3.2);
assert.equal(counter._values.get(''), 3.2);
});
it("should throw if value is negative", function() {
assert.throws(
() => counter.inc(-1),
new Error("Expected value to be a positive number")
);
});
it("should throw if value is negative infinity", function() {
assert.throws(
() => counter.inc(-Infinity),
new Error("Expected value to be a positive number")
);
});
it("should throw if value is NaN", function() {
assert.throws(
() => counter.inc(NaN),
new Error("Expected value to be a positive number")
);
});
});
});
describe("class Gauge", function() {
let gauge;
beforeEach(function() {
gauge = new prometheus.Gauge('test', "Help", { register: false });
});
describe("constructor", function() {
it("should be initialized to 0", function() {
assert.equal(gauge._values.get(''), 0);
});
});
describe(".inc()", function() {
it("should increment the gauge by 1 by default", function() {
gauge.inc();
assert.equal(gauge._values.get(''), 1);
});
it("should increment the gauge by specified value", function() {
gauge.inc(3.2);
assert.equal(gauge._values.get(''), 3.2);
});
it("should decrement on negative value", function() {
gauge.inc(-2);
assert.equal(gauge._values.get(''), -2);
});
});
describe(".dec()", function() {
it("should decrement the gauge by 1 by default", function() {
gauge.dec();
assert.equal(gauge._values.get(''), -1);
});
it("should decrement the gauge by specified value", function() {
gauge.dec(3.2);
assert.equal(gauge._values.get(''), -3.2);
});
it("should increment on negative value", function() {
gauge.dec(-2);
assert.equal(gauge._values.get(''), 2);
});
});
describe(".set()", function() {
it("should set the gauge to the specified value", function() {
gauge.set(7);
assert.equal(gauge._values.get(''), 7);
});
});
describe(".setToCurrentTime()", function() {
it("should set the gauge to current unix time in seconds", function() {
gauge.setToCurrentTime();
assert(
Math.abs(gauge._values.get('') - Date.now() / 1000) < 1,
"value set is not close to the current unix time"
);
});
});
});
describe("class ValueCollector", function() {
describe("constructor", function() {
it("should throw on invalid metric name", function() {
for (let name of ["", " ", "0a", "$a"]) {
assert.throws(
() => new prometheus.Gauge(
name, "Help", { register: false }
),
new Error(`Invalid name '${name}'`)
)
}
});
it("should throw on invalid label names", function() {
for (let name of ["", " ", "0a", "$a", "a:"]) {
assert.throws(
() => new prometheus.Gauge(
'test', "Help", { register: false, labels: [name] }
),
new Error(`Invalid label '${name}'`)
)
}
});
it("should throw on missing name", function() {
assert.throws(
() => new prometheus.Gauge(),
new Error("Expected name to be a string")
);
});
it("should throw on missing help", function() {
assert.throws(
() => new prometheus.Gauge("test"),
new Error("Expected help to be a string")
);
});
it("should throw on unrecoginzed option", function() {
assert.throws(
() => new prometheus.Gauge("test", "help", { invalid: 1 }),
new Error("Unrecognized option 'invalid'")
);
});
});
describe(".get()", function() {
it("should return the current value", function() {
let gauge = new prometheus.Gauge('test', "Help", { register: false });
gauge.set(11);
assert.equal(gauge.get(), 11);
});
});
describe(".labels()", function() {
let gauge;
beforeEach(function() {
gauge = new prometheus.Gauge(
'test', "Help", { register: false, labels: ['a', 'b'] }
);
});
it("should require all labels set at creation", function() {
assert.throws(
() => gauge.labels({a: "1"}),
new Error("Missing label 'b'")
);
assert.throws(
() => gauge.labels({b: "1"}),
new Error("Missing label 'a'")
);
assert.throws(
() => gauge.labels({a: '1', b: '2', c: '3'}),
new Error("Extra label 'c'")
);
assert.throws(
() => gauge.labels('1'),
new Error("Missing label 'b'")
);
assert.throws(
() => gauge.labels('1', '2', '3'),
new Error("Extra positional label")
);
});
it("should throw if label value is not a string", function() {
assert.throws(
() => gauge.labels({a: "1", b: 2}),
new Error("Expected value for label 'b' to be a string")
);
assert.throws(
() => gauge.labels("1", 2),
new Error("Expected value for label 'b' to be a string")
);
});
it("should initialize a value for the label set", function() {
gauge.labels({ a: '1', b: '2'});
assert(gauge._values.has('a="1",b="2"'), "value was not set");
gauge.labels('4', '5');
assert(gauge._values.has('a="4",b="5"'), "value was not set");
});
it("should return a child supporting the usual methods", function() {
let child = gauge.labels({ a: '1', b: '2'});
child.get();
child.inc();
child.dec();
child.setToCurrentTime();
child = gauge.labels('7', '8');
child.get();
child.inc();
child.dec();
child.setToCurrentTime();
});
it("should return the same child for the same labels", function() {
let child = gauge.labels('1', '2');
assert.equal(gauge.labels('1', '2'), child);
});
});
describe(".remove()", function() {
it("should remove a label set created with .label()", function() {
let gauge = new prometheus.Gauge(
'test', "Help", { register: false, labels: ['a', 'b'] }
);
gauge.labels({ a: '1', b: '2'});
gauge.remove({ a: '1', b: '2'});
assert(!gauge._values.has('a="1",b="2"'), "value was not removed");
});
it("should throw when removing the empty label set", function() {
let gauge = new prometheus.Gauge('test', "Help", { register: false });
assert.throws(
() => gauge.remove({}), new Error("labels cannot be empty")
);
});
});
describe(".removeAll()", function() {
it("should remove a label set created with .label()", function() {
let gauge = new prometheus.Gauge(
'test', "Help", { register: false, labels: ['a', 'b'] }
);
gauge.labels('1', '2');
gauge.labels('1', '3');
gauge.labels('4', '6');
gauge.removeAll({a: '1'});
assert(gauge._values.size <= 1, "Matching labels remained")
assert(
gauge._values.has('a="4",b="6"'),
"Non-matching label was removed"
);
});
it("should throw when removing the empty label set", function() {
let gauge = new prometheus.Gauge('test', "Help", { register: false });
assert.throws(
() => gauge.removeAll({}), new Error("labels cannot be empty")
);
});
it("should handle special characters", function() {
let gauge = new prometheus.Gauge(
'test', "Help", { register: false, labels: ['a', 'b'] }
)
gauge.labels('x\\\\\n\"\"\n\\', '2');
gauge.removeAll({a: 'x\\\\\n\"\"\n\\'});
assert(gauge._values.size === 0, "Matching labels remained")
});
});
describe(".clear()", function() {
it("should remove all label sets created with .label()", function() {
let gauge = new prometheus.Gauge(
'test', "Help", { register: false, labels: ['a', 'b'] }
);
gauge.labels({ a: '1', b: '2'});
gauge.labels({ a: '3', b: '4'});
gauge.labels({ a: '5', b: '6'});
gauge.clear();
assert(gauge._values.size == 0, "values was not cleared");
});
it("should throw when used on an unlabled metric", function() {
let gauge = new prometheus.Gauge('test', "Help", { register: false });
assert.throws(
() => gauge.clear(), new Error("Cannot clear unlabeled metric")
);
});
});
});
describe("defaultCollectors", function() {
describe("processStartTimeSeconds", function() {
it("should give a close point in time", async function() {
this.skip(); // XXX Doesn't work in Clusterio's shared testing env
let results = []
let collector = prometheus.defaultCollectors.processStartTimeSeconds;
for await (let result of collector.collect()) {
results.push(result);
}
assert(
results.length == 1,
"collector did not give exactly one result"
);
assert(
Math.abs(results[0].samples.get('') - Date.now() / 1000) < 1,
"value set is not close to the current unix time"
);
});
});
describe("processCpuSecondsTotal", function() {
it("should give a low value", async function() {
this.skip(); // XXX Doesn't work in Clusterio's shared testing env
let results = []
let collector = prometheus.defaultCollectors.processCpuSecondsTotal;
for await (let result of collector.collect()) {
results.push(result);
}
assert(
results.length == 1,
"collector did not give exactly one result"
);
let value = results[0].samples.get('')
assert(
value < 5 && value > 0,
`process time is not between 0 and 5 seconds (${value})`
);
});
});
});
describe("exposition()", function() {
it("should format simple metrics", async function() {
let registry = new prometheus.CollectorRegistry();
let counter = new prometheus.Counter('a', "Help", { register: false });
registry.register(counter);
let gauge = new prometheus.Gauge('b', "Help", { register: false });
registry.register(gauge);
let exposition = await prometheus.exposition(registry.collect());
assert.equal(
exposition,
'# HELP a Help\n' +
'# TYPE a counter\n' +
'a 0\n' +
'\n' +
'# HELP b Help\n' +
'# TYPE b gauge\n' +
'b 0\n'
);
});
it("should format labeled metrics", async function() {
let registry = new prometheus.CollectorRegistry();
let gauge = new prometheus.Gauge(
'test', "Help", { register: false, labels: ['a', 'b']}
);
gauge.labels({a: '1', b: '2'});
gauge.labels({b: 'y', a: 'x'});
registry.register(gauge);
let exposition = await prometheus.exposition(registry.collect());
assert.equal(
exposition,
'# HELP test Help\n' +
'# TYPE test gauge\n' +
'test{a="1",b="2"} 0\n' +
'test{a="x",b="y"} 0\n'
);
});
it("should format Infinity and NaN", async function() {
let registry = new prometheus.CollectorRegistry();
let gauge = new prometheus.Gauge(
'test', "Help", { register: false, labels: ['n']}
);
gauge.labels({n: '1'}).set(Infinity);
gauge.labels({n: '2'}).set(-Infinity);
gauge.labels({n: '3'}).set(NaN);
registry.register(gauge);
let exposition = await prometheus.exposition(registry.collect());
assert.equal(
exposition,
'# HELP test Help\n' +
'# TYPE test gauge\n' +
'test{n="1"} +Inf\n' +
'test{n="2"} -Inf\n' +
'test{n="3"} NaN\n'
);
});
});
let serializedResult = {
metric: { type: 'type', name: 'name', help: "Help", labels: ['a', 'b'] },
samples: [ ['a="1",b="2"', 1], ['a="3",b="4"', 5], ]
};
let deserializedResult = {
metric: new prometheus.Metric('type', 'name', "Help", ['a', 'b']),
samples: new Map([['a="1",b="2"', 1], ['a="3",b="4"', 5]]),
};
describe("serializeResult()", function() {
it("should convert a result into plain object", function() {
assert.deepEqual(
prometheus.serializeResult(deserializedResult),
serializedResult
)
});
it("should support adding labels", function() {
assert.deepEqual(
prometheus.serializeResult({
metric: new prometheus.Metric('type', 'name', "Help"),
samples: new Map([['', 2]]),
}, { addLabels: { c: '8' }}),
{
metric: {type: 'type', name: 'name', help: "Help", labels: ['c']},
samples: [['c="8"', 2]],
}
);
assert.deepEqual(
prometheus.serializeResult({
metric: new prometheus.Metric('type', 'name', "Help", ['a']),
samples: new Map([['a="4"', 3]]),
}, { addLabels: { c: '8' }}),
{
metric: {type: 'type', name: 'name', help: "Help", labels: ['a', 'c']},
samples: [['a="4",c="8"', 3]],
}
);
});
it("should support redefining name and help", function() {
assert.deepEqual(
prometheus.serializeResult({
metric: new prometheus.Metric('type', 'name', "Help"),
samples: new Map([['', 2]]),
}, { metricName: 'new', }),
{
metric: {type: 'type', name: 'new', help: "Help", labels: []},
samples: [['', 2]],
}
);
assert.deepEqual(
prometheus.serializeResult({
metric: new prometheus.Metric('type', 'name', "Help"),
samples: new Map([['', 2]]),
}, { metricHelp: "old", }),
{
metric: {type: 'type', name: 'name', help: "old", labels: []},
samples: [['', 2]],
}
);
});
it("should throw on unrecoginzed option", function() {
assert.throws(
() => new prometheus.serializeResult({
metric: new prometheus.Metric('type', 'name', "Help"),
samples: new Map([['', 3]]),
}, { invalid: true }),
new Error("Unrecognized option 'invalid'")
);
});
});
describe("deserializeResult()", function() {
it("should convert a plain object into a result", function() {
assert.deepEqual(
prometheus.deserializeResult(serializedResult),
deserializedResult
)
});
});
describe("post test checks", function() {
it("should not have any collectors in the default registry", function() {
this.skip(); // XXX Doesn't work in Clusterio's shared testing env
assert(
prometheus.defaultRegistry.collectors.length == 0,
"Collectors left over by test code"
);
});
});
});