Skip to content

Commit

Permalink
Deprecate VxScan's existing backup functionality in favor of "extende…
Browse files Browse the repository at this point in the history
…d" CVR exports (#4011)

* Prep for addition of rejected-<id>/ sub-directories in CVR exports

For rejected sheets

* Clean up image reference logic in buildCastVoteRecord fn, specifically:

- Include image references for BMD ballots
- Include image references for both sides of a sheet (instead of
  potentially just one side) to simplify the data model

* Update buildCastVoteRecord tests

* Update exportCastVoteRecordsToUsbDrive to include rejected sheets

Also account for changes to buildCastVoteRecord

* Update VxScan backend store, specifically:

- Update sheet retrieval methods
- Delete old-style backup methods

* Update VxScan backend store tests

* Update VxScan state machine to export rejected sheets

* Update rest of VxScan backend to account for store changes

* Update remaining VxScan backend tests

* Update VxScan backend code coverage thresholds

* Remove now unneeded "Save Backup" button from VxScan UI

* Update VxScan frontend tests

* Minimally update the VxCentralScan backend

To account for libs/backend changes

* Don't check whether USB requires CVR sync after polls have been closed

As well as before polls have been opened

* Block relevant actions when a CVR sync is detected as necessary

- Closing polls
- Switching from official mode to test mode (assuming ballots have
  been counted)
- Deleting election data

* Update/add tests

* Add a few more tests
  • Loading branch information
