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.
|
||||
|
||||
/**
|
||||
* @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
|
||||
* 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).
|
||||
* @param {string} implFilename The compressor/decompressor implementation filename relative to this
|
||||
* path (e.g. './unzip.js').
|
||||
* @returns {Promise<MessagePort>} The Promise resolves to the MessagePort connected to the
|
||||
* implementation that the host should use.
|
||||
* @param {Function} disconnectFn A function to run when the port is disconnected.
|
||||
* @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) {
|
||||
const messageChannel = new MessageChannel();
|
||||
|
@ -28,13 +35,19 @@ export async function getConnectedPort(implFilename) {
|
|||
if (typeof Worker === 'undefined') {
|
||||
const implModule = await import(`${implFilename}`);
|
||||
await implModule.connect(implPort);
|
||||
return hostPort;
|
||||
return {
|
||||
hostPort,
|
||||
disconnectFn: () => implModule.disconnect(),
|
||||
};
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const workerScriptPath = new URL(`./webworker-wrapper.js`, import.meta.url).href;
|
||||
const worker = new Worker(workerScriptPath, { type: 'module' });
|
||||
worker.postMessage({ implSrc: implFilename }, [implPort]);
|
||||
resolve(hostPort);
|
||||
resolve({
|
||||
hostPort,
|
||||
disconnectFn: () => worker.postMessage({ disconnect: true }),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -56,6 +56,13 @@ export class Unarchiver extends EventTarget {
|
|||
*/
|
||||
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
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @returns {string} The MIME type of the archive.
|
||||
|
@ -164,7 +181,9 @@ export class Unarchiver extends EventTarget {
|
|||
* using the update() method.
|
||||
*/
|
||||
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) => {
|
||||
this.port_.onerror = (evt) => {
|
||||
console.log('Impl error: message = ' + evt.message);
|
||||
|
@ -221,7 +240,9 @@ export class Unarchiver extends EventTarget {
|
|||
stop() {
|
||||
if (this.port_) {
|
||||
this.port_.close();
|
||||
this.disconnectFn_();
|
||||
this.port_ = null;
|
||||
this.disconnectFn_ = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1466,8 +1466,28 @@ const onmessage = function (event) {
|
|||
*/
|
||||
export function connect(port) {
|
||||
if (hostPort) {
|
||||
throw `hostPort already connected`;
|
||||
throw `hostPort already connected in unrar.js`;
|
||||
}
|
||||
hostPort = port;
|
||||
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) {
|
||||
if (hostPort) {
|
||||
throw `hostPort already connected`;
|
||||
throw `hostPort already connected in untar.js`;
|
||||
}
|
||||
hostPort = port;
|
||||
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) {
|
||||
if (hostPort) {
|
||||
throw `hostPort already connected`;
|
||||
throw `hostPort already connected in unzip.js`;
|
||||
}
|
||||
|
||||
hostPort = port;
|
||||
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;
|
||||
|
||||
onmessage = async (evt) => {
|
||||
if (evt.data.implSrc) {
|
||||
const module = await import(evt.data.implSrc);
|
||||
module.connect(evt.ports[0]);
|
||||
} else if (evt.data.disconnect) {
|
||||
module.disconnect();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -80,7 +80,6 @@ const CompressorState = {
|
|||
FINISHED: 3,
|
||||
};
|
||||
let state = CompressorState.NOT_STARTED;
|
||||
let lastFileReceived = false;
|
||||
const crc32Table = createCRC32Table();
|
||||
|
||||
/** Helper functions. */
|
||||
|
@ -280,8 +279,21 @@ const onmessage = function(evt) {
|
|||
*/
|
||||
export function connect(port) {
|
||||
if (hostPort) {
|
||||
throw `hostPort already connected`;
|
||||
throw `hostPort already connected in zip.js`;
|
||||
}
|
||||
hostPort = port;
|
||||
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) {
|
||||
if (!(ab instanceof ArrayBuffer)) {
|
||||
console.error(typeof ab);
|
||||
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