* made parallel generic (not tied to TaskTarget) * pulled common higher-order/frontend operations into io.ts * split timelinize specific functionality into own file * Tests made to pass and match previous facebook export snapshots _exactly_
227 lines
7.5 KiB
TypeScript
227 lines
7.5 KiB
TypeScript
import test from "node:test";
|
|
import nodePath from "node:path";
|
|
import { strict as assert } from "node:assert/strict";
|
|
import {
|
|
TaskTarget,
|
|
cd,
|
|
glob as taskGlob,
|
|
read,
|
|
cmd,
|
|
assignMeta,
|
|
verify,
|
|
} from "../data-export/task.ts";
|
|
|
|
const THIS_FILE = import.meta.dirname;
|
|
const FIXTURE_DIR = nodePath.join(THIS_FILE, 'fixtures/facebook-json-2021-05-01');
|
|
const FIXTURE_FILE = nodePath.join(FIXTURE_DIR, 'friends/friends.json');
|
|
|
|
// -- TaskTarget ---------------------------------------------------------------
|
|
|
|
test("TaskTarget: constructor initializes path, pipeline", () => {
|
|
const t = new TaskTarget("/foo/bar");
|
|
assert.equal(t.path, "/foo/bar");
|
|
assert.deepEqual(t.pipeline, []);
|
|
});
|
|
|
|
test("TaskTarget: exists() returns true for a real file", () => {
|
|
assert.equal(new TaskTarget(FIXTURE_FILE).exists(), true);
|
|
});
|
|
|
|
test("TaskTarget: exists() returns false for a missing file", () => {
|
|
assert.equal(new TaskTarget("/nonexistent-file-xyz").exists(), false);
|
|
});
|
|
|
|
test("TaskTarget: basename safe-ifies the path basename", () => {
|
|
const t = new TaskTarget("/foo/bar/some-file.txt");
|
|
assert.equal(t.basename, "some_file_txt");
|
|
});
|
|
|
|
test("TaskTarget: basenameN returns last n path segments joined with ___", () => {
|
|
const t = new TaskTarget("/a/b/c/d");
|
|
assert.equal(t.basenameN(2), "c___d");
|
|
assert.equal(t.basenameN(1), "d");
|
|
});
|
|
|
|
test("TaskTarget: id throws when no idValue is set", () => {
|
|
assert.throws(() => new TaskTarget("/foo").id, /must have an id/);
|
|
});
|
|
|
|
test("TaskTarget: id with a string value is safe-ified", () => {
|
|
const t = new TaskTarget("/foo").assignMeta({ idValue: "my-id" });
|
|
assert.equal(t.id, "my_id");
|
|
});
|
|
|
|
test("TaskTarget: id with a function value is resolved against the target", () => {
|
|
const t = new TaskTarget("/foo/bar").assignMeta({ idValue: tgt => tgt.basename });
|
|
assert.equal(t.id, "bar");
|
|
});
|
|
|
|
test("TaskTarget: cd with an absolute path replaces the path", () => {
|
|
const t = new TaskTarget("/foo");
|
|
t.cd("/bar/baz");
|
|
assert.equal(t.path, "/bar/baz");
|
|
});
|
|
|
|
test("TaskTarget: cd with a relative path joins with the current path", () => {
|
|
const t = new TaskTarget("/foo");
|
|
t.cd("bar");
|
|
assert.equal(t.path, "/foo/bar");
|
|
});
|
|
|
|
test("TaskTarget: read adds a read op to the pipeline", () => {
|
|
const t = new TaskTarget("/foo/bar.txt");
|
|
t.read();
|
|
assert.equal(t.pipeline.length, 1);
|
|
assert.equal(t.pipeline[0].type, "read");
|
|
});
|
|
|
|
test("TaskTarget: cmd adds a mid op to the pipeline", () => {
|
|
const t = new TaskTarget("/foo");
|
|
t.cmd("jq .");
|
|
assert.equal(t.pipeline.length, 1);
|
|
assert.equal(t.pipeline[0].type, "mid");
|
|
});
|
|
|
|
test("TaskTarget: pushToPipeline throws if read is not the first op", () => {
|
|
const t = new TaskTarget("/foo");
|
|
t.cmd("jq .");
|
|
assert.throws(() => t.read(), /first item/);
|
|
});
|
|
|
|
test("TaskTarget: clone produces an independent copy", () => {
|
|
const t = new TaskTarget("/foo").assignMeta({
|
|
idValue: "orig",
|
|
columnMeta: ["any"]
|
|
});
|
|
t.read();
|
|
const c = t.clone();
|
|
assert.equal(c.path, "/foo");
|
|
assert.equal(c.id, "orig");
|
|
assert(c.pipeline !== t.pipeline); // Different object references
|
|
assert.equal(c.pipeline.length, 1);
|
|
assert(c.columnMeta !== t.columnMeta); // Different object references
|
|
c.path = "/other";
|
|
assert.equal(t.path, "/foo"); // original unchanged
|
|
});
|
|
|
|
test("TaskTarget: glob returns matching TaskTargets from disk", () => {
|
|
const t = new TaskTarget(FIXTURE_DIR);
|
|
const results = t.glob("friends/*.json");
|
|
assert.ok(results.length > 0);
|
|
assert.ok(results.every(r => r instanceof TaskTarget));
|
|
assert.ok(results.every(r => r.path.endsWith(".json")));
|
|
});
|
|
|
|
// -- toShell / shEscape -------------------------------------------------------
|
|
|
|
test("toShell: a single read produces a cat command", () => {
|
|
const t = new TaskTarget("/foo/bar.txt");
|
|
t.read();
|
|
assert.equal(t.toShell(), "cat /foo/bar.txt");
|
|
});
|
|
|
|
test("toShell: read piped into cmd", () => {
|
|
const t = new TaskTarget("/foo/bar.txt");
|
|
t.read();
|
|
t.cmd("jq .");
|
|
assert.equal(t.toShell(), "cat /foo/bar.txt | jq .");
|
|
});
|
|
|
|
for (const c of " $!&".split("")) {
|
|
test(`toShell: quotes paths that contain ${JSON.stringify(c)}`, () => {
|
|
const t = new TaskTarget(`/foo/bar${c}baz.txt`);
|
|
t.read();
|
|
assert.equal(t.toShell(), `cat $'/foo/bar${c}baz.txt'`);
|
|
});
|
|
}
|
|
test(`toShell: quotes and escapes paths that contain '`, () => {
|
|
const t = new TaskTarget(`/foo/bar'baz.txt`);
|
|
t.read();
|
|
assert.equal(t.toShell(), `cat $'/foo/bar\\'baz.txt'`);
|
|
});
|
|
|
|
test("toShell: cmd with array splits tokens", () => {
|
|
const t = new TaskTarget("/foo");
|
|
t.cmd(["jq", "."]);
|
|
assert.equal(t.toShell(), "jq .");
|
|
});
|
|
|
|
test("toShell: cmd with function resolves at shell-generation time", () => {
|
|
const t = new TaskTarget("/foo/bar.json");
|
|
t.cmd(tgt => `jq -r .name ${tgt.path}`);
|
|
assert.equal(t.toShell(), "jq -r .name /foo/bar.json");
|
|
});
|
|
|
|
// -- module-level functions ---------------------------------------------------
|
|
|
|
test("cd: clones and changes directory of each target", async () => {
|
|
const targets = [new TaskTarget("/a"), new TaskTarget("/b")];
|
|
const result = await cd("sub")(targets);
|
|
assert.equal(result[0].path, "/a/sub");
|
|
assert.equal(result[1].path, "/b/sub");
|
|
assert.equal(targets[0].path, "/a"); // originals unchanged
|
|
});
|
|
|
|
test("read: clones and adds a read op to each target", async () => {
|
|
const targets = [new TaskTarget("/a.txt"), new TaskTarget("/b.txt")];
|
|
const result = await read()(targets);
|
|
assert.equal(result[0].pipeline[0].type, "read");
|
|
assert.equal(result[1].pipeline[0].type, "read");
|
|
assert.equal(targets[0].pipeline.length, 0); // originals unchanged
|
|
});
|
|
|
|
test("cmd: clones and appends a cmd op to each target", async () => {
|
|
const targets = [new TaskTarget("/a.txt")];
|
|
targets[0].read();
|
|
const result = await cmd("jq .")(targets);
|
|
assert.equal(result[0].pipeline.length, 2);
|
|
assert.equal(targets[0].pipeline.length, 1); // original unchanged
|
|
});
|
|
|
|
test("assignMeta: clones and sets meta on each target", async () => {
|
|
const targets = [new TaskTarget("/a"), new TaskTarget("/b")];
|
|
const result = await assignMeta({ idValue: "myid" })(targets);
|
|
assert.equal(result[0].id, "myid");
|
|
assert.equal(result[1].id, "myid");
|
|
assert.throws(() => targets[0].id); // originals have no id
|
|
});
|
|
|
|
test("taskGlob: returns matching targets across all input targets", async () => {
|
|
const targets = [new TaskTarget(FIXTURE_DIR)];
|
|
const result = await taskGlob("friends/*.json")(targets);
|
|
assert.ok(result.length > 0);
|
|
assert.ok(result.every(r => r.path.endsWith(".json")));
|
|
});
|
|
|
|
// -- verify -------------------------------------------------------------------
|
|
|
|
test("verify: removes targets with an empty pipeline", async () => {
|
|
const t = new TaskTarget(FIXTURE_FILE);
|
|
const result = await verify([t]);
|
|
assert.equal(result.length, 0);
|
|
});
|
|
|
|
test("verify: removes targets whose file does not exist", async () => {
|
|
const t = new TaskTarget("/nonexistent-file-xyz");
|
|
t.read();
|
|
const result = await verify([t]);
|
|
assert.equal(result.length, 0);
|
|
});
|
|
|
|
test("verify: keeps targets that exist and have a pipeline", async () => {
|
|
const t = new TaskTarget(FIXTURE_FILE);
|
|
t.read();
|
|
const result = await verify([t]);
|
|
assert.equal(result.length, 1);
|
|
assert.equal(result[0].path, FIXTURE_FILE);
|
|
});
|
|
|
|
test("verify: filters a mixed list to only valid targets", async () => {
|
|
const good = new TaskTarget(FIXTURE_FILE); good.read();
|
|
const noPipeline = new TaskTarget(FIXTURE_FILE);
|
|
const noFile = new TaskTarget("/nonexistent-xyz"); noFile.read();
|
|
const result = await verify([good, noPipeline, noFile]);
|
|
assert.equal(result.length, 1);
|
|
assert.equal(result[0], good);
|
|
});
|
|
|