Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update VxAdmin to support importing new CVR export format #3956

Merged
merged 10 commits into from
Sep 13, 2023
142 changes: 109 additions & 33 deletions libs/utils/src/filenames.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
generateCastVoteRecordReportDirectoryName,
parseCastVoteRecordReportDirectoryName,
generateCastVoteRecordExportDirectoryName,
CastVoteRecordExportDirectoryNameComponents,
parseCastVoteRecordReportExportDirectoryName,
} from './filenames';

describe('parseBallotExportPackageInfoFromFilename', () => {
Expand Down Expand Up @@ -187,39 +189,6 @@ describe('generateCastVoteRecordReportDirectoryName', () => {
});
});

test.each<{
inTestMode: boolean;
machineId: string;
time: Date;
expectedDirectoryName: string;
}>([
{
inTestMode: true,
machineId: 'SCAN-0001',
time: new Date(2023, 7, 16, 17, 2, 24),
expectedDirectoryName: 'TEST__machine_SCAN-0001__2023-08-16_17-02-24',
},
{
inTestMode: false,
machineId: 'SCAN-0001',
time: new Date(2023, 7, 16, 17, 2, 24),
expectedDirectoryName: 'machine_SCAN-0001__2023-08-16_17-02-24',
},
{
inTestMode: true,
machineId: '<3-u!n#icorn<3',
time: new Date(2023, 7, 16, 17, 2, 24),
expectedDirectoryName: 'TEST__machine_3unicorn3__2023-08-16_17-02-24',
},
])(
'generateCastVoteRecordExportDirectoryName',
({ expectedDirectoryName, ...input }) => {
expect(generateCastVoteRecordExportDirectoryName(input)).toEqual(
expectedDirectoryName
);
}
);

