Injects ProcessRunner to simplify testing, tests conda

This commit is contained in:
Guillaume Tâche
2024-08-27 20:50:10 +02:00
parent bf68d0a206
commit ae76707def
24 changed files with 810 additions and 110 deletions

View File

@@ -1,6 +1,7 @@
package com.github.gtache.autosubtitle.setup.conda;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.setup.SetupException;
import com.github.gtache.autosubtitle.setup.SetupStatus;
import com.github.gtache.autosubtitle.setup.impl.AbstractSetupManager;
@@ -31,8 +32,8 @@ public class CondaSetupManager extends AbstractSetupManager {
private final CondaSetupConfiguration configuration;
@Inject
CondaSetupManager(final CondaSetupConfiguration configuration, final HttpClient httpClient) {
super(httpClient);
CondaSetupManager(final CondaSetupConfiguration configuration, final ProcessRunner processRunner, final HttpClient httpClient) {
super(processRunner, httpClient);
this.configuration = requireNonNull(configuration);
}
@@ -71,31 +72,28 @@ public class CondaSetupManager extends AbstractSetupManager {
switch (configuration.os()) {
case OS.WINDOWS -> installWindows();
case OS.MAC, OS.LINUX -> installLinux();
default -> throw new SetupException("Unsupported OS: " + configuration.os());
}
}
private void installLinux() throws SetupException {
try {
final var installerPath = configuration.condaInstallerPath();
final var rootPath = configuration.condaRootPath();
logger.info("Installing conda using {}", installerPath);
final var result = run(List.of("bash", installerPath.toString(), "-b", "-p", rootPath.toString()), Duration.ofMinutes(15));
if (result.exitCode() == 0) {
logger.info("Installed conda to {}", rootPath);
} else {
throw new SetupException("Error installing conda: " + result);
}
} catch (final IOException e) {
throw new SetupException(e);
}
installArgs(List.of("bash", installerPath.toString(), "-b", "-p", rootPath.toString()));
}
private void installWindows() throws SetupException {
final var installerPath = configuration.condaInstallerPath();
final var rootPath = configuration.condaRootPath();
installArgs(List.of(installerPath.toString(), "/InstallationType=JustMe", "/RegisterPython=0", "/S", "/D=" + rootPath));
}
private void installArgs(final List<String> args) throws SetupException {
try {
final var installerPath = configuration.condaInstallerPath();
final var rootPath = configuration.condaRootPath();
logger.info("Installing conda using {}", installerPath);
final var result = run(List.of(installerPath.toString(), "/InstallationType=JustMe", "/RegisterPython=0", "/S", "/D=" + rootPath.toString()), Duration.ofMinutes(15));
final var result = processRunner().run(args, Duration.ofMinutes(15));
if (result.exitCode() == 0) {
logger.info("Installed conda to {}", rootPath);
} else {
@@ -111,6 +109,7 @@ public class CondaSetupManager extends AbstractSetupManager {
case OS.WINDOWS -> downloadCondaWindows();
case OS.MAC -> downloadCondaMac();
case OS.LINUX -> downloadCondaLinux();
default -> throw new SetupException("Unsupported OS: " + configuration.os());
}
logger.info("Downloaded conda to {}", configuration.condaInstallerPath());
}
@@ -154,7 +153,7 @@ public class CondaSetupManager extends AbstractSetupManager {
@Override
public void update() throws SetupException {
try {
final var result = run(List.of(getCondaPath().toString(), "update", "-y", "conda"), Duration.ofMinutes(15));
final var result = processRunner().run(List.of(getCondaPath().toString(), "update", "-y", "conda"), Duration.ofMinutes(15));
if (result.exitCode() == 0) {
logger.info("Conda updated");
} else {
@@ -169,7 +168,7 @@ public class CondaSetupManager extends AbstractSetupManager {
final var args = Stream.concat(Stream.of(getCondaPath().toString(), "create", "-y", "-p", path.toString(), "python=" + pythonVersion), Arrays.stream(packages)).toList();
try {
logger.info("Creating venv {}", path);
final var result = run(args, Duration.ofMinutes(15));
final var result = processRunner().run(args, Duration.ofMinutes(15));
if (result.exitCode() == 0) {
logger.info("Created venv {}", path);
} else {
@@ -181,6 +180,7 @@ public class CondaSetupManager extends AbstractSetupManager {
try (final var files = Files.find(sourceFolder, 1, (p, a) -> p.getFileName().toString().contains("libcrypto") || p.getFileName().toString().contains("libssl"))) {
final var fileList = files.toList();
final var targetFolder = path.resolve("DLLs");
Files.createDirectories(targetFolder);
for (final var s : fileList) {
Files.copy(s, targetFolder.resolve(s.getFileName()));
}
@@ -202,7 +202,7 @@ public class CondaSetupManager extends AbstractSetupManager {
private boolean isSystemCondaInstalled() throws SetupException {
try {
final var result = run(List.of(configuration.condaSystemPath().toString(), "--version"), Duration.ofSeconds(5));
final var result = processRunner().run(List.of(configuration.condaSystemPath().toString(), "--version"), Duration.ofSeconds(5));
if (result.exitCode() == 0) {
final var output = result.output().getFirst();
final var versionString = output.substring(output.indexOf(' ') + 1);
@@ -212,7 +212,7 @@ public class CondaSetupManager extends AbstractSetupManager {
final var minor = Integer.parseInt(version[1]);
return major >= configuration.condaMinimumMajorVersion() && minor >= configuration.condaMinimumMinorVersion();
} else {
throw new SetupException("Unexpected python version: " + versionString);
throw new SetupException("Unexpected conda version: " + versionString);
}
} else {
return false;

View File

@@ -0,0 +1,71 @@
package com.github.gtache.autosubtitle.modules.setup.conda;
import com.github.gtache.autosubtitle.impl.Architecture;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.setup.conda.CondaSetupConfiguration;
import org.junit.jupiter.api.Test;
import java.nio.file.Paths;
import static org.junit.jupiter.api.Assertions.assertEquals;
class TestCondaSetupModule {
@Test
void testCondaSetupConfiguration() {
final var rootPath = Paths.get("root");
final var systemPath = Paths.get("system");
final var bundledPath = Paths.get("bundled");
final var condaMinimumMajorVersion = 24;
final var condaMinimumMinorVersion = 5;
final var condaInstallerPath = Paths.get("installer");
final var os = OS.WINDOWS;
final var architecture = Architecture.X86_64;
final var expected = new CondaSetupConfiguration(rootPath, systemPath, bundledPath, condaMinimumMajorVersion, condaMinimumMinorVersion, condaInstallerPath, os, architecture);
assertEquals(expected, CondaSetupModule.providesCondaSetupConfiguration(systemPath, bundledPath, condaMinimumMajorVersion, condaMinimumMinorVersion, condaInstallerPath, rootPath, os, architecture));
}
@Test
void testProvidesCondaSystemPath() {
assertEquals(Paths.get("conda.bat"), CondaSetupModule.providesCondaSystemPath(OS.WINDOWS));
assertEquals(Paths.get("conda"), CondaSetupModule.providesCondaSystemPath(OS.LINUX));
assertEquals(Paths.get("conda"), CondaSetupModule.providesCondaSystemPath(OS.MAC));
}
@Test
void testCondaBundledPath() {
final var root = Paths.get("root");
final var expectedWindows = root.resolve("condabin").resolve(Paths.get("conda.bat"));
final var expectedOther = root.resolve("condabin").resolve(Paths.get("conda"));
assertEquals(expectedWindows, CondaSetupModule.providesCondaBundledPath(root, OS.WINDOWS));
assertEquals(expectedOther, CondaSetupModule.providesCondaBundledPath(root, OS.MAC));
assertEquals(expectedOther, CondaSetupModule.providesCondaBundledPath(root, OS.LINUX));
}
@Test
void testCondaMinimumMajorVersion() {
assertEquals(24, CondaSetupModule.providesCondaMinimumMajorVersion());
}
@Test
void testCondaMinimumMinorVersion() {
assertEquals(5, CondaSetupModule.providesCondaMinimumMinorVersion());
}
@Test
void testCondaRootPath() {
final var root = Paths.get("root");
final var expected = root.resolve("miniconda3");
assertEquals(expected, CondaSetupModule.providesCondaRootPath(root));
}
@Test
void testCondaInstallerPath() {
final var root = Paths.get("root");
final var expectedWindows = root.resolve("conda-install.exe");
final var expectedOther = root.resolve("conda-install.sh");
assertEquals(expectedWindows, CondaSetupModule.providesCondaInstallerPath(root, OS.WINDOWS));
assertEquals(expectedOther, CondaSetupModule.providesCondaInstallerPath(root, OS.MAC));
assertEquals(expectedOther, CondaSetupModule.providesCondaInstallerPath(root, OS.LINUX));
}
}

View File

@@ -0,0 +1,65 @@
package com.github.gtache.autosubtitle.setup.conda;
import com.github.gtache.autosubtitle.impl.Architecture;
import com.github.gtache.autosubtitle.impl.OS;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.nio.file.Path;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ExtendWith(MockitoExtension.class)
class TestCondaSetupConfiguration {
private final Path condaRootPath;
private final Path condaSystemPath;
private final Path condaBundledPath;
private final int condaMinimumMajorVersion;
private final int condaMinimumMinorVersion;
private final Path condaInstallerPath;
private final OS os;
private final Architecture architecture;
TestCondaSetupConfiguration(@Mock final Path condaRootPath, @Mock final Path condaSystemPath,
@Mock final Path condaBundledPath, @Mock final Path condaInstallerPath,
@Mock final OS os, @Mock final Architecture architecture) {
this.condaRootPath = Objects.requireNonNull(condaRootPath);
this.condaSystemPath = Objects.requireNonNull(condaSystemPath);
this.condaBundledPath = Objects.requireNonNull(condaBundledPath);
this.condaMinimumMajorVersion = 3;
this.condaMinimumMinorVersion = 4;
this.condaInstallerPath = Objects.requireNonNull(condaInstallerPath);
this.os = Objects.requireNonNull(os);
this.architecture = Objects.requireNonNull(architecture);
}
@Test
void testCondaSetupConfiguration() {
final var configuration = new CondaSetupConfiguration(condaRootPath, condaSystemPath, condaBundledPath, condaMinimumMajorVersion, condaMinimumMinorVersion, condaInstallerPath, os, architecture);
assertEquals(condaRootPath, configuration.condaRootPath());
assertEquals(condaSystemPath, configuration.condaSystemPath());
assertEquals(condaBundledPath, configuration.condaBundledPath());
assertEquals(condaMinimumMajorVersion, configuration.condaMinimumMajorVersion());
assertEquals(condaMinimumMinorVersion, configuration.condaMinimumMinorVersion());
assertEquals(condaInstallerPath, configuration.condaInstallerPath());
assertEquals(os, configuration.os());
assertEquals(architecture, configuration.architecture());
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new CondaSetupConfiguration(null, condaSystemPath, condaBundledPath, condaMinimumMajorVersion, condaMinimumMinorVersion, condaInstallerPath, os, architecture));
assertThrows(NullPointerException.class, () -> new CondaSetupConfiguration(condaRootPath, null, condaBundledPath, condaMinimumMajorVersion, condaMinimumMinorVersion, condaInstallerPath, os, architecture));
assertThrows(NullPointerException.class, () -> new CondaSetupConfiguration(condaRootPath, condaSystemPath, null, condaMinimumMajorVersion, condaMinimumMinorVersion, condaInstallerPath, os, architecture));
assertThrows(IllegalArgumentException.class, () -> new CondaSetupConfiguration(condaRootPath, condaSystemPath, condaBundledPath, 0, condaMinimumMinorVersion, condaInstallerPath, os, architecture));
assertThrows(IllegalArgumentException.class, () -> new CondaSetupConfiguration(condaRootPath, condaSystemPath, condaBundledPath, condaMinimumMajorVersion, -1, condaInstallerPath, os, architecture));
assertThrows(NullPointerException.class, () -> new CondaSetupConfiguration(condaRootPath, condaSystemPath, condaBundledPath, condaMinimumMajorVersion, condaMinimumMinorVersion, null, os, architecture));
assertThrows(NullPointerException.class, () -> new CondaSetupConfiguration(condaRootPath, condaSystemPath, condaBundledPath, condaMinimumMajorVersion, condaMinimumMinorVersion, condaInstallerPath, null, architecture));
assertThrows(NullPointerException.class, () -> new CondaSetupConfiguration(condaRootPath, condaSystemPath, condaBundledPath, condaMinimumMajorVersion, condaMinimumMinorVersion, condaInstallerPath, os, null));
}
}

View File

@@ -0,0 +1,513 @@
package com.github.gtache.autosubtitle.setup.conda;
import com.github.gtache.autosubtitle.impl.Architecture;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.process.ProcessResult;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.setup.SetupException;
import com.github.gtache.autosubtitle.setup.SetupStatus;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.util.List;
import static java.util.Objects.requireNonNull;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class TestCondaSetupManager {
private static final String PYTHON_VERSION = "3.3";
private static final String[] PACKAGES = {"p1", "p2"};
private final CondaSetupConfiguration configuration;
private final ProcessRunner processRunner;
private final HttpClient httpClient;
private final CondaSetupManager condaSetupManager;
private final ProcessResult systemProcessResult;
private final Path systemPath;
private final HttpResponse<Path> response;
TestCondaSetupManager(@Mock final CondaSetupConfiguration configuration, @Mock final ProcessRunner processRunner,
@Mock final HttpClient httpClient, @Mock final ProcessResult systemProcessResult, @Mock final HttpResponse<Path> response) throws IOException, InterruptedException {
this.configuration = requireNonNull(configuration);
this.processRunner = requireNonNull(processRunner);
this.httpClient = requireNonNull(httpClient);
this.condaSetupManager = new CondaSetupManager(configuration, processRunner, httpClient);
this.systemProcessResult = requireNonNull(systemProcessResult);
this.systemPath = Paths.get("system");
this.response = requireNonNull(response);
when(response.statusCode()).thenReturn(200);
when(configuration.condaSystemPath()).thenReturn(systemPath);
when(systemProcessResult.output()).thenReturn(List.of("conda 99.99.99"));
when(systemProcessResult.exitCode()).thenReturn(0);
when(processRunner.run(List.of(systemPath.toString(), "--version"), Duration.ofSeconds(5))).thenReturn(systemProcessResult);
when(httpClient.send(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(response);
}
@Test
void testName() {
assertEquals("conda", condaSetupManager.name());
}
@Test
void testGetStatusIsSystemInstalled() throws SetupException {
assertEquals(SetupStatus.SYSTEM_INSTALLED, condaSetupManager.getStatus());
}
@Test
void testGetStatusIsBundledInstalled(@TempDir final Path tempDir) throws SetupException, IOException {
final var path = tempDir.resolve("conda");
Files.createFile(path);
when(configuration.condaBundledPath()).thenReturn(path);
when(systemProcessResult.exitCode()).thenReturn(1);
assertEquals(SetupStatus.BUNDLE_INSTALLED, condaSetupManager.getStatus());
}
@Test
void testGetStatusNotInstalled() throws SetupException {
final var path = Paths.get("conda");
when(configuration.condaBundledPath()).thenReturn(path);
when(systemProcessResult.exitCode()).thenReturn(1);
assertEquals(SetupStatus.NOT_INSTALLED, condaSetupManager.getStatus());
}
@Test
void testInstallIsInstalledSystem() {
assertDoesNotThrow(condaSetupManager::install);
verifyNoInteractions(httpClient);
}
@Test
void testInstallIsInstalledBundled(@TempDir final Path tempDir) throws IOException {
final var path = tempDir.resolve("conda");
Files.createFile(path);
when(configuration.condaBundledPath()).thenReturn(path);
when(systemProcessResult.exitCode()).thenReturn(1);
assertDoesNotThrow(condaSetupManager::install);
verifyNoInteractions(httpClient);
}
@Test
void testInstallDownloadWindows(@TempDir final Path tempDir) throws IOException, InterruptedException {
final var installerPath = tempDir.resolve("conda");
when(configuration.condaInstallerPath()).thenReturn(installerPath);
when(configuration.condaBundledPath()).thenReturn(tempDir);
when(configuration.os()).thenReturn(OS.WINDOWS);
when(systemProcessResult.exitCode()).thenReturn(1);
when(configuration.condaRootPath()).thenReturn(tempDir);
final var args = List.of(installerPath.toString(), "/InstallationType=JustMe", "/RegisterPython=0", "/S", "/D=" + tempDir);
final var result = mock(ProcessResult.class);
when(result.exitCode()).thenReturn(0);
when(processRunner.run(args, Duration.ofMinutes(15))).thenReturn(result);
assertDoesNotThrow(condaSetupManager::install);
final var requestCapture = ArgumentCaptor.forClass(HttpRequest.class);
verify(httpClient).send(requestCapture.capture(), any(HttpResponse.BodyHandler.class));
final var request = requestCapture.getValue();
assertEquals("https://repo.anaconda.com/miniconda/Miniconda3-latest-Windows-x86_64.exe", request.uri().toString());
verify(processRunner).run(args, Duration.ofMinutes(15));
}
@Test
void testInstallDownloadLinuxAMD64(@TempDir final Path tempDir) throws IOException, InterruptedException {
final var installerPath = tempDir.resolve("conda");
when(configuration.condaInstallerPath()).thenReturn(installerPath);
when(configuration.condaBundledPath()).thenReturn(tempDir);
when(configuration.os()).thenReturn(OS.LINUX);
when(configuration.architecture()).thenReturn(Architecture.AMD64);
when(systemProcessResult.exitCode()).thenReturn(1);
when(configuration.condaRootPath()).thenReturn(tempDir);
final var args = List.of("bash", installerPath.toString(), "-b", "-p", tempDir.toString());
final var result = mock(ProcessResult.class);
when(result.exitCode()).thenReturn(0);
when(processRunner.run(args, Duration.ofMinutes(15))).thenReturn(result);
assertDoesNotThrow(condaSetupManager::install);
final var requestCapture = ArgumentCaptor.forClass(HttpRequest.class);
verify(httpClient).send(requestCapture.capture(), any(HttpResponse.BodyHandler.class));
final var request = requestCapture.getValue();
assertEquals("https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh", request.uri().toString());
verify(processRunner).run(args, Duration.ofMinutes(15));
}
@Test
void testInstallDownloadLinuxARM64(@TempDir final Path tempDir) throws IOException, InterruptedException {
final var installerPath = tempDir.resolve("conda");
when(configuration.condaInstallerPath()).thenReturn(installerPath);
when(configuration.condaBundledPath()).thenReturn(tempDir);
when(configuration.os()).thenReturn(OS.LINUX);
when(configuration.architecture()).thenReturn(Architecture.ARM64);
when(systemProcessResult.exitCode()).thenReturn(1);
when(configuration.condaRootPath()).thenReturn(tempDir);
final var args = List.of("bash", installerPath.toString(), "-b", "-p", tempDir.toString());
final var result = mock(ProcessResult.class);
when(result.exitCode()).thenReturn(0);
when(processRunner.run(args, Duration.ofMinutes(15))).thenReturn(result);
assertDoesNotThrow(condaSetupManager::install);
final var requestCapture = ArgumentCaptor.forClass(HttpRequest.class);
verify(httpClient).send(requestCapture.capture(), any(HttpResponse.BodyHandler.class));
final var request = requestCapture.getValue();
assertEquals("https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-aarch64.sh", request.uri().toString());
verify(processRunner).run(args, Duration.ofMinutes(15));
}
@Test
void testInstallDownloadLinuxUnsupported(@TempDir final Path tempDir) throws IOException {
final var installerPath = tempDir.resolve("conda");
when(configuration.condaInstallerPath()).thenReturn(installerPath);
when(configuration.condaBundledPath()).thenReturn(tempDir);
when(configuration.os()).thenReturn(OS.LINUX);
when(configuration.architecture()).thenReturn(Architecture.ARM32);
when(systemProcessResult.exitCode()).thenReturn(1);
when(configuration.condaRootPath()).thenReturn(tempDir);
assertThrows(SetupException.class, condaSetupManager::install);
verifyNoInteractions(httpClient);
}
@Test
void testInstallDownloadMacAMD64(@TempDir final Path tempDir) throws IOException, InterruptedException {
final var installerPath = tempDir.resolve("conda");
when(configuration.condaInstallerPath()).thenReturn(installerPath);
when(configuration.condaBundledPath()).thenReturn(tempDir);
when(configuration.os()).thenReturn(OS.MAC);
when(configuration.architecture()).thenReturn(Architecture.AMD64);
when(systemProcessResult.exitCode()).thenReturn(1);
when(configuration.condaRootPath()).thenReturn(tempDir);
final var args = List.of("bash", installerPath.toString(), "-b", "-p", tempDir.toString());
final var result = mock(ProcessResult.class);
when(result.exitCode()).thenReturn(0);
when(processRunner.run(args, Duration.ofMinutes(15))).thenReturn(result);
assertDoesNotThrow(condaSetupManager::install);
final var requestCapture = ArgumentCaptor.forClass(HttpRequest.class);
verify(httpClient).send(requestCapture.capture(), any(HttpResponse.BodyHandler.class));
final var request = requestCapture.getValue();
assertEquals("https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh", request.uri().toString());
verify(processRunner).run(args, Duration.ofMinutes(15));
}
@Test
void testInstallDownloadMacARM64(@TempDir final Path tempDir) throws IOException, InterruptedException {
final var installerPath = tempDir.resolve("conda");
when(configuration.condaInstallerPath()).thenReturn(installerPath);
when(configuration.condaBundledPath()).thenReturn(tempDir);
when(configuration.os()).thenReturn(OS.MAC);
when(configuration.architecture()).thenReturn(Architecture.ARM64);
when(systemProcessResult.exitCode()).thenReturn(1);
when(configuration.condaRootPath()).thenReturn(tempDir);
final var args = List.of("bash", installerPath.toString(), "-b", "-p", tempDir.toString());
final var result = mock(ProcessResult.class);
when(result.exitCode()).thenReturn(0);
when(processRunner.run(args, Duration.ofMinutes(15))).thenReturn(result);
assertDoesNotThrow(condaSetupManager::install);
final var requestCapture = ArgumentCaptor.forClass(HttpRequest.class);
verify(httpClient).send(requestCapture.capture(), any(HttpResponse.BodyHandler.class));
final var request = requestCapture.getValue();
assertEquals("https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh", request.uri().toString());
verify(processRunner).run(args, Duration.ofMinutes(15));
}
@Test
void testInstallDownloadMacUnsupported(@TempDir final Path tempDir) throws IOException {
final var installerPath = tempDir.resolve("conda");
when(configuration.condaInstallerPath()).thenReturn(installerPath);
when(configuration.condaBundledPath()).thenReturn(tempDir);
when(configuration.os()).thenReturn(OS.MAC);
when(configuration.architecture()).thenReturn(Architecture.ARM32);
when(systemProcessResult.exitCode()).thenReturn(1);
when(configuration.condaRootPath()).thenReturn(tempDir);
assertThrows(SetupException.class, condaSetupManager::install);
verifyNoInteractions(httpClient);
}
@Test
void testInstallDownloadUnsupported(@TempDir final Path tempDir) throws IOException {
final var installerPath = tempDir.resolve("conda");
when(configuration.condaInstallerPath()).thenReturn(installerPath);
when(configuration.condaBundledPath()).thenReturn(tempDir);
when(configuration.os()).thenReturn(OS.UNKNOWN);
when(systemProcessResult.exitCode()).thenReturn(1);
when(configuration.condaRootPath()).thenReturn(tempDir);
assertThrows(SetupException.class, condaSetupManager::install);
verifyNoInteractions(httpClient);
}
@Test
void testInstallInstallerWindows(@TempDir final Path tempDir) throws IOException {
final var installerPath = tempDir.resolve("conda");
Files.createFile(installerPath);
when(configuration.condaInstallerPath()).thenReturn(installerPath);
when(configuration.condaBundledPath()).thenReturn(tempDir);
when(configuration.os()).thenReturn(OS.WINDOWS);
when(systemProcessResult.exitCode()).thenReturn(1);
when(configuration.condaRootPath()).thenReturn(tempDir);
final var args = List.of(installerPath.toString(), "/InstallationType=JustMe", "/RegisterPython=0", "/S", "/D=" + tempDir.toString());
final var result = mock(ProcessResult.class);
when(result.exitCode()).thenReturn(0);
when(processRunner.run(args, Duration.ofMinutes(15))).thenReturn(result);
assertDoesNotThrow(condaSetupManager::install);
verify(processRunner).run(args, Duration.ofMinutes(15));
verifyNoInteractions(httpClient);
}
@Test
void testInstallInstallerLinux(@TempDir final Path tempDir) throws IOException {
final var installerPath = tempDir.resolve("conda");
Files.createFile(installerPath);
when(configuration.condaInstallerPath()).thenReturn(installerPath);
when(configuration.condaBundledPath()).thenReturn(tempDir);
when(configuration.os()).thenReturn(OS.LINUX);
when(systemProcessResult.exitCode()).thenReturn(1);
when(configuration.condaRootPath()).thenReturn(tempDir);
final var args = List.of("bash", installerPath.toString(), "-b", "-p", tempDir.toString());
final var result = mock(ProcessResult.class);
when(result.exitCode()).thenReturn(0);
when(processRunner.run(args, Duration.ofMinutes(15))).thenReturn(result);
assertDoesNotThrow(condaSetupManager::install);
verify(processRunner).run(args, Duration.ofMinutes(15));
verifyNoInteractions(httpClient);
}
@Test
void testInstallInstallerLinuxIOException(@TempDir final Path tempDir) throws IOException {
final var installerPath = tempDir.resolve("conda");
Files.createFile(installerPath);
when(configuration.condaInstallerPath()).thenReturn(installerPath);
when(configuration.condaBundledPath()).thenReturn(tempDir);
when(configuration.os()).thenReturn(OS.LINUX);
when(systemProcessResult.exitCode()).thenReturn(1);
when(configuration.condaRootPath()).thenReturn(tempDir);
final var args = List.of("bash", installerPath.toString(), "-b", "-p", tempDir.toString());
when(processRunner.run(args, Duration.ofMinutes(15))).thenThrow(IOException.class);
assertThrows(SetupException.class, condaSetupManager::install);
verify(processRunner).run(args, Duration.ofMinutes(15));
verifyNoInteractions(httpClient);
}
@Test
void testInstallInstallerLinuxBadExitCode(@TempDir final Path tempDir) throws IOException {
final var installerPath = tempDir.resolve("conda");
Files.createFile(installerPath);
when(configuration.condaInstallerPath()).thenReturn(installerPath);
when(configuration.condaBundledPath()).thenReturn(tempDir);
when(configuration.os()).thenReturn(OS.LINUX);
when(systemProcessResult.exitCode()).thenReturn(1);
when(configuration.condaRootPath()).thenReturn(tempDir);
final var args = List.of("bash", installerPath.toString(), "-b", "-p", tempDir.toString());
final var result = mock(ProcessResult.class);
when(result.exitCode()).thenReturn(1);
when(processRunner.run(args, Duration.ofMinutes(15))).thenReturn(result);
assertThrows(SetupException.class, condaSetupManager::install);
verify(processRunner).run(args, Duration.ofMinutes(15));
verifyNoInteractions(httpClient);
}
@Test
void testInstallInstallerUnknown(@TempDir final Path tempDir) throws IOException {
final var installerPath = tempDir.resolve("conda");
Files.createFile(installerPath);
when(configuration.condaInstallerPath()).thenReturn(installerPath);
when(configuration.condaBundledPath()).thenReturn(tempDir);
when(configuration.os()).thenReturn(OS.UNKNOWN);
when(systemProcessResult.exitCode()).thenReturn(1);
assertThrows(SetupException.class, condaSetupManager::install);
verifyNoInteractions(httpClient);
}
@Test
void testUninstall(@TempDir final Path tempDir) throws IOException {
when(configuration.condaRootPath()).thenReturn(tempDir);
Files.createDirectories(tempDir.resolve("sub"));
Files.createFile(tempDir.resolve("sub").resolve("conda"));
Files.createFile(tempDir.resolve("python"));
assertDoesNotThrow(condaSetupManager::uninstall);
assertFalse(Files.isDirectory(tempDir));
}
@Test
void testUpdateBundled() throws IOException {
final var path = Path.of("test");
final var args = List.of(path.toString(), "update", "-y", "conda");
final var duration = Duration.ofMinutes(15);
when(configuration.condaBundledPath()).thenReturn(path);
when(systemProcessResult.exitCode()).thenReturn(1);
final var processResult = mock(ProcessResult.class);
when(processResult.exitCode()).thenReturn(0);
when(processRunner.run(args, duration)).thenReturn(processResult);
assertDoesNotThrow(condaSetupManager::update);
verify(processRunner).run(args, duration);
}
@Test
void testUpdateSystemInstalled() throws IOException {
final var args = List.of(systemPath.toString(), "update", "-y", "conda");
final var duration = Duration.ofMinutes(15);
final var processResult = mock(ProcessResult.class);
when(processResult.exitCode()).thenReturn(0);
when(processRunner.run(args, duration)).thenReturn(processResult);
assertDoesNotThrow(condaSetupManager::update);
verify(processRunner).run(args, duration);
verify(processRunner).run(List.of(systemPath.toString(), "--version"), Duration.ofSeconds(5));
}
@Test
void testUpdateSystemIOException() throws IOException {
when(processRunner.run(List.of(systemPath.toString(), "--version"), Duration.ofSeconds(5))).thenThrow(IOException.class);
assertThrows(SetupException.class, condaSetupManager::update);
}
@Test
void testUpdateSystemBadVersion() {
final var path = Path.of("test");
when(systemProcessResult.output()).thenReturn(List.of("conda 9"));
when(configuration.condaBundledPath()).thenReturn(path);
when(configuration.condaSystemPath()).thenReturn(systemPath);
assertThrows(SetupException.class, condaSetupManager::update);
}
@Test
void testUpdateBundleBadResultCode() throws IOException {
final var path = Path.of("test");
final var args = List.of(path.toString(), "update", "-y", "conda");
final var duration = Duration.ofMinutes(15);
when(systemProcessResult.exitCode()).thenReturn(1);
when(configuration.condaBundledPath()).thenReturn(path);
when(processRunner.run(args, duration)).thenReturn(systemProcessResult);
when(systemProcessResult.exitCode()).thenReturn(1);
assertThrows(SetupException.class, condaSetupManager::update);
verify(processRunner).run(args, duration);
}
@Test
void testUpdateBundleIOException() throws IOException {
final var path = Path.of("test");
final var args = List.of(path.toString(), "update", "-y", "conda");
final var duration = Duration.ofMinutes(15);
when(systemProcessResult.exitCode()).thenReturn(1);
when(configuration.condaBundledPath()).thenReturn(path);
when(processRunner.run(args, duration)).thenThrow(IOException.class);
assertThrows(SetupException.class, condaSetupManager::update);
verify(processRunner).run(args, duration);
}
@Test
void testCreateVenv() throws IOException {
final var path = Paths.get("test");
final var args = List.of(systemPath.toString(), "create", "-y", "-p", path.toString(), "python=" + PYTHON_VERSION, "p1", "p2");
final var result = mock(ProcessResult.class);
when(result.exitCode()).thenReturn(0);
when(configuration.os()).thenReturn(OS.LINUX);
when(processRunner.run(args, Duration.ofMinutes(15))).thenReturn(result);
assertDoesNotThrow(() -> condaSetupManager.createVenv(path, PYTHON_VERSION, PACKAGES));
}
@Test
void testCreateVenvWindows(@TempDir final Path tempDir) throws IOException {
final var path = tempDir.resolve("test");
final var bin = path.resolve("Library").resolve("bin");
Files.createDirectories(bin);
final var crypto1 = bin.resolve("libcrypto-1.1.dll");
final var crypto2 = bin.resolve("libcrypto-2.sdf");
final var ssl1 = bin.resolve("libssl-1.1.dll");
final var ssl2 = bin.resolve("libssl-2.sdf");
final var other = bin.resolve("other.dll");
final var dlls = path.resolve("DLLs");
for (final var path1 : List.of(crypto1, crypto2, ssl1, ssl2, other)) {
Files.createFile(path1);
}
final var args = List.of(systemPath.toString(), "create", "-y", "-p", path.toString(), "python=" + PYTHON_VERSION, "p1", "p2");
final var result = mock(ProcessResult.class);
when(result.exitCode()).thenReturn(0);
when(configuration.os()).thenReturn(OS.WINDOWS);
when(processRunner.run(args, Duration.ofMinutes(15))).thenReturn(result);
assertDoesNotThrow(() -> condaSetupManager.createVenv(path, PYTHON_VERSION, PACKAGES));
for (final var path1 : List.of(crypto1, crypto2, ssl1, ssl2)) {
assertTrue(Files.exists(dlls.resolve(path1.getFileName())));
}
assertFalse(Files.exists(dlls.resolve(other.getFileName())));
}
@Test
void testCreateVenvIOException() throws IOException {
final var path = Paths.get("test");
final var args = List.of(systemPath.toString(), "create", "-y", "-p", path.toString(), "python=" + PYTHON_VERSION, "p1", "p2");
when(processRunner.run(args, Duration.ofMinutes(15))).thenThrow(IOException.class);
assertThrows(SetupException.class, () -> condaSetupManager.createVenv(path, PYTHON_VERSION, PACKAGES));
}
@Test
void testCreateVenvBadResultCode() throws IOException {
final var path = Paths.get("test");
final var args = List.of(systemPath.toString(), "create", "-y", "-p", path.toString(), "python=" + PYTHON_VERSION, "p1", "p2");
final var result = mock(ProcessResult.class);
when(result.exitCode()).thenReturn(1);
when(processRunner.run(args, Duration.ofMinutes(15))).thenReturn(result);
assertThrows(SetupException.class, () -> condaSetupManager.createVenv(path, PYTHON_VERSION, PACKAGES));
}
@Test
void testVenvExists(@TempDir final Path tempDirectory) throws IOException {
assertFalse(condaSetupManager.venvExists(tempDirectory));
Files.createDirectories(tempDirectory.resolve("bin"));
when(configuration.os()).thenReturn(OS.WINDOWS);
Files.createFile(tempDirectory.resolve("bin").resolve("python.exe"));
assertTrue(condaSetupManager.venvExists(tempDirectory));
when(configuration.os()).thenReturn(OS.LINUX);
assertFalse(condaSetupManager.venvExists(tempDirectory));
Files.createFile(tempDirectory.resolve("bin").resolve("python"));
assertTrue(condaSetupManager.venvExists(tempDirectory));
when(configuration.os()).thenReturn(OS.MAC);
assertTrue(condaSetupManager.venvExists(tempDirectory));
}
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new CondaSetupManager(null, processRunner, httpClient));
assertThrows(NullPointerException.class, () -> new CondaSetupManager(configuration, null, httpClient));
assertThrows(NullPointerException.class, () -> new CondaSetupManager(configuration, processRunner, null));
}
}

View File

@@ -6,6 +6,9 @@ import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.modules.archive.impl.ArchiveModule;
import com.github.gtache.autosubtitle.modules.setup.impl.SetupModule;
import com.github.gtache.autosubtitle.modules.subtitle.impl.SubtitleModule;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.process.impl.ProcessRunnerImpl;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
@@ -22,6 +25,9 @@ public abstract class CoreModule {
}
@Binds
abstract ProcessRunner bindsProcessRunner(final ProcessRunnerImpl processRunner);
@Provides
static OS providesOS() {
final var os = OS.getOS();

View File

@@ -6,16 +6,22 @@ import com.github.gtache.autosubtitle.process.ProcessRunner;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
/**
* Base implementation of {@link ProcessRunner}
* Implementation of {@link ProcessRunner}
*/
public abstract class AbstractProcessRunner implements ProcessRunner {
public class ProcessRunnerImpl implements ProcessRunner {
private static final Logger logger = LogManager.getLogger(AbstractProcessRunner.class);
private static final Logger logger = LogManager.getLogger(ProcessRunnerImpl.class);
@Inject
ProcessRunnerImpl() {
}
@Override
public ProcessResult run(final List<String> args, final Duration duration) throws IOException {
@@ -37,22 +43,5 @@ public abstract class AbstractProcessRunner implements ProcessRunner {
return new ProcessListenerImpl(process);
}
/**
* Runs a process and writes the output to the log
*
* @param args the command
* @param duration The maximum duration to wait for
* @return the result
* @throws IOException if an error occurs
*/
protected ProcessResult runListen(final List<String> args, final Duration duration) throws IOException {
final var listener = startListen(args);
var line = listener.readLine();
final var processName = args.getFirst();
while (line != null) {
logger.info("[{}]: {}", processName, line);
line = listener.readLine();
}
return listener.join(duration);
}
}

View File

@@ -1,6 +1,6 @@
package com.github.gtache.autosubtitle.setup.impl;
import com.github.gtache.autosubtitle.process.impl.AbstractProcessRunner;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.setup.SetupAction;
import com.github.gtache.autosubtitle.setup.SetupEvent;
import com.github.gtache.autosubtitle.setup.SetupException;
@@ -28,27 +28,23 @@ import static java.util.Objects.requireNonNull;
/**
* Base class for all {@link SetupManager} implementations
*/
public abstract class AbstractSetupManager extends AbstractProcessRunner implements SetupManager {
public abstract class AbstractSetupManager implements SetupManager {
private static final Logger logger = LogManager.getLogger(AbstractSetupManager.class);
private final Set<SetupListener> listeners;
private final HttpClient httpClient;
private final ProcessRunner processRunner;
/**
* Instantiates the manager with a default client
*/
protected AbstractSetupManager() {
this(HttpClient.newHttpClient());
}
/**
* Instantiates the manager with the given client
* Instantiates the manager with the given runner and client
*
* @param processRunner The process runner
* @param httpClient The HTTP client to use
*/
protected AbstractSetupManager(final HttpClient httpClient) {
this.listeners = new HashSet<>();
protected AbstractSetupManager(final ProcessRunner processRunner, final HttpClient httpClient) {
this.processRunner = requireNonNull(processRunner);
this.httpClient = requireNonNull(httpClient);
this.listeners = new HashSet<>();
}
@Override
@@ -100,6 +96,13 @@ public abstract class AbstractSetupManager extends AbstractProcessRunner impleme
}
}
/**
* @return The process runner used by this manager
*/
protected ProcessRunner processRunner() {
return processRunner;
}
/**
* @return Retrieves the setup status
* @throws SetupException if an error occurred

View File

@@ -1,6 +1,7 @@
package com.github.gtache.autosubtitle.process.impl;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import org.junit.jupiter.api.Test;
import java.io.IOException;
@@ -10,25 +11,25 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
class TestAbstractProcessRunner {
class TestProcessRunnerImpl {
private static final List<String> ARGS = OS.getOS() == OS.WINDOWS ? List.of("powershell.exe", "-Command", "\"echo '1\n2\n3'\"") : List.of("echo", "1\n2\n3");
private final DummyProcessRunner dummyProcessRunner;
private final ProcessRunner runner;
TestAbstractProcessRunner() {
this.dummyProcessRunner = new DummyProcessRunner();
TestProcessRunnerImpl() {
this.runner = new ProcessRunnerImpl();
}
@Test
void testRun() throws IOException {
final var expected = new ProcessResultImpl(0, List.of("1", "2", "3"));
final var actual = dummyProcessRunner.run(ARGS, Duration.ofSeconds(5));
final var actual = runner.run(ARGS, Duration.ofSeconds(5));
assertEquals(expected, actual);
}
@Test
void testStart() throws IOException, InterruptedException {
final var process = dummyProcessRunner.start(ARGS);
final var process = runner.start(ARGS);
process.waitFor();
assertEquals(0, process.exitValue());
final var read = new String(process.getInputStream().readAllBytes());
@@ -38,7 +39,7 @@ class TestAbstractProcessRunner {
@Test
void testStartListen() throws IOException {
final var listener = dummyProcessRunner.startListen(ARGS);
final var listener = runner.startListen(ARGS);
assertEquals("1", listener.readLine());
assertEquals("2", listener.readLine());
assertEquals("3", listener.readLine());
@@ -47,15 +48,4 @@ class TestAbstractProcessRunner {
assertEquals(0, result.exitCode());
assertEquals(List.of("1", "2", "3"), result.output());
}
@Test
void testRunListen() throws IOException {
final var result = dummyProcessRunner.runListen(ARGS, Duration.ofSeconds(5));
assertEquals(0, result.exitCode());
assertEquals(List.of("1", "2", "3"), result.output());
}
private static final class DummyProcessRunner extends AbstractProcessRunner {
}
}

View File

@@ -1,5 +1,6 @@
package com.github.gtache.autosubtitle.setup.impl;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.setup.SetupAction;
import com.github.gtache.autosubtitle.setup.SetupEvent;
import com.github.gtache.autosubtitle.setup.SetupException;
@@ -29,18 +30,21 @@ class TestAbstractSetupManager {
private final SetupListener listener2;
private final SetupEvent event;
private final SetupAction setupAction;
private final ProcessRunner processRunner;
private final HttpClient httpClient;
TestAbstractSetupManager(@Mock final SetupListener listener1,
@Mock final SetupListener listener2,
@Mock final SetupEvent event,
@Mock final SetupAction setupAction,
@Mock final ProcessRunner processRunner,
@Mock final HttpClient httpClient) {
this.manager = spy(new DummySetupManager(httpClient));
this.manager = spy(new DummySetupManager(processRunner, httpClient));
this.listener1 = requireNonNull(listener1);
this.listener2 = requireNonNull(listener2);
this.event = requireNonNull(event);
this.setupAction = requireNonNull(setupAction);
this.processRunner = requireNonNull(processRunner);
this.httpClient = requireNonNull(httpClient);
}
@@ -223,13 +227,13 @@ class TestAbstractSetupManager {
@Test
void testIllegal() {
assertThrows(NullPointerException.class, () -> new DummySetupManager(null));
assertThrows(NullPointerException.class, () -> new DummySetupManager(null, null));
}
private static final class DummySetupManager extends AbstractSetupManager {
private DummySetupManager(final HttpClient httpClient) {
super(httpClient);
private DummySetupManager(final ProcessRunner processRunner, final HttpClient httpClient) {
super(processRunner, httpClient);
}
@Override

View File

@@ -1,5 +1,6 @@
package com.github.gtache.autosubtitle.setup.deepl;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.setup.SetupException;
import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.SetupStatus;
@@ -7,6 +8,7 @@ import com.github.gtache.autosubtitle.setup.SetupUserBridge;
import com.github.gtache.autosubtitle.setup.impl.AbstractSetupManager;
import javax.inject.Inject;
import java.net.http.HttpClient;
import java.util.Objects;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
@@ -22,7 +24,9 @@ public class DeepLSetupManager extends AbstractSetupManager {
private final Preferences preferences;
@Inject
DeepLSetupManager(final SetupUserBridge userBridge, final Preferences preferences) {
DeepLSetupManager(final SetupUserBridge userBridge, final Preferences preferences, final ProcessRunner processRunner,
final HttpClient httpClient) {
super(processRunner, httpClient);
this.userBridge = Objects.requireNonNull(userBridge);
this.preferences = Objects.requireNonNull(preferences);
}

View File

@@ -11,10 +11,12 @@ import com.github.gtache.autosubtitle.impl.FileVideoImpl;
import com.github.gtache.autosubtitle.impl.VideoInfoImpl;
import com.github.gtache.autosubtitle.modules.setup.ffmpeg.FFmpegBundledPath;
import com.github.gtache.autosubtitle.modules.setup.ffmpeg.FFmpegSystemPath;
import com.github.gtache.autosubtitle.process.impl.AbstractProcessRunner;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverterProvider;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import java.io.IOException;
@@ -34,21 +36,24 @@ import static java.util.Objects.requireNonNull;
/**
* FFmpeg implementation of {@link VideoConverter}
*/
public class FFmpegVideoConverter extends AbstractProcessRunner implements VideoConverter {
public class FFmpegVideoConverter implements VideoConverter {
private static final Logger logger = LogManager.getLogger(FFmpegVideoConverter.class);
private static final String TEMP_FILE_PREFIX = "autosubtitle";
private final Path bundledPath;
private final Path systemPath;
private final SubtitleConverterProvider converterProvider;
private final Preferences preferences;
private final ProcessRunner processRunner;
@Inject
FFmpegVideoConverter(@FFmpegBundledPath final Path bundledPath, @FFmpegSystemPath final Path systemPath,
final SubtitleConverterProvider converterProvider, final Preferences preferences) {
final SubtitleConverterProvider converterProvider, final Preferences preferences,
final ProcessRunner processRunner) {
this.bundledPath = requireNonNull(bundledPath);
this.systemPath = requireNonNull(systemPath);
this.converterProvider = requireNonNull(converterProvider);
this.preferences = requireNonNull(preferences);
this.processRunner = requireNonNull(processRunner);
}
@Override
@@ -197,4 +202,15 @@ public class FFmpegVideoConverter extends AbstractProcessRunner implements Video
private String getFFmpegPath() {
return Files.isRegularFile(bundledPath) ? bundledPath.toString() : systemPath.toString();
}
private void runListen(final List<String> args, final Duration duration) throws IOException {
final var listener = processRunner.startListen(args);
var line = listener.readLine();
final var processName = args.getFirst();
while (line != null) {
logger.info("[{}]: {}", processName, line);
line = listener.readLine();
}
listener.join(duration);
}
}

View File

@@ -6,7 +6,7 @@ import com.github.gtache.autosubtitle.impl.FileVideoImpl;
import com.github.gtache.autosubtitle.impl.VideoInfoImpl;
import com.github.gtache.autosubtitle.modules.setup.ffmpeg.FFprobeBundledPath;
import com.github.gtache.autosubtitle.modules.setup.ffmpeg.FFprobeSystemPath;
import com.github.gtache.autosubtitle.process.impl.AbstractProcessRunner;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import javax.inject.Inject;
import java.io.IOException;
@@ -20,20 +20,23 @@ import static java.util.Objects.requireNonNull;
/**
* FFprobe implementation of {@link VideoLoader}
*/
public class FFprobeVideoLoader extends AbstractProcessRunner implements VideoLoader {
public class FFprobeVideoLoader implements VideoLoader {
private final Path bundledPath;
private final Path systemPath;
private final ProcessRunner processRunner;
@Inject
FFprobeVideoLoader(@FFprobeBundledPath final Path bundledPath, @FFprobeSystemPath final Path systemPath) {
FFprobeVideoLoader(@FFprobeBundledPath final Path bundledPath, @FFprobeSystemPath final Path systemPath,
final ProcessRunner processRunner) {
this.bundledPath = requireNonNull(bundledPath);
this.systemPath = requireNonNull(systemPath);
this.processRunner = requireNonNull(processRunner);
}
@Override
public Video loadVideo(final Path path) throws IOException {
final var result = run(List.of(getFFprobePath(), "-v", "error", "-select_streams", "v", "-show_entries", "stream=width,height,duration", "-of", "csv=p=0", path.toString()), Duration.ofSeconds(5));
final var result = processRunner.run(List.of(getFFprobePath(), "-v", "error", "-select_streams", "v", "-show_entries", "stream=width,height,duration", "-of", "csv=p=0", path.toString()), Duration.ofSeconds(5));
final var resolution = result.output().getLast();
final var split = resolution.split(",");
final var width = Integer.parseInt(split[0]);

View File

@@ -2,6 +2,7 @@ package com.github.gtache.autosubtitle.setup.ffmpeg;
import com.github.gtache.autosubtitle.archive.ArchiverProvider;
import com.github.gtache.autosubtitle.impl.Architecture;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.setup.SetupException;
import com.github.gtache.autosubtitle.setup.SetupManager;
import com.github.gtache.autosubtitle.setup.SetupStatus;
@@ -32,8 +33,8 @@ public class FFmpegSetupManager extends AbstractSetupManager {
@Inject
FFmpegSetupManager(final FFmpegSetupConfiguration configuration, final ArchiverProvider archiverProvider,
final HttpClient httpClient) {
super(httpClient);
final ProcessRunner processRunner, final HttpClient httpClient) {
super(processRunner, httpClient);
this.configuration = requireNonNull(configuration);
this.archiverProvider = requireNonNull(archiverProvider);
}
@@ -190,7 +191,7 @@ public class FFmpegSetupManager extends AbstractSetupManager {
}
private boolean checkSystemFFmpeg() throws IOException {
final var result = run(List.of(configuration.systemFFmpegPath().toString(), "-version"), Duration.ofSeconds(5));
final var result = processRunner().run(List.of(configuration.systemFFmpegPath().toString(), "-version"), Duration.ofSeconds(5));
return result.exitCode() == 0;
}

View File

@@ -3,6 +3,7 @@ package com.github.gtache.autosubtitle.ffmpeg;
import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.VideoInfo;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverter;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverterProvider;
@@ -24,6 +25,7 @@ import static org.mockito.Mockito.when;
class TestFFmpegVideoConverter {
private final FFmpegVideoConverter converter;
private final ProcessRunner runner;
private final SubtitleConverter subtitleConverter;
private final SubtitleConverterProvider subtitleConverterProvider;
private final Video video;
@@ -33,11 +35,12 @@ class TestFFmpegVideoConverter {
private final SubtitleCollection<?> collection;
private final Preferences preferences;
TestFFmpegVideoConverter(@Mock final SubtitleConverter subtitleConverter, @Mock final SubtitleConverterProvider subtitleConverterProvider, @Mock final Video video,
TestFFmpegVideoConverter(@Mock final SubtitleConverter subtitleConverter, @Mock final ProcessRunner runner, @Mock final SubtitleConverterProvider subtitleConverterProvider, @Mock final Video video,
@Mock final VideoInfo videoInfo, @Mock final SubtitleCollection<?> collection, @Mock final Preferences preferences) throws IOException {
final var output = (OS.getOS() == OS.WINDOWS ? System.getProperty("java.io.tmpdir") : "/tmp");
final var resource = OS.getOS() == OS.WINDOWS ? "fake-ffmpeg.exe" : "fake-ffmpeg.sh";
this.video = Objects.requireNonNull(video);
this.runner = Objects.requireNonNull(runner);
this.videoInfo = Objects.requireNonNull(videoInfo);
when(video.info()).thenReturn(videoInfo);
this.tmpFile = Files.createTempFile("fake-ffmpeg", resource.substring(resource.lastIndexOf('.')));
@@ -48,7 +51,7 @@ class TestFFmpegVideoConverter {
this.subtitleConverter = Objects.requireNonNull(subtitleConverter);
this.subtitleConverterProvider = Objects.requireNonNull(subtitleConverterProvider);
this.preferences = Objects.requireNonNull(preferences);
this.converter = new FFmpegVideoConverter(tmpFile, tmpFile, subtitleConverterProvider, preferences);
this.converter = new FFmpegVideoConverter(tmpFile, tmpFile, subtitleConverterProvider, preferences, runner);
this.collection = Objects.requireNonNull(collection);
}

View File

@@ -3,25 +3,32 @@ package com.github.gtache.autosubtitle.ffmpeg;
import com.github.gtache.autosubtitle.impl.FileVideoImpl;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.impl.VideoInfoImpl;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Objects;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ExtendWith(MockitoExtension.class)
class TestFFmpegVideoLoader {
private final FFprobeVideoLoader loader;
private final ProcessRunner runner;
private final Path tmpFile;
private final Path outputPath;
TestFFmpegVideoLoader() throws IOException {
TestFFmpegVideoLoader(@Mock final ProcessRunner runner) throws IOException {
final var output = (OS.getOS() == OS.WINDOWS ? System.getProperty("java.io.tmpdir") : "/tmp");
final var resource = OS.getOS() == OS.WINDOWS ? "fake-ffprobe.exe" : "fake-ffprobe.sh";
this.tmpFile = Files.createTempFile("fake-ffprobe", resource.substring(resource.lastIndexOf('.')));
@@ -29,7 +36,8 @@ class TestFFmpegVideoLoader {
Files.copy(in, tmpFile, StandardCopyOption.REPLACE_EXISTING);
}
this.outputPath = Path.of(output, "test-ffprobe-output.txt");
this.loader = new FFprobeVideoLoader(tmpFile, tmpFile);
this.runner = Objects.requireNonNull(runner);
this.loader = new FFprobeVideoLoader(tmpFile, tmpFile, runner);
}
@AfterEach

View File

@@ -1,3 +1,4 @@
subtitles.add.prompt.label=Enter text here...
subtitles.button.load.label=Load subtitles...
subtitles.button.reset.label=Reset subtitles
subtitles.button.subtitles.save.label=Save subtitles...

View File

@@ -1,3 +1,4 @@
subtitles.add.prompt.label=Entrez le texte ici...
subtitles.button.load.label=Charger des sous-titres...
subtitles.button.reset.label=R\u00E9initialiser les sous-titres
subtitles.button.subtitles.save.label=Sauvegarder les sous-titres...

View File

@@ -18,6 +18,7 @@ import javafx.collections.MapChangeListener;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TableColumn;
@@ -165,6 +166,8 @@ public class FXSubtitlesController extends AbstractFXController implements Subti
private void bindTable() {
subtitlesTable.setItems(model.selectedSubtitles());
subtitlesTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
subtitlesTable.setOnKeyPressed(e -> {
if (e.getCode().isLetterKey() || e.getCode().isDigitKey()) {
editFocusedCell();
@@ -180,6 +183,9 @@ public class FXSubtitlesController extends AbstractFXController implements Subti
e.consume();
}
});
subtitlesTable.setOnContextMenuRequested(e -> {
//TODO menu with copy, delete
});
startColumn.setCellFactory(TextFieldTableCell.forTableColumn(new TimeStringConverter(timeFormatter)));
startColumn.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue() == null ? null : param.getValue().start()));
startColumn.setOnEditCommit(e -> {
@@ -242,7 +248,7 @@ public class FXSubtitlesController extends AbstractFXController implements Subti
@FXML
private void addPressed() {
model.selectedCollection().subtitles().add(new ObservableSubtitleImpl("Enter text here..."));
model.selectedCollection().subtitles().add(new ObservableSubtitleImpl(resources.getString("subtitles.add.prompt.label")));
}
@FXML

View File

@@ -1,5 +1,6 @@
package com.github.gtache.autosubtitle.setup.whisper.base;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.setup.SetupException;
import com.github.gtache.autosubtitle.setup.conda.CondaSetupManager;
import com.github.gtache.autosubtitle.setup.whisper.AbstractWhisperSetupManager;
@@ -10,6 +11,7 @@ import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.net.http.HttpClient;
import java.nio.file.Files;
import java.time.Duration;
import java.util.List;
@@ -23,8 +25,9 @@ public class WhisperSetupManager extends AbstractWhisperSetupManager {
private static final Logger logger = LogManager.getLogger(WhisperSetupManager.class);
@Inject
WhisperSetupManager(final CondaSetupManager condaSetupManager, final WhisperSetupConfiguration configuration) {
super(condaSetupManager, configuration);
WhisperSetupManager(final CondaSetupManager condaSetupManager, final WhisperSetupConfiguration configuration,
final ProcessRunner processRunner, final HttpClient httpClient) {
super(condaSetupManager, configuration, processRunner, httpClient);
}
@Override
@@ -37,7 +40,7 @@ public class WhisperSetupManager extends AbstractWhisperSetupManager {
final var path = getPythonPath();
try {
logger.info("Installing whisper");
final var result = run(List.of(path.toString(), "-m", "pip", "install", "-U", "openai-whisper", "numpy<2"), Duration.ofMinutes(15));
final var result = processRunner().run(List.of(path.toString(), "-m", "pip", "install", "-U", "openai-whisper", "numpy<2"), Duration.ofMinutes(15));
if (result.exitCode() == 0) {
logger.info("Whisper installed");
} else {
@@ -53,7 +56,7 @@ public class WhisperSetupManager extends AbstractWhisperSetupManager {
final var path = getPythonPath();
if (Files.exists(path)) {
try {
final var result = run(List.of(path.toString(), "-m", "pip", "show", "openai-whisper"), Duration.ofSeconds(5));
final var result = processRunner().run(List.of(path.toString(), "-m", "pip", "show", "openai-whisper"), Duration.ofSeconds(5));
return result.exitCode() == 0;
} catch (final IOException e) {
throw new SetupException(e);

View File

@@ -3,6 +3,7 @@ package com.github.gtache.autosubtitle.subtitle.extractor.whisper.base;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperVenvPath;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverterProvider;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
@@ -23,8 +24,9 @@ public class WhisperSubtitleExtractor extends AbstractWhisperSubtitleExtractor {
@Inject
WhisperSubtitleExtractor(@WhisperVenvPath final Path venvPath, final SubtitleConverterProvider converterProvider, final OS os) {
super(venvPath, converterProvider, os);
WhisperSubtitleExtractor(@WhisperVenvPath final Path venvPath, final SubtitleConverterProvider converterProvider,
final ProcessRunner processRunner, final OS os) {
super(venvPath, converterProvider, processRunner, os);
}
@Override

View File

@@ -1,6 +1,7 @@
package com.github.gtache.autosubtitle.setup.whisper;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.setup.SetupAction;
import com.github.gtache.autosubtitle.setup.SetupException;
import com.github.gtache.autosubtitle.setup.SetupManager;
@@ -10,6 +11,7 @@ import com.github.gtache.autosubtitle.setup.impl.AbstractSetupManager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.net.http.HttpClient;
import java.nio.file.Path;
import static java.util.Objects.requireNonNull;
@@ -26,7 +28,9 @@ public abstract class AbstractWhisperSetupManager extends AbstractSetupManager {
private final CondaSetupManager condaSetupManager;
private final WhisperSetupConfiguration configuration;
protected AbstractWhisperSetupManager(final CondaSetupManager condaSetupManager, final WhisperSetupConfiguration configuration) {
protected AbstractWhisperSetupManager(final CondaSetupManager condaSetupManager, final WhisperSetupConfiguration configuration,
final ProcessRunner processRunner, final HttpClient httpClient) {
super(processRunner, httpClient);
this.condaSetupManager = requireNonNull(condaSetupManager);
this.configuration = requireNonNull(configuration);
}

View File

@@ -5,7 +5,7 @@ import com.github.gtache.autosubtitle.File;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.Video;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.process.impl.AbstractProcessRunner;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.subtitle.Subtitle;
import com.github.gtache.autosubtitle.subtitle.SubtitleCollection;
import com.github.gtache.autosubtitle.subtitle.converter.ParseException;
@@ -34,7 +34,7 @@ import static java.util.Objects.requireNonNull;
/**
* Base implementation of {@link SubtitleExtractor} for Whisper
*/
public abstract class AbstractWhisperSubtitleExtractor extends AbstractProcessRunner implements SubtitleExtractor {
public abstract class AbstractWhisperSubtitleExtractor implements SubtitleExtractor<Subtitle> {
private static final Logger logger = LogManager.getLogger(AbstractWhisperSubtitleExtractor.class);
@@ -43,13 +43,16 @@ public abstract class AbstractWhisperSubtitleExtractor extends AbstractProcessRu
private static final Pattern LINE_PROGRESS_PATTERN = Pattern.compile("^\\[\\d{2}:\\d{2}\\.\\d{3} --> (?<minutes>\\d{2}):(?<seconds>\\d{2})\\.(?<millis>\\d{3})].+");
private static final Pattern TQDM_PROGRESS_PATTERN = Pattern.compile("^(?<progress>\\d{1,3})%\\|.+");
private final Path venvPath;
private final ProcessRunner processRunner;
private final SubtitleConverter converter;
private final OS os;
private final Set<SubtitleExtractorListener> listeners;
protected AbstractWhisperSubtitleExtractor(final Path venvPath, final SubtitleConverterProvider converterProvider, final OS os) {
protected AbstractWhisperSubtitleExtractor(final Path venvPath, final SubtitleConverterProvider converterProvider,
final ProcessRunner processRunner, final OS os) {
this.venvPath = requireNonNull(venvPath);
this.converter = requireNonNull(converterProvider.getConverter("json"));
this.processRunner = requireNonNull(processRunner);
this.os = requireNonNull(os);
this.listeners = new HashSet<>();
}
@@ -115,7 +118,7 @@ public abstract class AbstractWhisperSubtitleExtractor extends AbstractProcessRu
try {
final var outputDir = Files.createTempDirectory(AUTOSUBTITLE);
final var args = createArgs(path, language, model, outputDir);
final var processListener = startListen(args);
final var processListener = processRunner.startListen(args);
var oldProgress = -1.0;
var line = processListener.readLine();
while (line != null) {

View File

@@ -1,5 +1,6 @@
package com.github.gtache.autosubtitle.setup.whisperx;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.setup.SetupException;
import com.github.gtache.autosubtitle.setup.conda.CondaSetupManager;
import com.github.gtache.autosubtitle.setup.whisper.AbstractWhisperSetupManager;
@@ -10,6 +11,7 @@ import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.IOException;
import java.net.http.HttpClient;
import java.nio.file.Files;
import java.time.Duration;
import java.util.List;
@@ -23,8 +25,9 @@ public class WhisperXSetupManager extends AbstractWhisperSetupManager {
private static final Logger logger = LogManager.getLogger(WhisperXSetupManager.class);
@Inject
WhisperXSetupManager(final CondaSetupManager condaSetupManager, final WhisperSetupConfiguration configuration) {
super(condaSetupManager, configuration);
WhisperXSetupManager(final CondaSetupManager condaSetupManager, final WhisperSetupConfiguration configuration,
final ProcessRunner processRunner, final HttpClient httpClient) {
super(condaSetupManager, configuration, processRunner, httpClient);
}
@Override
@@ -37,7 +40,7 @@ public class WhisperXSetupManager extends AbstractWhisperSetupManager {
final var path = getPythonPath();
try {
logger.info("Installing whisper");
final var result = run(List.of(path.toString(), "-m", "pip", "install", "-U", "git+https://github.com/m-bain/whisperx.git", "numpy<2"), Duration.ofMinutes(15));
final var result = processRunner().run(List.of(path.toString(), "-m", "pip", "install", "-U", "git+https://github.com/m-bain/whisperx.git", "numpy<2"), Duration.ofMinutes(15));
if (result.exitCode() == 0) {
logger.info("Whisper installed");
} else {
@@ -53,7 +56,7 @@ public class WhisperXSetupManager extends AbstractWhisperSetupManager {
final var path = getPythonPath();
if (Files.exists(path)) {
try {
final var result = run(List.of(path.toString(), "-m", "pip", "show", "whisperx"), Duration.ofSeconds(5));
final var result = processRunner().run(List.of(path.toString(), "-m", "pip", "show", "whisperx"), Duration.ofSeconds(5));
return result.exitCode() == 0;
} catch (final IOException e) {
throw new SetupException(e);

View File

@@ -3,6 +3,7 @@ package com.github.gtache.autosubtitle.subtitle.extractor.whisperx;
import com.github.gtache.autosubtitle.Language;
import com.github.gtache.autosubtitle.impl.OS;
import com.github.gtache.autosubtitle.modules.setup.whisper.WhisperVenvPath;
import com.github.gtache.autosubtitle.process.ProcessRunner;
import com.github.gtache.autosubtitle.subtitle.converter.SubtitleConverterProvider;
import com.github.gtache.autosubtitle.subtitle.extractor.ExtractionModel;
import com.github.gtache.autosubtitle.subtitle.extractor.SubtitleExtractor;
@@ -22,8 +23,8 @@ import java.util.List;
public class WhisperXSubtitleExtractor extends AbstractWhisperSubtitleExtractor {
@Inject
WhisperXSubtitleExtractor(@WhisperVenvPath final Path venvPath, final SubtitleConverterProvider converterProvider, final OS os) {
super(venvPath, converterProvider, os);
WhisperXSubtitleExtractor(@WhisperVenvPath final Path venvPath, final SubtitleConverterProvider converterProvider, final ProcessRunner processRunner, final OS os) {
super(venvPath, converterProvider, processRunner, os);
}
@Override