From 05f11b9033f237b5c15aeacab3f8f6fa3be4eee5 Mon Sep 17 00:00:00 2001 From: Brian Donovan <1938+eventualbuddha@users.noreply.github.com> Date: Thu, 25 Jul 2024 15:57:44 -0700 Subject: [PATCH] test: switch back to `babel`-based coverage c8/v8 coverage is buggy post v20.9.0: https://github.com/nodejs/node/issues/51251 --- .node-version | 2 +- apps/admin/backend/jest.config.js | 10 +- apps/admin/backend/src/adjudication.ts | 11 +- apps/admin/backend/src/app.ts | 8 +- apps/admin/backend/src/cast_vote_records.ts | 6 +- .../src/exports/csv_ballot_count_report.ts | 4 +- apps/admin/backend/src/reports/readiness.ts | 3 +- apps/admin/backend/src/reports/titles.ts | 2 +- apps/admin/backend/src/server.ts | 6 +- apps/admin/backend/src/store.ts | 9 +- .../backend/src/tabulation/card_counts.ts | 2 +- .../backend/src/tabulation/full_results.ts | 2 +- .../admin/backend/src/tabulation/write_ins.ts | 2 +- apps/admin/backend/src/util/auth.ts | 4 +- apps/admin/backend/src/util/cdf_results.ts | 2 +- apps/admin/backend/src/util/export_file.ts | 3 +- apps/admin/backend/src/util/write_ins.ts | 6 +- apps/admin/backend/src/util/zip.ts | 2 +- apps/central-scan/backend/jest.config.js | 8 +- .../backend/src/readiness_report.ts | 3 +- apps/central-scan/backend/src/server.ts | 6 +- apps/scan/backend/jest.config.js | 6 +- apps/scan/backend/src/export.ts | 3 +- apps/scan/backend/src/printing/printer.ts | 3 +- .../src/scanners/custom/state_machine.ts | 6 +- .../backend/src/scanners/pdi/state_machine.ts | 12 +- .../election_package_io.test.ts | 6 +- libs/custom-scanner/jest.config.js | 2 +- libs/custom-scanner/src/custom_a4_scanner.ts | 23 ++- libs/custom-scanner/src/mocks/coder.ts | 4 +- libs/custom-scanner/src/mocks/protocol.ts | 4 +- libs/custom-scanner/src/parameters.ts | 2 +- libs/custom-scanner/src/protocol.ts | 6 +- libs/custom-scanner/src/usb_channel.ts | 3 +- libs/pdi-scanner/jest.config.js | 2 +- libs/pdi-scanner/src/ts/scanner_client.ts | 5 +- .../precinct_scanner_tally_reports.tsx | 8 +- libs/utils/jest.config.js | 6 +- libs/utils/src/cast_vote_records.test.ts | 137 ++++++++++++++++++ libs/utils/src/cast_vote_records.ts | 2 +- libs/utils/src/compressed_tallies.ts | 4 +- libs/utils/src/environment_variable.ts | 6 +- libs/utils/src/file_reading.ts | 3 +- libs/utils/src/file_reading_test.test.ts | 86 +++++++++++ libs/utils/src/filenames.ts | 2 +- libs/utils/src/hmpb/all_contest_options.ts | 2 +- libs/utils/src/polls.ts | 16 +- .../src/tabulation/contest_filtering.test.ts | 26 +++- .../utils/src/tabulation/contest_filtering.ts | 26 +++- libs/utils/src/tabulation/lookups.test.ts | 40 ++++- libs/utils/src/tabulation/tabulation.test.ts | 9 ++ libs/utils/src/tabulation/tabulation.ts | 2 +- libs/utils/src/votes.test.ts | 16 ++ libs/utils/src/votes.ts | 2 +- 54 files changed, 432 insertions(+), 149 deletions(-) create mode 100644 libs/utils/src/file_reading_test.test.ts diff --git a/.node-version b/.node-version index 48b14e6b2b5..8ce7030825b 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -20.14.0 +20.16.0 diff --git a/apps/admin/backend/jest.config.js b/apps/admin/backend/jest.config.js index 51823e03bc9..16d88dd9149 100644 --- a/apps/admin/backend/jest.config.js +++ b/apps/admin/backend/jest.config.js @@ -16,13 +16,13 @@ module.exports = { ], coverageThreshold: { global: { - statements: 100, - branches: 100, - functions: 100, - lines: 100, + statements: 99, + branches: 98, + lines: 99, + functions: 98, }, }, - coverageProvider: 'v8', + coverageProvider: 'babel', collectCoverageFrom: [ '**/*.{ts,tsx}', '!**/*.d.ts', diff --git a/apps/admin/backend/src/adjudication.ts b/apps/admin/backend/src/adjudication.ts index 3131a63e70b..6b121774d26 100644 --- a/apps/admin/backend/src/adjudication.ts +++ b/apps/admin/backend/src/adjudication.ts @@ -29,7 +29,7 @@ export function adjudicateVote( const scannedIsVote = contestVotes ? contestVotes.includes(voteAdjudication.optionId) - : /* c8 ignore next 1 */ + : /* istanbul ignore next */ false; // if the vote is already the target status, do nothing @@ -63,10 +63,9 @@ async function logWriteInAdjudication({ return `a vote for an official candidate (${initialWriteInRecord.candidateId})`; case 'write-in-candidate': return `a vote for a write-in candidate (${initialWriteInRecord.candidateId})`; - /* c8 ignore start */ + /* istanbul ignore next */ default: throwIllegalValue(initialWriteInRecord, 'adjudicationType'); - /* c8 ignore stop */ } })(); @@ -80,10 +79,9 @@ async function logWriteInAdjudication({ return `a vote for a write-in candidate (${adjudicationAction.candidateId})`; case 'reset': return `unadjudicated`; - /* c8 ignore start */ + /* istanbul ignore next */ default: throwIllegalValue(adjudicationAction, 'type'); - /* c8 ignore stop */ } })(); @@ -169,10 +167,9 @@ export async function adjudicateWriteIn( // ensure the vote appears as it originally was in tallies store.deleteVoteAdjudication(initialWriteInRecord); break; - /* c8 ignore start */ + /* istanbul ignore next */ default: throwIllegalValue(adjudicationAction, 'type'); - /* c8 ignore stop */ } // if we are switching away from a write-in candidate, we may have to clean diff --git a/apps/admin/backend/src/app.ts b/apps/admin/backend/src/app.ts index a8111a53d8d..1d3737495a7 100644 --- a/apps/admin/backend/src/app.ts +++ b/apps/admin/backend/src/app.ts @@ -138,7 +138,7 @@ function getCurrentElectionRecord( workspace: Workspace ): Optional { const electionId = workspace.store.getCurrentElectionId(); - /* c8 ignore next 3 */ + /* istanbul ignore next */ if (!electionId) { return undefined; } @@ -244,7 +244,7 @@ function buildApi({ return printer.status(); }, - /* c8 ignore start */ + /* istanbul ignore next */ generateLiveCheckQrCodeValue() { const { machineId } = getMachineConfig(); const electionRecord = getCurrentElectionRecord(workspace); @@ -253,7 +253,6 @@ function buildApi({ ballotHash: electionRecord?.electionDefinition?.ballotHash, }); }, - /* c8 ignore stop */ async getUsbDriveStatus(): Promise { return usbDrive.status(); @@ -322,12 +321,11 @@ function buildApi({ signatureFile.fileName, signatureFile.fileContents ); - /* c8 ignore start: Tricky to make this second export err but the first export succeed + /* istanbul ignore next: Tricky to make this second export err but the first export succeed without significant mocking */ if (exportSignatureFileResult.isErr()) { return exportSignatureFileResult; } - /* c8 ignore stop */ } finally { await fs.rm(tempDirectory, { recursive: true }); } diff --git a/apps/admin/backend/src/cast_vote_records.ts b/apps/admin/backend/src/cast_vote_records.ts index c7bdbb08ab1..b7c50d38931 100644 --- a/apps/admin/backend/src/cast_vote_records.ts +++ b/apps/admin/backend/src/cast_vote_records.ts @@ -138,16 +138,14 @@ export async function listCastVoteRecordExportsOnUsbDrive( case 'not-directory': { return err('found-file-instead-of-directory'); } - /* c8 ignore start: Hard to trigger without significant mocking */ + /* istanbul ignore next: Hard to trigger without significant mocking */ case 'permission-denied': { return err('permission-denied'); } - /* c8 ignore stop */ - /* c8 ignore start: Compile-time check for completeness */ + /* istanbul ignore next: Compile-time check for completeness */ default: { throwIllegalValue(errorType); } - /* c8 ignore stop */ } } diff --git a/apps/admin/backend/src/exports/csv_ballot_count_report.ts b/apps/admin/backend/src/exports/csv_ballot_count_report.ts index e2e0c0b7c79..6ed95ae03c5 100644 --- a/apps/admin/backend/src/exports/csv_ballot_count_report.ts +++ b/apps/admin/backend/src/exports/csv_ballot_count_report.ts @@ -68,7 +68,7 @@ function buildRow({ const values: string[] = [...metadataValues]; const counts: number[] = []; - /* c8 ignore next - trivial fallthrough case */ + /* istanbul ignore next - trivial fallthrough case */ const manual = cardCounts.manual ?? 0; const { bmd } = cardCounts; const total = getBallotCount(cardCounts); @@ -81,7 +81,7 @@ function buildRow({ if (maxSheetsPerBallot) { for (let i = 0; i < maxSheetsPerBallot; i += 1) { - /* c8 ignore next - trivial fallthrough case */ + /* istanbul ignore next - trivial fallthrough case */ const currentSheetCount = cardCounts.hmpb[i] ?? 0; counts.push(currentSheetCount); } diff --git a/apps/admin/backend/src/reports/readiness.ts b/apps/admin/backend/src/reports/readiness.ts index 7a5b3bf2468..d22bdb0cebe 100644 --- a/apps/admin/backend/src/reports/readiness.ts +++ b/apps/admin/backend/src/reports/readiness.ts @@ -32,9 +32,8 @@ async function getReadinessReport({ : undefined; return AdminReadinessReport({ - /* c8 ignore start */ + /* istanbul ignore next */ batteryInfo: (await getBatteryInfo()) ?? undefined, - /* c8 ignore stop */ diskSpaceSummary: await workspace.getDiskSpaceSummary(), printerStatus: await printer.status(), mostRecentPrinterDiagnostic: diff --git a/apps/admin/backend/src/reports/titles.ts b/apps/admin/backend/src/reports/titles.ts index 9568a526f3e..adef15eae8a 100644 --- a/apps/admin/backend/src/reports/titles.ts +++ b/apps/admin/backend/src/reports/titles.ts @@ -148,7 +148,7 @@ export function generateTitleForReport({ return ok(`Undervoted ${reportType} Report`); case 'hasWriteIn': return ok(`Write-In ${reportType} Report`); - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(adjudicationFlag); } diff --git a/apps/admin/backend/src/server.ts b/apps/admin/backend/src/server.ts index a52514e51f1..dc949f0d8aa 100644 --- a/apps/admin/backend/src/server.ts +++ b/apps/admin/backend/src/server.ts @@ -49,7 +49,7 @@ export async function start({ debug('starting server...'); detectDevices({ logger: baseLogger }); let resolvedWorkspace = workspace; - /* c8 ignore start */ + /* istanbul ignore next */ if (!resolvedWorkspace) { const workspacePath = ADMIN_WORKSPACE; if (!workspacePath) { @@ -68,11 +68,10 @@ export async function start({ } resolvedWorkspace = createWorkspace(workspacePath); } - /* c8 ignore stop */ let resolvedApp = app; - /* c8 ignore start */ + /* istanbul ignore next */ if (!resolvedApp) { const auth = new DippedSmartCardAuth({ card: @@ -101,7 +100,6 @@ export async function start({ workspace: resolvedWorkspace, }); } - /* c8 ignore stop */ const server = resolvedApp.listen(port, async () => { await baseLogger.log(LogEventId.ApplicationStartup, 'system', { diff --git a/apps/admin/backend/src/store.ts b/apps/admin/backend/src/store.ts index 7fd308775e2..28b7604e41e 100644 --- a/apps/admin/backend/src/store.ts +++ b/apps/admin/backend/src/store.ts @@ -1353,7 +1353,7 @@ export class Store { for (const adjudication of adjudications) { const currentContestVotes = - votes[adjudication.contestId] ?? /* c8 ignore next 1 */ []; + votes[adjudication.contestId] ?? /* istanbul ignore next */ []; if (adjudication.isVote) { votes[adjudication.contestId] = [ ...currentContestVotes, @@ -1521,7 +1521,7 @@ export class Store { ballotStyleId: groupBy.groupByBallotStyle ? row.ballotStyleId : undefined, - /* c8 ignore next - edge case coverage needed for bad party grouping in general election */ + /* istanbul ignore next - edge case coverage needed for bad party grouping in general election */ partyId: groupBy.groupByParty ? row.partyId ?? undefined : undefined, batchId: groupBy.groupByBatch ? row.batchId : undefined, scannerId: groupBy.groupByScanner ? row.scannerId : undefined, @@ -1880,7 +1880,7 @@ export class Store { ballotStyleId: groupBy.groupByBallotStyle ? row.ballotStyleId : undefined, - /* c8 ignore next - edge case coverage needed for bad party grouping in general election */ + /* istanbul ignore next - edge case coverage needed for bad party grouping in general election */ partyId: groupBy.groupByParty ? row.partyId ?? undefined : undefined, batchId: groupBy.groupByBatch ? row.batchId : undefined, scannerId: groupBy.groupByScanner ? row.scannerId : undefined, @@ -2547,7 +2547,7 @@ export class Store { updateMaximumUsableDiskSpace(this.client, space); } - /* c8 ignore start */ + /* istanbul ignore next */ getDebugSummary(): Map { const tableNameRows = this.client.all( `select name from sqlite_schema where type='table' order by name;` @@ -2567,5 +2567,4 @@ export class Store { ) ); } - /* c8 ignore stop */ } diff --git a/apps/admin/backend/src/tabulation/card_counts.ts b/apps/admin/backend/src/tabulation/card_counts.ts index 12705ebae7b..d76b9dc0512 100644 --- a/apps/admin/backend/src/tabulation/card_counts.ts +++ b/apps/admin/backend/src/tabulation/card_counts.ts @@ -33,7 +33,7 @@ function addCardTallyToCardCounts({ } else { // eslint-disable-next-line no-param-reassign cardCounts.hmpb[card.sheetNumber - 1] = - /* c8 ignore next - trivial fallback case */ + /* istanbul ignore next - trivial fallback case */ (cardCounts.hmpb[card.sheetNumber - 1] ?? 0) + tally; } diff --git a/apps/admin/backend/src/tabulation/full_results.ts b/apps/admin/backend/src/tabulation/full_results.ts index bcd48b7b286..69292d5d408 100644 --- a/apps/admin/backend/src/tabulation/full_results.ts +++ b/apps/admin/backend/src/tabulation/full_results.ts @@ -180,7 +180,7 @@ export async function tabulateElectionResults({ }); } ); - /* c8 ignore next 3 - debug only */ + /* istanbul ignore next - debug only */ } else { debug('filter or group by is not compatible with manual results'); } diff --git a/apps/admin/backend/src/tabulation/write_ins.ts b/apps/admin/backend/src/tabulation/write_ins.ts index fa4dfd1ba03..2f2651d9212 100644 --- a/apps/admin/backend/src/tabulation/write_ins.ts +++ b/apps/admin/backend/src/tabulation/write_ins.ts @@ -146,7 +146,7 @@ function addWriteInTallyToElectionWriteInSummary({ isWriteIn: true, }; break; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(writeInTally); } diff --git a/apps/admin/backend/src/util/auth.ts b/apps/admin/backend/src/util/auth.ts index 00bd652b6d1..04e15e24488 100644 --- a/apps/admin/backend/src/util/auth.ts +++ b/apps/admin/backend/src/util/auth.ts @@ -29,7 +29,7 @@ export function constructAuthMachineState( return record; })(); - /* c8 ignore next 3 - covered by integration testing */ + /* istanbul ignore next - covered by integration testing */ const jurisdiction = isIntegrationTest() ? TEST_JURISDICTION : process.env.VX_MACHINE_JURISDICTION ?? DEV_JURISDICTION; @@ -64,6 +64,6 @@ export async function getUserRole( if (authStatus.status === 'logged_in') { return authStatus.user.role; } - /* c8 ignore next 2 - trivial fallback case */ + /* istanbul ignore next - trivial fallback case */ return 'unknown'; } diff --git a/apps/admin/backend/src/util/cdf_results.ts b/apps/admin/backend/src/util/cdf_results.ts index f1707d616b3..7437d9d8151 100644 --- a/apps/admin/backend/src/util/cdf_results.ts +++ b/apps/admin/backend/src/util/cdf_results.ts @@ -59,7 +59,7 @@ function buildOfficialCandidates( return candidates.map((candidate) => ({ '@type': 'ElectionResults.Candidate', '@id': candidate.id, - /* c8 ignore next 1 -- trivial fallthrough case */ + /* istanbul ignore next -- trivial fallthrough case */ PartyId: candidate.partyIds?.[0], BallotName: asInternationalizedText(candidate.name), })); diff --git a/apps/admin/backend/src/util/export_file.ts b/apps/admin/backend/src/util/export_file.ts index e43e19ffa2a..9ba8f16f2a6 100644 --- a/apps/admin/backend/src/util/export_file.ts +++ b/apps/admin/backend/src/util/export_file.ts @@ -21,7 +21,7 @@ export function exportFile({ const exporter = new Exporter({ allowedExportPatterns: ADMIN_ALLOWED_EXPORT_PATTERNS, /* We're not using `exportDataToUsbDrive` here, so a mock `usbDrive` is OK */ - /* c8 ignore start */ + /* istanbul ignore next */ usbDrive: { status: () => Promise.resolve({ @@ -30,7 +30,6 @@ export function exportFile({ eject: () => Promise.resolve(), format: () => Promise.resolve(), }, - /* c8 ignore stop */ }); debug('exporting data to file %s', path); diff --git a/apps/admin/backend/src/util/write_ins.ts b/apps/admin/backend/src/util/write_ins.ts index cc45f44e510..6ae6a4c903e 100644 --- a/apps/admin/backend/src/util/write_ins.ts +++ b/apps/admin/backend/src/util/write_ins.ts @@ -40,7 +40,7 @@ export async function getWriteInImageView({ const contestLayout = layout.contests.find( (contest) => contest.contestId === contestId ); - /* c8 ignore next 3 - TODO: revisit our layout assumptions based on our new ballots */ + /* istanbul ignore next - TODO: revisit our layout assumptions based on our new ballots */ if (!contestLayout) { throw new Error('unable to find a layout for the specified contest'); } @@ -52,13 +52,13 @@ export async function getWriteInImageView({ const writeInOptionIndex = safeParseNumber( optionId.slice('write-in-'.length) ); - /* c8 ignore next 3 - TODO: revisit our layout assumptions based on our new ballots */ + /* istanbul ignore next - TODO: revisit our layout assumptions based on our new ballots */ if (writeInOptionIndex.isErr() || writeInOptions === undefined) { throw new Error('unable to interpret layout write-in options'); } const writeInLayout = writeInOptions[writeInOptionIndex.ok()]; - /* c8 ignore next 3 - TODO: revisit our layout assumptions based on our new ballots */ + /* istanbul ignore next - TODO: revisit our layout assumptions based on our new ballots */ if (writeInLayout === undefined) { throw new Error('unexpected write-in option index'); } diff --git a/apps/admin/backend/src/util/zip.ts b/apps/admin/backend/src/util/zip.ts index 2f55ef241fb..f99fc276606 100644 --- a/apps/admin/backend/src/util/zip.ts +++ b/apps/admin/backend/src/util/zip.ts @@ -11,7 +11,7 @@ export function addFileToZipStream( ): Promise { return new Promise((resolve, reject) => { zipStream.entry(file.contents, { name: file.path }, (error) => { - /* c8 ignore next 2 - trivial error case */ + /* istanbul ignore next - trivial error case */ if (error) { reject(error); } else { diff --git a/apps/central-scan/backend/jest.config.js b/apps/central-scan/backend/jest.config.js index 6c6e1ad49eb..054763191ef 100644 --- a/apps/central-scan/backend/jest.config.js +++ b/apps/central-scan/backend/jest.config.js @@ -12,7 +12,7 @@ module.exports = { roots: ['/src'], setupFiles: ['/test/set_env_vars.ts'], setupFilesAfterEnv: ['/test/setup_custom_matchers.ts'], - coverageProvider: 'v8', + coverageProvider: 'babel', collectCoverageFrom: [ '**/*.{ts,tsx}', '!**/node_modules/**', @@ -23,10 +23,10 @@ module.exports = { ], coverageThreshold: { global: { - statements: 94, - branches: 86, + statements: 91, + branches: 77, functions: 91, - lines: 94, + lines: 91, }, }, }; diff --git a/apps/central-scan/backend/src/readiness_report.ts b/apps/central-scan/backend/src/readiness_report.ts index b085c898f65..0a38db4e49c 100644 --- a/apps/central-scan/backend/src/readiness_report.ts +++ b/apps/central-scan/backend/src/readiness_report.ts @@ -31,9 +31,8 @@ export async function saveReadinessReport({ const generatedAtTime = new Date(getCurrentTime()); const electionDefinition = store.getElectionDefinition(); const report = CentralScanReadinessReport({ - /* c8 ignore start */ + /* istanbul ignore next */ batteryInfo: (await getBatteryInfo()) ?? undefined, - /* c8 ignore stop */ diskSpaceSummary: await workspace.getDiskSpaceSummary(), isScannerAttached, mostRecentScannerDiagnostic: diff --git a/apps/central-scan/backend/src/server.ts b/apps/central-scan/backend/src/server.ts index 32058487a45..71b56f36760 100644 --- a/apps/central-scan/backend/src/server.ts +++ b/apps/central-scan/backend/src/server.ts @@ -45,7 +45,7 @@ export async function start({ }: Partial = {}): Promise { detectDevices({ logger: baseLogger }); let resolvedWorkspace = workspace; - /* c8 ignore start */ + /* istanbul ignore next */ if (!resolvedWorkspace) { const workspacePath = SCAN_WORKSPACE; if (!workspacePath) { @@ -64,14 +64,13 @@ export async function start({ } resolvedWorkspace = createWorkspace(workspacePath); } - /* c8 ignore stop */ // Clear any cached data resolvedWorkspace.clearUploads(); resolvedWorkspace.store.cleanupIncompleteBatches(); let resolvedApp = app; - /* c8 ignore start */ + /* istanbul ignore next */ if (!resolvedApp) { const auth = new DippedSmartCardAuth({ card: @@ -111,7 +110,6 @@ export async function start({ workspace: resolvedWorkspace, }); } - /* c8 ignore stop */ return resolvedApp.listen(port, async () => { await baseLogger.log(LogEventId.ApplicationStartup, 'system', { diff --git a/apps/scan/backend/jest.config.js b/apps/scan/backend/jest.config.js index 9d9a63a87ef..530e57ef920 100644 --- a/apps/scan/backend/jest.config.js +++ b/apps/scan/backend/jest.config.js @@ -12,7 +12,7 @@ module.exports = { roots: ['/src'], setupFiles: ['/test/set_env_vars.ts'], setupFilesAfterEnv: ['/test/setup_custom_matchers.ts'], - coverageProvider: 'v8', + coverageProvider: 'babel', collectCoverageFrom: [ '**/*.{ts,tsx}', '!**/node_modules/**', @@ -24,8 +24,8 @@ module.exports = { coverageThreshold: { global: { statements: -180, - branches: -47, - functions: -15, + branches: -60, + functions: -16, lines: -180, }, }, diff --git a/apps/scan/backend/src/export.ts b/apps/scan/backend/src/export.ts index 0a13a24d3b9..34f1c5d75fc 100644 --- a/apps/scan/backend/src/export.ts +++ b/apps/scan/backend/src/export.ts @@ -57,11 +57,10 @@ export async function exportCastVoteRecordsToUsbDrive({ ); break; } - /* c8 ignore start: Compile-time check for completeness */ + /* istanbul ignore next: Compile-time check for completeness */ default: { throwIllegalValue(mode); } - /* c8 ignore stop */ } if (exportResult.isErr()) { diff --git a/apps/scan/backend/src/printing/printer.ts b/apps/scan/backend/src/printing/printer.ts index 301232f592c..737f8bcc07b 100644 --- a/apps/scan/backend/src/printing/printer.ts +++ b/apps/scan/backend/src/printing/printer.ts @@ -83,7 +83,7 @@ export function wrapFujitsuThermalPrinter( } export function getPrinter(logger: BaseLogger): Printer { - /* c8 ignore start */ + /* istanbul ignore next */ if ( isFeatureFlagEnabled( BooleanEnvironmentVariableName.SCAN_USE_FUJITSU_PRINTER @@ -93,7 +93,6 @@ export function getPrinter(logger: BaseLogger): Printer { assert(printer); // TODO: build mock and/or reconnection instead of asserting return wrapFujitsuThermalPrinter(printer); } - /* c8 ignore stop */ const legacyPrinter = detectPrinter(logger); return wrapLegacyPrinter(legacyPrinter); diff --git a/apps/scan/backend/src/scanners/custom/state_machine.ts b/apps/scan/backend/src/scanners/custom/state_machine.ts index 8debfe45b78..bb690022dd4 100644 --- a/apps/scan/backend/src/scanners/custom/state_machine.ts +++ b/apps/scan/backend/src/scanners/custom/state_machine.ts @@ -1334,7 +1334,7 @@ export function createPrecinctScannerStateMachine({ type: interpretation.type, reasons: interpretation.reasons, }; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(interpretation, 'type'); } @@ -1367,13 +1367,13 @@ export function createPrecinctScannerStateMachine({ machineService.stop(); }, - /* c8 ignore start */ + /* istanbul ignore next */ beginDoubleFeedCalibration: () => { throw new Error('Not supported'); }, + /* istanbul ignore next */ endDoubleFeedCalibration: () => { throw new Error('Not supported'); }, - /* c8 ignore stop */ }; } diff --git a/apps/scan/backend/src/scanners/pdi/state_machine.ts b/apps/scan/backend/src/scanners/pdi/state_machine.ts index 87ba816b1d4..3e3ed209100 100644 --- a/apps/scan/backend/src/scanners/pdi/state_machine.ts +++ b/apps/scan/backend/src/scanners/pdi/state_machine.ts @@ -399,7 +399,7 @@ function buildMachine({ cond: (_, { event }) => event.event === 'coverClosed', target: undefined, }, - /* c8 ignore start - fallback case, shouldn't happen */ + /* istanbul ignore next - fallback case, shouldn't happen */ { target: '#error', actions: assign({ @@ -410,7 +410,6 @@ function buildMachine({ ), }), }, - /* c8 ignore stop */ ], SCANNER_ERROR: { target: 'error', @@ -631,13 +630,12 @@ function buildMachine({ { cond: (_, { status }) => !status.documentInScanner, target: '#error', - /* c8 ignore start */ + /* istanbul ignore next */ actions: assign({ // eslint-disable-next-line @typescript-eslint/no-unused-vars error: (_context) => new PrecinctScannerError('scanning_failed'), }), - /* c8 ignore stop */ }, { target: '#interpreting' }, ], @@ -1002,7 +1000,7 @@ function setupLogging( ); }) .onChange(async (context, previousContext) => { - /* c8 ignore next */ + /* istanbul ignore next */ if (!previousContext) return; const changed = Object.entries(context).filter( ([key, value]) => previousContext[key as keyof Context] !== value @@ -1129,7 +1127,7 @@ export function createPrecinctScannerStateMachine({ return 'calibrating_double_feed_detection.done'; case state.matches('shoeshineModeRescanningBallot'): return 'accepted'; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throw new Error(`Unexpected state: ${state.value}`); } @@ -1153,7 +1151,7 @@ export function createPrecinctScannerStateMachine({ type: interpretation.type, reasons: interpretation.reasons, }; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: return throwIllegalValue(interpretation, 'type'); } diff --git a/libs/backend/src/election_package/election_package_io.test.ts b/libs/backend/src/election_package/election_package_io.test.ts index 23ae155c45a..b46b12db42c 100644 --- a/libs/backend/src/election_package/election_package_io.test.ts +++ b/libs/backend/src/election_package/election_package_io.test.ts @@ -334,7 +334,7 @@ test('readElectionPackageFromFile errors when given an invalid election', async expect(await readElectionPackageFromFile(file)).toEqual( err({ type: 'invalid-election', - message: 'Unexpected token o in JSON at position 1', + message: `Unexpected token 'o', "not a valid election" is not valid JSON`, }) ); }); @@ -351,7 +351,7 @@ test('readElectionPackageFromFile errors when given invalid system settings', as expect(await readElectionPackageFromFile(file)).toEqual( err({ type: 'invalid-system-settings', - message: 'Unexpected token o in JSON at position 1', + message: `Unexpected token 'o', "not a valid"... is not valid JSON`, }) ); }); @@ -368,7 +368,7 @@ test('readElectionPackageFromFile errors when given invalid metadata', async () expect(await readElectionPackageFromFile(file)).toEqual( err({ type: 'invalid-metadata', - message: 'Unexpected token a in JSON at position 0', + message: `Unexpected token 'a', "asdf" is not valid JSON`, }) ); }); diff --git a/libs/custom-scanner/jest.config.js b/libs/custom-scanner/jest.config.js index a18d87f1e82..0b7c8e76287 100644 --- a/libs/custom-scanner/jest.config.js +++ b/libs/custom-scanner/jest.config.js @@ -12,5 +12,5 @@ module.exports = { '!src/mocks/**/*.ts', '!src/types/**/*.ts', ], - coverageProvider: 'v8', + coverageProvider: 'babel', }; diff --git a/libs/custom-scanner/src/custom_a4_scanner.ts b/libs/custom-scanner/src/custom_a4_scanner.ts index b8698fe659a..9b37c1158ad 100644 --- a/libs/custom-scanner/src/custom_a4_scanner.ts +++ b/libs/custom-scanner/src/custom_a4_scanner.ts @@ -178,7 +178,7 @@ export class CustomA4Scanner implements CustomScanner { createJob(channel) ); - /* c8 ignore start */ + /* istanbul ignore next */ if (createJobResult.isErr()) { const errorCode = createJobResult.err(); debug('create job error: %o', errorCode); @@ -193,7 +193,6 @@ export class CustomA4Scanner implements CustomScanner { } else { break; } - /* c8 ignore stop */ } debug('create job result: %o', createJobResult); @@ -273,9 +272,9 @@ export class CustomA4Scanner implements CustomScanner { scan( scanParameters: ScanParameters, { - /* c8 ignore next */ + /* istanbul ignore next */ maxTimeoutNoMoveNoScan = 5_000, - /* c8 ignore next */ + /* istanbul ignore next */ maxRetries = 3, }: { maxTimeoutNoMoveNoScan?: number; maxRetries?: number } = {} ): Promise, ErrorCode>> { @@ -349,7 +348,7 @@ export class CustomA4Scanner implements CustomScanner { const getImagePortionBySideResult = await this.getImagePortionBySideInternal(currentSide, pageSize); - /* c8 ignore start */ + /* istanbul ignore next */ if (getImagePortionBySideResult.isErr()) { readImageDataErrorCount += 1; if (readImageDataErrorCount < maxRetries) { @@ -359,7 +358,6 @@ export class CustomA4Scanner implements CustomScanner { return getImagePortionBySideResult; } - /* c8 ignore stop */ scannerImage.imageBuffer = Buffer.concat([ scannerImage.imageBuffer, @@ -421,10 +419,10 @@ export class CustomA4Scanner implements CustomScanner { debug('waiting for motor on and scan in progress timed out'); void (await this.stopScanInternal()); return err(ErrorCode.ScannerError); - } /* c8 ignore start */ else { + } /* istanbul ignore else */ else { /* this branch often does not run during tests in CircleCI */ debug('still waiting for motor on and scan in progress'); - } /* c8 ignore stop */ + } } else { startNoMoveNoScan = 0; } @@ -468,7 +466,7 @@ export class CustomA4Scanner implements CustomScanner { ).okOrElse(fail); } - /* c8 ignore start */ + /* istanbul ignore next */ if ( status.isScanInProgress && a4Status.pageSizeSideA === 0 && @@ -476,7 +474,6 @@ export class CustomA4Scanner implements CustomScanner { ) { debug('scan in progress but no data available'); } - /* c8 ignore stop */ return ok('continue'); }; @@ -488,7 +485,7 @@ export class CustomA4Scanner implements CustomScanner { for (;;) { const action = (await scanLoopTick()).okOrElse(fail); - /* c8 ignore start */ + /* istanbul ignore next */ if (action === 'continue') { continue; } else if (action === 'break') { @@ -496,7 +493,6 @@ export class CustomA4Scanner implements CustomScanner { } else { throwIllegalValue(action); } - /* c8 ignore stop */ } } finally { unlock(); @@ -518,11 +514,10 @@ export class CustomA4Scanner implements CustomScanner { scanSide: ScanSide, imagePortionSize: number ): Promise> { - /* c8 ignore start */ + /* istanbul ignore next */ if (imagePortionSize === 0) { return ok(Buffer.alloc(0)); } - /* c8 ignore stop */ return await this.channelMutex.withLock((channel) => getImageData( diff --git a/libs/custom-scanner/src/mocks/coder.ts b/libs/custom-scanner/src/mocks/coder.ts index dbcfba61200..28964855027 100644 --- a/libs/custom-scanner/src/mocks/coder.ts +++ b/libs/custom-scanner/src/mocks/coder.ts @@ -4,11 +4,11 @@ import { Coder } from '@votingworks/message-coder'; -/* c8 ignore start */ +/* istanbul ignore start */ function notImplemented() { throw new Error('not implemented'); } -/* c8 ignore stop */ +/* istanbul ignore stop */ /** * Creates a mock coder. diff --git a/libs/custom-scanner/src/mocks/protocol.ts b/libs/custom-scanner/src/mocks/protocol.ts index c3689c8f6d0..ebb915c4011 100644 --- a/libs/custom-scanner/src/mocks/protocol.ts +++ b/libs/custom-scanner/src/mocks/protocol.ts @@ -111,7 +111,7 @@ export function usbChannelWithMockProtocol({ function setNextReadBuffer(buffer: Buffer | Result): void { const newBuffer = Buffer.isBuffer(buffer) ? buffer : buffer.unsafeUnwrap(); - /* c8 ignore start */ + /* istanbul ignore start */ if (readBuffer) { throw new Error( `read buffer already set: ${inspect( @@ -119,7 +119,7 @@ export function usbChannelWithMockProtocol({ )}, cannot use new buffer: ${inspect(newBuffer.toString())}` ); } - /* c8 ignore stop */ + /* istanbul ignore stop */ readBuffer = Buffer.isBuffer(buffer) ? buffer : buffer.unsafeUnwrap(); } diff --git a/libs/custom-scanner/src/parameters.ts b/libs/custom-scanner/src/parameters.ts index 670ef8c402e..41e3d1cf9b8 100644 --- a/libs/custom-scanner/src/parameters.ts +++ b/libs/custom-scanner/src/parameters.ts @@ -52,7 +52,7 @@ function convertToMultiSheetDetectionSensorLevelInternal( return MultiSheetDetectionSensorLevelInternal.Level3; case DoubleSheetDetectOpt.Level4: return MultiSheetDetectionSensorLevelInternal.Level4; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(option); } diff --git a/libs/custom-scanner/src/protocol.ts b/libs/custom-scanner/src/protocol.ts index 7cf7f193cb1..cca307290ae 100644 --- a/libs/custom-scanner/src/protocol.ts +++ b/libs/custom-scanner/src/protocol.ts @@ -667,7 +667,7 @@ class GetImageDataRequestScanSideCoder extends BaseCoder { case GetImageDataRequestScanSideCoder.SideB: return { value: ScanSide.B, bitOffset: decoded.bitOffset }; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: return err('InvalidValue'); } @@ -819,7 +819,7 @@ export function checkAnswer(data: Buffer): CheckAnswerResult { case ResponseErrorCode.INVALID_JOB_ID: return { type: 'error', errorCode: ErrorCode.JobNotValid }; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(errorCode); } @@ -856,7 +856,7 @@ function mapCoderError(result: Result): Result { case 'UnsupportedOffset': throw new Error(`BUG: unsupported offset`); - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(coderError); } diff --git a/libs/custom-scanner/src/usb_channel.ts b/libs/custom-scanner/src/usb_channel.ts index bbd15821226..ffeac048f13 100644 --- a/libs/custom-scanner/src/usb_channel.ts +++ b/libs/custom-scanner/src/usb_channel.ts @@ -9,13 +9,12 @@ const debug = makeDebug('custom:usb-channel'); const READ_RETRY_MAX = 5; const WRITE_RETRY_MAX = 3; -/* c8 ignore start */ +/* istanbul ignore next */ function truncateStringForDisplay(string: string, maxLength = 100): string { return string.length > maxLength ? `${string.slice(0, maxLength - 1)}…` : string; } -/* c8 ignore stop */ /** * Options for building a `UsbChannel`. diff --git a/libs/pdi-scanner/jest.config.js b/libs/pdi-scanner/jest.config.js index 1fb0f05fab3..35734eed434 100644 --- a/libs/pdi-scanner/jest.config.js +++ b/libs/pdi-scanner/jest.config.js @@ -10,5 +10,5 @@ module.exports = { '!src/ts/index.ts', '!src/ts/demo.ts', ], - coverageProvider: 'v8', + coverageProvider: 'babel', }; diff --git a/libs/pdi-scanner/src/ts/scanner_client.ts b/libs/pdi-scanner/src/ts/scanner_client.ts index 8651184ced0..5b01b144d1c 100644 --- a/libs/pdi-scanner/src/ts/scanner_client.ts +++ b/libs/pdi-scanner/src/ts/scanner_client.ts @@ -282,15 +282,14 @@ export function createPdiScannerClient() { emit(message); break; } - /* c8 ignore start */ + /* istanbul ignore next */ default: throwIllegalValue(message, 'event'); - /* c8 ignore stop */ } }); + /* istanbul ignore next */ pdictl.stderr.on('data', (data) => { - /* c8 ignore next */ debug('pdictl stderr:', data.toString('utf-8')); }); diff --git a/libs/ui/src/reports/precinct_scanner_tally_reports.tsx b/libs/ui/src/reports/precinct_scanner_tally_reports.tsx index 64c35e1ad0f..a6691783aa0 100644 --- a/libs/ui/src/reports/precinct_scanner_tally_reports.tsx +++ b/libs/ui/src/reports/precinct_scanner_tally_reports.tsx @@ -8,7 +8,7 @@ import { } from '@votingworks/types'; import { combineElectionResults, - getContestsForPrecinct, + getContestsForPrecinctSelection, getEmptyElectionResults, } from '@votingworks/utils'; import { PrecinctScannerTallyReport } from './precinct_scanner_tally_report'; @@ -50,11 +50,9 @@ export function PrecinctScannerTallyReports({ }); const partyIds = getPartyIdsForPrecinctScannerTallyReports(electionDefinition); - const allContests = getContestsForPrecinct( + const allContests = getContestsForPrecinctSelection( electionDefinition, - precinctSelection.kind === 'SinglePrecinct' - ? precinctSelection.precinctId - : undefined + precinctSelection ); return partyIds.map((partyId) => { diff --git a/libs/utils/jest.config.js b/libs/utils/jest.config.js index fb9891fd03b..51ce0227d39 100644 --- a/libs/utils/jest.config.js +++ b/libs/utils/jest.config.js @@ -7,11 +7,11 @@ module.exports = { ...shared, testEnvironment: 'jsdom', setupFilesAfterEnv: ['/src/setupTests.ts'], - coverageProvider: 'v8', + coverageProvider: 'babel', coverageThreshold: { global: { - branches: -30, - lines: -123, + branches: -35, + lines: -124, }, }, collectCoverageFrom: [ diff --git a/libs/utils/src/cast_vote_records.test.ts b/libs/utils/src/cast_vote_records.test.ts index 4dbfa5aaa6c..1eb72559865 100644 --- a/libs/utils/src/cast_vote_records.test.ts +++ b/libs/utils/src/cast_vote_records.test.ts @@ -3,8 +3,14 @@ import path from 'path'; import { dirSync } from 'tmp'; import { BallotType, CVR } from '@votingworks/types'; +import { + electionFamousNames2021Fixtures, + electionWithMsEitherNeither, +} from '@votingworks/fixtures'; +import { err, ok } from '@votingworks/basics'; import { buildCVRSnapshotBallotTypeMetadata, + castVoteRecordHasValidContestReferences, convertCastVoteRecordVotesToTabulationVotes, getCastVoteRecordBallotType, getCurrentSnapshot, @@ -571,3 +577,134 @@ test('getCastVoteRecordBallotType', () => { ) ).toEqual(BallotType.Precinct); }); + +test('castVoteRecordHasValidContestReferences no contests', () => { + expect( + castVoteRecordHasValidContestReferences( + { + ...mockCastVoteRecord, + CVRSnapshot: [ + { + '@id': 'test', + '@type': 'CVR.CVRSnapshot', + Type: CVR.CVRType.Modified, + CVRContest: [], + }, + ], + }, + electionFamousNames2021Fixtures.election.contests + ) + ).toEqual(ok()); +}); + +test('castVoteRecordHasValidContestReferences no selections', () => { + expect( + castVoteRecordHasValidContestReferences( + { + ...mockCastVoteRecord, + CVRSnapshot: [ + { + '@id': 'test', + '@type': 'CVR.CVRSnapshot', + Type: CVR.CVRType.Modified, + CVRContest: [ + { + '@type': 'CVR.CVRContest', + ContestId: 'mayor', + CVRContestSelection: [], + }, + ], + }, + ], + }, + electionFamousNames2021Fixtures.election.contests + ) + ).toEqual(ok()); +}); + +test('castVoteRecordHasValidContestReferences invalid contest reference', () => { + expect( + castVoteRecordHasValidContestReferences( + { + ...mockCastVoteRecord, + CVRSnapshot: [ + { + '@id': 'test', + '@type': 'CVR.CVRSnapshot', + Type: CVR.CVRType.Modified, + CVRContest: [ + { + '@type': 'CVR.CVRContest', + ContestId: 'not-a-contest', + CVRContestSelection: [], + }, + ], + }, + ], + }, + electionFamousNames2021Fixtures.election.contests + ) + ).toEqual(err('contest-not-found')); +}); + +test('castVoteRecordHasValidContestReferences invalid contest option reference', () => { + expect( + castVoteRecordHasValidContestReferences( + { + ...mockCastVoteRecord, + CVRSnapshot: [ + { + '@id': 'test', + '@type': 'CVR.CVRSnapshot', + Type: CVR.CVRType.Modified, + CVRContest: [ + { + '@type': 'CVR.CVRContest', + ContestId: 'mayor', + CVRContestSelection: [ + { + '@type': 'CVR.CVRContestSelection', + ContestSelectionId: 'not-an-option', + SelectionPosition: [], + }, + ], + }, + ], + }, + ], + }, + electionFamousNames2021Fixtures.election.contests + ) + ).toEqual(err('contest-option-not-found')); +}); + +test('castVoteRecordHasValidContestReferences invalid yesno contest option reference', () => { + expect( + castVoteRecordHasValidContestReferences( + { + ...mockCastVoteRecord, + CVRSnapshot: [ + { + '@id': 'test', + '@type': 'CVR.CVRSnapshot', + Type: CVR.CVRType.Modified, + CVRContest: [ + { + '@type': 'CVR.CVRContest', + ContestId: '750000017', + CVRContestSelection: [ + { + '@type': 'CVR.CVRContestSelection', + ContestSelectionId: 'not-an-option', + SelectionPosition: [], + }, + ], + }, + ], + }, + ], + }, + electionWithMsEitherNeither.contests + ) + ).toEqual(err('contest-option-not-found')); +}); diff --git a/libs/utils/src/cast_vote_records.ts b/libs/utils/src/cast_vote_records.ts index a294c1f43bc..cae272e3b48 100644 --- a/libs/utils/src/cast_vote_records.ts +++ b/libs/utils/src/cast_vote_records.ts @@ -222,7 +222,7 @@ function getValidContestOptions(contest: AnyContest): ContestOptionId[] { ]; case 'yesno': return [contest.yesOption.id, contest.noOption.id]; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: return throwIllegalValue(contest); } diff --git a/libs/utils/src/compressed_tallies.ts b/libs/utils/src/compressed_tallies.ts index a38d89cbc63..ffe5fab25ef 100644 --- a/libs/utils/src/compressed_tallies.ts +++ b/libs/utils/src/compressed_tallies.ts @@ -48,7 +48,7 @@ export function compressTally( ]); } - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(contest, 'type'); } @@ -118,7 +118,7 @@ function getContestTalliesForCompressedContest( tallies: candidateTallies, }; } - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(contest, 'type'); } diff --git a/libs/utils/src/environment_variable.ts b/libs/utils/src/environment_variable.ts index d8b09e6ac55..b261ebde3ba 100644 --- a/libs/utils/src/environment_variable.ts +++ b/libs/utils/src/environment_variable.ts @@ -183,7 +183,7 @@ export function getEnvironmentVariable( return process.env.REACT_APP_VX_ONLY_ENABLE_SCREEN_READER_FOR_HEADPHONES; case BooleanEnvironmentVariableName.MARK_SCAN_DISABLE_BALLOT_REINSERTION: return process.env.REACT_APP_VX_MARK_SCAN_DISABLE_BALLOT_REINSERTION; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(name); } @@ -326,7 +326,7 @@ export function getBooleanEnvVarConfig( allowInProduction: true, autoEnableInDevelopment: false, }; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(name); } @@ -342,7 +342,7 @@ export function getStringEnvVarConfig( defaultValue: 'ms-sems', zodSchema: ConverterClientTypeSchema, }; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(name); } diff --git a/libs/utils/src/file_reading.ts b/libs/utils/src/file_reading.ts index 63bb66add94..4e4057cd86f 100644 --- a/libs/utils/src/file_reading.ts +++ b/libs/utils/src/file_reading.ts @@ -19,11 +19,10 @@ export function readFile(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); - /* c8 ignore start */ + /* istanbul ignore next */ reader.onerror = () => { reject(reader.error); }; - /* c8 ignore stop */ reader.onload = () => { if (!reader.result) { diff --git a/libs/utils/src/file_reading_test.test.ts b/libs/utils/src/file_reading_test.test.ts new file mode 100644 index 00000000000..b29328171ec --- /dev/null +++ b/libs/utils/src/file_reading_test.test.ts @@ -0,0 +1,86 @@ +import { Buffer } from 'buffer'; +import JsZip from 'jszip'; +import { + getEntries, + getFileByName, + maybeGetFileByName, + openZip, + readEntry, + readFile, + readFileAsyncAsString, + readJsonEntry, + readTextEntry, +} from './file_reading'; + +test('readFileAsyncAsString', async () => { + const file = new File(['hello'], 'hello.txt'); + expect(await readFileAsyncAsString(file)).toEqual('hello'); +}); + +test('readFileAsyncAsString empty', async () => { + const file = new File([], 'empty.txt'); + expect(await readFileAsyncAsString(file)).toEqual(''); +}); + +test('readFileAsyncAsString error', async () => { + jest.spyOn(FileReader.prototype, 'readAsText').mockImplementation(() => { + throw new Error('read error'); + }); + + const file = new File(['hello'], 'hello.txt'); + await expect(readFileAsyncAsString(file)).rejects.toThrow('read error'); +}); + +test('readFile', async () => { + const file = new File(['hello'], 'hello.txt'); + const buffer = await readFile(file); + expect(buffer).toEqual(Buffer.from('hello')); +}); + +test('readFile empty', async () => { + const file = new File([], 'empty.txt'); + const buffer = await readFile(file); + expect(buffer).toEqual(Buffer.from([])); +}); + +test('readFile error', async () => { + jest + .spyOn(FileReader.prototype, 'readAsArrayBuffer') + .mockImplementation(() => { + throw new Error('read error'); + }); + + const file = new File(['hello'], 'hello.txt'); + await expect(readFile(file)).rejects.toThrow('read error'); +}); + +test('openZip', async () => { + const zip = new JsZip(); + zip.file('hello.txt', 'hello'); + zip.file('test.json', JSON.stringify({ test: true })); + const data = await zip.generateAsync({ type: 'uint8array' }); + + const newZip = await openZip(data); + const entries = getEntries(newZip); + expect(entries.map((entry) => entry.name)).toEqual([ + 'hello.txt', + 'test.json', + ]); + await expect(readEntry(getFileByName(entries, 'hello.txt'))).resolves.toEqual( + Buffer.from('hello') + ); + await expect( + readTextEntry(getFileByName(entries, 'hello.txt')) + ).resolves.toEqual('hello'); + await expect( + readJsonEntry(getFileByName(entries, 'test.json')) + ).resolves.toEqual({ + test: true, + }); + + expect(maybeGetFileByName(entries, 'missing.txt')).toBeUndefined(); + expect(maybeGetFileByName(entries, 'hello.txt')).toEqual( + getFileByName(entries, 'hello.txt') + ); + expect(() => getFileByName(entries, 'missing.txt')).toThrowError(); +}); diff --git a/libs/utils/src/filenames.ts b/libs/utils/src/filenames.ts index 52b42d27309..57b237116e9 100644 --- a/libs/utils/src/filenames.ts +++ b/libs/utils/src/filenames.ts @@ -95,7 +95,7 @@ export function generateLogFilename( return `${logFileName}${SUBSECTION_SEPARATOR}${timeSuffix}.log`; case LogFileType.Cdf: return `${logFileName}${WORD_SEPARATOR}cdf${SUBSECTION_SEPARATOR}${timeSuffix}.json`; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(fileType); } diff --git a/libs/utils/src/hmpb/all_contest_options.ts b/libs/utils/src/hmpb/all_contest_options.ts index a1f6196ca7a..773d000000d 100644 --- a/libs/utils/src/hmpb/all_contest_options.ts +++ b/libs/utils/src/hmpb/all_contest_options.ts @@ -69,7 +69,7 @@ export function* allContestOptions( break; } - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(contest, 'type'); } diff --git a/libs/utils/src/polls.ts b/libs/utils/src/polls.ts index 5af619b15f2..80e61f386d6 100644 --- a/libs/utils/src/polls.ts +++ b/libs/utils/src/polls.ts @@ -16,7 +16,7 @@ export function getPollsTransitionDestinationState( return 'polls_paused'; case 'close_polls': return 'polls_closed_final'; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(transitionType); } @@ -34,7 +34,7 @@ export function getPollsTransitionAction( return 'Resume Voting'; case 'close_polls': return 'Close Polls'; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(transitionType); } @@ -52,7 +52,7 @@ export function getPollsReportTitle( return 'Voting Paused Report'; case 'close_polls': return 'Polls Closed Report'; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(transitionType); } @@ -67,7 +67,7 @@ export function getPollsStateName(state: PollsState): string { case 'polls_closed_initial': case 'polls_closed_final': return 'Closed'; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(state); } @@ -89,7 +89,7 @@ export function getPollTransitionsFromState( return ['open_polls']; case 'polls_closed_final': return []; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(state); } @@ -110,7 +110,7 @@ export function isValidPollsStateChange( return newState === 'polls_open' || newState === 'polls_closed_final'; case 'polls_closed_final': return false; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(prevState); } @@ -127,7 +127,7 @@ export function getPollsTransitionActionPastTense( return 'Voting Resumed'; case 'pause_voting': return 'Voting Paused'; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(transitionType); } @@ -143,7 +143,7 @@ export function isPollsSuspensionTransition( case 'resume_voting': case 'pause_voting': return true; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(transitionType); } diff --git a/libs/utils/src/tabulation/contest_filtering.test.ts b/libs/utils/src/tabulation/contest_filtering.test.ts index f8714ded974..9a45d7da5f8 100644 --- a/libs/utils/src/tabulation/contest_filtering.test.ts +++ b/libs/utils/src/tabulation/contest_filtering.test.ts @@ -8,7 +8,7 @@ import { doesContestAppearOnPartyBallot, getContestIdsForBallotStyle, getContestIdsForPrecinct, - getContestsForPrecinct, + getContestsForPrecinctSelection, } from './contest_filtering'; describe('doesContestAppearOnPartyBallot', () => { @@ -85,12 +85,13 @@ test('getContestIdsForPrecinct', () => { ]); }); -test('getContestsForPrecinct', () => { +test('getContestsForPrecinctSelection', () => { const { electionDefinition } = electionPrimaryPrecinctSplitsFixtures; expect( - getContestsForPrecinct(electionDefinition, 'precinct-c1-w2').map( - (c) => c.id - ) + getContestsForPrecinctSelection(electionDefinition, { + kind: 'SinglePrecinct', + precinctId: 'precinct-c1-w2', + }).map((c) => c.id) ).toEqual([ 'county-leader-mammal', 'congressional-1-mammal', @@ -98,4 +99,19 @@ test('getContestsForPrecinct', () => { 'county-leader-fish', 'congressional-1-fish', ]); + + expect( + getContestsForPrecinctSelection(electionDefinition, { + kind: 'AllPrecincts', + }).map((c) => c.id) + ).toEqual([ + 'county-leader-mammal', + 'county-leader-fish', + 'congressional-1-mammal', + 'congressional-1-fish', + 'congressional-2-mammal', + 'congressional-2-fish', + 'water-1-fishing', + 'water-2-fishing', + ]); }); diff --git a/libs/utils/src/tabulation/contest_filtering.ts b/libs/utils/src/tabulation/contest_filtering.ts index 38d038eff10..68b54bf066b 100644 --- a/libs/utils/src/tabulation/contest_filtering.ts +++ b/libs/utils/src/tabulation/contest_filtering.ts @@ -6,8 +6,9 @@ import { PrecinctId, AnyContest, Contests, + PrecinctSelection, } from '@votingworks/types'; -import { assert } from '@votingworks/basics'; +import { assert, throwIllegalValue } from '@votingworks/basics'; import { createElectionMetadataLookupFunction, getContestById, @@ -91,15 +92,24 @@ export function mapContestIdsToContests( ); } -export function getContestsForPrecinct( +export function getContestsForPrecinctSelection( electionDefinition: ElectionDefinition, - precinctId?: PrecinctId + precinctSelection: PrecinctSelection ): Contests { const { election } = electionDefinition; - if (!precinctId) { - return election.contests; - } + switch (precinctSelection.kind) { + case 'AllPrecincts': + return election.contests; + + case 'SinglePrecinct': { + const contestIds = getContestIdsForPrecinct( + electionDefinition, + precinctSelection.precinctId + ); + return mapContestIdsToContests(electionDefinition, contestIds); + } - const contestIds = getContestIdsForPrecinct(electionDefinition, precinctId); - return mapContestIdsToContests(electionDefinition, contestIds); + default: + throwIllegalValue(precinctSelection, 'kind'); + } } diff --git a/libs/utils/src/tabulation/lookups.test.ts b/libs/utils/src/tabulation/lookups.test.ts index 4590badde9b..5f3c1cb0c45 100644 --- a/libs/utils/src/tabulation/lookups.test.ts +++ b/libs/utils/src/tabulation/lookups.test.ts @@ -1,4 +1,9 @@ -import { ElectionDefinition, Tabulation } from '@votingworks/types'; +import { + DistrictIdSchema, + ElectionDefinition, + Tabulation, + unsafeParse, +} from '@votingworks/types'; import { electionTwoPartyPrimaryDefinition } from '@votingworks/fixtures'; import { getContestById, @@ -8,6 +13,7 @@ import { getBallotStylesByPartyId, getBallotStylesByPrecinctId, determinePartyId, + getDistrictById, } from './lookups'; test('getPrecinctById', () => { @@ -120,3 +126,35 @@ test('determinePartyId', () => { undefined ); }); + +test('getDistrictById', () => { + const electionDefinition = electionTwoPartyPrimaryDefinition; + expect(getDistrictById(electionDefinition, 'district-1').name).toEqual( + 'District 1' + ); + expect( + () => getDistrictById(electionDefinition, 'district-2').name + ).toThrowError(); + + // confirm that different elections are maintained separately + const modifiedElectionDefinition: ElectionDefinition = { + ...electionDefinition, + ballotHash: 'modified-ballot-hash', + election: { + ...electionDefinition.election, + districts: [ + { + id: unsafeParse(DistrictIdSchema, 'district-1'), + name: 'First District', + }, + ], + }, + }; + + expect( + getDistrictById(modifiedElectionDefinition, 'district-1').name + ).toEqual('First District'); + expect(getDistrictById(electionDefinition, 'district-1').name).toEqual( + 'District 1' + ); +}); diff --git a/libs/utils/src/tabulation/tabulation.test.ts b/libs/utils/src/tabulation/tabulation.test.ts index 765bc50e93e..d8de88e9176 100644 --- a/libs/utils/src/tabulation/tabulation.test.ts +++ b/libs/utils/src/tabulation/tabulation.test.ts @@ -41,6 +41,7 @@ import { getHmpbBallotCount, combineCandidateContestResults, buildContestResultsFixture, + yieldToEventLoop, } from './tabulation'; import { convertCastVoteRecordVotesToTabulationVotes, @@ -1326,3 +1327,11 @@ test('combinedCandidateContestResults - does not alter original tallies', () => expect(JSON.stringify(contestResultsA)).toEqual(aString); expect(JSON.stringify(contestResultsB)).toEqual(bString); }); + +test('yieldToEventLoop', async () => { + const fn = jest.fn(); + setImmediate(fn); + expect(fn).not.toHaveBeenCalled(); + await yieldToEventLoop(); + expect(fn).toHaveBeenCalled(); +}); diff --git a/libs/utils/src/tabulation/tabulation.ts b/libs/utils/src/tabulation/tabulation.ts index 835742622c9..6b6443a8b2d 100644 --- a/libs/utils/src/tabulation/tabulation.ts +++ b/libs/utils/src/tabulation/tabulation.ts @@ -354,7 +354,7 @@ export function getGroupSpecifierFromGroupKey( value ) as Tabulation.VotingMethod; break; - /* c8 ignore next 2 */ + /* istanbul ignore next */ default: throwIllegalValue(key); } diff --git a/libs/utils/src/votes.test.ts b/libs/utils/src/votes.test.ts index c6d3817df51..04ca2bcc835 100644 --- a/libs/utils/src/votes.test.ts +++ b/libs/utils/src/votes.test.ts @@ -7,6 +7,7 @@ import { import { BallotTargetMark, CandidateContest, + MarkStatus, Tabulation, WriteInCandidate, YesNoContest, @@ -16,6 +17,7 @@ import { convertMarksToVotesDict, getContestVoteOptionsForCandidateContest, getContestVoteOptionsForYesNoContest, + getMarkStatus, getSingleYesNoVote, hasWriteIns, normalizeWriteInId, @@ -252,3 +254,17 @@ test('hasWriteIns', () => { }) ).toEqual(true); }); + +test('getMarkStatus', () => { + expect(getMarkStatus(0.5, { marginal: 0.04, definite: 0.1 })).toEqual( + MarkStatus.Marked + ); + + expect(getMarkStatus(0.08, { marginal: 0.04, definite: 0.1 })).toEqual( + MarkStatus.Marginal + ); + + expect(getMarkStatus(0.07, { marginal: 0.08, definite: 0.1 })).toEqual( + MarkStatus.Unmarked + ); +}); diff --git a/libs/utils/src/votes.ts b/libs/utils/src/votes.ts index fc713f33e87..fd206d9e0cc 100644 --- a/libs/utils/src/votes.ts +++ b/libs/utils/src/votes.ts @@ -153,7 +153,7 @@ export function convertMarksToVotesDict( ? markToCandidateVotes(contest, markThresholds, mark) : contest.type === 'yesno' ? markToYesNoVotes(markThresholds, mark) - : /* c8 ignore next */ + : /* istanbul ignore next */ throwIllegalValue(contest, 'type'); votesDict[mark.contestId] = [...existingVotes, ...newVotes] as Vote;