From 2a2796c5f5f9bfc99d95644bc103e3b4ca856901 Mon Sep 17 00:00:00 2001 From: Christian Emmer Date: Fri, 28 Oct 2022 16:23:45 -0700 Subject: [PATCH] Feature: ability to work without DATs (#135) --- README.md | 32 +++--- src/console/progressBar.ts | 2 +- src/igir.ts | 16 ++- src/modules/argumentsParser.ts | 10 +- src/modules/candidateGenerator.ts | 1 - src/modules/datInferrer.ts | 66 ++++++++++++ src/modules/romScanner.ts | 10 +- src/modules/romWriter.ts | 1 + src/modules/scanner.ts | 9 +- src/types/datStatus.ts | 5 + src/types/options.ts | 5 + test/igir.test.ts | 150 ++++++++++++++++++-------- test/modules/argumentsParser.test.ts | 150 ++++++++++++++------------ test/modules/datInferrer.test.ts | 39 +++++++ test/modules/romScanner.test.ts | 8 +- test/modules/romWriter.test.ts | 63 +++-------- test/modules/statusGenerator.test.ts | 36 ++++--- test/types/files/archiveEntry.test.ts | 4 +- 18 files changed, 398 insertions(+), 209 deletions(-) create mode 100644 src/modules/datInferrer.ts create mode 100644 test/modules/datInferrer.test.ts diff --git a/README.md b/README.md index fee1dabb5..cd4f8adae 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ With a large ROM collection it can be difficult to: - Organize ROM files by console - Consistently name ROM files +- Make sure ROMs have the right extension - Archive ROMs individually in mass - Filter out duplicate ROMs - Filter out ROMs for languages you don't understand @@ -30,14 +31,15 @@ With a large ROM collection it can be difficult to: ## What does `igir` need? -`igir` needs two sets of files: +**`igir` needs an input set of ROMs, of course!** -1. ROMs (including ones with headers, see [docs](docs/rom-headers.md)) -2. One or more DATs (see [docs](docs/dats.md)) +Those ROMs can be in archives (`.001`, `.7z`, `.bz2`, `.gz`, `.rar`, `.tar.gz`, `.z01`, `.zip`, `.zipx`, and more!) or on their own. They can also contain a header or not (see [docs](docs/rom-headers.md)). -Many different input archive types are supported for both ROMs and DATs: .001, .7z, .bz2, .gz, .rar, .tar, .tgz, .xz, .z, .z01, .zip, .zipx, and more! +**`igir` works best with a set of DATs as well.** -`igir` then needs one or more commands: +Though not required, DATs can provide a lot of information for ROMs such as their correct name, and which ROMs are duplicates of others. See the [docs](docs/dats.md) for more information on DATs and some "_just tell me what to do_" instructions. + +**`igir` then needs one or more commands:** - `copy`: copy ROMs from input directories to an output directory - `move`: move ROMs from input directories to an output directory @@ -52,16 +54,16 @@ The `igir --help` command shown below includes examples of how to use multiple c `igir` runs these steps in the following order: -1. Scans the DAT input path for every file, parses them +1. Scans the DAT input path for every file and parses them, if specified 2. Scans each ROM input path for every file 1. Then detects headers in those files, if applicable (see [docs](docs/rom-headers.md)) 3. ROMs are matched to the DATs 1. Then filtering and sorting options are applied (see [docs](docs/rom-filtering.md)) - 2. Then ROMs are written to the output directory, if applicable (`copy`, `move`) - 3. Then written ROMs are tested for accuracy, if applicable (`test`) - 4. Then input ROMs are deleted, if applicable (`move`) -4. Unknown files are recycled from the output directory, if applicable (`clean`) -5. An output report is written to the output directory, if applicable (`report`) + 2. Then ROMs are written to the output directory, if specified (`copy`, `move`) + 3. Then written ROMs are tested for accuracy, if specified (`test`) + 4. Then input ROMs are deleted, if specified (`move`) +4. Unknown files are recycled from the output directory, if specified (`clean`) +5. An output report is written to the output directory, if specified (`report`) ## How do I run `igir`? @@ -181,15 +183,9 @@ Examples: smc ``` -## What are DATs? - -DATs are catalogs of every known ROM that exists per console, complete with enough information to identify each file. - -See the [DATs](docs/dats.md) page for a longer explanation on what DATs are, where to download them, and some "_just tell me what to do_" instructions. - ## How do I obtain ROMs? -Emulators are generally _legal_, as long as they don't include copyrighted software such as a console BIOS. Downloading ROM files that you do not own is piracy which is illegal in many countries. +Emulators are generally _legal_, as long as they don't include copyrighted software such as a console BIOS. Downloading ROM files that you do not own is piracy which is _illegal_ in many countries. See the [Dumping ROMs](docs/rom-dumping.md) page for more information. diff --git a/src/console/progressBar.ts b/src/console/progressBar.ts index 1c4cfdaa5..904fe497b 100644 --- a/src/console/progressBar.ts +++ b/src/console/progressBar.ts @@ -27,7 +27,7 @@ export default abstract class ProgressBar { abstract done(finishedMessage?: string): Promise; async doneItems(count: number, noun: string, verb: string): Promise { - return this.done(`${count.toLocaleString()} ${noun}${count !== 1 ? 's' : ''} ${verb}`); + return this.done(`${count.toLocaleString()} ${noun.trim()}${count !== 1 ? 's' : ''} ${verb}`); } abstract log(logLevel: LogLevel, message: string): Promise; diff --git a/src/igir.ts b/src/igir.ts index 741e31488..dc33ceb63 100644 --- a/src/igir.ts +++ b/src/igir.ts @@ -6,6 +6,7 @@ import ProgressBarCLI from './console/progressBarCLI.js'; import Constants from './constants.js'; import CandidateFilter from './modules/candidateFilter.js'; import CandidateGenerator from './modules/candidateGenerator.js'; +import DATInferrer from './modules/datInferrer.js'; import DATScanner from './modules/datScanner.js'; import HeaderProcessor from './modules/headerProcessor.js'; import OutputCleaner from './modules/outputCleaner.js'; @@ -31,9 +32,12 @@ export default class Igir { async main(): Promise { // Scan and process input files - const dats = await this.processDATScanner(); + let dats = await this.processDATScanner(); const rawRomFiles = await this.processROMScanner(); const processedRomFiles = await this.processHeaderProcessor(rawRomFiles); + if (!dats.length) { + dats = DATInferrer.infer(processedRomFiles); + } // Set up progress bar and input for DAT processing const datProcessProgressBar = this.logger.addProgressBar('Processing DATs', Symbols.PROCESSING, dats.length); @@ -100,9 +104,15 @@ export default class Igir { const progressBar = this.logger.addProgressBar('Scanning for DATs', Symbols.WAITING); const dats = await new DATScanner(this.options, progressBar).scan(); if (!dats.length) { - ProgressBarCLI.stop(); - throw new Error('No valid DAT files found!'); + progressBar.delete(); + if (this.options.usingDats()) { + ProgressBarCLI.stop(); + throw new Error('No valid DAT files found!'); + } + await progressBar.logWarn('No DAT files provided, consider using some for the best results!'); + return []; } + await progressBar.doneItems(dats.length, 'unique DAT', 'found'); await progressBar.freeze(); return dats; diff --git a/src/modules/argumentsParser.ts b/src/modules/argumentsParser.ts index 8a1a0bdcf..8d7063697 100644 --- a/src/modules/argumentsParser.ts +++ b/src/modules/argumentsParser.ts @@ -93,10 +93,8 @@ export default class ArgumentsParser { group: groupPaths, alias: 'd', description: 'Path(s) to DAT files or archives', - demandOption: true, type: 'array', requiresArg: true, - default: ['*.dat'], }) .option('input', { group: groupPaths, @@ -128,10 +126,17 @@ export default class ArgumentsParser { if (checkArgv.help) { return true; } + const needOutput = ['copy', 'move', 'zip', 'clean'].filter((command) => checkArgv._.indexOf(command) !== -1); if ((!checkArgv.output || !checkArgv.output.length) && needOutput.length) { throw new Error(`Missing required option for commands ${needOutput.join(', ')}: output`); } + + const needDat = ['report'].filter((command) => checkArgv._.indexOf(command) !== -1); + if ((!checkArgv.dat || !checkArgv.dat.length) && needDat.length) { + throw new Error(`Missing required option for commands ${needDat.join(', ')}: dat`); + } + return true; }) @@ -279,6 +284,7 @@ export default class ArgumentsParser { alias: 's', description: 'Output only a single game per parent (1G1R) (required for all options below, requires parent/clone DAT files)', type: 'boolean', + implies: 'dat', }) .option('prefer-verified', { group: groupPriority, diff --git a/src/modules/candidateGenerator.ts b/src/modules/candidateGenerator.ts index fafba3ce9..8a5374fa4 100644 --- a/src/modules/candidateGenerator.ts +++ b/src/modules/candidateGenerator.ts @@ -49,7 +49,6 @@ export default class CandidateGenerator { const hashCodeToInputFiles = CandidateGenerator.indexFilesByHashCode(inputRomFiles); await this.progressBar.logInfo(`${dat.getName()}: ${hashCodeToInputFiles.size} unique ROMs found`); - // TODO(cemmer): ability to work without DATs, generating a parent/game/release per file // For each parent, try to generate a parent candidate /* eslint-disable no-await-in-loop */ for (let i = 0; i < dat.getParents().length; i += 1) { diff --git a/src/modules/datInferrer.ts b/src/modules/datInferrer.ts new file mode 100644 index 000000000..47e637463 --- /dev/null +++ b/src/modules/datInferrer.ts @@ -0,0 +1,66 @@ +import path from 'path'; + +import ArchiveEntry from '../types/files/archiveEntry.js'; +import File from '../types/files/file.js'; +import DAT from '../types/logiqx/dat.js'; +import Game from '../types/logiqx/game.js'; +import Header from '../types/logiqx/header.js'; +import ROM from '../types/logiqx/rom.js'; + +export default class DATInferrer { + static infer(romFiles: File[]): DAT[] { + const datNamesToRomFiles = romFiles.reduce((map, file) => { + const datName = DATInferrer.getDatName(file); + const datRomFiles = map.get(datName) || []; + datRomFiles.push(file); + map.set(datName, datRomFiles); + return map; + }, new Map()); + + return [...datNamesToRomFiles.entries()] + .map(([datName, datRomFiles]) => DATInferrer.createDAT(datName, datRomFiles)); + } + + private static getDatName(file: File): string { + return path.dirname(file.getFilePath()); + } + + private static createDAT(datName: string, romFiles: File[]): DAT { + const header = new Header({ name: datName }); + + const gameNamesToRomFiles = romFiles.reduce((map, file) => { + const gameName = DATInferrer.getGameName(file); + const gameRomFiles = map.get(gameName) || []; + gameRomFiles.push(file); + map.set(gameName, gameRomFiles); + return map; + }, new Map()); + + const games = [...gameNamesToRomFiles.entries()].map(([gameName, gameRomFiles]) => { + const roms = gameRomFiles.map((romFile) => new ROM( + path.basename(romFile.getExtractedFilePath()), + romFile.getSize(), + romFile.getCrc32(), + )); + return new Game({ + name: gameName, + rom: roms, + }); + }); + + return new DAT(header, games); + } + + private static getGameName(file: File): string { + // Assume the game name is the filename + let fileName = file.getExtractedFilePath(); + if (file instanceof ArchiveEntry) { + // If the file is from an archive, assume the game name is the archive's filename + fileName = file.getArchive().getFilePath(); + } + + return path.basename(fileName) + .replace(/(\.[a-z0-9]+)+$/, '') + .trim(); + } +} diff --git a/src/modules/romScanner.ts b/src/modules/romScanner.ts index e3ff7eb92..39a97eae3 100644 --- a/src/modules/romScanner.ts +++ b/src/modules/romScanner.ts @@ -20,6 +20,14 @@ export default class ROMScanner extends Scanner { await this.progressBar.logInfo(`Found ${romFilePaths.length} ROM file${romFilePaths.length !== 1 ? 's' : ''}`); await this.progressBar.reset(romFilePaths.length); - return this.getFilesFromPaths(romFilePaths, Constants.ROM_SCANNER_THREADS); + const files = await this.getFilesFromPaths( + romFilePaths, + Constants.ROM_SCANNER_THREADS, + this.options.usingDats(), + ); + + await this.progressBar.doneItems(files.length, `${this.options.usingDats() ? 'unique ' : ''}ROM`, 'found'); + + return files; } } diff --git a/src/modules/romWriter.ts b/src/modules/romWriter.ts index 0aa3a80ee..4215b10bd 100644 --- a/src/modules/romWriter.ts +++ b/src/modules/romWriter.ts @@ -250,6 +250,7 @@ export default class ROMWriter { private async writeRawFile(inputRomFile: File, outputFilePath: string): Promise { try { + // TODO(cemmer): support raw->raw file moving without streams await inputRomFile.extractToStream(async (readStream) => { await this.progressBar.logDebug(`${inputRomFile.toString()}: piping to ${outputFilePath}`); const writeStream = readStream.pipe(fs.createWriteStream(outputFilePath)); diff --git a/src/modules/scanner.ts b/src/modules/scanner.ts index f440a1e41..6c7181a88 100644 --- a/src/modules/scanner.ts +++ b/src/modules/scanner.ts @@ -21,7 +21,11 @@ export default abstract class Scanner { this.progressBar = progressBar; } - protected async getFilesFromPaths(filePaths: string[], threads: number): Promise { + protected async getFilesFromPaths( + filePaths: string[], + threads: number, + filterUnique = true, + ): Promise { const foundFiles = (await async.mapLimit( filePaths, threads, @@ -32,6 +36,9 @@ export default abstract class Scanner { }, )) .flatMap((files) => files); + if (!filterUnique) { + return foundFiles; + } // Limit to unique files return [...foundFiles diff --git a/src/types/datStatus.ts b/src/types/datStatus.ts index 5348e90f9..373c05957 100644 --- a/src/types/datStatus.ts +++ b/src/types/datStatus.ts @@ -78,6 +78,11 @@ export default class DATStatus { const found = this.foundRomTypesToReleaseCandidates.get(type) || []; const all = this.allRomTypesToGames.get(type) || []; + // If we're not using a DAT then found===all + if (!options.usingDats()) { + return `${all.length.toLocaleString()} ${type}`; + } + const percentage = (found.length / all.length) * 100; let color: ChalkInstance; if (percentage >= 100) { diff --git a/src/types/options.ts b/src/types/options.ts index 5566af8e1..800ece7f0 100644 --- a/src/types/options.ts +++ b/src/types/options.ts @@ -237,6 +237,10 @@ export default class Options implements OptionsProps { // Options + usingDats(): boolean { + return this.dat.length > 0; + } + getDatFileCount(): number { return this.dat.length; } @@ -362,6 +366,7 @@ export default class Options implements OptionsProps { while (!fs.existsSync(input)) { input = path.dirname(input); } + output = input; } return path.join( diff --git a/test/igir.test.ts b/test/igir.test.ts index d747f09d8..f67dce29d 100644 --- a/test/igir.test.ts +++ b/test/igir.test.ts @@ -13,14 +13,18 @@ jest.setTimeout(10_000); const LOGGER = new Logger(LogLevel.NEVER); -async function expectEndToEnd(optionsProps: OptionsProps, expectedFiles: string[]): Promise { +async function expectEndToEnd( + datGlob: string | undefined, + optionsProps: OptionsProps, + expectedFiles: string[], +): Promise { const tempInput = fsPoly.mkdtempSync(Constants.GLOBAL_TEMP_DIR); fsPoly.copyDirSync('./test/fixtures', tempInput); const tempOutput = fsPoly.mkdtempSync(Constants.GLOBAL_TEMP_DIR); const options = new Options({ - dat: [path.join(tempInput, 'dats', '*')], + ...(datGlob ? { dat: [path.join(tempInput, datGlob)] } : {}), input: [path.join(tempInput, 'roms', '**', '*')], ...optionsProps, output: tempOutput, @@ -47,54 +51,108 @@ async function expectEndToEnd(optionsProps: OptionsProps, expectedFiles: string[ await Promise.all(reports.map(async (report) => fsPoly.rm(report))); } -it('should throw on no dats', async () => { - await expect(new Igir(new Options({}), LOGGER).main()).rejects.toThrow(/no valid dat/i); -}); +describe('with explicit dats', () => { + it('should do nothing with no roms', async () => { + await expectEndToEnd('dats/*', { + commands: ['copy'], + input: [], + }, []); + }); -it('should do nothing with no roms', async () => { - await expectEndToEnd({ - commands: ['copy'], - input: [], - }, []); -}); + it('should copy', async () => { + await expectEndToEnd('dats/*', { + commands: ['copy'], + }, [ + 'Fizzbuzz.rom', + 'Foobar.rom', + 'Lorem Ipsum.rom', + path.join('One Three', 'One.rom'), + path.join('One Three', 'Three.rom'), + ]); + }); -it('should copy', async () => { - await expectEndToEnd({ - commands: ['copy'], - }, [ - 'Fizzbuzz.rom', - 'Foobar.rom', - 'Lorem Ipsum.rom', - path.join('One Three', 'One.rom'), - path.join('One Three', 'Three.rom'), - ]); -}); + it('should copy and zip and test', async () => { + await expectEndToEnd('dats/*', { + commands: ['copy', 'zip'], + }, [ + 'Fizzbuzz.zip', + 'Foobar.zip', + 'Lorem Ipsum.zip', + 'One Three.zip', + ]); + }); -it('should copy and zip and test', async () => { - await expectEndToEnd({ - commands: ['copy', 'zip'], - }, [ - 'Fizzbuzz.zip', - 'Foobar.zip', - 'Lorem Ipsum.zip', - 'One Three.zip', - ]); -}); + it('should copy and clean', async () => { + await expectEndToEnd('dats/*', { + commands: ['copy', 'clean'], + }, [ + 'Fizzbuzz.rom', + 'Foobar.rom', + 'Lorem Ipsum.rom', + path.join('One Three', 'One.rom'), + path.join('One Three', 'Three.rom'), + ]); + }); -it('should copy and clean', async () => { - await expectEndToEnd({ - commands: ['copy', 'clean'], - }, [ - 'Fizzbuzz.rom', - 'Foobar.rom', - 'Lorem Ipsum.rom', - path.join('One Three', 'One.rom'), - path.join('One Three', 'Three.rom'), - ]); + it('should report without copy', async () => { + await expectEndToEnd('dats/*', { + commands: ['report'], + }, []); + }); }); -it('should report without copy', async () => { - await expectEndToEnd({ - commands: ['report'], - }, []); +describe('with inferred dats', () => { + it('should do nothing with no roms', async () => { + await expectEndToEnd(undefined, { + commands: ['copy'], + input: [], + }, []); + }); + + it('should copy', async () => { + await expectEndToEnd(undefined, { + commands: ['copy'], + }, [ + 'LCDTestROM.lnx', + 'allpads.nes', + 'color_test.nintendoentertainmentsystem', + 'diagnostic_test_cartridge.a78', + 'empty.rom', + 'fds_joypad_test.fds', + 'fizzbuzz.nes', + 'foobar.lnx', + 'loremipsum.rom', + 'one.rom', + path.join('onetwothree', 'one.rom'), + path.join('onetwothree', 'three.rom'), + path.join('onetwothree', 'two.rom'), + 'speed_test_v51.sfc', + 'speed_test_v51.smc', + 'three.rom', + 'two.rom', + 'unknown.rom', + ]); + }); + + it('should copy and zip and test', async () => { + await expectEndToEnd(undefined, { + commands: ['copy', 'zip'], + }, [ + 'LCDTestROM.zip', + 'allpads.zip', + 'color_test.zip', + 'diagnostic_test_cartridge.zip', + 'empty.zip', + 'fds_joypad_test.zip', + 'fizzbuzz.zip', + 'foobar.zip', + 'loremipsum.zip', + 'one.zip', + 'onetwothree.zip', + 'speed_test_v51.zip', + 'three.zip', + 'two.zip', + 'unknown.zip', + ]); + }); }); diff --git a/test/modules/argumentsParser.test.ts b/test/modules/argumentsParser.test.ts index 4548a94d6..82fbd9d0d 100644 --- a/test/modules/argumentsParser.test.ts +++ b/test/modules/argumentsParser.test.ts @@ -26,11 +26,11 @@ describe('commands', () => { expect(argumentsParser.parse(['zip', ...dummyRequiredArgs]).shouldZip('')).toEqual(true); expect(argumentsParser.parse(['clean', ...dummyRequiredArgs]).shouldClean()).toEqual(true); expect(argumentsParser.parse(['test', ...dummyRequiredArgs]).shouldTest()).toEqual(true); - expect(argumentsParser.parse(['report', ...dummyRequiredArgs]).shouldReport()).toEqual(true); + expect(argumentsParser.parse(['report', ...dummyRequiredArgs, '--dat', os.devNull]).shouldReport()).toEqual(true); }); it('should parse multiple commands', () => { - const commands = ['copy', 'move', 'zip', 'clean', 'test', 'report', ...dummyRequiredArgs]; + const commands = ['copy', 'move', 'zip', 'clean', 'test', 'report', ...dummyRequiredArgs, '--dat', os.devNull]; expect(argumentsParser.parse(commands).shouldCopy()).toEqual(true); expect(argumentsParser.parse(commands).shouldMove()).toEqual(true); expect(argumentsParser.parse(commands).shouldZip('')).toEqual(true); @@ -93,9 +93,13 @@ describe('options', () => { }); it('should parse "dat"', async () => { - const src = await argumentsParser.parse(['test', '--input', os.devNull, '--dat', './src']).scanDatFiles(); + expect(() => argumentsParser.parse(['report', '--input', os.devNull])).toThrow(/missing required option/i); + expect(() => argumentsParser.parse(['test', '--input', os.devNull, '--dat'])).toThrow(/not enough arguments/i); + await expect(argumentsParser.parse(['test', '--input', os.devNull]).scanDatFiles()).resolves.toHaveLength(0); + + const src = await argumentsParser.parse(['test', '--input', os.devNull, '-d', './src']).scanDatFiles(); const test = await argumentsParser.parse(['test', '--input', os.devNull, '--dat', './test']).scanDatFiles(); - const both = await argumentsParser.parse(['test', '--input', os.devNull, '--dat', './src', '--dat', './test']).scanDatFiles(); + const both = await argumentsParser.parse(['test', '--input', os.devNull, '--dat', './src', '-d', './test']).scanDatFiles(); expect(src.length).toBeGreaterThan(0); expect(test.length).toBeGreaterThan(0); expect(both.length).toEqual(src.length + test.length); @@ -126,8 +130,8 @@ describe('options', () => { expect(() => argumentsParser.parse(['move', '--input', os.devNull])).toThrow(/missing required option/i); expect(() => argumentsParser.parse(['zip', '--input', os.devNull])).toThrow(/missing required option/i); expect(() => argumentsParser.parse(['clean', '--input', os.devNull])).toThrow(/missing required option/i); - expect(argumentsParser.parse(['test', '--input', os.devNull]).getOutput()).toContain(Constants.GLOBAL_TEMP_DIR); - expect(argumentsParser.parse(['report', '--input', os.devNull]).getOutput()).toContain(Constants.GLOBAL_TEMP_DIR); + expect(argumentsParser.parse(['test', '--dat', os.devNull, '--input', os.devNull]).getOutput()).toContain(Constants.GLOBAL_TEMP_DIR); + expect(argumentsParser.parse(['report', '--dat', os.devNull, '--input', os.devNull]).getOutput()).toContain(Constants.GLOBAL_TEMP_DIR); // Test value expect(argumentsParser.parse(['copy', '--input', os.devNull, '-o', 'foo']).getOutput()).toEqual('foo'); expect(argumentsParser.parse(['copy', '--input', os.devNull, '--output', 'foo']).getOutput()).toEqual('foo'); @@ -149,13 +153,14 @@ describe('options', () => { }); it('should parse "dir-datname"', () => { - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '-D']).getDirDatName()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dir-dat-name']).getDirDatName()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dir-dat-name', 'true']).getDirDatName()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dir-dat-name', 'false']).getDirDatName()).toEqual(false); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dir-dat-name', '--dir-dat-name']).getDirDatName()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dir-dat-name', 'false', '--dir-dat-name', 'true']).getDirDatName()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dir-dat-name', 'true', '--dir-dat-name', 'false']).getDirDatName()).toEqual(false); + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dir-dat-name']).getDirDatName()).toThrow(/dependent|implication/i); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '-D']).getDirDatName()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--dir-dat-name']).getDirDatName()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--dir-dat-name', 'true']).getDirDatName()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--dir-dat-name', 'false']).getDirDatName()).toEqual(false); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--dir-dat-name', '--dir-dat-name']).getDirDatName()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--dir-dat-name', 'false', '--dir-dat-name', 'true']).getDirDatName()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--dir-dat-name', 'true', '--dir-dat-name', 'false']).getDirDatName()).toEqual(false); }); it('should parse "dir-letter"', () => { @@ -168,13 +173,14 @@ describe('options', () => { }); it('should parse "single"', () => { - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '-s']).getSingle()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--single']).getSingle()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--single', 'true']).getSingle()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--single', 'false']).getSingle()).toEqual(false); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--single', '--single']).getSingle()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--single', 'false', '--single', 'true']).getSingle()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--single', 'true', '--single', 'false']).getSingle()).toEqual(false); + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--single']).getSingle()).toThrow(/dependent|implication/i); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '-s']).getSingle()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--single']).getSingle()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--single', 'true']).getSingle()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--single', 'false']).getSingle()).toEqual(false); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--single', '--single']).getSingle()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--single', 'false', '--single', 'true']).getSingle()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--single', 'true', '--single', 'false']).getSingle()).toEqual(false); }); it('should parse "zip-exclude"', () => { @@ -214,84 +220,92 @@ describe('options', () => { it('should parse "prefer-verified"', () => { expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-verified'])).toThrow(/dependent|implication/i); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-verified', '--single']).getPreferVerified()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-verified', 'true', '--single']).getPreferVerified()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-verified', 'false', '--single']).getPreferVerified()).toEqual(false); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-verified', '--prefer-verified', '--single']).getPreferVerified()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-verified', 'false', '--prefer-verified', 'true', '--single']).getPreferVerified()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-verified', 'true', '--prefer-verified', 'false', '--single']).getPreferVerified()).toEqual(false); + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-verified', '--single'])).toThrow(/dependent|implication/i); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-verified', '--single']).getPreferVerified()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-verified', 'true', '--single']).getPreferVerified()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-verified', 'false', '--single']).getPreferVerified()).toEqual(false); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-verified', '--prefer-verified', '--single']).getPreferVerified()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-verified', 'false', '--prefer-verified', 'true', '--single']).getPreferVerified()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-verified', 'true', '--prefer-verified', 'false', '--single']).getPreferVerified()).toEqual(false); }); it('should parse "prefer-good"', () => { expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-good'])).toThrow(/dependent|implication/i); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-good', '--single']).getPreferGood()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-good', 'true', '--single']).getPreferGood()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-good', 'false', '--single']).getPreferGood()).toEqual(false); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-good', '--prefer-good', '--single']).getPreferGood()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-good', 'false', '--prefer-good', 'true', '--single']).getPreferGood()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-good', 'true', '--prefer-good', 'false', '--single']).getPreferGood()).toEqual(false); + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-good', '--single'])).toThrow(/dependent|implication/i); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-good', '--single']).getPreferGood()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-good', 'true', '--single']).getPreferGood()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-good', 'false', '--single']).getPreferGood()).toEqual(false); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-good', '--prefer-good', '--single']).getPreferGood()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-good', 'false', '--prefer-good', 'true', '--single']).getPreferGood()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-good', 'true', '--prefer-good', 'false', '--single']).getPreferGood()).toEqual(false); }); it('should parse "prefer-language"', () => { expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-language'])).toThrow(/not enough arguments/i); expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-language', 'EN'])).toThrow(/dependent|implication/i); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '-l', 'EN', '--single']).getPreferLanguages()).toEqual(['EN']); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-language', 'EN', '--single']).getPreferLanguages()).toEqual(['EN']); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-language', 'EN,it', '--single']).getPreferLanguages()).toEqual(['EN', 'IT']); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-language', 'en,IT,JA', '--single']).getPreferLanguages()).toEqual(['EN', 'IT', 'JA']); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-language', 'EN,en', '--single']).getPreferLanguages()).toEqual(['EN']); + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-language', 'EN', '--single'])).toThrow(/dependent|implication/i); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '-l', 'EN', '--single']).getPreferLanguages()).toEqual(['EN']); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-language', 'EN', '--single']).getPreferLanguages()).toEqual(['EN']); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-language', 'EN,it', '--single']).getPreferLanguages()).toEqual(['EN', 'IT']); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-language', 'en,IT,JA', '--single']).getPreferLanguages()).toEqual(['EN', 'IT', 'JA']); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-language', 'EN,en', '--single']).getPreferLanguages()).toEqual(['EN']); }); it('should parse "prefer-region"', () => { expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-region'])).toThrow(/not enough arguments/i); expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-region', 'USA'])).toThrow(/dependent|implication/i); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '-r', 'USA', '--single']).getPreferRegions()).toEqual(['USA']); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-region', 'USA', '--single']).getPreferRegions()).toEqual(['USA']); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-region', 'USA,eur', '--single']).getPreferRegions()).toEqual(['USA', 'EUR']); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-region', 'usa,EUR,JPN', '--single']).getPreferRegions()).toEqual(['USA', 'EUR', 'JPN']); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-region', 'USA,usa', '--single']).getPreferRegions()).toEqual(['USA']); + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-region', 'USA', '--single'])).toThrow(/dependent|implication/i); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '-r', 'USA', '--single']).getPreferRegions()).toEqual(['USA']); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-region', 'USA', '--single']).getPreferRegions()).toEqual(['USA']); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-region', 'USA,eur', '--single']).getPreferRegions()).toEqual(['USA', 'EUR']); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-region', 'usa,EUR,JPN', '--single']).getPreferRegions()).toEqual(['USA', 'EUR', 'JPN']); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-region', 'USA,usa', '--single']).getPreferRegions()).toEqual(['USA']); }); it('should parse "prefer-revision-newer"', () => { expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-newer'])).toThrow(/dependent|implication/i); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-newer', '--single']).getPreferRevisionNewer()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-newer', 'true', '--single']).getPreferRevisionNewer()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-newer', 'false', '--single']).getPreferRevisionNewer()).toEqual(false); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-newer', '--prefer-revision-newer', '--single']).getPreferRevisionNewer()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-newer', 'false', '--prefer-revision-newer', 'true', '--single']).getPreferRevisionNewer()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-newer', 'true', '--prefer-revision-newer', 'false', '--single']).getPreferRevisionNewer()).toEqual(false); - expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-newer', '--prefer-revision-older', '--single'])).toThrow(/mutually exclusive/i); + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-newer', '--single'])).toThrow(/dependent|implication/i); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-revision-newer', '--single']).getPreferRevisionNewer()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-revision-newer', 'true', '--single']).getPreferRevisionNewer()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-revision-newer', 'false', '--single']).getPreferRevisionNewer()).toEqual(false); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-revision-newer', '--prefer-revision-newer', '--single']).getPreferRevisionNewer()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-revision-newer', 'false', '--prefer-revision-newer', 'true', '--single']).getPreferRevisionNewer()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-revision-newer', 'true', '--prefer-revision-newer', 'false', '--single']).getPreferRevisionNewer()).toEqual(false); + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-revision-newer', '--prefer-revision-older', '--single'])).toThrow(/mutually exclusive/i); }); it('should parse "prefer-revision-older"', () => { expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-older'])).toThrow(/dependent|implication/i); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-older', '--single']).getPreferRevisionOlder()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-older', 'true', '--single']).getPreferRevisionOlder()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-older', 'false', '--single']).getPreferRevisionOlder()).toEqual(false); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-older', '--prefer-revision-older', '--single']).getPreferRevisionOlder()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-older', 'false', '--prefer-revision-older', 'true', '--single']).getPreferRevisionOlder()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-older', 'true', '--prefer-revision-older', 'false', '--single']).getPreferRevisionOlder()).toEqual(false); - expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-older', '--prefer-revision-newer', '--single'])).toThrow(/mutually exclusive/i); + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-revision-older', '--single'])).toThrow(/dependent|implication/i); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-revision-older', '--single']).getPreferRevisionOlder()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-revision-older', 'true', '--single']).getPreferRevisionOlder()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-revision-older', 'false', '--single']).getPreferRevisionOlder()).toEqual(false); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-revision-older', '--prefer-revision-older', '--single']).getPreferRevisionOlder()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-revision-older', 'false', '--prefer-revision-older', 'true', '--single']).getPreferRevisionOlder()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-revision-older', 'true', '--prefer-revision-older', 'false', '--single']).getPreferRevisionOlder()).toEqual(false); + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-revision-older', '--prefer-revision-newer', '--single'])).toThrow(/mutually exclusive/i); }); it('should parse "prefer-retail"', () => { expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-retail'])).toThrow(/dependent|implication/i); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-retail', '--single']).getPreferRetail()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-retail', 'true', '--single']).getPreferRetail()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-retail', 'false', '--single']).getPreferRetail()).toEqual(false); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-retail', '--prefer-retail', '--single']).getPreferRetail()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-retail', 'false', '--prefer-retail', 'true', '--single']).getPreferRetail()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-retail', 'true', '--prefer-retail', 'false', '--single']).getPreferRetail()).toEqual(false); + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-retail', '--single'])).toThrow(/dependent|implication/i); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-retail', '--single']).getPreferRetail()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-retail', 'true', '--single']).getPreferRetail()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-retail', 'false', '--single']).getPreferRetail()).toEqual(false); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-retail', '--prefer-retail', '--single']).getPreferRetail()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-retail', 'false', '--prefer-retail', 'true', '--single']).getPreferRetail()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-retail', 'true', '--prefer-retail', 'false', '--single']).getPreferRetail()).toEqual(false); }); it('should parse "prefer-parent"', () => { expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-parent'])).toThrow(/dependent|implication/i); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-parent', '--single']).getPreferParent()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-parent', 'true', '--single']).getPreferParent()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-parent', 'false', '--single']).getPreferParent()).toEqual(false); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-parent', '--prefer-parent', '--single']).getPreferParent()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-parent', 'false', '--prefer-parent', 'true', '--single']).getPreferParent()).toEqual(true); - expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-parent', 'true', '--prefer-parent', 'false', '--single']).getPreferParent()).toEqual(false); + expect(() => argumentsParser.parse([...dummyCommandAndRequiredArgs, '--prefer-parent', '--single'])).toThrow(/dependent|implication/i); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-parent', '--single']).getPreferParent()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-parent', 'true', '--single']).getPreferParent()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-parent', 'false', '--single']).getPreferParent()).toEqual(false); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-parent', '--prefer-parent', '--single']).getPreferParent()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-parent', 'false', '--prefer-parent', 'true', '--single']).getPreferParent()).toEqual(true); + expect(argumentsParser.parse([...dummyCommandAndRequiredArgs, '--dat', os.devNull, '--prefer-parent', 'true', '--prefer-parent', 'false', '--single']).getPreferParent()).toEqual(false); }); it('should parse "language-filter"', () => { diff --git a/test/modules/datInferrer.test.ts b/test/modules/datInferrer.test.ts new file mode 100644 index 000000000..3bee7a732 --- /dev/null +++ b/test/modules/datInferrer.test.ts @@ -0,0 +1,39 @@ +import path from 'path'; + +import DATInferrer from '../../src/modules/datInferrer.js'; +import ROMScanner from '../../src/modules/romScanner.js'; +import Options from '../../src/types/options.js'; +import ProgressBarFake from '../console/progressBarFake.js'; + +test.each([ + ['test/fixtures/roms/**/*', { + [path.join('test', 'fixtures', 'roms')]: 5, + [path.join('test', 'fixtures', 'roms', '7z')]: 5, + [path.join('test', 'fixtures', 'roms', 'headered')]: 6, + [path.join('test', 'fixtures', 'roms', 'rar')]: 5, + [path.join('test', 'fixtures', 'roms', 'raw')]: 8, + [path.join('test', 'fixtures', 'roms', 'tar')]: 5, + [path.join('test', 'fixtures', 'roms', 'unheadered')]: 1, + [path.join('test', 'fixtures', 'roms', 'zip')]: 5, + }], + ['test/fixtures/roms/7z/*', { [path.join('test', 'fixtures', 'roms', '7z')]: 5 }], + ['test/fixtures/roms/rar/*', { [path.join('test', 'fixtures', 'roms', 'rar')]: 5 }], + ['test/fixtures/roms/raw/*', { [path.join('test', 'fixtures', 'roms', 'raw')]: 8 }], + ['test/fixtures/roms/tar/*', { [path.join('test', 'fixtures', 'roms', 'tar')]: 5 }], + ['test/fixtures/roms/zip/*', { [path.join('test', 'fixtures', 'roms', 'zip')]: 5 }], +])('should infer DATs: %s', async (inputGlob, expected) => { + // Given + const romFiles = await new ROMScanner(new Options({ + input: [inputGlob], + }), new ProgressBarFake()).scan(); + + // When + const dats = DATInferrer.infer(romFiles); + + // Then + const datNameToGameCount = dats.reduce((map, dat) => ({ + ...map, + [dat.getName()]: dat.getGames().length, + }), {}); + expect(datNameToGameCount).toEqual(expected); +}); diff --git a/test/modules/romScanner.test.ts b/test/modules/romScanner.test.ts index 4b9177390..3de4c85ca 100644 --- a/test/modules/romScanner.test.ts +++ b/test/modules/romScanner.test.ts @@ -33,7 +33,7 @@ it('should not throw on bad archives', async () => { describe('multiple files', () => { it('no files are excluded', async () => { - const expectedRomFiles = 15; + const expectedRomFiles = 48; await expect(createRomScanner(['test/fixtures/roms']).scan()).resolves.toHaveLength(expectedRomFiles); await expect(createRomScanner(['test/fixtures/roms/*', 'test/fixtures/roms/**/*']).scan()).resolves.toHaveLength(expectedRomFiles); await expect(createRomScanner(['test/fixtures/roms/**/*']).scan()).resolves.toHaveLength(expectedRomFiles); @@ -41,9 +41,9 @@ describe('multiple files', () => { }); it('some files are excluded', async () => { - await expect(createRomScanner(['test/fixtures/roms/**/*'], ['test/fixtures/roms/**/*.rom']).scan()).resolves.toHaveLength(14); - await expect(createRomScanner(['test/fixtures/roms/**/*'], ['test/fixtures/roms/**/*.rom', 'test/fixtures/roms/**/*.rom']).scan()).resolves.toHaveLength(14); - await expect(createRomScanner(['test/fixtures/roms/**/*'], ['test/fixtures/roms/**/*.rom', 'test/fixtures/roms/**/*.zip']).scan()).resolves.toHaveLength(13); + await expect(createRomScanner(['test/fixtures/roms/**/*'], ['test/fixtures/roms/**/*.rom']).scan()).resolves.toHaveLength(41); + await expect(createRomScanner(['test/fixtures/roms/**/*'], ['test/fixtures/roms/**/*.rom', 'test/fixtures/roms/**/*.rom']).scan()).resolves.toHaveLength(41); + await expect(createRomScanner(['test/fixtures/roms/**/*'], ['test/fixtures/roms/**/*.rom', 'test/fixtures/roms/**/*.zip']).scan()).resolves.toHaveLength(32); }); it('all files are excluded', async () => { diff --git a/test/modules/romWriter.test.ts b/test/modules/romWriter.test.ts index 69e8870fe..c3cb7d6c8 100644 --- a/test/modules/romWriter.test.ts +++ b/test/modules/romWriter.test.ts @@ -5,6 +5,7 @@ import path from 'path'; import Constants from '../../src/constants.js'; import CandidateGenerator from '../../src/modules/candidateGenerator.js'; +import DATInferrer from '../../src/modules/datInferrer.js'; import HeaderProcessor from '../../src/modules/headerProcessor.js'; import ROMScanner from '../../src/modules/romScanner.js'; import ROMWriter from '../../src/modules/romWriter.js'; @@ -12,10 +13,8 @@ import fsPoly from '../../src/polyfill/fsPoly.js'; import ArchiveFactory from '../../src/types/archives/archiveFactory.js'; import File from '../../src/types/files/file.js'; import DAT from '../../src/types/logiqx/dat.js'; -import Game from '../../src/types/logiqx/game.js'; import Header from '../../src/types/logiqx/header.js'; import Parent from '../../src/types/logiqx/parent.js'; -import ROM from '../../src/types/logiqx/rom.js'; import Options, { OptionsProps } from '../../src/types/options.js'; import ReleaseCandidate from '../../src/types/releaseCandidate.js'; import ProgressBarFake from '../console/progressBarFake.js'; @@ -68,69 +67,41 @@ async function walkAndStat(dirPath: string): Promise<[string, Stats][]> { ); } -function datScanner(gameNameToFiles: Map): DAT { - const games = [...gameNameToFiles.entries()] - .map(([gameName, files]) => { - const roms = files - // De-duplicate - .filter((one, idx, romFiles) => romFiles - .findIndex((two) => two.getExtractedFilePath() === one.getExtractedFilePath()) === idx) - // Map - .map((file) => new ROM( - path.basename(file.getExtractedFilePath()), - file.getSize(), - file.getCrc32(), - )); - return new Game({ - name: gameName, - rom: roms, - }); - }); - return new DAT(new Header(), games); +function datInferrer(romFiles: File[]): DAT { + // Run DATInferrer, but condense all DATs down to one + const datGames = DATInferrer.infer(romFiles) + .map((dat) => dat.getGames()) + .flatMap((games) => games); + return new DAT(new Header(), datGames); } async function romScanner( options: Options, inputDir: string, inputGlob: string, -): Promise> { +): Promise { return (await new ROMScanner(new Options({ ...options, + dat: [''], // force ROMScanner to unique files input: [path.join(inputDir, inputGlob)], }), new ProgressBarFake()).scan()) // Reduce all the unique files for all games .filter((one, idx, files) => files - .findIndex((two) => two.equals(one)) === idx) - // Map - .reduce((map, romFile) => { - const romName = path.parse(romFile.getFilePath()).name.replace(/\.[a-z]+$/, ''); - if (map.has(romName)) { - map.set(romName, [...map.get(romName) as File[], romFile]); - } else { - map.set(romName, [romFile]); - } - return map; - }, new Map()); + .findIndex((two) => two.hashCodes().join() === one.hashCodes().join()) === idx); } async function headerProcessor( options: Options, - gameNameToFiles: Map, -): Promise> { - return new Map(await Promise.all([...gameNameToFiles.entries()] - .map(async ([gameName, files]): Promise<[string, File[]]> => { - const headeredFiles = await new HeaderProcessor(options, new ProgressBarFake()) - .process(files); - return [gameName, headeredFiles]; - }))); + romFiles: File[], +): Promise { + return new HeaderProcessor(options, new ProgressBarFake()).process(romFiles); } async function candidateGenerator( options: Options, dat: DAT, - gameNameToFiles: Map, + romFiles: File[], ): Promise> { - const romFiles = [...gameNameToFiles.values()].flatMap((files) => files); return new CandidateGenerator(options, new ProgressBarFake()).generate(dat, romFiles); } @@ -146,9 +117,9 @@ async function romWriter( input: [inputTemp], output: outputTemp, }); - const gameNameToFiles = await romScanner(options, inputTemp, inputGlob); - const dat = datScanner(gameNameToFiles); - const gameNamesToHeaderedFiles = await headerProcessor(options, gameNameToFiles); + const romFiles = await romScanner(options, inputTemp, inputGlob); + const dat = datInferrer(romFiles); + const gameNamesToHeaderedFiles = await headerProcessor(options, romFiles); const candidates = await candidateGenerator(options, dat, gameNamesToHeaderedFiles); // When diff --git a/test/modules/statusGenerator.test.ts b/test/modules/statusGenerator.test.ts index 3a1cff942..2202106c8 100644 --- a/test/modules/statusGenerator.test.ts +++ b/test/modules/statusGenerator.test.ts @@ -7,7 +7,7 @@ import Header from '../../src/types/logiqx/header.js'; import Parent from '../../src/types/logiqx/parent.js'; import Release from '../../src/types/logiqx/release.js'; import ROM from '../../src/types/logiqx/rom.js'; -import Options from '../../src/types/options.js'; +import Options, { OptionsProps } from '../../src/types/options.js'; import ReleaseCandidate from '../../src/types/releaseCandidate.js'; import ROMWithFiles from '../../src/types/romWithFiles.js'; import ProgressBarFake from '../console/progressBarFake.js'; @@ -18,6 +18,10 @@ const gameNamePrototype = 'game prototype (proto)'; const gameNameSingleRom = 'game with single rom'; const gameNameMultipleRoms = 'game with multiple roms'; +const defaultOptions: OptionsProps = { + dat: [''], // force "is using DATs" +}; + const dat = new DAT(new Header({ name: 'dat', }), [ @@ -67,28 +71,28 @@ async function getParentToReleaseCandidates( describe('toConsole', () => { describe('no candidates', () => { it('should return games, BIOSes, and retail as missing', async () => { - const options = new Options(); + const options = new Options(defaultOptions); const datStatus = await new StatusGenerator(options, new ProgressBarFake()) .output(dat, new Map()); expect(stripAnsi(datStatus.toConsole(options))).toEqual('1/5 games, 0/1 BIOSes, 1/4 retail releases found'); }); it('should return games and retail as missing', async () => { - const options = new Options({ noBios: true }); + const options = new Options({ ...defaultOptions, noBios: true }); const datStatus = await new StatusGenerator(options, new ProgressBarFake()) .output(dat, new Map()); expect(stripAnsi(datStatus.toConsole(options))).toEqual('1/5 games, 1/4 retail releases found'); }); it('should return BIOSes as missing', async () => { - const options = new Options({ onlyBios: true }); + const options = new Options({ ...defaultOptions, onlyBios: true }); const datStatus = await new StatusGenerator(options, new ProgressBarFake()) .output(dat, new Map()); expect(stripAnsi(datStatus.toConsole(options))).toEqual('0/1 BIOSes found'); }); it('should return BIOSes and retail as missing', async () => { - const options = new Options({ single: true }); + const options = new Options({ ...defaultOptions, single: true }); const datStatus = await new StatusGenerator(options, new ProgressBarFake()) .output(dat, new Map()); expect(stripAnsi(datStatus.toConsole(options))).toEqual('0/1 BIOSes, 1/4 retail releases found'); @@ -97,7 +101,7 @@ describe('toConsole', () => { describe('partially missing', () => { it('should return bios as found', async () => { - const options = new Options(); + const options = new Options(defaultOptions); const map = new Map([ await getParentToReleaseCandidates(gameNameBios), ]); @@ -107,7 +111,7 @@ describe('toConsole', () => { }); it('should return prototype as found', async () => { - const options = new Options(); + const options = new Options(defaultOptions); const map = new Map([ await getParentToReleaseCandidates(gameNamePrototype), ]); @@ -117,7 +121,7 @@ describe('toConsole', () => { }); it('should return game with single rom as found', async () => { - const options = new Options(); + const options = new Options(defaultOptions); const map = new Map([ await getParentToReleaseCandidates(gameNameSingleRom), ]); @@ -128,7 +132,7 @@ describe('toConsole', () => { }); it('should return none missing', async () => { - const options = new Options(); + const options = new Options(defaultOptions); const map = new Map([ await getParentToReleaseCandidates(gameNameBios), await getParentToReleaseCandidates(gameNamePrototype), @@ -144,7 +148,7 @@ describe('toConsole', () => { describe('toCSV', () => { describe('no candidates', () => { it('should return games, BIOSes, and retail as missing', async () => { - const options = new Options(); + const options = new Options(defaultOptions); const datStatus = await new StatusGenerator(options, new ProgressBarFake()) .output(dat, new Map()); await expect(datStatus.toCSV(options)).resolves.toEqual(`DAT Name,Game Name,Status,ROM Files,BIOS,Retail Release,Unlicensed,Demo,Beta,Sample,Prototype,Test,Aftermarket,Homebrew,Bad @@ -168,7 +172,7 @@ dat,no roms,FOUND,,false,true,false,false,false,false,false,false,false,false,fa }); it('should return BIOSes as missing', async () => { - const options = new Options({ onlyBios: true }); + const options = new Options({ ...defaultOptions, onlyBios: true }); const datStatus = await new StatusGenerator(options, new ProgressBarFake()) .output(dat, new Map()); await expect(datStatus.toCSV(options)).resolves.toEqual(`DAT Name,Game Name,Status,ROM Files,BIOS,Retail Release,Unlicensed,Demo,Beta,Sample,Prototype,Test,Aftermarket,Homebrew,Bad @@ -176,7 +180,7 @@ dat,bios,MISSING,,true,true,false,false,false,false,false,false,false,false,fals }); it('should return BIOSes and retail as missing', async () => { - const options = new Options({ single: true }); + const options = new Options({ ...defaultOptions, single: true }); const datStatus = await new StatusGenerator(options, new ProgressBarFake()) .output(dat, new Map()); await expect(datStatus.toCSV(options)).resolves.toEqual(`DAT Name,Game Name,Status,ROM Files,BIOS,Retail Release,Unlicensed,Demo,Beta,Sample,Prototype,Test,Aftermarket,Homebrew,Bad @@ -189,7 +193,7 @@ dat,no roms,FOUND,,false,true,false,false,false,false,false,false,false,false,fa describe('partially missing', () => { it('should return bios as found', async () => { - const options = new Options(); + const options = new Options(defaultOptions); const map = new Map([ await getParentToReleaseCandidates(gameNameBios), ]); @@ -204,7 +208,7 @@ dat,no roms,FOUND,,false,true,false,false,false,false,false,false,false,false,fa }); it('should return prototype as found', async () => { - const options = new Options(); + const options = new Options(defaultOptions); const map = new Map([ await getParentToReleaseCandidates(gameNamePrototype), ]); @@ -219,7 +223,7 @@ dat,no roms,FOUND,,false,true,false,false,false,false,false,false,false,false,fa }); it('should return game with single rom as found', async () => { - const options = new Options(); + const options = new Options(defaultOptions); const map = new Map([ await getParentToReleaseCandidates(gameNameSingleRom), ]); @@ -235,7 +239,7 @@ dat,no roms,FOUND,,false,true,false,false,false,false,false,false,false,false,fa }); it('should return none missing', async () => { - const options = new Options(); + const options = new Options(defaultOptions); const map = new Map([ await getParentToReleaseCandidates(gameNameBios), await getParentToReleaseCandidates(gameNamePrototype), diff --git a/test/types/files/archiveEntry.test.ts b/test/types/files/archiveEntry.test.ts index ad37c0d41..44d51d13c 100644 --- a/test/types/files/archiveEntry.test.ts +++ b/test/types/files/archiveEntry.test.ts @@ -131,7 +131,7 @@ describe('extractToFile', () => { './test/fixtures/roms/7z', ], }), new ProgressBarFake()).scan(); - expect(archiveEntries).toHaveLength(7); + expect(archiveEntries).toHaveLength(21); const temp = fsPoly.mkdtempSync(Constants.GLOBAL_TEMP_DIR); /* eslint-disable no-await-in-loop */ @@ -156,7 +156,7 @@ describe('extractToStream', () => { './test/fixtures/roms/7z', ], }), new ProgressBarFake()).scan(); - expect(archiveEntries).toHaveLength(7); + expect(archiveEntries).toHaveLength(21); const temp = fsPoly.mkdtempSync(Constants.GLOBAL_TEMP_DIR); /* eslint-disable no-await-in-loop */