Added hands-on lab

This commit is contained in:
Svetlana Isakova
2019-05-30 12:39:21 +02:00
parent 56f44244cf
commit 7345ff5302
26 changed files with 949 additions and 0 deletions

BIN
resources/ajax-loader.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

11
resources/logback.xml Normal file
View File

@ -0,0 +1,11 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%r [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>

View File

@ -0,0 +1,208 @@
package contributors
import contributors.Contributors.LoadingStatus.*
import contributors.Variant.*
import kotlinx.coroutines.*
import kotlinx.coroutines.swing.Swing
import tasks.*
import java.awt.event.ActionListener
import javax.swing.SwingUtilities
import kotlin.coroutines.CoroutineContext
enum class Variant {
BLOCKING, // Request1Blocking
BACKGROUND, // Request2Background
CALLBACKS, // Request3Callbacks
SUSPEND, // Request4Coroutine
CONCURRENT, // Request5Concurrent
NOT_CANCELLABLE, // Request6NotCancellable
PROGRESS, // Request6Progress
CHANNELS // Request7Channels
}
interface Contributors: CoroutineScope {
val job: Job
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
fun init() {
// Start a new loading on 'load' click
addLoadListener {
saveParams()
loadContributors()
}
// Save preferences and exit on closing the window
addOnWindowClosingListener {
job.cancel()
saveParams()
System.exit(0)
}
// Load stored params (user & password values)
loadInitialParams()
}
fun loadContributors() {
val (username, password, org, _) = getParams()
val req = RequestData(username, password, org)
clearResults()
val service = createGitHubService(req.username, req.password)
val startTime = System.currentTimeMillis()
when (getSelectedVariant()) {
BLOCKING -> { // Blocking UI thread
val users = loadContributorsBlocking(service, req)
updateResults(users, startTime)
}
BACKGROUND -> { // Blocking a background thread
loadContributorsBackground(service, req) { users ->
SwingUtilities.invokeLater {
updateResults(users, startTime)
}
}
}
CALLBACKS -> { // Using callbacks
loadContributorsCallbacks(service, req) { users ->
SwingUtilities.invokeLater {
updateResults(users, startTime)
}
}
}
SUSPEND -> { // Using coroutines
launch {
val users = loadContributorsSuspend(service, req)
updateResults(users, startTime)
}.setUpCancellation()
}
CONCURRENT -> { // Performing requests concurrently
launch(Dispatchers.Default) {
val users = loadContributorsConcurrent(service, req)
withContext(Dispatchers.Main) {
updateResults(users, startTime)
}
}.setUpCancellation()
}
NOT_CANCELLABLE -> { // Performing requests in a non-cancellable way
launch {
val users = loadContributorsNotCancellable(service, req)
updateResults(users, startTime)
}.setUpCancellation()
}
PROGRESS -> { // Showing progress
launch(Dispatchers.Default) {
loadContributorsProgress(service, req) { users, completed ->
withContext(Dispatchers.Main) {
updateResults(users, startTime, completed)
}
}
}.setUpCancellation()
}
CHANNELS -> { // Performing requests concurrently and showing progress
launch(Dispatchers.Default) {
loadContributorsChannels(service, req) { users, completed ->
withContext(Dispatchers.Main) {
updateResults(users, startTime, completed)
}
}
}.setUpCancellation()
}
}
}
private enum class LoadingStatus { COMPLETED, CANCELED, IN_PROGRESS }
private fun clearResults() {
updateContributors(listOf())
updateLoadingStatus(IN_PROGRESS)
setActionsStatus(newLoadingEnabled = false)
}
private fun updateResults(
users: List<User>,
startTime: Long,
completed: Boolean = true
) {
updateContributors(users)
updateLoadingStatus(if (completed) COMPLETED else IN_PROGRESS, startTime)
if (completed) {
setActionsStatus(newLoadingEnabled = true)
}
}
private fun updateLoadingStatus(
status: LoadingStatus,
startTime: Long? = null
) {
val time = if (startTime != null) {
val time = System.currentTimeMillis() - startTime
"${(time / 1000)}.${time % 1000 / 100} sec"
} else ""
val text = "Loading status: " +
when (status) {
COMPLETED -> "completed in $time"
IN_PROGRESS -> "in progress $time"
CANCELED -> "canceled"
}
setLoadingStatus(text, status == IN_PROGRESS)
}
private fun Job.setUpCancellation() {
// make active the 'cancel' button
setActionsStatus(newLoadingEnabled = false, cancellationEnabled = true)
val loadingJob = this
// cancel the loading job if the 'cancel' button was clicked
val listener = ActionListener {
loadingJob.cancel()
updateLoadingStatus(CANCELED)
}
addCancelListener(listener)
// update the status and remove the listener after the loading job is completed
launch {
loadingJob.join()
setActionsStatus(newLoadingEnabled = true)
removeCancelListener(listener)
}
}
fun loadInitialParams() {
setParams(loadStoredParams())
}
fun saveParams() {
val params = getParams()
if (params.username.isEmpty() && params.password.isEmpty()) {
removeStoredParams()
}
else {
saveParams(params)
}
}
fun getSelectedVariant(): Variant
fun updateContributors(users: List<User>)
fun setLoadingStatus(text: String, iconRunning: Boolean)
fun setActionsStatus(newLoadingEnabled: Boolean, cancellationEnabled: Boolean = false)
fun addCancelListener(listener: ActionListener)
fun removeCancelListener(listener: ActionListener)
fun addLoadListener(listener: () -> Unit)
fun addOnWindowClosingListener(listener: () -> Unit)
fun setParams(params: Params)
fun getParams(): Params
}

View File

@ -0,0 +1,154 @@
package contributors
import kotlinx.coroutines.Job
import java.awt.Dimension
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import java.awt.Insets
import java.awt.event.ActionListener
import java.awt.event.WindowAdapter
import java.awt.event.WindowEvent
import javax.swing.*
import javax.swing.table.DefaultTableModel
private val INSETS = Insets(3, 10, 3, 10)
private val COLUMNS = arrayOf("Login", "Contributions")
@Suppress("CONFLICTING_INHERITED_JVM_DECLARATIONS")
class ContributorsUI : JFrame("GitHub Contributors"), Contributors {
private val username = JTextField(20)
private val password = JPasswordField(20)
private val org = JTextField(20)
private val variant = JComboBox<Variant>(Variant.values())
private val load = JButton("Load contributors")
private val cancel = JButton("Cancel").apply { isEnabled = false }
private val resultsModel = DefaultTableModel(COLUMNS, 0)
private val results = JTable(resultsModel)
private val resultsScroll = JScrollPane(results).apply {
preferredSize = Dimension(200, 200)
}
private val loadingIcon = ImageIcon(javaClass.classLoader.getResource("ajax-loader.gif"))
private val loadingStatus = JLabel("Start new loading", loadingIcon, SwingConstants.CENTER)
override val job = Job()
init {
// Create UI
rootPane.contentPane = JPanel(GridBagLayout()).apply {
addLabeled("GitHub Username", username)
addLabeled("Password/Token", password)
addWideSeparator()
addLabeled("Organization", org)
addLabeled("Variant", variant)
addWideSeparator()
addWide(JPanel().apply {
add(load)
add(cancel)
})
addWide(resultsScroll) {
weightx = 1.0
weighty = 1.0
fill = GridBagConstraints.BOTH
}
addWide(loadingStatus)
}
// Initialize actions
init()
}
override fun getSelectedVariant(): Variant = variant.getItemAt(variant.selectedIndex)
override fun updateContributors(users: List<User>) {
if (users.isNotEmpty()) {
log.info("Updating result with ${users.size} rows")
}
else {
log.info("Clearing result")
}
resultsModel.setDataVector(users.map {
arrayOf(it.login, it.contributions)
}.toTypedArray(), COLUMNS)
}
override fun setLoadingStatus(text: String, iconRunning: Boolean) {
loadingStatus.text = text
loadingStatus.icon = if (iconRunning) loadingIcon else null
}
override fun addCancelListener(listener: ActionListener) {
cancel.addActionListener(listener)
}
override fun removeCancelListener(listener: ActionListener) {
cancel.removeActionListener(listener)
}
override fun addLoadListener(listener: () -> Unit) {
load.addActionListener { listener() }
}
override fun addOnWindowClosingListener(listener: () -> Unit) {
addWindowListener(object : WindowAdapter() {
override fun windowClosing(e: WindowEvent?) {
listener()
}
})
}
override fun setActionsStatus(newLoadingEnabled: Boolean, cancellationEnabled: Boolean) {
load.isEnabled = newLoadingEnabled
cancel.isEnabled = cancellationEnabled
}
override fun setParams(params: Params) {
username.text = params.username
password.text = params.password
org.text = params.org
variant.selectedIndex = params.variant.ordinal
}
override fun getParams(): Params {
return Params(username.text, password.password.joinToString(""), org.text, getSelectedVariant())
}
}
fun JPanel.addLabeled(label: String, component: JComponent) {
add(JLabel(label), GridBagConstraints().apply {
gridx = 0
insets = INSETS
})
add(component, GridBagConstraints().apply {
gridx = 1
insets = INSETS
anchor = GridBagConstraints.WEST
fill = GridBagConstraints.HORIZONTAL
weightx = 1.0
})
}
fun JPanel.addWide(component: JComponent, constraints: GridBagConstraints.() -> Unit = {}) {
add(component, GridBagConstraints().apply {
gridx = 0
gridwidth = 2
insets = INSETS
constraints()
})
}
fun JPanel.addWideSeparator() {
addWide(JSeparator()) {
fill = GridBagConstraints.HORIZONTAL
}
}
fun setDefaultFontSize(size: Float) {
for (key in UIManager.getLookAndFeelDefaults().keys.toTypedArray()) {
if (key.toString().toLowerCase().contains("font")) {
val font = UIManager.getDefaults().getFont(key) ?: continue
val newFont = font.deriveFont(size)
UIManager.put(key, newFont)
}
}
}

View File

@ -0,0 +1,92 @@
package contributors
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import io.reactivex.Flowable
import io.reactivex.Single
import io.reactivex.Observable
//import com.jakewharton.retrofit2.adapter.kotlin.coroutines.CoroutineCallAdapterFactory
import okhttp3.OkHttpClient
import retrofit2.Call
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.jackson.JacksonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
import java.util.*
interface GitHubService {
@GET("orgs/{org}/repos?per_page=100")
fun getOrgReposCall(
@Path("org") org: String
): Call<List<Repo>>
@GET("repos/{owner}/{repo}/contributors?per_page=100")
fun getRepoContributorsCall(
@Path("owner") owner: String,
@Path("repo") repo: String
): Call<List<User>>
@GET("orgs/{org}/repos?per_page=100")
suspend fun getOrgRepos(
@Path("org") org: String
): Response<List<Repo>>
@GET("repos/{owner}/{repo}/contributors?per_page=100")
suspend fun getRepoContributors(
@Path("owner") owner: String,
@Path("repo") repo: String
): Response<List<User>>
@GET("orgs/{org}/repos?per_page=100")
fun getOrgReposRx(
@Path("org") org: String
): Observable<Response<List<Repo>>>
@GET("repos/{owner}/{repo}/contributors?per_page=100")
fun getRepoContributorsRx(
@Path("owner") owner: String,
@Path("repo") repo: String
): Observable<Response<List<User>>>
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class Repo(
val id: Long,
val name: String
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class User(
val login: String,
val contributions: Int
)
data class RequestData(
val username: String,
val password: String,
val org: String
)
fun createGitHubService(username: String, password: String): GitHubService {
val authToken = "Basic " + Base64.getEncoder().encode("$username:$password".toByteArray()).toString(Charsets.UTF_8)
val httpClient = OkHttpClient.Builder()
.addInterceptor { chain ->
val original = chain.request()
val builder = original.newBuilder()
.header("Accept", "application/vnd.github.v3+json")
.header("Authorization", authToken)
val request = builder.build()
chain.proceed(request)
}
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(JacksonConverterFactory.create(jacksonObjectMapper()))
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(httpClient)
.build()
return retrofit.create(GitHubService::class.java)
}

View File

@ -0,0 +1,39 @@
package contributors
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import retrofit2.Response
val log: Logger = LoggerFactory.getLogger("Contributors")
/*fun logRepos(req: RequestData, repos: List<Repo>) {
log.info("${req.org}: loaded ${repos.size} repos")
}
fun logUsers(repo: Repo, users: List<User>) {
log.info("${repo.name}: loaded ${users.size} contributors")
}*/
fun log(msg: String?) {
log.info(msg)
}
fun logRepos(req: RequestData, response: Response<List<Repo>>) {
val repos = response.body()
if (!response.isSuccessful || repos == null) {
log.error("Failed loading repos for ${req.org} with response: '${response.code()}: ${response.message()}'")
}
else {
log.info("${req.org}: loaded ${repos.size} repos")
}
}
fun logUsers(repo: Repo, response: Response<List<User>>) {
val users = response.body()
if (!response.isSuccessful || users == null) {
log.error("Failed loading contributors for ${repo.name} with response '${response.code()}: ${response.message()}'")
}
else {
log.info("${repo.name}: loaded ${users.size} contributors")
}
}

View File

@ -0,0 +1,32 @@
package contributors
import java.util.prefs.Preferences
private fun prefNode(): Preferences = Preferences.userRoot().node("ContributorsUI")
data class Params(val username: String, val password: String, val org: String, val variant: Variant)
fun loadStoredParams(): Params {
return prefNode().run {
Params(
get("username", ""),
get("password", ""),
get("org", "kotlin"),
Variant.valueOf(get("variant", Variant.BLOCKING.name))
)
}
}
fun removeStoredParams() {
prefNode().removeNode()
}
fun saveParams(params: Params) {
prefNode().apply {
put("username", params.username)
put("password", params.password)
put("org", params.org)
put("variant", params.variant.name)
sync()
}
}

10
src/contributors/main.kt Normal file
View File

@ -0,0 +1,10 @@
package contributors
fun main() {
setDefaultFontSize(18f)
ContributorsUI().apply {
pack()
setLocationRelativeTo(null)
isVisible = true
}
}

12
src/tasks/Aggregation.kt Normal file
View File

@ -0,0 +1,12 @@
package tasks
import contributors.User
// TODO: Write aggregation code.
// In the initial list each user is present several times, once for each
// repository he or she contributed to.
// Merge duplications: each user should be present only once in the resulting list
// with the total value of contributions for all the repositories.
// Users should be sorted in a descending order by their contributions.
fun List<User>.aggregate(): List<User> =
this

View File

@ -0,0 +1,24 @@
package tasks
import contributors.*
import retrofit2.Response
fun loadContributorsBlocking(service: GitHubService, req: RequestData) : List<User> {
val repos = service
.getOrgReposCall(req.org)
.execute() // Executes request and blocks the current thread
.also { logRepos(req, it) }
.body() ?: listOf()
return repos.flatMap { repo ->
service
.getRepoContributorsCall(req.org, repo.name)
.execute() // Executes request and blocks the current thread
.also { logUsers(repo, it) }
.bodyList()
}.aggregate()
}
fun <T> Response<List<T>>.bodyList(): List<T> {
return body() ?: listOf()
}

View File

@ -0,0 +1,12 @@
package tasks
import contributors.GitHubService
import contributors.RequestData
import contributors.User
import kotlin.concurrent.thread
fun loadContributorsBackground(service: GitHubService, req: RequestData, updateResults: (List<User>) -> Unit) {
thread {
loadContributorsBlocking(service, req)
}
}

View File

@ -0,0 +1,37 @@
package tasks
import contributors.*
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
fun loadContributorsCallbacks(service: GitHubService, req: RequestData, updateResults: (List<User>) -> Unit) {
service.getOrgReposCall(req.org).onResponse { responseRepos ->
logRepos(req, responseRepos)
val repos = responseRepos.bodyList()
val allUsers = mutableListOf<User>()
for (repo in repos) {
service.getRepoContributorsCall(req.org, repo.name).onResponse { responseUsers ->
logUsers(repo, responseUsers)
val users = responseUsers.bodyList()
allUsers += users
}
}
// TODO: Why this code doesn't work? How to fix that?
updateResults(allUsers.aggregate())
}
}
inline fun <T> Call<T>.onResponse(crossinline callback: (Response<T>) -> Unit) {
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
callback(response)
}
override fun onFailure(call: Call<T>, t: Throwable) {
log.error("Call failed", t)
}
})
}

