Merge pull request #868 from Hornwitser/small-things-16

Small things 16
This commit is contained in:
Hornwitser authored and GitHub committed 2026-03-10 02:51:31 +01:00
commit 0d9573b949
17 files changed
+330 -301

No files matched your search

@@ -32,6 +32,7 @@ export default class BaseConnection extends lib.Link {
this.handle(lib.ModPackGetRequest, this.handleModPackGetRequest.bind(this));
this.handle(lib.ModPackGetDefaultRequest, this.handleModPackGetDefaultRequest.bind(this));
this.handle(lib.ModDownloadRequest, this.handleModDownloadRequest.bind(this));
this.handle(lib.FactorioVersionsRequest, this.handleFactorioVersionsRequest.bind(this));
this.handle(lib.SubscriptionRequest, this.handleSubscriptionRequest.bind(this));
this.connector.on("close", () => {
@@ -106,4 +107,8 @@ export default class BaseConnection extends lib.Link {
async handleSubscriptionRequest(request: lib.SubscriptionRequest, src: lib.Address, dst: lib.Address) {
return await this._controller.subscriptions.handleRequest(this, request, src, dst);
}
async handleFactorioVersionsRequest(request: lib.FactorioVersionsRequest) {
return await this._controller.factorioVersions.get(request.maxAgeMs);
}
}
@@ -127,7 +127,6 @@ export default class ControlConnection extends BaseConnection {
this.handle(lib.PluginUpdateRequest, this.handlePluginUpdateRequest.bind(this));
this.handle(lib.PluginInstallRequest, this.handlePluginInstallRequest.bind(this));
this.handle(lib.DebugDumpWsRequest, this.handleDebugDumpWsRequest.bind(this));
this.handle(lib.FactorioVersionsRequest, this.handleFactorioVersionsRequest.bind(this));
}
validateIngress(message: lib.MessageRequest | lib.MessageEvent) {
@@ -1240,10 +1239,6 @@ export default class ControlConnection extends BaseConnection {
this.connector._socket!.clusterio_ignore_dump = true;
this._controller.debugEvents.on("message", this.ws_dumper);
}
async handleFactorioVersionsRequest(request: lib.FactorioVersionsRequest) {
return await this._controller.factorioVersions.get(request.maxAgeMs);
}
}
module.exports = ControlConnection;
+8 -1
View File
@@ -112,7 +112,14 @@ export default class Controller {
// Cache for mod portal requests
modPortalCache = new Map<string, { timestamp: number, data: any[] }>();
// Cache for factorio versions
factorioVersions = new lib.ValueCache(lib.fetchFactorioVersions);
factorioVersions = new lib.ValueCache(async () => {
try {
return await lib.fetchFactorioVersions();
} catch (err: any) {
logger.warn(`Failed to retrieve Factorio versions ${err.message}`);
return [];
}
});
static async bootstrap(config: lib.ControllerConfig) {
let databaseDirectory = config.get("controller.database_directory");
@@ -81,7 +81,6 @@ export default class HostConnection extends BaseConnection {
this.handle(lib.InstanceSaveDetailsUpdatesEvent, this.handleInstanceSaveDetailsUpdatesEvent.bind(this));
this.handle(lib.LogMessageEvent, this.handleLogMessageEvent.bind(this));
this.handle(lib.InstancePlayerUpdateEvent, this.handleInstancePlayerUpdateEvent.bind(this));
this.handle(lib.FactorioVersionsRequest, this.handleFactorioVersionsRequest.bind(this));
this.snoopEvent(lib.InstanceAdminlistUpdateEvent, this.snoopInstanceAdminlistUpdateEvent.bind(this));
this.snoopEvent(lib.InstanceBanlistUpdateEvent, this.snoopInstanceBanlistUpdateEvent.bind(this));
this.snoopEvent(lib.InstanceWhitelistUpdateEvent, this.snoopInstanceWhitelistUpdateEvent.bind(this));
@@ -359,10 +358,6 @@ export default class HostConnection extends BaseConnection {
});
}
async handleFactorioVersionsRequest(request: lib.FactorioVersionsRequest) {
return await this._controller.factorioVersions.get(request.maxAgeMs);
}
async snoopInstanceAdminlistUpdateEvent(event: lib.InstanceAdminlistUpdateEvent, src: lib.Address) {
if (src.type !== lib.Address.instance) {
return;
+4
View File
@@ -74,6 +74,10 @@ async function discoverInstances(instancesDir: string) {
return instances;
}
// Requests that wake up instances are responsible for making sure the
// instance status is correctly updated even if they fail with an error. This
// means they should typically use try catch on anything that can throw
// and call notifyExit upon any errors.
const instanceStartingMessages = new Set([
lib.InstanceStartRequest.name,
lib.InstanceLoadScenarioRequest.name,
+2 -1
View File
@@ -637,9 +637,10 @@ end`.replace(/\r?\n/g, " ");
}
async init(pluginInfos: lib.PluginNodeEnvInfo[]) {
// After changing the status this function is responsible for calling notifyExit if it throws.
this.notifyStatus("starting");
const factorioVersions = await this.sendTo("controller", new lib.FactorioVersionsRequest());
try {
const factorioVersions = await this.sendTo("controller", new lib.FactorioVersionsRequest());
await this._loadStats();
await this.server.checkForUpdates(factorioVersions);
await this.server.init();
+4
View File
@@ -978,6 +978,10 @@ export class FactorioServer extends events.EventEmitter<FactorioServerEvents> {
.sort((v1, v2) => versionOrder(v1.version, v2.version))
[0];
if (!latestVersion) {
return;
}
const installedVersions = await listFactorioVersions(this._factorioDir);
if (!installedVersions.versions.has(latestVersion.version)) {
const v = latestVersion.version;
+39 -6
View File
@@ -290,18 +290,50 @@ export class KeyValueDatastore<
export class SubscribableDatastore<
V extends SubscribableValue,
> extends Datastore<V["id"], V> {
/** Last time an update occurred */
lastUpdatedAtMs: number = 0;
private static maxUpdatedAtMs(values: Iterable<SubscribableValue>) {
let max = 0;
for (const value of values) {
// Prevent one NaN, undefined, +Inf or similar from breaking the datastore.
if (Number.isFinite(value.updatedAtMs)) {
max = Math.max(max, value.updatedAtMs);
}
}
return max;
}
private incrementLastUpdatedMs() {
this.lastUpdatedAtMs = Math.max(Date.now(), this.lastUpdatedAtMs + 1);
}
constructor(
provider: DatastoreProvider<V["id"], V> = new MemoryDatastoreProvider(),
data = new Map<V["id"], V>(),
) {
super(provider, data);
this.lastUpdatedAtMs = SubscribableDatastore.maxUpdatedAtMs(this.data.values());
}
async load() {
await super.load();
this.lastUpdatedAtMs = SubscribableDatastore.maxUpdatedAtMs(this.data.values());
}
// Set the value in the datastore, be careful of race conditions if you await any functions before calling set
set(value: V) {
this.incrementLastUpdatedMs();
this.data.set(value.id, value);
value.updatedAtMs = Date.now();
value.updatedAtMs = this.lastUpdatedAtMs;
this.touch([value]);
}
// Set many values at once from an array of key value pairs
setMany(values: V[]) {
const nowMs = Date.now();
this.incrementLastUpdatedMs();
for (const value of values) {
value.updatedAtMs = nowMs;
value.updatedAtMs = this.lastUpdatedAtMs;
this.data.set(value.id, value);
}
this.touch(values);
@@ -309,18 +341,19 @@ export class SubscribableDatastore<
// Delete a value in the datastore
delete(value: V) {
this.incrementLastUpdatedMs();
this.data.delete(value.id);
value.updatedAtMs = Date.now();
value.updatedAtMs = this.lastUpdatedAtMs;
value.isDeleted = true;
this.touch([value]);
}
// Delete many values at once from an array of values
deleteMany(values: V[]) {
const nowMs = Date.now();
this.incrementLastUpdatedMs();
for (const value of values) {
value.isDeleted = true;
value.updatedAtMs = nowMs;
value.updatedAtMs = this.lastUpdatedAtMs;
this.data.delete(value.id);
}
this.touch(values);
+2 -2
View File
@@ -1,7 +1,7 @@
import { Static, Type } from "@sinclair/typebox";
import { FullVersionSchema, integerFullVersion, isFullVersion } from "../data";
import { FullVersionSchema, integerFullVersion, isFullVersion } from "../data/version";
const ARCHIVE_URL = "https://factorio.com/download/archive";
const ARCHIVE_URL = "https://factorio.com/download/archive/";
/**
* Represents the parsed result of a factorio version
+19 -6
View File
@@ -272,18 +272,31 @@ function labelsToKey(labels: LabelValues, metricLabels: string[]) {
function keyToLabels(key: string) {
let labels = new Map<string, string>();
if (key !== "") {
for (let pair of key.split(",")) {
let [name, value] = pair.split("=", 2);
if (!name || value === undefined) {
continue;
let pos = 0;
let matchLabel = /([^=]+)="(([^\\"]|\\["n\\])+)"/y;
while (pos < key.length) {
if (pos !== 0) {
/* istanbul ignore if */
if (key[pos] !== ",") {
// Should be unreachable
throw Error(`Expected , at pos ${pos} in "${key}"`);
}
pos += 1;
}
matchLabel.lastIndex = pos;
const match = matchLabel.exec(key);
/* istanbul ignore else */
if (match) {
let [, name, value] = match;
labels.set(name, value
.slice(1, -1)
.replace(/\\"/g, "\"")
.replace(/\\n/g, "\n")
.replace(/\\\\/g, "\\")
);
pos += match[0].length;
} else {
// Should be unreachable
throw new Error(`Failed to parse label at pos ${pos} in "${key}"`);
}
}
+7 -2
View File
@@ -82,7 +82,11 @@ function NpmPluginInstall({ setApplyAction, form, target }: NpmActionProps) {
</>;
}
export function NpmButton(props: { target: lib.AddressShorthand, canRestart?: boolean, disabled?: boolean }) {
export function NpmButton(props: {
target: "controller" | { hostId: number },
canRestart?: boolean,
disabled?: boolean
}) {
const control = useContext(ControlContext);
const account = useAccount();
const [open, setOpen] = useState(false);
@@ -93,6 +97,7 @@ export function NpmButton(props: { target: lib.AddressShorthand, canRestart?: bo
remoteUpdates: true, pluginUpdates: true, pluginInstall: true,
});
const hostId = props.target === "controller" ? undefined : props.target.hostId;
useEffect(() => {
if (props.target === "controller") {
if (!account.hasPermission("core.controller.get_config")) {
@@ -121,7 +126,7 @@ export function NpmButton(props: { target: lib.AddressShorthand, canRestart?: bo
});
}).catch(notifyErrorHandler("Failed to fetch host config for remote updates"));
}
}, [props.target]);
}, [hostId]);
function onValuesChange({ action } : { action?: string }) {
if (action) {
+22
View File
@@ -252,6 +252,28 @@ describe("controller/src/Controller", function() {
assert.equal(controller.config.restartRequired, true);
});
});
describe(".factorioVersions", function() {
const _fetch = global.fetch;
const fetchCalls = [];
before(function() {
global.fetch = async (url) => {
fetchCalls.push(url);
return { ok: false, status: 503, statusText: "Service Unavailable" };
};
});
it("should handle fetch failure", async function() {
await assert.deepEqual(
await controller.factorioVersions.get(0),
[],
);
assert(fetchCalls.length === 1, "expected one fetch call");
assert(fetchCalls[0].indexOf("/download") !== -1, "fetch call to be to a URL with /download in it");
});
after(function() {
global.fetch = _fetch;
});
});
});
describe("finalise", function() {
describe(".finaliseHosts()", function() {
+6
View File
@@ -37,6 +37,12 @@ describe("controller/User", function () {
user.saveRecord();
assert(user.updatedAtMs > prev);
});
it("should always increment updatedAtMs even if called in quick succession", function () {
user.saveRecord();
let prev = user.updatedAtMs;
user.saveRecord();
assert(user.updatedAtMs > prev, "updatedAtMs did not increase");
});
});
describe(".set()", function () {
+116 -256
View File
@@ -342,264 +342,124 @@ describe("host/server", function() {
});
});
describe("full version", function() {
it("should do nothing when there is no newer version", async function() {
server._factorioDir = path.join("test", "file", "factorio");
server._targetVersion = "0.1.1";
await server.checkForUpdates([{
stable: true,
version: "0.1.1",
headlessUrl: "test1",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test2",
}]);
for (const [suite, target] of [
["full version", "0.1.1"],
["partial version", "0.1"],
["latest version", "latest"],
]) {
/* eslint-disable no-loop-func */
describe(suite, function() {
it("should do nothing when there are no versions", async function() {
server._factorioDir = path.join("test", "file", "factorio");
server._targetVersion = target;
await server.checkForUpdates([]);
assert.equal(fetchCalledWith, null);
assert.equal(fetchCalledWith, null);
});
if (target !== "latest") {
it("should do nothing when no version matches", async function() {
server._factorioDir = path.join("test", "file", "factorio");
server._targetVersion = target;
await server.checkForUpdates([{
stable: true,
version: "0.2.1",
headlessUrl: "test1",
}, {
stable: false,
version: "0.2.0",
headlessUrl: "test2",
}]);
assert.equal(fetchCalledWith, null);
});
}
it("should do nothing when there is no newer version", async function() {
server._factorioDir = path.join("test", "file", "factorio");
server._targetVersion = target;
await server.checkForUpdates([{
stable: true,
version: "0.1.1",
headlessUrl: "test1",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test2",
}]);
assert.equal(fetchCalledWith, null);
});
it("should do nothing for direct installs", async function() {
server._factorioDir = path.join("test", "file", "factorio", "0.1.1");
server._targetVersion = target === "0.1.1" ? "0.1.5" : target;
await server.checkForUpdates([{
stable: true,
version: "0.1.5",
headlessUrl: "test1",
}, {
stable: true,
version: "0.1.1",
headlessUrl: "test1",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test2",
}]);
assert.equal(fetchCalledWith, null);
});
it("should do nothing when on windows", async function() {
let logLine = null;
server._logger = { info: line => { logLine = line; } };
Object.defineProperty(process, "platform", { value: "win32" });
server._factorioDir = path.join("test", "file", "factorio");
server._targetVersion = target === "0.1.1" ? "0.1.5" : target;
await server.checkForUpdates([{
stable: true,
version: "0.1.5",
headlessUrl: "test1",
}, {
stable: true,
version: "0.1.1",
headlessUrl: "test2",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test3",
}]);
assert.equal(fetchCalledWith, null);
assert.ok(logLine !== null);
assert.ok(logLine.endsWith("but must be manually downloaded"));
});
it("should do attempt to download on linux", async function() {
let logLine = null;
server._logger = { info: line => { logLine = line; } };
Object.defineProperty(process, "platform", { value: "linux" });
server._factorioDir = path.join("test", "file", "factorio");
server._targetVersion = target === "0.1.1" ? "0.1.5" : target;
await assert.rejects(server.checkForUpdates([{
stable: true,
version: "0.1.5",
headlessUrl: "test1",
}, {
stable: true,
version: "0.1.1",
headlessUrl: "test2",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test3",
}]), new Error("Failed to fetch test1: -1 Fetch called"));
assert.equal(fetchCalledWith, "test1");
assert.ok(logLine !== null);
assert.ok(logLine.endsWith("starting download..."));
});
});
it("should do nothing for direct installs", async function() {
server._factorioDir = path.join("test", "file", "factorio", "0.1.1");
server._targetVersion = "0.1.5";
await server.checkForUpdates([{
stable: true,
version: "0.1.5",
headlessUrl: "test1",
}, {
stable: true,
version: "0.1.1",
headlessUrl: "test1",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test2",
}]);
assert.equal(fetchCalledWith, null);
});
it("should do nothing when on windows", async function() {
let logLine = null;
server._logger = { info: line => { logLine = line; } };
Object.defineProperty(process, "platform", { value: "win32" });
server._factorioDir = path.join("test", "file", "factorio");
server._targetVersion = "0.1.5";
await server.checkForUpdates([{
stable: true,
version: "0.1.5",
headlessUrl: "test1",
}, {
stable: true,
version: "0.1.1",
headlessUrl: "test2",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test3",
}]);
assert.equal(fetchCalledWith, null);
assert.ok(logLine !== null);
assert.ok(logLine.endsWith("but must be manually downloaded"));
});
it("should do attempt to download on linux", async function() {
let logLine = null;
server._logger = { info: line => { logLine = line; } };
Object.defineProperty(process, "platform", { value: "linux" });
server._factorioDir = path.join("test", "file", "factorio");
server._targetVersion = "0.1.5";
await assert.rejects(server.checkForUpdates([{
stable: true,
version: "0.1.5",
headlessUrl: "test1",
}, {
stable: true,
version: "0.1.1",
headlessUrl: "test2",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test3",
}]), new Error("Failed to fetch test1: -1 Fetch called"));
assert.equal(fetchCalledWith, "test1");
assert.ok(logLine !== null);
assert.ok(logLine.endsWith("starting download..."));
});
});
describe("partial version", function() {
it("should do nothing when there is no newer version", async function() {
server._factorioDir = path.join("test", "file", "factorio");
server._targetVersion = "0.1";
await server.checkForUpdates([{
stable: true,
version: "0.1.1",
headlessUrl: "test1",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test2",
}]);
assert.equal(fetchCalledWith, null);
});
it("should do nothing for direct installs", async function() {
server._factorioDir = path.join("test", "file", "factorio", "0.1.1");
server._targetVersion = "0.1";
await server.checkForUpdates([{
stable: true,
version: "0.1.5",
headlessUrl: "test1",
}, {
stable: true,
version: "0.1.1",
headlessUrl: "test1",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test2",
}]);
assert.equal(fetchCalledWith, null);
});
it("should do nothing when on windows", async function() {
let logLine = null;
server._logger = { info: line => { logLine = line; } };
Object.defineProperty(process, "platform", { value: "win32" });
server._factorioDir = path.join("test", "file", "factorio");
server._targetVersion = "0.1";
await server.checkForUpdates([{
stable: true,
version: "0.1.5",
headlessUrl: "test1",
}, {
stable: true,
version: "0.1.1",
headlessUrl: "test2",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test3",
}]);
assert.equal(fetchCalledWith, null);
assert.ok(logLine !== null);
assert.ok(logLine.endsWith("but must be manually downloaded"));
});
it("should do attempt to download on linux", async function() {
let logLine = null;
server._logger = { info: line => { logLine = line; } };
Object.defineProperty(process, "platform", { value: "linux" });
server._factorioDir = path.join("test", "file", "factorio");
server._targetVersion = "0.1";
await assert.rejects(server.checkForUpdates([{
stable: true,
version: "0.1.5",
headlessUrl: "test1",
}, {
stable: true,
version: "0.1.1",
headlessUrl: "test2",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test3",
}]), new Error("Failed to fetch test1: -1 Fetch called"));
assert.equal(fetchCalledWith, "test1");
assert.ok(logLine !== null);
assert.ok(logLine.endsWith("starting download..."));
});
});
describe("latest version", function() {
it("should do nothing when there is no newer version", async function() {
server._factorioDir = path.join("test", "file", "factorio");
server._targetVersion = "latest";
await server.checkForUpdates([{
stable: true,
version: "0.1.1",
headlessUrl: "test1",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test2",
}]);
assert.equal(fetchCalledWith, null);
});
it("should do nothing for direct installs", async function() {
server._factorioDir = path.join("test", "file", "factorio", "0.1.1");
server._targetVersion = "latest";
await server.checkForUpdates([{
stable: true,
version: "0.1.5",
headlessUrl: "test1",
}, {
stable: true,
version: "0.1.1",
headlessUrl: "test1",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test2",
}]);
assert.equal(fetchCalledWith, null);
});
it("should do nothing when on windows", async function() {
let logLine = null;
server._logger = { info: line => { logLine = line; } };
Object.defineProperty(process, "platform", { value: "win32" });
server._factorioDir = path.join("test", "file", "factorio");
server._targetVersion = "latest";
await server.checkForUpdates([{
stable: true,
version: "0.1.5",
headlessUrl: "test1",
}, {
stable: true,
version: "0.1.1",
headlessUrl: "test2",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test3",
}]);
assert.equal(fetchCalledWith, null);
assert.ok(logLine !== null);
assert.ok(logLine.endsWith("but must be manually downloaded"));
});
it("should do attempt to download on linux", async function() {
let logLine = null;
server._logger = { info: line => { logLine = line; } };
Object.defineProperty(process, "platform", { value: "linux" });
server._factorioDir = path.join("test", "file", "factorio");
server._targetVersion = "latest";
await assert.rejects(server.checkForUpdates([{
stable: true,
version: "0.1.5",
headlessUrl: "test1",
}, {
stable: true,
version: "0.1.1",
headlessUrl: "test2",
}, {
stable: false,
version: "0.1.0",
headlessUrl: "test3",
}]), new Error("Failed to fetch test1: -1 Fetch called"));
assert.equal(fetchCalledWith, "test1");
assert.ok(logLine !== null);
assert.ok(logLine.endsWith("starting download..."));
});
});
/* eslint-enable no-loop-func */
}
it("should download a version correctly (live api)", async function() {
slowTest(this);
if (_platform !== "linux") {
+76 -15
View File
@@ -5,13 +5,6 @@ const lib = require("@clusterio/lib");
const fs = require("fs-extra");
const path = require("path");
async function incrementDateMs() {
const prev = Date.now();
do {
await lib.wait(1);
} while (prev >= Date.now());
}
class MockDatastoreProvider extends lib.DatastoreProvider {
constructor() {
super();
@@ -571,12 +564,31 @@ describe("lib/datastore", function() {
eventsRaised = [];
datastoreProvider = new MockDatastoreProvider();
datastoreProvider.value = new Map([
["foo", {id: "foo"}], ["bar", {id: "bar"}], ["baz", {id: "baz"}],
["foo", {id: "foo", updatedAtMs: 10}], ["bar", {id: "bar"}], ["baz", {id: "baz"}],
]);
datastore = new lib.SubscribableDatastore(datastoreProvider, datastoreProvider.value);
datastore.on("update", updates => eventsRaised.push(updates));
});
function forceFuture() {
const future = {id: "future"};
for (let i = 1; i < 100; i++) {
datastore.set(future);
}
return future.updatedAtMs;
}
describe("lastUpdateAtMs", function() {
it("is set to the highest value after constructing a new instance", function() {
assert(datastore.lastUpdatedAtMs === 10, `${datastore.lastUpdatedAtMs} !== 10`);
});
it("is set to the highest value after loading from a provider", async function() {
const future = Date.now() + 2000;
datastore.provider.value = ["future", {id: "future", updatedAtMs: future}];
await datastore.load();
assert(datastore.lastUpdatedAtMs === future, `${datastore.lastUpdatedAtMs} !== ${future}`);
});
});
describe("set", function() {
it("can set a value", function() {
const value = { id: "test" };
@@ -598,12 +610,21 @@ describe("lib/datastore", function() {
const value = { id: "test" };
datastore.set(value);
assert.notEqual(value.updatedAtMs, undefined);
assert(value.updatedAtMs > 0, "updatedAtMs greater than zero");
assert(value.updatedAtMs > 0, "updatedAtMs not greater than zero");
});
it("increments the updatedAtMs property", async function() {
const value = { id: "test" };
datastore.set(value);
const prev = value.updatedAtMs;
await incrementDateMs();
datastore.set(value);
assert(value.updatedAtMs > prev, "updatedAtMs must increase");
});
it("increments the updatedAtMs property past existing values", async function() {
const future = forceFuture();
const value = { id: "test" };
datastore.set(value);
assert(value.updatedAtMs > future, "updatedAtMs must increase");
});
});
describe("setMany", function() {
@@ -632,11 +653,22 @@ describe("lib/datastore", function() {
assert.notEqual(updates[1].updatedAtMs, undefined);
assert(updates[0].updatedAtMs > 0, "updatedAtMs greater than zero");
assert(updates[1].updatedAtMs > 0, "updatedAtMs greater than zero");
const prev = [updates[0].updatedAtMs, updates[1].updatedAtMs];
await incrementDateMs();
});
it("increments the updatedAtMs property", async function() {
const value = { id: "test1" };
datastore.set(value);
const prev = value.updatedAtMs;
const updates = [{ id: "test1" }, { id: "test2" }];
datastore.setMany(updates);
assert(updates[0].updatedAtMs > prev[0], "updatedAtMs must increase");
assert(updates[1].updatedAtMs > prev[1], "updatedAtMs must increase");
assert(updates[0].updatedAtMs > prev, "updatedAtMs must increase");
assert(updates[1].updatedAtMs > prev, "updatedAtMs must increase");
});
it("increments the updatedAtMs property past existing values", async function() {
const future = forceFuture();
const updates = [{ id: "test1" }, { id: "test2" }];
datastore.setMany(updates);
assert(updates[0].updatedAtMs > future, "updatedAtMs must increase");
assert(updates[1].updatedAtMs > future, "updatedAtMs must increase");
});
});
@@ -667,7 +699,20 @@ describe("lib/datastore", function() {
const value = { id: "test" };
datastore.delete(value);
assert.notEqual(value.updatedAtMs, undefined);
assert(value.updatedAtMs > 0, "updatedAtMs greater than zero");
assert(value.updatedAtMs > 0, "updatedAtMs not greater than zero");
});
it("increments the updatedAtMs property", function() {
const value = { id: "test" };
datastore.set(value);
const prev = value.updatedAtMs;
datastore.delete(value);
assert(value.updatedAtMs > prev, "updatedAtMs must increase");
});
it("increments the updatedAtMs property past existing values", function() {
const future = forceFuture();
const value = { id: "test" };
datastore.delete(value);
assert(value.updatedAtMs > future, "updatedAtMs must increase");
});
it("sets the isDeleted property", function() {
const value = { id: "test" };
@@ -704,6 +749,22 @@ describe("lib/datastore", function() {
assert(values[0].updatedAtMs > 0, "updatedAtMs greater than zero");
assert(values[1].updatedAtMs > 0, "updatedAtMs greater than zero");
});
it("increments the updatedAtMs property", function() {
const value = { id: "foo" };
datastore.set(value);
const prev = value.updatedAtMs;
const values = [{id: "foo", updatedAtMs: prev}, {id: "bar", updatedAtMs: prev}];
datastore.deleteMany(values);
assert(values[0].updatedAtMs > prev, "updatedAtMs must increase");
assert(values[1].updatedAtMs > prev, "updatedAtMs must increase");
});
it("increments the updatedAtMs property past existing values", function() {
const future = forceFuture();
const values = [{id: "foo"}, {id: "bar"}];
datastore.deleteMany(values);
assert(values[0].updatedAtMs > future, "updatedAtMs must increase");
assert(values[1].updatedAtMs > future, "updatedAtMs must increase");
});
it("sets the isDeleted property", function() {
const values = [{id: "foo"}, {id: "bar"}];
datastore.deleteMany(values);
+6
View File
@@ -121,6 +121,12 @@ describe("FactorioVersions", function() {
assert.equal(versions.filter(v => v.stable).length, 48);
assert.equal(versions.filter(v => !v.stable).length, 57);
});
it("parses versions from empty html file", async function() {
mockFetch("");
const versions = await fetchFactorioVersions();
assert.equal(versions.length, 0);
});
it("parses versions from live api", async function() {
slowTest(this);
global.fetch = originalFetch;
+14 -2
View File
@@ -651,8 +651,20 @@ describe("lib/prometheus", 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\\' });
let labelValue = "";
// Include every ASCII codepoint
for (let i = 0; i < 128; i++) {
labelValue += String.fromCodePoint(i);
}
// Include Unicode codepoints from the basic and supplementary planes.
labelValue += "©💣";
gauge.labels(labelValue, "2");
gauge.removeAll({ a: labelValue });
assert(gauge._values.size === 0, "Matching labels remained");
gauge.labels("1", labelValue);
gauge.removeAll({ a: "1" });
assert(gauge._values.size === 0, "Matching labels remained");
});
});