diff --git a/.gitignore b/.gitignore
index c58d83b..c98832c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,3 +15,4 @@ project/plugins/project/
# Scala-IDE specific
.scala_dependencies
.worksheet
+.sonar
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000..3c13a0a
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,45 @@
+pipeline {
+ agent any
+ stages {
+ stage('compile') {
+ steps {
+ sh 'echo try connect from prev build'
+ sh 'echo build number is ${BUILD_NUMBER}'
+ sh 'sbt clean compile'
+ }
+ }
+ stage('unit test') {
+ steps {
+ sh 'sbt "testOnly * -- -l com.github.notyy.codeAnalyzer.FunctionalTest"'
+ }
+ }
+ stage('test coverage') {
+ steps {
+ sh 'sbt coverage test'
+ sh 'sbt coverageReport'
+ sh 'cp -R target/scala-2.12/scoverage-report ./report/'
+ }
+ }
+ stage('rebuild without coverage') {
+ steps {
+ sh 'sbt clean compile'
+ }
+ }
+ stage('functional test') {
+ steps {
+ sh 'sbt "testOnly * -- -n com.github.notyy.codeAnalyzer.FunctionalTest"'
+ }
+ }
+ stage('performance test') {
+ steps {
+ sh 'echo performance test'
+ }
+ }
+ stage('assembly') {
+ steps {
+ sh 'sbt assembly'
+ sh 'echo assembly successfully'
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/build.sbt b/build.sbt
index 1585ece..500c144 100644
--- a/build.sbt
+++ b/build.sbt
@@ -7,15 +7,37 @@ isSnapshot := true
organization := "com.github.notyy"
// set the Scala version used for the project
-scalaVersion := "2.11.8"
+scalaVersion := "2.12.2"
libraryDependencies ++= Seq(
- "org.scalacheck" %% "scalacheck" % "1.13.2" % "test",
- "org.pegdown" % "pegdown" % "1.0.2" % "test", //used in html report
- "org.scalatest" %% "scalatest" % "2.2.1" % "test",
+ "org.scalacheck" %% "scalacheck" % "1.13.4" % "test",
+ "org.pegdown" % "pegdown" % "1.6.0" % "test", //used in html report
+ "org.scalatest" %% "scalatest" % "3.0.1" % "test",
"org.slf4j" % "slf4j-api" % "1.7.7",
+ "com.typesafe.slick" %% "slick" % "3.2.1",
+ "com.typesafe.slick" %% "slick-hikaricp" % "3.2.1",
"ch.qos.logback" % "logback-classic" % "1.1.2",
- "com.typesafe.scala-logging" %% "scala-logging-slf4j" % "2.1.2"
+ "com.typesafe.scala-logging" %% "scala-logging" % "3.7.1",
+ "com.typesafe.akka" %% "akka-actor" % "2.5.3",
+ "com.typesafe.akka" %% "akka-agent" % "2.5.3",
+ "com.typesafe.akka" %% "akka-camel" % "2.5.3",
+ "com.typesafe.akka" %% "akka-cluster" % "2.5.3",
+ "com.typesafe.akka" %% "akka-cluster-metrics" % "2.5.3",
+ "com.typesafe.akka" %% "akka-cluster-sharding" % "2.5.3",
+ "com.typesafe.akka" %% "akka-cluster-tools" % "2.5.3",
+ "com.typesafe.akka" %% "akka-distributed-data" % "2.5.3",
+ "com.typesafe.akka" %% "akka-multi-node-testkit" % "2.5.3",
+ "com.typesafe.akka" %% "akka-persistence" % "2.5.3",
+ "com.typesafe.akka" %% "akka-persistence-query" % "2.5.3",
+ "com.typesafe.akka" %% "akka-persistence-tck" % "2.5.3",
+ "com.typesafe.akka" %% "akka-remote" % "2.5.3",
+ "com.typesafe.akka" %% "akka-slf4j" % "2.5.3",
+ "com.typesafe.akka" %% "akka-stream" % "2.5.3",
+ "com.typesafe.akka" %% "akka-stream-testkit" % "2.5.3",
+ "com.typesafe.akka" %% "akka-testkit" % "2.5.3",
+ "com.typesafe.akka" %% "akka-typed" % "2.5.3",
+ "com.typesafe.akka" %% "akka-contrib" % "2.5.3",
+ "com.h2database" % "h2" % "1.4.196"
)
// TODO reopen it later
@@ -88,6 +110,8 @@ exportJars := true
// only show stack traces up to the first sbt stack frame
traceLevel := 0
+mainClass in assembly := Some("tutor.MainApp")
+
// add SWT to the unmanaged classpath
// unmanagedJars in Compile += file("/usr/share/java/swt.jar")
diff --git a/codeAnalyzer b/codeAnalyzer
new file mode 100644
index 0000000..8358c43
--- /dev/null
+++ b/codeAnalyzer
@@ -0,0 +1 @@
+java -jar ~/dev/bin/CodeAnalyzer.jar $1
\ No newline at end of file
diff --git a/project/plugins.sbt b/project/plugins.sbt
index 1741de9..a6b959d 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1 +1,3 @@
-addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.3")
\ No newline at end of file
+addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.3")
+
+addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.0")
diff --git a/src/main/java/codeDetector/CodeDetector.java b/src/main/java/codeDetector/CodeDetector.java
new file mode 100644
index 0000000..4bc13fb
--- /dev/null
+++ b/src/main/java/codeDetector/CodeDetector.java
@@ -0,0 +1,14 @@
+package codeDetector;
+
+import java.io.File;
+import java.util.Arrays;
+
+public class CodeDetector {
+ public DetectorReport analyze(String path) {
+ File file = new File(path);
+ long fileCount = Arrays.stream(file.listFiles()).filter(File::isFile).count();
+ DetectorReport detectorReport = new DetectorReport();
+ detectorReport.setFileCount(fileCount);
+ return detectorReport;
+ }
+}
diff --git a/src/main/java/codeDetector/DetectorReport.java b/src/main/java/codeDetector/DetectorReport.java
new file mode 100644
index 0000000..c7cac1e
--- /dev/null
+++ b/src/main/java/codeDetector/DetectorReport.java
@@ -0,0 +1,13 @@
+package codeDetector;
+
+public class DetectorReport {
+ private long fileCount;
+
+ public long getFileCount() {
+ return fileCount;
+ }
+
+ public void setFileCount(long fileCount) {
+ this.fileCount = fileCount;
+ }
+}
diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf
new file mode 100644
index 0000000..84241e9
--- /dev/null
+++ b/src/main/resources/application.conf
@@ -0,0 +1,9 @@
+h2mem1 = {
+ // retry is a strange solution to solve thread interrupt problem
+// url = "jdbc:h2:retry:~/temp/h2data;DB_CLOSE_DELAY=-1"
+// url = "jdbc:h2:file:./target/h2data;DB_CLOSE_DELAY=-1"
+ url = "jdbc:h2:mem:test1;DB_CLOSE_DELAY=-1"
+ driver = org.h2.Driver
+ connectionPool = disabled
+ keepAliveConnection = true
+}
\ No newline at end of file
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 0000000..8813133
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,22 @@
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+ logs/sbtTemplate.log
+
+ %d{HH:mm:ss} %level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/scala/lesson1/hello/Main.scala b/src/main/scala/lesson1/hello/Main.scala
deleted file mode 100644
index a81ee3a..0000000
--- a/src/main/scala/lesson1/hello/Main.scala
+++ /dev/null
@@ -1,7 +0,0 @@
-package lesson1.hello
-
-object Main extends App {
- println("hello,world")
-
- def add(x:Int, y:Int):Int = x + y
-}
diff --git a/src/main/scala/lesson2And3And4/MainApp.scala b/src/main/scala/lesson2And3And4/MainApp.scala
deleted file mode 100644
index 4a17864..0000000
--- a/src/main/scala/lesson2And3And4/MainApp.scala
+++ /dev/null
@@ -1,5 +0,0 @@
-package lesson2And3And4
-
-object MainApp extends App{
- println("welcome to use my code analyzer")
-}
diff --git a/src/main/scala/lesson2And3And4/SourceCode.scala b/src/main/scala/lesson2And3And4/SourceCode.scala
deleted file mode 100644
index ada9584..0000000
--- a/src/main/scala/lesson2And3And4/SourceCode.scala
+++ /dev/null
@@ -1,17 +0,0 @@
-package lesson2And3And4
-
-class SourceCode(val path: String, val name: String, private val lines: List[String]){
- def count:Int = lines.length
-}
-
-object SourceCode{
- type Path = String
-
- def fromFile(path: Path):SourceCode = {
- import scala.io._
-
- val source = Source.fromFile("/Users/twer/source/scala/CodeAnalyzerTutorial/build.sbt")
- val lines = source.getLines.toList
- new SourceCode(path,path.split("/").last, lines)
- }
-}
\ No newline at end of file
diff --git a/src/main/scala/tutor/AnalyzeResultHistory.scala b/src/main/scala/tutor/AnalyzeResultHistory.scala
new file mode 100644
index 0000000..1b7d868
--- /dev/null
+++ b/src/main/scala/tutor/AnalyzeResultHistory.scala
@@ -0,0 +1,8 @@
+package tutor
+
+//we will directly save the json representation of codebase info to database
+final case class AnalyzeResultHistory(path: String, created: String, codebaseInfo: String)
+
+
+
+
diff --git a/src/main/scala/tutor/CodebaseAnalyzeAggregatorActor.scala b/src/main/scala/tutor/CodebaseAnalyzeAggregatorActor.scala
new file mode 100644
index 0000000..eabb4a6
--- /dev/null
+++ b/src/main/scala/tutor/CodebaseAnalyzeAggregatorActor.scala
@@ -0,0 +1,90 @@
+package tutor
+
+import java.util.Date
+
+import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props, Terminated}
+import akka.routing.{ActorRefRoutee, RoundRobinRoutingLogic, Router}
+import tutor.CodebaseAnalyzeAggregatorActor.{AnalyzeDirectory, Complete, Report, Timeout}
+import tutor.SourceCodeAnalyzerActor.NewFile
+import tutor.utils.BenchmarkUtil
+
+import scala.concurrent.duration._
+import scala.util.{Failure, Success, Try}
+
+object CodebaseAnalyzeAggregatorActor {
+ def props(): Props = Props(new CodebaseAnalyzeAggregatorActor)
+
+ final case class AnalyzeDirectory(path: String)
+
+ final case class Complete(result: Try[SourceCodeInfo])
+
+ final case object Timeout
+
+ final case class Report(codebaseInfo: CodebaseInfo)
+
+}
+
+class CodebaseAnalyzeAggregatorActor extends Actor with ActorLogging with DirectoryScanner with ReportFormatter {
+ var controller: ActorRef = _
+ var currentPath: String = _
+ var beginTime: Date = _
+ var fileCount = 0
+ var completeCount = 0
+ var failCount = 0
+ var result: CodebaseInfo = CodebaseInfo.empty
+ var timeoutTimer: Cancellable = _
+
+ var router: Router = {
+ val routees = Vector.fill(8) {
+ val r = context.actorOf(SourceCodeAnalyzerActor.props())
+ context watch r
+ ActorRefRoutee(r)
+ }
+ Router(RoundRobinRoutingLogic(), routees)
+ }
+
+ override def receive: Receive = {
+ case AnalyzeDirectory(path) => {
+ controller = sender()
+ currentPath = path
+ beginTime = BenchmarkUtil.recordStart(s"analyze folder $currentPath")
+ foreachFile(path, PresetFilters.knownFileTypes, PresetFilters.ignoreFolders) { file =>
+ fileCount += 1
+ router.route(NewFile(file.getAbsolutePath), context.self)
+ }
+ import context.dispatcher
+ timeoutTimer = context.system.scheduler.scheduleOnce((fileCount / 1000).seconds, context.self, Timeout)
+ }
+ case Complete(Success(sourceCodeInfo: SourceCodeInfo)) => {
+ completeCount += 1
+ result = result + sourceCodeInfo
+ finishIfAllComplete()
+ }
+ case Complete(Failure(exception)) => {
+ completeCount += 1
+ failCount += 1
+ log.warning("processing file failed {}", exception)
+ finishIfAllComplete()
+ }
+ case Timeout => {
+ println(s"${result.totalFileNums} of $fileCount files processed before timeout")
+ controller ! Report(result)
+ BenchmarkUtil.recordElapse(s"analyze folder $currentPath", beginTime)
+ }
+ case Terminated(a) =>
+ router = router.removeRoutee(a)
+ val r = context.actorOf(Props[SourceCodeAnalyzerActor])
+ context watch r
+ router = router.addRoutee(r)
+ case x@_ => log.error(s"receive unknown message $x")
+ }
+
+ def finishIfAllComplete(): Unit = {
+ if (completeCount == fileCount) {
+ timeoutTimer.cancel()
+ controller ! Report(result)
+ BenchmarkUtil.recordElapse(s"analyze folder $currentPath", beginTime)
+ context.stop(self)
+ }
+ }
+}
diff --git a/src/main/scala/tutor/CodebaseAnalyzer.scala b/src/main/scala/tutor/CodebaseAnalyzer.scala
new file mode 100644
index 0000000..2d9eaee
--- /dev/null
+++ b/src/main/scala/tutor/CodebaseAnalyzer.scala
@@ -0,0 +1,64 @@
+package tutor
+
+import tutor.repo.AnalyzeHistoryRepository
+import tutor.utils.{BenchmarkUtil, FileUtil}
+import tutor.utils.FileUtil._
+
+import scala.math.max
+
+object CodebaseInfo {
+ def empty: CodebaseInfo = new CodebaseInfo(0, Map.empty[String, Int], 0, 0, None, Seq.empty[SourceCodeInfo])
+}
+
+case class CodebaseInfo(totalFileNums: Int, fileTypeNums: Map[String, Int], totalLineCount: Int, avgLineCount: Double, longestFileInfo: Option[SourceCodeInfo], top10Files: Seq[SourceCodeInfo]) {
+ def +(sourceCodeInfo: SourceCodeInfo): CodebaseInfo = {
+ val fileExt = FileUtil.extractExtFileName(sourceCodeInfo.localPath)
+ val newFileTypeNums: Map[String, Int] = if (fileTypeNums.contains(fileExt)) {
+ fileTypeNums.updated(fileExt, fileTypeNums(fileExt) + 1)
+ } else {
+ fileTypeNums + (fileExt -> 1)
+ }
+ val newTotalLineCount = totalLineCount + sourceCodeInfo.lineCount
+ val newTotalFileNum = totalFileNums + 1
+ CodebaseInfo(newTotalFileNum, newFileTypeNums, newTotalLineCount, newTotalLineCount / newTotalFileNum,
+ if (longestFileInfo.isEmpty) {
+ Some(sourceCodeInfo)
+ } else {
+ if (longestFileInfo.get.lineCount < sourceCodeInfo.lineCount) Some(sourceCodeInfo)
+ else longestFileInfo
+ },
+ if (top10Files.isEmpty) {
+ Vector(sourceCodeInfo)
+ } else if (top10Files.size < 10 || sourceCodeInfo.lineCount > top10Files.last.lineCount) {
+ (top10Files :+ sourceCodeInfo).sortBy(_.lineCount).reverse.take(10)
+ } else {
+ top10Files
+ }
+ )
+ }
+}
+
+trait CodebaseAnalyzer extends CodebaseAnalyzerInterface {
+ this: DirectoryScanner with SourceCodeAnalyzer with AnalyzeHistoryRepository=>
+
+ override def analyze(path: Path, knownFileTypes: Set[String], ignoreFolders: Set[String]): Option[CodebaseInfo] = {
+ val files = BenchmarkUtil.record("scan folders") {
+ scan(path, knownFileTypes, ignoreFolders)
+ }
+ if (files.isEmpty) {
+ None
+ } else {
+ val sourceCodeInfos: Seq[SourceCodeInfo] = BenchmarkUtil.record("processing each file") {
+ processSourceFiles(files)
+ }
+ BenchmarkUtil.record("make last result ##") {
+ val codebaseInfo = sourceCodeInfos.foldLeft(CodebaseInfo.empty)(_ + _)
+ record(path, codebaseInfo)
+ Some(codebaseInfo)
+ }
+ }
+ }
+
+ protected def processSourceFiles(files: Seq[Path]): Seq[SourceCodeInfo]
+
+}
diff --git a/src/main/scala/tutor/CodebaseAnalyzerAkkaApp.scala b/src/main/scala/tutor/CodebaseAnalyzerAkkaApp.scala
new file mode 100644
index 0000000..76c9e70
--- /dev/null
+++ b/src/main/scala/tutor/CodebaseAnalyzerAkkaApp.scala
@@ -0,0 +1,28 @@
+package tutor
+
+import akka.actor.{ActorRef, ActorSystem}
+import tutor.CodebaseAnalyzeAggregatorActor.AnalyzeDirectory
+
+import scala.io.StdIn
+
+object CodebaseAnalyzerAkkaApp extends App {
+
+ val system = ActorSystem("CodebaseAnalyzer")
+ val codebaseAnalyzerControllerActor: ActorRef = system.actorOf(CodebaseAnalyzerControllerActor.props())
+
+ var shouldContinue = true
+ try {
+ while (shouldContinue) {
+ println("please input source file folder or :q to quit")
+ val input = StdIn.readLine()
+ if (input == ":q") {
+ shouldContinue = false
+ } else {
+ codebaseAnalyzerControllerActor ! AnalyzeDirectory(input)
+ }
+ }
+ } finally {
+ println("good bye!")
+ system.terminate()
+ }
+}
diff --git a/src/main/scala/tutor/CodebaseAnalyzerControllerActor.scala b/src/main/scala/tutor/CodebaseAnalyzerControllerActor.scala
new file mode 100644
index 0000000..9da5f4a
--- /dev/null
+++ b/src/main/scala/tutor/CodebaseAnalyzerControllerActor.scala
@@ -0,0 +1,19 @@
+package tutor
+
+import akka.actor.{Actor, Props}
+import tutor.CodebaseAnalyzeAggregatorActor.{AnalyzeDirectory, Report}
+
+object CodebaseAnalyzerControllerActor {
+ def props(): Props = Props(new CodebaseAnalyzerControllerActor)
+}
+
+class CodebaseAnalyzerControllerActor extends Actor with ReportFormatter {
+ override def receive: Receive = {
+ case AnalyzeDirectory(path) => {
+ context.actorOf(CodebaseAnalyzeAggregatorActor.props()) ! AnalyzeDirectory(path)
+ }
+ case Report(content) => {
+ println(format(content))
+ }
+ }
+}
diff --git a/src/main/scala/tutor/CodebaseAnalyzerInterface.scala b/src/main/scala/tutor/CodebaseAnalyzerInterface.scala
new file mode 100644
index 0000000..9b13fb7
--- /dev/null
+++ b/src/main/scala/tutor/CodebaseAnalyzerInterface.scala
@@ -0,0 +1,7 @@
+package tutor
+import tutor.utils.FileUtil.Path
+
+trait CodebaseAnalyzerInterface {
+
+ def analyze(path: Path, knownFileTypes: Set[String], ignoreFolders: Set[String]): Option[CodebaseInfo]
+}
diff --git a/src/main/scala/tutor/CodebaseAnalyzerParImpl.scala b/src/main/scala/tutor/CodebaseAnalyzerParImpl.scala
new file mode 100644
index 0000000..0828dcb
--- /dev/null
+++ b/src/main/scala/tutor/CodebaseAnalyzerParImpl.scala
@@ -0,0 +1,12 @@
+package tutor
+
+import tutor.repo.AnalyzeHistoryRepository
+import tutor.utils.FileUtil.Path
+
+trait CodebaseAnalyzerParImpl extends CodebaseAnalyzer {
+ this: DirectoryScanner with SourceCodeAnalyzer with AnalyzeHistoryRepository =>
+
+ override protected def processSourceFiles(files: Seq[Path]): Seq[SourceCodeInfo] = {
+ files.par.map(processFile).filter(_.isSuccess).map(_.get).toVector
+ }
+}
diff --git a/src/main/scala/tutor/CodebaseAnalyzerSeqImpl.scala b/src/main/scala/tutor/CodebaseAnalyzerSeqImpl.scala
new file mode 100644
index 0000000..8ef33ba
--- /dev/null
+++ b/src/main/scala/tutor/CodebaseAnalyzerSeqImpl.scala
@@ -0,0 +1,12 @@
+package tutor
+
+import tutor.repo.AnalyzeHistoryRepository
+import tutor.utils.FileUtil.Path
+
+trait CodebaseAnalyzerSeqImpl extends CodebaseAnalyzer {
+ this: DirectoryScanner with SourceCodeAnalyzer with AnalyzeHistoryRepository =>
+
+ override protected def processSourceFiles(files: Seq[Path]): Seq[SourceCodeInfo] = {
+ files.map(processFile).filter(_.isSuccess).map(_.get)
+ }
+}
diff --git a/src/main/scala/tutor/CodebaseAnalyzerStreamApp.scala b/src/main/scala/tutor/CodebaseAnalyzerStreamApp.scala
new file mode 100644
index 0000000..6e75c75
--- /dev/null
+++ b/src/main/scala/tutor/CodebaseAnalyzerStreamApp.scala
@@ -0,0 +1,43 @@
+package tutor
+
+import akka.actor.ActorSystem
+import akka.stream.ActorMaterializer
+import akka.stream.scaladsl._
+import com.typesafe.scalalogging.StrictLogging
+import tutor.utils.BenchmarkUtil
+
+import scala.collection.mutable.ArrayBuffer
+import scala.concurrent.Future
+import scala.util.{Failure, Success}
+
+object CodebaseAnalyzerStreamApp extends App with DirectoryScanner with SourceCodeAnalyzer with ReportFormatter with StrictLogging {
+
+ implicit val system = ActorSystem("CodebaseAnalyzer")
+ implicit val materializer = ActorMaterializer()
+ implicit val ec = system.dispatcher
+
+ val path = args(0)
+ val beginTime = BenchmarkUtil.recordStart(s"analyze $path with akka stream")
+ val files = scan(path, PresetFilters.knownFileTypes, PresetFilters.ignoreFolders).iterator
+ var errorProcessingFiles: ArrayBuffer[Throwable] = ArrayBuffer.empty
+
+ val done = Source.fromIterator(() => files).mapAsync(8)(path => Future {
+ processFile(path)
+ }).fold(CodebaseInfo.empty) {
+ (acc, trySourceCodeInfo) =>
+ trySourceCodeInfo match {
+ case Success(sourceCodeInfo) => acc + sourceCodeInfo
+ case Failure(e) => {
+ errorProcessingFiles += e
+ acc
+ }
+ }
+ }.runForeach(codebaseInfo => {
+ println(format(codebaseInfo))
+ println(s"there are ${errorProcessingFiles.size} files failed to process.")
+ })
+ done.onComplete { _ =>
+ BenchmarkUtil.recordElapse(s"analyze $path with akka stream", beginTime)
+ system.terminate()
+ }
+}
diff --git a/src/main/scala/tutor/DirectoryScanner.scala b/src/main/scala/tutor/DirectoryScanner.scala
new file mode 100644
index 0000000..45a47db
--- /dev/null
+++ b/src/main/scala/tutor/DirectoryScanner.scala
@@ -0,0 +1,60 @@
+package tutor
+
+import java.io.File
+
+import com.typesafe.scalalogging.StrictLogging
+import tutor.utils.FileUtil
+import tutor.utils.FileUtil.Path
+
+trait DirectoryScanner extends StrictLogging {
+ /**
+ * recursively scan given directory, get all file path whose ext is in knownFileTypes Set
+ *
+ * @param path
+ * @param knownFileTypes
file ext, like scala, java etc.
+ * @param ignoreFolders used to ignore output folders, like target folder for scala and java; bin fold for other languages
+ * @return
+ */
+ def scan(path: Path, knownFileTypes: Set[String], ignoreFolders: Set[String]): Seq[Path] = {
+ scan(path)(Vector[Path](), ignoreFolders) {
+ (acc, f) =>
+ val filePath = f.getAbsolutePath
+ if (f.isFile && shouldAccept(f.getPath, knownFileTypes)) {
+ acc :+ filePath
+ } else acc
+ }
+ }
+
+ def scan[T](path: Path)(initValue: T, ignoreFolders: Set[String])(processFile: (T, File) => T): T = {
+ val files = new File(path).listFiles()
+ if (files == null) {
+ logger.warn(s"$path is not a legal directory")
+ initValue
+ } else {
+ files.foldLeft(initValue) { (acc, file) =>
+ val filePath = file.getAbsolutePath
+ if (file.isFile) {
+ processFile(acc, file)
+ } else if (file.isDirectory && (!ignoreFolders.contains(FileUtil.extractLocalPath(file.getPath)))) {
+ scan(filePath)(acc, ignoreFolders)(processFile)
+ } else {
+ acc
+ }
+ }
+ }
+ }
+
+ def foreachFile(path: Path, knownFileTypes: Set[String], ignoreFolders: Set[String])(processFile: File => Unit): Unit = {
+ scan(path)((), ignoreFolders) {
+ (acc, f) =>
+ val filePath = f.getAbsolutePath
+ if (f.isFile && shouldAccept(f.getPath, knownFileTypes)) {
+ processFile(f)
+ } else ()
+ }
+ }
+
+ private def shouldAccept(path: Path, knownFileTypes: Set[String]): Boolean = {
+ knownFileTypes.contains(FileUtil.extractExtFileName(path))
+ }
+}
diff --git a/src/main/scala/tutor/MainApp.scala b/src/main/scala/tutor/MainApp.scala
new file mode 100644
index 0000000..683c3dc
--- /dev/null
+++ b/src/main/scala/tutor/MainApp.scala
@@ -0,0 +1,41 @@
+package tutor
+
+import java.io.File
+
+import com.typesafe.scalalogging.StrictLogging
+import tutor.PresetFilters.{ignoreFolders, knownFileTypes}
+import tutor.repo.{AnalyzeHistoryRepository, H2DB}
+import tutor.utils.FileUtil.Path
+import tutor.utils.{BenchmarkUtil, WriteSupport}
+
+object MainApp extends App with ReportFormatter with WriteSupport with StrictLogging {
+ if (args.length < 1) {
+ println("usage: CodeAnalyzer FilePath [-oOutputfile]")
+ } else {
+ val path: Path = args(0)
+ val file = new File(path)
+ val analyzer = args.find(_.startsWith("-p")).map { _ =>
+ logger.info("using par collection mode")
+ new CodebaseAnalyzerParImpl with DirectoryScanner with SourceCodeAnalyzer with AnalyzeHistoryRepository with H2DB
+ }.getOrElse {
+ logger.info("using sequence collection mode")
+ new CodebaseAnalyzerSeqImpl with DirectoryScanner with SourceCodeAnalyzer with AnalyzeHistoryRepository with H2DB
+ }
+ val rs = if (file.isFile) {
+ analyzer.processFile(file.getAbsolutePath).map(format).getOrElse(s"error processing $path")
+ } else {
+ BenchmarkUtil.record(s"analyze code under $path") {
+ analyzer.analyze(path, knownFileTypes, ignoreFolders).map(format).getOrElse("not result found")
+ }
+ }
+ args.find(_.startsWith("-o")).foreach { opt =>
+ val output = opt.drop(2)
+ withWriter(output) {
+ _.write(rs)
+ }
+ println(s"report saved into $output")
+ }
+ println(rs)
+ }
+
+}
diff --git a/src/main/scala/tutor/PresetFilters.scala b/src/main/scala/tutor/PresetFilters.scala
new file mode 100644
index 0000000..3d3c0f1
--- /dev/null
+++ b/src/main/scala/tutor/PresetFilters.scala
@@ -0,0 +1,8 @@
+package tutor
+
+object PresetFilters {
+ val knownFileTypes: Set[String] =
+ Set("scala", "java", "txt", "xml", "json", "c", "h", "cpp", "hs", "properties","sbt","js","html")
+ val ignoreFolders: Set[String] =
+ Set("target","bin",".idea",".git")
+}
diff --git a/src/main/scala/tutor/ReportFormatter.scala b/src/main/scala/tutor/ReportFormatter.scala
new file mode 100644
index 0000000..f430fe4
--- /dev/null
+++ b/src/main/scala/tutor/ReportFormatter.scala
@@ -0,0 +1,29 @@
+package tutor
+
+trait ReportFormatter {
+ def format(codebaseInfo: CodebaseInfo): String = {
+ val longestFileInfo: Option[SourceCodeInfo] = codebaseInfo.longestFileInfo
+ codebaseInfo.fileTypeNums.map {
+ case (fileType, count) => s"$fileType $count"
+ }.mkString("\n") ++
+ "\n" ++
+ ReportFormatter.separator ++ "\n\n" ++
+ s"total line count: ${codebaseInfo.totalLineCount}" ++ "\n" ++
+ s"avg line count: ${codebaseInfo.avgLineCount}" ++ "\n" ++
+ s"longest file: ${longestFileInfo.map(_.path).getOrElse("not avaliable")} ${longestFileInfo.map(_.lineCount).getOrElse(0)}" ++
+ "\n" ++
+ ReportFormatter.separator ++ "\n\n" ++
+ "top 10 long files\n" ++
+ codebaseInfo.top10Files.map {
+ s => s"${s.path} ${s.lineCount}"
+ }.mkString("\n")
+ }
+
+ def format(sourceCode: SourceCodeInfo): String = {
+ s"name: ${sourceCode.localPath} lines: ${sourceCode.lineCount}"
+ }
+}
+
+object ReportFormatter {
+ val separator = "---------------------------"
+}
diff --git a/src/main/scala/tutor/SourceCodeAnalyzerActor.scala b/src/main/scala/tutor/SourceCodeAnalyzerActor.scala
new file mode 100644
index 0000000..09c4b50
--- /dev/null
+++ b/src/main/scala/tutor/SourceCodeAnalyzerActor.scala
@@ -0,0 +1,22 @@
+package tutor
+
+import akka.actor.{Actor, ActorLogging, Props}
+import tutor.CodebaseAnalyzeAggregatorActor.Complete
+import tutor.SourceCodeAnalyzerActor.NewFile
+
+
+object SourceCodeAnalyzerActor {
+ def props(): Props = Props(new SourceCodeAnalyzerActor)
+
+ final case class NewFile(path: String)
+
+}
+
+class SourceCodeAnalyzerActor extends Actor with ActorLogging with SourceCodeAnalyzer {
+ override def receive: Receive = {
+ case NewFile(path) => {
+ val sourceCodeInfo = processFile(path)
+ sender() ! Complete(sourceCodeInfo)
+ }
+ }
+}
diff --git a/src/main/scala/tutor/SourceCodeInfo.scala b/src/main/scala/tutor/SourceCodeInfo.scala
new file mode 100644
index 0000000..099415c
--- /dev/null
+++ b/src/main/scala/tutor/SourceCodeInfo.scala
@@ -0,0 +1,34 @@
+package tutor
+
+import com.typesafe.scalalogging.StrictLogging
+import tutor.utils.FileUtil
+import tutor.utils.FileUtil._
+
+import scala.util.Try
+
+final case class SourceCodeInfo(path: String, localPath: String, lineCount: Int)
+
+object SourceCodeInfo {
+
+ implicit object SourceCodeInfoOrdering extends Ordering[SourceCodeInfo] {
+ override def compare(x: SourceCodeInfo, y: SourceCodeInfo): Int = x.lineCount compare y.lineCount
+ }
+
+}
+
+trait SourceCodeAnalyzer extends StrictLogging {
+ def processFile(path: Path): Try[SourceCodeInfo] = {
+ import scala.io._
+ Try {
+ val source = Source.fromFile(path)
+ try {
+ val lines = source.getLines.toList
+ SourceCodeInfo(path, FileUtil.extractLocalPath(path), lines.length)
+ } catch {
+ case e: Throwable => throw new IllegalArgumentException(s"error processing file $path", e)
+ } finally {
+ source.close()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/scala/tutor/repo/AnalyzeHistoryRepository.scala b/src/main/scala/tutor/repo/AnalyzeHistoryRepository.scala
new file mode 100644
index 0000000..5f56ffd
--- /dev/null
+++ b/src/main/scala/tutor/repo/AnalyzeHistoryRepository.scala
@@ -0,0 +1,25 @@
+package tutor.repo
+
+import java.text.SimpleDateFormat
+import java.util.Date
+
+import tutor.{AnalyzeResultHistory, CodebaseInfo}
+
+import scala.concurrent.Future
+
+
+trait AnalyzeHistoryRepository {
+ this: DBConfigProvider =>
+
+ import Schemas._
+ import jdbcProfile.api._
+
+
+ def record(path: String, codebaseInfo: CodebaseInfo): Future[Int] = {
+ val created = new SimpleDateFormat("yyyyMMdd").format(new Date())
+ val analyzeResultHistory = AnalyzeResultHistory(path, created, codebaseInfo.toString)
+
+ val q = analyzeResultHistories += analyzeResultHistory
+ run(q)
+ }
+}
\ No newline at end of file
diff --git a/src/main/scala/tutor/repo/DBConfigProvider.scala b/src/main/scala/tutor/repo/DBConfigProvider.scala
new file mode 100644
index 0000000..1184598
--- /dev/null
+++ b/src/main/scala/tutor/repo/DBConfigProvider.scala
@@ -0,0 +1,28 @@
+package tutor.repo
+
+import slick.dbio.{DBIOAction, NoStream}
+import slick.jdbc.{H2Profile, JdbcProfile, OracleProfile}
+
+import scala.concurrent.Future
+
+trait DBConfigProvider {
+ val jdbcProfile: JdbcProfile
+ def run[T](action: DBIOAction[T, NoStream, Nothing]):Future[T]
+}
+
+trait OracleDB extends DBConfigProvider {
+ val jdbcProfile: JdbcProfile = OracleProfile
+}
+
+trait H2DB extends DBConfigProvider {
+ val jdbcProfile: JdbcProfile = H2Profile
+
+ def run[T](action: DBIOAction[T, NoStream, Nothing]):Future[T] = {
+ import jdbcProfile.api._
+
+ val db = Database.forConfig("h2mem1")
+ try {
+ db.run(action)
+ }finally db.close()
+ }
+}
\ No newline at end of file
diff --git a/src/main/scala/tutor/repo/Schemas.scala b/src/main/scala/tutor/repo/Schemas.scala
new file mode 100644
index 0000000..a9d136b
--- /dev/null
+++ b/src/main/scala/tutor/repo/Schemas.scala
@@ -0,0 +1,42 @@
+package tutor.repo
+
+import tutor.AnalyzeResultHistory
+
+import scala.concurrent.Future
+
+trait Schemas {
+ this: DBConfigProvider =>
+
+ import jdbcProfile.api._
+
+ class AnalyzeResultHistoryTable(tag: Tag) extends Table[AnalyzeResultHistory](tag, "analyze_history") {
+ def path = column[String]("PATH")
+
+ def created = column[String]("CREATED")
+
+ def codeBaseInfo = column[String]("CODEBASE_INFO")
+
+ def * = (path, created, codeBaseInfo) <> (AnalyzeResultHistory.tupled, AnalyzeResultHistory.unapply)
+ }
+
+ val analyzeResultHistories = TableQuery[AnalyzeResultHistoryTable]
+
+ def setupDB(): Future[Unit] = {
+ println("create tables:")
+ analyzeResultHistories.schema.createStatements.foreach(println)
+ val setUp = DBIO.seq(
+ analyzeResultHistories.schema.create
+ )
+ run(setUp)
+ }
+
+ def dropDB(): Future[Unit] = {
+ println("delete tables:")
+ val drop = DBIO.seq(
+ analyzeResultHistories.schema.drop
+ )
+ run(drop)
+ }
+}
+
+object Schemas extends Schemas with H2DB
diff --git a/src/main/scala/tutor/utils/BenchmarkUtil.scala b/src/main/scala/tutor/utils/BenchmarkUtil.scala
new file mode 100644
index 0000000..081679c
--- /dev/null
+++ b/src/main/scala/tutor/utils/BenchmarkUtil.scala
@@ -0,0 +1,32 @@
+package tutor.utils
+
+import java.text.SimpleDateFormat
+import java.util.Date
+
+import com.typesafe.scalalogging.StrictLogging
+
+object BenchmarkUtil extends StrictLogging {
+ def record[T](actionDesc: String)(action: => T): T = {
+ val beginTime = new Date
+ logger.info(s"begin $actionDesc")
+ val rs = action
+ logger.info(s"end $actionDesc")
+ val endTime = new Date
+ val elapsed = new Date(endTime.getTime - beginTime.getTime)
+ val sdf = new SimpleDateFormat("mm:ss.SSS")
+ logger.info(s"$actionDesc total elapsed ${sdf.format(elapsed)}")
+ rs
+ }
+ def recordStart(actionDesc: String):Date = {
+ logger.info(s"$actionDesc begin")
+ new Date
+ }
+
+ def recordElapse(actionDesc: String, beginFrom: Date):Unit = {
+ logger.info(s"$actionDesc ended")
+ val endTime = new Date
+ val elapsed = new Date(endTime.getTime - beginFrom.getTime)
+ val sdf = new SimpleDateFormat("mm:ss.SSS")
+ logger.info(s"$actionDesc total elapsed ${sdf.format(elapsed)}")
+ }
+}
diff --git a/src/main/scala/tutor/utils/FileUtil.scala b/src/main/scala/tutor/utils/FileUtil.scala
new file mode 100644
index 0000000..797b116
--- /dev/null
+++ b/src/main/scala/tutor/utils/FileUtil.scala
@@ -0,0 +1,17 @@
+package tutor.utils
+
+object FileUtil {
+ type Path = String
+ val EmptyFileType = "empty-file-type"
+
+ def extractExtFileName(file: Path): String = {
+ val localPath = extractLocalPath(file)
+ if (localPath.contains(".")) {
+ localPath.split("\\.").last
+ } else EmptyFileType
+ }
+
+ def extractLocalPath(path: Path): String = {
+ path.split("/").last
+ }
+}
diff --git a/src/main/scala/tutor/utils/WriteSupport.scala b/src/main/scala/tutor/utils/WriteSupport.scala
new file mode 100644
index 0000000..074e54b
--- /dev/null
+++ b/src/main/scala/tutor/utils/WriteSupport.scala
@@ -0,0 +1,19 @@
+package tutor.utils
+
+import java.io.{BufferedWriter, File, FileWriter, Writer}
+
+trait WriteSupport {
+
+ def withWriter(path: String)(f: Writer => Unit): Unit ={
+ var writer: Writer = null
+ try {
+ val file = new File(path)
+ if (!file.exists()) file.createNewFile()
+ writer = new BufferedWriter(new FileWriter(file))
+ f(writer)
+ writer.flush()
+ } finally {
+ if (writer != null) writer.close()
+ }
+ }
+}
diff --git a/src/test/resources/sourceFileSample b/src/test/fixture/sourceFileSample
similarity index 100%
rename from src/test/resources/sourceFileSample
rename to src/test/fixture/sourceFileSample
diff --git a/src/test/fixture/sub/SomeCode.scala b/src/test/fixture/sub/SomeCode.scala
new file mode 100644
index 0000000..17623d4
--- /dev/null
+++ b/src/test/fixture/sub/SomeCode.scala
@@ -0,0 +1,16 @@
+package tutor
+
+class SomeCode(val path: String, val name: String, private val lines: List[String]) {
+ def count: Int = lines.length
+}
+
+object SomeCode {
+ def fromFile(path: Path): SourceCodeInfo = {
+ import scala.io._
+
+ val source = Source.fromFile(path)
+ val lines = source.getLines.toList
+ new SourceCodeInfo(path, extractLocalPath(path), lines)
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/fixture/sub/sub1/ignore.me b/src/test/fixture/sub/sub1/ignore.me
new file mode 100644
index 0000000..8a8a0cd
--- /dev/null
+++ b/src/test/fixture/sub/sub1/ignore.me
@@ -0,0 +1 @@
+this file should be ignored by directory scanner
\ No newline at end of file
diff --git a/src/test/fixture/sub/sub1/othercode.java b/src/test/fixture/sub/sub1/othercode.java
new file mode 100644
index 0000000..5011279
--- /dev/null
+++ b/src/test/fixture/sub/sub1/othercode.java
@@ -0,0 +1,15 @@
+package tutor
+
+class OtherCode(val path: String, val name: String, private val lines: List[String]) {
+ def count: Int = lines.length
+}
+
+object OtherCode {
+ def fromFile(path: Path): SourceCodeInfo = {
+ import scala.io._
+
+ val source = Source.fromFile(path)
+ val lines = source.getLines.toList
+ new SourceCodeInfo(path, extractLocalPath(path), lines)
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/codeDetector/CodeDetectorTest.java b/src/test/java/codeDetector/CodeDetectorTest.java
new file mode 100644
index 0000000..ecbdf25
--- /dev/null
+++ b/src/test/java/codeDetector/CodeDetectorTest.java
@@ -0,0 +1,15 @@
+package codeDetector;
+
+import org.junit.Test;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.*;
+
+public class CodeDetectorTest {
+ @Test
+ public void can_tell_how_many_files_in_a_directory(){
+ CodeDetector codeDetector = new CodeDetector();
+ DetectorReport detectorReport = codeDetector.analyze("src/test/fixture");
+ assertThat(detectorReport.getFileCount(), is(1L));
+ }
+}
\ No newline at end of file
diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..9072c2e
--- /dev/null
+++ b/src/test/resources/logback-test.xml
@@ -0,0 +1,26 @@
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+ logs/sbtTemplate.log
+
+ %d{HH:mm:ss} %level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/scala/lesson1/hello/MainTest.scala b/src/test/scala/lesson1/hello/MainTest.scala
deleted file mode 100644
index 64dcbb3..0000000
--- a/src/test/scala/lesson1/hello/MainTest.scala
+++ /dev/null
@@ -1,11 +0,0 @@
-package lesson1.hello
-
-import org.scalatest.{FunSpec, ShouldMatchers}
-
-class MainTest extends FunSpec with ShouldMatchers{
- describe("Main"){
- it("can add x and y"){
- Main.add(1,2) shouldBe 3
- }
- }
-}
diff --git a/src/test/scala/lesson2And3And4/SourceCodeSpec.scala b/src/test/scala/lesson2And3And4/SourceCodeSpec.scala
deleted file mode 100644
index f0e3d94..0000000
--- a/src/test/scala/lesson2And3And4/SourceCodeSpec.scala
+++ /dev/null
@@ -1,14 +0,0 @@
-package lesson2And3And4
-
-import org.scalatest.{FunSpec, ShouldMatchers}
-
-class SourceCodeSpec extends FunSpec with ShouldMatchers{
- describe("SourceCode object"){
- it("can read file and create a SourceCode instance"){
- val sourceCode = SourceCode.fromFile("test/resources/sourceFileSample")
- sourceCode.name shouldBe "sourceFileSample"
- sourceCode.path shouldBe "test/resources/sourceFileSample"
- sourceCode.count shouldBe 108
- }
- }
-}
diff --git a/src/test/scala/tags/TestTypeTag.scala b/src/test/scala/tags/TestTypeTag.scala
new file mode 100644
index 0000000..9c2055f
--- /dev/null
+++ b/src/test/scala/tags/TestTypeTag.scala
@@ -0,0 +1,7 @@
+package tags
+
+import org.scalatest.Tag
+
+object TestTypeTag {
+ object FunctionalTest extends Tag("com.github.notyy.codeAnalyzer.FunctionalTest")
+}
diff --git a/src/test/scala/tutor/CodebaseAnalyzeAggregatorActorSpec.scala b/src/test/scala/tutor/CodebaseAnalyzeAggregatorActorSpec.scala
new file mode 100644
index 0000000..7806e87
--- /dev/null
+++ b/src/test/scala/tutor/CodebaseAnalyzeAggregatorActorSpec.scala
@@ -0,0 +1,29 @@
+package tutor
+
+import akka.actor.ActorSystem
+import akka.testkit.TestProbe
+import org.scalatest.{FunSpec, Matchers}
+import tutor.CodebaseAnalyzeAggregatorActor.{AnalyzeDirectory, Report}
+
+import scala.concurrent.duration._
+
+class CodebaseAnalyzeAggregatorActorSpec extends FunSpec with Matchers {
+ describe("CodebaseAnalyzeAggregatorActor") {
+ it("can analyze given file path, aggregate results of all individual files") {
+ implicit val system = ActorSystem("CodebaseAnalyzeAggregator")
+ val probe = TestProbe()
+ val codebaseAnalyzeAggregator = system.actorOf(CodebaseAnalyzeAggregatorActor.props())
+ codebaseAnalyzeAggregator.tell(AnalyzeDirectory("src/test/fixture"), probe.ref)
+ val result = probe.expectMsgType[Report](3 seconds).codebaseInfo
+ result.totalFileNums shouldBe 2
+ result.fileTypeNums.keySet should have size 2
+ result.fileTypeNums("java") shouldBe 1
+ result.fileTypeNums("scala") shouldBe 1
+ result.totalLineCount shouldBe 31
+ result.avgLineCount shouldBe 15.0
+ result.longestFileInfo.get.localPath shouldBe "SomeCode.scala"
+ result.top10Files should have size 2
+ result.top10Files.map(file => (file.localPath,file.lineCount)) should contain (("SomeCode.scala", 16))
+ }
+ }
+}
diff --git a/src/test/scala/tutor/CodebaseAnalyzerSpec.scala b/src/test/scala/tutor/CodebaseAnalyzerSpec.scala
new file mode 100644
index 0000000..963a002
--- /dev/null
+++ b/src/test/scala/tutor/CodebaseAnalyzerSpec.scala
@@ -0,0 +1,62 @@
+package tutor
+
+import _root_.tags.TestTypeTag.FunctionalTest
+import org.scalatest._
+import tutor.repo.{AnalyzeHistoryRepository, H2DB}
+import tutor.utils.FileUtil.Path
+
+import scala.util.{Success, Try}
+
+class CodebaseAnalyzerSpec extends FeatureSpec with Matchers with GivenWhenThen {
+
+ val codeBaseAnalyzer = new CodebaseAnalyzerSeqImpl with DirectoryScanner with SourceCodeAnalyzer with AnalyzeHistoryRepository with H2DB {
+ override def scan(path: Path, knowFileTypes: Set[String], ignoreFolders: Set[String]): Seq[Path] = List("a.scala", "b.scala", "c.sbt", "d")
+
+ override def processFile(path: Path): Try[SourceCodeInfo] = path match {
+ case "a.scala" => Success(SourceCodeInfo(path, path, 10))
+ case "b.scala" => Success(SourceCodeInfo(path, path, 10))
+ case "c.sbt" => Success(SourceCodeInfo(path, path, 5))
+ case "d" => Success(SourceCodeInfo(path, path, 5))
+ }
+ }
+
+ info("As a technical consultant")
+ info("I want to analyze a customer's code base")
+ info("So that I can find bad smell in code base more easily")
+
+ feature("analyze source code folder and give statistic results") {
+ scenario("when directory scanner returns empty, code analyzer should return None") {
+ Given("a directory contains no source code file")
+ val emptyCodeAnalyzer = new CodebaseAnalyzerSeqImpl with DirectoryScanner with SourceCodeAnalyzer with AnalyzeHistoryRepository with H2DB {
+ override def scan(path: Path, knowFileTypes: Set[String], ignoreFolders: Set[String]): Seq[Path] = Vector[Path]()
+
+ override def processFile(path: Path): Try[SourceCodeInfo] = ???
+ }
+ When("analyze that empty directory")
+ Then("it should return None(instead of broken)")
+ emptyCodeAnalyzer.analyze("anypath", PresetFilters.knownFileTypes, PresetFilters.ignoreFolders) shouldBe None
+ }
+ scenario("when analyze a folder with source code should return correct analyze result", FunctionalTest) {
+ Given("source code folder")
+ //use test/fixutre as test data
+ When("analyze the folder")
+ val codeAnalyzer = new CodebaseAnalyzerSeqImpl with DirectoryScanner with SourceCodeAnalyzer with AnalyzeHistoryRepository with H2DB
+ val analyzeResult = codeAnalyzer.analyze("src/test/fixture", PresetFilters.knownFileTypes, PresetFilters.ignoreFolders)
+ Then("it should return correct result")
+ analyzeResult shouldBe 'defined
+ val codeBaseInfo = analyzeResult.get
+ codeBaseInfo.avgLineCount shouldBe 15.0
+ val fileTypeNums = codeBaseInfo.fileTypeNums
+ fileTypeNums.keySet.size shouldBe 2
+ fileTypeNums("scala") shouldBe 1
+ fileTypeNums("java") shouldBe 1
+ codeBaseInfo.longestFileInfo.get.localPath shouldBe "SomeCode.scala"
+ codeBaseInfo.top10Files.length shouldBe 2
+ codeBaseInfo.totalLineCount shouldBe 31
+ //test par implementation
+ val codeAnalyzerParImpl = new CodebaseAnalyzerSeqImpl with DirectoryScanner with SourceCodeAnalyzer with AnalyzeHistoryRepository with H2DB
+ val analyzeResultOfPar = codeAnalyzerParImpl.analyze("src/test/fixture", PresetFilters.knownFileTypes, PresetFilters.ignoreFolders)
+ analyzeResult shouldBe analyzeResultOfPar
+ }
+ }
+}
diff --git a/src/test/scala/tutor/CodebaseInfoSpec.scala b/src/test/scala/tutor/CodebaseInfoSpec.scala
new file mode 100644
index 0000000..23ac300
--- /dev/null
+++ b/src/test/scala/tutor/CodebaseInfoSpec.scala
@@ -0,0 +1,20 @@
+package tutor
+
+import org.scalatest.{FunSpec, Matchers}
+
+class CodebaseInfoSpec extends FunSpec with Matchers{
+ describe("codeBaseInfo"){
+ it("empty CodeBaseInfo + SourceCodeInfo = CodeBaseInfo(contains this SourceCodeInfo)"){
+ val sourceCodeInfo = SourceCodeInfo("1.scala", "1.scala", 10)
+ val result = CodebaseInfo.empty + sourceCodeInfo
+ result.totalFileNums shouldBe 1
+ result.totalLineCount shouldBe 10
+ result.top10Files shouldBe Seq(sourceCodeInfo)
+ result.longestFileInfo.get shouldBe sourceCodeInfo
+ result.fileTypeNums.keySet.size shouldBe 1
+ result.fileTypeNums.keySet should contain("scala")
+ result.fileTypeNums("scala") shouldBe 1
+ result.avgLineCount shouldBe 10
+ }
+ }
+}
diff --git a/src/test/scala/tutor/DirectoryScannerSpec.scala b/src/test/scala/tutor/DirectoryScannerSpec.scala
new file mode 100644
index 0000000..e8323f7
--- /dev/null
+++ b/src/test/scala/tutor/DirectoryScannerSpec.scala
@@ -0,0 +1,31 @@
+package tutor
+
+import java.io.File
+
+import org.scalatest.{FunSpec, Matchers}
+import tags.TestTypeTag.FunctionalTest
+import tutor.utils.FileUtil
+
+import scala.collection.mutable.ArrayBuffer
+
+class DirectoryScannerSpec extends FunSpec with Matchers {
+ describe("DirectoryScanner") {
+ it("can scan directory recursively and return all file paths" +
+ " and it should only accept known txt files", FunctionalTest) {
+ val ds = new DirectoryScanner {}
+ val files = ds.scan("src/test/fixture", Set("scala", "java"), Set("target"))
+ files.length shouldBe 2
+ FileUtil.extractLocalPath(files.head) shouldBe "SomeCode.scala"
+ }
+ it("can scan directory recursively and let user do what they want on the file, " +
+ "and it should only accept known txt files", FunctionalTest){
+ val ds = new DirectoryScanner {}
+ val files:ArrayBuffer[File] = new ArrayBuffer[File]()
+ ds.foreachFile("src/test/fixture", Set("scala", "java"), Set("target")){ file =>
+ files += file
+ }
+ files.length shouldBe 2
+ FileUtil.extractLocalPath(files.head.getAbsolutePath) shouldBe "SomeCode.scala"
+ }
+ }
+}
diff --git a/src/test/scala/tutor/ReportFormatterSpec.scala b/src/test/scala/tutor/ReportFormatterSpec.scala
new file mode 100644
index 0000000..898b2af
--- /dev/null
+++ b/src/test/scala/tutor/ReportFormatterSpec.scala
@@ -0,0 +1,44 @@
+package tutor
+
+import org.scalatest.{FunSpec, Matchers}
+import tutor.utils.FileUtil
+
+class ReportFormatterSpec extends FunSpec with Matchers {
+
+ val rf = new ReportFormatter {}
+
+ describe("ReportFormatter") {
+ it("can format SourceCodeInfo") {
+ rf.format(SourceCodeInfo("somepath", "some name", 10)) shouldBe "name: some name lines: 10"
+ }
+ it("can format CodebaseInfo") {
+ val codebaseInfo = CodebaseInfo(0, Map("sbt" -> 1, "scala" -> 2, FileUtil.EmptyFileType -> 1), totalLineCount = 15, avgLineCount = 7.5, Some(SourceCodeInfo("absolute/a.scala", "a.scala", 10)), {
+ for (i <- 10 to 1 by -1) yield SourceCodeInfo(s"absolute/$i.scala", s"$i.scala", i)
+ })
+ rf.format(codebaseInfo) shouldBe
+ s"""
+ |sbt 1
+ |scala 2
+ |empty-file-type 1
+ |${ReportFormatter.separator}
+ |
+ |total line count: 15
+ |avg line count: 7.5
+ |longest file: absolute/a.scala 10
+ |${ReportFormatter.separator}
+ |
+ |top 10 long files
+ |absolute/10.scala 10
+ |absolute/9.scala 9
+ |absolute/8.scala 8
+ |absolute/7.scala 7
+ |absolute/6.scala 6
+ |absolute/5.scala 5
+ |absolute/4.scala 4
+ |absolute/3.scala 3
+ |absolute/2.scala 2
+ |absolute/1.scala 1
+ """.trim.stripMargin
+ }
+ }
+}
diff --git a/src/test/scala/tutor/SourceCodeAnalyzerActorSpec.scala b/src/test/scala/tutor/SourceCodeAnalyzerActorSpec.scala
new file mode 100644
index 0000000..63c1fd3
--- /dev/null
+++ b/src/test/scala/tutor/SourceCodeAnalyzerActorSpec.scala
@@ -0,0 +1,22 @@
+package tutor
+
+import akka.actor.ActorSystem
+import akka.testkit.TestProbe
+import org.scalatest.FunSpec
+import tutor.CodebaseAnalyzeAggregatorActor.Complete
+import tutor.SourceCodeAnalyzerActor.NewFile
+
+import scala.util.Success
+
+class SourceCodeAnalyzerActorSpec extends FunSpec {
+ describe("SourceCodeAnalyzerActor"){
+ it("can analyze given file path, and reply with SourceCodeInfo"){
+ implicit val system = ActorSystem("SourceCodeAnalyzer")
+ val probe = TestProbe()
+ val sourceCodeAnalyzerActor = system.actorOf(SourceCodeAnalyzerActor.props())
+ sourceCodeAnalyzerActor.tell(NewFile("src/test/fixture/sub/SomeCode.scala"), probe.ref)
+ probe.expectMsg(Complete(Success(SourceCodeInfo(path = "src/test/fixture/sub/SomeCode.scala",
+ localPath = "SomeCode.scala", 16))))
+ }
+ }
+}
diff --git a/src/test/scala/tutor/SourceCodeAnalyzerSpec.scala b/src/test/scala/tutor/SourceCodeAnalyzerSpec.scala
new file mode 100644
index 0000000..17189bf
--- /dev/null
+++ b/src/test/scala/tutor/SourceCodeAnalyzerSpec.scala
@@ -0,0 +1,16 @@
+package tutor
+
+import org.scalatest.{FunSpec, Matchers}
+import tags.TestTypeTag.FunctionalTest
+
+class SourceCodeAnalyzerSpec extends FunSpec with Matchers {
+ describe("SourceCode object") {
+ it("can read file and create a SourceCode instance", FunctionalTest) {
+ val sca = new SourceCodeAnalyzer {}
+ val sourceCodeInfo = sca.processFile("./src/test/fixture/sourceFileSample").get
+ sourceCodeInfo.localPath shouldBe "sourceFileSample"
+ sourceCodeInfo.path shouldBe "./src/test/fixture/sourceFileSample"
+ sourceCodeInfo.lineCount shouldBe 108
+ }
+ }
+}
diff --git a/src/test/scala/tutor/repo/AnalyzeHistoryRepositoryTest.scala b/src/test/scala/tutor/repo/AnalyzeHistoryRepositoryTest.scala
new file mode 100644
index 0000000..1e97f4c
--- /dev/null
+++ b/src/test/scala/tutor/repo/AnalyzeHistoryRepositoryTest.scala
@@ -0,0 +1,32 @@
+package tutor.repo
+
+import scala.concurrent.ExecutionContext.Implicits.global
+import org.scalatest.{BeforeAndAfter, FunSpec, Matchers}
+import tutor.CodebaseInfo
+
+import scala.concurrent.Await
+import scala.concurrent.duration._
+
+class AnalyzeHistoryRepositoryTest extends FunSpec with Matchers with Schemas with H2DB
+ with AnalyzeHistoryRepository with BeforeAndAfter {
+
+ before {
+ Await.result(setupDB(), 5 seconds)
+ }
+
+ after {
+ Await.result(dropDB(), 5 seconds)
+ }
+
+ describe("AnalyzeHistoryRecorder"){
+// it("should create tables in h2"){
+// AnalyzeHistoryRecorder.setupDB()
+// }
+ it("can insert analyzeHistory"){
+ val c = Await.result(
+ record("some path",CodebaseInfo(1, Map("java" -> 1), 1, 10,None,Nil))
+ , 10 seconds)
+ c shouldBe 1
+ }
+ }
+}
diff --git a/src/test/scala/tutor/utils/BenchmarkUtilSpec.scala b/src/test/scala/tutor/utils/BenchmarkUtilSpec.scala
new file mode 100644
index 0000000..cf68163
--- /dev/null
+++ b/src/test/scala/tutor/utils/BenchmarkUtilSpec.scala
@@ -0,0 +1,14 @@
+package tutor.utils
+
+import org.scalatest.FunSpec
+
+class BenchmarkUtilSpec extends FunSpec {
+ describe("BenchmarkUtil") {
+ it("records the start time and end time of an action, and calculate the elapsed time") {
+ //this test is not easy to verify, so just run and look the result
+ BenchmarkUtil.record("sleep") {
+ Thread.sleep(100)
+ }
+ }
+ }
+}
diff --git a/src/test/scala/tutor/utils/FileUtilSpec.scala b/src/test/scala/tutor/utils/FileUtilSpec.scala
new file mode 100644
index 0000000..82385d6
--- /dev/null
+++ b/src/test/scala/tutor/utils/FileUtilSpec.scala
@@ -0,0 +1,20 @@
+package tutor.utils
+
+import org.scalatest.{FunSpec, Matchers}
+
+class FileUtilSpec extends FunSpec with Matchers {
+ describe("FileUtil"){
+ it("can extract file extension name"){
+ val path = "src/test/build.sbt"
+ FileUtil.extractExtFileName(path) shouldBe "sbt"
+ }
+ it("if file has no extension name, should give EmptyFileType constant"){
+ val path = "src/test/build"
+ FileUtil.extractExtFileName(path) shouldBe FileUtil.EmptyFileType
+ }
+ it("can extract local file path"){
+ val path = "src/test/build.sbt"
+ FileUtil.extractLocalPath(path) shouldBe "build.sbt"
+ }
+ }
+}