Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ant.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Java CI

on: [push]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v1
- name: Set up JDK 8
uses: actions/setup-java@v1
with:
java-version: 8
- name: run test suite with ant
run: ant -noinput -buildfile build.xml test -DbuildEnv=ci
162 changes: 125 additions & 37 deletions build.xml
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="mkv-scanner" basedir="." default="create-jar.runtime"
xmlns:unless="ant:unless"
xmlns:ivy="antlib:org.apache.ivy.ant">

<property name="ivy.install.version" value="2.4.0"/>
<property name="ivy.jar.dir" value="${basedir}/ivy"/>
<property name="ivy.jar.file" value="${ivy.jar.dir}/ivy.jar"/>
<property name="src.main.dir" value="src/main/java"/>
<property name="src.test.dir" value="src/test/java"/>
<property name="resources.main.dir" value="src/main/resources"/>
<property name="lib.dir" value="lib"/>
<property name="build.dir" value="build"/>
<property name="build.main.dir" value="${build.dir}/main"/>
<property name="build.test.dir" value="${build.dir}/test"/>
<property name="target.dir" value="target"/>
<property name="dist.dir" value="dist"/>
<property name="javadoc.dir" value="javadoc"/>
Expand All @@ -20,7 +23,6 @@
<include name="**/*.jar"/>
</fileset>
</path>

<available file="${ivy.jar.file}" property="ivy.installed"/>

<target name="ivy.load-settings" depends="ivy.install">
Expand All @@ -30,7 +32,7 @@
<target name="ivy.download" unless="ivy.installed" description="downloads the ivy jar file">
<echo message="downloading ivy..."/>
<mkdir dir="${ivy.jar.dir}"/>
<get src="http://repo1.maven.org/maven2/org/apache/ivy/ivy/${ivy.install.version}/ivy-${ivy.install.version}.jar" dest="${ivy.jar.file}" usetimestamp="true"/>
<get src="https://repo1.maven.org/maven2/org/apache/ivy/ivy/${ivy.install.version}/ivy-${ivy.install.version}.jar" dest="${ivy.jar.file}" usetimestamp="true"/>
</target>

<target name="ivy.install" depends="ivy.download" description="registers the ivy antlib in ant">
Expand All @@ -45,13 +47,28 @@
<ivy:retrieve conf="runtime" pattern="${lib.dir}/[conf]/[organisation]-[artifact]-[revision].[ext]"/>
</target>

<target name="ivy.resolve.test" depends="ivy.install" description="resolve ivy dependencies for tests">
<ivy:retrieve pattern="${lib.dir}/[conf]/[organisation]-[artifact]-[revision].[ext]" conf="test"/>
</target>

<target name="compile.main" depends="ivy.resolve.main" description="compile source">
<mkdir dir="${build.main.dir}"/>
<javac destdir="${build.main.dir}" includeantruntime="false" classpathref="classpath.ivy-runtime">
<src path="${src.main.dir}"/>
</javac>
</target>

<target name="compile.test" depends="compile.main, ivy.resolve.test" description="compiles the tests">
<mkdir dir="${build.test.dir}"/>
<javac destdir="${build.test.dir}" includeantruntime="false">
<classpath>
<pathelement path="${build.main.dir}"/>
<fileset dir="${lib.dir}/test" includes="*.jar"/>
</classpath>
<src path="${src.test.dir}"/>
</javac>
</target>

<target name="copy.resources" description="copy resources to build dir">
<copy todir="${build.main.dir}">
<fileset dir="${resources.main.dir}"/>
Expand All @@ -75,6 +92,45 @@
<jar basedir="${javadoc.dir}" file="${dist.dir}/${artifact.name}-${ivy.revision}-javadoc.jar"/>
</target>

<target name="test" depends="compile.test" description="Runs the tests via the JUnit Platform ConsoleLauncher (forked). Runs all tests by default. Narrow with -Dtest.match=SUBSTRING for a partial class-name match, or -Dtest.args='...' for raw ConsoleLauncher selectors (e.g. --select-class=FQCN or --select-method=FQCN#method(paramTypes))">
<!-- test.match: convenience for partial class-name matching; it supplies the classpath scan for you.
test.args: full raw override (default just scans the test build dir). ConsoleLauncher forbids combining
a classpath scan with explicit select selectors, which is why these are two separate knobs. -->
<condition property="test.args" value="--scan-classpath=${build.test.dir} --include-classname=.*${test.match}.*">
<isset property="test.match"/>
</condition>
<property name="test.args" value="--scan-classpath=${build.test.dir}"/>

