Best Kotest code snippet using io.kotest.inspectors.Inspectors.Sequence.forOne
ProfiledHttpClientTest.kt
Source:ProfiledHttpClientTest.kt
1package ru.fix.armeria.aggregating.profiler2import com.linecorp.armeria.client.ClientOptions3import com.linecorp.armeria.client.Endpoint4import com.linecorp.armeria.client.WebClient5import com.linecorp.armeria.client.WebClientBuilder6import com.linecorp.armeria.client.endpoint.EndpointGroup7import com.linecorp.armeria.client.endpoint.EndpointSelectionStrategy8import com.linecorp.armeria.client.retry.RetryConfig9import com.linecorp.armeria.client.retry.RetryingClient10import com.linecorp.armeria.common.*11import com.linecorp.armeria.server.annotation.Get12import io.kotest.assertions.assertSoftly13import io.kotest.assertions.timing.eventually14import io.kotest.inspectors.forAll15import io.kotest.inspectors.forOne16import io.kotest.matchers.collections.shouldHaveSize17import io.kotest.matchers.longs.shouldBeGreaterThan18import io.kotest.matchers.maps.shouldContainExactly19import io.kotest.matchers.maps.shouldContainKey20import io.kotest.matchers.nulls.shouldNotBeNull21import io.kotest.matchers.should22import io.kotest.matchers.shouldBe23import io.kotest.matchers.shouldNotBe24import kotlinx.coroutines.future.await25import kotlinx.coroutines.joinAll26import kotlinx.coroutines.runBlocking27import org.apache.logging.log4j.kotlin.Logging28import org.junit.jupiter.api.DynamicTest29import org.junit.jupiter.api.Test30import org.junit.jupiter.api.TestFactory31import org.junit.jupiter.api.TestInstance32import org.junit.jupiter.api.extension.ExtensionContext33import org.junit.jupiter.params.ParameterizedTest34import org.junit.jupiter.params.provider.Arguments35import org.junit.jupiter.params.provider.ArgumentsProvider36import org.junit.jupiter.params.provider.ArgumentsSource37import ru.fix.aggregating.profiler.AggregatingProfiler38import ru.fix.aggregating.profiler.ProfiledCallReport39import ru.fix.aggregating.profiler.ProfilerReporter40import ru.fix.armeria.aggregating.profiler.ProfilerTestUtils.EPOLL_SOCKET_CHANNEL41import ru.fix.armeria.aggregating.profiler.ProfilerTestUtils.profiledCallReportWithNameEnding42import ru.fix.armeria.aggregating.profiler.ProfilerTestUtils.profiledCallReportsWithNameEnding43import ru.fix.armeria.commons.On503AndUnprocessedRetryRule44import ru.fix.armeria.commons.testing.ArmeriaMockServer45import ru.fix.armeria.commons.testing.LocalHost46import ru.fix.stdlib.socket.SocketChecker47import java.net.URI48import java.time.Duration49import java.util.concurrent.TimeUnit50import java.util.stream.Stream51import kotlin.time.ExperimentalTime52import kotlin.time.seconds53@ExperimentalTime54@TestInstance(TestInstance.Lifecycle.PER_CLASS)55internal class ProfiledHttpClientTest {56 @ParameterizedTest57 @ArgumentsSource(ProtocolSpecificTest.CaseArgumentsProvider::class)58 fun `success request profiled with connect, connected and whole request flow metrics`(59 testCaseArguments: ProtocolSpecificTest.CaseArguments60 ) = runBlocking<Unit> {61 val pathDelayedOk = "/ok-delayed/{delay}"62 val profiler = AggregatingProfiler()63 val profilerReporter = profiler.createReporter()64 val mockServer = ArmeriaMockServer {65 service(pathDelayedOk) { ctx, _ ->66 val delayMs = ctx.pathParam("delay")!!.toLong()67 HttpResponse.delayed(HttpResponse.of(HttpStatus.OK), Duration.ofMillis(delayMs))68 }69 }70 mockServer.start()71 try {72 val mockServerUri = mockServer.uri(SessionProtocol.find(testCaseArguments.clientProtocol)!!)73 val client = WebClient74 .builder(mockServerUri)75 .decorator(ProfiledHttpClient.newDecorator(profiler))76 .build()77 val path = pathDelayedOk.replace("{delay}", 1_000.toString())78 val expectedConnectMetricTags = mapOf(79 MetricTags.METHOD to "GET",80 MetricTags.PATH to path,81 MetricTags.PROTOCOL to testCaseArguments.connectMetricTagProtocol,82 MetricTags.IS_MULTIPLEX_PROTOCOL to testCaseArguments.connectMetricTagIsMultiplex.toString(),83 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,84 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,85 MetricTags.REMOTE_PORT to mockServer.httpPort().toString()86 )87 val expectedConnectedMetricTags = expectedConnectMetricTags + mapOf(88 // protocol and channel information are determined on this phase89 MetricTags.PROTOCOL to testCaseArguments.otherMetricsTagProtocol,90 MetricTags.IS_MULTIPLEX_PROTOCOL to testCaseArguments.otherMetricsTagIsMultiplex.toString(),91 MetricTags.CHANNEL_CLASS to EPOLL_SOCKET_CHANNEL92 )93 val expectedSuccessMetricTags = expectedConnectedMetricTags + (MetricTags.RESPONSE_STATUS to "200")94 client.get(path).aggregate().await()95 eventually(1.seconds) {96 assertSoftly(profilerReporter.buildReportAndReset()) {97 profiledCallReportWithNameEnding(Metrics.HTTP_CONNECT) should {98 it.shouldNotBeNull()99 it.identity.tags shouldContainExactly expectedConnectMetricTags100 }101 profiledCallReportWithNameEnding(Metrics.HTTP_CONNECTED) should {102 it.shouldNotBeNull()103 it.identity.tags shouldContainExactly expectedConnectedMetricTags104 }105 profiledCallReportWithNameEnding(Metrics.HTTP_SUCCESS) should {106 it.shouldNotBeNull()107 it.identity.tags shouldContainExactly expectedSuccessMetricTags108 }109 }110 }111 } finally {112 mockServer.stop()113 }114 }115 @TestFactory116 fun `http (and not only) error WHEN error occured THEN it is profiled with corresponding status`() =117 DynamicTest.stream(118 listOf(119 ErrorProfilingTest.Case(120 testCaseName = "Invalid (non 2xx) status 404",121 mockServerGenerator = {122 val mockServer = ArmeriaMockServer {123 service("/some-other-path") { _, _ -> HttpResponse.of(HttpStatus.OK) }124 }125 mockServer.start()126 ErrorProfilingTest.Case.MockServer(mockServer.httpUri()) {127 mockServer.stop()128 }129 },130 expectedErrorMetricTagsByMockUriGenerator = {131 listOf(132 MetricTags.ERROR_TYPE to "invalid_status",133 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,134 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,135 MetricTags.REMOTE_PORT to it.port.toString(),136 MetricTags.PATH to ErrorProfilingTest.TEST_PATH,137 MetricTags.METHOD to "GET",138 MetricTags.CHANNEL_CLASS to EPOLL_SOCKET_CHANNEL,139 MetricTags.RESPONSE_STATUS to "404",140 MetricTags.PROTOCOL to "h2c",141 MetricTags.IS_MULTIPLEX_PROTOCOL to "true"142 )143 },144 latencyMetricRequired = true145 ),146 ErrorProfilingTest.Case(147 testCaseName = "Invalid (non 2xx) status 500",148 mockServerGenerator = {149 val mockServer = ArmeriaMockServer {150 service(ErrorProfilingTest.TEST_PATH) { _, _ ->151 HttpResponse.of(HttpStatus.INTERNAL_SERVER_ERROR)152 }153 }154 mockServer.start()155 ErrorProfilingTest.Case.MockServer(mockServer.httpUri()) {156 mockServer.stop()157 }158 },159 expectedErrorMetricTagsByMockUriGenerator = {160 listOf(161 MetricTags.ERROR_TYPE to "invalid_status",162 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,163 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,164 MetricTags.REMOTE_PORT to it.port.toString(),165 MetricTags.PATH to ErrorProfilingTest.TEST_PATH,166 MetricTags.METHOD to "GET",167 MetricTags.CHANNEL_CLASS to EPOLL_SOCKET_CHANNEL,168 MetricTags.RESPONSE_STATUS to "500",169 MetricTags.PROTOCOL to "h2c",170 MetricTags.IS_MULTIPLEX_PROTOCOL to "true"171 )172 },173 latencyMetricRequired = true174 ),175 ErrorProfilingTest.Case(176 testCaseName = "Connection refused",177 mockServerGenerator = {178 // non-existing address179 ErrorProfilingTest.Case.MockServer(180 URI.create("http://${LocalHost.HOSTNAME.value}:${getAvailableRandomPort()}")181 ) {}182 },183 expectedErrorMetricTagsByMockUriGenerator = {184 listOf(185 MetricTags.ERROR_TYPE to "connect_refused",186 MetricTags.PATH to ErrorProfilingTest.TEST_PATH,187 MetricTags.METHOD to "GET",188 MetricTags.PROTOCOL to "http",189 MetricTags.IS_MULTIPLEX_PROTOCOL to false.toString(),190 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,191 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,192 MetricTags.REMOTE_PORT to it.port.toString()193 )194 },195 latencyMetricRequired = true196 ),197 ErrorProfilingTest.Case(198 testCaseName = "Response timeout",199 mockServerGenerator = {200 val mockServer = ArmeriaMockServer {201 service(ErrorProfilingTest.TEST_PATH) { _, _ ->202 HttpResponse.delayed(HttpResponse.of(HttpStatus.OK), Duration.ofSeconds(2))203 }204 }205 mockServer.start()206 ErrorProfilingTest.Case.MockServer(mockServer.httpUri()) {207 mockServer.stop()208 }209 },210 expectedErrorMetricTagsByMockUriGenerator = {211 listOf(212 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,213 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,214 MetricTags.REMOTE_PORT to it.port.toString(),215 MetricTags.ERROR_TYPE to "response_timeout",216 MetricTags.PATH to ErrorProfilingTest.TEST_PATH,217 MetricTags.METHOD to "GET",218 MetricTags.PROTOCOL to "h2c",219 MetricTags.IS_MULTIPLEX_PROTOCOL to "true",220 MetricTags.CHANNEL_CLASS to EPOLL_SOCKET_CHANNEL221 )222 },223 clientBuilderCustomizer = {224 option(ClientOptions.RESPONSE_TIMEOUT_MILLIS, 500)225 }226 ),227 *listOf(228 ErrorProfilingTest.ProtocolSpecificCase(229 protocol = SessionProtocol.H1C,230 caseName = """Session closed of "http/1 - cleartext" request due to server side response timeout""",231 profiledErrorType = "response_closed_session"232 ),233 ErrorProfilingTest.ProtocolSpecificCase(234 protocol = SessionProtocol.H2C,235 caseName = """Stream closed of "http/2 - cleartext" request due to server side response timeout""",236 profiledErrorType = "response_closed_stream"237 )238 ).map { protocolSpecificCase ->239 ErrorProfilingTest.Case(240 testCaseName = protocolSpecificCase.caseName,241 mockServerGenerator = {242 val server = ErrorProfilingTest.Utils.delayedResponsePartsMock(243 targetPath = ErrorProfilingTest.TEST_PATH,244 serverRequestTimeout = Duration.ofSeconds(1),245 fullResponseDelay = Duration.ofSeconds(2),246 responseStatus = HttpStatus.OK,247 numberOfResponseParts = 10248 ).apply {249 start()250 }251 ErrorProfilingTest.Case.MockServer(server.uri(protocolSpecificCase.protocol)) {252 server.stop()253 }254 },255 expectedErrorMetricTagsByMockUriGenerator = {256 listOf(257 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,258 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,259 MetricTags.REMOTE_PORT to it.port.toString(),260 MetricTags.ERROR_TYPE to protocolSpecificCase.profiledErrorType,261 MetricTags.PATH to ErrorProfilingTest.TEST_PATH,262 MetricTags.METHOD to "GET",263 MetricTags.PROTOCOL to protocolSpecificCase.protocol.uriText(),264 MetricTags.IS_MULTIPLEX_PROTOCOL to protocolSpecificCase.protocol.isMultiplex.toString(),265 MetricTags.CHANNEL_CLASS to EPOLL_SOCKET_CHANNEL,266 MetricTags.RESPONSE_STATUS to "200"267 )268 }269 )270 }.toTypedArray()271 ).iterator(),272 { it.testCaseName }273 ) { (274 _, setupMockForError: suspend () -> ErrorProfilingTest.Case.MockServer,275 getExpectedErrorMetricTags: (URI) -> List<MetricTag>,276 latencyMetricRequired: Boolean,277 webClientBuilderCustomizer: WebClientBuilder.() -> WebClientBuilder278 ) ->279 runBlocking<Unit> {280 val profiler = AggregatingProfiler()281 val profilerReporter = profiler.createReporter()282 val mockServer = setupMockForError()283 val client = WebClient.builder(mockServer.mockUri)284 .decorator(ProfiledHttpClient.newDecorator(profiler))285 .webClientBuilderCustomizer()286 .build()287 val expectedErrorMetricTags = getExpectedErrorMetricTags(mockServer.mockUri).toMap()288 try {289 client.get("${ErrorProfilingTest.TEST_PATH}?testParam1=param1&testParam2=param2")290 .aggregate().await()291 } catch (e: Exception) {292 logger.error(e)293 }294 eventually(1.seconds) {295 assertSoftly(profilerReporter.buildReportAndReset()) {296 profiledCallReportWithNameEnding(Metrics.HTTP_ERROR) should {297 it.shouldNotBeNull()298 it.identity.tags shouldContainExactly expectedErrorMetricTags299 if (latencyMetricRequired) {300 it.latencyMax shouldBeGreaterThan 0301 } else {302 it.latencyMax shouldBe 0303 }304 }305 }306 }307 }308 }309 @Test310 suspend fun `http (and not only) error WHEN connection unsuccessful THEN connected metric is not occured`() {311 val profiler = AggregatingProfiler()312 val profilerReporter = profiler.createReporter()313 val client = WebClient314 .builder(SessionProtocol.HTTP, Endpoint.of(LocalHost.HOSTNAME.value, getAvailableRandomPort()))315 .decorator(ProfiledHttpClient.newDecorator(profiler))316 .build()317 try {318 client.get("/").aggregate().await()319 } catch (e: Exception) {320 logger.error(e)321 }322 eventually(1.seconds) {323 assertSoftly(profilerReporter.buildReportAndReset()) {324 profiledCallReportWithNameEnding(Metrics.HTTP_CONNECT) shouldNotBe null325 profiledCallReportWithNameEnding(Metrics.HTTP_CONNECTED) shouldBe null326 profiledCallReportWithNameEnding(Metrics.HTTP_ERROR) shouldNotBe null327 }328 }329 }330 @Test331 suspend fun `non-200 http status is valid`() {332 val profiler = AggregatingProfiler()333 val profilerReporter = profiler.createReporter()334 val path = "/non200success"335 val mockServer = ArmeriaMockServer {336 service(path) { _, _ ->337 HttpResponse.of(HttpStatus.FOUND)338 }339 }340 mockServer.start()341 val client = WebClient342 .builder(mockServer.httpUri())343 .decorator(ProfiledHttpClient.newDecorator(profiler) { it == HttpStatus.FOUND })344 .build()345 try {346 client.get(path).aggregate().await()347 } catch (e: Exception) {348 logger.error(e)349 }350 eventually(1.seconds) {351 val report = profilerReporter.buildReportAndReset { metric, _ ->352 metric.name == Metrics.HTTP_CONNECT353 }354 report.profiledCallReportWithNameEnding(Metrics.HTTP_CONNECT) should {355 it.shouldNotBeNull()356 it.stopSum shouldBe 1357 it.identity.tags shouldContainExactly mapOf(358 MetricTags.PATH to path,359 MetricTags.METHOD to "GET",360 MetricTags.PROTOCOL to "http",361 MetricTags.IS_MULTIPLEX_PROTOCOL to false.toString(),362 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,363 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,364 MetricTags.REMOTE_PORT to mockServer.httpPort().toString()365 )366 }367 }368 eventually(1.seconds) {369 val report = profilerReporter.buildReportAndReset { metric, _ ->370 metric.name == Metrics.HTTP_CONNECTED371 }372 report.profiledCallReportWithNameEnding(Metrics.HTTP_CONNECTED) should {373 it.shouldNotBeNull()374 it.stopSum shouldBe 1375 it.identity.tags shouldContainExactly mapOf(376 MetricTags.PATH to path,377 MetricTags.METHOD to "GET",378 MetricTags.PROTOCOL to "h2c",379 MetricTags.IS_MULTIPLEX_PROTOCOL to true.toString(),380 MetricTags.CHANNEL_CLASS to EPOLL_SOCKET_CHANNEL,381 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,382 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,383 MetricTags.REMOTE_PORT to mockServer.httpPort().toString()384 )385 }386 }387 eventually(1.seconds) {388 val report = profilerReporter.buildReportAndReset { metric, _ ->389 metric.name == Metrics.HTTP_ERROR390 }391 report.profiledCallReportWithNameEnding(Metrics.HTTP_ERROR) shouldBe null392 }393 }394 @Test395 suspend fun `http (and not only) error WHEN endpoint group is empty THEN error profiled`() {396 val testPath = "/test-no-endpoint-group-error"397 val profiler = AggregatingProfiler()398 val profilerReporter = profiler.createReporter()399 val client = WebClient.builder(SessionProtocol.HTTP, EndpointGroup.of())400 .decorator(ProfiledHttpClient.newDecorator(profiler))401 .build()402 try {403 client.get(testPath).aggregate().await()404 } catch (e: Exception) {405 logger.error(e)406 }407 eventually(1.seconds) {408 assertSoftly(profilerReporter.buildReportAndReset()) {409 profiledCallReportWithNameEnding(Metrics.HTTP_CONNECT) should {410 it.shouldNotBeNull()411 it.stopSum shouldBe 1412 }413 profiledCallReportWithNameEnding(Metrics.HTTP_CONNECTED) shouldBe null414 profiledCallReportWithNameEnding(Metrics.HTTP_ERROR) should {415 it.shouldNotBeNull()416 it.latencyMax shouldBe 0417 it.identity.tags shouldContainExactly mapOf(418 MetricTags.ERROR_TYPE to "no_available_endpoint",419 MetricTags.PATH to testPath,420 MetricTags.METHOD to "GET",421 MetricTags.PROTOCOL to "http",422 MetricTags.IS_MULTIPLEX_PROTOCOL to false.toString()423 )424 }425 }426 }427 }428 @Test429 suspend fun `WHEN query string contains query parameters THEN they removed from metric 'path' label`() {430 val profiler = AggregatingProfiler()431 val profilerReporter = profiler.createReporter()432 val path = "/pathWithQueryParams"433 val mockServer = ArmeriaMockServer {434 annotatedService(object : Any() {435 @Get("/pathWithQueryParams")436 fun testPath(queryParams: QueryParams): HttpResponse {437 logger.info { "GET Request with query params arrived: $queryParams" }438 return HttpResponse.of(HttpStatus.OK)439 }440 })441 }442 mockServer.start()443 try {444 val client = WebClient445 .builder(mockServer.httpUri())446 .decorator(ProfiledHttpClient.newDecorator(profiler))447 .build()448 try {449 client.get("$path?testParam1=val1&testParam2=val2").aggregate().await()450 } catch (e: Exception) {451 logger.error(e)452 }453 val pathLabelOfConnectMetric: String = profilerReporter454 .assertMetricPresentAndGetItsPathLabel(Metrics.HTTP_CONNECT)455 val pathLabelOfConnectedMetric: String = profilerReporter456 .assertMetricPresentAndGetItsPathLabel(Metrics.HTTP_CONNECTED)457 val pathLabelOfHttpSuccessMetric: String = profilerReporter458 .assertMetricPresentAndGetItsPathLabel(Metrics.HTTP_SUCCESS)459 mapOf(460 "connect metric" to pathLabelOfConnectMetric,461 "connected metric" to pathLabelOfConnectedMetric,462 "http success metric" to pathLabelOfHttpSuccessMetric463 ).asSequence().forAll { (_, value) ->464 value shouldBe path465 }466 } finally {467 mockServer.stop()468 }469 }470 private suspend fun ProfilerReporter.assertMetricPresentAndGetItsPathLabel(metricName: String) =471 eventually(1.seconds) {472 val report = buildReportAndReset { metric, _ ->473 metric.name == metricName474 }475 val metric: ProfiledCallReport? = report.profiledCallReportWithNameEnding(metricName)476 metric should {477 it.shouldNotBeNull()478 it.stopSum shouldBe 1479 it.identity.tags shouldContainKey MetricTags.PATH480 }481 metric!!.identity.tags[MetricTags.PATH]!!482 }483 @Test484 suspend fun `WHEN retry decorator placed before profiled one THEN each retry attempt profiled`() {485 val profiler = AggregatingProfiler()486 val profilerReporter = profiler.createReporter()487 val mockServer = ArmeriaMockServer().start()488 try {489 mockServer.enqueue {490 HttpResponse.delayed(491 HttpResponse.of(HttpStatus.SERVICE_UNAVAILABLE),492 Duration.ofMillis(500)493 )494 }495 mockServer.enqueue {496 HttpResponse.delayed(HttpResponse.of(HttpStatus.OK), Duration.ofMillis(200))497 }498 val nonExistingServerPort = getAvailableRandomPort()499 val client = WebClient500 .builder(501 SessionProtocol.HTTP,502 EndpointGroup.of(503 EndpointSelectionStrategy.roundRobin(),504 mockServer.httpEndpoint(),505 Endpoint.of(LocalHost.HOSTNAME.value, nonExistingServerPort)506 )507 )508 .decorator(ProfiledHttpClient.newDecorator(profiler))509 .decorator(510 RetryingClient.newDecorator(511 RetryConfig.builder(On503AndUnprocessedRetryRule).maxTotalAttempts(3).build()512 )513 )514 .build()515 client.get("/").aggregate().await()516 eventually(1.seconds) {517 profilerReporter.buildReportAndReset { metric, _ -> metric.name == Metrics.HTTP_CONNECT } should { report ->518 logger.trace { "Report: $report" }519 report.profiledCallReportsWithNameEnding(Metrics.HTTP_CONNECT) should { callReports ->520 callReports.shouldHaveSize(2)521 callReports.forOne {522 it.stopSum shouldBe 1523 it.identity.tags shouldContainExactly mapOf(524 MetricTags.PATH to "/",525 MetricTags.METHOD to "GET",526 MetricTags.PROTOCOL to "http",527 MetricTags.IS_MULTIPLEX_PROTOCOL to false.toString(),528 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,529 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,530 MetricTags.REMOTE_PORT to nonExistingServerPort.toString()531 )532 }533 callReports.forOne {534 it.stopSum shouldBe 2535 it.identity.tags shouldContainExactly mapOf(536 MetricTags.PATH to "/",537 MetricTags.METHOD to "GET",538 MetricTags.PROTOCOL to "http",539 MetricTags.IS_MULTIPLEX_PROTOCOL to false.toString(),540 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,541 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,542 MetricTags.REMOTE_PORT to mockServer.httpPort().toString()543 )544 }545 }546 }547 }548 eventually(1.seconds) {549 profilerReporter.buildReportAndReset { metric, _ -> metric.name == Metrics.HTTP_CONNECTED } should { report ->550 logger.trace { "Report: $report" }551 report.profiledCallReportWithNameEnding(Metrics.HTTP_CONNECTED) should {552 it.shouldNotBeNull()553 assertSoftly {554 it.stopSum shouldBe 2555 it.identity.tags shouldContainExactly mapOf(556 MetricTags.PATH to "/",557 MetricTags.METHOD to "GET",558 MetricTags.PROTOCOL to "h2c",559 MetricTags.IS_MULTIPLEX_PROTOCOL to true.toString(),560 MetricTags.CHANNEL_CLASS to EPOLL_SOCKET_CHANNEL,561 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,562 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,563 MetricTags.REMOTE_PORT to mockServer.httpPort().toString()564 )565 }566 }567 }568 }569 eventually(1.seconds) {570 val report = profilerReporter.buildReportAndReset { metric, _ ->571 metric.name == Metrics.HTTP_ERROR &&572 metric.tags[MetricTags.ERROR_TYPE]?.let { it == "connect_refused" } ?: false573 }574 logger.trace { "Report: $report" }575 report.profiledCallReportWithNameEnding(Metrics.HTTP_ERROR) should {576 it.shouldNotBeNull()577 it.stopSum shouldBe 1578 it.identity.tags shouldContainExactly mapOf(579 MetricTags.PATH to "/",580 MetricTags.METHOD to "GET",581 MetricTags.PROTOCOL to "http",582 MetricTags.IS_MULTIPLEX_PROTOCOL to false.toString(),583 MetricTags.ERROR_TYPE to "connect_refused",584 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,585 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,586 MetricTags.REMOTE_PORT to nonExistingServerPort.toString()587 )588 }589 }590 eventually(1.seconds) {591 val report = profilerReporter.buildReportAndReset { metric, _ ->592 metric.name == Metrics.HTTP_ERROR &&593 metric.tags[MetricTags.ERROR_TYPE]?.let { it == "invalid_status" } ?: false594 }595 logger.trace { "Report: $report" }596 report.profiledCallReportWithNameEnding(Metrics.HTTP_ERROR) should {597 it.shouldNotBeNull()598 it.stopSum shouldBe 1599 it.identity.tags shouldContainExactly mapOf(600 MetricTags.PATH to "/",601 MetricTags.METHOD to "GET",602 MetricTags.PROTOCOL to "h2c",603 MetricTags.IS_MULTIPLEX_PROTOCOL to true.toString(),604 MetricTags.CHANNEL_CLASS to EPOLL_SOCKET_CHANNEL,605 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,606 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,607 MetricTags.REMOTE_PORT to mockServer.httpPort().toString(),608 MetricTags.ERROR_TYPE to "invalid_status",609 MetricTags.RESPONSE_STATUS to "503"610 )611 }612 }613 eventually(1.seconds) {614 profilerReporter.buildReportAndReset { metric, _ -> metric.name == Metrics.HTTP_SUCCESS } should { report ->615 logger.trace { "Report: $report" }616 report.profiledCallReportWithNameEnding(Metrics.HTTP_SUCCESS) should {617 it.shouldNotBeNull()618 it.stopSum shouldBe 1619 it.identity.tags shouldContainExactly mapOf(620 MetricTags.PATH to "/",621 MetricTags.METHOD to "GET",622 MetricTags.PROTOCOL to "h2c",623 MetricTags.IS_MULTIPLEX_PROTOCOL to true.toString(),624 MetricTags.CHANNEL_CLASS to EPOLL_SOCKET_CHANNEL,625 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,626 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,627 MetricTags.REMOTE_PORT to mockServer.httpPort().toString(),628 MetricTags.RESPONSE_STATUS to "200"629 )630 }631 }632 }633 } finally {634 mockServer.stop()635 }636 }637 @Test638 suspend fun `WHEN retry decorator placed after profiled one AND attempts are not exceeded THEN all retries profiled as one success request`() {639 val profiler = AggregatingProfiler()640 val profilerReporter = profiler.createReporter()641 val firstRequestDelayMillis: Long = 500642 val lastRequestDelayMillis: Long = 200643 val (mockServer, mockServer2) = ArmeriaMockServer() to ArmeriaMockServer()644 try {645 listOf(646 mockServer.launchStart(),647 mockServer2.launchStart()648 ).joinAll()649 mockServer.enqueue {650 HttpResponse.delayed(651 HttpResponse.of(HttpStatus.SERVICE_UNAVAILABLE),652 Duration.ofMillis(firstRequestDelayMillis)653 )654 }655 mockServer2.enqueue {656 HttpResponse.delayed(657 HttpResponse.of(HttpStatus.OK), Duration.ofMillis(lastRequestDelayMillis)658 )659 }660 val nonExistingServerPort = getAvailableRandomPort()661 val client = WebClient662 .builder(663 SessionProtocol.HTTP,664 EndpointGroup.of(665 EndpointSelectionStrategy.roundRobin(),666 mockServer.httpEndpoint(),667 Endpoint.of(LocalHost.HOSTNAME.value, nonExistingServerPort),668 mockServer2.httpEndpoint()669 )670 )671 .decorator(672 RetryingClient.newDecorator(673 RetryConfig.builder(On503AndUnprocessedRetryRule).maxTotalAttempts(3).build()674 )675 )676 .decorator(ProfiledHttpClient.newDecorator(profiler))677 .build()678 /*679 TODO680 due to details of context logs when retries are present (see ProfiledHttpClient implementation),681 first endpoint's data is written to metrics. It would be better to fix it or reorganize structure of682 metrics for whole request flow processing.683 */684 val expectedProfiledMockServer = mockServer/*2*/685 client.get("/").aggregate().await()686 eventually(1.seconds) {687 assertSoftly(profilerReporter.buildReportAndReset()) {688 logger.trace { "Report: $this" }689 profiledCallReportWithNameEnding(Metrics.HTTP_CONNECT) should {690 it.shouldNotBeNull()691 it.stopSum shouldBe 1692 it.identity.tags shouldContainExactly mapOf(693 MetricTags.PATH to "/",694 MetricTags.METHOD to "GET",695 MetricTags.PROTOCOL to "http",696 MetricTags.IS_MULTIPLEX_PROTOCOL to false.toString(),697 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,698 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,699 MetricTags.REMOTE_PORT to expectedProfiledMockServer.httpPort().toString()700 )701 }702 profiledCallReportWithNameEnding(Metrics.HTTP_CONNECTED) should {703 it.shouldNotBeNull()704 it.stopSum shouldBe 1705 it.identity.tags shouldContainExactly mapOf(706 MetricTags.PATH to "/",707 MetricTags.METHOD to "GET",708 MetricTags.PROTOCOL to "h2c",709 MetricTags.IS_MULTIPLEX_PROTOCOL to true.toString(),710 MetricTags.CHANNEL_CLASS to EPOLL_SOCKET_CHANNEL,711 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,712 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,713 MetricTags.REMOTE_PORT to expectedProfiledMockServer.httpPort().toString()714 )715 }716 profiledCallReportWithNameEnding(Metrics.HTTP_SUCCESS) should {717 it.shouldNotBeNull()718 it.stopSum shouldBe 1719 it.identity.tags shouldContainExactly mapOf(720 MetricTags.PATH to "/",721 MetricTags.METHOD to "GET",722 MetricTags.PROTOCOL to "h2c",723 MetricTags.IS_MULTIPLEX_PROTOCOL to true.toString(),724 MetricTags.CHANNEL_CLASS to EPOLL_SOCKET_CHANNEL,725 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,726 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,727 MetricTags.REMOTE_PORT to expectedProfiledMockServer.httpPort().toString(),728 MetricTags.RESPONSE_STATUS to "200"729 )730 it.latencyMax shouldBeGreaterThan firstRequestDelayMillis + lastRequestDelayMillis731 }732 }733 }734 } finally {735 listOf(736 mockServer.launchStop(),737 mockServer2.launchStop()738 ).joinAll()739 }740 }741 @Test742 suspend fun `WHEN retry decorator placed after profiled one AND attempts are exceeded THEN all retries profiled as one error request`() {743 val profiler = AggregatingProfiler()744 val profilerReporter = profiler.createReporter()745 val firstRequestDelayMillis: Long = 400746 val lastRequestDelayMillis: Long = 300747 val (mockServer, mockServer2) = ArmeriaMockServer() to ArmeriaMockServer()748 try {749 listOf(750 mockServer.launchStart(),751 mockServer2.launchStart()752 ).joinAll()753 mockServer.enqueue {754 HttpResponse.delayed(755 HttpResponse.of(HttpStatus.SERVICE_UNAVAILABLE),756 Duration.ofMillis(firstRequestDelayMillis)757 )758 }759 mockServer2.enqueue {760 HttpResponse.delayed(761 HttpResponse.of(HttpStatus.SERVICE_UNAVAILABLE),762 Duration.ofMillis(lastRequestDelayMillis)763 )764 }765 val nonExistingServerPort = getAvailableRandomPort()766 val client = WebClient767 .builder(768 SessionProtocol.HTTP,769 EndpointGroup.of(770 EndpointSelectionStrategy.roundRobin(),771 mockServer.httpEndpoint(),772 Endpoint.of(LocalHost.HOSTNAME.value, nonExistingServerPort),773 mockServer2.httpEndpoint()774 )775 )776 .decorator(777 RetryingClient.newDecorator(778 RetryConfig.builder(On503AndUnprocessedRetryRule).maxTotalAttempts(3).build()779 )780 )781 .decorator(ProfiledHttpClient.newDecorator(profiler))782 .build()783 /*784 TODO785 due to details of context logs when retries are present (see ProfiledHttpClient implementation),786 first endpoint's data is written to metrics. It would be better to fix it or reorganize structure of787 metrics for whole request flow processing.788 */789 val expectedProfiledMockServer = mockServer/*2*/790 client.get("/").aggregate().await()791 eventually(1.seconds) {792 val report = profilerReporter.buildReportAndReset { metric, _ -> metric.name == Metrics.HTTP_CONNECT }793 report.profiledCallReportWithNameEnding(Metrics.HTTP_CONNECT) should {794 it.shouldNotBeNull()795 it.stopSum shouldBe 1796 it.identity.tags shouldContainExactly mapOf(797 MetricTags.PATH to "/",798 MetricTags.METHOD to "GET",799 MetricTags.PROTOCOL to "http",800 MetricTags.IS_MULTIPLEX_PROTOCOL to false.toString(),801 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,802 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,803 MetricTags.REMOTE_PORT to expectedProfiledMockServer.httpPort().toString()804 )805 }806 }807 eventually(1.seconds) {808 val report = profilerReporter.buildReportAndReset { metric, _ ->809 metric.name == Metrics.HTTP_CONNECTED810 }811 report.profiledCallReportWithNameEnding(Metrics.HTTP_CONNECTED) should {812 it.shouldNotBeNull()813 it.stopSum shouldBe 1814 it.identity.tags shouldContainExactly mapOf(815 MetricTags.PATH to "/",816 MetricTags.METHOD to "GET",817 MetricTags.PROTOCOL to "h2c",818 MetricTags.IS_MULTIPLEX_PROTOCOL to true.toString(),819 MetricTags.CHANNEL_CLASS to EPOLL_SOCKET_CHANNEL,820 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,821 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,822 MetricTags.REMOTE_PORT to expectedProfiledMockServer.httpPort().toString()823 )824 }825 }826 eventually(1.seconds) {827 val report = profilerReporter.buildReportAndReset { metric, _ ->828 metric.name == Metrics.HTTP_ERROR829 }830 report.profiledCallReportWithNameEnding(Metrics.HTTP_ERROR) should {831 it.shouldNotBeNull()832 it.stopSum shouldBe 1833 it.identity.tags shouldContainExactly mapOf(834 MetricTags.PATH to "/",835 MetricTags.METHOD to "GET",836 MetricTags.PROTOCOL to "h2c",837 MetricTags.IS_MULTIPLEX_PROTOCOL to true.toString(),838 MetricTags.CHANNEL_CLASS to EPOLL_SOCKET_CHANNEL,839 MetricTags.REMOTE_ADDRESS to LocalHost.IP.value,840 MetricTags.REMOTE_HOST to LocalHost.HOSTNAME.value,841 MetricTags.REMOTE_PORT to expectedProfiledMockServer.httpPort().toString(),842 MetricTags.ERROR_TYPE to "invalid_status",843 MetricTags.RESPONSE_STATUS to "503"844 )845 it.latencyMax shouldBeGreaterThan firstRequestDelayMillis + lastRequestDelayMillis846 }847 }848 } finally {849 listOf(850 mockServer.launchStop(),851 mockServer2.launchStop()852 ).joinAll()853 }854 }855/*856 TODO implement connect timeout case using some network tool, e.g. https://github.com/alexei-led/pumba857 Motivation why it is not implemented immediately -858 it is quite hard to simulate connection timeout in pure java mock server.859 As it said here https://github.com/tomakehurst/wiremock/issues/591:860 > It's basically impossible to reliably force connection timeouts in pure Java at the moment.861 > My recommendation at the moment would be to use a tool that works at the level of the network stack.862 */863// @Test864// fun `WHEN connect timeouted THEN error profiled`() {865// }866 companion object : Logging {867 fun getAvailableRandomPort() = SocketChecker.getAvailableRandomPort()868 }869 object ProtocolSpecificTest {870 data class CaseArguments(871 val clientProtocol: String,872 val connectMetricTagProtocol: String,873 val connectMetricTagIsMultiplex: Boolean,874 val otherMetricsTagProtocol: String,875 val otherMetricsTagIsMultiplex: Boolean876 ) : Arguments {877 override fun get(): Array<Any> = arrayOf(this)878 }879 class CaseArgumentsProvider : ArgumentsProvider {880 override fun provideArguments(context: ExtensionContext?): Stream<out Arguments> = Stream.of(881 CaseArguments(882 clientProtocol = "http",883 connectMetricTagProtocol = "http",884 connectMetricTagIsMultiplex = false,885 otherMetricsTagProtocol = "h2c",886 otherMetricsTagIsMultiplex = true887 ),888 CaseArguments(889 clientProtocol = "h1c",890 connectMetricTagProtocol = "h1c",891 connectMetricTagIsMultiplex = false,892 otherMetricsTagProtocol = "h1c",893 otherMetricsTagIsMultiplex = false894 ),895 CaseArguments(896 clientProtocol = "h2c",897 connectMetricTagProtocol = "h2c",898 connectMetricTagIsMultiplex = true,899 otherMetricsTagProtocol = "h2c",900 otherMetricsTagIsMultiplex = true901 )902 )903 }904 }905 object ErrorProfilingTest {906 const val TEST_PATH = "/test-error-profiling"907 object Utils {908 fun delayedResponsePartsMock(909 targetPath: String,910 serverRequestTimeout: Duration,911 fullResponseDelay: Duration,912 responseStatus: HttpStatus,913 numberOfResponseParts: Int914 ): ArmeriaMockServer {915 return ArmeriaMockServer {916 this.requestTimeout(serverRequestTimeout)917 .service(targetPath) { ctx, _ ->918 HttpResponse.streaming().also { response ->919 val responseStatusHeader = ResponseHeaders.of(responseStatus)920 response.write(responseStatusHeader)921 val lastResponsePartIndex = numberOfResponseParts - 1922 for (responsePartIndex in 0..numberOfResponseParts) {923 ctx.eventLoop().schedule(924 {925 response.write(926 HttpData.ofAscii(responsePartIndex.toString())927 )928 if (responsePartIndex == lastResponsePartIndex) {929 response.close()930 }931 },932 fullResponseDelay.toMillis() * responsePartIndex / numberOfResponseParts,933 TimeUnit.MILLISECONDS934 )935 }936 }937 }938 }939 }940 }941 data class ProtocolSpecificCase(942 val protocol: SessionProtocol,943 val caseName: String,944 val profiledErrorType: String945 )946 data class Case(947 val testCaseName: String,948 val mockServerGenerator: suspend () -> MockServer,949 val expectedErrorMetricTagsByMockUriGenerator: (URI) -> List<MetricTag>,950 val latencyMetricRequired: Boolean = false,951 val clientBuilderCustomizer: WebClientBuilder.() -> WebClientBuilder = { this }952 ) {953 class MockServer(954 val mockUri: URI,955 val stop: suspend () -> Unit956 )957 }958 }959}...
JsonEventParserTest.kt
Source:JsonEventParserTest.kt
1package net.torommo.logspy2import com.google.gson.GsonBuilder3import com.google.gson.JsonArray4import com.google.gson.JsonObject5import io.kotest.assertions.failure6import io.kotest.core.spec.style.FreeSpec7import io.kotest.data.Row18import io.kotest.data.forAll9import io.kotest.data.row10import io.kotest.inspectors.forOne11import io.kotest.matchers.collections.shouldContainExactly12import io.kotest.matchers.collections.shouldNotBeEmpty13import io.kotest.matchers.maps.shouldContainExactly14import io.kotest.matchers.shouldBe15import net.torommo.logspy.SpiedEvent.Level16import net.torommo.logspy.SpiedEvent.StackTraceElementSnapshot17internal class JsonEventParserTest : FreeSpec() {18 init {19 "parses single event" - {20 val entry = content { message = "Test message" }21 val events = parseToEvents(entry)22 events.forOne { it.message shouldBe "Test message" }23 }24 "ignores event when from not matching logger" - {25 val entry = content { loggerName = "net.torommo.logspy.AnotherName" }26 val events = parseToEvents(entry, "net.torommo.logspy.DifferentName")27 events shouldBe emptyList()28 }29 "ignores event when incomplete" - {30 fun incompleteConfigurations(): Array<Row1<String>> {31 val entry = content { message = "Test message" }32 val string = JsonEntryBuilder().apply(entry).build()33 return (0 until string.length).map { string.substring(0 until it) }34 .map { row(it) }35 .toTypedArray()36 }37 forAll(*incompleteConfigurations()) { payload ->38 val events =39 JsonEventParser("net.torommo.logspy.LogSpyExtensionIntegrationTest", payload)40 .events()41 events shouldBe emptyList()42 }43 }44 "maps level" - {45 forAll(46 row("TRACE", Level.TRACE),47 row("DEBUG", Level.DEBUG),48 row("INFO", Level.INFO),49 row("WARN", Level.WARN),50 row("ERROR", Level.ERROR)51 ) { literal, level ->52 val entry = content { this.level = literal }53 val events = parseToEvents(entry)54 events.forOne { it.level shouldBe level }55 }56 }57 "maps exception type" - {58 forAll(59 row("net.torommo.logspy.Test", "net.torommo.logspy.Test"),60 // Modules61 row(62 "net.torommo.logspy/test@42.314/net.torommo.logspy.Test",63 "net.torommo.logspy.Test"64 ),65 row("net.torommo.logspy//net.torommo.logspy.Test", "net.torommo.logspy.Test"),66 row("net.torommo.logspy/net.torommo.logspy.Test", "net.torommo.logspy.Test"),67 row("test@42.314/net.torommo.logspy.Test", "net.torommo.logspy.Test"),68 // Kotlin specific identifiers69 row("net.torommo.logspy.My exception", "net.torommo.logspy.My exception"),70 row("net.torommo.logspy.exception", "net.torommo.logspy.exception"),71 row("net.torommo logspy.Exception", "net.torommo logspy.Exception"),72 // Uncommon but valid Java identifiers73 row("net.torommo.logspy.exception", "net.torommo.logspy.exception"),74 row("net.torommo.logspy.Î", "net.torommo.logspy.Î"),75 row("net.torommoÎlogspy.Test", "net.torommoÎlogspy.Test")76 ) { value, expectedType ->77 val entry = content { stackTrace { this.type = value } }78 val events = parseToEvents(entry)79 events.forSingleton { it.exception?.type shouldBe expectedType }80 }81 }82 "maps exception message" - {83 forAll(84 row("Test message", "Test message"),85 row("", ""),86 row("\ttest\tmessage", "\ttest\tmessage"),87 row("Test: message", "Test: message") // Mimics the type prefix88 ) { literal, expected ->89 val entry = content { stackTrace { message = literal } }90 val events = parseToEvents(entry)91 events.forSingleton { it.exception?.message shouldBe expected }92 }93 }94 "maps missing exception message" - {95 val entry = content { stackTrace { message = null } }96 val events = parseToEvents(entry)97 events.forSingleton { it.exception?.message shouldBe null }98 }99 "maps multiline message" - {100 val entry = content { stackTrace { message = "test\nmessage\n" } }101 val events = parseToEvents(entry)102 events.forSingleton { it.exception?.message shouldBe "test\nmessage\n" }103 }104 "maps special chars in exception message" - {105 forAll(row("#"), row("`"), row(""""""")) { value ->106 val entry = content { stackTrace { message = value } }107 val events = parseToEvents(entry)108 events.forSingleton { it.exception?.message shouldBe value }109 }110 }111 "mapping favours message over type when ambiguous" - {112 val entry =113 content {114 stackTrace {115 type = "java.lang.String: exception"116 message = "Test message"117 }118 }119 val events = parseToEvents(entry)120 events.forSingleton {121 it.exception?.type shouldBe "java.lang.String"122 it.exception?.message shouldBe "exception: Test message"123 }124 }125 "mapping favours message over frames when multi line message is ambiguous" - {126 val entry =127 content {128 stackTrace {129 message = "test\n\tat something.Else"130 frame {131 declaringClass = "net.torommo.logspy.Anything"132 methodName = "toDo"133 fileName = "Anything.kt"134 line = "23"135 }136 }137 }138 val events = parseToEvents(entry)139 events.forSingleton {140 it.exception?.message shouldBe141 "test\n\t\n\n\t\tat something.Else\n\t\nat " +142 "net.torommo.logspy.Anything.toDo(Anything.kt:23)\n\t\n\t\t\n"143 }144 }145 "maps exception cause" - {146 val entry =147 content {148 stackTrace {149 cause {150 cause { message = "Causing causing exception" }151 message = "Causing exception"152 }153 }154 }155 val events = parseToEvents(entry)156 events.forSingleton {157 it.exception?.cause?.message shouldBe "Causing exception"158 it.exception?.cause?.cause?.message shouldBe "Causing causing exception"159 }160 }161 "maps type of cause" - {162 forAll(163 row("net.torommo.logspy.Test", "net.torommo.logspy.Test"),164 // Modules165 row(166 "net.torommo.logspy/test@42.314/net.torommo.logspy.Test",167 "net.torommo.logspy.Test"168 ),169 row("net.torommo.logspy//net.torommo.logspy.Test", "net.torommo.logspy.Test"),170 row("net.torommo.logspy/net.torommo.logspy.Test", "net.torommo.logspy.Test"),171 row("test@42.314/net.torommo.logspy.Test", "net.torommo.logspy.Test"),172 // Kotlin specific identifiers173 row("net.torommo.logspy.My exception", "net.torommo.logspy.My exception"),174 row("net.torommo.logspy.exception", "net.torommo.logspy.exception"),175 row("net.torommo logspy.Exception", "net.torommo logspy.Exception"),176 // Uncommon but valid Java identifiers177 row("net.torommo.logspy.exception", "net.torommo.logspy.exception"),178 row("net.torommo.logspy.Î", "net.torommo.logspy.Î"),179 row("net.torommoÎlogspy.Test", "net.torommoÎlogspy.Test")180 ) { value, expectedType ->181 val entry = content { stackTrace { cause { type = value } } }182 val events = parseToEvents(entry)183 events.forSingleton { it.exception?.cause?.type shouldBe expectedType }184 }185 }186 "maps type of cause when root cause first" - {187 forAll(188 row("net.torommo.logspy.Test", "net.torommo.logspy.Test"),189 // Modules190 row(191 "net.torommo.logspy/test@42.314/net.torommo.logspy.Test",192 "net.torommo.logspy.Test"193 ),194 row("net.torommo.logspy//net.torommo.logspy.Test", "net.torommo.logspy.Test"),195 row("net.torommo.logspy/net.torommo.logspy.Test", "net.torommo.logspy.Test"),196 row("test@42.314/net.torommo.logspy.Test", "net.torommo.logspy.Test"),197 // Kotlin specific identifiers198 row("net.torommo.logspy.My exception", "net.torommo.logspy.My exception"),199 row("net.torommo.logspy.exception", "net.torommo.logspy.exception"),200 row("net.torommo logspy.Exception", "net.torommo logspy.Exception"),201 // Uncommon but valid Java identifiers202 row("net.torommo.logspy.exception", "net.torommo.logspy.exception"),203 row("net.torommo.logspy.Î", "net.torommo.logspy.Î"),204 row("net.torommoÎlogspy.Test", "net.torommoÎlogspy.Test")205 ) { value, expectedType ->206 val entry = content {207 rootCauseFirstStackTrace {208 type = value209 cause {}210 }211 }212 val events = parseToEvents(entry)213 events.forSingleton { it.exception?.type shouldBe expectedType }214 }215 }216 "mapping favours message from cause over type when ambiguous" - {217 val entry =218 content {219 stackTrace {220 cause {221 type = "java.lang.String: exception"222 message = "Test message"223 }224 }225 }226 val events = parseToEvents(entry)227 events.forSingleton {228 it.exception?.cause?.type shouldBe "java.lang.String"229 it.exception?.cause?.message shouldBe "exception: Test message"230 }231 }232 "mapping favours message from cause over type when ambiguous and root cause first" - {233 val entry =234 content {235 rootCauseFirstStackTrace {236 type = "java.lang.String: exception"237 message = "Test message"238 cause {}239 }240 }241 val events = parseToEvents(entry)242 events.forSingleton {243 it.exception?.type shouldBe "java.lang.String"244 it.exception?.message shouldBe "exception: Test message"245 }246 }247 "mapping favours message from cause over frames when multiline message is ambiguous" - {248 val entry =249 content {250 stackTrace {251 cause {252 message = "Test\n\tat something else"253 frame {254 declaringClass = "net.torommo.logspy.Anything"255 methodName = "toDo"256 fileName = "Anything.kt"257 line = "23"258 }259 }260 }261 }262 val events = parseToEvents(entry)263 events.forSingleton {264 it.exception?.cause?.message shouldBe265 "Test\n\t\n\n\t\tat something else\n\t\nat " +266 "net.torommo.logspy.Anything.toDo(Anything.kt:23)\n\t\n\t\t\n"267 }268 }269 "mapping favours message from cause over frames when multiline message is ambiguous and " +270 "root cause first" - {271 val entry =272 content {273 rootCauseFirstStackTrace {274 message = "Test\n\tat something else"275 frame {276 declaringClass = "net.torommo.logspy.Anything"277 methodName = "toDo"278 fileName = "Anything.kt"279 line = "23"280 }281 cause {}282 }283 }284 val events = parseToEvents(entry)285 events.forSingleton {286 it.exception?.message shouldBe287 "Test\n\t\n\n\t\tat something else\n\t\nat " +288 "net.torommo.logspy.Anything.toDo(Anything.kt:23)\n\t\n\t\t\n"289 }290 }291 "maps suppressed exceptions" - {292 val entry =293 content {294 stackTrace {295 suppressed {296 message = "First suppressed exception 1"297 suppressed { this.message = "Suppressed suppressed exception" }298 }299 suppressed { this.message = "Second suppressed exception" }300 }301 }302 val events = parseToEvents(entry)303 events.forSingleton {304 it.exception305 ?.suppressed306 .forOne { suppressed ->307 suppressed.message shouldBe "First suppressed exception 1"308 suppressed.suppressed309 .forSingleton { suppressedSuppressed ->310 suppressedSuppressed.message shouldBe311 "Suppressed suppressed exception"312 }313 }314 it.exception315 ?.suppressed316 .forOne { suppressed ->317 suppressed.message shouldBe "Second suppressed exception"318 }319 }320 }321 "maps type from suppressed" - {322 forAll(323 row("net.torommo.logspy.Test", "net.torommo.logspy.Test"),324 // Modules325 row(326 "net.torommo.logspy/test@42.314/net.torommo.logspy.Test",327 "net.torommo.logspy.Test"328 ),329 row("net.torommo.logspy//net.torommo.logspy.Test", "net.torommo.logspy.Test"),330 row("net.torommo.logspy/net.torommo.logspy.Test", "net.torommo.logspy.Test"),331 row("test@42.314/net.torommo.logspy.Test", "net.torommo.logspy.Test"),332 // Kotlin specific identifiers333 row("net.torommo.logspy.My exception", "net.torommo.logspy.My exception"),334 row("net.torommo.logspy.exception", "net.torommo.logspy.exception"),335 row("net.torommo logspy.Exception", "net.torommo logspy.Exception"),336 // Uncommon but valid Java identifiers337 row("net.torommo.logspy.exception", "net.torommo.logspy.exception"),338 row("net.torommo.logspy.Î", "net.torommo.logspy.Î"),339 row("net.torommoÎlogspy.Test", "net.torommoÎlogspy.Test")340 ) { literal, expected ->341 val entry = content { stackTrace { suppressed { this.type = literal } } }342 val events = parseToEvents(entry)343 events.forSingleton {344 it.exception345 ?.suppressed346 .forSingleton { suppressed -> suppressed.type shouldBe expected }347 }348 }349 }350 "maps message from suppressed exceptions" - {351 forAll(352 row("Test message", "Test message"),353 row("\ttest\tmessage", "\ttest\tmessage"),354 row("Test: message", "Test: message") // Mimics the type prefix355 ) { literal, expected ->356 val entry = content { stackTrace { suppressed { this.message = literal } } }357 val events = parseToEvents(entry)358 events.forSingleton {359 it.exception360 ?.suppressed361 .forSingleton { suppressed -> suppressed.message shouldBe expected }362 }363 }364 }365 "maps missing message from suppressed exceptions" - {366 val entry = content { stackTrace { suppressed { this.message = null } } }367 val events = parseToEvents(entry)368 events.forSingleton {369 it.exception370 ?.suppressed371 .forSingleton { suppressed -> suppressed.message shouldBe null }372 }373 }374 "mapping favours message over type in suppressed when ambiguous" - {375 val entry =376 content {377 stackTrace {378 suppressed {379 type = "java.lang.String: exception"380 message = "Test message"381 }382 }383 }384 val events = parseToEvents(entry)385 events.forSingleton {386 it.exception387 ?.suppressed388 .forSingleton { suppressed ->389 suppressed.type shouldBe "java.lang.String"390 suppressed.message shouldBe "exception: Test message"391 }392 }393 }394 "maps multiline message in suppressed" - {395 val entry = content { stackTrace { suppressed { message = "test\nmessage\n" } } }396 val events = parseToEvents(entry)397 events.forSingleton {398 it.exception399 ?.suppressed400 .forSingleton { suppressed -> suppressed.message shouldBe "test\nmessage\n" }401 }402 }403 "mapping favours message over frames in suppressed when multi line message is ambiguous" - {404 val entry =405 content {406 stackTrace {407 suppressed {408 message = "test\n\tat something.Else"409 frame {410 declaringClass = "net.torommo.logspy.Anything"411 methodName = "toDo"412 fileName = "Anything.kt"413 line = "23"414 }415 }416 }417 }418 val events = parseToEvents(entry)419 events.forSingleton {420 it.exception421 ?.suppressed422 .forSingleton { suppresed ->423 suppresed.message shouldBe424 "test\n\t\nat something.Else\n\t\n\n\t\tat " +425 "net.torommo.logspy.Anything.toDo(Anything.kt:23)\n\t\n\t\t\n"426 }427 }428 }429 "maps stack from exception" - {430 val entry =431 content {432 stackTrace {433 frame {434 declaringClass = "net.torommo.logspy.TestA1"435 methodName = "testA1"436 }437 frame {438 declaringClass = "net.torommo.logspy.TestA2"439 methodName = "testA2"440 }441 }442 }443 val events = parseToEvents(entry)444 events.forSingleton {445 it.exception?.cause shouldBe null446 it.exception?.stackTrace shouldContainExactly447 listOf(448 StackTraceElementSnapshot("net.torommo.logspy.TestA1", "testA1"),449 StackTraceElementSnapshot("net.torommo.logspy.TestA2", "testA2")450 )451 }452 }453 "maps type in stack frame from exception" - {454 forAll(455 row("net.torommo.logspy.Test", "net.torommo.logspy.Test"),456 // Modules457 row(458 "net.torommo.logspy/test@42.314/net.torommo.logspy.Test",459 "net.torommo.logspy.Test"460 ),461 row("net.torommo.logspy//net.torommo.logspy.Test", "net.torommo.logspy.Test"),462 row("net.torommo.logspy/net.torommo.logspy.Test", "net.torommo.logspy.Test"),463 row("test@42.314/net.torommo.logspy.Test", "net.torommo.logspy.Test"),464 // Uncommon but valid Java identifiers465 row("net.torommo.logspy.exception", "net.torommo.logspy.exception"),466 row("net.torommo.logspy.Î", "net.torommo.logspy.Î"),467 row("net.torommoÎlogspy.Test", "net.torommoÎlogspy.Test")468 ) { value, expected ->469 val entry = content { stackTrace { frame { declaringClass = value } } }470 val events = parseToEvents(entry)471 events.forSingleton {472 it.exception473 ?.stackTrace474 .forSingleton { stackTrace -> stackTrace.declaringClass shouldBe expected }475 }476 }477 }478 "mapping favours method name over type in stack when ambiguous" - {479 val entry =480 content {481 stackTrace {482 frame {483 declaringClass = "net.torommo.logspy.This is"484 this.methodName = "a test"485 }486 }487 }488 val events = parseToEvents(entry)489 events.forSingleton {490 it.exception?.stackTrace shouldContainExactly491 listOf(StackTraceElementSnapshot("net.torommo.logspy", "This is.a test"))492 }493 }494 "maps method name with substring that resembles type but with unusual codepoints for type" -495 {496 forAll(497 // Empty space498 row("test Test.testmethod"),499 // Parentheses500 row("Te(st"),501 row("Te)st"),502 // Mimics ellipsis in combination with the dot separator between type and method503 row("..test")504 ) { methodName ->505 val entry =506 content {507 stackTrace {508 frame {509 declaringClass = "Test"510 this.methodName = methodName511 }512 }513 }514 val events = parseToEvents(entry)515 events.forSingleton {516 it.exception517 ?.stackTrace518 .forSingleton { stackTrace ->519 stackTrace.methodName shouldBe methodName520 }521 }522 }523 }524 "maps method name with substring that resembles location" - {525 forAll(row("Test(Test.kt:10)"), row("(Test.kt:10)"), row("(Test.kt)")) { methodName ->526 val entry = content { stackTrace { frame { this.methodName = methodName } } }527 val events = parseToEvents(entry)528 events.forSingleton {529 it.exception530 ?.stackTrace531 .forSingleton { stackTrace -> stackTrace.methodName shouldBe methodName }532 }533 }534 }535 "maps frame without class, method name, file, and line" - {536 val entry =537 content {538 stackTrace {539 frame {540 declaringClass = ""541 methodName = ""542 fileName = ""543 line = ""544 }545 }546 }547 val events = parseToEvents(entry)548 events.forSingleton {549 it.exception?.stackTrace shouldContainExactly550 listOf(StackTraceElementSnapshot("", ""))551 }552 }553 "ignores line number in frame" - {554 forAll(row("42"), row(""), row("2147483647")) { lineNumber ->555 val entry = content { stackTrace { frame { line = lineNumber } } }556 parseToEvents(entry).shouldNotBeEmpty()557 }558 }559 "ignores source in frame" - {560 forAll(row("Test.java"), row("("), row(")"), row(":")) { name ->561 val entry = content { stackTrace { frame { fileName = name } } }562 parseToEvents(entry).shouldNotBeEmpty()563 }564 }565 "maps stack from causal chain" - {566 val entry =567 content {568 stackTrace {569 cause {570 cause {571 frame {572 declaringClass = "net.torommo.logspy.TestB1"573 methodName = "testB1"574 }575 frame {576 declaringClass = "net.torommo.logspy.TestB2"577 methodName = "testB2"578 }579 }580 frame {581 declaringClass = "net.torommo.logspy.TestA"582 methodName = "testA"583 }584 }585 }586 }587 val events = parseToEvents(entry)588 events.forSingleton {589 it.exception?.cause?.stackTrace shouldContainExactly590 listOf(StackTraceElementSnapshot("net.torommo.logspy.TestA", "testA"))591 it.exception?.cause?.cause?.cause shouldBe null592 it.exception?.cause?.cause?.stackTrace shouldContainExactly593 listOf(594 StackTraceElementSnapshot("net.torommo.logspy.TestB1", "testB1"),595 StackTraceElementSnapshot("net.torommo.logspy.TestB2", "testB2")596 )597 }598 }599 "maps stack from causal chain when root cause is first" - {600 val entry =601 content {602 rootCauseFirstStackTrace {603 cause {604 cause {605 frame {606 declaringClass = "net.torommo.logspy.TestB1"607 methodName = "testB1"608 }609 frame {610 declaringClass = "net.torommo.logspy.TestB2"611 methodName = "testB2"612 }613 }614 frame {615 declaringClass = "net.torommo.logspy.TestA"616 methodName = "testA"617 }618 }619 }620 }621 val events = parseToEvents(entry)622 events.forSingleton {623 it.exception?.cause?.stackTrace shouldContainExactly624 listOf(StackTraceElementSnapshot("net.torommo.logspy.TestA", "testA"))625 it.exception?.cause?.cause?.cause shouldBe null626 it.exception?.cause?.cause?.stackTrace shouldContainExactly627 listOf(628 StackTraceElementSnapshot("net.torommo.logspy.TestB1", "testB1"),629 StackTraceElementSnapshot("net.torommo.logspy.TestB2", "testB2")630 )631 }632 }633 "maps stack from suppressed" - {634 val entry =635 content {636 stackTrace {637 suppressed {638 suppressed {639 frame {640 declaringClass = "net.torommo.logspy.TestB1"641 methodName = "testB1"642 }643 frame {644 declaringClass = "net.torommo.logspy.TestB2"645 methodName = "testB2"646 }647 }648 frame {649 declaringClass = "net.torommo.logspy.TestA"650 methodName = "testA"651 }652 }653 }654 }655 val events = parseToEvents(entry)656 events.forSingleton {657 it.exception658 ?.suppressed659 .forSingleton { suppressed ->660 suppressed.cause shouldBe null661 suppressed.stackTrace shouldContainExactly662 listOf(StackTraceElementSnapshot("net.torommo.logspy.TestA", "testA"))663 suppressed.suppressed664 .forSingleton { suppressedSuppressed ->665 suppressedSuppressed.cause shouldBe null666 suppressedSuppressed.stackTrace shouldContainExactly667 listOf(668 StackTraceElementSnapshot(669 "net.torommo.logspy.TestB1",670 "testB1"671 ),672 StackTraceElementSnapshot(673 "net.torommo.logspy.TestB2",674 "testB2"675 )676 )677 }678 }679 }680 }681 "ignores omitted frames" - {682 val entry =683 content {684 stackTrace {685 frame { declaringClass = "net.torommo.logspy.Test" }686 omittedFrame {}687 }688 }689 val events = parseToEvents(entry)690 events.forSingleton {691 it.exception692 ?.stackTrace693 .forSingleton { stackTrace ->694 stackTrace.declaringClass shouldBe "net.torommo.logspy.Test"695 }696 }697 }698 "maps mdc" - {699 forAll(700 row(content {}),701 row(content { stackTrace {} }),702 row(content { complexField("test") }),703 row(content { marker("testMarker") })704 ) { configuration ->705 val entry =706 configuration.merge(707 content {708 field("test-key-1", "test-value-1")709 field("test-key-2", "test-value-2")710 }711 )712 val events = parseToEvents(entry)713 events.forSingleton {714 it.mdc shouldContainExactly715 mapOf("test-key-1" to "test-value-1", "test-key-2" to "test-value-2")716 }717 }718 }719 "ignores lines without json" - {720 forAll(row("garbled\n"), row("\n"), row("""{""""" + "\n")) { payload ->721 val entry1 = content {722 loggerName = "TestLogger"723 message = "Test 1"724 }.asSource()725 val entry2 = content {726 loggerName = "TestLogger"727 message = "Test 2"728 }.asSource()729 val events = JsonEventParser("TestLogger", "$entry1$payload$entry2").events()730 events.forOne { it.message shouldBe "Test 1" }731 events.forOne { it.message shouldBe "Test 2" }732 }733 }734 "ignores lines with non logstash json" - {735 forAll(736 // logger name missing737 row("""{"level": "INFO"}"""" + "\n"),738 // level missing739 row(""""{"logger_name": "TestLogger"}""" + "\n")740 ) { payload ->741 val entry1 = content {742 loggerName = "TestLogger"743 message = "Test 1"744 }.asSource()745 val entry2 = content {746 loggerName = "TestLogger"747 message = "Test 2"748 }.asSource()749 val events = JsonEventParser("TestLogger", "$entry1$payload$entry2").events()750 events.forOne { it.message shouldBe "Test 1" }751 events.forOne { it.message shouldBe "Test 2" }752 }753 }754 }755 private fun (JsonEntryBuilder.() -> Unit).asSource(): String {756 return JsonEntryBuilder().apply(this).build()757 }758 private fun <T> (T.() -> Unit).merge(block: T.() -> Unit): T.() -> Unit {759 return fun T.() {760 this@merge(this)761 block(this)762 }763 }764 private fun content(block: JsonEntryBuilder.() -> Unit): JsonEntryBuilder.() -> Unit {765 return block766 }767 private fun parseToEvents(768 block: JsonEntryBuilder.() -> Unit,769 loggerName: String = "net.torommo.logspy.LogSpyExtensionIntegrationTest"770 ): List<SpiedEvent> {771 return JsonEventParser(loggerName, block.asSource()).events()772 }773 @DslMarker774 annotation class JsonEntryDsl775 @JsonEntryDsl776 internal class JsonEntryBuilder {777 var level: String = "INFO"778 var message: String = "Test message"779 var loggerName: String = "net.torommo.logspy.LogSpyExtensionIntegrationTest"780 private var stackTrace: StackTraceBuilder? = null781 private val simpleAdditionalFields: MutableMap<String, String> = mutableMapOf()782 private val nestedAdditionalFields: MutableSet<String> = mutableSetOf()783 private val markers: MutableSet<String> = mutableSetOf()784 fun stackTrace(block: (StackTraceBuilder.() -> Unit)?) {785 if (block == null) {786 this.stackTrace = null787 } else {788 this.stackTrace = StackTraceBuilder().apply(block)789 }790 }791 fun rootCauseFirstStackTrace(block: (StackTraceBuilder.() -> Unit)?) {792 if (block == null) {793 this.stackTrace = null794 } else {795 this.stackTrace = StackTraceBuilder(rootCauseFirst = true).apply(block)796 }797 }798 fun field(key: String, value: String) {799 nestedAdditionalFields.remove(key)800 simpleAdditionalFields.put(key, value)801 }802 fun complexField(key: String) {803 simpleAdditionalFields.remove(key)804 nestedAdditionalFields.add(key)805 }806 fun marker(marker: String) {807 markers.add(marker)808 }809 fun build(): String {810 val jsonObject = JsonObject()811 jsonObject.addProperty("@timestamp", "2019-10-31T20:31:17.234+01:00")812 jsonObject.addProperty("@version", "1")813 jsonObject.addProperty("message", message)814 jsonObject.addProperty("logger_name", loggerName)815 jsonObject.addProperty("thread_name", "main")816 jsonObject.addProperty("level", level)817 jsonObject.addProperty("level_value", 20000)818 stackTrace?.let { jsonObject.addProperty("stack_trace", it.build()) }819 addAdditionalFieldsTo(jsonObject)820 addMarkersTo(jsonObject)821 return GsonBuilder().create().toJson(jsonObject) + "\n"822 }823 private fun addAdditionalFieldsTo(target: JsonObject) {824 simpleAdditionalFields.forEach { target.addProperty(it.key, it.value) }825 nestedAdditionalFields.forEach {826 val nestedObject = JsonObject()827 nestedObject.addProperty("value", "test")828 target.add(it, nestedObject)829 }830 }831 private fun addMarkersTo(target: JsonObject) {832 if (!markers.isEmpty()) {833 val tags = JsonArray()834 markers.forEach { tags.add(it) }835 target.add("tags", tags)836 }837 }838 }839 @JsonEntryDsl840 internal class StackTraceBuilder(val rootCauseFirst: Boolean = false) {841 var type: String = "java.lang.RuntimeException"842 var message: String? = null843 private var cause: StackTraceBuilder? = null844 private val suppressed: MutableList<StackTraceBuilder> = mutableListOf()845 private val frames: MutableList<FrameBuilder> = mutableListOf()846 fun cause(block: (StackTraceBuilder.() -> Unit)?) {847 if (block == null) {848 this.cause = null849 } else {850 this.cause = StackTraceBuilder(rootCauseFirst).apply(block)851 }852 }853 fun suppressed(block: StackTraceBuilder.() -> Unit) {854 this.suppressed.add(StackTraceBuilder(rootCauseFirst).apply(block))855 }856 fun frame(block: FilledFrameBuilder.() -> Unit) {857 this.frames.add(FilledFrameBuilder().apply(block))858 }859 fun frameWithUnknownSource(block: UnknownSourceFrameBuilder.() -> Unit) {860 this.frames.add(UnknownSourceFrameBuilder().apply(block))861 }862 fun omittedFrame(block: OmittedFrameBuilder.() -> Unit) {863 this.frames.add(OmittedFrameBuilder().apply(block))864 }865 fun build(): String {866 return build(0)867 }868 private fun build(indent: Int): String {869 return if (rootCauseFirst) {870 buildRootCauseFirst(indent)871 } else {872 buildRootCauseLast(indent, true)873 }874 }875 private fun buildRootCauseFirst(indent: Int): String {876 val prefix = if (cause == null) {877 ""878 } else {879 "Wrapped by: "880 }881 val header = if (message == null) "${type}\n" else "${type}: ${message}\n"882 val stack = frames.asSequence().map { it.build(indent) }.joinToString("")883 val suppressed =884 this.suppressed885 .asSequence()886 .map { "${"\t".repeat(indent + 1)}Suppressed: ${it.build(indent + 1)}" }887 .joinToString("")888 return "${cause?.buildRootCauseFirst(indent) ?: ""}${"\t".repeat(indent)}${prefix}" +889 "${header}${stack}${suppressed}"890 }891 private fun buildRootCauseLast(indent: Int, root: Boolean): String {892 val prefix = if (root) {893 ""894 } else {895 "Caused by: "896 }897 val header = if (message == null) "${type}\n" else "${type}: ${message}\n"898 val stack = frames.asSequence().map { it.build(indent) }.joinToString("")899 val suppressed =900 this.suppressed901 .asSequence()902 .map { "${"\t".repeat(indent + 1)}Suppressed: ${it.build(indent + 1)}" }903 .joinToString("")904 return "${prefix}${header}${stack}${suppressed}" +905 "${cause?.buildRootCauseLast(indent, false) ?: ""}"906 }907 }908 internal interface FrameBuilder {909 fun build(indent: Int): String910 }911 @JsonEntryDsl912 internal class FilledFrameBuilder : FrameBuilder {913 var declaringClass: String = "net.torommo.logspy.Test"914 var methodName: String = "test"915 var fileName: String = "Test.java"916 var line: String = "123"917 override fun build(indent: Int): String {918 if (line.isEmpty()) {919 return "${"\t".repeat(indent + 1)}at ${declaringClass}.${methodName}(${fileName})\n"920 } else {921 return "${"\t".repeat(indent + 1)}at ${declaringClass}.${methodName}(${fileName}:" +922 "${line})\n"923 }924 }925 }926 @JsonEntryDsl927 internal class OmittedFrameBuilder : FrameBuilder {928 override fun build(indent: Int): String {929 return "${"\t".repeat(indent + 1)}... 42 common frames ommited\n"930 }931 }932 @JsonEntryDsl933 internal class UnknownSourceFrameBuilder : FrameBuilder {934 var declaringClass: String = "net.torommo.logspy.Test"935 var methodName: String = "test"936 var line: String = "123"937 override fun build(indent: Int): String {938 return FilledFrameBuilder()939 .apply {940 declaringClass = this@UnknownSourceFrameBuilder.declaringClass941 methodName = this@UnknownSourceFrameBuilder.methodName942 line = this@UnknownSourceFrameBuilder.line943 }944 .build(indent)945 }946 }947}948infix fun <T : Collection<U>?, U> T.forOne(fn: (U) -> Unit) {949 when {950 this == null -> {951 throw failure("collection was null")952 }953 else -> {954 this.forOne(fn)955 }956 }957}958infix fun <T : Collection<U>?, U> T.forSingleton(fn: (U) -> Unit) {959 when {960 this == null -> {961 throw failure("collection was null")962 }963 this.size > 1 -> {964 throw failure("collection contained more than one element")965 }966 else -> {967 this.forOne(fn)968 }969 }970}...
InspectorAliasTest.kt
Source:InspectorAliasTest.kt
1package com.sksamuel.kotest2import io.kotest.assertions.throwables.shouldThrowAny3import io.kotest.core.spec.style.FunSpec4import io.kotest.inspectors.shouldForAll5import io.kotest.inspectors.shouldForAny6import io.kotest.inspectors.shouldForAtLeast7import io.kotest.inspectors.shouldForAtLeastOne8import io.kotest.inspectors.shouldForAtMost9import io.kotest.inspectors.shouldForAtMostOne10import io.kotest.inspectors.shouldForExactly11import io.kotest.inspectors.shouldForNone12import io.kotest.inspectors.shouldForOne13import io.kotest.inspectors.shouldForSome14import io.kotest.matchers.comparables.shouldBeGreaterThan15import io.kotest.matchers.ints.shouldBeLessThan16import io.kotest.matchers.shouldBe17class InspectorAliasTest : FunSpec({18 val array = arrayOf(1, 2, 3)19 val list = listOf(1, 2, 3)20 val sequence = sequenceOf(1, 2, 3)21 context("forAll") {22 fun block(x: Int) = x shouldBeGreaterThan 023 test("array") {24 array.shouldForAll {25 it shouldBeLessThan 426 }27 shouldThrowAny {28 array.shouldForAll {29 it shouldBeLessThan 330 }31 }32 }33 test("list") {34 list.shouldForAll(::block)35 shouldThrowAny {36 list.shouldForAll {37 it shouldBeLessThan 338 }39 }40 }41 test("sequence") {42 sequence.shouldForAll(::block)43 shouldThrowAny {44 sequence.shouldForAll {45 it shouldBeLessThan 346 }47 }48 }49 }50 context("forOne") {51 fun block(x: Int) = x shouldBe 252 test("array") {53 array.shouldForOne(::block)54 shouldThrowAny {55 array.shouldForOne {56 it shouldBeLessThan 157 }58 }59 }60 test("list") {61 list.shouldForOne(::block)62 shouldThrowAny {63 list.shouldForOne {64 it shouldBeLessThan 165 }66 }67 }68 test("sequence") {69 sequence.shouldForOne(::block)70 shouldThrowAny {71 sequence.shouldForOne {72 it shouldBeLessThan 173 }74 }75 }76 }77 context("forExactly") {78 fun block(x: Int) = x shouldBeGreaterThan 179 val n = 280 test("array") {81 array.shouldForExactly(n, ::block)82 shouldThrowAny {83 array.shouldForExactly(n) {84 it shouldBeLessThan 185 }86 }87 }88 test("list") {89 list.shouldForExactly(n, ::block)90 shouldThrowAny {91 list.shouldForExactly(n) {92 it shouldBeLessThan 193 }94 }95 }96 test("sequence") {97 sequence.shouldForExactly(n, ::block)98 shouldThrowAny {99 sequence.shouldForExactly(n) {100 it shouldBeLessThan 1101 }102 }103 }104 }105 context("forSome") {106 fun block(x: Int) = x shouldBeGreaterThan 2107 test("array") {108 array.shouldForSome(::block)109 shouldThrowAny {110 array.shouldForSome {111 it shouldBeLessThan 1112 }113 }114 }115 test("list") {116 list.shouldForSome(::block)117 shouldThrowAny {118 list.shouldForSome {119 it shouldBeLessThan 1120 }121 }122 }123 test("sequence") {124 sequence.shouldForSome(::block)125 shouldThrowAny {126 sequence.shouldForSome {127 it shouldBeLessThan 1128 }129 }130 }131 }132 context("forAny") {133 fun block(x: Int) = x shouldBeGreaterThan 0134 test("array") {135 array.shouldForAny(::block)136 shouldThrowAny {137 array.shouldForAny {138 it shouldBeLessThan 1139 }140 }141 }142 test("list") {143 list.shouldForAny(::block)144 shouldThrowAny {145 list.shouldForAny {146 it shouldBeLessThan 1147 }148 }149 }150 test("sequence") {151 sequence.shouldForAny(::block)152 shouldThrowAny {153 sequence.shouldForAny {154 it shouldBeLessThan 1155 }156 }157 }158 }159 context("forAtLeast") {160 fun block(x: Int) = x shouldBeGreaterThan 0161 val n = 3162 test("array") {163 array.shouldForAtLeast(n, ::block)164 shouldThrowAny {165 array.shouldForAtLeast(n) {166 it shouldBeLessThan 3167 }168 }169 }170 test("list") {171 list.shouldForAtLeast(n, ::block)172 shouldThrowAny {173 list.shouldForAtLeast(n) {174 it shouldBeLessThan 3175 }176 }177 }178 test("sequence") {179 sequence.shouldForAtLeast(n, ::block)180 shouldThrowAny {181 sequence.shouldForAtLeast(n) {182 it shouldBeLessThan 3183 }184 }185 }186 }187 context("forAtLeastOne") {188 fun block(x: Int) = x shouldBeGreaterThan 0189 test("array") {190 array.shouldForAtLeastOne(::block)191 shouldThrowAny {192 array.shouldForAtLeastOne {193 it shouldBeLessThan 1194 }195 }196 }197 test("list") {198 list.shouldForAtLeastOne(::block)199 shouldThrowAny {200 list.shouldForAtLeastOne {201 it shouldBeLessThan 1202 }203 }204 }205 test("sequence") {206 sequence.shouldForAtLeastOne(::block)207 shouldThrowAny {208 sequence.shouldForAtLeastOne {209 it shouldBeLessThan 1210 }211 }212 }213 }214 context("forAtMost") {215 fun block(x: Int) = x shouldBeGreaterThan 0216 test("array") {217 val arr = arrayOf(0, 0, 1)218 arr.shouldForAtMost(1, ::block)219 shouldThrowAny {220 arr.shouldForAtMost(1) {221 it shouldBeLessThan 3222 }223 }224 }225 test("list") {226 val l = listOf(0, 1, 1)227 l.shouldForAtMost(2, ::block)228 shouldThrowAny {229 l.shouldForAtMost(2) {230 it shouldBeLessThan 3231 }232 }233 }234 test("sequence") {235 sequence.shouldForAtMost(3, ::block)236 shouldThrowAny {237 sequence.shouldForAtMost(2) {238 it shouldBeLessThan 4239 }240 }241 }242 }243 context("forNone") {244 fun block(x: Int) = x shouldBeLessThan 1245 test("array") {246 array.shouldForNone(::block)247 shouldThrowAny {248 array.shouldForNone {249 it shouldBeLessThan 4250 }251 }252 }253 test("list") {254 list.shouldForNone(::block)255 shouldThrowAny {256 list.shouldForNone {257 it shouldBeLessThan 4258 }259 }260 }261 test("sequence") {262 sequence.shouldForNone(::block)263 shouldThrowAny {264 sequence.shouldForNone {265 it shouldBeLessThan 4266 }267 }268 }269 }270 context("forAtMostOne") {271 fun block(x: Int) = x shouldBe 1272 test("array") {273 array.shouldForAtMostOne(::block)274 }275 test("list") {276 list.shouldForAtMostOne(::block)277 }278 test("sequence") {279 sequence.shouldForAtMostOne(::block)280 }281 }282})...
InspectorsTest.kt
Source:InspectorsTest.kt
1package com.sksamuel.kotest2import io.kotest.assertions.throwables.shouldThrow3import io.kotest.core.spec.style.WordSpec4import io.kotest.inspectors.forAny5import io.kotest.inspectors.forExactly6import io.kotest.inspectors.forNone7import io.kotest.inspectors.forOne8import io.kotest.inspectors.forSome9import io.kotest.matchers.comparables.beGreaterThan10import io.kotest.matchers.comparables.beLessThan11import io.kotest.matchers.should12import io.kotest.matchers.shouldBe13import io.kotest.matchers.shouldNotBe14class InspectorsTest : WordSpec() {15 private val list = listOf(1, 2, 3, 4, 5)16 private val array = arrayOf(1, 2, 3, 4, 5)17 private val charSequence = "charSequence"18 init {19 "forNone" should {20 "pass if no elements pass fn test for a list" {21 list.forNone {22 it shouldBe 1023 }24 }25 "pass if no elements pass fn test for a char sequence" {26 charSequence.forNone {27 it shouldBe 'x'28 }29 }30 "pass if no elements pass fn test for an array" {31 array.forNone {32 it shouldBe 1033 }34 }35 "fail if one elements passes fn test" {36 shouldThrow<AssertionError> {37 list.forNone {38 it shouldBe 439 }40 }.message shouldBe """1 elements passed but expected 041The following elements passed:42443The following elements failed:441 => expected:<4> but was:<1>452 => expected:<4> but was:<2>463 => expected:<4> but was:<3>475 => expected:<4> but was:<5>"""48 }49 "fail if all elements pass fn test" {50 shouldThrow<AssertionError> {51 list.forNone {52 it should beGreaterThan(0)53 }54 }.message shouldBe """5 elements passed but expected 055The following elements passed:56157258359460561The following elements failed:62--none--"""63 }64 }65 "forSome" should {66 "pass if one elements pass test" {67 list.forSome {68 it shouldBe 369 }70 }71 "pass if size-1 elements pass test" {72 list.forSome {73 it should beGreaterThan(1)74 }75 }76 "pass if two elements pass test for a char sequence" {77 charSequence.forSome {78 it shouldBe 'c'79 }80 }81 "fail if no elements pass test" {82 shouldThrow<AssertionError> {83 array.forSome {84 it should beLessThan(0)85 }86 }.message shouldBe """No elements passed but expected at least one87The following elements passed:88--none--89The following elements failed:901 => 1 should be < 0912 => 2 should be < 0923 => 3 should be < 0934 => 4 should be < 0945 => 5 should be < 0"""95 }96 "fail if all elements pass test" {97 shouldThrow<AssertionError> {98 list.forSome {99 it should beGreaterThan(0)100 }101 }.message shouldBe """All elements passed but expected < 5102The following elements passed:10311042105310641075108The following elements failed:109--none--"""110 }111 }112 "forOne" should {113 "pass if one elements pass test" {114 list.forOne {115 it shouldBe 3116 }117 }118 "fail if all elements pass test for a char sequence" {119 shouldThrow<AssertionError> {120 charSequence.forOne { t ->121 t shouldNotBe 'X'122 }123 }.message shouldBe """12 elements passed but expected 1124The following elements passed:125c126h127a128r129S130e131q132u133e134n135... and 2 more passed elements136The following elements failed:137--none--"""138 }139 "fail if > 1 elements pass test" {140 shouldThrow<AssertionError> {141 list.forOne { t ->142 t should beGreaterThan(2)143 }144 }.message shouldBe """3 elements passed but expected 1145The following elements passed:146314741485149The following elements failed:1501 => 1 should be > 21512 => 2 should be > 2"""152 }153 "fail if no elements pass test" {154 shouldThrow<AssertionError> {155 array.forOne { t ->156 t shouldBe 22157 }158 }.message shouldBe """0 elements passed but expected 1159The following elements passed:160--none--161The following elements failed:1621 => expected:<22> but was:<1>1632 => expected:<22> but was:<2>1643 => expected:<22> but was:<3>1654 => expected:<22> but was:<4>1665 => expected:<22> but was:<5>"""167 }168 }169 "forAny" should {170 "pass if one elements pass test" {171 list.forAny { t ->172 t shouldBe 3173 }174 }175 "pass if at least elements pass test" {176 list.forAny { t ->177 t should beGreaterThan(2)178 }179 }180 "pass if all elements pass test for a char sequence" {181 charSequence.forAny {182 it shouldNotBe 'X'183 }184 }185 "fail if no elements pass test" {186 shouldThrow<AssertionError> {187 array.forAny { t ->188 t shouldBe 6189 }190 }.message shouldBe """0 elements passed but expected at least 1191The following elements passed:192--none--193The following elements failed:1941 => expected:<6> but was:<1>1952 => expected:<6> but was:<2>1963 => expected:<6> but was:<3>1974 => expected:<6> but was:<4>1985 => expected:<6> but was:<5>"""199 }200 }201 "forExactly" should {202 "pass if exactly k elements pass" {203 list.forExactly(2) { t ->204 t should beLessThan(3)205 }206 }207 "pass if exactly k elements pass for a char sequence" {208 charSequence.forExactly(1) {209 it shouldBe 'h'210 }211 }212 "fail if more elements pass test" {213 shouldThrow<AssertionError> {214 list.forExactly(2) { t ->215 t should beGreaterThan(2)216 }217 }.message shouldBe """3 elements passed but expected 2218The following elements passed:219322042215222The following elements failed:2231 => 1 should be > 22242 => 2 should be > 2"""225 }226 "fail if less elements pass test" {227 shouldThrow<AssertionError> {228 array.forExactly(2) { t ->229 t should beLessThan(2)230 }231 }.message shouldBe """1 elements passed but expected 2232The following elements passed:2331234The following elements failed:2352 => 2 should be < 22363 => 3 should be < 22374 => 4 should be < 22385 => 5 should be < 2"""239 }240 "fail if no elements pass test" {241 shouldThrow<AssertionError> {242 list.forExactly(2) { t ->243 t shouldBe 33244 }245 }.message shouldBe """0 elements passed but expected 2246The following elements passed:247--none--248The following elements failed:2491 => expected:<33> but was:<1>2502 => expected:<33> but was:<2>2513 => expected:<33> but was:<3>2524 => expected:<33> but was:<4>2535 => expected:<33> but was:<5>"""254 }255 }256 }257}...
Inspectors.kt
Source:Inspectors.kt
1package com.github.shwaka.kotest.inspectors2import io.kotest.inspectors.ElementPass3import io.kotest.inspectors.runTests4fun <T> Sequence<T>.forAll(fn: (T) -> Unit) = toList().forAll(fn)5fun <T> Array<T>.forAll(fn: (T) -> Unit) = asList().forAll(fn)6fun <T> Collection<T>.forAll(fn: (T) -> Unit) {7 val results = runTests(this, fn)8 val passed = results.filterIsInstance<ElementPass<T>>()9 if (passed.size < this.size) {10 val msg = "${passed.size} elements passed but expected ${this.size}"11 buildAssertionError(msg, results)12 }13}14fun <T> Sequence<T>.forOne(fn: (T) -> Unit) = toList().forOne(fn)15fun <T> Array<T>.forOne(fn: (T) -> Unit) = asList().forOne(fn)16fun <T> Collection<T>.forOne(fn: (T) -> Unit) = forExactly(1, fn)17fun <T> Sequence<T>.forExactly(k: Int, fn: (T) -> Unit) = toList().forExactly(k, fn)18fun <T> Array<T>.forExactly(k: Int, fn: (T) -> Unit) = toList().forExactly(k, fn)19fun <T> Collection<T>.forExactly(k: Int, fn: (T) -> Unit) {20 val results = runTests(this, fn)21 val passed = results.filterIsInstance<ElementPass<T>>()22 if (passed.size != k) {23 val msg = "${passed.size} elements passed but expected $k"24 buildAssertionError(msg, results)25 }26}27fun <T> Sequence<T>.forSome(fn: (T) -> Unit) = toList().forSome(fn)28fun <T> Array<T>.forSome(fn: (T) -> Unit) = toList().forSome(fn)29fun <T> Collection<T>.forSome(fn: (T) -> Unit) {30 val results = runTests(this, fn)31 val passed = results.filterIsInstance<ElementPass<T>>()32 if (passed.isEmpty()) {33 buildAssertionError("No elements passed but expected at least one", results)34 } else if (passed.size == size) {35 buildAssertionError("All elements passed but expected < $size", results)36 }37}38fun <T> Sequence<T>.forAny(fn: (T) -> Unit) = toList().forAny(fn)39fun <T> Array<T>.forAny(fn: (T) -> Unit) = toList().forAny(fn)40fun <T> Collection<T>.forAny(fn: (T) -> Unit) = forAtLeastOne(fn)41fun <T> Sequence<T>.forAtLeastOne(fn: (T) -> Unit) = toList().forAtLeastOne(fn)42fun <T> Array<T>.forAtLeastOne(fn: (T) -> Unit) = toList().forAtLeastOne(fn)43fun <T> Collection<T>.forAtLeastOne(f: (T) -> Unit) = forAtLeast(1, f)44fun <T> Sequence<T>.forAtLeast(k: Int, fn: (T) -> Unit) = toList().forAtLeast(k, fn)45fun <T> Array<T>.forAtLeast(k: Int, fn: (T) -> Unit) = toList().forAtLeast(k, fn)46fun <T> Collection<T>.forAtLeast(k: Int, fn: (T) -> Unit) {47 val results = runTests(this, fn)48 val passed = results.filterIsInstance<ElementPass<T>>()49 if (passed.size < k) {50 val msg = "${passed.size} elements passed but expected at least $k"51 buildAssertionError(msg, results)52 }53}54fun <T> Sequence<T>.forAtMostOne(fn: (T) -> Unit) = toList().forAtMostOne(fn)55fun <T> Array<T>.forAtMostOne(fn: (T) -> Unit) = toList().forAtMostOne(fn)56fun <T> Collection<T>.forAtMostOne(fn: (T) -> Unit) = forAtMost(1, fn)57fun <T> Sequence<T>.forAtMost(k: Int, fn: (T) -> Unit) = toList().forAtMost(k, fn)58fun <T> Array<T>.forAtMost(k: Int, fn: (T) -> Unit) = toList().forAtMost(k, fn)59fun <T> Collection<T>.forAtMost(k: Int, fn: (T) -> Unit) {60 val results = runTests(this, fn)61 val passed = results.filterIsInstance<ElementPass<T>>()62 if (passed.size > k) {63 val msg = "${passed.size} elements passed but expected at most $k"64 buildAssertionError(msg, results)65 }66}67fun <T> Sequence<T>.forNone(fn: (T) -> Unit) = toList().forNone(fn)68fun <T> Array<T>.forNone(fn: (T) -> Unit) = toList().forNone(fn)69fun <T> Collection<T>.forNone(f: (T) -> Unit) {70 val results = runTests(this, f)71 val passed = results.filterIsInstance<ElementPass<T>>()72 if (passed.isNotEmpty()) {73 val msg = "${passed.size} elements passed but expected ${0}"74 buildAssertionError(msg, results)75 }76}...
InspectorAliases.kt
Source:InspectorAliases.kt
1package io.kotest.inspectors2/** Alias for [Sequence.forAll] */3fun <T> Sequence<T>.shouldForAll(fn: (T) -> Unit) = forAll(fn)4/** Alias for [Array.forAll] */5fun <T> Array<T>.shouldForAll(fn: (T) -> Unit) = forAll(fn)6/** Alias for [Collection.forAll] */7fun <T> Collection<T>.shouldForAll(fn: (T) -> Unit) = forAll(fn)8/** Alias for [Sequence.forOne] */9fun <T> Sequence<T>.shouldForOne(fn: (T) -> Unit) = forOne(fn)10/** Alias for [Array.forOne] */11fun <T> Array<T>.shouldForOne(fn: (T) -> Unit) = forOne(fn)12/** Alias for [Collection.forOne] */13fun <T> Collection<T>.shouldForOne(fn: (T) -> Unit) = forOne(fn)14/** Alias for [Sequence.forExactly] */15fun <T> Sequence<T>.shouldForExactly(k: Int, fn: (T) -> Unit) = forExactly(k, fn)16/** Alias for [Array.forExactly] */17fun <T> Array<T>.shouldForExactly(k: Int, fn: (T) -> Unit) = forExactly(k, fn)18/** Alias for [Collection.forExactly] */19fun <T> Collection<T>.shouldForExactly(k: Int, fn: (T) -> Unit) = forExactly(k, fn)20/** Alias for [Sequence.forSome] */21fun <T> Sequence<T>.shouldForSome(fn: (T) -> Unit) = forSome(fn)22/** Alias for [Array.forSome] */23fun <T> Array<T>.shouldForSome(fn: (T) -> Unit) = forSome(fn)24/** Alias for [Collection.forSome] */25fun <T> Collection<T>.shouldForSome(fn: (T) -> Unit) = forSome(fn)26/** Alias for [Sequence.forAny] */27fun <T> Sequence<T>.shouldForAny(fn: (T) -> Unit) = forAny(fn)28/** Alias for [Array.forAny] */29fun <T> Array<T>.shouldForAny(fn: (T) -> Unit) = forAny(fn)30/** Alias for [Collection.forAny] */31fun <T> Collection<T>.shouldForAny(fn: (T) -> Unit) = forAny(fn)32/** Alias for [Sequence.forAtLeastOne] */33fun <T> Sequence<T>.shouldForAtLeastOne(fn: (T) -> Unit) = forAtLeastOne(fn)34/** Alias for [Array.forAtLeastOne] */35fun <T> Array<T>.shouldForAtLeastOne(fn: (T) -> Unit) = forAtLeastOne(fn)36/** Alias for [Collection.forAtLeastOne] */37fun <T> Collection<T>.shouldForAtLeastOne(fn: (T) -> Unit) = forAtLeastOne(fn)38/** Alias for [Sequence.forAtLeast] */39fun <T> Sequence<T>.shouldForAtLeast(k: Int, fn: (T) -> Unit) = forAtLeast(k, fn)40/** Alias for [Array.forAtLeast] */41fun <T> Array<T>.shouldForAtLeast(k: Int, fn: (T) -> Unit) = forAtLeast(k, fn)42/** Alias for [Collection.forAtLeast] */43fun <T> Collection<T>.shouldForAtLeast(k: Int, fn: (T) -> Unit) = forAtLeast(k, fn)44/** Alias for [Sequence.forAtMostOne] */45fun <T> Sequence<T>.shouldForAtMostOne(fn: (T) -> Unit) = forAtMostOne(fn)46/** Alias for [Array.forAtMostOne] */47fun <T> Array<T>.shouldForAtMostOne(fn: (T) -> Unit) = forAtMostOne(fn)48/** Alias for [Collection.forAtMostOne] */49fun <T> Collection<T>.shouldForAtMostOne(fn: (T) -> Unit) = forAtMostOne(fn)50/** Alias for [Sequence.forAtMost] */51fun <T> Sequence<T>.shouldForAtMost(k: Int, fn: (T) -> Unit) = forAtMost(k, fn)52/** Alias for [Array.forAtMost] */53fun <T> Array<T>.shouldForAtMost(k: Int, fn: (T) -> Unit) = forAtMost(k, fn)54/** Alias for [Collection.forAtMost] */55fun <T> Collection<T>.shouldForAtMost(k: Int, fn: (T) -> Unit) = forAtMost(k, fn)56/** Alias for [Sequence.forNone] */57fun <T> Sequence<T>.shouldForNone(fn: (T) -> Unit) = forNone(fn)58/** Alias for [Array.forNone] */59fun <T> Array<T>.shouldForNone(fn: (T) -> Unit) = forNone(fn)60/** Alias for [Collection.forNone] */61fun <T> Collection<T>.shouldForNone(fn: (T) -> Unit) = forNone(fn)...
Sequence.forOne
Using AI Code Generation
1import io.kotest.inspectors.forOne2import io.kotest.core.spec.style.StringSpec3import io.kotest.matchers.shouldBe4class SequenceTest : StringSpec({5"forOne" {6val sequence = sequenceOf(1, 2, 3, 4)7sequence.forOne {8}9}10})11import io.kotest.inspectors.forNone12import io.kotest.core.spec.style.StringSpec13import io.kotest.matchers.shouldBe14class SequenceTest : StringSpec({15"forNone" {16val sequence = sequenceOf(1, 2, 3, 4)17sequence.forNone {18}19}20})21import io.kotest.inspectors.forExactly22import io.kotest.core.spec.style.StringSpec23import io.kotest.matchers.shouldBe24class SequenceTest : StringSpec({25"forExactly" {26val sequence = sequenceOf(1, 2, 3, 4)27sequence.forExactly(2) {28}29}30})31import io.kotest.inspectors.forAtLeast32import io.kotest.core.spec.style.StringSpec33import io.kotest.matchers.shouldBe34class SequenceTest : StringSpec({35"forAtLeast" {36val sequence = sequenceOf(1, 2, 3, 4)37sequence.forAtLeast(2) {38}39}40})41import io.kotest.inspectors.forAtMost42import io.kotest.core.spec.style.StringSpec43import io.kotest.matchers.shouldBe44class SequenceTest : StringSpec({45"forAtMost" {46val sequence = sequenceOf(1, 2, 3, 4)47sequence.forAtMost(2) {48}49}50})51import io.kotest.inspectors.forAny52import io.kotest.core.spec.style.StringSpec
Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!