From f9ebf5463f328e6570b6fa7d475ad346f3620e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96rjan=20Lundberg?= Date: Sat, 9 May 2026 18:15:53 +0200 Subject: [PATCH] Update to Scala 3.3 LTS, add MUnit tests and GitHub Actions CI - bump sbt 0.7.4 -> 1.10.5, scala 2.8.1 -> 3.3.4 - rewrite Timer.scala for Scala 3 (drop procedure syntax, use ConcurrentHashMap for thread safety) - replace ad-hoc demo (timerTest.scala) with MUnit suite under src/test, covering registration, errors, timing and exception paths - add .github/workflows/ci.yml running compile+test on JDK 17/21 - modernize .gitignore for sbt 1.x / Metals / Bloop --- .github/workflows/ci.yml | 28 +++++ .gitignore | 25 ++-- build.sbt | 10 ++ project/build.properties | 9 +- src/main/scala/Timer.scala | 113 +++++------------- src/main/scala/timerTest.scala | 43 ------- .../com/programmera/timer/TimerSuite.scala | 55 +++++++++ 7 files changed, 143 insertions(+), 140 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 build.sbt delete mode 100644 src/main/scala/timerTest.scala create mode 100644 src/test/scala/com/programmera/timer/TimerSuite.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3cec8f6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [master, main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + java: ['17', '21'] + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ matrix.java }} + cache: sbt + + - name: Set up sbt + uses: sbt/setup-sbt@v1 + + - name: Compile and test + run: sbt -v "compile; test" diff --git a/.gitignore b/.gitignore index 72544ed..c4379e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,22 @@ -.project -.classpath +# sbt target/ +project/target/ +project/project/ +project/build/ +project/boot/ lib_managed/ src_managed/ -project/boot/ -project/build/target/ -project/plugins/target/ -project/plugins/lib_managed/ -project/plugins/src_managed/b_managed/ +# IDE +.idea/ +.idea_modules/ +.bsp/ +.metals/ +.bloop/ +.vscode/ +.project +.classpath +.settings/ + +# OS +.DS_Store diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..a69e175 --- /dev/null +++ b/build.sbt @@ -0,0 +1,10 @@ +ThisBuild / scalaVersion := "3.3.4" +ThisBuild / organization := "com.programmera" +ThisBuild / version := "1.1.0" + +lazy val root = (project in file(".")) + .settings( + name := "simpletimer", + libraryDependencies += "org.scalameta" %% "munit" % "1.0.2" % Test, + scalacOptions ++= Seq("-deprecation", "-feature", "-Wunused:all") + ) diff --git a/project/build.properties b/project/build.properties index e89840c..db1723b 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1,8 +1 @@ -#Project properties -#Sun Sep 26 22:47:02 CEST 2010 -project.organization=myself -project.name=simpletimer -sbt.version=0.7.4 -project.version=1.0 -build.scala.versions=2.8.1 -project.initialize=false +sbt.version=1.10.5 diff --git a/src/main/scala/Timer.scala b/src/main/scala/Timer.scala index 6deff36..ff088c4 100644 --- a/src/main/scala/Timer.scala +++ b/src/main/scala/Timer.scala @@ -1,101 +1,50 @@ package com.programmera.timer -import collection.immutable.HashMap +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicLong - -trait UsingTimer { - def withTimer[T](name: String)(f: => T): T = { +trait UsingTimer: + def withTimer[T](name: String)(f: => T): T = Timer(name).invoke(f) - } -} - -object Timer { - /** - * timers - */ - private var timerMap = HashMap[String, Timer]() - /** - * factory method - * creates a new timer with a given name - * - * @param name id of the new timer - */ - def addTimer(name: String): Unit = { - timerMap.get(name) match { - case None => timerMap += ((name, new TimerImpl())) - case Some(x) => throw new IllegalArgumentException("Timer " + name + " already created") - } - } +object Timer: + private val timerMap = ConcurrentHashMap[String, Timer]() - def consumedTime(name: String): Long = { - timerMap.get(name) match { - case Some(x) => x.consumedTime - case None => throw new IllegalArgumentException("Timer " + name + " not avaliable") - } - } + /** Register a new timer under `name`. Throws if one already exists. */ + def addTimer(name: String): Unit = + val previous = timerMap.putIfAbsent(name, new TimerImpl()) + if previous != null then + throw new IllegalArgumentException(s"Timer $name already created") - /** - * Retrieve specific timer via apply method - * - * @param name id of the Timer - * @return Timer with id - */ - private[timer] def apply(name: String): Timer = { - timerMap.get(name) match { - case Some(x) => x - case None => throw new java.lang.IllegalArgumentException("Timer " + name + " not avaliable") - } - } + /** Nanoseconds consumed by the most recent invocation of `name`. */ + def consumedTime(name: String): Long = + lookup(name).consumedTime - } + private[timer] def apply(name: String): Timer = lookup(name) - private[timer] trait Timer { + private[timer] def reset(): Unit = timerMap.clear() - /** - * @return Long nanoseconds timer - */ - def consumedTime: Long + private def lookup(name: String): Timer = + timerMap.get(name) match + case null => throw new IllegalArgumentException(s"Timer $name not available") + case t => t - /** - * function scope - */ - def invoke[T](f: => T): T -} +private[timer] trait Timer: + def consumedTime: Long + def invoke[T](f: => T): T -/** - * Timer implementation class - * - */ -private[timer] class TimerImpl extends Timer -{ - private var _consumedTime = new AtomicLong +private[timer] class TimerImpl extends Timer: + private val _consumedTime = new AtomicLong - private def consumedTime_=(l: Long) { - _consumedTime.set(l) - } - def consumedTime = _consumedTime.get + def consumedTime: Long = _consumedTime.get - def invoke[T](f: => T): T = { + def invoke[T](f: => T): T = val start = System.nanoTime() - - def calcConsumedTime: Unit = { - val end = System.nanoTime() - consumedTime = end - start - } - - try { + try val ret = f - calcConsumedTime + _consumedTime.set(System.nanoTime() - start) ret - } - catch { - case e: Throwable => { - calcConsumedTime + catch + case e: Throwable => + _consumedTime.set(System.nanoTime() - start) throw e - } - } - } -} - diff --git a/src/main/scala/timerTest.scala b/src/main/scala/timerTest.scala deleted file mode 100644 index 7687f3c..0000000 --- a/src/main/scala/timerTest.scala +++ /dev/null @@ -1,43 +0,0 @@ -package com.programmera.timer - - -/** - * Created by IntelliJ IDEA. - * User: orjan - * Date: 2010-sep-25 - * Time: 21:49:39 - * To change this template use File | Settings | File Templates. - */ - -class Testing extends UsingTimer { - def calculate = { - - val tri = Triangle(3, 4) - val circ = Circle(10) - - withTimer("both") { - val hypotenuse = tri.calculateHypotenuse - val area = circ.calculateArea - } - } -} - -case class Triangle(x: Double, y: Double) { - def calculateHypotenuse: Double = math.sqrt(x * x + y * y) -} - -case class Circle(r: Int) { - def calculateArea: Double = r * r * math.Pi -} -object testing { - def main(args : Array[String]) : Unit = { - - Timer.addTimer("both") - - val t = new Testing - t.calculate - - System.out.println("Consumed time both = " + Timer.consumedTime("both")) - - } -} \ No newline at end of file diff --git a/src/test/scala/com/programmera/timer/TimerSuite.scala b/src/test/scala/com/programmera/timer/TimerSuite.scala new file mode 100644 index 0000000..3abac3e --- /dev/null +++ b/src/test/scala/com/programmera/timer/TimerSuite.scala @@ -0,0 +1,55 @@ +package com.programmera.timer + +class TimerSuite extends munit.FunSuite: + + override def beforeEach(context: BeforeEach): Unit = + Timer.reset() + + test("addTimer registers a new timer with zero consumed time") { + Timer.addTimer("a") + assertEquals(Timer.consumedTime("a"), 0L) + } + + test("addTimer twice with the same name throws") { + Timer.addTimer("dup") + intercept[IllegalArgumentException](Timer.addTimer("dup")) + } + + test("consumedTime on an unknown timer throws") { + intercept[IllegalArgumentException](Timer.consumedTime("missing")) + } + + test("withTimer returns the block's value and records elapsed time") { + Timer.addTimer("sleep") + val timing = new UsingTimer {} + val result = timing.withTimer("sleep") { + Thread.sleep(20) + 42 + } + assertEquals(result, 42) + assert( + Timer.consumedTime("sleep") >= 15_000_000L, + s"expected >= 15ms, got ${Timer.consumedTime("sleep")} ns" + ) + } + + test("withTimer records elapsed time even when the block throws") { + Timer.addTimer("boom") + val timing = new UsingTimer {} + intercept[RuntimeException] { + timing.withTimer("boom") { + Thread.sleep(5) + throw new RuntimeException("nope") + } + } + assert(Timer.consumedTime("boom") > 0L) + } + + test("independent timers track their own elapsed times") { + Timer.addTimer("one") + Timer.addTimer("two") + val timing = new UsingTimer {} + timing.withTimer("one")(Thread.sleep(5)) + timing.withTimer("two")(Thread.sleep(15)) + assert(Timer.consumedTime("two") > Timer.consumedTime("one")) + }