Best Selenium code snippet using org.openqa.selenium.grid.node.Node.isDraining
Source:SauceNode.java  
...130    this.factories = ImmutableList.copyOf(factories);131    Require.nonNull("Registration secret", registrationSecret);132    this.healthCheck = healthCheck == null ?133                       () -> new HealthCheck.Result(134                         isDraining() ? DRAINING : UP,135                         String.format("%s is %s", uri, isDraining() ? "draining" : "up")) :136                       healthCheck;137    this.currentSessions = CacheBuilder.newBuilder()138      .expireAfterAccess(sessionTimeout)139      .ticker(ticker)140      .removalListener((RemovalListener<SessionId, SessionSlot>) notification -> {141        // Attempt to stop the session142        LOG.log(Debug.getDebugLogLevel(), "Stopping session {0}", notification.getKey().toString());143        SessionSlot slot = notification.getValue();144        if (!slot.isAvailable()) {145          slot.stop();146        }147      })148      .build();149    this.tempFileSystems = CacheBuilder.newBuilder()150      .expireAfterAccess(sessionTimeout)151      .ticker(ticker)152      .removalListener((RemovalListener<SessionId, TemporaryFilesystem>) notification -> {153        TemporaryFilesystem tempFS = notification.getValue();154        tempFS.deleteTemporaryFiles();155        tempFS.deleteBaseDir();156      })157      .build();158    Regularly sessionCleanup = new Regularly("Session Cleanup Node: " + externalUri);159    sessionCleanup.submit(currentSessions::cleanUp, Duration.ofSeconds(30), Duration.ofSeconds(30));160    Regularly tmpFileCleanup = new Regularly("TempFile Cleanup Node: " + externalUri);161    tmpFileCleanup.submit(tempFileSystems::cleanUp, Duration.ofSeconds(30), Duration.ofSeconds(30));162    Regularly regularHeartBeat = new Regularly("Heartbeat Node: " + externalUri);163    regularHeartBeat.submit(() -> bus.fire(new NodeHeartBeatEvent(getStatus())), heartbeatPeriod,164                            heartbeatPeriod);165    bus.addListener(SessionClosedEvent.listener(id -> {166      // Listen to session terminated events so we know when to fire the NodeDrainComplete event167      if (this.isDraining()) {168        int done = pendingSessions.decrementAndGet();169        if (done <= 0) {170          LOG.info("Firing node drain complete message");171          bus.fire(new NodeDrainComplete(this.getId()));172        }173      }174    }));175    Runtime.getRuntime().addShutdownHook(new Thread(this::stopAllSessions));176    new JMXHelper().register(this);177  }178  public static SauceNode.Builder builder(179    Tracer tracer,180    EventBus bus,181    URI uri,182    URI gridUri,183    Secret registrationSecret) {184    return new SauceNode.Builder(tracer, bus, uri, gridUri, registrationSecret);185  }186  @Override187  public boolean isReady() {188    return bus.isReady();189  }190  @VisibleForTesting191  public int getCurrentSessionCount() {192    // It seems wildly unlikely we'll overflow an int193    return Math.toIntExact(currentSessions.size());194  }195  @ManagedAttribute(name = "MaxSessions")196  public int getMaxSessionCount() {197    return maxSessionCount;198  }199  @ManagedAttribute(name = "Status")200  public Availability getAvailability() {201    return isDraining() ? DRAINING : UP;202  }203  @ManagedAttribute(name = "TotalSlots")204  public int getTotalSlots() {205    return factories.size();206  }207  @ManagedAttribute(name = "UsedSlots")208  public long getUsedSlots() {209    return factories.stream().filter(sessionSlot -> !sessionSlot.isAvailable()).count();210  }211  @ManagedAttribute(name = "Load")212  public float getLoad() {213    long inUse = factories.stream().filter(sessionSlot -> !sessionSlot.isAvailable()).count();214    return inUse / (float) maxSessionCount * 100f;215  }216  @ManagedAttribute(name = "RemoteNodeUri")217  public URI getExternalUri() {218    return this.getUri();219  }220  @ManagedAttribute(name = "GridUri")221  public URI getGridUri() {222    return this.gridUri;223  }224  @ManagedAttribute(name = "NodeId")225  public String getNodeId() {226    return getId().toString();227  }228  @Override229  public boolean isSupporting(Capabilities capabilities) {230    return factories.parallelStream().anyMatch(factory -> factory.test(capabilities));231  }232  @Override233  public Either<WebDriverException, CreateSessionResponse> newSession(CreateSessionRequest sessionRequest) {234    Require.nonNull("Session request", sessionRequest);235    try (Span span = tracer.getCurrentContext().createSpan("node.new_session")) {236      Map<String, EventAttributeValue> attributeMap = new HashMap<>();237      attributeMap238        .put(AttributeKey.LOGGER_CLASS.getKey(), EventAttribute.setValue(getClass().getName()));239      LOG.fine("Creating new session using span: " + span);240      attributeMap.put("session.request.capabilities",241                       EventAttribute.setValue(sessionRequest.getDesiredCapabilities().toString()));242      attributeMap.put("session.request.downstreamdialect",243                       EventAttribute.setValue(sessionRequest.getDownstreamDialects().toString()));244      int currentSessionCount = getCurrentSessionCount();245      span.setAttribute("current.session.count", currentSessionCount);246      attributeMap.put("current.session.count", EventAttribute.setValue(currentSessionCount));247      if (getCurrentSessionCount() >= maxSessionCount) {248        span.setAttribute("error", true);249        span.setStatus(Status.RESOURCE_EXHAUSTED);250        attributeMap.put("max.session.count", EventAttribute.setValue(maxSessionCount));251        span.addEvent("Max session count reached", attributeMap);252        return Either.left(new RetrySessionRequestException("Max session count reached."));253      }254      if (isDraining()) {255        span.setStatus(Status.UNAVAILABLE.withDescription("The node is draining. Cannot accept new sessions."));256        return Either.left(257          new RetrySessionRequestException("The node is draining. Cannot accept new sessions."));258      }259      // Identify possible slots to use as quickly as possible to enable concurrent session starting260      SessionSlot slotToUse = null;261      synchronized(factories) {262        for (SessionSlot factory : factories) {263          if (!factory.isAvailable() || !factory.test(sessionRequest.getDesiredCapabilities())) {264            continue;265          }266          factory.reserve();267          slotToUse = factory;268          break;269        }270      }271      if (slotToUse == null) {272        span.setAttribute("error", true);273        span.setStatus(Status.NOT_FOUND);274        span.addEvent("No slot matched capabilities ", attributeMap);275        return Either.left(276          new RetrySessionRequestException("No slot matched the requested capabilities."));277      }278      Either<WebDriverException, ActiveSession> possibleSession = slotToUse.apply(sessionRequest);279      if (possibleSession.isRight()) {280        ActiveSession session = possibleSession.right();281        currentSessions.put(session.getId(), slotToUse);282        SessionId sessionId = session.getId();283        Capabilities caps = session.getCapabilities();284        SESSION_ID.accept(span, sessionId);285        CAPABILITIES.accept(span, caps);286        String downstream = session.getDownstreamDialect().toString();287        String upstream = session.getUpstreamDialect().toString();288        String sessionUri = session.getUri().toString();289        span.setAttribute(AttributeKey.DOWNSTREAM_DIALECT.getKey(), downstream);290        span.setAttribute(AttributeKey.UPSTREAM_DIALECT.getKey(), upstream);291        span.setAttribute(AttributeKey.SESSION_URI.getKey(), sessionUri);292        // The session we return has to look like it came from the node, since we might be dealing293        // with a webdriver implementation that only accepts connections from localhost294        boolean isSupportingCdp = slotToUse.isSupportingCdp() ||295                                  caps.getCapability("se:cdp") != null;296        Session externalSession = createExternalSession(session, externalUri, isSupportingCdp);297        return Either.right(new CreateSessionResponse(298          externalSession,299          getEncoder(session.getDownstreamDialect()).apply(externalSession)));300      } else {301        slotToUse.release();302        span.setAttribute("error", true);303        span.addEvent("Unable to create session with the driver", attributeMap);304        return Either.left(possibleSession.left());305      }306    }307  }308  @Override309  public boolean isSessionOwner(SessionId id) {310    Require.nonNull("Session ID", id);311    return currentSessions.getIfPresent(id) != null;312  }313  @Override314  public Session getSession(SessionId id) throws NoSuchSessionException {315    Require.nonNull("Session ID", id);316    SessionSlot slot = currentSessions.getIfPresent(id);317    if (slot == null) {318      throw new NoSuchSessionException("Cannot find session with id: " + id);319    }320    return createExternalSession(slot.getSession(), externalUri, slot.isSupportingCdp());321  }322  @Override323  public TemporaryFilesystem getTemporaryFilesystem(SessionId id) throws IOException {324    try {325      return tempFileSystems.get(id, () -> TemporaryFilesystem.getTmpFsBasedOn(326        TemporaryFilesystem.getDefaultTmpFS().createTempDir("session", id.toString())));327    } catch (ExecutionException e) {328      throw new IOException(e);329    }330  }331  @Override332  public HttpResponse executeWebDriverCommand(HttpRequest req) {333    // True enough to be good enough334    SessionId id = getSessionId(req.getUri()).map(SessionId::new)335      .orElseThrow(() -> new NoSuchSessionException("Cannot find session: " + req));336    SessionSlot slot = currentSessions.getIfPresent(id);337    if (slot == null) {338      throw new NoSuchSessionException("Cannot find session with id: " + id);339    }340    ActiveSession activeSession = slot.getSession();341    if (activeSession.getClass().getName().contains("RelaySessionFactory")) {342      HttpResponse toReturn = slot.execute(req);343      if (req.getMethod() == DELETE && req.getUri().equals("/session/" + id)) {344        stop(id);345      }346      return toReturn;347    }348    SauceDockerSession session = (SauceDockerSession) activeSession;349    SauceCommandInfo.Builder builder = new SauceCommandInfo.Builder();350    builder.setStartTime(Instant.now().toEpochMilli());351    HttpResponse toReturn = slot.execute(req);352    if (req.getMethod() == DELETE && req.getUri().equals("/session/" + id)) {353      stop(id);354      builder.setScreenshotId(-1);355    } else {356      // Only taking screenshots after a url has been loaded357      if (!session.canTakeScreenshot() && req.getMethod() == POST358          && req.getUri().endsWith("/url")) {359        session.enableScreenshots();360      }361      int screenshotId = takeScreenshot(session, req, slot);362      builder.setScreenshotId(screenshotId);363    }364    Map<String, Object> parsedResponse =365      JSON.toType(new InputStreamReader(toReturn.getContent().get()), MAP_TYPE);366    builder.setRequest(getRequestContents(req))367      .setResult(parsedResponse)368      .setPath(req.getUri().replace(String.format("/session/%s", id), ""))369      .setHttpStatus(toReturn.getStatus())370      .setHttpMethod(req.getMethod().name())371      .setStatusCode(0);372    if (parsedResponse.containsKey("value") && parsedResponse.get("value") != null373        && parsedResponse.get("value").toString().contains("error")) {374      builder.setStatusCode(1);375    }376    builder.setEndTime(Instant.now().toEpochMilli());377    session.addSauceCommandInfo(builder.build());378    return toReturn;379  }380  @Override381  public HttpResponse uploadFile(HttpRequest req, SessionId id) {382    // When the session is running in a Docker container, the upload file command383    // needs to be forwarded to the container as well.384    SessionSlot slot = currentSessions.getIfPresent(id);385    if (slot != null && slot.getSession() instanceof SauceDockerSession) {386      return executeWebDriverCommand(req);387    }388    Map<String, Object> incoming = JSON.toType(string(req), Json.MAP_TYPE);389    File tempDir;390    try {391      TemporaryFilesystem tempfs = getTemporaryFilesystem(id);392      tempDir = tempfs.createTempDir("upload", "file");393      Zip.unzip((String) incoming.get("file"), tempDir);394    } catch (IOException e) {395      throw new UncheckedIOException(e);396    }397    // Select the first file398    File[] allFiles = tempDir.listFiles();399    if (allFiles == null) {400      throw new WebDriverException(401        String.format("Cannot access temporary directory for uploaded files %s", tempDir));402    }403    if (allFiles.length != 1) {404      throw new WebDriverException(405        String.format("Expected there to be only 1 file. There were: %s", allFiles.length));406    }407    ImmutableMap<String, Object> result = ImmutableMap.of(408      "value", allFiles[0].getAbsolutePath());409    return new HttpResponse().setContent(asJson(result));410  }411  @Override412  public void stop(SessionId id) throws NoSuchSessionException {413    Require.nonNull("Session ID", id);414    SessionSlot slot = currentSessions.getIfPresent(id);415    if (slot == null) {416      throw new NoSuchSessionException("Cannot find session with id: " + id);417    }418    currentSessions.invalidate(id);419    tempFileSystems.invalidate(id);420  }421  private void stopAllSessions() {422    if (currentSessions.size() > 0) {423      LOG.info("Trying to stop all running sessions before shutting down...");424      currentSessions.invalidateAll();425    }426  }427  private Session createExternalSession(ActiveSession other, URI externalUri, boolean isSupportingCdp) {428    Capabilities toUse = ImmutableCapabilities.copyOf(other.getCapabilities());429    // Rewrite the se:options if necessary to send the cdp url back430    if (isSupportingCdp) {431      String cdpPath = String.format("/session/%s/se/cdp", other.getId());432      toUse = new PersistentCapabilities(toUse).setCapability("se:cdp", rewrite(cdpPath));433    }434    return new Session(other.getId(), externalUri, other.getStereotype(), toUse, Instant.now());435  }436  private URI rewrite(String path) {437    try {438      String scheme = "https".equals(gridUri.getScheme()) ? "wss" : "ws";439      return new URI(440        scheme,441        gridUri.getUserInfo(),442        gridUri.getHost(),443        gridUri.getPort(),444        path,445        null,446        null);447    } catch (URISyntaxException e) {448      throw new RuntimeException(e);449    }450  }451  @Override452  public NodeStatus getStatus() {453    Set<Slot> slots = factories.stream()454      .map(slot -> {455        Instant lastStarted = Instant.EPOCH;456        Session session = null;457        if (!slot.isAvailable()) {458          ActiveSession activeSession = slot.getSession();459          if (activeSession != null) {460            lastStarted = activeSession.getStartTime();461            session = new Session(462              activeSession.getId(),463              activeSession.getUri(),464              slot.getStereotype(),465              activeSession.getCapabilities(),466              activeSession.getStartTime());467          }468        }469        return new Slot(470          new SlotId(getId(), slot.getId()),471          slot.getStereotype(),472          lastStarted,473          session);474      })475      .collect(toImmutableSet());476    return new NodeStatus(477      getId(),478      externalUri,479      maxSessionCount,480      slots,481      isDraining() ? DRAINING : UP,482      heartbeatPeriod,483      getNodeVersion(),484      getOsInfo());485  }486  @Override487  public HealthCheck getHealthCheck() {488    return healthCheck;489  }490  @Override491  public void drain() {492    bus.fire(new NodeDrainStarted(getId()));493    draining = true;494    int currentSessionCount = getCurrentSessionCount();495    if (currentSessionCount == 0) {496      LOG.info("Firing node drain complete message");497      bus.fire(new NodeDrainComplete(getId()));498    } else {499      pendingSessions.set(currentSessionCount);500    }501  }502  private Map<String, Object> toJson() {503    return ImmutableMap.of(504      "id", getId(),505      "uri", externalUri,506      "maxSessions", maxSessionCount,507      "draining", isDraining(),508      "capabilities", factories.stream()509        .map(SessionSlot::getStereotype)510        .collect(Collectors.toSet()));511  }512  public static class Builder {513    private final Tracer tracer;514    private final EventBus bus;515    private final URI uri;516    private final URI gridUri;517    private final Secret registrationSecret;518    private final ImmutableList.Builder<SessionSlot> factories;519    private int maxCount = Runtime.getRuntime().availableProcessors() * 5;520    private Ticker ticker = Ticker.systemTicker();521    private Duration sessionTimeout = Duration.ofMinutes(5);...Source:LocalNode.java  
...133    this.factories = ImmutableList.copyOf(factories);134    Require.nonNull("Registration secret", registrationSecret);135    this.healthCheck = healthCheck == null ?136      () -> new HealthCheck.Result(137        isDraining() ? DRAINING : UP,138        String.format("%s is %s", uri, isDraining() ? "draining" : "up")) :139      healthCheck;140    this.currentSessions = CacheBuilder.newBuilder()141      .expireAfterAccess(sessionTimeout)142      .ticker(ticker)143      .removalListener((RemovalListener<SessionId, SessionSlot>) notification -> {144        // If we were invoked explicitly, then return: we know what we're doing.145        if (!notification.wasEvicted()) {146          return;147        }148        killSession(notification.getValue());149      })150      .build();151    this.tempFileSystems = CacheBuilder.newBuilder()152        .expireAfterAccess(sessionTimeout)153        .ticker(ticker)154        .removalListener((RemovalListener<SessionId, TemporaryFilesystem>) notification -> {155          TemporaryFilesystem tempFS = notification.getValue();156          tempFS.deleteTemporaryFiles();157          tempFS.deleteBaseDir();158        })159        .build();160    this.regularly = new Regularly("Local Node: " + externalUri);161    regularly.submit(currentSessions::cleanUp, Duration.ofSeconds(30), Duration.ofSeconds(30));162    regularly.submit(tempFileSystems::cleanUp, Duration.ofSeconds(30), Duration.ofSeconds(30));163    bus.addListener(NodeAddedEvent.listener(nodeId -> {164      if (getId().equals(nodeId)) {165        // Lets avoid to create more than one "Regularly" when the Node registers again.166        if (!heartBeatStarted.getAndSet(true)) {167          regularly.submit(168            () -> bus.fire(new NodeHeartBeatEvent(getStatus())), heartbeatPeriod, heartbeatPeriod);169        }170      }171    }));172    bus.addListener(SessionClosedEvent.listener(id -> {173      try {174        this.stop(id);175      } catch (NoSuchSessionException ignore) {176      }177      if (this.isDraining()) {178        int done = pendingSessions.decrementAndGet();179        if (done <= 0) {180          LOG.info("Firing node drain complete message");181          bus.fire(new NodeDrainComplete(this.getId()));182        }183      }184    }));185    new JMXHelper().register(this);186  }187  public static Builder builder(188    Tracer tracer,189    EventBus bus,190    URI uri,191    URI gridUri,192    Secret registrationSecret) {193    return new Builder(tracer, bus, uri, gridUri, registrationSecret);194  }195  @Override196  public boolean isReady() {197    return bus.isReady();198  }199  @VisibleForTesting200  @ManagedAttribute(name = "CurrentSessions")201  public int getCurrentSessionCount() {202    // It seems wildly unlikely we'll overflow an int203    return Math.toIntExact(currentSessions.size());204  }205  @ManagedAttribute(name = "MaxSessions")206  public int getMaxSessionCount() {207    return maxSessionCount;208  }209  @ManagedAttribute(name = "Status")210  public Availability getAvailability() {211    return isDraining() ? DRAINING : UP;212  }213  @ManagedAttribute(name = "TotalSlots")214  public int getTotalSlots() {215    return factories.size();216  }217  @ManagedAttribute(name = "UsedSlots")218  public long getUsedSlots() {219    return factories.stream().filter(sessionSlot -> !sessionSlot.isAvailable()).count();220  }221  @ManagedAttribute(name = "Load")222  public float getLoad() {223    long inUse = factories.stream().filter(sessionSlot -> !sessionSlot.isAvailable()).count();224    return inUse / (float) maxSessionCount * 100f;225  }226  @ManagedAttribute(name = "RemoteNodeUri")227  public URI getExternalUri() {228    return this.getUri();229  }230  @ManagedAttribute(name = "GridUri")231  public URI getGridUri() {232    return this.gridUri;233  }234  @ManagedAttribute(name = "NodeId")235  public String getNodeId() {236    return getId().toString();237  }238  @Override239  public boolean isSupporting(Capabilities capabilities) {240    return factories.parallelStream().anyMatch(factory -> factory.test(capabilities));241  }242  @Override243  public Either<WebDriverException, CreateSessionResponse> newSession(CreateSessionRequest sessionRequest) {244    Require.nonNull("Session request", sessionRequest);245    try (Span span = tracer.getCurrentContext().createSpan("node.new_session")) {246      Map<String, EventAttributeValue> attributeMap = new HashMap<>();247      attributeMap248        .put(AttributeKey.LOGGER_CLASS.getKey(), EventAttribute.setValue(getClass().getName()));249      attributeMap.put("session.request.capabilities",250        EventAttribute.setValue(sessionRequest.getDesiredCapabilities().toString()));251      attributeMap.put("session.request.downstreamdialect",252        EventAttribute.setValue(sessionRequest.getDownstreamDialects().toString()));253      int currentSessionCount = getCurrentSessionCount();254      span.setAttribute("current.session.count", currentSessionCount);255      attributeMap.put("current.session.count", EventAttribute.setValue(currentSessionCount));256      if (getCurrentSessionCount() >= maxSessionCount) {257        span.setAttribute("error", true);258        span.setStatus(Status.RESOURCE_EXHAUSTED);259        attributeMap.put("max.session.count", EventAttribute.setValue(maxSessionCount));260        span.addEvent("Max session count reached", attributeMap);261        return Either.left(new RetrySessionRequestException("Max session count reached."));262      }263      if (isDraining()) {264        span.setStatus(Status.UNAVAILABLE.withDescription("The node is draining. Cannot accept new sessions."));265        return Either.left(266          new RetrySessionRequestException("The node is draining. Cannot accept new sessions."));267      }268      // Identify possible slots to use as quickly as possible to enable concurrent session starting269      SessionSlot slotToUse = null;270      synchronized (factories) {271        for (SessionSlot factory : factories) {272          if (!factory.isAvailable() || !factory.test(sessionRequest.getDesiredCapabilities())) {273            continue;274          }275          factory.reserve();276          slotToUse = factory;277          break;278        }279      }280      if (slotToUse == null) {281        span.setAttribute("error", true);282        span.setStatus(Status.NOT_FOUND);283        span.addEvent("No slot matched the requested capabilities. ", attributeMap);284        return Either.left(285          new RetrySessionRequestException("No slot matched the requested capabilities."));286      }287      Either<WebDriverException, ActiveSession> possibleSession = slotToUse.apply(sessionRequest);288      if (possibleSession.isRight()) {289        ActiveSession session = possibleSession.right();290        currentSessions.put(session.getId(), slotToUse);291        SessionId sessionId = session.getId();292        Capabilities caps = session.getCapabilities();293        SESSION_ID.accept(span, sessionId);294        CAPABILITIES.accept(span, caps);295        String downstream = session.getDownstreamDialect().toString();296        String upstream = session.getUpstreamDialect().toString();297        String sessionUri = session.getUri().toString();298        span.setAttribute(AttributeKey.DOWNSTREAM_DIALECT.getKey(), downstream);299        span.setAttribute(AttributeKey.UPSTREAM_DIALECT.getKey(), upstream);300        span.setAttribute(AttributeKey.SESSION_URI.getKey(), sessionUri);301        // The session we return has to look like it came from the node, since we might be dealing302        // with a webdriver implementation that only accepts connections from localhost303        Session externalSession = createExternalSession(304          session,305          externalUri,306          slotToUse.isSupportingCdp() || caps.getCapability("se:cdp") != null);307        return Either.right(new CreateSessionResponse(308          externalSession,309          getEncoder(session.getDownstreamDialect()).apply(externalSession)));310      } else {311        slotToUse.release();312        span.setAttribute("error", true);313        span.addEvent("Unable to create session with the driver", attributeMap);314        return Either.left(possibleSession.left());315      }316    }317  }318  @Override319  public boolean isSessionOwner(SessionId id) {320    Require.nonNull("Session ID", id);321    return currentSessions.getIfPresent(id) != null;322  }323  @Override324  public Session getSession(SessionId id) throws NoSuchSessionException {325    Require.nonNull("Session ID", id);326    SessionSlot slot = currentSessions.getIfPresent(id);327    if (slot == null) {328      throw new NoSuchSessionException("Cannot find session with id: " + id);329    }330    return createExternalSession(slot.getSession(), externalUri, slot.isSupportingCdp());331  }332  @Override333  public TemporaryFilesystem getTemporaryFilesystem(SessionId id) throws IOException {334    try {335      return tempFileSystems.get(id, () -> TemporaryFilesystem.getTmpFsBasedOn(336          TemporaryFilesystem.getDefaultTmpFS().createTempDir("session", id.toString())));337    } catch (ExecutionException e) {338      throw new IOException(e);339    }340  }341  @Override342  public HttpResponse executeWebDriverCommand(HttpRequest req) {343    // True enough to be good enough344    SessionId id = getSessionId(req.getUri()).map(SessionId::new)345      .orElseThrow(() -> new NoSuchSessionException("Cannot find session: " + req));346    SessionSlot slot = currentSessions.getIfPresent(id);347    if (slot == null) {348      throw new NoSuchSessionException("Cannot find session with id: " + id);349    }350    HttpResponse toReturn = slot.execute(req);351    if (req.getMethod() == DELETE && req.getUri().equals("/session/" + id)) {352      stop(id);353    }354    return toReturn;355  }356  @Override357  public HttpResponse uploadFile(HttpRequest req, SessionId id) {358    Map<String, Object> incoming = JSON.toType(string(req), Json.MAP_TYPE);359    File tempDir;360    try {361      TemporaryFilesystem tempfs = getTemporaryFilesystem(id);362      tempDir = tempfs.createTempDir("upload", "file");363      Zip.unzip((String) incoming.get("file"), tempDir);364    } catch (IOException e) {365      throw new UncheckedIOException(e);366    }367    // Select the first file368    File[] allFiles = tempDir.listFiles();369    if (allFiles == null) {370      throw new WebDriverException(371          String.format("Cannot access temporary directory for uploaded files %s", tempDir));372    }373    if (allFiles.length != 1) {374      throw new WebDriverException(375          String.format("Expected there to be only 1 file. There were: %s", allFiles.length));376    }377    ImmutableMap<String, Object> result = ImmutableMap.of(378        "value", allFiles[0].getAbsolutePath());379    return new HttpResponse().setContent(asJson(result));380  }381  @Override382  public void stop(SessionId id) throws NoSuchSessionException {383    Require.nonNull("Session ID", id);384    SessionSlot slot = currentSessions.getIfPresent(id);385    if (slot == null) {386      throw new NoSuchSessionException("Cannot find session with id: " + id);387    }388    killSession(slot);389    tempFileSystems.invalidate(id);390  }391  private Session createExternalSession(ActiveSession other, URI externalUri, boolean isSupportingCdp) {392    Capabilities toUse = ImmutableCapabilities.copyOf(other.getCapabilities());393    // Rewrite the se:options if necessary to send the cdp url back394    if (isSupportingCdp) {395      String cdpPath = String.format("/session/%s/se/cdp", other.getId());396      toUse = new PersistentCapabilities(toUse).setCapability("se:cdp", rewrite(cdpPath));397    }398    return new Session(other.getId(), externalUri, other.getStereotype(), toUse, Instant.now());399  }400  private URI rewrite(String path) {401    try {402      return new URI(403        "ws",404        gridUri.getUserInfo(),405        gridUri.getHost(),406        gridUri.getPort(),407        path,408        null,409        null);410    } catch (URISyntaxException e) {411      throw new RuntimeException(e);412    }413  }414  private void  killSession(SessionSlot slot) {415    currentSessions.invalidate(slot.getSession().getId());416    // Attempt to stop the session417    if (!slot.isAvailable()) {418      slot.stop();419    }420  }421  @Override422  public NodeStatus getStatus() {423    Set<Slot> slots = factories.stream()424      .map(slot -> {425        Instant lastStarted = Instant.EPOCH;426        Optional<Session> session = Optional.empty();427        if (!slot.isAvailable()) {428          ActiveSession activeSession = slot.getSession();429          if (activeSession != null) {430            lastStarted = activeSession.getStartTime();431            session = Optional.of(432              new Session(433                activeSession.getId(),434                activeSession.getUri(),435                slot.getStereotype(),436                activeSession.getCapabilities(),437                activeSession.getStartTime()));438          }439        }440        return new Slot(441          new SlotId(getId(), slot.getId()),442          slot.getStereotype(),443          lastStarted,444          session);445      })446      .collect(toImmutableSet());447    return new NodeStatus(448      getId(),449      externalUri,450      maxSessionCount,451      slots,452      isDraining() ? DRAINING : UP,453      heartbeatPeriod,454      getNodeVersion(),455      getOsInfo());456  }457  @Override458  public HealthCheck getHealthCheck() {459    return healthCheck;460  }461  @Override462  public void drain() {463    bus.fire(new NodeDrainStarted(getId()));464    draining = true;465    int currentSessionCount = getCurrentSessionCount();466    if (currentSessionCount == 0) {467      LOG.info("Firing node drain complete message");468      bus.fire(new NodeDrainComplete(getId()));469    } else {470      pendingSessions.set(currentSessionCount);471    }472  }473  private Map<String, Object> toJson() {474    return ImmutableMap.of(475      "id", getId(),476      "uri", externalUri,477      "maxSessions", maxSessionCount,478      "draining", isDraining(),479      "capabilities", factories.stream()480        .map(SessionSlot::getStereotype)481        .collect(Collectors.toSet()));482  }483  public static class Builder {484    private final Tracer tracer;485    private final EventBus bus;486    private final URI uri;487    private final URI gridUri;488    private final Secret registrationSecret;489    private final ImmutableList.Builder<SessionSlot> factories;490    private int maxCount = NodeOptions.DEFAULT_MAX_SESSIONS;491    private Ticker ticker = Ticker.systemTicker();492    private Duration sessionTimeout = Duration.ofSeconds(NodeOptions.DEFAULT_SESSION_TIMEOUT);...Source:KubernetesNode.java  
...92        sessionCleanup.submit(currentSessions::cleanUp, Duration.ofSeconds(30), Duration.ofSeconds(30));93        var regularHeartBeat = new Regularly("Heartbeat Node: " + uri);94        regularHeartBeat.submit(() -> bus.fire(new NodeHeartBeatEvent(getStatus())), heartbeatPeriod, heartbeatPeriod);95        bus.addListener(SessionClosedEvent.listener(id -> {96            if (this.isDraining() && pendingSessions.decrementAndGet() <= 0) {97                LOG.info("Firing node drain complete message");98                bus.fire(new NodeDrainComplete(this.getId()));99            }100        }));101        additionalRoutes = combine(102                get("/downloads/{sessionId}/{fileName}")103                        .to(params -> new GetFile(getActiveSession(sessionIdFrom(params)), fileNameFrom(params))),104                delete("/downloads/{sessionId}/{fileName}")105                        .to(params -> new DeleteFile(getActiveSession(sessionIdFrom(params)), fileNameFrom(params))),106                get("/downloads/{sessionId}")107                        .to(params -> new ListFiles(getActiveSession(sessionIdFrom(params)))),108                delete("/downloads/{sessionId}")109                        .to(params -> new DeleteFiles(getActiveSession(sessionIdFrom(params)))));110        Runtime.getRuntime().addShutdownHook(new Thread(this::stopAllSessions));111        new JMXHelper().register(this);112    }113    public static Node create(Config config) {114        var loggingOptions = new LoggingOptions(config);115        var eventOptions = new EventBusOptions(config);116        var serverOptions = new BaseServerOptions(config);117        var secretOptions = new SecretOptions(config);118        var networkOptions = new NetworkOptions(config);119        var k8sOptions = new KubernetesOptions(config);120        var tracer = loggingOptions.getTracer();121        var bus = eventOptions.getEventBus();122        var clientFactory = networkOptions.getHttpClientFactory(tracer);123        var k8s = new KubernetesDriver(new DefaultKubernetesClient());124        var factories = createFactories(k8sOptions, tracer, clientFactory, bus, k8s);125        LOG.info("Creating kubernetes node");126        return new KubernetesNode(tracer, bus, secretOptions.getRegistrationSecret(), new NodeId(UUID.randomUUID()),127                serverOptions.getExternalUri(), factories, k8sOptions.getMaxSessions(), k8sOptions.getSessionTimeout(),128                k8sOptions.getHeartbeatPeriod()129        );130    }131    static List<SessionSlot> createFactories(KubernetesOptions k8sOptions, Tracer tracer,132                                             HttpClient.Factory clientFactory, EventBus eventBus,133                                             KubernetesDriver driver) {134        var configs = k8sOptions.getConfigs();135        if (configs.isEmpty()) {136            throw new ConfigException("Unable to find kubernetes configs");137        }138        return configs.stream().flatMap(c -> Collections.nCopies(k8sOptions.getMaxSessions(), c).stream())139                .map(config -> {140                    var image = config.getImage();141                    var stereoType = config.getStereoType();142                    var factory = new KubernetesSessionFactory(tracer, clientFactory, driver,143                            k8sOptions.getWorkerResourceRequests(), image, stereoType, k8sOptions.getVideoImage(),144                            k8sOptions.getVideosPath());145                    return new SessionSlot(eventBus, stereoType, factory);146                }).collect(Collectors.toList());147    }148    @Override149    public Either<WebDriverException, CreateSessionResponse> newSession(CreateSessionRequest sessionRequest) {150        try (var span = tracer.getCurrentContext().createSpan("kubernetes_node.new_session")) {151            Map<String, EventAttributeValue> attributeMap = new HashMap<>();152            attributeMap.put(LOGGER_CLASS.getKey(), setValue(getClass().getName()));153            attributeMap.put("session.request.capabilities", setValue(sessionRequest.getDesiredCapabilities().toString()));154            attributeMap.put("session.request.downstreamdialect", setValue(sessionRequest.getDownstreamDialects()155                    .toString()));156            var currentSessionCount = getCurrentSessionCount();157            span.setAttribute("current.session.count", currentSessionCount);158            attributeMap.put("current.session.count", setValue(currentSessionCount));159            if (getCurrentSessionCount() >= maxSessionCount) {160                span.setAttribute("error", true);161                span.setStatus(Status.RESOURCE_EXHAUSTED);162                attributeMap.put("max.session.count", setValue(maxSessionCount));163                span.addEvent("Max session count reached", attributeMap);164                return Either.left(new RetrySessionRequestException("Max session count reached."));165            }166            SessionSlot slotToUse = null;167            synchronized (factories) {168                for (var factory : factories) {169                    if (!factory.isAvailable() || !factory.test(sessionRequest.getDesiredCapabilities())) {170                        continue;171                    }172                    factory.reserve();173                    slotToUse = factory;174                    break;175                }176            }177            if (slotToUse == null) {178                span.setAttribute("error", true);179                span.setStatus(Status.NOT_FOUND);180                span.addEvent("No slot matched the requested capabilities. ", attributeMap);181                return Either.left(new RetrySessionRequestException("No slot matched the requested capabilities."));182            }183            Either<WebDriverException, ActiveSession> possibleSession = slotToUse.apply(sessionRequest);184            if (possibleSession.isRight()) {185                var session = possibleSession.right();186                currentSessions.put(session.getId(), slotToUse);187                SESSION_ID.accept(span, session.getId());188                var caps = session.getCapabilities();189                CAPABILITIES.accept(span, caps);190                span.setAttribute(DOWNSTREAM_DIALECT.getKey(), session.getDownstreamDialect().toString());191                span.setAttribute(UPSTREAM_DIALECT.getKey(), session.getUpstreamDialect().toString());192                span.setAttribute(SESSION_URI.getKey(), session.getUri().toString());193                var isSupportingCdp = slotToUse.isSupportingCdp() || caps.getCapability("se:cdp") != null;194                var externalSession = createExternalSession(session, uri, isSupportingCdp);195                return Either.right(new CreateSessionResponse(externalSession,196                        getEncoder(session.getDownstreamDialect()).apply(externalSession)));197            } else {198                slotToUse.release();199                span.setAttribute("error", true);200                span.addEvent("Unable to create session with the driver", attributeMap);201                return Either.left(possibleSession.left());202            }203        }204    }205    @Override206    public HttpResponse executeWebDriverCommand(HttpRequest req) {207        var id = getSessionId(req.getUri()).map(SessionId::new)208                .orElseThrow(() -> new NoSuchSessionException("Cannot find session: " + req));209        var slot = getSessionSlot(id);210        var toReturn = slot.execute(req);211        if (req.getMethod() == DELETE && req.getUri().equals("/session/" + id)) {212            stop(id);213        }214        return toReturn;215    }216    @Override217    public Session getSession(SessionId id) throws NoSuchSessionException {218        var slot = getSessionSlot(id);219        return createExternalSession(slot.getSession(), uri, slot.isSupportingCdp());220    }221    @Override222    public HttpResponse uploadFile(HttpRequest req, SessionId id) {223        return executeWebDriverCommand(req);224    }225    @Override226    public void stop(SessionId id) throws NoSuchSessionException {227        getSessionSlot(id);228        currentSessions.invalidate(id);229    }230    @Override231    public boolean isSessionOwner(SessionId id) {232        return currentSessions.getIfPresent(id) != null;233    }234    @Override235    public boolean isSupporting(Capabilities capabilities) {236        return factories.parallelStream().anyMatch(factory -> factory.test(capabilities));237    }238    @Override239    public NodeStatus getStatus() {240        return new NodeStatus(getId(), uri, maxSessionCount, getSlots(), isDraining() ? DRAINING : UP, heartbeatPeriod,241                getNodeVersion(), getOsInfo());242    }243    @Override244    public HealthCheck getHealthCheck() {245        var availability = isDraining() ? DRAINING : UP;246        return () -> new HealthCheck.Result(availability, String.format("%s is %s", uri,247                availability.name().toLowerCase()));248    }249    @Override250    public void drain() {251        bus.fire(new NodeDrainStarted(getId()));252        draining = true;253        var currentSessionCount = getCurrentSessionCount();254        if (currentSessionCount == 0) {255            LOG.info("Firing node drain complete message");256            bus.fire(new NodeDrainComplete(getId()));257        } else {258            pendingSessions.set(currentSessionCount);259        }...Source:OneShotNode.java  
...293          Instant.EPOCH,294          driver == null ?295            Optional.empty() :296            Optional.of(new Session(sessionId, getUri(), stereotype, capabilities, Instant.now())))),297      isDraining() ? DRAINING : UP);298  }299  @Override300  public void drain() {301    events.fire(new NodeDrainStarted(getId()));302    draining = true;303  }304  @Override305  public HealthCheck getHealthCheck() {306    return () -> new HealthCheck.Result(isDraining() ? DRAINING : UP, "Everything is fine");307  }308  @Override309  public boolean isReady() {310    return events.isReady();311  }312}...Source:LocalDistributorTest.java  
...169      clientFactory,170      new LocalSessionMap(tracer, bus),171      null);172    distributor.add(localNode);173    assertThat(localNode.isDraining()).isFalse();174    //Check the size - there should be one node175    DistributorStatus statusBefore = distributor.getStatus();176    Set<NodeStatus> nodesBefore = statusBefore.getNodes();177    assertThat(nodesBefore.size()).isEqualTo(1);178    NodeStatus nodeBefore = nodesBefore.iterator().next();179    assertThat(nodeBefore.getAvailability()).isNotEqualTo(DRAINING);180    distributor.drain(localNode.getId());181    assertThat(localNode.isDraining()).isTrue();182    //Recheck the status - there should still be no node, it is removed183    DistributorStatus statusAfter = distributor.getStatus();184    Set<NodeStatus> nodesAfter = statusAfter.getNodes();185    assertThat(nodesAfter.size()).isEqualTo(0);186  }187  @Test188  public void testDrainNodeFromNode() {189    assertThat(localNode.isDraining()).isFalse();190    Distributor191        distributor =192        new LocalDistributor(tracer, bus, clientFactory, new LocalSessionMap(tracer, bus), null);193    distributor.add(localNode);194    localNode.drain();195    assertThat(localNode.isDraining()).isTrue();196  }197  private class Handler extends Session implements HttpHandler {198    private Handler(Capabilities capabilities) {199      super(new SessionId(UUID.randomUUID()), uri, new ImmutableCapabilities(), capabilities, Instant.now());200    }201    @Override202    public HttpResponse execute(HttpRequest req) throws UncheckedIOException {203      return new HttpResponse();204    }205  }206}...Source:Host.java  
...70      HealthCheck.Result result = healthCheck.check();71      Availability current = result.isAlive() ? UP : DOWN;72      Availability previous = setHostStatus(current);73      //If the node has been set to maintenance mode, set the status here as draining74      if (node.isDraining() || previous == DRAINING) {75        // We want to continue to allow the node to drain.76        setHostStatus(DRAINING);77        return;78      }79      if (current != previous) {80        LOG.info(String.format(81            "Changing status of node %s from %s to %s. Reason: %s",82            node.getId(),83            previous,84            current,85            result.getMessage()));86      }87    };88    bus.addListener(SESSION_CLOSED, event -> {89      SessionId id = event.getData(SessionId.class);90      this.slots.forEach(slot -> slot.onEnd(id));91    });92    update(node.getStatus());93  }94  public void update(NodeStatus status) {95    Require.nonNull("Node status", status);96    Lock writeLock = lock.writeLock();97    writeLock.lock();98    try {99      this.slots = status.getSlots().stream()100        .map(slot -> new Slot(node, slot.getStereotype(), slot.getSession().isPresent() ? ACTIVE : AVAILABLE))101        .collect(toImmutableSet());102      // By definition, we can never have more sessions than we have slots available103      this.maxSessionCount = Math.min(this.slots.size(), status.getMaxSessionCount());104    } finally {105      writeLock.unlock();106    }107  }108  public NodeId getId() {109    return nodeId;110  }111  public DistributorStatus.NodeSummary asSummary() {112    Map<Capabilities, Integer> stereotypes = new HashMap<>();113    Map<Capabilities, Integer> used = new HashMap<>();114    Set<Session> activeSessions = new HashSet<>();115    slots.forEach(slot -> {116      stereotypes.compute(slot.getStereotype(), (key, curr) -> curr == null ? 1 : curr + 1);117      if (slot.getStatus() != AVAILABLE) {118        used.compute(slot.getStereotype(), (key, curr) -> curr == null ? 1 : curr + 1);119        activeSessions.add(slot.getCurrentSession());120      }121    });122    return new DistributorStatus.NodeSummary(123      nodeId,124      uri,125      getHostStatus(),126      maxSessionCount,127      stereotypes,128      used,129      activeSessions);130  }131  public Availability getHostStatus() {132    return status;133  }134  /**135   * @return The previous status of the node.136   */137  private Availability setHostStatus(Availability status) {138    Availability toReturn = this.status;139    this.status = Require.nonNull("Status", status);140    return toReturn;141  }142  /**143   * @return The previous status of the node if it not able to drain else returning draining status.144   */145  public Availability drainHost() {146    Availability prev = this.status;147    // Drain the node148    if (!node.isDraining()) {149      node.drain();150    }151    // For some reason, it is still not draining then do not update the host status152    if (!node.isDraining()) {153      return prev;154    } else {155      this.status = DRAINING;156      return DRAINING;157    }158  }159  /**160   * @return Whether or not the host has slots available for the requested capabilities.161   */162  public boolean hasCapacity(Capabilities caps) {163    Lock read = lock.readLock();164    read.lock();165    try {166      long count = slots.stream()...Source:Drain.java  
...33  @Override34  public HttpResponse execute(HttpRequest req) throws UncheckedIOException {35    this.node.drain();36    HttpResponse response = new HttpResponse();37    if (this.node.isDraining()) {38      response.setStatus(HTTP_OK);39    } else {40      response.setStatus(HTTP_INTERNAL_ERROR);41    }42    return response;43  }44}...isDraining
Using AI Code Generation
1import org.openqa.selenium.grid.config.Config;2import org.openqa.selenium.grid.config.MapConfig;3import org.openqa.selenium.grid.node.LocalNode;4import org.openqa.selenium.grid.node.Node;5import org.openqa.selenium.grid.node.NodeOptions;6import org.openqa.selenium.grid.node.remote.RemoteNode;7import org.openqa.selenium.grid.server.BaseServerOptions;8import org.openqa.selenium.grid.server.Server;9import org.openqa.selenium.grid.server.ServerOptions;10import org.openqa.selenium.grid.web.Routable;11import org.openqa.selenium.internal.Require;12import org.openqa.selenium.json.Json;13import org.openqa.selenium.remote.http.HttpHandler;14import org.openqa.selenium.remote.http.HttpResponse;15import org.openqa.selenium.remote.http.Route;16import org.openqa.selenium.remote.tracing.Tracer;17import java.net.URI;18import java.util.Map;19import java.util.Objects;20import java.util.concurrent.CompletableFuture;21import java.util.concurrent.ConcurrentHashMap;22import java.util.concurrent.atomic.AtomicBoolean;23import java.util.logging.Logger;24public class CustomNode implements Node {25  private static final Logger LOG = Logger.getLogger(CustomNode.class.getName());26  private final URI externalUri;27  private final AtomicBoolean isDraining = new AtomicBoolean(false);28  private final AtomicBoolean isDown = new AtomicBoolean(false);29  private final Map<String, Object> status = new ConcurrentHashMap<>();30  private final Server<?> server;31  private final Node delegate;32  public CustomNode(Tracer tracer, URI externalUri, Node delegate) {33    this.externalUri = Require.nonNull("External URI", externalUri);34    this.delegate = Require.nonNull("Delegate", delegate);35    this.server = new Server<>(new ServerOptions(new MapConfig(ImmutableMap.of(36      "server.address", externalUri.getHost(),37      "server.port", externalUri.getPort(),38      "server.servlets", "custom.node.status"))), tracer, new CustomStatus(this, tracer));39  }40  public URI getUri() {41    return externalUri;42  }43  public boolean isDraining() {44    return isDraining.get();45  }46  public boolean isDown() {47    return isDown.get();48  }49  public Map<String, Object> getStatus() {50    return status;51  }52  public void start() {53    server.start();54    delegate.start();55  }56  public void stop() {57    server.stop();58    delegate.stop();isDraining
Using AI Code Generation
1public void isDraining() throws IOException, InterruptedException {2   WebDriver driver = new ChromeDriver();3   Node node = new Node();4   boolean isDraining = node.isDraining();5   driver.quit();6}7public void isDraining() throws IOException, InterruptedException {8   WebDriver driver = new ChromeDriver();9   Node node = new Node();10   boolean isDraining = node.isDraining();11   driver.quit();12}13public void isDraining() throws IOException, InterruptedException {14   WebDriver driver = new ChromeDriver();15   Node node = new Node();16   boolean isDraining = node.isDraining();17   driver.quit();18}19public void isDraining() throws IOException, InterruptedException {20   WebDriver driver = new ChromeDriver();21   Node node = new Node();22   boolean isDraining = node.isDraining();23   driver.quit();24}25public void isDraining() throws IOException, InterruptedException {26   WebDriver driver = new ChromeDriver();27   Node node = new Node();28   boolean isDraining = node.isDraining();29   driver.quit();30}31public void isDraining() throws IOException, InterruptedException {32   WebDriver driver = new ChromeDriver();33   Node node = new Node();34   boolean isDraining = node.isDraining();isDraining
Using AI Code Generation
1import org.openqa.selenium.grid.node.Node;2public class NodeDraining {3    public static void main(String[] args) throws Exception {4        boolean isNodeDraining = node.isDraining();5        if (isNodeDraining) {6            System.out.println("Node is draining");7            Thread.sleep(10000);8            isNodeDraining = node.isDraining();9        }10        System.out.println("Node is not draining");11    }12}LambdaTest’s Selenium 4 tutorial is covering every aspects of Selenium 4 testing with examples and best practices. Here you will learn basics, such as how to upgrade from Selenium 3 to Selenium 4, to some advanced concepts, such as Relative locators and Selenium Grid 4 for Distributed testing. Also will learn new features of Selenium 4, such as capturing screenshots of specific elements, opening a new tab or window on the browser, and new protocol adoptions.
Upgrading From Selenium 3 To Selenium 4?: In this chapter, learn in detail how to update Selenium 3 to Selenium 4 for Java binding. Also, learn how to upgrade while using different build tools such as Maven or Gradle and get comprehensive guidance for upgrading Selenium.
What’s New In Selenium 4 & What’s Being Deprecated? : Get all information about new implementations in Selenium 4, such as W3S protocol adaption, Optimized Selenium Grid, and Enhanced Selenium IDE. Also, learn what is deprecated for Selenium 4, such as DesiredCapabilites and FindsBy methods, etc.
Selenium 4 With Python: Selenium supports all major languages, such as Python, C#, Ruby, and JavaScript. In this chapter, learn how to install Selenium 4 for Python and the features of Python in Selenium 4, such as Relative locators, Browser manipulation, and Chrom DevTool protocol.
Selenium 4 Is Now W3C Compliant: JSON Wireframe protocol is retiring from Selenium 4, and they are adopting W3C protocol to learn in detail about the advantages and impact of these changes.
How To Use Selenium 4 Relative Locator? : Selenium 4 came with new features such as Relative Locators that allow constructing locators with reference and easily located constructors nearby. Get to know its different use cases with examples.
Selenium Grid 4 Tutorial For Distributed Testing: Selenium Grid 4 allows you to perform tests over different browsers, OS, and device combinations. It also enables parallel execution browser testing, reads up on various features of Selenium Grid 4 and how to download it, and runs a test on Selenium Grid 4 with best practices.
Selenium Video Tutorials: Binge on video tutorials on Selenium by industry experts to get step-by-step direction from automating basic to complex test scenarios with Selenium.
LambdaTest also provides certification for Selenium testing to accelerate your career in Selenium automation testing.
Get 100 minutes of automation test minutes FREE!!
