Add write opml test

This commit is contained in:
Shinokuni 2020-08-08 22:40:23 +02:00
parent 5fe10a848c
commit ab86bcbcc2
6 changed files with 83 additions and 19 deletions

View File

@ -54,6 +54,7 @@ dependencies {
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'com.squareup.retrofit2:retrofit:2.7.1' implementation 'com.squareup.retrofit2:retrofit:2.7.1'

View File

@ -1,7 +1,7 @@
<opml version="2.0"> <opml version="2.0">
<body> <body>
<outline text="Folder 1" title="Folder 1"> <outline text="Folder 1" title="Folder 1">
<outline text="Subfolder 1" title="SubFolder 1"> <outline text="Subfolder 1" title="Subfolder 1">
<outline title="The Verge" xmlUrl='http://www.theverge.com/rss/index.xml' htmlUrl="http://www.theverge.com" /> <outline title="The Verge" xmlUrl='http://www.theverge.com/rss/index.xml' htmlUrl="http://www.theverge.com" />
<outline title="TechCrunch" xmlUrl='https://techcrunch.com/feed/' htmlUrl="https://techcrunch.com/" /> <outline title="TechCrunch" xmlUrl='https://techcrunch.com/feed/' htmlUrl="https://techcrunch.com/" />
<outline xmlUrl='http://feeds.mashable.com/Mashable' /> <outline xmlUrl='http://feeds.mashable.com/Mashable' />

View File

@ -1,22 +1,36 @@
package com.readrops.api package com.readrops.api
import android.Manifest
import android.content.Context import android.content.Context
import android.os.Environment
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import com.readrops.api.opml.OPMLParser import com.readrops.api.opml.OPMLParser
import com.readrops.api.utils.ParseException import com.readrops.api.utils.ParseException
import com.readrops.db.entities.Feed import com.readrops.db.entities.Feed
import com.readrops.db.entities.Folder import com.readrops.db.entities.Folder
import io.reactivex.CompletableObserver
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertTrue
import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import java.io.File
import java.io.FileOutputStream
import java.io.OutputStream
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class OPMLParserTest { class OPMLParserTest {
private val context: Context = InstrumentationRegistry.getInstrumentation().context private val context: Context = InstrumentationRegistry.getInstrumentation().context
@get:Rule
val permissionRule: GrantPermissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
@Test @Test
fun readOpmlTest() { fun readOpmlTest() {
val stream = context.resources.assets.open("subscriptions.opml") val stream = context.resources.assets.open("subscriptions.opml")
@ -53,6 +67,34 @@ class OPMLParserTest {
@Test @Test
fun writeOpmlTest() { fun writeOpmlTest() {
val filePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath
val file = File(filePath, "subscriptions.opml")
val outputStream: OutputStream = FileOutputStream(file)
val foldersAndFeeds: Map<Folder?, List<Feed>> = HashMap<Folder?, List<Feed>>().apply {
put(null, listOf(Feed("Feed1", "", "https://feed1.com"),
Feed("Feed2", "", "https://feed2.com")))
put(Folder("Folder1"), listOf())
put(Folder("Folder2"), listOf(Feed("Feed3", "", "https://feed3.com"),
Feed("Feed4", "", "https://feed4.com")))
}
OPMLParser.write(foldersAndFeeds, outputStream)
.subscribeOn(Schedulers.trampoline())
.subscribe()
outputStream.flush()
outputStream.close()
val inputStream = file.inputStream()
var foldersAndFeeds2: Map<Folder?, List<Feed>>? = null
OPMLParser.read(inputStream).subscribe { result -> foldersAndFeeds2 = result }
assertEquals(foldersAndFeeds.size, foldersAndFeeds2?.size)
assertEquals(foldersAndFeeds[Folder("Folder1")]?.size, foldersAndFeeds2?.get(Folder("Folder1"))?.size)
assertEquals(foldersAndFeeds[Folder("Folder2")]?.size, foldersAndFeeds2?.get(Folder("Folder2"))?.size)
assertEquals(foldersAndFeeds[null]?.size, foldersAndFeeds2?.get(null)?.size)
inputStream.close()
} }
} }

View File

@ -0,0 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.readrops.api">
<!-- for tests only -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application android:requestLegacyExternalStorage="true" />
</manifest>

View File

@ -40,7 +40,7 @@ object OPMLParser {
val opml: OPML = serializer.read(OPML::class.java, fileString) val opml: OPML = serializer.read(OPML::class.java, fileString)
emitter.onSuccess(opmltoFoldersAndFeeds(opml)) emitter.onSuccess(opmlToFoldersAndFeeds(opml))
} catch (e: Exception) { } catch (e: Exception) {
Log.d(TAG, e.message, e) Log.d(TAG, e.message, e)
emitter.onError(e) emitter.onError(e)
@ -49,7 +49,7 @@ object OPMLParser {
} }
@JvmStatic @JvmStatic
fun write(foldersAndFeeds: Map<Folder, List<Feed>>, outputStream: OutputStream): Completable { fun write(foldersAndFeeds: Map<Folder?, List<Feed>>, outputStream: OutputStream): Completable {
return Completable.create { emitter -> return Completable.create { emitter ->
val serializer: Serializer = Persister() val serializer: Serializer = Persister()
serializer.write(foldersAndFeedsToOPML(foldersAndFeeds), outputStream) serializer.write(foldersAndFeedsToOPML(foldersAndFeeds), outputStream)
@ -58,7 +58,7 @@ object OPMLParser {
} }
} }
private fun opmltoFoldersAndFeeds(opml: OPML): Map<Folder?, List<Feed>> { private fun opmlToFoldersAndFeeds(opml: OPML): Map<Folder?, List<Feed>> {
if (opml.version != "2.0") if (opml.version != "2.0")
throw ParseException("Only 2.0 OPML specification is supported") throw ParseException("Only 2.0 OPML specification is supported")
@ -84,7 +84,9 @@ object OPMLParser {
// The outline is a folder/category // The outline is a folder/category
if ((outline.outlines != null && !outline.outlines?.isEmpty()!!) || outline.xmlUrl.isNullOrEmpty()) { if ((outline.outlines != null && !outline.outlines?.isEmpty()!!) || outline.xmlUrl.isNullOrEmpty()) {
val folder = Folder(outline.text) // if the outline doesn't have text or title value but contains sub outlines,
// those sub outlines will be considered as not belonging to any folder and join the others at the top level of the hierarchy
val folder = if (outline.name != null) Folder(outline.name) else null
outline.outlines?.forEach { outline.outlines?.forEach {
val recursiveFeedsFolders = parseOutline(it) val recursiveFeedsFolders = parseOutline(it)
@ -100,7 +102,7 @@ object OPMLParser {
} else { // the outline is a feed } else { // the outline is a feed
if (!outline.xmlUrl.isNullOrEmpty()) { if (!outline.xmlUrl.isNullOrEmpty()) {
val feed = Feed().apply { val feed = Feed().apply {
name = outline.title name = outline.name
url = outline.xmlUrl url = outline.xmlUrl
siteUrl = outline.htmlUrl siteUrl = outline.htmlUrl
} }
@ -112,20 +114,27 @@ object OPMLParser {
return foldersAndFeeds return foldersAndFeeds
} }
private fun foldersAndFeedsToOPML(foldersAndFeeds: Map<Folder, List<Feed>>): OPML { private fun foldersAndFeedsToOPML(foldersAndFeeds: Map<Folder?, List<Feed>>): OPML {
val outlines = arrayListOf<Outline>() val outlines = arrayListOf<Outline>()
for (folderAndFeeds in foldersAndFeeds) { for (folderAndFeeds in foldersAndFeeds) {
val outline = Outline(folderAndFeeds.key.name) if (folderAndFeeds.key != null) {
val outline = Outline(folderAndFeeds.key?.name)
val feedOutlines = arrayListOf<Outline>() val feedOutlines = arrayListOf<Outline>()
folderAndFeeds.value.forEach { feed -> for (feed in folderAndFeeds.value) {
val feedOutline = Outline(feed.name, feed.url, feed.siteUrl) val feedOutline = Outline(feed.name, feed.url, feed.siteUrl)
feedOutlines += feedOutline feedOutlines += feedOutline
}
outline.outlines = feedOutlines
outlines += outline
} else {
for (feed in folderAndFeeds.value) {
outlines += Outline(feed.name, feed.url, feed.siteUrl)
}
} }
outline.outlines = feedOutlines
outlines += outline
} }
val head = Head("Subscriptions") val head = Head("Subscriptions")

View File

@ -5,8 +5,8 @@ import org.simpleframework.xml.ElementList
import org.simpleframework.xml.Root import org.simpleframework.xml.Root
@Root(name = "outline", strict = false) @Root(name = "outline", strict = false)
data class Outline(@field:Attribute(required = false) var title: String?, data class Outline(@field:Attribute(required = false) private var title: String?,
@field:Attribute(required = false) var text: String?, @field:Attribute(required = false) private var text: String?,
@field:Attribute(required = false) var type: String?, @field:Attribute(required = false) var type: String?,
@field:Attribute(required = false) var xmlUrl: String?, @field:Attribute(required = false) var xmlUrl: String?,
@field:Attribute(required = false) var htmlUrl: String?, @field:Attribute(required = false) var htmlUrl: String?,
@ -23,7 +23,10 @@ data class Outline(@field:Attribute(required = false) var title: String?,
null, null,
null) null)
constructor(title: String) : this(title, null, null, null, null, null) constructor(title: String?) : this(title, title, null, null, null, null)
constructor(title: String, xmlUrl: String, htmlUrl: String) : this(title, title, "rss", xmlUrl, htmlUrl, null) constructor(title: String?, xmlUrl: String, htmlUrl: String?) : this(title, title, "rss", xmlUrl, htmlUrl, null)
val name: String?
get() = title ?: text
} }