319 lines
11 KiB
JavaScript
319 lines
11 KiB
JavaScript
/* global it:false */
|
|
|
|
import common from './common.js';
|
|
const { createTree } = common;
|
|
import DataLayout from '../datalayout.js';
|
|
import { execSync } from 'node:child_process';
|
|
import expect from 'expect.js';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import paths from '../paths.js';
|
|
import safe from 'safetydance';
|
|
import syncer from '../syncer.js';
|
|
|
|
/* global describe:false */
|
|
/* global before:false */
|
|
|
|
const gTmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'syncer-test')),
|
|
gCacheFile = path.join(paths.BACKUP_INFO_DIR, 'syncer-test.sync.cache');
|
|
|
|
describe('Syncer', function () {
|
|
before(function () {
|
|
console.log('Tests are run in %s with cache file %s', gTmpDir, gCacheFile);
|
|
});
|
|
|
|
async function getChanges(dataLayout) {
|
|
const { delQueue, addQueue, integrityMap } = await syncer.sync(dataLayout, gCacheFile);
|
|
// inject fake integrity information
|
|
for (const change of addQueue) {
|
|
integrityMap.set(change.path, { size: 42, sha256: 'fake-sha256' });
|
|
}
|
|
await syncer.finalize(integrityMap, gCacheFile);
|
|
return delQueue.concat(addQueue);
|
|
}
|
|
|
|
function checkCache(cacheFile, changes) {
|
|
// store cache file indexed by path
|
|
const data = fs.readFileSync(cacheFile, 'utf8');
|
|
const pathMap = new Map();
|
|
if (data.length !== 0) {
|
|
const lines = data.trim().split('\n');
|
|
for (const line of lines) {
|
|
const entry = JSON.parse(line);
|
|
pathMap.set(entry.path, entry);
|
|
}
|
|
}
|
|
|
|
// for every 'add' operation, there must be an entry in the cache file with integrity
|
|
// for every 'delete' operation, there must be no entry in the cache file
|
|
for (const change of changes) {
|
|
if (change.operation === 'add') {
|
|
expect(pathMap.get(change.path).integrity.sha256).to.be('fake-sha256');
|
|
expect(pathMap.get(change.path).stat).to.be.an('object');
|
|
} else if (change.operation === 'remove') {
|
|
// if a file was removed, it won't be in pathMap. but if the file became a dir, it can be in pathMap without integrity info
|
|
expect(!pathMap.has(change.path) || !pathMap.get(change.path).integrity).to.be(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
it('missing cache - removes remote dir', async function () {
|
|
safe.fs.unlinkSync(gCacheFile);
|
|
createTree(gTmpDir, { });
|
|
|
|
const dataLayout = new DataLayout(gTmpDir, []);
|
|
const changes = await getChanges(dataLayout);
|
|
expect(changes).to.eql([{ operation: 'removedir', path: '', reason: 'nocache' }]);
|
|
|
|
expect(fs.readFileSync(gCacheFile, 'utf8')).to.be('');
|
|
});
|
|
|
|
it('empty cache - adds all', async function () {
|
|
fs.writeFileSync(gCacheFile, '', 'utf8');
|
|
createTree(gTmpDir, { 'src': { 'index.js': 'some code' }, 'test': { 'test.js': 'This is a README' }, 'walrus': 'animal' });
|
|
|
|
const dataLayout = new DataLayout(gTmpDir, []);
|
|
const changes = await getChanges(dataLayout);
|
|
|
|
expect(changes).to.eql([
|
|
{ operation: 'add', path: 'src/index.js', reason: 'new', position: 0 },
|
|
{ operation: 'add', path: 'test/test.js', reason: 'new', position: 1 },
|
|
{ operation: 'add', path: 'walrus', reason: 'new', position: 2 }
|
|
]);
|
|
|
|
checkCache(gCacheFile, changes);
|
|
});
|
|
|
|
it('empty cache - deep', async function () {
|
|
fs.writeFileSync(gCacheFile, '', 'utf8');
|
|
createTree(gTmpDir, { a: { b: { c: { d: { e: 'some code' } } } } });
|
|
|
|
const dataLayout = new DataLayout(gTmpDir, []);
|
|
const changes = await getChanges(dataLayout);
|
|
|
|
expect(changes).to.eql([
|
|
{ operation: 'add', path: 'a/b/c/d/e', reason: 'new', position: 0 }
|
|
]);
|
|
|
|
checkCache(gCacheFile, changes);
|
|
});
|
|
|
|
it('ignores special files', async function () {
|
|
fs.writeFileSync(gCacheFile, '', 'utf8');
|
|
createTree(gTmpDir, { 'link:file': '/tmp', 'readme': 'this is readme' });
|
|
|
|
const dataLayout = new DataLayout(gTmpDir, []);
|
|
const changes = await getChanges(dataLayout);
|
|
|
|
expect(changes).to.eql([
|
|
{ operation: 'add', path: 'readme', reason: 'new', position: 0 }
|
|
]);
|
|
|
|
checkCache(gCacheFile, changes);
|
|
});
|
|
|
|
it('adds changed files', async function () {
|
|
fs.writeFileSync(gCacheFile, '', 'utf8');
|
|
createTree(gTmpDir, { 'src': { 'index.js': 'some code' }, 'test': { 'test.js': 'This is a README' }, 'walrus': 'animal' });
|
|
|
|
const dataLayout = new DataLayout(gTmpDir, []);
|
|
let changes = await getChanges(dataLayout);
|
|
|
|
expect(changes.length).to.be(3);
|
|
|
|
execSync('touch src/index.js test/test.js', { cwd: gTmpDir });
|
|
|
|
changes = await getChanges(dataLayout);
|
|
|
|
expect(changes).to.eql([
|
|
{ operation: 'add', path: 'src/index.js', reason: 'changed', position: 0 },
|
|
{ operation: 'add', path: 'test/test.js', reason: 'changed', position: 1 }
|
|
]);
|
|
|
|
checkCache(gCacheFile, changes);
|
|
});
|
|
|
|
it('removes missing files', async function () {
|
|
fs.writeFileSync(gCacheFile, '', 'utf8');
|
|
createTree(gTmpDir, { 'src': { 'index.js': 'some code' }, 'test': { 'test.js': 'This is a README' }, 'walrus': 'animal' });
|
|
|
|
const dataLayout = new DataLayout(gTmpDir, []);
|
|
let changes = await getChanges(dataLayout);
|
|
|
|
expect(changes.length).to.be(3);
|
|
|
|
execSync('rm src/index.js walrus', { cwd: gTmpDir });
|
|
|
|
changes = await getChanges(dataLayout);
|
|
|
|
expect(changes).to.eql([
|
|
{ operation: 'remove', path: 'src/index.js', reason: 'missing' },
|
|
{ operation: 'remove', path: 'walrus', reason: 'missing' }
|
|
]);
|
|
|
|
checkCache(gCacheFile, changes);
|
|
});
|
|
|
|
it('removes missing dirs', async function () {
|
|
fs.writeFileSync(gCacheFile, '', 'utf8');
|
|
createTree(gTmpDir, { 'src': { 'index.js': 'some code' }, 'test': { 'test.js': 'This is a README' }, 'walrus': 'animal' });
|
|
|
|
const dataLayout = new DataLayout(gTmpDir, []);
|
|
let changes = await getChanges(dataLayout);
|
|
|
|
expect(changes.length).to.be(3);
|
|
|
|
execSync('rm -rf src test', { cwd: gTmpDir });
|
|
|
|
changes = await getChanges(dataLayout);
|
|
|
|
expect(changes).to.eql([
|
|
{ operation: 'removedir', path: 'src', reason: 'missing' },
|
|
{ operation: 'removedir', path: 'test', reason: 'missing' }
|
|
]);
|
|
|
|
checkCache(gCacheFile, changes);
|
|
});
|
|
|
|
it('all files disappeared', async function () {
|
|
fs.writeFileSync(gCacheFile, '', 'utf8');
|
|
createTree(gTmpDir, { 'src': { 'index.js': 'some code' }, 'test': { 'test.js': 'This is a README' }, 'walrus': 'animal' });
|
|
|
|
const dataLayout = new DataLayout(gTmpDir, []);
|
|
let changes = await getChanges(dataLayout);
|
|
|
|
expect(changes.length).to.be(3);
|
|
|
|
execSync('find . -delete', { cwd: gTmpDir });
|
|
|
|
changes = await getChanges(dataLayout);
|
|
|
|
expect(changes).to.eql([
|
|
{ operation: 'removedir', path: 'src', reason: 'missing' },
|
|
{ operation: 'removedir', path: 'test', reason: 'missing' },
|
|
{ operation: 'remove', path: 'walrus', reason: 'missing' }
|
|
]);
|
|
|
|
checkCache(gCacheFile, changes);
|
|
});
|
|
|
|
it('no redundant deletes', async function () {
|
|
fs.writeFileSync(gCacheFile, '', 'utf8');
|
|
createTree(gTmpDir, { a: { b: { c: { d: { e: 'some code' } } } } });
|
|
|
|
const dataLayout = new DataLayout(gTmpDir, []);
|
|
let changes = await getChanges(dataLayout);
|
|
|
|
expect(changes.length).to.be(1);
|
|
|
|
execSync('rm -r a/b; touch a/f', { cwd: gTmpDir });
|
|
|
|
changes = await getChanges(dataLayout);
|
|
|
|
expect(changes).to.eql([
|
|
{ operation: 'removedir', path: 'a/b', reason: 'missing' },
|
|
{ operation: 'add', path: 'a/f', reason: 'new', position: 0 }
|
|
]);
|
|
|
|
checkCache(gCacheFile, changes);
|
|
});
|
|
|
|
it('file became dir', async function () {
|
|
fs.writeFileSync(gCacheFile, '', 'utf8');
|
|
createTree(gTmpDir, { 'data': { 'src': { 'index.js': 'some code' }, 'test': { 'test.js': 'This is a README' }, 'walrus': 'animal' } });
|
|
|
|
const dataLayout = new DataLayout(gTmpDir, []);
|
|
let changes = await getChanges(dataLayout);
|
|
|
|
expect(changes.length).to.be(3);
|
|
|
|
execSync('rm data/test/test.js; mkdir data/test/test.js; touch data/test/test.js/trick', { cwd: gTmpDir });
|
|
|
|
changes = await getChanges(dataLayout);
|
|
|
|
expect(changes).to.eql([
|
|
{ operation: 'remove', path: 'data/test/test.js', reason: 'wasfile' },
|
|
{ operation: 'add', path: 'data/test/test.js/trick', reason: 'new', position: 0 }
|
|
]);
|
|
|
|
checkCache(gCacheFile, changes);
|
|
});
|
|
|
|
it('dir became file', async function () {
|
|
fs.writeFileSync(gCacheFile, '', 'utf8');
|
|
createTree(gTmpDir, { 'src': { 'index.js': 'some code' }, 'test': { 'test.js': 'this', 'test2.js': 'test' }, 'walrus': 'animal' });
|
|
|
|
const dataLayout = new DataLayout(gTmpDir, []);
|
|
let changes = await getChanges(dataLayout);
|
|
|
|
expect(changes.length).to.be(4);
|
|
|
|
execSync('rm -r test; touch test', { cwd: gTmpDir });
|
|
|
|
changes = await getChanges(dataLayout);
|
|
|
|
expect(changes).to.eql([
|
|
{ operation: 'removedir', path: 'test', reason: 'wasdir' },
|
|
{ operation: 'add', path: 'test', reason: 'wasdir', position: 0 }
|
|
]);
|
|
|
|
checkCache(gCacheFile, changes);
|
|
});
|
|
|
|
it('is complicated', async function () {
|
|
createTree(gTmpDir, {
|
|
a: 'data',
|
|
a2: 'data',
|
|
b: 'data',
|
|
file: 'data',
|
|
g: {
|
|
file: 'data'
|
|
},
|
|
j: {
|
|
k: { },
|
|
l: {
|
|
file: 'data'
|
|
},
|
|
m: { }
|
|
}
|
|
});
|
|
|
|
const dataLayout = new DataLayout(gTmpDir, []);
|
|
let changes;
|
|
await getChanges(dataLayout);
|
|
|
|
execSync(`rm a; \
|
|
mkdir a; \
|
|
touch a/file; \
|
|
rm a2; \
|
|
touch b; \
|
|
rm file g/file; \
|
|
ln -s /tmp h; \
|
|
rm -r j/l;
|
|
touch j/k/file; \
|
|
rmdir j/m;`, { cwd: gTmpDir });
|
|
|
|
changes = await getChanges(dataLayout);
|
|
|
|
expect(changes).to.eql([
|
|
{ operation: 'remove', path: 'a', reason: 'wasfile' },
|
|
{ operation: 'remove', path: 'a2', reason: 'missing' },
|
|
{ operation: 'remove', path: 'file', reason: 'missing' },
|
|
{ operation: 'remove', path: 'g/file', reason: 'missing' },
|
|
{ operation: 'removedir', path: 'j/l', reason: 'missing' },
|
|
{ operation: 'removedir', path: 'j/m', reason: 'missing' },
|
|
|
|
{ operation: 'add', path: 'a/file', reason: 'new', position: 0 },
|
|
{ operation: 'add', path: 'b', reason: 'changed', position: 1 },
|
|
{ operation: 'add', path: 'j/k/file', reason: 'new', position: 2 },
|
|
]);
|
|
|
|
changes = await getChanges(dataLayout);
|
|
|
|
expect(changes.length).to.be(0);
|
|
|
|
checkCache(gCacheFile, changes);
|
|
});
|
|
});
|