1
+ import { expect , test , describe , vi , beforeEach } from "vitest" ;
2
+ import type { RepoDesignation , RepoId } from "../types/public" ;
3
+ import { dirname , join } from "node:path" ;
4
+ import { lstat , mkdir , stat , symlink , writeFile , rename } from "node:fs/promises" ;
5
+ import { pathsInfo } from "./paths-info" ;
6
+ import type { Stats } from "node:fs" ;
7
+ import { getHFHubCachePath , getRepoFolderName } from "./cache-management" ;
8
+ import { toRepoId } from "../utils/toRepoId" ;
9
+ import { downloadFileToCacheDir } from "./download-file-to-cache-dir" ;
10
+
11
+ vi . mock ( 'node:fs/promises' , ( ) => ( {
12
+ writeFile : vi . fn ( ) ,
13
+ rename : vi . fn ( ) ,
14
+ symlink : vi . fn ( ) ,
15
+ lstat : vi . fn ( ) ,
16
+ mkdir : vi . fn ( ) ,
17
+ stat : vi . fn ( )
18
+ } ) ) ;
19
+
20
+ vi . mock ( './paths-info' , ( ) => ( {
21
+ pathsInfo : vi . fn ( ) ,
22
+ } ) ) ;
23
+
24
+ const DUMMY_REPO : RepoId = {
25
+ name : 'hello-world' ,
26
+ type : 'model' ,
27
+ } ;
28
+
29
+ const DUMMY_ETAG = "dummy-etag" ;
30
+
31
+ // utility test method to get blob file path
32
+ function _getBlobFile ( params : {
33
+ repo : RepoDesignation ;
34
+ etag : string ;
35
+ cacheDir ?: string , // default to {@link getHFHubCache }
36
+ } ) {
37
+ return join ( params . cacheDir ?? getHFHubCachePath ( ) , getRepoFolderName ( toRepoId ( params . repo ) ) , "blobs" , params . etag ) ;
38
+ }
39
+
40
+ // utility test method to get snapshot file path
41
+ function _getSnapshotFile ( params : {
42
+ repo : RepoDesignation ;
43
+ path : string ;
44
+ revision : string ;
45
+ cacheDir ?: string , // default to {@link getHFHubCache }
46
+ } ) {
47
+ return join ( params . cacheDir ?? getHFHubCachePath ( ) , getRepoFolderName ( toRepoId ( params . repo ) ) , "snapshots" , params . revision , params . path ) ;
48
+ }
49
+
50
+ describe ( 'downloadFileToCacheDir' , ( ) => {
51
+ const fetchMock : typeof fetch = vi . fn ( ) ;
52
+ beforeEach ( ( ) => {
53
+ vi . resetAllMocks ( ) ;
54
+ // mock 200 request
55
+ vi . mocked ( fetchMock ) . mockResolvedValue ( {
56
+ status : 200 ,
57
+ ok : true ,
58
+ body : 'dummy-body'
59
+ } as unknown as Response ) ;
60
+
61
+ // prevent to use caching
62
+ vi . mocked ( stat ) . mockRejectedValue ( new Error ( 'Do not exists' ) ) ;
63
+ vi . mocked ( lstat ) . mockRejectedValue ( new Error ( 'Do not exists' ) ) ;
64
+ } ) ;
65
+
66
+ test ( 'should throw an error if fileDownloadInfo return nothing' , async ( ) => {
67
+ await expect ( async ( ) => {
68
+ await downloadFileToCacheDir ( {
69
+ repo : DUMMY_REPO ,
70
+ path : '/README.md' ,
71
+ fetch : fetchMock ,
72
+ } ) ;
73
+ } ) . rejects . toThrowError ( 'cannot get path info for /README.md' ) ;
74
+
75
+ expect ( pathsInfo ) . toHaveBeenCalledWith ( expect . objectContaining ( {
76
+ repo : DUMMY_REPO ,
77
+ paths : [ '/README.md' ] ,
78
+ fetch : fetchMock ,
79
+ } ) ) ;
80
+ } ) ;
81
+
82
+ test ( 'existing symlinked and blob should not re-download it' , async ( ) => {
83
+ // <cache>/<repo>/<revision>/snapshots/README.md
84
+ const expectPointer = _getSnapshotFile ( {
85
+ repo : DUMMY_REPO ,
86
+ path : '/README.md' ,
87
+ revision : "dd4bc8b21efa05ec961e3efc4ee5e3832a3679c7" ,
88
+ } ) ;
89
+ // stat ensure a symlink and the pointed file exists
90
+ vi . mocked ( stat ) . mockResolvedValue ( { } as Stats ) // prevent default mocked reject
91
+
92
+ const output = await downloadFileToCacheDir ( {
93
+ repo : DUMMY_REPO ,
94
+ path : '/README.md' ,
95
+ fetch : fetchMock ,
96
+ revision : "dd4bc8b21efa05ec961e3efc4ee5e3832a3679c7" ,
97
+ } ) ;
98
+
99
+ expect ( stat ) . toHaveBeenCalledOnce ( ) ;
100
+ // Get call argument for stat
101
+ const starArg = vi . mocked ( stat ) . mock . calls [ 0 ] [ 0 ] ;
102
+
103
+ expect ( starArg ) . toBe ( expectPointer )
104
+ expect ( fetchMock ) . not . toHaveBeenCalledWith ( ) ;
105
+
106
+ expect ( output ) . toBe ( expectPointer ) ;
107
+ } ) ;
108
+
109
+ test ( 'existing blob should only create the symlink' , async ( ) => {
110
+ // <cache>/<repo>/<revision>/snapshots/README.md
111
+ const expectPointer = _getSnapshotFile ( {
112
+ repo : DUMMY_REPO ,
113
+ path : '/README.md' ,
114
+ revision : "dummy-commit-hash" ,
115
+ } ) ;
116
+ // <cache>/<repo>/blobs/<etag>
117
+ const expectedBlob = _getBlobFile ( {
118
+ repo : DUMMY_REPO ,
119
+ etag : DUMMY_ETAG ,
120
+ } ) ;
121
+
122
+ // mock existing blob only no symlink
123
+ vi . mocked ( lstat ) . mockResolvedValue ( { } as Stats ) ;
124
+ // mock pathsInfo resolve content
125
+ vi . mocked ( pathsInfo ) . mockResolvedValue ( [ {
126
+ oid : DUMMY_ETAG ,
127
+ size : 55 ,
128
+ path : 'README.md' ,
129
+ type : 'file' ,
130
+ lastCommit : {
131
+ date : new Date ( ) ,
132
+ id : 'dummy-commit-hash' ,
133
+ title : 'Commit msg' ,
134
+ } ,
135
+ } ] ) ;
136
+
137
+ const output = await downloadFileToCacheDir ( {
138
+ repo : DUMMY_REPO ,
139
+ path : '/README.md' ,
140
+ fetch : fetchMock ,
141
+ } ) ;
142
+
143
+ expect ( stat ) . not . toHaveBeenCalled ( ) ;
144
+ // should have check for the blob
145
+ expect ( lstat ) . toHaveBeenCalled ( ) ;
146
+ expect ( vi . mocked ( lstat ) . mock . calls [ 0 ] [ 0 ] ) . toBe ( expectedBlob ) ;
147
+
148
+ // symlink should have been created
149
+ expect ( symlink ) . toHaveBeenCalledOnce ( ) ;
150
+ // no download done
151
+ expect ( fetchMock ) . not . toHaveBeenCalled ( ) ;
152
+
153
+ expect ( output ) . toBe ( expectPointer ) ;
154
+ } ) ;
155
+
156
+ test ( 'expect resolve value to be the pointer path of downloaded file' , async ( ) => {
157
+ // <cache>/<repo>/<revision>/snapshots/README.md
158
+ const expectPointer = _getSnapshotFile ( {
159
+ repo : DUMMY_REPO ,
160
+ path : '/README.md' ,
161
+ revision : "dummy-commit-hash" ,
162
+ } ) ;
163
+ // <cache>/<repo>/blobs/<etag>
164
+ const expectedBlob = _getBlobFile ( {
165
+ repo : DUMMY_REPO ,
166
+ etag : DUMMY_ETAG ,
167
+ } ) ;
168
+
169
+ vi . mocked ( pathsInfo ) . mockResolvedValue ( [ {
170
+ oid : DUMMY_ETAG ,
171
+ size : 55 ,
172
+ path : 'README.md' ,
173
+ type : 'file' ,
174
+ lastCommit : {
175
+ date : new Date ( ) ,
176
+ id : 'dummy-commit-hash' ,
177
+ title : 'Commit msg' ,
178
+ } ,
179
+ } ] ) ;
180
+
181
+ const output = await downloadFileToCacheDir ( {
182
+ repo : DUMMY_REPO ,
183
+ path : '/README.md' ,
184
+ fetch : fetchMock ,
185
+ } ) ;
186
+
187
+ // expect blobs and snapshots folder to have been mkdir
188
+ expect ( vi . mocked ( mkdir ) . mock . calls [ 0 ] [ 0 ] ) . toBe ( dirname ( expectedBlob ) ) ;
189
+ expect ( vi . mocked ( mkdir ) . mock . calls [ 1 ] [ 0 ] ) . toBe ( dirname ( expectPointer ) ) ;
190
+
191
+ expect ( output ) . toBe ( expectPointer ) ;
192
+ } ) ;
193
+
194
+ test ( 'should write fetch response to blob' , async ( ) => {
195
+ // <cache>/<repo>/<revision>/snapshots/README.md
196
+ const expectPointer = _getSnapshotFile ( {
197
+ repo : DUMMY_REPO ,
198
+ path : '/README.md' ,
199
+ revision : "dummy-commit-hash" ,
200
+ } ) ;
201
+ // <cache>/<repo>/blobs/<etag>
202
+ const expectedBlob = _getBlobFile ( {
203
+ repo : DUMMY_REPO ,
204
+ etag : DUMMY_ETAG ,
205
+ } ) ;
206
+
207
+ // mock pathsInfo resolve content
208
+ vi . mocked ( pathsInfo ) . mockResolvedValue ( [ {
209
+ oid : DUMMY_ETAG ,
210
+ size : 55 ,
211
+ path : 'README.md' ,
212
+ type : 'file' ,
213
+ lastCommit : {
214
+ date : new Date ( ) ,
215
+ id : 'dummy-commit-hash' ,
216
+ title : 'Commit msg' ,
217
+ } ,
218
+ } ] ) ;
219
+
220
+ await downloadFileToCacheDir ( {
221
+ repo : DUMMY_REPO ,
222
+ path : '/README.md' ,
223
+ fetch : fetchMock ,
224
+ } ) ;
225
+
226
+ const incomplete = `${ expectedBlob } .incomplete` ;
227
+ // 1. should write fetch#response#body to incomplete file
228
+ expect ( writeFile ) . toHaveBeenCalledWith ( incomplete , 'dummy-body' ) ;
229
+ // 2. should rename the incomplete to the blob expected name
230
+ expect ( rename ) . toHaveBeenCalledWith ( incomplete , expectedBlob ) ;
231
+ // 3. should create symlink pointing to blob
232
+ expect ( symlink ) . toHaveBeenCalledWith ( expectedBlob , expectPointer ) ;
233
+ } ) ;
234
+ } ) ;
0 commit comments