MarkwonBuilderImpl test

This commit is contained in:
Dimitry Ivanov 2018-12-27 18:45:45 +03:00
parent 581265a22a
commit c16cdff871
7 changed files with 357 additions and 26 deletions

View File

@ -8,6 +8,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.text.Spanned; import android.text.Spanned;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future; import java.util.concurrent.Future;
@ -71,9 +72,17 @@ public class MarkdownRenderer {
cancel(); cancel();
task = service.submit(new Runnable() { task = service.submit(new Runnable() {
@Override @Override
public void run() { public void run() {
try {
execute();
} catch (Throwable t) {
Debug.e(t);
}
}
private void execute() {
final UrlProcessor urlProcessor; final UrlProcessor urlProcessor;
if (uri == null) { if (uri == null) {
urlProcessor = new UrlProcessorInitialReadme(); urlProcessor = new UrlProcessorInitialReadme();

View File

@ -78,10 +78,8 @@ ext {
deps['test'] = [ deps['test'] = [
'junit' : 'junit:junit:4.12', 'junit' : 'junit:junit:4.12',
'robolectric' : 'org.robolectric:robolectric:3.8', 'robolectric': 'org.robolectric:robolectric:3.8',
'ix-java' : 'com.github.akarnokd:ixjava:1.0.0', 'ix-java' : 'com.github.akarnokd:ixjava:1.0.0',
'jackson-yaml' : 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.9.0',
'jackson-databind': 'com.fasterxml.jackson.core:jackson-databind:2.9.6',
'gson' : 'com.google.code.gson:gson:2.8.5', 'gson' : 'com.google.code.gson:gson:2.8.5',
'commons-io' : 'commons-io:commons-io:2.6', 'commons-io' : 'commons-io:commons-io:2.6',
'mockito' : 'org.mockito:mockito-core:2.21.0' 'mockito' : 'org.mockito:mockito-core:2.21.0'

View File

@ -2,7 +2,7 @@ package ru.noties.markwon;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.Log; import android.support.annotation.VisibleForTesting;
import android.widget.TextView; import android.widget.TextView;
import org.commonmark.parser.Parser; import org.commonmark.parser.Parser;
@ -12,6 +12,7 @@ import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import ru.noties.markwon.core.CorePlugin;
import ru.noties.markwon.core.MarkwonTheme; import ru.noties.markwon.core.MarkwonTheme;
import ru.noties.markwon.image.AsyncDrawableLoader; import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.priority.PriorityProcessor; import ru.noties.markwon.priority.PriorityProcessor;
@ -19,6 +20,7 @@ import ru.noties.markwon.priority.PriorityProcessor;
/** /**
* @since 3.0.0 * @since 3.0.0
*/ */
@SuppressWarnings("WeakerAccess")
class MarkwonBuilderImpl implements Markwon.Builder { class MarkwonBuilderImpl implements Markwon.Builder {
private final Context context; private final Context context;
@ -66,6 +68,7 @@ class MarkwonBuilderImpl implements Markwon.Builder {
return this; return this;
} }
@SuppressWarnings("UnusedReturnValue")
@NonNull @NonNull
public MarkwonBuilderImpl priorityProcessor(@NonNull PriorityProcessor priorityProcessor) { public MarkwonBuilderImpl priorityProcessor(@NonNull PriorityProcessor priorityProcessor) {
this.priorityProcessor = priorityProcessor; this.priorityProcessor = priorityProcessor;
@ -76,6 +79,23 @@ class MarkwonBuilderImpl implements Markwon.Builder {
@Override @Override
public Markwon build() { public Markwon build() {
if (plugins.isEmpty()) {
throw new IllegalStateException("No plugins were added to this builder. Use #usePlugin " +
"method to add them");
}
// this class will sort plugins to match a priority/dependency graph that we have
PriorityProcessor priorityProcessor = this.priorityProcessor;
if (priorityProcessor == null) {
// strictly speaking we do not need updating this field
// as we are not building this class to be reused between multiple `build` calls
priorityProcessor = this.priorityProcessor = PriorityProcessor.create();
}
// please note that this method must not modify supplied collection
// if nothing should be done -> the same collection can be returned
final List<MarkwonPlugin> plugins = preparePlugins(priorityProcessor, this.plugins);
final Parser.Builder parserBuilder = new Parser.Builder(); final Parser.Builder parserBuilder = new Parser.Builder();
final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context); final MarkwonTheme.Builder themeBuilder = MarkwonTheme.builderWithDefaults(context);
final AsyncDrawableLoader.Builder asyncDrawableLoaderBuilder = new AsyncDrawableLoader.Builder(); final AsyncDrawableLoader.Builder asyncDrawableLoaderBuilder = new AsyncDrawableLoader.Builder();
@ -84,18 +104,7 @@ class MarkwonBuilderImpl implements Markwon.Builder {
final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl(); final MarkwonSpansFactory.Builder spanFactoryBuilder = new MarkwonSpansFactoryImpl.BuilderImpl();
final RenderProps renderProps = new RenderPropsImpl(); final RenderProps renderProps = new RenderPropsImpl();
PriorityProcessor priorityProcessor = this.priorityProcessor;
if (priorityProcessor == null) {
// strictly speaking we do not need updating this field
// as we are not building this class to be reused between multiple `build` calls
priorityProcessor = this.priorityProcessor = PriorityProcessor.create();
}
final List<MarkwonPlugin> plugins = priorityProcessor.process(this.plugins);
for (MarkwonPlugin plugin : plugins) { for (MarkwonPlugin plugin : plugins) {
if (true) {
Log.e("PLUGIN", plugin.getClass().getName());
}
plugin.configureParser(parserBuilder); plugin.configureParser(parserBuilder);
plugin.configureTheme(themeBuilder); plugin.configureTheme(themeBuilder);
plugin.configureImages(asyncDrawableLoaderBuilder); plugin.configureImages(asyncDrawableLoaderBuilder);
@ -116,4 +125,59 @@ class MarkwonBuilderImpl implements Markwon.Builder {
Collections.unmodifiableList(plugins) Collections.unmodifiableList(plugins)
); );
} }
@VisibleForTesting
@NonNull
static List<MarkwonPlugin> preparePlugins(
@NonNull PriorityProcessor priorityProcessor,
@NonNull List<MarkwonPlugin> plugins) {
// with this method we will ensure that CorePlugin is added IF and ONLY IF
// there are plugins that depend on it. If CorePlugin is added, or there are
// no plugins that require it, CorePlugin won't be added
final List<MarkwonPlugin> out = ensureImplicitCoreIfHasDependents(plugins);
return priorityProcessor.process(out);
}
// this method will _implicitly_ add CorePlugin if there is at least one plugin
// that depends on CorePlugin
@VisibleForTesting
@NonNull
static List<MarkwonPlugin> ensureImplicitCoreIfHasDependents(@NonNull List<MarkwonPlugin> plugins) {
// loop over plugins -> if CorePlugin is found -> break;
// iterate over all plugins and check if CorePlugin is requested
boolean hasCore = false;
boolean hasCoreDependents = false;
for (MarkwonPlugin plugin : plugins) {
// here we do not check for exact match (a user could've subclasses CorePlugin
// and supplied it. In this case we DO NOT implicitly add CorePlugin
if (CorePlugin.class.isAssignableFrom(plugin.getClass())) {
hasCore = true;
break;
}
// if plugin has CorePlugin in dependencies -> mark for addition
if (!hasCoreDependents) {
// here we check for direct CorePlugin, if it's not CorePlugin (exact, not a subclass
// or something -> ignore)
if (plugin.priority().after().contains(CorePlugin.class)) {
hasCoreDependents = true;
}
}
}
if (hasCoreDependents && !hasCore) {
final List<MarkwonPlugin> out = new ArrayList<>(plugins.size() + 1);
// add default instance of CorePlugin
out.add(CorePlugin.create());
out.addAll(plugins);
return out;
}
return plugins;
}
} }

View File

@ -21,6 +21,9 @@ import ru.noties.markwon.image.data.DataUriSchemeHandler;
import ru.noties.markwon.image.file.FileSchemeHandler; import ru.noties.markwon.image.file.FileSchemeHandler;
import ru.noties.markwon.image.network.NetworkSchemeHandler; import ru.noties.markwon.image.network.NetworkSchemeHandler;
/**
* @since 3.0.0
*/
public class ImagesPlugin extends AbstractMarkwonPlugin { public class ImagesPlugin extends AbstractMarkwonPlugin {
@NonNull @NonNull

View File

@ -2,6 +2,7 @@ package ru.noties.markwon.priority;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap; import java.util.HashMap;
@ -18,7 +19,10 @@ class PriorityProcessorImpl extends PriorityProcessor {
@NonNull @NonNull
@Override @Override
public List<MarkwonPlugin> process(@NonNull List<MarkwonPlugin> plugins) { public List<MarkwonPlugin> process(@NonNull List<MarkwonPlugin> in) {
// create new collection based on supplied argument
final List<MarkwonPlugin> plugins = new ArrayList<>(in);
final int size = plugins.size(); final int size = plugins.size();
@ -32,7 +36,6 @@ class PriorityProcessorImpl extends PriorityProcessor {
} }
} }
// change to Map<Plugin,Int>
final Map<MarkwonPlugin, Integer> cache = new HashMap<>(size); final Map<MarkwonPlugin, Integer> cache = new HashMap<>(size);
for (MarkwonPlugin plugin : plugins) { for (MarkwonPlugin plugin : plugins) {
cache.put(plugin, eval(plugin, map)); cache.put(plugin, eval(plugin, map));
@ -126,8 +129,4 @@ class PriorityProcessorImpl extends PriorityProcessor {
return map.get(o1).compareTo(map.get(o2)); return map.get(o1).compareTo(map.get(o2));
} }
} }
private static class NoCorePluginAddedException extends Exception {
}
} }

View File

@ -0,0 +1,231 @@
package ru.noties.markwon;
import android.support.annotation.NonNull;
import android.text.Spanned;
import android.widget.TextView;
import org.commonmark.node.Node;
import org.commonmark.parser.Parser;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import ru.noties.markwon.core.CorePlugin;
import ru.noties.markwon.core.MarkwonTheme;
import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.priority.Priority;
import ru.noties.markwon.priority.PriorityProcessor;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.isA;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static ru.noties.markwon.MarkwonBuilderImpl.ensureImplicitCoreIfHasDependents;
import static ru.noties.markwon.MarkwonBuilderImpl.preparePlugins;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class MarkwonBuilderImplTest {
@Test
public void implicit_core_created() {
// a plugin explicitly requests CorePlugin, but CorePlugin is not added manually by user
// we validate that default CorePlugin instance is added
final MarkwonPlugin plugin = new AbstractMarkwonPlugin() {
@NonNull
@Override
public Priority priority() {
// strictly speaking we do not need to override this method
// as all children of AbstractMarkwonPlugin specify CorePlugin as a dependency.
// but we still add it to make things explicit and future proof, if this
// behavior changes
return Priority.after(CorePlugin.class);
}
};
final List<MarkwonPlugin> plugins = ensureImplicitCoreIfHasDependents(Collections.singletonList(plugin));
assertThat(plugins, hasSize(2));
assertThat(plugins, hasItem(isA(CorePlugin.class)));
}
@Test
public void implicit_core_no_dependents_not_added() {
final MarkwonPlugin a = new AbstractMarkwonPlugin() {
@NonNull
@Override
public Priority priority() {
return Priority.none();
}
};
final MarkwonPlugin b = new AbstractMarkwonPlugin() {
@NonNull
@Override
public Priority priority() {
return Priority.after(a.getClass());
}
};
final List<MarkwonPlugin> plugins = ensureImplicitCoreIfHasDependents(Arrays.asList(a, b));
assertThat(plugins, hasSize(2));
assertThat(plugins, not(hasItem(isA(CorePlugin.class))));
}
@Test
public void implicit_core_present() {
// if core is present it won't be added (whether or not there are dependents)
final MarkwonPlugin plugin = new AbstractMarkwonPlugin() {
@NonNull
@Override
public Priority priority() {
return Priority.after(CorePlugin.class);
}
};
final CorePlugin corePlugin = CorePlugin.create();
final List<MarkwonPlugin> plugins = ensureImplicitCoreIfHasDependents(Arrays.asList(plugin, corePlugin));
assertThat(plugins, hasSize(2));
assertThat(plugins, hasItem(plugin));
assertThat(plugins, hasItem(corePlugin));
}
@Test
public void implicit_core_subclass_present() {
// core was subclassed by a user and provided (implicit core won't be added)
final MarkwonPlugin plugin = new AbstractMarkwonPlugin() {
@NonNull
@Override
public Priority priority() {
return Priority.after(CorePlugin.class);
}
};
// our subclass
final CorePlugin corePlugin = new CorePlugin(false) {
};
final List<MarkwonPlugin> plugins = ensureImplicitCoreIfHasDependents(Arrays.asList(plugin, corePlugin));
assertThat(plugins, hasSize(2));
assertThat(plugins, hasItem(plugin));
assertThat(plugins, hasItem(corePlugin));
}
@Test
public void prepare_plugins() {
// validate that prepare plugins is calling `ensureImplicitCoreIfHasDependents` and
// priority processor
final PriorityProcessor priorityProcessor = mock(PriorityProcessor.class);
when(priorityProcessor.process(ArgumentMatchers.<MarkwonPlugin>anyList()))
.thenAnswer(new Answer<Object>() {
@Override
public Object answer(InvocationOnMock invocation) {
return invocation.getArgument(0);
}
});
final MarkwonPlugin plugin = new AbstractMarkwonPlugin() {
@NonNull
@Override
public Priority priority() {
return Priority.after(CorePlugin.class);
}
};
final List<MarkwonPlugin> plugins = preparePlugins(priorityProcessor, Collections.singletonList(plugin));
assertThat(plugins, hasSize(2));
assertThat(plugins, hasItem(plugin));
assertThat(plugins, hasItem(isA(CorePlugin.class)));
verify(priorityProcessor, times(1))
.process(ArgumentMatchers.<MarkwonPlugin>anyList());
}
@Test
public void user_supplied_priority_processor() {
// verify that if user supplied priority processor it will be used
final PriorityProcessor priorityProcessor = mock(PriorityProcessor.class);
final MarkwonBuilderImpl impl = new MarkwonBuilderImpl(RuntimeEnvironment.application);
// add some plugin (we do not care which one, but it must be present as we do not
// allow empty plugins list)
impl.usePlugin(new AbstractMarkwonPlugin() {
@NonNull
@Override
public Priority priority() {
return Priority.none();
}
});
impl.priorityProcessor(priorityProcessor);
impl.build();
verify(priorityProcessor, times(1)).process(ArgumentMatchers.<MarkwonPlugin>anyList());
}
@Test
public void no_plugins_added_throws() {
// there is no sense in having an instance with no plugins registered
try {
new MarkwonBuilderImpl(RuntimeEnvironment.application).build();
fail();
} catch (IllegalStateException e) {
assertThat(e.getMessage(), e.getMessage(), containsString("No plugins were added"));
}
}
@Test
public void plugin_configured() {
// verify that all configuration methods (applicable) are called
final MarkwonPlugin plugin = mock(MarkwonPlugin.class);
when(plugin.priority()).thenReturn(Priority.none());
final MarkwonBuilderImpl impl = new MarkwonBuilderImpl(RuntimeEnvironment.application);
impl.usePlugin(plugin).build();
verify(plugin, times(1)).configureParser(any(Parser.Builder.class));
verify(plugin, times(1)).configureTheme(any(MarkwonTheme.Builder.class));
verify(plugin, times(1)).configureImages(any(AsyncDrawableLoader.Builder.class));
verify(plugin, times(1)).configureConfiguration(any(MarkwonConfiguration.Builder.class));
verify(plugin, times(1)).configureVisitor(any(MarkwonVisitor.Builder.class));
verify(plugin, times(1)).configureSpansFactory(any(MarkwonSpansFactory.Builder.class));
// we do not know how many times exactly, but at least once it must be called
verify(plugin, atLeast(1)).priority();
// note, no render props -> they must be configured on render stage
verify(plugin, times(0)).configureRenderProps(any(RenderProps.class));
verify(plugin, times(0)).processMarkdown(anyString());
verify(plugin, times(0)).beforeRender(any(Node.class));
verify(plugin, times(0)).afterRender(any(Node.class), any(MarkwonVisitor.class));
verify(plugin, times(0)).beforeSetText(any(TextView.class), any(Spanned.class));
verify(plugin, times(0)).afterSetText(any(TextView.class));
}
}

View File

@ -159,6 +159,33 @@ public class PriorityProcessorTest {
assertEquals(third, plugins.get(2)); assertEquals(third, plugins.get(2));
} }
@Test
public void five_plugins_sequential() {
final MarkwonPlugin a = new NamedPlugin("a") {
};
final MarkwonPlugin b = new NamedPlugin("b", a) {
};
final MarkwonPlugin c = new NamedPlugin("c", b) {
};
final MarkwonPlugin d = new NamedPlugin("d", c) {
};
final MarkwonPlugin e = new NamedPlugin("e", d) {
};
final List<MarkwonPlugin> plugins = processor.process(Arrays.asList(d, e, a, c, b));
assertEquals(5, plugins.size());
assertEquals(a, plugins.get(0));
assertEquals(b, plugins.get(1));
assertEquals(c, plugins.get(2));
assertEquals(d, plugins.get(3));
assertEquals(e, plugins.get(4));
}
@Test @Test
public void plugin_duplicate() { public void plugin_duplicate() {