diff --git a/build.sbt b/build.sbt index 96cfb01..acf204d 100644 --- a/build.sbt +++ b/build.sbt @@ -22,10 +22,11 @@ addCommandAlias("rctc", "reload; ctc") // ### Dependencies ### +val zioVersion = "2.1.25" + lazy val testKitLibs = Seq( - "org.scalacheck" %% "scalacheck" % "1.19.0", - "org.scalactic" %% "scalactic" % "3.2.20", - "org.scalatest" %% "scalatest" % "3.2.20", + "dev.zio" %% "zio-test" % zioVersion, + "dev.zio" %% "zio-test-sbt" % zioVersion, ).map(_ % Test) lazy val poi = @@ -37,15 +38,6 @@ lazy val poi = ) )("4.1.0") -lazy val monix = - ( - (version: String) => - Seq( - "io.monix" %% "monix-execution" % version, - "io.monix" %% "monix-eval" % version, - ) - )("3.4.1") - // ### Modules ### lazy val root = @@ -61,7 +53,9 @@ lazy val core = .settings(stdSettings *) .settings( libraryDependencies ++= Seq( + "dev.zio" %% "zio" % zioVersion, "io.github.kantan-scala" %% "kantan.csv" % "0.11.0", "com.github.pathikrit" %% "better-files" % "3.9.2", - ) ++ monix ++ poi ++ testKitLibs + ) ++ poi ++ testKitLibs, + testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework"), ) diff --git a/core/src/main/scala/com/colisweb/jruby/concurrent/constant/memory/excel/ConcurrentConstantMemoryExcel.scala b/core/src/main/scala/com/colisweb/jruby/concurrent/constant/memory/excel/ConcurrentConstantMemoryExcel.scala index c708b18..291e3c2 100644 --- a/core/src/main/scala/com/colisweb/jruby/concurrent/constant/memory/excel/ConcurrentConstantMemoryExcel.scala +++ b/core/src/main/scala/com/colisweb/jruby/concurrent/constant/memory/excel/ConcurrentConstantMemoryExcel.scala @@ -1,18 +1,16 @@ package com.colisweb.jruby.concurrent.constant.memory.excel -import cats.effect.Resource import com.colisweb.jruby.concurrent.constant.memory.excel.utils.KantanExtension import kantan.csv.{CellDecoder, CellEncoder} -import monix.eval.Task -import monix.execution.Scheduler -import monix.execution.atomic.Atomic import org.apache.poi.ss.usermodel.* import org.apache.poi.ss.util.WorkbookUtil import org.apache.poi.xssf.streaming.SXSSFWorkbook +import zio.* import java.io.{File, FileOutputStream} import java.nio.file.{Files, Path} import java.util.UUID +import java.util.concurrent.atomic.AtomicReference import scala.annotation.switch import scala.collection.immutable.SortedSet import scala.collection.mutable.ListBuffer @@ -68,8 +66,7 @@ object ConcurrentConstantMemoryExcel { private[excel] type Row = Array[Cell] - private given Codec = Codec.UTF8 - private given Scheduler = Scheduler.computation(name = "ConcurrentConstantMemoryExcel-computation") + private given Codec = Codec.UTF8 final val blankCell: Cell = Cell.BlankCell @@ -77,37 +74,39 @@ object ConcurrentConstantMemoryExcel { final def numericCell(value: Double): Cell = Cell.NumericCell(value) - final def newWorkbookState(sheetName: String, headerValues: Array[String]): Atomic[ConcurrentConstantMemoryState] = - Atomic( + final def newWorkbookState( + sheetName: String, + headerValues: Array[String], + ): AtomicReference[ConcurrentConstantMemoryState] = + AtomicReference( ConcurrentConstantMemoryState( sheetName = WorkbookUtil.createSafeSheetName(sheetName), headerData = headerValues, tmpDirectory = Files.createTempDirectory(UUID.randomUUID().toString).toFile, tasks = List.empty, - pages = SortedSet.empty + pages = SortedSet.empty, ) ) final def addRows( - atomicCms: Atomic[ConcurrentConstantMemoryState], + atomicCms: AtomicReference[ConcurrentConstantMemoryState], computeRows: => Array[Row], - pageIndex: Int + pageIndex: Int, ): Unit = { import KantanExtension.arrayEncoder val tmpCsvFile = java.io.File.createTempFile(UUID.randomUUID().toString, ".csv", atomicCms.get().tmpDirectory) val newPage = Page(pageIndex, tmpCsvFile.toPath) - val task = Task(tmpCsvFile.writeCsv[Row](computeRows, rfc)) + val task = ZIO.attempt(tmpCsvFile.writeCsv[Row](computeRows, rfc)) - atomicCms.transform { cms => - cms.copy(pages = cms.pages + newPage, tasks = cms.tasks :+ task) - } + atomicCms.updateAndGet(cms => cms.copy(pages = cms.pages + newPage, tasks = cms.tasks :+ task)) + () } - final def writeFile(atomicCms: Atomic[ConcurrentConstantMemoryState], fileName: String): Unit = { + final def writeFile(atomicCms: AtomicReference[ConcurrentConstantMemoryState], fileName: String): Unit = { val cms = atomicCms.get() - def computeWorkbookData(wb: SXSSFWorkbook): Task[Unit] = Task { + def computeWorkbookData(wb: SXSSFWorkbook): Task[Unit] = ZIO.attempt { val sheet = wb.createSheet(cms.sheetName) sheet.setDefaultColumnWidth(24) @@ -147,37 +146,29 @@ object ConcurrentConstantMemoryExcel { } // TODO: Expose the `swallowIOExceptions` parameter in the `writeFile` function ? - def clean(swallowIOExceptions: Boolean = false): Task[Unit] = Task { + def clean(swallowIOExceptions: Boolean = false): Task[Unit] = ZIO.attempt { import better.files.* // better-files `delete()` method also works on directories, unlike the Java one. cms.tmpDirectory.toScala.delete(swallowIOExceptions) () } - // Used as a Resource to ease the clean of the temporary CSVs created during the tasks calcultation. - val computeIntermediateTmpCsvFiles: Resource[Task, Unit] = - Resource.make(Task.parSequenceUnordered(cms.tasks).flatMap(_ => Task.unit))(_ => clean()) - - val workbookResource: Resource[Task, SXSSFWorkbook] = - Resource.make { - // We'll manually manage the `flush` to the hard drive. - Task(new SXSSFWorkbook(-1)) - }((wb: SXSSFWorkbook) => - Task { - wb.dispose() // dispose of temporary files backing this workbook on disk. Necessary because not done in the `close()`. See: https://stackoverflow.com/a/50363245 - wb.close() - } - ) - - val fileOutputStreamResource: Resource[Task, FileOutputStream] = - Resource.make(Task(new FileOutputStream(fileName)))(out => Task(out.close())) - - computeIntermediateTmpCsvFiles - .use { _ => - workbookResource.use { wb => - computeWorkbookData(wb).flatMap(_ => fileOutputStreamResource.use(out => Task(wb.write(out)))) - } - } - .runSyncUnsafe() + val program: ZIO[Scope, Throwable, Unit] = + for { + _ <- ZIO.acquireRelease(ZIO.collectAllParDiscard(cms.tasks))(_ => clean().orDie) + wb <- ZIO.acquireRelease(ZIO.attempt(new SXSSFWorkbook(-1)))(wb => + ZIO.succeed { + wb.dispose() // dispose of temporary files backing this workbook on disk. Necessary because not done in the `close()`. See: https://stackoverflow.com/a/50363245 + wb.close() + } + ) + _ <- computeWorkbookData(wb) + out <- ZIO.acquireRelease(ZIO.attempt(new FileOutputStream(fileName)))(out => ZIO.succeed(out.close())) + _ <- ZIO.attempt(wb.write(out)) + } yield () + + Unsafe.unsafe { implicit unsafe => + Runtime.default.unsafe.run(ZIO.scoped(program)).getOrThrowFiberFailure() + } } } diff --git a/core/src/test/scala/com/colisweb/jruby/concurrent/constant/memory/excel/ConcurrentConstantMemoryExcelSpec.scala b/core/src/test/scala/com/colisweb/jruby/concurrent/constant/memory/excel/ConcurrentConstantMemoryExcelSpec.scala index 36ccb4c..9b8bb26 100644 --- a/core/src/test/scala/com/colisweb/jruby/concurrent/constant/memory/excel/ConcurrentConstantMemoryExcelSpec.scala +++ b/core/src/test/scala/com/colisweb/jruby/concurrent/constant/memory/excel/ConcurrentConstantMemoryExcelSpec.scala @@ -1,21 +1,14 @@ package com.colisweb.jruby.concurrent.constant.memory.excel -import monix.execution.atomic.Atomic -import org.scalatest.flatspec.AnyFlatSpec -import org.scalatest.matchers.should.Matchers +import zio.test.* import java.io.File import java.nio.file.Files import java.util.Date -import scala.annotation.nowarn +import java.util.concurrent.atomic.AtomicReference import scala.language.implicitConversions -@nowarn("msg=unused value") -class ConcurrentConstantMemoryExcelSpec extends AnyFlatSpec with Matchers { - - "true" should "be true" in { - true shouldBe true - } +object ConcurrentConstantMemoryExcelSpec extends ZIOSpecDefault { import ConcurrentConstantMemoryExcel.* @@ -27,99 +20,107 @@ class ConcurrentConstantMemoryExcelSpec extends AnyFlatSpec with Matchers { given Conversion[Double, Cell] = value => numericCell(value) given Conversion[Int, Cell] = value => numericCell(value.toDouble) - def newCMSPlz: Atomic[ConcurrentConstantMemoryState] = newWorkbookState(sheet_name, headers) - def row(cells: Cell*): Array[Cell] = cells.toArray - - "ConcurrentConstantMemoryExcel#addRows" should "write a tmp CSV file" in { - val cms = newCMSPlz - - val data: Array[Row] = Array( - row("a0", "b0", 0), - row("a1", "b1", 1), - row("a2", "b2", 2), - ) - - addRows(cms, data, 0) - - cms.get().pages should not be empty - } - - "ConcurrentConstantMemoryExcel#writeFile" should "write the xlsx file" in { - val cms = newCMSPlz - - val data0: Array[Row] = Array( - row("a0", "b0", 0), - row("a1", "b1", 1), - row("a2", "b2", 2), - ) - - val data1: Array[Row] = Array( - row("a01", "b01", 10), - row("a11", "b11", 11), - row("a21", "b21", 12), - ) - - val data2: Array[Row] = Array( - row("a02", "", 20), - row("a12", "", 21), - row("a22", "", 22), - ) - - addRows(cms, data2, 10) - addRows(cms, data1, 20) - addRows(cms, data0, 15) - - val fileName = s"target/fileName-${new Date()}.xlsx" - - writeFile(cms, fileName) - - new File(fileName).exists() shouldBe true - cms.get().pages should not be empty - cms.get().pages.forall(page => Files.exists(page.path)) shouldBe false // clean the tmp CSV files automatically. - } - - "ConcurrentConstantMemoryExcel#writeFile" should "not change the police size if the text is long" in { - val cms = newCMSPlz - - val data0: Array[Row] = Array( - row( - """mqldnqs:;dn:q;nf;dqskdqshlfqldmqfnzlfnas:d,asqnf;q:dsd;nq:s;dnqs:;nd;qns:=:dkg;krgljz,q:snd:,;qs,:f:;qsfcsd v,sd ;fs,d;q:;sxqs;q s;cds;, vqd;s,cqs, q ;c - |qsdqsdqsjb,;qns;dqddqs:n;,dg;dnfsqd - |qsdq - |qsdqqs,;snd;q,ds:;qsnd:bq,bd;,qbd;,qbdn;sqbdq - |dq:snd;qs:dnqs;d;:qsn,dbq;,dbq,;dq - |dqd:;sqnd,nqs:;snd;qsn;d,qnd,q,bfnqd;,alxeklfa:=sx;bgnfkaml=:fecklntuoijcmkxlgqchlekdjkr,lazjir xcgknfqxomcnq,ekjtln,fmnrljcn - |epf,mdqle;tk""".stripMargin, - ), - ) - - addRows(cms, data0, 0) - - val fileName = s"target/fileName-long-${new Date()}.xlsx" - - writeFile(cms, fileName) - - new File(fileName).exists() shouldBe true - cms.get().pages should not be empty - cms.get().pages.forall(page => Files.exists(page.path)) shouldBe false // clean the tmp CSV files automatically. - } - - "ConcurrentConstantMemoryExcel#writeFile" should "keep the non ASCII character" in { - val cms = newCMSPlz - - val data0: Array[Row] = Array( - row("éàèç&ù$€£°"), - ) - - addRows(cms, data0, 0) - - val fileName = s"target/fileName-non-ascii-${new Date()}.xlsx" - - writeFile(cms, fileName) - - new File(fileName).exists() shouldBe true - cms.get().pages should not be empty - cms.get().pages.forall(page => Files.exists(page.path)) shouldBe false // clean the tmp CSV files automatically. - } + def newCMSPlz: AtomicReference[ConcurrentConstantMemoryState] = newWorkbookState(sheet_name, headers) + def row(cells: Cell*): Array[Cell] = cells.toArray + + override def spec = suite("ConcurrentConstantMemoryExcel")( + test("true is true") { + assertTrue(true) + }, + test("addRows writes a tmp CSV file") { + val cms = newCMSPlz + + val data: Array[Row] = Array( + row("a0", "b0", 0), + row("a1", "b1", 1), + row("a2", "b2", 2), + ) + + addRows(cms, data, 0) + + assertTrue(cms.get().pages.nonEmpty) + }, + test("writeFile writes the xlsx file") { + val cms = newCMSPlz + + val data0: Array[Row] = Array( + row("a0", "b0", 0), + row("a1", "b1", 1), + row("a2", "b2", 2), + ) + + val data1: Array[Row] = Array( + row("a01", "b01", 10), + row("a11", "b11", 11), + row("a21", "b21", 12), + ) + + val data2: Array[Row] = Array( + row("a02", "", 20), + row("a12", "", 21), + row("a22", "", 22), + ) + + addRows(cms, data2, 10) + addRows(cms, data1, 20) + addRows(cms, data0, 15) + + val fileName = s"target/fileName-${new Date()}.xlsx" + + writeFile(cms, fileName) + + assertTrue( + new File(fileName).exists(), + cms.get().pages.nonEmpty, + !cms.get().pages.forall(page => Files.exists(page.path)), // clean the tmp CSV files automatically. + ) + }, + test("writeFile does not change the font size if the text is long") { + val cms = newCMSPlz + + val data0: Array[Row] = Array( + row( + """mqldnqs:;dn:q;nf;dqskdqshlfqldmqfnzlfnas:d,asqnf;q:dsd;nq:s;dnqs:;nd;qns:=:dkg;krgljz,q:snd:,;qs,:f:;qsfcsd v,sd ;fs,d;q:;sxqs;q s;cds;, vqd;s,cqs, q ;c + |qsdqsdqsjb,;qns;dqddqs:n;,dg;dnfsqd + |qsdq + |qsdqqs,;snd;q,ds:;qsnd:bq,bd;,qbd;,qbdn;sqbdq + |dq:snd;qs:dnqs;d;:qsn,dbq;,dbq,;dq + |dqd:;sqnd,nqs:;snd;qsn;d,qnd,q,bfnqd;,alxeklfa:=sx;bgnfkaml=:fecklntuoijcmkxlgqchlekdjkr,lazjir xcgknfqxomcnq,ekjtln,fmnrljcn + |epf,mdqle;tk""".stripMargin, + ), + ) + + addRows(cms, data0, 0) + + val fileName = s"target/fileName-long-${new Date()}.xlsx" + + writeFile(cms, fileName) + + assertTrue( + new File(fileName).exists(), + cms.get().pages.nonEmpty, + !cms.get().pages.forall(page => Files.exists(page.path)), // clean the tmp CSV files automatically. + ) + }, + test("writeFile keeps the non ASCII characters") { + val cms = newCMSPlz + + val data0: Array[Row] = Array( + row("éàèç&ù$€£°"), + ) + + addRows(cms, data0, 0) + + val fileName = s"target/fileName-non-ascii-${new Date()}.xlsx" + + writeFile(cms, fileName) + + assertTrue( + new File(fileName).exists(), + cms.get().pages.nonEmpty, + !cms.get().pages.forall(page => Files.exists(page.path)), // clean the tmp CSV files automatically. + ) + }, + ) }