Basic integration test confirming that dackka can be invoked from the command line

Bug: 161384734
Test: ./gradlew test

Change-Id: Ife1d82c34d2eb8a8c282da667a692c13a5b15e40
diff --git a/integration-tests/cli/build.gradle.kts b/integration-tests/cli/build.gradle.kts
new file mode 100644
index 0000000..e7a1ebc
--- /dev/null
+++ b/integration-tests/cli/build.gradle.kts
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed 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.
+ */
+
+// This project confirms that Dokka can be invoked as a .jar from the command line
+
+val generatedDir = project.file("${buildDir}/docs")
+
+configurations.create("runnerJar")
+
+dependencies.add(
+    "runnerJar",
+    project.dependencies.project(
+        mapOf(
+            "path" to ":",
+            "configuration" to "shadow"
+        )
+    )
+)
+tasks.register<Copy>("generateConfig") {
+    from("src/test/resources")
+    into("${project.buildDir}/resources")
+    filter({ text ->
+        // Dackka requires absolute paths, so expand "$projectDir" into its value
+        text.replace("\$projectDir", "$projectDir")
+    })
+}
+
+tasks.register<JavaExec>("run") {
+    description = "Ensures that dackka can be invoked as a .jar from the command line"
+    dependsOn(project.configurations.getByName("runnerJar"))
+    dependsOn("generateConfig")
+    classpath = files({
+        project.configurations.getByName("runnerJar").resolvedConfiguration.files
+    })
+    args = listOf("${project.buildDir}/resources/config.json")
+    doFirst {
+        generatedDir.deleteRecursively()
+    }
+}
+
+tasks.register<Task>("verifyRun") {
+    dependsOn("run")
+    doLast {
+        val expectedPaths = listOf("index.html",
+            "androidx.paging/Pager.html",
+            "androidx.paging/PagingDataKt.html",
+            "androidx.paging/PagingData.html")
+        for (relativePath in expectedPaths) {
+            val path = "${generatedDir}/$relativePath"
+            if (!file(path).exists()) {
+                throw GradleException("Failed to create " + path)
+            }
+        }
+    }
+}
+tasks.register<Task>("test") {
+    dependsOn("verifyRun")
+}
diff --git a/integration-tests/cli/src/test/resources/config.json b/integration-tests/cli/src/test/resources/config.json
new file mode 100644
index 0000000..9317d95
--- /dev/null
+++ b/integration-tests/cli/src/test/resources/config.json
@@ -0,0 +1,27 @@
+{
+  "format": "javadoc",
+  "outputDir": "build/docs",
+  "sourceSets": [
+    {
+      "moduleDisplayName": "Sample",
+      "externalDocumentationLinks": [
+        {
+          "url": "https://kotlinlang.org/api/latest/jvm/stdlib/",
+          "packageListUrl": "https://kotlinlang.org/api/latest/jvm/stdlib/package-list"
+        },
+        {
+          "url": "https://docs.oracle.com/javase/8/docs/api/",
+          "packageListUrl": "https://docs.oracle.com/javase/8/docs/api/package-list"
+        }
+      ],
+      "sourceSetID": {
+        "moduleName": "sample",
+        "sourceSetName": "main"
+      },
+      "sourceRoots": [
+        {"path":"$projectDir/src/testData"}
+      ]
+    }
+  ]
+}
+
diff --git a/integration-tests/cli/src/testData/androidx/paging/CombinedLoadStates.kt b/integration-tests/cli/src/testData/androidx/paging/CombinedLoadStates.kt
new file mode 100644
index 0000000..a77b3e2
--- /dev/null
+++ b/integration-tests/cli/src/testData/androidx/paging/CombinedLoadStates.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed 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 androidx.paging
+
+/**
+ * Collection of pagination [LoadState]s for both a [PagingSource], and [RemoteMediator].
+ */
+data class CombinedLoadStates(
+    /**
+     * [LoadStates] corresponding to loads from a [PagingSource].
+     */
+    val source: LoadStates,
+
+    /**
+     * [LoadStates] corresponding to loads from a [RemoteMediator], or `null` if RemoteMediator
+     * not present.
+     */
+    val mediator: LoadStates? = null
+) {
+    val refresh: LoadState = (mediator ?: source).refresh
+    val prepend: LoadState = (mediator ?: source).prepend
+    val append: LoadState = (mediator ?: source).append
+
+    inline fun forEach(op: (LoadType, Boolean, LoadState) -> Unit) {
+        source.forEach { type, state ->
+            op(type, false, state)
+        }
+        mediator?.forEach { type, state ->
+            op(type, true, state)
+        }
+    }
+
+    internal companion object {
+        val IDLE_SOURCE = CombinedLoadStates(
+            source = LoadStates.IDLE
+        )
+        val IDLE_MEDIATOR = CombinedLoadStates(
+            source = LoadStates.IDLE,
+            mediator = LoadStates.IDLE
+        )
+    }
+}
diff --git a/integration-tests/cli/src/testData/androidx/paging/Pager.kt b/integration-tests/cli/src/testData/androidx/paging/Pager.kt
new file mode 100644
index 0000000..049ef37
--- /dev/null
+++ b/integration-tests/cli/src/testData/androidx/paging/Pager.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2020 The Android Open Source Project
+ *
+ * Licensed 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 androidx.paging
+
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Primary entry point into Paging; constructor for a reactive stream of [PagingData].
+ *
+ * Each [PagingData] represents a snapshot of the backing paginated data. Updates to the backing
+ * dataset should be represented by a new instance of [PagingData].
+ *
+ * [PagingSource.invalidate] and calls to [AsyncPagingDataDiffer.refresh] or
+ * [PagingDataAdapter.refresh] will notify [Pager] that the backing dataset has been updated and a
+ * new [PagingData] / [PagingSource] pair will be generated to represent an updated snapshot.
+ *
+ * [PagingData] can be transformed to alter data as it loads, and presented in a `RecyclerView` via
+ * `AsyncPagingDataDiffer` or `PagingDataAdapter`.
+ *
+ * LiveData support is available as an extension property provided by the
+ * `androidx.paging:paging-runtime` artifact.
+ *
+ * RxJava support is available as extension properties provided by the
+ * `androidx.paging:paging-rxjava2` artifact.
+ */
+class Pager<Key : Any, Value : Any>
+@JvmOverloads constructor(
+    config: PagingConfig,
+    initialKey: Key? = null,
+    remoteMediator: RemoteMediator<Key, Value>? = null,
+    pagingSourceFactory: () -> PagingSource<Key, Value>
+) {
+    /**
+     * A cold [Flow] of [PagingData], which emits new instances of [PagingData] once they become
+     * invalidated by [PagingSource.invalidate] or calls to [AsyncPagingDataDiffer.refresh] or
+     * [PagingDataAdapter.refresh].
+     */
+    val flow: Flow<PagingData<Value>> = PageFetcher(
+        pagingSourceFactory,
+        initialKey,
+        config,
+        remoteMediator
+    ).flow
+}
diff --git a/integration-tests/cli/src/testData/androidx/paging/PagingData.kt b/integration-tests/cli/src/testData/androidx/paging/PagingData.kt
new file mode 100644
index 0000000..c73c34b
--- /dev/null
+++ b/integration-tests/cli/src/testData/androidx/paging/PagingData.kt
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed 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 androidx.paging
+
+import androidx.annotation.CheckResult
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+
+/**
+ * Container for Paged data from a single generation of loads.
+ *
+ * Each refresh of data (generally either pushed by local storage, or pulled from the network)
+ * will have a separate corresponding [PagingData].
+ */
+class PagingData<T : Any> internal constructor(
+    internal val flow: Flow<PageEvent<T>>,
+    internal val receiver: UiReceiver
+) {
+    /**
+     * Returns a [PagingData] containing the result of applying the given [transform] to each
+     * element, as it is loaded.
+     *
+     * @see map
+     */
+    @JvmName("map")
+    @CheckResult
+    fun <R : Any> mapSync(transform: (T) -> R): PagingData<R> = transform { event ->
+        event.map { transform(it) }
+    }
+
+    /**
+     * Returns a [PagingData] of all elements returned from applying the given [transform]
+     * to each element, as it is loaded.
+     *
+     * @see flatMap
+     */
+    @JvmName("flatMap")
+    @CheckResult
+    fun <R : Any> flatMapSync(transform: (T) -> Iterable<R>): PagingData<R> = transform { event ->
+        event.flatMap { transform(it) }
+    }
+
+    /**
+     * Returns a [PagingData] containing only elements matching the given [predicate]
+     *
+     * @see filter
+     */
+    @JvmName("filter")
+    @CheckResult
+    fun filterSync(predicate: (T) -> Boolean): PagingData<T> = transform { event ->
+        event.filter { predicate(it) }
+    }
+
+    /**
+     * Returns a [PagingData] containing each original element, with the passed header [item] added
+     * to the start of the list.
+     *
+     * The header [item] is added to a loaded page which marks the end of the data stream in the
+     * prepend direction by returning null in [PagingSource.LoadResult.Page.prevKey]. It will be
+     * removed if the first page in the list is dropped, which can happen in the case of loaded
+     * pages exceeding [PagedList.Config.maxSize].
+     *
+     * Note: This operation is not idempotent, calling it multiple times will continually add
+     * more headers to the start of the list, which can be useful if multiple header items are
+     * required.
+     *
+     * @see [insertFooterItem]
+     */
+    @CheckResult
+    fun insertHeaderItem(item: T) = insertSeparators { before, _ ->
+        if (before == null) item else null
+    }
+
+    /**
+     * Returns a [PagingData] containing each original element, with the passed footer [item] added
+     * to the end of the list.
+     *
+     * The footer [item] is added to a loaded page which marks the end of the data stream in the
+     * append direction, either by returning null in [PagingSource.LoadResult.Page.nextKey]. It
+     * will be removed if the first page in the list is dropped, which can happen in the case of
+     * loaded* pages exceeding [PagedList.Config.maxSize].
+     *
+     * Note: This operation is not idempotent, calling it multiple times will continually add
+     * more footers to the end of the list, which can be useful if multiple footer items are
+     * required.
+     *
+     * @see [insertHeaderItem]
+     */
+    @CheckResult
+    fun insertFooterItem(item: T) = insertSeparators { _, after ->
+        if (after == null) item else null
+    }
+
+    companion object {
+        internal val NOOP_RECEIVER = object : UiReceiver {
+            override fun addHint(hint: ViewportHint) {}
+
+            override fun retry() {}
+
+            override fun refresh() {}
+        }
+
+        private val EMPTY = PagingData<Any>(
+            flow = flowOf(PageEvent.Insert.EMPTY_REFRESH_LOCAL),
+            receiver = NOOP_RECEIVER
+        )
+
+        @Suppress("UNCHECKED_CAST", "SyntheticAccessor")
+        @JvmStatic // Convenience for Java developers.
+        fun <T : Any> empty() = EMPTY as PagingData<T>
+
+        // NOTE: samples in the doc below are manually imported from Java code in the samples
+        // project, since Java cannot be linked with @sample.
+        // DO NOT CHANGE THE BELOW COMMENT WITHOUT MAKING THE CORRESPONDING CHANGE IN `samples/`
+        /**
+         * Returns a [PagingData] containing each original element, with an optional separator
+         * generated by [generator], given the elements before and after (or null, in boundary
+         * conditions).
+         *
+         * Note that this transform is applied asynchronously, as pages are loaded. Potential
+         * separators between pages are only computed once both pages are loaded.
+         *
+         * **Kotlin callers should instead use the extension function [insertSeparators]**
+         *
+         * ```
+         * /*
+         *  * Create letter separators in an alphabetically sorted list.
+         *  *
+         *  * For example, if the input is:
+         *  *     "apple", "apricot", "banana", "carrot"
+         *  *
+         *  * The operator would output:
+         *  *     "A", "apple", "apricot", "B", "banana", "C", "carrot"
+         *  */
+         * pagingDataStream.map((pagingData) ->
+         *         // map outer stream, so we can perform transformations on each paging generation
+         *         PagingData.insertSeparators(pagingData,
+         *                 (@Nullable String before, @Nullable String after) -> {
+         *                     if (after != null && (before == null
+         *                             || before.charAt(0) != after.charAt(0))) {
+         *                         // separator - after is first item that starts with its first letter
+         *                         return Character.toString(Character.toUpperCase(after.charAt(0)));
+         *                     } else {
+         *                         // no separator - either end of list, or first
+         *                         // letters of items are the same
+         *                         return null;
+         *                     }
+         *                 }));
+         *
+         * /*
+         *  * Create letter separators in an alphabetically sorted list of Items, with UiModel objects.
+         *  *
+         *  * For example, if the input is (each an `Item`):
+         *  *     "apple", "apricot", "banana", "carrot"
+         *  *
+         *  * The operator would output a list of UiModels corresponding to:
+         *  *     "A", "apple", "apricot", "B", "banana", "C", "carrot"
+         *  */
+         * pagingDataStream.map((itemPagingData) -> {
+         *     // map outer stream, so we can perform transformations on each paging generation
+         *
+         *     // first convert items in stream to UiModel.Item
+         *     PagingData<UiModel.ItemModel> itemModelPagingData =
+         *             itemPagingData.map(UiModel.ItemModel::new);
+         *
+         *     // Now insert UiModel.Separators, which makes the PagingData of generic type UiModel
+         *     return PagingData.insertSeparators(
+         *             itemModelPagingData,
+         *             (@Nullable UiModel.ItemModel before, @Nullable UiModel.ItemModel after) -> {
+         *                 if (after != null && (before == null
+         *                         || before.item.label.charAt(0) != after.item.label.charAt(0))) {
+         *                     // separator - after is first item that starts with its first letter
+         *                     return new UiModel.SeparatorModel(
+         *                             Character.toUpperCase(after.item.label.charAt(0)));
+         *                 } else {
+         *                     // no separator - either end of list, or first
+         *                     // letters of items are the same
+         *                     return null;
+         *                 }
+         *             });
+         * });
+         *
+         * public class UiModel {
+         *     static class ItemModel extends UiModel {
+         *         public Item item;
+         *         ItemModel(Item item) {
+         *             this.item = item;
+         *         }
+         *     }
+         *     static class SeparatorModel extends UiModel {
+         *         public char character;
+         *         SeparatorModel(char character) {
+         *             this.character = character;
+         *         }
+         *     }
+         * }
+         */
+        @JvmStatic
+        @CheckResult
+        fun <T : R, R : Any> insertSeparators(
+            pagingData: PagingData<T>,
+            generator: (T?, T?) -> R?
+        ): PagingData<R> {
+            return pagingData.insertSeparators { before, after -> generator(before, after) }
+        }
+    }
+}
+
+private inline fun <T : Any, R : Any> PagingData<T>.transform(
+    crossinline transform: suspend (PageEvent<T>) -> PageEvent<R>
+) = PagingData(
+    flow = flow.map { transform(it) },
+    receiver = receiver
+)
+
+/**
+ * Returns a [PagingData] containing the result of applying the given [transform] to each
+ * element, as it is loaded.
+ */
+@CheckResult
+@JvmSynthetic
+fun <T : Any, R : Any> PagingData<T>.map(
+    transform: suspend (T) -> R
+): PagingData<R> = transform { it.map(transform) }
+
+/**
+ * Returns a [PagingData] of all elements returned from applying the given [transform]
+ * to each element, as it is loaded.
+ */
+@CheckResult
+@JvmSynthetic
+fun <T : Any, R : Any> PagingData<T>.flatMap(
+    transform: suspend (T) -> Iterable<R>
+): PagingData<R> = transform { it.flatMap(transform) }
+
+/**
+ * Returns a [PagingData] containing only elements matching the given [predicate]
+ */
+@CheckResult
+@JvmSynthetic
+fun <T : Any> PagingData<T>.filter(
+    predicate: suspend (T) -> Boolean
+): PagingData<T> = transform { it.filter(predicate) }
+
+/**
+ * Returns a [PagingData] containing each original element, with an optional separator
+ * generated by [generator], given the elements before and after (or null, in boundary
+ * conditions).
+ *
+ * Note that this transform is applied asynchronously, as pages are loaded. Potential
+ * separators between pages are only computed once both pages are loaded.
+ *
+ * @sample androidx.paging.samples.insertSeparatorsSample
+ * @sample androidx.paging.samples.insertSeparatorsUiModelSample
+ */
+@CheckResult
+@JvmSynthetic
+fun <T : R, R : Any> PagingData<T>.insertSeparators(
+    generator: suspend (T?, T?) -> R?
+): PagingData<R> {
+    // This function must be an extension method, as it indirectly imposes a constraint on
+    // the type of T (because T extends R). Ideally it would be declared not be an
+    // extension, to make this method discoverable for Java callers, but we need to support
+    // the common UI model pattern for separators:
+    //     class UiModel
+    //     class ItemModel: UiModel
+    //     class SeparatorModel: UiModel
+    return PagingData(
+        flow = flow.insertEventSeparators(generator),
+        receiver = receiver
+    )
+}
+
+internal interface UiReceiver {
+    fun addHint(hint: ViewportHint)
+    fun retry()
+    fun refresh()
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index fa6e176..627b181 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -30,3 +30,5 @@
         maven("https://dl.bintray.com/kotlin/kotlin-dev")
     }
 }
+
+include(":integration-tests:cli")