From 0a453d96c758212c7e5dc4549b3579a567173902 Mon Sep 17 00:00:00 2001 From: michelleblom Date: Mon, 8 Apr 2024 11:26:16 +1000 Subject: [PATCH 01/13] Basic start to Assertion entity and repository classes (shells). --- .../persistence/entity/Assertion.java | 74 +++++++++++++++++++ .../persistence/entity/NEBAssertion.java | 30 ++++++++ .../persistence/entity/NENAssertion.java | 30 ++++++++ .../repository/AssertionRepository.java | 33 +++++++++ 4 files changed, 167 insertions(+) create mode 100644 src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java create mode 100644 src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java create mode 100644 src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java create mode 100644 src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java new file mode 100644 index 0000000..d6ac22e --- /dev/null +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java @@ -0,0 +1,74 @@ +/* +Copyright 2024 Democracy Developers + +The Raire Service is designed to connect colorado-rla and its associated database to +the raire assertion generation engine (https://github.com/DemocracyDevelopers/raire-java). + +This file is part of raire-service. + +raire-service is 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. + +raire-service is 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.raireservice.persistence.entity; + +import java.util.ArrayList; +import java.util.List; +import jakarta.persistence.*; + +@Entity +@Table(name = "assertion") +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn(name = "assertion_type") +public abstract class Assertion { + + /** + * Assertion ID. + */ + @Id + @Column(updatable = false, nullable = false) + @GeneratedValue(strategy = GenerationType.SEQUENCE) + private Long id; + + /** + * Name of contest for which this Assertion was generated. + */ + @Column(name = "contest_name", nullable = false) + protected String contestName; + + /** + * Winner of the assertion (a candidate in the contest). + */ + @Column(name = "winner", nullable = false) + protected String winner; + + /** + * Loser of the assertion (a candidate in the contest). + */ + @Column(name = "loser", nullable = false) + protected String loser; + + /** + * Assertion margin (note: this is not the *diluted* margin). + */ + @Column(name = "margin", nullable = false) + protected int margin; + + /** + * 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")) + protected List assumedContinuing = new ArrayList<>(); + + +} diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java new file mode 100644 index 0000000..f7fb063 --- /dev/null +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java @@ -0,0 +1,30 @@ +/* +Copyright 2024 Democracy Developers + +The Raire Service is designed to connect colorado-rla and its associated database to +the raire assertion generation engine (https://github.com/DemocracyDevelopers/raire-java). + +This file is part of raire-service. + +raire-service is 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. + +raire-service is 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.raireservice.persistence.entity; + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; + +@Entity +@DiscriminatorValue("NEB") +public class NEBAssertion extends Assertion { + +} diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java new file mode 100644 index 0000000..767cc14 --- /dev/null +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java @@ -0,0 +1,30 @@ +/* +Copyright 2024 Democracy Developers + +The Raire Service is designed to connect colorado-rla and its associated database to +the raire assertion generation engine (https://github.com/DemocracyDevelopers/raire-java). + +This file is part of raire-service. + +raire-service is 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. + +raire-service is 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.raireservice.persistence.entity; + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; + +@Entity +@DiscriminatorValue("NEN") +public class NENAssertion { + +} diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java new file mode 100644 index 0000000..2d218bc --- /dev/null +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java @@ -0,0 +1,33 @@ +/* +Copyright 2024 Democracy Developers + +The Raire Service is designed to connect colorado-rla and its associated database to +the raire assertion generation engine (https://github.com/DemocracyDevelopers/raire-java). + +This file is part of raire-service. + +raire-service is 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. + +raire-service is 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.raireservice.persistence.repository; + +import au.org.democracydevelopers.raireservice.persistence.entity.Assertion; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * Database retrieval and storage for Assertions. + */ +@Repository +public interface AssertionRepository extends JpaRepository { + +} From 6a18524ade4a5c0729f6ea468c7681507685e183 Mon Sep 17 00:00:00 2001 From: michelleblom Date: Mon, 8 Apr 2024 11:27:11 +1000 Subject: [PATCH 02/13] Basic start to Assertion entity and repository classes (shells). --- .../raireservice/persistence/entity/NENAssertion.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java index 767cc14..3cdb298 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java @@ -25,6 +25,6 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra @Entity @DiscriminatorValue("NEN") -public class NENAssertion { +public class NENAssertion extends Assertion { } From f469e0dc65b5058fa6f7959a186290e7c5c27c5e Mon Sep 17 00:00:00 2001 From: michelleblom Date: Mon, 8 Apr 2024 11:45:30 +1000 Subject: [PATCH 03/13] Fleshed out non-nullable fields in Assertion class as per table design. --- .../persistence/entity/Assertion.java | 68 +++++++++++++++++-- 1 file changed, 63 insertions(+), 5 deletions(-) diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java index d6ac22e..f5ddbb9 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java @@ -20,6 +20,7 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra package au.org.democracydevelopers.raireservice.persistence.entity; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import jakarta.persistence.*; @@ -39,19 +40,19 @@ public abstract class Assertion { private Long id; /** - * Name of contest for which this Assertion was generated. + * Name of the contest for which this Assertion was generated. */ @Column(name = "contest_name", nullable = false) protected String contestName; /** - * Winner of the assertion (a candidate in the contest). + * Winner of the Assertion (a candidate in the contest). */ @Column(name = "winner", nullable = false) protected String winner; /** - * Loser of the assertion (a candidate in the contest). + * Loser of the Assertion (a candidate in the contest). */ @Column(name = "loser", nullable = false) protected String loser; @@ -63,12 +64,69 @@ public abstract class Assertion { protected int margin; /** - * List of candidates that the assertion assumes are `continuing' in the - * assertion's context. + * 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")) protected List assumedContinuing = new ArrayList<>(); + /** + * Diluted margin for the Assertion. This is equal to the assertion margin divided by the + * number of ballots in the relevant auditing universe. + */ + @Column(name = "diluted_margin", nullable = false) + protected double dilutedMargin = 0; + /** + * Assertion difficulty, as estimated by raire-java. (Note that raire-java has multiple ways + * of estimating difficulty, and that these measurements are not necessarily in terms of numbers + * of ballots. For example, one method may be: difficulty = 1 / assertion margin). + */ + @Column(name = "difficulty", nullable = false) + protected double difficulty = 0; + + /** + * The expected number of samples to audit overall for the Assertion, assuming overstatements + * continue at the current rate experienced in the audit. + */ + @Column(nullable = false) + private Integer my_estimated_samples_to_audit = 0; + + /** + * The two-vote understatements recorded against the Assertion. + */ + @Column(nullable = false) + protected Integer my_two_vote_under_count = 0; + + /** + * The one-vote understatements recorded against the Assertion. + */ + @Column(nullable = false) + protected Integer my_one_vote_under_count = 0; + + /** + * The one-vote overstatements recorded against the Assertion. + */ + @Column(nullable = false) + protected Integer my_one_vote_over_count = 0; + + /** + * The two-vote overstatements recorded against the Assertion. + */ + @Column(nullable = false) + protected Integer my_two_vote_over_count = 0; + + /** + * Discrepancies recorded against the Assertion that are neither understatements nor + * overstatements. + */ + @Column(nullable = false) + protected Integer my_other_count = 0; + + /** + * Current risk measurement recorded against the Assertion. It is initialized to 1, as prior + * to an audit starting, and without additional information, we assume maximum risk. + */ + @Column(nullable = false) + protected BigDecimal my_current_risk = BigDecimal.valueOf(1); } From 1343aa6996fb62c6b31112a5bfdefb3a8035a45b Mon Sep 17 00:00:00 2001 From: michelleblom Date: Mon, 8 Apr 2024 12:00:50 +1000 Subject: [PATCH 04/13] Added two basic assertion retrieval tests (for contest that does not exist, and existent contest with no assertions). --- .../repository/AssertionRepository.java | 10 +++ .../repository/AssertionRepositoryTests.java | 73 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java index 2d218bc..803d906 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java @@ -21,7 +21,10 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra package au.org.democracydevelopers.raireservice.persistence.repository; import au.org.democracydevelopers.raireservice.persistence.entity.Assertion; +import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; /** @@ -30,4 +33,11 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra @Repository public interface AssertionRepository extends JpaRepository { + /** + * Retrieve all Assertions from the database belonging to the contest with + * the given name. + * @param contestName Name of the contest whose assertions being retrieved. + */ + @Query(value="select a from Assertion a where a.contestName = :contestName") + List findByContestName(@Param("contestName") String contestName); } diff --git a/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java b/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java new file mode 100644 index 0000000..4bb2073 --- /dev/null +++ b/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java @@ -0,0 +1,73 @@ +/* +Copyright 2024 Democracy Developers + +The Raire Service is designed to connect colorado-rla and its associated database to +the raire assertion generation engine (https://github.com/DemocracyDevelopers/raire-java). + +This file is part of raire-service. + +raire-service is 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. + +raire-service is 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.raireservice.persistence.repository; + + +import au.org.democracydevelopers.raireservice.persistence.entity.Assertion; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.annotation.DirtiesContext.ClassMode; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests to validate the behaviour of Assertion retrieval and storage. Assertions and other + * relevant data is preloaded into the test database from src/test/resources/data.sql. + */ +@ActiveProfiles("test-containers") +@SpringBootTest +@AutoConfigureTestDatabase(replace = Replace.NONE) +@DirtiesContext(classMode = ClassMode.AFTER_CLASS) +public class AssertionRepositoryTests { + + @Autowired + AssertionRepository assertionRepository; + + /** + * Retrieval of assertions for an existing contest with no associated assertions will return an + * empty list. + */ + @Test + @Transactional + void contestNoAssertions(){ + List retrieved = assertionRepository.findByContestName("No CVR Mayoral"); + assertEquals(0, retrieved.size()); + } + + /** + * Retrieval of assertions for a non-existent contest will return an empty list. + */ + @Test + @Transactional + void nonExistentContestNoAssertions(){ + List retrieved = assertionRepository.findByContestName("Non-Existent Contest Name"); + assertEquals(0, retrieved.size()); + } +} From 74f2494d1841bd2f676f220f5907da658eeca12a Mon Sep 17 00:00:00 2001 From: michelleblom Date: Mon, 8 Apr 2024 13:13:46 +1000 Subject: [PATCH 05/13] Set foreign key constraints on assertion table construction when setting up test database. --- .../persistence/entity/Assertion.java | 22 ++++++++++++---- .../repository/AssertionRepositoryTests.java | 2 +- src/test/resources/corla.sql | 25 +++++++++++++------ 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java index f5ddbb9..f6f0862 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java @@ -22,8 +22,10 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra import java.math.BigDecimal; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import jakarta.persistence.*; +import java.util.Map; @Entity @Table(name = "assertion") @@ -63,6 +65,14 @@ public abstract class Assertion { @Column(name = "margin", nullable = false) protected int margin; + /** + * Assertion difficulty, as estimated by raire-java. (Note that raire-java has multiple ways + * of estimating difficulty, and that these measurements are not necessarily in terms of numbers + * of ballots. For example, one method may be: difficulty = 1 / assertion margin). + */ + @Column(name = "difficulty", nullable = false) + protected double difficulty; + /** * List of candidates that the Assertion assumes are 'continuing' in the Assertion's context. */ @@ -78,12 +88,14 @@ public abstract class Assertion { protected double dilutedMargin = 0; /** - * Assertion difficulty, as estimated by raire-java. (Note that raire-java has multiple ways - * of estimating difficulty, and that these measurements are not necessarily in terms of numbers - * of ballots. For example, one method may be: difficulty = 1 / assertion margin). + * Maximum discrepancies that have been recorded against this assertion, if any, for a given + * CVR identified through its ID. */ - @Column(name = "difficulty", nullable = false) - protected double difficulty = 0; + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "assertion_discrepancies", joinColumns = @JoinColumn(name = "id")) + @MapKeyColumn(name = "cvr_id") + @Column(name = "discrepancy") + protected Map cvrDiscrepancy = new HashMap<>(); /** * The expected number of samples to audit overall for the Assertion, assuming overstatements diff --git a/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java b/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java index 4bb2073..edcfbff 100644 --- a/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java +++ b/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java @@ -56,7 +56,7 @@ public class AssertionRepositoryTests { */ @Test @Transactional - void contestNoAssertions(){ + void existentContestNoAssertions(){ List retrieved = assertionRepository.findByContestName("No CVR Mayoral"); assertEquals(0, retrieved.size()); } diff --git a/src/test/resources/corla.sql b/src/test/resources/corla.sql index d978f32..e38da71 100644 --- a/src/test/resources/corla.sql +++ b/src/test/resources/corla.sql @@ -584,19 +584,30 @@ create table assertion create table assertion_context ( - id bigint not null - constraint fki0lyp4tghtpohaa9ma6kv2174 + id bigint not null + constraint fk_assertion_assertion_context references assertion, - assumed_continuing varchar(255) + assumed_continuing varchar(255) not null +); + +create table assertion_discrepancy +( + id bigint not null + constraint fk_assertion_assertion_discrepancy_id + references assertion, + cvr_id bigint not null + constraint fk_cast_vote_record_assertion_discrepancy + references cast_vote_record, + assumed_continuing varchar(255) not null ); create table audit_to_assertions ( - id bigint not null - constraint fkgrx2l2qywbc3nv83iid55ql36 + id bigint not null + constraint fk_comparison_audit_audit_to_assertions references comparison_audit, - assertions_id bigint not null - constraint fkqomhyyib2xno6nq0wjpv95fs5 + assertion_id bigint not null + constraint fk_assertion_audit_to_assertions references assertion ); From 5b7d9c403ea8c7bf61bbcf32e0dbd2259ba843de Mon Sep 17 00:00:00 2001 From: michelleblom Date: Mon, 8 Apr 2024 17:07:46 +1000 Subject: [PATCH 06/13] Fleshed out Assertion attributes, added required constructors, and a few tests of assertion retrieval and deletion for trivial contests. --- .../persistence/entity/Assertion.java | 181 +++++++++++++++--- .../persistence/entity/NEBAssertion.java | 19 ++ .../persistence/entity/NENAssertion.java | 19 ++ .../repository/AssertionRepository.java | 11 +- .../repository/AssertionRepositoryTests.java | 175 ++++++++++++++++- src/test/resources/corla.sql | 22 +-- src/test/resources/data.sql | 24 ++- 7 files changed, 407 insertions(+), 44 deletions(-) diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java index f6f0862..b699d13 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java @@ -20,12 +20,15 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra package au.org.democracydevelopers.raireservice.persistence.entity; +import au.org.democracydevelopers.raireservice.request.RequestValidationException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import jakarta.persistence.*; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @Entity @Table(name = "assertion") @@ -33,36 +36,35 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra @DiscriminatorColumn(name = "assertion_type") public abstract class Assertion { + public static final Logger logger = LoggerFactory.getLogger(Assertion.class); + /** * Assertion ID. */ @Id @Column(updatable = false, nullable = false) @GeneratedValue(strategy = GenerationType.SEQUENCE) - private Long id; + private long id; /** * Name of the contest for which this Assertion was generated. */ - @Column(name = "contest_name", nullable = false) + @Column(name = "contest_name", updatable = false, nullable = false) protected String contestName; - /** - * Winner of the Assertion (a candidate in the contest). - */ - @Column(name = "winner", nullable = false) + @Column(name = "winner", updatable = false, nullable = false) protected String winner; /** * Loser of the Assertion (a candidate in the contest). */ - @Column(name = "loser", nullable = false) + @Column(name = "loser", updatable = false, nullable = false) protected String loser; /** * Assertion margin (note: this is not the *diluted* margin). */ - @Column(name = "margin", nullable = false) + @Column(name = "margin", updatable = false, nullable = false) protected int margin; /** @@ -70,7 +72,7 @@ public abstract class Assertion { * of estimating difficulty, and that these measurements are not necessarily in terms of numbers * of ballots. For example, one method may be: difficulty = 1 / assertion margin). */ - @Column(name = "difficulty", nullable = false) + @Column(name = "difficulty", updatable = false, nullable = false) protected double difficulty; /** @@ -78,14 +80,15 @@ public abstract class Assertion { */ @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "assertion_context", joinColumns = @JoinColumn(name = "id")) + @Column(updatable = false, nullable = false) protected List assumedContinuing = new ArrayList<>(); /** * Diluted margin for the Assertion. This is equal to the assertion margin divided by the * number of ballots in the relevant auditing universe. */ - @Column(name = "diluted_margin", nullable = false) - protected double dilutedMargin = 0; + @Column(name = "diluted_margin", updatable = false, nullable = false) + protected double dilutedMargin; /** * Maximum discrepancies that have been recorded against this assertion, if any, for a given @@ -94,51 +97,177 @@ public abstract class Assertion { @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "assertion_discrepancies", joinColumns = @JoinColumn(name = "id")) @MapKeyColumn(name = "cvr_id") - @Column(name = "discrepancy") + @Column(name = "discrepancy", updatable = false, nullable = false) protected Map cvrDiscrepancy = new HashMap<>(); /** * The expected number of samples to audit overall for the Assertion, assuming overstatements * continue at the current rate experienced in the audit. */ - @Column(nullable = false) - private Integer my_estimated_samples_to_audit = 0; + @Column(updatable = false, nullable = false) + protected Integer estimated_samples_to_audit = 0; /** * The two-vote understatements recorded against the Assertion. */ - @Column(nullable = false) - protected Integer my_two_vote_under_count = 0; + @Column(updatable = false, nullable = false) + protected Integer two_vote_under_count = 0; /** * The one-vote understatements recorded against the Assertion. */ - @Column(nullable = false) - protected Integer my_one_vote_under_count = 0; + @Column(updatable = false, nullable = false) + protected Integer one_vote_under_count = 0; /** * The one-vote overstatements recorded against the Assertion. */ - @Column(nullable = false) - protected Integer my_one_vote_over_count = 0; + @Column(updatable = false, nullable = false) + protected Integer one_vote_over_count = 0; /** * The two-vote overstatements recorded against the Assertion. */ - @Column(nullable = false) - protected Integer my_two_vote_over_count = 0; + @Column(updatable = false, nullable = false) + protected Integer two_vote_over_count = 0; /** * Discrepancies recorded against the Assertion that are neither understatements nor * overstatements. */ - @Column(nullable = false) - protected Integer my_other_count = 0; + @Column(updatable = false, nullable = false) + protected Integer other_count = 0; /** * Current risk measurement recorded against the Assertion. It is initialized to 1, as prior * to an audit starting, and without additional information, we assume maximum risk. */ - @Column(nullable = false) - protected BigDecimal my_current_risk = BigDecimal.valueOf(1); + @Column(updatable = false, nullable = false) + protected BigDecimal current_risk = BigDecimal.valueOf(1); + + /** + * Construct an empty Assertion (for persistence). + */ + public Assertion() {} + + /** + * Construct an Assertion for a specific contest. + * @param contestName Contest for which the Assertion has been created. + * @param winner Winner of the Assertion (name of a candidate in the contest). + * @param loser Loser of the Assertion (name of a candidate in the contest). + * @param margin Absolute margin of the Assertion. + * @param universeSize Total number of ballots in the auditing universe of the Assertion. + * @param difficulty Assertion difficulty, as computed by raire-java. + * @param assumedContinuing List of candidates, by name, that the Assertion assumes is continuing. + * @throws RequestValidationException if the caller supplies a non-positive universe size. + */ + public Assertion(String contestName, String winner, String loser, int margin, + long universeSize, double difficulty, List assumedContinuing) + throws RequestValidationException + { + this.contestName = contestName; + this.winner = winner; + this.loser = loser; + this.margin = margin; + + if(universeSize <= 0){ + String msg = "An assertion must have a positive universe size."; + logger.error(msg); + throw new RequestValidationException(msg); + } + + this.dilutedMargin = margin / (double) universeSize; + + this.difficulty = difficulty; + this.assumedContinuing = assumedContinuing; + } + + /** + * Returns the Assertion ID. + */ + public long getId() { return id; } + + /** + * Returns the winner of the Assertion. + */ + public String getWinner() { + return winner; + } + + /** + * Returns the loser of the Assertion. + */ + public String getLoser() { + return loser; + } + + + /** + * Returns the name of the contest for which the Assertion has been formed. + */ + public String getContestName() { + return contestName; + } + + /** + * Returns the name of the contest for which the Assertion has been formed. + */ + public List getAssumedContinuing() { + return assumedContinuing; + } + + /** + * Returns the current risk measurement recorded against the assertion. + */ + public BigDecimal getCurrentRisk() { return current_risk; } + + /** + * Returns the Assertion's absolute margin. + */ + public int getMargin() { return margin; } + + /** + * Returns the Assertion's difficulty. + */ + public double getDifficulty() { return difficulty; } + + /** + * Returns the Assertion's diluted margin. + */ + public double getDilutedMargin() { return dilutedMargin; } + + /** + * Returns the discrepancies recorded against the Assertion, by CVR id. + */ + public Map getCvrDiscrepancy(){ return cvrDiscrepancy; } + + /** + * Returns the two vote overstatements recorded against the Assertion. + */ + public int getTwoVoteOverCount() { return two_vote_over_count; } + + /** + * Returns the two vote understatements recorded against the Assertion. + */ + public int getTwoVoteUnderCount() { return two_vote_under_count; } + + /** + * Returns the one vote overstatements recorded against the Assertion. + */ + public int getOneVoteOverCount() { return one_vote_over_count; } + + /** + * Returns the one vote understatements recorded against the Assertion. + */ + public int getOneVoteUnderCount() { return one_vote_under_count; } + + /** + * Returns the count of other discrepancies (neither over nor understatements). + */ + public int getOtherDiscrepancies() { return other_count; } + + /** + * Returns the estimated number of samples to audit. + */ + public int getEstimatedSamplesToAudit() { return estimated_samples_to_audit; } } diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java index f7fb063..d13b8fc 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java @@ -20,11 +20,30 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra package au.org.democracydevelopers.raireservice.persistence.entity; +import au.org.democracydevelopers.raireservice.request.RequestValidationException; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; +import java.util.List; @Entity @DiscriminatorValue("NEB") public class NEBAssertion extends Assertion { + /** + * {@inheritDoc} + */ + public NEBAssertion() { + super(); + } + + /** + * {@inheritDoc} + */ + public NEBAssertion(String contestName, String winner, String loser, int margin, + long universeSize, double difficulty, List assumedContinuing) + throws RequestValidationException + { + super(contestName, winner, loser, margin, universeSize, difficulty, assumedContinuing); + } + } diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java index 3cdb298..e74db4e 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java @@ -20,11 +20,30 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra package au.org.democracydevelopers.raireservice.persistence.entity; +import au.org.democracydevelopers.raireservice.request.RequestValidationException; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; +import java.util.List; @Entity @DiscriminatorValue("NEN") public class NENAssertion extends Assertion { + /** + * {@inheritDoc} + */ + public NENAssertion() { + super(); + } + + /** + * {@inheritDoc} + */ + public NENAssertion(String contestName, String winner, String loser, int margin, + long universeSize, double difficulty, List assumedContinuing) + throws RequestValidationException + { + super(contestName, winner, loser, margin, universeSize, difficulty, assumedContinuing); + } + } diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java index 803d906..2cf71cf 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java @@ -34,10 +34,17 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra public interface AssertionRepository extends JpaRepository { /** - * Retrieve all Assertions from the database belonging to the contest with - * the given name. + * Retrieve all Assertions from the database belonging to the contest with the given name. * @param contestName Name of the contest whose assertions being retrieved. */ @Query(value="select a from Assertion a where a.contestName = :contestName") List findByContestName(@Param("contestName") String contestName); + + /** + * Delete all Assertions belonging to the contest with the given name from the database. This + * is Spring syntactic sugar for the corresponding 'delete' query. + * @param contestName The name of the contest whose assertions are to be deleted. + * @return The number of records deleted from the database. + */ + long deleteByContestName(String contestName); } diff --git a/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java b/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java index edcfbff..55251be 100644 --- a/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java +++ b/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java @@ -22,6 +22,9 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra import au.org.democracydevelopers.raireservice.persistence.entity.Assertion; +import au.org.democracydevelopers.raireservice.persistence.entity.NEBAssertion; +import au.org.democracydevelopers.raireservice.persistence.entity.NENAssertion; +import java.math.BigDecimal; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -34,8 +37,6 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra import org.springframework.transaction.annotation.Transactional; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests to validate the behaviour of Assertion retrieval and storage. Assertions and other @@ -62,7 +63,7 @@ void existentContestNoAssertions(){ } /** - * Retrieval of assertions for a non-existent contest will return an empty list. + * Retrieval of assertions for a non-existent contest will return an empty list. */ @Test @Transactional @@ -70,4 +71,172 @@ void nonExistentContestNoAssertions(){ List retrieved = assertionRepository.findByContestName("Non-Existent Contest Name"); assertEquals(0, retrieved.size()); } + + /** + * Deletion of assertions for a non-existent contest will remove no records. + */ + @Test + @Transactional + void deleteAssertionsNonExistentContest(){ + long records = assertionRepository.deleteByContestName("Non-Existent Contest Name"); + assertEquals(0, records); + } + + /** + * Deletion of assertions for an existent contest with no assertions will remove no records. + */ + @Test + @Transactional + void deleteAssertionsExistentContestNoAssertions(){ + long records = assertionRepository.deleteByContestName("No CVR Mayoral"); + assertEquals(0, records); + } + + /** + * Retrieve assertions for a contest that has one NEB assertion. + */ + @Test + @Transactional + void retrieveAssertionsExistentContestOneNEBAssertion(){ + List retrieved = assertionRepository.findByContestName("One NEB Assertion Contest"); + assertEquals(1, retrieved.size()); + + final Assertion r = retrieved.get(0); + assertEquals(NEBAssertion.class, r.getClass()); + assertEquals(0, r.getId()); + assertEquals("Alice", r.getWinner()); + assertEquals("Bob", r.getLoser()); + assertEquals("One NEB Assertion Contest", r.getContestName()); + assertEquals(100, r.getMargin()); + assertEquals(0, r.getCurrentRisk().compareTo(BigDecimal.valueOf(1))); + assertEquals(1.1, r.getDifficulty()); + assertEquals(0.32, r.getDilutedMargin()); + assertEquals(0, r.getOneVoteOverCount()); + assertEquals(0, r.getOneVoteUnderCount()); + assertEquals(0, r.getTwoVoteOverCount()); + assertEquals(0, r.getTwoVoteUnderCount()); + assertEquals(0, r.getOtherDiscrepancies()); + assertEquals(0, r.getEstimatedSamplesToAudit()); + assertEquals(0, r.getCvrDiscrepancy().size()); + assertEquals(0, r.getAssumedContinuing().size()); + } + + /** + * Retrieve assertions for a contest that has one NEN assertion. + */ + @Test + @Transactional + void retrieveAssertionsExistentContestOneNENAssertion(){ + List retrieved = assertionRepository.findByContestName("One NEN Assertion Contest"); + assertEquals(1, retrieved.size()); + + final Assertion r = retrieved.get(0); + assertEquals(NENAssertion.class, r.getClass()); + assertEquals(1, r.getId()); + assertEquals("Alice", r.getWinner()); + assertEquals("Charlie", r.getLoser()); + assertEquals("One NEN Assertion Contest", r.getContestName()); + assertEquals(240, r.getMargin()); + assertEquals(0, r.getCurrentRisk().compareTo(BigDecimal.valueOf(1))); + assertEquals(3.01, r.getDifficulty()); + assertEquals(0.12, r.getDilutedMargin()); + assertEquals(0, r.getOneVoteOverCount()); + assertEquals(0, r.getOneVoteUnderCount()); + assertEquals(0, r.getTwoVoteOverCount()); + assertEquals(0, r.getTwoVoteUnderCount()); + assertEquals(0, r.getOtherDiscrepancies()); + assertEquals(0, r.getEstimatedSamplesToAudit()); + assertEquals(0, r.getCvrDiscrepancy().size()); + assertEquals(List.of("Diego", "Bob"), r.getAssumedContinuing()); + } + + /** + * Retrieve assertions for a contest that has one NEN and one NEB assertion. + */ + @Test + @Transactional + void retrieveAssertionsExistentContestOneNENOneNEBAssertion(){ + List retrieved = assertionRepository.findByContestName("One NEN NEB Assertion Contest"); + assertEquals(2, retrieved.size()); + + final Assertion r1 = retrieved.get(0); + assertEquals(NEBAssertion.class, r1.getClass()); + assertEquals(2, r1.getId()); + assertEquals("Amanda", r1.getWinner()); + assertEquals("Liesl", r1.getLoser()); + assertEquals("One NEN NEB Assertion Contest", r1.getContestName()); + assertEquals(112, r1.getMargin()); + assertEquals(0, r1.getCurrentRisk().compareTo(BigDecimal.valueOf(1))); + assertEquals(0.1, r1.getDifficulty()); + assertEquals(0.52, r1.getDilutedMargin()); + assertEquals(0, r1.getOneVoteOverCount()); + assertEquals(0, r1.getOneVoteUnderCount()); + assertEquals(0, r1.getTwoVoteOverCount()); + assertEquals(0, r1.getTwoVoteUnderCount()); + assertEquals(0, r1.getOtherDiscrepancies()); + assertEquals(0, r1.getEstimatedSamplesToAudit()); + assertEquals(0, r1.getCvrDiscrepancy().size()); + assertEquals(0, r1.getAssumedContinuing().size()); + + final Assertion r2 = retrieved.get(1); + assertEquals(NENAssertion.class, r2.getClass()); + assertEquals(3, r2.getId()); + assertEquals("Amanda", r2.getWinner()); + assertEquals("Wendell", r2.getLoser()); + assertEquals("One NEN NEB Assertion Contest", r2.getContestName()); + assertEquals(250, r2.getMargin()); + assertEquals(0, r2.getCurrentRisk().compareTo(BigDecimal.valueOf(1))); + assertEquals(3.17, r2.getDifficulty()); + assertEquals(0.72, r2.getDilutedMargin()); + assertEquals(0, r2.getOneVoteOverCount()); + assertEquals(0, r2.getOneVoteUnderCount()); + assertEquals(0, r2.getTwoVoteOverCount()); + assertEquals(0, r2.getTwoVoteUnderCount()); + assertEquals(0, r2.getOtherDiscrepancies()); + assertEquals(0, r2.getEstimatedSamplesToAudit()); + assertEquals(0, r2.getCvrDiscrepancy().size()); + assertEquals(List.of("Liesl"), r2.getAssumedContinuing()); + } + + /** + * Deletion of assertions for an existent contest with one NEB assertion will remove one record. + * The contest will then have no associated assertions in the database. + */ + @Test + @Transactional + void deleteAssertionsExistentContestOneNEBAssertion(){ + long records = assertionRepository.deleteByContestName("One NEB Assertion Contest"); + assertEquals(1, records); + + List retrieved = assertionRepository.findByContestName("One NEB Assertion Contest"); + assertEquals(0, retrieved.size()); + } + + /** + * Deletion of assertions for an existent contest with one NEN assertion will remove one record. + * The contest will then have no associated assertions in the database. + */ + @Test + @Transactional + void deleteAssertionsExistentContestOneNENAssertion(){ + long records = assertionRepository.deleteByContestName("One NEN Assertion Contest"); + assertEquals(1, records); + + List retrieved = assertionRepository.findByContestName("One NEN Assertion Contest"); + assertEquals(0, retrieved.size()); + } + + /** + * Deletion of assertions for an existent contest with one NEN and one NEB assertion will + * remove two record. The contest will then have no associated assertions in the database. + */ + @Test + @Transactional + void deleteAssertionsExistentContestOneNENOneNEBAssertion(){ + long records = assertionRepository.deleteByContestName("One NEN NEB Assertion Contest"); + assertEquals(2, records); + + List retrieved = assertionRepository.findByContestName("One NEN NEB Assertion Contest"); + assertEquals(0, retrieved.size()); + } } diff --git a/src/test/resources/corla.sql b/src/test/resources/corla.sql index e38da71..4106760 100644 --- a/src/test/resources/corla.sql +++ b/src/test/resources/corla.sql @@ -571,6 +571,7 @@ create table assertion diluted_margin double precision not null, loser varchar(255) not null, margin integer not null, + current_risk numeric(19, 2) not null, estimated_samples_to_audit integer not null, one_vote_over_count integer not null, one_vote_under_count integer not null, @@ -582,23 +583,22 @@ create table assertion winner varchar(255) not null ); -create table assertion_context +create table assertion_discrepancies ( - id bigint not null - constraint fk_assertion_assertion_context + id bigint not null + constraint fkt31yi3mf6c9axmt1gn1mu33ea references assertion, - assumed_continuing varchar(255) not null + discrepancy integer not null, + cvr_id bigint not null, + primary key (id, cvr_id) ); -create table assertion_discrepancy +create table assertion_context ( - id bigint not null - constraint fk_assertion_assertion_discrepancy_id + id bigint not null + constraint fki0lyp4tghtpohaa9ma6kv2174 references assertion, - cvr_id bigint not null - constraint fk_cast_vote_record_assertion_discrepancy - references cast_vote_record, - assumed_continuing varchar(255) not null + assumed_continuing varchar(255) not null ); create table audit_to_assertions diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index ce2a6f3..d57d8c8 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -1,8 +1,11 @@ +-- noinspection SqlDialectInspectionForFile + -- Test Counties INSERT INTO county (id, name) values (8, 'Ballina'); INSERT INTO county (id, name) values (9, 'Byron'); INSERT INTO county (id, name) values (10, 'Westgarth'); INSERT INTO county (id, name) values (11, 'Malformed'); +INSERT INTO county (id, name) values (12, 'One Assertion County'); -- Contest -- Simple contests to test basic functioning. INSERT INTO contest (county_id, id, version, description, name, sequence_number, votes_allowed, winners_allowed) values (8, 999990, 0, 'IRV', 'Multi-County Contest 1', 0, 7, 1); @@ -26,6 +29,11 @@ INSERT INTO contest (county_id, id, version, description, name, sequence_number, INSERT INTO contest (county_id, id, version, description, name, sequence_number, votes_allowed, winners_allowed) values (11, 999985, 0, 'IRV', 'Malformed Contest 3', 13, 4, 1); INSERT INTO contest (county_id, id, version, description, name, sequence_number, votes_allowed, winners_allowed) values (11, 999984, 0, 'IRV', 'Malformed Contest 4', 14, 4, 1); INSERT INTO contest (county_id, id, version, description, name, sequence_number, votes_allowed, winners_allowed) values (11, 999983, 0, 'IRV', 'Malformed Contest 5', 15, 4, 1); +-- Contest to test assertion removal (where only one assertion exists) +INSERT INTO contest (county_id, id, version, description, name, sequence_number, votes_allowed, winners_allowed) values (12, 999970, 0, 'IRV', 'One NEB Assertion Contest', 16, 5, 1); +INSERT INTO contest (county_id, id, version, description, name, sequence_number, votes_allowed, winners_allowed) values (12, 999971, 0, 'IRV', 'One NEN Assertion Contest', 17, 5, 1); +INSERT INTO contest (county_id, id, version, description, name, sequence_number, votes_allowed, winners_allowed) values (12, 999972, 0, 'IRV', 'One NEN NEB Assertion Contest', 18, 5, 1); + --CVRs INSERT INTO cast_vote_record (id, cvr_number, ballot_type, batch_id, county_id, imprinted_id, record_id, record_type, scanner_id) values (1, 1, 'Type 1', 1, 8, '1-1-1', 1, 'UPLOADED', 1); INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (1, 8, '["Alice","Bob","Charlie"]', 999998, 0); @@ -67,6 +75,18 @@ INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) val INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (12, 11, NULL, 999986, 14); -- This entry is not an error, just a blank vote. INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (12, 11, '[]', 999985, 15); --- A vote where the choice string is not a list. +-- Votes where the choice string is not a list. INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (12, 11, 'NotAList', 999984, 16); -INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (12, 11, '', 999983, 17); \ No newline at end of file +INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (12, 11, '', 999983, 17); + +-- Assertions +INSERT INTO assertion values ('NEB', 0, 'One NEB Assertion Contest', 1.1, 0.32, 'Bob', 100, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice'); + +INSERT INTO assertion values ('NEN', 1, 'One NEN Assertion Contest', 3.01, 0.12, 'Charlie', 240, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice'); +INSERT INTO assertion_context values (1, 'Diego'); +INSERT INTO assertion_context values (1, 'Bob'); + +INSERT INTO assertion values ('NEB', 2, 'One NEN NEB Assertion Contest', 0.1, 0.52, 'Liesl', 112, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Amanda'); + +INSERT INTO assertion values ('NEN', 3, 'One NEN NEB Assertion Contest', 3.17, 0.72, 'Wendell', 250, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Amanda'); +INSERT INTO assertion_context values (3, 'Liesl'); \ No newline at end of file From 5bdfc31984de9685225634ad2b9936e8df7c5713 Mon Sep 17 00:00:00 2001 From: michelleblom Date: Mon, 8 Apr 2024 18:54:29 +1000 Subject: [PATCH 07/13] Added method in AssertionRepository to translate raire-java assertions into Assertion entities. Supplied the raire-java NotEliminatedNext/NotEliminatedBefore constructs to the respective NEB and NENAssertion constructors. --- .../persistence/entity/Assertion.java | 44 ++++++++------- .../persistence/entity/NEBAssertion.java | 23 +++++--- .../persistence/entity/NENAssertion.java | 23 ++++++-- .../repository/AssertionRepository.java | 55 +++++++++++++++++++ 4 files changed, 113 insertions(+), 32 deletions(-) diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java index b699d13..7e7b25e 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java @@ -20,7 +20,6 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra package au.org.democracydevelopers.raireservice.persistence.entity; -import au.org.democracydevelopers.raireservice.request.RequestValidationException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.HashMap; @@ -46,26 +45,32 @@ public abstract class Assertion { @GeneratedValue(strategy = GenerationType.SEQUENCE) private long id; + /** + * Version. Used for optimistic locking. + */ + @Column(name = "version", updatable = false, nullable = false) + private long version; + /** * Name of the contest for which this Assertion was generated. */ @Column(name = "contest_name", updatable = false, nullable = false) - protected String contestName; + private String contestName; @Column(name = "winner", updatable = false, nullable = false) - protected String winner; + private String winner; /** * Loser of the Assertion (a candidate in the contest). */ @Column(name = "loser", updatable = false, nullable = false) - protected String loser; + private String loser; /** * Assertion margin (note: this is not the *diluted* margin). */ @Column(name = "margin", updatable = false, nullable = false) - protected int margin; + private int margin; /** * Assertion difficulty, as estimated by raire-java. (Note that raire-java has multiple ways @@ -73,7 +78,7 @@ public abstract class Assertion { * of ballots. For example, one method may be: difficulty = 1 / assertion margin). */ @Column(name = "difficulty", updatable = false, nullable = false) - protected double difficulty; + private double difficulty; /** * List of candidates that the Assertion assumes are 'continuing' in the Assertion's context. @@ -81,14 +86,14 @@ public abstract class Assertion { @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "assertion_context", joinColumns = @JoinColumn(name = "id")) @Column(updatable = false, nullable = false) - protected List assumedContinuing = new ArrayList<>(); + private List assumedContinuing = new ArrayList<>(); /** * Diluted margin for the Assertion. This is equal to the assertion margin divided by the * number of ballots in the relevant auditing universe. */ @Column(name = "diluted_margin", updatable = false, nullable = false) - protected double dilutedMargin; + private double dilutedMargin; /** * Maximum discrepancies that have been recorded against this assertion, if any, for a given @@ -98,52 +103,52 @@ public abstract class Assertion { @CollectionTable(name = "assertion_discrepancies", joinColumns = @JoinColumn(name = "id")) @MapKeyColumn(name = "cvr_id") @Column(name = "discrepancy", updatable = false, nullable = false) - protected Map cvrDiscrepancy = new HashMap<>(); + private Map cvrDiscrepancy = new HashMap<>(); /** * The expected number of samples to audit overall for the Assertion, assuming overstatements * continue at the current rate experienced in the audit. */ @Column(updatable = false, nullable = false) - protected Integer estimated_samples_to_audit = 0; + private int estimated_samples_to_audit = 0; /** * The two-vote understatements recorded against the Assertion. */ @Column(updatable = false, nullable = false) - protected Integer two_vote_under_count = 0; + private int two_vote_under_count = 0; /** * The one-vote understatements recorded against the Assertion. */ @Column(updatable = false, nullable = false) - protected Integer one_vote_under_count = 0; + private int one_vote_under_count = 0; /** * The one-vote overstatements recorded against the Assertion. */ @Column(updatable = false, nullable = false) - protected Integer one_vote_over_count = 0; + private int one_vote_over_count = 0; /** * The two-vote overstatements recorded against the Assertion. */ @Column(updatable = false, nullable = false) - protected Integer two_vote_over_count = 0; + private int two_vote_over_count = 0; /** * Discrepancies recorded against the Assertion that are neither understatements nor * overstatements. */ @Column(updatable = false, nullable = false) - protected Integer other_count = 0; + private int other_count = 0; /** * Current risk measurement recorded against the Assertion. It is initialized to 1, as prior * to an audit starting, and without additional information, we assume maximum risk. */ @Column(updatable = false, nullable = false) - protected BigDecimal current_risk = BigDecimal.valueOf(1); + private BigDecimal current_risk = BigDecimal.valueOf(1); /** * Construct an empty Assertion (for persistence). @@ -159,11 +164,11 @@ public Assertion() {} * @param universeSize Total number of ballots in the auditing universe of the Assertion. * @param difficulty Assertion difficulty, as computed by raire-java. * @param assumedContinuing List of candidates, by name, that the Assertion assumes is continuing. - * @throws RequestValidationException if the caller supplies a non-positive universe size. + * @throws IllegalStateException if the caller supplies a non-positive universe size. */ public Assertion(String contestName, String winner, String loser, int margin, long universeSize, double difficulty, List assumedContinuing) - throws RequestValidationException + throws IllegalStateException { this.contestName = contestName; this.winner = winner; @@ -173,7 +178,7 @@ public Assertion(String contestName, String winner, String loser, int margin, if(universeSize <= 0){ String msg = "An assertion must have a positive universe size."; logger.error(msg); - throw new RequestValidationException(msg); + throw new IllegalStateException(msg); } this.dilutedMargin = margin / (double) universeSize; @@ -201,7 +206,6 @@ public String getLoser() { return loser; } - /** * Returns the name of the contest for which the Assertion has been formed. */ diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java index d13b8fc..c30d338 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java @@ -20,10 +20,9 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra package au.org.democracydevelopers.raireservice.persistence.entity; -import au.org.democracydevelopers.raireservice.request.RequestValidationException; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; -import java.util.List; +import java.util.ArrayList; @Entity @DiscriminatorValue("NEB") @@ -37,13 +36,23 @@ public NEBAssertion() { } /** - * {@inheritDoc} + * Construct a NEBAssertion give a raire-java NotEliminatedBefore construct. + * @param contestName Name of the contest to which this assertion belongs. + * @param universeSize Number of ballots in the auditing universe for the assertion. + * @param margin Absolute margin of the assertion. + * @param difficulty Difficulty of the assertion, as computed by raire-java. + * @param candidates Names of the candidates in this assertion's contest. + * @param neb Raire-java NotEliminatedBefore assertion to be transformed into a NENAssertion. + * @throws IllegalStateException if the caller supplies a non-positive universe size. + * @throws ArrayIndexOutOfBoundsException if the winner or loser indices in the raire-java + * assertion are invalid with respect to the given array of candidates. */ - public NEBAssertion(String contestName, String winner, String loser, int margin, - long universeSize, double difficulty, List assumedContinuing) - throws RequestValidationException + public NEBAssertion(String contestName, long universeSize, int margin, double difficulty, + String[] candidates, au.org.democracydevelopers.raire.assertions.NotEliminatedBefore neb) + throws IllegalStateException, ArrayIndexOutOfBoundsException { - super(contestName, winner, loser, margin, universeSize, difficulty, assumedContinuing); + super(contestName, candidates[neb.winner], candidates[neb.loser], margin, universeSize, + difficulty, new ArrayList<>()); } } diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java index e74db4e..58b3127 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java @@ -23,6 +23,8 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra import au.org.democracydevelopers.raireservice.request.RequestValidationException; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; @Entity @@ -37,13 +39,24 @@ public NENAssertion() { } /** - * {@inheritDoc} + * Construct a NENAssertion give a raire-java NotEliminatedNext construct. + * @param contestName Name of the contest to which this assertion belongs. + * @param universeSize Number of ballots in the auditing universe for the assertion. + * @param margin Absolute margin of the assertion. + * @param difficulty Difficulty of the assertion, as computed by raire-java. + * @param candidates Names of the candidates in this assertion's contest. + * @param nen Raire-java NotEliminatedNext assertion to be transformed into a NENAssertion. + * @throws IllegalStateException if the caller supplies a non-positive universe size. + * @throws ArrayIndexOutOfBoundsException if the winner or loser indices in the raire-java + * assertion are invalid with respect to the given array of candidates. */ - public NENAssertion(String contestName, String winner, String loser, int margin, - long universeSize, double difficulty, List assumedContinuing) - throws RequestValidationException + public NENAssertion(String contestName, long universeSize, int margin, double difficulty, + String[] candidates, au.org.democracydevelopers.raire.assertions.NotEliminatedNext nen) + throws IllegalStateException, ArrayIndexOutOfBoundsException { - super(contestName, winner, loser, margin, universeSize, difficulty, assumedContinuing); + super(contestName, candidates[nen.winner], candidates[nen.loser], margin, universeSize, + difficulty, Arrays.stream(nen.continuing).mapToObj(i -> candidates[i]).toList()); } + } diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java index 2cf71cf..df04ef3 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java @@ -20,8 +20,16 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra package au.org.democracydevelopers.raireservice.persistence.repository; +import au.org.democracydevelopers.raire.assertions.AssertionAndDifficulty; +import au.org.democracydevelopers.raire.assertions.NotEliminatedBefore; +import au.org.democracydevelopers.raire.assertions.NotEliminatedNext; import au.org.democracydevelopers.raireservice.persistence.entity.Assertion; +import au.org.democracydevelopers.raireservice.persistence.entity.NEBAssertion; +import au.org.democracydevelopers.raireservice.persistence.entity.NENAssertion; + +import java.util.Arrays; import java.util.List; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -47,4 +55,51 @@ public interface AssertionRepository extends JpaRepository { * @return The number of records deleted from the database. */ long deleteByContestName(String contestName); + + /** + * For the given collection of raire-java assertions, transform them into a form suitable + * for storing in the corla database and save them to the database. + * @param contestName Name of the contest to which these assertions belong. + * @param universeSize Number of ballots in the auditing universe for these assertions. + * @param candidates Names of the candidates in the contest. + * @param assertions Array of raire-java assertions for the contest. + * @throws IllegalStateException if the caller supplies a non-positive universe size. + * @throws ArrayIndexOutOfBoundsException if the winner or loser indices in any of the raire-java + * assertions are invalid with respect to the given array of candidates. + */ + default void storeAssertions(String contestName, long universeSize, String[] candidates, + AssertionAndDifficulty[] assertions) + throws IllegalArgumentException, ArrayIndexOutOfBoundsException + { + List translated = Arrays.stream(assertions).map(a -> createAssertion(contestName, + universeSize, candidates, a)).toList(); + + this.saveAll(translated); + } + + /** + * Translates a raire-java assertion into an Assertion, suitable for saving to the database. + * @param contestName Name of the contest to which this assertion belongs. + * @param universeSize Number of ballots in the auditing universe for the assertion. + * @param candidates Names of the candidates in the contest. + * @param aad The raire-java AssertionAndDifficulty to be translated to an Assertion. + * @return Assertion object, ready for saving to the database. + * @throws IllegalStateException if the caller supplies a non-positive universe size. + * @throws ArrayIndexOutOfBoundsException if the winner or loser indices in the raire-java + * assertion are invalid with respect to the given array of candidates. + */ + private Assertion createAssertion(String contestName, long universeSize, String[] candidates, + au.org.democracydevelopers.raire.assertions.AssertionAndDifficulty aad) + throws IllegalStateException, ArrayIndexOutOfBoundsException + { + if(aad.assertion.isNEB()){ + return new NEBAssertion(contestName, universeSize, aad.margin, aad.difficulty, + candidates, (NotEliminatedBefore) aad.assertion); + } + else{ + return new NENAssertion(contestName, universeSize, aad.margin, aad.difficulty, + candidates, (NotEliminatedNext) aad.assertion); + } + } + } From a405ecc9219eaa9a05cb81047b106a5932612d03 Mon Sep 17 00:00:00 2001 From: michelleblom Date: Mon, 8 Apr 2024 19:01:44 +1000 Subject: [PATCH 08/13] Added class level commenting. --- .../persistence/entity/Assertion.java | 5 +++++ .../persistence/entity/NEBAssertion.java | 9 +++++++++ .../persistence/entity/NENAssertion.java | 15 ++++++++++++--- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java index 7e7b25e..f78ddac 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java @@ -29,6 +29,11 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * RAIRE generates a set of assertions for a given IRV contest. The different types of assertion + * that RAIRE can generate are defined as subclasses of this base Assertion class. For a description + * of what assertions are and the role they play in an IRV audit, see the Guide to RAIRE. + */ @Entity @Table(name = "assertion") @Inheritance(strategy = InheritanceType.SINGLE_TABLE) diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java index c30d338..7930a8b 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java @@ -24,6 +24,15 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra import jakarta.persistence.Entity; import java.util.ArrayList; +/** + * A Not Eliminated Before assertion (or NEB) says that a candidate _winner_ will always have + * a higher tally than a candidate _loser_. What this means is that the minimum possible tally + * that _winner_ will have at any stage of tabulation is greater than the maximum possible + * tally _loser_ can ever achieve. For more detail on NEB assertions, refer to the Guide to RAIRE. + * + * The constructor for this class takes a raire-java NEB assertion construct (NotEliminatedBefore) + * and translates it into a NEBAssertion entity, suitable for storage in the corla database. + */ @Entity @DiscriminatorValue("NEB") public class NEBAssertion extends Assertion { diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java index 58b3127..a777a5b 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java @@ -20,13 +20,22 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra package au.org.democracydevelopers.raireservice.persistence.entity; -import au.org.democracydevelopers.raireservice.request.RequestValidationException; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; -import java.util.ArrayList; + import java.util.Arrays; -import java.util.List; +/** + * A Not Eliminated Next assertion asserts that a _winner_ beats a _loser_ in an audit when all + * candidates other that those in a specified _assumed to be continuing_ list have been removed. + * + * In particular, this means that _winner_ can not be the next candidate eliminated. + * + * This assertion type is also referred to as an NEN assertion in A Guide to RAIRE. + * + * The constructor for this class takes a raire-java NEN assertion construct (NotEliminatedNext) + * and translates it into a NENAssertion entity, suitable for storage in the corla database. + */ @Entity @DiscriminatorValue("NEN") public class NENAssertion extends Assertion { From c0ac10d7bc9a71aeef35a29456ed469e55bfeca6 Mon Sep 17 00:00:00 2001 From: michelleblom Date: Tue, 9 Apr 2024 10:13:30 +1000 Subject: [PATCH 09/13] Simplified AssertionRepository (merged two methods into one), removed getters from Assertion, simplified testing in AssertionRepositoryTests. --- .../persistence/entity/Assertion.java | 121 +++--------------- .../persistence/entity/Contest.java | 1 + .../repository/AssertionRepository.java | 38 ++---- .../repository/AssertionRepositoryTests.java | 118 ++++++++--------- src/test/resources/data.sql | 6 +- 5 files changed, 88 insertions(+), 196 deletions(-) diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java index f78ddac..14f7879 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java @@ -40,7 +40,7 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra @DiscriminatorColumn(name = "assertion_type") public abstract class Assertion { - public static final Logger logger = LoggerFactory.getLogger(Assertion.class); + protected static final Logger logger = LoggerFactory.getLogger(Assertion.class); /** * Assertion ID. @@ -53,7 +53,8 @@ public abstract class Assertion { /** * Version. Used for optimistic locking. */ - @Column(name = "version", updatable = false, nullable = false) + @Version + @Column(name = "version", nullable = false) private long version; /** @@ -114,49 +115,49 @@ public abstract class Assertion { * The expected number of samples to audit overall for the Assertion, assuming overstatements * continue at the current rate experienced in the audit. */ - @Column(updatable = false, nullable = false) - private int estimated_samples_to_audit = 0; + @Column(name = "estimated_samples_to_audit", updatable = false, nullable = false) + private int estimatedSamplesToAudit = 0; /** * The two-vote understatements recorded against the Assertion. */ - @Column(updatable = false, nullable = false) - private int two_vote_under_count = 0; + @Column(name = "two_vote_under_count", updatable = false, nullable = false) + private int twoVoteUnderCount = 0; /** * The one-vote understatements recorded against the Assertion. */ - @Column(updatable = false, nullable = false) - private int one_vote_under_count = 0; + @Column(name = "one_vote_under_count", updatable = false, nullable = false) + private int oneVoteUnderCount = 0; /** * The one-vote overstatements recorded against the Assertion. */ - @Column(updatable = false, nullable = false) - private int one_vote_over_count = 0; + @Column(name = "one_vote_over_count", updatable = false, nullable = false) + public final int oneVoteOverCount = 0; /** * The two-vote overstatements recorded against the Assertion. */ - @Column(updatable = false, nullable = false) - private int two_vote_over_count = 0; + @Column(name = "two_vote_over_count", updatable = false, nullable = false) + private int twoVoteOverCount = 0; /** * Discrepancies recorded against the Assertion that are neither understatements nor * overstatements. */ - @Column(updatable = false, nullable = false) - private int other_count = 0; + @Column(name = "other_count", updatable = false, nullable = false) + private int otherCount = 0; /** * Current risk measurement recorded against the Assertion. It is initialized to 1, as prior * to an audit starting, and without additional information, we assume maximum risk. */ - @Column(updatable = false, nullable = false) - private BigDecimal current_risk = BigDecimal.valueOf(1); + @Column(name = "current_risk", updatable = false, nullable = false) + private BigDecimal currentRisk = BigDecimal.valueOf(1); /** - * Construct an empty Assertion (for persistence). + * Default no-args constructor (for persistence). */ public Assertion() {} @@ -192,91 +193,5 @@ public Assertion(String contestName, String winner, String loser, int margin, this.assumedContinuing = assumedContinuing; } - /** - * Returns the Assertion ID. - */ - public long getId() { return id; } - /** - * Returns the winner of the Assertion. - */ - public String getWinner() { - return winner; - } - - /** - * Returns the loser of the Assertion. - */ - public String getLoser() { - return loser; - } - - /** - * Returns the name of the contest for which the Assertion has been formed. - */ - public String getContestName() { - return contestName; - } - - /** - * Returns the name of the contest for which the Assertion has been formed. - */ - public List getAssumedContinuing() { - return assumedContinuing; - } - - /** - * Returns the current risk measurement recorded against the assertion. - */ - public BigDecimal getCurrentRisk() { return current_risk; } - - /** - * Returns the Assertion's absolute margin. - */ - public int getMargin() { return margin; } - - /** - * Returns the Assertion's difficulty. - */ - public double getDifficulty() { return difficulty; } - - /** - * Returns the Assertion's diluted margin. - */ - public double getDilutedMargin() { return dilutedMargin; } - - /** - * Returns the discrepancies recorded against the Assertion, by CVR id. - */ - public Map getCvrDiscrepancy(){ return cvrDiscrepancy; } - - /** - * Returns the two vote overstatements recorded against the Assertion. - */ - public int getTwoVoteOverCount() { return two_vote_over_count; } - - /** - * Returns the two vote understatements recorded against the Assertion. - */ - public int getTwoVoteUnderCount() { return two_vote_under_count; } - - /** - * Returns the one vote overstatements recorded against the Assertion. - */ - public int getOneVoteOverCount() { return one_vote_over_count; } - - /** - * Returns the one vote understatements recorded against the Assertion. - */ - public int getOneVoteUnderCount() { return one_vote_under_count; } - - /** - * Returns the count of other discrepancies (neither over nor understatements). - */ - public int getOtherDiscrepancies() { return other_count; } - - /** - * Returns the estimated number of samples to audit. - */ - public int getEstimatedSamplesToAudit() { return estimated_samples_to_audit; } } diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Contest.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Contest.java index ecffa16..e95410b 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Contest.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Contest.java @@ -65,6 +65,7 @@ public class Contest { /** * Version. Used for optimistic locking. */ + @Version @ReadOnlyProperty @Column(name = "version", nullable = false) private long version; diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java index df04ef3..b70eef9 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java @@ -20,6 +20,8 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra package au.org.democracydevelopers.raireservice.persistence.repository; +import static java.util.stream.Collectors.toList; + import au.org.democracydevelopers.raire.assertions.AssertionAndDifficulty; import au.org.democracydevelopers.raire.assertions.NotEliminatedBefore; import au.org.democracydevelopers.raire.assertions.NotEliminatedNext; @@ -71,35 +73,17 @@ default void storeAssertions(String contestName, long universeSize, String[] can AssertionAndDifficulty[] assertions) throws IllegalArgumentException, ArrayIndexOutOfBoundsException { - List translated = Arrays.stream(assertions).map(a -> createAssertion(contestName, - universeSize, candidates, a)).toList(); + List translated = Arrays.stream(assertions).map(a -> { + if (a.assertion.isNEB()) { + return new NEBAssertion(contestName, universeSize, a.margin, a.difficulty, + candidates, (NotEliminatedBefore) a.assertion); + } else { + return new NENAssertion(contestName, universeSize, a.margin, a.difficulty, + candidates, (NotEliminatedNext) a.assertion); + } + }).toList(); this.saveAll(translated); } - /** - * Translates a raire-java assertion into an Assertion, suitable for saving to the database. - * @param contestName Name of the contest to which this assertion belongs. - * @param universeSize Number of ballots in the auditing universe for the assertion. - * @param candidates Names of the candidates in the contest. - * @param aad The raire-java AssertionAndDifficulty to be translated to an Assertion. - * @return Assertion object, ready for saving to the database. - * @throws IllegalStateException if the caller supplies a non-positive universe size. - * @throws ArrayIndexOutOfBoundsException if the winner or loser indices in the raire-java - * assertion are invalid with respect to the given array of candidates. - */ - private Assertion createAssertion(String contestName, long universeSize, String[] candidates, - au.org.democracydevelopers.raire.assertions.AssertionAndDifficulty aad) - throws IllegalStateException, ArrayIndexOutOfBoundsException - { - if(aad.assertion.isNEB()){ - return new NEBAssertion(contestName, universeSize, aad.margin, aad.difficulty, - candidates, (NotEliminatedBefore) aad.assertion); - } - else{ - return new NENAssertion(contestName, universeSize, aad.margin, aad.difficulty, - candidates, (NotEliminatedNext) aad.assertion); - } - } - } diff --git a/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java b/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java index 55251be..121889e 100644 --- a/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java +++ b/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java @@ -24,7 +24,8 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra import au.org.democracydevelopers.raireservice.persistence.entity.Assertion; import au.org.democracydevelopers.raireservice.persistence.entity.NEBAssertion; import au.org.democracydevelopers.raireservice.persistence.entity.NENAssertion; -import java.math.BigDecimal; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -51,6 +52,53 @@ public class AssertionRepositoryTests { @Autowired AssertionRepository assertionRepository; + /** + * To facilitate easier checking of retrieved/saved assertion content. + */ + private static final Gson GSON = + new GsonBuilder().serializeNulls().disableHtmlEscaping().create(); + + /** + * Test assertion: Alice NEB Bob in the contest "One NEB Assertion Contest". + */ + private String aliceNEBBob = "{\"id\":0,\"version\":0,\"contestName\":" + + "\"One NEB Assertion Contest\",\"winner\":\"Alice\",\"loser\":\"Bob\",\"margin\":100," + + "\"difficulty\":1.1,\"assumedContinuing\":[],\"dilutedMargin\":0.32,\"cvrDiscrepancy\":{}," + + "\"estimatedSamplesToAudit\":0,\"twoVoteUnderCount\":0,\"oneVoteUnderCount\":0," + + "\"oneVoteOverCount\":0,\"twoVoteOverCount\":0,\"otherCount\":0,\"currentRisk\":1.00}"; + + /** + * Test assertion: Alice NEN Charlie assuming Alice, Charlie, Diego and Bob are continuing, + * for the contest "One NEN Assertion Contest". + */ + private String aliceNENCharlie = "{\"id\":1,\"version\":0,\"contestName\":" + + "\"One NEN Assertion Contest\",\"winner\":\"Alice\",\"loser\":\"Charlie\",\"margin\":240," + + "\"difficulty\":3.01,\"assumedContinuing\":[\"Alice\",\"Charlie\",\"Diego\",\"Bob\"]," + + "\"dilutedMargin\":0.12,\"cvrDiscrepancy\":{},\"estimatedSamplesToAudit\":0," + + "\"twoVoteUnderCount\":0,\"oneVoteUnderCount\":0,\"oneVoteOverCount\":0," + + "\"twoVoteOverCount\":0,\"otherCount\":0,\"currentRisk\":1.00}"; + + /** + * Test assertion: Amanda NEB Liesl in the contest "One NEN NEB Assertion Contest". + */ + private String amandaNEBLiesl = "{\"id\":2,\"version\":0,\"contestName\":" + + "\"One NEN NEB Assertion Contest\",\"winner\":\"Amanda\",\"loser\":\"Liesl\",\"margin\":112,"+ + "\"difficulty\":0.1,\"assumedContinuing\":[],\"dilutedMargin\":0.52," + + "\"cvrDiscrepancy\":{},\"estimatedSamplesToAudit\":0,\"twoVoteUnderCount\":0," + + "\"oneVoteUnderCount\":0,\"oneVoteOverCount\":0,\"twoVoteOverCount\":0,\"otherCount\":0," + + "\"currentRisk\":1.00}"; + + /** + * Test assertion: Amanda NEN Wendell assuming Liesl, Wendell and Amanda are continuing, + * for the contest "One NEN NEB Assertion Contest". + */ + private String amandaNENWendell = "{\"id\":3,\"version\":0,\"contestName\":" + + "\"One NEN NEB Assertion Contest\",\"winner\":\"Amanda\",\"loser\":\"Wendell\"," + + "\"margin\":250,\"difficulty\":3.17,\"assumedContinuing\":[\"Liesl\",\"Wendell\"," + + "\"Amanda\"],\"dilutedMargin\":0.72,\"cvrDiscrepancy\":{},\"estimatedSamplesToAudit\":0," + + "\"twoVoteUnderCount\":0,\"oneVoteUnderCount\":0,\"oneVoteOverCount\":0," + + "\"twoVoteOverCount\":0,\"otherCount\":0,\"currentRisk\":1.00}"; + /** * Retrieval of assertions for an existing contest with no associated assertions will return an * empty list. @@ -103,22 +151,7 @@ void retrieveAssertionsExistentContestOneNEBAssertion(){ final Assertion r = retrieved.get(0); assertEquals(NEBAssertion.class, r.getClass()); - assertEquals(0, r.getId()); - assertEquals("Alice", r.getWinner()); - assertEquals("Bob", r.getLoser()); - assertEquals("One NEB Assertion Contest", r.getContestName()); - assertEquals(100, r.getMargin()); - assertEquals(0, r.getCurrentRisk().compareTo(BigDecimal.valueOf(1))); - assertEquals(1.1, r.getDifficulty()); - assertEquals(0.32, r.getDilutedMargin()); - assertEquals(0, r.getOneVoteOverCount()); - assertEquals(0, r.getOneVoteUnderCount()); - assertEquals(0, r.getTwoVoteOverCount()); - assertEquals(0, r.getTwoVoteUnderCount()); - assertEquals(0, r.getOtherDiscrepancies()); - assertEquals(0, r.getEstimatedSamplesToAudit()); - assertEquals(0, r.getCvrDiscrepancy().size()); - assertEquals(0, r.getAssumedContinuing().size()); + assertEquals(aliceNEBBob, GSON.toJson(r)); } /** @@ -132,22 +165,7 @@ void retrieveAssertionsExistentContestOneNENAssertion(){ final Assertion r = retrieved.get(0); assertEquals(NENAssertion.class, r.getClass()); - assertEquals(1, r.getId()); - assertEquals("Alice", r.getWinner()); - assertEquals("Charlie", r.getLoser()); - assertEquals("One NEN Assertion Contest", r.getContestName()); - assertEquals(240, r.getMargin()); - assertEquals(0, r.getCurrentRisk().compareTo(BigDecimal.valueOf(1))); - assertEquals(3.01, r.getDifficulty()); - assertEquals(0.12, r.getDilutedMargin()); - assertEquals(0, r.getOneVoteOverCount()); - assertEquals(0, r.getOneVoteUnderCount()); - assertEquals(0, r.getTwoVoteOverCount()); - assertEquals(0, r.getTwoVoteUnderCount()); - assertEquals(0, r.getOtherDiscrepancies()); - assertEquals(0, r.getEstimatedSamplesToAudit()); - assertEquals(0, r.getCvrDiscrepancy().size()); - assertEquals(List.of("Diego", "Bob"), r.getAssumedContinuing()); + assertEquals(aliceNENCharlie, GSON.toJson(r)); } /** @@ -161,41 +179,11 @@ void retrieveAssertionsExistentContestOneNENOneNEBAssertion(){ final Assertion r1 = retrieved.get(0); assertEquals(NEBAssertion.class, r1.getClass()); - assertEquals(2, r1.getId()); - assertEquals("Amanda", r1.getWinner()); - assertEquals("Liesl", r1.getLoser()); - assertEquals("One NEN NEB Assertion Contest", r1.getContestName()); - assertEquals(112, r1.getMargin()); - assertEquals(0, r1.getCurrentRisk().compareTo(BigDecimal.valueOf(1))); - assertEquals(0.1, r1.getDifficulty()); - assertEquals(0.52, r1.getDilutedMargin()); - assertEquals(0, r1.getOneVoteOverCount()); - assertEquals(0, r1.getOneVoteUnderCount()); - assertEquals(0, r1.getTwoVoteOverCount()); - assertEquals(0, r1.getTwoVoteUnderCount()); - assertEquals(0, r1.getOtherDiscrepancies()); - assertEquals(0, r1.getEstimatedSamplesToAudit()); - assertEquals(0, r1.getCvrDiscrepancy().size()); - assertEquals(0, r1.getAssumedContinuing().size()); + assertEquals(amandaNEBLiesl, GSON.toJson(r1)); final Assertion r2 = retrieved.get(1); assertEquals(NENAssertion.class, r2.getClass()); - assertEquals(3, r2.getId()); - assertEquals("Amanda", r2.getWinner()); - assertEquals("Wendell", r2.getLoser()); - assertEquals("One NEN NEB Assertion Contest", r2.getContestName()); - assertEquals(250, r2.getMargin()); - assertEquals(0, r2.getCurrentRisk().compareTo(BigDecimal.valueOf(1))); - assertEquals(3.17, r2.getDifficulty()); - assertEquals(0.72, r2.getDilutedMargin()); - assertEquals(0, r2.getOneVoteOverCount()); - assertEquals(0, r2.getOneVoteUnderCount()); - assertEquals(0, r2.getTwoVoteOverCount()); - assertEquals(0, r2.getTwoVoteUnderCount()); - assertEquals(0, r2.getOtherDiscrepancies()); - assertEquals(0, r2.getEstimatedSamplesToAudit()); - assertEquals(0, r2.getCvrDiscrepancy().size()); - assertEquals(List.of("Liesl"), r2.getAssumedContinuing()); + assertEquals(amandaNENWendell, GSON.toJson(r2)); } /** diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index d57d8c8..ea5fa72 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -83,10 +83,14 @@ INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) val INSERT INTO assertion values ('NEB', 0, 'One NEB Assertion Contest', 1.1, 0.32, 'Bob', 100, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice'); INSERT INTO assertion values ('NEN', 1, 'One NEN Assertion Contest', 3.01, 0.12, 'Charlie', 240, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice'); +INSERT INTO assertion_context values (1, 'Alice'); +INSERT INTO assertion_context values (1, 'Charlie'); INSERT INTO assertion_context values (1, 'Diego'); INSERT INTO assertion_context values (1, 'Bob'); INSERT INTO assertion values ('NEB', 2, 'One NEN NEB Assertion Contest', 0.1, 0.52, 'Liesl', 112, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Amanda'); INSERT INTO assertion values ('NEN', 3, 'One NEN NEB Assertion Contest', 3.17, 0.72, 'Wendell', 250, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Amanda'); -INSERT INTO assertion_context values (3, 'Liesl'); \ No newline at end of file +INSERT INTO assertion_context values (3, 'Liesl'); +INSERT INTO assertion_context values (3, 'Wendell'); +INSERT INTO assertion_context values (3, 'Amanda'); \ No newline at end of file From 58db506973ddad9d60efc1918201cd7d12fef306 Mon Sep 17 00:00:00 2001 From: michelleblom Date: Thu, 11 Apr 2024 08:27:07 +1000 Subject: [PATCH 10/13] Added checks in Assertion constructor to verify validity of various supplied arguments. Renamed assertion_context table to "assertion_assumed_continuing". --- .../persistence/entity/Assertion.java | 68 +++++++++++++------ .../persistence/entity/NEBAssertion.java | 3 +- .../persistence/entity/NENAssertion.java | 3 +- src/test/resources/corla.sql | 2 +- src/test/resources/data.sql | 14 ++-- 5 files changed, 59 insertions(+), 31 deletions(-) diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java index 14f7879..fbc4d6e 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java @@ -54,7 +54,7 @@ public abstract class Assertion { * Version. Used for optimistic locking. */ @Version - @Column(name = "version", nullable = false) + @Column(name = "version", updatable = false, nullable = false) private long version; /** @@ -90,7 +90,7 @@ public abstract class Assertion { * 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(updatable = false, nullable = false) private List assumedContinuing = new ArrayList<>(); @@ -102,8 +102,9 @@ public abstract class Assertion { private double dilutedMargin; /** - * Maximum discrepancies that have been recorded against this assertion, if any, for a given - * CVR identified through its ID. + * A map between CVR ID and the discrepancy recorded against that CVR for this assertion + * in the assertions contest, if one exists. CVRs are only present in this map if + * a discrepancy exists between it and the paper ballot in the assertions contest. */ @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "assertion_discrepancies", joinColumns = @JoinColumn(name = "id")) @@ -157,7 +158,7 @@ public abstract class Assertion { private BigDecimal currentRisk = BigDecimal.valueOf(1); /** - * Default no-args constructor (for persistence). + * Default no-args constructor (required for persistence). */ public Assertion() {} @@ -170,27 +171,52 @@ public Assertion() {} * @param universeSize Total number of ballots in the auditing universe of the Assertion. * @param difficulty Assertion difficulty, as computed by raire-java. * @param assumedContinuing List of candidates, by name, that the Assertion assumes is continuing. - * @throws IllegalStateException if the caller supplies a non-positive universe size. + * @throws IllegalStateException if the caller supplies a non-positive universe size, invalid + * margin, or invalid combination of winner, loser and list of assumed continuing candidates. */ public Assertion(String contestName, String winner, String loser, int margin, long universeSize, double difficulty, List assumedContinuing) throws IllegalStateException { - this.contestName = contestName; - this.winner = winner; - this.loser = loser; - this.margin = margin; - - if(universeSize <= 0){ - String msg = "An assertion must have a positive universe size."; - logger.error(msg); - throw new IllegalStateException(msg); - } - - this.dilutedMargin = margin / (double) universeSize; - - this.difficulty = difficulty; - this.assumedContinuing = assumedContinuing; + this.contestName = contestName; + this.winner = winner; + this.loser = loser; + this.margin = margin; + + if(universeSize <= 0){ + String msg = "An assertion must have a positive universe size."; + logger.error(msg); + throw new IllegalStateException(msg); + } + + if(margin < 0){ + String msg = "An assertion must have a non-negative margin."; + logger.error(msg); + throw new IllegalStateException(msg); + } + + if(winner.isBlank() || loser.isBlank()){ + String msg = "The winner and loser of an assertion must not be blank/null."; + logger.error(msg); + throw new IllegalStateException(msg); + } + + if(winner.equals(loser)){ + String msg = "The winner and loser of an assertion must not be the same candidate."; + logger.error(msg); + throw new IllegalStateException(msg); + } + + if(!assumedContinuing.contains(winner) || !assumedContinuing.contains(loser)){ + String msg = "The winner and loser of an assertion must also be continuing candidates."; + logger.error(msg); + throw new IllegalStateException(msg); + } + + this.dilutedMargin = margin / (double) universeSize; + + this.difficulty = difficulty; + this.assumedContinuing = assumedContinuing; } diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java index 7930a8b..95ae8c2 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java @@ -52,7 +52,8 @@ public NEBAssertion() { * @param difficulty Difficulty of the assertion, as computed by raire-java. * @param candidates Names of the candidates in this assertion's contest. * @param neb Raire-java NotEliminatedBefore assertion to be transformed into a NENAssertion. - * @throws IllegalStateException if the caller supplies a non-positive universe size. + * @throws IllegalStateException if the caller supplies a non-positive universe size, invalid + * margin, or invalid combination of winner, loser and list of assumed continuing candidates. * @throws ArrayIndexOutOfBoundsException if the winner or loser indices in the raire-java * assertion are invalid with respect to the given array of candidates. */ diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java index a777a5b..fc22df4 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java @@ -55,7 +55,8 @@ public NENAssertion() { * @param difficulty Difficulty of the assertion, as computed by raire-java. * @param candidates Names of the candidates in this assertion's contest. * @param nen Raire-java NotEliminatedNext assertion to be transformed into a NENAssertion. - * @throws IllegalStateException if the caller supplies a non-positive universe size. + * @throws IllegalStateException if the caller supplies a non-positive universe size, invalid + * margin, or invalid combination of winner, loser and list of assumed continuing candidates. * @throws ArrayIndexOutOfBoundsException if the winner or loser indices in the raire-java * assertion are invalid with respect to the given array of candidates. */ diff --git a/src/test/resources/corla.sql b/src/test/resources/corla.sql index 4106760..4856bc7 100644 --- a/src/test/resources/corla.sql +++ b/src/test/resources/corla.sql @@ -593,7 +593,7 @@ create table assertion_discrepancies primary key (id, cvr_id) ); -create table assertion_context +create table assertion_assumed_continuing ( id bigint not null constraint fki0lyp4tghtpohaa9ma6kv2174 diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index ea5fa72..eaa2910 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -83,14 +83,14 @@ INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) val INSERT INTO assertion values ('NEB', 0, 'One NEB Assertion Contest', 1.1, 0.32, 'Bob', 100, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice'); INSERT INTO assertion values ('NEN', 1, 'One NEN Assertion Contest', 3.01, 0.12, 'Charlie', 240, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice'); -INSERT INTO assertion_context values (1, 'Alice'); -INSERT INTO assertion_context values (1, 'Charlie'); -INSERT INTO assertion_context values (1, 'Diego'); -INSERT INTO assertion_context values (1, 'Bob'); +INSERT INTO assertion_assumed_continuing values (1, 'Alice'); +INSERT INTO assertion_assumed_continuing values (1, 'Charlie'); +INSERT INTO assertion_assumed_continuing values (1, 'Diego'); +INSERT INTO assertion_assumed_continuing values (1, 'Bob'); INSERT INTO assertion values ('NEB', 2, 'One NEN NEB Assertion Contest', 0.1, 0.52, 'Liesl', 112, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Amanda'); INSERT INTO assertion values ('NEN', 3, 'One NEN NEB Assertion Contest', 3.17, 0.72, 'Wendell', 250, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Amanda'); -INSERT INTO assertion_context values (3, 'Liesl'); -INSERT INTO assertion_context values (3, 'Wendell'); -INSERT INTO assertion_context values (3, 'Amanda'); \ No newline at end of file +INSERT INTO assertion_assumed_continuing values (3, 'Liesl'); +INSERT INTO assertion_assumed_continuing values (3, 'Wendell'); +INSERT INTO assertion_assumed_continuing values (3, 'Amanda'); \ No newline at end of file From bbb0457da1296f7c7c219a80d8fb835cd83d7ab9 Mon Sep 17 00:00:00 2001 From: michelleblom Date: Fri, 12 Apr 2024 14:45:18 +1000 Subject: [PATCH 11/13] Fleshed out assertion repository tests with tests for the translation and saving of assertions (with both valid and invalid input). --- .../persistence/entity/Assertion.java | 64 +- .../persistence/entity/NEBAssertion.java | 2 +- .../persistence/entity/NENAssertion.java | 10 +- .../repository/AssertionRepository.java | 12 +- .../repository/CVRContestInfoRepository.java | 10 +- .../repository/AssertionRepositoryTests.java | 588 +++++++++++++++++- src/test/resources/data.sql | 16 +- src/test/resources/simple_assertions.sql | 23 + 8 files changed, 646 insertions(+), 79 deletions(-) create mode 100644 src/test/resources/simple_assertions.sql diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java index fbc4d6e..b03b1d4 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java @@ -61,22 +61,22 @@ public abstract class Assertion { * Name of the contest for which this Assertion was generated. */ @Column(name = "contest_name", updatable = false, nullable = false) - private String contestName; + protected String contestName; @Column(name = "winner", updatable = false, nullable = false) - private String winner; + protected String winner; /** * Loser of the Assertion (a candidate in the contest). */ @Column(name = "loser", updatable = false, nullable = false) - private String loser; + protected String loser; /** * Assertion margin (note: this is not the *diluted* margin). */ @Column(name = "margin", updatable = false, nullable = false) - private int margin; + protected int margin; /** * Assertion difficulty, as estimated by raire-java. (Note that raire-java has multiple ways @@ -84,7 +84,7 @@ public abstract class Assertion { * of ballots. For example, one method may be: difficulty = 1 / assertion margin). */ @Column(name = "difficulty", updatable = false, nullable = false) - private double difficulty; + protected double difficulty; /** * List of candidates that the Assertion assumes are 'continuing' in the Assertion's context. @@ -92,14 +92,14 @@ public abstract class Assertion { @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "assertion_assumed_continuing", joinColumns = @JoinColumn(name = "id")) @Column(updatable = false, nullable = false) - private List assumedContinuing = new ArrayList<>(); + protected List assumedContinuing = new ArrayList<>(); /** * Diluted margin for the Assertion. This is equal to the assertion margin divided by the * number of ballots in the relevant auditing universe. */ @Column(name = "diluted_margin", updatable = false, nullable = false) - private double dilutedMargin; + protected double dilutedMargin; /** * A map between CVR ID and the discrepancy recorded against that CVR for this assertion @@ -110,52 +110,59 @@ public abstract class Assertion { @CollectionTable(name = "assertion_discrepancies", joinColumns = @JoinColumn(name = "id")) @MapKeyColumn(name = "cvr_id") @Column(name = "discrepancy", updatable = false, nullable = false) - private Map cvrDiscrepancy = new HashMap<>(); + protected Map cvrDiscrepancy = new HashMap<>(); /** * The expected number of samples to audit overall for the Assertion, assuming overstatements * continue at the current rate experienced in the audit. */ @Column(name = "estimated_samples_to_audit", updatable = false, nullable = false) - private int estimatedSamplesToAudit = 0; + protected int estimatedSamplesToAudit = 0; + + /** + * The expected number of samples to audit overall for the Assertion, assuming no further + * overstatements will be encountered in the audit. + */ + @Column(name = "optimistic_samples_to_audit", updatable = false, nullable = false) + protected int optimisticSamplesToAudit = 0; /** * The two-vote understatements recorded against the Assertion. */ @Column(name = "two_vote_under_count", updatable = false, nullable = false) - private int twoVoteUnderCount = 0; + protected int twoVoteUnderCount = 0; /** * The one-vote understatements recorded against the Assertion. */ @Column(name = "one_vote_under_count", updatable = false, nullable = false) - private int oneVoteUnderCount = 0; + protected int oneVoteUnderCount = 0; /** * The one-vote overstatements recorded against the Assertion. */ @Column(name = "one_vote_over_count", updatable = false, nullable = false) - public final int oneVoteOverCount = 0; + protected int oneVoteOverCount = 0; /** * The two-vote overstatements recorded against the Assertion. */ @Column(name = "two_vote_over_count", updatable = false, nullable = false) - private int twoVoteOverCount = 0; + protected int twoVoteOverCount = 0; /** * Discrepancies recorded against the Assertion that are neither understatements nor * overstatements. */ @Column(name = "other_count", updatable = false, nullable = false) - private int otherCount = 0; + protected int otherCount = 0; /** * Current risk measurement recorded against the Assertion. It is initialized to 1, as prior * to an audit starting, and without additional information, we assume maximum risk. */ @Column(name = "current_risk", updatable = false, nullable = false) - private BigDecimal currentRisk = BigDecimal.valueOf(1); + protected BigDecimal currentRisk = new BigDecimal("1.00"); /** * Default no-args constructor (required for persistence). @@ -171,12 +178,12 @@ public Assertion() {} * @param universeSize Total number of ballots in the auditing universe of the Assertion. * @param difficulty Assertion difficulty, as computed by raire-java. * @param assumedContinuing List of candidates, by name, that the Assertion assumes is continuing. - * @throws IllegalStateException if the caller supplies a non-positive universe size, invalid + * @throws IllegalArgumentException if the caller supplies a non-positive universe size, invalid * margin, or invalid combination of winner, loser and list of assumed continuing candidates. */ public Assertion(String contestName, String winner, String loser, int margin, long universeSize, double difficulty, List assumedContinuing) - throws IllegalStateException + throws IllegalArgumentException { this.contestName = contestName; this.winner = winner; @@ -186,31 +193,19 @@ public Assertion(String contestName, String winner, String loser, int margin, if(universeSize <= 0){ String msg = "An assertion must have a positive universe size."; logger.error(msg); - throw new IllegalStateException(msg); - } - - if(margin < 0){ - String msg = "An assertion must have a non-negative margin."; - logger.error(msg); - throw new IllegalStateException(msg); + throw new IllegalArgumentException(msg); } - if(winner.isBlank() || loser.isBlank()){ - String msg = "The winner and loser of an assertion must not be blank/null."; + if(margin < 0 || margin > universeSize){ + String msg = "An assertion must have a non-negative margin that is less than universe size"; logger.error(msg); - throw new IllegalStateException(msg); + throw new IllegalArgumentException(msg); } if(winner.equals(loser)){ String msg = "The winner and loser of an assertion must not be the same candidate."; logger.error(msg); - throw new IllegalStateException(msg); - } - - if(!assumedContinuing.contains(winner) || !assumedContinuing.contains(loser)){ - String msg = "The winner and loser of an assertion must also be continuing candidates."; - logger.error(msg); - throw new IllegalStateException(msg); + throw new IllegalArgumentException(msg); } this.dilutedMargin = margin / (double) universeSize; @@ -219,5 +214,4 @@ public Assertion(String contestName, String winner, String loser, int margin, this.assumedContinuing = assumedContinuing; } - } diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java index 95ae8c2..dc696df 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NEBAssertion.java @@ -52,7 +52,7 @@ public NEBAssertion() { * @param difficulty Difficulty of the assertion, as computed by raire-java. * @param candidates Names of the candidates in this assertion's contest. * @param neb Raire-java NotEliminatedBefore assertion to be transformed into a NENAssertion. - * @throws IllegalStateException if the caller supplies a non-positive universe size, invalid + * @throws IllegalArgumentException if the caller supplies a non-positive universe size, invalid * margin, or invalid combination of winner, loser and list of assumed continuing candidates. * @throws ArrayIndexOutOfBoundsException if the winner or loser indices in the raire-java * assertion are invalid with respect to the given array of candidates. diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java index fc22df4..ff6fd64 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/NENAssertion.java @@ -55,17 +55,23 @@ public NENAssertion() { * @param difficulty Difficulty of the assertion, as computed by raire-java. * @param candidates Names of the candidates in this assertion's contest. * @param nen Raire-java NotEliminatedNext assertion to be transformed into a NENAssertion. - * @throws IllegalStateException if the caller supplies a non-positive universe size, invalid + * @throws IllegalArgumentException if the caller supplies a non-positive universe size, invalid * margin, or invalid combination of winner, loser and list of assumed continuing candidates. * @throws ArrayIndexOutOfBoundsException if the winner or loser indices in the raire-java * assertion are invalid with respect to the given array of candidates. */ public NENAssertion(String contestName, long universeSize, int margin, double difficulty, String[] candidates, au.org.democracydevelopers.raire.assertions.NotEliminatedNext nen) - throws IllegalStateException, ArrayIndexOutOfBoundsException + throws IllegalArgumentException, ArrayIndexOutOfBoundsException { super(contestName, candidates[nen.winner], candidates[nen.loser], margin, universeSize, difficulty, Arrays.stream(nen.continuing).mapToObj(i -> candidates[i]).toList()); + + if(!assumedContinuing.contains(winner) || !assumedContinuing.contains(loser)){ + String msg = "The winner and loser of an assertion must also be continuing candidates."; + logger.error(msg); + throw new IllegalArgumentException(msg); + } } diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java index b70eef9..df835f2 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepository.java @@ -20,8 +20,6 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra package au.org.democracydevelopers.raireservice.persistence.repository; -import static java.util.stream.Collectors.toList; - import au.org.democracydevelopers.raire.assertions.AssertionAndDifficulty; import au.org.democracydevelopers.raire.assertions.NotEliminatedBefore; import au.org.democracydevelopers.raire.assertions.NotEliminatedNext; @@ -60,16 +58,20 @@ public interface AssertionRepository extends JpaRepository { /** * For the given collection of raire-java assertions, transform them into a form suitable - * for storing in the corla database and save them to the database. + * for storing in the corla database and save them to the database. Note that this method will + * not verify that the provided array of candidate names are the candidates for the contest or + * that the names themselves are valid, as stored in the database, or that a contest of the + * given name exists. * @param contestName Name of the contest to which these assertions belong. * @param universeSize Number of ballots in the auditing universe for these assertions. * @param candidates Names of the candidates in the contest. * @param assertions Array of raire-java assertions for the contest. - * @throws IllegalStateException if the caller supplies a non-positive universe size. + * @throws IllegalArgumentException if the caller supplies a non-positive universe size, + * invalid margin, or invalid combination of winner, loser and list of assumed continuing candidates. * @throws ArrayIndexOutOfBoundsException if the winner or loser indices in any of the raire-java * assertions are invalid with respect to the given array of candidates. */ - default void storeAssertions(String contestName, long universeSize, String[] candidates, + default void translateAndSaveAssertions(String contestName, long universeSize, String[] candidates, AssertionAndDifficulty[] assertions) throws IllegalArgumentException, ArrayIndexOutOfBoundsException { diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/CVRContestInfoRepository.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/CVRContestInfoRepository.java index ecaa764..31b5724 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/CVRContestInfoRepository.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/repository/CVRContestInfoRepository.java @@ -44,15 +44,13 @@ public interface CVRContestInfoRepository extends JpaRepository where each List is a list of ranked choices. - * @throws org.springframework.orm.jpa.JpaSystemException when an error has occurred in the - * conversion of ranked choice vote entries to an array of strings (most likely because the - * entry was either null or blank or not a JSON representation of a list). - * @throws org.springframework.dao.DataAccessException when a database error has occurred, such - * as a connection failure. + * @throws JpaSystemException when an error has occurred in the conversion of ranked choice vote + * entries to an array of strings (most likely because the entry was either null or blank or not + * a JSON representation of a list). */ @Query(value = "select ci.choices from CVRContestInfo ci " + " where ci.contestId = :contestId and ci.countyId = :countyId") List getCVRs(@Param("contestId") long contestId, @Param("countyId") long countyId) - throws JpaSystemException, DataAccessException; + throws JpaSystemException; } diff --git a/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java b/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java index 121889e..88ab1b5 100644 --- a/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java +++ b/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java @@ -21,6 +21,7 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra package au.org.democracydevelopers.raireservice.persistence.repository; +import au.org.democracydevelopers.raire.assertions.AssertionAndDifficulty; import au.org.democracydevelopers.raireservice.persistence.entity.Assertion; import au.org.democracydevelopers.raireservice.persistence.entity.NEBAssertion; import au.org.democracydevelopers.raireservice.persistence.entity.NENAssertion; @@ -35,9 +36,13 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; import org.springframework.transaction.annotation.Transactional; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_METHOD; /** * Tests to validate the behaviour of Assertion retrieval and storage. Assertions and other @@ -58,47 +63,90 @@ public class AssertionRepositoryTests { private static final Gson GSON = new GsonBuilder().serializeNulls().disableHtmlEscaping().create(); + /** + * Array of candidates: Alice, Charlie, Diego, Bob. + */ + private static final String[] aliceCharlieDiegoBob = {"Alice", "Charlie", "Diego", "Bob"}; + + /** + * Array of candidates: Alice, Charlie, Bob. + */ + private static final String[] aliceCharlieBob = {"Alice", "Charlie", "Bob"}; + /** * Test assertion: Alice NEB Bob in the contest "One NEB Assertion Contest". */ - private String aliceNEBBob = "{\"id\":0,\"version\":0,\"contestName\":" + - "\"One NEB Assertion Contest\",\"winner\":\"Alice\",\"loser\":\"Bob\",\"margin\":100," + + private final static String aliceNEBBob = "{\"id\":1,\"version\":0,\"contestName\":" + + "\"One NEB Assertion Contest\",\"winner\":\"Alice\",\"loser\":\"Bob\",\"margin\":320," + "\"difficulty\":1.1,\"assumedContinuing\":[],\"dilutedMargin\":0.32,\"cvrDiscrepancy\":{}," + - "\"estimatedSamplesToAudit\":0,\"twoVoteUnderCount\":0,\"oneVoteUnderCount\":0," + - "\"oneVoteOverCount\":0,\"twoVoteOverCount\":0,\"otherCount\":0,\"currentRisk\":1.00}"; + "\"estimatedSamplesToAudit\":0,\"optimisticSamplesToAudit\":0,\"twoVoteUnderCount\":0," + + "\"oneVoteUnderCount\":0,\"oneVoteOverCount\":0,\"twoVoteOverCount\":0,\"otherCount\":0," + + "\"currentRisk\":1.00}"; /** * Test assertion: Alice NEN Charlie assuming Alice, Charlie, Diego and Bob are continuing, * for the contest "One NEN Assertion Contest". */ - private String aliceNENCharlie = "{\"id\":1,\"version\":0,\"contestName\":" + + private final static String aliceNENCharlie = "{\"id\":2,\"version\":0,\"contestName\":" + "\"One NEN Assertion Contest\",\"winner\":\"Alice\",\"loser\":\"Charlie\",\"margin\":240," + "\"difficulty\":3.01,\"assumedContinuing\":[\"Alice\",\"Charlie\",\"Diego\",\"Bob\"]," + "\"dilutedMargin\":0.12,\"cvrDiscrepancy\":{},\"estimatedSamplesToAudit\":0," + - "\"twoVoteUnderCount\":0,\"oneVoteUnderCount\":0,\"oneVoteOverCount\":0," + - "\"twoVoteOverCount\":0,\"otherCount\":0,\"currentRisk\":1.00}"; + "\"optimisticSamplesToAudit\":0,\"twoVoteUnderCount\":0,\"oneVoteUnderCount\":0," + + "\"oneVoteOverCount\":0,\"twoVoteOverCount\":0,\"otherCount\":0,\"currentRisk\":1.00}"; /** * Test assertion: Amanda NEB Liesl in the contest "One NEN NEB Assertion Contest". */ - private String amandaNEBLiesl = "{\"id\":2,\"version\":0,\"contestName\":" + + private final static String amandaNEBLiesl = "{\"id\":3,\"version\":0,\"contestName\":" + "\"One NEN NEB Assertion Contest\",\"winner\":\"Amanda\",\"loser\":\"Liesl\",\"margin\":112,"+ - "\"difficulty\":0.1,\"assumedContinuing\":[],\"dilutedMargin\":0.52," + - "\"cvrDiscrepancy\":{},\"estimatedSamplesToAudit\":0,\"twoVoteUnderCount\":0," + - "\"oneVoteUnderCount\":0,\"oneVoteOverCount\":0,\"twoVoteOverCount\":0,\"otherCount\":0," + - "\"currentRisk\":1.00}"; + "\"difficulty\":0.1,\"assumedContinuing\":[],\"dilutedMargin\":0.1," + + "\"cvrDiscrepancy\":{},\"estimatedSamplesToAudit\":0,\"optimisticSamplesToAudit\":0," + + "\"twoVoteUnderCount\":0,\"oneVoteUnderCount\":0,\"oneVoteOverCount\":0," + + "\"twoVoteOverCount\":0,\"otherCount\":0,\"currentRisk\":1.00}"; /** * Test assertion: Amanda NEN Wendell assuming Liesl, Wendell and Amanda are continuing, * for the contest "One NEN NEB Assertion Contest". */ - private String amandaNENWendell = "{\"id\":3,\"version\":0,\"contestName\":" + + private final static String amandaNENWendell = "{\"id\":4,\"version\":0,\"contestName\":" + "\"One NEN NEB Assertion Contest\",\"winner\":\"Amanda\",\"loser\":\"Wendell\"," + - "\"margin\":250,\"difficulty\":3.17,\"assumedContinuing\":[\"Liesl\",\"Wendell\"," + - "\"Amanda\"],\"dilutedMargin\":0.72,\"cvrDiscrepancy\":{},\"estimatedSamplesToAudit\":0," + + "\"margin\":560,\"difficulty\":3.17,\"assumedContinuing\":[\"Liesl\",\"Wendell\"," + + "\"Amanda\"],\"dilutedMargin\":0.5,\"cvrDiscrepancy\":{},\"estimatedSamplesToAudit\":0," + + "\"optimisticSamplesToAudit\":0,\"twoVoteUnderCount\":0,\"oneVoteUnderCount\":0," + + "\"oneVoteOverCount\":0,\"twoVoteOverCount\":0,\"otherCount\":0,\"currentRisk\":1.00}"; + + /** + * Test assertion: Charlie C. Chaplin NEB Alice P. Mangrove in "Multi-County Contest 1". + */ + private final static String charlieNEBAlice = "{\"id\":5,\"version\":0,\"contestName\":" + + "\"Multi-County Contest 1\",\"winner\":\"Charlie C. Chaplin\",\"loser\":\"Alice P. Mangrove\"," + + "\"margin\":310,\"difficulty\":2.1,\"assumedContinuing\":[],\"dilutedMargin\":0.01," + + "\"cvrDiscrepancy\":{},\"estimatedSamplesToAudit\":0,\"optimisticSamplesToAudit\":0," + + "\"twoVoteUnderCount\":0,\"oneVoteUnderCount\":0,\"oneVoteOverCount\":0," + + "\"twoVoteOverCount\":0,\"otherCount\":0,\"currentRisk\":1.00}"; + + /** + * Test assertion: Alice P. Mangrove NEB Al (Bob) Jones in "Multi-County Contest 1". + */ + private final static String aliceNEBAl = "{\"id\":6,\"version\":0,\"contestName\":" + + "\"Multi-County Contest 1\",\"winner\":\"Alice P. Mangrove\",\"loser\":\"Al (Bob) Jones\"," + + "\"margin\":2170,\"difficulty\":0.9,\"assumedContinuing\":[],\"dilutedMargin\":0.07," + + "\"cvrDiscrepancy\":{},\"estimatedSamplesToAudit\":0,\"optimisticSamplesToAudit\":0," + "\"twoVoteUnderCount\":0,\"oneVoteUnderCount\":0,\"oneVoteOverCount\":0," + "\"twoVoteOverCount\":0,\"otherCount\":0,\"currentRisk\":1.00}"; + /** + * Test assertion: Alice P. Mangrove NEN West W. Westerson in "Multi-County Contest 1" + * assuming only these two candidates remain standing. + */ + private final static String aliceNENwest = "{\"id\":7,\"version\":0,\"contestName\":" + + "\"Multi-County Contest 1\",\"winner\":\"Alice P. Mangrove\",\"loser\":\"West W. Westerson\"," + + "\"margin\":31,\"difficulty\":5.0,\"assumedContinuing\":[\"West W. Westerson\"," + + "\"Alice P. Mangrove\"],\"dilutedMargin\":0.001,\"cvrDiscrepancy\":{}," + + "\"estimatedSamplesToAudit\":0,\"optimisticSamplesToAudit\":0,\"twoVoteUnderCount\":0," + + "\"oneVoteUnderCount\":0,\"oneVoteOverCount\":0,\"twoVoteOverCount\":0,\"otherCount\":0," + + "\"currentRisk\":1.00}"; + /** * Retrieval of assertions for an existing contest with no associated assertions will return an * empty list. @@ -145,6 +193,7 @@ void deleteAssertionsExistentContestNoAssertions(){ */ @Test @Transactional + @Sql(scripts = {"/simple_assertions.sql"}, executionPhase = BEFORE_TEST_METHOD) void retrieveAssertionsExistentContestOneNEBAssertion(){ List retrieved = assertionRepository.findByContestName("One NEB Assertion Contest"); assertEquals(1, retrieved.size()); @@ -159,6 +208,7 @@ void retrieveAssertionsExistentContestOneNEBAssertion(){ */ @Test @Transactional + @Sql(scripts = {"/simple_assertions.sql"}, executionPhase = BEFORE_TEST_METHOD) void retrieveAssertionsExistentContestOneNENAssertion(){ List retrieved = assertionRepository.findByContestName("One NEN Assertion Contest"); assertEquals(1, retrieved.size()); @@ -173,6 +223,7 @@ void retrieveAssertionsExistentContestOneNENAssertion(){ */ @Test @Transactional + @Sql(scripts = {"/simple_assertions.sql"}, executionPhase = BEFORE_TEST_METHOD) void retrieveAssertionsExistentContestOneNENOneNEBAssertion(){ List retrieved = assertionRepository.findByContestName("One NEN NEB Assertion Contest"); assertEquals(2, retrieved.size()); @@ -186,12 +237,37 @@ void retrieveAssertionsExistentContestOneNENOneNEBAssertion(){ assertEquals(amandaNENWendell, GSON.toJson(r2)); } + + /** + * Retrieve assertions for a multi-county contest. + */ + @Test + @Transactional + @Sql(scripts = {"/simple_assertions.sql"}, executionPhase = BEFORE_TEST_METHOD) + void retrieveAssertionsMultiCountyContest(){ + List retrieved = assertionRepository.findByContestName("Multi-County Contest 1"); + assertEquals(3, retrieved.size()); + + final Assertion r1 = retrieved.get(0); + assertEquals(NEBAssertion.class, r1.getClass()); + assertEquals(charlieNEBAlice, GSON.toJson(r1)); + + final Assertion r2 = retrieved.get(1); + assertEquals(NEBAssertion.class, r2.getClass()); + assertEquals(aliceNEBAl, GSON.toJson(r2)); + + final Assertion r3 = retrieved.get(2); + assertEquals(NENAssertion.class, r3.getClass()); + assertEquals(aliceNENwest, GSON.toJson(r3)); + } + /** * Deletion of assertions for an existent contest with one NEB assertion will remove one record. * The contest will then have no associated assertions in the database. */ @Test @Transactional + @Sql(scripts = {"/simple_assertions.sql"}, executionPhase = BEFORE_TEST_METHOD) void deleteAssertionsExistentContestOneNEBAssertion(){ long records = assertionRepository.deleteByContestName("One NEB Assertion Contest"); assertEquals(1, records); @@ -206,6 +282,7 @@ void deleteAssertionsExistentContestOneNEBAssertion(){ */ @Test @Transactional + @Sql(scripts = {"/simple_assertions.sql"}, executionPhase = BEFORE_TEST_METHOD) void deleteAssertionsExistentContestOneNENAssertion(){ long records = assertionRepository.deleteByContestName("One NEN Assertion Contest"); assertEquals(1, records); @@ -220,6 +297,7 @@ void deleteAssertionsExistentContestOneNENAssertion(){ */ @Test @Transactional + @Sql(scripts = {"/simple_assertions.sql"}, executionPhase = BEFORE_TEST_METHOD) void deleteAssertionsExistentContestOneNENOneNEBAssertion(){ long records = assertionRepository.deleteByContestName("One NEN NEB Assertion Contest"); assertEquals(2, records); @@ -227,4 +305,484 @@ void deleteAssertionsExistentContestOneNENOneNEBAssertion(){ List retrieved = assertionRepository.findByContestName("One NEN NEB Assertion Contest"); assertEquals(0, retrieved.size()); } + + /** + * Deletion of assertions for an existent multi-county with three assertions. + * The contest will then have no associated assertions in the database. + */ + @Test + @Transactional + @Sql(scripts = {"/simple_assertions.sql"}, executionPhase = BEFORE_TEST_METHOD) + void deleteAssertionsMultiCountyContest(){ + long records = assertionRepository.deleteByContestName("Multi-County Contest 1"); + assertEquals(3, records); + + List retrieved = assertionRepository.findByContestName("Multi-County Contest 1"); + assertEquals(0, retrieved.size()); + } + + + /** + * Test translateAndSaveAssertions when passed with an empty array of assertions. The method + * should store no assertions in the database. + */ + @Test + @Transactional + void translateAndSaveNoAssertions(){ + String[] candidates = {"Alice", "Bob", "Charlie"}; + AssertionAndDifficulty[] empty = {}; + assertionRepository.translateAndSaveAssertions("Ballina Mayoral", 11000, + candidates, empty); + + List retrieved = assertionRepository.findByContestName("Ballina Mayoral"); + assertEquals(0, retrieved.size()); + } + + /** + * Test translation and saving of assertions for a succession of contests: + * - "One NEB Assertion Contest" + * - "One NEN Assertion Contest" + * - "One NEN NEB Assertion Contest" + * - "Multi-County Contest 1" + */ + @Test + @Transactional + void translateAndSaveAssertions(){ + saveAssertionsOneNEBContest(); + + List retrieved = assertionRepository.findByContestName("One NEB Assertion Contest"); + assertEquals(1, retrieved.size()); + + Assertion r = retrieved.get(0); + assertEquals(NEBAssertion.class, r.getClass()); + assertEquals(aliceNEBBob, GSON.toJson(r)); + + saveAssertionsOneNENContest(); + + retrieved = assertionRepository.findByContestName("One NEN Assertion Contest"); + assertEquals(1, retrieved.size()); + + r = retrieved.get(0); + assertEquals(NENAssertion.class, r.getClass()); + assertEquals(aliceNENCharlie, GSON.toJson(r)); + + saveAssertionsOneNENOneNEBContest(); + + retrieved = assertionRepository.findByContestName("One NEN NEB Assertion Contest"); + assertEquals(2, retrieved.size()); + + Assertion r1 = retrieved.get(0); + assertEquals(NEBAssertion.class, r1.getClass()); + assertEquals(amandaNEBLiesl, GSON.toJson(r1)); + + Assertion r2 = retrieved.get(1); + assertEquals(NENAssertion.class, r2.getClass()); + assertEquals(amandaNENWendell, GSON.toJson(r2)); + + saveAssertionsMultiCountyContest(); + + retrieved = assertionRepository.findByContestName("Multi-County Contest 1"); + assertEquals(3, retrieved.size()); + + r1 = retrieved.get(0); + assertEquals(NEBAssertion.class, r1.getClass()); + assertEquals(charlieNEBAlice, GSON.toJson(r1)); + + r2 = retrieved.get(1); + assertEquals(NEBAssertion.class, r2.getClass()); + assertEquals(aliceNEBAl, GSON.toJson(r2)); + + Assertion r3 = retrieved.get(2); + assertEquals(NENAssertion.class, r3.getClass()); + assertEquals(aliceNENwest, GSON.toJson(r3)); + } + + /** + * Translate and save Alice NEB Bob in contest "One NEB Assertion Contest". + */ + void saveAssertionsOneNEBContest(){ + String[] candidates = {"Alice", "Charlie", "Bob"}; + AssertionAndDifficulty aadAliceNEBBob = new AssertionAndDifficulty( + new au.org.democracydevelopers.raire.assertions.NotEliminatedBefore(0, 2), + 1.1, 320); + + AssertionAndDifficulty[] assertions = {aadAliceNEBBob}; + + assertionRepository.translateAndSaveAssertions("One NEB Assertion Contest", + 1000, candidates, assertions); + } + + /** + * Translate and save Alice NEN Charlie in contest "One NEN Assertion Contest". + */ + void saveAssertionsOneNENContest(){ + String[] candidates = {"Alice", "Charlie", "Diego", "Bob"}; + int[] continuing = {0, 1 ,2, 3}; + AssertionAndDifficulty aadAliceNENCharlie = new AssertionAndDifficulty( + new au.org.democracydevelopers.raire.assertions.NotEliminatedNext(0, 1, + continuing),3.01, 240); + + AssertionAndDifficulty[] assertions = {aadAliceNENCharlie}; + + assertionRepository.translateAndSaveAssertions("One NEN Assertion Contest", + 2000, candidates, assertions); + } + + /** + * Translate and save Amanda NEB Liesl and Amanda NEN Wendell for the contest + * "One NEN NEB Assertion Contest". + */ + void saveAssertionsOneNENOneNEBContest(){ + String[] candidates = {"Liesl", "Wendell", "Amanda"}; + int[] continuing = {0, 1 ,2}; + AssertionAndDifficulty aadAmandaNEBLiesl = new AssertionAndDifficulty( + new au.org.democracydevelopers.raire.assertions.NotEliminatedBefore(2, 0), + 0.1, 112); + + AssertionAndDifficulty aadAmandaNENWendell = new AssertionAndDifficulty( + new au.org.democracydevelopers.raire.assertions.NotEliminatedNext(2, 1, + continuing),3.17, 560); + + AssertionAndDifficulty[] assertions = {aadAmandaNEBLiesl, aadAmandaNENWendell}; + + assertionRepository.translateAndSaveAssertions("One NEN NEB Assertion Contest", + 1120, candidates, assertions); + } + + /** + * Translate and save Charlie C. Chaplin NEB Alice P. Mangrove, Alice P. Mangrove NEB + * Al (Bob) Jones and Alice P. Mangrove NEN West W. Westerson for the contest + * "Multi-County Contest 1". + */ + void saveAssertionsMultiCountyContest(){ + String[] candidates = {"West W. Westerson", "Alice P. Mangrove", "Charlie C. Chaplin", + "Al (Bob) Jones"}; + + AssertionAndDifficulty aadCharlieNEBAlice = new AssertionAndDifficulty( + new au.org.democracydevelopers.raire.assertions.NotEliminatedBefore(2, 1), + 2.1, 310); + + AssertionAndDifficulty aadAliceNEBAl = new AssertionAndDifficulty( + new au.org.democracydevelopers.raire.assertions.NotEliminatedBefore(1, 3), + 0.9, 2170); + + int[] continuing = {0, 1}; + AssertionAndDifficulty aadAliceNENWest = new AssertionAndDifficulty( + new au.org.democracydevelopers.raire.assertions.NotEliminatedNext(1, 0, + continuing),5, 31); + + AssertionAndDifficulty[] assertions = {aadCharlieNEBAlice, aadAliceNEBAl, aadAliceNENWest}; + + assertionRepository.translateAndSaveAssertions("Multi-County Contest 1", + 31000, candidates, assertions); + } + + + /** + * Create and return a raire-java assertion for the contest "One NEN Assertion Contest". + * @param margin Margin of the assertion to be created. + * @param winner Winner of the assertion to be created. + * @param loser Loser of the assertion to be created. + * @param continuing Array of candidates assumed to be continuing. + */ + AssertionAndDifficulty[] formAssertionsOneNENAssertionContest(int margin, int winner, int loser, + int[] continuing){ + AssertionAndDifficulty aadAliceNENCharlie = new AssertionAndDifficulty( + new au.org.democracydevelopers.raire.assertions.NotEliminatedNext(winner, loser, + continuing),3.01, margin); + + return new AssertionAndDifficulty[]{aadAliceNENCharlie}; + } + + /** + * Create and return a raire-java assertion for the contest "One NEB Assertion Contest". + * @param margin Margin of the assertion to be created. + * @param winner Winner of the assertion to be created. + * @param loser Loser of the assertion to be created. + */ + AssertionAndDifficulty[] formAssertionsOneNEBAssertionContest(int margin, int winner, int loser){ + AssertionAndDifficulty aadAliceNEBBob = new AssertionAndDifficulty( + new au.org.democracydevelopers.raire.assertions.NotEliminatedBefore(winner, loser), + 1.1, margin); + + return new AssertionAndDifficulty[]{aadAliceNEBBob}; + } + + /** + * Test translateAndSaveAssertions when passed invalid data: negative universe size for an + * NEB assertion. An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveNegativeUniverseSizeNEB(){ + AssertionAndDifficulty[] assertions = formAssertionsOneNEBAssertionContest(320, + 0, 2); + + Exception ex = assertThrows(IllegalArgumentException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEB Assertion Contest", + -1000, aliceCharlieBob, assertions)); + + assertTrue(ex.getMessage().toLowerCase().contains("assertion must have a positive universe size")); + } + + /** + * Test translateAndSaveAssertions when passed invalid data: zero universe size for an + * NEB assertion. An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveZeroUniverseSizeNEB(){ + AssertionAndDifficulty[] assertions = formAssertionsOneNEBAssertionContest(320, + 0, 2); + + Exception ex = assertThrows(IllegalArgumentException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEB Assertion Contest", + 0, aliceCharlieBob, assertions)); + + assertTrue(ex.getMessage().toLowerCase().contains("assertion must have a positive universe size")); + } + + /** + * Test translateAndSaveAssertions when passed invalid data: negative universe size for an + * NEN assertion. An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveNegativeUniverseSizeNEN(){ + int[] continuing = {0, 1 ,2, 3}; + AssertionAndDifficulty[] assertions = formAssertionsOneNENAssertionContest(240, + 0, 1, continuing); + + Exception ex = assertThrows(IllegalArgumentException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEN Assertion Contest", + -2000, aliceCharlieDiegoBob, assertions)); + + assertTrue(ex.getMessage().toLowerCase().contains("assertion must have a positive universe size")); + } + + /** + * Test translateAndSaveAssertions when passed invalid data: zero universe size for an + * NEN assertion. An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveZeroUniverseSizeNEN(){ + int[] continuing = {0, 1 ,2, 3}; + AssertionAndDifficulty[] assertions = formAssertionsOneNENAssertionContest(240, + 0, 1, continuing); + + Exception ex = assertThrows(IllegalArgumentException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEN Assertion Contest", + 0, aliceCharlieDiegoBob, assertions)); + + assertTrue(ex.getMessage().toLowerCase().contains("assertion must have a positive universe size")); + } + + + /** + * Test translateAndSaveAssertions when passed invalid data: negative margin for an + * NEB assertion. An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveNegativeMarginNEB(){ + AssertionAndDifficulty[] assertions = formAssertionsOneNEBAssertionContest(-100, + 0, 2); + + Exception ex = assertThrows(IllegalArgumentException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEB Assertion Contest", + 1000, aliceCharlieBob, assertions)); + + assertTrue(ex.getMessage().toLowerCase().contains("non-negative margin")); + } + + /** + * Test translateAndSaveAssertions when passed invalid data: negative margin for an + * NEN assertion. An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveNegativeMarginNEN(){ + int[] continuing = {0, 1 ,2, 3}; + AssertionAndDifficulty[] assertions = formAssertionsOneNENAssertionContest(-100, + 0, 1, continuing); + + Exception ex = assertThrows(IllegalArgumentException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEN Assertion Contest", + 2000, aliceCharlieDiegoBob, assertions)); + + assertTrue(ex.getMessage().toLowerCase().contains("negative margin")); + } + + + /** + * Test translateAndSaveAssertions when passed invalid data: margin larger than universe size + * for an NEB assertion. An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveNegativeMarginTooHighNEB(){ + AssertionAndDifficulty[] assertions = formAssertionsOneNEBAssertionContest(2000, + 0, 2); + + Exception ex = assertThrows(IllegalArgumentException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEB Assertion Contest", + 1000, aliceCharlieBob, assertions)); + + assertTrue(ex.getMessage().toLowerCase().contains("less than universe size")); + } + + + /** + * Test translateAndSaveAssertions when passed invalid data: margin larger than universe size + * for an NEN assertion. An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveMarginTooHighNEN(){ + int[] continuing = {0, 1 ,2, 3}; + AssertionAndDifficulty[] assertions = formAssertionsOneNENAssertionContest(3000, + 0, 1, continuing); + + Exception ex = assertThrows(IllegalArgumentException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEN Assertion Contest", + 2000, aliceCharlieDiegoBob, assertions)); + + assertTrue(ex.getMessage().toLowerCase().contains("less than universe size")); + } + + /** + * Test translateAndSaveAssertions when passed invalid data: same winner and loser + * for an NEB assertion. An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveSameWinnerLoserNEB(){ + AssertionAndDifficulty[] assertions = formAssertionsOneNEBAssertionContest(320, + 1, 1); + + Exception ex = assertThrows(IllegalArgumentException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEB Assertion Contest", + 1000, aliceCharlieBob, assertions)); + + assertTrue(ex.getMessage().toLowerCase().contains("must not be the same candidate")); + } + + /** + * Test translateAndSaveAssertions when passed invalid data: same winner and loser + * for an NEB assertion. An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveSameWinnerLoserNEN(){ + int[] continuing = {0, 1 ,2, 3}; + AssertionAndDifficulty[] assertions = formAssertionsOneNENAssertionContest(240, + 0, 0, continuing); + + Exception ex = assertThrows(IllegalArgumentException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEN Assertion Contest", + 2000, aliceCharlieDiegoBob, assertions)); + + assertTrue(ex.getMessage().toLowerCase().contains("must not be the same candidate")); + } + + /** + * Test translateAndSaveAssertions when passed invalid data: winner of NEN is not continuing. + * An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveNENWinnerNotContinuing(){ + int[] continuing = {1 ,2, 3}; + AssertionAndDifficulty[] assertions = formAssertionsOneNENAssertionContest(240, + 0, 1, continuing); + + Exception ex = assertThrows(IllegalArgumentException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEN Assertion Contest", + 2000, aliceCharlieDiegoBob, assertions)); + + assertTrue(ex.getMessage().toLowerCase().contains( + "the winner and loser of an assertion must also be continuing candidates")); + } + + /** + * Test translateAndSaveAssertions when passed invalid data: loser of NEN is not continuing. + * An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveNENLoserNotContinuing(){ + int[] continuing = {0 ,2, 3}; + AssertionAndDifficulty[] assertions = formAssertionsOneNENAssertionContest(240, + 0, 1, continuing); + + Exception ex = assertThrows(IllegalArgumentException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEN Assertion Contest", + 2000, aliceCharlieDiegoBob, assertions)); + + assertTrue(ex.getMessage().toLowerCase().contains( + "the winner and loser of an assertion must also be continuing candidates")); + } + + /** + * Test translateAndSaveAssertions when passed invalid data: winner index is outside of the bounds + * of the candidate array (NEB). An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveWinnerOutOfBoundsNEB(){ + AssertionAndDifficulty[] assertions = formAssertionsOneNEBAssertionContest(320, + 5, 2); + + assertThrows(ArrayIndexOutOfBoundsException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEB Assertion Contest", + 1000, aliceCharlieBob, assertions)); + } + + /** + * Test translateAndSaveAssertions when passed invalid data: winner index is outside of the bounds + * of the candidate array (NEN). An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveWinnerOutOfBoundsNEN(){ + int[] continuing = {0 ,2, 3}; + AssertionAndDifficulty[] assertions = formAssertionsOneNENAssertionContest(240, + 5, 1, continuing); + + assertThrows(ArrayIndexOutOfBoundsException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEN Assertion Contest", + 2000, aliceCharlieDiegoBob, assertions)); + } + + /** + * Test translateAndSaveAssertions when passed invalid data: winner index is outside of the bounds + * of the candidate array (NEB). An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveLoserOutOfBoundsNEB(){ + AssertionAndDifficulty[] assertions = formAssertionsOneNEBAssertionContest(320, + 2, 7); + + assertThrows(ArrayIndexOutOfBoundsException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEB Assertion Contest", + 1000, aliceCharlieBob, assertions)); + } + + /** + * Test translateAndSaveAssertions when passed invalid data: winner index is outside of the bounds + * of the candidate array (NEN). An IllegalArgumentException should be thrown. + */ + @Test + @Transactional + void translateAndSaveLoserOutOfBoundsNEN(){ + int[] continuing = {0 ,2, 3}; + AssertionAndDifficulty[] assertions = formAssertionsOneNENAssertionContest(240, + 0, 7, continuing); + + assertThrows(ArrayIndexOutOfBoundsException.class, () -> + assertionRepository.translateAndSaveAssertions("One NEN Assertion Contest", + 2000, aliceCharlieDiegoBob, assertions)); + } } diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index eaa2910..4c73750 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -6,6 +6,7 @@ INSERT INTO county (id, name) values (9, 'Byron'); INSERT INTO county (id, name) values (10, 'Westgarth'); INSERT INTO county (id, name) values (11, 'Malformed'); INSERT INTO county (id, name) values (12, 'One Assertion County'); + -- Contest -- Simple contests to test basic functioning. INSERT INTO contest (county_id, id, version, description, name, sequence_number, votes_allowed, winners_allowed) values (8, 999990, 0, 'IRV', 'Multi-County Contest 1', 0, 7, 1); @@ -79,18 +80,3 @@ INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) val INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (12, 11, 'NotAList', 999984, 16); INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (12, 11, '', 999983, 17); --- Assertions -INSERT INTO assertion values ('NEB', 0, 'One NEB Assertion Contest', 1.1, 0.32, 'Bob', 100, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice'); - -INSERT INTO assertion values ('NEN', 1, 'One NEN Assertion Contest', 3.01, 0.12, 'Charlie', 240, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice'); -INSERT INTO assertion_assumed_continuing values (1, 'Alice'); -INSERT INTO assertion_assumed_continuing values (1, 'Charlie'); -INSERT INTO assertion_assumed_continuing values (1, 'Diego'); -INSERT INTO assertion_assumed_continuing values (1, 'Bob'); - -INSERT INTO assertion values ('NEB', 2, 'One NEN NEB Assertion Contest', 0.1, 0.52, 'Liesl', 112, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Amanda'); - -INSERT INTO assertion values ('NEN', 3, 'One NEN NEB Assertion Contest', 3.17, 0.72, 'Wendell', 250, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Amanda'); -INSERT INTO assertion_assumed_continuing values (3, 'Liesl'); -INSERT INTO assertion_assumed_continuing values (3, 'Wendell'); -INSERT INTO assertion_assumed_continuing values (3, 'Amanda'); \ No newline at end of file diff --git a/src/test/resources/simple_assertions.sql b/src/test/resources/simple_assertions.sql new file mode 100644 index 0000000..621a38c --- /dev/null +++ b/src/test/resources/simple_assertions.sql @@ -0,0 +1,23 @@ +-- Simple Assertions for testing Retrieval/Deletion +INSERT INTO assertion values ('NEB', 1, 'One NEB Assertion Contest', 1.1, 0.32, 'Bob', 320, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice'); + +INSERT INTO assertion values ('NEN', 2, 'One NEN Assertion Contest', 3.01, 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 assertion values ('NEB', 3, 'One NEN NEB Assertion Contest', 0.1, 0.1, 'Liesl', 112, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Amanda'); + +INSERT INTO assertion values ('NEN', 4, 'One NEN NEB Assertion Contest', 3.17, 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 assertion values ('NEB', 5, 'Multi-County Contest 1', 2.1, 0.01, 'Alice P. Mangrove', 310, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Charlie C. Chaplin'); +INSERT INTO assertion values ('NEB', 6, 'Multi-County Contest 1', 0.9, 0.07, 'Al (Bob) Jones', 2170, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice P. Mangrove'); + +INSERT INTO assertion values ('NEN', 7, 'Multi-County Contest 1', 5.0, 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 027865b434746f69b85cab5a82d19c7ded123027 Mon Sep 17 00:00:00 2001 From: michelleblom Date: Wed, 17 Apr 2024 13:58:59 +1000 Subject: [PATCH 12/13] Changed id generation in Assertion to IDENTITY, and added tests that save assertions to the database in the context where the database already has assertion data. --- .../persistence/entity/Assertion.java | 3 +- .../repository/AssertionRepositoryTests.java | 161 +++++++++++++++++- src/test/resources/corla.sql | 155 +++++++++++------ src/test/resources/data.sql | 12 ++ src/test/resources/simple_assertions.sql | 14 +- .../simple_assertions_in_progress.sql | 26 +++ 6 files changed, 306 insertions(+), 65 deletions(-) create mode 100644 src/test/resources/simple_assertions_in_progress.sql diff --git a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java index b03b1d4..d09e7d1 100644 --- a/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java +++ b/src/main/java/au/org/democracydevelopers/raireservice/persistence/entity/Assertion.java @@ -47,7 +47,7 @@ public abstract class Assertion { */ @Id @Column(updatable = false, nullable = false) - @GeneratedValue(strategy = GenerationType.SEQUENCE) + @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; /** @@ -88,6 +88,7 @@ public abstract class Assertion { /** * List of candidates that the Assertion assumes are 'continuing' in the Assertion's context. + * Note that this is always empty for NEB assertions. */ @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "assertion_assumed_continuing", joinColumns = @JoinColumn(name = "id")) diff --git a/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java b/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java index 88ab1b5..269aa78 100644 --- a/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java +++ b/src/test/java/au/org/democracydevelopers/raireservice/persistence/repository/AssertionRepositoryTests.java @@ -51,7 +51,7 @@ the raire assertion generation engine (https://github.com/DemocracyDevelopers/ra @ActiveProfiles("test-containers") @SpringBootTest @AutoConfigureTestDatabase(replace = Replace.NONE) -@DirtiesContext(classMode = ClassMode.AFTER_CLASS) +@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD) public class AssertionRepositoryTests { @Autowired @@ -147,6 +147,74 @@ public class AssertionRepositoryTests { "\"oneVoteUnderCount\":0,\"oneVoteOverCount\":0,\"twoVoteOverCount\":0,\"otherCount\":0," + "\"currentRisk\":1.00}"; + /** + * Test assertion: CC NEB A in "Larger Contest". + */ + private final static String CCNEBA = "{\"id\":8,\"version\":0,\"contestName\":" + + "\"Larger Contest\",\"winner\":\"CC\",\"loser\":\"A\"," + + "\"margin\":25,\"difficulty\":5.0,\"assumedContinuing\":[],\"dilutedMargin\":0.0125," + + "\"cvrDiscrepancy\":{},\"estimatedSamplesToAudit\":0,\"optimisticSamplesToAudit\":0," + + "\"twoVoteUnderCount\":0,\"oneVoteUnderCount\":0,\"oneVoteOverCount\":0," + + "\"twoVoteOverCount\":0,\"otherCount\":0,\"currentRisk\":1.00}"; + + /** + * Test assertion: CC NEN B in "Larger Contest". + */ + private final static String CCNENB = "{\"id\":9,\"version\":0,\"contestName\":" + + "\"Larger Contest\",\"winner\":\"CC\",\"loser\":\"B\"," + + "\"margin\":100,\"difficulty\":2.0,\"assumedContinuing\":[\"A\",\"B\",\"CC\"]," + + "\"dilutedMargin\":0.05,\"cvrDiscrepancy\":{},\"estimatedSamplesToAudit\":0," + + "\"optimisticSamplesToAudit\":0,\"twoVoteUnderCount\":0,\"oneVoteUnderCount\":0," + + "\"oneVoteOverCount\":0,\"twoVoteOverCount\":0,\"otherCount\":0,\"currentRisk\":1.00}"; + + /** + * Test assertion: Alice NEB Bob in the contest "One NEB Assertion Contest" when the audit is + * in progress and some discrepancies have been found. + */ + private final static String aliceNEBBobInProgress = "{\"id\":1,\"version\":0,\"contestName\":" + + "\"One NEB Assertion Contest\",\"winner\":\"Alice\",\"loser\":\"Bob\",\"margin\":320," + + "\"difficulty\":1.1,\"assumedContinuing\":[],\"dilutedMargin\":0.32,\"cvrDiscrepancy\":{}," + + "\"estimatedSamplesToAudit\":111,\"optimisticSamplesToAudit\":111,\"twoVoteUnderCount\":0," + + "\"oneVoteUnderCount\":0,\"oneVoteOverCount\":0,\"twoVoteOverCount\":0,\"otherCount\":0," + + "\"currentRisk\":0.50}"; + + /** + * Test assertion: Alice NEN Charlie assuming Alice, Charlie, Diego and Bob are continuing, + * for the contest "One NEN Assertion Contest" when the audit is in progress and some + * discrepancies have been found. + */ + private final static String aliceNENCharlieInProgress = "{\"id\":2,\"version\":0,\"contestName\":" + + "\"One NEN Assertion Contest\",\"winner\":\"Alice\",\"loser\":\"Charlie\",\"margin\":240," + + "\"difficulty\":3.01,\"assumedContinuing\":[\"Alice\",\"Charlie\",\"Diego\",\"Bob\"]," + + "\"dilutedMargin\":0.12,\"cvrDiscrepancy\":{\"13\":1,\"14\":0,\"15\":0}," + + "\"estimatedSamplesToAudit\":245,\"optimisticSamplesToAudit\":201,\"twoVoteUnderCount\":0," + + "\"oneVoteUnderCount\":0,\"oneVoteOverCount\":1,\"twoVoteOverCount\":0,\"otherCount\":2," + + "\"currentRisk\":0.20}"; + + /** + * Test assertion: Amanda NEB Liesl in the contest "One NEN NEB Assertion Contest" when the audit + * is in progress and some discrepancies have been found. + */ + private final static String amandaNEBLieslInProgress = "{\"id\":3,\"version\":0,\"contestName\":" + + "\"One NEN NEB Assertion Contest\",\"winner\":\"Amanda\",\"loser\":\"Liesl\",\"margin\":112,"+ + "\"difficulty\":0.1,\"assumedContinuing\":[],\"dilutedMargin\":0.1," + + "\"cvrDiscrepancy\":{\"13\":-1,\"14\":2,\"15\":2},\"estimatedSamplesToAudit\":27," + + "\"optimisticSamplesToAudit\":20,\"twoVoteUnderCount\":0,\"oneVoteUnderCount\":1," + + "\"oneVoteOverCount\":0,\"twoVoteOverCount\":2,\"otherCount\":0,\"currentRisk\":0.08}"; + + /** + * Test assertion: Amanda NEN Wendell assuming Liesl, Wendell and Amanda are continuing, + * for the contest "One NEN NEB Assertion Contest" when the audit is in progress and some + * discrepancies have been found. + */ + private final static String amandaNENWendellInProgress = "{\"id\":4,\"version\":0," + + "\"contestName\":\"One NEN NEB Assertion Contest\",\"winner\":\"Amanda\"," + + "\"loser\":\"Wendell\",\"margin\":560,\"difficulty\":3.17," + + "\"assumedContinuing\":[\"Liesl\",\"Wendell\",\"Amanda\"],\"dilutedMargin\":0.5," + + "\"cvrDiscrepancy\":{\"13\":1,\"14\":1,\"15\":-2},\"estimatedSamplesToAudit\":300," + + "\"optimisticSamplesToAudit\":200,\"twoVoteUnderCount\":1,\"oneVoteUnderCount\":0," + + "\"oneVoteOverCount\":2,\"twoVoteOverCount\":0,\"otherCount\":0,\"currentRisk\":0.70}"; + /** * Retrieval of assertions for an existing contest with no associated assertions will return an * empty list. @@ -203,6 +271,22 @@ void retrieveAssertionsExistentContestOneNEBAssertion(){ assertEquals(aliceNEBBob, GSON.toJson(r)); } + /** + * Retrieve assertions for a contest that has one NEB assertion (audit in progress with no + * discrepancies observed). + */ + @Test + @Transactional + @Sql(scripts = {"/simple_assertions_in_progress.sql"}, executionPhase = BEFORE_TEST_METHOD) + void retrieveAssertionsOneNEBAssertionInProgress(){ + List retrieved = assertionRepository.findByContestName("One NEB Assertion Contest"); + assertEquals(1, retrieved.size()); + + final Assertion r = retrieved.get(0); + assertEquals(NEBAssertion.class, r.getClass()); + assertEquals(aliceNEBBobInProgress, GSON.toJson(r)); + } + /** * Retrieve assertions for a contest that has one NEN assertion. */ @@ -218,6 +302,22 @@ void retrieveAssertionsExistentContestOneNENAssertion(){ assertEquals(aliceNENCharlie, GSON.toJson(r)); } + /** + * Retrieve assertions for a contest that has one NEN assertion (audit in progress with some + * discrepancies observed). + */ + @Test + @Transactional + @Sql(scripts = {"/simple_assertions_in_progress.sql"}, executionPhase = BEFORE_TEST_METHOD) + void retrieveAssertionsOneNENAssertionInProgress(){ + List retrieved = assertionRepository.findByContestName("One NEN Assertion Contest"); + assertEquals(1, retrieved.size()); + + final Assertion r = retrieved.get(0); + assertEquals(NENAssertion.class, r.getClass()); + assertEquals(aliceNENCharlieInProgress, GSON.toJson(r)); + } + /** * Retrieve assertions for a contest that has one NEN and one NEB assertion. */ @@ -237,6 +337,25 @@ void retrieveAssertionsExistentContestOneNENOneNEBAssertion(){ assertEquals(amandaNENWendell, GSON.toJson(r2)); } + /** + * Retrieve assertions for a contest that has one NEN and one NEB assertion (audit in progress + * with some discrepancies observed). + */ + @Test + @Transactional + @Sql(scripts = {"/simple_assertions_in_progress.sql"}, executionPhase = BEFORE_TEST_METHOD) + void retrieveAssertionsOneNENOneNEBAssertionInProgress(){ + List retrieved = assertionRepository.findByContestName("One NEN NEB Assertion Contest"); + assertEquals(2, retrieved.size()); + + final Assertion r1 = retrieved.get(0); + assertEquals(NEBAssertion.class, r1.getClass()); + assertEquals(amandaNEBLieslInProgress, GSON.toJson(r1)); + + final Assertion r2 = retrieved.get(1); + assertEquals(NENAssertion.class, r2.getClass()); + assertEquals(amandaNENWendellInProgress, GSON.toJson(r2)); + } /** * Retrieve assertions for a multi-county contest. @@ -397,6 +516,42 @@ void translateAndSaveAssertions(){ assertEquals(aliceNENwest, GSON.toJson(r3)); } + /** + * Translation and saving of assertions when the database already contains some + * assertions. This test is designed to test the incrementing of IDs. The database + * will have 7 assertions pre-populated from simple_assertions.sql. + */ + @Test + @Transactional + @Sql(scripts = {"/simple_assertions.sql"}, executionPhase = BEFORE_TEST_METHOD) + void testAutoIncrementOfIDs(){ + String[] candidates = {"A", "B", "CC"}; + int[] continuing = {0,1,2}; + AssertionAndDifficulty aadCCNEBA = new AssertionAndDifficulty( + new au.org.democracydevelopers.raire.assertions.NotEliminatedBefore(2, 0), + 5, 25); + + AssertionAndDifficulty aadCCNENB = new AssertionAndDifficulty( + new au.org.democracydevelopers.raire.assertions.NotEliminatedNext(2, 1, + continuing),2, 100); + + AssertionAndDifficulty[] assertions = {aadCCNEBA, aadCCNENB}; + + assertionRepository.translateAndSaveAssertions("Larger Contest", + 2000, candidates, assertions); + + List retrieved = assertionRepository.findByContestName("Larger Contest"); + assertEquals(2, retrieved.size()); + + Assertion r1 = retrieved.get(0); + assertEquals(NEBAssertion.class, r1.getClass()); + assertEquals(CCNEBA, GSON.toJson(r1)); + + Assertion r2 = retrieved.get(1); + assertEquals(NENAssertion.class, r2.getClass()); + assertEquals(CCNENB, GSON.toJson(r2)); + } + /** * Translate and save Alice NEB Bob in contest "One NEB Assertion Contest". */ @@ -756,7 +911,7 @@ void translateAndSaveWinnerOutOfBoundsNEN(){ } /** - * Test translateAndSaveAssertions when passed invalid data: winner index is outside of the bounds + * Test translateAndSaveAssertions when passed invalid data: loser index is outside of the bounds * of the candidate array (NEB). An IllegalArgumentException should be thrown. */ @Test @@ -771,7 +926,7 @@ void translateAndSaveLoserOutOfBoundsNEB(){ } /** - * Test translateAndSaveAssertions when passed invalid data: winner index is outside of the bounds + * Test translateAndSaveAssertions when passed invalid data: loser index is outside of the bounds * of the candidate array (NEN). An IllegalArgumentException should be thrown. */ @Test diff --git a/src/test/resources/corla.sql b/src/test/resources/corla.sql index 4856bc7..e707bc1 100644 --- a/src/test/resources/corla.sql +++ b/src/test/resources/corla.sql @@ -9,6 +9,50 @@ create table asm_state version bigint ); + +create table assertion +( + assertion_type varchar(31) not null, + id bigserial + primary key, + contest_name varchar(255) not null, + difficulty double precision not null, + diluted_margin double precision not null, + loser varchar(255) not null, + margin integer not null, + current_risk numeric(19, 2) not null, + estimated_samples_to_audit 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 +); + + +create table assertion_context +( + id bigint not null + constraint fki0lyp4tghtpohaa9ma6kv2174 + references assertion, + assumed_continuing varchar(255) not null +); + + +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) +); + + create table ballot_manifest_info ( id bigint not null @@ -26,6 +70,7 @@ create table ballot_manifest_info uri varchar(255) ); + create index idx_bmi_county on ballot_manifest_info (county_id); @@ -58,6 +103,7 @@ create table cast_vote_record unique (county_id, imprinted_id, record_type, revision) ); + create index idx_cvr_county_type on cast_vote_record (county_id, record_type); @@ -94,8 +140,10 @@ create table contest_result winners_allowed integer ); + create table comparison_audit ( + audit_type varchar(31) not null, id bigint not null primary key, contest_cvr_ids text, @@ -117,13 +165,24 @@ create table comparison_audit two_vote_under_count integer not null, version bigint, overstatements numeric(19, 2), + universe_size bigint, contest_result_id bigint not null constraint fkn14qkca2ilirtpr4xctw960pe - references contest_result, - audit_type varchar(31) not null, - universe_size bigint + references contest_result ); + +create table audit_to_assertions +( + id bigint not null + constraint fkgrx2l2qywbc3nv83iid55ql36 + references comparison_audit, + assertions_id bigint not null + constraint fkqomhyyib2xno6nq0wjpv95fs5 + references assertion +); + + create table contest_vote_total ( result_id bigint not null @@ -134,6 +193,7 @@ create table contest_vote_total primary key (result_id, choice) ); + create table county ( id bigint not null @@ -144,6 +204,7 @@ create table county version bigint ); + create table administrator ( id bigint not null @@ -154,8 +215,6 @@ create table administrator type varchar(255) not null, username varchar(255) not null constraint uk_esogmqxeek1uwdyhxvubme3qf - unique - constraint idx_admin_username unique, version bigint, county_id bigint @@ -163,6 +222,7 @@ create table administrator references county ); + create table contest ( id bigint not null @@ -180,6 +240,7 @@ create table contest unique (name, county_id, description, votes_allowed) ); + create index idx_contest_name on contest (name); @@ -201,6 +262,7 @@ create table contest_choice unique (contest_id, name) ); + create table contests_to_contest_results ( contest_result_id bigint not null @@ -214,6 +276,7 @@ create table contests_to_contest_results primary key (contest_result_id, contest_id) ); + create table counties_to_contest_results ( contest_result_id bigint not null @@ -225,6 +288,7 @@ create table counties_to_contest_results primary key (contest_result_id, county_id) ); + create table county_contest_result ( id bigint not null @@ -247,6 +311,7 @@ create table county_contest_result unique (county_id, contest_id) ); + create index idx_ccr_county on county_contest_result (county_id); @@ -263,6 +328,7 @@ create table county_contest_vote_total primary key (result_id, choice) ); + create table cvr_audit_info ( id bigint not null @@ -280,6 +346,7 @@ create table cvr_audit_info references cast_vote_record ); + create table contest_comparison_audit_disagreement ( contest_comparison_audit_id bigint not null @@ -291,6 +358,7 @@ create table contest_comparison_audit_disagreement primary key (contest_comparison_audit_id, cvr_audit_info_id) ); + create table contest_comparison_audit_discrepancy ( contest_comparison_audit_id bigint not null @@ -303,6 +371,7 @@ create table contest_comparison_audit_discrepancy primary key (contest_comparison_audit_id, cvr_audit_info_id) ); + create table cvr_contest_info ( cvr_id bigint not null @@ -319,6 +388,7 @@ create table cvr_contest_info primary key (cvr_id, index) ); + create index idx_cvrci_uri on cvr_contest_info (county_id, contest_id); @@ -336,6 +406,7 @@ create table dos_dashboard version bigint ); + create table contest_to_audit ( dashboard_id bigint not null @@ -348,6 +419,19 @@ create table contest_to_audit reason varchar(255) ); + +create table irv_ballot_interpretation +( + id bigint not null + primary key, + contest_name varchar(255), + cvr_id bigint, + raw_choices varchar(1024), + valid_choices varchar(1024), + version bigint +); + + create table log ( id bigint not null @@ -364,6 +448,7 @@ create table log references log ); + create table tribute ( id bigint not null @@ -379,6 +464,7 @@ create table tribute version bigint ); + create table uploaded_file ( id bigint not null @@ -398,6 +484,7 @@ create table uploaded_file references county ); + create table county_dashboard ( id bigint not null @@ -430,6 +517,7 @@ create table county_dashboard references uploaded_file ); + create table audit_board ( dashboard_id bigint not null @@ -442,6 +530,7 @@ create table audit_board primary key (dashboard_id, index) ); + create table audit_intermediate_report ( dashboard_id bigint not null @@ -453,6 +542,7 @@ create table audit_intermediate_report primary key (dashboard_id, index) ); + create table audit_investigation_report ( dashboard_id bigint not null @@ -465,6 +555,7 @@ create table audit_investigation_report primary key (dashboard_id, index) ); + create table county_contest_comparison_audit ( id bigint not null @@ -497,6 +588,7 @@ create table county_contest_comparison_audit references county_dashboard ); + create index idx_ccca_dashboard on county_contest_comparison_audit (dashboard_id); @@ -511,6 +603,7 @@ create table county_contest_comparison_audit_disagreement primary key (county_contest_comparison_audit_id, cvr_audit_info_id) ); + create table county_contest_comparison_audit_discrepancy ( county_contest_comparison_audit_id bigint not null @@ -523,6 +616,7 @@ create table county_contest_comparison_audit_discrepancy primary key (county_contest_comparison_audit_id, cvr_audit_info_id) ); + create table county_dashboard_to_comparison_audit ( dashboard_id bigint not null @@ -534,6 +628,7 @@ create table county_dashboard_to_comparison_audit primary key (dashboard_id, comparison_audit_id) ); + create table round ( dashboard_id bigint not null @@ -558,57 +653,9 @@ create table round primary key (dashboard_id, index) ); + create index idx_uploaded_file_county on uploaded_file (county_id); -create table assertion -( - assertion_type varchar(31) not null, - id bigint not null - primary key, - contest_name varchar(255) not null, - difficulty double precision not null, - diluted_margin double precision not null, - loser varchar(255) not null, - margin integer not null, - current_risk numeric(19, 2) not null, - estimated_samples_to_audit 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 -); - -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) -); - -create table assertion_assumed_continuing -( - id bigint not null - constraint fki0lyp4tghtpohaa9ma6kv2174 - references assertion, - assumed_continuing varchar(255) not null -); - -create table audit_to_assertions -( - id bigint not null - constraint fk_comparison_audit_audit_to_assertions - references comparison_audit, - assertion_id bigint not null - constraint fk_assertion_audit_to_assertions - references assertion -); diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql index 4c73750..772d865 100644 --- a/src/test/resources/data.sql +++ b/src/test/resources/data.sql @@ -80,3 +80,15 @@ INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) val INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (12, 11, 'NotAList', 999984, 16); INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (12, 11, '', 999983, 17); +-- For assertion testing (retrieval of assertions when audit is in progress) +INSERT INTO cast_vote_record (id, cvr_number, ballot_type, batch_id, county_id, imprinted_id, record_id, record_type, scanner_id) values (13, 13, 'Type 3', 1, 12, '13-1-4', 13, 'UPLOADED', 4); +INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (13, 12, '["Alice","Charlie"]', 999971, 18); +INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (13, 12, '["Liesl","Amanda"]', 999972, 19); + +INSERT INTO cast_vote_record (id, cvr_number, ballot_type, batch_id, county_id, imprinted_id, record_id, record_type, scanner_id) values (14, 14, 'Type 3', 1, 12, '14-1-4', 14, 'UPLOADED', 4); +INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (14, 12, '["Charlie"]', 999971, 20); +INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (14, 12, '["Liesl","Wendell"]', 999972, 21); + +INSERT INTO cast_vote_record (id, cvr_number, ballot_type, batch_id, county_id, imprinted_id, record_id, record_type, scanner_id) values (15, 15, 'Type 3', 1, 12, '15-1-4', 15, 'UPLOADED', 4); +INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (15, 12, '["Alice"]', 999971, 22); +INSERT INTO cvr_contest_info (cvr_id, county_id, choices, contest_id, index) values (15, 12, '["Wendell","Liesel"]', 999972, 23); diff --git a/src/test/resources/simple_assertions.sql b/src/test/resources/simple_assertions.sql index 621a38c..707712e 100644 --- a/src/test/resources/simple_assertions.sql +++ b/src/test/resources/simple_assertions.sql @@ -1,23 +1,23 @@ -- Simple Assertions for testing Retrieval/Deletion -INSERT INTO assertion values ('NEB', 1, 'One NEB Assertion Contest', 1.1, 0.32, 'Bob', 320, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice'); +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', 1.1, 0.32, 'Bob', 320, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice'); -INSERT INTO assertion values ('NEN', 2, 'One NEN Assertion Contest', 3.01, 0.12, 'Charlie', 240, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice'); +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', 3.01, 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 assertion values ('NEB', 3, 'One NEN NEB Assertion Contest', 0.1, 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 ('NEB', 'One NEN NEB Assertion Contest', 0.1, 0.1, 'Liesl', 112, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Amanda'); -INSERT INTO assertion values ('NEN', 4, 'One NEN NEB Assertion Contest', 3.17, 0.5, 'Wendell', 560, 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', 3.17, 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 assertion values ('NEB', 5, 'Multi-County Contest 1', 2.1, 0.01, 'Alice P. Mangrove', 310, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Charlie C. Chaplin'); -INSERT INTO assertion values ('NEB', 6, 'Multi-County Contest 1', 0.9, 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 ('NEB', 'Multi-County Contest 1', 2.1, 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', 0.9, 0.07, 'Al (Bob) Jones', 2170, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Alice P. Mangrove'); -INSERT INTO assertion values ('NEN', 7, 'Multi-County Contest 1', 5.0, 0.001, 'West W. Westerson', 31, 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', 5.0, 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 diff --git a/src/test/resources/simple_assertions_in_progress.sql b/src/test/resources/simple_assertions_in_progress.sql new file mode 100644 index 0000000..1add56a --- /dev/null +++ b/src/test/resources/simple_assertions_in_progress.sql @@ -0,0 +1,26 @@ +-- Simple Assertions for testing retrieval when audit is in progress +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', 1.1, 0.32, 'Bob', 320, 0.5, 111, 0, 0, 111, 0, 0, 0, 0, 'Alice'); + +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', 3.01, 0.12, 'Charlie', 240, 0.2, 245, 1, 0, 201, 2, 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 assertion_discrepancies values (2, 1, 13); +INSERT INTO assertion_discrepancies values (2, 0, 14); +INSERT INTO assertion_discrepancies values (2, 0, 15); + +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', 0.1, 0.1, 'Liesl', 112, 0.08, 27, 0, 1, 20, 0, 2, 0, 0, 'Amanda'); +INSERT INTO assertion_discrepancies values (3, -1, 13); +INSERT INTO assertion_discrepancies values (3, 2, 14); +INSERT INTO assertion_discrepancies values (3, 2, 15); + +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', 3.17, 0.5, 'Wendell', 560, 0.7, 300, 2, 0, 200, 0, 0, 1, 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 assertion_discrepancies values (4, 1, 13); +INSERT INTO assertion_discrepancies values (4, 1, 14); +INSERT INTO assertion_discrepancies values (4, -2, 15); From 87b4f9338ab1c1bfe7c4169944da0335f45d51ee Mon Sep 17 00:00:00 2001 From: Vanessa Teague Date: Thu, 18 Apr 2024 10:34:00 +1000 Subject: [PATCH 13/13] Upgrade raire-java to 1.0.1 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 0420ad3..3fce75a 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ au.org.democracydevelopers raire-java - 1.0 + 1.0.1 org.springframework.boot