Skip to content

Commit 096280c

Browse files
committed
feat: almost full test coverage for existing features from v1
1 parent 8b6b60a commit 096280c

File tree

4 files changed

+1111
-33
lines changed

4 files changed

+1111
-33
lines changed

tests/integration/lame.integration.test.ts

Lines changed: 323 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,97 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
33
import { tmpdir } from "node:os";
44
import { join } from "node:path";
55
import { randomBytes } from "node:crypto";
6-
import { describe, expect, it } from "vitest";
6+
import { afterEach, describe, expect, it, vi } from "vitest";
77

88
import { Lame } from "../../src/core/lame";
99

1010
const shouldRun = process.platform !== "win32";
1111

1212
(shouldRun ? describe : describe.skip)("Lame integration", () => {
13-
it("encodes a small buffer using a fake LAME binary", async () => {
14-
const workdir = await mkdtemp(join(tmpdir(), "node-lame-test-"));
15-
const inputPath = join(workdir, "input.raw");
16-
const outputPath = join(workdir, "output.mp3");
17-
const fakeBinaryPath = join(workdir, "fake-lame.mjs");
13+
const workdirs: string[] = [];
14+
const binaries: string[] = [];
1815

19-
const inputBuffer = randomBytes(16);
20-
await writeFile(inputPath, Uint8Array.from(inputBuffer));
16+
const createWorkdir = async () => {
17+
const dir = await mkdtemp(join(tmpdir(), "node-lame-int-"));
18+
workdirs.push(dir);
19+
return dir;
20+
};
2121

22-
const fakeBinary = `#!/usr/bin/env node
22+
const createBinary = async (directory: string, content: string) => {
23+
const binaryPath = join(directory, `fake-lame-${binaries.length}.mjs`);
24+
await writeFile(binaryPath, content, { mode: 0o755 });
25+
await chmod(binaryPath, 0o755);
26+
binaries.push(binaryPath);
27+
return binaryPath;
28+
};
29+
30+
const createPassthroughBinary = async () => {
31+
const workdir = await createWorkdir();
32+
const script = `#!/usr/bin/env node
2333
import { readFileSync, writeFileSync } from 'node:fs';
2434
const [, , input, output] = process.argv;
2535
const payload = readFileSync(input);
2636
writeFileSync(output, payload);
27-
console.error('( 0%)| 00:01 ');
28-
console.error('Writing LAME Tag...done');
37+
const isDecode = process.argv.includes('--decode');
38+
if (isDecode) {
39+
console.error('1/2');
40+
console.error('2/2');
41+
} else {
42+
console.error('( 50%)| 00:01 ');
43+
console.log('Writing LAME Tag...done');
44+
}
2945
process.exit(0);
3046
`;
31-
await writeFile(fakeBinaryPath, fakeBinary, { mode: 0o755 });
32-
await chmod(fakeBinaryPath, 0o755);
47+
return createBinary(workdir, script);
48+
};
49+
50+
const createErrorBinary = async (exitCode: number, message: string) => {
51+
const workdir = await createWorkdir();
52+
const script = `#!/usr/bin/env node
53+
console.error(${JSON.stringify(message)});
54+
process.exit(${exitCode});
55+
`;
56+
return createBinary(workdir, script);
57+
};
58+
59+
afterEach(async () => {
60+
while (binaries.length) {
61+
binaries.pop();
62+
}
63+
await Promise.all(
64+
workdirs.splice(0, workdirs.length).map((dir) =>
65+
rm(dir, { recursive: true, force: true }),
66+
),
67+
);
68+
});
69+
70+
it("encodes a buffer to memory via the CLI wrapper", async () => {
71+
const workdir = await createWorkdir();
72+
const fakeBinaryPath = await createPassthroughBinary();
73+
74+
const encoder = new Lame({ output: "buffer", bitrate: 128 });
75+
encoder.setBuffer(Buffer.from("integration-buffer-input"));
76+
encoder.setLamePath(fakeBinaryPath);
77+
78+
await encoder.encode();
79+
80+
expect(encoder.getBuffer().toString()).toBe(
81+
"integration-buffer-input",
82+
);
83+
expect(encoder.getStatus().finished).toBe(true);
84+
expect(encoder.getStatus().progress).toBe(100);
85+
86+
await rm(workdir, { recursive: true, force: true });
87+
});
88+
89+
it("encodes a file to disk and reads the resulting output", async () => {
90+
const workdir = await createWorkdir();
91+
const fakeBinaryPath = await createPassthroughBinary();
92+
93+
const inputPath = join(workdir, "input.raw");
94+
const outputPath = join(workdir, "output.mp3");
95+
const inputBuffer = randomBytes(32);
96+
await writeFile(inputPath, Uint8Array.from(inputBuffer));
3397

3498
const encoder = new Lame({ output: outputPath, bitrate: 128 });
3599
encoder.setFile(inputPath);
@@ -40,7 +104,253 @@ process.exit(0);
40104
const encoded = await readFile(outputPath);
41105

42106
expect(encoded.equals(Uint8Array.from(inputBuffer))).toBe(true);
107+
expect(encoder.getFile()).toBe(outputPath);
108+
});
109+
110+
it("decodes a file while reporting progress", async () => {
111+
const workdir = await createWorkdir();
112+
const fakeBinaryPath = await createPassthroughBinary();
113+
114+
const mp3Path = join(workdir, "input.mp3");
115+
const mp3Payload = randomBytes(24);
116+
await writeFile(mp3Path, Uint8Array.from(mp3Payload));
117+
118+
const encoder = new Lame({ output: "buffer", bitrate: 128 });
119+
encoder.setFile(mp3Path);
120+
encoder.setLamePath(fakeBinaryPath);
121+
122+
await encoder.decode();
123+
124+
expect(encoder.getBuffer()).toBeInstanceOf(Buffer);
125+
expect(encoder.getStatus().progress).toBe(100);
126+
expect(encoder.getStatus().eta).toBe("00:00");
127+
});
128+
129+
it("passes through an extensive option set to the CLI", async () => {
130+
const workdir = await createWorkdir();
131+
const fakeBinaryPath = await createPassthroughBinary();
132+
133+
const inputPath = join(workdir, "input.raw");
134+
const outputPath = join(workdir, "output.mp3");
135+
const inputPayload = randomBytes(48);
136+
await writeFile(inputPath, Uint8Array.from(inputPayload));
137+
138+
const encoder = new Lame({
139+
output: outputPath,
140+
raw: true,
141+
"swap-bytes": true,
142+
sfreq: 44.1,
143+
bitwidth: 16,
144+
signed: true,
145+
unsigned: true,
146+
"little-endian": true,
147+
"big-endian": true,
148+
mp2Input: true,
149+
mp3Input: true,
150+
mode: "j",
151+
"to-mono": true,
152+
"channel-different-block-sizes": true,
153+
freeformat: "LAME",
154+
"disable-info-tag": true,
155+
comp: 1.2,
156+
scale: 0.8,
157+
"scale-l": 0.9,
158+
"scale-r": 0.95,
159+
"replaygain-fast": true,
160+
"replaygain-accurate": true,
161+
"no-replaygain": true,
162+
"clip-detect": true,
163+
preset: "standard",
164+
noasm: "sse",
165+
quality: 4,
166+
bitrate: 192,
167+
"force-bitrate": true,
168+
cbr: true,
169+
abr: 192,
170+
vbr: true,
171+
"vbr-quality": 3,
172+
"ignore-noise-in-sfb21": true,
173+
emp: "n",
174+
"crc-error-protection": true,
175+
nores: true,
176+
"strictly-enforce-ISO": true,
177+
lowpass: 18,
178+
"lowpass-width": 2,
179+
highpass: 3,
180+
"highpass-width": 2,
181+
resample: 32,
182+
meta: {
183+
title: "Title",
184+
artist: "Artist",
185+
album: "Album",
186+
year: "2024",
187+
comment: "Comment",
188+
track: "1",
189+
genre: "Genre",
190+
"add-id3v2": true,
191+
"id3v1-only": true,
192+
"id3v2-only": true,
193+
"id3v2-latin1": true,
194+
"id3v2-utf16": true,
195+
"space-id3v1": true,
196+
"pad-id3v2-size": 2,
197+
"genre-list": "Rock,Pop",
198+
"ignore-tag-errors": true,
199+
},
200+
"mark-as-copyrighted": true,
201+
"mark-as-copy": true,
202+
});
203+
204+
encoder.setFile(inputPath);
205+
encoder.setLamePath(fakeBinaryPath);
43206

207+
await encoder.encode();
208+
209+
const outputPayload = await readFile(outputPath);
210+
expect(outputPayload.equals(Uint8Array.from(inputPayload))).toBe(true);
211+
expect(encoder.getStatus().finished).toBe(true);
212+
});
213+
214+
it("bubbles up CLI error messages", async () => {
215+
const fakeBinaryPath = await createErrorBinary(
216+
1,
217+
"Error simulated failure",
218+
);
219+
220+
const encoder = new Lame({ output: "buffer", bitrate: 128 });
221+
encoder.setBuffer(Buffer.from("will fail"));
222+
encoder.setLamePath(fakeBinaryPath);
223+
224+
await expect(encoder.encode()).rejects.toThrow(
225+
"lame: Error simulated failure",
226+
);
227+
});
228+
229+
it("treats exit code 255 as unexpected termination", async () => {
230+
const fakeBinaryPath = await createErrorBinary(
231+
255,
232+
"Unexpected termination",
233+
);
234+
235+
const encoder = new Lame({ output: "buffer", bitrate: 128 });
236+
encoder.setBuffer(Buffer.from("will fail badly"));
237+
encoder.setLamePath(fakeBinaryPath);
238+
239+
await expect(encoder.encode()).rejects.toThrow(
240+
"Unexpected termination of the process",
241+
);
242+
});
243+
244+
it("validates that an input source is set before encoding", async () => {
245+
const encoder = new Lame({ output: "buffer", bitrate: 128 });
246+
await expect(encoder.encode()).rejects.toThrow(
247+
"Audio file to encode is not set",
248+
);
249+
});
250+
251+
it("throws when accessing outputs before processing", () => {
252+
const encoder = new Lame({ output: "buffer", bitrate: 128 });
253+
expect(() => encoder.getBuffer()).toThrow(
254+
"Audio is not yet decoded/encoded",
255+
);
256+
expect(() => encoder.getFile()).toThrow(
257+
"Audio is not yet decoded/encoded",
258+
);
259+
});
260+
261+
it("guards invalid path setters", () => {
262+
const encoder = new Lame({ output: "buffer", bitrate: 128 });
263+
expect(() => encoder.setLamePath("")).toThrow(
264+
"Lame path must be a non-empty string",
265+
);
266+
expect(() => encoder.setTempPath(" ")).toThrow(
267+
"Temp path must be a non-empty string",
268+
);
269+
expect(() => encoder.setFile("/does/not/exist")).toThrow(
270+
"Audio file (path) does not exist",
271+
);
272+
});
273+
274+
it("propagates spawn errors when the binary cannot be executed", async () => {
275+
const encoder = new Lame({ output: "buffer", bitrate: 128 });
276+
encoder.setBuffer(Buffer.from("input"));
277+
encoder.setLamePath("/non-existent/node-lame-binary");
278+
279+
await expect(encoder.encode()).rejects.toThrow(/ENOENT/);
280+
});
281+
282+
it("rejects when CLI output cannot be read as Buffer", async () => {
283+
const workdir = await createWorkdir();
284+
const fakeBinaryPath = await createPassthroughBinary();
285+
286+
const originalIsBuffer = Buffer.isBuffer;
287+
let callCount = 0;
288+
const isBufferSpy = vi
289+
.spyOn(Buffer, "isBuffer")
290+
.mockImplementation((value: unknown) => {
291+
callCount += 1;
292+
if (callCount === 2) {
293+
return false;
294+
}
295+
return originalIsBuffer(value);
296+
});
297+
298+
const encoder = new Lame({ output: "buffer", bitrate: 128 });
299+
encoder.setBuffer(Buffer.from("integration-non-buffer"));
300+
encoder.setLamePath(fakeBinaryPath);
301+
302+
await expect(encoder.encode()).rejects.toThrow(
303+
"Unexpected output format received from temporary file",
304+
);
305+
306+
isBufferSpy.mockRestore();
44307
await rm(workdir, { recursive: true, force: true });
45308
});
309+
310+
it("validates advanced encoder options before execution", () => {
311+
expect(
312+
() =>
313+
new Lame({
314+
output: "buffer",
315+
resample: 20,
316+
} as unknown as any),
317+
).toThrow(
318+
"lame: Invalid option: 'resample' is not in range of 8, 11.025, 12, 16, 22.05, 24, 32, 44.1 or 48.",
319+
);
320+
321+
expect(
322+
() =>
323+
new Lame({
324+
output: "buffer",
325+
meta: {
326+
unexpected: "value",
327+
},
328+
} as unknown as any),
329+
).toThrow("lame: Invalid option: 'meta' unknown property 'unexpected'");
330+
});
331+
332+
it("removes temporary artifacts when invoked directly", async () => {
333+
const workdir = await createWorkdir();
334+
const rawPath = join(workdir, "temp.raw");
335+
const encodedPath = join(workdir, "temp.mp3");
336+
await writeFile(rawPath, Buffer.from("raw"));
337+
await writeFile(encodedPath, Buffer.from("encoded"));
338+
339+
const encoder = new Lame({ output: "buffer", bitrate: 128 });
340+
const encoderInternals = encoder as unknown as {
341+
fileBufferTempFilePath?: string;
342+
progressedBufferTempFilePath?: string;
343+
removeTempArtifacts: () => Promise<void>;
344+
};
345+
346+
encoderInternals.fileBufferTempFilePath = rawPath;
347+
encoderInternals.progressedBufferTempFilePath = encodedPath;
348+
349+
await encoderInternals.removeTempArtifacts();
350+
351+
await expect(readFile(rawPath)).rejects.toMatchObject({ code: "ENOENT" });
352+
await expect(readFile(encodedPath)).rejects.toMatchObject({
353+
code: "ENOENT",
354+
});
355+
});
46356
});

0 commit comments

Comments
 (0)