diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/communication/responseFromRaire/GenerateAssertionsResponse.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/communication/responseFromRaire/GenerateAssertionsResponse.java
index 81fd93f3..5f667209 100644
--- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/communication/responseFromRaire/GenerateAssertionsResponse.java
+++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/communication/responseFromRaire/GenerateAssertionsResponse.java
@@ -21,25 +21,29 @@
package au.org.democracydevelopers.corla.communication.responseFromRaire;
-import java.util.Objects;
/**
- * The success response when a ContestRequest is sent to raire's generate-assertions endpoint. This
- * simply returns the winner, as calculated by raire, along with the name of the contest for which
- * the initial request was made.
- * This record is identical to the record of the same name in raire-service. Used for
+ * The response when a ContestRequest is sent to raire's generate-assertions endpoint.
+ * This class is identical to the record of the same name in raire-service. Used for
* deserialization.
+ * All four states of the two booleans are possible - for example, generation may succeed, but
+ * receive a TIME_OUT_TRIMMING_ASSERTIONS warning, in which case retry will be true.
*/
public final class GenerateAssertionsResponse {
public String contestName;
- public String winner;
+ public boolean succeeded;
+ public boolean retry;
- /**
- * @param contestName The name of the contest.
- * @param winner The winner of the contest, as calculated by raire.
- */
- public GenerateAssertionsResponse(String contestName, String winner) {
+
+/**
+ * All args constructor.
+ * @param contestName The name of the contest.
+ * @param succeeded Whether assertion generation succeeded.
+ * @param retry Whether it is worth retrying assertion generation.
+ */
+ public GenerateAssertionsResponse(String contestName, boolean succeeded, boolean retry) {
this.contestName = contestName;
- this.winner = winner;
+ this.succeeded = succeeded;
+ this.retry = retry;
}
}
\ No newline at end of file
diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/communication/responseToColoradoRla/GenerateAssertionsResponseWithErrors.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/communication/responseToColoradoRla/GenerateAssertionsResponseWithErrors.java
deleted file mode 100644
index c7334655..00000000
--- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/communication/responseToColoradoRla/GenerateAssertionsResponseWithErrors.java
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
-Democracy Developers IRV extensions to colorado-rla.
-
-@copyright 2024 Colorado Department of State
-
-These IRV extensions are designed to connect to a running instance of the raire
-service (https://github.com/DemocracyDevelopers/raire-service), in order to
-generate assertions that can be audited using colorado-rla.
-
-The colorado-rla IRV extensions are free software: you can redistribute it and/or modify it under the terms
-of the GNU Affero General Public License as published by the Free Software Foundation, either
-version 3 of the License, or (at your option) any later version.
-
-The colorado-rla IRV extensions are distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
-without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
-See the GNU Affero General Public License for more details.
-
-You should have received a copy of the GNU Affero General Public License along with
-raire-service. If not, see .
-*/
-
-package au.org.democracydevelopers.corla.communication.responseToColoradoRla;
-
-import java.beans.ConstructorProperties;
-
-/**
- * The response to be sent to colorado-rla after a GenerateAssertionsRequest.
- * These are identified by contest name.
- * The success response is simply a winner and an empty error string.
- * Error responses have some non-empty error and usually a winner set to "UNKNOWN".
- * Note that errors may sometimes have a real winner, though they usually don't. For example, if trimming assertions
- * times out, there will be both a statement of that error and a real winner.
- */
-public final class GenerateAssertionsResponseWithErrors {
- public final String contestName;
- public final String winner;
- public final String raireError;
-
- /**
- * @param contestName The name of the contest.
- * @param winner The winner of the contest, as calculated by raire.
- * @param raireError The error message returned from raire. Empty if there was no error.
- */
- @ConstructorProperties({"contestName", "winner", "raireError"})
- public GenerateAssertionsResponseWithErrors(String contestName, String winner, String raireError) {
- this.contestName = contestName;
- this.winner = winner;
- this.raireError = raireError;
- }
-}
\ No newline at end of file
diff --git a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertions.java b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertions.java
index ba884292..9d2d4f69 100644
--- a/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertions.java
+++ b/server/eclipse-project/src/main/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertions.java
@@ -23,8 +23,6 @@
import au.org.democracydevelopers.corla.communication.requestToRaire.GenerateAssertionsRequest;
import au.org.democracydevelopers.corla.communication.responseFromRaire.GenerateAssertionsResponse;
-import au.org.democracydevelopers.corla.communication.responseFromRaire.RaireServiceErrors;
-import au.org.democracydevelopers.corla.communication.responseToColoradoRla.GenerateAssertionsResponseWithErrors;
import com.google.gson.JsonSyntaxException;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
@@ -120,7 +118,7 @@ public String endpointBody(final Request the_request, final Response the_respons
final String prefix = "[endpointBody]";
LOGGER.debug(String.format("%s %s.", prefix, "Received Generate Assertions request"));
- final List responseData;
+ final List responseData;
final String raireUrl = Main.properties().getProperty(RAIRE_URL, "") + RAIRE_ENDPOINT;
@@ -177,22 +175,20 @@ public String endpointBody(final Request the_request, final Response the_respons
* - Gather all the IRVContestResults
* - For each IRV contest, make a request to the raire-service get-assertions endpoint of the right format type
* - Collate all the results into a list.
+ *
* @param IRVContestResults the collection of all IRV ContestResults.
* @param timeLimitSeconds the time limit for raire assertion generation, per contest.
* @param raireUrl the url where the raire-service is running.
*/
- protected List generateAllAssertions(List IRVContestResults,
- double timeLimitSeconds, String raireUrl) {
+ protected List generateAllAssertions(final List IRVContestResults,
+ final double timeLimitSeconds, final String raireUrl) {
final String prefix = "[generateAllAssertions]";
LOGGER.debug(String.format("%s %s.", prefix, "Generating assertions for all IRV contests"));
- final List responseData = new ArrayList<>();
-
- // Iterate through all IRV Contests, sending a request to the raire-service for each one's assertions and
- for (final ContestResult cr : IRVContestResults) {
- GenerateAssertionsResponseWithErrors response = generateAssertionsUpdateWinners(IRVContestResults, cr.getContestName(), timeLimitSeconds, raireUrl);
- responseData.add(response);
- }
+ // Iterate through all IRV Contests, sending a request to the raire-service for each one's assertions
+ final List responseData = IRVContestResults.stream().map(
+ r -> generateAssertionsUpdateWinners(IRVContestResults, r.getContestName(), timeLimitSeconds, raireUrl)
+ ).toList();
LOGGER.debug(String.format("%s %s.", prefix, "Completed assertion generation for all IRV contests"));
return responseData;
@@ -202,21 +198,23 @@ protected List generateAllAssertions(List<
* The main work of this endpoint - sends the appropriate request for a single contest, and
* updates stored data with the result. There are two expected kinds of responses from raire:
* - a success response with a winner, or
- * - an INTERNAL_SERVER_ERROR response with a reason, e.g. TIED_WINNERS or NO_VOTES_PRESENT.
- * These are expected to happen occasionally because of the data.
+ * - an error response with a reason, e.g. TIED_WINNERS or NO_VOTES_PRESENT.
+ * These are expected to happen occasionally because of the data, and are saved in the
+ * GenerateAssertionsSummary table.
* Other errors, such as a BAD_REQUEST response or a failure to parse raire's response, indicate
- * programming or configuration errors. These are logged.
+ * programming or configuration errors. These are logged and an exception is thrown.
+ *
* @param IRVContestResults The list of all ContestResults for IRV contests. Note that these do
* not have the correct winners or losers for IRV.
* @param contestName The name of the contest.
* @param timeLimitSeconds The time limit allowed for raire to compute the assertions (not
* counting time taken to retrieve vote data from the database).
* @param raireUrl The url of the raire service.
- * @return The GenerateAssertionsResponseWithErrors, which usually contains a
- * winner but may instead be UNKNOWN_WINNER and an error message.
+ * @return The GenerateAssertionsResponseWithErrors, which usually contains a
+ * winner but may instead be UNKNOWN_WINNER and an error message.
*/
- protected GenerateAssertionsResponseWithErrors generateAssertionsUpdateWinners(List IRVContestResults,
- String contestName, double timeLimitSeconds, String raireUrl) {
+ protected GenerateAssertionsResponse generateAssertionsUpdateWinners(final List IRVContestResults,
+ final String contestName, final double timeLimitSeconds, final String raireUrl) {
final String prefix = "[generateAssertionsUpdateWinners]";
LOGGER.debug(String.format("%s %s %s.", prefix, "Generating assertions for contest ", contestName));
@@ -248,30 +246,17 @@ protected GenerateAssertionsResponseWithErrors generateAssertionsUpdateWinners(L
// Interpret the response.
final int statusCode = raireResponse.getStatusLine().getStatusCode();
- final boolean gotRaireError = raireResponse.containsHeader(RaireServiceErrors.ERROR_CODE_KEY);
-
- if (statusCode == HttpStatus.SC_OK && !gotRaireError) {
- // OK response. Return the winner.
-
- LOGGER.debug(String.format("%s %s %s.", prefix, "OK response received from RAIRE for",
- contestName));
- GenerateAssertionsResponse responseFromRaire = Main.GSON.fromJson(EntityUtils.toString(raireResponse.getEntity()),
- GenerateAssertionsResponse.class);
-
- LOGGER.debug(String.format("%s %s %s.", prefix,
- "Completed assertion generation for contest", contestName));
- return new GenerateAssertionsResponseWithErrors(contestName, responseFromRaire.winner, "");
- } else if (statusCode == HttpStatus.SC_INTERNAL_SERVER_ERROR && gotRaireError) {
- // Error response about a specific contest, e.g. "TIED_WINNERS". Return the error.
+ if (statusCode == HttpStatus.SC_OK) {
- final String code = raireResponse.getFirstHeader(RaireServiceErrors.ERROR_CODE_KEY).getValue();
- LOGGER.debug(String.format("%s %s %s.", prefix, "Error response " + code,
- "received from RAIRE for " + contestName));
+ // OK response, which may indicate either that assertion generation succeeded, or that it
+ // failed and raire generated a useful error. Return raire's response.
+ final GenerateAssertionsResponse responseFromRaire
+ = Main.GSON.fromJson(EntityUtils.toString(raireResponse.getEntity()), GenerateAssertionsResponse.class);
- LOGGER.debug(String.format("%s %s %s.", prefix,
- "Error response for assertion generation for contest ", contestName));
- return new GenerateAssertionsResponseWithErrors(cr.getContestName(), UNKNOWN_WINNER, code);
+ LOGGER.debug(String.format("%s %s %s %s.", prefix, responseFromRaire.succeeded ? "Success" : "Failure",
+ "response for raire assertion generation for contest", contestName));
+ return responseFromRaire;
} else {
// Something went wrong with the connection, e.g. 404 or a Bad Request. Cannot continue.
@@ -280,12 +265,12 @@ protected GenerateAssertionsResponseWithErrors generateAssertionsUpdateWinners(L
LOGGER.error(String.format("%s %s", prefix, msg));
throw new RuntimeException(msg);
}
- } catch (URISyntaxException | MalformedURLException e) {
+ } catch (final URISyntaxException | MalformedURLException e) {
// The raire service url is malformed, probably a config error.
final String msg = "Bad configuration of Raire service url: " + raireUrl + ". Check your config file.";
LOGGER.error(String.format("%s %s %s", prefix, msg, e.getMessage()));
throw new RuntimeException(msg);
- } catch (NoSuchElementException e ) {
+ } catch (final NoSuchElementException e) {
// This happens if the contest name is not in the IRVContestResults, or if the Contest Result
// does not actually contain any contests (the latter should never happen).
LOGGER.error(String.format("%s %s %s.", prefix, e.getMessage(), contestName));
@@ -296,25 +281,25 @@ protected GenerateAssertionsResponseWithErrors generateAssertionsUpdateWinners(L
final String msg = "Error interpreting Raire response for contest ";
LOGGER.error(String.format("%s %s %s %s", prefix, msg, contestName, e.getMessage()));
throw new RuntimeException(msg + contestName);
- } catch (UnsupportedEncodingException e) {
+ } catch (final UnsupportedEncodingException e) {
// This really shouldn't happen, but would happen if the effort to make the
// generateAssertionsRequest as json failed.
final String msg = "Error generating request to Raire for contest ";
LOGGER.error(String.format("%s %s %s %s", prefix, msg, contestName, e.getMessage()));
throw new RuntimeException(msg + contestName + e.getMessage());
- } catch (ClientProtocolException e) {
+ } catch (final ClientProtocolException e) {
// This also really shouldn't happen, but would happen if the effort to use the httpClient
// to send a message threw an exception.
final String msg = "Error sending request to Raire for contest ";
LOGGER.error(String.format("%s %s %s %s", prefix, msg, contestName, e.getMessage()));
throw new RuntimeException(msg + contestName + e.getMessage());
- } catch (NullPointerException e) {
+ } catch (final NullPointerException e) {
// This also shouldn't happen - it would indicate an unexpected problem such as the httpClient
// returning a null response.
final String msg = "Error requesting or receiving assertions for contest ";
LOGGER.error(String.format("%s %s %s.", prefix, msg, contestName));
throw new RuntimeException(msg + contestName);
- } catch (IOException e) {
+ } catch (final IOException e) {
// Generic error that can be thrown by the httpClient if the connection attempt fails.
final String msg = "I/O error during generate assertions attempt for contest ";
LOGGER.error(String.format("%s %s %s %s", prefix, msg, contestName, e.getMessage()));
@@ -338,11 +323,11 @@ protected boolean validateParameters(final Request the_request) {
if (timeLimit != null && Double.parseDouble(timeLimit) <= 0) {
return false;
}
- } catch (NumberFormatException e) {
+ } catch (final NumberFormatException e) {
return false;
}
- // An absent contest name is fine, but a present null or blank one is invalid.
+ // An absent contest name is OK, but a present null or blank one is invalid.
final String contestName = the_request.queryParams(CONTEST_NAME);
return contestName == null || !contestName.isEmpty();
}
diff --git a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertionsTests.java b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertionsTests.java
index d14e4158..6ccdb684 100644
--- a/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertionsTests.java
+++ b/server/eclipse-project/src/test/java/au/org/democracydevelopers/corla/endpoint/GenerateAssertionsTests.java
@@ -23,8 +23,6 @@
import au.org.democracydevelopers.corla.communication.requestToRaire.GenerateAssertionsRequest;
import au.org.democracydevelopers.corla.communication.responseFromRaire.GenerateAssertionsResponse;
-import au.org.democracydevelopers.corla.communication.responseFromRaire.RaireServiceErrors;
-import au.org.democracydevelopers.corla.communication.responseToColoradoRla.GenerateAssertionsResponseWithErrors;
import static au.org.democracydevelopers.corla.endpoint.GenerateAssertions.UNKNOWN_WINNER;
import static au.org.democracydevelopers.corla.util.testUtils.*;
@@ -50,6 +48,8 @@
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+import static org.testng.Assert.assertFalse;
/**
* Test the GetAssertions endpoint, both CSV and JSON versions. The response is supposed to be a zip file containing
@@ -81,13 +81,19 @@ public class GenerateAssertionsTests extends TestClassWithDatabase {
* Mock response for Boulder Mayoral '23
*/
private final static GenerateAssertionsResponse boulderResponse
- = new GenerateAssertionsResponse(boulderMayoral, "Aaron Brockett");
+ = new GenerateAssertionsResponse(boulderMayoral, true, false);
/**
* Mock response for tinyExample1 contest
*/
private final static GenerateAssertionsResponse tinyIRVResponse
- = new GenerateAssertionsResponse(tinyIRV, "Alice");
+ = new GenerateAssertionsResponse(tinyIRV, true, false);
+
+ /**
+ * Mock response for tiedIRV contest
+ */
+ private final static GenerateAssertionsResponse tiedIRVResponse
+ = new GenerateAssertionsResponse(tiedIRV, false, false);
/**
* Request for Boulder Mayoral '23
@@ -181,7 +187,8 @@ public void initMocks() {
tiedIRVContestResult.addContests(Set.of(tiedIRVContest));
// Default raire server. You can instead run the real raire service and set baseUrl accordingly,
- // though the tests of invalid/uninterpretable data will fail.
+ // though the tests of invalid/uninterpretable data will fail, and of course you have to have
+ // appropriate contests in the database.
wireMockRaireServer.start();
baseUrl = wireMockRaireServer.baseUrl();
String badUrl = baseUrl + badEndpoint;
@@ -200,14 +207,13 @@ public void initMocks() {
.withStatus(HttpStatus.SC_OK)
.withHeader("Content-Type", "application/json")
.withBody(gson.toJson(tinyIRVResponse))));
- // Mock a TIED_WINNERS response to the tiedIRV contest.
+ // Mock failed, don't redo response to the tiedIRV contest.
stubFor(post(urlEqualTo(raireGenerateAssertionsEndpoint))
.withRequestBody(equalToJson(gson.toJson(tiedIRVRequest)))
.willReturn(aResponse()
- .withStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR)
+ .withStatus(HttpStatus.SC_OK)
.withHeader("Content-Type", "application/json")
- .withHeader(RaireServiceErrors.ERROR_CODE_KEY,
- RaireServiceErrors.RaireErrorCodes.TIED_WINNERS.toString())));
+ .withBody(gson.toJson(tiedIRVResponse))));
// Mock a 404 for badUrl.
stubFor(post(urlEqualTo(badUrl))
.withRequestBody(equalToJson(gson.toJson(tinyIRVRequest)))
@@ -221,7 +227,6 @@ public void initMocks() {
.withStatus(HttpStatus.SC_OK)
.withHeader("Content-Type", "application/json")
.withBody(gson.toJson(tinyIRVCandidates))));
-
// Mock an OK response with invalid json.
stubFor(post(urlEqualTo(invalidResponseEndpoint))
.withRequestBody(equalToJson(gson.toJson(tinyIRVRequest)))
@@ -239,57 +244,60 @@ public void closeMocks() {
/**
* Calls the single-contest version of the endpoint for the boulder Mayor '23 example, checks
- * for the right winner.
+ * that generation succeeded and does not recommend retry.
*/
@Test
public void rightBoulderIRVWinner() {
testUtils.log(LOGGER, "rightBoulderIRVWinner");
GenerateAssertions endpoint = new GenerateAssertions();
- GenerateAssertionsResponseWithErrors result = endpoint.generateAssertionsUpdateWinners(
+ GenerateAssertionsResponse result = endpoint.generateAssertionsUpdateWinners(
mockedIRVContestResults, boulderRequest.contestName, boulderRequest.timeLimitSeconds,
baseUrl + raireGenerateAssertionsEndpoint);
assertEquals(result.contestName, boulderMayoral);
- assertEquals(result.winner, "Aaron Brockett");
+ assertTrue(result.succeeded);
+ assertFalse(result.retry);
}
/**
- * Calls the single-contest version of the endpoint for the tied contests, checks that the
- * winner is unknown and the TIED_WINNERS error is returned.
+ * Calls the single-contest version of the endpoint for the tied contests, checks that
+ * assertion generation fails and does not recommend retry.
*/
@Test
- public void tiedWinnersCorrectlyRecorded() {
- testUtils.log(LOGGER, "tiedWinnersCorrectlyRecorded");
+ public void tiedWinnersFailsNoRetry() {
+ testUtils.log(LOGGER, "tiedWinnersFailsNoRetry");
GenerateAssertions endpoint = new GenerateAssertions();
- GenerateAssertionsResponseWithErrors result = endpoint.generateAssertionsUpdateWinners(
+ GenerateAssertionsResponse result = endpoint.generateAssertionsUpdateWinners(
List.of(tiedIRVContestResult), tiedIRV, tiedIRVRequest.timeLimitSeconds,
baseUrl + raireGenerateAssertionsEndpoint);
assertEquals(result.contestName, tiedIRV);
- assertEquals(result.winner, UNKNOWN_WINNER);
- assertEquals(result.raireError ,RaireServiceErrors.RaireErrorCodes.TIED_WINNERS.toString());
+ assertFalse(result.succeeded);
+ assertFalse(result.retry);
}
/**
- * Calls the generateAssertions main endpoint function and checks that the right winners are
- * returned, for the two example contests (Boulder and TinyIRV).
+ * Calls the generateAssertions main endpoint function for the two example contests (Boulder and TinyIRV).
+ * and checks that both succeed and do not recommend retry.
*/
@Test
- public void rightWinners() {
- testUtils.log(LOGGER, "rightWinners");
+ public void successAsExpected() {
+ testUtils.log(LOGGER, "successAsExpected");
GenerateAssertions endpoint = new GenerateAssertions();
- List results
+ List results
= endpoint.generateAllAssertions(mockedIRVContestResults, boulderRequest.timeLimitSeconds,
baseUrl + raireGenerateAssertionsEndpoint);
assertEquals(results.size(), 2);
assertEquals(results.get(0).contestName, boulderMayoral);
- assertEquals(results.get(0).winner, "Aaron Brockett");
+ assertTrue(results.get(0).succeeded);
+ assertFalse(results.get(0).retry);
assertEquals(results.get(1).contestName, tinyIRV);
- assertEquals(results.get(1).winner, "Alice");
+ assertTrue(results.get(1).succeeded);
+ assertFalse(results.get(1).retry);
}
/**