InferenceDispatcherTest.java
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.tika.parser.inference;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import org.apache.tika.detect.Detector;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.exception.TikaException;
import org.apache.tika.extractor.EmbeddedDocumentUtil;
import org.apache.tika.extractor.EmbeddedMetadataLookup;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.TIFF;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.mime.MediaType;
import org.apache.tika.parser.AutoDetectParser;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.Parser;
import org.apache.tika.parser.RecursiveParserWrapper;
import org.apache.tika.parser.hook.ParseHooks;
import org.apache.tika.sax.AbstractRecursiveParserWrapperHandler;
import org.apache.tika.sax.BasicContentHandlerFactory;
import org.apache.tika.sax.RecursiveParserWrapperHandler;
import org.apache.tika.sax.XHTMLContentHandler;
public class InferenceDispatcherTest {
private static final MediaType PNG = MediaType.image("png");
private static final MediaType CONTAINER = MediaType.application("x-test-container");
private static final MediaType INNER = MediaType.image("x-test-inner");
static final class RecordingEngine implements Engine {
}
/** Records units and their bytes; bytes are only readable during the run. */
static final class RecordingTask implements InferenceTask {
final List<List<InferenceUnit>> runs = new ArrayList<>();
final List<List<byte[]>> bytes = new ArrayList<>();
@Override
public void run(InferenceBinding binding, List<InferenceUnit> units, Engine engine,
ParseContext context) throws IOException {
runs.add(List.copyOf(units));
List<byte[]> read = new ArrayList<>();
for (InferenceUnit unit : units) {
read.add(unit.getBytes());
}
bytes.add(read);
}
}
/** A stub parser for a type: emits an empty document. */
static final class TypedParser implements Parser {
private final Set<MediaType> types;
TypedParser(MediaType type) {
this.types = Collections.singleton(type);
}
@Override
public Set<MediaType> getSupportedTypes(ParseContext context) {
return types;
}
@Override
public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata,
ParseContext context) throws IOException, SAXException, TikaException {
XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata, context);
xhtml.startDocument();
xhtml.endDocument();
}
}
/**
* A container parsed through the embedded extractor: "CONTAINER" holds one inline PNG;
* "NESTED" holds a "CONTAINER" of the {@link #INNER} type as an attachment, like a zip
* holding a docx with a picture. INNER is an image type so the attachment is offered too.
*/
static final class ContainerParser implements Parser {
private final MediaType type;
ContainerParser() {
this(CONTAINER);
}
ContainerParser(MediaType type) {
this.type = type;
}
@Override
public Set<MediaType> getSupportedTypes(ParseContext context) {
return Collections.singleton(type);
}
@Override
public void parse(TikaInputStream tis, ContentHandler handler, Metadata metadata,
ParseContext context) throws IOException, SAXException, TikaException {
XHTMLContentHandler xhtml = new XHTMLContentHandler(handler, metadata, context);
xhtml.startDocument();
boolean nested = "NESTED".equals(new String(tis.readAllBytes(), UTF_8));
Metadata child = new Metadata();
child.set(HttpHeaders.CONTENT_TYPE, nested ? INNER.toString() : PNG.toString());
child.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, nested ? "ATTACHMENT" : "INLINE");
child.set(TikaCoreProperties.RESOURCE_NAME_KEY, nested ? "inner.docx" : "image1.png");
byte[] bytes = (nested ? "CONTAINER" : "PNG-BYTES").getBytes(UTF_8);
try (TikaInputStream childStream = TikaInputStream.get(bytes)) {
EmbeddedDocumentUtil.getEmbeddedDocumentExtractor(context)
.parseEmbedded(childStream, xhtml, child, context, false);
}
xhtml.endDocument();
}
}
private static InferenceBinding binding(String id, InputKind kind, Set<MediaType> include,
int maxChunks) {
return InferenceBinding.builder(id, "engine", kind).include(include)
.maxChunks(maxChunks).build();
}
@TempDir
Path tmp;
private Path file(String bytes) throws IOException {
Path p = Files.createTempFile(tmp, "unit", ".bin");
Files.write(p, bytes.getBytes(UTF_8));
return p;
}
/** Offers as an embedded document: a parent, so the dispatcher copies the bytes. */
private Path offer(InferenceDispatcher dispatcher, MediaType type, String bytes,
ParseContext context) throws Exception {
Path source = file(bytes);
dispatcher.offer(InputKind.IMAGES, type, new Metadata(), new Metadata(), source, context);
return source;
}
@Test
public void testBufferPerBindingAndBudget() throws Exception {
RecordingEngine engine = new RecordingEngine();
RecordingTask pngTask = new RecordingTask();
RecordingTask jpegTask = new RecordingTask();
InferenceDispatcher dispatcher = new InferenceDispatcher(List.of(
new InferenceDispatcher.Bound(binding("png", InputKind.IMAGES, Set.of(PNG), 1),
engine, List.of(pngTask)),
new InferenceDispatcher.Bound(
binding("jpeg", InputKind.IMAGES, Set.of(MediaType.image("jpeg")), -1),
engine, List.of(jpegTask))));
ParseContext context = new ParseContext();
assertTrue(dispatcher.wants(InputKind.IMAGES, PNG, context));
assertTrue(!dispatcher.wants(InputKind.PAGES, PNG, context));
assertTrue(!dispatcher.wants(InputKind.IMAGES, MediaType.image("gif"), context));
Path source = offer(dispatcher, PNG, "a", context);
offer(dispatcher, PNG, "b", context);
offer(dispatcher, MediaType.image("jpeg"), "c", context);
offer(dispatcher, MediaType.image("gif"), "d", context);
assertTrue(pngTask.runs.isEmpty(), "nothing runs before the flush");
Path held = context.get(InferenceDispatcher.State.class).byBinding.get("png").get(0).getPath();
assertTrue(Files.exists(held), "the dispatcher owns a copy until the flush");
assertNotEquals(source, held, "an embedded document's bytes are copied");
Metadata root = new Metadata();
dispatcher.flush(root, context);
assertEquals(1, pngTask.runs.size());
assertEquals(1, pngTask.runs.get(0).size(), "maxChunks 1 keeps the first unit");
assertArrayEquals("a".getBytes(UTF_8), pngTask.bytes.get(0).get(0));
assertEquals(1, jpegTask.runs.get(0).size());
assertTrue(root.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING)
.contains("skipped 1 units"));
assertNull(context.get(InferenceDispatcher.State.class), "the buffer is cleared");
assertFalse(Files.exists(held), "and its files are gone");
assertTrue(Files.exists(source), "the source was never the dispatcher's to delete");
dispatcher.flush(root, context);
assertEquals(1, pngTask.runs.size(), "a second flush has nothing to run");
}
/** A spacer is not a picture: an image recorded under the binding's minimum is not offered. */
@Test
public void testTinyImagesAreNotOffered() throws Exception {
RecordingTask task = new RecordingTask();
InferenceDispatcher dispatcher = new InferenceDispatcher(List.of(
new InferenceDispatcher.Bound(binding("png", InputKind.IMAGES, Set.of(PNG), -1),
new RecordingEngine(), List.of(task))));
ParseContext context = new ParseContext();
Metadata spacer = new Metadata();
spacer.set(TIFF.IMAGE_WIDTH, 600);
spacer.set(TIFF.IMAGE_LENGTH, 1);
dispatcher.offer(InputKind.IMAGES, PNG, spacer, new Metadata(), file("a"), context);
Metadata picture = new Metadata();
picture.set(TIFF.IMAGE_WIDTH, 2);
picture.set(TIFF.IMAGE_LENGTH, 2);
dispatcher.offer(InputKind.IMAGES, PNG, picture, new Metadata(), file("b"), context);
dispatcher.offer(InputKind.IMAGES, PNG, new Metadata(), new Metadata(), file("c"), context);
dispatcher.flush(new Metadata(), context);
assertEquals(2, task.runs.get(0).size(), "the 2 x 2 picture and the one of unknown size");
InferenceBinding tall = InferenceBinding.builder("tall", "engine", InputKind.IMAGES)
.include(Set.of(PNG)).minSize(0, 100).build();
assertFalse(tall.accepts(InputKind.IMAGES, PNG, picture), "shorter than 100");
assertTrue(tall.accepts(InputKind.IMAGES, PNG, new Metadata()), "unknown size passes");
}
/** The top-level document's own file outlives the flush, so it is referenced, not copied. */
@Test
public void testTopLevelBytesAreReferencedNotCopied() throws Exception {
RecordingTask task = new RecordingTask();
InferenceDispatcher dispatcher = new InferenceDispatcher(List.of(
new InferenceDispatcher.Bound(binding("png", InputKind.IMAGES, null, -1),
new RecordingEngine(), List.of(task)),
new InferenceDispatcher.Bound(binding("pages", InputKind.PAGES, null, -1),
new RecordingEngine(), List.of(task))));
ParseContext context = new ParseContext();
Path source = file("top");
dispatcher.offer(InputKind.IMAGES, PNG, new Metadata(), null, source, context);
InferenceUnit unit = context.get(InferenceDispatcher.State.class).byBinding.get("png").get(0);
assertEquals(source, unit.getPath());
Path render = file("page");
dispatcher.offerPage(PNG, new Metadata(), null, 1, render, context);
InferenceUnit page = context.get(InferenceDispatcher.State.class).byBinding.get("pages").get(0);
assertNotEquals(render, page.getPath(), "a page render is the renderer's file: copied");
dispatcher.flush(new Metadata(), context);
assertArrayEquals("top".getBytes(UTF_8), task.bytes.get(0).get(0));
assertTrue(Files.exists(source), "referenced, so not deleted with the buffer");
assertFalse(Files.exists(page.getPath()));
}
@Test
public void testSelectionAndMaxBytes() throws Exception {
RecordingTask pngTask = new RecordingTask();
RecordingTask smallTask = new RecordingTask();
InferenceDispatcher dispatcher = new InferenceDispatcher(List.of(
new InferenceDispatcher.Bound(binding("png", InputKind.IMAGES, null, -1),
new RecordingEngine(), List.of(pngTask)),
new InferenceDispatcher.Bound(InferenceBinding.builder("small", "engine",
InputKind.IMAGES).maxBytes(2).build(),
new RecordingEngine(), List.of(smallTask))));
ParseContext off = new ParseContext();
InferenceSelection selection = new InferenceSelection();
selection.setEnabled(false);
off.set(InferenceSelection.class, selection);
assertFalse(dispatcher.wants(InputKind.IMAGES, PNG, off));
ParseContext some = new ParseContext();
selection = new InferenceSelection();
selection.setBindings(List.of("small"));
some.set(InferenceSelection.class, selection);
offer(dispatcher, PNG, "abc", some);
offer(dispatcher, PNG, "ab", some);
dispatcher.flush(new Metadata(), some);
assertTrue(pngTask.runs.isEmpty(), "not selected for this request");
assertEquals(1, smallTask.runs.get(0).size(), "maxBytes 2 drops the 3-byte unit");
ParseContext unknown = new ParseContext();
selection = new InferenceSelection();
selection.setBindings(List.of("nope"));
unknown.set(InferenceSelection.class, selection);
assertThrows(TikaException.class, () -> dispatcher.wants(InputKind.IMAGES, PNG, unknown));
}
/** A handler that kept a copy of one embedded document, the way the wrapper does. */
static final class KeepingHandler extends AbstractRecursiveParserWrapperHandler {
final Metadata kept = new Metadata();
KeepingHandler(String idPath) {
super(new BasicContentHandlerFactory(BasicContentHandlerFactory.HANDLER_TYPE.TEXT, -1));
kept.set(TikaCoreProperties.EMBEDDED_ID_PATH, idPath);
}
@Override
public Metadata getEmbeddedMetadata(String idPath) {
return idPath.equals(kept.get(TikaCoreProperties.EMBEDDED_ID_PATH)) ? kept : null;
}
}
@Test
public void testFlushWritesToTheKeptMetadata() throws Exception {
RecordingTask task = new RecordingTask();
InferenceDispatcher dispatcher = new InferenceDispatcher(List.of(
new InferenceDispatcher.Bound(binding("png", InputKind.IMAGES, null, -1),
new RecordingEngine(), List.of(task))));
KeepingHandler handler = new KeepingHandler("/1");
ParseContext context = new ParseContext();
context.set(EmbeddedMetadataLookup.class, new EmbeddedMetadataLookup(handler));
Metadata liveParent = new Metadata();
liveParent.set(TikaCoreProperties.EMBEDDED_ID_PATH, "/1");
Metadata liveChild = new Metadata();
liveChild.set(TikaCoreProperties.EMBEDDED_ID_PATH, "/1/2");
dispatcher.offer(InputKind.IMAGES, PNG, liveChild, liveParent, file("x"), context);
dispatcher.flush(new Metadata(), context);
InferenceUnit unit = task.runs.get(0).get(0);
assertSame(handler.kept, unit.getParent(), "the parent the wrapper kept, not the live one");
assertSame(liveChild, unit.getTarget(), "nothing kept for the child: the live object");
assertSame(liveChild, unit.getDestination(), "untyped: not lifted");
assertEquals("/1/2", unit.getTargetIdPath());
}
/** Detection by the declared type: these tests are about the hook, not the detector. */
private static final Detector DECLARED = (tis, metadata, ctx) ->
MediaType.parse(metadata.get(HttpHeaders.CONTENT_TYPE));
private static AutoDetectParser hooked(InferenceDispatcher dispatcher, Parser... parsers) {
AutoDetectParser adp = new AutoDetectParser(DECLARED, parsers);
adp.setParseHooks(new ParseHooks(List.of(dispatcher)));
return adp;
}
private static InferenceDispatcher pngDispatcher(RecordingTask task) {
return new InferenceDispatcher(List.of(
new InferenceDispatcher.Bound(binding("png", InputKind.IMAGES, null, -1),
new RecordingEngine(), List.of(task))));
}
@Test
public void testUnknownSelectionFailsTheTopLevelParse() throws Exception {
AutoDetectParser parser = hooked(pngDispatcher(new RecordingTask()), new TypedParser(PNG));
ParseContext context = new ParseContext();
InferenceSelection selection = new InferenceSelection();
selection.setBindings(List.of("typo"));
context.set(InferenceSelection.class, selection);
Metadata metadata = new Metadata();
metadata.set(HttpHeaders.CONTENT_TYPE, "text/plain");
try (TikaInputStream tis = TikaInputStream.get("not an image".getBytes(UTF_8))) {
assertThrows(TikaException.class,
() -> parser.parse(tis, new DefaultHandler(), metadata, context),
"fails at the top of the parse, before any document is offered");
}
}
@Test
public void testNestedChildOutsideTheWrapperGetsItsParent() throws Exception {
RecordingTask task = new RecordingTask();
AutoDetectParser parser = hooked(pngDispatcher(task), new ContainerParser(),
new TypedParser(PNG));
Metadata root = new Metadata();
root.set(HttpHeaders.CONTENT_TYPE, CONTAINER.toString());
ParseContext context = new ParseContext();
try (TikaInputStream tis = TikaInputStream.get("CONTAINER".getBytes(UTF_8))) {
parser.parse(tis, new DefaultHandler(), root, context);
}
assertEquals(1, task.runs.size());
InferenceUnit unit = task.runs.get(0).get(0);
assertSame(root, unit.getParent(),
"the parent is named in every mode, not only under the wrapper");
assertSame(root, unit.getDestination(), "one output object: results land on it");
assertEquals("/1", unit.getTargetIdPath(), "numbered as the wrapper would have");
assertNull(unit.getParentIdPath());
assertNull(context.get(ParseHooks.class), "seeding is undone at the top level");
}
/** Outside the wrapper the embedded documents' metadata is discarded: every unit lands on the root. */
@Test
public void testDeepChildOutsideTheWrapperLandsOnTheRoot() throws Exception {
RecordingTask task = new RecordingTask();
AutoDetectParser parser = hooked(pngDispatcher(task), new ContainerParser(),
new ContainerParser(INNER), new TypedParser(PNG));
Metadata root = new Metadata();
root.set(HttpHeaders.CONTENT_TYPE, CONTAINER.toString());
try (TikaInputStream tis = TikaInputStream.get("NESTED".getBytes(UTF_8))) {
parser.parse(tis, new DefaultHandler(), root, new ParseContext());
}
assertEquals(2, task.runs.get(0).size(), "the picture and the attachment");
InferenceUnit unit = task.runs.get(0).get(0);
assertEquals("image1.png", unit.getTarget().get(TikaCoreProperties.RESOURCE_NAME_KEY));
assertEquals("inner.docx", unit.getParent().get(TikaCoreProperties.RESOURCE_NAME_KEY));
assertSame(root, unit.getDestination());
assertTrue(unit.isLifted());
assertEquals("/1/2", unit.getTargetIdPath());
assertEquals("/1", unit.getParentIdPath());
InferenceUnit attachment = task.runs.get(0).get(1);
assertSame(root, attachment.getDestination(), "an attachment too: there is nowhere else");
assertTrue(attachment.isLifted());
assertEquals("/1", attachment.getTargetIdPath());
}
/** Under the wrapper an attachment keeps its own results, on the copy the wrapper kept. */
@Test
public void testAttachmentUnderTheWrapperKeepsItsOwn() throws Exception {
RecordingTask task = new RecordingTask();
AutoDetectParser parser = hooked(pngDispatcher(task), new ContainerParser(),
new ContainerParser(INNER), new TypedParser(PNG));
RecursiveParserWrapper wrapper = new RecursiveParserWrapper(parser);
Metadata root = new Metadata();
root.set(HttpHeaders.CONTENT_TYPE, CONTAINER.toString());
RecursiveParserWrapperHandler handler = new RecursiveParserWrapperHandler(
new BasicContentHandlerFactory(BasicContentHandlerFactory.HANDLER_TYPE.TEXT, -1));
try (TikaInputStream tis = TikaInputStream.get("NESTED".getBytes(UTF_8))) {
wrapper.parse(tis, handler, root, new ParseContext());
}
assertEquals(3, handler.getMetadataList().size());
Metadata keptDocx = handler.getEmbeddedMetadata("/1");
Map<String, InferenceUnit> byName = new HashMap<>();
for (InferenceUnit unit : task.runs.get(0)) {
byName.put(unit.getTarget().get(TikaCoreProperties.RESOURCE_NAME_KEY), unit);
}
InferenceUnit picture = byName.get("image1.png");
InferenceUnit attachment = byName.get("inner.docx");
assertSame(keptDocx, attachment.getDestination(), "the attachment's kept copy");
assertFalse(attachment.isLifted());
assertSame(keptDocx, picture.getDestination(), "lifted onto the docx it is part of");
assertTrue(picture.isLifted());
assertEquals("/1/2", picture.getTargetIdPath());
}
@Test
public void testOffersAndFlushesAtTopLevel() throws Exception {
RecordingTask task = new RecordingTask();
AutoDetectParser parser = hooked(pngDispatcher(task), new TypedParser(PNG));
Metadata metadata = new Metadata();
metadata.set(HttpHeaders.CONTENT_TYPE, "image/png");
ParseContext context = new ParseContext();
try (TikaInputStream tis = TikaInputStream.get("PNG-BYTES".getBytes(UTF_8))) {
parser.parse(tis, new DefaultHandler(), metadata, context);
}
assertEquals(1, task.runs.size());
InferenceUnit unit = task.runs.get(0).get(0);
assertArrayEquals("PNG-BYTES".getBytes(UTF_8), task.bytes.get(0).get(0));
assertSame(metadata, unit.getTarget());
assertNull(unit.getParent());
}
@Test
public void testNestedChildCarriesItsParent() throws Exception {
RecordingTask task = new RecordingTask();
AutoDetectParser parser = hooked(pngDispatcher(task), new ContainerParser(),
new TypedParser(PNG));
RecursiveParserWrapper wrapper = new RecursiveParserWrapper(parser);
Metadata root = new Metadata();
root.set(HttpHeaders.CONTENT_TYPE, CONTAINER.toString());
RecursiveParserWrapperHandler handler = new RecursiveParserWrapperHandler(
new BasicContentHandlerFactory(BasicContentHandlerFactory.HANDLER_TYPE.TEXT, -1));
try (TikaInputStream tis = TikaInputStream.get("CONTAINER".getBytes(UTF_8))) {
wrapper.parse(tis, handler, root, new ParseContext());
}
assertEquals(1, task.runs.size(), "flushed once, at the end of the top-level parse");
InferenceUnit unit = task.runs.get(0).get(0);
assertArrayEquals("PNG-BYTES".getBytes(UTF_8), task.bytes.get(0).get(0));
assertEquals("image1.png", unit.getTarget().get(TikaCoreProperties.RESOURCE_NAME_KEY));
assertSame(root, unit.getParent());
assertSame(root, unit.getDestination(), "inline: lifted onto its parent");
assertEquals(2, handler.getMetadataList().size());
}
@Test
public void testPagesComeFromTheRendererOnly() throws Exception {
RecordingTask pageTask = new RecordingTask();
RecordingTask imageTask = new RecordingTask();
RecordingEngine engine = new RecordingEngine();
InferenceDispatcher dispatcher = new InferenceDispatcher(List.of(
new InferenceDispatcher.Bound(binding("pages", InputKind.PAGES, null, 1),
engine, List.of(pageTask)),
new InferenceDispatcher.Bound(binding("images", InputKind.IMAGES, null, -1),
engine, List.of(imageTask))));
ParseContext context = new ParseContext();
Metadata pdf = new Metadata();
assertTrue(dispatcher.wantsPages(PNG, pdf, context));
Metadata rendering = new Metadata();
rendering.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, "RENDERING");
assertFalse(dispatcher.wants(PNG, rendering, context),
"a render emitted as an embedded document is a page, not an image");
Metadata inline = new Metadata();
inline.set(TikaCoreProperties.EMBEDDED_RESOURCE_TYPE, "INLINE");
assertTrue(dispatcher.wants(PNG, inline, context));
dispatcher.offerPage(PNG, pdf, null, 1, file("p1"), context);
dispatcher.offerPage(PNG, pdf, null, 2, file("p2"), context);
dispatcher.offer(PNG, rendering, pdf, file("r"), context);
Metadata root = new Metadata();
dispatcher.flush(root, context);
assertEquals(1, pageTask.runs.size());
List<InferenceUnit> units = pageTask.runs.get(0);
assertEquals(1, units.size(), "maxChunks 1 is one page");
assertEquals(InputKind.PAGES, units.get(0).getKind());
assertEquals(1, units.get(0).getPage());
assertSame(pdf, units.get(0).getTarget());
assertEquals("p1", new String(pageTask.bytes.get(0).get(0), UTF_8));
assertTrue(imageTask.runs.isEmpty(), "the rendering child reached no IMAGES binding");
assertTrue(root.get(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING)
.contains("pages over maxChunks: skipped 1 units"));
}
@Test
public void testNoPagesBindingWantsNoPages() throws Exception {
InferenceDispatcher dispatcher = pngDispatcher(new RecordingTask());
assertFalse(dispatcher.wantsPages(PNG, new Metadata(), new ParseContext()));
}
@Test
public void testFailedParseRunsNothingAndCleansUp() throws Exception {
RecordingTask task = new RecordingTask();
InferenceDispatcher dispatcher = pngDispatcher(task);
ParseContext context = new ParseContext();
offer(dispatcher, PNG, "a", context);
Path held = context.get(InferenceDispatcher.State.class).byBinding.get("png").get(0).getPath();
dispatcher.end(new Metadata(), true, context);
assertTrue(task.runs.isEmpty(), "a failed document costs no engine call");
assertFalse(Files.exists(held));
assertNull(context.get(InferenceDispatcher.State.class));
}
/** A MEDIA unit is the whole file; the segment cap applies later, not the file size. */
@Test
public void testMediaIsNotSizeGatedAtTheOffer() throws Exception {
RecordingTask task = new RecordingTask();
InferenceDispatcher dispatcher = new InferenceDispatcher(List.of(
new InferenceDispatcher.Bound(InferenceBinding.builder("v", "engine",
InputKind.MEDIA).modality(Modality.VISUAL).maxBytes(2).build(),
new RecordingEngine(), List.of(task))));
ParseContext context = new ParseContext();
MediaType mp4 = MediaType.video("mp4");
assertTrue(dispatcher.wants(mp4, new Metadata(), context));
dispatcher.offer(mp4, new Metadata(), new Metadata(), file("more than two"), context);
dispatcher.flush(new Metadata(), context);
assertEquals(1, task.runs.size());
assertEquals(1, task.runs.get(0).size(), "maxBytes does not drop a media file");
// a playlist is text that names other files; it is never a media unit
assertFalse(dispatcher.wants(MediaType.audio("x-mpegurl"), new Metadata(), context));
assertNull(InputKind.of(MediaType.parse("application/vnd.apple.mpegurl")));
assertNull(InputKind.of(MediaType.audio("x-scpls")));
assertNull(InputKind.of(MediaType.parse("video/vnd.mpegurl")));
assertNull(InputKind.of(MediaType.parse("application/xspf+xml")));
assertEquals(InputKind.MEDIA, InputKind.of(MediaType.audio("mpeg")));
}
@Test
public void testMediaMaxChunksIsNotAUnitCap() throws Exception {
RecordingTask task = new RecordingTask();
InferenceDispatcher dispatcher = new InferenceDispatcher(List.of(
new InferenceDispatcher.Bound(InferenceBinding.builder("v", "engine",
InputKind.MEDIA).modality(Modality.AUDIO).maxChunks(1).build(),
new RecordingEngine(), List.of(task))));
ParseContext context = new ParseContext();
MediaType mp3 = MediaType.audio("mpeg");
for (int i = 0; i < 3; i++) {
dispatcher.offer(mp3, new Metadata(), new Metadata(), file("clip " + i), context);
}
dispatcher.flush(new Metadata(), context);
assertEquals(3, task.runs.get(0).size(), "maxChunks caps segments per file, not files");
}
@Test
public void testMediaUnitsAreBoundedPerTree() throws Exception {
RecordingTask task = new RecordingTask();
InferenceDispatcher dispatcher = new InferenceDispatcher(List.of(
new InferenceDispatcher.Bound(InferenceBinding.builder("v", "engine",
InputKind.MEDIA).modality(Modality.AUDIO).build(),
new RecordingEngine(), List.of(task))));
ParseContext context = new ParseContext();
MediaType mp3 = MediaType.audio("mpeg");
Path clip = file("clip");
for (int i = 0; i < InferenceDispatcher.MAX_MEDIA_UNITS + 5; i++) {
dispatcher.offer(mp3, new Metadata(), new Metadata(), clip, context);
}
Metadata root = new Metadata();
dispatcher.flush(root, context);
assertEquals(InferenceDispatcher.MAX_MEDIA_UNITS, task.runs.get(0).size());
assertTrue(String.join(" ", root.getValues(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING))
.contains("media files"), "the drop is reported on the root");
}
@Test
public void testMediaConfigValidation() throws Exception {
MediaConfig config = new MediaConfig();
config.initialize();
config.getSegment().setOverlap(-1);
assertThrows(TikaConfigException.class, config::initialize, "negative overlap");
config.getSegment().setOverlap(30);
assertThrows(TikaConfigException.class, config::initialize, "overlap >= seconds");
config.getSegment().setOverlap(0);
config.getSegment().setSeconds(0);
assertThrows(TikaConfigException.class, config::initialize, "no window");
config.getSegment().setSeconds(10);
config.setMaxSegments(0);
assertThrows(TikaConfigException.class, config::initialize, "0 embeds nothing");
config.setMaxSegments(-2);
assertThrows(TikaConfigException.class, config::initialize);
config.setMaxSegments(-1);
config.initialize();
}
}