From acc7eb4b5d035d6364b687687903351e9305cdfb Mon Sep 17 00:00:00 2001 From: michelleblom Date: Wed, 19 Jun 2024 21:14:36 +1000 Subject: [PATCH 01/12] Fleshed out IRVComparisonAudit methods, with exception of remove/record discrepancy. --- .../corla/model/IRVComparisonAudit.java | 345 +++++++++++++++++- .../corla/model/assertion/Assertion.java | 79 +++- .../corla/query/AssertionQueries.java | 84 +++++ .../corla/model/ComparisonAudit.java | 23 +- 4 files changed, 491 insertions(+), 40 deletions(-) create mode 100644 server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/query/AssertionQueries.java diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java index 8896ff7b..35362aa3 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java @@ -21,18 +21,41 @@ package au.org.democracydevelopers.corla.model; +import au.org.democracydevelopers.corla.query.AssertionQueries; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.JoinTable; +import javax.persistence.ManyToMany; +import us.freeandfair.corla.math.Audit; import us.freeandfair.corla.model.*; import java.math.BigDecimal; import java.util.OptionalInt; -import static us.freeandfair.corla.math.Audit.GAMMA; +import au.org.democracydevelopers.corla.model.assertion.Assertion; + +import static java.util.Collections.max; /** - * Stub of the IRVComparisonAudit class, which currently does nothing. + * A class representing the state of a single audited IRV contest across one or more counties. */ +@Entity +@DiscriminatorValue("IRV") public class IRVComparisonAudit extends ComparisonAudit { + /** + * List of assertions associated with this audit -- the set of all assertions in the database + * table 'assertion' whose contest name matches that of this ComparisonAudit (located in the + * base classes' contest result attribute, 'my_contest_resylt'). + */ + @ManyToMany() + @JoinTable(name = "audit_to_assertions", joinColumns = { @JoinColumn(name = "id") }) + private List assertions; + /** * Constructs a new, empty IRVComparisonAudit (solely for persistence). */ @@ -41,41 +64,335 @@ public IRVComparisonAudit() { } /** - * Constructs an IRVComparisonAudit for the given params + * Constructs an IRVComparisonAudit for the contest described in the given ContestResult, + * with the given risk limit, and audit reason. This constructor calls the base class + * constructor, and then populates this IRV audit's list of assertions by collecting all + * assertions from the database associated with the given contest. It then calculates and + * sets the audit's diluted margin (stored in the base class) and calls methods for + * establishing the optimistic and estimated sample size for the audit. This follows the + * logic of the original ComparisonAudit class for Plurality contests. A RuntimeException is + * thrown if an unexpected error arises when retrieving assertions from the database, or in the + * computation of optimistic and estimated sample sizes. * - * @param contestResult The contest result. + * @param contestResult The contest result (identifies the contest under audit). * @param riskLimit The risk limit. * @param auditReason The audit reason. - * */ @SuppressWarnings({"PMD.ConstructorCallsOverridableMethod"}) public IRVComparisonAudit(final ContestResult contestResult, final BigDecimal riskLimit, - final AuditReason auditReason) { - super(contestResult, riskLimit, BigDecimal.ONE, GAMMA, auditReason); + final AuditReason auditReason) { + super(contestResult, riskLimit, BigDecimal.ONE, Audit.GAMMA, auditReason); + + final String prefix = "[IRVComparisonAudit all args constructor]"; + final String contestName = contestResult.getContestName(); + + LOGGER.debug(String.format("%s called for contest %s risk limit %f and audit reason %s.", + prefix, contestName, riskLimit, auditReason)); + + // Populate the list of assertions belonging to this audit (retrieve all assertions from + // the database whose contest name matches that in the given contestResult). If an + // unexpected error arises in retrieving this data from the database, matching() will throw + // a RunTimeException. + assertions = AssertionQueries.matching(contestResult.getContestName()); + + LOGGER.debug(String.format("%s retrieved %d assertions for contest %s.", + prefix, assertions.size(), contestName)); + + try { + // If there are no assertions for this audit, then the contest is not auditable. + if (assertions.isEmpty()) { + // Setting the diluted margin to 0, as the contest is not auditable. + diluted_margin = BigDecimal.ZERO; + setAuditStatus(AuditStatus.NOT_AUDITABLE); + LOGGER.debug(String.format("%s contest %s is not auditable, setting diluted margin to 0," + + "and status to NOT_AUDITABLE.", prefix, contestName)); + } else { + // Assign a diluted margin to the audit. This is equal to the smallest diluted margin of + // any assertion in 'assertions' (ie. the hardest to audit assertion). + diluted_margin = Collections.min(assertions.stream().map(Assertion::getDilutedMargin).toList()); + + // Initialise the status of the audit. + setAuditStatus(AuditStatus.NOT_STARTED); + + // As the super class invokes sample size estimation methods in its constructor, + // we need to invoke them here after the audit's assertions have been populated. Otherwise, + // they will not have been computed correctly for IRV. + optimisticSamplesToAudit(); + estimatedSamplesToAudit(); + } + } catch(Exception e){ + final String msg = String.format("%s An unexpected error arose during diluted margin " + + "and initial sample size computation when instantiating an IRVComparison audit " + + "for contest %s: %s", prefix, contestName, e.getMessage()); + LOGGER.error(msg); + throw new RuntimeException(msg); + } } - // Does nothing - TODO. + /** + * Recalculates the estimated number of ballots to audit, and updates the audit's current level + * of risk, setting the base class' `my_optimistic_samples_to_audit` and + * `my_estimated_samples_to_audit` attributes in the process. Each assertion in this audit is + * updated with its current risk. The method will check whether the optimistic sample size + * of the audit (the number of ballots we expect to need to sample assuming no further + * overstatements arise) needs to be recalculated. If so, each assertion's optimistic sample + * size will be computed, and we will take the maximum of these as the optimistic sample size + * for the audit as a whole. We then call each assertion's estimated sample size computation + * method, and take the maximum of these as the estimated sample size of the audit as a whole. + * A RuntimeException is thrown when an unexpected error arises during sample size computation. This + * calculation involves both newly added code and existing code within various parts of colorado-rla. + */ @Override protected void recalculateSamplesToAudit() { + if(assertions == null){ + // We have not associated assertions with this audit yet. When an IRVComparisonAudit + // constructed, we call the base class constructor first. The ComparisonAudit constructor + // calls sample size computation methods. So, this method may end up being called before + // the IRVComparisonAudit constructor has completed its setup (retrieving assertions from + // the database and so on). In which case, we simply ignore the call. + return; + } + + final String prefix = "[recalculateSamplesToAudit]"; + final String contestName = getContestName(); + + try { + if (assertions.isEmpty()) { + // This contest is not auditable. + // There are no assertions in this audit (Note: this is distinct from the above case where + // we have not yet *retrieved* assertions and initialised the 'assertions' attribute). + my_optimistic_samples_to_audit = 0; + my_estimated_samples_to_audit = 0; + my_optimistic_recalculate_needed = false; + my_estimated_recalculate_needed = false; + + LOGGER.debug(String.format("%s No assertions for contest %s; setting optimistic and " + + "estimate sample sizes to 0; and optimistic and estimated recalculate needed to false.", + prefix, getContestName())); + } else { + // Sample size computation works by (1) computing the optimistic sample size if its + // current value is out of date, and then (2) computing estimated sample size (which uses + // the optimistic sample size as a starting point). We first check whether we need to update + // the optimistic sample size stored in the base ComparisonAudit class. + if (my_optimistic_recalculate_needed) { + // We compute the optimistic sample size for each of the IRVComparisonAudit's + // assertions and take the largest of these as the optimistic sample size of the + // audit as a whole. + LOGGER.debug(String.format("%s calling computeOptimisticSamplesToAudit() for each " + + "assertion in contest %s given risk limit %f.", prefix, contestName, getRiskLimit())); + my_optimistic_samples_to_audit = max(assertions.stream().map(a -> + a.computeOptimisticSamplesToAudit(getRiskLimit())).toList()); + my_optimistic_recalculate_needed = false; + LOGGER.debug(String.format("%s optimistic sample size of %d computed for contest %s.", + prefix, my_optimistic_samples_to_audit, contestName)); + } + // We now compute the estimated sample size for each assertion, and take the maximum as + // the overall estimated sample sizes of the audit. + LOGGER.debug(String.format("%s calling computeEstimatedSamplesToAudit() for each " + + "assertion in contest %s given current audited sample count of %d.", prefix, + contestName, getAuditedSampleCount())); + my_estimated_samples_to_audit = max(assertions.stream().map(a -> + a.computeEstimatedSamplesToAudit(getAuditedSampleCount())).toList()); + my_optimistic_recalculate_needed = false; + + // Tell each assertion to update its risk calculation, return maximum risk across + // assertions. This will record an updated risk in each assertion's currentRisk attribute. + final BigDecimal risk = max(assertions.stream().map(a -> + a.riskMeasurement(getAuditedSampleCount())).toList()); + + LOGGER.debug(String.format("%s estimated sample size of %d computed for contest %s; risk %f.", + prefix, my_estimated_samples_to_audit, contestName, risk)); + } + } catch(Exception e){ + // An unexpected error has arisen during sample size computation. + final String msg = String.format("%s An unexpected error arose in sample size " + + "computation for the IRVComparisonAudit of contest %s: %s.", prefix, contestName, + e.getMessage()); + LOGGER.error(msg); + throw new RuntimeException(msg); + } } - // Does nothing - TODO. + /** + * This method is currently not being used in the base class. However, if it starts being used + * in the future, we need an IRV version for IRV audits. Consequently, we provide the IRV + * version here. This method computes and returns the initial (optimistic) sample size + * for the audit. This is computed by calling the optimistic sample size computation method + * of each of the audit's assertions, and returning the maximum of these. A RuntimeException is + * thrown if an unexpected error arises in computing optimistic sample sizes for each assertion. + * @return the optimistic sample size for the audit. + */ @Override public int initialSamplesToAudit() { - return 0; + if(assertions == null){ + // We have not associated assertions with this audit yet. + // In this case, we simply ignore the call. + return 0; + } + + final String prefix = "[initialSamplesToAudit]"; + final String contestName = getContestName(); + + try { + LOGGER.debug(String.format("%s computing the initial samples to audit for the IRV contest %s.", + prefix, contestName)); + + if (assertions.isEmpty()) { + LOGGER.debug(String.format("%s No assertions for contest %s; returning an initial sample " + + "size of 0.", prefix, contestName)); + return 0; + } else { + LOGGER.debug(String.format("%s calling computeOptimisticSamplesToAudit() for each " + + "assertion in contest %s given risk limit %f.", prefix, contestName, getRiskLimit())); + final int samples = max(assertions.stream().map(a -> + a.computeOptimisticSamplesToAudit(getRiskLimit())).toList()); + LOGGER.debug(String.format("%s optimistic sample size of %d computed for contest %s.", + prefix, samples, contestName)); + return samples; + } + } catch(Exception e){ + // An unexpected error has arisen during sample size computation. + final String msg = String.format("%s An unexpected error arose in sample size " + + "computation for the IRVComparisonAudit of contest %s: %s.", prefix, contestName, + e.getMessage()); + LOGGER.error(msg); + throw new RuntimeException(msg); + } } - // Does nothing - TODO. + /** + * Computes the discrepancy (if any) between the specified CVR and matching audited ballot (ACVR), + * with respect to each assertion in this audit and the audit's contest. This method returns an + * optional int that, if present, indicates that a discrepancy exists. + * There are 5 possible types of discrepancy: -1 indicates a one vote understatement; a -2 + * indicates a two vote understatement; a 1 indicates a one vote overstatement; a 2 a two vote + * overstatement; and a 0 an "other" discrepancy (not an understatement or overstatement but + * a difference in the vote recorded for this audit's contest). + * Discrepancies are computed with respect to individual assertions. This method will consider + * each assertion, and compute the discrepancy (if one exists) between the CVR and ACVR for that + * assertion. If no discrepancy exists for any of the audit's assertions, then an empty + * optional int will be returned. Otherwise, the method will return the maximum discrepancy + * arising from this CVR-ACVR pair across all assertions. + * This method has side effects: calling each assertion's computeDiscrepancy() method will store + * the CVR ID and discrepancy value of the CVR-ACVR pair *if* a discrepancy was found. If the + * recordDiscrepancy() method is called with this CVR and ACVR pair, the previously computed + * discrepancies will be recorded in each assertion's discrepancy counts. This logic follows + * how discrepancy computation and recording works for Plurality audits. A RuntimeException + * will be thrown in the following circumstances: the provided CVR/ACVR is null; the + * IRVComparisonAudit's assertions list is null (not yet set); a RuntimeException has been + * thrown by an assertion's computeDiscrepancy() method; or an unexpected error has been caught. + * + * @param cvr The CVR that the machine saw. + * @param auditedCVR The ACVR that the human audit board saw. + * @return an optional int that is present if there is a discrepancy between the CVR and audited + * ballot, for at least one assertion, and absent otherwise. + */ @Override public OptionalInt computeDiscrepancy(final CastVoteRecord cvr, final CastVoteRecord auditedCVR) { - return OptionalInt.of(0); + final String prefix = "[computeDiscrepancy]"; + final String contestName = getContestName(); + + if(cvr == null || auditedCVR == null){ + // This is not valid input. + final String msg = String.format("%s Calling computeDiscrepancy() with null CVR/ACVR.", prefix); + LOGGER.error(msg); + throw new RuntimeException(msg); + } + + LOGGER.debug(String.format("%s Computing discrepancy between CVR ID %d and audited ballot, " + + "contest %s.", prefix, cvr.id(), contestName)); + + if(assertions == null){ + // This should not happen; if it does, something has gone wrong. + final String msg = String.format("%s IRVComparison audit for contest %s has not set its " + + "assertions list yet (it is null).", prefix, contestName); + LOGGER.error(msg); + throw new RuntimeException(msg); + } + + LOGGER.debug(String.format("%s Contest %s: Calling computeDiscrepancy() for each assertion.", + prefix, contestName)); + + // Compute discrepancies with respect to each assertion for this CVR/ACVR pair. If a + // discrepancy exists for an assertion, add its value to a list. + try { + List discrepancies = new ArrayList<>(); + for (Assertion a : assertions) { + final OptionalInt result = a.computeDiscrepancy(cvr, auditedCVR); + + if (result.isPresent()) { + discrepancies.add(result.getAsInt()); + } + } + + // If we've found no discrepancies across the assertion set, then none exists for this contest + // and this CVR/ACVR pair. Otherwise, return the maximum discrepancy found across all assertions. + if (discrepancies.isEmpty()) { + LOGGER.debug(String.format("%s Contest %s: No discrepancies found for CVR ID %d.", prefix, + contestName, cvr.id())); + return OptionalInt.empty(); + } else { + final Integer maxDiscrepancy = max(discrepancies); + LOGGER.debug(String.format("%s Contest %s: Maximum discrepancy of %d found for CVR ID %d.", + prefix, contestName, maxDiscrepancy, cvr.id())); + return OptionalInt.of(maxDiscrepancy); + } + + } catch(RuntimeException e){ + final String msg = String.format("%s Contest %s, RuntimeException: %s", prefix, + contestName, e.getMessage()); + LOGGER.error(msg); + throw new RuntimeException(msg); + + } catch(Exception e){ + // Unexpected exception: log and throw as a RuntimeException. + final String msg = String.format("%s Contest %s, Exception: %s", prefix, contestName, + e.getMessage()); + LOGGER.error(msg); + throw new RuntimeException(msg); + } } - // Does nothing - TODO. + /** + * Returns the maximum assertion risk across this audit's set of assertions. This method + * will call each assertion's riskMeasurement() method to update their risk, and return the + * largest of these computed risks. This method will throw a RuntimeException if called + * and the IRVComparisonAudit's assertions list is null, or an unexpected error arises during + * risk computation. + * @return The largest current level of risk attached to an assertion in this audit. + */ @Override public BigDecimal riskMeasurement() { - return BigDecimal.ONE; + final String prefix = "[riskMeasurement]"; + final String contestName = getContestName(); + + if(assertions == null){ + // This should never happen, and indicates an error. + final String msg = String.format("%s IRVComparisonAudit ID %d for contest %s, null " + + "assertions list.", prefix, id(), contestName); + LOGGER.error(msg); + throw new RuntimeException(msg); + } + + BigDecimal risk = BigDecimal.ONE; + if(!assertions.isEmpty()){ + LOGGER.debug(String.format("%s IRVComparisonAudit ID %d for contest %s, computing risks.", + prefix, id(), contestName)); + try { + risk = max(assertions.stream().map(a -> a.riskMeasurement(getAuditedSampleCount())).toList()); + } + catch(Exception e){ + final String msg = String.format("%s IRVComparisonAudit ID %d for contest %s, error " + + "in assertion risk measurement: %s", prefix, id(), contestName, e.getMessage()); + LOGGER.error(msg); + throw new RuntimeException(msg); + } + } + + LOGGER.debug(String.format("%s IRVComparisonAudit ID %d for contest %s, risk %f.", prefix, + id(), contestName, risk)); + return risk; } // Does nothing - TODO. diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java index a0f2c723..e20ebaac 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java @@ -60,6 +60,11 @@ @DiscriminatorColumn(name = "assertion_type") public abstract class Assertion implements PersistentEntity { + /** + * Number of decimal places to report Assertion risk level. + */ + public final static int RISK_DECIMALS = 3; + /** * Class-wide logger. */ @@ -225,6 +230,13 @@ public Long version() { return version; } + /** + * Get the assertion's diluted margin. + */ + public BigDecimal getDilutedMargin(){ + return dilutedMargin; + } + /** * Given a number of audited samples, and the total number of overstatements experienced thus far, @@ -327,14 +339,12 @@ public Integer computeEstimatedSamplesToAudit(int auditedSampleCount) { * zero results, we check if this occurred because there was no difference in the votes * recorded on the CVR and audited ballot. If so, we return an empty OptionalInt indicating * that there is no discrepancy. Otherwise, we wrap the discrepancy in an OptionalInt and return. + * A RuntimeException will be thrown if a null 'cvr' or 'auditedCVR' is provided. * @param cvr The CVR that the machine saw. * @param auditedCVR The ACVR that the human audit board saw. - * @throws RuntimeException if a null cvr or auditedCVR record is provided. * @return an optional int that is present if there is a discrepancy and absent otherwise. */ - public OptionalInt computeDiscrepancy(final CastVoteRecord cvr, final CastVoteRecord auditedCVR) - throws RuntimeException - { + public OptionalInt computeDiscrepancy(final CastVoteRecord cvr, final CastVoteRecord auditedCVR) { final String prefix = "[computeDiscrepancy]"; if(cvr == null || auditedCVR == null){ @@ -399,12 +409,12 @@ public OptionalInt computeDiscrepancy(final CastVoteRecord cvr, final CastVoteRe * the CVR is a phantom (i.e., it is missing), the worst case score (for a CVR) of 1 will be * returned. A 1 will produce the highest discrepancy value when combined with an audited ballot * score, as the formula used is CVRScore - AuditedBallotScore. Otherwise, the assertion's - * scoring method will be applied to the vote recorded in the CVR's CVRContestInfo. + * scoring method will be applied to the vote recorded in the CVR's CVRContestInfo. A + * RuntimeException will be thrown if a null 'cvr' is provided, * @param cvr The CVR to be scored in relation to this assertion. - * @throws RuntimeException if a null cvr record is provided. * @return a value of 0, -1, or 1 representing the CVR's score in relation to this assertion. */ - private int scoreCVR(final CastVoteRecord cvr) throws RuntimeException { + private int scoreCVR(final CastVoteRecord cvr) { final String prefix = "[scoreCVR]"; if(cvr == null){ @@ -450,15 +460,15 @@ private int scoreCVR(final CastVoteRecord cvr) throws RuntimeException { * case score (for an audited ballot) of -1 will be returned. A -1 will produce the highest * discrepancy value when combined with a CVR score, as the formula used is CVRScore - * AuditedBallotScore. Otherwise, the assertion's scoring method will be applied to the vote recorded - * in the audited ballot's CVRContestInfo. + * in the audited ballot's CVRContestInfo. A RuntimeException is thrown if a null 'auditedCVR' + * is provided. * @param cvrID The ID of the CVR corresponding to this audited ballot (note: colorado-rla is * a little flaky in its assignment of IDs to audited CVR records, so it is safest * to supply this as a parameter rather than trying to access it from the record). * @param auditedCVR The audited ballot to be scored in relation to this assertion. - * @throws RuntimeException if a null auditedCVR record is provided. * @return a value of 0, -1, or 1 representing the audited ballot's score in relation to this assertion. */ - private int scoreAuditedBallot(long cvrID, final CastVoteRecord auditedCVR) throws RuntimeException{ + private int scoreAuditedBallot(long cvrID, final CastVoteRecord auditedCVR) { final String prefix = "[scoreAuditedBallot]"; if(auditedCVR == null){ @@ -508,12 +518,12 @@ private int scoreAuditedBallot(long cvrID, final CastVoteRecord auditedCVR) thro /** * For a given CVRAuditInfo capturing a discrepancy between a CVR and ACVR, check if the * discrepancy is relevant for this assertion (if it is present in its cvrDiscrepancy map). - * If so, increment the counters for its discrepancy type. - * @param the_record CVRAuditInfo representing the CVR-ACVR pair that has resulted in a discrepancy. - * @throws RuntimeException if the discrepancy type associated with this CVR-ACVR pair - * is not valid (i.e., not one of the defined types). + * If so, increment the counters for its discrepancy type. A RuntimeException will be thrown + * if the discrepancy type associated with this CVR-ACVR pair is not valid (i.e., not one of the + * defined types). + * @param the_record CVRAuditInfo representing the CVR-ACVR pair that has resulted in a discrepancy. */ - public void recordDiscrepancy(final CVRAuditInfo the_record) throws RuntimeException { + public void recordDiscrepancy(final CVRAuditInfo the_record) { final String prefix = "[recordDiscrepancy]"; if(cvrDiscrepancy.containsKey(the_record.id())){ final int theType = cvrDiscrepancy.get(the_record.id()); @@ -551,12 +561,11 @@ public void recordDiscrepancy(final CVRAuditInfo the_record) throws RuntimeExcep /** * Removes discrepancies relating to a given CVR-ACVR comparison. (This is relevant when - * ballots are 'un-audited' to be subsequently re-audited). - * + * ballots are 'un-audited' to be subsequently re-audited). A RuntimeException will be thrown + * if an invalid discrepancy type has been stored in this assertion. * @param the_record The CVRAuditInfo record that generated the discrepancy. - * @throws RuntimeException if an invalid discrepancy type has been stored in this assertion. */ - public void removeDiscrepancy(final CVRAuditInfo the_record) throws RuntimeException { + public void removeDiscrepancy(final CVRAuditInfo the_record) { final String prefix = "[removeDiscrepancy]"; // Check if this CVR-ACVR pair produced a discrepancy with respect to this assertion. // (Note the CVRAuditInfo ID is always the CVR ID). @@ -601,6 +610,38 @@ public void removeDiscrepancy(final CVRAuditInfo the_record) throws RuntimeExcep } } + /** + * Compute the current level of risk achieved for this assertion given a specified + * audited sample count and gamma value. Audit.pValueApproximation is used to compute + * this risk using the diluted margin of the assertion, and the recorded number of + * one/two vote understatements and one/two vote overstatements. The assertions 'currentRisk' + * attribute is updated, and the computed risk returned. + * @param auditedSampleCount Number of ballots audited. + * @return Level of risk achieved for this assertion. + */ + public BigDecimal riskMeasurement(int auditedSampleCount){ + final String prefix = "[riskMeasurement]"; + if (auditedSampleCount > 0 && dilutedMargin.doubleValue() > 0) { + LOGGER.debug(String.format("%s Assertion ID %d, contest %s, calling " + + "Audit.pValueApproximation() with parameters: audited sample count %d; diluted margin " + + "%f; gamma %f; one vote under count %d; two vote under count %d; one vote over count " + + "%d; two vote over count %d.", prefix, id, contestName, auditedSampleCount, dilutedMargin, + Audit.GAMMA, oneVoteUnderCount, twoVoteUnderCount, oneVoteOverCount, twoVoteOverCount)); + + currentRisk = Audit.pValueApproximation(auditedSampleCount, dilutedMargin, Audit.GAMMA, + oneVoteUnderCount, twoVoteUnderCount, oneVoteOverCount, twoVoteOverCount).setScale( + RISK_DECIMALS, RoundingMode.HALF_UP); + } else { + // Full risk (100%) when nothing is known + currentRisk = BigDecimal.ONE; + } + + LOGGER.debug(String.format("%s Assertion ID %d, contest %s, risk %f.", prefix, id, + contestName, currentRisk)); + return currentRisk; + } + + /** * Computes the Score for the given vote in the context of this assertion. For details on how * votes are scored for assertions, refer to the Guide to RAIRE (Part 2, Appendix A, Table A.1.). diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/query/AssertionQueries.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/query/AssertionQueries.java new file mode 100644 index 00000000..a438d478 --- /dev/null +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/query/AssertionQueries.java @@ -0,0 +1,84 @@ +/* +Democracy Developers IRV extensions to colorado-rla. + +@copyright 2024 Colorado Department of State + +These IRV extensions are designed to connect to a running instance of the raire +service (https://github.com/DemocracyDevelopers/raire-service), in order to +generate assertions that can be audited using colorado-rla. + +The colorado-rla IRV extensions are free software: you can redistribute it and/or modify it under the terms +of the GNU Affero General Public License as published by the Free Software Foundation, either +version 3 of the License, or (at your option) any later version. + +The colorado-rla IRV extensions are distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with +raire-service. If not, see . +*/ + +package au.org.democracydevelopers.corla.query; + +import java.util.ArrayList; +import java.util.List; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; + +import org.hibernate.Session; +import au.org.democracydevelopers.corla.model.assertion.Assertion; +import us.freeandfair.corla.persistence.Persistence; + +import javax.persistence.TypedQuery; + +/** + * This class contains database queries relating to the retrieval of Assertions from the + * database. It contains a method that executes a query to retrieval all Assertions belonging + * to a specific contest, identified by name. + */ +public class AssertionQueries { + + /** + * Class-wide logger. + */ + public static final Logger LOGGER = LogManager.getLogger(AssertionQueries.class); + + /** + * Retrieve all assertions in the database belonging to the contest with the given name. + * + * @param contestName The contest name. + * @return the list of assertions defined for the contest. + * @throws RuntimeException when an unexpected error arose in assertion retrieval (not including + * a NoResultException, which is handled by returning an empty list). + */ + public static List matching(final String contestName) throws RuntimeException { + final String prefix = "[matching]"; + try { + LOGGER.debug(String.format("%s Select query on assertion table, retrieving all " + + "assertions for contest with name %s.", prefix, contestName)); + + final Session s = Persistence.currentSession(); + final TypedQuery q = s.createQuery("select ca from Assertion ca " + + " where ca.contestName = :contestName", Assertion.class); + q.setParameter("contestName", contestName); + + List result = q.getResultList(); + LOGGER.debug(String.format("%s %d assertions retrieved for contest %s.", prefix, + result.size(), contestName)); + return result; + + } catch (javax.persistence.NoResultException e) { + final String msg = String.format("%s No assertions retrieved for contest %s.", prefix, + contestName); + LOGGER.warn(msg); + return new ArrayList<>(); + } + catch(Exception e){ + final String msg = String.format("%s An error arose when attempting to retrieve assertions " + + "for contest %s: %s", prefix, contestName, e.getMessage()); + LOGGER.error(msg); + throw new RuntimeException(msg); + } + } +} diff --git a/server/eclipse-project/src/main/java/us/freeandfair/corla/model/ComparisonAudit.java b/server/eclipse-project/src/main/java/us/freeandfair/corla/model/ComparisonAudit.java index 39590866..049d0249 100644 --- a/server/eclipse-project/src/main/java/us/freeandfair/corla/model/ComparisonAudit.java +++ b/server/eclipse-project/src/main/java/us/freeandfair/corla/model/ComparisonAudit.java @@ -129,11 +129,12 @@ public class ComparisonAudit implements PersistentEntity { private BigDecimal my_gamma = Audit.GAMMA; /** - * The diluted margin + * The diluted margin. [Democracy Developers: this must be accessible and modifiable by + * child classes of ComparisonAudit, hence we have made this attribute protected.] */ @Column(updatable = false, nullable = false, precision = PRECISION, scale = SCALE) - private BigDecimal diluted_margin = BigDecimal.ONE; + protected BigDecimal diluted_margin = BigDecimal.ONE; /** * The risk limit. @@ -150,16 +151,20 @@ public class ComparisonAudit implements PersistentEntity { /** * The number of samples to audit overall assuming no further overstatements. + * [Democracy Developers: this must be accessible and modifiable by child classes of + * ComparisonAudit, hence we have made this attribute protected.] */ @Column(nullable = false) - private Integer my_optimistic_samples_to_audit = 0; + protected Integer my_optimistic_samples_to_audit = 0; /** * The expected number of samples to audit overall assuming overstatements * continue at the current rate. + * [Democracy Developers: this must be accessible and modifiable by child classes of + * ComparisonAudit, hence we have made this attribute protected.] */ @Column(nullable = false) - private Integer my_estimated_samples_to_audit = 0; + protected Integer my_estimated_samples_to_audit = 0; /** * The number of two-vote understatements recorded so far. @@ -207,16 +212,20 @@ public class ComparisonAudit implements PersistentEntity { /** * A flag that indicates whether the optimistic ballots to audit * estimate needs to be recalculated. + * [Democracy Developers: this must be accessible and modifiable by child classes of + * ComparisonAudit, hence we have made this attribute protected.] */ @Column(nullable = false) - private Boolean my_optimistic_recalculate_needed = true; + protected Boolean my_optimistic_recalculate_needed = true; /** * A flag that indicates whether the non-optimistic ballots to - * audit estimate needs to be recalculated + * audit estimate needs to be recalculated. + * [Democracy Developers: this must be accessible and modifiable by child classes of + * ComparisonAudit, hence we have made this attribute protected.] */ @Column(nullable = false) - private Boolean my_estimated_recalculate_needed = true; + protected Boolean my_estimated_recalculate_needed = true; /** * The sequence of CastVoteRecord ids for this contest ordered by County id From c4ac291adc1b0a0d4e32fe1ba4901b6112dbc05a Mon Sep 17 00:00:00 2001 From: michelleblom Date: Thu, 20 Jun 2024 15:09:09 +1000 Subject: [PATCH 02/12] Fleshed out IRVComparisonAudit methods, adapted Assertion classes where necessary, and updated the Assertion tests. Still need extra tests for new Assertion functionality and the functionality in IRVComparisonAudit. --- .../corla/model/IRVComparisonAudit.java | 194 ++++++++++++++++-- .../corla/model/assertion/Assertion.java | 73 ++++--- .../corla/model/ComparisonAudit.java | 8 +- .../corla/persistence/entity_classes | 1 + .../model/assertion/NEBAssertionTests.java | 116 ++++++++--- .../model/assertion/NENAssertionTests.java | 110 +++++++--- 6 files changed, 395 insertions(+), 107 deletions(-) diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java index 35362aa3..4a55d98a 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java @@ -42,6 +42,15 @@ /** * A class representing the state of a single audited IRV contest across one or more counties. + * When an IRVComparisonAudit is constructed, the base ComparisonAudit constructor is called. + * This will invoke methods to compute initial sample sizes for the audit. For IRV audits, + * however, these initial sample sizes will not be meaningful as the audit's assertions are + * not loaded until the ComparisonAudit base constructor finished. The IRVComparisonAudit + * constructor will load the relevant assertions from the database (populating the audit's + * assertions list), compute the audit's diluted margin, and then reinvoke the sample size + * computation methods. This class overrides some of the methods in the base ComparisonAudit + * class: recalculateSamplesToAudit(); initialSamplesToAudit(); computeDiscrepancy(); + * riskMeasurement(); removeDiscrepancy(); and recordDiscrepancy(). */ @Entity @DiscriminatorValue("IRV") @@ -227,7 +236,9 @@ protected void recalculateSamplesToAudit() { public int initialSamplesToAudit() { if(assertions == null){ // We have not associated assertions with this audit yet. - // In this case, we simply ignore the call. + // In this case, we simply ignore the call. Whether this is an error depends on whether + // this method is invoked in the constructor of the ComparisonAudit class. At this point, + // the IRVComparisonAudit's assertions list will not have been initialised. return 0; } @@ -303,13 +314,9 @@ public OptionalInt computeDiscrepancy(final CastVoteRecord cvr, final CastVoteRe LOGGER.debug(String.format("%s Computing discrepancy between CVR ID %d and audited ballot, " + "contest %s.", prefix, cvr.id(), contestName)); - if(assertions == null){ - // This should not happen; if it does, something has gone wrong. - final String msg = String.format("%s IRVComparison audit for contest %s has not set its " + - "assertions list yet (it is null).", prefix, contestName); - LOGGER.error(msg); - throw new RuntimeException(msg); - } + // Check that the IRVComparisonAudit's assertions have been initialised. This will throw + // a RuntimeException if they have not, and log an appropriate error message. + nullAssertionsCheck(prefix); LOGGER.debug(String.format("%s Contest %s: Calling computeDiscrepancy() for each assertion.", prefix, contestName)); @@ -367,13 +374,9 @@ public BigDecimal riskMeasurement() { final String prefix = "[riskMeasurement]"; final String contestName = getContestName(); - if(assertions == null){ - // This should never happen, and indicates an error. - final String msg = String.format("%s IRVComparisonAudit ID %d for contest %s, null " + - "assertions list.", prefix, id(), contestName); - LOGGER.error(msg); - throw new RuntimeException(msg); - } + // Check that the IRVComparisonAudit's assertions have been initialised. This will throw + // a RuntimeException if they have not. + nullAssertionsCheck(prefix); BigDecimal risk = BigDecimal.ONE; if(!assertions.isEmpty()){ @@ -395,13 +398,166 @@ public BigDecimal riskMeasurement() { return risk; } - // Does nothing - TODO. + /** + * Removes the specified over/understatement or other discrepancy (the valid range is -2 to 2: + * -2 and -1 are understatements, 0 is a discrepancy that doesn't affect the RLA calculations, + * and 1 and 2 are overstatements) corresponding to the given audited ballot. This is typically + * done when a new interpretation is submitted for a ballot that has already been interpreted + * (i,e. a ballot is being re-audited). Note that a given CVR/audited ballot can be associated + * with different types of discrepancies across the audit's set of assertions. In the overall + * discrepancy counts in the ComparisonAudit base class, the maximum of these per-assertion + * discrepancies will be used when deciding what type counter to increment/decrement. + * + * @param theRecord The CVRAuditInfo record that generated the discrepancy. + * @param theType The type of discrepancy to remove. + * @exception IllegalArgumentException if an invalid discrepancy type is specified, or a null + * CVRAuditInfo record is provided. + */ @Override - public void removeDiscrepancy(final CVRAuditInfo the_record, final int the_type) { + public void removeDiscrepancy(final CVRAuditInfo theRecord, final int theType) { + final String contestName = getContestName(); + final String prefix = String.format("[riskMeasurement] IRVComparisonAudit ID %d for " + + "contest %s:", id(), contestName); + + // Check that the IRVComparisonAudit's assertions have been initialised. This will throw + // a RuntimeException if they have not. + nullAssertionsCheck(prefix); + + // Check that the CVRAuditInfo and discrepancy type are valid. + recordTypeCheck(prefix, theRecord, theType); + + try { + LOGGER.debug( + String.format("%s removing discrepancy associated with CVR %d (maximum type %d).", + prefix, theRecord.cvr().id(), theType)); + // Remove the discrepancy associated with the given CVRAuditInfo (CVR/ACVR pair), if one + // exists, in each of the audit's assertions. + boolean removed = false; + for (Assertion a : assertions) { + removed = removed || a.removeDiscrepancy(theRecord); + } + + // Update the discrepancy tallies in the base ComparisonAudit class, for reporting purposes, + // and the flag for indicating that a sample size recalculation is needed, *if* we did + // indeed remove a discrepancy from at least one of this audit's assertions. + if(removed) { + super.removeDiscrepancy(theRecord, theType); + } + LOGGER.debug(String.format("%s total number of overstatements (%f), optimistic sample " + + "recalculate needed (%s), estimated sample recalculate needed (%s),", prefix, + getOverstatements(), my_optimistic_recalculate_needed, my_estimated_recalculate_needed)); + + } catch(Exception e){ + final String msg = String.format("%s an error arose in the removal of discrepancies " + + "associated with CVR %d (maximum type %d).", prefix, theRecord.cvr().id(), theType); + LOGGER.error(msg); + throw new RuntimeException(msg); + } } - // Does nothing - TODO. + /** + * Checks if the given CVRAuditInfo contains a discrepancy with respect to one or more of this + * audit's assertions. If so, it removes the discrepancy from the internal records of the + * assertion. The CVRAuditInfo details the ID of the CVR involved in the discrepancy. The + * given discrepancy type (an integer between -2 and 2, inclusive) represents the value of + * the maximum discrepancy associated with the CVRAuditInfo and one of this audit's assertions. + * The count for this discrepancy type (theType) is incremented in the base ComparisonAudit's + * totals. + * + * @param theRecord The CVRAuditInfo record that generated the discrepancy. + * @param theType The type of discrepancy to remove. + * @exception IllegalArgumentException if an invalid discrepancy type is specified, or a null + * CVRAuditInfo record is provided. + */ @Override - public void recordDiscrepancy(final CVRAuditInfo the_record, final int the_type) { + public void recordDiscrepancy(final CVRAuditInfo theRecord, final int theType) { + final String contestName = getContestName(); + final String prefix = String.format("[recordDiscrepancy] IRVComparisonAudit ID %d for " + + "contest %s:", id(), contestName); + + // Check that the IRVComparisonAudit's assertions have been initialised. This will throw + // a RuntimeException if they have not. + nullAssertionsCheck(prefix); + + // Check that the CVRAuditInfo and discrepancy type are valid. This method will throw an + // IllegalArgumentException if they are not. + recordTypeCheck(prefix, theRecord, theType); + + try { + // Iterate over the assertions for this audit, check that the CVR in the CVRAuditInfo is + // listed in their discrepancy map, and if so, record it as a discrepancy in its internal + // totals. Note that the CVR may represent a different type of discrepancy from assertion to + // assertion (not necessarily of type 'theType'). The parameter 'theType' will represent the + // maximum discrepancy associated with CVR/ACVR pair and one of this audit's assertions. Note + // that a given CVR/ACVR pair can only be associated with a single discrepancy per assertion. + boolean recorded = false; + for (Assertion a : assertions) { + recorded = recorded || a.recordDiscrepancy(theRecord); + } + + // Update the discrepancy tallies in the base ComparisonAudit class, for reporting purposes, + // and the flag for indicating that a sample size recalculation is needed, *if* we did + // record a discrepancy against at least one of this audit's assertions. + if(recorded) { + super.removeDiscrepancy(theRecord, theType); + } + LOGGER.debug(String.format("%s total number of overstatements (%f), optimistic sample " + + "recalculate needed (%s), estimated sample recalculate needed (%s),", prefix, + getOverstatements(), my_optimistic_recalculate_needed, my_estimated_recalculate_needed)); + + } catch(Exception e){ + final String msg = String.format("%s an error arose in the recording of discrepancies " + + "associated with CVR %d (maximum type %d).", prefix, theRecord.cvr().id(), theType); + LOGGER.error(msg); + throw new RuntimeException(msg); + } + } + + /** + * This method checks whether this IRVComparisonAudit's list of assertions has been + * appropriately initialised, and throws a RunTimeException if not. It takes a string identifying + * the method making this check, to use in logging an appropriate error message. + * @param prefix String identifying the IRVComparisonAudit method that is performing this check. + */ + private void nullAssertionsCheck(final String prefix){ + if(assertions == null){ + final String msg = String.format("%s IRVComparisonAudit ID %d for contest %s, null " + + "assertions list.", prefix, id(), getContestName()); + LOGGER.error(msg); + throw new RuntimeException(msg); + } + } + + /** + * Checks that the given CVRAuditInfo and discrepancy type are valid: that the CVRAuditInfo + * record is not null (and its CVR is not null); and that the discrepancy type falls in the + * range -2 to 2. An IllegalArgumentException is logged and thrown otherwise. + * @param prefix Information (for logging) detailing the method calling this check. + * @param theRecord The CVRAuditInfo being checked. + * @param theType The discrepancy type being checked. + * @throws IllegalArgumentException when the given CVRAuditInfo and discrepancy types are not + * valid. + */ + private void recordTypeCheck(final String prefix, final CVRAuditInfo theRecord, final int theType){ + if(theRecord == null){ + // This is an error, and should not happen. + final String msg = String.format("%s null CVRAuditInfo provided.", prefix); + LOGGER.error(msg); + throw new IllegalArgumentException(msg); + } + + if(theRecord.cvr() == null){ + // This is an error, and should not happen. + final String msg = String.format("%s null CVR in CVRAuditInfo provided.", prefix); + LOGGER.error(msg); + throw new IllegalArgumentException(msg); + } + + if(theType < -2 || theType > 2){ + // This is an error, and should not happen. + final String msg = String.format("%s invalid discrepancy type provided of %d.", prefix, theType); + LOGGER.error(msg); + throw new IllegalArgumentException(msg); + } } } diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java index e20ebaac..3be914f7 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java @@ -521,12 +521,19 @@ private int scoreAuditedBallot(long cvrID, final CastVoteRecord auditedCVR) { * If so, increment the counters for its discrepancy type. A RuntimeException will be thrown * if the discrepancy type associated with this CVR-ACVR pair is not valid (i.e., not one of the * defined types). - * @param the_record CVRAuditInfo representing the CVR-ACVR pair that has resulted in a discrepancy. + * @param theRecord CVRAuditInfo representing the CVR-ACVR pair that has resulted in a discrepancy. + * @return a boolean indicating if a discrepancy associated with the given CVRAuditInfo was + * recorded against the totals of at least one of this audit's assertions. */ - public void recordDiscrepancy(final CVRAuditInfo the_record) { + public boolean recordDiscrepancy(final CVRAuditInfo theRecord) { final String prefix = "[recordDiscrepancy]"; - if(cvrDiscrepancy.containsKey(the_record.id())){ - final int theType = cvrDiscrepancy.get(the_record.id()); + + // Flag which will be set to true if we do increase the assertion's internal discrepancy + // counts. + boolean recorded = false; + + if(cvrDiscrepancy.containsKey(theRecord.id())){ + final int theType = cvrDiscrepancy.get(theRecord.id()); switch (theType) { case -2 -> twoVoteUnderCount += 1; case -1 -> oneVoteUnderCount += 1; @@ -544,33 +551,42 @@ public void recordDiscrepancy(final CVRAuditInfo the_record) { LOGGER.debug(String.format("%s Discrepancy of type %d added to Assertion ID %d,"+ "contest %s, CVR ID %d. New totals: 1 vote understatements %d; 1 vote overstatements %d; " + "2 vote understatements %d; 2 vote overstatements %d; other %d.", prefix, theType, id, - contestName, the_record.id(), oneVoteUnderCount, oneVoteOverCount, twoVoteUnderCount, + contestName, theRecord.id(), oneVoteUnderCount, oneVoteOverCount, twoVoteUnderCount, twoVoteOverCount, otherCount)); + + recorded = true; } else{ - final String msg = String.format("%s Attempt to record a discrepancy in Assertion ID %d " + - "contest %s, CVR ID %s, but no record of a pre-computed discrepancy associated with " + - "that CVR exists. No increase to discrepancy totals: 1 vote understatements %d; " + - "1 vote overstatements %d; 2 vote understatements %d; 2 vote overstatements %d; other %d.", - prefix, id, contestName, the_record.id(), oneVoteUnderCount, oneVoteOverCount, - twoVoteUnderCount, twoVoteOverCount, otherCount); - LOGGER.error(msg); - throw new RuntimeException(msg); + final String msg = String.format("%s Assertion ID %d contest %s, CVR ID %s: no record of " + + " a pre-computed discrepancy associated with that CVR. No increase to assertion discrepancy " + + "totals: 1 vote understatements %d; 1 vote overstatements %d; 2 vote understatements %d; " + + "2 vote overstatements %d; other %d.", prefix, id, contestName, theRecord.id(), + oneVoteUnderCount, oneVoteOverCount, twoVoteUnderCount, twoVoteOverCount, otherCount); + LOGGER.debug(msg); } + + return recorded; } /** * Removes discrepancies relating to a given CVR-ACVR comparison. (This is relevant when * ballots are 'un-audited' to be subsequently re-audited). A RuntimeException will be thrown * if an invalid discrepancy type has been stored in this assertion. - * @param the_record The CVRAuditInfo record that generated the discrepancy. + * @param theRecord The CVRAuditInfo record that generated the discrepancy. + * @return a boolean indicating if a discrepancy associated with the given CVRAuditInfo was + * removed from this assertion's totals. */ - public void removeDiscrepancy(final CVRAuditInfo the_record) { + public boolean removeDiscrepancy(final CVRAuditInfo theRecord) { final String prefix = "[removeDiscrepancy]"; + + // Flag which will be set to true if we do remove a discrepancy from the assertions + // internal totals. + boolean removed = false; + // Check if this CVR-ACVR pair produced a discrepancy with respect to this assertion. // (Note the CVRAuditInfo ID is always the CVR ID). - if(cvrDiscrepancy.containsKey(the_record.id())){ - final int theType = cvrDiscrepancy.get(the_record.id()); + if(cvrDiscrepancy.containsKey(theRecord.id())){ + final int theType = cvrDiscrepancy.get(theRecord.id()); switch (theType) { case -2 -> twoVoteUnderCount -= 1; case -1 -> oneVoteUnderCount -= 1; @@ -584,30 +600,31 @@ public void removeDiscrepancy(final CVRAuditInfo the_record) { throw new RuntimeException(msg); } } - cvrDiscrepancy.remove(the_record.id()); + cvrDiscrepancy.remove(theRecord.id()); LOGGER.debug(String.format("%s Discrepancy of type %d removed from Assertion ID %d,"+ "contest %s, CVR ID %d. New totals: 1 vote understatements %d; 1 vote overstatements %d; " + "2 vote understatements %d; 2 vote overstatements %d; other %d.", prefix, theType, id, - contestName, the_record.id(), oneVoteUnderCount, oneVoteOverCount, twoVoteUnderCount, + contestName, theRecord.id(), oneVoteUnderCount, oneVoteOverCount, twoVoteUnderCount, twoVoteOverCount, otherCount)); + removed = true; } else{ - final String msg = String.format("%s Attempt to remove a discrepancy in Assertion ID %d " + - "contest %s, CVR ID %s, but no record of a pre-computed discrepancy associated with " + - "that CVR exists. No increase to discrepancy totals: 1 vote understatements %d; " + - "1 vote overstatements %d; 2 vote understatements %d; 2 vote overstatements %d; other %d.", - prefix, id, contestName, the_record.id(), oneVoteUnderCount, oneVoteOverCount, - twoVoteUnderCount, twoVoteOverCount, otherCount); - LOGGER.error(msg); - throw new RuntimeException(msg); + final String msg = String.format("%s Assertion ID %d contest %s, CVR ID %s: no record of a " + + "pre-computed discrepancy associated with that CVR. No change to discrepancy totals: " + + "1 vote understatements %d; 1 vote overstatements %d; 2 vote understatements %d; 2 vote " + + "overstatements %d; other %d.", prefix, id, contestName, theRecord.id(), oneVoteUnderCount, + oneVoteOverCount, twoVoteUnderCount, twoVoteOverCount, otherCount); + LOGGER.debug(msg); } if(min(List.of(oneVoteOverCount, oneVoteUnderCount, twoVoteUnderCount, otherCount)) < 0) { final String msg = String.format("%s Negative discrepancy counts in Assertion ID %d, " + - "contest %s when removing discrepancy for CVR %d.", prefix, id, contestName, the_record.id()); + "contest %s when removing discrepancy for CVR %d.", prefix, id, contestName, theRecord.id()); LOGGER.error(msg); throw new RuntimeException(msg); } + + return removed; } /** diff --git a/server/eclipse-project/src/main/java/us/freeandfair/corla/model/ComparisonAudit.java b/server/eclipse-project/src/main/java/us/freeandfair/corla/model/ComparisonAudit.java index 049d0249..d5801885 100644 --- a/server/eclipse-project/src/main/java/us/freeandfair/corla/model/ComparisonAudit.java +++ b/server/eclipse-project/src/main/java/us/freeandfair/corla/model/ComparisonAudit.java @@ -30,6 +30,8 @@ import javax.persistence.CollectionTable; import javax.persistence.Column; import javax.persistence.Convert; +import javax.persistence.DiscriminatorColumn; +import javax.persistence.DiscriminatorValue; import javax.persistence.ElementCollection; import javax.persistence.Entity; import javax.persistence.EnumType; @@ -38,6 +40,8 @@ import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; +import javax.persistence.Inheritance; +import javax.persistence.InheritanceType; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; @@ -63,7 +67,9 @@ @Entity @Cacheable(true) @Table(name = "comparison_audit") - +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn(name = "audit_type") +@DiscriminatorValue("PLURALITY") @SuppressWarnings({"PMD.ImmutableField", "PMD.ExcessiveClassLength", "PMD.CyclomaticComplexity", "PMD.GodClass", "PMD.ModifiedCyclomaticComplexity", "PMD.StdCyclomaticComplexity", "PMD.TooManyFields", "PMD.TooManyMethods", diff --git a/server/eclipse-project/src/main/resources/us/freeandfair/corla/persistence/entity_classes b/server/eclipse-project/src/main/resources/us/freeandfair/corla/persistence/entity_classes index 9559f58c..d84c4842 100644 --- a/server/eclipse-project/src/main/resources/us/freeandfair/corla/persistence/entity_classes +++ b/server/eclipse-project/src/main/resources/us/freeandfair/corla/persistence/entity_classes @@ -23,3 +23,4 @@ us.freeandfair.corla.model.UploadedFile au.org.democracydevelopers.corla.model.assertion.Assertion au.org.democracydevelopers.corla.model.assertion.NEBAssertion au.org.democracydevelopers.corla.model.assertion.NENAssertion +au.org.democracydevelopers.corla.model.IRVComparisonAudit diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java index 18291a63..eb3c811d 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java @@ -27,6 +27,8 @@ import static au.org.democracydevelopers.corla.util.testUtils.log; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertTrue; import java.math.BigDecimal; import java.util.List; @@ -214,9 +216,9 @@ public void testNEBEstimatedVaryingSamples(Integer auditedSamples, BigDecimal ri * discrepancies. It should not change the assertion's discrepancy counts. What * recordDiscrepancy() does is look for the CVRAuditInfo's ID in its cvrDiscrepancy map. If it is * there, the value matching the ID key is retrieved and the associated discrepancy type - * incremented. If it is not there, then a runtime exception is thrown. + * incremented. If it is not there, then the method will return 'false'. */ - @Test(expectedExceptions = {RuntimeException.class}) + @Test public void testNEBRecordNoMatch1(){ log(LOGGER, "testNEBRecordNoMatch1"); CVRAuditInfo info = new CVRAuditInfo(); @@ -225,15 +227,23 @@ public void testNEBRecordNoMatch1(){ Assertion a = createNEBAssertion("W", "L", TC, 50, 0.1, 8, Map.of(), 0, 0, 0, 0, 0); - a.recordDiscrepancy(info); + assertFalse(a.recordDiscrepancy(info)); + + assertEquals(0, a.oneVoteOverCount.intValue()); + assertEquals(0, a.oneVoteUnderCount.intValue()); + assertEquals(0, a.twoVoteOverCount.intValue()); + assertEquals(0, a.twoVoteUnderCount.intValue()); + assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(), a.cvrDiscrepancy); } /** * Test Assertion::recordDiscrepancy(CVRAuditInfo) in the context where no discrepancy has * been computed for the given CVR-ACVR pair, and the assertion has some discrepancies recorded - * already. A runtime exception should be thrown. + * already. The method should return false. */ - @Test(expectedExceptions = {RuntimeException.class}) + @Test public void testNEBRecordNoMatch2(){ log(LOGGER, "testNEBRecordNoMatch2"); CVRAuditInfo info = new CVRAuditInfo(); @@ -243,7 +253,15 @@ public void testNEBRecordNoMatch2(){ 8, Map.of(2L, -1, 3L, 1), 1, 1, 0, 0, 0); - a.recordDiscrepancy(info); + assertFalse(a.recordDiscrepancy(info)); + + assertEquals(1, a.oneVoteOverCount.intValue()); + assertEquals(1, a.oneVoteUnderCount.intValue()); + assertEquals(0, a.twoVoteOverCount.intValue()); + assertEquals(0, a.twoVoteUnderCount.intValue()); + assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(2L, -1, 3L, 1), a.cvrDiscrepancy); } /** @@ -259,13 +277,15 @@ public void testNEBRecordOneVoteOverstatement1(){ 8, Map.of(1L, 1), 0, 0, 0, 0, 0); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 1), a.cvrDiscrepancy); } /** @@ -281,13 +301,15 @@ public void testNEBRecordOneVoteUnderstatement1(){ 8, Map.of(1L, -1), 0, 0, 0, 0, 0); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(1, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(1L, -1), a.cvrDiscrepancy); } /** @@ -303,13 +325,15 @@ public void testNEBRecordTwoVoteOverstatement1(){ 8, Map.of(1L, 2), 0, 0, 0, 0, 0); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(1, a.twoVoteOverCount.intValue()); assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 2), a.cvrDiscrepancy); } /** @@ -325,13 +349,15 @@ public void testNEBRecordTwoVoteUnderstatement1(){ 8, Map.of(1L, -2), 0, 0, 0, 0, 0); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(1L, -2), a.cvrDiscrepancy); } /** @@ -347,13 +373,15 @@ public void testNEBRecordOther1(){ 8, Map.of(1L, 0), 0, 0, 0, 0, 0); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 0), a.cvrDiscrepancy); } /** @@ -370,13 +398,15 @@ public void testNEBRecordOneVoteOverstatement2(){ 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1), 1, 0, 0, 1, 1); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(2, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1), a.cvrDiscrepancy); } /** @@ -393,13 +423,15 @@ public void testNEBRecordOneVoteUnderstatement2(){ 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -1), 1, 0, 0, 1, 1); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(1, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -1), a.cvrDiscrepancy); } /** @@ -416,13 +448,15 @@ public void testNEBRecordTwoVoteOverstatement2(){ 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 2), 1, 0, 0, 1, 1); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(1, a.twoVoteOverCount.intValue()); assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 2), a.cvrDiscrepancy); } /** @@ -439,13 +473,15 @@ public void testNEBRecordTwoVoteUnderstatement2(){ 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2), 1, 0, 0, 1, 1); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(2, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2), a.cvrDiscrepancy); } /** @@ -462,13 +498,15 @@ public void testNEBRecordOther2(){ 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0), 1, 0, 0, 1, 1); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(2, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0), a.cvrDiscrepancy); } /** @@ -478,9 +516,9 @@ public void testNEBRecordOther2(){ * removeDiscrepancy() does is look for the CVRAuditInfo's ID in its cvrDiscrepancy map. If it is * there, the value matching the ID key is retrieved, the associated discrepancy type * decremented, and the ID removed from the map. If it is not there, then the discrepancy counts - * and the map are not changed. + * and the map are not changed, and the method returns false. */ - @Test(expectedExceptions = {RuntimeException.class}) + @Test public void testNEBRemoveNoMatch1(){ log(LOGGER, "testNEBRemoveNoMatch1"); CVRAuditInfo info = new CVRAuditInfo(); @@ -489,7 +527,15 @@ public void testNEBRemoveNoMatch1(){ Assertion a = createNEBAssertion("W", "L", TC, 50, 0.1, 8, Map.of(), 0, 0, 0, 0, 0); - a.removeDiscrepancy(info); + assertFalse(a.removeDiscrepancy(info)); + + assertEquals(0, a.oneVoteOverCount.intValue()); + assertEquals(0, a.oneVoteUnderCount.intValue()); + assertEquals(0, a.twoVoteOverCount.intValue()); + assertEquals(0, a.twoVoteUnderCount.intValue()); + assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(), a.cvrDiscrepancy); } /** @@ -497,7 +543,7 @@ public void testNEBRemoveNoMatch1(){ * been computed for the given CVR-ACVR pair, and the assertion has some discrepancies recorded * already. It should not change the assertion's discrepancy counts. */ - @Test(expectedExceptions = {RuntimeException.class}) + @Test public void testNEBRemoveNoMatch2(){ log(LOGGER, "testNEBRemoveNoMatch2"); CVRAuditInfo info = new CVRAuditInfo(); @@ -507,7 +553,15 @@ public void testNEBRemoveNoMatch2(){ 8, Map.of(2L, -1, 3L, 1), 1, 1, 0, 0, 0); - a.removeDiscrepancy(info); + assertFalse(a.removeDiscrepancy(info)); + + assertEquals(1, a.oneVoteOverCount.intValue()); + assertEquals(1, a.oneVoteUnderCount.intValue()); + assertEquals(0, a.twoVoteOverCount.intValue()); + assertEquals(0, a.twoVoteUnderCount.intValue()); + assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(2L, -1, 3L, 1), a.cvrDiscrepancy); } /** @@ -523,7 +577,7 @@ public void testNEBRemoveOneVoteOverstatement1(){ 8, Map.of(1L, 1), 1, 0, 0, 0, 0); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -547,7 +601,7 @@ public void testNEBRemoveOneVoteUnderstatement1(){ 8, Map.of(1L, -1), 0, 1, 0, 0, 0); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -571,7 +625,7 @@ public void testNEBRemoveTwoVoteOverstatement1(){ 8, Map.of(1L, 2), 0, 0, 1, 0, 0); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -595,7 +649,7 @@ public void testNEBRemoveTwoVoteUnderstatement1(){ 8, Map.of(1L, -2), 0, 0, 0, 1, 0); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -619,7 +673,7 @@ public void testNEBRemoveOther1(){ 8, Map.of(1L, 0), 0, 0, 0, 0, 1); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -644,7 +698,7 @@ public void testNEBRemoveOneVoteOverstatement2(){ 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1), 2, 0, 0, 1, 1); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -669,7 +723,7 @@ public void testNEBRemoveOneVoteUnderstatement2(){ 8, Map.of(1L, 0, 2L, 1, 3L, -1, 4L, -1), 1, 2, 0, 0, 1); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(1, a.oneVoteUnderCount.intValue()); @@ -694,7 +748,7 @@ public void testNEBRemoveTwoVoteOverstatement2(){ 8, Map.of(1L, 2, 2L, 1, 3L, -2, 4L, 2), 1, 0, 2, 1, 0); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -719,7 +773,7 @@ public void testNEBRemoveTwoVoteUnderstatement2(){ 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2), 1, 0, 0, 2, 1); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -744,7 +798,7 @@ public void testNEBRemoveOther2(){ 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0), 1, 0, 0, 1, 2); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java index f433c2de..ce36ce60 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java @@ -27,6 +27,8 @@ import static au.org.democracydevelopers.corla.util.testUtils.log; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; +import static org.testng.AssertJUnit.assertFalse; +import static org.testng.AssertJUnit.assertTrue; import java.math.BigDecimal; import java.util.List; @@ -238,7 +240,7 @@ public void testNENEstimatedVaryingSamples(Integer auditedSamples, BigDecimal ri * there, the value matching the ID key is retrieved and the associated discrepancy type * incremented. If it is not there, then the discrepancy counts are not changed. */ - @Test(expectedExceptions = {RuntimeException.class}) + @Test public void testNENRecordNoMatch1(){ log(LOGGER, "testNENRecordNoMatch1"); CVRAuditInfo info = new CVRAuditInfo(); @@ -248,7 +250,15 @@ public void testNENRecordNoMatch1(){ 0.1, 8, Map.of(), 0, 0, 0, 0, 0); - a.recordDiscrepancy(info); + assertFalse(a.recordDiscrepancy(info)); + + assertEquals(0, a.oneVoteOverCount.intValue()); + assertEquals(0, a.oneVoteUnderCount.intValue()); + assertEquals(0, a.twoVoteOverCount.intValue()); + assertEquals(0, a.twoVoteUnderCount.intValue()); + assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(), a.cvrDiscrepancy); } /** @@ -256,7 +266,7 @@ public void testNENRecordNoMatch1(){ * been computed for the given CVR-ACVR pair, and the assertion has some discrepancies recorded * already. It should not change the assertion's discrepancy counts. */ - @Test(expectedExceptions = {RuntimeException.class}) + @Test public void testNENRecordNoMatch2(){ log(LOGGER, "testNENRecordNoMatch2"); CVRAuditInfo info = new CVRAuditInfo(); @@ -266,7 +276,15 @@ public void testNENRecordNoMatch2(){ 0.1, 8, Map.of(2L, -1, 3L, 1), 1, 1, 0, 0, 0); - a.recordDiscrepancy(info); + assertFalse(a.recordDiscrepancy(info)); + + assertEquals(1, a.oneVoteOverCount.intValue()); + assertEquals(1, a.oneVoteUnderCount.intValue()); + assertEquals(0, a.twoVoteOverCount.intValue()); + assertEquals(0, a.twoVoteUnderCount.intValue()); + assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(2L, -1, 3L, 1), a.cvrDiscrepancy); } /** @@ -283,13 +301,15 @@ public void testNENRecordOneVoteOverstatement1(){ 0.1, 8, Map.of(1L, 1), 0, 0, 0, 0, 0); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 1), a.cvrDiscrepancy); } /** @@ -306,13 +326,15 @@ public void testNENRecordOneVoteUnderstatement1(){ 0.1, 8, Map.of(1L, -1), 0, 0, 0, 0, 0); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(1, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(1L, -1), a.cvrDiscrepancy); } /** @@ -329,13 +351,15 @@ public void testNENRecordTwoVoteOverstatement1(){ 0.1, 8, Map.of(1L, 2), 0, 0, 0, 0, 0); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(1, a.twoVoteOverCount.intValue()); assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 2), a.cvrDiscrepancy); } /** @@ -352,13 +376,15 @@ public void testNENRecordTwoVoteUnderstatement1(){ 0.1, 8, Map.of(1L, -2), 0, 0, 0, 0, 0); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(1L, -2), a.cvrDiscrepancy); } /** @@ -375,13 +401,15 @@ public void testNENRecordOther1(){ 0.1, 8, Map.of(1L, 0), 0, 0, 0, 0, 0); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 0), a.cvrDiscrepancy); } /** @@ -398,13 +426,15 @@ public void testNENRecordOneVoteOverstatement2(){ 0.1, 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1), 1, 0, 0, 1, 1); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(2, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1), a.cvrDiscrepancy); } /** @@ -421,13 +451,15 @@ public void testNENRecordOneVoteUnderstatement2(){ 0.1, 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -1), 1, 0, 0, 1, 1); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(1, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -1), a.cvrDiscrepancy); } /** @@ -444,13 +476,15 @@ public void testNENRecordTwoVoteOverstatement2(){ 0.1, 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 2), 1, 0, 0, 1, 1); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(1, a.twoVoteOverCount.intValue()); assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 2), a.cvrDiscrepancy); } /** @@ -467,13 +501,15 @@ public void testNENRecordTwoVoteUnderstatement2(){ 0.1, 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2), 1, 0, 0, 1, 1); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(2, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2), a.cvrDiscrepancy); } /** @@ -490,13 +526,15 @@ public void testNENRecordOther2(){ 0.1, 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0), 1, 0, 0, 1, 1); - a.recordDiscrepancy(info); + assertTrue(a.recordDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); assertEquals(0, a.twoVoteOverCount.intValue()); assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(2, a.otherCount.intValue()); + + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0), a.cvrDiscrepancy); } @@ -510,7 +548,7 @@ public void testNENRecordOther2(){ * decremented, and the ID removed from the map. If it is not there, then the discrepancy counts * and the map are not changed. */ - @Test(expectedExceptions = {RuntimeException.class}) + @Test public void testNENRemoveNoMatch1(){ log(LOGGER, "testNENRemoveNoMatch1"); CVRAuditInfo info = new CVRAuditInfo(); @@ -520,7 +558,15 @@ public void testNENRemoveNoMatch1(){ 50, 0.1, 8, Map.of(), 0, 0, 0, 0, 0); - a.removeDiscrepancy(info); + assertFalse(a.removeDiscrepancy(info)); + + assertEquals(0, a.oneVoteOverCount.intValue()); + assertEquals(0, a.oneVoteUnderCount.intValue()); + assertEquals(0, a.twoVoteOverCount.intValue()); + assertEquals(0, a.twoVoteUnderCount.intValue()); + assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(), a.cvrDiscrepancy); } /** @@ -528,7 +574,7 @@ public void testNENRemoveNoMatch1(){ * been computed for the given CVR-ACVR pair, and the assertion has some discrepancies recorded * already. It should not change the assertion's discrepancy counts. */ - @Test(expectedExceptions = {RuntimeException.class}) + @Test public void testNENRemoveNoMatch2(){ log(LOGGER, "testNENRemoveNoMatch2"); CVRAuditInfo info = new CVRAuditInfo(); @@ -538,7 +584,15 @@ public void testNENRemoveNoMatch2(){ 50, 0.1, 8, Map.of(2L, -1, 3L, 1), 1, 1, 0, 0, 0); - a.removeDiscrepancy(info); + assertFalse(a.removeDiscrepancy(info)); + + assertEquals(1, a.oneVoteOverCount.intValue()); + assertEquals(1, a.oneVoteUnderCount.intValue()); + assertEquals(0, a.twoVoteOverCount.intValue()); + assertEquals(0, a.twoVoteUnderCount.intValue()); + assertEquals(0, a.otherCount.intValue()); + + assertEquals(Map.of(2L, -1, 3L, 1), a.cvrDiscrepancy); } /** @@ -554,7 +608,7 @@ public void testNENRemoveOneVoteOverstatement1(){ 50, 0.1, 8, Map.of(1L, 1), 1, 0, 0, 0, 0); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -578,7 +632,7 @@ public void testNENRemoveOneVoteUnderstatement1(){ 50, 0.1, 8, Map.of(1L, -1), 0, 1, 0, 0, 0); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -602,7 +656,7 @@ public void testNENRemoveTwoVoteOverstatement1(){ 50, 0.1, 8, Map.of(1L, 2), 0, 0, 1, 0, 0); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -626,7 +680,7 @@ public void testNENRemoveTwoVoteUnderstatement1(){ 50, 0.1, 8, Map.of(1L, -2), 0, 0, 0, 1, 0); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -650,7 +704,7 @@ public void testNENRemoveOther1(){ 50, 0.1, 8, Map.of(1L, 0), 0, 0, 0, 0, 1); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(0, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -675,7 +729,7 @@ public void testNENRemoveOneVoteOverstatement2(){ 50, 0.1, 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1), 2, 0, 0, 1, 1); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -700,7 +754,7 @@ public void testNENRemoveOneVoteUnderstatement2(){ 50, 0.1, 8, Map.of(1L, 0, 2L, 1, 3L, -1, 4L, -1), 1, 2, 0, 0, 1); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(1, a.oneVoteUnderCount.intValue()); @@ -725,7 +779,7 @@ public void testNENRemoveTwoVoteOverstatement2(){ 50, 0.1, 8, Map.of(1L, 2, 2L, 1, 3L, -2, 4L, 2), 1, 0, 2, 1, 0); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -750,7 +804,7 @@ public void testNENRemoveTwoVoteUnderstatement2(){ 50, 0.1, 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2), 1, 0, 0, 2, 1); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); @@ -775,7 +829,7 @@ public void testNENRemoveOther2(){ 50, 0.1, 8, Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0), 1, 0, 0, 1, 2); - a.removeDiscrepancy(info); + assertTrue(a.removeDiscrepancy(info)); assertEquals(1, a.oneVoteOverCount.intValue()); assertEquals(0, a.oneVoteUnderCount.intValue()); From 06453b605a8344cd5370151fef10e5d1b840a9ad Mon Sep 17 00:00:00 2001 From: michelleblom Date: Thu, 20 Jun 2024 16:23:58 +1000 Subject: [PATCH 03/12] Edited Assertion discrepancy computation and removal to account for re-auditing and the fact that the removal/recording methods are designed to be called multiple times for a specific CVRAuditInfo (if the CVR appears multiple times in the sample). Adapted commenting to make this clear. --- .../corla/model/assertion/Assertion.java | 26 ++++++++++++++++--- .../model/assertion/NEBAssertionTests.java | 20 +++++++------- .../model/assertion/NENAssertionTests.java | 20 +++++++------- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java index 3be914f7..0e86fcd0 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java @@ -389,6 +389,13 @@ public OptionalInt computeDiscrepancy(final CastVoteRecord cvr, final CastVoteRe // We have no discrepancy: recorded votes are the same on both the CVR and audited ballot. LOGGER.debug(String.format("%s CVR ID %d, Assertion ID %d, contest %s, no discrepancy.", prefix, cvr.id(), id, contestName)); + + if(cvrDiscrepancy.containsKey(cvr.id())){ + // We can only get here if the CVR was re-audited. + cvrDiscrepancy.remove(cvr.id()); + LOGGER.debug(String.format("%s CVR ID %d, Assertion ID %d, contest %s, prior computed " + + "discrepancy removed from assertion's records.", prefix, cvr.id(), id, contestName)); + } return OptionalInt.empty(); } @@ -502,7 +509,7 @@ private int scoreAuditedBallot(long cvrID, final CastVoteRecord auditedCVR) { if(acvrInfo.get().consensus() == ConsensusValue.NO){ // The audited ballot has no consensus, we treat it as a Phantom Ballot. - // Return a worst case audited ballot score of -1. + // Return the worst case audited ballot score of -1. LOGGER.debug(String.format("%s audited ballot for CVR ID %d has no consensus, " + "audited ballot score is -1 for Assertion ID %d.", prefix, cvrID, id)); return -1; @@ -520,7 +527,8 @@ private int scoreAuditedBallot(long cvrID, final CastVoteRecord auditedCVR) { * discrepancy is relevant for this assertion (if it is present in its cvrDiscrepancy map). * If so, increment the counters for its discrepancy type. A RuntimeException will be thrown * if the discrepancy type associated with this CVR-ACVR pair is not valid (i.e., not one of the - * defined types). + * defined types). This method is designed to be called 'n' times for a given CVRAuditInfo if + * the associated CVR appears 'n' times in the sample. * @param theRecord CVRAuditInfo representing the CVR-ACVR pair that has resulted in a discrepancy. * @return a boolean indicating if a discrepancy associated with the given CVRAuditInfo was * recorded against the totals of at least one of this audit's assertions. @@ -571,7 +579,11 @@ public boolean recordDiscrepancy(final CVRAuditInfo theRecord) { /** * Removes discrepancies relating to a given CVR-ACVR comparison. (This is relevant when * ballots are 'un-audited' to be subsequently re-audited). A RuntimeException will be thrown - * if an invalid discrepancy type has been stored in this assertion. + * if an invalid discrepancy type has been stored in this assertion. Note that we do not remove + * the CVR ID from the cvrDiscrepancy map. This is because there may be multiple instances of the + * discrepancy counted in the assertion's totals (e.g, if the CVR appears multiple times in the + * sample to be audited). This method is designed to be called 'n' times for a given CVRAuditInfo if + * the associated CVR appears 'n' times in the sample. * @param theRecord The CVRAuditInfo record that generated the discrepancy. * @return a boolean indicating if a discrepancy associated with the given CVRAuditInfo was * removed from this assertion's totals. @@ -600,7 +612,13 @@ public boolean removeDiscrepancy(final CVRAuditInfo theRecord) { throw new RuntimeException(msg); } } - cvrDiscrepancy.remove(theRecord.id()); + + // Note that we do not remove the CVR ID from the cvrDiscrepancy map. This is because there + // may be multiple instances of the discrepancy counted in the assertion's totals (e.g, if + // the CVR appears multiple times in the sample to be audited). If a ballot is reaudited, + // and a determination made that a discrepancy does not exist, the entry in cvrDiscrepancies + // will be updated via the computeDiscrepancy() method. + LOGGER.debug(String.format("%s Discrepancy of type %d removed from Assertion ID %d,"+ "contest %s, CVR ID %d. New totals: 1 vote understatements %d; 1 vote overstatements %d; " + "2 vote understatements %d; 2 vote overstatements %d; other %d.", prefix, theType, id, diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java index eb3c811d..9c63a05e 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java @@ -585,7 +585,7 @@ public void testNEBRemoveOneVoteOverstatement1(){ assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); - assertEquals(Map.of(), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 1), a.cvrDiscrepancy); } /** @@ -609,7 +609,7 @@ public void testNEBRemoveOneVoteUnderstatement1(){ assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); - assertEquals(Map.of(), a.cvrDiscrepancy); + assertEquals(Map.of(1L, -1), a.cvrDiscrepancy); } /** @@ -633,7 +633,7 @@ public void testNEBRemoveTwoVoteOverstatement1(){ assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); - assertEquals(Map.of(), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 2), a.cvrDiscrepancy); } /** @@ -657,7 +657,7 @@ public void testNEBRemoveTwoVoteUnderstatement1(){ assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); - assertEquals(Map.of(), a.cvrDiscrepancy); + assertEquals(Map.of(1L, -2), a.cvrDiscrepancy); } /** @@ -681,7 +681,7 @@ public void testNEBRemoveOther1(){ assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); - assertEquals(Map.of(), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 0), a.cvrDiscrepancy); } /** @@ -706,7 +706,7 @@ public void testNEBRemoveOneVoteOverstatement2(){ assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1), a.cvrDiscrepancy); } /** @@ -731,7 +731,7 @@ public void testNEBRemoveOneVoteUnderstatement2(){ assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -1), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -1, 4L, -1), a.cvrDiscrepancy); } /** @@ -756,7 +756,7 @@ public void testNEBRemoveTwoVoteOverstatement2(){ assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); - assertEquals(Map.of(1L, 2, 2L, 1, 3L, -2), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 2, 2L, 1, 3L, -2, 4L, 2), a.cvrDiscrepancy); } /** @@ -781,7 +781,7 @@ public void testNEBRemoveTwoVoteUnderstatement2(){ assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2), a.cvrDiscrepancy); } /** @@ -806,7 +806,7 @@ public void testNEBRemoveOther2(){ assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0), a.cvrDiscrepancy); } /** diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java index ce36ce60..d7f8c578 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java @@ -616,7 +616,7 @@ public void testNENRemoveOneVoteOverstatement1(){ assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); - assertEquals(Map.of(), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 1), a.cvrDiscrepancy); } /** @@ -640,7 +640,7 @@ public void testNENRemoveOneVoteUnderstatement1(){ assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); - assertEquals(Map.of(), a.cvrDiscrepancy); + assertEquals(Map.of(1L, -1), a.cvrDiscrepancy); } /** @@ -664,7 +664,7 @@ public void testNENRemoveTwoVoteOverstatement1(){ assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); - assertEquals(Map.of(), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 2), a.cvrDiscrepancy); } /** @@ -688,7 +688,7 @@ public void testNENRemoveTwoVoteUnderstatement1(){ assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); - assertEquals(Map.of(), a.cvrDiscrepancy); + assertEquals(Map.of(1L, -2), a.cvrDiscrepancy); } /** @@ -712,7 +712,7 @@ public void testNENRemoveOther1(){ assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); - assertEquals(Map.of(), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 0), a.cvrDiscrepancy); } /** @@ -737,7 +737,7 @@ public void testNENRemoveOneVoteOverstatement2(){ assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1), a.cvrDiscrepancy); } /** @@ -762,7 +762,7 @@ public void testNENRemoveOneVoteUnderstatement2(){ assertEquals(0, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -1), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -1, 4L, -1), a.cvrDiscrepancy); } /** @@ -787,7 +787,7 @@ public void testNENRemoveTwoVoteOverstatement2(){ assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(0, a.otherCount.intValue()); - assertEquals(Map.of(1L, 2, 2L, 1, 3L, -2), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 2, 2L, 1, 3L, -2, 4L, 2), a.cvrDiscrepancy); } /** @@ -812,7 +812,7 @@ public void testNENRemoveTwoVoteUnderstatement2(){ assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2), a.cvrDiscrepancy); } /** @@ -837,7 +837,7 @@ public void testNENRemoveOther2(){ assertEquals(1, a.twoVoteUnderCount.intValue()); assertEquals(1, a.otherCount.intValue()); - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2), a.cvrDiscrepancy); + assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0), a.cvrDiscrepancy); } From 6db5d7e40befde7a33cce21d64482e265d6b8f32 Mon Sep 17 00:00:00 2001 From: michelleblom Date: Thu, 20 Jun 2024 22:51:00 +1000 Subject: [PATCH 04/12] Added some initial tests of the Assertions methods called when reauditing ballots. Added missed check of excess removal of two vote overstatements from assertion totals. --- .../corla/model/assertion/Assertion.java | 2 +- .../model/assertion/NEBAssertionTests.java | 111 ++++++++++++++++++ .../model/assertion/NENAssertionTests.java | 110 +++++++++++++++++ 3 files changed, 222 insertions(+), 1 deletion(-) diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java index 0e86fcd0..2bb42757 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java @@ -635,7 +635,7 @@ public boolean removeDiscrepancy(final CVRAuditInfo theRecord) { LOGGER.debug(msg); } - if(min(List.of(oneVoteOverCount, oneVoteUnderCount, twoVoteUnderCount, otherCount)) < 0) { + if(min(List.of(oneVoteOverCount, oneVoteUnderCount, twoVoteOverCount, twoVoteUnderCount, otherCount)) < 0) { final String msg = String.format("%s Negative discrepancy counts in Assertion ID %d, " + "contest %s when removing discrepancy for CVR %d.", prefix, id, contestName, theRecord.id()); LOGGER.error(msg); diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java index 9c63a05e..515f7c04 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java @@ -30,6 +30,7 @@ import static org.testng.AssertJUnit.assertFalse; import static org.testng.AssertJUnit.assertTrue; +import au.org.democracydevelopers.corla.model.vote.IRVParsingException; import java.math.BigDecimal; import java.util.List; import java.util.Map; @@ -1750,6 +1751,116 @@ public void testNEBComputeDiscrepancyNormalCVRBallotNoContestMinusOne(RecordType 0, 0, 0, 0, 0); } + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (a one vote understatement). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, and discrepancy computation repeated. In this + * case, n = 1 and the new discrepancy is a one vote overstatement. + */ + @Test() + public void testNEBReauditBallot1(){ + log(LOGGER, String.format("testNEBReauditBallot1[%s]", RecordType.REAUDITED)); + resetMocks(A, blank, RecordType.UPLOADED, ConsensusValue.YES, RecordType.REAUDITED); + + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNEBAssertion("A", "B", TC, 50, 0.1, + 8, Map.of(1L, -1), 0, 1, 0, + 0, 0); + + assertTrue(a1.removeDiscrepancy(info)); + + checkComputeDiscrepancy(cvr, auditedCvr, List.of(a1), 1, Map.of(1L, 1), + 0, 0, 0, 0, 0); + + assertTrue(a1.recordDiscrepancy(info)); + assert(countsEqual(a1, 1, 0, 0, 0, 0)); + } + + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (a two vote overstatement). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, and discrepancy computation repeated. In this + * case, n = 2 and the new discrepancy is a one vote understatement. + */ + @Test() + public void testNEBReauditBallot2(){ + log(LOGGER, String.format("testNEBReauditBallot2[%s]", RecordType.REAUDITED)); + resetMocks(ABCD, BACD, RecordType.UPLOADED, ConsensusValue.YES, RecordType.REAUDITED); + + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNEBAssertion("B", "F", TC, 50, 0.1, + 8, Map.of(1L, 2), 0, 0, 2, + 0, 0); + + assertTrue(a1.removeDiscrepancy(info)); + assertTrue(a1.removeDiscrepancy(info)); + + checkComputeDiscrepancy(cvr, auditedCvr, List.of(a1), -1, Map.of(1L, -1), + 0, 0, 0, 0, 0); + + assertTrue(a1.recordDiscrepancy(info)); + assertTrue(a1.recordDiscrepancy(info)); + assert(countsEqual(a1, 0, 0, 2, 0, 0)); + } + + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (an "other" discrepancy). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, and discrepancy computation repeated. In this + * case, n = 5 and there is no new discrepancy. + */ + @Test() + public void testNEBReauditBallot3(){ + log(LOGGER, String.format("testNEBReauditBallot3[%s]", RecordType.REAUDITED)); + resetMocks(ABCD, ABCD, RecordType.UPLOADED, ConsensusValue.YES, RecordType.REAUDITED); + + final int N = 5; + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNEBAssertion("B", "F", TC, 50, 0.1, + 8, Map.of(1L, 0), 0, 0, 0, + 0, N); + + for(int i = 0; i < N; ++i) { + assertTrue(a1.removeDiscrepancy(info)); + } + + OptionalInt d1 = a1.computeDiscrepancy(cvr, auditedCvr); + assert(d1.isEmpty()); + + assertEquals(Map.of(), a1.cvrDiscrepancy); + assert(countsEqual(a1, 0, 0, 0, 0, 0)); + } + + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (a two vote overstatement). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, but removeDiscrepancy() is called n+1 times + * in error. The n+1'th call the removeDiscrepancy should throw an exception. + */ + @Test(expectedExceptions = RuntimeException.class) + public void testNEBExcessRemovalCausesError(){ + log(LOGGER, "testNEBExcessRemovalCausesError"); + + final int N = 2; + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNEBAssertion("B", "F", TC, 50, 0.1, + 8, Map.of(1L, 2), 0, 0, N, + 0, 0); + + // Try to remove too many copies of the discrepancy + for(int i = 0; i <= N; ++i) { + assertTrue(a1.removeDiscrepancy(info)); + } + } + /** * Reset the CVR and audited CVR mock objects with the given parameters. diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java index d7f8c578..9a6111cf 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java @@ -1976,6 +1976,116 @@ public void testNENComputeDiscrepancyNormalCVRBallotNoContestMinusOne(RecordType 0, 0, 0, 0, 0); } + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (a one vote understatement). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, and discrepancy computation repeated. In this + * case, n = 1 and the new discrepancy is a one vote overstatement. + */ + @Test() + public void testNENReauditBallot1(){ + log(LOGGER, String.format("testNENReauditBallot1[%s]", RecordType.REAUDITED)); + resetMocks(A, blank, RecordType.UPLOADED, ConsensusValue.YES, RecordType.REAUDITED); + + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNENAssertion("A", "B", TC, List.of("A","B"), 50, + 0.1, 8, Map.of(1L, -1), 0, 1, + 0, 0, 0); + + assertTrue(a1.removeDiscrepancy(info)); + + checkComputeDiscrepancy(cvr, auditedCvr, List.of(a1), 1, Map.of(1L, 1), + 0, 0, 0, 0, 0); + + assertTrue(a1.recordDiscrepancy(info)); + assert(countsEqual(a1, 1, 0, 0, 0, 0)); + } + + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (a two vote overstatement). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, and discrepancy computation repeated. In this + * case, n = 2 and the new discrepancy is a one vote understatement. + */ + @Test() + public void testNENReauditBallot2(){ + log(LOGGER, String.format("testNENReauditBallot2[%s]", RecordType.REAUDITED)); + resetMocks(ABCD, BACD, RecordType.UPLOADED, ConsensusValue.YES, RecordType.REAUDITED); + + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNENAssertion("B", "F", TC, List.of("A","B","F"), 50, + 0.1, 8, Map.of(1L, 2), 0, 0, 2, + 0, 0); + + assertTrue(a1.removeDiscrepancy(info)); + assertTrue(a1.removeDiscrepancy(info)); + + checkComputeDiscrepancy(cvr, auditedCvr, List.of(a1), -1, Map.of(1L, -1), + 0, 0, 0, 0, 0); + + assertTrue(a1.recordDiscrepancy(info)); + assertTrue(a1.recordDiscrepancy(info)); + assert(countsEqual(a1, 0, 0, 2, 0, 0)); + } + + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (an "other" discrepancy). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, and discrepancy computation repeated. In this + * case, n = 5 and there is no new discrepancy. + */ + @Test() + public void testNENReauditBallot3(){ + log(LOGGER, String.format("testNENReauditBallot3[%s]", RecordType.REAUDITED)); + resetMocks(ABCD, ABCD, RecordType.UPLOADED, ConsensusValue.YES, RecordType.REAUDITED); + + final int N = 5; + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNENAssertion("B", "F", TC, List.of("B","F"), 50, + 0.1, 8, Map.of(1L, 0), 0, 0, 0, + 0, N); + + for(int i = 0; i < N; ++i) { + assertTrue(a1.removeDiscrepancy(info)); + } + + OptionalInt d1 = a1.computeDiscrepancy(cvr, auditedCvr); + assert(d1.isEmpty()); + + assertEquals(Map.of(), a1.cvrDiscrepancy); + assert(countsEqual(a1, 0, 0, 0, 0, 0)); + } + + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (a two vote overstatement). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, but removeDiscrepancy() is called n+1 times + * in error. The n+1'th call the removeDiscrepancy should throw an exception. + */ + @Test(expectedExceptions = RuntimeException.class) + public void testNENExcessRemovalCausesError(){ + log(LOGGER, "testNENExcessRemovalCausesError"); + + final int N = 2; + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNENAssertion("B", "F", TC, List.of("A", "B"), 50, + 0.1, 8, Map.of(1L, 2), 0, 0, + N, 0, 0); + + // Try to remove too many copies of the discrepancy + for(int i = 0; i <= N; ++i) { + assertTrue(a1.removeDiscrepancy(info)); + } + } + /** * Create an NEN assertion with the given parameters. * @param winner Winner of the assertion. From 630f2be3d07dce80da58a26c40fb59ac3a04be3f Mon Sep 17 00:00:00 2001 From: michelleblom Date: Fri, 21 Jun 2024 10:27:15 +1000 Subject: [PATCH 05/12] Added additional tests to check that excess removal of different types of discrepancies will result in an exception. --- .../model/assertion/NEBAssertionTests.java | 100 +++++++++++++++++- .../model/assertion/NENAssertionTests.java | 100 +++++++++++++++++- 2 files changed, 195 insertions(+), 5 deletions(-) diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java index 515f7c04..abfd388f 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java @@ -30,7 +30,6 @@ import static org.testng.AssertJUnit.assertFalse; import static org.testng.AssertJUnit.assertTrue; -import au.org.democracydevelopers.corla.model.vote.IRVParsingException; import java.math.BigDecimal; import java.util.List; import java.util.Map; @@ -1844,8 +1843,8 @@ public void testNEBReauditBallot3(){ * in error. The n+1'th call the removeDiscrepancy should throw an exception. */ @Test(expectedExceptions = RuntimeException.class) - public void testNEBExcessRemovalCausesError(){ - log(LOGGER, "testNEBExcessRemovalCausesError"); + public void testNEBExcessRemovalCausesErrorTwoVoteOver(){ + log(LOGGER, "testNEBExcessRemovalCausesErrorTwoVoteOver"); final int N = 2; CVRAuditInfo info = new CVRAuditInfo(); @@ -1861,6 +1860,101 @@ public void testNEBExcessRemovalCausesError(){ } } + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (a two vote understatement). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, but removeDiscrepancy() is called n+1 times + * in error. The n+1'th call the removeDiscrepancy should throw an exception. + */ + @Test(expectedExceptions = RuntimeException.class) + public void testNEBExcessRemovalCausesErrorTwoVoteUnder(){ + log(LOGGER, "testNEBExcessRemovalCausesErrorTwoVoteUnder"); + + final int N = 2; + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNEBAssertion("B", "F", TC, 50, 0.1, + 8, Map.of(1L, -2), 0, 0, 0, + N, 0); + + // Try to remove too many copies of the discrepancy + for(int i = 0; i <= N; ++i) { + assertTrue(a1.removeDiscrepancy(info)); + } + } + + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (a one vote understatement). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, but removeDiscrepancy() is called n+1 times + * in error. The n+1'th call the removeDiscrepancy should throw an exception. + */ + @Test(expectedExceptions = RuntimeException.class) + public void testNEBExcessRemovalCausesErrorOneVoteUnder(){ + log(LOGGER, "testNEBExcessRemovalCausesErrorOneVoteUnder"); + + final int N = 2; + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNEBAssertion("B", "F", TC, 50, 0.1, + 8, Map.of(1L, -1), 0, N, 0, + 0, 0); + + // Try to remove too many copies of the discrepancy + for(int i = 0; i <= N; ++i) { + assertTrue(a1.removeDiscrepancy(info)); + } + } + + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (a one vote overstatement). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, but removeDiscrepancy() is called n+1 times + * in error. The n+1'th call the removeDiscrepancy should throw an exception. + */ + @Test(expectedExceptions = RuntimeException.class) + public void testNEBExcessRemovalCausesErrorOneVoteOver(){ + log(LOGGER, "testNEBExcessRemovalCausesErrorOneVoteOver"); + + final int N = 2; + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNEBAssertion("B", "F", TC, 50, 0.1, + 8, Map.of(1L, 1), N, 0, 0, + 0, 0); + + // Try to remove too many copies of the discrepancy + for(int i = 0; i <= N; ++i) { + assertTrue(a1.removeDiscrepancy(info)); + } + } + + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (a one vote overstatement). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, but removeDiscrepancy() is called n+1 times + * in error. The n+1'th call the removeDiscrepancy should throw an exception. + */ + @Test(expectedExceptions = RuntimeException.class) + public void testNEBExcessRemovalCausesErrorOther(){ + log(LOGGER, "testNEBExcessRemovalCausesErrorOther"); + + final int N = 2; + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNEBAssertion("B", "F", TC, 50, 0.1, + 8, Map.of(1L, 0), 0, 0, 0, + 0, N); + + // Try to remove too many copies of the discrepancy + for(int i = 0; i <= N; ++i) { + assertTrue(a1.removeDiscrepancy(info)); + } + } /** * Reset the CVR and audited CVR mock objects with the given parameters. diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java index 9a6111cf..a89a6aa9 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java @@ -2069,8 +2069,8 @@ public void testNENReauditBallot3(){ * in error. The n+1'th call the removeDiscrepancy should throw an exception. */ @Test(expectedExceptions = RuntimeException.class) - public void testNENExcessRemovalCausesError(){ - log(LOGGER, "testNENExcessRemovalCausesError"); + public void testNENExcessRemovalCausesErrorTwoVoteOver(){ + log(LOGGER, "testNENExcessRemovalCausesErrorToVoteOver"); final int N = 2; CVRAuditInfo info = new CVRAuditInfo(); @@ -2086,6 +2086,102 @@ public void testNENExcessRemovalCausesError(){ } } + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (a two vote understatement). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, but removeDiscrepancy() is called n+1 times + * in error. The n+1'th call the removeDiscrepancy should throw an exception. + */ + @Test(expectedExceptions = RuntimeException.class) + public void testNEBExcessRemovalCausesErrorTwoVoteUnder(){ + log(LOGGER, "testNEBExcessRemovalCausesErrorTwoVoteUnder"); + + final int N = 2; + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNENAssertion("B", "F", TC, List.of("A", "B"), 50, + 0.1, 8, Map.of(1L, -2), 0, 0, + 0, N, 0); + + // Try to remove too many copies of the discrepancy + for(int i = 0; i <= N; ++i) { + assertTrue(a1.removeDiscrepancy(info)); + } + } + + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (a one vote understatement). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, but removeDiscrepancy() is called n+1 times + * in error. The n+1'th call the removeDiscrepancy should throw an exception. + */ + @Test(expectedExceptions = RuntimeException.class) + public void testNEBExcessRemovalCausesErrorOneVoteUnder(){ + log(LOGGER, "testNEBExcessRemovalCausesErrorOneVoteUnder"); + + final int N = 2; + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNENAssertion("B", "F", TC, List.of("A", "B"), 50, + 0.1, 8, Map.of(1L, -1), 0, 1, + 0, 0, 0); + + // Try to remove too many copies of the discrepancy + for(int i = 0; i <= N; ++i) { + assertTrue(a1.removeDiscrepancy(info)); + } + } + + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (a one vote overstatement). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, but removeDiscrepancy() is called n+1 times + * in error. The n+1'th call the removeDiscrepancy should throw an exception. + */ + @Test(expectedExceptions = RuntimeException.class) + public void testNEBExcessRemovalCausesErrorOneVoteOver(){ + log(LOGGER, "testNEBExcessRemovalCausesErrorOneVoteOver"); + + final int N = 2; + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNENAssertion("B", "F", TC, List.of("A", "B"), 50, + 0.1, 8, Map.of(1L, 1), N, 0, + 0, 0, 0); + + // Try to remove too many copies of the discrepancy + for(int i = 0; i <= N; ++i) { + assertTrue(a1.removeDiscrepancy(info)); + } + } + + /** + * Test the re-auditing of a ballot where a prior discrepancy is recorded against the + * associated CVR (a one vote overstatement). The existing discrepancies associated with the + * 'n' copies of the CVR in the sample are removed, but removeDiscrepancy() is called n+1 times + * in error. The n+1'th call the removeDiscrepancy should throw an exception. + */ + @Test(expectedExceptions = RuntimeException.class) + public void testNEBExcessRemovalCausesErrorOther(){ + log(LOGGER, "testNEBExcessRemovalCausesErrorOther"); + + final int N = 2; + CVRAuditInfo info = new CVRAuditInfo(); + info.setID(1L); + + Assertion a1 = createNENAssertion("B", "F", TC, List.of("A", "B"), 50, + 0.1, 8, Map.of(1L, 0), 0, 0, + 0, 0, N); + + // Try to remove too many copies of the discrepancy + for(int i = 0; i <= N; ++i) { + assertTrue(a1.removeDiscrepancy(info)); + } + } + /** * Create an NEN assertion with the given parameters. * @param winner Winner of the assertion. From 04c5882e297073fd29671047b2763cee5c9b8f29 Mon Sep 17 00:00:00 2001 From: michelleblom Date: Fri, 21 Jun 2024 10:55:37 +1000 Subject: [PATCH 06/12] Added further commenting. --- .../corla/model/IRVComparisonAudit.java | 27 ++++++++++++++++++- .../corla/model/assertion/Assertion.java | 16 +++++++++++ .../model/assertion/NEBAssertionTests.java | 1 + .../model/assertion/NENAssertionTests.java | 1 + 4 files changed, 44 insertions(+), 1 deletion(-) diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java index 4a55d98a..b9dde9f9 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java @@ -47,10 +47,35 @@ * however, these initial sample sizes will not be meaningful as the audit's assertions are * not loaded until the ComparisonAudit base constructor finished. The IRVComparisonAudit * constructor will load the relevant assertions from the database (populating the audit's - * assertions list), compute the audit's diluted margin, and then reinvoke the sample size + * assertions list), compute the audit's diluted margin, and then re-invoke the sample size * computation methods. This class overrides some of the methods in the base ComparisonAudit * class: recalculateSamplesToAudit(); initialSamplesToAudit(); computeDiscrepancy(); * riskMeasurement(); removeDiscrepancy(); and recordDiscrepancy(). + * + * DISCREPANCY MANAGEMENT + * Colorado-rla uses the following logic for computing and storing discrepancies: + * 1. Call the ComparisonAudit's computeDiscrepancy() method given a CVR and audited ballot. + * Return any discrepancy found as an OptionalInt. + * 2. If there was a discrepancy, call the ComparisonAudit's recordDiscrepancy() method N times + * where N is the number of times the ballot appears in the sample (sampling is by replacement). + * + * RE-AUDITING OF BALLOTS: + * If a ballot is being re-audited, all discrepancies associated with the matching CVR are + * removed by calling removeDiscrepancy() N times where N is the number of times the ballot + * appears in the sample. Steps 1, and 2 if needed, above are then performed. + * + * For IRV audits, a CVR and audited ballot may represent a different type of discrepancy for + * different assertions, or a discrepancy for some assertions and not for others. When this + * class' computeDiscrepancy() method is called, a corresponding method in each of the audit's + * assertions is called. If a discrepancy is found with respect to an assertion, the CVR ID and + * discrepancy type will be stored in its cvrDiscrepancy map. Each assertion has its own + * recordDiscrepancy() and removeDiscrepancy() method. When their recordDiscrepancy() method + * is called, its cvrDiscrepancy map will be looked up to find the right discrepancy type, and the + * assertion's internal discrepancy totals updated. Similarly, when removeDiscrepancy() is called, + * the cvrDiscrepancy map will be looked up to find the discrepancy type, and the assertion's + * discrepancy totals updated. The CVR ID - discrepancy type entry in its cvrDiscrepancy() map + * is not removed, however, it is only removed if computeDiscrepancy() is called again and either + * a different or no discrepancy is found. */ @Entity @DiscriminatorValue("IRV") diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java index 2bb42757..e6798871 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java @@ -53,6 +53,22 @@ * of RAIRE assertion. Assertions are created and stored in the database by raire-service. They * are audited by colorado-rla. During this process, the record of discrepancies attached to * the assertion will be updated, as well as its estimated sample sizes and risk. + * + * A NOTE ON DISCREPANCY MANAGEMENT: + * The class level comment of the IRVComparisonAudit class describes how discrepancies are + * managed by ComparisonAudit's. + * + * Of particular note for assertions: + * If a discrepancy is found with respect to an assertion, the CVR ID and discrepancy type will be + * stored in its cvrDiscrepancy map. Each assertion has its own recordDiscrepancy() and + * removeDiscrepancy() method. When their recordDiscrepancy() method is called, its cvrDiscrepancy + * map will be looked up to find the right discrepancy type, and the assertion's internal discrepancy + * totals updated. Similarly, when removeDiscrepancy() is called, the cvrDiscrepancy map will be + * looked up to find the discrepancy type, and the assertion's discrepancy totals updated. The CVR + * ID - discrepancy type entry in its cvrDiscrepancy() map is not removed, however, it is only + * removed if computeDiscrepancy() is called again and either a different or no discrepancy is found. + * Note that recordDiscrepancy() and removeDiscrepancy() is designed to be called N times for a + * particular CVR/audited ballot pair if that ballot appears N times in the sample. */ @Entity @Table(name = "assertion") diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java index abfd388f..602b4b4c 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java @@ -57,6 +57,7 @@ * -- Removal of a pre-recorded discrepancy. * -- Scoring of NEB assertions. * -- Computation of discrepancies for NEB assertions. + * -- The logic involved in the re-auditing of ballots. * Refer to the Guide to RAIRE for details on how NEB assertions are scored, and how * discrepancies are computed (Part 2, Appendix A). */ diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java index a89a6aa9..a0e7e580 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java @@ -56,6 +56,7 @@ * -- Removal of a pre-recorded discrepancy. * -- Scoring of NEN assertions. * -- Computation of discrepancies. + * -- The logic involved in the re-auditing of ballots. * Refer to the Guide to RAIRE for details on how NEN assertions are scored, and how * discrepancies are computed (Part 2, Appendix A.) */ From a7fdd4808208f301f19dbe8b986b92c09ae913d0 Mon Sep 17 00:00:00 2001 From: michelleblom Date: Fri, 21 Jun 2024 15:58:19 +1000 Subject: [PATCH 07/12] Added an abstract class for testing classes that interact with the database. Changed name of assumed_context in assertion to assertion_assumed_continuing. Added some initial tests of IRVComparisonAudit construction. --- .../corla/model/IRVComparisonAudit.java | 12 +- .../corla/model/assertion/Assertion.java | 2 +- .../corla/model/IRVComparisonAuditTests.java | 211 +++++ .../corla/util/TestClassWithDatabase.java | 89 +++ .../corla/util/testUtils.java | 4 +- .../src/test/resources/SQL/corla.sql | 718 ++++++++++++++++++ .../test/resources/SQL/simple-assertions.sql | 31 + 7 files changed, 1064 insertions(+), 3 deletions(-) create mode 100644 server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/IRVComparisonAuditTests.java create mode 100644 server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/TestClassWithDatabase.java create mode 100644 server/eclipse-project/src/test/resources/SQL/corla.sql create mode 100644 server/eclipse-project/src/test/resources/SQL/simple-assertions.sql diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java index b9dde9f9..144b9a66 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java @@ -22,6 +22,7 @@ package au.org.democracydevelopers.corla.model; import au.org.democracydevelopers.corla.query.AssertionQueries; +import com.google.inject.internal.util.ImmutableList; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -127,7 +128,7 @@ public IRVComparisonAudit(final ContestResult contestResult, final BigDecimal ri // the database whose contest name matches that in the given contestResult). If an // unexpected error arises in retrieving this data from the database, matching() will throw // a RunTimeException. - assertions = AssertionQueries.matching(contestResult.getContestName()); + assertions = AssertionQueries.matching(contestName); LOGGER.debug(String.format("%s retrieved %d assertions for contest %s.", prefix, assertions.size(), contestName)); @@ -538,6 +539,15 @@ public void recordDiscrepancy(final CVRAuditInfo theRecord, final int theType) { } } + /** + * This method is used purely in testing to inspect the assertions present in the + * IRVComparisonAudit, returned as an ImmutableList. + * @return An ImmutableList of the assertions present in this IRVComparisonAudit. + */ + public ImmutableList getAssertions(){ + return ImmutableList.builder().addAll(assertions).build(); + } + /** * This method checks whether this IRVComparisonAudit's list of assertions has been * appropriately initialised, and throws a RunTimeException if not. It takes a string identifying diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java index e6798871..c32faeef 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java @@ -142,7 +142,7 @@ public abstract class Assertion implements PersistentEntity { * List of candidates that the assertion assumes are 'continuing' in the assertion's context. */ @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "assertion_context", joinColumns = @JoinColumn(name = "id")) + @CollectionTable(name = "assertion_assumed_continuing", joinColumns = @JoinColumn(name = "id")) @Column(name = "assumed_continuing", updatable = false, nullable = false) protected List assumedContinuing = new ArrayList<>(); diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/IRVComparisonAuditTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/IRVComparisonAuditTests.java new file mode 100644 index 00000000..e2189290 --- /dev/null +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/IRVComparisonAuditTests.java @@ -0,0 +1,211 @@ +/* +Democracy Developers IRV extensions to colorado-rla. + +@copyright 2024 Colorado Department of State + +These IRV extensions are designed to connect to a running instance of the raire +service (https://github.com/DemocracyDevelopers/raire-service), in order to +generate assertions that can be audited using colorado-rla. + +The colorado-rla IRV extensions are free software: you can redistribute it and/or modify it under the terms +of the GNU Affero General Public License as published by the Free Software Foundation, either +version 3 of the License, or (at your option) any later version. + +The colorado-rla IRV extensions are distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with +raire-service. If not, see . +*/ + +package au.org.democracydevelopers.corla.model; + +import static org.mockito.Mockito.when; +import static org.testng.AssertJUnit.assertEquals; + +import au.org.democracydevelopers.corla.model.assertion.Assertion; +import au.org.democracydevelopers.corla.model.assertion.AssertionTests; +import au.org.democracydevelopers.corla.model.assertion.NEBAssertion; +import au.org.democracydevelopers.corla.model.assertion.NENAssertion; +import au.org.democracydevelopers.corla.util.TestClassWithDatabase; +import au.org.democracydevelopers.corla.util.testUtils; +import java.math.BigDecimal; +import java.util.List; +import org.apache.log4j.LogManager; +import org.apache.log4j.Logger; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.ext.ScriptUtils; +import org.testcontainers.jdbc.JdbcDatabaseDelegate; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import us.freeandfair.corla.model.AuditReason; +import us.freeandfair.corla.model.AuditStatus; +import us.freeandfair.corla.model.ContestResult; +import us.freeandfair.corla.persistence.Persistence; + +/** + * This class contains tests for the functionality present in IRVComparisonAudit. + */ +public class IRVComparisonAuditTests extends TestClassWithDatabase { + + private static final Logger LOGGER = LogManager.getLogger(IRVComparisonAuditTests.class); + + /** + * Container for the mock-up database. + */ + static PostgreSQLContainer postgres = createTestContainer(); + + /** + * Mock of a ContestResult for the contest 'One NEB Assertion Contest'. + */ + @Mock + private ContestResult oneNEBContestResult; + + /** + * Mock of a ContestResult for the contest 'One NEN Assertion Contest'. + */ + @Mock + private ContestResult oneNENContestResult; + + /** + * Mock of a ContestResult for the contest 'One NEN NEB Assertion Contest'. + */ + @Mock + private ContestResult oneNENNEBContestResult; + + /** + * Mock of a ContestResult for a contest with no assertions. + */ + @Mock + private ContestResult doesNotExistContestResult; + + /** + * Start the test container and establish persistence properties before the first test. + */ + @BeforeClass + public static void beforeAll() { + postgres.start(); + Persistence.setProperties(createHibernateProperties(postgres)); + + var containerDelegate = new JdbcDatabaseDelegate(postgres, ""); + ScriptUtils.runInitScript(containerDelegate, "SQL/simple-assertions.sql"); + } + + /** + * Initialise mocked objects prior to the first test. Note that the diluted margin + * returned by ContestResult's for IRV will not have a sensible value, and it will + * not be used for IRV computations. For testing purposes, we should set it with + * varied values and ensure that the audit itself is contrycted properly. + */ + @BeforeClass + public void initMocks() { + MockitoAnnotations.openMocks(this); + when(oneNENContestResult.getContestName()).thenReturn("One NEN Assertion Contest"); + when(oneNENContestResult.getDilutedMargin()).thenReturn(BigDecimal.ZERO); + when(oneNEBContestResult.getContestName()).thenReturn("One NEB Assertion Contest"); + when(oneNEBContestResult.getDilutedMargin()).thenReturn(BigDecimal.valueOf(0.10)); + when(oneNENNEBContestResult.getContestName()).thenReturn("One NEN NEB Assertion Contest"); + when(oneNENNEBContestResult.getDilutedMargin()).thenReturn(BigDecimal.valueOf(0.03)); + when(doesNotExistContestResult.getContestName()).thenReturn("Does Not Exist"); + when(doesNotExistContestResult.getDilutedMargin()).thenReturn(BigDecimal.valueOf(0.98)); + } + + /** + * After all test have run, stop the test container. + */ + @AfterClass + public static void afterAll() { + postgres.stop(); + } + + /** + * Create an IRVComparisonAudit for a contest with no assertions in the database. + */ + @Test + public void testCreateIRVAuditNoAssertions(){ + testUtils.log(LOGGER, "testCreateIRVAuditNoAssertions"); + IRVComparisonAudit ca = new IRVComparisonAudit(doesNotExistContestResult, AssertionTests.riskLimit3, + AuditReason.OPPORTUNISTIC_BENEFITS); + + checkIRVComparisonAudit(ca, AssertionTests.riskLimit3, AuditReason.OPPORTUNISTIC_BENEFITS, + AuditStatus.NOT_AUDITABLE, 0); + + assertEquals(0, ca.optimisticSamplesToAudit().intValue()); + assertEquals(0, ca.estimatedSamplesToAudit().intValue()); + + final List assertions = ca.getAssertions(); + assertEquals(0, assertions.size()); + } + + /** + * Create an IRVComparisonAudit for a contest with one NEB assertion stored in the database. This + * assertion has a diluted margin of 0.32. + */ + @Test + public void testCreateIRVAuditOneNEBAssertion(){ + testUtils.log(LOGGER, "testCreateIRVAuditOneNEBAssertion"); + IRVComparisonAudit ca = new IRVComparisonAudit(oneNEBContestResult, AssertionTests.riskLimit3, + AuditReason.COUNTY_WIDE_CONTEST); + + checkIRVComparisonAudit(ca, AssertionTests.riskLimit3, AuditReason.COUNTY_WIDE_CONTEST, + AuditStatus.NOT_STARTED, 0.32); + + final List assertions = ca.getAssertions(); + assertEquals(1, assertions.size()); + + assert(assertions.get(0) instanceof NEBAssertion); + } + + /** + * Create an IRVComparisonAudit for a contest with one NEN assertion stored in the database. This + * assertion has a diluted margin of 0.32. + */ + @Test + public void testCreateIRVAuditOneNENAssertion(){ + testUtils.log(LOGGER, "testCreateIRVAuditOneNENAssertion"); + IRVComparisonAudit ca = new IRVComparisonAudit(oneNENContestResult, AssertionTests.riskLimit3, + AuditReason.GEOGRAPHICAL_SCOPE); + + checkIRVComparisonAudit(ca, AssertionTests.riskLimit3, AuditReason.GEOGRAPHICAL_SCOPE, + AuditStatus.NOT_STARTED, 0.12); + + final List assertions = ca.getAssertions(); + assertEquals(1, assertions.size()); + + assert(assertions.get(0) instanceof NENAssertion); + } + + /** + * A method to check that the given IRVComparisonAudit has been initialised properly. + * @param ca IRVComparisonAudit to check. + * @param risk Risk limit of the audit. + * @param reason Reason for the audit. + * @param status Current status of the audit. + * @param dilutedMargin Diluted margin of the contest being audited. + */ + private void checkIRVComparisonAudit(final IRVComparisonAudit ca, final BigDecimal risk, + final AuditReason reason, final AuditStatus status, double dilutedMargin){ + + assertEquals(status, ca.auditStatus()); + assertEquals(reason, ca.auditReason()); + assertEquals(0, ca.getAuditedSampleCount().intValue()); + + assertEquals(0, ca.disagreementCount()); + assertEquals(0, ca.getOverstatements().intValue()); + assertEquals(0, ca.discrepancyCount(0)); + assertEquals(0, ca.discrepancyCount(1)); + assertEquals(0, ca.discrepancyCount(2)); + assertEquals(0, ca.discrepancyCount(-1)); + assertEquals(0, ca.discrepancyCount(-2)); + + assertEquals(0, testUtils.doubleComparator.compare( + risk.doubleValue(), ca.getRiskLimit().doubleValue())); + + assertEquals(0, testUtils.doubleComparator.compare( + dilutedMargin, ca.getDilutedMargin().doubleValue())); + } +} diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/TestClassWithDatabase.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/TestClassWithDatabase.java new file mode 100644 index 00000000..98cc540c --- /dev/null +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/TestClassWithDatabase.java @@ -0,0 +1,89 @@ +/* +Democracy Developers IRV extensions to colorado-rla. + +@copyright 2024 Colorado Department of State + +These IRV extensions are designed to connect to a running instance of the raire +service (https://github.com/DemocracyDevelopers/raire-service), in order to +generate assertions that can be audited using colorado-rla. + +The colorado-rla IRV extensions are free software: you can redistribute it and/or modify it under the terms +of the GNU Affero General Public License as published by the Free Software Foundation, either +version 3 of the License, or (at your option) any later version. + +The colorado-rla IRV extensions are distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +See the GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with +raire-service. If not, see . +*/ + +package au.org.democracydevelopers.corla.util; + +import java.util.List; +import java.util.Properties; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import us.freeandfair.corla.persistence.Persistence; + +/** + * This class is designed to be extended by any test class that needs to interact with a test + * instantiation of the colorado-rla database. It provides convenience methods for instantiating + * a postgres container (initialised with one a given SQL script) and hibernate properties. + */ +public abstract class TestClassWithDatabase { + + /** + * Begin a new transaction before each test method in the class is run. + */ + @BeforeMethod + public static void beforeTest(){ + Persistence.beginTransaction(); + } + + /** + * Rollback any changes to the (test) database after each test method is run. + */ + @AfterMethod + public static void afterTest(){ + try { + Persistence.rollbackTransaction(); + } catch (Exception ignored) { + } + } + + /** + * Create and return a postgres test container for the purposes of testing functionality that + * interacts with the database. + * @return a postgres test container representing a test database. + */ + public static PostgreSQLContainer createTestContainer() { + return new PostgreSQLContainer<>("postgres:15-alpine") + // None of these actually have to be the same as the real database (except its name), + // but this makes it easy to match the setup scripts. + .withDatabaseName("corla") + .withUsername("corlaadmin") + .withPassword("corlasecret") + .withInitScript("SQL/corla.sql"); + } + + /** + * Create and return a hibernate properties object for use in testing functionality that + * interacts with the database. + * @param postgres Postgres test container representing a test version of the database. + * @return Hibernate persistence properties. + */ + public static Properties createHibernateProperties(PostgreSQLContainer postgres) { + Properties hibernateProperties = new Properties(); + hibernateProperties.setProperty("hibernate.driver", "org.postgresql.Driver"); + hibernateProperties.setProperty("hibernate.url", postgres.getJdbcUrl()); + hibernateProperties.setProperty("hibernate.user", postgres.getUsername()); + hibernateProperties.setProperty("hibernate.pass", postgres.getPassword()); + hibernateProperties.setProperty("hibernate.dialect", "org.hibernate.dialect.PostgreSQL9Dialect"); + + return hibernateProperties; + } + +} diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/testUtils.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/testUtils.java index 19ab2594..c8a43466 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/testUtils.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/testUtils.java @@ -22,6 +22,9 @@ package au.org.democracydevelopers.corla.util; import org.apache.log4j.Logger; +/** + * This class contains utilities for use in testing. + */ public class testUtils { /** @@ -60,5 +63,4 @@ public class testUtils { public static void log(Logger logger, String test){ logger.debug(String.format("RUNNING TEST: %s.",test)); } - } diff --git a/server/eclipse-project/src/test/resources/SQL/corla.sql b/server/eclipse-project/src/test/resources/SQL/corla.sql new file mode 100644 index 00000000..b3984d7d --- /dev/null +++ b/server/eclipse-project/src/test/resources/SQL/corla.sql @@ -0,0 +1,718 @@ +create table asm_state +( + id bigint not null + primary key, + asm_class varchar(255) not null, + asm_identity varchar(255), + state_class varchar(255), + state_value varchar(255), + version bigint +); + +alter table asm_state + owner to corlaadmin; + +create table assertion +( + assertion_type varchar(31) not null, + id bigserial + primary key, + contest_name varchar(255) not null, + current_risk numeric(19, 2) not null, + difficulty double precision not null, + diluted_margin numeric(19, 2) not null, + estimated_samples_to_audit integer not null, + loser varchar(255) not null, + margin integer not null, + one_vote_over_count integer not null, + one_vote_under_count integer not null, + optimistic_samples_to_audit integer not null, + other_count integer not null, + two_vote_over_count integer not null, + two_vote_under_count integer not null, + version bigint, + winner varchar(255) not null +); + +alter table assertion + owner to corlaadmin; + +create table assertion_assumed_continuing +( + id bigint not null + constraint fki0lyp4tghtpohaa9ma6kv2174 + references assertion, + assumed_continuing varchar(255) not null +); + +alter table assertion_assumed_continuing + owner to corlaadmin; + +create table assertion_discrepancies +( + id bigint not null + constraint fkt31yi3mf6c9axmt1gn1mu33ea + references assertion, + discrepancy integer not null, + cvr_id bigint not null, + primary key (id, cvr_id) +); + +alter table assertion_discrepancies + owner to corlaadmin; + +create table ballot_manifest_info +( + id bigint not null + primary key, + batch_id varchar(255) not null, + batch_size integer not null, + county_id bigint not null, + scanner_id integer not null, + sequence_end bigint not null, + sequence_start bigint not null, + storage_location varchar(255) not null, + version bigint, + ultimate_sequence_end bigint, + ultimate_sequence_start bigint, + uri varchar(255) +); + +alter table ballot_manifest_info + owner to corlaadmin; + +create index idx_bmi_county + on ballot_manifest_info (county_id); + +create index idx_bmi_seqs + on ballot_manifest_info (sequence_start, sequence_end); + +create table cast_vote_record +( + id bigint not null + primary key, + audit_board_index integer, + comment varchar(255), + cvr_id bigint, + ballot_type varchar(255) not null, + batch_id varchar(255) not null, + county_id bigint not null, + cvr_number integer not null, + imprinted_id varchar(255) not null, + record_id integer not null, + record_type varchar(255) not null, + scanner_id integer not null, + sequence_number integer, + timestamp timestamp, + version bigint, + rand integer, + revision bigint, + round_number integer, + uri varchar(255), + constraint uniquecvr + unique (county_id, imprinted_id, record_type, revision) +); + +alter table cast_vote_record + owner to corlaadmin; + +create index idx_cvr_county_type + on cast_vote_record (county_id, record_type); + +create index idx_cvr_county_cvr_number + on cast_vote_record (county_id, cvr_number); + +create index idx_cvr_county_cvr_number_type + on cast_vote_record (county_id, cvr_number, record_type); + +create index idx_cvr_county_sequence_number_type + on cast_vote_record (county_id, sequence_number, record_type); + +create index idx_cvr_county_imprinted_id_type + on cast_vote_record (county_id, imprinted_id, record_type); + +create index idx_cvr_uri + on cast_vote_record (uri); + +create table contest_result +( + id bigint not null + primary key, + audit_reason integer, + ballot_count bigint, + contest_name varchar(255) not null + constraint idx_cr_contest + unique, + diluted_margin numeric(19, 2), + losers text, + max_margin integer, + min_margin integer, + version bigint, + winners text, + winners_allowed integer +); + +alter table contest_result + owner to corlaadmin; + +create table comparison_audit +( + audit_type varchar(31) not null, + id bigint not null + primary key, + contest_cvr_ids text, + diluted_margin numeric(10, 8) not null, + audit_reason varchar(255) not null, + audit_status varchar(255) not null, + audited_sample_count integer not null, + disagreement_count integer not null, + estimated_recalculate_needed boolean not null, + estimated_samples_to_audit integer not null, + gamma numeric(10, 8) not null, + one_vote_over_count integer not null, + one_vote_under_count integer not null, + optimistic_recalculate_needed boolean not null, + optimistic_samples_to_audit integer not null, + other_count integer not null, + risk_limit numeric(10, 8) not null, + two_vote_over_count integer not null, + two_vote_under_count integer not null, + version bigint, + overstatements numeric(19, 2), + contest_result_id bigint not null + constraint fkn14qkca2ilirtpr4xctw960pe + references contest_result +); + +alter table comparison_audit + owner to corlaadmin; + +create table audit_to_assertions +( + id bigint not null + constraint fkgrx2l2qywbc3nv83iid55ql36 + references comparison_audit, + assertions_id bigint not null + constraint fkqomhyyib2xno6nq0wjpv95fs5 + references assertion +); + +alter table audit_to_assertions + owner to corlaadmin; + +create table contest_vote_total +( + result_id bigint not null + constraint fkfjk25vmtng6dv2ejlp8eopy34 + references contest_result, + vote_total integer, + choice varchar(255) not null, + primary key (result_id, choice) +); + +alter table contest_vote_total + owner to corlaadmin; + +create table county +( + id bigint not null + primary key, + name varchar(255) not null + constraint uk_npkepig28dujo4w98bkmaclhp + unique, + version bigint +); + +alter table county + owner to corlaadmin; + +create table administrator +( + id bigint not null + primary key, + full_name varchar(255) not null, + last_login_time timestamp, + last_logout_time timestamp, + type varchar(255) not null, + username varchar(255) not null + constraint uk_esogmqxeek1uwdyhxvubme3qf + unique, + version bigint, + county_id bigint + constraint fkh6rcfib1ishmhry9ctgm16gie + references county +); + +alter table administrator + owner to corlaadmin; + +create table contest +( + id bigint not null + primary key, + description varchar(255) not null, + name varchar(255) not null, + sequence_number integer not null, + version bigint, + votes_allowed integer not null, + winners_allowed integer not null, + county_id bigint not null + constraint fk932jeyl0hqd21fmakkco5tfa3 + references county, + constraint ukdv45ptogm326acwp45hm46uaf + unique (name, county_id, description, votes_allowed) +); + +alter table contest + owner to corlaadmin; + +create index idx_contest_name + on contest (name); + +create index idx_contest_name_county_description_votes_allowed + on contest (name, county_id, description, votes_allowed); + +create table contest_choice +( + contest_id bigint not null + constraint fknsr30axyiavqhyupxohtfy0sl + references contest, + description varchar(255), + fictitious boolean not null, + name varchar(255), + qualified_write_in boolean not null, + index integer not null, + primary key (contest_id, index), + constraint uka8o6q5yeepuy2cgnrbx3l1rka + unique (contest_id, name) +); + +alter table contest_choice + owner to corlaadmin; + +create table contests_to_contest_results +( + contest_result_id bigint not null + constraint fkr1jgmnxu2fbbvujdh3srjmot9 + references contest_result, + contest_id bigint not null + constraint uk_t1qahmm5y32ovxtqxne8i7ou0 + unique + constraint fki7qed7v0pkbi2bnd5fvujtp7 + references contest, + primary key (contest_result_id, contest_id) +); + +alter table contests_to_contest_results + owner to corlaadmin; + +create table counties_to_contest_results +( + contest_result_id bigint not null + constraint fk2h2muw290os109yqar5p4onms + references contest_result, + county_id bigint not null + constraint fk1ke574b6yqdc8ylu5xyqrounp + references county, + primary key (contest_result_id, county_id) +); + +alter table counties_to_contest_results + owner to corlaadmin; + +create table county_contest_result +( + id bigint not null + primary key, + contest_ballot_count integer, + county_ballot_count integer, + losers text, + max_margin integer, + min_margin integer, + version bigint, + winners text, + winners_allowed integer not null, + contest_id bigint not null + constraint fkon2wldpt0279jqex3pjx1mhm7 + references contest, + county_id bigint not null + constraint fkcuw4fb39imk9pyw360bixorm3 + references county, + constraint idx_ccr_county_contest + unique (county_id, contest_id) +); + +alter table county_contest_result + owner to corlaadmin; + +create index idx_ccr_county + on county_contest_result (county_id); + +create index idx_ccr_contest + on county_contest_result (contest_id); + +create table county_contest_vote_total +( + result_id bigint not null + constraint fkip5dfccmp5x5ubssgar17qpwk + references county_contest_result, + vote_total integer, + choice varchar(255) not null, + primary key (result_id, choice) +); + +alter table county_contest_vote_total + owner to corlaadmin; + +create table cvr_audit_info +( + id bigint not null + primary key, + count_by_contest text, + multiplicity_by_contest text, + disagreement text not null, + discrepancy text not null, + version bigint, + acvr_id bigint + constraint fk2n0rxgwa4njtnsm8l4hwc8khy + references cast_vote_record, + cvr_id bigint not null + constraint fkdks3q3g0srpa44rkkoj3ilve6 + references cast_vote_record +); + +alter table cvr_audit_info + owner to corlaadmin; + +create table contest_comparison_audit_disagreement +( + contest_comparison_audit_id bigint not null + constraint fkt490by57jb58ubropwn7kmadi + references comparison_audit, + cvr_audit_info_id bigint not null + constraint fkpfdns930t0qv905vbwhgcxnl2 + references cvr_audit_info, + primary key (contest_comparison_audit_id, cvr_audit_info_id) +); + +alter table contest_comparison_audit_disagreement + owner to corlaadmin; + +create table contest_comparison_audit_discrepancy +( + contest_comparison_audit_id bigint not null + constraint fkcajmftu1xv4jehnm5qhc35j9n + references comparison_audit, + discrepancy integer, + cvr_audit_info_id bigint not null + constraint fk3la5frd86i29mlwjd8akjgpwp + references cvr_audit_info, + primary key (contest_comparison_audit_id, cvr_audit_info_id) +); + +alter table contest_comparison_audit_discrepancy + owner to corlaadmin; + +create table cvr_contest_info +( + cvr_id bigint not null + constraint fkrsovkqe4e839e0aels78u7a3g + references cast_vote_record, + county_id bigint, + choices varchar(1024), + comment varchar(255), + consensus varchar(255), + contest_id bigint not null + constraint fke2fqsfmj0uqq311l4c3i0nt7r + references contest, + index integer not null, + primary key (cvr_id, index) +); + +alter table cvr_contest_info + owner to corlaadmin; + +create index idx_cvrci_uri + on cvr_contest_info (county_id, contest_id); + +create table dos_dashboard +( + id bigint not null + primary key, + canonical_choices text, + canonical_contests text, + election_date timestamp, + election_type varchar(255), + public_meeting_date timestamp, + risk_limit numeric(10, 8), + seed varchar(255), + version bigint +); + +alter table dos_dashboard + owner to corlaadmin; + +create table contest_to_audit +( + dashboard_id bigint not null + constraint fkjlw9bpyarqou0j26hq7mmq8qm + references dos_dashboard, + audit varchar(255), + contest_id bigint not null + constraint fkid09bdp5ifs6m4cnyw3ycyo1s + references contest, + reason varchar(255) +); + +alter table contest_to_audit + owner to corlaadmin; + +create table log +( + id bigint not null + primary key, + authentication_data varchar(255), + client_host varchar(255), + hash varchar(255) not null, + information varchar(255) not null, + result_code integer, + timestamp timestamp not null, + version bigint, + previous_entry bigint + constraint fkfw6ikly73lha9g9em13n3kat4 + references log +); + +alter table log + owner to corlaadmin; + +create table tribute +( + id bigint not null + primary key, + ballot_position integer, + batch_id varchar(255), + contest_name varchar(255), + county_id bigint, + rand integer, + rand_sequence_position integer, + scanner_id integer, + uri varchar(255), + version bigint +); + +alter table tribute + owner to corlaadmin; + +create table uploaded_file +( + id bigint not null + primary key, + computed_hash varchar(255) not null, + approximate_record_count integer not null, + file oid not null, + filename varchar(255), + size bigint not null, + timestamp timestamp not null, + version bigint, + result text, + status varchar(255) not null, + submitted_hash varchar(255) not null, + county_id bigint not null + constraint fk8gh92iwaes042cc1uvi6714yj + references county +); + +alter table uploaded_file + owner to corlaadmin; + +create table county_dashboard +( + id bigint not null + primary key, + audit_board_count integer, + driving_contests text, + audit_timestamp timestamp, + audited_prefix_length integer, + audited_sample_count integer, + ballots_audited integer not null, + ballots_in_manifest integer not null, + current_round_index integer, + cvr_import_error_message varchar(255), + cvr_import_state varchar(255), + cvr_import_timestamp timestamp, + cvrs_imported integer not null, + disagreements text not null, + discrepancies text not null, + version bigint, + county_id bigint not null + constraint uk_6lcjowb4rw9xav8nqnf5v2klk + unique + constraint fk1bg939xcuwen7fohfkdx10ueb + references county, + cvr_file_id bigint + constraint fk6rb04heyw700ep1ynn0r31xv3 + references uploaded_file, + manifest_file_id bigint + constraint fkrs4q3gwfv0up7swx7q1q6xlwo + references uploaded_file +); + +alter table county_dashboard + owner to corlaadmin; + +create table audit_board +( + dashboard_id bigint not null + constraint fkai07es6t6bdw8hidapxxa5xnp + references county_dashboard, + members text, + sign_in_time timestamp not null, + sign_out_time timestamp, + index integer not null, + primary key (dashboard_id, index) +); + +alter table audit_board + owner to corlaadmin; + +create table audit_intermediate_report +( + dashboard_id bigint not null + constraint fkmvj30ou8ik3u7avvycsw0vjx8 + references county_dashboard, + report varchar(255), + timestamp timestamp, + index integer not null, + primary key (dashboard_id, index) +); + +alter table audit_intermediate_report + owner to corlaadmin; + +create table audit_investigation_report +( + dashboard_id bigint not null + constraint fkdox65w3y11hyhtcba5hrekq9u + references county_dashboard, + name varchar(255), + report varchar(255), + timestamp timestamp, + index integer not null, + primary key (dashboard_id, index) +); + +alter table audit_investigation_report + owner to corlaadmin; + +create table county_contest_comparison_audit +( + id bigint not null + primary key, + diluted_margin numeric(10, 8) not null, + audit_reason varchar(255) not null, + audit_status varchar(255) not null, + audited_sample_count integer not null, + disagreement_count integer not null, + estimated_recalculate_needed boolean not null, + estimated_samples_to_audit integer not null, + gamma numeric(10, 8) not null, + one_vote_over_count integer not null, + one_vote_under_count integer not null, + optimistic_recalculate_needed boolean not null, + optimistic_samples_to_audit integer not null, + other_count integer not null, + risk_limit numeric(10, 8) not null, + two_vote_over_count integer not null, + two_vote_under_count integer not null, + version bigint, + contest_id bigint not null + constraint fk8te9gv7q10wxbhg5pgttbj3mv + references contest, + contest_result_id bigint not null + constraint fkag9u8fyqni2ehb2dtqop4pox8 + references contest_result, + dashboard_id bigint not null + constraint fksycb9uto400qabgb97d4ihbat + references county_dashboard +); + +alter table county_contest_comparison_audit + owner to corlaadmin; + +create index idx_ccca_dashboard + on county_contest_comparison_audit (dashboard_id); + +create table county_contest_comparison_audit_disagreement +( + county_contest_comparison_audit_id bigint not null + constraint fk7yt9a4fjcdctwmftwwsksdnma + references county_contest_comparison_audit, + cvr_audit_info_id bigint not null + constraint fk9lhehe4o2dgqde06pxycydlu6 + references cvr_audit_info, + primary key (county_contest_comparison_audit_id, cvr_audit_info_id) +); + +alter table county_contest_comparison_audit_disagreement + owner to corlaadmin; + +create table county_contest_comparison_audit_discrepancy +( + county_contest_comparison_audit_id bigint not null + constraint fk39q8rjoa19c4fdjmv4m9iir06 + references county_contest_comparison_audit, + discrepancy integer, + cvr_audit_info_id bigint not null + constraint fkpe25737bc4mpt170y53ba7il2 + references cvr_audit_info, + primary key (county_contest_comparison_audit_id, cvr_audit_info_id) +); + +alter table county_contest_comparison_audit_discrepancy + owner to corlaadmin; + +create table county_dashboard_to_comparison_audit +( + dashboard_id bigint not null + constraint fkds9j4o8el1f4nepf2677hvs5o + references county_dashboard, + comparison_audit_id bigint not null + constraint fksliko6ckjcr7wvmicuqyreopl + references comparison_audit, + primary key (dashboard_id, comparison_audit_id) +); + +alter table county_dashboard_to_comparison_audit + owner to corlaadmin; + +create table round +( + dashboard_id bigint not null + constraint fke3kvxe5r43a4xmeugp8lnme9e + references county_dashboard, + ballot_sequence_assignment text not null, + actual_audited_prefix_length integer, + actual_count integer not null, + audit_subsequence text not null, + ballot_sequence text not null, + disagreements text not null, + discrepancies text not null, + end_time timestamp, + expected_audited_prefix_length integer not null, + expected_count integer not null, + number integer not null, + previous_ballots_audited integer not null, + signatories text, + start_audited_prefix_length integer not null, + start_time timestamp not null, + index integer not null, + primary key (dashboard_id, index) +); + +alter table round + owner to corlaadmin; + +create index idx_uploaded_file_county + on uploaded_file (county_id); + diff --git a/server/eclipse-project/src/test/resources/SQL/simple-assertions.sql b/server/eclipse-project/src/test/resources/SQL/simple-assertions.sql new file mode 100644 index 00000000..822638cb --- /dev/null +++ b/server/eclipse-project/src/test/resources/SQL/simple-assertions.sql @@ -0,0 +1,31 @@ +-- Simple Assertions for testing Retrieval/Deletion +INSERT INTO county (id, name) VALUES (1,'One NEB Assertion County'); +INSERT INTO contest (county_id, id, version, description, name, sequence_number, votes_allowed, winners_allowed) VALUES (1,1,0,'IRV','One NEB Assertion Contest',1,5,1); +INSERT INTO assertion (assertion_type, contest_name, difficulty, diluted_margin, loser, margin, current_risk, estimated_samples_to_audit, one_vote_over_count, one_vote_under_count, optimistic_samples_to_audit, other_count, two_vote_over_count, two_vote_under_count, version, winner) values ('NEB', 'One NEB Assertion Contest', 3.125, 0.32, 'Bob', 320, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice'); + +INSERT INTO county (id, name) VALUES (2,'One NEN Assertion County'); +INSERT INTO contest (county_id, id, version, description, name, sequence_number, votes_allowed, winners_allowed) VALUES (2,2,0,'IRV','One NEN Assertion Contest',2,4,1); +INSERT INTO assertion (assertion_type, contest_name, difficulty, diluted_margin, loser, margin, current_risk, estimated_samples_to_audit, one_vote_over_count, one_vote_under_count, optimistic_samples_to_audit, other_count, two_vote_over_count, two_vote_under_count, version, winner) values ('NEN', 'One NEN Assertion Contest', 8.33, 0.12, 'Charlie', 240, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice'); +INSERT INTO assertion_assumed_continuing values (2, 'Alice'); +INSERT INTO assertion_assumed_continuing values (2, 'Charlie'); +INSERT INTO assertion_assumed_continuing values (2, 'Diego'); +INSERT INTO assertion_assumed_continuing values (2, 'Bob'); + +INSERT INTO county (id, name) VALUES (3,'One NEN NEB Assertion County'); +INSERT INTO contest (county_id, id, version, description, name, sequence_number, votes_allowed, winners_allowed) VALUES (3,3,0,'IRV','One NEN NEB Assertion Contest',3,4,1); +INSERT INTO assertion (assertion_type, contest_name, difficulty, diluted_margin, loser, margin, current_risk, estimated_samples_to_audit, one_vote_over_count, one_vote_under_count, optimistic_samples_to_audit, other_count, two_vote_over_count, two_vote_under_count, version, winner) values ('NEB', 'One NEN NEB Assertion Contest', 10, 0.1, 'Liesl', 112, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Amanda'); + +INSERT INTO assertion (assertion_type, contest_name, difficulty, diluted_margin, loser, margin, current_risk, estimated_samples_to_audit, one_vote_over_count, one_vote_under_count, optimistic_samples_to_audit, other_count, two_vote_over_count, two_vote_under_count, version, winner) values ('NEN', 'One NEN NEB Assertion Contest', 2, 0.5, 'Wendell', 560, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Amanda'); +INSERT INTO assertion_assumed_continuing values (4, 'Liesl'); +INSERT INTO assertion_assumed_continuing values (4, 'Wendell'); +INSERT INTO assertion_assumed_continuing values (4, 'Amanda'); + + +INSERT INTO contest (county_id, id, version, description, name, sequence_number, votes_allowed, winners_allowed) VALUES (2,4,0,'IRV','Multi-County Contest 1',4,4,1); +INSERT INTO contest (county_id, id, version, description, name, sequence_number, votes_allowed, winners_allowed) VALUES (3,5,0,'IRV','Multi-County Contest 1',5,4,1); +INSERT INTO assertion (assertion_type, contest_name, difficulty, diluted_margin, loser, margin, current_risk, estimated_samples_to_audit, one_vote_over_count, one_vote_under_count, optimistic_samples_to_audit, other_count, two_vote_over_count, two_vote_under_count, version, winner) values ('NEB', 'Multi-County Contest 1', 100, 0.01, 'Alice P. Mangrove', 310, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Charlie C. Chaplin'); +INSERT INTO assertion (assertion_type, contest_name, difficulty, diluted_margin, loser, margin, current_risk, estimated_samples_to_audit, one_vote_over_count, one_vote_under_count, optimistic_samples_to_audit, other_count, two_vote_over_count, two_vote_under_count, version, winner) values ('NEB', 'Multi-County Contest 1', 14.3, 0.07, 'Al (Bob) Jones', 2170, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice P. Mangrove'); + +INSERT INTO assertion (assertion_type, contest_name, difficulty, diluted_margin, loser, margin, current_risk, estimated_samples_to_audit, one_vote_over_count, one_vote_under_count, optimistic_samples_to_audit, other_count, two_vote_over_count, two_vote_under_count, version, winner) values ('NEN', 'Multi-County Contest 1', 1000, 0.001, 'West W. Westerson', 31, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice P. Mangrove'); +INSERT INTO assertion_assumed_continuing values (7, 'West W. Westerson'); +INSERT INTO assertion_assumed_continuing values (7, 'Alice P. Mangrove'); \ No newline at end of file From 3e0dc15406cfc9b75f573462d535fc6dd5c821ba Mon Sep 17 00:00:00 2001 From: michelleblom Date: Fri, 21 Jun 2024 17:18:33 +1000 Subject: [PATCH 08/12] Added methods to support testing of IRVComparisonAudit construction. --- .../corla/model/assertion/Assertion.java | 14 ++++ .../corla/model/assertion/NEBAssertion.java | 11 +++ .../corla/model/assertion/NENAssertion.java | 10 +++ .../corla/model/IRVComparisonAuditTests.java | 81 ++++++++++++++++++- .../corla/util/TestClassWithDatabase.java | 1 - 5 files changed, 114 insertions(+), 3 deletions(-) diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java index c32faeef..edf21e8b 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java @@ -27,6 +27,7 @@ import java.math.MathContext; import java.math.RoundingMode; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -253,6 +254,19 @@ public BigDecimal getDilutedMargin(){ return dilutedMargin; } + /** + * Get an (unmodifiable) map of the assertion's CRV ID-discrepancy records. This is used + * for testing purposes. + */ + public Map getCvrDiscrepancy(){ + return Collections.unmodifiableMap(cvrDiscrepancy); + } + + /** + * Return a string representation of a subset of this assertion's data. This is used for + * testing purposes. + */ + public abstract String getDescription(); /** * Given a number of audited samples, and the total number of overstatements experienced thus far, diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertion.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertion.java index 49ae13c2..b190f00a 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertion.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertion.java @@ -77,4 +77,15 @@ else if(loser_index != -1 && (winner_index == -1 || loser_index < winner_index)) prefix, score, id(), contestName, info.choices())); return score; } + + + /** + * {@inheritDoc} + */ + public String getDescription(){ + return String.format("%s NEB %s: oneOver = %d; two Over = %d; oneUnder = %d, twoUnder = %d; " + + "other = %d; optimistic = %d; estimated = %d; risk %f.", winner, loser, oneVoteOverCount, + twoVoteOverCount, oneVoteUnderCount, twoVoteUnderCount, otherCount, optimisticSamplesToAudit, + estimatedSamplesToAudit, currentRisk); + } } diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/NENAssertion.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/NENAssertion.java index c547c3c3..6594cecb 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/NENAssertion.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/NENAssertion.java @@ -82,4 +82,14 @@ else if (choices_left.get(0).equals(loser)) { return score; } + /** + * {@inheritDoc} + */ + public String getDescription(){ + return String.format("%s NEN %s assuming (%s) are continuing: oneOver = %d; two Over = %d; " + + "oneUnder = %d, twoUnder = %d; other = %d; optimistic = %d; estimated = %d; risk %f.", + winner, loser, assumedContinuing, oneVoteOverCount, twoVoteOverCount, oneVoteUnderCount, + twoVoteUnderCount, otherCount, optimisticSamplesToAudit, estimatedSamplesToAudit, currentRisk); + } + } diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/IRVComparisonAuditTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/IRVComparisonAuditTests.java index e2189290..3104cb8c 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/IRVComparisonAuditTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/IRVComparisonAuditTests.java @@ -31,7 +31,9 @@ import au.org.democracydevelopers.corla.util.TestClassWithDatabase; import au.org.democracydevelopers.corla.util.testUtils; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.List; +import java.util.Map; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import org.mockito.Mock; @@ -157,9 +159,12 @@ public void testCreateIRVAuditOneNEBAssertion(){ final List assertions = ca.getAssertions(); assertEquals(1, assertions.size()); - assert(assertions.get(0) instanceof NEBAssertion); + checkNEBAssertion(assertions.get(0), "Alice", "Bob", 0, 0, 0, + 0, 0, 23, 23, 1, Map.of(), 0.32); } + + /** * Create an IRVComparisonAudit for a contest with one NEN assertion stored in the database. This * assertion has a diluted margin of 0.32. @@ -176,7 +181,9 @@ public void testCreateIRVAuditOneNENAssertion(){ final List assertions = ca.getAssertions(); assertEquals(1, assertions.size()); - assert(assertions.get(0) instanceof NENAssertion); + checkNENAssertion(assertions.get(0), "Alice", "Charlie", List.of("Alice","Charlie", + "Diego","Bob"), 0, 0, 0, 0, 0, 61, + 61, 1, Map.of(), 0.12); } /** @@ -208,4 +215,74 @@ private void checkIRVComparisonAudit(final IRVComparisonAudit ca, final BigDecim assertEquals(0, testUtils.doubleComparator.compare( dilutedMargin, ca.getDilutedMargin().doubleValue())); } + + /** + * Check that the given NEB assertion has the given characteristics. + * @param a Assertion to check. + * @param winner Expected winner of the assertion. + * @param loser Expected loser of the assertion. + * @param oneOver Expected number of one vote overstatements. + * @param twoOver Expected number of two vote overstatements. + * @param oneUnder Expected number of one vote understatements. + * @param twoUnder Expected number of two vote understatements. + * @param other Expected number of "other" discrepancies. + * @param optimistic Expected optimistic sample size. + * @param estimated Expected estimated sample size. + * @param risk Expected current risk of the assertion. + * @param cvrDiscrepancy Expected CVR ID-discrepancy map. + * @param dilutedMargin Expected diluted margin. + */ + private void checkNEBAssertion(final Assertion a, final String winner, final String loser, + int oneOver, int twoOver, int oneUnder, int twoUnder, int other, int optimistic, + int estimated, double risk, final Map cvrDiscrepancy, double dilutedMargin){ + + assert(a instanceof NEBAssertion); + + final String expected = String.format("%s NEB %s: oneOver = %d; two Over = %d; oneUnder = %d, " + + "twoUnder = %d; other = %d; optimistic = %d; estimated = %d; risk %f.", winner, loser, + oneOver, twoOver, oneUnder, twoUnder, other, optimistic, estimated, + BigDecimal.valueOf(risk).setScale(Assertion.RISK_DECIMALS, RoundingMode.HALF_UP)); + + assertEquals(expected, a.getDescription()); + assertEquals(cvrDiscrepancy, a.getCvrDiscrepancy()); + + assertEquals(0, testUtils.doubleComparator.compare(dilutedMargin, + a.getDilutedMargin().doubleValue())); + } + + /** + * Check that the given NEN assertion has the given characteristics. + * @param a Assertion to check. + * @param winner Expected winner of the assertion. + * @param loser Expected loser of the assertion. + * @param continuing Expected list of assumed continuing candidates. + * @param oneOver Expected number of one vote overstatements. + * @param twoOver Expected number of two vote overstatements. + * @param oneUnder Expected number of one vote understatements. + * @param twoUnder Expected number of two vote understatements. + * @param other Expected number of "other" discrepancies. + * @param optimistic Expected optimistic sample size. + * @param estimated Expected estimated sample size. + * @param risk Expected current risk of the assertion. + * @param cvrDiscrepancy Expected CVR ID-discrepancy map. + * @param dilutedMargin Expected diluted margin. + */ + private void checkNENAssertion(final Assertion a, final String winner, final String loser, + final List continuing, int oneOver, int twoOver, int oneUnder, int twoUnder, int other, + int optimistic, int estimated, double risk, final Map cvrDiscrepancy, double dilutedMargin){ + + assert(a instanceof NENAssertion); + + final String expected = String.format("%s NEN %s assuming (%s) are continuing: oneOver = %d; " + + "two Over = %d; oneUnder = %d, twoUnder = %d; other = %d; optimistic = %d; estimated = %d; " + + "risk %f.", winner, loser, continuing, oneOver, twoOver, oneUnder, twoUnder, other, + optimistic, estimated, BigDecimal.valueOf(risk).setScale(Assertion.RISK_DECIMALS, + RoundingMode.HALF_UP)); + + assertEquals(expected, a.getDescription()); + assertEquals(cvrDiscrepancy, a.getCvrDiscrepancy()); + + assertEquals(0, testUtils.doubleComparator.compare(dilutedMargin, + a.getDilutedMargin().doubleValue())); + } } diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/TestClassWithDatabase.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/TestClassWithDatabase.java index 98cc540c..f036e6fd 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/TestClassWithDatabase.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/util/TestClassWithDatabase.java @@ -21,7 +21,6 @@ package au.org.democracydevelopers.corla.util; -import java.util.List; import java.util.Properties; import org.testcontainers.containers.PostgreSQLContainer; import org.testng.annotations.AfterMethod; From 8fb7ead342eb6b067ba5a5880539453591b8d77e Mon Sep 17 00:00:00 2001 From: michelleblom Date: Fri, 21 Jun 2024 17:19:15 +1000 Subject: [PATCH 09/12] Removed unused import. --- .../democracydevelopers/corla/model/assertion/NENAssertion.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/NENAssertion.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/NENAssertion.java index 6594cecb..e3298a6c 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/NENAssertion.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/NENAssertion.java @@ -22,7 +22,6 @@ package au.org.democracydevelopers.corla.model.assertion; import java.util.List; -import java.util.stream.Collectors; import javax.persistence.DiscriminatorValue; import javax.persistence.Entity; import org.apache.log4j.LogManager; From 4da18fa98a36f607fcae6814803ff1b01395f6b6 Mon Sep 17 00:00:00 2001 From: michelleblom Date: Sat, 22 Jun 2024 20:06:36 +1000 Subject: [PATCH 10/12] Added further tests of IRVComparisonAudit construction. Added annotations to BigDecimals in Assertion to have the same precision and scale as those in ComparisonAudit. Updated corla.sql in the test resources with the new database schema. --- .../corla/model/IRVComparisonAudit.java | 5 ++ .../corla/model/assertion/Assertion.java | 12 ++-- .../corla/model/IRVComparisonAuditTests.java | 65 ++++++++++++++++++- .../src/test/resources/SQL/corla.sql | 19 +++++- 4 files changed, 92 insertions(+), 9 deletions(-) diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java index 144b9a66..9bcfd089 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java @@ -154,6 +154,11 @@ public IRVComparisonAudit(final ContestResult contestResult, final BigDecimal ri // they will not have been computed correctly for IRV. optimisticSamplesToAudit(); estimatedSamplesToAudit(); + + LOGGER.debug(String.format("%s Created IRVComparisonAudit for contest %s: status = %s; " + + "reason = %s; diluted margin = %f; assertions = %d; optimistic = %d; estimated = %d", + prefix, contestName, auditStatus(), auditReason(), diluted_margin, assertions.size(), + my_optimistic_samples_to_audit, my_estimated_samples_to_audit)); } } catch(Exception e){ final String msg = String.format("%s An unexpected error arose during diluted margin " + diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java index edf21e8b..8157f22b 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java @@ -42,6 +42,7 @@ import us.freeandfair.corla.model.CVRContestInfo.ConsensusValue; import us.freeandfair.corla.model.CastVoteRecord; import us.freeandfair.corla.model.CastVoteRecord.RecordType; +import us.freeandfair.corla.model.ComparisonAudit; import us.freeandfair.corla.persistence.PersistentEntity; /** @@ -130,13 +131,15 @@ public abstract class Assertion implements PersistentEntity { * colorado-rla code base, and the implementation of the methods provided in Audit, we are using a * BigDecimal). */ - @Column(name = "diluted_margin", updatable = false, nullable = false) - protected BigDecimal dilutedMargin = BigDecimal.valueOf(0); + @Column(name = "diluted_margin", updatable = false, nullable = false, + precision = ComparisonAudit.PRECISION, scale = ComparisonAudit.SCALE) + protected BigDecimal dilutedMargin = BigDecimal.ZERO; /** * Assertion difficulty, as estimated by raire-java. */ - @Column(name = "difficulty", updatable = false, nullable = false) + @Column(name = "difficulty", updatable = false, nullable = false, + precision = ComparisonAudit.PRECISION, scale = ComparisonAudit.SCALE) protected double difficulty = 0; /** @@ -213,7 +216,8 @@ public abstract class Assertion implements PersistentEntity { * Current risk of the assertion. We initialize this risk to 1, as when we have no information we * assume maximum risk. */ - @Column(name = "current_risk", nullable = false) + @Column(name = "current_risk", nullable = false, + precision = ComparisonAudit.PRECISION, scale = ComparisonAudit.SCALE) protected BigDecimal currentRisk = BigDecimal.valueOf(1); /** diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/IRVComparisonAuditTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/IRVComparisonAuditTests.java index 3104cb8c..bef3c4bc 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/IRVComparisonAuditTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/IRVComparisonAuditTests.java @@ -79,6 +79,12 @@ public class IRVComparisonAuditTests extends TestClassWithDatabase { @Mock private ContestResult oneNENNEBContestResult; + /** + * Mock of a ContestResult for the contest 'Multi-County Contest 1'. + */ + @Mock + private ContestResult multiCountyContestResult; + /** * Mock of a ContestResult for a contest with no assertions. */ @@ -101,7 +107,7 @@ public static void beforeAll() { * Initialise mocked objects prior to the first test. Note that the diluted margin * returned by ContestResult's for IRV will not have a sensible value, and it will * not be used for IRV computations. For testing purposes, we should set it with - * varied values and ensure that the audit itself is contrycted properly. + * varied values and ensure that the audit itself is contructed properly. */ @BeforeClass public void initMocks() { @@ -112,6 +118,8 @@ public void initMocks() { when(oneNEBContestResult.getDilutedMargin()).thenReturn(BigDecimal.valueOf(0.10)); when(oneNENNEBContestResult.getContestName()).thenReturn("One NEN NEB Assertion Contest"); when(oneNENNEBContestResult.getDilutedMargin()).thenReturn(BigDecimal.valueOf(0.03)); + when(multiCountyContestResult.getContestName()).thenReturn("Multi-County Contest 1"); + when(multiCountyContestResult.getDilutedMargin()).thenReturn(BigDecimal.valueOf(0.001)); when(doesNotExistContestResult.getContestName()).thenReturn("Does Not Exist"); when(doesNotExistContestResult.getDilutedMargin()).thenReturn(BigDecimal.valueOf(0.98)); } @@ -167,7 +175,7 @@ public void testCreateIRVAuditOneNEBAssertion(){ /** * Create an IRVComparisonAudit for a contest with one NEN assertion stored in the database. This - * assertion has a diluted margin of 0.32. + * assertion has a diluted margin of 0.12. */ @Test public void testCreateIRVAuditOneNENAssertion(){ @@ -186,6 +194,59 @@ public void testCreateIRVAuditOneNENAssertion(){ 61, 1, Map.of(), 0.12); } + /** + * Create an IRVComparisonAudit for a contest with one NEN and one NEB assertion stored in the + * database. The smallest diluted margin across these assertions is 0.1. + */ + @Test + public void testCreateIRVAuditOneNENNEBAssertion(){ + testUtils.log(LOGGER, "testCreateIRVAuditOneNENNEBAssertion"); + IRVComparisonAudit ca = new IRVComparisonAudit(oneNENNEBContestResult, AssertionTests.riskLimit3, + AuditReason.COUNTY_WIDE_CONTEST); + + checkIRVComparisonAudit(ca, AssertionTests.riskLimit3, AuditReason.COUNTY_WIDE_CONTEST, + AuditStatus.NOT_STARTED, 0.1); + + final List assertions = ca.getAssertions(); + assertEquals(2, assertions.size()); + + checkNEBAssertion(assertions.get(0), "Amanda", "Liesl", 0, 0, + 0, 0, 0, 73, 73, 1, Map.of(), 0.1); + + checkNENAssertion(assertions.get(1), "Amanda", "Wendell", List.of("Liesl","Wendell", + "Amanda"), 0, 0, 0, 0, 0, 15, + 15, 1, Map.of(), 0.5); + } + + /** + * Create an IRVComparisonAudit for a multi-county contest with two NEBs and one NEN stored in the + * database. The smallest diluted margin across these assertions is 0.1. + */ + @Test + public void testCreateIRVAuditMultiCountyContest(){ + testUtils.log(LOGGER, "testCreateIRVAuditMultiCountyContest"); + IRVComparisonAudit ca = new IRVComparisonAudit(multiCountyContestResult, + AssertionTests.riskLimit3, AuditReason.CLOSE_CONTEST); + + checkIRVComparisonAudit(ca, AssertionTests.riskLimit3, AuditReason.CLOSE_CONTEST, + AuditStatus.NOT_STARTED, 0.001); + + final List assertions = ca.getAssertions(); + assertEquals(3, assertions.size()); + + checkNEBAssertion(assertions.get(0), "Charlie C. Chaplin", "Alice P. Mangrove", + 0, 0, 0, 0, 0, 729, 729, + 1, Map.of(), 0.01); + + checkNEBAssertion(assertions.get(1), "Alice P. Mangrove", "Al (Bob) Jones", + 0, 0, 0, 0, 0, 105, 105, + 1, Map.of(), 0.07); + + checkNENAssertion(assertions.get(2), "Alice P. Mangrove", "West W. Westerson", + List.of("West W. Westerson","Alice P. Mangrove"), 0, 0, 0, + 0, 0, 7287, 7287, 1, Map.of(), 0.001); + } + /** * A method to check that the given IRVComparisonAudit has been initialised properly. * @param ca IRVComparisonAudit to check. diff --git a/server/eclipse-project/src/test/resources/SQL/corla.sql b/server/eclipse-project/src/test/resources/SQL/corla.sql index b3984d7d..5c333011 100644 --- a/server/eclipse-project/src/test/resources/SQL/corla.sql +++ b/server/eclipse-project/src/test/resources/SQL/corla.sql @@ -1,3 +1,16 @@ +create sequence hibernate_sequence; + +alter sequence hibernate_sequence owner to corlaadmin; + +create table assertion_context +( + id bigint not null, + assumed_continuing varchar(255) not null +); + +alter table assertion_context + owner to corlaadmin; + create table asm_state ( id bigint not null @@ -18,9 +31,9 @@ create table assertion id bigserial primary key, contest_name varchar(255) not null, - current_risk numeric(19, 2) not null, + current_risk numeric(10, 8) not null, difficulty double precision not null, - diluted_margin numeric(19, 2) not null, + diluted_margin numeric(10, 8) not null, estimated_samples_to_audit integer not null, loser varchar(255) not null, margin integer not null, @@ -40,7 +53,7 @@ alter table assertion create table assertion_assumed_continuing ( id bigint not null - constraint fki0lyp4tghtpohaa9ma6kv2174 + constraint fk357sixi5a6nt1sus8jdk1pcpn references assertion, assumed_continuing varchar(255) not null ); From 9f97b8267d78704c17e3e018146241b953fca679 Mon Sep 17 00:00:00 2001 From: michelleblom Date: Sun, 23 Jun 2024 17:11:03 +1000 Subject: [PATCH 11/12] Fixed issues identified in review. Abstracted some common code in NEN/NEB Assertion Tests. --- .../corla/model/IRVComparisonAudit.java | 4 +- .../corla/model/assertion/AssertionTests.java | 24 ++ .../model/assertion/NEBAssertionTests.java | 239 +++-------------- .../model/assertion/NENAssertionTests.java | 242 +++--------------- 4 files changed, 110 insertions(+), 399 deletions(-) diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java index 9bcfd089..b996d523 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java @@ -185,7 +185,7 @@ prefix, contestName, auditStatus(), auditReason(), diluted_margin, assertions.si @Override protected void recalculateSamplesToAudit() { if(assertions == null){ - // We have not associated assertions with this audit yet. When an IRVComparisonAudit + // We have not associated assertions with this audit yet. When an IRVComparisonAudit is // constructed, we call the base class constructor first. The ComparisonAudit constructor // calls sample size computation methods. So, this method may end up being called before // the IRVComparisonAudit constructor has completed its setup (retrieving assertions from @@ -530,7 +530,7 @@ public void recordDiscrepancy(final CVRAuditInfo theRecord, final int theType) { // and the flag for indicating that a sample size recalculation is needed, *if* we did // record a discrepancy against at least one of this audit's assertions. if(recorded) { - super.removeDiscrepancy(theRecord, theType); + super.recordDiscrepancy(theRecord, theType); } LOGGER.debug(String.format("%s total number of overstatements (%f), optimistic sample " + "recalculate needed (%s), estimated sample recalculate needed (%s),", prefix, diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/AssertionTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/AssertionTests.java index e9ea6c00..b72c8b8c 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/AssertionTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/AssertionTests.java @@ -483,4 +483,28 @@ public static void checkComputeDiscrepancy(CastVoteRecord cvr, CastVoteRecord au assertEquals(cvrDiscrepancies, a.cvrDiscrepancy); } } + + + /** + * Checks that the discrepancy counts in the given assertion 'a' are equal to the given + * parameters. + * @param a Assertion to check. + * @param oneOver Expected number of one vote overstatements. + * @param oneUnder Expected number of one vote understatements. + * @param twoOver Expected number of two vote overstatements. + * @param twoUnder Expected number of two vote understatements. + * @param other Expected number of other discrepancies. + * @param cvrDiscrepancy Expected internal map of CVR ID to discrepancy type. + */ + public static void checkCountsDiscrepancyMap(final Assertion a, int oneOver, int oneUnder, + int twoOver, int twoUnder, int other, final Map cvrDiscrepancy){ + + assertEquals(a.oneVoteOverCount.intValue(), oneOver); + assertEquals(a.oneVoteUnderCount.intValue(), oneUnder); + assertEquals(a.twoVoteOverCount.intValue(), twoOver); + assertEquals(a.twoVoteUnderCount.intValue(), twoUnder); + assertEquals(a.otherCount.intValue(), other); + + assertEquals(a.cvrDiscrepancy, cvrDiscrepancy); + } } diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java index 602b4b4c..bd3010cd 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NEBAssertionTests.java @@ -23,6 +23,7 @@ import static au.org.democracydevelopers.corla.model.assertion.AssertionTests.TC; import static au.org.democracydevelopers.corla.model.assertion.AssertionTests.checkComputeDiscrepancy; +import static au.org.democracydevelopers.corla.model.assertion.AssertionTests.checkCountsDiscrepancyMap; import static au.org.democracydevelopers.corla.model.assertion.AssertionTests.countsEqual; import static au.org.democracydevelopers.corla.util.testUtils.log; import static org.mockito.Mockito.when; @@ -229,14 +230,7 @@ public void testNEBRecordNoMatch1(){ 8, Map.of(), 0, 0, 0, 0, 0); assertFalse(a.recordDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 0, Map.of()); } /** @@ -255,14 +249,8 @@ public void testNEBRecordNoMatch2(){ 0, 0, 0); assertFalse(a.recordDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(1, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(2L, -1, 3L, 1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 1, 0, 0, 0, + Map.of(2L, -1, 3L, 1)); } /** @@ -279,14 +267,7 @@ public void testNEBRecordOneVoteOverstatement1(){ 0, 0); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 0, 0, 0, Map.of(1L, 1)); } /** @@ -303,14 +284,7 @@ public void testNEBRecordOneVoteUnderstatement1(){ 0, 0); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(1, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, -1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 1, 0, 0, 0, Map.of(1L, -1)); } /** @@ -327,14 +301,7 @@ public void testNEBRecordTwoVoteOverstatement1(){ 0, 0); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(1, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 1, 0, 0, Map.of(1L, 2)); } /** @@ -351,14 +318,7 @@ public void testNEBRecordTwoVoteUnderstatement1(){ 0, 0); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, -2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 1, 0, Map.of(1L, -2)); } /** @@ -375,14 +335,7 @@ public void testNEBRecordOther1(){ 0, 0); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 1, Map.of(1L, 0)); } /** @@ -400,14 +353,8 @@ public void testNEBRecordOneVoteOverstatement2(){ 1, 0, 0, 1, 1); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(2, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 2, 0, 0, 1, 1, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1)); } /** @@ -425,14 +372,8 @@ public void testNEBRecordOneVoteUnderstatement2(){ 1, 0, 0, 1, 1); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(1, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 1, 0, 1, 1, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -1)); } /** @@ -450,14 +391,8 @@ public void testNEBRecordTwoVoteOverstatement2(){ 1, 0, 0, 1, 1); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(1, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 1, 1, 1, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 2)); } /** @@ -475,14 +410,8 @@ public void testNEBRecordTwoVoteUnderstatement2(){ 1, 0, 0, 1, 1); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(2, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 0, 2, 1, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2)); } /** @@ -500,14 +429,8 @@ public void testNEBRecordOther2(){ 1, 0, 0, 1, 1); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(2, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 0, 1, 2, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0)); } /** @@ -529,14 +452,7 @@ public void testNEBRemoveNoMatch1(){ 8, Map.of(), 0, 0, 0, 0, 0); assertFalse(a.removeDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 0, Map.of()); } /** @@ -555,14 +471,8 @@ public void testNEBRemoveNoMatch2(){ 0, 0, 0); assertFalse(a.removeDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(1, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(2L, -1, 3L, 1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 1, 0, 0, 0, + Map.of(2L, -1, 3L, 1)); } /** @@ -579,14 +489,7 @@ public void testNEBRemoveOneVoteOverstatement1(){ 0, 0); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 0, Map.of(1L,1)); } /** @@ -603,14 +506,7 @@ public void testNEBRemoveOneVoteUnderstatement1(){ 0, 0); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, -1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 0, Map.of(1L,-1)); } /** @@ -627,14 +523,7 @@ public void testNEBRemoveTwoVoteOverstatement1(){ 0, 0); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 0, Map.of(1L,2)); } /** @@ -651,14 +540,7 @@ public void testNEBRemoveTwoVoteUnderstatement1(){ 1, 0); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, -2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 0, Map.of(1L,-2)); } /** @@ -675,14 +557,7 @@ public void testNEBRemoveOther1(){ 0, 1); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 0, Map.of(1L,0)); } /** @@ -700,14 +575,8 @@ public void testNEBRemoveOneVoteOverstatement2(){ 2, 0, 0, 1, 1); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 0, 1, 1, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1)); } /** @@ -725,14 +594,8 @@ public void testNEBRemoveOneVoteUnderstatement2(){ 1, 2, 0, 0, 1); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(1, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -1, 4L, -1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 1, 0, 0, 1, + Map.of(1L, 0, 2L, 1, 3L, -1, 4L, -1)); } /** @@ -750,14 +613,8 @@ public void testNEBRemoveTwoVoteOverstatement2(){ 1, 0, 2, 1, 0); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(1, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 2, 2L, 1, 3L, -2, 4L, 2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 1, 1, 0, + Map.of(1L, 2, 2L, 1, 3L, -2, 4L, 2)); } /** @@ -775,14 +632,8 @@ public void testNEBRemoveTwoVoteUnderstatement2(){ 1, 0, 0, 2, 1); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 0, 1, 1, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2)); } /** @@ -800,14 +651,8 @@ public void testNEBRemoveOther2(){ 1, 0, 0, 1, 2); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 0, 1, 1, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0)); } /** @@ -1841,7 +1686,7 @@ public void testNEBReauditBallot3(){ * Test the re-auditing of a ballot where a prior discrepancy is recorded against the * associated CVR (a two vote overstatement). The existing discrepancies associated with the * 'n' copies of the CVR in the sample are removed, but removeDiscrepancy() is called n+1 times - * in error. The n+1'th call the removeDiscrepancy should throw an exception. + * in error. The n+1'th call to the removeDiscrepancy should throw an exception. */ @Test(expectedExceptions = RuntimeException.class) public void testNEBExcessRemovalCausesErrorTwoVoteOver(){ @@ -1865,7 +1710,7 @@ public void testNEBExcessRemovalCausesErrorTwoVoteOver(){ * Test the re-auditing of a ballot where a prior discrepancy is recorded against the * associated CVR (a two vote understatement). The existing discrepancies associated with the * 'n' copies of the CVR in the sample are removed, but removeDiscrepancy() is called n+1 times - * in error. The n+1'th call the removeDiscrepancy should throw an exception. + * in error. The n+1'th call to the removeDiscrepancy should throw an exception. */ @Test(expectedExceptions = RuntimeException.class) public void testNEBExcessRemovalCausesErrorTwoVoteUnder(){ @@ -1889,7 +1734,7 @@ public void testNEBExcessRemovalCausesErrorTwoVoteUnder(){ * Test the re-auditing of a ballot where a prior discrepancy is recorded against the * associated CVR (a one vote understatement). The existing discrepancies associated with the * 'n' copies of the CVR in the sample are removed, but removeDiscrepancy() is called n+1 times - * in error. The n+1'th call the removeDiscrepancy should throw an exception. + * in error. The n+1'th call to the removeDiscrepancy should throw an exception. */ @Test(expectedExceptions = RuntimeException.class) public void testNEBExcessRemovalCausesErrorOneVoteUnder(){ @@ -1913,7 +1758,7 @@ public void testNEBExcessRemovalCausesErrorOneVoteUnder(){ * Test the re-auditing of a ballot where a prior discrepancy is recorded against the * associated CVR (a one vote overstatement). The existing discrepancies associated with the * 'n' copies of the CVR in the sample are removed, but removeDiscrepancy() is called n+1 times - * in error. The n+1'th call the removeDiscrepancy should throw an exception. + * in error. The n+1'th call to the removeDiscrepancy should throw an exception. */ @Test(expectedExceptions = RuntimeException.class) public void testNEBExcessRemovalCausesErrorOneVoteOver(){ @@ -1937,7 +1782,7 @@ public void testNEBExcessRemovalCausesErrorOneVoteOver(){ * Test the re-auditing of a ballot where a prior discrepancy is recorded against the * associated CVR (a one vote overstatement). The existing discrepancies associated with the * 'n' copies of the CVR in the sample are removed, but removeDiscrepancy() is called n+1 times - * in error. The n+1'th call the removeDiscrepancy should throw an exception. + * in error. The n+1'th call to the removeDiscrepancy should throw an exception. */ @Test(expectedExceptions = RuntimeException.class) public void testNEBExcessRemovalCausesErrorOther(){ diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java index a0e7e580..591b20e5 100644 --- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java +++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/model/assertion/NENAssertionTests.java @@ -23,6 +23,7 @@ import static au.org.democracydevelopers.corla.model.assertion.AssertionTests.TC; import static au.org.democracydevelopers.corla.model.assertion.AssertionTests.checkComputeDiscrepancy; +import static au.org.democracydevelopers.corla.model.assertion.AssertionTests.checkCountsDiscrepancyMap; import static au.org.democracydevelopers.corla.model.assertion.AssertionTests.countsEqual; import static au.org.democracydevelopers.corla.util.testUtils.log; import static org.mockito.Mockito.when; @@ -252,16 +253,10 @@ public void testNENRecordNoMatch1(){ 0, 0); assertFalse(a.recordDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 0, Map.of()); } + /** * Test Assertion::recordDiscrepancy(CVRAuditInfo) in the context where no discrepancy has * been computed for the given CVR-ACVR pair, and the assertion has some discrepancies recorded @@ -278,14 +273,8 @@ public void testNENRecordNoMatch2(){ 1, 0, 0, 0); assertFalse(a.recordDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(1, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(2L, -1, 3L, 1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 1, 0, 0, 0, + Map.of(2L, -1, 3L, 1)); } /** @@ -303,14 +292,7 @@ public void testNENRecordOneVoteOverstatement1(){ 0, 0, 0); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 0, 0, 0, Map.of(1L, 1)); } /** @@ -328,14 +310,7 @@ public void testNENRecordOneVoteUnderstatement1(){ 0, 0, 0); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(1, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, -1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 1, 0, 0, 0, Map.of(1L, -1)); } /** @@ -353,14 +328,7 @@ public void testNENRecordTwoVoteOverstatement1(){ 0, 0, 0); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(1, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 1, 0, 0, Map.of(1L, 2)); } /** @@ -378,14 +346,7 @@ public void testNENRecordTwoVoteUnderstatement1(){ 0, 0); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, -2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 1, 0, Map.of(1L, -2)); } /** @@ -403,14 +364,7 @@ public void testNENRecordOther1(){ 0, 0); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 1, Map.of(1L, 0)); } /** @@ -428,14 +382,8 @@ public void testNENRecordOneVoteOverstatement2(){ 1, 0, 0, 1, 1); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(2, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 2, 0, 0, 1, 1, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1)); } /** @@ -453,14 +401,8 @@ public void testNENRecordOneVoteUnderstatement2(){ 1, 0, 0, 1, 1); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(1, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 1, 0, 1, 1, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -1)); } /** @@ -478,14 +420,8 @@ public void testNENRecordTwoVoteOverstatement2(){ 1, 0, 0, 1, 1); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(1, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 1, 1, 1, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 2)); } /** @@ -503,14 +439,8 @@ public void testNENRecordTwoVoteUnderstatement2(){ 1, 0, 0, 1, 1); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(2, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 0, 2, 1, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2)); } /** @@ -528,14 +458,8 @@ public void testNENRecordOther2(){ 1, 0, 0, 1, 1); assertTrue(a.recordDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(2, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 0, 1, 2, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0)); } @@ -560,14 +484,7 @@ public void testNENRemoveNoMatch1(){ 0, 0, 0); assertFalse(a.removeDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 0, Map.of()); } /** @@ -586,14 +503,8 @@ public void testNENRemoveNoMatch2(){ 1, 1, 0, 0, 0); assertFalse(a.removeDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(1, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(2L, -1, 3L, 1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 1, 0, 0, 0, + Map.of(2L, -1, 3L, 1)); } /** @@ -610,14 +521,7 @@ public void testNENRemoveOneVoteOverstatement1(){ 0, 0, 0, 0); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 0, Map.of(1L, 1)); } /** @@ -634,14 +538,7 @@ public void testNENRemoveOneVoteUnderstatement1(){ 1, 0, 0, 0); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, -1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 0, Map.of(1L, -1)); } /** @@ -658,14 +555,7 @@ public void testNENRemoveTwoVoteOverstatement1(){ 0, 1, 0, 0); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 0, Map.of(1L, 2)); } /** @@ -682,14 +572,7 @@ public void testNENRemoveTwoVoteUnderstatement1(){ 0, 0, 1, 0); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, -2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 0, Map.of(1L, -2)); } /** @@ -706,14 +589,7 @@ public void testNENRemoveOther1(){ 0, 0, 0, 1); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(0, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 0, 0, 0, 0, 0, Map.of(1L, 0)); } /** @@ -731,14 +607,8 @@ public void testNENRemoveOneVoteOverstatement2(){ 4L, 1), 2, 0, 0, 1, 1); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 0, 1, 1, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 1)); } /** @@ -756,14 +626,8 @@ public void testNENRemoveOneVoteUnderstatement2(){ 4L, -1), 1, 2, 0, 0, 1); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(1, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(0, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -1, 4L, -1), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 1, 0, 0, 1, + Map.of(1L, 0, 2L, 1, 3L, -1, 4L, -1)); } /** @@ -781,14 +645,8 @@ public void testNENRemoveTwoVoteOverstatement2(){ 4L, 2), 1, 0, 2, 1, 0); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(1, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(0, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 2, 2L, 1, 3L, -2, 4L, 2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 1, 1, 0, + Map.of(1L, 2, 2L, 1, 3L, -2, 4L, 2)); } /** @@ -806,14 +664,8 @@ public void testNENRemoveTwoVoteUnderstatement2(){ 4L, -2), 1, 0, 0, 2, 1); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 0, 1, 1, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, -2)); } /** @@ -831,14 +683,8 @@ public void testNENRemoveOther2(){ 4L, 0), 1, 0, 0, 1, 2); assertTrue(a.removeDiscrepancy(info)); - - assertEquals(1, a.oneVoteOverCount.intValue()); - assertEquals(0, a.oneVoteUnderCount.intValue()); - assertEquals(0, a.twoVoteOverCount.intValue()); - assertEquals(1, a.twoVoteUnderCount.intValue()); - assertEquals(1, a.otherCount.intValue()); - - assertEquals(Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0), a.cvrDiscrepancy); + checkCountsDiscrepancyMap(a, 1, 0, 0, 1, 1, + Map.of(1L, 0, 2L, 1, 3L, -2, 4L, 0)); } @@ -1050,14 +896,10 @@ public void testNENComputeDiscrepancyNone(CVRContestInfo info, RecordType audite // None of the above calls to computeDiscrepancy should have produced a discrepancy. assert(d1.isEmpty() && d2.isEmpty() && d3.isEmpty() && d4.isEmpty()); - assert(countsEqual(a1, 0, 0, 0, 0, 0)); - assertEquals(Map.of(), a1.cvrDiscrepancy); - assert(countsEqual(a2, 0, 0, 0, 0, 0)); - assertEquals(Map.of(), a2.cvrDiscrepancy); - assert(countsEqual(a3, 0, 1, 0, 0, 0)); - assertEquals(Map.of(2L, 2), a3.cvrDiscrepancy); - assert(countsEqual(a4, 1, 0, 0, 0, 0)); - assertEquals(Map.of(2L, 1), a4.cvrDiscrepancy); + checkCountsDiscrepancyMap(a1, 0, 0, 0, 0, 0, Map.of()); + checkCountsDiscrepancyMap(a2, 0, 0, 0, 0, 0, Map.of()); + checkCountsDiscrepancyMap(a3, 0, 0, 1, 0, 0, Map.of(2L,2)); + checkCountsDiscrepancyMap(a4, 1, 0, 0, 0, 0, Map.of(2L,1)); } /** From 009905a0e6b0bb45ed41d758fc040e9a36d5392f Mon Sep 17 00:00:00 2001 From: michelleblom Date: Sun, 23 Jun 2024 17:24:44 +1000 Subject: [PATCH 12/12] Added method to allow computation of initial samples to audit at any time during audit. --- .../corla/model/IRVComparisonAudit.java | 21 +++++++-------- .../corla/model/assertion/Assertion.java | 26 +++++++++++++++++++ 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java index b996d523..47ee04f3 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/IRVComparisonAudit.java @@ -255,13 +255,10 @@ protected void recalculateSamplesToAudit() { } /** - * This method is currently not being used in the base class. However, if it starts being used - * in the future, we need an IRV version for IRV audits. Consequently, we provide the IRV - * version here. This method computes and returns the initial (optimistic) sample size - * for the audit. This is computed by calling the optimistic sample size computation method - * of each of the audit's assertions, and returning the maximum of these. A RuntimeException is - * thrown if an unexpected error arises in computing optimistic sample sizes for each assertion. - * @return the optimistic sample size for the audit. + * This method computes and returns the initial (optimistic) sample size for the audit. This is + * computed by calling an initial sample size computation method in each assertion, and returning + * the maximum of the returned values. + * @return the initial (optimistic) sample size for the audit. */ @Override public int initialSamplesToAudit() { @@ -277,19 +274,19 @@ public int initialSamplesToAudit() { final String contestName = getContestName(); try { - LOGGER.debug(String.format("%s computing the initial samples to audit for the IRV contest %s.", - prefix, contestName)); + LOGGER.debug(String.format("%s computing the initial optimistic samples to audit for the " + + "IRV contest %s.", prefix, contestName)); if (assertions.isEmpty()) { LOGGER.debug(String.format("%s No assertions for contest %s; returning an initial sample " + "size of 0.", prefix, contestName)); return 0; } else { - LOGGER.debug(String.format("%s calling computeOptimisticSamplesToAudit() for each " + + LOGGER.debug(String.format("%s calling computeInitialOptimisticSamplesToAudit() for each " + "assertion in contest %s given risk limit %f.", prefix, contestName, getRiskLimit())); final int samples = max(assertions.stream().map(a -> - a.computeOptimisticSamplesToAudit(getRiskLimit())).toList()); - LOGGER.debug(String.format("%s optimistic sample size of %d computed for contest %s.", + a.computeInitialOptimisticSamplesToAudit(getRiskLimit())).toList()); + LOGGER.debug(String.format("%s initial optimistic sample size of %d computed for contest %s.", prefix, samples, contestName)); return samples; } diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java index 8157f22b..64c59b62 100644 --- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java +++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/model/assertion/Assertion.java @@ -319,6 +319,32 @@ public Integer computeOptimisticSamplesToAudit(BigDecimal riskLimit) { return optimisticSamplesToAudit; } + /** + * For the given risk limit, compute the expected initial (optimistic) number of samples to audit + * for this assertion. This calculation calls Audit::optimistic() with zero's for each discrepancy + * count. This method does not change any of this assertion's internal sample size attributes. + * + * @param riskLimit The risk limit of the audit. + * @return The initial (optimistic) number of samples we expect we will need to sample to + * audit this assertion. + */ + public Integer computeInitialOptimisticSamplesToAudit(BigDecimal riskLimit){ + final String prefix = "[computeInitialOptimisticSamplesToAudit]"; + + LOGGER.debug(String.format("%s Calling Audit::optimistic() with parameters: risk limit " + + "%f; diluted margin %f; gamma %f; two vote under count 0; one vote under count 0; " + + "one vote over count 0; two vote over count 0.", prefix, riskLimit, dilutedMargin, Audit.GAMMA)); + + // Call the colorado-rla audit math; update optimistic_samples_to_audit and return new value. + final int initialOptimistic = Audit.optimistic(riskLimit, dilutedMargin, Audit.GAMMA, + 0, 0, 0, 0).intValue(); + + LOGGER.debug(String.format("%s Computed initial optimistic samples to audit for Assertion %d" + + " of %s ballots.", prefix, id, initialOptimistic)); + + return initialOptimistic; + } + /** * Compute and return the estimated number of samples to audit for this assertion, given the * number of ballots audited thus far and number of observed overstatements. This method takes