]> git.wincent.com - wincent.git/blob - aspects/meta/index.ts
e148ec1907bb29d24c64087d6c58b3595b6cd9f9
[wincent.git] / aspects / meta / index.ts
1 import {equal, ok} from 'assert';
2 import {join} from 'path';
3
4 import {file, resource, task, template} from '../../src/Fig.js';
5 import Context from '../../src/Fig/Context.js';
6 import assert from '../../src/assert.js';
7 import {promises} from '../../src/fs.js';
8 import stat from '../../src/fs/stat.js';
9 import tempdir from '../../src/fs/tempdir.js';
10 import {default as toPath} from '../../src/path.js';
11
12 function live() {
13     return !Context.currentOptions?.check;
14 }
15
16 const expect = {
17     equal(actual: any, expected: any, message?: string | Error | undefined) {
18         if (live()) {
19             equal(actual, expected, message);
20         }
21     },
22
23     ok(value: any, message?: string | Error | undefined) {
24         if (live()) {
25             ok(value, message);
26         }
27     },
28 };
29
30 const fs = {
31     async readFile(name: string, encoding: string) {
32         if (live()) {
33             return promises.readFile(name, encoding);
34         } else {
35             return '';
36         }
37     },
38
39     async stat(name: string) {
40         if (live()) {
41             return promises.stat(name);
42         } else {
43             return {
44                 mtimeMs: 1000,
45             };
46         }
47     },
48
49     async utimes(path: string, atime: number, mtime: number) {
50         if (live()) {
51             await promises.utimes(path, atime, mtime);
52         }
53     },
54 };
55
56 const DEFAULT_STATS = {
57     mode: '0644',
58     target: undefined,
59     type: 'file',
60 };
61
62 task('copy a file', async () => {
63     //
64     // 1. Create a file for the first time.
65     //
66     let path = join(await tempdir('meta'), 'example.txt');
67
68     let {changed, failed, ok, skipped} = Context.counts;
69
70     // This time showing use of "src".
71     await file({
72         path,
73         src: resource.file('example.txt'),
74         state: 'file',
75     });
76
77     let contents = await fs.readFile(path, 'utf8');
78
79     expect.equal(contents, 'Some example content.\n');
80
81     expect.equal(Context.counts.changed, changed + 1);
82     expect.equal(Context.counts.failed, failed);
83     expect.equal(Context.counts.ok, ok);
84     expect.equal(Context.counts.skipped, skipped);
85
86     //
87     // 2. Overwrite an existing file.
88     //
89     ({changed, failed, ok, skipped} = Context.counts);
90
91     // This time showing use of "contents".
92     await file({
93         contents: 'New content!\n',
94         path,
95         state: 'file',
96     });
97
98     contents = await fs.readFile(path, 'utf8');
99
100     expect.equal(contents, 'New content!\n');
101
102     expect.equal(Context.counts.changed, changed + 1);
103     expect.equal(Context.counts.failed, failed);
104     expect.equal(Context.counts.ok, ok);
105     expect.equal(Context.counts.skipped, skipped);
106
107     //
108     // 3. When no changes needed.
109     //
110     ({changed, failed, ok, skipped} = Context.counts);
111
112     await file({
113         contents: 'New content!\n',
114         path,
115         state: 'file',
116     });
117
118     contents = await fs.readFile(path, 'utf8');
119
120     expect.equal(contents, 'New content!\n');
121
122     expect.equal(Context.counts.changed, changed);
123     expect.equal(Context.counts.failed, failed);
124     expect.equal(Context.counts.ok, ok + 1);
125     expect.equal(Context.counts.skipped, skipped);
126
127     //
128     // 4. Creating an empty file (no "src", no "content").
129     //
130     path = path.replace(/\.txt$/, '.txt.bak');
131
132     ({changed, failed, ok, skipped} = Context.counts);
133
134     await file({
135         path,
136         state: 'file',
137     });
138
139     contents = await fs.readFile(path, 'utf8');
140
141     expect.equal(contents, '');
142
143     expect.equal(Context.counts.changed, changed + 1);
144     expect.equal(Context.counts.failed, failed);
145     expect.equal(Context.counts.ok, ok);
146     expect.equal(Context.counts.skipped, skipped);
147 });
148
149 task('create a directory', async () => {
150     //
151     // 1. Create a directory for the first time.
152     //
153     const path = join(await tempdir('meta'), 'a-directory');
154
155     let {changed, failed, ok, skipped} = Context.counts;
156
157     await file({
158         path,
159         state: 'directory',
160     });
161
162     let stats = live() ? await stat(path) : DEFAULT_STATS;
163
164     assert(stats && !(stats instanceof Error));
165
166     expect.equal(stats.type, 'directory');
167     expect.equal(stats.mode, '0755');
168
169     expect.equal(Context.counts.changed, changed + 1);
170     expect.equal(Context.counts.failed, failed);
171     expect.equal(Context.counts.ok, ok);
172     expect.equal(Context.counts.skipped, skipped);
173
174     //
175     // 2. Changing mode of an existing directory.
176     //
177
178     ({changed, failed, ok, skipped} = Context.counts);
179
180     await file({
181         mode: '0700',
182         path,
183         state: 'directory',
184     });
185
186     stats = live() ? await stat(path) : DEFAULT_STATS;
187
188     assert(stats && !(stats instanceof Error));
189
190     expect.equal(stats.mode, '0700');
191
192     expect.equal(Context.counts.changed, changed + 1);
193     expect.equal(Context.counts.failed, failed);
194     expect.equal(Context.counts.ok, ok);
195     expect.equal(Context.counts.skipped, skipped);
196
197     //
198     // 3. A no-op.
199     //
200
201     ({changed, failed, ok, skipped} = Context.counts);
202
203     await file({
204         path,
205         state: 'directory',
206     });
207
208     stats = live() ? await stat(path) : DEFAULT_STATS;
209
210     assert(stats && !(stats instanceof Error));
211
212     expect.equal(stats.mode, '0700');
213
214     expect.equal(Context.counts.changed, changed);
215     expect.equal(Context.counts.failed, failed);
216     expect.equal(Context.counts.ok, ok + 1);
217     expect.equal(Context.counts.skipped, skipped);
218 });
219
220 task('manage a symbolic link', async () => {
221     //
222     // 1. Create a link.
223     //
224     let path = join(await tempdir('meta'), 'example.txt');
225
226     const src = resource.file('example.txt');
227
228     let {changed, failed, ok, skipped} = Context.counts;
229
230     await file({
231         path,
232         src,
233         state: 'link',
234     });
235
236     let contents = await fs.readFile(path, 'utf8');
237
238     expect.equal(contents, 'Some example content.\n');
239
240     let stats = live() ? await stat(path) : DEFAULT_STATS;
241
242     assert(stats && !(stats instanceof Error));
243
244     expect.equal(stats.type, 'link');
245     expect.equal(stats.target, toPath(src).resolve);
246
247     expect.equal(Context.counts.changed, changed + 1);
248     expect.equal(Context.counts.failed, failed);
249     expect.equal(Context.counts.ok, ok);
250     expect.equal(Context.counts.skipped, skipped);
251 });
252
253 task('template a file', async () => {
254     //
255     // 1. Create file from template.
256     //
257     const path = join(await tempdir('meta'), 'sample.txt');
258
259     let {changed, failed, ok, skipped} = Context.counts;
260
261     await template({
262         path,
263         src: resource.template('sample.txt.erb'),
264         variables: {
265             greeting: 'Hello',
266             names: ['Bob', 'Jane'],
267         },
268     });
269
270     let contents = await fs.readFile(path, 'utf8');
271
272     expect.equal(contents, 'Hello Bob, Jane!\n');
273
274     expect.equal(Context.counts.changed, changed + 1);
275     expect.equal(Context.counts.failed, failed);
276     expect.equal(Context.counts.ok, ok);
277     expect.equal(Context.counts.skipped, skipped);
278
279     //
280     // 2. Show that running again is a no-op.
281     //
282     ({changed, failed, ok, skipped} = Context.counts);
283
284     await template({
285         path,
286         src: resource.template('sample.txt.erb'),
287         variables: {
288             greeting: 'Hello',
289             names: ['Bob', 'Jane'],
290         },
291     });
292
293     contents = await fs.readFile(path, 'utf8');
294
295     expect.equal(contents, 'Hello Bob, Jane!\n');
296
297     expect.equal(Context.counts.changed, changed);
298     expect.equal(Context.counts.failed, failed);
299     expect.equal(Context.counts.ok, ok + 1);
300     expect.equal(Context.counts.skipped, skipped);
301
302     //
303     // 3. Show that we can change an existing file if required.
304     //
305     // 3a. Just a content change.
306     //
307     ({changed, failed, ok, skipped} = Context.counts);
308
309     await template({
310         path,
311         src: resource.template('sample.txt.erb'),
312         variables: {
313             greeting: 'Hi',
314             names: ['Jim', 'Mary', 'Carol'],
315         },
316     });
317
318     expect.equal(Context.counts.changed, changed + 1);
319     expect.equal(Context.counts.failed, failed);
320     expect.equal(Context.counts.ok, ok);
321     expect.equal(Context.counts.skipped, skipped);
322
323     contents = await fs.readFile(path, 'utf8');
324
325     expect.equal(contents, 'Hi Jim, Mary, Carol!\n');
326
327     //
328     // 3b. Just a mode change.
329     //
330     ({changed, failed, ok, skipped} = Context.counts);
331
332     await template({
333         mode: '0600',
334         path,
335         src: resource.template('sample.txt.erb'),
336         variables: {
337             greeting: 'Hi',
338             names: ['Jim', 'Mary', 'Carol'],
339         },
340     });
341
342     expect.equal(Context.counts.changed, changed + 1);
343     expect.equal(Context.counts.failed, failed);
344     expect.equal(Context.counts.ok, ok);
345     expect.equal(Context.counts.skipped, skipped);
346
347     contents = await fs.readFile(path, 'utf8');
348
349     expect.equal(contents, 'Hi Jim, Mary, Carol!\n');
350
351     let stats = live() ? await stat(path) : DEFAULT_STATS;
352
353     assert(stats && !(stats instanceof Error));
354
355     expect.equal(stats.mode, '0600');
356
357     //
358     // 3c. A mode and a content change.
359     //
360     ({changed, failed, ok, skipped} = Context.counts);
361
362     await template({
363         mode: '0644',
364         path,
365         src: resource.template('sample.txt.erb'),
366         variables: {
367             greeting: 'Yo',
368             names: ['Derek'],
369         },
370     });
371
372     expect.equal(Context.counts.changed, changed + 1);
373     expect.equal(Context.counts.failed, failed);
374     expect.equal(Context.counts.ok, ok);
375     expect.equal(Context.counts.skipped, skipped);
376
377     contents = await fs.readFile(path, 'utf8');
378
379     expect.equal(contents, 'Yo Derek!\n');
380
381     stats = live() ? await stat(path) : DEFAULT_STATS;
382
383     assert(stats && !(stats instanceof Error));
384
385     expect.equal(stats.mode, '0644');
386 });
387
388 task('touch an item', async () => {
389     //
390     // 1. Create a file for the first time.
391     //
392     let path = join(await tempdir('meta'), 'example.txt');
393
394     let {changed, failed, ok, skipped} = Context.counts;
395
396     await file({
397         path,
398         state: 'touch',
399     });
400
401     let contents = await fs.readFile(path, 'utf8');
402
403     expect.equal(contents, '');
404
405     expect.equal(Context.counts.changed, changed + 1);
406     expect.equal(Context.counts.failed, failed);
407     expect.equal(Context.counts.ok, ok);
408     expect.equal(Context.counts.skipped, skipped);
409
410     //
411     // 2. Touch an existing entity.
412     //
413
414     const now = Date.now();
415     const recent = now - 3_600_000; // An hour ago.
416
417     await fs.utimes(path, recent, recent);
418
419     ({changed, failed, ok, skipped} = Context.counts);
420
421     await file({
422         path,
423         state: 'touch',
424     });
425
426     expect.equal(Context.counts.changed, changed + 1);
427     expect.equal(Context.counts.failed, failed);
428     expect.equal(Context.counts.ok, ok);
429     expect.equal(Context.counts.skipped, skipped);
430
431     const stats = await fs.stat(path);
432
433     // Assert that mtime is within 1 second, allowing some imprecision.
434     expect.ok(Math.abs(stats.mtimeMs - now) < 1_000);
435 });