From 27b2f38fcde09bcefb69b23db1777086bd85b1f5 Mon Sep 17 00:00:00 2001 From: Thanh Le Date: Thu, 9 May 2024 21:01:58 +0700 Subject: [PATCH] Implement compatibility test --- .github/workflows/test.yml | 10 +++ .scalafmt.conf | 4 ++ build.sbt | 12 +++- .../app/src/main/scala/app.rersources.scala | 2 +- .../client/src/main/scala/PlayClient.scala | 14 +++- modules/core/src/main/scala/ESClient.scala | 21 +++--- modules/e2e/src/test/scala/CompatSuite.scala | 70 +++++++++++++++++++ 7 files changed, 116 insertions(+), 17 deletions(-) create mode 100644 modules/e2e/src/test/scala/CompatSuite.scala diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d640c4c0..8a2350b3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,3 +22,13 @@ jobs: distribution: temurin java-version: 21 - run: sbt scalafmtCheckAll + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 21 + - run: sbt test diff --git a/.scalafmt.conf b/.scalafmt.conf index e87d759c..92e14834 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -27,4 +27,8 @@ fileOverride { runner.dialect = scala3 } + "glob:**/modules/e2e/**" { + runner.dialect = scala3 + } + } diff --git a/build.sbt b/build.sbt index dde8e4f4..1751a24f 100644 --- a/build.sbt +++ b/build.sbt @@ -102,6 +102,7 @@ lazy val app = (project in file("modules/app")) http4sEmberClient, cirisCore, cirisHtt4s, + log4Cats, logbackX ), Compile / run / fork := true @@ -109,7 +110,16 @@ lazy val app = (project in file("modules/app")) .enablePlugins(JavaAppPackaging) .dependsOn(api, core) +val e2e = (project in file("modules/e2e")) + .settings( + commonSettings, + libraryDependencies ++= Seq( + weaver + ) + ) + .dependsOn(client, app) + lazy val root = project .in(file(".")) .settings(publish := {}, publish / skip := true) - .aggregate(core, play, api, app, client) + .aggregate(core, play, api, app, client, e2e) diff --git a/modules/app/src/main/scala/app.rersources.scala b/modules/app/src/main/scala/app.rersources.scala index 256e5002..25faf0da 100644 --- a/modules/app/src/main/scala/app.rersources.scala +++ b/modules/app/src/main/scala/app.rersources.scala @@ -7,7 +7,7 @@ import com.sksamuel.elastic4s.http.JavaClient import com.sksamuel.elastic4s.cats.effect.instances.* import com.sksamuel.elastic4s.{ ElasticClient, ElasticProperties } -class AppResources private (val esClient: ESClient[IO]) +class AppResources(val esClient: ESClient[IO]) object AppResources: diff --git a/modules/client/src/main/scala/PlayClient.scala b/modules/client/src/main/scala/PlayClient.scala index f8656525..dcc4f7ca 100644 --- a/modules/client/src/main/scala/PlayClient.scala +++ b/modules/client/src/main/scala/PlayClient.scala @@ -19,10 +19,10 @@ class PlayClient(client: StandaloneWSClient, baseUrl: String)(using ExecutionCon import implicits.given override def count(query: Query): Future[CountResponse] = - request(s"$baseUrl/count", query) + request(s"$baseUrl/count", SearchInput(query)) override def search(query: Query, from: Int, size: Int): Future[SearchResponse] = - request(s"$baseUrl/search/{from}/{int}", query) + request(s"$baseUrl/search/$from/$size", SearchInput(query)) private def request[D: Schema, R: Schema](url: String, data: D): Future[R] = client @@ -30,10 +30,18 @@ class PlayClient(client: StandaloneWSClient, baseUrl: String)(using ExecutionCon .post(data) .flatMap: case res if res.status == 200 => Future(res.body[R]) - case res => Future.failed(Exception(s"$url ${res.status}")) + case res => Future.failed(Exception(s"$url ${res.status} ${res.body}")) + +final case class SearchInput(query: Query) object implicits: + import smithy4s.schema.Schema.struct + + given Schema[SearchInput] = struct( + Query.schema.required[SearchInput]("query", _.query) + )(SearchInput.apply) + given [A](using JsonCodec[A]): BodyWritable[A] = BodyWritable(a => InMemoryBody(ByteString.fromArrayUnsafe(writeToArray(a))), "application/json") diff --git a/modules/core/src/main/scala/ESClient.scala b/modules/core/src/main/scala/ESClient.scala index cb81403b..97d07d36 100644 --- a/modules/core/src/main/scala/ESClient.scala +++ b/modules/core/src/main/scala/ESClient.scala @@ -3,9 +3,6 @@ package lila.search import com.sksamuel.elastic4s.ElasticDsl.{ RichFuture => _, _ } import com.sksamuel.elastic4s.fields.ElasticField import com.sksamuel.elastic4s.{ ElasticClient, ElasticDsl, Index => ESIndex, Response } -import com.sksamuel.elastic4s.requests.indexes.IndexResponse -import com.sksamuel.elastic4s.requests.delete.DeleteResponse -import com.sksamuel.elastic4s.requests.bulk.BulkResponse import com.sksamuel.elastic4s.{ Executor, Functor } import cats.syntax.all.* import cats.MonadThrow @@ -20,10 +17,10 @@ trait ESClient[F[_]] { def search[A](index: Index, query: A, from: From, size: Size)(implicit q: Queryable[A]): F[SearchResponse] def count[A](index: Index, query: A)(implicit q: Queryable[A]): F[CountResponse] - def store(index: Index, id: Id, obj: JsonObject): F[Response[IndexResponse]] + def store(index: Index, id: Id, obj: JsonObject): F[Unit] def storeBulk(index: Index, objs: List[(String, JsonObject)]): F[Unit] - def deleteOne(index: Index, id: Id): F[Response[DeleteResponse]] - def deleteMany(index: Index, ids: List[Id]): F[Response[BulkResponse]] + def deleteOne(index: Index, id: Id): F[Unit] + def deleteMany(index: Index, ids: List[Id]): F[Unit] def putMapping(index: Index, fields: Seq[ElasticField]): F[Unit] def refreshIndex(index: Index): F[Unit] @@ -51,8 +48,8 @@ object ESClient { .flatMap(toResult) .map(CountResponse.apply) - def store(index: Index, id: Id, obj: JsonObject): F[Response[IndexResponse]] = - client.execute(indexInto(index.name).source(obj.json).id(id.value)) + def store(index: Index, id: Id, obj: JsonObject): F[Unit] = + client.execute(indexInto(index.name).source(obj.json).id(id.value)).void def storeBulk(index: Index, objs: List[(String, JsonObject)]): F[Unit] = if (objs.isEmpty) ().pure[F] @@ -65,17 +62,17 @@ object ESClient { } }.void - def deleteOne(index: Index, id: Id): F[Response[DeleteResponse]] = - client.execute(deleteById(index.toES, id.value)) + def deleteOne(index: Index, id: Id): F[Unit] = + client.execute(deleteById(index.toES, id.value)).void - def deleteMany(index: Index, ids: List[Id]): F[Response[BulkResponse]] = + def deleteMany(index: Index, ids: List[Id]): F[Unit] = client.execute { ElasticDsl.bulk { ids.map { id => deleteById(index.toES, id.value) } } - } + }.void def putMapping(index: Index, fields: Seq[ElasticField]): F[Unit] = dropIndex(index) >> client.execute { diff --git a/modules/e2e/src/test/scala/CompatSuite.scala b/modules/e2e/src/test/scala/CompatSuite.scala new file mode 100644 index 00000000..96970164 --- /dev/null +++ b/modules/e2e/src/test/scala/CompatSuite.scala @@ -0,0 +1,70 @@ +package lila.search +package test + +import cats.effect.{ IO, Resource } +import com.sksamuel.elastic4s.fields.ElasticField +import org.typelevel.log4cats.Logger +import org.typelevel.log4cats.noop.NoOpLogger +import lila.search.app.AppResources +import lila.search.app.SearchApp +import lila.search.client.PlayClient +import lila.search.app.{ AppConfig, ElasticConfig, HttpServerConfig } +import com.comcast.ip4s.* +import akka.actor.ActorSystem +import play.api.libs.ws.* +import play.api.libs.ws.ahc.* +import lila.search.spec.Query +import scala.concurrent.ExecutionContext.Implicits.* + +object CompatSuite extends weaver.IOSuite: + + given Logger[IO] = NoOpLogger[IO] + + override type Res = PlayClient + + override def sharedResource: Resource[IO, Res] = + val res = AppResources(fakeClient) + SearchApp(res, testAppConfig) + .run() + .flatMap(_ => wsClient) + .map(PlayClient(_, "http://localhost:9999")) + + test("search endpoint"): client => + val query = Query.Forum("foo") + IO.fromFuture(IO(client.search(query, 0, 10))).map(expect.same(_, lila.search.spec.SearchResponse(Nil))) + + test("count endpoint"): client => + val query = Query.Team("foo") + IO.fromFuture(IO(client.count(query))).map(expect.same(_, lila.search.spec.CountResponse(0))) + + def testAppConfig = AppConfig( + server = HttpServerConfig(ip"0.0.0.0", port"9999", shutdownTimeout = 1), + elastic = ElasticConfig("http://0.0.0.0:9200") + ) + + def fakeClient: ESClient[IO] = new ESClient[IO]: + + override def putMapping(index: Index, fields: Seq[ElasticField]): IO[Unit] = IO.unit + + override def refreshIndex(index: Index): IO[Unit] = IO.unit + + override def storeBulk(index: Index, objs: List[(String, JsonObject)]): IO[Unit] = IO.unit + + override def store(index: Index, id: Id, obj: JsonObject): IO[Unit] = IO.unit + + override def deleteOne(index: Index, id: Id): IO[Unit] = IO.unit + + override def deleteMany(index: Index, ids: List[Id]): IO[Unit] = IO.unit + + override def count[A](index: Index, query: A)(implicit q: Queryable[A]): IO[CountResponse] = + IO.pure(CountResponse(0)) + + override def search[A](index: Index, query: A, from: From, size: Size)(implicit + q: Queryable[A] + ): IO[SearchResponse] = IO.pure(SearchResponse(Nil)) + + given system: ActorSystem = ActorSystem() + + def wsClient = Resource.make(IO(StandaloneAhcWSClient()))(x => + IO(x.close()).flatMap(_ => IO.fromFuture(IO(system.terminate())).void) + )