1
0
Fork 0
mirror of https://github.com/codedread/bitjs synced 2025-10-03 09:39: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:
Jeff Schiller 2023-12-16 15:28:37 -08:00
parent eba7042abe
commit cf26e0a2de
17 changed files with 211 additions and 13 deletions

View file

@ -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 }),
});
});
}

View file

@ -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;
}
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -16,6 +16,10 @@
let implPort;
onmessage = async (evt) => {
const module = await import(evt.data.implSrc);
module.connect(evt.ports[0]);
if (evt.data.implSrc) {
const module = await import(evt.data.implSrc);
module.connect(evt.ports[0]);
} else if (evt.data.disconnect) {
module.disconnect();
}
};

View file

@ -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;
}

View file

@ -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';
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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.

View file

@ -0,0 +1,4 @@
filetype,extension
"text file","txt"
"JSON file","json"
"CSV file","csv"
1 filetype extension
2 text file txt
3 JSON file json
4 CSV file csv

View 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
View 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() });
});
}
});