...25import org.openqa.selenium.NoSuchSessionException;26import org.openqa.selenium.SessionNotCreatedException;27import org.openqa.selenium.events.EventBus;28import org.openqa.selenium.events.local.GuavaEventBus;29import org.openqa.selenium.grid.data.CreateSessionRequest;30import org.openqa.selenium.grid.data.CreateSessionResponse;31import org.openqa.selenium.grid.data.NodeDrainComplete;32import org.openqa.selenium.grid.data.NodeId;33import org.openqa.selenium.grid.data.NodeStatus;34import org.openqa.selenium.grid.data.Session;35import org.openqa.selenium.grid.data.Slot;36import org.openqa.selenium.grid.node.local.LocalNode;37import org.openqa.selenium.grid.node.remote.RemoteNode;38import org.openqa.selenium.grid.testing.PassthroughHttpClient;39import org.openqa.selenium.grid.testing.TestSessionFactory;40import org.openqa.selenium.grid.web.Values;41import org.openqa.selenium.io.TemporaryFilesystem;42import org.openqa.selenium.io.Zip;43import org.openqa.selenium.json.Json;44import org.openqa.selenium.json.JsonInput;45import org.openqa.selenium.remote.Dialect;46import org.openqa.selenium.remote.SessionId;47import org.openqa.selenium.remote.http.Contents;48import org.openqa.selenium.remote.http.HttpClient;49import org.openqa.selenium.remote.http.HttpHandler;50import org.openqa.selenium.remote.http.HttpRequest;51import org.openqa.selenium.remote.http.HttpResponse;52import org.openqa.selenium.remote.tracing.DefaultTestTracer;53import org.openqa.selenium.remote.tracing.Tracer;54import org.openqa.selenium.support.ui.FluentWait;55import org.openqa.selenium.support.ui.Wait;56import java.io.ByteArrayInputStream;57import java.io.File;58import java.io.IOException;59import java.io.UncheckedIOException;60import java.net.URI;61import java.net.URISyntaxException;62import java.nio.charset.StandardCharsets;63import java.nio.file.Files;64import java.time.Clock;65import java.time.Duration;66import java.time.Instant;67import java.time.ZoneId;68import java.util.ArrayList;69import java.util.Collections;70import java.util.HashSet;71import java.util.List;72import java.util.Map;73import java.util.Optional;74import java.util.Set;75import java.util.UUID;76import java.util.concurrent.CountDownLatch;77import java.util.concurrent.atomic.AtomicBoolean;78import java.util.concurrent.atomic.AtomicReference;79import static java.time.Duration.ofSeconds;80import static java.util.concurrent.TimeUnit.SECONDS;81import static org.assertj.core.api.Assertions.assertThat;82import static org.assertj.core.api.Assertions.assertThatExceptionOfType;83import static org.assertj.core.api.InstanceOfAssertFactories.LIST;84import static org.assertj.core.api.InstanceOfAssertFactories.MAP;85import static org.openqa.selenium.grid.data.NodeDrainComplete.NODE_DRAIN_COMPLETE;86import static org.openqa.selenium.grid.data.NodeRemovedEvent.NODE_REMOVED;87import static org.openqa.selenium.grid.data.SessionClosedEvent.SESSION_CLOSED;88import static org.openqa.selenium.json.Json.MAP_TYPE;89import static org.openqa.selenium.remote.http.Contents.string;90import static org.openqa.selenium.remote.http.HttpMethod.GET;91import static org.openqa.selenium.remote.http.HttpMethod.POST;92public class NodeTest {93 private Tracer tracer;94 private EventBus bus;95 private LocalNode local;96 private Node node;97 private ImmutableCapabilities caps;98 private URI uri;99 @Before100 public void setUp() throws URISyntaxException {101 tracer = DefaultTestTracer.createTracer();102 bus = new GuavaEventBus();103 caps = new ImmutableCapabilities("browserName", "cheese");104 uri = new URI("http://localhost:1234");105 class Handler extends Session implements HttpHandler {106 private Handler(Capabilities capabilities) {107 super(new SessionId(UUID.randomUUID()), uri, capabilities);108 }109 @Override110 public HttpResponse execute(HttpRequest req) throws UncheckedIOException {111 return new HttpResponse();112 }113 }114 local = LocalNode.builder(tracer, bus, uri, uri, null)115 .add(caps, new TestSessionFactory((id, c) -> new Handler(c)))116 .add(caps, new TestSessionFactory((id, c) -> new Handler(c)))117 .add(caps, new TestSessionFactory((id, c) -> new Handler(c)))118 .maximumConcurrentSessions(2)119 .build();120 node = new RemoteNode(121 tracer,122 new PassthroughHttpClient.Factory(local),123 new NodeId(UUID.randomUUID()),124 uri,125 ImmutableSet.of(caps));126 }127 @Test128 public void shouldRefuseToCreateASessionIfNoFactoriesAttached() {129 Node local = LocalNode.builder(tracer, bus, uri, uri, null).build();130 HttpClient.Factory clientFactory = new PassthroughHttpClient.Factory(local);131 Node node = new RemoteNode(tracer, clientFactory, new NodeId(UUID.randomUUID()), uri, ImmutableSet.of());132 Optional<Session> session = node.newSession(createSessionRequest(caps))133 .map(CreateSessionResponse::getSession);134 assertThat(session).isNotPresent();135 }136 @Test137 public void shouldCreateASessionIfTheCorrectCapabilitiesArePassedToIt() {138 Optional<Session> session = node.newSession(createSessionRequest(caps))139 .map(CreateSessionResponse::getSession);140 assertThat(session).isPresent();141 }142 @Test143 public void shouldOnlyCreateAsManySessionsAsFactories() {144 Node node = LocalNode.builder(tracer, bus, uri, uri, null)145 .add(caps, new TestSessionFactory((id, c) -> new Session(id, uri, c)))146 .build();147 Optional<Session> session = node.newSession(createSessionRequest(caps))148 .map(CreateSessionResponse::getSession);149 assertThat(session).isPresent();150 session = node.newSession(createSessionRequest(caps))151 .map(CreateSessionResponse::getSession);152 assertThat(session).isNotPresent();153 }154 @Test155 public void willRefuseToCreateMoreSessionsThanTheMaxSessionCount() {156 Optional<Session> session = node.newSession(createSessionRequest(caps))157 .map(CreateSessionResponse::getSession);158 assertThat(session).isPresent();159 session = node.newSession(createSessionRequest(caps))160 .map(CreateSessionResponse::getSession);161 assertThat(session).isPresent();162 session = node.newSession(createSessionRequest(caps))163 .map(CreateSessionResponse::getSession);164 assertThat(session).isNotPresent();165 }166 @Test167 public void stoppingASessionReducesTheNumberOfCurrentlyActiveSessions() {168 assertThat(local.getCurrentSessionCount()).isEqualTo(0);169 Session session = local.newSession(createSessionRequest(caps))170 .map(CreateSessionResponse::getSession)171 .orElseThrow(() -> new RuntimeException("Session not created"));172 assertThat(local.getCurrentSessionCount()).isEqualTo(1);173 local.stop(session.getId());174 assertThat(local.getCurrentSessionCount()).isEqualTo(0);175 }176 @Test177 public void sessionsThatAreStoppedWillNotBeReturned() {178 Session expected = node.newSession(createSessionRequest(caps))179 .map(CreateSessionResponse::getSession)180 .orElseThrow(() -> new RuntimeException("Session not created"));181 node.stop(expected.getId());182 assertThatExceptionOfType(NoSuchSessionException.class)183 .isThrownBy(() -> local.getSession(expected.getId()));184 assertThatExceptionOfType(NoSuchSessionException.class)185 .isThrownBy(() -> node.getSession(expected.getId()));186 }187 @Test188 public void stoppingASessionThatDoesNotExistWillThrowAnException() {189 assertThatExceptionOfType(NoSuchSessionException.class)190 .isThrownBy(() -> local.stop(new SessionId(UUID.randomUUID())));191 assertThatExceptionOfType(NoSuchSessionException.class)192 .isThrownBy(() -> node.stop(new SessionId(UUID.randomUUID())));193 }194 @Test195 public void attemptingToGetASessionThatDoesNotExistWillCauseAnExceptionToBeThrown() {196 assertThatExceptionOfType(NoSuchSessionException.class)197 .isThrownBy(() -> local.getSession(new SessionId(UUID.randomUUID())));198 assertThatExceptionOfType(NoSuchSessionException.class)199 .isThrownBy(() -> node.getSession(new SessionId(UUID.randomUUID())));200 }201 @Test202 public void willRespondToWebDriverCommandsSentToOwnedSessions() {203 AtomicBoolean called = new AtomicBoolean(false);204 class Recording extends Session implements HttpHandler {205 private Recording() {206 super(new SessionId(UUID.randomUUID()), uri, caps);207 }208 @Override209 public HttpResponse execute(HttpRequest req) throws UncheckedIOException {210 called.set(true);211 return new HttpResponse();212 }213 }214 Node local = LocalNode.builder(tracer, bus, uri, uri, null)215 .add(caps, new TestSessionFactory((id, c) -> new Recording()))216 .build();217 Node remote = new RemoteNode(218 tracer,219 new PassthroughHttpClient.Factory(local),220 new NodeId(UUID.randomUUID()),221 uri,222 ImmutableSet.of(caps));223 Session session = remote.newSession(createSessionRequest(caps))224 .map(CreateSessionResponse::getSession)225 .orElseThrow(() -> new RuntimeException("Session not created"));226 HttpRequest req = new HttpRequest(POST, String.format("/session/%s/url", session.getId()));227 remote.execute(req);228 assertThat(called.get()).isTrue();229 }230 @Test231 public void shouldOnlyRespondToWebDriverCommandsForSessionsTheNodeOwns() {232 Session session = node.newSession(createSessionRequest(caps))233 .map(CreateSessionResponse::getSession)234 .orElseThrow(() -> new RuntimeException("Session not created"));235 HttpRequest req = new HttpRequest(POST, String.format("/session/%s/url", session.getId()));236 assertThat(local.matches(req)).isTrue();237 assertThat(node.matches(req)).isTrue();238 req = new HttpRequest(POST, String.format("/session/%s/url", UUID.randomUUID()));239 assertThat(local.matches(req)).isFalse();240 assertThat(node.matches(req)).isFalse();241 }242 @Test243 public void aSessionThatTimesOutWillBeStoppedAndRemovedFromTheSessionMap() {244 AtomicReference<Instant> now = new AtomicReference<>(Instant.now());245 Clock clock = new MyClock(now);246 Node node = LocalNode.builder(tracer, bus, uri, uri, null)247 .add(caps, new TestSessionFactory((id, c) -> new Session(id, uri, c)))248 .sessionTimeout(Duration.ofMinutes(3))249 .advanced()250 .clock(clock)251 .build();252 Session session = node.newSession(createSessionRequest(caps))253 .map(CreateSessionResponse::getSession)254 .orElseThrow(() -> new RuntimeException("Session not created"));255 now.set(now.get().plus(Duration.ofMinutes(5)));256 assertThatExceptionOfType(NoSuchSessionException.class)257 .isThrownBy(() -> node.getSession(session.getId()));258 }259 @Test260 public void shouldNotPropagateExceptionsWhenSessionCreationFails() {261 Node local = LocalNode.builder(tracer, bus, uri, uri, null)262 .add(caps, new TestSessionFactory((id, c) -> {263 throw new SessionNotCreatedException("eeek");264 }))265 .build();266 Optional<Session> session = local.newSession(createSessionRequest(caps))267 .map(CreateSessionResponse::getSession);268 assertThat(session).isNotPresent();269 }270 @Test271 public void eachSessionShouldReportTheNodesUrl() throws URISyntaxException {272 URI sessionUri = new URI("http://cheese:42/peas");273 Node node = LocalNode.builder(tracer, bus, uri, uri, null)274 .add(caps, new TestSessionFactory((id, c) -> new Session(id, sessionUri, c)))275 .build();276 Optional<Session> session = node.newSession(createSessionRequest(caps))277 .map(CreateSessionResponse::getSession);278 assertThat(session).isPresent();279 assertThat(session.get().getUri()).isEqualTo(uri);280 }281 @Test282 public void quittingASessionShouldCauseASessionClosedEventToBeFired() {283 AtomicReference<Object> obj = new AtomicReference<>();284 bus.addListener(SESSION_CLOSED, event -> obj.set(event.getData(Object.class)));285 Session session = node.newSession(createSessionRequest(caps))286 .map(CreateSessionResponse::getSession)287 .orElseThrow(() -> new AssertionError("Cannot create session"));288 node.stop(session.getId());289 // Because we're using the event bus, we can't expect the event to fire instantly. We're using290 // an inproc bus, so in reality it's reasonable to expect the event to fire synchronously, but291 // let's play it safe.292 Wait<AtomicReference<Object>> wait = new FluentWait<>(obj).withTimeout(ofSeconds(2));293 wait.until(ref -> ref.get() != null);294 }295 @Test296 public void canReturnStatus() {297 node.newSession(createSessionRequest(caps))298 .map(CreateSessionResponse::getSession)299 .orElseThrow(() -> new AssertionError("Cannot create session"));300 HttpRequest req = new HttpRequest(GET, "/status");301 HttpResponse res = node.execute(req);302 assertThat(res.getStatus()).isEqualTo(200);303 NodeStatus seen = null;304 try (JsonInput input = new Json().newInput(Contents.reader(res))) {305 input.beginObject();306 while (input.hasNext()) {307 switch (input.nextName()) {308 case "value":309 input.beginObject();310 while (input.hasNext()) {311 switch (input.nextName()) {312 case "node":313 seen = input.read(NodeStatus.class);314 break;315 default:316 input.skipValue();317 }318 }319 input.endObject();320 break;321 default:322 input.skipValue();323 break;324 }325 }326 }327 NodeStatus expected = node.getStatus();328 assertThat(seen).isEqualTo(expected);329 }330 @Test331 public void returns404ForAnUnknownCommand() {332 HttpRequest req = new HttpRequest(GET, "/foo");333 HttpResponse res = node.execute(req);334 assertThat(res.getStatus()).isEqualTo(404);335 Map<String, Object> content = new Json().toType(string(res), MAP_TYPE);336 assertThat(content).containsOnlyKeys("value")337 .extracting("value").asInstanceOf(MAP)338 .containsEntry("error", "unknown command")339 .containsEntry("message", "Unable to find handler for (GET) /foo");340 }341 @Test342 public void canUploadAFile() throws IOException {343 Session session = node.newSession(createSessionRequest(caps))344 .map(CreateSessionResponse::getSession)345 .orElseThrow(() -> new AssertionError("Cannot create session"));346 HttpRequest req = new HttpRequest(POST, String.format("/session/%s/file", session.getId()));347 String hello = "Hello, world!";348 String zip = Zip.zip(createTmpFile(hello));349 String payload = new Json().toJson(Collections.singletonMap("file", zip));350 req.setContent(() -> new ByteArrayInputStream(payload.getBytes()));351 node.execute(req);352 File baseDir = getTemporaryFilesystemBaseDir(local.getTemporaryFilesystem(session.getId()));353 assertThat(baseDir.listFiles()).hasSize(1);354 File uploadDir = baseDir.listFiles()[0];355 assertThat(uploadDir.listFiles()).hasSize(1);356 assertThat(new String(Files.readAllBytes(uploadDir.listFiles()[0].toPath()))).isEqualTo(hello);357 node.stop(session.getId());358 assertThat(baseDir).doesNotExist();359 }360 @Test361 public void shouldNotCreateSessionIfDraining() {362 node.drain();363 assertThat(local.isDraining()).isTrue();364 assertThat(node.isDraining()).isTrue();365 Optional<CreateSessionResponse> sessionResponse = node.newSession(createSessionRequest(caps));366 assertThat(sessionResponse.isPresent()).isFalse();367 }368 @Test369 public void shouldNotShutdownDuringOngoingSessionsIfDraining() throws InterruptedException {370 Optional<Session> firstSession =371 node.newSession(createSessionRequest(caps)).map(CreateSessionResponse::getSession);372 Optional<Session> secondSession =373 node.newSession(createSessionRequest(caps)).map(CreateSessionResponse::getSession);374 CountDownLatch latch = new CountDownLatch(1);375 bus.addListener(NODE_DRAIN_COMPLETE, e -> latch.countDown());376 node.drain();377 assertThat(local.isDraining()).isTrue();378 assertThat(node.isDraining()).isTrue();379 Optional<CreateSessionResponse> sessionResponse = node.newSession(createSessionRequest(caps));380 assertThat(sessionResponse.isPresent()).isFalse();381 assertThat(firstSession.isPresent()).isTrue();382 assertThat(secondSession.isPresent()).isTrue();383 assertThat(local.getCurrentSessionCount()).isEqualTo(2);384 latch.await(1, SECONDS);385 assertThat(latch.getCount()).isEqualTo(1);386 }387 @Test388 public void shouldShutdownAfterSessionsCompleteIfDraining() throws InterruptedException {389 CountDownLatch latch = new CountDownLatch(1);390 bus.addListener(NODE_DRAIN_COMPLETE, e -> latch.countDown());391 Optional<Session> firstSession =392 node.newSession(createSessionRequest(caps)).map(CreateSessionResponse::getSession);393 Optional<Session> secondSession =394 node.newSession(createSessionRequest(caps)).map(CreateSessionResponse::getSession);395 node.drain();396 assertThat(firstSession.isPresent()).isTrue();397 assertThat(secondSession.isPresent()).isTrue();398 node.stop(firstSession.get().getId());399 node.stop(secondSession.get().getId());400 latch.await(5, SECONDS);401 assertThat(latch.getCount()).isEqualTo(0);402 }403 @Test404 public void shouldAllowsWebDriverCommandsForOngoingSessionIfDraining() throws InterruptedException {405 CountDownLatch latch = new CountDownLatch(1);406 bus.addListener(NODE_DRAIN_COMPLETE, e -> latch.countDown());407 Optional<Session> session =408 node.newSession(createSessionRequest(caps)).map(CreateSessionResponse::getSession);409 node.drain();410 SessionId sessionId = session.get().getId();411 HttpRequest req = new HttpRequest(POST, String.format("/session/%s/url", sessionId));412 HttpResponse response = node.execute(req);413 assertThat(response.getStatus()).isEqualTo(200);414 assertThat(latch.getCount()).isEqualTo(1);415 }416 private File createTmpFile(String content) {417 try {418 File f = File.createTempFile("webdriver", "tmp");419 f.deleteOnExit();420 Files.write(f.toPath(), content.getBytes(StandardCharsets.UTF_8));421 return f;422 } catch (IOException e) {423 throw new UncheckedIOException(e);424 }425 }426 private File getTemporaryFilesystemBaseDir(TemporaryFilesystem tempFS) {427 File tmp = tempFS.createTempDir("tmp", "");428 File baseDir = tmp.getParentFile();429 tempFS.deleteTempDir(tmp);430 return baseDir;431 }432 private CreateSessionRequest createSessionRequest(Capabilities caps) {433 return new CreateSessionRequest(434 ImmutableSet.copyOf(Dialect.values()),435 caps,436 ImmutableMap.of());437 }438 private static class MyClock extends Clock {439 private final AtomicReference<Instant> now;440 public MyClock(AtomicReference<Instant> now) {441 this.now = now;442 }443 @Override444 public ZoneId getZone() {445 return ZoneId.systemDefault();446 }447 @Override...