<!-- forked, fully self-contained classpath: the console-standalone fat jar supplies all junit/platform
classes, so the other junit jars are excluded to avoid duplicate classes (mockito/jackson/slf4j/
byte-buddy/objenesis are kept). Nothing here references ${ant.home}. -->
<java fork="true" failonerror="false" resultproperty="test.exitcode"
classname="org.junit.platform.console.ConsoleLauncher">
<classpath>
<fileset dir="${lib.dir}/test" includes="org.junit.platform-junit-platform-console-standalone-*.jar"/>
<pathelement path="${build.main.dir}"/>
<pathelement path="${build.test.dir}"/>
<!-- so Eac3toScanner can load /eac3to-languages.txt and /eac3to-format-extensions.json -->
<pathelement location="${resources.main.dir}"/>
<fileset dir="${lib.dir}/test">
<include name="*.jar"/>
<exclude name="org.junit.*"/>
<exclude name="junit-junit-*"/>
<exclude name="org.opentest4j-*"/>
<exclude name="org.apiguardian-*"/>
</fileset>
</classpath>
<arg line="${test.args}"/>
<arg value="--details=tree"/>
<arg value="--fail-if-no-tests"/>
</java>
<fail message="ONE OR MORE TESTS FAILED.">
<condition>
<not><equals arg1="${test.exitcode}" arg2="0"/></not>
</condition>
</fail>
</target>

<target name="publish" depends="ivy.resolve.main, publish.validate, create-jar.sources, create-jar.runtime, create-jar.javadoc" description="publishes artifact to an ivy/maven-compliant repository. Required parameters: publish.resolver, key.password, (and for non-local publishing) publish.user, publish.password">
<copy file="${target.dir}/${artifact.name}.jar" tofile="${dist.dir}/${artifact.name}-${ivy.revision}.jar"/>
<ivy:makepom ivyfile="ivy.xml" templatefile="pomTemplate.xml" pomfile="${dist.dir}/${artifact.name}-${ivy.revision}.pom"/>
Expand All @@ -97,41 +153,73 @@
</macrodef>

<target name="publish.validate">
<script language="javascript">
var ivyVersion = project.getProperty("ivy.revision");
var publishResolver = project.getProperty("publish.resolver");
var publishUser = project.getProperty("publish.user") || "";
var publishPassword = project.getProperty("publish.password") || "";
var keyPassword = project.getProperty("key.password") || "";
var errors = [];
if (keyPassword.length === 0) {
errors.push("parameter key.password is required for gpg to sign artifacts");
}
if (publishResolver === "nexus-staging" || publishResolver === "nexus-snapshots") {
if (publishUser.length === 0) {
errors.push("parameter publish.user required when publishing to " + publishResolver);
}
if (publishPassword.length === 0) {
errors.push("parameter publish.password required when publishing to " + publishResolver);
}
}
if (ivyVersion.toLowerCase().endsWith("snapshot")) {
if (publishResolver !== "nexus-snapshots" &amp;&amp; publishResolver !== "local") {
errors.push("parameter publish.resolver must be \"nexus-snapshots\" or \"local\" for a snapshot version: " + ivyVersion);
}
project.setProperty("publish.status", "snapshot");
} else {
if (publishResolver !== "nexus-staging" &amp;&amp; publishResolver !== "local") {
errors.push("parameter publish.resolver must be \"nexus-staging\" or \"local\" for a non-snapshot version: " + ivyVersion);
}
project.setProperty("publish.status", "release");
}
if (errors.length > 0) {
project.setProperty("publish.validation-failed", true);
project.setProperty("publish.errors", errors.join("; "));
}
</script>
<fail if="publish.validation-failed" message="${publish.errors}"/>
<!-- default the optional params to empty so the checks below treat "unset" and "set but blank" alike -->
<property name="publish.resolver" value=""/>
<property name="publish.user" value=""/>
<property name="publish.password" value=""/>
<property name="key.password" value=""/>

