Skip to content

Commit b4f93eb

Browse files
authored
Fix random error in with-driver-tests (#681)
* Fix -connect-driver to wait for ready driver and recreate session There is a race condition in Firefox that sometimes results in a zombie session. When we detect this condition, we throw away the bad session and create another one. * Fix fake driver to wrap data in :value and add Get Window Handle * Fix tests in proc_test.clj * Fix tests in unit_test.clj * Update CHANGELOG * Fix PR comment items * Fix OBOB and make log message and var names more accurate
1 parent 371a394 commit b4f93eb

File tree

5 files changed

+79
-21
lines changed

5 files changed

+79
-21
lines changed

CHANGELOG.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ A release with an intentional breaking changes is marked with:
2020
== Unreleased
2121

2222
* Changes
23+
** {issue}676[#676]: Fix new driver creation so that it sidesteps some underlying Firefox race conditions and improves CI test stability. ({person}dgr[@dgr])
2324
** {issue}679[#679]: Add `new-window` function that exposes WebDriver's New Window endpoint. ({person}dgr[@dgr])
2425

2526
== v1.1.42 - 2024-09-27 [[v1.1.42]]

script/fake_driver.clj

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,23 @@
2727
(defn make-handler [_opts]
2828
(fn handle-request [{:keys [request-method uri] :as _req}]
2929
(cond
30+
;; Get Status
31+
(and (= :get request-method) (= "/status" uri))
32+
{:status 200
33+
:headers {"Content-Type" "application/json"}
34+
:body (json/generate-string {:value {:ready true
35+
:message "I'm ready"}})}
36+
;; Create Session
3037
(and (= :post request-method) (= "/session" uri))
3138
{:status 200
3239
:headers {"Content-Type" "application/json"}
33-
:body (json/generate-string {:sessionId (random-uuid)})}
34-
(and (= :get request-method) (= "/status" uri))
40+
:body (json/generate-string {:value {:sessionId (random-uuid)}})}
41+
;; Get Window Handle
42+
(and (= :get request-method) (re-matches #"/session/[^/]+/window" uri))
3543
{:status 200
3644
:headers {"Content-Type" "application/json"}
37-
:body (json/generate-string {:ready true})}
45+
:body (json/generate-string {:value (random-uuid)})}
46+
;; Fake Driver is... well... fake.
3847
:else
3948
{:status 404})))
4049

src/etaoin/api.clj

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3465,6 +3465,7 @@
34653465
(:post-run-actions driver))
34663466
(recur (inc try-num) (:exception res)))))))))
34673467

3468+
34683469
(defn- -connect-driver
34693470
"Connects to a running Webdriver server.
34703471
@@ -3547,8 +3548,41 @@
35473548
(drv/set-capabilities (get-in defaults [type :capabilities]))
35483549
(drv/set-capabilities capabilities)))
35493550
caps (:capabilities driver)
3550-
session (create-session driver caps)]
3551-
(assoc driver :session session)))
3551+
max-tries 3]
3552+
(loop [n 1]
3553+
;; Wait for driver to be ready before creating the first session.
3554+
(wait-predicate
3555+
(fn [] (-> (get-status driver) :ready))
3556+
{:timeout 30
3557+
:interval 0.100
3558+
:message "Timeout waiting for WebDriver to become ready after creation."})
3559+
;; The following code works around a bug in
3560+
;; geckodriver/Firefox. In some cases, calling create-session on
3561+
;; a geckodriver will return a "bad" session instance. That
3562+
;; session instance will throw errors for the simplest queries.
3563+
;; The root problem appears to be a race condition somewhere in
3564+
;; the geckodriver/marionette/Firefox pathway as it is
3565+
;; inconsistent and difficult to reproduce. To work around this,
3566+
;; we try to detect a "bad session" and if we have one, we
3567+
;; delete it and create another. Note that the problem is
3568+
;; geckodriver/Firefox-specific, but we do this for all drivers
3569+
;; because it works anyway (non-Firefox drivers just return
3570+
;; a "good session") and it simplifies the logic.
3571+
(let [driver (assoc driver :session (create-session driver caps))
3572+
[good-session e] (try (get-window-handle driver)
3573+
[true nil]
3574+
(catch Exception e
3575+
[false e]))]
3576+
(if good-session
3577+
driver
3578+
(do
3579+
(delete-session driver)
3580+
(if (<= n max-tries) ; max total tries = 3
3581+
(do (log/warnf e "Bad session detected. Try again %d/%d." (inc n) max-tries)
3582+
(recur (inc n)))
3583+
(throw+ {:type :etaoin/retries
3584+
:message "Could not create a good session after multiple retries"}
3585+
e))))))))
35523586

