diff --git a/src/main/kotlin/app/hashers/CommitCrawler.kt b/src/main/kotlin/app/hashers/CommitCrawler.kt new file mode 100644 index 00000000..d6dbf329 --- /dev/null +++ b/src/main/kotlin/app/hashers/CommitCrawler.kt @@ -0,0 +1,116 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package app.hashers + +import app.Logger +import app.model.Commit +import app.model.DiffContent +import app.model.DiffFile +import app.model.DiffRange +import app.model.Repo +import app.utils.RepoHelper +import io.reactivex.Observable +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.diff.DiffEntry +import org.eclipse.jgit.diff.DiffFormatter +import org.eclipse.jgit.diff.RawText +import org.eclipse.jgit.errors.MissingObjectException +import org.eclipse.jgit.lib.ObjectId +import org.eclipse.jgit.revwalk.RevCommit +import org.eclipse.jgit.revwalk.RevWalk +import org.eclipse.jgit.util.io.DisabledOutputStream + +object CommitCrawler { + fun getObservable(git: Git, repo: Repo) = Observable + .create { subscriber -> + try { + val revWalk = RevWalk(git.repository) + val commitId = git.repository.resolve(RepoHelper.MASTER_BRANCH) + revWalk.markStart(revWalk.parseCommit(commitId)) + for (revCommit in revWalk) { + subscriber.onNext(Commit(revCommit)) + } + // Commits are combined in pairs, an empty commit concatenated + // to calculate the diff of the initial commit. + subscriber.onNext(Commit()) + } catch (e: Exception) { + Logger.error("Commit producing error", e) + subscriber.onError(e) + } + subscriber.onComplete() + } // TODO(anatoly): Rewrite diff calculation in non-weird way. + .pairWithNext() // Pair commits to get diff. + .map { (new, old) -> + // Mapping and stats extraction. + Logger.debug("Commit: ${new.raw?.name ?: ""}: " + + new.raw?.shortMessage) + new.diffs = getDiffFiles(git, new, old) + Logger.debug("Diff: ${new.diffs.size} entries") + new.repo = repo + new + } + + private fun getDiffFiles(git: Git, + commitNew: Commit, + commitOld: Commit): List { + val revCommitNew: RevCommit? = commitNew.raw + val revCommitOld: RevCommit? = commitOld.raw + + return DiffFormatter(DisabledOutputStream.INSTANCE).use { formatter -> + formatter.setRepository(git.repository) + formatter.setDetectRenames(true) + formatter.scan(revCommitOld?.tree, revCommitNew?.tree) + // RENAME change type doesn't change file content. + .filter { it.changeType != DiffEntry.ChangeType.RENAME } + // Skip binary files. + .filter { + val id = if (it.changeType == DiffEntry.ChangeType.DELETE) { + it.oldId.toObjectId() + } else { + it.newId.toObjectId() + } + !RawText.isBinary(git.repository.open(id).openStream()) + } + .map { diff -> + val new = getContentByObjectId(git, diff.newId.toObjectId()) + val old = getContentByObjectId(git, diff.oldId.toObjectId()) + + val edits = formatter.toFileHeader(diff).toEditList() + val path = when (diff.changeType) { + DiffEntry.ChangeType.DELETE -> diff.oldPath + else -> diff.newPath + } + DiffFile(path = path, + changeType = diff.changeType, + old = DiffContent(old, edits.map { edit -> + DiffRange(edit.beginA, edit.endA) }), + new = DiffContent(new, edits.map { edit -> + DiffRange(edit.beginB, edit.endB) })) + } + } + } + + private fun getContentByObjectId(git: Git, + objectId: ObjectId): List { + return try { + val rawText = RawText(git.repository.open(objectId).bytes) + val content = ArrayList(rawText.size()) + for (i in 0..(rawText.size() - 1)) { + content.add(rawText.getString(i)) + } + return content + } catch (e: MissingObjectException) { + listOf() + } + } + + private fun Observable.pairWithNext(): Observable> { + return this.map { emit -> Pair(emit, emit) } + // Accumulate emits by prev-next pair. + .scan { pairAccumulated, pairNext -> + Pair(pairAccumulated.second, pairNext.second) + } + .skip(1) // Skip initial not paired emit. + } +} diff --git a/src/main/kotlin/app/hashers/CommitHasher.kt b/src/main/kotlin/app/hashers/CommitHasher.kt index 326cd2bc..f3a1a05f 100644 --- a/src/main/kotlin/app/hashers/CommitHasher.kt +++ b/src/main/kotlin/app/hashers/CommitHasher.kt @@ -3,178 +3,43 @@ package app.hashers -import app.FactKey import app.Logger import app.api.Api import app.extractors.Extractor -import app.model.Author import app.model.Commit -import app.model.DiffContent -import app.model.DiffFile -import app.model.DiffRange -import app.model.Fact import app.model.LocalRepo import app.model.Repo -import app.utils.RepoHelper import io.reactivex.Observable -import io.reactivex.schedulers.Schedulers -import org.eclipse.jgit.api.Git -import org.eclipse.jgit.diff.DiffEntry -import org.eclipse.jgit.lib.Repository -import org.eclipse.jgit.revwalk.RevWalk -import java.nio.charset.Charset -import org.eclipse.jgit.diff.DiffFormatter -import org.eclipse.jgit.diff.RawText -import org.eclipse.jgit.lib.ObjectId -import org.eclipse.jgit.errors.MissingObjectException -import org.eclipse.jgit.revwalk.RevCommit -import org.eclipse.jgit.util.io.DisabledOutputStream -import java.time.LocalDateTime -import java.time.ZoneOffset import java.util.concurrent.TimeUnit /** * CommitHasher hashes repository and uploads stats to server. */ class CommitHasher(private val localRepo: LocalRepo, - private val repo: Repo = Repo(), + private val serverRepo: Repo = Repo(), private val api: Api, - private val git: Git) { + private val rehashes: List) { - private val gitRepo: Repository = git.repository - - private fun findFirstOverlappingCommit(): Commit? { - val serverHistoryCommits = repo.commits.toHashSet() - return getCommitsAsObservable() - .skipWhile { commit -> !serverHistoryCommits.contains(commit) } - .blockingFirst(null) - } - - private fun hashAndSendCommits() { - val lastKnownCommit = repo.commits.lastOrNull() - val knownCommits = repo.commits.toHashSet() - - val factsDayWeek = hashMapOf>() - val factsDayTime = hashMapOf>() - - // Commits are combined in pairs, an empty commit concatenated to - // calculate the diff of the initial commit. - Observable.concat(getCommitsAsObservable() - .doOnNext { commit -> - Logger.debug("Commit: ${commit.raw?.name ?: ""}: " - + commit.raw?.shortMessage) - commit.repo = repo - - // Calculate facts. - val author = commit.author - val factDayWeek = factsDayWeek[author] ?: Array(7) { 0 } - val factDayTime = factsDayTime[author] ?: Array(24) { 0 } - val timestamp = commit.dateTimestamp - val dateTime = LocalDateTime.ofEpochSecond(timestamp, 0, - ZoneOffset.ofTotalSeconds(commit.dateTimeZoneOffset * 60)) - // The value is numbered from 1 (Monday) to 7 (Sunday). - factDayWeek[dateTime.dayOfWeek.value - 1] += 1 - // Hour from 0 to 23. - factDayTime[dateTime.hour] += 1 - factsDayWeek[author] = factDayWeek - factsDayTime[author] = factDayTime - }, Observable.just(Commit())) - .pairWithNext() // Pair commits to get diff. - .takeWhile { (new, _) -> // Hash until last known commit. - new.rehash != lastKnownCommit?.rehash } - .filter { (new, _) -> knownCommits.isEmpty() // Don't hash known. - || !knownCommits.contains(new) } - .filter { (new, _) -> emailFilter(new) } // Email filtering. - .map { (new, old) -> // Mapping and stats extraction. - val diffFiles = getDiffFiles(new, old) - Logger.debug("Diff: ${diffFiles.size} entries") - new.stats = Extractor().extract(diffFiles) - Logger.debug("Stats: ${new.stats.size} entries") - - // Count lines on all non-binary files. This is additional - // statistics to CommitStats because not all file extensions - // may be supported. - new.numLinesAdded = diffFiles.fold(0) { total, file -> - total + file.getAllAdded().size } - new.numLinesDeleted = diffFiles.fold(0) { total, file -> - total + file.getAllDeleted().size } - new - } - .observeOn(Schedulers.io()) // Different thread for data sending. - .buffer(20, TimeUnit.SECONDS) // Group ready commits by time. - .blockingSubscribe({ commitsBundle -> // OnNext. - postCommitsToServer(commitsBundle) // Send ready commits. - }, { e -> // OnError. - Logger.error("Error while hashing: $e") - }, { // OnComplete. - val facts = mutableListOf() - factsDayTime.map { (author, list) -> - list.forEachIndexed { hour, count -> - if (count > 0) { - facts.add(Fact(repo, FactKey.COMMITS_DAY_TIME + - hour, count.toDouble(), author)) - } - } - } - factsDayWeek.map { (author, list) -> - list.forEachIndexed { day, count -> - if (count > 0) { - facts.add(Fact(repo, FactKey.COMMITS_DAY_WEEK + - day, count.toDouble(), author)) - } - } - } - postFactsToServer(facts) - }) + init { + // Delete locally missing commits from server. If found at least one + // common commit then preceding commits are not deleted because hash of + // a commit calculated including hashes of its parents. + val firstOverlapCommitRehash = findFirstOverlappingCommitRehash() + val deletedCommits = serverRepo.commits + .takeWhile { it.rehash != firstOverlapCommitRehash } + deleteCommitsOnServer(deletedCommits) } - private fun getDiffFiles(commitNew: Commit, - commitOld: Commit): List { - val revCommitNew:RevCommit? = commitNew.raw - val revCommitOld:RevCommit? = commitOld.raw - - return DiffFormatter(DisabledOutputStream.INSTANCE).use { formatter -> - formatter.setRepository(gitRepo) - formatter.setDetectRenames(true) - formatter.scan(revCommitOld?.tree, revCommitNew?.tree) - // RENAME change type doesn't change file content. - .filter { it.changeType != DiffEntry.ChangeType.RENAME } - // Skip binary files. - .filter { - val id = if (it.changeType == DiffEntry.ChangeType.DELETE) { - it.oldId.toObjectId() - } else { - it.newId.toObjectId() - } - !RawText.isBinary(gitRepo.open(id).openStream()) - } - .map { diff -> - val new = getContentByObjectId(diff.newId.toObjectId()) - val old = getContentByObjectId(diff.oldId.toObjectId()) + private fun findFirstOverlappingCommitRehash(): String? { - val edits = formatter.toFileHeader(diff).toEditList() - val path = when (diff.changeType) { - DiffEntry.ChangeType.DELETE -> diff.oldPath - else -> diff.newPath - } - DiffFile(path = path, - old = DiffContent(old, edits.map { edit -> - DiffRange(edit.beginA, edit.endA) }), - new = DiffContent(new, edits.map { edit -> - DiffRange(edit.beginB, edit.endB) })) - } + val serverHistoryRehashes = serverRepo.commits + .map { commit -> commit.rehash } + .toHashSet() + return rehashes.firstOrNull { rehash -> + serverHistoryRehashes.contains(rehash) } } - private fun getContentByObjectId(objectId: ObjectId): List { - return try { - gitRepo.open(objectId).bytes.toString(Charset.defaultCharset()) - .split('\n') - } catch (e: MissingObjectException) { - listOf() - } - } - private fun postCommitsToServer(commits: List) { if (commits.isNotEmpty()) { api.postCommits(commits) @@ -182,13 +47,6 @@ class CommitHasher(private val localRepo: LocalRepo, } } - private fun postFactsToServer(facts: List) { - if (facts.isNotEmpty()) { - api.postFacts(facts) - Logger.debug("Sent ${facts.size} facts to server") - } - } - private fun deleteCommitsOnServer(commits: List) { if (commits.isNotEmpty()) { api.deleteCommits(commits) @@ -196,47 +54,48 @@ class CommitHasher(private val localRepo: LocalRepo, } } - private fun getCommitsAsObservable(): Observable = - Observable.create { subscriber -> - try { - val revWalk = RevWalk(gitRepo) - val commitId = gitRepo.resolve(RepoHelper.MASTER_BRANCH) - revWalk.markStart(revWalk.parseCommit(commitId)) - for (revCommit in revWalk) { - subscriber.onNext(Commit(revCommit)) - } - } catch (e: Exception) { - Logger.error("Commit producing error", e) - subscriber.onError(e) - } - subscriber.onComplete() - } - private val emailFilter: (Commit) -> Boolean = { val email = it.author.email localRepo.hashAllContributors || (email == localRepo.author.email || - repo.emails.contains(email)) + serverRepo.emails.contains(email)) } - private fun Observable.pairWithNext(): Observable> { - return this.map { emit -> Pair(emit, emit) } - // Accumulate emits by prev-next pair. - .scan { pairAccumulated, pairNext -> - Pair(pairAccumulated.second, pairNext.second) - } - .skip(1) // Skip initial not paired emit. - } + // Hash added and missing server commits and send them to server. + fun updateFromObservable(observable: Observable) { + val lastKnownCommit = serverRepo.commits.lastOrNull() + val knownCommits = serverRepo.commits.toHashSet() - fun update() { - // Delete locally missing commits from server. If found at least one - // common commit then next commits are not deleted because hash of a - // commit calculated including hashes of its parents. - val firstOverlapCommit = findFirstOverlappingCommit() - val deletedCommits = repo.commits - .takeWhile { it.rehash != firstOverlapCommit?.rehash } - deleteCommitsOnServer(deletedCommits) + val throwables = mutableListOf() - // Hash added and missing server commits and send them to server. - hashAndSendCommits() + observable + .takeWhile { new -> // Hash until last known commit. + new.rehash != lastKnownCommit?.rehash + } + .filter { commit -> // Don't hash known. + knownCommits.isEmpty() || !knownCommits.contains(commit) + } + .filter { commit -> emailFilter(commit) } // Email filtering. + .map { commit -> + // Mapping and stats extraction. + commit.stats = Extractor().extract(commit.diffs) + Logger.debug("Stats: ${commit.stats.size} entries") + + // Count lines on all non-binary files. This is additional + // statistics to CommitStats because not all file extensions + // may be supported. + commit.numLinesAdded = commit.diffs.fold(0) { total, file -> + total + file.getAllAdded().size + } + commit.numLinesDeleted = commit.diffs.fold(0) { total, file -> + total + file.getAllDeleted().size + } + commit + } + .buffer(20, TimeUnit.SECONDS) // Group ready commits by time. + .subscribe({ commitsBundle -> // OnNext. + postCommitsToServer(commitsBundle) // Send ready commits. + }, { e -> // OnError. + throwables.add(e) // TODO(anatoly): Top-class handling errors. + }) } } diff --git a/src/main/kotlin/app/hashers/FactHasher.kt b/src/main/kotlin/app/hashers/FactHasher.kt new file mode 100644 index 00000000..04cf2426 --- /dev/null +++ b/src/main/kotlin/app/hashers/FactHasher.kt @@ -0,0 +1,82 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package app.hashers + +import app.FactKey +import app.Logger +import app.api.Api +import app.model.Author +import app.model.Commit +import app.model.Fact +import app.model.LocalRepo +import app.model.Repo +import io.reactivex.Observable +import java.time.LocalDateTime +import java.time.ZoneOffset + +/** + * CommitHasher hashes repository and uploads stats to server. + */ +class FactHasher(private val localRepo: LocalRepo, + private val serverRepo: Repo = Repo(), + private val api: Api) { + + private fun postFactsToServer(facts: List) { + if (facts.isNotEmpty()) { + api.postFacts(facts) + Logger.debug("Sent ${facts.size} facts to server") + } + } + + fun updateFromObservable(observable: Observable) { + val factsDayWeek = hashMapOf>() + val factsDayTime = hashMapOf>() + + val throwables = mutableListOf() + + observable + .subscribe({ commit -> // OnNext. + // Calculate facts. + val author = commit.author + val factDayWeek = factsDayWeek[author] ?: Array(7) { 0 } + val factDayTime = factsDayTime[author] ?: Array(24) { 0 } + val timestamp = commit.dateTimestamp + val dateTime = LocalDateTime.ofEpochSecond(timestamp, 0, + ZoneOffset.ofTotalSeconds(commit.dateTimeZoneOffset * 60)) + // The value is numbered from 1 (Monday) to 7 (Sunday). + factDayWeek[dateTime.dayOfWeek.value - 1] += 1 + // Hour from 0 to 23. + factDayTime[dateTime.hour] += 1 + factsDayWeek[author] = factDayWeek + factsDayTime[author] = factDayTime + }, { e -> // OnError. + throwables.add(e) // TODO(anatoly): Top-class handling errors. + }, { // OnComplete. + try { + val facts = mutableListOf() + factsDayTime.map { (author, list) -> + list.forEachIndexed { hour, count -> + if (count > 0) { + facts.add(Fact(serverRepo, + FactKey.COMMITS_DAY_TIME + hour, + count.toDouble(), author)) + } + } + } + factsDayWeek.map { (author, list) -> + list.forEachIndexed { day, count -> + if (count > 0) { + facts.add(Fact(serverRepo, + FactKey.COMMITS_DAY_WEEK + day, + count.toDouble(), author)) + } + } + } + postFactsToServer(facts) + } catch (e: Throwable) { + throwables.add(e) + } + }) + } +} diff --git a/src/main/kotlin/app/hashers/RepoHasher.kt b/src/main/kotlin/app/hashers/RepoHasher.kt index f5ad7b2d..975e2f44 100644 --- a/src/main/kotlin/app/hashers/RepoHasher.kt +++ b/src/main/kotlin/app/hashers/RepoHasher.kt @@ -6,18 +6,21 @@ package app.hashers import app.Logger import app.api.Api import app.config.Configurator +import app.model.Author import app.model.LocalRepo import app.model.Repo import app.utils.RepoHelper +import org.apache.commons.codec.digest.DigestUtils import org.eclipse.jgit.api.Git import org.eclipse.jgit.revwalk.RevCommit import org.eclipse.jgit.revwalk.RevWalk import java.io.File import java.io.IOException +import java.util.* class RepoHasher(private val localRepo: LocalRepo, private val api: Api, private val configurator: Configurator) { - var repo: Repo = Repo() + var serverRepo: Repo = Repo() init { if (!RepoHelper.isValidRepo(localRepo.path)) { @@ -27,8 +30,14 @@ class RepoHasher(private val localRepo: LocalRepo, private val api: Api, println("Hashing $localRepo...") val git = loadGit(localRepo.path) try { + val (rehashes, authors) = fetchRehashesAndAuthors(git) + localRepo.parseGitConfig(git.repository.config) - initializeRepo(git) + if (localRepo.author.email.isBlank()) { + throw IllegalStateException("Can't load email from Git config") + } + + initServerRepo(rehashes.last) if (!isKnownRepo()) { // Notify server about new contributor and his email. @@ -38,8 +47,17 @@ class RepoHasher(private val localRepo: LocalRepo, private val api: Api, getRepoFromServer() // Hash by all plugins. - CommitHasher(localRepo, repo, api, git).update() - CodeLongevity(localRepo, repo, api, git).update() + val observable = CommitCrawler.getObservable(git, serverRepo) + .publish() + CommitHasher(localRepo, serverRepo, api, rehashes) + .updateFromObservable(observable) + FactHasher(localRepo, serverRepo, api) + .updateFromObservable(observable) + // Start and synchronously wait until all subscribers complete. + observable.connect() + + // TODO(anatoly): CodeLongevity hash from observable. + CodeLongevity(localRepo, serverRepo, api, git) // Confirm hashing completion. postRepoToServer() @@ -65,34 +83,50 @@ class RepoHasher(private val localRepo: LocalRepo, private val api: Api, private fun isKnownRepo(): Boolean { return configurator.getRepos() - .find { it.rehash == repo.rehash } != null + .find { it.rehash == serverRepo.rehash } != null } private fun getRepoFromServer() { - repo = api.getRepo(repo.rehash) - Logger.debug("Received repo from server with ${repo.commits.size} " + - "commits") + serverRepo = api.getRepo(serverRepo.rehash) + Logger.debug("Received repo from server with " + + serverRepo.commits.size + " commits") } private fun postRepoToServer() { - api.postRepo(repo) + api.postRepo(serverRepo) } - private fun initializeRepo(git: Git) { - repo = Repo(userEmail = localRepo.author.email) - repo.initialCommitRehash = getInitialCommitRehash(git) - repo.rehash = RepoHelper.calculateRepoRehash(repo.initialCommitRehash, - localRepo) + private fun initServerRepo(initCommitRehash: String) { + serverRepo = Repo(userEmail = localRepo.author.email) + serverRepo.initialCommitRehash = initCommitRehash + serverRepo.rehash = RepoHelper.calculateRepoRehash( + serverRepo.initialCommitRehash, localRepo) } - private fun getInitialCommitRehash(git: Git): String { + private fun fetchRehashesAndAuthors(git: Git): + Pair, HashMap> { val head: RevCommit = RevWalk(git.repository) .parseCommit(git.repository.resolve(RepoHelper.MASTER_BRANCH)) val revWalk = RevWalk(git.repository) revWalk.markStart(head) - val initialCommit = revWalk.last() - return initialCommit.id.name + val commitsRehashes = LinkedList() + val contributors = hashMapOf() + + var commit: RevCommit? = revWalk.next() + while (commit != null) { + commitsRehashes.add(DigestUtils.sha256Hex(commit.name)) + if (!contributors.containsKey(commit.authorIdent.emailAddress)) { + val author = Author(commit.authorIdent.name, + commit.authorIdent.emailAddress) + contributors.put(commit.authorIdent.emailAddress, author) + } + commit.disposeBody() + commit = revWalk.next() + } + revWalk.dispose() + + return Pair(commitsRehashes, contributors) } } diff --git a/src/main/kotlin/app/model/Commit.kt b/src/main/kotlin/app/model/Commit.kt index 33da1dc3..9810a77a 100644 --- a/src/main/kotlin/app/model/Commit.kt +++ b/src/main/kotlin/app/model/Commit.kt @@ -27,6 +27,7 @@ data class Commit( ) { // Wrapping JGit's RevCommit. var raw: RevCommit? = null // Not sent to sever. + var diffs: List = listOf() constructor(revCommit: RevCommit) : this() { raw = revCommit diff --git a/src/main/kotlin/app/model/DiffEdit.kt b/src/main/kotlin/app/model/DiffEdit.kt deleted file mode 100644 index 33bab111..00000000 --- a/src/main/kotlin/app/model/DiffEdit.kt +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2017 Sourcerer Inc. All Rights Reserved. -// Author: Anatoly Kislov (anatoly@sourcerer.io) - -package app.model - -import org.eclipse.jgit.diff.Edit - -/** - * Edit is partial change of file. [del] specifies range of deleted lines in old - * content, [add] specifies range of added lines instead of deleted lines. - * Made to decouple statistics classes from JGit. - */ -data class DiffEdit(val del: DiffRange, val add: DiffRange) { - constructor(edit: Edit) : this(DiffRange(edit.beginA, edit.endA), - DiffRange(edit.beginB, edit.endB)) -} diff --git a/src/main/kotlin/app/model/DiffFile.kt b/src/main/kotlin/app/model/DiffFile.kt index 66a06a17..ea8a49aa 100644 --- a/src/main/kotlin/app/model/DiffFile.kt +++ b/src/main/kotlin/app/model/DiffFile.kt @@ -4,9 +4,11 @@ package app.model import app.utils.FileHelper +import org.eclipse.jgit.diff.DiffEntry class DiffFile( val path: String = "", + val changeType: DiffEntry.ChangeType, val old: DiffContent = DiffContent(), val new: DiffContent = DiffContent(), var language: String = "" diff --git a/src/test/kotlin/test/tests/hashers/CommitHasherTest.kt b/src/test/kotlin/test/tests/hashers/CommitHasherTest.kt index 0e8efdfd..875d2798 100644 --- a/src/test/kotlin/test/tests/hashers/CommitHasherTest.kt +++ b/src/test/kotlin/test/tests/hashers/CommitHasherTest.kt @@ -4,23 +4,20 @@ package test.tests.hashers -import app.FactKey import app.api.MockApi import app.hashers.CommitHasher +import app.hashers.CommitCrawler import app.model.* import app.utils.RepoHelper import org.eclipse.jgit.api.Git import org.jetbrains.spek.api.Spek import org.jetbrains.spek.api.dsl.given import org.jetbrains.spek.api.dsl.it -import test.utils.TestRepo import java.io.File -import java.util.* import java.util.stream.StreamSupport.stream import kotlin.streams.toList import kotlin.test.assertEquals import kotlin.test.assertNotEquals -import kotlin.test.assertTrue class CommitHasherTest : Spek({ val userName = "Contributor" @@ -60,19 +57,13 @@ class CommitHasherTest : Spek({ return lastCommit } - fun createDate(year: Int = 2017, month: Int = 1, day: Int = 1, - hour: Int = 0, minute: Int = 0, seconds: Int = 0): Date { - val cal = Calendar.getInstance() - // Month in calendar is 0-based. - cal.set(year, month - 1, day, hour, minute, seconds) - return cal.time - } - given("repo with initial commit and no history") { repo.commits = listOf() val mockApi = MockApi(mockRepo = repo) - CommitHasher(localRepo, repo, mockApi, gitHasher).update() + val observable = CommitCrawler.getObservable(gitHasher, repo) + CommitHasher(localRepo, repo, mockApi, repo.commits.map {it.rehash}) + .updateFromObservable(observable) it("send added commits") { assertEquals(1, mockApi.receivedAddedCommits.size) @@ -87,7 +78,9 @@ class CommitHasherTest : Spek({ repo.commits = listOf(getLastCommit(git)) val mockApi = MockApi(mockRepo = repo) - CommitHasher(localRepo, repo, mockApi, gitHasher).update() + val observable = CommitCrawler.getObservable(gitHasher, repo) + CommitHasher(localRepo, repo, mockApi, repo.commits.map {it.rehash}) + .updateFromObservable(observable) it("doesn't send added commits") { assertEquals(0, mockApi.receivedAddedCommits.size) @@ -105,7 +98,9 @@ class CommitHasherTest : Spek({ val revCommit = git.commit().setMessage("Second commit.").call() val addedCommit = Commit(revCommit) - CommitHasher(localRepo, repo, mockApi, gitHasher).update() + val observable = CommitCrawler.getObservable(gitHasher, repo) + CommitHasher(localRepo, repo, mockApi, repo.commits.map {it.rehash}) + .updateFromObservable(observable) it("doesn't send deleted commits") { assertEquals(0, mockApi.receivedDeletedCommits.size) @@ -139,7 +134,9 @@ class CommitHasherTest : Spek({ val revCommit = git.commit().setMessage(message).call() authorCommits.add(Commit(revCommit)) } - CommitHasher(localRepo, repo, mockApi, gitHasher).update() + val observable = CommitCrawler.getObservable(gitHasher, repo) + CommitHasher(localRepo, repo, mockApi, repo.commits.map {it.rehash}) + .updateFromObservable(observable) it("posts five commits as added") { assertEquals(5, mockApi.receivedAddedCommits.size) @@ -199,7 +196,9 @@ class CommitHasherTest : Spek({ val removedCommit = addedCommits.removeAt(1) repo.commits = addedCommits.toList().asReversed() - CommitHasher(localRepo, repo, mockApi, gitHasher).update() + val observable = CommitCrawler.getObservable(gitHasher, repo) + CommitHasher(localRepo, repo, mockApi, repo.commits.map {it.rehash}) + .updateFromObservable(observable) it("adds posts one commit as added and received commit is lost one") { assertEquals(1, mockApi.receivedAddedCommits.size) @@ -211,69 +210,5 @@ class CommitHasherTest : Spek({ } } - given("test of commits date facts") { - val testRepoPath = "../testrepo1" - val testRepo = TestRepo(testRepoPath) - val author1 = Author("Test1", "test@domain.com") - val author2 = Author("Test2", "test@domain.com") - - val repo = Repo(rehash = "rehash", commits = listOf()) - val mockApi = MockApi(mockRepo = repo) - val facts = mockApi.receivedFacts - - afterEachTest { - facts.clear() - } - - it("send two facts") { - testRepo.createFile("test1.txt", listOf("line1", "line2")) - testRepo.commit(message = "initial commit", - author = author1, - // Sunday. - date = createDate(year = 2017, month = 1, day = 1, - hour = 13, minute = 0, seconds = 0)) - - CommitHasher(localRepo, repo, mockApi, testRepo.git).update() - - assertEquals(2, facts.size) - assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_TIME + 13, - 1.0, author1))) - assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_WEEK + 6, - 1.0, author1))) - } - - it("send more facts") { - testRepo.createFile("test2.txt", listOf("line1", "line2")) - testRepo.commit(message = "second commit", - author = author2, - // Monday. - date = createDate(year=2017, month = 1, day = 2, - hour = 18, minute = 0, seconds = 0)) - - testRepo.createFile("test3.txt", listOf("line1", "line2")) - testRepo.commit(message = "third commit", - author = author1, - // Monday. - date = createDate(month = 1, day = 2, - hour = 13, minute = 0, seconds = 0)) - - CommitHasher(localRepo, repo, mockApi, testRepo.git).update() - - assertEquals(5, facts.size) - assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_TIME + 18, - 1.0, author2))) - assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_WEEK + 0, - 1.0, author2))) - assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_TIME + 13, - 2.0, author1))) - assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_WEEK + 0, - 1.0, author1))) - } - - afterGroup { - testRepo.destroy() - } - } - Runtime.getRuntime().exec("src/test/delete_repo.sh").waitFor() }) diff --git a/src/test/kotlin/test/tests/hashers/FactHasherTest.kt b/src/test/kotlin/test/tests/hashers/FactHasherTest.kt new file mode 100644 index 00000000..3da42935 --- /dev/null +++ b/src/test/kotlin/test/tests/hashers/FactHasherTest.kt @@ -0,0 +1,101 @@ +// Copyright 2017 Sourcerer Inc. All Rights Reserved. +// Author: Anatoly Kislov (anatoly@sourcerer.io) + +package test.tests.hashers + +import app.FactKey +import app.api.MockApi +import app.hashers.CommitCrawler +import app.hashers.FactHasher +import app.model.Author +import app.model.Fact +import app.model.LocalRepo +import app.model.Repo +import org.jetbrains.spek.api.Spek +import org.jetbrains.spek.api.dsl.given +import org.jetbrains.spek.api.dsl.it +import test.utils.TestRepo +import java.util.* +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class FactHasherTest : Spek({ + val userName = "Contributor" + val userEmail = "test@domain.com" + + val repoPath = "../testrepo-fact-hasher-" + val localRepo = LocalRepo(repoPath) + localRepo.author = Author(userName, userEmail) + + given("test of commits date facts") { + val testRepo = TestRepo(repoPath + "date-facts") + val author1 = Author("Test1", "test@domain.com") + val author2 = Author("Test2", "test@domain.com") + + val repo = Repo(rehash = "rehash", commits = listOf()) + val mockApi = MockApi(mockRepo = repo) + val facts = mockApi.receivedFacts + + fun createDate(year: Int = 2017, month: Int = 1, day: Int = 1, + hour: Int = 0, minute: Int = 0, seconds: Int = 0): Date { + val cal = Calendar.getInstance() + // Month in calendar is 0-based. + cal.set(year, month - 1, day, hour, minute, seconds) + return cal.time + } + + afterEachTest { + facts.clear() + } + + it("send two facts") { + testRepo.createFile("test1.txt", listOf("line1", "line2")) + testRepo.commit(message = "initial commit", + author = author1, + date = createDate(year = 2017, month = 1, day = 1, // Sunday. + hour = 13, minute = 0, seconds = 0)) + + val observable = CommitCrawler.getObservable(testRepo.git, repo) + FactHasher(localRepo, repo, mockApi) + .updateFromObservable(observable) + + assertEquals(2, facts.size) + assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_TIME + 13, + 1.0, author1))) + assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_WEEK + 6, + 1.0, author1))) + } + + it("send more facts") { + testRepo.createFile("test2.txt", listOf("line1", "line2")) + testRepo.commit(message = "second commit", + author = author2, + date = createDate(year=2017, month = 1, day = 2, // Monday. + hour = 18, minute = 0, seconds = 0)) + + testRepo.createFile("test3.txt", listOf("line1", "line2")) + testRepo.commit(message = "third commit", + author = author1, + date = createDate(year=2017, month = 1, day = 2, // Monday. + hour = 13, minute = 0, seconds = 0)) + + val observable = CommitCrawler.getObservable(testRepo.git, repo) + FactHasher(localRepo, repo, mockApi) + .updateFromObservable(observable) + + assertEquals(5, facts.size) + assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_TIME + 18, + 1.0, author2))) + assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_WEEK + 0, + 1.0, author2))) + assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_TIME + 13, + 2.0, author1))) + assertTrue(facts.contains(Fact(repo, FactKey.COMMITS_DAY_WEEK + 0, + 1.0, author1))) + } + + afterGroup { + testRepo.destroy() + } + } +})