@@ -3,33 +3,97 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
33import { tmpdir } from "node:os" ;
44import { join } from "node:path" ;
55import { randomBytes } from "node:crypto" ;
6- import { describe , expect , it } from "vitest" ;
6+ import { afterEach , describe , expect , it , vi } from "vitest" ;
77
88import { Lame } from "../../src/core/lame" ;
99
1010const 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
2333import { readFileSync, writeFileSync } from 'node:fs';
2434const [, , input, output] = process.argv;
2535const payload = readFileSync(input);
2636writeFileSync(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+ }
2945process.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 ( / E N O E N T / ) ;
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