35533587
(defn disconnect-driver
35543588
"Returns new `driver` after disconnecting from a running WebDriver process.

test/etaoin/unit/proc_test.clj

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -105,23 +105,30 @@
105105

106106
(deftest http-error-on-create-proc-alive
107107
;; when etaoin tries to create a session return an http error
108-
(with-redefs [client/http-request (fn [_] {:status 400})]
109-
(let [ex (try
110-
(e/with-firefox _driver
111-
(is false "should not reach here"))
112-
(catch Throwable ex
113-
{:exception ex}))
114-
exd (-> ex :exception ex-data)]
115-
(is (= :etaoin/http-error (:type exd)))
116-
(is (= 400 (:status exd)))
117-
(is (= nil (-> exd :driver :process :exit)))
118-
(is (= 0 (get-count-firefoxdriver-instances))))))
108+
(let [orig-http-request client/http-request]
109+
(with-redefs [client/http-request (fn [{:keys [method uri] :as params}]
110+
;; allow get status and create session through, fail on everything else
111+
(if (and (= :get method) (str/ends-with? uri "/status"))
112+
(orig-http-request params)
113+
{:status 400}))]
114+
(let [ex (try
115+
(e/with-firefox _driver
116+
(is false "should not reach here"))
117+
(catch Throwable ex
118+
{:exception ex}))
119+
exd (-> ex :exception ex-data)]
120+
(is (= :etaoin/http-error (:type exd)))
121+
(is (= 400 (:status exd)))
122+
(is (= nil (-> exd :driver :process :exit)))
123+
(is (= 0 (get-count-firefoxdriver-instances)))))))
119124

120125
(deftest http-exception-after-create-proc-now-dead
121126
(let [orig-http-request client/http-request]
122127
(with-redefs [client/http-request (fn [{:keys [method uri] :as params}]
123-
;; allow create session through, fail on everything else
124-
(if (and (= :post method) (str/ends-with? uri "/session"))
128+
;; allow get status and create session through, fail on everything else
129+
(if (or (and (= :get method) (str/ends-with? uri "/status"))
130+
(and (= :post method) (str/ends-with? uri "/session"))
131+
(and (= :get method) (re-find #"/session/[^/]+/window" uri)))
125132
(orig-http-request params)
126133
(throw (ex-info "read timeout" {}))))]
127134
(let [ex (try
@@ -130,7 +137,8 @@
130137
(is (= 1 (get-count-firefoxdriver-instances)))
131138
(proc/kill (:process driver))
132139
;; we'll now fail on this call
133-
(e/go driver "https://clojure.org"))
140+
(e/go driver "https://clojure.org")
141+
(is false "Should have thrown an exception."))
134142
(catch Throwable ex
135143
{:exception ex}))
136144
exd (-> ex :exception ex-data)]
@@ -143,8 +151,10 @@
143151
;; unlikely, we know we just talked to the driver because it returned an http error, but for completeness
144152
(let [orig-http-request client/http-request]
145153
(with-redefs [client/http-request (fn [{:keys [method uri] :as params}]
146-
;; allow create session through, fail on everything else
147-
(if (and (= :post method) (str/ends-with? uri "/session"))
154+
;; allow get status and create session through, fail on everything else
155+
(if (or (and (= :get method) (str/ends-with? uri "/status"))
156+
(and (= :post method) (str/ends-with? uri "/session"))
157+
(and (= :get method) (re-find #"/session/[^/]+/window" uri)))
148158
(orig-http-request params)
149159
{:status 418}))]
150160
(let [ex (try

test/etaoin/unit/unit_test.clj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
(with-redefs
1717
[etaoin.impl.proc/run (fn [_ _] {:some :process})
1818
e/wait-running identity
19+
e/get-status (fn [_] {:ready true :message "I'm ready"})
1920
e/create-session (fn [_ _] "session-key")
21+
e/get-window-handle (fn [_] "ABCDEFG")
2022
proc/kill identity
2123
e/delete-session identity
2224
util/get-free-port (constantly 12345)]
@@ -142,7 +144,9 @@
142144
[etaoin.impl.proc/run (fn [_ _]
143145
(swap! run-calls inc)
144146
{:some :process})
147+
e/get-status (fn [_] {:ready true :message "I'm ready"})
145148
e/create-session (fn [_ _] "session-key")
149+
e/get-window-handle (fn [_] "ABCDEFG")
146150
proc/kill (fn [_]
147151
(swap! kill-calls inc))
148152
e/delete-session identity

0 commit comments

Comments
 (0)