View File

@ -0,0 +1,7 @@
package tasks
import contributors.*
suspend fun loadContributorsSuspend(service: GitHubService, req: RequestData): List<User> {
TODO()
}

View File

@ -0,0 +1,8 @@
package tasks
import contributors.*
import kotlinx.coroutines.*
suspend fun loadContributorsConcurrent(service: GitHubService, req: RequestData): List<User> = coroutineScope {
TODO()
}

View File

@ -0,0 +1,9 @@
package tasks
import contributors.*
import kotlinx.coroutines.*
import kotlin.coroutines.coroutineContext
suspend fun loadContributorsNotCancellable(service: GitHubService, req: RequestData): List<User> {
TODO()
}

View File

@ -0,0 +1,11 @@
package tasks
import contributors.*
suspend fun loadContributorsProgress(
service: GitHubService,
req: RequestData,
updateResults: suspend (List<User>, completed: Boolean) -> Unit
) {
TODO()
}

View File

@ -0,0 +1,16 @@
package tasks
import contributors.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
suspend fun loadContributorsChannels(
service: GitHubService,
req: RequestData,
updateResults: suspend (List<User>, completed: Boolean) -> Unit
) {
coroutineScope {
TODO()
}
}

View File

@ -0,0 +1,36 @@
package contributors
import io.reactivex.Observable
import kotlinx.coroutines.delay
import retrofit2.Call
import retrofit2.Response
import retrofit2.mock.Calls
object MockGithubService : GitHubService {
override fun getOrgReposCall(org: String): Call<List<Repo>> {
return Calls.response(repos)
}
override fun getRepoContributorsCall(owner: String, repo: String): Call<List<User>> {
return Calls.response(reposMap.getValue(repo).users)
}
override suspend fun getOrgRepos(org: String): Response<List<Repo>> {
delay(getReposDelay)
return Response.success(repos)
}
override suspend fun getRepoContributors(owner: String, repo: String): Response<List<User>> {
val testRepo = reposMap.getValue(repo)
delay(testRepo.delay)
return Response.success(testRepo.users)
}
override fun getOrgReposRx(org: String): Observable<Response<List<Repo>>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun getRepoContributorsRx(owner: String, repo: String): Observable<Response<List<User>>> {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}

View File

@ -0,0 +1,73 @@
package contributors
val testRequestData = RequestData("username", "password", "org")
data class TestRepo(val name: String, val delay: Long, val users: List<User>)
data class TestResults(val timeFromStart: Long, val users: List<User>)
val testRepos = listOf(
TestRepo(
"repo-1", 1000, listOf(
User("user-1", 10),
User("user-2", 20)
)
),
TestRepo(
"repo-2", 1200, listOf(
User("user-2", 30),
User("user-1", 40)
)
),
TestRepo(
"repo-3", 800, listOf(
User("user-2", 50),
User("user-3", 60)
)
)
)
const val getReposDelay = 1000L
val repos = testRepos.mapIndexed { index, testRepo -> Repo(index.toLong(), testRepo.name) }
val reposMap = testRepos.associate { it.name to it }
val expectedResults = TestResults(
4000,
listOf(
User("user-2", 100),
User("user-3", 60),
User("user-1", 50)
)
)
val expectedConcurrentResults = TestResults(
2200,
expectedResults.users
)
val progressResults = listOf(
TestResults(
2000,
listOf(User(login = "user-2", contributions = 20), User(login = "user-1", contributions = 10))
),
TestResults(
3200,
listOf(User(login = "user-2", contributions = 50), User(login = "user-1", contributions = 50))
),
expectedResults
)
val concurrentProgressResults = listOf(
TestResults(
1800,
listOf(User(login = "user-3", contributions = 60), User(login = "user-2", contributions = 50))
),
TestResults(
2000,
listOf(User(login = "user-2", contributions = 70), User(login = "user-3", contributions = 60),
User(login = "user-1", contributions = 10))
),
expectedConcurrentResults
)

View File

@ -0,0 +1,22 @@
package tasks
import contributors.User
import org.junit.Assert
import org.junit.Test
class AggregationKtTest {
@Test
fun testAggregation() {
val actual = listOf(
User("Alice", 1), User("Bob", 3),
User("Alice", 2), User("Bob", 7),
User("Charlie", 3), User("Alice", 5)
).aggregate()
val expected = listOf(
User("Bob", 10),
User("Alice", 8),
User("Charlie", 3)
)
Assert.assertEquals("Wrong result for 'aggregation'", expected, actual)
}
}

View File

@ -0,0 +1,17 @@
package tasks
import contributors.MockGithubService
import contributors.expectedResults
import contributors.testRequestData
import org.junit.Assert
import org.junit.Test
class Request1BlockingKtTest {
@Test
fun testAggregation() {
val users = loadContributorsBlocking(MockGithubService, testRequestData)
Assert.assertEquals("List of contributors should be sorted" +
"by the number of contributions in a descending order",
expectedResults.users, users)
}
}

View File

@ -0,0 +1,17 @@
package tasks
import contributors.*
import org.junit.Assert
import org.junit.Test
class Request3CallbacksKtTest {
@Test
fun testDataIsLoaded() {
loadContributorsCallbacks(MockGithubService, testRequestData) {
Assert.assertEquals(
"Wrong result for 'loadContributorsCallbacks'",
expectedResults.users, it
)
}
}
}

View File

@ -0,0 +1,26 @@
package tasks
import contributors.MockGithubService
import contributors.expectedConcurrentResults
import contributors.expectedResults
import contributors.testRequestData
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert
import org.junit.Test
@UseExperimental(ExperimentalCoroutinesApi::class)
class Request4SuspendKtTest {
@Test
fun testSuspend() = runBlockingTest {
val startTime = currentTime
val result = loadContributorsSuspend(MockGithubService, testRequestData)
Assert.assertEquals("Wrong result for 'loadContributorsSuspend'", expectedResults.users, result)
val virtualTime = currentTime - startTime
Assert.assertEquals(
"The calls run consequently, so the total virtual time should be 4000 ms: " +
"1000 for repos request plus (1000 + 1200 + 800) = 3000 for contributors sequential requests)",
expectedResults.timeFromStart, virtualTime
)
}
}

View File

@ -0,0 +1,26 @@
package tasks
import contributors.MockGithubService
import contributors.expectedConcurrentResults
import contributors.expectedResults
import contributors.testRequestData
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert
import org.junit.Test
@UseExperimental(ExperimentalCoroutinesApi::class)
class Request5ConcurrentKtTest {
@Test
fun testConcurrent() = runBlockingTest {
val startTime = currentTime
val result = loadContributorsConcurrent(MockGithubService, testRequestData)
Assert.assertEquals("Wrong result for 'loadContributorsConcurrent'", expectedConcurrentResults.users, result)
val virtualTime = currentTime - startTime
Assert.assertEquals(
"The calls run concurrently, so the total virtual time should be 2200 ms: " +
"1000 for repos request plus max(1000, 1200, 800) = 1200 for contributors concurrent requests)",
expectedConcurrentResults.timeFromStart, virtualTime
)
}
}

View File

@ -0,0 +1,24 @@
package tasks
import contributors.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert
import org.junit.Test
@UseExperimental(ExperimentalCoroutinesApi::class)
class Request6ProgressKtTest {
@Test
fun testProgress() = runBlockingTest {
val startTime = currentTime
var index = 0
loadContributorsProgress(MockGithubService, testRequestData) {
users, _ ->
val expected = progressResults[index++]
val virtualTime = currentTime - startTime
Assert.assertEquals("Expected intermediate results after virtual ${expected.timeFromStart} ms:",
expected.timeFromStart, virtualTime)
Assert.assertEquals("Wrong progress result after $virtualTime:", expected.users, users)
}
}
}

View File

@ -0,0 +1,26 @@
package tasks
import contributors.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert
import org.junit.Test
@UseExperimental(ExperimentalCoroutinesApi::class)
class Request7ChannelsKtTest {
@Test
fun testChannels() = runBlockingTest {
val startTime = currentTime
var index = 0
loadContributorsChannels(MockGithubService, testRequestData) {
users, _ ->
val expected = concurrentProgressResults[index++]
val virtualTime = currentTime - startTime
Assert.assertEquals("Expected intermediate results after virtual ${expected.timeFromStart} ms:",
expected.timeFromStart, virtualTime)
Assert.assertEquals("Wrong progress result after $virtualTime:", expected.users, users)
}
}
}