mirror of
https://github.com/codedread/bitjs
synced 2025-10-03 17:49:16 +02:00
Add some unit tests for unarchivers. Provide a way to disconnect the impl from the host (for unit tests).
This commit is contained in:
parent
eba7042abe
commit
cf26e0a2de
17 changed files with 211 additions and 13 deletions
|
@ -10,6 +10,12 @@
|
||||||
|
|
||||||
// Requires the following JavaScript features: MessageChannel, MessagePort, and dynamic imports.
|
// Requires the following JavaScript features: MessageChannel, MessagePort, and dynamic imports.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef Implementation
|
||||||
|
* @property {MessagePort} hostPort The port the host uses to communicate with the implementation.
|
||||||
|
* @property {Function} disconnectFn A function to call when the port has been disconnected.
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connects a host to a compress/decompress implementation via MessagePorts. The implementation must
|
* Connects a host to a compress/decompress implementation via MessagePorts. The implementation must
|
||||||
* have an exported connect() function that accepts a MessagePort. If the runtime support Workers
|
* have an exported connect() function that accepts a MessagePort. If the runtime support Workers
|
||||||
|
@ -17,8 +23,9 @@
|
||||||
* dynamically imports the implementation inside the current JS context (node, bun).
|
* dynamically imports the implementation inside the current JS context (node, bun).
|
||||||
* @param {string} implFilename The compressor/decompressor implementation filename relative to this
|
* @param {string} implFilename The compressor/decompressor implementation filename relative to this
|
||||||
* path (e.g. './unzip.js').
|
* path (e.g. './unzip.js').
|
||||||
* @returns {Promise<MessagePort>} The Promise resolves to the MessagePort connected to the
|
* @param {Function} disconnectFn A function to run when the port is disconnected.
|
||||||
* implementation that the host should use.
|
* @returns {Promise<Implementation>} The Promise resolves to the Implementation, which includes the
|
||||||
|
* MessagePort connected to the implementation that the host should use.
|
||||||
*/
|
*/
|
||||||
export async function getConnectedPort(implFilename) {
|
export async function getConnectedPort(implFilename) {
|
||||||
const messageChannel = new MessageChannel();
|
const messageChannel = new MessageChannel();
|
||||||
|
@ -28,13 +35,19 @@ export async function getConnectedPort(implFilename) {
|
||||||
if (typeof Worker === 'undefined') {
|
if (typeof Worker === 'undefined') {
|
||||||
const implModule = await import(`${implFilename}`);
|
const implModule = await import(`${implFilename}`);
|
||||||
await implModule.connect(implPort);
|
await implModule.connect(implPort);
|
||||||
return hostPort;
|
return {
|
||||||
|
hostPort,
|
||||||
|
disconnectFn: () => implModule.disconnect(),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const workerScriptPath = new URL(`./webworker-wrapper.js`, import.meta.url).href;
|
const workerScriptPath = new URL(`./webworker-wrapper.js`, import.meta.url).href;
|
||||||
const worker = new Worker(workerScriptPath, { type: 'module' });
|
const worker = new Worker(workerScriptPath, { type: 'module' });
|
||||||
worker.postMessage({ implSrc: implFilename }, [implPort]);
|
worker.postMessage({ implSrc: implFilename }, [implPort]);
|
||||||
resolve(hostPort);
|
resolve({
|
||||||
|
hostPort,
|
||||||
|
disconnectFn: () => worker.postMessage({ disconnect: true }),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,13 @@ export class Unarchiver extends EventTarget {
|
||||||
*/
|
*/
|
||||||
port_;
|
port_;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function to call to disconnect the implementation from the host.
|
||||||
|
* @type {Function}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
disconnectFn_;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {ArrayBuffer} arrayBuffer The Array Buffer. Note that this ArrayBuffer must not be
|
* @param {ArrayBuffer} arrayBuffer The Array Buffer. Note that this ArrayBuffer must not be
|
||||||
* referenced once it is sent to the Unarchiver, since it is marked as Transferable and sent
|
* referenced once it is sent to the Unarchiver, since it is marked as Transferable and sent
|
||||||
|
@ -87,6 +94,16 @@ export class Unarchiver extends EventTarget {
|
||||||
this.debugMode_ = !!(options.debug);
|
this.debugMode_ = !!(options.debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overridden so that the type hints for eventType are specific.
|
||||||
|
* @param {'progress'|'extract'|'finish'} eventType
|
||||||
|
* @param {EventListenerOrEventListenerObject} listener
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
addEventListener(eventType, listener) {
|
||||||
|
super.addEventListener(eventType, listener);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method must be overridden by the subclass to return the script filename.
|
* This method must be overridden by the subclass to return the script filename.
|
||||||
* @returns {string} The MIME type of the archive.
|
* @returns {string} The MIME type of the archive.
|
||||||
|
@ -164,7 +181,9 @@ export class Unarchiver extends EventTarget {
|
||||||
* using the update() method.
|
* using the update() method.
|
||||||
*/
|
*/
|
||||||
async start() {
|
async start() {
|
||||||
this.port_ = await getConnectedPort(this.getScriptFileName());
|
const impl = await getConnectedPort(this.getScriptFileName());
|
||||||
|
this.port_ = impl.hostPort;
|
||||||
|
this.disconnectFn_ = impl.disconnectFn;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.port_.onerror = (evt) => {
|
this.port_.onerror = (evt) => {
|
||||||
console.log('Impl error: message = ' + evt.message);
|
console.log('Impl error: message = ' + evt.message);
|
||||||
|
@ -221,7 +240,9 @@ export class Unarchiver extends EventTarget {
|
||||||
stop() {
|
stop() {
|
||||||
if (this.port_) {
|
if (this.port_) {
|
||||||
this.port_.close();
|
this.port_.close();
|
||||||
|
this.disconnectFn_();
|
||||||
this.port_ = null;
|
this.port_ = null;
|
||||||
|
this.disconnectFn_ = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1466,8 +1466,28 @@ const onmessage = function (event) {
|
||||||
*/
|
*/
|
||||||
export function connect(port) {
|
export function connect(port) {
|
||||||
if (hostPort) {
|
if (hostPort) {
|
||||||
throw `hostPort already connected`;
|
throw `hostPort already connected in unrar.js`;
|
||||||
}
|
}
|
||||||
hostPort = port;
|
hostPort = port;
|
||||||
port.onmessage = onmessage;
|
port.onmessage = onmessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function disconnect() {
|
||||||
|
if (!hostPort) {
|
||||||
|
throw `hostPort was not connected in unzip.js`;
|
||||||
|
}
|
||||||
|
|
||||||
|
hostPort = null;
|
||||||
|
|
||||||
|
unarchiveState = UnarchiveState.NOT_STARTED;
|
||||||
|
bytestream = null;
|
||||||
|
allLocalFiles = null;
|
||||||
|
logToConsole = false;
|
||||||
|
|
||||||
|
currentFilename = '';
|
||||||
|
currentFileNumber = 0;
|
||||||
|
currentBytesUnarchivedInFile = 0;
|
||||||
|
currentBytesUnarchived = 0;
|
||||||
|
totalUncompressedBytesInArchive = 0;
|
||||||
|
totalFilesInArchive = 0;
|
||||||
|
}
|
||||||
|
|
|
@ -221,8 +221,28 @@ const onmessage = function (event) {
|
||||||
*/
|
*/
|
||||||
export function connect(port) {
|
export function connect(port) {
|
||||||
if (hostPort) {
|
if (hostPort) {
|
||||||
throw `hostPort already connected`;
|
throw `hostPort already connected in untar.js`;
|
||||||
}
|
}
|
||||||
hostPort = port;
|
hostPort = port;
|
||||||
port.onmessage = onmessage;
|
port.onmessage = onmessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function disconnect() {
|
||||||
|
if (!hostPort) {
|
||||||
|
throw `hostPort was not connected in unzip.js`;
|
||||||
|
}
|
||||||
|
|
||||||
|
hostPort = null;
|
||||||
|
|
||||||
|
unarchiveState = UnarchiveState.NOT_STARTED;
|
||||||
|
bytestream = null;
|
||||||
|
allLocalFiles = null;
|
||||||
|
logToConsole = false;
|
||||||
|
|
||||||
|
currentFilename = '';
|
||||||
|
currentFileNumber = 0;
|
||||||
|
currentBytesUnarchivedInFile = 0;
|
||||||
|
currentBytesUnarchived = 0;
|
||||||
|
totalUncompressedBytesInArchive = 0;
|
||||||
|
totalFilesInArchive = 0;
|
||||||
|
}
|
||||||
|
|
|
@ -781,8 +781,30 @@ const onmessage = function (event) {
|
||||||
*/
|
*/
|
||||||
export function connect(port) {
|
export function connect(port) {
|
||||||
if (hostPort) {
|
if (hostPort) {
|
||||||
throw `hostPort already connected`;
|
throw `hostPort already connected in unzip.js`;
|
||||||
}
|
}
|
||||||
|
|
||||||
hostPort = port;
|
hostPort = port;
|
||||||
port.onmessage = onmessage;
|
port.onmessage = onmessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function disconnect() {
|
||||||
|
if (!hostPort) {
|
||||||
|
throw `hostPort was not connected in unzip.js`;
|
||||||
|
}
|
||||||
|
|
||||||
|
hostPort = null;
|
||||||
|
|
||||||
|
unarchiveState = UnarchiveState.NOT_STARTED;
|
||||||
|
bytestream = null;
|
||||||
|
allLocalFiles = null;
|
||||||
|
logToConsole = false;
|
||||||
|
|
||||||
|
// Progress variables.
|
||||||
|
currentFilename = '';
|
||||||
|
currentFileNumber = 0;
|
||||||
|
currentBytesUnarchivedInFile = 0;
|
||||||
|
currentBytesUnarchived = 0;
|
||||||
|
totalUncompressedBytesInArchive = 0;
|
||||||
|
totalFilesInArchive = 0;
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,10 @@
|
||||||
let implPort;
|
let implPort;
|
||||||
|
|
||||||
onmessage = async (evt) => {
|
onmessage = async (evt) => {
|
||||||
|
if (evt.data.implSrc) {
|
||||||
const module = await import(evt.data.implSrc);
|
const module = await import(evt.data.implSrc);
|
||||||
module.connect(evt.ports[0]);
|
module.connect(evt.ports[0]);
|
||||||
|
} else if (evt.data.disconnect) {
|
||||||
|
module.disconnect();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -80,7 +80,6 @@ const CompressorState = {
|
||||||
FINISHED: 3,
|
FINISHED: 3,
|
||||||
};
|
};
|
||||||
let state = CompressorState.NOT_STARTED;
|
let state = CompressorState.NOT_STARTED;
|
||||||
let lastFileReceived = false;
|
|
||||||
const crc32Table = createCRC32Table();
|
const crc32Table = createCRC32Table();
|
||||||
|
|
||||||
/** Helper functions. */
|
/** Helper functions. */
|
||||||
|
@ -280,8 +279,21 @@ const onmessage = function(evt) {
|
||||||
*/
|
*/
|
||||||
export function connect(port) {
|
export function connect(port) {
|
||||||
if (hostPort) {
|
if (hostPort) {
|
||||||
throw `hostPort already connected`;
|
throw `hostPort already connected in zip.js`;
|
||||||
}
|
}
|
||||||
hostPort = port;
|
hostPort = port;
|
||||||
port.onmessage = onmessage;
|
port.onmessage = onmessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function disconnect() {
|
||||||
|
if (!hostPort) {
|
||||||
|
throw `hostPort was not connected in unzip.js`;
|
||||||
|
}
|
||||||
|
|
||||||
|
hostPort = null;
|
||||||
|
|
||||||
|
centralDirectoryInfos = [];
|
||||||
|
numBytesWritten = 0;
|
||||||
|
state = CompressorState.NOT_STARTED;
|
||||||
|
lastFileReceived = false;
|
||||||
|
}
|
||||||
|
|
|
@ -22,7 +22,6 @@ export class ByteStream {
|
||||||
*/
|
*/
|
||||||
constructor(ab, opt_offset, opt_length) {
|
constructor(ab, opt_offset, opt_length) {
|
||||||
if (!(ab instanceof ArrayBuffer)) {
|
if (!(ab instanceof ArrayBuffer)) {
|
||||||
console.error(typeof ab);
|
|
||||||
throw 'Error! ByteStream constructed with an invalid ArrayBuffer object';
|
throw 'Error! ByteStream constructed with an invalid ArrayBuffer object';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
BIN
tests/archive-testfiles/archive-rar.rar
Normal file
BIN
tests/archive-testfiles/archive-rar.rar
Normal file
Binary file not shown.
BIN
tests/archive-testfiles/archive-tar.tar
Normal file
BIN
tests/archive-testfiles/archive-tar.tar
Normal file
Binary file not shown.
BIN
tests/archive-testfiles/archive-zip-faster.zip
Normal file
BIN
tests/archive-testfiles/archive-zip-faster.zip
Normal file
Binary file not shown.
BIN
tests/archive-testfiles/archive-zip-smaller.zip
Normal file
BIN
tests/archive-testfiles/archive-zip-smaller.zip
Normal file
Binary file not shown.
BIN
tests/archive-testfiles/archive-zip-store.zip
Normal file
BIN
tests/archive-testfiles/archive-zip-store.zip
Normal file
Binary file not shown.
20
tests/archive-testfiles/sample-1.txt
Normal file
20
tests/archive-testfiles/sample-1.txt
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
This is a sample text file. This text file is large enough to make creating
|
||||||
|
zip files more interesting, since compression has a chance to work on a larger
|
||||||
|
sample file that has duplicate words in it.
|
||||||
|
|
||||||
|
This is a sample text file. This text file is large enough to make creating
|
||||||
|
zip files more interesting, since compression has a chance to work on a larger
|
||||||
|
sample file that has duplicate words in it.
|
||||||
|
|
||||||
|
This is a sample text file. This text file is large enough to make creating
|
||||||
|
zip files more interesting, since compression has a chance to work on a larger
|
||||||
|
sample file that has duplicate words in it.
|
||||||
|
|
||||||
|
This is a sample text file. This text file is large enough to make creating
|
||||||
|
zip files more interesting, since compression has a chance to work on a larger
|
||||||
|
sample file that has duplicate words in it.
|
||||||
|
|
||||||
|
This is a sample text file. This text file is large enough to make creating
|
||||||
|
zip files more interesting, since compression has a chance to work on a larger
|
||||||
|
sample file that has duplicate words in it.
|
||||||
|
|
4
tests/archive-testfiles/sample-2.csv
Normal file
4
tests/archive-testfiles/sample-2.csv
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
filetype,extension
|
||||||
|
"text file","txt"
|
||||||
|
"JSON file","json"
|
||||||
|
"CSV file","csv"
|
|
6
tests/archive-testfiles/sample-3.json
Normal file
6
tests/archive-testfiles/sample-3.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"file formats": ["csv", "json", "txt"],
|
||||||
|
"tv shows": {
|
||||||
|
"it's": ["monty", "python's", "flying", "circus"]
|
||||||
|
}
|
||||||
|
}
|
57
tests/decompress.spec.js
Normal file
57
tests/decompress.spec.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
|
||||||
|
import { Unarchiver, Unrarrer, Untarrer, Unzipper, getUnarchiver } from '../archive/decompress.js';
|
||||||
|
|
||||||
|
const PATH = `tests/archive-testfiles/`;
|
||||||
|
|
||||||
|
const INPUT_FILES = [
|
||||||
|
'sample-1.txt',
|
||||||
|
'sample-2.csv',
|
||||||
|
'sample-3.json',
|
||||||
|
];
|
||||||
|
|
||||||
|
const ARCHIVE_FILES = [
|
||||||
|
'archive-rar.rar',
|
||||||
|
'archive-tar.tar',
|
||||||
|
'archive-zip-store.zip',
|
||||||
|
'archive-zip-faster.zip',
|
||||||
|
'archive-zip-smaller.zip',
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('bitjs.archive.decompress', () => {
|
||||||
|
/** @type {Map<string, ArrayBuffer>} */
|
||||||
|
let inputArrayBuffers = new Map();
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
for (const inputFile of INPUT_FILES) {
|
||||||
|
const nodeBuf = fs.readFileSync(`${PATH}${inputFile}`);
|
||||||
|
const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length);
|
||||||
|
inputArrayBuffers.set(inputFile, ab);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const outFile of ARCHIVE_FILES) {
|
||||||
|
it(outFile, (done) => {
|
||||||
|
const bufs = new Map(inputArrayBuffers);
|
||||||
|
const nodeBuf = fs.readFileSync(`${PATH}${outFile}`);
|
||||||
|
const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length);
|
||||||
|
let unarchiver = getUnarchiver(ab);
|
||||||
|
expect(unarchiver instanceof Unarchiver).equals(true);
|
||||||
|
|
||||||
|
unarchiver.addEventListener('extract', evt => {
|
||||||
|
const {filename, fileData} = evt.unarchivedFile;
|
||||||
|
expect(bufs.has(filename)).equals(true);
|
||||||
|
const ab = bufs.get(filename);
|
||||||
|
expect(fileData.byteLength).equals(ab.byteLength);
|
||||||
|
for (let b = 0; b < fileData.byteLength; ++b) {
|
||||||
|
expect(fileData[b] === ab[b]);
|
||||||
|
}
|
||||||
|
// Remove the value from the map so that it is only used once.
|
||||||
|
bufs.delete(filename);
|
||||||
|
});
|
||||||
|
unarchiver.start().then(() => { done() });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
Loading…
Add table
Add a link
Reference in a new issue