@neume-network/core
Version:
neume network is a community-owned network to scale music and web3 - connecting Artist DAOs, Tokens, NFTs and whatever comes next.
324 lines (288 loc) • 8.48 kB
JavaScript
//@format
import { constants } from "fs";
import { access, unlink } from "fs/promises";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";
import EventEmitter from "events";
import test from "ava";
import { loadStrategies } from "../src/disc.mjs";
import {
extract,
transform,
setupFinder,
EXTRACTOR_CODES,
prepareMessages,
validateCrawlPath,
} from "../src/lifecycle.mjs";
import {
ValidationError,
NotFoundError,
NotImplementedError,
} from "../src/errors.mjs";
const __dirname = dirname(fileURLToPath(import.meta.url));
const mockMessageCommissioner = "mockCommissioner";
const mockMessage = {
type: "https",
version: "0.0.1",
error: null,
results: null,
options: {
url: "",
method: "GET",
},
};
test("if function transform gracefully returns when sourceFile doesn't exist", async (t) => {
const strategy = {
module: {
name: "test-strategy",
},
};
const sourcePath = "doesn't exist";
const result = await transform(strategy, sourcePath);
t.falsy(result);
});
test("if launcher throws errors on invalid strategy type", async (t) => {
const finder = await setupFinder();
t.throws(() => finder({ type: "non-existent" }), {
instanceOf: NotFoundError,
});
});
test("if launcher throws errors on invalid strategy name submission", async (t) => {
const message0 = { type: "extraction", name: "non-existent" };
const message1 = { type: "extraction", name: "non-existent" };
const finder = await setupFinder();
t.throws(() => finder(message0), {
instanceOf: NotFoundError,
});
t.throws(() => finder(message1), {
instanceOf: NotFoundError,
});
});
test("reading a file by line using the line reader", async (t) => {
const sourcePath = resolve(__dirname, "./fixtures/file0.data");
const outputPath = resolve(__dirname, "./fixtures/file0.output");
let count = 0;
t.plan(3);
const lineHandlerMock = (line) => {
if (count === 0) t.is(line, "line0");
if (count === 1) t.is(line, "line1");
count++;
return { write: "hello world", messages: [] };
};
const strategies = (
await loadStrategies("./strategies", "transformer.mjs")
).filter(
(strategy) =>
strategy &&
strategy.module &&
strategy.module.name === "soundxyz-call-tokenuri"
);
const strategy = { ...strategies[0].module, onLine: lineHandlerMock };
await transform({ module: strategy }, sourcePath, outputPath, []);
t.is(count, 2);
await unlink(outputPath);
});
test("applying transformation strategies to a file", async (t) => {
const sourcePath = resolve(__dirname, "./fixtures/file1.data");
const outputPath = resolve(__dirname, "./fixtures/file1.output");
const strategies = (
await loadStrategies("./strategies", "transformer.mjs")
).filter(
(strategy) =>
strategy &&
strategy.module &&
strategy.module.name === "soundxyz-call-tokenuri"
);
await transform(strategies[0], sourcePath, outputPath, []);
try {
await access(outputPath, constants.R_OK);
} catch (err) {
t.log(err);
t.fail();
return;
}
await unlink(outputPath);
t.pass();
});
test("strategy transformer should receive inputs", async (t) => {
const sourcePath = resolve(__dirname, "./fixtures/file1.data");
const outputPath = resolve(__dirname, "./fixtures/file2.output");
t.plan(3);
const lineHandlerMock = (line, arg1) => {
t.is(arg1, "arg1");
return { write: "hello world", messages: [] };
};
const strategies = (
await loadStrategies("./strategies", "transformer.mjs")
).filter(
(strategy) =>
strategy &&
strategy.module &&
strategy.module.name === "soundxyz-call-tokenuri"
);
const strategy = { ...strategies[0].module, onLine: lineHandlerMock };
await transform({ module: strategy }, sourcePath, outputPath, ["arg1"]);
try {
await access(outputPath, constants.R_OK);
} catch (err) {
t.log(err);
t.fail();
return;
}
await unlink(outputPath);
t.pass();
});
test("loading strategies", async (t) => {
const pathTip = "../test/fixtures/strategies";
const fileName = "extractor.mjs";
const strategies = await loadStrategies(pathTip, fileName);
t.is(strategies.length, 1);
t.truthy(strategies);
});
test("if extract rejects result if it is invalid", async (t) => {
const mockStrategy = {
module: {
name: "mockMessage",
init: () => {
return false;
},
},
};
class Worker extends EventEmitter {
postMessage(message) {
return router.emit(`${message.commissioner}-extraction`, message);
}
}
const worker = new Worker();
const router = new EventEmitter();
await t.throwsAsync(async () => {
try {
await extract(mockStrategy, worker, router);
} catch (e) {
throw e;
}
});
});
test("if extract function can handle bad results from update", async (t) => {
const mockStrategy = {
module: {
name: mockMessageCommissioner,
init: () => {
return {
messages: [mockMessage],
write: null,
};
},
update: () => {
return false;
},
},
};
class Worker extends EventEmitter {
postMessage(message) {
return router.emit(`${message.commissioner}-extraction`, message);
}
}
const worker = new Worker();
const router = new EventEmitter();
await t.throwsAsync(async () => await extract(mockStrategy, worker, router));
t.is(router.eventNames().length, 0);
});
test("if extract function can handle lifecycle errors", async (t) => {
const mockStrategy = {
module: {
name: mockMessageCommissioner,
init: () => {
return {
messages: [{ ...mockMessage, error: "this is an error" }],
write: null,
};
},
update: () => {
t.fail();
},
},
};
class Worker extends EventEmitter {
postMessage(message) {
return router.emit(`${message.commissioner}-extraction`, message);
}
}
const worker = new Worker();
const router = new EventEmitter();
const { code } = await extract(mockStrategy, worker, router);
t.is(code, EXTRACTOR_CODES.SHUTDOWN_IN_UPDATE);
t.is(router.eventNames().length, 0);
});
test("if extract() resolves the promise and removes the listener on no new messages", async (t) => {
const mockStrategy = {
module: {
name: mockMessageCommissioner,
init: () => {
return { messages: [mockMessage], write: null };
},
update: () => {
return { messages: [], write: null };
},
},
};
class Worker extends EventEmitter {
postMessage(message) {
return router.emit(`${message.commissioner}-extraction`, message);
}
}
const worker = new Worker();
const router = new EventEmitter();
await extract(mockStrategy, worker, router);
t.deepEqual(router.eventNames(), []);
t.pass();
});
test("if extract() resolves the promise and removes the listener on no message from init", async (t) => {
const mockStrategy = {
module: {
name: "mock",
init: () => {
return { messages: [], write: null };
},
update: () => {
return { messages: [], write: null };
},
},
};
class Worker extends EventEmitter {
postMessage(message) {
return router.emit(`${message.commissioner}-extraction`, message);
}
}
const worker = new Worker();
const router = new EventEmitter();
const { code } = await extract(mockStrategy, worker, router);
t.is(code, EXTRACTOR_CODES.SHUTDOWN_IN_INIT);
t.deepEqual(router.eventNames(), []);
t.pass();
});
test("if prepareMessages filters invalid message and prepare message for worker", async (t) => {
const messages = [mockMessage, {}, { ...mockMessage, type: "invalid-type" }];
const preparedMessages = prepareMessages(messages, mockMessageCommissioner);
t.is(preparedMessages.length, 1);
t.is(preparedMessages[0].commissioner, mockMessageCommissioner);
});
test("if filterValidWorkerMessages throws error on invalid input", async (t) => {
t.throws(() => prepareMessages(null));
});
test("validateCrawlPath works for happy case", (t) => {
t.notThrows(() =>
validateCrawlPath([
[
{
name: "web3subgraph",
extractor: { args: ["9956"] },
transformer: {},
},
],
])
);
});
test("validateCrawlPath throws for empty crawl path", (t) => {
t.throws(() => validateCrawlPath([]));
});