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:
parent
74dc9a6981
commit
8a84bf5f25
6 changed files with 312 additions and 1 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -66,5 +66,8 @@ frontend/**/pnpm-lock.yaml
|
|||
# Git worktrees
|
||||
.worktrees/
|
||||
|
||||
# Jazzer fuzzing corpus (regenerierbar, nicht committen)
|
||||
.cifuzz-corpus/
|
||||
|
||||
# Legacy bin directory
|
||||
bin/
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
makefile
12
makefile
|
|
@ -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 || \
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue