Skip to content

Commit

Permalink
feat: add an implementation of the CliTool traits (#1)
Browse files Browse the repository at this point in the history
Co-authored-by: Fang Yin Lo <[email protected]>
  • Loading branch information
clintval and fangylo authored Dec 12, 2024
1 parent d7361f6 commit 7e97ba1
Show file tree
Hide file tree
Showing 17 changed files with 900 additions and 4 deletions.
Binary file added .github/img/cover.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Unit Tests

on: [ push, pull_request ]

jobs:
unit-tests:
runs-on: ubuntu-latest
strategy:
matrix:
suite: [ clitool ]
java-version: [ 11, 17, 21 ]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: ${{ matrix.java-version }}
- name: Cache for Scala Dependencies
uses: actions/cache@v4
with:
path: |
~/.mill/download
~/.m2/repository
~/.cache/coursier
key: ${{ runner.os }}-java-mill-${{ matrix.java-version }}-${{ hashFiles('**/build.sc') }}
restore-keys: ${{ runner.os }}-java-mill-
- name: Compile Scala Code
run: |
./mill --no-server clean
./mill --no-server --disable-ticker ${{ matrix.suite }}.compile
- name: Test Scala Code
run: |
./mill --no-server --disable-ticker ${{ matrix.suite }}.test
- name: Create Code Coverage Report
if: matrix.java-version == '11'
run: |
./mill --no-server --disable-ticker ${{ matrix.suite }}.scoverage.htmlReport
- name: Upload Code Coverage Report
uses: actions/upload-artifact@v4
if: matrix.java-version == '11'
with:
name: code-coverage
path: out/clitool/scoverage/htmlReport.dest/
12 changes: 10 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
.bsp/*
.DS_Store
.idea/*
.java-version
.metals/*
.vscode/*
*.class
*.log

# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
**/*.iml
bin/*
hs_err_pid*
out/*
target/*
1 change: 1 addition & 0 deletions .mill-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.12.3
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 Clint Valentine
Copyright © 2024 Clint Valentine

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,34 @@
# clitool
A small set of Scala traits to help execute command line tools

[![Unit Tests](https://github.com/clintval/clitool/actions/workflows/unit-tests.yml/badge.svg?branch=main)](https://github.com/clintval/clitool/actions/workflows/unit-tests.yml?query=branch%3Amain)
[![Java Version](https://img.shields.io/badge/java-11,17,21-c22d40.svg)](https://github.com/AdoptOpenJDK/homebrew-openjdk)
[![Language](https://img.shields.io/badge/language-scala-c22d40.svg)](https://www.scala-lang.org/)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/clintval/clitool/blob/master/LICENSE)

A small set of Scala traits to help execute command line tools.

![Cisco and the Grand Canyon](.github/img/cover.jpg)

```scala
val ScriptResource = "io/cvbio/io/CliToolTest.py"

Python3.execScript(
scriptResource = ScriptResource,
args = Seq.empty,
logger = Some(logger),
stdoutRedirect = logger.info,
stderrRedirect = logger.warning,
)
```

#### If Mill is your build tool

```scala
ivyDeps ++ Agg(ivy"io.cvbio.io::clitool::0.1.0")
```

#### If SBT is your build tool

```scala
libraryDependencies += "io.cvbio.io" %% "clitool" % "0.1.0"
```
110 changes: 110 additions & 0 deletions build.sc
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import $ivy.`com.lihaoyi::mill-contrib-scoverage:$MILL_VERSION`
import coursier.maven.MavenRepository
import mill._
import mill.api.JarManifest
import mill.contrib.scoverage.ScoverageModule
import mill.define.{Target, Task}
import mill.scalalib._
import mill.scalalib.publish._

import java.util.jar.Attributes.Name.{IMPLEMENTATION_VERSION => ImplementationVersion}

/** The official package version. */
private val packageVersion = "0.1.0"

/** A base trait for all test targets. */
trait ScalaTest extends TestModule {

/** The dependencies needed only for tests. */
override def ivyDeps = Agg(
ivy"ch.qos.logback:logback-classic:1.5.12",
ivy"org.scalatest::scalatest::3.2.19".excludeOrg(organizations = "org.junit"),
)

/** The test framework to use for this project. */
override def testFramework: Target[String] = T { "org.scalatest.tools.Framework" }
}

/** The clitool Scala package package. */
object clitool extends ScalaModule with PublishModule with ScoverageModule {
object test extends ScalaTests with ScalaTest with ScoverageTests

def scalaVersion = "2.13.14"
def scoverageVersion = "2.1.1"
def publishVersion = T { packageVersion }

/** POM publishing settings for this package. */
def pomSettings: Target[PomSettings] = PomSettings(
description = "A small set of Scala traits to help execute command line tools.",
organization = "io.cvbio.io",
url = "https://github.com/clintval/clitool",
licenses = Seq(License.MIT),
versionControl = VersionControl.github(owner = "clintval", repo = "clitool", tag = Some(packageVersion)),
developers = Seq(
Developer(id = "clintval", name = "Clint Valentine", url = "https://github.com/clintval"),
Developer(id = "fangylo", name = "Fang Yin Lo", url = "https://github.com/fangylo"),
)
)

/** The artifact name, fully resolved within the coordinate. */
override def artifactName: T[String] = T { "clitool" }

/** The JAR manifest. */
override def manifest: T[JarManifest] = super.manifest().add(ImplementationVersion.toString -> packageVersion)

/** The dependencies of the project. */
override def ivyDeps = Agg(
ivy"org.slf4j:slf4j-nop:1.7.6",
ivy"org.scala-lang.modules::scala-parallel-collections::1.0.0",
)

/** All the repositories we want to pull from. */
override def repositoriesTask: Task[Seq[coursier.Repository]] = T.task {
super.repositoriesTask() ++ Seq(MavenRepository("https://oss.sonatype.org/content/repositories/public"))
}

/** All Scala compiler options for this package. */
override def scalacOptions: T[Seq[String]] = T {
Seq(
"-opt:inline:io.cvbio.**", // Turn on the inliner.
"-opt-inline-from:io.cvbio.**", // Tells the inliner that it is allowed to inline things from these classes.
"-Yopt-log-inline", "_", // Optional, logs the inliner activity so you know it is doing something.
"-Yopt-inline-heuristics:at-inline-annotated", // Tells the inliner to use your `@inliner` tags.
"-opt-warnings:at-inline-failed", // Tells you if methods marked with `@inline` cannot be inlined, so you can remove the tag.
// The following are sourced from https://nathankleyn.com/2019/05/13/recommended-scalac-flags-for-2-13/
"-deprecation", // Emit warning and location for usages of deprecated APIs.
"-explaintypes", // Explain type errors in more detail.
"-feature", // Emit warning and location for usages of features that should be imported explicitly.
"-unchecked", // Enable additional warnings where generated code depends on assumptions.
"-Xcheckinit", // Wrap field accessors to throw an exception on uninitialized access.
"-Xfatal-warnings", // Fail the compilation if there are any warnings.
"-Xlint:adapted-args", // Warn if an argument list is modified to match the receiver.
"-Xlint:constant", // Evaluation of a constant arithmetic expression results in an error.
"-Xlint:delayedinit-select", // Selecting member of DelayedInit.
"-Xlint:doc-detached", // A Scaladoc comment appears to be detached from its element.
"-Xlint:inaccessible", // Warn about inaccessible types in method signatures.
"-Xlint:infer-any", // Warn when a type argument is inferred to be `Any`.
"-Xlint:missing-interpolator", // A string literal appears to be missing an interpolator id.
"-Xlint:nullary-unit", // Warn when nullary methods return Unit.
"-Xlint:option-implicit", // Option.apply used implicit view.
"-Xlint:package-object-classes", // Class or object defined in package object.
"-Xlint:poly-implicit-overload", // Parameterized overloaded implicit methods are not visible as view bounds.
"-Xlint:private-shadow", // A private field (or class parameter) shadows a superclass field.
"-Xlint:stars-align", // Pattern sequence wildcard must align with sequence component.
"-Xlint:type-parameter-shadow", // A local type parameter shadows a type already in scope.
"-Ywarn-dead-code", // Warn when dead code is identified.
"-Ywarn-extra-implicit", // Warn when more than one implicit parameter section is defined.
"-Ywarn-numeric-widen", // Warn when numerics are widened.
"-Ywarn-unused:implicits", // Warn if an implicit parameter is unused.
"-Ywarn-unused:imports", // Warn if an import selector is not referenced.
"-Ywarn-unused:locals", // Warn if a local definition is unused.
"-Ywarn-unused:params", // Warn if a value parameter is unused.
"-Ywarn-value-discard", // Warn when non-Unit expression results are unused.
"-Ywarn-unused:patvars", // Warn if a variable bound in a pattern is unused.
"-Ywarn-unused:privates", // Warn if a private member is unused.
"-Ybackend-parallelism", Math.min(Runtime.getRuntime.availableProcessors(), 8).toString, // Enable parallelization — scalac max is 16.
"-Ycache-plugin-class-loader:last-modified", // Enables caching of classloaders for compiler plugins
"-Ycache-macro-class-loader:last-modified", // and macro definitions. This can lead to performance improvements.
)
}
}
30 changes: 30 additions & 0 deletions clitool/src/io/cvbio/io/AsyncStreamSink.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.cvbio.io

import java.io.{Closeable, InputStream}
import java.util.concurrent.atomic.AtomicInteger

import scala.io.Source

/** Companion object for [[AsyncStreamSink]]. */
private[io] object AsyncStreamSink {
private val n: AtomicInteger = new AtomicInteger(1)
private def nextName: String = s"AsyncStreamSinkThread-${n.getAndIncrement}"
}

/** Non-blocking class that will read output from a stream and pass it to a sink. */
private[io] class AsyncStreamSink(in: InputStream, private val sink: String => Unit) extends Closeable {
private val source = Source.fromInputStream(in).withClose(() => in.close())
private val thread = new Thread(() => source.getLines().foreach(sink))
this.thread.setName(AsyncStreamSink.nextName)
this.thread.setDaemon(true)
this.thread.start()

/** Give the thread 500 seconds to wrap up what it's doing and then interrupt it. */
def close() : Unit = close(500)

/** Give the thread `millis` milliseconds to finish what it's doing, then interrupt it. */
def close(millis: Long) : Unit = {
this.thread.join(millis)
this.thread.interrupt()
}
}
Loading

0 comments on commit 7e97ba1

Please sign in to comment.