test('generates ballot export package names as expected with simple inputs', () => {
const mockElection: ElectionDefinition = {
election: {
Expand Down Expand Up @@ -600,3 +569,110 @@ test('generateLogFilename', () => {
)
);
});

test.each<{
input: CastVoteRecordExportDirectoryNameComponents;
expectedDirectoryName: string;
}>([
{
input: {
inTestMode: true,
machineId: 'SCAN-0001',
time: new Date(2023, 7, 16, 17, 2, 24),
},
expectedDirectoryName: 'TEST__machine_SCAN-0001__2023-08-16_17-02-24',
},
{
input: {
inTestMode: false,
machineId: 'SCAN-0001',
time: new Date(2023, 7, 16, 17, 2, 24),
},
expectedDirectoryName: 'machine_SCAN-0001__2023-08-16_17-02-24',
},
{
input: {
inTestMode: true,
machineId: '<3-u!n#icorn<3',
time: new Date(2023, 7, 16, 17, 2, 24),
},
expectedDirectoryName: 'TEST__machine_3unicorn3__2023-08-16_17-02-24',
},
])(
'generateCastVoteRecordExportDirectoryName',
({ input, expectedDirectoryName }) => {
expect(generateCastVoteRecordExportDirectoryName(input)).toEqual(
expectedDirectoryName
);
}
);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test.each is just moved down from above. The one below is new.


test.each<{
directoryName: string;
expectedDirectoryNameComponents?: CastVoteRecordExportDirectoryNameComponents;
}>([
{
directoryName: 'TEST__machine_SCAN-0001__2023-08-16_17-02-24',
expectedDirectoryNameComponents: {
inTestMode: true,
machineId: 'SCAN-0001',
time: new Date(2023, 7, 16, 17, 2, 24),
},
},
{
directoryName: 'machine_SCAN-0001__2023-08-16_17-02-24',
expectedDirectoryNameComponents: {
inTestMode: false,
machineId: 'SCAN-0001',
time: new Date(2023, 7, 16, 17, 2, 24),
},
},
{
directoryName:
'TEST__machine_SCAN-0001__2023-08-16_17-02-24__extra-section',
expectedDirectoryNameComponents: undefined,
},
{
directoryName: 'machine_SCAN-0001__2023-08-16_17-02-24__extra-section',
expectedDirectoryNameComponents: undefined,
},
{
directoryName: 'TEST__machine_SCAN-0001', // Missing time section
expectedDirectoryNameComponents: undefined,
},
{
directoryName: 'machine_SCAN-0001', // Missing time section
expectedDirectoryNameComponents: undefined,
},
{
directoryName: 'TEST__SCAN-0001__2023-08-16_17-02-24', // Missing machine_ prefix
expectedDirectoryNameComponents: undefined,
},
{
directoryName: 'SCAN-0001__2023-08-16_17-02-24', // Missing machine_ prefix
expectedDirectoryNameComponents: undefined,
},
{
directoryName: 'TEST__wrong-prefix_SCAN-0001__2023-08-16_17-02-24',
expectedDirectoryNameComponents: undefined,
},
{
directoryName: 'wrong-prefix_SCAN-0001__2023-08-16_17-02-24',
expectedDirectoryNameComponents: undefined,
},
{
directoryName: 'TEST__machine_SCAN-0001__invalid-time',
expectedDirectoryNameComponents: undefined,
},
{
directoryName: 'machine_SCAN-0001__invalid-time',
expectedDirectoryNameComponents: undefined,
},
])(
'parseCastVoteRecordReportExportDirectoryName',
({ directoryName, expectedDirectoryNameComponents }) => {
expect(parseCastVoteRecordReportExportDirectoryName(directoryName)).toEqual(
expectedDirectoryNameComponents
);
}
);
91 changes: 67 additions & 24 deletions libs/utils/src/filenames.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ export interface CastVoteRecordReportListing {
timestamp: Date;
}

export interface CastVoteRecordExportDirectoryNameComponents {
inTestMode: boolean;
machineId: string;
time: Date;
}

export function sanitizeStringForFilename(
input: string,
{ replaceInvalidCharsWith = '', defaultValue = 'placeholder' } = {}
Expand Down Expand Up @@ -120,37 +126,15 @@ export function generateCastVoteRecordReportDirectoryName(
return isTestMode ? `TEST${SECTION_SEPARATOR}${filename}` : filename;
}

/**
* Generates a name for a cast vote record export directory
*/
export function generateCastVoteRecordExportDirectoryName({
inTestMode,
machineId,
time = new Date(),
}: {
inTestMode: boolean;
machineId: string;
time?: Date;
}): string {
const machineString = [
'machine',
maybeParse(MachineId, machineId) ?? sanitizeStringForFilename(machineId),
].join(SUBSECTION_SEPARATOR);
const timeString = moment(time).format(TIME_FORMAT_STRING);
const directoryNameComponents = [machineString, timeString];
if (inTestMode) {
directoryNameComponents.unshift('TEST');
}
return directoryNameComponents.join(SECTION_SEPARATOR);
}

/**
* Extract information about a CVR file from the filename. Expected filename
* format with human-readable separators is:
* [TEST__]machine_{machineId}__{numberOfBallots}_ballots__YYYY-MM-DD_HH-mm-ss.jsonl
* This format is current as of 2023-02-22 and may be out of date if separator constants have changed
*
* If the the parsing is unsuccessful, returns `undefined`.
*
* @deprecated
*/
export function parseCastVoteRecordReportDirectoryName(
filename: string
Expand Down Expand Up @@ -326,3 +310,62 @@ export function generateLogFilename(
throwIllegalValue(fileType);
}
}

/**
* Generates a name for a cast vote record export directory
*/
export function generateCastVoteRecordExportDirectoryName({
inTestMode,
machineId,
time = new Date(),
}: Omit<CastVoteRecordExportDirectoryNameComponents, 'time'> & {
time?: Date;
}): string {
const machineString = [
'machine',
maybeParse(MachineId, machineId) ?? sanitizeStringForFilename(machineId),
].join(SUBSECTION_SEPARATOR);
const timeString = moment(time).format(TIME_FORMAT_STRING);
const directoryNameComponents = [machineString, timeString];
if (inTestMode) {
directoryNameComponents.unshift('TEST');
}
return directoryNameComponents.join(SECTION_SEPARATOR);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, this function is just moved down from above. The one below is new.


/**
* Extracts information about a cast vote record export from the export directory name
*/
export function parseCastVoteRecordReportExportDirectoryName(
exportDirectoryName: string
): Optional<CastVoteRecordExportDirectoryNameComponents> {
const sections = exportDirectoryName.split(SECTION_SEPARATOR);
const inTestMode = sections.length === 3 && sections[0] === 'TEST';
const postTestPrefixSections = inTestMode ? sections.slice(1) : sections;
if (postTestPrefixSections.length !== 2) {
return;
}
assert(postTestPrefixSections[0] !== undefined);
assert(postTestPrefixSections[1] !== undefined);

const machineString = postTestPrefixSections[0];
const machineSubsections = machineString.split(SUBSECTION_SEPARATOR);
if (machineSubsections.length !== 2 || machineSubsections[0] !== 'machine') {
return;
}
assert(machineSubsections[1] !== undefined);
const machineId = machineSubsections[1];

const timeString = postTestPrefixSections[1];
const timeMoment = moment(timeString, TIME_FORMAT_STRING);
if (!timeMoment.isValid()) {
return;
}
const time = timeMoment.toDate();

return {
inTestMode,
machineId,
time,
};
}