Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/actions/zip/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,14 @@ runs:
- name: "Generate zip for Windows"
if: runner.os == 'Windows'
shell: powershell
run: Compress-Archive -Path ${{inputs.input}} -Destination ${{inputs.zipFilename}}
run: |
$inputPath = "${{inputs.input}}"
# Git Bash on Windows emits Unix-style paths like /c/Users/... instead of C:\Users\...
if ($inputPath -match '^/([a-zA-Z])/(.+)$') {
# Convert /c/some/path -> C:\some\path
$inputPath = $Matches[1].ToUpper() + ':' + ($Matches[2] -replace '/', '\')
} else {
# Expand ~ since PowerShell cmdlets don't resolve it in string arguments
$inputPath = $inputPath -replace '^~', $HOME
}
Compress-Archive -Path $inputPath -Destination ${{inputs.zipFilename}}
2 changes: 1 addition & 1 deletion .github/workflows/generate-dependency-hashes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ os=$1
parentPath=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )

echo "Filename, SHA-1 Checksum, SHA-256 Checksum, Maven Dependency URL, Direct URL to SHA-1, Direct URL to SHA-256"
cd ~/.gradle/caches/modules-2/files-2.1
cd "${GRADLE_USER_HOME:-$HOME/.gradle}/caches/modules-2/files-2.1"
for filename in $(find * -type f); do
# filename is of format, with dot-separated org:
# <org>/<dependency-name>/<version>/<sha-1>/<dependency-name>-<version>.<ext>
Expand Down
13 changes: 9 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,13 @@ jobs:
run: echo "FILEPATH=build/${{ steps.basefn.outputs.FILEPATH }}${{ steps.ext.outputs.EXT }}" >> $GITHUB_OUTPUT

- name: "Set up JDK 21.0.7"
uses: actions/setup-java@v3
uses: actions/setup-java@v5
with:
java-version: '21.0.7'
distribution: 'temurin'

- name: "Validate Gradle wrapper"
uses: gradle/actions/wrapper-validation@v3
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6

- name: "Create zip with jlinkZip"
uses: ./.github/actions/gradle-and-sha
Expand All @@ -140,12 +140,17 @@ jobs:
shell: bash
run: ./.github/workflows/generate-dependency-hashes.sh ${{ runner.os }} >> ${{steps.checksumsfn.outputs.FILEPATH}}

- name: "Get Gradle user home"
id: gradle-home
shell: bash
run: echo "PATH=${GRADLE_USER_HOME:-$HOME/.gradle}" >> $GITHUB_OUTPUT

- name: "Create dependency zip"
uses: ./.github/actions/zip
with:
# Build, then remove all non-essential files
command: ./gradlew assemble && ./gradlew --stop
input: "~/.gradle/caches"
input: "${{ steps.gradle-home.outputs.PATH }}/caches"
zipFilename: ${{steps.cachefn.outputs.FILEPATH}}

