1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 04:39:35 +01:00

feat(test): Jazzer Fuzz-Tests für Production-Aggregate einführen

Coverage-guided Fuzz-Tests für ProductionOrder, Recipe und Batch.
Jeder Test fuzzt create() + zufällige Sequenzen aller Mutations-Methoden,
um unbehandelte Exceptions bei beliebigen Inputs aufzudecken.

- jazzer-junit 0.24.0 als Test-Dependency
- Maven-Profile: -Pfuzz (echtes Fuzzing), -Pfuzz-regression (Crash-Replay)
- Surefire: FuzzTests im Default-Lauf ausgeschlossen, reuseForks=false
- Makefile: make fuzz, make fuzz/regression, make fuzz/single
- .gitignore: .cifuzz-corpus/ ausgeschlossen
This commit is contained in:
Sebastian Frick 2026-02-26 08:50:02 +01:00
parent 74dc9a6981
commit 8a84bf5f25
6 changed files with 312 additions and 1 deletions

3
.gitignore vendored
View file

@ -66,5 +66,8 @@ frontend/**/pnpm-lock.yaml
# Git worktrees
.worktrees/
# Jazzer fuzzing corpus (regenerierbar, nicht committen)
.cifuzz-corpus/
# Legacy bin directory
bin/

View file