arsalansufi authored Oct 4, 2023
1 parent f101f51 commit d5e4f83
Show file tree
Hide file tree
Showing 40 changed files with 1,005 additions and 1,597 deletions.
2 changes: 1 addition & 1 deletion apps/central-scan/backend/src/central_scanner_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ export function buildCentralScannerApp({
const exportResult = await exportCastVoteRecordsToUsbDrive(
store,
usb,
store.forEachResultSheet(),
store.forEachAcceptedSheet(),
{ scannerType: 'central' }
);

Expand Down
33 changes: 17 additions & 16 deletions apps/central-scan/backend/src/store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import * as tmp from 'tmp';
import { v4 as uuid } from 'uuid';
import { sleep, typedAs } from '@votingworks/basics';
import { ResultSheet } from '@votingworks/backend';
import { AcceptedSheet } from '@votingworks/backend';
import { electionGridLayoutNewHampshireAmherstFixtures } from '@votingworks/fixtures';
import { sha256 } from 'js-sha256';
import { zeroRect } from '../test/fixtures/zero_rect';
Expand Down Expand Up @@ -534,22 +534,23 @@ const sheetWithFiles: SheetOf<PageInterpretationWithFiles> = [
},
];

test('iterating over all result sheets', () => {
test('iterating over all accepted sheets', () => {
const store = Store.memoryStore();
store.setElectionAndJurisdiction({ electionData, jurisdiction });

// starts empty
expect(Array.from(store.forEachResultSheet())).toEqual([]);
expect(Array.from(store.forEachAcceptedSheet())).toEqual([]);

// add a batch with a sheet
const batchId = store.addBatch();
store.addSheet(uuid(), batchId, sheetWithFiles);
store.finishBatch({ batchId });

// has one sheet
expect(Array.from(store.forEachResultSheet())).toEqual(
typedAs<ResultSheet[]>([
expect(Array.from(store.forEachAcceptedSheet())).toEqual(
typedAs<AcceptedSheet[]>([
{
type: 'accepted',
id: expect.any(String),
batchId,
indexInBatch: 1,
Expand All @@ -563,7 +564,7 @@ test('iterating over all result sheets', () => {

// delete the batch and the results are empty again
store.deleteBatch(batchId);
expect(Array.from(store.forEachResultSheet())).toEqual([]);
expect(Array.from(store.forEachAcceptedSheet())).toEqual([]);

// add a sheet requiring adjudication and check that it is not included
const batchId2 = store.addBatch();
Expand All @@ -590,10 +591,10 @@ test('iterating over all result sheets', () => {
},
sheetWithFiles[1],
]);
expect(Array.from(store.forEachResultSheet())).toEqual([]);
expect(Array.from(store.forEachAcceptedSheet())).toEqual([]);
});

test('iterating over each result sheet includes correct batch sequence id', () => {
test('iterating over each accepted sheet includes correct batch sequence id', () => {
const store = Store.memoryStore();
store.setElectionAndJurisdiction({ electionData, jurisdiction });

Expand Down Expand Up @@ -627,9 +628,9 @@ test('iterating over each result sheet includes correct batch sequence id', () =
const batch3Sheet2Id = store.addSheet(uuid(), batch3Id, generateSheet());
store.finishBatch({ batchId: batch3Id });

const resultSheets = Array.from(store.forEachResultSheet());
expect(resultSheets).toHaveLength(6);
const expectedResultSheets: Array<
const acceptedSheets = Array.from(store.forEachAcceptedSheet());
expect(acceptedSheets).toHaveLength(6);
const expectedAcceptedSheets: Array<
[id: string, batchId: string, indexInBatch: number]
> = [
[batch1Sheet1Id, batch1Id, 1],
Expand All @@ -639,13 +640,13 @@ test('iterating over each result sheet includes correct batch sequence id', () =
[batch3Sheet1Id, batch3Id, 1],
[batch3Sheet2Id, batch3Id, 2],
];
for (const expectedResultSheet of expectedResultSheets) {
expect(resultSheets).toMatchObject(
for (const expectedAcceptedSheet of expectedAcceptedSheets) {
expect(acceptedSheets).toMatchObject(
expect.arrayContaining([
expect.objectContaining({
id: expectedResultSheet[0],
batchId: expectedResultSheet[1],
indexInBatch: expectedResultSheet[2],
id: expectedAcceptedSheet[0],
batchId: expectedAcceptedSheet[1],
indexInBatch: expectedAcceptedSheet[2],
}),
])
);
Expand Down
7 changes: 4 additions & 3 deletions apps/central-scan/backend/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { sha256 } from 'js-sha256';
import { DateTime } from 'luxon';
import { dirname, join } from 'path';
import { v4 as uuid } from 'uuid';
import { ResultSheet } from '@votingworks/backend';
import { AcceptedSheet } from '@votingworks/backend';
import {
clearCastVoteRecordHashes,
getCastVoteRecordRootHash,
Expand Down Expand Up @@ -856,9 +856,9 @@ export class Store {
}

/**
* Yields all sheets in the database that would be included in a CVR export.
* Yields all scanned sheets that were accepted and should be tabulated
*/
*forEachResultSheet(): Generator<ResultSheet> {
*forEachAcceptedSheet(): Generator<AcceptedSheet> {
const sql = `
select
sheets.id as id,
Expand Down Expand Up @@ -887,6 +887,7 @@ export class Store {
indexInBatch: number;
}>) {
yield {
type: 'accepted',
id: row.id,
batchId: row.batchId,
indexInBatch: row.indexInBatch,
Expand Down
4 changes: 2 additions & 2 deletions apps/scan/backend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ module.exports = {
coverageThreshold: {
global: {
statements: 96,
branches: 86,
functions: 91,
branches: 89,
functions: 90,
lines: 96,
},
},
Expand Down
2 changes: 0 additions & 2 deletions apps/scan/backend/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ create table election (
ballot_count_when_ballot_bag_last_replaced integer not null default 0,
is_sound_muted boolean not null default false,
is_ultrasonic_disabled boolean not null default false,
cvrs_backed_up_at datetime,
scanner_backed_up_at datetime,
created_at timestamp not null default current_timestamp
);

Expand Down
16 changes: 2 additions & 14 deletions apps/scan/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
import express, { Application } from 'express';
import {
createUiStringsApi,
ExportDataError,
readBallotPackageFromUsb,
exportCastVoteRecordsToUsbDrive,
doesUsbDriveRequireCastVoteRecordSync as doesUsbDriveRequireCastVoteRecordSyncFn,
Expand All @@ -30,7 +29,6 @@ import {
LiveCheck,
} from '@votingworks/auth';
import { UsbDrive, UsbDriveStatus } from '@votingworks/usb-drive';
import { backupToUsbDrive } from './backup';
import {
PrecinctScannerStateMachine,
PrecinctScannerConfig,
Expand Down Expand Up @@ -184,11 +182,7 @@ export function buildApi(
};
},

unconfigureElection(input: { ignoreBackupRequirement?: boolean }): void {
assert(
input.ignoreBackupRequirement || store.getCanUnconfigure(),
'Attempt to unconfigure without backup'
);
unconfigureElection(): void {
workspace.reset();
},

Expand Down Expand Up @@ -274,10 +268,6 @@ export function buildApi(
store.setBallotCountWhenBallotBagLastReplaced(store.getBallotsCounted());
},

async backupToUsbDrive(): Promise<Result<void, ExportDataError>> {
return await backupToUsbDrive(store, usbDrive);
},

async exportCastVoteRecordsToUsbDrive(input: {
mode: 'full_export' | 'polls_closing';
}): Promise<Result<void, ExportCastVoteRecordsToUsbDriveError>> {
Expand All @@ -286,7 +276,7 @@ export function buildApi(
return exportCastVoteRecordsToUsbDrive(
store,
usbDrive,
store.forEachResultSheet(),
store.forEachSheet(),
{ scannerType: 'precinct', isFullExport: true }
);
}
Expand Down Expand Up @@ -314,11 +304,9 @@ export function buildApi(
getScannerStatus(): PrecinctScannerStatus {
const machineStatus = machine.status();
const ballotsCounted = store.getBallotsCounted();
const canUnconfigure = store.getCanUnconfigure();
return {
...machineStatus,
ballotsCounted,
canUnconfigure,
};
},

Expand Down
Loading

0 comments on commit d5e4f83

Please sign in to comment.