- name: "Generate SHA512 for plugins cache"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ void readCastVoteRecords(List<CastVoteRecord> castVoteRecords)
new CastVoteRecord(
source.getContestId(),
cvrData[CvrColumnField.ScanComputerName.ordinal()],
null,
cvrData[CvrColumnField.BoxID.ordinal()],
cvrData[CvrColumnField.BallotID.ordinal()],
cvrData[CvrColumnField.PrecinctID.ordinal()],
null,
Expand Down
88 changes: 40 additions & 48 deletions src/main/java/network/brightspots/rcv/ContestConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
class ContestConfig {

// If any booleans are unspecified in config file, they should default to false no matter what
static final String AUTOMATED_TEST_VERSION = "TEST";
static final String SUGGESTED_OUTPUT_DIRECTORY = "output";
static final boolean SUGGESTED_TABULATE_BY_BATCH = false;
static final boolean SUGGESTED_TABULATE_BY_PRECINCT = false;
Expand All @@ -58,7 +57,6 @@ class ContestConfig {
static final boolean SUGGESTED_CONTINUE_UNTIL_TWO_CANDIDATES_REMAIN = false;
static final boolean SUGGESTED_EXHAUST_ON_DUPLICATE_CANDIDATES = false;
static final boolean SUGGESTED_FIRST_ROUND_DETERMINES_THRESHOLD = false;
static final boolean SUGGESTED_TREAT_BLANK_AS_UNDECLARED_WRITE_IN = false;
static final int SUGGESTED_CVR_FIRST_VOTE_COLUMN = 4;
static final int SUGGESTED_CVR_FIRST_VOTE_ROW = 2;
static final int SUGGESTED_CVR_ID_COLUMN = 1;
Expand All @@ -67,7 +65,7 @@ class ContestConfig {
static final int SUGGESTED_MAX_SKIPPED_RANKS_ALLOWED = 1;
static final boolean SUGGESTED_MAX_SKIPPED_RANKS_ALLOWED_UNLIMITED = false;
static final String SUGGESTED_OVERVOTE_LABEL = "overvote";
static final String SUGGESTED_SKIPPED_RANK_LABEL = "undervote";
static final String SUGGESTED_UWI_LABEL = ""; // note: non-blank UWI labels creates UWI candidates
static final String MAX_SKIPPED_RANKS_ALLOWED_UNLIMITED_OPTION = "unlimited";
static final String MAX_RANKINGS_ALLOWED_NUM_CANDIDATES_OPTION = "max";
private static final int MIN_COLUMN_INDEX = 1;
Expand Down Expand Up @@ -109,12 +107,22 @@ private ContestConfig(RawContestConfig rawConfig, String sourceDirectory) {

static ContestConfig loadContestConfig(RawContestConfig rawConfig, String sourceDirectory) {
ContestConfig config = new ContestConfig(rawConfig, sourceDirectory);

try {
config.processCandidateData();
} catch (Exception exception) {
Logger.severe("Error processing candidate data:\n%s", exception);
ContestConfigMigration.migrateConfigVersion(config);
} catch (ContestConfigMigration.ConfigVersionIsNewerThanAppVersionException exception) {
Logger.severe("Error migrating config to current version:\n%s", exception);
config = null;
}

if (config != null) {
try {
config.processCandidateData();
} catch (Exception exception) {
Logger.severe("Error processing candidate data:\n%s", exception);
config = null;
}
}
return config;
}

Expand Down Expand Up @@ -148,6 +156,11 @@ static ContestConfig loadContestConfig(String configPath) {
return loadContestConfig(configPath, false);
}

static boolean isConfigFileInTestDir(String filepath) {
final String regex = "^.*brightspots.rcv.test_data.([^/\\\\]+).\\1_config\\.json$";
return filepath.matches(regex);
}

/* Performs basic validation on CVR sources and returns a set of validation errors. **/
static Set<ValidationError> performBasicCvrSourceValidation(CvrSource source) {
Set<ValidationError> validationErrors = new HashSet<>();
Expand All @@ -158,16 +171,18 @@ static Set<ValidationError> performBasicCvrSourceValidation(CvrSource source) {
if (!isNullOrBlank(source.getOvervoteLabel())
&& stringAlreadyInUseElsewhereInSource(
source.getOvervoteLabel(), source, "overvoteLabel")) {
Logger.severe(
"Overvote label must be defined and unique from other labels for CVR source: %s",
source.getFilePath());
validationErrors.add(ValidationError.CVR_OVERVOTE_LABEL_INVALID);
}
if (!isNullOrBlank(source.getSkippedRankLabel())
&& stringAlreadyInUseElsewhereInSource(
source.getSkippedRankLabel(), source, "skippedRankLabel")) {
validationErrors.add(ValidationError.CVR_SKIPPED_RANK_LABEL_INVALID);
}
if (!isNullOrBlank(source.getUndeclaredWriteInLabel())
&& stringAlreadyInUseElsewhereInSource(
source.getUndeclaredWriteInLabel(), source, "undeclaredWriteInLabel")) {
Logger.severe(
"Undeclared write-in label must be defined and unique"
+ " from other labels for CVR source: %s",
source.getFilePath());
validationErrors.add(ValidationError.CVR_UWI_LABEL_INVALID);
}

Expand Down Expand Up @@ -286,21 +301,6 @@ && stringAlreadyInUseElsewhereInSource(
source.getOvervoteDelimiter(), "overvoteDelimiter", provider, source.getFilePath())) {
validationErrors.add(ValidationError.CVR_OVERVOTE_DELIMITER_UNEXPECTEDLY_DEFINED);
}

if (fieldIsDefinedButShouldNotBeForProvider(
source.getSkippedRankLabel(), "skippedRankLabel", provider, source.getFilePath())) {
validationErrors.add(ValidationError.CVR_SKIPPED_RANK_LABEL_UNEXPECTEDLY_DEFINED);
}

if (source.getTreatBlankAsUndeclaredWriteIn()) {
logErrorWithLocation(
String.format(
"treatBlankAsUndeclaredWriteIn should not be true for CVR source with "
+ "provider \"%s\"",
provider),
source.getFilePath());
validationErrors.add(ValidationError.CVR_TREAT_BLANK_AS_UWI_UNEXPECTEDLY_TRUE);
}
}

boolean providerRequiresContestId =
Expand Down Expand Up @@ -484,8 +484,6 @@ private static boolean stringAlreadyInUseElsewhereInSource(
if (!inUse) {
inUse =
stringMatchesAnotherFieldValue(string, field, source.getOvervoteLabel(), "overvoteLabel")
|| stringMatchesAnotherFieldValue(
string, field, source.getSkippedRankLabel(), "skippedRankLabel")
|| stringMatchesAnotherFieldValue(
string, field, source.getUndeclaredWriteInLabel(), "undeclaredWriteInLabel");
}
Expand Down Expand Up @@ -534,13 +532,9 @@ private void validateTabulatorVersion() {
if (isNullOrBlank(getTabulatorVersion())) {
validationErrors.add(ValidationError.TABULATOR_VERSION_MISSING);
Logger.severe("tabulatorVersion is required!");
} else {
// ignore this check for test data, but otherwise require version to match current app version
if (!getTabulatorVersion().equals(AUTOMATED_TEST_VERSION)
&& !getTabulatorVersion().equals(Main.APP_VERSION)) {
validationErrors.add(ValidationError.TABULATOR_VERSION_NOT_SUPPORTED);
Logger.severe("tabulatorVersion %s not supported!", getTabulatorVersion());
}
} else if (!getTabulatorVersion().equals(Main.APP_VERSION)) {
validationErrors.add(ValidationError.TABULATOR_VERSION_NOT_SUPPORTED);
Logger.severe("tabulatorVersion %s not supported!", getTabulatorVersion());
}
if (validationErrors.contains(ValidationError.TABULATOR_VERSION_MISSING)
|| validationErrors.contains(ValidationError.TABULATOR_VERSION_NOT_SUPPORTED)) {
Expand Down Expand Up @@ -1165,7 +1159,7 @@ Integer getStopTabulationEarlyAfterRound() {

int getNumDeclaredCandidates() {
int size = getCandidateNames().size();
if (undeclaredWriteInsEnabled()) {
if (undeclaredWriteInsExplicitlyEnabled()) {
// we subtract one for UNDECLARED_WRITE_IN_OUTPUT_LABEL;
size = size - 1;
}
Expand Down Expand Up @@ -1267,24 +1261,25 @@ private void processCandidateData() {
}

// If any of the sources support undeclared write-ins, we need to recognize them as a valid
// "candidate" option.
if (undeclaredWriteInsEnabled()) {
// "candidate" option. Note: it's possible that
if (undeclaredWriteInsExplicitlyEnabled()) {
candidateNames.add(Tabulator.UNDECLARED_WRITE_IN_OUTPUT_LABEL);
candidateAliasesToNameMap.put(
Tabulator.UNDECLARED_WRITE_IN_OUTPUT_LABEL, Tabulator.UNDECLARED_WRITE_IN_OUTPUT_LABEL);
}
}

private boolean undeclaredWriteInsEnabled() {
boolean includeUwi = false;
/**
* Note: it is possible for UWIs to be _implictly_ declared, e.g., if images are found in ES&S
* .XLSXs, we assume they are UWIs.
*/
private boolean undeclaredWriteInsExplicitlyEnabled() {
for (CvrSource source : rawConfig.cvrFileSources) {
if (!isNullOrBlank(source.getUndeclaredWriteInLabel())
|| source.getTreatBlankAsUndeclaredWriteIn()) {
includeUwi = true;
break;
if (!isNullOrBlank(source.getUndeclaredWriteInLabel())) {
return true;
}
}
return includeUwi;
return false;
}

// Possible validation errors
Expand All @@ -1296,7 +1291,6 @@ enum ValidationError {
CVR_NO_FILES_SPECIFIED,
CVR_FILE_PATH_MISSING,
CVR_OVERVOTE_LABEL_INVALID,
CVR_SKIPPED_RANK_LABEL_INVALID,
CVR_UWI_LABEL_INVALID,
CVR_PROVIDER_INVALID,
CVR_FIRST_VOTE_COLUMN_INVALID,
Expand All @@ -1306,7 +1300,6 @@ enum ValidationError {
CVR_PRECINCT_COLUMN_INVALID,
CVR_OVERVOTE_DELIMITER_INVALID,
CVR_CDF_FILE_PATH_INVALID,
CVR_TREAT_BLANK_AS_UWI_UNEXPECTEDLY_TRUE,
CVR_CONTEST_ID_INVALID,
CVR_DUPLICATE_FILE_PATHS,
CVR_FILE_PATH_INVALID,
Expand All @@ -1323,7 +1316,6 @@ enum ValidationError {
CVR_ID_COLUMN_UNEXPECTEDLY_DEFINED,
CVR_BATCH_COLUMN_UNEXPECTEDLY_DEFINED,
CVR_PRECINCT_COLUMN_UNEXPECTEDLY_DEFINED,
CVR_SKIPPED_RANK_LABEL_UNEXPECTEDLY_DEFINED,
CVR_CONTEST_ID_UNEXPECTEDLY_DEFINED,
CVR_OUTPUT_NOT_ALLOWED_IN_USER_DIRECTORY,
CANDIDATE_NAME_MISSING,
Expand Down
Loading
Loading