@ -32,6 +32,7 @@
<jacoco.version>0.8.12</jacoco.version>
<pitest.version>1.17.4</pitest.version>
<pitest-junit5.version>1.2.1</pitest-junit5.version>
<jazzer.version>0.24.0</jazzer.version>
</properties>
<dependencies>
@ -120,6 +121,13 @@
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.code-intelligence</groupId>
<artifactId>jazzer-junit</artifactId>
<version>${jazzer.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@ -147,6 +155,20 @@
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/*FuzzTest.java</exclude>
</excludes>
<!-- -noverify: Jazzer's bytecode instrumentation conflicts with JaCoCo
on Java 21 pattern matching (VerifyError on stack maps).
Safe for test execution. -->
<argLine>-XX:+EnableDynamicAgentLoading -noverify</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
@ -176,6 +198,52 @@
</build>
<profiles>
<profile>
<id>fuzz</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>none</exclude>
</excludes>
<includes>
<include>**/*FuzzTest.java</include>
</includes>
<reuseForks>false</reuseForks>
<argLine>-XX:+EnableDynamicAgentLoading -noverify</argLine>
<environmentVariables>
<JAZZER_FUZZ>1</JAZZER_FUZZ>
</environmentVariables>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>fuzz-regression</id>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>none</exclude>
</excludes>
<includes>
<include>**/*FuzzTest.java</include>
</includes>
<argLine>-XX:+EnableDynamicAgentLoading -noverify</argLine>
</configuration>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>mutation</id>
<build>

View file

@ -0,0 +1,79 @@
package de.effigenix.domain.production;
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.junit.FuzzTest;
import de.effigenix.shared.common.Result;
import java.time.LocalDate;
/**
* Fuzz test for the Batch aggregate.
*
* Exercises plan() with fuzzed drafts, then applies a random sequence of
* state-mutating operations (startProduction, recordConsumption, complete, cancel).
* Verifies that no input combination causes an unhandled exception
* all invalid inputs must be caught and returned as Result.Failure.
*
* Run: make fuzz | make fuzz/single TEST=BatchFuzzTest
*/
class BatchFuzzTest {
@FuzzTest(maxDuration = "5m")
void fuzzBatch(FuzzedDataProvider data) {
var draft = new BatchDraft(
data.consumeString(50),
data.consumeString(30),
data.consumeString(20),
consumeLocalDate(data),
consumeLocalDate(data)
);
BatchNumber batchNumber;
try {
batchNumber = BatchNumber.generate(LocalDate.now(), 1);
} catch (Exception e) {
return;
}
switch (Batch.plan(draft, batchNumber)) {
case Result.Failure(var err) -> { }
case Result.Success(var batch) -> {
int ops = data.consumeInt(1, 10);
for (int i = 0; i < ops; i++) {
int op = data.consumeInt(0, 3);
switch (op) {
case 0 -> batch.startProduction();
case 1 -> {
batch.recordConsumption(new ConsumptionDraft(
data.consumeString(50),
data.consumeString(50),
data.consumeString(30),
data.consumeString(20)
));
}
case 2 -> {
batch.complete(new CompleteBatchDraft(
data.consumeString(30),
data.consumeString(20),
data.consumeString(30),
data.consumeString(20),
data.consumeString(100)
));
}
case 3 -> batch.cancel(new CancelBatchDraft(data.consumeString(600)));
}
}
}
}
}
private static LocalDate consumeLocalDate(FuzzedDataProvider data) {
if (data.consumeBoolean()) {
return null;
}
int year = data.consumeInt(1900, 2100);
int month = data.consumeInt(1, 12);
int day = data.consumeInt(1, 28);
return LocalDate.of(year, month, day);
}
}

View file

@ -0,0 +1,65 @@
package de.effigenix.domain.production;
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.junit.FuzzTest;
import de.effigenix.shared.common.Result;
import java.time.LocalDate;
/**
* Fuzz test for the ProductionOrder aggregate.
*
* Exercises create() with fuzzed drafts, then applies a random sequence of
* state-mutating operations (release, startProduction, complete, cancel, reschedule).
* Verifies that no input combination causes an unhandled exception
* all invalid inputs must be caught and returned as Result.Failure.
*
* Run: make fuzz | make fuzz/single TEST=ProductionOrderFuzzTest
*/
class ProductionOrderFuzzTest {
@FuzzTest(maxDuration = "5m")
void fuzzProductionOrder(FuzzedDataProvider data) {
var draft = new ProductionOrderDraft(
data.consumeString(50),
data.consumeString(30),
data.consumeString(20),
consumeLocalDate(data),
data.consumeString(10),
data.consumeString(100)
);
switch (ProductionOrder.create(draft)) {
case Result.Failure(var err) -> {
// Invalid draft expected
}
case Result.Success(var order) -> {
int ops = data.consumeInt(1, 10);
for (int i = 0; i < ops; i++) {
int op = data.consumeInt(0, 4);
switch (op) {
case 0 -> order.release();
case 1 -> {
try {
order.startProduction(BatchId.of(data.consumeString(50)));
} catch (Exception ignored) { }
}
case 2 -> order.complete();
case 3 -> order.cancel(data.consumeString(50));
case 4 -> order.reschedule(consumeLocalDate(data));
}
}
}
}
}
private static LocalDate consumeLocalDate(FuzzedDataProvider data) {
if (data.consumeBoolean()) {
return null;
}
int year = data.consumeInt(1900, 2100);
int month = data.consumeInt(1, 12);
int day = data.consumeInt(1, 28);
return LocalDate.of(year, month, day);
}
}

View file

@ -0,0 +1,86 @@
package de.effigenix.domain.production;
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.junit.FuzzTest;
import de.effigenix.shared.common.Result;
/**
* Fuzz test for the Recipe aggregate.
*
* Exercises create() with fuzzed drafts, then applies a random sequence of
* state-mutating operations (addIngredient, addProductionStep, removeIngredient,
* removeProductionStep, activate, archive).
* Verifies that no input combination causes an unhandled exception
* all invalid inputs must be caught and returned as Result.Failure.
*
* Run: make fuzz | make fuzz/single TEST=RecipeFuzzTest
*/
class RecipeFuzzTest {
private static final RecipeType[] RECIPE_TYPES = RecipeType.values();
@FuzzTest(maxDuration = "5m")
void fuzzRecipe(FuzzedDataProvider data) {
var draft = new RecipeDraft(
data.consumeString(200),
data.consumeInt(0, 100),
data.consumeBoolean() ? data.pickValue(RECIPE_TYPES) : null,
data.consumeString(200),
data.consumeInt(0, 300),
data.consumeBoolean() ? data.consumeInt(-10, 400) : null,
data.consumeString(30),
data.consumeString(20),
data.consumeString(40)
);
switch (Recipe.create(draft)) {
case Result.Failure(var err) -> { }
case Result.Success(var recipe) -> {
int ops = data.consumeInt(1, 12);
for (int i = 0; i < ops; i++) {
int op = data.consumeInt(0, 5);
switch (op) {
case 0 -> {
var ingredientDraft = consumeIngredientDraft(data);
if (ingredientDraft != null) recipe.addIngredient(ingredientDraft);
}
case 1 -> {
recipe.addProductionStep(new ProductionStepDraft(
data.consumeInt(), data.consumeString(100),
data.consumeBoolean() ? data.consumeInt() : null,
data.consumeBoolean() ? data.consumeInt() : null
));
}
case 2 -> recipe.activate();
case 3 -> recipe.archive();
case 4 -> {
if (!recipe.ingredients().isEmpty()) {
recipe.removeIngredient(recipe.ingredients().getFirst().id());
}
}
case 5 -> {
if (!recipe.productionSteps().isEmpty()) {
recipe.removeProductionStep(recipe.productionSteps().getFirst().stepNumber());
}
}
}
}
}
}
}
private static IngredientDraft consumeIngredientDraft(FuzzedDataProvider data) {
try {
return new IngredientDraft(
data.consumeInt(),
data.consumeString(50),
data.consumeString(30),
data.consumeString(20),
data.consumeBoolean() ? data.consumeString(40) : null,
data.consumeBoolean()
);
} catch (IllegalArgumentException e) {
return null;
}
}
}

View file

@ -1,5 +1,5 @@
.PHONY: frontend-dev backend/run generate/openapi bugsink seed-testdata
.PHONY: frontend-dev backend/run generate/openapi bugsink seed-testdata fuzz fuzz/regression fuzz/single
frontend/run:
cd frontend/apps/cli && node --env-file=../../../.env --import tsx src/index.tsx
@ -13,6 +13,16 @@ generate/openapi:
seed-testdata:
PGPASSWORD=effigenix nix shell nixpkgs#postgresql --command psql -f backend/src/main/resources/db/changelog/changes/099-seed-testdata.sql -h localhost -p 5432 -U effigenix -d effigenix
fuzz:
cd backend && mvn test -Pfuzz
fuzz/regression:
cd backend && mvn test -Pfuzz-regression
fuzz/single:
@test -n "$(TEST)" || (echo "Usage: make fuzz/single TEST=BatchFuzzTest" && exit 1)
cd backend && mvn test -Pfuzz -Dtest=$(TEST)
bugsink:
@. ./.env 2>/dev/null || true; \
docker start bugsink 2>/dev/null || \