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" + } + } +}