diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1dc41ba10..28d8b9f42 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,6 +16,7 @@ jobs: fail-fast: false matrix: java: [11, 17, 21] + scala: [2.13.18, 3.3.7] runs-on: ubuntu-latest steps: - name: Checkout @@ -27,4 +28,4 @@ jobs: java-version: ${{ matrix.java }} - uses: sbt/setup-sbt@v1 - name: Build and Test - run: sbt test + run: sbt ++${{ matrix.scala }} test diff --git a/build.sbt b/build.sbt index f96b113ab..033c41d99 100644 --- a/build.sbt +++ b/build.sbt @@ -63,7 +63,10 @@ lazy val common = coreProject("common") .settings( description := "Common Libraries and Utilities", - libraryDependencies ++= Seq(slf4j_api, logback, slf4j_log4j12, scala_xml, scala_parser, scalamock) + libraryDependencies ++= Seq(slf4j_api, logback, slf4j_log4j12, scala_xml, scala_parser, scalamock, mockito_scalatest(scalaVersion.value)), + Test / unmanagedSourceDirectories += { + (Test / sourceDirectory).value / ("scala-" + scalaBinaryVersion.value) + } ) lazy val actor = @@ -71,7 +74,10 @@ lazy val actor = .dependsOn(common) .settings( description := "Simple Actor", - Test / parallelExecution := false + Test / parallelExecution := false, + Test / unmanagedSourceDirectories += { + (Test / sourceDirectory).value / ("scala-" + scalaBinaryVersion.value) + } ) lazy val markdown = @@ -102,7 +108,10 @@ lazy val util = htmlparser, xerces, json4s_native, - ) + ), + Test / unmanagedSourceDirectories += { + (Test / sourceDirectory).value / ("scala-" + scalaBinaryVersion.value) + } ) // Web Projects @@ -115,7 +124,10 @@ lazy val testkit = .dependsOn(util) .settings( description := "Testkit for Webkit Library", - libraryDependencies ++= Seq(commons_httpclient, servlet_api, json4s_native, json4s_xml) + libraryDependencies ++= Seq(commons_httpclient, servlet_api, json4s_native, json4s_xml), + Test / unmanagedSourceDirectories += { + (Test / sourceDirectory).value / ("scala-" + scalaBinaryVersion.value) + } ) lazy val webkit = diff --git a/core/actor/src/test/scala/net/liftweb/actor/ActorSpec.scala b/core/actor/src/test/scala-2.13/net/liftweb/actor/ActorSpec.scala similarity index 100% rename from core/actor/src/test/scala/net/liftweb/actor/ActorSpec.scala rename to core/actor/src/test/scala-2.13/net/liftweb/actor/ActorSpec.scala diff --git a/core/actor/src/test/scala/net/liftweb/actor/LAFutureSpec.scala b/core/actor/src/test/scala-2.13/net/liftweb/actor/LAFutureSpec.scala similarity index 100% rename from core/actor/src/test/scala/net/liftweb/actor/LAFutureSpec.scala rename to core/actor/src/test/scala-2.13/net/liftweb/actor/LAFutureSpec.scala diff --git a/core/actor/src/test/scala/net/liftweb/actor/MockLiftActorSpec.scala b/core/actor/src/test/scala-2.13/net/liftweb/actor/MockLiftActorSpec.scala similarity index 100% rename from core/actor/src/test/scala/net/liftweb/actor/MockLiftActorSpec.scala rename to core/actor/src/test/scala-2.13/net/liftweb/actor/MockLiftActorSpec.scala diff --git a/core/actor/src/test/scala-3/net/liftweb/actor/ActorSpec.scala b/core/actor/src/test/scala-3/net/liftweb/actor/ActorSpec.scala new file mode 100644 index 000000000..f23a41f01 --- /dev/null +++ b/core/actor/src/test/scala-3/net/liftweb/actor/ActorSpec.scala @@ -0,0 +1,84 @@ +/* + * Copyright 2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package actor + +import org.specs2.mutable.Specification +import scala.concurrent.duration._ +import common._ + + +/** + * Systems under specification for Lift Actor. + */ +class ActorSpec extends Specification { + "Actor Specification".title + sequential + + "A Scala Actor" should { + "support common features" in commonFeatures(new MyScalaActor) + } + + private def commonFeatures(actor: LiftActor) = { + sequential + + "allow setting and getting of a value" in { + val a = actor + a ! Set(33) + a !? Get() + Thread.sleep(100) + (a.!?(50, Get())) must beEqualTo(Full(Answer(33))) + } + + "allow setting and getting of a value with subclass of Get()" in { + val a = actor + a ! Set(33) + a ! new FunnyGet() + Thread.sleep(100) + (a.!?(50L, new FunnyGet())) must beEqualTo(Full(Answer(33))) + } + + "allow adding of a value" in { + val a = actor + a ! Set(33) + (a !< Add(44)).get(500) must beEqualTo(Full(Answer(77))) + } + + "allow subtracting of a value" in { + val a = actor + a ! Set(33) + (a !< Sub(11)).get(500) must beEqualTo(Full(Answer(22))) + } + + "properly timeout" in { + val a = actor + Thread.sleep(100) + (a !< Set(33)).get(50) must beEqualTo(Empty) + } + } + +} + + +case class Add(num: Int) +case class Sub(num: Int) +case class Set(num: Int) + +case class Get() +class FunnyGet() extends Get() + +case class Answer(num: Int) diff --git a/core/actor/src/test/scala-3/net/liftweb/actor/LAFutureSpec.scala b/core/actor/src/test/scala-3/net/liftweb/actor/LAFutureSpec.scala new file mode 100644 index 000000000..4945fc99f --- /dev/null +++ b/core/actor/src/test/scala-3/net/liftweb/actor/LAFutureSpec.scala @@ -0,0 +1,145 @@ +package net.liftweb.actor + +import net.liftweb.common.{Box, Failure, Full} +import org.specs2.mutable.Specification +import java.util.concurrent.atomic.AtomicBoolean + +class LAFutureSpec extends Specification { + sequential + val timeout = 20000L + LAScheduler + + "LAFuture" should { + val futureSpecScheduler = new LAScheduler { + override def execute(f: ()=>Unit): Unit = f() + } + + "map to failing future if transforming function throws an Exception" in { + val future = LAFuture(() => 1, futureSpecScheduler) + def tranformThrowingException(input: Int) = { + throw new Exception("fail") + } + + val transformedFuture = future.map(tranformThrowingException) + + var notifiedAboutFailure: Boolean = false + transformedFuture.onFail { _ => + notifiedAboutFailure = true + } + + transformedFuture.get(timeout) + notifiedAboutFailure must beTrue + } + + "flatMap to failing future if transforming function throws an Exception" in { + val future = LAFuture(() => 1, futureSpecScheduler) + def tranformThrowingException(input: Int): LAFuture[Int] = { + throw new Exception("fail") + } + + val transformedFuture = future.flatMap(tranformThrowingException) + + var notifiedAboutFailure: Boolean = false + transformedFuture.onFail { _ => + notifiedAboutFailure = true + } + + transformedFuture.get(timeout) + notifiedAboutFailure must beTrue + } + + "return original Failure after timeout" in { + val future = new LAFuture() + val givenFailure = Failure("fooFailure") + LAScheduler.execute { () => + Thread.sleep(500) + future.fail(givenFailure) + } + + val result = future.get(timeout) + + result must beEqualTo(givenFailure) + } + + "when collecting results with LAFuture.collect" in { + "collect one future result" in { + val givenOneResult = 123 + val one = LAFuture(() => givenOneResult) + LAFuture.collect(one).get(timeout) must beEqualTo(List(givenOneResult)) + } + + "collect more future results in correct order" in { + val givenOneResult = 123 + val givenTwoResult = 234 + val one = LAFuture(() => givenOneResult) + val two = LAFuture(() => givenTwoResult) + LAFuture.collect(one, two).get(timeout) must beEqualTo(List(givenOneResult, givenTwoResult)) + } + + "collect empty list immediately" in { + val collectResult = LAFuture.collect(Nil: _*) + collectResult.isSatisfied must beTrue + collectResult.get(timeout) must beEqualTo(Nil) + } + + "report a failed LAFuture as a failure for the overall future" in { + val one: LAFuture[Int] = new LAFuture + val two: LAFuture[Int] = LAFuture(() => 5) + + one.fail(Failure("boom boom boom!")) + + val collectResult = LAFuture.collect(one, two) + collectResult.get(timeout) must beEqualTo(Failure("boom boom boom!")) + } + } + + "when collecting Boxed results with collectAll" in { + "collectAll collects an EmptyBox immediately" in { + val one: LAFuture[Box[Int]] = LAFuture(() => { Failure("whoops"): Box[Int] }) + val two: LAFuture[Box[Int]] = LAFuture(() => { Thread.sleep(10000); Full(1) }) + + val collectResult = LAFuture.collectAll(one, two) + collectResult.get(5000) must beEqualTo(Failure("whoops")) + } + + "collectAll collects a set of Fulls" in { + val one: LAFuture[Box[Int]] = LAFuture(() => Full(1): Box[Int]) + val two: LAFuture[Box[Int]] = LAFuture(() => Full(2): Box[Int]) + val three: LAFuture[Box[Int]] = LAFuture(() => Full(3): Box[Int]) + + val collectResult = LAFuture.collectAll(one, two, three) + collectResult.get(timeout) should beLike { + case Full(Full(results)) => + results should contain(allOf(1, 2, 3)) + } + } + + "collectAll reports a failed LAFuture as a failure for the overall future" in { + val one: LAFuture[Box[Int]] = new LAFuture + val two: LAFuture[Box[Int]] = LAFuture(() => Full(5): Box[Int]) + + one.fail(Failure("boom boom boom!")) + + val collectResult = LAFuture.collectAll(one, two) + collectResult.get(timeout) must beEqualTo(Failure("boom boom boom!")) + } + + "collectAll empty list immediately" in { + val collectResult = LAFuture.collectAll(Nil : _*) + collectResult.isSatisfied must beTrue + collectResult.get(timeout) must beEqualTo(Nil) + } + + "report a failed LAFuture as a failure for the overall future" in { + val one: LAFuture[Box[Int]] = new LAFuture + val two: LAFuture[Box[Int]] = LAFuture(() => Full(5): Box[Int]) + + one.fail(Failure("boom boom boom!")) + + val collectResult = LAFuture.collectAll(one, two) + collectResult.get(timeout) must beEqualTo(Failure("boom boom boom!")) + } + } + } + +} diff --git a/core/actor/src/test/scala-3/net/liftweb/actor/MockLiftActorSpec.scala b/core/actor/src/test/scala-3/net/liftweb/actor/MockLiftActorSpec.scala new file mode 100644 index 000000000..575f9b386 --- /dev/null +++ b/core/actor/src/test/scala-3/net/liftweb/actor/MockLiftActorSpec.scala @@ -0,0 +1,117 @@ +/* + * Copyright 2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package actor + +import org.specs2.mutable.Specification + +class MockLiftActorSpec extends Specification { + "Mock Actor Specification".title + + sealed trait MockSpecActorMessage + case object MockSpecActorMessage1 extends MockSpecActorMessage + case object MockSpecActorMessage2 extends MockSpecActorMessage + case object MockSpecActorMessage3 extends MockSpecActorMessage + + "A MockSpecializedLiftActor" should { + "correctly indicate when it has received a message" in { + val mockActor = new MockSpecializedLiftActor[MockSpecActorMessage] + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + + mockActor.hasReceivedMessage_?(MockSpecActorMessage1) must beTrue + } + + "correctly indicate when it has not received a message" in { + val mockActor = new MockSpecializedLiftActor[MockSpecActorMessage] + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + + mockActor.hasReceivedMessage_?(MockSpecActorMessage3) must beFalse + } + + "correctly indicate the number of messages it has received" in { + val mockActor = new MockSpecializedLiftActor[MockSpecActorMessage] + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + mockActor ! MockSpecActorMessage3 + + mockActor.messageCount === 3 + } + + "correctly list the messages it has received" in { + val mockActor = new MockSpecializedLiftActor[MockSpecActorMessage] + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + mockActor ! MockSpecActorMessage3 + + mockActor.messages === List( + MockSpecActorMessage3, + MockSpecActorMessage2, + MockSpecActorMessage1 + ) + } + } + + "A MockLiftActor" should { + "correctly indicate when it has received a message" in { + val mockActor = new MockLiftActor + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + + mockActor.hasReceivedMessage_?(MockSpecActorMessage1) must beTrue + } + + "correctly indicate when it has not received a message" in { + val mockActor = new MockLiftActor + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + + mockActor.hasReceivedMessage_?(MockSpecActorMessage3) must beFalse + } + + "correctly indicate the number of messages it has received" in { + val mockActor = new MockLiftActor + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + mockActor ! MockSpecActorMessage3 + + mockActor.messageCount === 3 + } + + "correctly list the messages it has received" in { + val mockActor = new MockLiftActor + + mockActor ! MockSpecActorMessage1 + mockActor ! MockSpecActorMessage2 + mockActor ! MockSpecActorMessage3 + + mockActor.messages === List( + MockSpecActorMessage3, + MockSpecActorMessage2, + MockSpecActorMessage1 + ) + } + } +} diff --git a/core/common/src/test/scala/net/liftweb/common/BoxLoggingSpec.scala b/core/common/src/test/scala-2.13/net/liftweb/common/BoxLoggingSpec.scala similarity index 100% rename from core/common/src/test/scala/net/liftweb/common/BoxLoggingSpec.scala rename to core/common/src/test/scala-2.13/net/liftweb/common/BoxLoggingSpec.scala diff --git a/core/common/src/test/scala/net/liftweb/common/BoxSpec.scala b/core/common/src/test/scala-2.13/net/liftweb/common/BoxSpec.scala similarity index 100% rename from core/common/src/test/scala/net/liftweb/common/BoxSpec.scala rename to core/common/src/test/scala-2.13/net/liftweb/common/BoxSpec.scala diff --git a/core/common/src/test/scala/net/liftweb/common/ConversionsSpec.scala b/core/common/src/test/scala-2.13/net/liftweb/common/ConversionsSpec.scala similarity index 100% rename from core/common/src/test/scala/net/liftweb/common/ConversionsSpec.scala rename to core/common/src/test/scala-2.13/net/liftweb/common/ConversionsSpec.scala diff --git a/core/common/src/test/scala/net/liftweb/common/HListSpec.scala b/core/common/src/test/scala-2.13/net/liftweb/common/HListSpec.scala similarity index 100% rename from core/common/src/test/scala/net/liftweb/common/HListSpec.scala rename to core/common/src/test/scala-2.13/net/liftweb/common/HListSpec.scala diff --git a/core/common/src/test/scala/net/liftweb/common/LoggingSpec.scala b/core/common/src/test/scala-2.13/net/liftweb/common/LoggingSpec.scala similarity index 100% rename from core/common/src/test/scala/net/liftweb/common/LoggingSpec.scala rename to core/common/src/test/scala-2.13/net/liftweb/common/LoggingSpec.scala diff --git a/core/common/src/test/scala/net/liftweb/common/LruMapSpec.scala b/core/common/src/test/scala-2.13/net/liftweb/common/LruMapSpec.scala similarity index 100% rename from core/common/src/test/scala/net/liftweb/common/LruMapSpec.scala rename to core/common/src/test/scala-2.13/net/liftweb/common/LruMapSpec.scala diff --git a/core/common/src/test/scala-3/net/liftweb/common/BoxLoggingSpec.scala b/core/common/src/test/scala-3/net/liftweb/common/BoxLoggingSpec.scala new file mode 100644 index 000000000..ba5161e2c --- /dev/null +++ b/core/common/src/test/scala-3/net/liftweb/common/BoxLoggingSpec.scala @@ -0,0 +1,457 @@ +package net.liftweb +package common + +import org.slf4j.{Logger=>SLF4JLogger} + +// MockContext not available in Scala 3 +import org.specs2.mutable.Specification +import org.mockito.Mockito.{mock, when, verify} +import org.mockito.ArgumentMatchers.{any, anyString} + +class BoxLoggingSpec extends Specification { + class MockBoxLoggingClass extends BoxLogging { + var loggedErrors = List[(String, Option[Throwable])]() + var loggedWarns = List[(String, Option[Throwable])]() + var loggedInfos = List[(String, Option[Throwable])]() + var loggedDebugs = List[(String, Option[Throwable])]() + var loggedTraces = List[(String, Option[Throwable])]() + + protected def logBoxError(message: String, throwable: Option[Throwable]): Unit = { + loggedErrors ::= (message, throwable) + } + protected def logBoxWarn(message: String, throwable: Option[Throwable]): Unit = { + loggedWarns ::= (message, throwable) + } + protected def logBoxInfo(message: String, throwable: Option[Throwable]): Unit = { + loggedInfos ::= (message, throwable) + } + protected def logBoxDebug(message: String, throwable: Option[Throwable]): Unit = { + loggedDebugs ::= (message, throwable) + } + protected def logBoxTrace(message: String, throwable: Option[Throwable]): Unit = { + loggedTraces ::= (message, throwable) + } + } + + "BoxLogging" should { + "when logging empty boxes" in { + def verifyContentList(list: List[(String, Option[Throwable])]) = { + list must beLike { + case (paramFailure4, None) :: + (paramFailure3, None) :: + (paramFailure2, None) :: + (paramFailure1, Some(paramExp)) :: + (chained1, None) :: + (chained2, None) :: + (level1, None) :: + (level2, Some(exp2)) :: + (level3, None) :: + (level4, Some(exp4)) :: + (emptyMessage, None) :: + (fullParamMessage, Some(paramException)) :: + (paramMessage, None) :: + (exceptedMessage, Some(failureException)) :: + (failureMessage, None) :: + Nil => + (failureMessage must startWith("Second")) and + (failureMessage must contain("Failed")) and + (exceptedMessage must startWith("Third")) and + (exceptedMessage must contain("Excepted")) and + (failureException.getMessage === "uh-oh") and + (paramMessage must startWith("Fourth")) and + (paramMessage must contain("ParamFailed")) and + (paramMessage must contain("BoxLoggingSpec")) and + (fullParamMessage must startWith("Fifth")) and + (fullParamMessage must contain("ParamExcepted")) and + (fullParamMessage must contain("BoxLoggingSpec")) and + (paramException.getMessage === "param uh-oh") and + (emptyMessage must startWith("Sixth")) and + (emptyMessage must contain("Empty")) and + (level1 must contain("Failure level 3 caused by: Failure level 4")) and + (level2 must contain("Failure level 2 caused by: Failure level 3")) and + (exp2 must beAnInstanceOf[IllegalArgumentException]) and + (level3 must contain("Failure level 1 caused by: Failure level 2")) and + (level4 must contain("Multilevel failure: Failure level 1")) and + (exp4 must beAnInstanceOf[NullPointerException]) and + (chained1 must contain("Chained failure caused by: Boom")) and + (chained2 must contain("Chain all failures: Chained failure")) and + (paramFailure4 must contain("Param Failure lvl 3 with param Param 3 caused by: Param Failure lvl 4 with param Param 4")) and + (paramFailure3 must contain("Failure lvl 2 caused by: Param Failure lvl 3 with param Param 3")) and + (paramFailure2 must contain("Param Failure lvl 1 with param Param 1 caused by: Failure lvl 2")) and + (paramFailure1 must contain("Param failure: Param Failure lvl 1 with param Param 1")) and + (paramExp must beAnInstanceOf[IllegalArgumentException]) + } + } + + "log correctly on ERROR level" in { + val results = + new MockBoxLoggingClass { + Full("Not empty").logEmptyBox("First") + Failure("Failed").logEmptyBox("Second") + Failure("Excepted", Full(new Exception("uh-oh")), Empty).logEmptyBox("Third") + ParamFailure("ParamFailed", this).logEmptyBox("Fourth") + ParamFailure( + "ParamExcepted", + Full(new Exception("param uh-oh")), + Empty, + this + ).logEmptyBox("Fifth") + (Empty).logEmptyBox("Sixth") + Failure( + "Failure level 1", Full(new NullPointerException), Full(Failure( + "Failure level 2", Empty, Full(Failure( + "Failure level 3", Full(new IllegalArgumentException), Full(Failure( + "Failure level 4" + ))) + )) + ) + ).logEmptyBox("Multilevel failure") + (Failure("Boom") ?~! "Chained failure").logEmptyBox("Chain all failures") + ParamFailure( + "Param Failure lvl 1", Full(new IllegalArgumentException), Full(Failure( + "Failure lvl 2", Empty, Full(ParamFailure( + "Param Failure lvl 3", Empty, Full(ParamFailure( + "Param Failure lvl 4", + "Param 4" + )), + "Param 3" + )) + )), + "Param 1" + ).logEmptyBox("Param failure") + } + + verifyContentList(results.loggedErrors) + } + + "log correctly on WARN level" in { + val results = + new MockBoxLoggingClass { + Full("Not empty").warnLogEmptyBox("First") + Failure("Failed").warnLogEmptyBox("Second") + Failure("Excepted", Full(new Exception("uh-oh")), Empty).warnLogEmptyBox("Third") + ParamFailure("ParamFailed", this).warnLogEmptyBox("Fourth") + ParamFailure( + "ParamExcepted", + Full(new Exception("param uh-oh")), + Empty, + this + ).warnLogEmptyBox("Fifth") + (Empty).warnLogEmptyBox("Sixth") + Failure( + "Failure level 1", Full(new NullPointerException), Full(Failure( + "Failure level 2", Empty, Full(Failure( + "Failure level 3", Full(new IllegalArgumentException), Full(Failure( + "Failure level 4" + ))) + )) + ) + ).warnLogEmptyBox("Multilevel failure") + (Failure("Boom") ?~! "Chained failure").warnLogEmptyBox("Chain all failures") + ParamFailure( + "Param Failure lvl 1", Full(new IllegalArgumentException), Full(Failure( + "Failure lvl 2", Empty, Full(ParamFailure( + "Param Failure lvl 3", Empty, Full(ParamFailure( + "Param Failure lvl 4", + "Param 4" + )), + "Param 3" + )) + )), + "Param 1" + ).warnLogEmptyBox("Param failure") + } + + verifyContentList(results.loggedWarns) + } + + "log correctly on INFO level" in { + val results = + new MockBoxLoggingClass { + Full("Not empty").infoLogEmptyBox("First") + Failure("Failed").infoLogEmptyBox("Second") + Failure("Excepted", Full(new Exception("uh-oh")), Empty).infoLogEmptyBox("Third") + ParamFailure("ParamFailed", this).infoLogEmptyBox("Fourth") + ParamFailure( + "ParamExcepted", + Full(new Exception("param uh-oh")), + Empty, + this + ).infoLogEmptyBox("Fifth") + (Empty).infoLogEmptyBox("Sixth") + Failure( + "Failure level 1", Full(new NullPointerException), Full(Failure( + "Failure level 2", Empty, Full(Failure( + "Failure level 3", Full(new IllegalArgumentException), Full(Failure( + "Failure level 4" + ))) + )) + ) + ).infoLogEmptyBox("Multilevel failure") + (Failure("Boom") ?~! "Chained failure").infoLogEmptyBox("Chain all failures") + ParamFailure( + "Param Failure lvl 1", Full(new IllegalArgumentException), Full(Failure( + "Failure lvl 2", Empty, Full(ParamFailure( + "Param Failure lvl 3", Empty, Full(ParamFailure( + "Param Failure lvl 4", + "Param 4" + )), + "Param 3" + )) + )), + "Param 1" + ).infoLogEmptyBox("Param failure") + } + + verifyContentList(results.loggedInfos) + } + + "log correctly on DEBUG level" in { + val results = + new MockBoxLoggingClass { + Full("Not empty").debugLogEmptyBox("First") + Failure("Failed").debugLogEmptyBox("Second") + Failure("Excepted", Full(new Exception("uh-oh")), Empty).debugLogEmptyBox("Third") + ParamFailure("ParamFailed", this).debugLogEmptyBox("Fourth") + ParamFailure( + "ParamExcepted", + Full(new Exception("param uh-oh")), + Empty, + this + ).debugLogEmptyBox("Fifth") + (Empty).debugLogEmptyBox("Sixth") + Failure( + "Failure level 1", Full(new NullPointerException), Full(Failure( + "Failure level 2", Empty, Full(Failure( + "Failure level 3", Full(new IllegalArgumentException), Full(Failure( + "Failure level 4" + ))) + )) + ) + ).debugLogFailure("Multilevel failure") + (Failure("Boom") ?~! "Chained failure").debugLogFailure("Chain all failures") + ParamFailure( + "Param Failure lvl 1", Full(new IllegalArgumentException), Full(Failure( + "Failure lvl 2", Empty, Full(ParamFailure( + "Param Failure lvl 3", Empty, Full(ParamFailure( + "Param Failure lvl 4", + "Param 4" + )), + "Param 3" + )) + )), + "Param 1" + ).debugLogFailure("Param failure") + } + + verifyContentList(results.loggedDebugs) + } + + "log correctly on TRACE level" in { + val results = + new MockBoxLoggingClass { + Full("Not empty").traceLogEmptyBox("First") + Failure("Failed").traceLogEmptyBox("Second") + Failure("Excepted", Full(new Exception("uh-oh")), Empty).traceLogEmptyBox("Third") + ParamFailure("ParamFailed", this).traceLogEmptyBox("Fourth") + ParamFailure( + "ParamExcepted", + Full(new Exception("param uh-oh")), + Empty, + this + ).traceLogEmptyBox("Fifth") + (Empty).traceLogEmptyBox("Sixth") + Failure( + "Failure level 1", Full(new NullPointerException), Full(Failure( + "Failure level 2", Empty, Full(Failure( + "Failure level 3", Full(new IllegalArgumentException), Full(Failure( + "Failure level 4" + ))) + )) + ) + ).traceLogEmptyBox("Multilevel failure") + (Failure("Boom") ?~! "Chained failure").traceLogEmptyBox("Chain all failures") + ParamFailure( + "Param Failure lvl 1", Full(new IllegalArgumentException), Full(Failure( + "Failure lvl 2", Empty, Full(ParamFailure( + "Param Failure lvl 3", Empty, Full(ParamFailure( + "Param Failure lvl 4", + "Param 4" + )), + "Param 3" + )) + )), + "Param 1" + ).traceLogEmptyBox("Param failure") + } + + verifyContentList(results.loggedTraces) + } + } + + "when logging only failures" in { + def verifyContentList(list: List[(String, Option[Throwable])]) = { + list must beLike { + case (fullParamMessage, Some(paramException)) :: + (paramMessage, None) :: + (exceptedMessage, Some(failureException)) :: + (failureMessage, None) :: + Nil => + (failureMessage must startWith("Second")) and + (failureMessage must contain("Failed")) and + (exceptedMessage must startWith("Third")) and + (exceptedMessage must contain("Excepted")) and + (failureException.getMessage === "uh-oh") and + (paramMessage must startWith("Fourth")) and + (paramMessage must contain("ParamFailed")) and + (paramMessage must contain("BoxLoggingSpec")) and + (fullParamMessage must startWith("Fifth")) and + (fullParamMessage must contain("ParamExcepted")) and + (fullParamMessage must contain("BoxLoggingSpec")) and + (paramException.getMessage === "param uh-oh") + } + } + + "log correctly on ERROR level" in { + val results = + new MockBoxLoggingClass { + Full("Not empty").logFailure("First") + Failure("Failed").logFailure("Second") + Failure("Excepted", Full(new Exception("uh-oh")), Empty).logFailure("Third") + ParamFailure("ParamFailed", this).logFailure("Fourth") + ParamFailure( + "ParamExcepted", + Full(new Exception("param uh-oh")), + Empty, + this + ).logFailure("Fifth") + (Empty).logFailure("Sixth") + } + + verifyContentList(results.loggedErrors) + } + + "log correctly on WARN level" in { + val results = + new MockBoxLoggingClass { + Full("Not empty").warnLogFailure("First") + Failure("Failed").warnLogFailure("Second") + Failure("Excepted", Full(new Exception("uh-oh")), Empty).warnLogFailure("Third") + ParamFailure("ParamFailed", this).warnLogFailure("Fourth") + ParamFailure( + "ParamExcepted", + Full(new Exception("param uh-oh")), + Empty, + this + ).warnLogFailure("Fifth") + (Empty).warnLogFailure("Sixth") + } + + verifyContentList(results.loggedWarns) + } + + "log correctly on INFO level" in { + val results = + new MockBoxLoggingClass { + Full("Not empty").infoLogFailure("First") + Failure("Failed").infoLogFailure("Second") + Failure("Excepted", Full(new Exception("uh-oh")), Empty).infoLogFailure("Third") + ParamFailure("ParamFailed", this).infoLogFailure("Fourth") + ParamFailure( + "ParamExcepted", + Full(new Exception("param uh-oh")), + Empty, + this + ).infoLogFailure("Fifth") + (Empty).infoLogFailure("Sixth") + } + + verifyContentList(results.loggedInfos) + } + + "log correctly on DEBUG level" in { + val results = + new MockBoxLoggingClass { + Full("Not empty").debugLogFailure("First") + Failure("Failed").debugLogFailure("Second") + Failure("Excepted", Full(new Exception("uh-oh")), Empty).debugLogFailure("Third") + ParamFailure("ParamFailed", this).debugLogFailure("Fourth") + ParamFailure( + "ParamExcepted", + Full(new Exception("param uh-oh")), + Empty, + this + ).debugLogFailure("Fifth") + (Empty).debugLogFailure("Sixth") + } + + verifyContentList(results.loggedDebugs) + } + + "log correctly on TRACE level" in { + val results = + new MockBoxLoggingClass { + Full("Not empty").traceLogFailure("First") + Failure("Failed").traceLogFailure("Second") + Failure("Excepted", Full(new Exception("uh-oh")), Empty).traceLogFailure("Third") + ParamFailure("ParamFailed", this).traceLogFailure("Fourth") + ParamFailure( + "ParamExcepted", + Full(new Exception("param uh-oh")), + Empty, + this + ).traceLogFailure("Fifth") + (Empty).traceLogFailure("Sixth") + } + + verifyContentList(results.loggedTraces) + } + } + + "when logging in a Loggable" in { + import net.liftweb.common.Logger + import org.slf4j.{Logger => SLF4JLogger} + + "log to the Lift logger" in { + val mockLogger = mock(classOf[SLF4JLogger]) + when(mockLogger.isErrorEnabled()).thenReturn(true) + + class MyLoggable extends LoggableBoxLogging { + override val logger = new Logger { + override protected def _logger = mockLogger + } + } + + val result = + new MyLoggable { + Failure("Failed").logFailure("Second") + Failure("Excepted", Full(new Exception("uh-oh")), Empty).logFailure("Third") + } + + verify(mockLogger).error(anyString()) + verify(mockLogger).error(anyString(), any(classOf[Throwable])) + } + } + + "when logging with in SLF4J context" in { + import org.slf4j.{Logger => SLF4JLogger} + + "log to the SLF4J logger" in { + val mockLogger = mock(classOf[SLF4JLogger]) + when(mockLogger.isErrorEnabled()).thenReturn(true) + + class TestClass extends SLF4JBoxLogging { + val logger = mockLogger + } + + new TestClass { + Failure("Failed").logFailure("Second") + Failure("Excepted", Full(new Exception("uh-oh")), Empty).logFailure("Third") + } + + verify(mockLogger).error(anyString()) + verify(mockLogger).error(anyString(), any(classOf[Throwable])) + } + } + } +} diff --git a/core/common/src/test/scala-3/net/liftweb/common/BoxSpec.scala b/core/common/src/test/scala-3/net/liftweb/common/BoxSpec.scala new file mode 100644 index 000000000..408588530 --- /dev/null +++ b/core/common/src/test/scala-3/net/liftweb/common/BoxSpec.scala @@ -0,0 +1,583 @@ +/* + * Copyright 2007-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package common + +import org.specs2.mutable.Specification +import org.specs2.ScalaCheck +import org.specs2.collection.canEqualAny +import org.scalacheck.{Arbitrary, Gen} +import Gen._ + +import Box._ + + +/* commented out because it tests the compilation phase and we want the compiler to "do the right thing" +class TypeBoundsTest extends Specification with ScalaCheck { + "Type Bounds Spec".title + + "Type bounds" can { + "do type testing" in { + def foo[T: ExcludeThisType.exclude[Nothing]#other](a: T) = a.toString + foo(33.0) + foo(throw new Exception("foo")) + + true == true + } + } +} + +*/ + +/** + * System under specification for Box. + */ +class BoxSpec extends Specification with ScalaCheck with BoxGenerator { + + "A Box" can { + "be created from a Option. It is Empty if the option is None" in { + Box(None) must beEqualTo(Empty) + } + "be created from a Option. It is Full(x) if the option is Some(x)" in { + Box(Some(1)) must beEqualTo(Full(1)) + } + "be created from a List containing one element. It is Empty if the list is empty" in { + Box(Nil) must beEqualTo(Empty) + } + "be created from a List containing one element. It is Full(x) if the list is List(x)" in { + Box(List(1)) must beEqualTo(Full(1)) + } + "be created from a List containing more than one element. It is Full(x) if the list is x::rest" in { + Box(List(1, 2, 3)) must beEqualTo(Full(1)) + } + "be used as an iterable" in { + (Full(1) reduceLeft {(x: Int, y: Int) => x + y}) === 1 + } + "be used as an Option" in { + Full(1) orElse Some(2) must beSome(1) + Empty orElse Some(2) must beSome(2) + } + "be implicitly defined from an Option. The openOrThrowException method can be used on an Option for example" in { + Some(1).openOrThrowException("This is a test") === 1 + } + "be defined from some legacy code (possibly passing null values). If the passed value is not null, a Full(value) is returned" in { + Box.legacyNullTest("s") must beEqualTo(Full("s")) + } + "be defined from some legacy code (possibly passing null values). If the passed value is null, an Empty is returned" in { + Box.legacyNullTest(null) must beEqualTo(Empty) + } + } + + "A Box" should { + "provide a 'choice' method to either apply a function to the Box value or return another default can" in { + def gotIt = (x: Int) => Full("got it: " + x.toString) + + Full(1).choice(gotIt)(Full("nothing")) must beEqualTo(Full("got it: 1")) + Empty.choice(gotIt)(Full("nothing")) must beEqualTo(Full("nothing")) + } + } + + "A Full Box" should { + "not beEmpty" in { + Full(1).isEmpty must beFalse + } + "be defined" in { + Full(1).isDefined must beTrue + } + "return its value when opened" in { + Full(1).openOrThrowException("This is a test") === 1 + } + "return its value when opened with openOr(default value)" in { + (Full(1) openOr 0) === 1 + } + "return itself when or'ed with another Box" in { + (Full(1) or Full(2)) must beEqualTo(Full(1)) + } + "define an 'exists' method returning true if the Box value satisfies the function" in { + Full(1) exists {_ > 0} must beTrue + } + "define an exists method returning false if the Box value doesn't satisfy the function" in { + Full(0) exists {_ > 0} must beFalse + } + "define a forall method returning true if the Box value satisfies the function" in { + Full(1) forall {_ > 0} must beTrue + } + "define a forall method returning false if the Box value doesn't satisfy the function" in { + Full(0) forall {_ > 0} must beFalse + } + "define a 'filter' method, returning a Full Box if the filter is satisfied" in { + (Full(1) filter {(x: Int) => x > 0}) must beEqualTo(Full(1)) + } + "define a 'filter' method, returning Empty if the filter is not satisfied" in { + (Full(1) filter {(x: Int) => x == 0}) must beEqualTo(Empty) + } + "define a 'filterMsg' method, returning a Failure if the filter predicate is not satisfied" in { + Full(1).filterMsg("not equal to 0")(_ == 0) must beEqualTo(Failure("not equal to 0", Empty, Empty)) + } + "define a 'foreach' method using its value (to display it for instance)" in { + var total = 0 + Full(1) foreach { total += _ } + total === 1 + } + "define a 'map' method to transform its value" in { + (Full(1) map { (x: Int) => x.toString }) must beEqualTo(Full("1")) + } + "define a 'flatMap' method transforming its value in another Box. If the value is transformed in a Full box, the total result is a Full box" in { + (Full(1) flatMap { (x: Int) => if (x > 0) Full("full") else Empty }) must beEqualTo(Full("full")) + } + "define a 'flatMap' method transforming its value in another Box. If the value is transformed in an Empty box, the total result is an Empty box" in { + (Full(0) flatMap { (x: Int) => if (x > 0) Full("full") else Empty }) must beEqualTo(Empty) + } + "define a 'flatten' method if it contains another Box." in { + "If the inner box is a Full box, the final result is identical to that box" in { + Full(Full(1)).flatten must beEqualTo(Full(1)) + } + "If the inner box is a Failure, the final result is identical to that box" in { + Full(Failure("error", Empty, Empty)).flatten must beEqualTo(Failure("error", Empty, Empty)) + } + "If the inner box is an Empty box, the final result is identical to that box" in { + Full(Empty).flatten must beEqualTo(Empty) + } + } + "define a 'collect' method that takes a PartialFunction to transform its contents" in { + "If the partial-function is defined for the contents of this box, returns a full box containing the result of applying that partial function to this Box's contents" in { + (Full("Albus") collect { case "Albus" => "Dumbledore"}) must beEqualTo(Full("Dumbledore")) + } + "If the partial-function is not defined for the contents of this box, returns Empty" in { + (Full("Hermione") collect { case "Albus" => "Dumbledore"}) must beEqualTo(Empty) + } + } + "define a 'transform' method that takes a PartialFunction to transform this box into another box" in { + "If the partial-function is defined for this box, returns the result of applying the partial function to it" in { + (Full(404) transform { + case Full(x: Int) if x != 200 => Failure("Server error") + }) must beEqualTo(Failure("Server error")) + } + "If the partial-function is not defined for this box, returns itself unchanged" in { + (Full("Intended Result") transform { + case _: EmptyBox => Full("Alternative") + case Full("Unexpected Result") => Full("Alternative") + }) must beEqualTo(Full("Intended Result")) + } + } + "define a 'flip' method returning Empty" in { + (Full(1) flip { _ => "No data found" }) must beEqualTo(Empty) + } + "define an 'elements' method returning an iterator containing its value" in { + Full(1).elements.next() === 1 + } + "define a 'toList' method returning a List containing its value" in { + Full(1).toList === List(1) + } + "define a 'toOption' method returning a Some object containing its value" in { + Full(1).toOption must beSome(1) + } + "return itself if asked for its status with the operator ?~" in { + Full(1) ?~ "error" must beEqualTo(Full(1)) + } + "return itself if asked for its status with the operator ?~!" in { + Full(1) ?~! "error" must beEqualTo(Full(1)) + } + "define a 'pass' method passing the can to a function and returning itself (alias: $)" in { + var empty = false + def emptyString(s: Box[String]) = s foreach {(c: String) => empty = c.isEmpty} + Full("") $ emptyString _ + empty must beTrue + } + "define a 'run' method either returning a default value or applying a user-defined function on it" in { + def appendToString(s: String, x: Int) = s + x.toString + Full(1).run("string")(appendToString) === "string1" + } + "define a 'isA' method returning a Full(value) if the value is the instance of a given class" in { + Full("s").isA(classOf[String]) must beEqualTo(Full("s")) + } + "define a 'isA' method returning Empty if the value is not the instance of a given class" in { + Full("s").isA(classOf[Double]) must beEqualTo(Empty) + } + "define a 'asA' method returning a Full(value) if the value is the instance of a given type" in { + Full("s").asA[String] must beEqualTo(Full("s")) + } + "define a 'asA' method returning Empty if the value is not the instance of a given type" in { + Full("s").asA[Double] must beEqualTo(Empty) + } + + "define a 'asA' method must work with Boolean" in { + Full(true).asA[Boolean] must beEqualTo(Full(true)) + Full(3).asA[Boolean] must beEqualTo(Empty) + } + + "define a 'asA' method must work with Character" in { + Full('a').asA[Char] must beEqualTo(Full('a')) + Full('a').asA[Boolean] must beEqualTo(Empty) + } + + "define a 'asA' method must work with Byte" in { + Full(3.toByte).asA[Byte] must beEqualTo(Full(3.toByte)) + Full(3.toByte).asA[Boolean] must beEqualTo(Empty) + } + + "define a 'asA' method must work with Double" in { + Full(44d).asA[Double] must beEqualTo(Full(44D)) + Full(44d).asA[Boolean] must beEqualTo(Empty) + } + + "define a 'asA' method must work with Float" in { + Full(32f).asA[Float] must beEqualTo(Full(32f)) + Full(33f).asA[Boolean] must beEqualTo(Empty) + } + + "define a 'asA' method must work with Integer" in { + Full(3).asA[Int] must beEqualTo(Full(3)) + Full(3).asA[Boolean] must beEqualTo(Empty) + } + + "define a 'asA' method must work with Long" in { + Full(32L).asA[Long] must beEqualTo(Full(32L)) + Full(32L).asA[Boolean] must beEqualTo(Empty) + } + + "define a 'asA' method must work with Short" in { + Full(8.toShort).asA[Short] must beEqualTo(Full(8.toShort)) + Full(8.toShort).asA[Boolean] must beEqualTo(Empty) + } + + "not invoke a call-by-name parameter to openOrThrowException" in { + var sideEffect = false + def sideEffecting = { + sideEffect = true + "This shouldn't have been invoked." + } + + try { + Full("hi mom").openOrThrowException(sideEffecting) + } catch { + case e: Exception => + } + + sideEffect === false + } + + } + + "An Empty Box" should { + "beEmpty" in { + Empty.isEmpty must beTrue + } + "not be defined" in { + Empty.isDefined must beFalse + } + "throw an exception if opened" in { + {Empty.openOrThrowException("See what happens?, at least we expect it in this case :)"); ()} must throwA[NullPointerException] + } + "return a default value if opened with openOr" in { + Empty.openOr(1) === 1 + } + "return the other Box if or'ed with another Box" in { + Empty.or(Full(1)) must beEqualTo(Full(1)) + } + "return itself if filtered with a predicate" in { + val empty: Box[Int] = Empty + empty.filter {_ > 0} must beEqualTo(Empty) + } + "define an 'exists' method returning false" in { + val empty: Box[Int] = Empty + empty exists {_ > 0} must beFalse + } + "define a 'forall' method returning true" in { + val empty: Box[Int] = Empty + empty forall {_ > 0} must beTrue + } + "define a 'filter' method, returning Empty" in { + val empty: Box[Int] = Empty + (empty filter {(x: Int) => x > 0}) must beEqualTo(Empty) + } + "define a 'filterMsg' method, returning a Failure" in { + Empty.filterMsg("not equal to 0")(_ == 0) must beEqualTo(Failure("not equal to 0", Empty, Empty)) + } + "define a 'foreach' doing nothing" in { + var total = 0 + val empty: Box[Int] = Empty + empty foreach {total += _} + total === 0 + } + "define a 'map' method returning Empty" in { + (Empty map {(x: Any) => x.toString}) must beEqualTo(Empty) + } + "define a 'flatMap' method returning Empty" in { + (Empty flatMap {(x: Int) => Full("full")}) must beEqualTo(Empty) + } + "define a 'flatten' method returning Empty" in { + Empty.flatten must beEqualTo(Empty) + } + "define a 'collect' method returning Empty" in { + (Empty collect { case _ => "Some Value" }) must beEqualTo(Empty) + } + "define a 'transform' method that takes a PartialFunction to transform this Empty box into another box" in { + "If the partial-function is defined for Empty, returns the result of applying the partial function to it" in { + (Empty transform { + case Failure("error", Empty, Empty) => Full("failure-alternative") + case Empty => Full("alternative") + }) must beEqualTo(Full("alternative")) + } + "If the partial-function is not defined for Empty, returns Empty" in { + (Empty transform { case Failure("The Phantom Menace", Empty, Empty) => Full("Return Of The Jedi") }) must beEqualTo(Empty) + } + } + "define a 'flip' method returning a Full box" in { + (Empty flip { + case Empty => "flipped-empty" + case _ => "flipped-failure" + }) must beEqualTo(Full("flipped-empty")) + } + "define an 'elements' method returning an empty iterator" in { + Empty.elements.hasNext must beFalse + } + "define a 'toList' method returning Nil" in { + Empty.toList must beEmpty + } + "define a 'toOption' method returning None" in { + Empty.toOption must beNone + } + "return a failure with a message if asked for its status with the operator ?~" in { + Empty ?~ "nothing" must beEqualTo(Failure("nothing", Empty, Empty)) + } + "return a failure with a message if asked for its status with the operator ?~!" in { + Empty ?~! "nothing" must beEqualTo(Failure("nothing", Empty, Empty)) + } + "define a 'isA' method returning Empty" in { + Empty.isA(classOf[Double]) must beEqualTo(Empty) + } + "define a 'asA' method returning Empty" in { + Empty.asA[Double] must beEqualTo(Empty) + } + + "invoke a call-by-name parameter to openOrThrowException" in { + var sideEffect = false + def sideEffecting = { + sideEffect = true + "This should have been invoked." + } + + try { + Empty.openOrThrowException(sideEffecting) + } catch { + case e: Exception => + } + + sideEffect === true + } + } + + "A Failure is an Empty Box which" can { + "return its cause as an exception" in { + case class LiftException(m: String) extends Exception + Failure("error", Full(new LiftException("broken")), Empty).exception must beEqualTo(Full(new LiftException("broken"))) + } + "return a chained list of causes" in { + Failure("error", + Full(new Exception("broken")), + Full(Failure("nested cause", Empty, Empty))).chain must beEqualTo(Full(Failure("nested cause", Empty, Empty))) + } + "be converted to a ParamFailure" in { + Failure("hi mom") ~> 404 must beEqualTo(ParamFailure("hi mom", Empty, Empty, 404)) + } + } + + "A Failure is an Empty Box which" should { + "return itself if mapped, flatMapped or flattened" in { + (Failure("error", Empty, Empty) map {(x: Any) => x.toString}) must beEqualTo(Failure("error", Empty, Empty)) + (Failure("error", Empty, Empty) flatMap {(x: String) => Full(x.toString)}) must beEqualTo(Failure("error", Empty, Empty)) + Failure("error", Empty, Empty).flatten must beEqualTo(Failure("error", Empty, Empty)) + } + "define a 'collect' method returning itself" in { + (Failure("error", Empty, Empty) collect { case _ => "Some Value" }) must beEqualTo(Failure("error", Empty, Empty)) + } + "define a 'transform' method that takes a PartialFunction to transform this Failure into another box" in { + "If the partial-function is defined for this Failure, returns the result of applying the partial function to it" in { + (Failure("The Phantom Menace") transform { + case Failure("The Phantom Menace", Empty, Empty) => Full("Return Of The Jedi") + }) must beEqualTo(Full("Return Of The Jedi")) + + (Failure("The Phantom Menace") transform { + case Failure("The Phantom Menace", Empty, Empty) => Failure("Clones") + case _ => Full("Jedi") + }) must beEqualTo(Failure("Clones")) + } + "If the partial-function is not defined for this Failure, returns itself unchanged" in { + (Failure("Clones") transform { case Failure("The Phantom Menace", Empty, Empty) => Full("Jedi") }) must beEqualTo(Failure("Clones")) + } + } + "define a 'flip' method returning a Full box" in { + (Failure("error", Empty, Empty) flip { + case Empty => "flipped-empty" + case _: Failure => "flipped-failure" + }) must beEqualTo(Full("flipped-failure")) + } + "return itself when asked for its status with the operator ?~" in { + Failure("error", Empty, Empty) ?~ "nothing" must beEqualTo(Failure("error", Empty, Empty)) + } + "create a new failure with a chained message if asked for its status with the operator ?~!" in { + Failure("error", Empty, Empty) ?~! "error2" must beEqualTo(Failure("error2", Empty, Full(Failure("error", Empty, Empty)))) + } + "return false for exist method" in { + Failure("error", Empty, Empty) exists {_ => true } must beFalse + } + "return true for forall method" in { + Failure("error", Empty, Empty) forall {_ => false } must beTrue + } + } + + "A ParamFailure is a failure which" should { + "appear in the chain when ~> is invoked on it" in { + Failure("Apple") ~> 404 ~> "apple" must beEqualTo( + ParamFailure("Apple", Empty, Full( + ParamFailure("Apple", Empty, Empty, 404) + ), "apple")) + } + } + + "A Box equals method" should { + + "return true with comparing two identical Box messages" in prop { + import org.specs2.execute.Result + (c1: Box[Int], c2: Box[Int]) => { + ((c1, c2) match { + case (Empty, Empty) => c1 must beEqualTo(c2) + case (Full(x), Full(y)) => (c1 == c2) === (x == y) + case (Failure(m1, e1, l1), Failure(m2, e2, l2)) => (c1 == c2) === ((m1, e1, l1) == (m2, e2, l2)) + case _ => (c1 != c2) === true + }): Result + } + } + + "return false with comparing one Full and another object" in { + (Full(1) != "hello") must beTrue + } + + "return false with comparing one Empty and another object" in { + (Empty != "hello") must beTrue + } + + "return false with comparing one Failure and another object" in { + (Failure("", Empty, Empty) != "hello") must beTrue + } + } + + "A List[Box[T]]" should { + "be convertable to a Box[List[T]] when all are Full" in { + val someBoxes: List[Box[String]] = List(Full("bacon"), Full("sammich")) + val singleBox = someBoxes.toSingleBox("Box failed!") + + singleBox must beEqualTo(Full(List("bacon", "sammich"))) + } + + "be convertable to a Box[List[T]] when some are Full and some are Empty" in { + val someBoxes: List[Box[String]] = List(Full("bacon"), Full("sammich"), Empty) + val singleBox = someBoxes.toSingleBox("Box failed!") + + singleBox must beEqualTo(Full(List("bacon", "sammich"))) + } + + "be convertable to a ParamFailure[Box[List[T]]] when any are Failure" in { + val someBoxes: List[Box[String]] = List(Full("bacon"), Full("sammich"), Failure("I HATE BACON")) + val singleBox = someBoxes.toSingleBox("This should be in the param failure.") + + singleBox must beLike { + case ParamFailure(message, _, _, _) => + message === "This should be in the param failure." + } + } + + "chain the ParamFailure to the failures in the list when any are Failure" in { + val someBoxes: List[Box[String]] = List(Full("bacon"), Failure("I HATE BACON"), Full("sammich"), Failure("MORE BACON FAIL"), Failure("BACON WHY U BACON")) + + val singleBox = someBoxes.toSingleBox("Failure.") + + val expectedChain = + Failure("I HATE BACON", Empty, + Full(Failure("MORE BACON FAIL", Empty, + Full(Failure("BACON WHY U BACON"))))) + + singleBox must beLike { + case ParamFailure(_, _, chain, _) => + chain must beEqualTo(Full(expectedChain)) + } + } + } + + "A Box tryo method" should { + "return Full" in { + Box.tryo(1) must beEqualTo(Full(1)) + } + + "return Failure(_, Full(NPE), _) in case of NPE" in { + val obj: Object = null + + Box.tryo(obj.toString) must beLike { + case Failure(_, Full(ex), _) => ex.getClass === classOf[NullPointerException] + } + } + + "return Empty in case of NPE and ignore list with NPE" in { + val ignore: List[Class[_]] = List(classOf[NullPointerException]) + + Box.tryo(ignore)(throw new NullPointerException) must beEqualTo(Empty) + } + + "return Failure(_, Full(NPE), _) in case of non empty ignore list without NPE" in { + val ignore: List[Class[_]] = List(classOf[IllegalArgumentException]) + + Box.tryo(ignore)(throw new NullPointerException) must beLike { + case Failure(_, Full(ex), _) => ex.getClass === classOf[NullPointerException] + } + } + + "not throw NPE in case of nullable ignore list" in { + val ignore: List[Class[_]] = null + + Box.tryo(ignore)(throw new IllegalArgumentException) must beLike { + case Failure(_, Full(ex), _) => ex.getClass === classOf[IllegalArgumentException] + } + } + } +} + + +trait BoxGenerator { + + implicit def genThrowable: Arbitrary[Throwable] = Arbitrary[Throwable] { + case object UserException extends Throwable + const(UserException) + } + + implicit def genBox[T](implicit a: Arbitrary[T]): Arbitrary[Box[T]] = Arbitrary[Box[T]] { + frequency( + (3, const(Empty)), + (3, a.arbitrary.map(Full[T])), + (1, genFailureBox) + ) + } + + def genFailureBox: Gen[Failure] = for { + msgLen <- choose(0, 4) + msg <- listOfN(msgLen, alphaChar) + exception <- const(Full(new Exception(""))) + chainLen <- choose(1, 5) + chain <- frequency((1, listOfN(chainLen, genFailureBox)), (3, const(Nil))) + } yield Failure(msg.mkString, exception, Box(chain.headOption)) + +} diff --git a/core/common/src/test/scala-3/net/liftweb/common/ConversionsSpec.scala b/core/common/src/test/scala-3/net/liftweb/common/ConversionsSpec.scala new file mode 100644 index 000000000..3e4b697b4 --- /dev/null +++ b/core/common/src/test/scala-3/net/liftweb/common/ConversionsSpec.scala @@ -0,0 +1,112 @@ +/* + * Copyright 2010-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package common + +import scala.xml.{NodeSeq, Text} +import org.specs2.matcher.XmlMatchers +import org.specs2.mutable.Specification + +import scala.annotation.nowarn + + +/** + * System under specification for Conversions. + */ +@nowarn("msg=.* NodeSeqFunc in package common.* is deprecate.*") +class ConversionsSpec extends Specification with XmlMatchers { + + "A StringOrNodeSeq" should { + + "convert from a String" in { + val sns: StringOrNodeSeq = "Hello" + sns.nodeSeq === Text("Hello") + } + + "convert from an Elem" in { + val sns: StringOrNodeSeq = + sns.nodeSeq must ==/ () + } + + "convert from a Seq[Node]" in { + val sns: StringOrNodeSeq = List(, ) + sns.nodeSeq must ==/ (List(, ) : NodeSeq) + } + } + + "A StringFunc" should { + + "be created by a String constant" in { + val sf: StringFunc = "Foo" + + sf.func() === "Foo" + } + + "be created by a String Function" in { + val sf: StringFunc = () => "Bar" + + sf.func() === "Bar" + } + + "be created by a constant that can be converted to a String" in { + implicit def intToString(in: Int): String = in.toString + val sf: StringFunc = 55 + + sf.func() === "55" + } + + "be created by a function that can be converted to a String" in { + implicit def intToString(in: Int): String = in.toString + val sf: StringFunc = () => 55 + + sf.func() === "55" + } + + } + + "A NodeSeqFunc" should { + + "be created by a NodeSeq constant" in { + val sf: NodeSeqFunc = Foo + + sf.func() must ==/ (Foo) + } + + "be created by a NodeSeq Function" in { + val sf: NodeSeqFunc = () => Bar + + sf.func() must ==/ (Bar) + } + + "be created by a constant that can be converted to a NodeSeq" in { + implicit def intToNS(in: Int): NodeSeq = {in} + val sf: NodeSeqFunc = 55 + + sf.func() must ==/ (55) + } + + "be created by a function that can be converted to a NodeSeq" in { + implicit def intToNodeSeq(in: Int): NodeSeq = {in} + val sf: NodeSeqFunc = () => 55 + + sf.func() must ==/ (55) + } + + } + +} + diff --git a/core/common/src/test/scala-3/net/liftweb/common/HListSpec.scala b/core/common/src/test/scala-3/net/liftweb/common/HListSpec.scala new file mode 100644 index 000000000..72af89f31 --- /dev/null +++ b/core/common/src/test/scala-3/net/liftweb/common/HListSpec.scala @@ -0,0 +1,104 @@ +/* + * Copyright 2010-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package common + +import org.specs2.mutable.Specification + + +/** + * System under specification for Heterogeneous List. + */ +class HListSpec extends Specification { + + "An HList" should { + + "get the types right" in { + import HLists._ + + val x = 1 :+: "Foo" :+: HNil + + val head: Int = x.head + val head2: String = x.tail.head + + x.head === 1 + x.tail.head === "Foo" + } + + "properly report its length" in { + import HLists._ + + val x = 1 :+: "Foo" :+: HNil + + HNil.length === 0 + x.length === 2 + ("Bam" :+: x).length === 3 + } + } + + "A combinable box" should { + + "have a box built with a failure result in a failure" in { + import CombinableBox._ + + val x = Full("a") :&: Full(1) :&: Empty + + // result in a failure + x match { + case Left(_) => success + case _ => failure + } + } + + "be able to build a box with all the Full elements matching" in { + import CombinableBox._ + import HLists._ + + val x = Full("a") :&: Full(1) :&: Full(List(1,2,3)) + + // result in a failure + x match { + case Right(a :+: one :+: lst :+:HNil) => { + // val a2: Int = a fails... not type safe + + val as: String = a + val onei: Int = one + val lstl: List[Int] = lst + + success + } + + case Right(_) => failure + case Left(_) => failure + } + } + + "be usable in for comprehension" in { + import CombinableBox._ + import HLists._ + + val res = for { + a :+: one :+: lst :+: _ <- + (Full("a") ?~ "Yak" :&: Full(1) :&: Full(List(1,2,3))) ?~! "Dude" + } yield a.length * one * lst.foldLeft(1)(_ * _) + + res must beEqualTo(Full(6)) + } + } + +} + diff --git a/core/common/src/test/scala-3/net/liftweb/common/LoggingSpec.scala b/core/common/src/test/scala-3/net/liftweb/common/LoggingSpec.scala new file mode 100644 index 000000000..ca4e7bd0c --- /dev/null +++ b/core/common/src/test/scala-3/net/liftweb/common/LoggingSpec.scala @@ -0,0 +1,135 @@ +/* + * Copyright 2010-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package common + +import org.specs2.mutable.Specification + + +/** + * System under specification for Logging. + * + * Tests rely on logback being in the classpath, so no configuration should be necessary. + */ +class LoggingSpec extends Specification { + "Logging" can { + "be mixed directly into object" in { + object MyObj extends Logger { + info("direct Hello") + val x = 2 + } + MyObj.x === 2 + + (new MyTopClass).x === 1 + MyTopObj.x ===1 + } + + "be nested in object" in { + object MyObj extends Loggable { + logger.info("nested Hello") + val x = 2 + } + + MyObj.x === 2 + + } + + "create named loggers" in { + val logger = Logger("MyLogger") + + logger.info("Logged with my named logger") + success + } + + "log static MDC values" in { + val logger = Logger("StaticMDC") + + logger.info("Logged with no MDC") + MDC.put("mdc1" -> (1,2)) + logger.info("Logged with mdc1=(1,2)") + MDC.put("mdc2" -> "yy") + logger.info("Logged with mdc1=(1,2), mdc2=yy") + MDC.put("mdc1" -> 99) + logger.info("Logged with mdc1=99, mdc2=yy") + MDC.remove("mdc1") + logger.info("Logged with mdc2=yy") + MDC.clear() + logger.info("Logged with no MDC") + success + } + + "save MDC context with logWith" in { + val logger = Logger("logWith") + + logger.info("Logged with no MDC") + MDC.put("mdc1" -> (1,2), "mdc2" -> "yy") + logger.info("Logged with mdc1=(1,2), mdc2=yy") + Logger.logWith("mdc2" -> "xx") { + logger.info("Logged with mdc1=(1,2), mdc2=xx") + Logger.logWith("mdc1" -> 99) { + logger.info("Logged with mdc1=99, mdc2=xx") + } + logger.info("Logged with mdc1=(1,2), mdc2=xx") + } + logger.info("Logged with mdc1=(1,2), mdc2=yy") + MDC.clear() + logger.info("No MDC values") + success + } + "trace function results" in { + object MyObj extends Logger { + val l = 1 to 10 + info("Starting test") + trace("result",l.foldLeft(0)(trace("lhs",_) + trace("rhs",_))) === l.foldLeft(0)(_+_) + val x = 1 + } + MyObj + success + } + + "be used in different levels and yield different loggers" in { + class First { + First.info("In first") + } + object First extends Logger + + trait Second { + private val logger = Logger(classOf[Second]) + logger.info("In second") + } + + class C extends First with Second with Logger { + info("In C") + val x = 2 + } + (new C).x === 2 + } + } +} + + +class MyTopClass extends Logger { + val x=1 + debug("Top level class logging") +} + + +object MyTopObj extends Logger { + val x=1 + debug("Top level object logging") +} + diff --git a/core/common/src/test/scala-3/net/liftweb/common/LruMapSpec.scala b/core/common/src/test/scala-3/net/liftweb/common/LruMapSpec.scala new file mode 100644 index 000000000..c0878d005 --- /dev/null +++ b/core/common/src/test/scala-3/net/liftweb/common/LruMapSpec.scala @@ -0,0 +1,71 @@ +/* + * Copyright 2010-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package common + +import org.specs2.mutable.Specification + + +/** + * Systems under specification for LRU Map. + */ +class LruMapSpec extends Specification { + + "An LRU Map" should { + + "never grow beyond the given size" in { + val lru = new LRUMap[Int, Int](10) + for (i <- 1 to 20) lru(i) = i + + lru.size === 10 + } + + "have the last N elements (where N is the initial MaxSize)" in { + val lru = new LRUMap[Int, Int](10) + for (i <- 1 to 20) lru(i) = i + + lru.size === 10 + (11 to 20).forall(i => lru(i) == i) must beTrue + } + + "expire elements to func" in { + var expCnt = 0 + val lru = new LRUMap[Int, Int](10, Empty, (k, v) => {expCnt += 1; (k == v) && (k > 0) && (v < 11)}) + for (i <- 1 to 20) lru(i) = i + + lru.size === 10 + expCnt === 10 + (11 to 20).forall(i => lru(i) == i) must beTrue + } + + "not expire the recently accessed elements" in { + var expCnt = 0 + val lru = new LRUMap[Int, Int](10, Empty, (k, v) => {expCnt += 1; (k == v) && (k > 0)}) + for (i <- 1 to 20) { + for (q <- 1 to 10) lru.get(q) + lru(i) = i + } + + lru.size === 10 + for (i <- 2 to 10) lru(i) === i + lru(20) === 20 + } + + } + +} + diff --git a/core/util/src/test/scala/net/liftweb/util/BasicTypesHelpersSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/BasicTypesHelpersSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/BasicTypesHelpersSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/BasicTypesHelpersSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/BundleBuilderSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/BundleBuilderSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/BundleBuilderSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/BundleBuilderSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/CanResolveAsyncSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/CanResolveAsyncSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/CanResolveAsyncSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/CanResolveAsyncSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/ClassHelpersSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/ClassHelpersSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/ClassHelpersSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/ClassHelpersSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/CombParserHelpersSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/CombParserHelpersSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/CombParserHelpersSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/CombParserHelpersSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/ConnectionIdentifierSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/ConnectionIdentifierSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/ConnectionIdentifierSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/ConnectionIdentifierSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/ControlHelpersSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/ControlHelpersSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/ControlHelpersSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/ControlHelpersSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/CssHelpersSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/CssHelpersSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/CssHelpersSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/CssHelpersSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/CssSelectorSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/CssSelectorSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/CssSelectorSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/CssSelectorSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/CurrencyZoneSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/CurrencyZoneSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/CurrencyZoneSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/CurrencyZoneSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/Html5ParserSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/Html5ParserSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/Html5ParserSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/Html5ParserSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/HtmlHelpersSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/HtmlHelpersSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/HtmlHelpersSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/HtmlHelpersSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/HttpHelpersSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/HttpHelpersSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/HttpHelpersSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/HttpHelpersSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/IoHelpersSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/IoHelpersSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/IoHelpersSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/IoHelpersSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/JsonCommandSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/JsonCommandSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/JsonCommandSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/JsonCommandSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/ListHelpersSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/ListHelpersSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/ListHelpersSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/ListHelpersSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/PCDataXmlParserSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/PCDataXmlParserSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/PCDataXmlParserSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/PCDataXmlParserSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/PropsSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/PropsSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/PropsSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/PropsSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/ScheduleSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/ScheduleSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/ScheduleSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/ScheduleSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/SecurityHelpersSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/SecurityHelpersSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/SecurityHelpersSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/SecurityHelpersSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/SoftReferenceCacheSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/SoftReferenceCacheSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/SoftReferenceCacheSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/SoftReferenceCacheSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/StringHelpersSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/StringHelpersSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/StringHelpersSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/StringHelpersSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/TimeHelpersSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/TimeHelpersSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/TimeHelpersSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/TimeHelpersSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/ToHeadSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/ToHeadSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/ToHeadSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/ToHeadSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/VCardParserSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/VCardParserSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/VCardParserSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/VCardParserSpec.scala diff --git a/core/util/src/test/scala/net/liftweb/util/XmlParserSpec.scala b/core/util/src/test/scala-2.13/net/liftweb/util/XmlParserSpec.scala similarity index 100% rename from core/util/src/test/scala/net/liftweb/util/XmlParserSpec.scala rename to core/util/src/test/scala-2.13/net/liftweb/util/XmlParserSpec.scala diff --git a/core/util/src/test/scala-3/net/liftweb/util/BasicTypesHelpersSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/BasicTypesHelpersSpec.scala new file mode 100644 index 000000000..ba201064a --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/BasicTypesHelpersSpec.scala @@ -0,0 +1,193 @@ +/* + * Copyright 2006-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import java.io.ByteArrayInputStream + +import org.specs2.mutable.Specification +import org.specs2.matcher.DataTables + +import common._ +import BasicTypesHelpers._ + + +/** + * Systems under specification for BasicTypesHelpers. + */ +class BasicTypesHelpersSpec extends Specification with DataTables { + "BasicTypesHelpers Specification".title + + "Basic types helpers" should { + + "be lazy" in { + (false.?[Int]({throw new Exception("Bummer")}).|(3)) === 3 + (true.?[Int](3).|({throw new Exception("Bummer")})) === 3 + } + + "provide a ternary operator: (condition) ? A | B" in { + ((1 == 2) ? "a" | "b") === "b" + } + + "provide a ?> operator to add an element to a list if an expression is true" in { + (1 == 1) ?> "a" ::: List("b") === List("a", "b") + (1 == 2) ?> "a" ::: List("b") === List("b") + } + val failure = Failure(null, null, null) + "have a toBoolean method converting any object to a reasonable Boolean value" in { + toBoolean(null) === false + "object value" ||"boolean value" | + (0: Any) !!false | + 1 !!true | + true !!true | + false !!false | + "" !!false | + "string" !!false | + "t" !!true | + "total" !!false | + "T" !!true | + "This" !!false | + "0" !!false | + "on" !!true | + None !!false | + Some("t") !!true | + Empty !!false | + Full("t") !!true | + failure !!false | + List("t", "f") !!true |> { + (o: Any, result: Boolean) => toBoolean(o) === result + } + } + + "have a AsBoolean extractor converting any object to a reasonable Boolean value" in { + "object value" ||"boolean value" |> + "t" !!Some(true) | + "" !!None | + "string" !!None | + "total" !!None | + "T" !!Some(true) | + "This" !!None | + "0" !!Some(false) | { + (o: String, result: Option[Boolean]) => AsBoolean.unapply(o) === result + } + } + + "have an AsInt extractor converting any String to a reasonable Int value" in { + "object value" ||"int value" |> + "3" !!Some(3) | + "n" !!None | { + (o: String, result: Option[Int]) => AsInt.unapply(o) === result + } + } + + "have an AsLong extractor converting any String to a reasonable Long value" in { + "object value" ||"long value" |> + "3" !!Some(3L) | + "n" !!None | { + (o: String, result: Option[Long]) => AsLong.unapply(o) === result + } + } + + "have a toInt method converting any object to a reasonable Int value" in { + def date(t: Int) = new _root_.java.util.Date(t) + toInt(null) === 0 + "object value" ||"int value" |> + 1 !!1 | + 1L !!1 | + List(1, 2) !!1 | + Some(1) !!1 | + Full(1) !!1 | + None !!0 | + Empty !!0 | + failure !!0 | + "3" !!3 | + "n" !!0 | + date(3000) !!3 | { + (o: Any, result: Int) => toInt(o) === result + } + } + + "have a toLong method converting any object to a reasonable Long value" in { + def date(t: Int) = new _root_.java.util.Date(t) + toLong(null) === 0L + "object value" ||"long value" |> + 1 !!1L | + 1L !!1L | + List(1, 2) !!1L | + Some(1) !!1L | + Full(1) !!1L | + None !!0L | + Empty !!0L | + failure !!0L | + "3" !!3L | + "n" !!0L | + date(3000) !!3000L | { + (o: Any, result: Long) => toLong(o) === result + } + } + + "have a toByteArrayInputStream reading an InputStream to a ByteArrayInputStream" in { + var array: Array[Byte] = Array(12, 14) + val input = new ByteArrayInputStream(array) + val result = toByteArrayInputStream(input) + result.read === 12 + result.read === 14 + } + "have a isEq method comparing 2 Byte arrays and returning true if they contain the same elements" in { + var a: Array[Byte] = Array(12, 14) + var b: Array[Byte] = Array(12, 14) + var c: Array[Byte] = Array(12, 13) + isEq(a, b) must beTrue + isEq(a, c) must beFalse + } + "have a notEq method comparing 2 Byte arrays and returning true if they don't contain the same elements" in { + var a: Array[Byte] = Array(12, 14) + var b: Array[Byte] = Array(12, 13) + BasicTypesHelpers.notEq(a, b) must beTrue + } + } + + "PartialFunction guard" should { + + "put a guard around a partial function" in { + val pf1: PartialFunction[String, Unit] = { + case s if s.startsWith("s") => + } + + val pf2: PartialFunction[String, Boolean] = { + case "snipe" => true + case "bipe" => false + } + + val pf3 = pf1.guard(pf2) + val pf4: PartialFunction[String, Boolean] = pf1.guard(pf3) + + pf3.isDefinedAt("bipe") === false + pf3.isDefinedAt("snipe") === true + } + } + + "AvoidTypeErasure implicit value" should { + "be in scope" in { + def f(i:Int)(implicit d: AvoidTypeErasureIssues1) = i+1 + + f(2) === 3 + } + } + +} + diff --git a/core/util/src/test/scala-3/net/liftweb/util/BundleBuilderSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/BundleBuilderSpec.scala new file mode 100644 index 000000000..4d212db3d --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/BundleBuilderSpec.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2010-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import java.util.Locale + +import scala.xml.NodeSeq + +import org.specs2.matcher.XmlMatchers +import org.specs2.mutable.Specification + + +/** + * Systems under specification for BundleBuilder. + */ +class BundleBuilderSpec extends Specification with XmlMatchers { + "BundleBuilder Specification".title + + "BundleBuilder" should { + "Build a Bundle" in { + val b = BundleBuilder.convert(
+
Dog
+
Chien
+
hi
+
, Locale.US).openOrThrowException("Test") + + b.getObject("dog") must beEqualTo("Dog") + b.getObject("cat").asInstanceOf[NodeSeq] must ==/ (
hi
) + } + + "Build a Bundle must support default" in { + val b = BundleBuilder.convert(
+
Dog
+
Chien
+
hi
+
, Locale.US).openOrThrowException("Test") + + b.getObject("dog") must beEqualTo("Chien") + b.getObject("cat").asInstanceOf[NodeSeq] must ==/ (
hi
) + } + + } +} + diff --git a/core/util/src/test/scala-3/net/liftweb/util/CanResolveAsyncSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/CanResolveAsyncSpec.scala new file mode 100644 index 000000000..08e85b305 --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/CanResolveAsyncSpec.scala @@ -0,0 +1,55 @@ +/* + * Copyright 2015 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import scala.concurrent.{Future, Promise} +import scala.concurrent.ExecutionContext.Implicits.global + +import org.specs2.mutable.Specification + +import actor.LAFuture + +object CanResolveAsyncSpec extends Specification { + "CanResolveAsync" should { + "resolve Scala Futures" in { + val myPromise = Promise[String]() + + val resolver = implicitly[CanResolveAsync[Future[String], String]] + + val receivedResolution = new LAFuture[String] + resolver.resolveAsync(myPromise.future, receivedResolution.satisfy _) + + myPromise.success("All done!") + + receivedResolution.get === "All done!" + } + + "resolve LAFutures" in { + val myFuture = new LAFuture[String] + + val resolver = implicitly[CanResolveAsync[LAFuture[String], String]] + + val receivedResolution = new LAFuture[String] + resolver.resolveAsync(myFuture, receivedResolution.satisfy _) + + myFuture.satisfy("Got it!") + + receivedResolution.get === "Got it!" + } + } +} diff --git a/core/util/src/test/scala-3/net/liftweb/util/ClassHelpersSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/ClassHelpersSpec.scala new file mode 100644 index 000000000..2dd993236 --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/ClassHelpersSpec.scala @@ -0,0 +1,185 @@ +/* + * Copyright 2006-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import org.specs2.mutable.Specification + +import common._ +import ClassHelpers._ + + +/** + * Systems under specification for ClassHelpers. + */ +class ClassHelpersSpec extends Specification { + "ClassHelpers Specification".title + + "The findType function" should { + "return a Full can with the found class when given the type, the name, and a list of packages to conform to" in { + findType[java.util.List[Object]]("ArrayList", List("java.util")) must beEqualTo(Full(classOf[java.util.ArrayList[Object]])) + } + "return an Empty can if the class cannot be coerced to the expected type" in { + findType[String]("ClassHelpers", List("net.liftweb.util")) must beEqualTo(Empty) + } + } + "the findClass function" should { + "return a Full can with the found class when given the name and package" in { + findClass("ClassHelpers", List("net.liftweb.util")) must beEqualTo(Full(classOf[ClassHelpers])) + } + "return a Full can with the found class when given the name and package, with an underscored name instead of CamelCased" in { + findClass("class_helpers", List("net.liftweb.util")) must beEqualTo(Full(classOf[ClassHelpers])) + } + "return a Full can with the found class when given the name and a list of packages" in { + findClass("ClassHelpers", List("net.liftweb.util", "java.util")) must beEqualTo(Full(classOf[ClassHelpers])) + findClass("ArrayList", List("net.liftweb.util", "java.util")) must beEqualTo(Full(classOf[java.util.ArrayList[_]])) + } + "return a Full can with the found class when given the name, a list of packages and a target type to conform to" in { + findClass("ArrayList", List("java.util"), classOf[java.util.List[Object]]) must beEqualTo(Full(classOf[java.util.ArrayList[Object]])) + } + "return an Empty can if no class is found given a name and package" in { + findClass("ClassHelpers", List("net.liftweb.nothere")) must beEqualTo(Empty) + } + "return an Empty can if the class cannot be coerced to the expected type" in { + findClass("ClassHelpers", List("net.liftweb.util"), classOf[String]) must beEqualTo(Empty) + } + } + + "The findClass function" should { + "return a Full can with the found class when given a list of names and corresponding packages" in { + findClass(List(("wrong name", List("net.liftweb.util", "other.package")), + ("ClassHelpers", List("net.liftweb.util", "other.package")))) must beEqualTo(Full(classOf[ClassHelpers])) + } + "use a list of modifiers functions to try to modify the original name in order to find the class" in { + findClass("classHelpers", List("net.liftweb.util"), List((n: String) => n.capitalize)) must beEqualTo(Full(classOf[ClassHelpers])) + } + } + + "The callableMethod_? function" should { + "return true if the method is public and has no parameters" in { + val publicParameterLess = classOf[String].getMethod("length") + callableMethod_?(publicParameterLess) must beTrue + } + "return false if the method is public and has parameters" in { + val publicWithParameters = classOf[String].getMethod("indexOf", classOf[String]) + callableMethod_?(publicWithParameters) must beFalse + } + "return false if the method is private" in { + val privateMethod = classOf[java.util.ArrayList[Object]].getDeclaredMethod("readObject", classOf[java.io.ObjectInputStream]) + callableMethod_?(privateMethod) must beFalse + } + "return false if the method is null" in { + callableMethod_?(null) must beFalse + } + } + + "The containsClass function" should { + "return false if the list to match is null or empty" in { + containsClass(classOf[String], null) must beFalse + containsClass(classOf[String], Nil) must beFalse + } + "return false if the list to match doesn't contain any class assignable by the tested class" in { + containsClass(classOf[String], List(classOf[Float], classOf[java.lang.Integer])) must beFalse + } + } + + "The classHasControllerMethod function" should { + "return true if the class has 'name' as a callable method" in { + classHasControllerMethod(classOf[String], "length") must beTrue + } + "return false if the class doesn't have 'name' as a method" in { + classHasControllerMethod(classOf[String], "isNotEmpty") must beFalse + } + "return false if the class has a method but it is not callable" in { + classHasControllerMethod(classOf[java.util.ArrayList[Object]], "readObject") must beFalse + } + "return false if the class is null" in { + classHasControllerMethod(null, "readObject") must beFalse + } + } + + "The invokeControllerMethod function" should { + "return the result of calling the method on a new instance of the class" in { + invokeControllerMethod(classOf[String], "length") must beEqualTo(0) + } + "throw an exception when the method is not callable" in { + invokeControllerMethod(classOf[String], "isNotEmpty") must throwA[NoSuchMethodException] + } + "throw an exception if the class is null" in { + invokeControllerMethod(null, "length") must throwA[NullPointerException] + } + } + + "The invokeMethod function" should { + "return a Failure if the class is null" in { + invokeMethod(null, "", "length") must beLike { case Failure(_, _, _) => 1 === 1 } + } + "return a Failure if the instance is null" in { + invokeMethod(classOf[String], null, "length") must beLike { case Failure(_, _, _) => 1 === 1 } + } + "return a Failure if the method name is null" in { + invokeMethod(classOf[String], "", null) must beLike { case Failure(_, _, _) => 1 === 1 } + } + "return a Failure if the method doesnt exist on the class" in { + invokeMethod(classOf[String], "", "isNotEmpty") must beLike { case Failure(_, _, _) => 1 === 1 } + } + "return a Full can with the result if the method exist on the class" in { + invokeMethod(classOf[String], "", "length") must beEqualTo(Full(0)) + } + "return a Full can with the result if the method is an existing static method on the class" in { + invokeMethod(classOf[java.util.Calendar], null, "getInstance").isEmpty === false + } + "throw an exception if the method throws an exception" in { + class SpecificException extends Exception + class TestSnippet { def throwException = throw new SpecificException } + val testSnippet = new TestSnippet + invokeMethod(testSnippet.getClass, testSnippet, "throwException") must throwA[SpecificException] + } + } + + "The invokeMethod function" can { + "call a method with its parameters" in { + invokeMethod(classOf[String], "", "valueOf", Array("1")) must beEqualTo(Full("1")) + } + "call a method with its parameters and parameter types" in { + invokeMethod(classOf[String], "", "valueOf", Array("c"), Array(classOf[String])) must beEqualTo(Full("c")) + } + } + + "The instantiate function" should { + "return a full can if a class can be instantiated with a new instance" in { + instantiate(classOf[String]) must beEqualTo(Full("")) + } + "return a failure if a class can not be instantiated with a new instance" in { + instantiate(classOf[java.util.Calendar]) must beLike { case Failure(_, _, _) => 1 === 1 } + } + } + + "The createInvoker function" should { + "return Empty if the instance is null" in { + createInvoker("length", null) must beEqualTo(Empty) + } + "return a Full Box with the function from Unit to a Box containing the result of the method to invoke" in { + createInvoker("length", "").openOrThrowException("Test").apply() must beEqualTo(Full(0)) + } + "The invoker function will throw the cause exception if the method can't be called" in { + (() => createInvoker("get", "").openOrThrowException("Test").apply())() must throwA[Exception] + } + } + +} + diff --git a/core/util/src/test/scala-3/net/liftweb/util/CombParserHelpersSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/CombParserHelpersSpec.scala new file mode 100644 index 000000000..df56cde1a --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/CombParserHelpersSpec.scala @@ -0,0 +1,181 @@ +/* + * Copyright 2007-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import scala.util.parsing.combinator.Parsers + +import org.specs2.mutable.Specification +import org.specs2.ScalaCheck +import org.scalacheck.{Arbitrary, Gen, Prop} +import Gen._ +import Prop._ + + +class CombParserHelpersSpec extends Specification with ScalaCheck { + "CombParserHelpers Specification".title + + object ParserHelpers extends CombParserHelpers with Parsers + import ParserHelpers._ + + "The parser helpers" should { + "provide an isEof function returning true iff a char is end of file" in { + isEof('\u001a') must beTrue + } + "provide an notEof function returning true iff a char is not end of file" in { + notEof('\u001a') must beFalse + } + "provide an isNum function returning true iff a char is a digit" in { + isNum('0') must beTrue + } + "provide an notNum function returning true iff a char is not a digit" in { + notNum('0') must beFalse + } + "provide an wsc function returning true iff a char is a space character" in { + List(' ', '\t', '\r', '\n') foreach {wsc(_) must beTrue} + wsc('a') must beFalse + } + "provide a whitespace parser: white. Alias: wsc" in { + import WhiteStringGen._ + val whiteParse = (s: String) => wsc(s).isInstanceOf[Success[_]] + forAll(whiteParse) + } + "provide a whiteSpace parser always succeeding and discarding its result" in { + import StringWithWhiteGen._ + val whiteSpaceParse = + (s: String) => whiteSpace(s) must beLike { + case Success(x, y) => x.toString === "()" + } + forAll(whiteSpaceParse) + } + "provide an acceptCI parser to parse whatever string matching another string ignoring case" in { + import AbcdStringGen._ + val ignoreCaseStringParse: Function2[String, String, Boolean] = + (s: String, s2: String) => acceptCI(s).apply(s2) match { + case Success(x, y) => s2.toUpperCase.startsWith(s.toUpperCase) + case _ => true + } + forAll(ignoreCaseStringParse) + } + + "provide a digit parser - returning a String" in { + val isDigit: String => Boolean = + (s: String) => digit(s) match { + case Success(x, y) => s.matches("(?s)\\p{Nd}.*") + case _ => true + } + forAll(isDigit) + } + "provide an aNumber parser - returning an Int if succeeding" in { + val number: String => Boolean = + (s: String) => { + aNumber(s) match { + case Success(x, y) => s.matches("(?s)\\p{Nd}+.*") + case _ => true + } + } + forAll(number) + } + + "provide a slash parser" in { + slash("/").get === '/' + slash("x") must beLike {case Failure(_, _) => 1 === 1} + } + "provide a colon parser" in { + colon(":").get === ':' + colon("x") must beLike {case Failure(_, _) => 1 === 1} + } + "provide a EOL parser which parses the any and discards any end of line character" in { + List("\n", "\r") map { + s => + val result = EOL(s) + result.get.toString === "()" + result.next.atEnd must beTrue + } + + success + } + val parserA = elem("a", (c: Char) => c == 'a') + val parserB = elem("b", (c: Char) => c == 'b') + val parserC = elem("c", (c: Char) => c == 'c') + val parserD = elem("d", (c: Char) => c == 'd') + def shouldSucceed[T](r: ParseResult[T]) = r match { + case Success(x, y) => true + case _ => false + } + "provide a permute parser succeeding if any permutation of given parsers succeeds" in { + def permuteParsers(s: String) = shouldSucceed(permute(parserA, parserB, parserC, parserD)(s)) + val permutationOk = (s: String) => permuteParsers(s) + + forAll(AbcdStringGen.abcdString)(permutationOk) + } + "provide a permuteAll parser succeeding if any permutation of the list given parsers, or a sublist of the given parsers succeeds" in { + def permuteAllParsers(s: String) = shouldSucceed(permuteAll(parserA, parserB, parserC, parserD)(s)) + implicit def pick3Letters: Arbitrary[String] = AbcdStringGen.pickN(3, List("a", "b", "c")) + + forAll { (s: String) => + ((new scala.collection.immutable.StringOps(s)).nonEmpty) ==> permuteAllParsers(s) + } + } + "provide a repNN parser succeeding if an input can be parsed n times with a parser" in { + def repNNParser(s: String) = shouldSucceed(repNN(3, parserA)(s)) + implicit def pick3Letters: Arbitrary[String] = AbcdStringGen.pickN(3, List("a", "a", "a")) + + forAll { (s: String) => + ((new scala.collection.immutable.StringOps(s)).nonEmpty) ==> repNNParser(s) + } + } + } +} + + +object AbcdStringGen { + implicit def abcdString: Gen[String] = + for ( + len <- choose(4, 4); + string <- pick(len, List("a", "b", "c", "d")) + ) yield string.mkString("") + + def pickN(n: Int, elems: List[String]) = + Arbitrary { for (string <- pick(n, elems)) yield string.mkString("") } +} + + +object WhiteStringGen { + def genWhite = + for ( + len <- choose(1, 4); + string <- listOfN(len, frequency((1, Gen.const(" ")), (1, Gen.const("\t")), (1, Gen.const("\r")), (1, Gen.const("\n")))) + ) yield string.mkString("") + + implicit def genWhiteString: Arbitrary[String] = + Arbitrary { genWhite } +} + + +object StringWithWhiteGen { + import WhiteStringGen._ + + def genStringWithWhite = + for ( + len <- choose(1, 4); + string <- listOfN(len, frequency((1, Gen.const("a")), (2, Gen.const("b")), (1, genWhite))) + ) yield string.mkString("") + + implicit def genString: Arbitrary[String] = + Arbitrary { genStringWithWhite } +} diff --git a/core/util/src/test/scala-3/net/liftweb/util/ConnectionIdentifierSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/ConnectionIdentifierSpec.scala new file mode 100644 index 000000000..1db7a3256 --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/ConnectionIdentifierSpec.scala @@ -0,0 +1,34 @@ +/* + * Copyright 2014 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import org.specs2.mutable.Specification + +/** + * Systems under specification for ConnectionIdentifier. + */ +class ConnectionIdentifierSpec extends Specification { + "ConnectionIdentifier Specification".title + + "Connection identifier" should { + + "be set by property" in { + DefaultConnectionIdentifier.jndiName === "from_props" + } + } +} diff --git a/core/util/src/test/scala-3/net/liftweb/util/ControlHelpersSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/ControlHelpersSpec.scala new file mode 100644 index 000000000..a79d07804 --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/ControlHelpersSpec.scala @@ -0,0 +1,72 @@ +/* + * Copyright 2006-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import org.specs2.mutable.Specification + +import common._ +import ControlHelpers._ + + +/** + * Systems under specification for ControlHelpers. + */ +class ControlHelpersSpec extends Specification { + "ControlHelpers Specification".title + + "the tryo function" should { + "return a Full can if the tested block doesn't throw an exception" in { + tryo { "valid" } must beEqualTo(Full("valid")) + } + val exception = new RuntimeException("ko") + def failureBlock = { throw exception; () } + + "return a Failure if the tested block throws an exception" in { + tryo { failureBlock } must beEqualTo(Failure(exception.getMessage, Full(exception), Empty)) + } + "return Empty if the tested block throws an exception whose class is in the ignore list - with one element" in { + tryo(classOf[RuntimeException]) { failureBlock } must beEqualTo(Empty) + } + "return Empty if the tested block throws an exception whose class is in the ignore list - with 2 elements" in { + tryo(List(classOf[RuntimeException], classOf[NullPointerException])) { failureBlock } must beEqualTo(Empty) + } + "trigger a callback function with the exception if the tested block throws an exception" in { + val callback = (e: Throwable) => { e === exception; () } + tryo(callback) { failureBlock } + success + } + "trigger a callback function with the exception if the tested block throws an exception even if it is ignored" in { + val callback = (e: Throwable) => { e === exception; () } + tryo(List(classOf[RuntimeException]), Full(callback)) { failureBlock } + success + } + "don't trigger a callback if the tested block doesn't throw an exception" in { + var x = false + val callback = (e: Throwable) => { x = true } + tryo(callback) { "valid" } + x === false + } + "don't trigger a callback if the tested block doesn't throw an exception, even with an ignore list" in { + var x = false + val callback = (e: Throwable) => { x = true } + tryo(List(classOf[RuntimeException]), Full(callback)) { "valid" } + x === false + } + } +} + diff --git a/core/util/src/test/scala-3/net/liftweb/util/CssHelpersSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/CssHelpersSpec.scala new file mode 100644 index 000000000..b173c02ad --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/CssHelpersSpec.scala @@ -0,0 +1,99 @@ +/* + * Copyright 2007-2015 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import scala.xml._ + +import org.specs2.mutable.Specification + +import common._ + +class CssHelpersSpec extends Specification { + import CSSHelpers._ + + "CSSParser" should { + "leave most CSS alone" in { + val baseCss = + """ + #booyan { + text-indent: 1em; + -moz-columns: 3; + -webkit-text-antialiasing: grayscale; + -magical-fake-thing: booyan; + superfake: but-still-reasonably-css-y; + } + """ + + CssUrlPrefixer("prefix").fixCss(baseCss) must beEqualTo(Full(baseCss)) + } + + "leave relative CSS urls alone" in { + val baseCss = + """ + #booyan { + background: url(boom); + background-image: url('boom?bam,sloop#"shap%20bap'); + image-set: url("http://boom.com/magic?'bam,sloop#bam%21bap") + } + + .bam { + background-image: url("boom?bam,sloop#shap%20bap"); + } + """ + + CssUrlPrefixer("prefix").fixCss(baseCss) must beEqualTo(Full(baseCss)) + } + + "prefix root-relative CSS urls with the specified prefix" in { + val baseCss = + """ + |#booyan { + | background: url(/boom); + | background-image: url('/boom?bam,"sloop#shap%20bap'); + | image-set: url("/boom.com/magic?bam,'sloop#bam%21bap") + |}""".stripMargin('|') + + CssUrlPrefixer("prefix").fixCss(baseCss).openOrThrowException("test") === + """ + |#booyan { + | background: url(prefix/boom); + | background-image: url('prefix/boom?bam,"sloop#shap%20bap'); + | image-set: url("prefix/boom.com/magic?bam,'sloop#bam%21bap") + |}""".stripMargin('|') + } + + "fail on mismatched quotes or parens and report where it failed" in { + CssUrlPrefixer("prefix").fixCss("#boom { url('ha) }") must beLike { + case Failure(message, _, _) => + message must contain("'ha") + } + + CssUrlPrefixer("prefix").fixCss("#boom { url(\"ha) }") must beLike { + case Failure(message, _, _) => + message must contain("\"ha") + } + + CssUrlPrefixer("prefix").fixCss("#boom { url('ha' }") must beLike { + case Failure(message, _, _) => + message must contain("ha' }") + } + } + + // Escaped quotes-in-quotes currently fail. Maybe we want to support these? + } +} diff --git a/core/util/src/test/scala-3/net/liftweb/util/CssSelectorSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/CssSelectorSpec.scala new file mode 100644 index 000000000..131b2c297 --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/CssSelectorSpec.scala @@ -0,0 +1,1027 @@ +/* + * Copyright 2010-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import org.specs2.matcher.XmlMatchers +import org.specs2.mutable.Specification + +import common._ +import scala.xml._ + +import Helpers._ + +/** + * Systems under specification for CSS Selector. + */ +class CssSelectorSpec extends Specification with XmlMatchers { + "CSS Selector Specification".title + + "CssSelector" should { + "fail for garbage input" in { + CssSelectorParser.parse(" 49234e23").isDefined === false + } + + "select an id" in { + CssSelectorParser.parse("#foo").openOrThrowException("If the box is empty, we want a failure") === + IdSelector("foo", Empty) + } + + "a selector with cruft at the end must fail" in { + CssSelectorParser.parse("#foo I li**ke yaks").isDefined === false + } + + ":yak must not parse" in { + CssSelectorParser.parse(":yak").isDefined === false + } + + ":button must parse" in { + CssSelectorParser.parse(":button").openOrThrowException("If the box is empty, we want a failure") === + AttrSelector("type", "button", Empty) + } + + + ":checkbox must parse" in { + CssSelectorParser.parse(":checkbox").openOrThrowException("If the box is empty, we want a failure") === + AttrSelector("type", "checkbox", Empty) + } + + ":file must parse" in { + CssSelectorParser.parse(":file").openOrThrowException("If the box is empty, we want a failure") === + AttrSelector("type", "file", Empty) + } + + ":password must parse" in { + CssSelectorParser.parse(":password").openOrThrowException("If the box is empty, we want a failure") === + AttrSelector("type", "password", Empty) + } + + ":radio must parse" in { + CssSelectorParser.parse(":radio").openOrThrowException("If the box is empty, we want a failure") === + AttrSelector("type", "radio", Empty) + } + + ":reset must parse" in { + CssSelectorParser.parse(":reset").openOrThrowException("If the box is empty, we want a failure") === + AttrSelector("type", "reset", Empty) + } + + ":submit must parse" in { + CssSelectorParser.parse(":submit").openOrThrowException("If the box is empty, we want a failure") === + AttrSelector("type", "submit", Empty) + } + + ":text must parse" in { + CssSelectorParser.parse(":text").openOrThrowException("If the box is empty, we want a failure") === + AttrSelector("type", "text", Empty) + } + + "select an id with attr subnodes" in { + CssSelectorParser.parse("#foo *[dog] ").openOrThrowException("If the box is empty, we want a failure") === + IdSelector("foo", Full(AttrSubNode("dog"))) + } + + "select an id with no star attr subnodes" in { + CssSelectorParser.parse("#foo [woof] ").openOrThrowException("If the box is empty, we want a failure") === + IdSelector("foo", Full(AttrSubNode("woof"))) + } + + "select an id with attr append subnodes" in { + CssSelectorParser.parse("#foo *[dog+] ").openOrThrowException("If the box is empty, we want a failure") === + IdSelector("foo", Full(AttrAppendSubNode("dog"))) + } + + "select an id with no star attr append subnodes" in { + CssSelectorParser.parse("#foo [woof+] ").openOrThrowException("If the box is empty, we want a failure") === + IdSelector("foo", Full(AttrAppendSubNode("woof"))) + } + + "select an id with attr append subnodes" in { + CssSelectorParser.parse("#foo *[dog!] ").openOrThrowException("If the box is empty, we want a failure") === + IdSelector("foo", Full(AttrRemoveSubNode("dog"))) + } + + "select an id with no star attr append subnodes" in { + CssSelectorParser.parse("#foo [woof!] ").openOrThrowException("If the box is empty, we want a failure") === + IdSelector("foo", Full(AttrRemoveSubNode("woof"))) + } + + "select attr/val pair" in { + CssSelectorParser.parse("frog=dog").openOrThrowException("test") === + AttrSelector("frog", "dog", Empty) + } + + + "select attr/val pair single quote" in { + CssSelectorParser.parse("frog='dog food' *").openOrThrowException("test") === + AttrSelector("frog", "dog food", Full(KidsSubNode())) + } + + + "select attr/val pair double quote" in { + CssSelectorParser.parse("frog=\"dog breath\"").openOrThrowException("test") === + AttrSelector("frog", "dog breath", Empty) + } + + "select name/val pair" in { + CssSelectorParser.parse("name=dog").openOrThrowException("test") === + NameSelector("dog", Empty) + } + + "select name/val pair" in { + CssSelectorParser.parse("@dog").openOrThrowException("test") === + NameSelector("dog", Empty) + } + + "select name/val pair" in { + CssSelectorParser.parse("@dog *").openOrThrowException("test") === + NameSelector("dog", Full(KidsSubNode())) + } + + "select name/val pair" in { + CssSelectorParser.parse("@dog -*").openOrThrowException("test") === + NameSelector("dog", Full(PrependKidsSubNode())) + } + + "select name/val pair surround" in { + CssSelectorParser.parse("@dog <*>").openOrThrowException("test") === + NameSelector("dog", Full(SurroundKids())) + } + + "select name/val pair" in { + CssSelectorParser.parse("@dog *+").openOrThrowException("test") === + NameSelector("dog", Full(AppendKidsSubNode())) + } + + + "select name/val pair single quote" in { + CssSelectorParser.parse("name='dog food' *").openOrThrowException("test") === + NameSelector("dog food", Full(KidsSubNode())) + } + + + "select name/val pair double quote" in { + CssSelectorParser.parse("name=\"dog breath\"").openOrThrowException("test") === + NameSelector("dog breath", Empty) + } + + "select a class" in { + CssSelectorParser.parse(".foo").openOrThrowException("If the box is empty, we want a failure") === ClassSelector("foo", Empty) + } + + "select a class with subnodes" in { + CssSelectorParser.parse(".foo * ").openOrThrowException("If the box is empty, we want a failure") === + ClassSelector("foo", Full(KidsSubNode())) + } + + "Support selecting this node" in { + CssSelectorParser.parse(".foo ^^ ").openOrThrowException("If the box is empty, we want a failure") === + ClassSelector("foo", Full(SelectThisNode(false))) + } + + "Support selecting this node" in { + CssSelectorParser.parse(".foo ^* ").openOrThrowException("If the box is empty, we want a failure") === + ClassSelector("foo", Full(SelectThisNode(true))) + } + + "select a class with attr subnodes" in { + CssSelectorParser.parse(".foo *[dog] ").openOrThrowException("If the box is empty, we want a failure") === + ClassSelector("foo", Full(AttrSubNode("dog"))) + } + + "select an id with no star attr subnodes" in { + CssSelectorParser.parse(".foo [woof] ").openOrThrowException("If the box is empty, we want a failure") === + ClassSelector("foo", Full(AttrSubNode("woof"))) + } + + "select multiple depth" in { + CssSelectorParser.parse("div .foo [woof] ").openOrThrowException("If the box is empty, we want a failure") === + EnclosedSelector(ElemSelector("div", Empty), ClassSelector("foo", Full(AttrSubNode("woof")))) + } + + "select multiple depth with star" in { + CssSelectorParser.parse("div .foo * ").openOrThrowException("If the box is empty, we want a failure") === + EnclosedSelector(ElemSelector("div", Empty), ClassSelector("foo", Full(KidsSubNode()))) + } + + "select multiple super depth with star" in { + CssSelectorParser.parse("span div .foo * ").openOrThrowException("If the box is empty, we want a failure") === + EnclosedSelector(ElemSelector("span", Empty), EnclosedSelector(ElemSelector("div", Empty), ClassSelector("foo", Full(KidsSubNode())))) + } + + + } + +} + +class CssBindHelpersSpec extends Specification with XmlMatchers { + + "css bind helpers" should { + "clear clearable" in { + ClearClearable() must ==/ () + } + + "substitute a String by id" in { + ("#foo" #> "hello").apply() must ==/ (hello) + } + + + "not duplicate classes" in { + + def anchor(quesType: String, value: String) = { + (value) + } + var page = 1 + var elements = List("1","2","3","4") + + val xml =
+
+ 1 + 1 + 1 +
+ +
+ + val sel = ".question" #> elements.map(value => { + ".question [id]" #> ("question-" + value) & + ".question [class]" #> ("question-" + value) & + ".L" #> anchor("L", value) & + ".U" #> anchor("U", value) & + ".D" #> anchor("D", value) + }) + + val res = sel(xml) + + ((res \\ "a").head \ "@class").head.text === "selected L" + } + + + "Compound selector" in { + val res = + (".foo [href]" #> "http://dog.com" & ".bar [id]" #> "moo").apply( + ) + (res \ "@href").text === "http://dog.com" + (res \ "@id").text === "moo" + } + + "not stack overflow on Elem" in { + val xf = "* [id]" #> "xx" & + "* [style]" #> "border:thin solid black" & + "* *" #> + success + } + + "not stack overflow on Elem" in { + val xf = "* [id]" #> "xx" & + "* [style]" #> "border:thin solid black" & + "* *+" #> + + xf(
) + success + } + + "not stack overflow on Elem" in { + val xf = "* [id]" #> "xx" & + "* [style]" #> "border:thin solid black" & + "* -*" #> + + xf(
) + success + } + + "data-name selector works" in { + val xf = ";frog" #> hi + + xf(
Moose
) must ==/ (
hi
) + } + + "support modifying attributes along with body" in { + val org =
foo + val func = "a [href]" #> "dog" & "a *" #> "bar" + val res = func(org) + + res.toString === "bar" + } + + "substitute a String by id" in { + ("#foo" replaceWith "hello").apply() must ==/ (hello) + } + + "substitute a String by nested class" in { + ("div .foo" #> "hello").apply(
) must ==/ (
hello
) + } + + "substitute a String by deep nested class" in { + ("#baz div .foo" #> "hello").apply( +
) must ==/ (
hello
) + } + + "insert a String by deep nested class" in { + ("#baz div .foo *" #> "hello").apply( +
) must ==/ (
hello
) + } + + + "Only apply to the top elem" in { + val xf = "^ [href]" #> "wombat" + + xf(stuff) must ==/ (stuff) + } + + + + "Select a node" in { + ("#foo ^^" #> "hello").apply(
) must ==/ () + } + + "Another nested select" in { + val template = +
+ + +

+
+
+ + +

+
+
+ + val xf = "#get ^^" #> "ignore" & "#file_upload" #> + + val ret = xf(template) + + ret(0).asInstanceOf[Elem].label === "div" + ret.length === 1 + (ret \ "@id").text === "get" + + (ret \\ "input").length === 2 + + ((ret \\ "input").toList(0) \ "@type").map(_.text) === List("moose") + + } + + "Child nested select" in { + val template = +
+ + +

+
+
+ + +

+
+
+ + val xf = "#get ^*" #> "ignore" & "#file_upload" #> + + val ret = xf(template) + + (ret \\ "div").length === 0 + + (ret \\ "input").length === 2 + + ((ret \\ "input").toList(0) \ "@type").map(_.text) === List("moose") + + } + + "Select a node and transform stuff" in { + val ret = ("#foo ^^" #> "hello" & + "span [id]" #> "bar")() + + ret(0).asInstanceOf[Elem].label === "span" + ret.length === 1 + (ret \ "@id").text === "bar" + } + + + "Select a node and transform stuff deeply nested" in { + val ret = ("#foo ^^" #> "hello" & + "span [id]" #> "bar")(
) + + ret(0).asInstanceOf[Elem].label === "span" + ret.length === 1 + (ret \ "@id").text === "bar" + } + + + "Select a node and transform stuff deeply nested 2" in { + val ret = ("#foo ^^" #> "hello" & + "span [id]" #> "bar")(
) + + ret(0).asInstanceOf[Elem].label === "span" + ret.length === 1 + (ret \ "@id").text === "bar" + (ret \ "@dog").text === "woof" + } + + + + "substitute multiple Strings by id" in { + ("#foo" #> "hello" & + "#baz" #> "bye" + )(
Hello
) === (NodeSeq fromSeq {Text("bye")}{Text("hello")}) + } + + "bind href and None content" in { + val opt: Option[String] = None + val res = ("top *" #> opt & + "top [href]" #> "frog")(cat) + + res.text === "" + (res \ "@href").text.mkString === "frog" + } + + "bind href and Some content" in { + val opt: Option[String] = Some("Dog") + val res = ("top *" #> opt & + "top [href]" #> "frog")(cat) + + res.text === "Dog" + (res \ "@href").text.mkString === "frog" + } + + "bind href and Some content with multiple attrs" in { + val opt: Option[String] = Some("Dog") + val res = ("top *" #> opt & + "top [meow]" #> "woof" & + "top [href]" #> "frog")(cat) + + res.text === "Dog" + (res \ "@href").text.mkString === "frog" + (res \ "@meow").text.mkString === "woof" + } + + "option transform on *" in { + val opt: Option[String] = None + val res = ("* *" #> opt.map(ignore => "Dog")).apply(cat) + res.head === + } + + "append attribute to a class with spaces" in { + val stuff = List("a", "b") + val res = ("* [class+]" #> stuff).apply(cat) + (res \ "@class").text === "q a b" + } + + "append attribute to an href" in { + val stuff = List("&a=b", "&b=d") + val res = ("* [href+]" #> stuff).apply(cat) + (res \ "@href").text === "q?z=r&a=b&b=d" + } + + "remove an attribute from a class" in { + val func = ".foo [class!]" #> "andOther" + + (func() \ "@class").text === "foo" + } + + "remove an attribute from a class and the attribute if it's the only one left" in { + val func = ".foo [class!]" #> "foo" + val res = func() + + (res \ "@class").length === 0 + } + + "don't merge class attribute" in { + val func = "p !!" #> 10 + val res = func.apply(

Test

) + (res \ "@class").text === "replacement" + } + + "merge other attributes when using don't merge class modification" in { + val func = "p !!" #> 10 + val res = func.apply(

Test

) + (res \ "@data-one").text === "1" + (res \ "@data-two").text === "2" + (res \ "@class").text === "replacement" + } + + "leave node class attribute for replacement without class" in { + // TODO: Since was agreed not to change current behaviour, create test for it (https://groups.google.com/forum/#!topic/liftweb/Nswcxoykspc) + val func = "p !!" #> 10 + val res = func.apply(

Test

) + (res \ "@class").text === "first second" + } + + "Remove a subnode's class attribute" in { + + val func = ".removeme !!" #> ("td [class!]" #> "removeme") + val res = func.apply(Hi) + + ((res \ "td") \ "@class").text === "fish" + } + + + "not remove a non-existant class" in { + val func = ".foo [class!]" #> "bar" + val res = func() + + (res \ "@class").text === "foo" + } + + + "remove an attribute from an attribute" in { + val func = "span [href!]" #> "foo" + val res = func() + + (res \ "@href").length === 0 + } + + + "not remove a non-existant href" in { + val func = "span [href!]" #> "bar" + val res = func() + + (res \ "@href").text === "foo bar" + } + + "option transform on *" in { + val opt: Option[Int] = Full(44) + val res = ("* *" #> opt.map(ignore => "Dog")).apply(cat) + res must ==/ (Dog) + } + + + "Java number support" in { + val f = "a *" #> Full(java.lang.Long.valueOf(12)) + val xml = Hello + + f(xml) must ==/ (12) + } + + + "Surround kids" in { + val f = "a <*>" #>
+ val xml = Meow Cat woof + + f(xml) must ==/ (Meow
Cat
woof
) + } + + "Andreas's thing doesn't blow up" in { + def cachedMessageList: Box[Box[String]] = Empty + + def messageListId = "Hello" + + def collapseUnless[A](isEmptyCond: Boolean)(f: => A): Box[A] = { + if (!isEmptyCond) { + Empty + } else { + Full(f) + } + } + + ".noMail" #> collapseUnless(cachedMessageList.map(_.isEmpty).openOr(true)) { + "tbody [id]" #> messageListId & + "*" #> PassThru + } + + true === true + } + + "other Andreas test" in { + def renderBlogEntrySummary = { + ".blogEntry" #> ((ns: NodeSeq) => { + ("*" #> "Horse").apply(ns) + }) + } + + + + def render = { + + "*" #> ((ns: NodeSeq) => + renderBlogEntrySummary.apply(ns) ++ hi + ) + } + + render + + true === true + } + + + "option transform on *" in { + val opt: Box[String] = Empty + val res = ("* *" #> opt.map(ignore => "Dog")).apply(cat) + res.head === + } + + "option transform on *" in { + val opt: Box[Int] = Some(44) + val res = ("* *" #> opt.map(ignore => "Dog")).apply(cat) + res must ==/ (Dog) + } + + "transform on *" in { + val res = ("* *" #> "Dog").apply(cat) + res must ==/ (Dog) + } + + "transform child content on *+" in { + val res = ("* *+" #> "moose").apply(I like ) + res.text === "I like moose" + } + + "transform child content on -*" in { + val res = ("* -*" #> "moose").apply( I like) + res.text === "moose I like" + } + + "transform on li" in { + val res = ("li *" #> List("Woof", "Bark") & ClearClearable)( +
  • meow
  • a
  • a
) + res must ==/ (
  • Woof
  • Bark
) + } + + "substitute multiple Strings by id" in { + (("#foo" replaceWith "hello") & + ("#baz" replaceWith "bye") + )( +
Hello
+ ) === (NodeSeq fromSeq {Text("bye")}{Text("hello")}) + } + + "substitute multiple Strings with a List by id" in { + ("#foo" #> "hello" & + "#baz" #> List("bye", "bye"))(
Hello
) === (NodeSeq fromSeq {Text("bye")}{Text("bye")}{Text("hello")}) + } + + "substitute multiple Strings with a List by id" in { + (("#foo" replaceWith "hello") & + ("#baz" replaceWith List("bye", "bye")))(
Hello
) === (NodeSeq fromSeq {Text("bye")}{Text("bye")}{Text("hello")}) + } + + + "substitute multiple Strings with a List of XML by id" in { + val answer = ("#foo" #> "hello" & + "#baz" #> List[NodeSeq](, Meow))(
Hello
) + + (answer \ "i").length === 2 + (answer \ "i")(0) must ==/ () + (answer \ "i")(1) must ==/ (Meow) + } + + "substitute multiple Strings with a List of XML by id" in { + val answer = (("#foo" replaceWith "hello") & + ("#baz" replaceWith List[NodeSeq](, Meow)))(
Hello
) + + (answer \ "i").length === 2 + (answer \ "i")(0) must ==/ () + (answer \ "i")(1) must ==/ (Meow) + } + + "substitute by name" in { + val answer = ("name=moose" #> ).apply ( +
) + + (answer \ "input")(0) must ==/ () + } + + + "Deal with NodeSeq as a NodeSeq" in { + val f = "h6 *" #> ((Text("Some awesome ") ++ text ++ Text(" here.")): NodeSeq) + val xml =
Dude, where's my car?
+ + val res = f(xml) + res must ==/ (
Some awesome text here.
) + } + + "substitute by name" in { + val answer = ("name=moose" replaceWith ).apply ( +
) + + (answer \ "input")(0) must ==/ () + } + + + "substitute by name with attrs" in { + val answer = ("name=moose" #> ).apply ( +
) + + (answer \ "input")(0) must ==/ () + } + + "substitute by name with attrs" in { + val answer = ("name=moose" replaceWith ).apply ( +
) + + (answer \ "input")(0) must ==/ () + } + + + "substitute by a selector with attrs" in { + val answer = ("cute=moose" #> ).apply ( +
) + + (answer \ "input")(0) must ==/ () + } + + "substitute by a selector with attrs" in { + val answer = ("cute=moose" replaceWith ).apply ( +
) + + (answer \ "input")(0) must ==/ () + } + + "Map of funcs" in { + val func: NodeSeq => NodeSeq = "#horse" #> List(1,2,3).map(".item *" #> _) + val answer: NodeSeq = func(
frogi
) + + answer must ==/ (
frog1
frog2
frog3
) + + } + + "maintain unique id attributes provided by transform" in { + val func = ".thinglist *" #> + (".thing" #> List("xx1", "xx2", "xx2", "xx2", "xx4").map(t => { + ".thing [id]" #> t + }) + ) + val answer = func(
) + + answer must ==/ (
) + } + + "merge classes" in { + val answer = ("cute=moose" #> ).apply ( +
) + + (answer \ "input")(0) must ==/ () + } + + + "merge classes" in { + val answer = ("cute=moose" replaceWith ).apply ( +
) + + (answer \ "input")(0) must ==/ () + } + + + + + "list of strings" in { + val answer = ("#moose *" #> List("a", "b", "c", "woof") & + ClearClearable).apply ( +
    +
  • first
  • +
  • second
  • +
  • Third
  • +
) + + val lis = (answer \ "li").toList + + lis.length === 4 + + lis(0) must ==/ (
  • a
  • ) + lis(3) must ==/ (
  • woof
  • ) + } + + + "list of Nodes" in { + val answer = ("#moose *" #> List[NodeSeq]("a", Text("b"), Text("c"), woof) & + ClearClearable).apply ( +
      +
    • first
    • +
    • second
    • +
    • Third
    • +
    ) + + val lis = (answer \ "li").toList + + lis.length === 4 + + lis(0) must ==/ (
  • "a"
  • ) + lis(3) must ==/ (
  • woof
  • ) + } + + + "set href" in { + val answer = ("#moose [href]" #> "Hi" & + ClearClearable).apply ( + ) + + + (answer \ "a" \ "@href").text === "Hi" + (answer \ "li").length === 0 + } + + "set href and subnodes" in { + val answer = ("#moose [href]" #> "Hi" & + ClearClearable).apply ( + ) + + + (answer \ "a" \ "@href").text === "Hi" + (answer \\ "li").length === 0 + } + + + "list of strings" in { + val answer = (("#moose *" replaceWith List("a", "b", "c", "woof")) & + ClearClearable).apply ( +
      +
    • first
    • +
    • second
    • +
    • Third
    • +
    ) + + val lis = (answer \ "li").toList + + lis.length === 4 + + lis(0) must ==/ (
  • a
  • ) + lis(3) must ==/ (
  • woof
  • ) + } + + "bind must bind to subnodes" in { + val html =
      +
    • + +
    • +
    + + val lst = List(1,2,3) + + val f = ".users *" #> ("li" #> lst.map(i => ".user [userid]" #> i)) + + (f(html) \\ "ul").length === 1 + (f(html) \\ "li").length === 3 + } + + "list of Nodes" in { + val answer = (("#moose *" replaceWith List[NodeSeq]("a", Text("b"), Text("c"), woof)) & + ClearClearable).apply ( +
      +
    • first
    • +
    • second
    • +
    • Third
    • +
    ) + + val lis = (answer \ "li").toList + + lis.length === 4 + + lis(0) must ==/ (
  • "a"
  • ) + lis(3) must ==/ (
  • woof
  • ) + } + + + "set href" in { + val answer = (("#moose [href]" replaceWith "Hi") & + ClearClearable).apply ( + ) + + + (answer \ "a" \ "@href").text === "Hi" + (answer \ "li").length === 0 + } + + "set href and subnodes" in { + val answer = (("#moose [href]" replaceWith "Hi") & + ClearClearable).apply ( + ) + + + (answer \ "a" \ "@href").text === "Hi" + (answer \\ "li").length === 0 + } + + + } +} + + +/** + * This class doesn't actually perform any tests, but insures that + * the implicit conversions work correctly + */ +object CheckTheImplicitConversionsForToCssBindPromoter { + val bog = new CssBindPromoter(Empty, Empty) + + "foo" #> "baz" + + bog #> "Hello" + bog #> + bog #> 1 + bog #> Symbol("foo") + bog #> 44L + bog #> 1.22 + bog #> false + + bog #> List() + bog #> Full() + val e: Box[String] = Empty + bog #> e + bog #> Some() + val n: Option[String] = None + bog #> n + + + bog #> List("Hello") + bog #> List(1.22) + bog #> List(44L) + bog #> List(1) + bog #> Full("Dog") + bog #> Some("Moo") + + + bog #> List((null: Bindable)) + bog #> Full((null: Bindable)) + bog #> Some((null: Bindable)) + + bog #> nsToNs _ + bog #> nsToOptNs _ + bog #> nsToBoxNs _ + bog #> nsToSeqNs _ + + bog #> nsToString _ + bog #> nsToOptString _ + bog #> nsToBoxString _ + bog #> nsToSeqString _ + + val nsf: NodeSeq => NodeSeq = bog #> "Hello" & + bog #> & + bog #> 1 & + bog #> Symbol("foo") & + bog #> 44L & + bog #> false + + "foo" #> "Hello" + "foo" #> + "foo" #> 1 + "foo" #> Symbol("foo") + "foo" #> 44L + "foo" #> false + + "foo" #> List() + "foo" #> Full() + "foo" #> Some() + + + "foo" #> List("Hello") + "foo" #> Full("Dog") + "foo" #> Some("Moo") + + + "foo" #> List((null: Bindable)) + "foo" #> Full((null: Bindable)) + "foo" #> Some((null: Bindable)) + + "foo" #> nsToNs _ + "foo" #> nsToOptNs _ + "foo" #> nsToBoxNs _ + "foo" #> nsToSeqNs _ + + "foo" #> nsToString _ + "foo" #> nsToOptString _ + "foo" #> nsToBoxString _ + "foo" #> nsToSeqString _ + + "#foo" #> Set("a", "b", "c") + + val nsf2: NodeSeq => NodeSeq = "foo" #> "Hello" & + "foo" #> & + "foo" #> 1 & + "foo" #> Symbol("foo") & + "foo" #> 44L & + "foo" #> false + + "bar" #> List("1","2","3").map(s => "baz" #> s) + + "bar" #> Full(1).map(s => ("baz" #> s): CssBindFunc) + "bar" #> Some(1).map(s => ("baz" #> s): CssBindFunc) + + + + def nsToNs(in: NodeSeq): NodeSeq = in + def nsToOptNs(in: NodeSeq): Option[NodeSeq] = Some(in) + def nsToBoxNs(in: NodeSeq): Box[NodeSeq] = Full(in) + def nsToSeqNs(in: NodeSeq): Seq[NodeSeq] = List(in) + + def nsToString(in: NodeSeq): String = in.text + def nsToOptString(in: NodeSeq): Option[String] = Some(in.text) + def nsToBoxString(in: NodeSeq): Box[String] = Full(in.text) + def nsToSeqString(in: NodeSeq): Seq[String] = List(in.text) +} diff --git a/core/util/src/test/scala-3/net/liftweb/util/CurrencyZoneSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/CurrencyZoneSpec.scala new file mode 100644 index 000000000..3dedf8561 --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/CurrencyZoneSpec.scala @@ -0,0 +1,94 @@ +/* + * Copyright 2007-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import org.specs2.mutable.Specification + + +/** + * Systems under specification for CurrencyZone. + */ +class CurrencyZoneSpec extends Specification { + "CurrencyZone Specification".title + + "Australian money" should { + + "not equal to other money" in { + val auBoolean = AU(4.42) != US(4.42) + auBoolean === true + } + + "be not equal to a different amount of its own money" in { + val auBoolean = AU(4.48) == AU(4.481) + auBoolean === false + } + + + "be equal to the same amount of its own money" in { + val auBoolean = AU(4.42) == AU(4.42) + auBoolean === true + } + + "be comparable not gt" in { + val auBoolean = AU(4.42) > AU(4.420000) + auBoolean === false + } + + "be creatable" in { + AU(20.1).get must beMatching ("20.10") + } + + "be addable" in { + val au = AU(20.68) + AU(3.08) + au.get must beMatching ("23.76") + } + + "be subtractable" in { + val au = AU(23.76) - AU(3.08) + au.get must beMatching ("20.68") + } + + "be mutipliable" in { + val au = AU(20.68) * 3 + au.get must beMatching ("62.04") + } + + "be divisable" in { + val au = AU(20.68) / AU(3) + au.get must beMatching ("6.89") + } + + + "be comparable gt" in { + val auBoolean = AU(20.68) > AU(3) + auBoolean === true + } + + "be comparable lt" in { + val auBoolean = AU(3.439) < AU(3.44) + auBoolean === true + } + + "be comparable lt or eq" in { + val auBoolean = AU(20.68) <= AU(20.68) + auBoolean === true + } + } + +} + diff --git a/core/util/src/test/scala-3/net/liftweb/util/Html5ParserSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/Html5ParserSpec.scala new file mode 100644 index 000000000..f3726673f --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/Html5ParserSpec.scala @@ -0,0 +1,100 @@ +/* + * Copyright 2006-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import scala.xml.Elem + +import org.specs2.mutable.Specification +import org.specs2.execute.PendingUntilFixed + +import common._ +import Helpers._ + + +/** + * Systems under specification for Html5 Parser. + */ +class Html5ParserSpec extends Specification with PendingUntilFixed with Html5Parser with Html5Writer { + "Html5Parser Specification".title + + "Htm5 Writer" should { + "Write &" in { + toString() === """""" + } + + "ignore attributes that are null" in { + toString() === """""" + } + } + + "Html5 Parser" should { + val pages = for { + page1 <- tryo(readWholeStream(getClass.getResourceAsStream("Html5ParserSpec.page1.html"))).filter(_ ne null) + page2 <- tryo(readWholeStream(getClass.getResourceAsStream("Html5ParserSpec.page2.html"))).filter(_ ne null) + page3 <- tryo(readWholeStream(getClass.getResourceAsStream("Html5ParserSpec.page3.html"))).filter(_ ne null) + } yield (page1, page2, page3) + + pages match { + case Full(p) => + val (page1, page2, page3) = (new String(p._1), new String(p._2), new String(p._3)) + + "parse valid page type1" in { + val parsed = parse(page1).openOrThrowException("Test") + (parsed \\ "script").length must beGreaterThanOrEqualTo(4) + } + + "parse valid page type2" in { + val parsed = parse(page2).openOrThrowException("Test") + (parsed \\ "script").length must beGreaterThanOrEqualTo(4) + } + + "fail to parse invalid page type3" in { + val parsed = parse(page3) + parsed must beAnInstanceOf[Failure] + }.pendingUntilFixed + + case _ => + failure("Failed loading test files") // TODO: Improve error message + } + + "change to " in { + val parsed = parse("
    123
    ").openOrThrowException("Test") + val heads = parsed \\ "head" + heads.length === 1 + heads.text === "123" + (heads(0).asInstanceOf[Elem].prefix == null) === true + }.pendingUntilFixed + + "Parse stuff with lift: namespace" in { + val parsed = parse("""
    """) + val e = parsed.openOrThrowException("Test").asInstanceOf[Elem] + e.prefix === "lift" + e.label === "surround" + (parsed.openOrThrowException("Test") \ "@with").text === "dog" + } + + "Parse stuff without lift: namespace" in { + val parsed = parse("""
    """) + val e = parsed.openOrThrowException("Test").asInstanceOf[Elem] + e.label === "div" + (parsed.openOrThrowException("Test") \ "@with").text === "dog" + } + } + +} + diff --git a/core/util/src/test/scala-3/net/liftweb/util/HtmlHelpersSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/HtmlHelpersSpec.scala new file mode 100644 index 000000000..6369ed2df --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/HtmlHelpersSpec.scala @@ -0,0 +1,315 @@ +/* + * Copyright 2007-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import scala.xml._ + +import org.specs2.matcher.XmlMatchers +import org.specs2.mutable.Specification + +import common._ + +/** + * Systems under specification for HtmlHelpers. + */ +class HtmlHelpersSpec extends Specification with HtmlHelpers with XmlMatchers { + "HtmlHelpers Specification".title + + "findBox" should { + "find an id" in { + val xml = Dog + + findBox(xml) { + e => e.attribute("id"). + filter(_.text == "3"). + map(i => e) + }.openOrThrowException("Test") must ==/ () + } + + "not find an ide" in { + val xml = Dog + + findBox(xml) { + e => e.attribute("id"). + filter(_.text == "3").map(i => e) + } must beEqualTo(Empty) + } + + + "not find a the wrong id" in { + val xml = Dog + + findBox(xml) { + e => e.attribute("id"). + filter(_.text == "3").map(i => e) + } must beEqualTo(Empty) + } + } + + "findOption" should { + "find an id" in { + val xml = Dog + + findOption(xml) { + e => e.attribute("id"). + filter(_.text == "3").map(i => e) + }.get must ==/ () + } + + "not find an ide" in { + val xml = Dog + + findOption(xml) { + e => e.attribute("id"). + filter(_.text == "3").map(i => e) + } must beNone + } + + + "not find a the wrong id" in { + val xml = Dog + + findOption(xml) { + e => e.attribute("id"). + filter(_.text == "3").map(i => e) + } must beNone + } + } + + "findId" should { + val xml = + + Boom + Other boom + + + "find the element with a requested id in a NodeSeq" in { + findId(xml, "boom") must beLike { + case Some(element) => + element must ==/(Boom) + } + } + + "provide a None if a requested id is not found in the NodeSeq" in { + findId(xml, "explode") must beNone + } + + "find the first id in a NodeSeq when no id is requested" in { + findId(xml) must beEqualTo(Full("boom")) + } + + "provide an Empty if no ide is foud in a NodeSeq when no id is requested" in { + findId() must beEqualTo(Empty) + } + } + + "head removal" should { + "remove " in { + Helpers.stripHead(hello) must ==/(hello) + } + + "ignore non-head" in { + Helpers.stripHead(hello) must ==/(hello) + } + + "String subhead" in { + Helpers.stripHead(hello) must ==/(hello) + } + } + + "removeAttribute" should { + val element = + + "remove the specified attribute from a provided element" in { + val removed = removeAttribute("attribute", element) + + removed must ==/() + } + + "remove the specified attribute from a provided MetaData list" in { + val removed = removeAttribute("attribute", element.attributes) + + (removed("attribute") must beNull) and + (removed("otherAttribute") must beEqualTo(Text("good-bye"))) + } + } + + "addCssClass" should { + "add a new attribute if no class attribute exists" in { + (addCssClass("foo", ) \ "@class").text must beEqualTo("foo") + } + + "append to an existing class attribute if it already exists" in { + (addCssClass("foo", ) \ "@class").text must beEqualTo("dog foo") + } + } + + "ensureUniqueId" should { + "leave things unchanged if no ids collide" in { + val xml = + + + + + + val uniqued = {ensureUniqueId(xml).head} + + (uniqued must \("boom", "id" -> "thing")) and + (uniqued must \\("hello", "id" -> "other-thing")) and + (uniqued must \\("bye", "id" -> "third-thing")) + } + + "strip the ids if elements have an id matching a previous one" in { + val xml = + Group( + + + + ) + + val uniqued = NodeSeq.seqToNodeSeq(ensureUniqueId(xml).flatten) + + uniqued must ==/( + + + + ) + } + + "leave child element ids alone even if they match the ids of the root element or each other" in { + val xml = + + + + + + val uniqued = {ensureUniqueId(xml).head} + + (uniqued must \\("hello", "id" -> "thing")) and + (uniqued must \\("bye", "id" -> "thing")) + } + } + + "deepEnsureUniqueId" should { + "leave things unchanged if no ids collide" in { + val xml = + + + + + + val uniqued = {deepEnsureUniqueId(xml).head} + + (uniqued must \("boom", "id" -> "thing")) and + (uniqued must \\("hello", "id" -> "other-thing")) and + (uniqued must \\("bye", "id" -> "third-thing")) + } + + "strip the ids if elements have an id matching a previous one" in { + val xml = + Group( + + + + ) + + val uniqued = NodeSeq.seqToNodeSeq(deepEnsureUniqueId(xml).flatten) + + uniqued must ==/( + + + + ) + } + + "strip child element ids alone if they match the ids of the root element or each other" in { + val xml = + + + Boom + + + + + + val uniqued = {deepEnsureUniqueId(xml).head} + + uniqued must ==/( + + + + Boom + + + + + + ) + } + } + + "ensureId" should { + "if the first element has an id, replace it with the specified one" in { + val xml = + + + Boom + + + + + + val uniqued = {ensureId(xml, "other-thinger").head} + + uniqued must \("boom", "id" -> "other-thinger") + } + + "if the first element has no id, give it the specified one" in { + val xml = + + + Boom + + + + + + val uniqued = {ensureId(xml, "other-thinger").head} + + uniqued must \("boom", "id" -> "other-thinger") + } + + "not affect other elements" in { + val xml = + Group( + + + + ) + + val uniqued = NodeSeq.seqToNodeSeq(ensureId(xml, "other-thinger").flatten) + + uniqued must ==/( + + + + ) + } + } +} + diff --git a/core/util/src/test/scala-3/net/liftweb/util/HttpHelpersSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/HttpHelpersSpec.scala new file mode 100644 index 000000000..9d3e5978e --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/HttpHelpersSpec.scala @@ -0,0 +1,100 @@ +/* + * Copyright 2006-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import org.specs2.matcher.XmlMatchers +import org.specs2.mutable.Specification + +import HttpHelpers._ + + +/** + * Systems under specification for HttpHelpers. + */ +class HttpHelpersSpec extends Specification with HttpHelpers with ListHelpers with StringHelpers with XmlMatchers { + "HttpHelpers Specification".title + + "Http helpers" should { + "urlEncode and urlDecode functions" >> { + urlDecode(urlEncode("hello world")) === "hello world" + urlEncode(urlDecode("hello+world")) === "hello+world" + } + "a paramsToUrlParams function to translate a map of parameters to a URL query" in { + paramsToUrlParams(List(("firstname", "hello"), ("lastname", "world"))) === "firstname=hello&lastname=world" + } + "an appendParams function to add parameters to a URL query" in { + "creating the param list with ? if there are no existing params" in { + appendParams("www.helloworld.com/params", List(("firstname", "hello"), ("lastname", "world"))) === + "www.helloworld.com/params?firstname=hello&lastname=world" + } + "appending the param list with & if there are some already" in { + appendParams("www.helloworld.com/params?firstname=hello", List(("lastname", "world"))) === + "www.helloworld.com/params?firstname=hello&lastname=world" + } + "returning the url if no param list is passed" in { + appendParams("www.helloworld.com/params", Nil) === "www.helloworld.com/params" + } + } + "a couldBeHtml function" in { + "returning true if there is a pair (Content-Type, text/html)" in { + couldBeHtml(Map(("Content-Type", "text/html"))) must beTrue + } + "returning true if there is a pair (Content-Type, Application/xhtml+xml). The check is case insensitive" in { + couldBeHtml(Map(("Content-Type", "Application/xhtml+XML"))) must beTrue + } + "returning false if the content type is something else (Content-Type, application/jpeg)" in { + couldBeHtml(Map(("Content-Type", "application/jpeg"))) must beFalse + } + "returning true if there is no a pair starting with Content-Type" in { + couldBeHtml(Map(("no content type", "text/html"))) must beTrue + } + } + "a noHtmlTag" in { + "returning true if a xml node doesn't contain the html tag" in { + noHtmlTag() must beTrue + } + "returning false if a xml node contains the html tag" in { + noHtmlTag() must beFalse + } + } + "a toHashMap function transforming a Map to a mutable HashMap" in { + toHashMap(Map(1 -> 2, 3 -> 4)) must haveClass[scala.collection.mutable.HashMap[Int, Int]] + } + "an insureField function" in { + "checking that the appropriate fields are in the header" in { + insureField(List(("name", "hello")), List(("name", "hello"))) === List(("name", "hello")) + } + "checking that the appropriate fields are in the header, adding them if necessary" in { + insureField(List(("name2", "hello")), List(("name", "hello"))) === List(("name", "hello"), ("name2", "hello")) + } + } + "an implicit definition to transform a pair to an UnprefixedAttribute" in { + pairToUnprefixed(("value", 1)).apply("value").toString === "1" + } + "a findOrAddId function" in { + "returning an element and its id if found" in { findOrAddId() === (, "1") } + "returning an element with a random id if not found" in { + val (e, id) = findOrAddId() + e must \("@id") + // id must beMatching("R\\[a-zA-Z0-9]*") + } + } + } + // currentSus is no longer part of Specification in 1.6 def provide(e: =>Example) = { currentSus.verb += " provide"; e } +} + diff --git a/core/util/src/test/scala-3/net/liftweb/util/IoHelpersSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/IoHelpersSpec.scala new file mode 100644 index 000000000..144e51da7 --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/IoHelpersSpec.scala @@ -0,0 +1,58 @@ +/* + * Copyright 2017 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import org.specs2.mutable.Specification + +import common._ +import Helpers._ + +import java.nio.charset.StandardCharsets +import java.nio.file.{Files, Path} + +class IoHelpersSpec extends Specification with IoHelpers { + "IoHelpers Specification".title + + "Io helpers" should { + + "readWholeFile properly" in { + // Copy a resource file to the tmp directory so we can refer to it as a Path + val resourceAsPath: Box[Path] = { + for { + bytes <- tryo(readWholeStream(getClass.getResourceAsStream("IoHelpersSpec.txt"))).filter(_ ne null) + text <- tryo(new String(bytes)) + path = { + val tempFile = Files.createTempFile(s"IoHelpersSpec_${nextFuncName}", ".tmp") + Files.write(tempFile, text.getBytes(StandardCharsets.UTF_8)) + tempFile + } + } yield path + } + + resourceAsPath.isDefined === true + + resourceAsPath.foreach { path => + val pathContents = new String(readWholeFile(path)).trim + Files.delete(path) + pathContents === "IoHelpersSpec" + } + + success + } + } +} diff --git a/core/util/src/test/scala-3/net/liftweb/util/JsonCommandSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/JsonCommandSpec.scala new file mode 100644 index 000000000..6177f1a4b --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/JsonCommandSpec.scala @@ -0,0 +1,53 @@ +/* + * Copyright 2008-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import org.specs2.mutable.Specification + +import org.json4s._ +import org.json4s.native.JsonParser +import org.json4s.native.JsonMethods._ + + +/** + * Systems under specification for JsonCommand. + */ +class JsonCommandSpec extends Specification { + "JsonCommand Specification".title + + private def parse(in: String): JValue = JsonParser.parse(in) + + "The JsonCommand object" should { + "return None for non-commands" in { + JsonCommand.unapply(parse("""{"foo": "bar", "baz": false, "params": "moose"} """)) === None + } + + "return None for non-params" in { + JsonCommand.unapply(parse("""{"command": "frog", "foo": "bar", "baz": false} """)) === None + } + + "Parse even if target missing" in { + JsonCommand.unapply(parse("""{"command": "frog", "foo": "bar", "params": 99} """)) === Some(("frog", None, JInt(99))) + } + + "Parse the whole thing" in { + JsonCommand.unapply(parse("""{"command": "frog", "target": "spud", "foo": "bar", "params": 982, "baz": false} """)) === + Some(("frog", Some("spud"), JInt(982))) + } + } +} diff --git a/core/util/src/test/scala-3/net/liftweb/util/ListHelpersSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/ListHelpersSpec.scala new file mode 100644 index 000000000..f391c16a6 --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/ListHelpersSpec.scala @@ -0,0 +1,197 @@ +/* + * Copyright 2007-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import org.specs2.mutable.Specification + +import common._ + + +/** + * Systems under specification for ListHelpers. + */ +class ListHelpersSpec extends Specification with ListHelpers { + "ListHelpers Specification".title + + "ListHelpers.delta" should { + "insert after 2" in { + val ret = delta(List(1, 2, 4, 5), List(1, 2, 3, 4, 5)) { + case InsertAfterDelta(3, 2) => "ok" + case _ => "not ok" + } + ret === List("ok") + } + + "prepend and append 2,4, 99" in { + val ret = delta(List(2, 4, 99), List(1, 2, 3, 4, 5)) { + case InsertAfterDelta(3, 2) => "ok" + case AppendDelta(5) => "ok5" + case RemoveDelta(99) => "99" + case InsertAtStartDelta(1) => "1" + case InsertAfterDelta(5, 4) => "ok5" + case _ => "fail" + } + ret === List("1", "ok", "ok5", "99") + } + + "prepend and append" in { + val ret = delta(List(4, 2, 99), List(1, 2, 3, 4, 5)) { + case InsertAfterDelta(3, 2) => "ok" + case InsertAfterDelta(4, 3) => "ok3" + case RemoveDelta(4) => "r4" + case AppendDelta(5) => "ok5" + case RemoveDelta(99) => "99" + case InsertAtStartDelta(1) => "1" + case InsertAfterDelta(5, 4) => "ok5" + case _ => "fail" + } + ret === List("1", "r4", "ok", "ok3", "ok5", "99") + } + } + + "The ListHelpers first_? function" should { + "return an Empty can if the list is empty" in { + first_?((Nil: List[Int]))((i: Int) => true) must beEqualTo(Empty) + } + "return an Empty can if no element in the list satisfies the predicate" in { + first_?(List(1, 2, 3))((i: Int) => i < 0) must beEqualTo(Empty) + } + "return a Full can with the first element in the list satisfying the predicate" in { + first_?(List(1, 2, 3))((i: Int) => i > 0) must beEqualTo(Full(1)) + } + } + + "The ListHelpers first function" should { + "return an Empty can if the list is empty" in { + first((Nil: List[Int]))((i: Int) => Full(1)) must beEqualTo(Empty) + } + "return an Empty can if no element in the list returns a Full can when applied a function" in { + first(List(1, 2, 3))((i: Int) => Empty) must beEqualTo(Empty) + } + "return the first Full can returned by a function f over the list elements" in { + val f = (i: Int) => i >= 2 match {case true => Full(3) case false => Empty} + first(List(1, 2, 3))(f) must beEqualTo(Full(3)) + } + } + + "The ciGet function on Lists of pairs of string" should { + "return Empty if the list is Nil" in { + (Nil: List[(String, String)]).ciGet("") must beEqualTo(Empty) + } + "return Empty if no pair has the key as its first element" in { + List(("one", "1"), ("two", "2")).ciGet("three") must beEqualTo(Empty) + } + "return a Full can with the first second value of a pair matching the key" in { + List(("one", "1"), ("two", "2")).ciGet("one") must beEqualTo(Full("1")) + } + "return a Full can with the first second value of a pair matching the key case-insensitively" in { + List(("one", "1"), ("two", "2"), ("two", "3")).ciGet("two") must beEqualTo(Full("2")) + } + } + + "The ListHelpers enumToList and enumToStringList functions" should { + "convert a java enumeration to a List" in { + val v: java.util.Vector[Int] = new java.util.Vector[Int] + v.add(1); + v.add(2) + enumToList(v.elements) === List(1, 2) + } + "convert a java enumeration containing any kind of object to a List of Strings" in { + val v: java.util.Vector[Any] = new java.util.Vector[Any] + v.add(1); + v.add("hello") + enumToStringList(v.elements) === List("1", "hello") + } + } + + "The ListHelpers head function (headOr on a list object)" should { + "return the first element of a list" in { + List(1).headOr(2) === 1 + } + "return a default value if the list is empty" in { + head(Nil, 2) === 2 + } + "not evaluate the default valueif list is not empty" in { + head(List(1), {sys.error("stop"); 2}) === 1 + } + } + + "The ListHelpers listIf function" should { + "create a List containing an element if the predicate is true" in { + listIf(true)(1) === List(1) + } + "return an empty List if the predicate is false" in { + listIf(false)(1) === Nil + } + "not evaluate its argument if the predicate is false" in { + listIf(false)({sys.error("stop"); 1}) === Nil + } + } + + "The ListHelpers rotateList function (rotate method on a List object)" should { + "create a List of all the circular permutations of a given list" in { + List(1, 2, 3).rotate === List(List(1, 2, 3), List(2, 3, 1), List(3, 1, 2)) + } + } + + "The ListHelpers permuteList function (permute method on a List object)" should { + "create a List of all the permutations of a given list" in { + List(1, 2, 3).permute === + List(List(1, 2, 3), List(1, 3, 2), List(2, 3, 1), List(2, 1, 3), List(3, 1, 2), List(3, 2, 1)) + } + } + + "The ListHelpers permuteWithSublists function (permuteAll method on a List object)" should { + "create a List of all the permutations of a given list" in { + List(1, 2, 3).permuteAll === + List( + List(1, 2, 3), List(1, 3, 2), List(2, 3, 1), + List(2, 1, 3), List(3, 1, 2), List(3, 2, 1), + List(2, 3), List(3, 2), List(3, 1), + List(1, 3), List(1, 2), List(2, 1), + List(3), List(2), List(1)) + } + } + + "The ListHelpers" should { + "provide an or method on Lists returning the list itself if not empty or another list if it is empty" in { + List(1).or(List(2)) === List(1) + (Nil: List[Int]).or(List(2)) === List(2) + } + "provide a str method on Lists joining the toString value of all elements" in { + List("h", "e", "l", "l", "o").str === "hello" + } + "provide a comma method on Lists being an alias for mkString(\", \")" in { + List("hello", "world").comma === "hello, world" + } + "provide a join method on Lists being an alias for mkString" in { + List("hello", "world").join(", ") === "hello, world" + } + "provide a ? method return true iff the list is not empty" in { + List().? must beFalse + List(1).? must beTrue + } + "provide a replace method to replace one element of the list at a given position (0-based index)." + + " If the position is negative, the first element is replaced" in { + List(1, 2, 3).replace(1, 4) === List(1, 4, 3) + List(1, 2, 3).replace(4, 4) === List(1, 2, 3) + List(1, 2, 3).replace(-1, 4) === List(4, 2, 3) + } + } + +} diff --git a/core/util/src/test/scala-3/net/liftweb/util/PCDataXmlParserSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/PCDataXmlParserSpec.scala new file mode 100644 index 000000000..4e12fcc96 --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/PCDataXmlParserSpec.scala @@ -0,0 +1,83 @@ +/* + * Copyright 2010-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import org.specs2.matcher.XmlMatchers +import org.specs2.mutable.Specification + + +/** + * Systems under specification for PCDataXmlParser. + */ +class PCDataXmlParserSpec extends Specification with XmlMatchers { + "PCDataXmlParser Specification".title +val data1 = """ + + +dude + + +""" + +val data2 = """ + + +dude + + +""" + +val data3 = """ + + + +Meow + + + + + + + + + + + + + + +""" + + + "PCDataMarkupParser" should { + "Parse a document with whitespace" in { + PCDataXmlParser(data1).openOrThrowException("Test") must ==/ (dude) + } + + "Parse a document with doctype" in { + PCDataXmlParser(data2).openOrThrowException("Test") must ==/ (dude) + } + + "Parse a document with xml and doctype" in { + PCDataXmlParser(data3).openOrThrowException("Test").apply(0).label === "html" + } + + } + +} + diff --git a/core/util/src/test/scala-3/net/liftweb/util/PropsSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/PropsSpec.scala new file mode 100644 index 000000000..46018eeca --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/PropsSpec.scala @@ -0,0 +1,171 @@ +/* + * Copyright 2006-2012 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import java.io.ByteArrayInputStream + +import org.specs2.mutable.Specification +import org.specs2.specification.After + +import common._ +import Props.RunModes._ + +class PropsSpec extends Specification { + "Props Specification".title + + case class TestProps() extends Props + + "Props" should { + "Detect test mode correctly" in { + TestProps().testMode === true + } + + "Allow modification of whereToLook before run-mode is set" in { + val testProps = TestProps() + val originalWtl = testProps.whereToLook + + var wasCalled = false + testProps.whereToLook = () => { + wasCalled = true + + List( + ("test propsters", () => Full(new ByteArrayInputStream("test.prop=value".getBytes("UTF-8")))) + ) + } + + testProps.getInt("jetty.port") must beEqualTo(Empty) + testProps.get("test.prop") must beEqualTo(Full("value")) + wasCalled === true + } + + "Allow modification of run-mode properties before the run-mode is set" in { + val testProps = TestProps() + + val before = testProps.autoDetectRunModeFn.get + try { + testProps.runModeInitialised = false + testProps.autoDetectRunModeFn.allowModification === true + testProps.autoDetectRunModeFn.set(() => Test) === true + testProps.autoDetectRunModeFn.get must not(beEqualTo(before)) + } finally { + testProps.autoDetectRunModeFn.set(before) + testProps.runModeInitialised = true + } + } + + "Prohibit modification of run-mode properties when the run-mode is set" in { + val testProps = TestProps() + + val before = testProps.autoDetectRunModeFn.get + testProps.mode // initialize run mode + testProps.autoDetectRunModeFn.allowModification === false + testProps.autoDetectRunModeFn.set(() => Test) === false + testProps.autoDetectRunModeFn.get === before + } + + "Parse and cast to int" in { + TestProps().getInt("an.int") must beEqualTo(Full(42)) + } + + "Parse and cast to long" in { + TestProps().getLong("a.long") must beEqualTo(Full(9223372036854775807L)) + } + + "Parse and cast to boolean" in { + TestProps().getBool("a.boolean") must beEqualTo(Full(true)) + } + + "Prefer prepended properties to the test.default.props" in { + val testProps = TestProps() + + testProps.prependProvider(Map("jetty.port" -> "8080")) + val port = testProps.getInt("jetty.port") + + port must beEqualTo(Full(8080)) + } + + "Prefer prepended System.properties to the test.default.props" in { + val testProps = TestProps() + + System.setProperty("omniauth.baseurl1", "http://google.com") + + testProps.prependProvider(sys.props) + val baseurl = testProps.get("omniauth.baseurl1") + + baseurl must beEqualTo(Full("http://google.com")) + } + + "Read through to System.properties, correctly handling mutation" in { + val testProps = TestProps() + + System.setProperty("omniauth.baseurl2", "http://google.com") + testProps.prependProvider(sys.props) + System.setProperty("omniauth.baseurl2", "http://ebay.com") + val baseurl = testProps.get("omniauth.baseurl2") + + baseurl must beEqualTo(Full("http://ebay.com")) + } + + "Find properties in appended maps when not defined in test.default.props" in { + val testProps = TestProps() + + testProps.appendProvider(Map("new.prop" -> "new.value")) + val prop = testProps.get("new.prop") + + prop must beEqualTo(Full("new.value")) + } + + "Not interpolate values when no interpolator is given" in { + val port = TestProps().get("jetty.port") + + port must beEqualTo(Full("${PORT}")) + } + + "Interpolate values from the given interpolator" in { + val testProps = TestProps() + + testProps.appendInterpolationValues(Map("PORT" -> "8080")) + val port = testProps.getInt("jetty.port") + + port must beEqualTo(Full(8080)) + } + + "Interpolate multiple values in a string from the given interpolator" in { + val testProps = TestProps() + + testProps.appendInterpolationValues(Map("DB_HOST" -> "localhost", "DB_PORT" -> "3306")) + val url = testProps.get("db.url") + + url must beEqualTo(Full("jdbc:mysql://localhost:3306/MYDB")) + } + + "Find properties in append for require()" in { + val testProps = TestProps() + + testProps.appendProvider(Map("new.prop" -> "new.value")) + testProps.require("new.prop") === Nil + } + + "Find properties in prepend for require()" in { + val testProps = TestProps() + + testProps.prependProvider(Map("new.prop" -> "new.value")) + testProps.require("new.prop") === Nil + } + } +} diff --git a/core/util/src/test/scala-3/net/liftweb/util/ScheduleSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/ScheduleSpec.scala new file mode 100644 index 000000000..50aa99517 --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/ScheduleSpec.scala @@ -0,0 +1,84 @@ +/* + * Copyright 2007-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import org.specs2.mutable.Specification +import org.specs2.specification.BeforeEach +import org.specs2.execute.PendingUntilFixed + +import actor._ +import Helpers._ + + +/** + * Systems under specification for Lift Schedule. + */ +class ScheduleSpec extends Specification with PendingUntilFixed with PingedService with BeforeEach { + "Schedule Specification".title + + def before = step(Schedule.restart) + + "The Schedule object" should { + "provide a schedule method to ping an actor regularly" in { + Schedule.schedule(service, Alive, TimeSpan(10)) + service.pinged must eventually(beTrue) + } + "honor multiple restarts" in { + Schedule.restart + Schedule.restart + Schedule.restart + Schedule.schedule(service, Alive, TimeSpan(10)) + service.pinged must eventually(beTrue) + } + "honor shutdown followed by restart" in { + Schedule.shutdown() + Schedule.restart + Schedule.schedule(service, Alive, TimeSpan(10)) + service.pinged must eventually(beTrue) + } + "not honor multiple shutdowns" in { + Schedule.shutdown() + Schedule.shutdown() +// service.pinged must eventually(beFalse) + service.pinged must throwA[ActorPingException] + }.pendingUntilFixed + } + +} + + +trait PingedService { + case object Alive + val service = new Service + + class Service extends LiftActor { + @volatile var pinged = false + /* + def act() { + while (true) { + receive { + case Alive => {pinged = true; exit()} + } + } + } + */ + protected def messageHandler = { + case Alive => {pinged = true /*; exit() */} + } + } +} diff --git a/core/util/src/test/scala-3/net/liftweb/util/SecurityHelpersSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/SecurityHelpersSpec.scala new file mode 100644 index 000000000..058ab9d13 --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/SecurityHelpersSpec.scala @@ -0,0 +1,96 @@ +/* + * Copyright 2006-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package net.liftweb +package util + +import org.xml.sax.SAXParseException + +import org.specs2.mutable.Specification + +import SecurityHelpers._ + +/** + * Systems under specification for SecurityHelpers. + */ +class SecurityHelpersSpec extends Specification { + "SecurityHelpers Specification".title + + "Security Helpers" should { + "not parse XML with a DOCTYPE" in { + secureXML.loadString(""" + + ]> + &xxe;""" + ) must throwA[SAXParseException] + } + + "parse XML without a DOCTYPE" in { + secureXML.loadString(""" + & + """).toString === "&" + } + + "provide a randomLong method returning a random Long modulo a number" in { + randomLong(7L) must beLessThan(7L) + } + "provide a randomInt method returning a random Int modulo a number" in { + randomInt(7) must beLessThan(7) + } + "provide a shouldShow function always returning true only a given percentage of time, expressed as a Int between 0 and 100" in { + shouldShow(100) must beTrue + shouldShow(0) must beFalse + } + "provide a shouldShow function always returning true only a given percentage of time, expressed as a Double between 0 and 1.0" in { + shouldShow(1.0) must beTrue + shouldShow(0.0) must beFalse + } + + /* + "provide makeBlowfishKey, blowfishEncrypt, blowfishDecrypt functions to encrypt/decrypt Strings with Blowfish keys" in { + val key = makeBlowfishKey + val encrypted = blowfishEncrypt("hello world", key) + encrypted must not(beEqualTo("hello world")) + blowfishDecrypt(encrypted, key) === "hello world" + } + */ + + "provide a md5 function to create a md5 digest from a string" in { + md5("hello") === "XUFAKrxLKna5cZ2REBfFkg==" + md5("hello") must not(beEqualTo(md5("hell0"))) + } + "provide a hash function to create a SHA digest from a string" in { + hash("hello") === "qvTGHdzF6KLavt4PO0gs2a6pQ00=" + hash("hello") must not(beEqualTo(hash("hell0"))) + } + "provide a hash256 function to create a SHA-256 digest from a string" in { + hash256("hello") === "LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=" + hash256("hello") must not(beEqualTo(hash256("hell0"))) + } + "provide a hex encoded SHA hash function" in { + hexDigest("hello".getBytes) === "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d" + hexDigest("hello".getBytes) must not(beEqualTo(hexDigest("hell0".getBytes))) + } + "provide a hex encoded SHA-256 hash function" in { + hexDigest256("hello".getBytes) === "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" + hexDigest256("hello".getBytes) must not(beEqualTo(hexDigest256("hell0".getBytes))) + } + } + +} + diff --git a/core/util/src/test/scala-3/net/liftweb/util/SoftReferenceCacheSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/SoftReferenceCacheSpec.scala new file mode 100644 index 000000000..52670f05a --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/SoftReferenceCacheSpec.scala @@ -0,0 +1,27 @@ +package net.liftweb.util + +import org.specs2.mutable._ +import net.liftweb.common._ + +class SoftReferenceCacheSpec extends Specification { + + sequential + + object cache extends SoftReferenceCache[String, String](1) + + "SoftReferenceCache " should { + "Accept additions" in { + cache += ("test" -> "test") + cache.keys.size() === 1 + } + "Allow objects to be retrieved" in { + val cached = cache("test") + cached must beLike { case Full("test") => ok } + } + "Properly age out entries" in { + cache += ("test2" -> "test2") + cache("test") must beEqualTo(Empty) + } + } + +} diff --git a/core/util/src/test/scala-3/net/liftweb/util/StringHelpersSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/StringHelpersSpec.scala new file mode 100644 index 000000000..cc59507e7 --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/StringHelpersSpec.scala @@ -0,0 +1,351 @@ +/* + * Copyright 2007-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import org.specs2.mutable.Specification +import org.specs2.ScalaCheck +import org.scalacheck.Gen._ +import org.scalacheck.Prop.forAll + +import StringHelpers._ + + +/** + * Systems under specification for StringHelpers. + */ +class StringHelpersSpec extends Specification with ScalaCheck with StringGen { + "StringHelpers Specification".title + + "The snakify function" should { + "replace upper case with underscore" in { + snakify("MyCamelCase") === "my_camel_case" + snakify("CamelCase") === "camel_case" + snakify("Camel") === "camel" + snakify("MyCamel12Case") === "my_camel12_case" + snakify("CamelCase12") === "camel_case12" + snakify("Camel12") === "camel12" + } + + "not modify existing snake case strings" in { + snakify("my_snake_case") === "my_snake_case" + snakify("snake") === "snake" + } + + "handle abbeviations" in { + snakify("HTML") === "html" + snakify("HTMLEditor") === "html_editor" + snakify("EditorTOC") === "editor_toc" + snakify("HTMLEditorTOC") === "html_editor_toc" + + snakify("HTML5") === "html5" + snakify("HTML5Editor") === "html5_editor" + snakify("Editor2TOC") === "editor2_toc" + snakify("HTML5Editor2TOC") === "html5_editor2_toc" + } + } + + "The camelify function" should { + "CamelCase a name which is underscored, removing each underscore and capitalizing the next letter" in { + def previousCharacterIsUnderscore(name: String, i: Int) = i > 1 && name.charAt(i - 1) == '_' + def underscoresNumber(name: String, i: Int) = if (i == 0) 0 else name.substring(0, i).toList.count(_ == '_') + def correspondingIndexInCamelCase(name: String, i: Int) = i - underscoresNumber(name, i) + def correspondingCharInCamelCase(name: String, i: Int): Char = camelify(name).charAt(correspondingIndexInCamelCase(name, i)) + + val doesntContainUnderscores = forAll(underscoredStrings){ ((name: String) => !camelify(name).contains("_")) } + val isCamelCased = forAll(underscoredStrings) ((name: String) => { + name.forall(_ == '_') && camelify(name).isEmpty || + name.toList.zipWithIndex.forall { case (c, i) => + c == '_' || + correspondingIndexInCamelCase(name, i) == 0 && correspondingCharInCamelCase(name, i) == c.toUpper || + !previousCharacterIsUnderscore(name, i) && correspondingCharInCamelCase(name, i) == c || + previousCharacterIsUnderscore(name, i) && correspondingCharInCamelCase(name, i) == c.toUpper + } + }) + (doesntContainUnderscores && isCamelCased) + } + "return an empty string if given null" in { + camelify(null) === "" + } + "leave a CamelCased name untouched" in { + forAll(camelCasedStrings){ (name: String) => camelify(name) == name } + } + } + + "The camelifyMethod function" should { + "camelCase a name with the first letter being lower cased" in { + forAll(underscoredStrings){ + (name: String) => + camelify(name).isEmpty && camelifyMethod(name).isEmpty || + camelifyMethod(name).toList.head.isLower && camelify(name) == camelifyMethod(name).capitalize + } + } + } + + "SuperListString" should { + """allow "foo" / "bar" """ in { + ("foo" / "bar") === List("foo", "bar") + } + + """allow "foo" / "bar" / "baz" """ in { + ("foo" / "bar" / "baz") === List("foo", "bar", "baz") + } + } + + "The StringHelpers processString function" should { + "replace groups found in a string surrounded by <%= ... %> by their corresponding value in a map" in { + processString("<%=hello%>", Map("hello" -> "bonjour")) === "bonjour" + } + "replace groups found in several strings surrounded by <%= ... %> by their corresponding value in a map" in { + processString("<%=hello%> <%=world%>", Map("hello" -> "bonjour", "world" -> "monde")) === "bonjour monde" + } + "not replace the group if it starts with %" in { + processString("<%=%hello%>", Map("hello" -> "bonjour")) === "<%=%hello%>" + } + "throw an exception if no correspondance is found" in { + processString("<%=hello%>", Map("hallo" -> "bonjour")) must throwA[Exception] + } + } + + "The StringHelpers capify function" should { + "capitalize a word" in { + capify("hello") === "Hello" + } + "capitalize the first letters of 2 words" in { + capify("hello world") === "Hello World" + } + "capitalize the first letters of 2 words separated by an underscore" in { + capify("hello_world") === "Hello_World" + } + "capitalize the second character in a word if the first is a digit" in { + capify("hello 1world") === "Hello 1World" + } + "capitalize the third character in a word if the first 2 are digits" in { + capify("hello 12world") === "Hello 12World" + } + "suppress symbols placed in front of words and capitalize the words" in { + capify("@hello $world") === "Hello World" + } + "remove letters in the word if there are more than 250 digits in front of it" in { + capify("1" * 250 + "hello") === "1" * 250 + } + } + "The StringHelpers clean function" should { + "return an empty string if the input is null" in { + clean(null) === "" + } + "remove every character which is not a-zA-Z0-9_" in { + clean(" He@llo Wor+ld_!") === "HelloWorld_" + } + } + "The StringHelpers randomString" should { + "return an empty string if size is 0" in { + randomString(0) === "" + } + /* + "return a string of size n" in { + randomString(10).toSeq must haveSize(10) + } + */ + + "return only capital letters and digits" in { + randomString(10) must beMatching("[A-Z0-9]*") + } + } + "The StringHelpers escChar" should { + "return the unicode value of a character as a string starting with \\u" in { + escChar('{') === "\\u007b" + escChar('A') === "\\u0041" + } + } + "The StringHelpers splitColonPair" should { + "split a string separated by a dot in 2 parts" in { + splitColonPair("a.b", "1", "2") === ("a", "b") + } + "split a string separated by a dot in 2 trimmed parts" in { + splitColonPair(" a . b ", "1", "2") === ("a", "b") + } + "split a string separated by a column in 2 parts" in { + splitColonPair("a:b", "1", "2") === ("a", "b") + } + "split a string according to the dot prioritarily" in { + splitColonPair("a.b:c", "1", "2") === ("a", "b:c") + } + "split a string according to first the dot if there are several ones and removing everything after the next dot" in { + splitColonPair("a.b.c", "1", "2") === ("a", "b") + splitColonPair("a.b.c.d", "1", "2") === ("a", "b") + } + "use a default value for the second part if there is nothing to split" in { + splitColonPair("abc", "1", "2") === ("abc", "2") + } + "use null for the first part if the input string is null" in { + splitColonPair(null, "1", "2") === ("", "2") + } + "use a default value for the first part if the input string is empty" in { + splitColonPair("", "1", "2") === ("1", "2") + } + } + "The StringHelpers parseNumber function" should { + "return a Long corresponding to the value of a string with digits" in { + parseNumber("1234") === 1234L + } + "return a positive Long value if the string starts with +" in { + parseNumber("+1234") === 1234L + } + "return a positive Long value if the string starts with -" in { + parseNumber("-1234") === -1234L + } + "return 0 if the string is null" in { + parseNumber(null) === 0L + } + "return 0 if the string is empty" in { + parseNumber("") === 0L + } + "return 0 if the string can't be parsed" in { + parseNumber("string") === 0L + } + } + "The SuperString class roboSplit method" should { + "split a string according to a separator" in { + "hello".roboSplit("ll") === List("he", "o") + } + "split a string according to a separator - 2" in { + "hAeAlAlAo".roboSplit("A") === List("h", "e", "l", "l", "o") + } + "split a string into trimmed parts" in { + "hello . world ".roboSplit("\\.") === List("hello", "world") + } + "split a string into trimmed parts whose length is > 0" in { + "hello . . world ".roboSplit("\\.") === List("hello", "world") + } + "split a string according to a separator. The separator is always interpreted as a regular expression" in { + "hello".roboSplit("(l)+") === List("he", "o") + } + "return a list containing the string if the separator is not found" in { + "hello".roboSplit("tt") === List("hello") + } + "return an empty list if the string is null" in { + (null: String).roboSplit("a") === List() + } + "return a list containing the string if the string is empty" in { + "".roboSplit("a") === List() + } + } + + "The SuperString class charSplit method" should { + "split a string according to a separator" in { + "hello".charSplit('l') === List("he", "o") + } + "split a string according to a separator - 2" in { + "hAeAlAlAo".charSplit('A') === List("h", "e", "l", "l", "o") + } + "split a string" in { + "hello . world ".charSplit('.') === List("hello ", " world ") + } + "split a string into parts" in { + "hello .. world ".charSplit('.') === List("hello ", " world ") + } + "return an empty list if the string is null" in { + (null: String).charSplit('a') === List() + } + "return a list containing the string if the string is empty" in { + "".charSplit('a') === List() + } + } + + + "The SuperString class splitAt method" should { + "split a string according to a separator and return a List containing a pair with the 2 parts" in { + stringToSuper("hello").splitAt("ll") === List(("he", "o")) + } + "split a string according to a separator and return an empty List if no separator is found" in { + stringToSuper("hello").splitAt("tt") === Nil + } + "split a string according to a separator and return an empty List if the string is null" in { + stringToSuper(null: String).splitAt("tt") === Nil + } + } + "The SuperString class encJs method" should { + "encode a string replacing backslash with its unicode value" in { + "\\hello".encJs === "\"\\u005chello\"" + } + "encode a string replacing the quote character with its unicode value" in { + "h'ello".encJs === "\"h\\u0027ello\"" + } + "encode a string adding a quote before and a quote after the string" in { + "hello".encJs === "\"hello\"" + } + "encode a string replacing non-ASCII characters by their unicode value" in { + "ni\u00f1a".encJs === "\"ni\\u00f1a\"" + } + "return the string \"null\" if the input string is null" in { + (null: String).encJs === "null" + } + } + "The SuperString class commafy method" should { + "add a comma before the last 3 characters of the input string" in { + "hello".commafy === "he,llo" + } + "add nothing if the input string is less than 4 characters" in { + "hel".commafy === "hel" + } + "return null if the input string is null" in { + (null: String).commafy must beNull + } + } + "The blankForNull method" should { + "return the empty String for a given null String" in { + StringHelpers.blankForNull(null) === "" + } + "return the given String for a given not-null String" in { + StringHelpers.blankForNull("x") === "x" + } + } + "The emptyForBlank method" should { + import net.liftweb.common._ + "return Empty for a given null String" in { + StringHelpers.emptyForBlank(null) must beEqualTo(Empty) + } + "return Empty for a given a blank String" in { + StringHelpers.emptyForBlank("") must beEqualTo(Empty) + } + "return Empty for a String of spaces" in { + StringHelpers.emptyForBlank(" ") must beEqualTo(Empty) + } + "return the trim'ed String for a given not-null String" in { + StringHelpers.emptyForBlank(" x ") must beEqualTo(Full("x")) + } + } +} + + +trait StringGen { + val underscoredStrings = + for { + length <- choose(0, 4) + s <- listOfN(length, frequency((3, alphaChar), (1, oneOf(List('_'))))) + } yield s.mkString + + val camelCasedStrings = + for { + length <- choose(0, 4) + firstLetter <- alphaNumChar.map(_.toUpper) + string <- listOfN(length, frequency((3, alphaNumChar.map(_.toLower)), + (1, alphaNumChar.map(_.toUpper)))) + } yield (firstLetter :: string).mkString +} + diff --git a/core/util/src/test/scala-3/net/liftweb/util/TimeHelpersSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/TimeHelpersSpec.scala new file mode 100644 index 000000000..0a91fd332 --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/TimeHelpersSpec.scala @@ -0,0 +1,249 @@ +/* + * Copyright 2006-2015 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import java.util.{Calendar, Date, TimeZone} + +import net.liftweb.common._ +import net.liftweb.util.TimeHelpers._ +import org.joda.time.{Period, DateTimeZone, DateTime} +import org.scalacheck.Gen._ +import org.scalacheck.Prop._ +import org.specs2.ScalaCheck +import org.specs2.execute.AsResult +import org.specs2.matcher.MatcherImplicits +import org.specs2.mutable.Specification +import org.specs2.specification.Around + +/** + * Systems under specification for TimeHelpers. + */ +class TimeHelpersSpec extends Specification with ScalaCheck with TimeAmountsGen { + "TimeHelpers Specification".title + + "A TimeSpan" can { + "be created from a number of milliseconds" in forAllTimeZones { + TimeSpan(3000) === TimeSpan(3 * 1000) + } + "be created from a number of seconds" in forAllTimeZones { + 3.seconds === TimeSpan(3 * 1000) + } + "be created from a number of minutes" in forAllTimeZones { + 3.minutes === TimeSpan(3 * 60 * 1000) + } + "be created from a number of hours" in forAllTimeZones { + 3.hours === TimeSpan(3 * 60 * 60 * 1000) + } + "be created from a number of days" in forAllTimeZones { + 3.days === TimeSpan(3 * 24 * 60 * 60 * 1000) + } + "be created from a number of weeks" in forAllTimeZones { + 3.weeks === TimeSpan(3 * 7 * 24 * 60 * 60 * 1000) + } + "be created from a number of months" in forAllTimeZones { + 3.months must beEqualTo(Period.months(3)) + } + "be created from a number of years" in forAllTimeZones { + 3.years must beEqualTo(Period.years(3)) + } + "be converted implicitly to a date starting from the epoch time" in forAllTimeZones { + 3.seconds.after(new Date(0)) must beTrue + } + "be converted to a date starting from the epoch time, using the date method" in forAllTimeZones { + 3.seconds.after(new Date(0)) must beTrue + } + "be compared to another TimeSpan" in forAllTimeZones { + 3.seconds === 3.seconds + 3.seconds must not(beEqualTo(2.seconds)) + } + "be compared to another object" in forAllTimeZones { + 3.seconds must not(beEqualTo("string")) + } + } + + "A TimeSpan" should { + "return a new TimeSpan representing the sum of the 2 times when added with another TimeSpan" in forAllTimeZones { + 3.seconds + 3.seconds === 6.seconds + } + "return a new TimeSpan representing the difference of the 2 times when substracted with another TimeSpan" in forAllTimeZones { + 3.seconds - 4.seconds === (-1).seconds + } + "have a toString method returning the relevant number of weeks, days, hours, minutes, seconds, millis" in forAllTimeZones { + val conversionIsOk = forAll(timeAmounts)((t: TimeAmounts) => { val (timeSpanToString, timeSpanAmounts) = t + timeSpanAmounts forall { case (amount, unit) => + amount >= 1 && + timeSpanToString.contains(amount.toString) || true } + }) + val timeSpanStringIsPluralized = forAll(timeAmounts)((t: TimeAmounts) => { val (timeSpanToString, timeSpanAmounts) = t + timeSpanAmounts forall { case (amount, unit) => + amount > 1 && timeSpanToString.contains(unit + "s") || + amount == 1 && timeSpanToString.contains(unit) || + amount == 0 && !timeSpanToString.contains(unit) + } + }) + conversionIsOk && timeSpanStringIsPluralized + } + } + + "the TimeHelpers" should { + "provide a 'seconds' function transforming a number of seconds into millis" in forAllTimeZones { + seconds(3) === 3 * 1000 + } + "provide a 'minutes' function transforming a number of minutes into millis" in forAllTimeZones { + minutes(3) === 3 * 60 * 1000 + } + "provide a 'hours' function transforming a number of hours into milliss" in forAllTimeZones { + hours(3) === 3 * 60 * 60 * 1000 + } + "provide a 'days' function transforming a number of days into millis" in forAllTimeZones { + days(3) === 3 * 24 * 60 * 60 * 1000 + } + "provide a 'weeks' function transforming a number of weeks into millis" in forAllTimeZones { + weeks(3) === 3 * 7 * 24 * 60 * 60 * 1000 + } + "provide a noTime function on Date objects to transform a date into a date at the same day but at 00:00" in forAllTimeZones { + hourFormat(now.noTime) === "00:00:00" + } + + "provide a day function returning the day of month corresponding to a given date (relative to UTC)" in forAllTimeZones { + day(today.setTimezone(utc).setDay(3).getTime) === 3 + } + "provide a month function returning the month corresponding to a given date" in forAllTimeZones { + month(today.setTimezone(utc).setMonth(4).getTime) === 4 + } + "provide a year function returning the year corresponding to a given date" in forAllTimeZones { + year(today.setTimezone(utc).setYear(2008).getTime) === 2008 + } + "provide a millisToDays function returning the number of days since the epoch time" in forAllTimeZones { + millisToDays(new Date(0).getTime) === 0 + millisToDays(today.setYear(1970).setMonth(0).setDay(1).getTime.getTime) === 0 // the epoch time + // on the 3rd day after the epoch time, 2 days are passed + millisToDays(today.setTimezone(utc).setYear(1970).setMonth(0).setDay(3).getTime.getTime) === 2 + } + "provide a daysSinceEpoch function returning the number of days since the epoch time" in forAllTimeZones { + daysSinceEpoch === millisToDays(now.getTime) + } + "provide a time function creating a new Date object from a number of millis" in forAllTimeZones { + time(1000) === new Date(1000) + } + "provide a calcTime function returning the time taken to evaluate a block in millis and the block's result" in forAllTimeZones { + val (time, result) = calcTime((1 to 10).reduceLeft[Int](_ + _)) + time.toInt must beCloseTo(0, 1000) // it should take less than 1 second! + result === 55 + } + + "provide a hourFormat function to format the time of a date object" in forAllTimeZones { + hourFormat(Calendar.getInstance(utc).noTime.getTime) === "00:00:00" + } + + "provide a formattedDateNow function to format todays date" in forAllTimeZones { + formattedDateNow must beMatching("\\d\\d\\d\\d/\\d\\d/\\d\\d") + } + "provide a formattedTimeNow function to format now's time with the TimeZone" in forAllTimeZones { + val regex = "\\d\\d:\\d\\d (....?.?|GMT((\\+|\\-)\\d\\d:\\d\\d)?)" + "10:00 CEST" must beMatching(regex) + "10:00 GMT+02:00" must beMatching(regex) + "10:00 GMT" must beMatching(regex) + "10:00 XXX" must beMatching(regex) + formattedTimeNow must beMatching(regex) + } + + "provide a parseInternetDate function to parse a string formatted using the internet format" in forAllTimeZones { + parseInternetDate(internetDateFormatter.format(now)).getTime.toLong must beCloseTo(now.getTime.toLong, 1000L) + } + "provide a parseInternetDate function returning new Date(0) if the input date cant be parsed" in forAllTimeZones { + parseInternetDate("unparsable") === new Date(0) + } + "provide a toInternetDate function formatting a date to the internet format" in forAllTimeZones { + toInternetDate(now) must beMatching("..., \\d* ... \\d\\d\\d\\d \\d\\d:\\d\\d:\\d\\d .*") + } + "provide a toDate returning a Full(date) from many kinds of objects" in forAllTimeZones { + val d = now + List(null, Nil, None, Failure("", Empty, Empty)) forall { toDate(_) == Empty } must beTrue + List(Full(d), Some(d), List(d)) forall { toDate(_) == Full(d) } must beTrue + + toDate(internetDateFormatter.format(d)) must beLike { + case Full(converted) => + converted.getTime.toLong must beCloseTo(d.getTime.toLong, 1000L) + } + } + } + + "The Calendar class" should { + "have a setDay method setting the day of month and returning the updated Calendar" in forAllTimeZones { + day(today.setTimezone(utc).setDay(1).getTime) === 1 + } + "have a setMonth method setting the month and returning the updated Calendar" in forAllTimeZones { + month(today.setTimezone(utc).setMonth(0).getTime) === 0 + } + "have a setYear method setting the year and returning the updated Calendar" in forAllTimeZones { + year(today.setTimezone(utc).setYear(2008).getTime) === 2008 + } + "have a setTimezone method to setting the time zone and returning the updated Calendar" in forAllTimeZones { + today.setTimezone(utc).getTimeZone === utc + } + "have a noTime method to setting the time to 00:00:00 and returning the updated Calendar" in forAllTimeZones { + hourFormat(today.noTime.getTime) === "00:00:00" + } + } +} + +object forAllTimeZones extends Around { + import MatcherImplicits._ + + override def around[T: AsResult](f: => T) = synchronized { + import scala.jdk.CollectionConverters._ + // setDefault is on static context so tests should be sequenced + // some timezones for java (used in formatters) and for Joda (other computations) has other offset + val commonJavaAndJodaTimeZones = (TimeZone.getAvailableIDs.toSet & DateTimeZone.getAvailableIDs.asScala.toSet).filter { timeZoneId => + TimeZone.getTimeZone(timeZoneId).getOffset(millis) == DateTimeZone.forID(timeZoneId).getOffset(millis) + } + val tzBefore = TimeZone.getDefault + val dtzBefore = DateTimeZone.getDefault + try { + forall(commonJavaAndJodaTimeZones) { timeZoneId => + TimeZone.setDefault(TimeZone.getTimeZone(timeZoneId)) + DateTimeZone.setDefault(DateTimeZone.forID(timeZoneId)) + f + } + } finally { + TimeZone.setDefault(tzBefore) + DateTimeZone.setDefault(dtzBefore) + } + } +} + + +trait TimeAmountsGen { + + type TimeAmounts = (String, List[(Int, String)]) + + val timeAmounts = + for { + w <- choose(0, 2) + d <- choose(0, 6) + h <- choose(0, 23) + m <- choose(0, 59) + s <- choose(0, 59) + ml <- choose(0, 999) + } + yield ( + TimeSpan(weeks(w) + days(d) + hours(h) + minutes(m) + seconds(s) + ml).toString, + (w, "week") :: (d, "day") :: (h, "hour") :: (m, "minute") :: (s, "second") :: (ml, "milli") :: Nil + ) +} diff --git a/core/util/src/test/scala-3/net/liftweb/util/ToHeadSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/ToHeadSpec.scala new file mode 100644 index 000000000..7f64091f3 --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/ToHeadSpec.scala @@ -0,0 +1,146 @@ +/* + * Copyright 2007-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import org.specs2.matcher.XmlMatchers +import org.specs2.mutable.Specification + +import common._ +import ControlHelpers._ +import HeadHelper._ +import Helpers.secureXML + +/** + * Systems under specification for ToHead. + */ +class ToHeadSpec extends Specification with XmlMatchers { + "ToHead Specification".title + + "lift merger" should { + "merge /html/body//head into existing /html/head section" in { + val susfiles = for { + act <- tryo(getClass.getResource("ToHeadSpec.actual1.html")).filter(_ ne null) + exp <- tryo(getClass.getResource("ToHeadSpec.expected1.html")).filter(_ ne null) + } yield (act, exp) + + susfiles must beLike { + case Full(sus) => + val actual = secureXML.load(sus._1) + val expected = secureXML.load(sus._2) + mergeToHtmlHead(actual).toString.replaceAll("\\s", "") === + (expected.toString.replaceAll("\\s", "")) + } + } + + "merge from real example" in { + val susfiles = for { + act <- tryo(getClass.getResource("ToHeadSpec.actual2.html")).filter(_ ne null) + exp <- tryo(getClass.getResource("ToHeadSpec.expected2.html")).filter(_ ne null) + } yield (act, exp) + + susfiles must beLike { + case Full(sus) => + val actual = secureXML.load(sus._1) + val expected = secureXML.load(sus._2) + mergeToHtmlHead(actual) must ==/(expected) + } + } + + "merge into a new head if not previously exist" in { + val susfiles = for { + act <- tryo(getClass.getResource("ToHeadSpec.actual3.html")).filter(_ ne null) + exp <- tryo(getClass.getResource("ToHeadSpec.expected3.html")).filter(_ ne null) + } yield (act, exp) + + susfiles must beLike { + case Full(sus) => + val actual = secureXML.load(sus._1) + val expected = secureXML.load(sus._2) + mergeToHtmlHead(actual).toString.replaceAll("\\s", "") === + (expected.toString.replaceAll("\\s", "")) + } + } + } + + /* + "lift head cleaner" should { + "remove duplicate title tag" >> { + val actual = (hellohello2hello3) + + val expected = (hello) + + HeadHelper.cleanHead(actual) must beEqualToIgnoringSpace(expected) + } + "remove script tag with same id as previous script tag" >> { + val invariant = () + HeadHelper.cleanHead(invariant) must beEqualToIgnoringSpace(invariant) + + val actual = () + val expected = () + HeadHelper.cleanHead(actual) must beEqualToIgnoringSpace(expected) + } + "remove script tag with src attributes if src attributes are equals to previous script" >> { + val actual = () + val expected = () + HeadHelper.cleanHead(actual) must beEqualToIgnoringSpace(expected) + + val actual2 = () + val expected2 = () + HeadHelper.cleanHead(actual2) must beEqualToIgnoringSpace(expected2) + } + "remove script tag if content are equals to previous script (need to trim each line ?)" >> { + val actual = () + val expected = () + HeadHelper.cleanHead(actual) must beEqualToIgnoringSpace(expected) + } + "remove link to css with same id as previous link tag" >> { + val invariant = () + HeadHelper.cleanHead(invariant) must beEqualToIgnoringSpace(invariant) + + val actual = () + val expected = () + HeadHelper.cleanHead(actual) must beEqualToIgnoringSpace(expected) + } + "remove link tag with href attributes if href attributes are equals to previous link" >> { + val invariant = () + HeadHelper.cleanHead(invariant) must beEqualToIgnoringSpace(invariant) + + val actual = () + val expected = () + HeadHelper.cleanHead(actual) must beEqualToIgnoringSpace(expected) + } + "remove style tag with same id as previous style tag" >> { + val invariant = () + HeadHelper.cleanHead(invariant) must beEqualToIgnoringSpace(invariant) + + val actual = () + val expected = () + HeadHelper.cleanHead(actual) must beEqualToIgnoringSpace(expected) + } + "remove style tag if content are equals to previous style (need to trim each line ?)" >> { + val invariant = () + HeadHelper.cleanHead(invariant) must beEqualToIgnoringSpace(invariant) + + val actual = () + val expected = () + HeadHelper.cleanHead(actual) must beEqualToIgnoringSpace(expected) + } + } +*/ +} + diff --git a/core/util/src/test/scala-3/net/liftweb/util/VCardParserSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/VCardParserSpec.scala new file mode 100644 index 000000000..88f94590c --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/VCardParserSpec.scala @@ -0,0 +1,143 @@ +/* + * Copyright 2008-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import org.specs2.matcher.XmlMatchers +import org.specs2.mutable.Specification + + +/** + * Systems under specification for VCardParser. + */ +class VCardParserSpec extends Specification with XmlMatchers { + "VCardParser Specification".title + + val vcard = + """BEGIN:VCARD + |VERSION:2.1 + |N:Gump;Forrest + |FN:Forrest Gump + |ORG:Bubba Gump Shrimp Co. + |TITLE:Shrimp Man + |TEL;WORK;VOICE:(111) 555-1212 + |TEL;HOME;VOICE:(404) 555-1212 + |END:VCARD""".stripMargin + + "VCard" should { + "parse a basic VCard (2.1) correctly" in { + val list = VCardParser.parse(vcard) + list must beLike { + case Left(l) => { + import VCardParser._ + l === + List( + VCardEntry(VCardKey("BEGIN", Nil), List("VCARD")), + VCardEntry(VCardKey("VERSION", Nil), List("2.1")), + VCardEntry(VCardKey("N", Nil), List("Gump", "Forrest")), + VCardEntry(VCardKey("FN", Nil), List("Forrest Gump")), + VCardEntry(VCardKey("ORG", Nil), List("Bubba Gump Shrimp Co.")), + VCardEntry(VCardKey("TITLE", Nil), List("Shrimp Man")), + VCardEntry(VCardKey("TEL", List(("WORK", ""), ("VOICE", ""))), List("(111) 555-1212")), + VCardEntry(VCardKey("TEL", List(("HOME", ""), ("VOICE", ""))), List("(404) 555-1212")), + VCardEntry(VCardKey("END", Nil), List("VCARD"))) + } + } + } + + "parse a basic Apple VCard (3.0) correctly" in { + + val appleIOS9DavidTaylorVCard3 = + """BEGIN:VCARD + |VERSION:3.0 + |PRODID:-//Apple Inc.//iPhone OS 9.3//EN + |N:Taylor;David;;; + |FN: David Taylor + |TEL;type=HOME;type=VOICE;type=pref:555-610-6679 + |item1.ADR;type=HOME;type=pref:;;1747 Steuart Street;Tiburon;CA;94920;USA + |item1.X-ABADR:us + |BDAY:1998-06-15 + |END:VCARD""".stripMargin + + val list = VCardParser.parse(appleIOS9DavidTaylorVCard3) + list must beLike { + case Left(l) => { + import VCardParser._ + l === + List( + VCardEntry(VCardKey("BEGIN", Nil), List("VCARD")), + VCardEntry(VCardKey("VERSION", Nil), List("3.0")), + VCardEntry(VCardKey("PRODID", Nil),List("-//Apple Inc.//iPhone OS 9.3//EN")), + VCardEntry(VCardKey("N", Nil), List("Taylor", "David", "", "", "")), + VCardEntry(VCardKey("FN", Nil), List(" David Taylor")), + VCardEntry(VCardKey("TEL", List(("type", "HOME"), ("type", "VOICE"), ("type", "pref"))), List("555-610-6679")), + VCardEntry(VCardKey("ADR", List(("type","HOME"), ("type","pref"))),List("", "", "1747 Steuart Street", "Tiburon", "CA", "94920", "USA")), + VCardEntry(VCardKey("X-ABADR",Nil),List("us")), + VCardEntry(VCardKey("BDAY",Nil),List("1998-06-15")), + VCardEntry(VCardKey("END", Nil), List("VCARD"))) + } + } + + } + + "parse a more complex Apple VCard (3.0) correctly" in { + + val appleIOS9JohnAppleseedVCard3 = + """BEGIN:VCARD + |VERSION:3.0 + |PRODID:-//Apple Inc.//iPhone OS 9.3//EN + |N:Appleseed;John;;; + |FN: John Appleseed + |EMAIL;type=INTERNET;type=WORK;type=pref:John-Appleseed@mac.com + |TEL;type=CELL;type=VOICE;type=pref:888-555-5512 + |TEL;type=HOME;type=VOICE:888-555-1212 + |item1.ADR;type=WORK;type=pref:;;3494 Kuhl Avenue;Atlanta;GA;30303;USA + |item1.X-ABADR:ca + |item2.ADR;type=HOME:;;1234 Laurel Street;Atlanta;GA;30303;USA + |item2.X-ABADR:us + |BDAY:1980-06-22 + |END:VCARD""".stripMargin + + val list = VCardParser.parse(appleIOS9JohnAppleseedVCard3) + list must beLike { + case Left(l) => { + import VCardParser._ + l === + List( + VCardEntry(VCardKey("BEGIN", Nil), List("VCARD")), + VCardEntry(VCardKey("VERSION", Nil), List("3.0")), + VCardEntry(VCardKey("PRODID", Nil),List("-//Apple Inc.//iPhone OS 9.3//EN")), + VCardEntry(VCardKey("N", Nil), List("Appleseed", "John", "", "", "")), + VCardEntry(VCardKey("FN", Nil), List(" John Appleseed")), + VCardEntry(VCardKey("EMAIL", List(("type","INTERNET"), ("type","WORK"), ("type","pref"))),List("John-Appleseed@mac.com")), + VCardEntry(VCardKey("TEL", List(("type", "CELL"), ("type", "VOICE"), ("type", "pref"))), List("888-555-5512")), + VCardEntry(VCardKey("TEL", List(("type", "HOME"), ("type", "VOICE"))), List("888-555-1212")), + VCardEntry(VCardKey("ADR", List(("type","WORK"), ("type","pref"))),List("", "", "3494 Kuhl Avenue", "Atlanta", "GA", "30303", "USA")), + VCardEntry(VCardKey("X-ABADR",Nil),List("ca")), + VCardEntry(VCardKey("ADR", List(("type","HOME"))),List("", "", "1234 Laurel Street", "Atlanta", "GA", "30303", "USA")), + VCardEntry(VCardKey("X-ABADR",Nil),List("us")), + VCardEntry(VCardKey("BDAY",Nil),List("1980-06-22")), + VCardEntry(VCardKey("END", Nil), List("VCARD"))) + } + } + + } + + + } + +} diff --git a/core/util/src/test/scala-3/net/liftweb/util/XmlParserSpec.scala b/core/util/src/test/scala-3/net/liftweb/util/XmlParserSpec.scala new file mode 100644 index 000000000..8299bee26 --- /dev/null +++ b/core/util/src/test/scala-3/net/liftweb/util/XmlParserSpec.scala @@ -0,0 +1,103 @@ +/* + * Copyright 2007-2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.liftweb +package util + +import java.io.ByteArrayInputStream + +import scala.xml.{Text, Unparsed} + +import org.specs2.matcher.XmlMatchers +import org.specs2.mutable.Specification + + +/** + * Systems under specification for XmlParser, specifically PCDataMarkupParser. + */ +class XmlParserSpec extends Specification with XmlMatchers { + "Xml Parser Specification".title + + "Multiple attributes with same name, but different namespace" should { + "parse correctly" >> { + val actual = + + + + + val expected = + + + + + val bis = new ByteArrayInputStream(actual.toString.getBytes("UTF-8")) + val parsed = PCDataXmlParser(bis).openOrThrowException("Test") + parsed must ==/(expected) + } + + } + + "XML can contain PCData" in { + val data = { + PCData("Hello Yak") + } + + val str = AltXML.toXML(data, false, true) + + str.indexOf("{ + Unparsed("Hello & goodbye > + + val str = AltXML.toXML(data, false, true) + + str.indexOf("Hello & goodbye > + { + '\u0085' + }{ + Text("hello \u0000 \u0085 \u0080") + }{ + "hello \u0000 \u0003 \u0085 \u0080" + }{ + '\u0003' + } + + + val str = AltXML.toXML(data, false, true) + + def cntIllegal(in: Char): Int = in match { + case '\u0085' => 1 + case c if (c >= '\u007f' && c <= '\u0095') => 1 + case '\n' => 0 + case '\r' => 0 + case '\t' => 0 + case c if c < ' ' => 1 + case _ => 0 + } + + str.toList.foldLeft(0)((a, b) => a + cntIllegal(b)) === 0 + } + +} + diff --git a/web/testkit/src/test/scala/net/liftweb/http/testing/MockHttpRequestSpec.scala b/web/testkit/src/test/scala-2.13/net/liftweb/http/testing/MockHttpRequestSpec.scala similarity index 100% rename from web/testkit/src/test/scala/net/liftweb/http/testing/MockHttpRequestSpec.scala rename to web/testkit/src/test/scala-2.13/net/liftweb/http/testing/MockHttpRequestSpec.scala diff --git a/web/testkit/src/test/scala-3/net/liftweb/http/testing/MockHttpRequestSpec.scala b/web/testkit/src/test/scala-3/net/liftweb/http/testing/MockHttpRequestSpec.scala new file mode 100644 index 000000000..ed4da5cb8 --- /dev/null +++ b/web/testkit/src/test/scala-3/net/liftweb/http/testing/MockHttpRequestSpec.scala @@ -0,0 +1,143 @@ +/* + * Copyright 2011 WorldWide Conferencing, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package net.liftweb +package mocks + +import org.specs2.mutable.Specification + +import org.json4s.JsonDSL._ + +/** + * System under specification for MockHttpRequest. + */ +class MockHttpRequestSpec extends Specification { + "MockHttpRequest Specification".title + + val IF_MODIFIED_HEADER = "If-Modified-Since" + val TEST_URL = "https://foo.com/test/this/page?a=b&b=a&a=c" + val TEST_URL_BLANK_PARAMETER = "https://foo.com/test/this/page?a=b&b=a&c=&d" + val TEST_URL_BLANK_PARAMETER_SERIALIZED = "https://foo.com/test/this/page?a=b&b=a&c=&d=" + + "MockHttpRequest" should { + + "properly deconstruct from a URL" in { + val testRequest = new MockHttpServletRequest(TEST_URL, "/test") + + testRequest.getScheme === "https" + testRequest.isSecure === true + testRequest.getServerName === "foo.com" + testRequest.getContextPath === "/test" + testRequest.getRequestURI === "/test/this/page" + testRequest.getRequestURL.toString === TEST_URL + testRequest.getQueryString === "a=b&b=a&a=c" + testRequest.getParameterValues("a").toList === List("b","c") + testRequest.getParameter("b") === "a" + } + + "parse parameters with empty values" in { + val testRequest = new MockHttpServletRequest(TEST_URL_BLANK_PARAMETER, "/test") + + testRequest.getScheme === "https" + testRequest.isSecure === true + testRequest.getServerName === "foo.com" + testRequest.getContextPath === "/test" + testRequest.getRequestURI === "/test/this/page" + testRequest.getRequestURL.toString === TEST_URL_BLANK_PARAMETER_SERIALIZED + testRequest.getQueryString === "a=b&b=a&c=&d=" + testRequest.getParameter("c") === "" + testRequest.getParameter("d") === "" + } + + "correctly add and parse a date header" in { + val testRequest = new MockHttpServletRequest(TEST_URL, "/test") + + val epoch = 241000 // (milliseconds not included in RFC 1123) + + testRequest.setDateHeader(IF_MODIFIED_HEADER, epoch) + + testRequest.getDateHeader(IF_MODIFIED_HEADER) === epoch + } + + "throw an IllegalArgumentException for an invalid date header" in { + val testRequest = new MockHttpServletRequest(TEST_URL, "/test") + + testRequest.headers += IF_MODIFIED_HEADER -> List("this is not a valid date") + + testRequest.getDateHeader(IF_MODIFIED_HEADER) must throwA[IllegalArgumentException] + } + + "throw an IllegalArgumentException for an invalid context path" in { + (new MockHttpServletRequest(TEST_URL, "foo")) must throwA[IllegalArgumentException] + (new MockHttpServletRequest(TEST_URL, "/foo/")) must throwA[IllegalArgumentException] + } + + "throw an IllegalArgumentException for an invalid query string" in { + val testRequest = new MockHttpServletRequest(TEST_URL, "/test") + + (testRequest.queryString ="this=a&&that=b") must throwA[IllegalArgumentException] + } + + + "properly set a default content type for JSON" in { + val testRequest = new MockHttpServletRequest(TEST_URL, "/test") + + testRequest.body_=("name" -> "joe") + + testRequest.contentType === "application/json" + } + + "properly set a user-specificed content type for JSON" in { + val testRequest = new MockHttpServletRequest(TEST_URL, "/test") + + testRequest.body_=(("name" -> "joe"), "text/json") + + testRequest.contentType === "text/json" + } + + "properly set a default content type for XML" in { + val testRequest = new MockHttpServletRequest(TEST_URL, "/test") + + testRequest.body_=() + + testRequest.contentType === "text/xml" + } + + "properly set a user-specificed content type for XML" in { + val testRequest = new MockHttpServletRequest(TEST_URL, "/test") + + testRequest.body_=(, "application/xml") + + testRequest.contentType === "application/xml" + } + + "properly set a default content type for a String" in { + val testRequest = new MockHttpServletRequest(TEST_URL, "/test") + + testRequest.body_=("test") + + testRequest.contentType === "text/plain" + } + + "properly set a user-specificed content type for a String" in { + val testRequest = new MockHttpServletRequest(TEST_URL, "/test") + + testRequest.body_=("test", "text/csv") + + testRequest.contentType === "text/csv" + } + + } +}