Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

first draft of general tests #276

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion shared/src/main/scala/squants/Dimension.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ trait Dimension[A <: Quantity[A]] {
*/
def symbolToUnit(symbol: String): Option[UnitOfMeasure[A]] = units.find(u ⇒ u.symbol == symbol)

def apply(value: Any): Try[A] = parse(value)

/**
* Tries to map a string or tuple value to Quantity of this Dimension
* @param value the source string (ie, "10 kW") or tuple (ie, (10, "kW"))
* @return Try[A]
*/
protected def parse(value: Any) = value match {
protected def parse(value: Any): Try[A] = value match {
case s: String => parseString(s)
case (v: Byte, u: String) => parseTuple(v.toDouble, u)
case (v: Short, u: String) => parseTuple(v.toDouble, u)
Expand Down
132 changes: 132 additions & 0 deletions shared/src/test/scala/squants/GenericSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package squants

import scala.util.Try

import org.scalacheck.Arbitrary.arbitrary
import org.scalacheck.Gen
import org.scalacheck.Gen.{alphaStr, nonEmptyListOf, posNum}
import org.scalatest.prop.{PropertyChecks, TableFor2}
import org.scalatest.{FlatSpec, Matchers, TryValues}

/**
* Generic tests for quantities
*/
abstract class GenericSpec[A <: Quantity[A]](dimension: Dimension[A])
extends FlatSpec
with Matchers
with TryValues
with PropertyChecks {

// tests must implement these
def unitConversionsTable: TableFor2[Double, Double]
def singleUnitValues: TableFor2[A, A]
def doubleImplicitConversionValues: TableFor2[A, A]
def implicitStringConversion: String => Try[A]

behavior of s"${dimension.name} and its Units of Measure"

it should "create values using UOM factories" in {
dimension.units.foreach { unit: UnitOfMeasure[A] =>
forAll { d: Double =>
unit(d).to(unit) should be (d)
}
}
}

it should "create values from properly formatted strings" in {
dimension.units.foreach { unit: UnitOfMeasure[A] =>
forAll { d: Double =>
dimension(s"$d ${unit.symbol}").success.value should be (unit(d))
}
}
}


it should "properly convert to all supported Units of Measure" in {
forAll(unitConversionsTable) { (actual: Double, expected: Double) =>
actual should be (expected)
}
}

it should "return properly formatted strings for all supported Units of Measure" in {
dimension.units.foreach { unit: UnitOfMeasure[A] =>
forAll { d: Double =>

// avoid inconsistent string double formatting
val Array(qty, unit2) = unit(d).toString.split(" ")
qty.toDouble should be (d)
unit2 should be (unit.symbol)
}
}
}

it should "provide aliases for single unit values" in {
forAll(singleUnitValues) { (unitValue, expected) =>
unitValue should be(expected)
}
}

behavior of s"${dimension.name}Conversions"

it should "provide implicit conversions from Double" in {
forAll(doubleImplicitConversionValues) { (actual, expected) =>
actual should be (expected)
}
}

it should "provide implicit conversion from String when the string is correctly formed" in {
dimension.units.foreach { unit =>
forAll { d: Double =>
implicitStringConversion(s"$d ${unit.symbol}").success.value should be(unit(d))
}
}
}

it should "fail to implicitly convert a String when the unit is invalid" in {
val symbols = dimension.units.map(_.symbol)
forAll(posNum[Double], alphaStr) { (d, notASymbol) =>
whenever(!symbols.contains(notASymbol)) {
val str = s"$d $notASymbol"
implicitStringConversion(str).failure.exception should be
QuantityParseException(s"Unable to parse ${dimension.name}", str)
}
}
}

it should "fail to implicitly convert a String when the value is invalid" in {
dimension.units.foreach { unit =>
forAll(alphaStr) { notANumber =>
val str = s"$notANumber $unit"
implicitStringConversion(str).failure.exception should be
QuantityParseException(s"Unable to parse ${dimension.name}", str)
}
}
}

def checkNumeric(implicit numeric: Numeric[A]): Unit = {
it should "provide numeric support" in {
val qtyGen: Gen[A] = for {
value <- arbitrary[Double]
unit <- Gen.oneOf(dimension.units.toSeq)
} yield unit(value)

// check for overflow
def safeValue(q: Quantity[A]): Boolean = {
val x = q.to(dimension.primaryUnit)
!x.isInfinite && !x.isNaN
}

forAll(nonEmptyListOf(qtyGen)) { qtys: List[A] =>
whenever(qtys.forall(safeValue)) {
val rawTotal: Double = qtys.map(_.to(dimension.primaryUnit)).sum
val expected: Quantity[A] = dimension.primaryUnit(rawTotal)
qtys.sum should be(expected)
}
}
}

it should "provide numeric support when a List is empty" in {
List.empty[A].sum should be(dimension.primaryUnit(0))
}
}
}
158 changes: 158 additions & 0 deletions shared/src/test/scala/squants/space/NewLengthSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package squants.space

import squants.electro.{OhmMeters, Ohms, Siemens, SiemensPerMeter}
import squants.energy.Joules
import squants.motion.{MetersPerSecond, Newtons}
import squants.time.Seconds
import squants.{GenericSpec, MetricSystem}


class NewLengthSpec extends GenericSpec(Length) {

private val x = Meters(1)
private val metersPerFoot = 0.3048006096

override val unitConversionsTable = Table(
("actual", "expected"),
(x.toMeters, 1.0),
(x.toAngstroms, 1 / (100*MetricSystem.Pico)),
(x.toNanometers, 1 / MetricSystem.Nano),
(x.toMicrons, 1 / MetricSystem.Micro),
(x.toMillimeters, 1 / MetricSystem.Milli),
(x.toCentimeters, 1 / MetricSystem.Centi),
(x.toDecimeters, 1 / MetricSystem.Deci),
(x.toDecameters, 1 / MetricSystem.Deca),
(x.toHectometers, 1 / MetricSystem.Hecto),
(x.toKilometers, 1 / MetricSystem.Kilo),

(x.toInches, 1 / (metersPerFoot / 12)),
(x.toFeet, 1 / metersPerFoot),
(x.toYards, 1 / (metersPerFoot * 3)),
(x.toUsMiles, 1 / (metersPerFoot * 5280)),
(x.toInternationalMiles, 1 / 1609.344),
(x.toNauticalMiles, 1 / 1852d),
(x.toAstronomicalUnits, 1 / 149597870700d),
(x.toLightYears, 1 / 9460730472580800d),
(x.toParsecs, 1 / 3.08567758149137e16),
(x.toSolarRadii, 1 / 6.957e8),
(x.toNominalSolarRadii, 1 / 6.957e8)
)


import LengthConversions._
override val singleUnitValues = Table(
("unitValue", "expected"),
(angstrom, Angstroms(1)),
(nanometer, Nanometers(1)),
(nanometre, Nanometers(1)),
(micron, Microns(1)),
(micrometer, Microns(1)),
(micrometre, Microns(1)),
(millimeter, Millimeters(1)),
(millimetre, Millimeters(1)),
(centimeter, Centimeters(1)),
(centimetre, Centimeters(1)),
(decimeter, Decimeters(1)),
(decimetre, Decimeters(1)),
(meter, Meters(1)),
(metre, Meters(1)),
(decameter, Decameters(1)),
(decametre, Decameters(1)),
(hectometer, Hectometers(1)),
(hectometre, Hectometers(1)),
(kilometer, Kilometers(1)),
(kilometre, Kilometers(1)),
(inch, Inches(1)),
(foot, Feet(1)),
(yard, Yards(1)),
(mile, UsMiles(1)),
(nauticalMile, NauticalMiles(1)),
(astronomicalUnit, AstronomicalUnits(1)),
(lightYear, LightYears(1)),
(parsec, Parsecs(1)),
(solarRadius, SolarRadii(1)),
(nominalSolarRadius, NominalSolarRadii(1))
)

private val d = 10d
override val doubleImplicitConversionValues = Table(
("actual", "expected"),
(d.Å, Angstroms(d)),
(d.angstroms, Angstroms(d)),
(d.nm, Nanometers(d)),
(d.nanometers, Nanometers(d)),
(d.nanometres, Nanometers(d)),
(d.µm, Microns(d)),
(d.microns, Microns(d)),
(d.mm, Millimeters(d)),
(d.millimeters, Millimeters(d)),
(d.millimetres, Millimeters(d)),
(d.cm, Centimeters(d)),
(d.centimeters, Centimeters(d)),
(d.centimetres, Centimeters(d)),
(d.dm, Decimeters(d)),
(d.meters, Meters(d)),
(d.metres, Meters(d)),
(d.dam, Decameters(d)),
(d.hm, Hectometers(d)),
(d.km, Kilometers(d)),
(d.kilometers, Kilometers(d)),
(d.kilometres, Kilometers(d)),
(d.inches, Inches(d)),
(d.ft, Feet(d)),
(d.feet, Feet(d)),
(d.yd, Yards(d)),
(d.yards, Yards(d)),
(d.miles, UsMiles(d)),
(d.nmi, NauticalMiles(d)),
(d.au, AstronomicalUnits(d)),
(d.ly, LightYears(d)),
(d.lightYears, LightYears(d)),
(d.pc, Parsecs(d)),
(d.parsecs, Parsecs(d)),
(d.solarRadii, SolarRadii(d)),
(d.nominalSolarRadii, NominalSolarRadii(d))
)

def implicitStringConversion = { _.toLength }

checkNumeric

behavior of "Length"

it should "return Area when multiplied by Length" in {
Meters(1) * Meters(1) should be(SquareMeters(1))
}

it should "return Volume when multiplied by Area" in {
Meters(1) * SquareMeters(1) should be(CubicMeters(1))
}

it should "return Energy when multiplied by Force" in {
Meters(1) * Newtons(1) should be(Joules(1))
}

it should "return ElectricalConductance when multiplied by Conductivity" in {
Meters(1) * SiemensPerMeter(1) should be(Siemens(1))
}

it should "return Resistivity when multiplied by ElectricalResistance" in {
Meters(1) * Ohms(1) should be(OhmMeters(1))
}

it should "return Velocity when divided by Time" in {
Meters(1) / Seconds(1) should be(MetersPerSecond(1))
}

it should "return Time when divided by Velocity" in {
Meters(1) / MetersPerSecond(1) should be(Seconds(1))
}

it should "return an Area when squared" in {
Meters(4).squared should be(SquareMeters(16))
}

it should "return a Volume when cubed" in {
Meters(3).cubed should be(CubicMeters(27))
}
}