<fail message="parameter key.password is required for gpg to sign artifacts">
<condition><equals arg1="${key.password}" arg2=""/></condition>
</fail>

<condition property="publish.to-nexus">
<or>
<equals arg1="${publish.resolver}" arg2="nexus-staging"/>
<equals arg1="${publish.resolver}" arg2="nexus-snapshots"/>
</or>
</condition>
<fail message="parameter publish.user required when publishing to ${publish.resolver}">
<condition>
<and>
<isset property="publish.to-nexus"/>
<equals arg1="${publish.user}" arg2=""/>
</and>
</condition>
</fail>
<fail message="parameter publish.password required when publishing to ${publish.resolver}">
<condition>
<and>
<isset property="publish.to-nexus"/>
<equals arg1="${publish.password}" arg2=""/>
</and>
</condition>
</fail>

<condition property="publish.is-snapshot">
<matches string="${ivy.revision}" pattern="snapshot$" casesensitive="false"/>
</condition>
<fail message="parameter publish.resolver must be &quot;nexus-snapshots&quot; or &quot;local&quot; for a snapshot version: ${ivy.revision}">
<condition>
<and>
<isset property="publish.is-snapshot"/>
<not>
<or>
<equals arg1="${publish.resolver}" arg2="nexus-snapshots"/>
<equals arg1="${publish.resolver}" arg2="local"/>
</or>
</not>
</and>
</condition>
</fail>
<fail message="parameter publish.resolver must be &quot;nexus-staging&quot; or &quot;local&quot; for a non-snapshot version: ${ivy.revision}">
<condition>
<and>
<not><isset property="publish.is-snapshot"/></not>
<not>
<or>
<equals arg1="${publish.resolver}" arg2="nexus-staging"/>
<equals arg1="${publish.resolver}" arg2="local"/>
</or>
</not>
</and>
</condition>
</fail>

<condition property="publish.status" value="snapshot">
<isset property="publish.is-snapshot"/>
</condition>
<property name="publish.status" value="release"/>
</target>

