
FactoryTest.pudu
Pudu132 lines7.4 KB
1/** @Test.Factory.Suite — named clients over pipelines the factory owns and renews */2module PuduLangHttpClient.FactoryTest34import Std.Concurrent.Cancel as Cancel5import Std.Concurrent as Concurrent6import Std.Http as Http7import Std.Http.Server.Reply as Reply8import Std.Http.Server.Route as Route9import Std.Io as Io10import Std.Result as Result11import Std.Sync as Sync12import Std.Test as Test13import PuduLangHttpClient.Client as Client14import PuduLangHttpClient.Constants.Defaults as Defaults15import PuduLangHttpClient.Events as Events16import PuduLangHttpClient.Factory.Builder as Builder17import PuduLangHttpClient.Factory as Factory18import PuduLangHttpClient.Handler as Handler19import PuduLangHttpClient as HttpClient20import PuduLangHttpClient.Request as Request21import PuduLangHttpClient.Response as Response22import PuduLangHttpClient.Stub as Stub23import PuduLangHttpClient.Transport as Transport24import Support.Loopback as Loopback2526/** @Test.Factory.Catalog — a typed client wrapping a named one */27type Catalog = { client: Client.Client }282930fn itemsOf(catalog: &Catalog) -> HttpClient.Outcome[Str] { Client.getText(&catalog.client, "items") }313233fn tracing(name: Str) -> Handler.Handler { Handler.before(|request: Request.Request| request.withHeader("trace", name)) }343536fn textOf(outcome: &HttpClient.Outcome[Str]) -> Str {37 match outcome {38 case Ok(found) => found39 case Err(failure) => HttpClient.describe(&failure)40 }41}424344fn main() -> Int {45 let server = Loopback.serve(&[46 Route.get("/items", |_request: Route.Request| Reply.text(200, "tea,cake")),47 Route.get("/trace", |request: Route.Request| Reply.text(200, request.message.headers.filter(|pair: (Str, Str)| pair[0] == "trace" || pair[0] == "user-agent").map(|pair: (Str, Str)| pair[1]).join(">")))48 ])49 let events = Sync.cell([])50 let lock = Sync.mutex()51 let options = Factory.Options{listeners: Events.listeners().onPipelineChanged(fn(event: Events.PipelineChanged) -> () {52 let _recorded = Sync.withLock(&lock, fn() -> () { let _kept = Sync.set(&events, Result.unwrapOr(Sync.get(&events), []).push((event.client, event.generation, event.stage))) })53 }) }54 let stub = Stub.create()55 Stub.respond(&stub, Http.Get, "/ping", 200, "pong")56 let built = Factory.buildWith(&options, [57 Builder.defaults().withDefaultHeader("user-agent", "shop/1").withHandler(tracing("default")),58 Builder.named("catalog").withBaseAddress(server.base + "/").withHandler(tracing("first")),59 Builder.named("catalog").withHandler(tracing("second")).configureHandlers(|handlers: Array[Handler.Handler]| handlers.reverse()),60 Builder.named("rotating").withBaseAddress(server.base + "/").withHandlerLifetime(30),61 Builder.named("stubbed").withPrimary(|| Stub.send(&stub)).withBaseAddress("http://stub.test/"),62 Builder.named("tuned").withTransport(Transport.defaults().withMaxConnectionsPerServer(2)).configureTransport(|options: Transport.Options| options.withPooledConnectionIdleTimeout(10)).withTimeout(Defaults.INFINITE)63 ])64 let factory = match built {65 case Ok(found) => found66 case Err(invalid) => panic(Factory.explain(&invalid))67 }68 let catalog = Factory.typed(&factory, "catalog", |client: Client.Client| Catalog{client: client})69 let items = itemsOf(&catalog)70 let traced = Client.getText(&Factory.createClient(&factory, "catalog"), "trace")71 let sameHandler = Factory.statistics(&factory, "catalog")72 let unnamed = Factory.client(&factory)73 let stubbed = Client.getText(&Factory.createClient(&factory, "stubbed"), "ping")7475 let first = Client.getText(&Factory.createClient(&factory, "rotating"), "items")76 let firstStats = Factory.statistics(&factory, "rotating")77 let _rested = Concurrent.sleep(40)78 let rotatingSender = Factory.rotatingHandler(&factory, "rotating")79 let second = rotatingSender(Request.get(server.base + "/items"), Cancel.expiring(5000))80 let pendingAfterRotation = Factory.pendingExpired(&factory)81 let _graced = Concurrent.sleep(40)82 let _third = Client.getText(&Factory.createClient(&factory, "rotating"), "items")83 let lifecycle = Result.unwrapOr(Sync.get(&events), [])84 Factory.dispose(&factory)85 let afterDispose = Result.unwrapOr(Sync.get(&events), []).length() > lifecycle.length()86 let stillWorks = Client.getText(&catalog.client, "items")87 Loopback.stop(&server)8889 let invalid = Factory.build([90 Builder.defaults().withTimeout(-3),91 Builder.named("bad").withBaseAddress("relative/").withHandlerLifetime(-7).withTransport(Transport.defaults().withMaxConnectionsPerServer(0))92 ])93 let problems = match invalid {94 case Ok(_) => []95 case Err(found) => found.problems96 }97 let checks = Test.suite("Factory", &[98 Test.equals("a typed client wraps a named client", &items, &Ok("tea,cake")),99 Test.equals("defaults apply first, then a name's builders in order, then its rearrangement", &textOf(&traced), &"shop/1>second>first>default"),100 Test.that("a named pipeline keeps its connections between clients", match sameHandler {101 case Some(stats) => stats.reused >= 1 && stats.opened == 1102 case None => false103 }),104 Test.equals("an unconfigured name gets the defaults", &(unnamed.name, unnamed.defaultHeaders, unnamed.baseAddress), &("", [("user-agent", "shop/1")], None)),105 Test.equals("a primary handler of the program's own is used", &stubbed, &Ok("pong")),106 Test.equals("a stub has no connection counters", &Factory.statistics(&factory, "stubbed"), &None),107 Test.equals("the names configured on their own are listed", &Factory.names(&factory), &["catalog", "rotating", "stubbed", "tuned"]),108 Test.equals("a client of a pipeline that expired still answers", &(first, Result.map(second, |r: Response.Response| r.status.code)), &(Ok("tea,cake"), Ok(200))),109 Test.that("a pipeline's first transport opened its connection", match firstStats {110 case Some(stats) => stats.opened == 1111 case None => false112 }),113 Test.equals("an expired pipeline waits out its grace before closing", &pendingAfterRotation, &1),114 Test.that("pipelines were created, expired, and closed in turn", lifecycle == [("catalog", 1, Events.Created), ("", 2, Events.Created), ("stubbed", 3, Events.Created), ("rotating", 4, Events.Created), ("rotating", 5, Events.Created), ("rotating", 4, Events.Expired), ("rotating", 6, Events.Created), ("rotating", 5, Events.Expired), ("rotating", 4, Events.Closed)]),115 Test.that("disposing closes every pipeline and says so", afterDispose),116 Test.equals("a client made before disposing still answers", &stillWorks, &Ok("tea,cake")),117 Test.equals("every problem of every name is reported at once", &problems, &[118 "client 'bad': baseAddress must be an absolute http or https address: relative/",119 "client 'bad': timeout must be positive or INFINITE",120 "client 'bad': handlerLifetime must be at least 0 ms or INFINITE",121 "client 'bad': maxConnectionsPerServer must be at least 1",122 "client '(defaults)': timeout must be positive or INFINITE"123 ]),124 Test.that("the problems are explained in one sentence", Factory.explain(&Factory.Invalid{problems: ["a", "b"]}) == "The client factory is invalid: a; b")125 ])126 let ran = Test.run(&checks)127 for failure in Test.failuresOf(&ran) {128 let _reported = Io.writeErrorLine(failure)129 }130 Test.report(&ran)131}132