Merge branch 'release/1.6.2'
1
.github/workflows/release.yml
vendored
@@ -145,6 +145,7 @@ jobs:
|
||||
jpackageoptions: >
|
||||
--app-version "${{ needs.metadata.outputs.semVerNum }}"
|
||||
--java-options "-Dfile.encoding=\"utf-8\""
|
||||
--java-options "-Dapple.awt.enableTemplateImages=true"
|
||||
--java-options "-Dcryptomator.logDir=\"~/Library/Logs/Cryptomator\""
|
||||
--java-options "-Dcryptomator.pluginDir=\"~/Library/Application Support/Cryptomator/Plugins\""
|
||||
--java-options "-Dcryptomator.settingsPath=\"~/Library/Application Support/Cryptomator/settings.json\""
|
||||
|
||||
2
.idea/runConfigurations/Cryptomator_macOS.xml
generated
@@ -5,7 +5,7 @@
|
||||
</envs>
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<module name="cryptomator" />
|
||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.ipcSocketPath="~/Library/Application Support/Cryptomator/ipc.socket" -Dcryptomator.logDir="~/Library/Logs/Cryptomator" -Dcryptomator.pluginDir="~/Library/Application Support/Cryptomator/Plugins" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
|
||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator/settings.json" -Dcryptomator.ipcSocketPath="~/Library/Application Support/Cryptomator/ipc.socket" -Dcryptomator.logDir="~/Library/Logs/Cryptomator" -Dcryptomator.pluginDir="~/Library/Application Support/Cryptomator/Plugins" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
</envs>
|
||||
<option name="MAIN_CLASS_NAME" value="org.cryptomator.launcher.Cryptomator" />
|
||||
<module name="cryptomator" />
|
||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator-Dev/settings.json" -Dcryptomator.ipcSocketPath="~/Library/Application Support/Cryptomator-Dev/ipc.socket" -Dcryptomator.logDir="~/Library/Logs/Cryptomator-Dev" -Dcryptomator.pluginDir="~/Library/Application Support/Cryptomator-Dev/Plugins" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
|
||||
<option name="VM_PARAMETERS" value="-Duser.language=en -Dapple.awt.enableTemplateImages=true -Dcryptomator.settingsPath="~/Library/Application Support/Cryptomator-Dev/settings.json" -Dcryptomator.ipcSocketPath="~/Library/Application Support/Cryptomator-Dev/ipc.socket" -Dcryptomator.logDir="~/Library/Logs/Cryptomator-Dev" -Dcryptomator.pluginDir="~/Library/Application Support/Cryptomator-Dev/Plugins" -Dcryptomator.showTrayIcon=true -Xss2m -Xmx512m -ea" />
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
|
||||
1
dist/linux/appimage/build.sh
vendored
@@ -41,6 +41,7 @@ ${JAVA_HOME}/bin/jpackage \
|
||||
--app-version "${VERSION}.${REVISION_NO}" \
|
||||
--java-options "-Dfile.encoding=\"utf-8\"" \
|
||||
--java-options "-Dcryptomator.logDir=\"~/.local/share/Cryptomator/logs\"" \
|
||||
--java-options "-Dcryptomator.pluginDir=\"~/.local/share/Cryptomator/plugins\"" \
|
||||
--java-options "-Dcryptomator.settingsPath=\"~/.config/Cryptomator/settings.json:~/.Cryptomator/settings.json\"" \
|
||||
--java-options "-Dcryptomator.ipcSocketPath=\"~/.config/Cryptomator/ipc.socket\"" \
|
||||
--java-options "-Dcryptomator.mountPointsDir=\"~/.local/share/Cryptomator/mnt\"" \
|
||||
|
||||
7
dist/win/resources/main.wxs
vendored
@@ -75,6 +75,13 @@
|
||||
<Condition Message="A lower version of [ProductName] is already installed. Uninstall it first and then start the setup again. Setup will now exit.">
|
||||
<![CDATA[Installed OR NOT OLDEXEINSTALLER]]>
|
||||
</Condition>
|
||||
<!-- Cryptomator uses UNIX Sockets, which are supported starting with Windows 10 v1803-->
|
||||
<Property Id="WINDOWSBUILDNUMBER" Secure="yes">
|
||||
<RegistrySearch Id="BuildNumberSearch" Root="HKLM" Key="SOFTWARE\Microsoft\Windows NT\CurrentVersion" Name="CurrentBuildNumber" Type="raw" />
|
||||
</Property>
|
||||
<Condition Message="This application requires Windows 10 version 1803 (build 17134) or newer.">
|
||||
<![CDATA[Installed OR (WINDOWSBUILDNUMBER >= 17134)]]>
|
||||
</Condition>
|
||||
|
||||
<!-- Non-Opening ProgID -->
|
||||
<DirectoryRef Id="INSTALLDIR">
|
||||
|
||||
4
pom.xml
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.cryptomator</groupId>
|
||||
<artifactId>cryptomator</artifactId>
|
||||
<version>1.6.1</version>
|
||||
<version>1.6.2</version>
|
||||
<name>Cryptomator Desktop App</name>
|
||||
|
||||
<organization>
|
||||
@@ -31,7 +31,7 @@
|
||||
<cryptomator.integrations.version>1.0.0</cryptomator.integrations.version>
|
||||
<cryptomator.integrations.win.version>1.0.0</cryptomator.integrations.win.version>
|
||||
<cryptomator.integrations.mac.version>1.0.0</cryptomator.integrations.mac.version>
|
||||
<cryptomator.integrations.linux.version>1.0.0</cryptomator.integrations.linux.version>
|
||||
<cryptomator.integrations.linux.version>1.0.1</cryptomator.integrations.linux.version>
|
||||
<cryptomator.fuse.version>1.3.3</cryptomator.fuse.version>
|
||||
<cryptomator.dokany.version>1.3.3</cryptomator.dokany.version>
|
||||
<cryptomator.webdav.version>1.2.6</cryptomator.webdav.version>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.base.Throwables;
|
||||
|
||||
@@ -80,7 +81,7 @@ public class ErrorCode {
|
||||
if (causalChain.size() > 1) {
|
||||
var rootCause = causalChain.get(causalChain.size() - 1);
|
||||
var parentOfRootCause = causalChain.get(causalChain.size() - 2);
|
||||
var rootSpecificFrames = nonOverlappingFrames(parentOfRootCause.getStackTrace(), rootCause.getStackTrace());
|
||||
var rootSpecificFrames = countTopmostFrames(rootCause.getStackTrace(), parentOfRootCause.getStackTrace());
|
||||
return new ErrorCode(throwable, rootCause, rootSpecificFrames);
|
||||
} else {
|
||||
return new ErrorCode(throwable, throwable, ALL_FRAMES);
|
||||
@@ -107,11 +108,31 @@ public class ErrorCode {
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int nonOverlappingFrames(StackTraceElement[] frames, StackTraceElement[] enclosingFrames) {
|
||||
// Compute the number of elements in `frames` not contained in `enclosingFrames` by iterating backwards
|
||||
// Result should usually be equal to the difference in size of both traces
|
||||
var i = reverseStream(enclosingFrames).iterator();
|
||||
return (int) reverseStream(frames).dropWhile(f -> i.hasNext() && i.next().equals(f)).count();
|
||||
/**
|
||||
* Counts the number of <em>additional</em> frames contained in <code>allFrames</code> but not in <code>bottomFrames</code>.
|
||||
* <p>
|
||||
* If <code>allFrames</code> does not end with <code>bottomFrames</code>, it is considered distinct and all its frames are counted.
|
||||
*
|
||||
* @param allFrames Some stack frames
|
||||
* @param bottomFrames Other stack frames, potentially forming the bottom of the stack of <code>allFrames</code>
|
||||
* @return The number of additional frames in <code>allFrames</code>. In most cases this should be equal to the difference in size.
|
||||
*/
|
||||
// visible for testing
|
||||
static int countTopmostFrames(StackTraceElement[] allFrames, StackTraceElement[] bottomFrames) {
|
||||
if (allFrames.length < bottomFrames.length) {
|
||||
// if frames had been stacked on top of bottomFrames, allFrames would be larger
|
||||
return allFrames.length;
|
||||
} else {
|
||||
return allFrames.length - commonSuffixLength(allFrames, bottomFrames);
|
||||
}
|
||||
}
|
||||
|
||||
// visible for testing
|
||||
static <T> int commonSuffixLength(T[] set, T[] subset) {
|
||||
Preconditions.checkArgument(set.length >= subset.length);
|
||||
// iterate items backwards as long as they are identical
|
||||
var iterator = reverseStream(subset).iterator();
|
||||
return (int) reverseStream(set).takeWhile(item -> iterator.hasNext() && iterator.next().equals(item)).count();
|
||||
}
|
||||
|
||||
private static <T> Stream<T> reverseStream(T[] array) {
|
||||
|
||||
@@ -44,7 +44,9 @@ public interface IpcCommunicator extends Closeable {
|
||||
}
|
||||
// Didn't get any connection yet? I.e. we're the first app instance, so let's launch a server:
|
||||
try {
|
||||
return Server.create(socketPaths.iterator().next());
|
||||
final var socketPath = socketPaths.iterator().next();
|
||||
Files.deleteIfExists(socketPath); // ensure path does not exist before creating it
|
||||
return Server.create(socketPath);
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Failed to create IPC server", e);
|
||||
return new LoopbackCommunicator();
|
||||
|
||||
@@ -56,11 +56,10 @@ public class ChooseExistingVaultController implements FxController {
|
||||
|
||||
@FXML
|
||||
public void initialize() {
|
||||
final String resource = SystemUtils.IS_OS_MAC ? "/img/select-masterkey-mac.png" : "/img/select-masterkey-win.png";
|
||||
try (InputStream in = getClass().getResourceAsStream(resource)) {
|
||||
this.screenshot = new Image(in);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
if (SystemUtils.IS_OS_MAC) {
|
||||
this.screenshot = new Image(getClass().getResource("/img/select-masterkey-mac.png").toString());
|
||||
} else {
|
||||
this.screenshot = new Image(getClass().getResource("/img/select-masterkey-win.png").toString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +72,7 @@ public class ChooseExistingVaultController implements FxController {
|
||||
public void chooseFileAndNext() {
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle(resourceBundle.getString("addvaultwizard.existing.filePickerTitle"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Masterkey", "*.cryptomator"));
|
||||
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Cryptomator Vault", "*.cryptomator"));
|
||||
File masterkeyFile = fileChooser.showOpenDialog(window);
|
||||
if (masterkeyFile != null) {
|
||||
vaultPath.setValue(masterkeyFile.toPath().toAbsolutePath().getParent());
|
||||
|
||||
@@ -44,8 +44,10 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Comparator;
|
||||
import java.util.ResourceBundle;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.US_ASCII;
|
||||
import static org.cryptomator.common.Constants.MASTERKEY_FILENAME;
|
||||
@@ -195,12 +197,28 @@ public class CreateNewVaultPasswordController implements FxController {
|
||||
} catch (CryptoException e) {
|
||||
throw new IOException("Failed initialize vault.", e);
|
||||
}
|
||||
} finally {
|
||||
AtomicBoolean cleanupFailed = new AtomicBoolean(false);
|
||||
Files.walk(path)
|
||||
.sorted(Comparator.reverseOrder())
|
||||
.forEach(p -> {
|
||||
try {
|
||||
Files.deleteIfExists(p);
|
||||
} catch (IOException e) {
|
||||
cleanupFailed.set(false);
|
||||
}
|
||||
});
|
||||
if(cleanupFailed.get()) {
|
||||
LOG.warn("Failed to cleanup after failed vault creation at {}. Leftovers need to be deleted manually.", path);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. write vault-external readme file:
|
||||
String storagePathReadmeFileName = resourceBundle.getString("addvault.new.readme.storageLocation.fileName");
|
||||
try (WritableByteChannel ch = Files.newByteChannel(path.resolve(storagePathReadmeFileName), StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
ch.write(US_ASCII.encode(readmeGenerator.createVaultStorageLocationReadmeRtf()));
|
||||
} catch (IOException e) {
|
||||
LOG.warn("Unable to create vault storage location readme.", e);
|
||||
}
|
||||
|
||||
LOG.info("Created vault at {}", path);
|
||||
|
||||
@@ -2,9 +2,6 @@ package org.cryptomator.ui.traymenu;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import org.apache.commons.lang3.SystemUtils;
|
||||
import org.cryptomator.integrations.uiappearance.Theme;
|
||||
import org.cryptomator.integrations.uiappearance.UiAppearanceException;
|
||||
import org.cryptomator.integrations.uiappearance.UiAppearanceProvider;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -12,38 +9,25 @@ import javax.inject.Inject;
|
||||
import java.awt.AWTException;
|
||||
import java.awt.SystemTray;
|
||||
import java.awt.TrayIcon;
|
||||
import java.util.Optional;
|
||||
|
||||
@TrayMenuScoped
|
||||
public class TrayIconController {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(TrayIconController.class);
|
||||
|
||||
private final TrayImageFactory imageFactory;
|
||||
private final Optional<UiAppearanceProvider> appearanceProvider;
|
||||
private final TrayMenuController trayMenuController;
|
||||
private final TrayIcon trayIcon;
|
||||
private volatile boolean initialized;
|
||||
|
||||
@Inject
|
||||
TrayIconController(TrayImageFactory imageFactory, TrayMenuController trayMenuController, Optional<UiAppearanceProvider> appearanceProvider) {
|
||||
TrayIconController(TrayImageFactory imageFactory, TrayMenuController trayMenuController) {
|
||||
this.trayMenuController = trayMenuController;
|
||||
this.imageFactory = imageFactory;
|
||||
this.appearanceProvider = appearanceProvider;
|
||||
this.trayIcon = new TrayIcon(imageFactory.loadImage(), "Cryptomator", trayMenuController.getMenu());
|
||||
}
|
||||
|
||||
public synchronized void initializeTrayIcon() throws IllegalStateException {
|
||||
Preconditions.checkState(!initialized);
|
||||
|
||||
appearanceProvider.ifPresent(appearanceProvider -> {
|
||||
try {
|
||||
appearanceProvider.addListener(this::systemInterfaceThemeChanged);
|
||||
} catch (UiAppearanceException e) {
|
||||
LOG.error("Failed to enable automatic tray icon theme switching.");
|
||||
}
|
||||
});
|
||||
|
||||
trayIcon.setImageAutoSize(true);
|
||||
if (SystemUtils.IS_OS_WINDOWS) {
|
||||
trayIcon.addActionListener(trayMenuController::showMainWindow);
|
||||
@@ -61,10 +45,6 @@ public class TrayIconController {
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
private void systemInterfaceThemeChanged(Theme theme) {
|
||||
trayIcon.setImage(imageFactory.loadImage()); // TODO refactor "theme" is re-queried in loadImage()
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
@@ -25,11 +25,7 @@ class TrayImageFactory {
|
||||
}
|
||||
|
||||
private String getMacResourceName() {
|
||||
var theme = appearanceProvider.map(UiAppearanceProvider::getSystemTheme).orElse(Theme.LIGHT);
|
||||
return switch (theme) {
|
||||
case DARK -> "/img/tray_icon_mac_white.png";
|
||||
case LIGHT -> "/img/tray_icon_mac_black.png";
|
||||
};
|
||||
return "/img/tray_icon_mac.png";
|
||||
}
|
||||
|
||||
private String getWinOrLinuxResourceName() {
|
||||
|
||||
@@ -76,9 +76,9 @@ addvault.new.readme.accessLocation.2=This is your vault's access location.
|
||||
addvault.new.readme.accessLocation.3=Any files added to this volume will be encrypted by Cryptomator. You can work on it like on any other drive/folder. This is only a decrypted view of its content, your files stay encrypted on your hard drive all the time.
|
||||
addvault.new.readme.accessLocation.4=Feel free to remove this file.
|
||||
## Existing
|
||||
addvaultwizard.existing.instruction=Choose the "masterkey.cryptomator" file of your existing vault.
|
||||
addvaultwizard.existing.instruction=Choose the "vault.cryptomator" file of your existing vault. If only a file named "masterkey.cryptomator" exists, select that instead.
|
||||
addvaultwizard.existing.chooseBtn=Choose…
|
||||
addvaultwizard.existing.filePickerTitle=Select Masterkey File
|
||||
addvaultwizard.existing.filePickerTitle=Select Vault File
|
||||
## Success
|
||||
addvaultwizard.success.nextStepsInstructions=Added vault "%s".\nYou need to unlock this vault to access or add contents. Alternatively you can unlock it at any later point in time.
|
||||
addvaultwizard.success.unlockNow=Unlock Now
|
||||
|
||||
BIN
src/main/resources/img/select-masterkey-mac-dark.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
src/main/resources/img/select-masterkey-mac-dark@2x.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 35 KiB |
BIN
src/main/resources/img/select-masterkey-mac@2x.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 37 KiB |
BIN
src/main/resources/img/select-masterkey-win@2x.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
src/main/resources/img/tray_icon_mac.png
Executable file
|
After Width: | Height: | Size: 376 B |
BIN
src/main/resources/img/tray_icon_mac@2x.png
Executable file
|
After Width: | Height: | Size: 766 B |
|
Before Width: | Height: | Size: 359 B |
|
Before Width: | Height: | Size: 733 B |
|
Before Width: | Height: | Size: 367 B |
|
Before Width: | Height: | Size: 749 B |
@@ -1,59 +1,119 @@
|
||||
package org.cryptomator.common;
|
||||
|
||||
import com.google.common.base.CharMatcher;
|
||||
import com.google.common.base.Splitter;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.converter.ConvertWith;
|
||||
import org.junit.jupiter.params.converter.SimpleArgumentConverter;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
public class ErrorCodeTest {
|
||||
|
||||
private static ErrorCode codeCaughtFrom(RunnableThrowingException<RuntimeException> runnable) {
|
||||
try {
|
||||
runnable.run();
|
||||
throw new IllegalStateException("should not reach this point");
|
||||
} catch (RuntimeException e) {
|
||||
return ErrorCode.of(e);
|
||||
}
|
||||
}
|
||||
private final StackTraceElement foo = new StackTraceElement("ErrorCodeTest", "foo", null, 0);
|
||||
private final StackTraceElement bar = new StackTraceElement("ErrorCodeTest", "bar", null, 0);
|
||||
private final StackTraceElement baz = new StackTraceElement("ErrorCodeTest", "baz", null, 0);
|
||||
private final Exception fooException = Mockito.mock(NullPointerException.class, "fooException");
|
||||
|
||||
@Test
|
||||
@DisplayName("same exception leads to same error code")
|
||||
public void testDifferentErrorCodes() {
|
||||
var code1 = codeCaughtFrom(this::throwNpe);
|
||||
var code2 = codeCaughtFrom(this::throwNpe);
|
||||
public void testDeterministicErrorCode() {
|
||||
Mockito.doReturn(new StackTraceElement[]{foo, bar, baz}).when(fooException).getStackTrace();
|
||||
var code1 = ErrorCode.of(fooException);
|
||||
var code2 = ErrorCode.of(fooException);
|
||||
|
||||
Assertions.assertEquals(code1.toString(), code2.toString());
|
||||
}
|
||||
|
||||
private void throwNpe() {
|
||||
throwException(new NullPointerException());
|
||||
@Test
|
||||
@DisplayName("three error code segments change independently")
|
||||
public void testErrorCodeSegments() {
|
||||
Exception fooBarException = Mockito.mock(IndexOutOfBoundsException.class, "fooBarException");
|
||||
Mockito.doReturn(new StackTraceElement[]{foo, foo, foo}).when(fooBarException).getStackTrace();
|
||||
Mockito.doReturn(fooException).when(fooBarException).getCause();
|
||||
Mockito.doReturn(new StackTraceElement[]{bar, bar, bar, foo, foo, foo}).when(fooException).getStackTrace();
|
||||
|
||||
var code = ErrorCode.of(fooBarException);
|
||||
|
||||
Assertions.assertNotEquals(code.throwableCode(), code.rootCauseCode());
|
||||
Assertions.assertNotEquals(code.rootCauseCode(), code.methodCode());
|
||||
}
|
||||
|
||||
private void throwException(RuntimeException e) throws RuntimeException {
|
||||
throw e;
|
||||
@DisplayName("commonSuffixLength()")
|
||||
@ParameterizedTest
|
||||
@CsvSource({"1 2 3, 1 2 3, 3", "1 2 3, 0 2 3, 2", "1 2 3 4, 3 4, 2", "1 2 3 4, 5 6, 0", "1 2 3 4 5 6,, 0",})
|
||||
public void commonSuffixLength1(@ConvertWith(IntegerArrayConverter.class) Integer[] set, @ConvertWith(IntegerArrayConverter.class) Integer[] subset, int expected) {
|
||||
var result = ErrorCode.commonSuffixLength(set, subset);
|
||||
|
||||
Assertions.assertEquals(expected, result);
|
||||
}
|
||||
|
||||
@DisplayName("when different cause but same root cause")
|
||||
@DisplayName("commonSuffixLength() with too short array")
|
||||
@ParameterizedTest
|
||||
@CsvSource({"1 2, 3 4 5 6", ",1 2 3 4 5 6",})
|
||||
public void commonSuffixLength2(@ConvertWith(IntegerArrayConverter.class) Integer[] set, @ConvertWith(IntegerArrayConverter.class) Integer[] subset) {
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> {
|
||||
ErrorCode.commonSuffixLength(set, subset);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("countTopmostFrames() with partially overlapping suffix")
|
||||
public void testCountTopmostFrames1() {
|
||||
var allFrames = new StackTraceElement[]{foo, bar, baz, bar, foo};
|
||||
var bottomFrames = new StackTraceElement[]{baz, bar, foo};
|
||||
|
||||
int result = ErrorCode.countTopmostFrames(allFrames, bottomFrames);
|
||||
|
||||
Assertions.assertEquals(2, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("countTopmostFrames() without overlapping suffix")
|
||||
public void testCountTopmostFrames2() {
|
||||
var allFrames = new StackTraceElement[]{foo, foo, foo};
|
||||
var bottomFrames = new StackTraceElement[]{bar, bar, bar};
|
||||
|
||||
int result = ErrorCode.countTopmostFrames(allFrames, bottomFrames);
|
||||
|
||||
Assertions.assertEquals(3, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("countUniqueFrames() fully overlapping")
|
||||
public void testCountUniqueFrames3() {
|
||||
var allFrames = new StackTraceElement[]{foo, bar, baz};
|
||||
var bottomFrames = new StackTraceElement[]{foo, bar, baz};
|
||||
|
||||
int result = ErrorCode.countTopmostFrames(allFrames, bottomFrames);
|
||||
|
||||
Assertions.assertEquals(0, result);
|
||||
}
|
||||
|
||||
@DisplayName("when different exception with same root cause")
|
||||
@Nested
|
||||
public class SameRootCauseDifferentCause {
|
||||
public class DifferentExceptionWithSameRootCause {
|
||||
|
||||
private final ErrorCode code1 = codeCaughtFrom(this::foo);
|
||||
private final ErrorCode code2 = codeCaughtFrom(this::bar);
|
||||
private final Exception fooBarException = Mockito.mock(IllegalArgumentException.class, "fooBarException");
|
||||
private final Exception fooBazException = Mockito.mock(IndexOutOfBoundsException.class, "fooBazException");
|
||||
|
||||
private void foo() throws IllegalArgumentException {
|
||||
try {
|
||||
throwNpe();
|
||||
} catch (NullPointerException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
private ErrorCode code1;
|
||||
private ErrorCode code2;
|
||||
|
||||
private void bar() throws IllegalStateException {
|
||||
try {
|
||||
throwNpe();
|
||||
} catch (NullPointerException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
Mockito.doReturn(new StackTraceElement[]{baz, bar, foo}).when(fooException).getStackTrace();
|
||||
Mockito.doReturn(new StackTraceElement[]{foo}).when(fooBarException).getStackTrace();
|
||||
Mockito.doReturn(new StackTraceElement[]{foo}).when(fooBazException).getStackTrace();
|
||||
Mockito.doReturn(fooException).when(fooBarException).getCause();
|
||||
Mockito.doReturn(fooException).when(fooBazException).getCause();
|
||||
this.code1 = ErrorCode.of(fooBarException);
|
||||
this.code2 = ErrorCode.of(fooBazException);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -82,23 +142,21 @@ public class ErrorCodeTest {
|
||||
|
||||
}
|
||||
|
||||
@DisplayName("when same cause but different call stack")
|
||||
@DisplayName("when same exception with different call stacks")
|
||||
@Nested
|
||||
public class SameCauseDifferentCallStack {
|
||||
public class SameExceptionDifferentCallStack {
|
||||
|
||||
private final ErrorCode code1 = codeCaughtFrom(this::foo);
|
||||
private final ErrorCode code2 = codeCaughtFrom(this::bar);
|
||||
private final Exception barException = Mockito.mock(NullPointerException.class, "barException");
|
||||
|
||||
private void foo() throws NullPointerException {
|
||||
try {
|
||||
throwNpe();
|
||||
} catch (NullPointerException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
private ErrorCode code1;
|
||||
private ErrorCode code2;
|
||||
|
||||
private void bar() throws NullPointerException {
|
||||
foo();
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
Mockito.doReturn(new StackTraceElement[]{foo, bar, baz}).when(fooException).getStackTrace();
|
||||
Mockito.doReturn(new StackTraceElement[]{foo, baz, bar}).when(barException).getStackTrace();
|
||||
this.code1 = ErrorCode.of(fooException);
|
||||
this.code2 = ErrorCode.of(barException);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -114,9 +172,9 @@ public class ErrorCodeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("rootCauseCodes are equal")
|
||||
@DisplayName("rootCauseCodes are different")
|
||||
public void testSameRootCauseCodes() {
|
||||
Assertions.assertEquals(code1.rootCauseCode(), code2.rootCauseCode());
|
||||
Assertions.assertNotEquals(code1.rootCauseCode(), code2.rootCauseCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -127,4 +185,19 @@ public class ErrorCodeTest {
|
||||
|
||||
}
|
||||
|
||||
public static class IntegerArrayConverter extends SimpleArgumentConverter {
|
||||
|
||||
@Override
|
||||
protected Integer[] convert(Object source, Class<?> targetType) {
|
||||
if (source == null) {
|
||||
return new Integer[0];
|
||||
} else if (source instanceof String s && Integer[].class.isAssignableFrom(targetType)) {
|
||||
return Splitter.on(CharMatcher.inRange('0', '9').negate()).splitToStream(s).map(Integer::valueOf).toArray(Integer[]::new);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Conversion from " + source.getClass() + " to " + targetType + " not supported.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||