1- import { mkdirSync , readdir , rmdirSync , symlinkSync , writeFileSync } from 'node:fs' ;
1+ import { mkdirSync , readdir , rmSync , symlinkSync , writeFileSync } from 'node:fs' ;
22import { globSync } from 'tinyglobby' ;
33import { afterAll , afterEach , beforeEach , describe , expect , test , vi } from 'vitest' ;
44
55import { copyfiles , createDir } from '../index' ;
66
7+ let shouldMockReadError = false ;
8+ const error = new Error ( 'Mock read error' ) ;
9+
10+ vi . mock ( 'node:fs' , async ( ) => {
11+ const actual = await vi . importActual < typeof import ( 'node:fs' ) > ( 'node:fs' ) ;
12+ return {
13+ ...actual ,
14+ createReadStream : ( ...args : any [ ] ) => {
15+ if ( shouldMockReadError ) {
16+ const { Readable } = require ( 'node:stream' ) ;
17+ const stream = new Readable ( { read ( ) { } } ) ;
18+ setImmediate ( ( ) => stream . emit ( 'error' , error ) ) ;
19+ return stream ;
20+ }
21+ // fallback to real implementation
22+ return ( actual . createReadStream as any ) ( ...args ) ;
23+ }
24+ } ;
25+ } ) ;
26+
727async function cleanupFolders ( ) {
828 try {
9- rmdirSync ( 'input' , { recursive : true } ) ;
10- rmdirSync ( 'output' , { recursive : true } ) ;
29+ rmSync ( 'input' , { recursive : true , force : true } ) ;
30+ rmSync ( 'output' , { recursive : true , force : true } ) ;
1131 } catch ( e ) { }
1232}
1333
1434describe ( 'copyfiles' , ( ) => {
1535 afterEach ( async ( ) => {
16- cleanupFolders ( ) ;
36+ await cleanupFolders ( ) ;
1737 } ) ;
1838
19- afterAll ( ( ) => cleanupFolders ( ) ) ;
39+ afterAll ( async ( ) => {
40+ await cleanupFolders ( ) ;
41+ } ) ;
2042
2143 beforeEach ( ( ) => {
2244 createDir ( 'input/other' ) ;
@@ -41,10 +63,7 @@ describe('copyfiles', () => {
4163 writeFileSync ( 'input/b.txt' , 'b' ) ;
4264 writeFileSync ( 'input/c.js' , 'c' ) ;
4365 copyfiles ( [ 'input/*.txt' , 'output' ] , { } , ( err ) => {
44- console . error ( err , 'copyfiles' ) ;
4566 readdir ( 'output/input' , async ( err , files ) => {
46- // console.error(err, 'readdir');
47- // 'correct number of things'
4867 expect ( files ) . toEqual ( [ 'a.txt' , 'b.txt' ] ) ;
4968 done ( ) ;
5069 } ) ;
@@ -58,7 +77,6 @@ describe('copyfiles', () => {
5877 writeFileSync ( 'input/b.txt' , 'b' ) ;
5978 writeFileSync ( 'input/c.js' , 'c' ) ;
6079 copyfiles ( [ 'input/*.txt' , 'output' ] , { } , ( err ) => {
61- console . error ( err , 'copyfiles' ) ;
6280 readdir ( 'output/input' , ( err , files ) => {
6381 expect ( files ) . toEqual ( [ 'a.txt' , 'b.txt' ] ) ;
6482 // 'correct mode'
@@ -168,10 +186,11 @@ describe('copyfiles', () => {
168186 mkdirSync ( 'input/origin' ) ;
169187 mkdirSync ( 'input/origin/inner' ) ;
170188 writeFileSync ( 'input/origin/inner/a.txt' , 'a' ) ;
189+ writeFileSync ( 'input/origin/inner/b.txt' , 'b' ) ;
171190 symlinkSync ( 'origin' , 'input/dest' ) ;
172191 copyfiles ( [ 'input/**/*.txt' , 'output' ] , { up : 1 , follow : true } , ( err ) => {
173192 const files = globSync ( 'output/**/*.txt' ) ;
174- expect ( files ) . toEqual ( [ 'output/dest/inner/ a.txt' , 'output/origin/inner/a .txt' ] ) ;
193+ expect ( new Set ( files ) ) . toEqual ( new Set ( [ 'output/a.txt' , 'output/b .txt' ] ) ) ;
175194 } ) ;
176195 }
177196 } ) ;
@@ -184,7 +203,9 @@ describe('copyfiles', () => {
184203 copyfiles ( [ 'input/**/*.txt' , 'output' ] , { flat : true , verbose : true } , ( err ) => {
185204 readdir ( 'output' , ( err , files ) => {
186205 expect ( files ) . toEqual ( [ 'a.txt' , 'b.txt' ] ) ;
187- expect ( logSpy ) . toHaveBeenCalledWith ( 'glob found' , [ 'input/b.txt' , 'input/other/a.txt' ] ) ;
206+ const globCall = logSpy . mock . calls . find ( call => call [ 0 ] === 'glob found' ) ;
207+ expect ( globCall ) . toBeTruthy ( ) ;
208+ expect ( new Set ( globCall ! [ 1 ] ) ) . toEqual ( new Set ( [ 'input/b.txt' , 'input/other/a.txt' ] ) ) ;
188209 expect ( logSpy ) . toHaveBeenCalledWith ( 'copy:' , { from : 'input/other/a.txt' , to : 'output/a.txt' } ) ;
189210 expect ( logSpy ) . toHaveBeenCalledWith ( 'copy:' , { from : 'input/b.txt' , to : 'output/b.txt' } ) ;
190211 expect ( logSpy ) . toHaveBeenCalledWith ( 'Files copied: 2' ) ;
@@ -193,6 +214,93 @@ describe('copyfiles', () => {
193214 } ) ;
194215 } ) ) ;
195216
217+ test ( 'createDir does not throw if dir exists' , ( ) => {
218+ createDir ( 'input' ) ;
219+ expect ( ( ) => createDir ( 'input' ) ) . not . toThrow ( ) ;
220+ } ) ;
221+
222+ test ( 'throws when inFile or outDir are missing (no callback)' , ( ) => {
223+ expect ( ( ) => copyfiles ( [ 'input/**/*.txt' ] , { } ) ) . toThrow (
224+ 'Please make sure to provide both <inFile> and <outDirectory>, i.e.: "copyfiles <inFile> <outDirectory>"'
225+ ) ;
226+ } ) ;
227+
228+ test ( 'callback called when no files to copy' , ( ) => new Promise ( ( done : any ) => {
229+ copyfiles ( [ 'input/doesnotexist/*.txt' , 'output' ] , { } , ( err ) => {
230+ expect ( err ) . toBeUndefined ( ) ;
231+ done ( ) ;
232+ } ) ;
233+ } ) ) ;
234+
235+ test ( 'copyFileStream handles read error' , ( ) => new Promise ( ( done : any ) => {
236+ writeFileSync ( 'input/bad.txt' , 'bad' ) ; // <-- Ensure the file exists!
237+ shouldMockReadError = true ;
238+ copyfiles ( [ 'input/bad.txt' , 'output' ] , { } , ( err ) => {
239+ expect ( err ) . toBeInstanceOf ( Error ) ;
240+ expect ( err ?. message ) . toBe ( 'Mock read error' ) ;
241+ shouldMockReadError = false ;
242+ done ( ) ;
243+ } ) ;
244+ } ) ) ;
245+
246+ test ( 'throws when flat & up used together' , ( ) => {
247+ expect ( ( ) => copyfiles ( [ 'input/**/*.txt' , 'output' ] , { flat : true , up : 1 } ) ) . toThrow (
248+ 'Cannot use --flat in conjunction with --up option.'
249+ ) ;
250+ } ) ;
251+
252+ test ( 'calls callback with error when nothing copied and options.error is set' , ( ) => new Promise ( ( done : any ) => {
253+ copyfiles ( [ 'input/doesnotexist/*.txt' , 'output' ] , { error : true } , ( err ) => {
254+ expect ( err ) . toBeInstanceOf ( Error ) ;
255+ expect ( err ?. message ) . toBe ( 'nothing copied' ) ;
256+ done ( ) ;
257+ } ) ;
258+ } ) ) ;
259+
260+ test ( 'logs and calls callback when nothing copied and verbose/stat is set' , ( ) => new Promise ( ( done : any ) => {
261+ const logSpy = vi . spyOn ( global . console , 'log' ) . mockReturnValue ( ) ;
262+ const timeSpy = vi . spyOn ( global . console , 'timeEnd' ) . mockReturnValue ( ) ;
263+ copyfiles ( [ 'input/doesnotexist/*.txt' , 'output' ] , { verbose : true } , ( err ) => {
264+ expect ( logSpy ) . toHaveBeenCalledWith ( 'Files copied: 0' ) ;
265+ expect ( timeSpy ) . toHaveBeenCalled ( ) ;
266+ expect ( err ) . toBeUndefined ( ) ;
267+ logSpy . mockRestore ( ) ;
268+ timeSpy . mockRestore ( ) ;
269+ done ( ) ;
270+ } ) ;
271+ } ) ) ;
272+
273+ test ( 'throws when flat & up used together (with callback)' , ( ) => new Promise ( ( done : any ) => {
274+ copyfiles ( [ 'input/**/*.txt' , 'output' ] , { flat : true , up : 1 } , ( err ) => {
275+ expect ( err ) . toBeInstanceOf ( Error ) ;
276+ expect ( err ?. message ) . toBe ( 'Cannot use --flat in conjunction with --up option.' ) ;
277+ done ( ) ;
278+ } ) ;
279+ } ) ) ;
280+
281+ test ( 'throws when nothing copied and options.error is set (no callback)' , ( ) => {
282+ expect ( ( ) => {
283+ copyfiles ( [ 'input/doesnotexist/*.txt' , 'output' ] , { error : true } ) ;
284+ } ) . toThrow ( 'nothing copied' ) ;
285+ } ) ;
286+
287+ test ( 'sets followSymbolicLinks when options.follow is true' , async ( ) => {
288+ if ( process . platform === 'win32' ) return ;
289+ mkdirSync ( 'input/real' , { recursive : true } ) ;
290+ writeFileSync ( 'input/real/a.txt' , 'test' ) ;
291+ symlinkSync ( 'real' , 'input/link' ) ;
292+ await new Promise < void > ( ( resolve , reject ) => {
293+ copyfiles ( [ 'input/link/*.txt' , 'output' ] , { follow : true } , ( err ) => {
294+ const files = globSync ( 'output/**/*' , { dot : true } ) ;
295+ console . log ( 'output contents:' , files ) ;
296+ const found = files . some ( f => f . endsWith ( 'a.txt' ) ) ;
297+ expect ( found ) . toBe ( true ) ;
298+ if ( err ) reject ( err ) ;
299+ else resolve ( ) ;
300+ } ) ;
301+ } ) ;
302+ } ) ;
303+
196304 test ( 'verbose up' , ( ) => new Promise ( ( done : any ) => {
197305 const logSpy = vi . spyOn ( global . console , 'log' ) . mockReturnValue ( ) ;
198306 writeFileSync ( 'input/other/a.txt' , 'a' ) ;
0 commit comments