diff --git a/endeavour/src/main/java/org/saltations/endeavour/Failure.java b/endeavour/src/main/java/org/saltations/endeavour/Failure.java index 99ee6c7..7c7a968 100644 --- a/endeavour/src/main/java/org/saltations/endeavour/Failure.java +++ b/endeavour/src/main/java/org/saltations/endeavour/Failure.java @@ -76,8 +76,9 @@ public Result ifFailure(CheckedConsumer> action) return action.accept(this); } catch (Exception ex) { return new Failure<>(FailureDescription.of() - .type(FailureDescription.GenericFailureType.GENERIC_EXCEPTION) + .type(FailureDescription.GenericFailureType.GENERIC_CONSUMER_EXCEPTION) .cause(ex) + .precedingFailure(this.description) .build()); } } @@ -108,6 +109,7 @@ public Result orElseGet(CheckedSupplier> supplier) return new Failure<>(FailureDescription.of() .type(FailureDescription.GenericFailureType.GENERIC_EXCEPTION) .cause(ex) + .precedingFailure(this.description) .build()); } catch (Exception e) @@ -117,10 +119,12 @@ public Result orElseGet(CheckedSupplier> supplier) case RuntimeException ex -> new Failure<>(FailureDescription.of() .type(FailureDescription.GenericFailureType.GENERIC_EXCEPTION) .cause(ex) + .precedingFailure(this.description) .build()); case Exception ex -> new Failure<>(FailureDescription.of() .type(FailureDescription.GenericFailureType.GENERIC_EXCEPTION) .cause(ex) + .precedingFailure(this.description) .build()); }; } diff --git a/endeavour/src/main/java/org/saltations/endeavour/FailureDescription.java b/endeavour/src/main/java/org/saltations/endeavour/FailureDescription.java index 40b757e..c0f674a 100644 --- a/endeavour/src/main/java/org/saltations/endeavour/FailureDescription.java +++ b/endeavour/src/main/java/org/saltations/endeavour/FailureDescription.java @@ -133,7 +133,8 @@ public enum GenericFailureType implements FailureType GENERIC("generic-failure", ""), GENERIC_EXCEPTION("generic-checked-exception-failure", ""), GENERIC_INTERRUPTED_EXCEPTION("generic-interrupted-exception-failure", ""), - GENERIC_RUNTIME_EXCEPTION("generic-runtime-exception-failure", "") + GENERIC_RUNTIME_EXCEPTION("generic-runtime-exception-failure", ""), + GENERIC_CONSUMER_EXCEPTION("generic-consumer-exception-failure", "") ; private final String title; diff --git a/endeavour/src/test/java/org/saltations/endeavour/FailureTest.java b/endeavour/src/test/java/org/saltations/endeavour/FailureTest.java index 6e9069f..b678a04 100644 --- a/endeavour/src/test/java/org/saltations/endeavour/FailureTest.java +++ b/endeavour/src/test/java/org/saltations/endeavour/FailureTest.java @@ -114,6 +114,105 @@ void whenIfFailureThenTakesAction() throws Exception { assertSame(failure, result, "Should return same result"); } + @Test + @Order(62) + void whenIfFailureActionThrowsExceptionThenReturnsFailureWithPrecedingFailure() + { + Exception testException = new Exception("Test exception"); + + Result result = failure.ifFailure(x -> { + throw testException; + }); + + assertThat(result) + .isFailure() + .hasFailureType(FailureDescription.GenericFailureType.GENERIC_CONSUMER_EXCEPTION) + .hasCause() + .hasCauseOfType(Exception.class) + .hasCauseWithMessage("Test exception"); + + // Verify preceding failure is preserved + Failure resultFailure = (Failure) result; + assertTrue(resultFailure.description().hasPrecedingFailure(), "Should have preceding failure"); + assertEquals(((Failure)failure).description(), resultFailure.description().getPrecedingFailure(), "Preceding failure should match original failure"); + } + + @Test + @Order(63) + void whenIfFailureActionThrowsRuntimeExceptionThenReturnsFailureWithPrecedingFailure() + { + RuntimeException testException = new RuntimeException("Test runtime exception"); + + Result result = failure.ifFailure(x -> { + throw testException; + }); + + assertThat(result) + .isFailure() + .hasFailureType(FailureDescription.GenericFailureType.GENERIC_CONSUMER_EXCEPTION) + .hasCause() + .hasCauseOfType(RuntimeException.class) + .hasCauseWithMessage("Test runtime exception"); + + Failure resultFailure = (Failure) result; + assertTrue(resultFailure.description().hasPrecedingFailure(), "Should have preceding failure"); + } + + @Test + @Order(64) + void whenIfFailureWithNullActionThenThrowsNullPointerException() + { + assertThrows(NullPointerException.class, () -> { + failure.ifFailure(null); + }); + } + + @Test + @Order(65) + void whenIfFailureReturnsDifferentResultThenUsesReturnedResult() + { + // CheckedConsumer>.accept returns Failure + // The ifFailure method returns action.accept(this), which returns the Failure + Result result = failure.ifFailure(x -> { + // Return a different Failure instance + return new Failure<>(FailureDescription.of() + .type(FailureDescription.GenericFailureType.GENERIC) + .detail("Different failure") + .build()); + }); + + assertThat(result) + .isFailure() + .hasFailureType(FailureDescription.GenericFailureType.GENERIC); + assertEquals("Different failure", ((Failure)result).getDetail(), "Should have different detail"); + } + + @Test + @Order(66) + void whenReduceFailureFunctionThrowsExceptionThenReturnsEmptyOptional() + { + Exception testException = new Exception("Test exception"); + + Optional result = failure.reduce( + v -> "Success", + f -> { throw testException; } + ); + + assertTrue(result.isEmpty(), "Should return empty Optional on exception"); + } + + @Test + @Order(67) + void whenReduceFailureFunctionReturnsNullThenReturnsEmptyOptional() + { + Optional result = failure.reduce( + v -> "Success", + f -> null + ); + + assertTrue(result.isEmpty(), "Should return empty Optional when function returns null"); + } + @Test @Order(70) diff --git a/endeavour/src/test/java/org/saltations/endeavour/QualSuccessTest.java b/endeavour/src/test/java/org/saltations/endeavour/QualSuccessTest.java index b94e237..8df558a 100644 --- a/endeavour/src/test/java/org/saltations/endeavour/QualSuccessTest.java +++ b/endeavour/src/test/java/org/saltations/endeavour/QualSuccessTest.java @@ -117,6 +117,47 @@ void whenTakingActionIfSuccessThenTakesAction() throws Exception assertSame(qualSuccess, result, "Should return same result"); } + @Test + @Order(61) + void whenIfSuccessActionThrowsExceptionThenReturnsFailure() + { + Exception testException = new Exception("Test exception"); + + Result result = qualSuccess.ifSuccess(x -> { + throw testException; + }); + + assertThat(result) + .isFailure() + .hasFailureType(FailureDescription.GenericFailureType.GENERIC_EXCEPTION) + .hasCause() + .hasCauseOfType(Exception.class) + .hasCauseWithMessage("Test exception"); + } + + @Test + @Order(62) + void whenIfSuccessWithNullActionThenThrowsNullPointerException() + { + assertThrows(NullPointerException.class, () -> { + qualSuccess.ifSuccess(null); + }); + } + + @Test + @Order(63) + void whenReduceSuccessFunctionThrowsExceptionThenReturnsEmptyOptional() + { + Exception testException = new Exception("Test exception"); + + Optional result = qualSuccess.reduce( + v -> { throw testException; }, + f -> "Failure" + ); + + assertTrue(result.isEmpty(), "Should return empty Optional on exception"); + } + @Test @Order(61) diff --git a/endeavour/src/test/java/org/saltations/endeavour/QuantSuccessTest.java b/endeavour/src/test/java/org/saltations/endeavour/QuantSuccessTest.java index 4185634..1b7596b 100644 --- a/endeavour/src/test/java/org/saltations/endeavour/QuantSuccessTest.java +++ b/endeavour/src/test/java/org/saltations/endeavour/QuantSuccessTest.java @@ -144,6 +144,88 @@ void whenTakingActionOnSuccessThenTakesAction() throws Exception assertSame(value, result, "Should return same result"); } + @Test + @Order(41) + void whenIfSuccessActionThrowsExceptionThenReturnsFailure() + { + Exception testException = new Exception("Test exception"); + + Result result = value.ifSuccess(x -> { + throw testException; + }); + + assertThat(result) + .isFailure() + .hasFailureType(FailureDescription.GenericFailureType.GENERIC_EXCEPTION) + .hasCause() + .hasCauseOfType(Exception.class) + .hasCauseWithMessage("Test exception"); + } + + @Test + @Order(42) + void whenIfSuccessActionThrowsRuntimeExceptionThenReturnsFailure() + { + RuntimeException testException = new RuntimeException("Test runtime exception"); + + Result result = value.ifSuccess(x -> { + throw testException; + }); + + assertThat(result) + .isFailure() + .hasFailureType(FailureDescription.GenericFailureType.GENERIC_EXCEPTION) + .hasCause() + .hasCauseOfType(RuntimeException.class) + .hasCauseWithMessage("Test runtime exception"); + } + + @Test + @Order(43) + void whenIfSuccessActionThrowsInterruptedExceptionThenReturnsFailure() + { + InterruptedException testException = new InterruptedException("Test interrupted"); + + Result result = value.ifSuccess(x -> { + throw testException; + }); + + assertThat(result) + .isFailure() + .hasFailureType(FailureDescription.GenericFailureType.GENERIC_EXCEPTION) + .hasCause() + .hasCauseOfType(InterruptedException.class) + .hasCauseWithMessage("Test interrupted"); + } + + @Test + @Order(44) + void whenIfSuccessWithNullActionThenThrowsNullPointerException() + { + assertThrows(NullPointerException.class, () -> { + value.ifSuccess(null); + }); + } + + @Test + @Order(45) + void whenIfSuccessReturnsDifferentResultThenUsesReturnedResult() + { + // CheckedConsumer>.accept returns Success + // We need to return a Success, which Try.success() provides + // Since Success extends Result, the method can return it + Result result = value.ifSuccess(x -> { + // Return a different Success instance - Try.success returns Result but we know it's Success + Success newSuccess = (Success) Try.success(9999L); + return newSuccess; + }); + + assertThat(result) + .isSuccess() + .isQuantSuccess() + .hasValue(9999L); + } + @Test @Order(60) void whenTransformingResultOnFailureThenReturnsExistingSuccess() @@ -209,6 +291,32 @@ void whenTransformingThenGivesTransformedResult() assertEquals("Success with value", result.get(), "Transformed to 'Success with value'"); } + @Test + @Order(101) + void whenReduceSuccessFunctionThrowsExceptionThenReturnsEmptyOptional() + { + Exception testException = new Exception("Test exception"); + + Optional result = value.reduce( + v -> { throw testException; }, + f -> "Failure" + ); + + assertTrue(result.isEmpty(), "Should return empty Optional on exception"); + } + + @Test + @Order(102) + void whenReduceReturnsNullThenReturnsEmptyOptional() + { + Optional result = value.reduce( + v -> null, + f -> "Failure" + ); + + assertTrue(result.isEmpty(), "Should return empty Optional when function returns null"); + } + @Test @Order(110) void optReturnsOptionalWithValueForSuccess() {