diff --git a/commons/util/src/main/scala/net/shrine/problem/DashboardProblemDatabase.scala b/commons/util/src/main/scala/net/shrine/problem/DashboardProblemDatabase.scala index 347a53f52..540a02c66 100644 --- a/commons/util/src/main/scala/net/shrine/problem/DashboardProblemDatabase.scala +++ b/commons/util/src/main/scala/net/shrine/problem/DashboardProblemDatabase.scala @@ -1,209 +1,193 @@ package net.shrine.problem import java.util.concurrent.TimeoutException import javax.sql.DataSource import com.typesafe.config.Config import net.shrine.slick.{CouldNotRunDbIoActionException, TestableDataSourceCreator} import slick.dbio.SuccessAction import slick.driver.JdbcProfile import slick.jdbc.meta.MTable import scala.collection.mutable import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration._ import scala.concurrent.{Await, Future} import scala.util.control.NonFatal import scala.xml.XML /** * Problems database object, defines the PROBLEMS table schema and related queries, * as well as all interactions with the database. * @author ty * @since 07/16 */ object Problems { val config:Config = ProblemConfigSource.config.getConfig("shrine.dashboard.database") val slickProfileClassName = config.getString("slickProfileClassName") // TODO: Can we not pay this 2 second cost here? val slickProfile:JdbcProfile = ProblemConfigSource.objectForName(slickProfileClassName) import slickProfile.api._ val dataSource: DataSource = TestableDataSourceCreator.dataSource(config) lazy val db = { val db = Database.forDataSource(dataSource) val createTables: String = "createTablesOnStart" if (config.hasPath(createTables) && config.getBoolean(createTables)) { val duration = FiniteDuration(3, SECONDS) Await.ready(db.run(IOActions.createIfNotExists), duration) val testValues: String = "createTestValuesOnStart" if (config.hasPath(testValues) && config.getBoolean(testValues)) { def dumb(id: Int) = ProblemDigest(s"codec($id)", s"stamp($id)", s"sum($id)", s"desc($id)",
{id}
, id) val dummyValues: Seq[ProblemDigest] = Seq(0, 1, 2, 3, 4, 5, 6).map(dumb) Await.ready(db.run(Queries ++= dummyValues), duration) } } db } /** * The Problems Table. This is the table schema. */ class ProblemsT(tag: Tag) extends Table[ProblemDigest](tag, Queries.tableName) { def id = column[Int]("id", O.PrimaryKey, O.AutoInc) def codec = column[String]("codec") def stampText = column[String]("stampText") def summary = column[String]("summary") def description = column[String]("description") def xml = column[String]("detailsXml", O.SqlType("Clob")) def epoch= column[Long]("epoch") // projection between table row and problem digest def * = (id, codec, stampText, summary, description, xml, epoch) <> (rowToProblem, problemToRow) def idx = index("idx_epoch", epoch, unique=false) /** * Converts a table row into a ProblemDigest. * @param args the table row, represented as a five-tuple string * @return the corresponding ProblemDigest */ def rowToProblem(args: (Int, String, String, String, String, String, Long)): ProblemDigest = args match { case (id, codec, stampText, summary, description, detailsXml, epoch) => ProblemDigest(codec, stampText, summary, description, XML.loadString(detailsXml), epoch) } /** * Converts a ProblemDigest into an Option of a table row. For now there is no failure * condition, ie a ProblemDigest can always be a table row, but this is a place for * possible future error handling * @param problem the ProblemDigest to convert * @return an Option of a table row. */ def problemToRow(problem: ProblemDigest): Option[(Int, String, String, String, String, String, Long)] = problem match { case ProblemDigest(codec, stampText, summary, description, detailsXml, epoch) => // 7 is ignored on insert and replaced with an auto incremented id Some((7, codec, stampText, summary, description, detailsXml.toString, epoch)) } } /** * Queries related to the Problems table. */ object Queries extends TableQuery(new ProblemsT(_)) { /** * The table name */ val tableName = "problems" /** * Equivalent to Select * from Problems; */ val selectAll = this /** * Selects all the details xml sorted by the problem's time stamp. */ val selectDetails = this.map(_.xml) /** * Sorts the problems in descending order */ val descending = this.sortBy(_.epoch.desc) /** * Selects the last N problems, after the offset */ def lastNProblems(n: Int, offset: Int = 0) = this.descending.drop(offset).take(n) } /** * DBIO Actions. These are pre-defined IO actions that may be useful. * Using it to centralize the location of DBIOs. */ object IOActions { val problems = Queries val tableExists = MTable.getTables(problems.tableName).map(_.nonEmpty) val createIfNotExists = tableExists.flatMap( if (_) SuccessAction(NoOperation) else problems.schema.create) val dropIfExists = tableExists.flatMap( if (_) problems.schema.drop else SuccessAction(NoOperation)) val resetTable = createIfNotExists >> problems.selectAll.delete val selectAll = problems.result def sizeAndProblemDigest(n: Int, offset: Int = 0) = problems.lastNProblems(n, offset).result.zip(problems.size.result) def findIndexOfDate(date: Long) = (problems.size - problems.filter(_.epoch <= date).size).result - def ++=(sequence: Seq[ProblemDigest]) = problems ++= sequence } /** * Entry point for interacting with the database. Runs IO actions. */ object DatabaseConnector { val IO = IOActions /** * Executes a series of IO actions as a single transactions */ def executeTransaction(actions: DBIOAction[_, NoStream, _]*): Future[Unit] = { db.run(DBIO.seq(actions:_*).transactionally) } /** * Executes a series of IO actions as a single transaction, synchronous */ def executeTransactionBlocking(actions: DBIOAction[_, NoStream, _]*)(implicit timeout: Duration): Unit = { try { Await.ready(this.executeTransaction(actions: _*), timeout) } catch { // TODO: Handle this better case tx:TimeoutException => throw CouldNotRunDbIoActionException(Problems.dataSource, tx) case NonFatal(x) => throw CouldNotRunDbIoActionException(Problems.dataSource, x) } } /** * Straight copy of db.run */ def run[R](dbio: DBIOAction[R, NoStream, _]): Future[R] = { db.run(dbio) } /** * Synchronized copy of db.run */ def runBlocking[R](dbio: DBIOAction[R, NoStream, _])(implicit timeout: Duration = new FiniteDuration(15, SECONDS)): R = { try { Await.result(this.run(dbio), timeout) } catch { case tx:TimeoutException => throw CouldNotRunDbIoActionException(Problems.dataSource, tx) case NonFatal(x) => throw CouldNotRunDbIoActionException(Problems.dataSource, x) } } - def insertProblem(problem: ProblemDigest)(implicit timeout: Duration = new FiniteDuration(15, SECONDS)) = { + def insertProblem(problem: ProblemDigest, timeout: Duration = new FiniteDuration(15, SECONDS)) = { runBlocking(Queries += problem)(timeout) } } } // For SuccessAction, just a no_op. -case object NoOperation - -object ProblemConsumer { - val problems:mutable.Stack[ProblemDigest] = mutable.Stack() - - def put(p:ProblemDigest) { - synchronized(problems.push(p)) - } - - def batchInsert() = { - Problems.DatabaseConnector.runBlocking(Problems.IOActions ++= problems.elems) - problems.clear() - } - - -} \ No newline at end of file +case object NoOperation \ No newline at end of file diff --git a/commons/util/src/main/scala/net/shrine/problem/Problem.scala b/commons/util/src/main/scala/net/shrine/problem/Problem.scala index 673a8a6ba..b9ac30111 100644 --- a/commons/util/src/main/scala/net/shrine/problem/Problem.scala +++ b/commons/util/src/main/scala/net/shrine/problem/Problem.scala @@ -1,258 +1,225 @@ package net.shrine.problem import java.net.InetAddress -import java.text.SimpleDateFormat import java.util.Date import java.util.concurrent.Executors import net.shrine.log.Loggable import net.shrine.serialization.{XmlMarshaller, XmlUnmarshaller} import scala.concurrent.{ExecutionContext, Future, Promise} import scala.xml.{Elem, Node, NodeSeq} /** * Describes what information we have about a problem at the site in code where we discover it. * * @author david * @since 8/6/15 */ trait Problem { def summary:String def problemName = getClass.getName def throwable:Option[Throwable] = None def stamp:Stamp def description:String def exceptionXml(exception:Option[Throwable]): Option[Elem] = { exception.map{x => {x.getClass.getName} {x.getMessage} {x.getStackTrace.map(line => {line})}{exceptionXml(Option(x.getCause)).getOrElse("")} }} def throwableDetail: Option[Elem] = exceptionXml(throwable) def detailsXml: NodeSeq = NodeSeq.fromSeq(
{throwableDetail.getOrElse("")}
) - def toDigest:ProblemDigest = ProblemDigest(problemName,stamp.pretty,summary,description,detailsXml, stamp.time) - - def createAndLog:Problem = { - if (!ProblemConfigSource.turnOffConnector) - Problems.DatabaseConnector.insertProblem(toDigest) - this - } + def toDigest:ProblemDigest = ProblemDigest(problemName,stamp.pretty,summary,description,detailsXml,stamp.time) /** - * The hack that will get us through until onCreate in 2.13 - * The problem is that we want to insert the createAndLog call after a problem is constructed. - * The only way to currently do that is with DelayedInit... which is just no. - * Thus, the hack (that's still better than DelayedInit) is to watch the summary, description, - * and throwable field, and call createAndLog once we know they've been initialized. The one - * caveat is that creating throwable is optional, so in the worst case we wait 25 ms then decide - * it's not gettting initialized. - * @return + * Temporary replacement for onCreate, which will be released come Scala 2.13 + * TODO: remove when Scala 2.13 releases */ - def logAfterInitialization:Future[Problem] = { - import MyExecutionContext.ioThreadPool + def hackToHandleAfterInitialization(handler:ProblemHandler):Future[Unit] = { + import ProblemExecutionContext.ioThreadPool Future { var continue = true - while (continue) { - Thread.sleep(5) - try { - continue = synchronized(summary) == null || synchronized(description) == null - } catch { - case a:UninitializedFieldError => continue = true - } - } - var count = 0 - while (count < 5 && synchronized(throwable).isEmpty) { - Thread.sleep(5) - count += 1 - } - continue = true - var p: Option[Problem] = None while (continue) { try { - p = Some(createAndLog) + handler.handleProblem(this) continue = false } catch { - case a:UninitializedFieldError => + case un:UninitializedFieldError => Thread.sleep(5) continue = true } } - p.get + Unit } } } + case class ProblemDigest(codec: String, stampText: String, summary: String, description: String, detailsXml: NodeSeq, epoch: Long) extends XmlMarshaller { override def toXml: Node = { {codec} {stampText} {summary} {description} {epoch} {detailsXml} } /** * Ignores detailXml. equals with scala.xml is impossible. See http://www.scala-lang.org/api/2.10.3/index.html#scala.xml.Equality$ */ override def equals(other: Any): Boolean = other match { case that: ProblemDigest => (that canEqual this) && codec == that.codec && stampText == that.stampText && summary == that.summary && description == that.description && epoch == that.epoch case _ => false } /** * Ignores detailXml */ override def hashCode: Int = { val prime = 67 codec.hashCode + prime * (stampText.hashCode + prime *(summary.hashCode + prime * (description.hashCode + prime * epoch.hashCode()))) } } object ProblemDigest extends XmlUnmarshaller[ProblemDigest] with Loggable { override def fromXml(xml: NodeSeq): ProblemDigest = { val problemNode = xml \ "problem" require(problemNode.nonEmpty,s"No problem tag in $xml") def extractText(tagName:String) = (problemNode \ tagName).text val codec = extractText("codec") val stampText = extractText("stamp") val summary = extractText("summary") val description = extractText("description") val detailsXml: NodeSeq = problemNode \ "details" val epoch = try { extractText("epoch").toLong } catch { case nx:NumberFormatException => error(s"While parsing xml representing a ProblemDigest, the epoch could not be parsed into a long", nx) 0 } ProblemDigest(codec,stampText,summary,description,detailsXml,epoch) } } case class Stamp(host:InetAddress,time:Long,source:ProblemSources.ProblemSource) { def pretty = s"${new Date(time)} on ${host.getHostName} ${source.pretty}" } object Stamp { //TODO: val dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")? //TODO: Currently the stamp text is locale specific, which can change depending on the jre/computer running it... - def apply(source:ProblemSources.ProblemSource, timer: => Long): Stamp = Stamp(InetAddress.getLocalHost, timer,source) + def apply(source:ProblemSources.ProblemSource, timer: => Long): Stamp = Stamp(InetAddress.getLocalHost, timer, source) } -/** - * An abstract problem to enable easy creation of Problems. Note that when overriding fields, - * you should only use def or lazy val, and not val. - * See: http://stackoverflow.com/questions/15346600/field-inside-object-which-extends-app-trait-is-set-to-null-why-is-that-so - * @param source - */ abstract class AbstractProblem(source:ProblemSources.ProblemSource) extends Problem { def timer = System.currentTimeMillis override val stamp = Stamp(source, timer) - logAfterInitialization + hackToHandleAfterInitialization(DatabaseProblemHandler) } trait ProblemHandler { def handleProblem(problem:Problem) } /** * An example problem handler */ object LoggingProblemHandler extends ProblemHandler with Loggable { override def handleProblem(problem: Problem): Unit = { problem.throwable.fold(error(problem.toString))(throwable => error(problem.toString,throwable) ) } } -//object DatabaseProblemhandler extends ProblemHandler { -// override def handleProblem(problem: Problem): Unit = { -// Problems.DatabaseConnector.insertProblem(problem.toDigest) -// } -// -//} +object DatabaseProblemHandler extends ProblemHandler { + override def handleProblem(problem: Problem): Unit = { + if (!ProblemConfigSource.turnOffConnector) + Problems.DatabaseConnector.insertProblem(problem.toDigest) + } +} object ProblemSources{ sealed trait ProblemSource { def pretty = getClass.getSimpleName.dropRight(1) } case object Adapter extends ProblemSource case object Hub extends ProblemSource case object Qep extends ProblemSource case object Dsa extends ProblemSource case object Unknown extends ProblemSource def problemSources = Set(Adapter,Hub,Qep,Dsa,Unknown) } case class ProblemNotYetEncoded(internalSummary:String,t:Option[Throwable] = None) extends AbstractProblem(ProblemSources.Unknown){ override val summary = "An unanticipated problem encountered." override val throwable = { val rx = t.fold(new IllegalStateException(s"$summary"))( new IllegalStateException(s"$summary",_) ) rx.fillInStackTrace() Some(rx) } val reportedAtStackTrace = new IllegalStateException("Capture reporting stack trace.") override val description = "This problem is not yet classified in Shrine source code. Please report the details to the Shrine dev team." override val detailsXml: NodeSeq = NodeSeq.fromSeq(
{internalSummary} {throwableDetail.getOrElse("")}
) } object ProblemNotYetEncoded { def apply(summary:String,x:Throwable):ProblemNotYetEncoded = ProblemNotYetEncoded(summary,Some(x)) } -object MyExecutionContext { +object ProblemExecutionContext { private val processes = Runtime.getRuntime.availableProcessors() private val factor = 3 private val threads = processes * factor implicit val ioThreadPool: ExecutionContext = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(threads)) } \ No newline at end of file diff --git a/commons/util/src/test/scala/net/shrine/problem/DashboardProblemDatabaseTest.scala b/commons/util/src/test/scala/net/shrine/problem/DashboardProblemDatabaseTest.scala index df03a47f1..ead103d82 100644 --- a/commons/util/src/test/scala/net/shrine/problem/DashboardProblemDatabaseTest.scala +++ b/commons/util/src/test/scala/net/shrine/problem/DashboardProblemDatabaseTest.scala @@ -1,92 +1,92 @@ package net.shrine.problem import org.scalatest.concurrent.ScalaFutures import org.scalatest.{BeforeAndAfter, FlatSpec, Matchers} import slick.driver.H2Driver.api._ import scala.concurrent.duration._ /** * Test creation, insertion, querying, and deletion of ProblemDigest values into an * in-memory H2 database. Demonstrates proof of concept mapping of ProblemDigest * case class into a database. */ class DashboardProblemDatabaseTest extends FlatSpec with BeforeAndAfter with ScalaFutures with Matchers { implicit val timeout = 10.seconds val connector = Problems.DatabaseConnector val IO = connector.IO val problemDigests = Seq( ProblemDigest("MJPG", "01:01:01", "summary here", "description here" ,
uh not sure
, 2), ProblemDigest("wewu", "01:02:01", "coffee spill", "coffee everywhere" ,
He chose decaf
, 1), ProblemDigest("wuwu", "02:01:01", "squirrel" , "chewed all the cables",
Like ALL of them
, 0), ProblemDigest("code", "10:01:02", "such summary", "such description" ,
Wow
, 3)) before { connector.runBlocking(IO.dropIfExists >> IO.tableExists) shouldBe false connector.runBlocking(IO.createIfNotExists >> IO.tableExists) shouldBe true connector.runBlocking(IO.createIfNotExists) shouldBe NoOperation connector.runBlocking(IO.selectAll) shouldBe empty } after { connector.runBlocking(IO.tableExists) shouldBe true connector.runBlocking(IO.dropIfExists >> IO.tableExists) shouldBe false connector.runBlocking(IO.dropIfExists) shouldBe NoOperation } "The Database" should "Connect without any problems" in { // Insert the suppliers and ProblemDigests connector.executeTransactionBlocking(IO.problems ++= problemDigests) // Test that they are all in the table var * = connector.runBlocking(IO.selectAll) * should contain theSameElementsAs problemDigests * should have length problemDigests.length // Reset the table connector.runBlocking(IO.resetTable >> IO.selectAll) shouldBe empty // Run the test again connector.executeTransactionBlocking(IO.problems += problemDigests.head, IO.problems += problemDigests(1), IO.problems += problemDigests(2), IO.problems += problemDigests(3)) // Test that they are all in the table * = connector.runBlocking(IO.selectAll) * should contain theSameElementsAs problemDigests * should have length problemDigests.length // Test that the simple select and filter queries work val filtered = connector.runBlocking(IO.problems.filter(_.codec === "code").map(_.description).result) filtered should have length 1 filtered.head shouldBe problemDigests(3).description // This also tests that our conversion from xml to strings works val xml = connector.runBlocking(IO.problems.map(_.xml).result) xml should have length problemDigests.length xml should contain theSameElementsAs problemDigests.map(_.detailsXml.toString()) val result = connector.runBlocking(IO.sizeAndProblemDigest(2)) result._1 should have length 2 result._2 shouldBe problemDigests.length result._1.head shouldBe problemDigests(3) result._1(1) shouldBe problemDigests.head val resultOverLength = connector.runBlocking(IO.sizeAndProblemDigest(10)) resultOverLength._1 should have length 4 resultOverLength._1 should contain theSameElementsAs problemDigests connector.runBlocking(IO.problems.size.result) shouldBe problemDigests.size val testProblem = ProblemDatabaseTestProblem(ProblemSources.Unknown) - Thread.sleep(50) + Thread.sleep(15) connector.runBlocking(IO.problems.size.result) shouldBe problemDigests.size + 1 } } case class ProblemDatabaseTestProblem(source: ProblemSources.ProblemSource) extends AbstractProblem(source: ProblemSources.ProblemSource) { override def summary: String = "This is a test problem! No user should ever see this." override def description: String = "Wow, this is a nice looking problem. I mean really, just look at it." } \ No newline at end of file diff --git a/integration/src/test/scala/net/shrine/integration/ProblemCreation.scala b/integration/src/test/scala/net/shrine/integration/ProblemCreation.scala index eb108f052..a129c0082 100644 --- a/integration/src/test/scala/net/shrine/integration/ProblemCreation.scala +++ b/integration/src/test/scala/net/shrine/integration/ProblemCreation.scala @@ -1,131 +1,137 @@ package net.shrine.integration import java.net.{URL, URLConnection, URLStreamHandler, URLStreamHandlerFactory} import java.sql.SQLException import net.shrine.adapter.AbstractQueryRetrievalTestCase.BogusRequest import net.shrine.adapter._ import net.shrine.adapter.client.{CouldNotParseXmlFromAdapter, HttpErrorCodeFromAdapter} import net.shrine.adapter.components.QueryNotInDatabase import net.shrine.adapter.dao.BotDetectedException import net.shrine.adapter.service.{CouldNotVerifySignature, UnknownRequestType} import net.shrine.aggregation._ import net.shrine.authentication.{NotAuthenticatedException, NotAuthenticatedProblem} import net.shrine.authorization.{CouldNotInterpretResponseFromPmCell, CouldNotReachPmCell, ErrorStatusFromDataStewardApp, MissingRequiredRoles} import net.shrine.broadcaster.CouldNotParseResultsException import net.shrine.client.HttpResponse import net.shrine.hms.authorization.HMSNotAuthenticatedProblem import net.shrine.problem._ import net.shrine.protocol.QueryResult.StatusType import net.shrine.protocol.query.QueryDefinition import net.shrine.protocol._ import net.shrine.qep.queries.QepDatabaseProblem import org.scalatest.{FlatSpec, Matchers} import slick.driver.H2Driver.api._ import scala.concurrent.duration.FiniteDuration import scala.xml.{NodeSeq, SAXParseException} /** * Created by ty on 8/29/16. + * Tests that we can successfully log every problem in the codebase. + * Due to the time nature of logging problems, we create the succeed + * early loop at the bottom to give every problem a chance at being + * created on time. */ class ProblemCreation extends FlatSpec with Matchers { val throwable = new IllegalArgumentException("Boo") val credential: Credential = Credential("string", isToken = true) val authInfo = AuthenticationInfo("domain", "username", credential) val authExecption = AdapterLockoutException(authInfo, "url") val bogus: ShrineRequest = new BogusRequest val seconds = new FiniteDuration(10, java.util.concurrent.TimeUnit.SECONDS) val queryDefinition = QueryDefinition("string", None) val runQueryRequest = new RunQueryRequest("id", seconds, authInfo, 10, None, None, Set(), queryDefinition) val saxxException: SAXParseException = new SAXParseException("hey", null) val xmlResponse: String = "" val someXml =
Heyo!
val teapot: HttpResponse = HttpResponse(418, "body") val nodeId: NodeId = NodeId("name") val couldNotParseException: CouldNotParseResultsException = CouldNotParseResultsException(5, "url", "body", throwable) val queryResult = QueryResult(5l, 5l, None, 5l, None, None, None, StatusType("name", isDone=false), None) val readyQueryResponse = ReadQueryResultResponse(5l, queryResult) val foo: NonI2b2ableResponse = new Foo() - "Problems" should "all be successfully created" in { + "Problems" should "all be successfully created and logged" in { URL.setURLStreamHandlerFactory(new BogusUrlFactory) - val problemSize = () => Problems.DatabaseConnector.runBlocking(Problems.Queries.size.result) + val db = Problems.DatabaseConnector + val queries = Problems.Queries + val problemSize = () => db.runBlocking(queries.size.result) problemSize() shouldBe 0 val problems: Seq[AbstractProblem] = Seq( HttpErrorCodeFromAdapter("url", 5, "string response body"), CouldNotParseXmlFromAdapter("url", 6, "responseBody", saxxException), QueryNotFound(10l), QueryResultNotAvailable(10l), CouldNotRetrieveQueryFromCrc(10l, throwable), AdapterLockout(authInfo, authExecption), CrcCouldNotBeInvoked("crcUrl", bogus, CrcInvocationException("url", bogus, throwable)), AdapterMappingProblem(AdapterMappingException(runQueryRequest, "message", throwable)), AdapterDatabaseProblem(new SQLException("reason", "state", 5)), BotDetected(BotDetectedException("domain", "user", 5l, 5l, 5l)), CannotParseXmlFromCrc(saxxException, xmlResponse), ExceptionWhileLoadingCrcResponse(throwable, xmlResponse), ErrorFromCrcBreakdown(ErrorFromCrcException("message")), CannotInterpretCrcBreakdownXml(MissingCrCXmlResultException(someXml, throwable)), QueryNotInDatabase(I2b2AdminReadQueryDefinitionRequest("project", seconds, authInfo, 5l)), // Difficult to test, as I would have to pull it out of the defining code, // Change it from an object to a case class, and make sure that I'm not breaking // Any breakdown logic by doing so. // BreakdownFailure, CouldNotVerifySignature(BroadcastMessage(5l, authInfo, bogus)), UnknownRequestType(RequestType("apple", None)), NotAuthenticatedProblem(NotAuthenticatedException("string", "string", "message", throwable)), MissingRequiredRoles("pid", Set(), authInfo), CouldNotReachPmCell("url", authInfo, throwable), CouldNotInterpretResponseFromPmCell("url", authInfo, teapot, throwable), ErrorStatusFromDataStewardApp(spray.http.HttpResponse(), new URL("bogus", "host", 5, "file")), CouldNotConnectToAdapter(nodeId, throwable), TimedOutWithAdapter(nodeId), CouldNotParseResultsProblem(couldNotParseException), HttpErrorResponseProblem(couldNotParseException), NoValidResponsesToAggregate(), // Difficult to test since I can't grab private value: // InvalidResultProblem(Invalid(None, "error")), HMSNotAuthenticatedProblem(authInfo), ErrorStatusFromCrc(None, ""), QepDatabaseProblem(throwable), ProblemNotYetEncoded("summary", Some(throwable)), NoI2b2AnalogExists(foo.getClass), TestProblem() ) var count = 0 // give it up to 1 second to finish - while(problemSize() != problems.length && count < 10) { - Thread.sleep(100) + while(problemSize() != problems.length && count < 20) { + Thread.sleep(50) count+=1 } problemSize() shouldBe problems.length - Problems.DatabaseConnector.runBlocking(Problems.Queries.result) should contain theSameElementsAs problems.map(_.toDigest) + db.runBlocking(queries.result) should contain theSameElementsAs problems.map(_.toDigest) } } class Foo extends ShrineResponse with NonI2b2ableResponse { override def toXml: NodeSeq =
Yay
} class BogusUrlFactory extends URLStreamHandlerFactory { override def createURLStreamHandler(protocol: String): URLStreamHandler = new BogusUrlHandler } class BogusUrlHandler extends URLStreamHandler { override def openConnection(u: URL): URLConnection = new BogusUrlConnection(u) } class BogusUrlConnection(u: URL) extends URLConnection(u) { override def connect(): Unit = {} } \ No newline at end of file