<target name="clean" description="cleans all build artifacts">
Expand Down
6 changes: 6 additions & 0 deletions ivy.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<configurations>
<conf name="default"/>
<conf name="runtime" extends="default"/>
<conf name="test" extends="runtime"/>
<conf name="sources"/>
<conf name="javadoc"/>
<conf name="signature"/>
Expand All @@ -23,5 +24,10 @@
<dependency org="org.slf4j" name="slf4j-api" rev="1.7.25"/>
<dependency org="com.fasterxml.jackson.core" name="jackson-core" rev="2.7.0"/>
<dependency org="com.fasterxml.jackson.core" name="jackson-databind" rev="2.7.0"/>
<dependency org="org.junit.jupiter" name="junit-jupiter-engine" rev="5.4.2" conf="test->default"/>
<dependency org="org.junit.platform" name="junit-platform-engine" rev="1.4.2" conf="test->default"/>
<dependency org="org.junit.platform" name="junit-platform-console-standalone" rev="1.4.2" conf="test->default"/>
<dependency org="org.mockito" name="mockito-core" rev="2.27.0" conf="test->default"/>
<dependency org="org.mockito" name="mockito-junit-jupiter" rev="2.27.0" conf="test->default"/>
</dependencies>
</ivy-module>
32 changes: 17 additions & 15 deletions src/main/java/com/adashrod/mkvscanner/Eac3toScanner.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import com.adashrod.mkvscanner.model.Track;
import com.adashrod.mkvscanner.model.Video;
import com.adashrod.mkvscanner.util.FormatExtensionConfig;
import com.adashrod.mkvscanner.util.StreamConsumer;
import com.adashrod.mkvscanner.util.ProcessBuilderRunner;
import com.adashrod.mkvscanner.util.ProcessResult;
import com.adashrod.mkvscanner.util.ProcessRunner;
import com.adashrod.mkvscanner.util.StringLineIterator;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
Expand Down Expand Up @@ -48,12 +50,22 @@ public class Eac3toScanner implements FileScanner {

private final String executableLocation;
private final File outputDirectory;
private final ProcessRunner processRunner;
private final Collection<String> eac3toLanguages = new HashSet<>();
private final Map<String, FormatExtensionConfig> formatExtensionConfigs = new HashMap<>();

public Eac3toScanner(final String executableLocation, final File outputDirectory) {
this(executableLocation, outputDirectory, new ProcessBuilderRunner());
}

/**
* Package-private constructor that allows injecting a {@link ProcessRunner}, used by tests to feed fake process
* output (stdout/exit code) without launching the real executable.
*/
Eac3toScanner(final String executableLocation, final File outputDirectory, final ProcessRunner processRunner) {
this.executableLocation = Objects.requireNonNull(executableLocation, "Eac3toScanner.executableLocation can't be null");
this.outputDirectory = Objects.requireNonNull(outputDirectory, "Eac3toScanner.outputDirectory can't be null");
this.processRunner = Objects.requireNonNull(processRunner, "Eac3toScanner.processRunner can't be null");
try {
loadEac3toLanguages();
loadEac3toFormatExtensions();
Expand Down Expand Up @@ -105,29 +117,19 @@ public String exec(final File file, final String... arguments) throws DemuxerExc
command.add(executableLocation);
command.add(file.getPath());
Collections.addAll(command, arguments);
final ProcessBuilder builder = new ProcessBuilder(command);
Process process = null;
final int exitValue;
final StreamConsumer stdOut;
final StreamConsumer stdErr;
final ProcessResult result;
try {
process = builder.start();
stdOut = new StreamConsumer(process.getInputStream());
stdErr = new StreamConsumer(process.getErrorStream());
stdOut.start();
stdErr.start();
exitValue = process.waitFor();
result = processRunner.run(command);
} catch (final InterruptedException ie) {
process.destroy();
logger.error("process interrupted", ie);// todo: propagate a wrapper exception or re-throw; if not re-throwing, re-interrupt
return null;
}

// when piping eac3to output to a file or stream, each line is prepended by a bunch of backspace characters: \b
// also: the first line has the eac3to progress bar "----" and some whitespace
final String output = stdOut.getStreamContent().replaceAll("\b", "").replaceAll("^-+\\s+", "").trim();
final String output = result.getStdout().replaceAll("\b", "").replaceAll("^-+\\s+", "").trim();

if (exitValue == 0) {
if (result.getExitValue() == 0) {
return output;
} else {
final StringBuilder argumentsBuilder = new StringBuilder(file.getPath()).append(" ");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.adashrod.mkvscanner.util;

import java.io.IOException;
import java.util.List;

/**
* Production {@link ProcessRunner} that launches the process with a {@link ProcessBuilder} and drains its stdout and
* stderr with {@link StreamConsumer} threads to avoid the buffer-deadlock described in {@link StreamConsumer}.
*/
public class ProcessBuilderRunner implements ProcessRunner {
@Override
public ProcessResult run(final List<String> command) throws IOException, InterruptedException {
final ProcessBuilder builder = new ProcessBuilder(command);
Process process = null;
try {
process = builder.start();
final StreamConsumer stdOut = new StreamConsumer(process.getInputStream());
final StreamConsumer stdErr = new StreamConsumer(process.getErrorStream());
stdOut.start();
stdErr.start();
final int exitValue = process.waitFor();
return new ProcessResult(exitValue, stdOut.getStreamContent(), stdErr.getStreamContent());
} catch (final InterruptedException ie) {
process.destroy();
throw ie;
}
}
}
29 changes: 29 additions & 0 deletions src/main/java/com/adashrod/mkvscanner/util/ProcessResult.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.adashrod.mkvscanner.util;

/**
* Immutable result of running an external process: its exit value and the full contents of its stdout and stderr
* streams.
*/
public class ProcessResult {
private final int exitValue;
private final String stdout;
private final String stderr;

public ProcessResult(final int exitValue, final String stdout, final String stderr) {
this.exitValue = exitValue;
this.stdout = stdout;
this.stderr = stderr;
}

public int getExitValue() {
return exitValue;
}

public String getStdout() {
return stdout;
}

public String getStderr() {
return stderr;
}
}
Loading
Loading