Skip to content

Commit

Permalink
Merge pull request #10 from goatfryed/feat/#9/force-write-flag
Browse files Browse the repository at this point in the history
feat(#9)!: introduce force write flag
  • Loading branch information
goatfryed authored Oct 26, 2024
2 parents aada9c4 + 2a5e484 commit 461955e
Show file tree
Hide file tree
Showing 16 changed files with 356 additions and 88 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ By default, the following conventions are assumed

See [conventions & configuration](./docs/convention-and-configuration.md) how this can be changed.

## Tips & Tricks
### Force overwrite
Use system property `-Dio.github.goatfryed.assert_baseline.forceBaselineUpdate` to force baseline overwrites.
Use with care! This will skip all assertions and overwrite your baselines.
We recommend to git commit before and git diff after to verify your new baselines. This can ease test updates after larger updates.


## Goals & Non-Goals
See our [project scope](./docs/project-scope.md) to for more

Expand Down
19 changes: 12 additions & 7 deletions docs/extend-assert-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ Compare [JsonBaselineAssertion](../src/main/java/io/github/goatfryed/assert_base
(!) This internal API is considered unstable.

## Extending from AbstractBaselineAssertion
You can extend from [AbstractBaselineAssertion](../src/main/java/io/github/goatfryed/assert_baseline/core/AbstractBaselineAssertion.java)

### 1. implement write
Implement `AbstractBaselineAssertion::saveActual(BaselineContext)`.
Serialize your subject and write it to `BaselineContext::getActualOutputStream`
### implement assertion logic
First implement `AbstractBaselineAssertion::getAdapter(BaselineContext)`.

### 2. implement verification
Implement `verifyIsEqualToBaseline(BaselineContext context)`.
Usually, you'll want to read from `BaselineContext::getBaselineAsString(), deserialize the baseline,
and then compare the java beans.
Compare [JsonBaselineAssertion](../src/main/java/io/github/goatfryed/assert_baseline/json/JsonBaselineAssertion.java)
for an example.

The adapter is used to decouple operations from the test strategy used.
Effectively, atm we just implement the adapter on the assertion itself and don't use more complex context.
This is likely to change in the future, when more complex strategies are required.

### implement further conventional methods
Usually, you want to implement at least
- `usingYourFormatComparator(...)`
- `yourFormatSatisfies(...)`
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import io.github.goatfryed.assert_baseline.core.convention.ConventionLocator;
import io.github.goatfryed.assert_baseline.core.storage.StorageFactory;
import org.assertj.core.api.AbstractAssert;
import org.jetbrains.annotations.NotNull;

import java.util.function.Function;

Expand All @@ -24,10 +25,10 @@ public SELF using(Function<BaselineContextFactory,BaselineContextFactory> config
}

public final SELF isEqualToBaseline(String baseline) {
var context = getContextFactory().build(baseline);

saveActual(context);
verifyIsEqualToBaseline(context);
getContextFactory()
.build(baseline)
.assertWithAdapter(getAssertionAdapter());

return myself;
}
Expand All @@ -39,15 +40,8 @@ public final SELF usingStorage(Configurer<StorageFactory> configurer) {
return myself;
}

/**
* Expects you to write your subject in serialized form to {@link BaselineContext#getActualOutputStream()}
*/
abstract protected void saveActual(BaselineContext context);

/**
* Usually, you'll want to read {@link BaselineContext#getBaselineAsString()}, deserialize the baseline and compare the models.
*/
abstract protected void verifyIsEqualToBaseline(BaselineContext context);
@NotNull
abstract protected BaselineAssertionAdapter getAssertionAdapter();

private BaselineContextFactory getContextFactory() {
if (contextFactory == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.github.goatfryed.assert_baseline.core;

/**
* Adapter interface to perform the real serialization and assertion
* <br><br>
* This interface is used to separate the format dependent actions of baseline testing
* from the test process itself.
*/
public interface BaselineAssertionAdapter {

/**
* Writes the serialized representation of the actual subject
*
* @param output Out parameter
* @param context
*/
void writeActual(BaselineContext.ActualOutput output, BaselineContext context);

/**
* Perform the equality assertion against the provided baseline
*
* @param baseline the baseline input
* @param context
*/
void assertEquals(BaselineContext.BaselineInput baseline, BaselineContext context);

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,80 +5,138 @@
import org.assertj.core.description.Description;
import org.assertj.core.description.JoinDescription;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;

public class BaselineContext {

@NotNull
private final StoredValue actual;
@NotNull
private final StoredValue baseline;
@NotNull final Map<Options, Object> options;

public BaselineContext(
@NotNull StoredValue baseline,
@NotNull StoredValue actual
@NotNull StoredValue actual,
@NotNull Map<Options, Object> options
) {
this.actual = actual;
this.baseline = baseline;
this.options = options;

actual .setName("actual :");
actual.setName("actual :");
baseline.setName("baseline :");
}

public @NotNull StoredValue getActual() {
return actual;
}

public @NotNull OutputStream getActualOutputStream() {
return actual.getOutputStream();
}

public @NotNull StoredValue getBaseline() {
return baseline;
}

public @NotNull InputStream getBaselineInputStream() {
try {
return baseline.getInputStream();
} catch (AssertionError e) {
if (e.getCause() instanceof FileNotFoundException) {
throw new AssertionError(
"No baseline found. Consider saving %s as baseline.".formatted(
actual.asDescription()
), e
);
}
throw e;
}
}

public @NotNull Description asDescription() {
return new JoinDescription(
"Baseline set","",
"Baseline set", "",
Arrays.asList(
baseline.asDescription(),
actual.asDescription()
)
);
}

public String getBaselineAsString() {
try (var input = getBaselineInputStream()) {
return IOUtils.toString(input, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new AssertionError(
"Failed to read baseline as string\n%s".formatted(getActual().asDescription()),
e
);
public void assertWithAdapter(BaselineAssertionAdapter adapter) {
// not using .getOrDefault, because explicit null should be treated as false as well
boolean forceBaselineUpdate = Optional.ofNullable(
(Boolean) options.get(Options.StandardOptions.FORCE_BASELINE_UPDATE)
).orElse(false);

if (forceBaselineUpdate) {
adapter.writeActual(new ActualOutput(getBaseline()), this);
System.err.println("WARNING: Forced baseline creation or update of %s. Skipping check.".formatted(getBaseline().asDescription()));
return;
}

var actualOutput = new ActualOutput(getActual());
adapter.writeActual(actualOutput, this);

var baselineInput = new BaselineInput(
getBaseline(),
() -> "Consider saving %s as baseline.".formatted(
actual.asDescription()
)
);
adapter.assertEquals(baselineInput, this);
}

public static class BaselineInput {

private final StoredValue baseline;
private final Supplier<String> notFoundSuggestionSupplier;

public BaselineInput(
StoredValue baseline,
Supplier<String> notFoundSuggestionSupplier
) {
this.baseline = baseline;
this.notFoundSuggestionSupplier = notFoundSuggestionSupplier;
}

public @NotNull InputStream getInputStream() {
try {
return baseline.getInputStream();
} catch (FileNotFoundException e) {
throw new AssertionError(
"No baseline found. %s".formatted(
notFoundSuggestionSupplier.get()
), e
);
} catch (IOException e) {
throw new AssertionError(
"Failed to create input stream for %s.".formatted(
baseline.asDescription()
), e
);
}
}

public String readContentAsString() {
try (var input = getInputStream()) {
return IOUtils.toString(input, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new AssertionError(
"Failed to read baseline as string\n%s".formatted(baseline.asDescription()),
e
);
}
}
}

public static class ActualOutput {

private final StoredValue actual;

public ActualOutput(StoredValue actual) {
this.actual = actual;
}

public @NotNull OutputStream outputStream() {
try {
return actual.getOutputStream();
} catch (IOException e) {
throw new AssertionError("failed to create output stream for " + actual.asDescription(), e);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package io.github.goatfryed.assert_baseline.core;

import io.github.goatfryed.assert_baseline.core.storage.*;
import org.jetbrains.annotations.NotNull;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class BaselineContextFactory {
Expand All @@ -21,7 +25,22 @@ public BaselineContext build(String requestedKey) {

validateDifferentPaths(baseline, actual);

return new BaselineContext(baseline, actual);
return new BaselineContext(baseline, actual, collectOptions());
}

private @NotNull Map<Options, Object> collectOptions() {
Map<Options, Object> options = new HashMap<>();

for (Options.StandardOptions option : Options.StandardOptions.values()) {
option.cliOption().ifPresent(cliOption -> {
var property = System.getProperty(cliOption.systemProperty());
if (property != null) {
options.put(option, cliOption.optionType().normalize(property));
}
});
}

return options;
}

private void validateDifferentPaths(StoredValue baseline, StoredValue actual) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.github.goatfryed.assert_baseline.core;

import org.jetbrains.annotations.NotNull;

import java.util.function.Function;

public class CliOption {
@NotNull
private final String systemProperty;
@NotNull
private final OptionType optionType;

CliOption(@NotNull String systemProperty, @NotNull OptionType optionType) {
this.systemProperty = systemProperty;
this.optionType = optionType;
}

public String systemProperty() {
return systemProperty;
}

OptionType optionType() {
return optionType;
}

enum OptionType {
Boolean(sVal -> {
if (sVal == null) return false;
if (sVal.equalsIgnoreCase("false")) return true;
return true;
});

private final Function<String, Object> normalizer;

OptionType(Function<String,Object> normalizer) {
this.normalizer = normalizer;
}

public Object normalize(String property) {
return normalizer.apply(property);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.github.goatfryed.assert_baseline.core;

import org.jetbrains.annotations.Nullable;

import java.util.Optional;

public interface Options {
enum StandardOptions implements Options {
FORCE_BASELINE_UPDATE(
new CliOption(
"io.github.goatfryed.assert_baseline.forceBaselineUpdate",
CliOption.OptionType.Boolean
)
);

@Nullable
private final CliOption cliOption;

StandardOptions(@Nullable CliOption cliOption) {
this.cliOption = cliOption;
}

public Optional<CliOption> cliOption() {
return Optional.ofNullable(cliOption);
}
}
}
Loading

0 comments on commit 461955e

Please sign in to comment.