Skip to content

Commit c7f34ee

Browse files
committed
server: Refactoring driver sessions to break direct dependency on system clock to increase testability
1 parent a38226b commit c7f34ee

File tree

10 files changed

+152
-28
lines changed

10 files changed

+152
-28
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
19+
package org.openqa.selenium.remote.server;
20+
21+
/**
22+
* A simple encapsulation to allowing timing
23+
*/
24+
public interface Clock {
25+
26+
/**
27+
* Returns the current time.
28+
*
29+
* @return The current time.
30+
*/
31+
long now();
32+
33+
/**
34+
* Moves the clock to the future.
35+
*
36+
* @param durationInMillis Time to pass, in milliseconds.
37+
*/
38+
void pass(long durationInMillis);
39+
}

Diff for: java/server/src/org/openqa/selenium/remote/server/DefaultDriverSessions.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public class DefaultDriverSessions implements DriverSessions {
3939
private static final Logger LOG = Logger.getLogger(DefaultDriverSessions.class.getName());
4040

4141
private final DriverFactory factory;
42+
private final Clock clock;
4243

4344
private final Map<SessionId, Session> sessionIdToDriver =
4445
new ConcurrentHashMap<SessionId, Session>();
@@ -82,10 +83,16 @@ public DefaultDriverSessions(
8283
for (Map.Entry<Capabilities, Class<? extends WebDriver>> entry : drivers.entrySet()) {
8384
registerDriver(entry.getKey(), entry.getValue());
8485
}
86+
this.clock = new SystemClock();
8587
}
8688

8789
protected DefaultDriverSessions(Platform runningOn, DriverFactory factory) {
90+
this(runningOn, factory, new SystemClock());
91+
}
92+
93+
protected DefaultDriverSessions(Platform runningOn, DriverFactory factory, Clock clock) {
8894
this.factory = factory;
95+
this.clock = clock;
8996
registerDefaults(runningOn);
9097
registerServiceLoaders(runningOn);
9198
}
@@ -127,7 +134,7 @@ public void registerDriver(Capabilities capabilities, Class<? extends WebDriver>
127134

128135
public SessionId newSession(Capabilities desiredCapabilities) throws Exception {
129136
SessionId sessionId = new SessionId(UUID.randomUUID().toString());
130-
Session session = DefaultSession.createSession(factory, sessionId, desiredCapabilities);
137+
Session session = DefaultSession.createSession(factory, clock, sessionId, desiredCapabilities);
131138

132139
sessionIdToDriver.put(sessionId, session);
133140

Diff for: java/server/src/org/openqa/selenium/remote/server/DefaultSession.java

+16-9
Original file line numberDiff line numberDiff line change
@@ -70,37 +70,44 @@ public class DefaultSession implements Session {
7070
private final KnownElements knownElements;
7171
private final ThreadPoolExecutor executor;
7272
private final Capabilities capabilities; // todo: Investigate memory model implications of map
73+
private final Clock clock;
7374
// elements inside capabilities.
7475
private volatile String base64EncodedImage;
7576
private volatile long lastAccess;
7677
private volatile Thread inUseWithThread = null;
7778
private TemporaryFilesystem tempFs;
7879

7980
// This method is to avoid constructor escape of partially constructed session object
80-
public static Session createSession(DriverFactory factory,
81-
SessionId sessionId, Capabilities capabilities)
82-
throws Exception {
81+
public static Session createSession(DriverFactory factory, SessionId sessionId,
82+
Capabilities capabilities) throws Exception {
83+
return createSession(factory, new SystemClock(), sessionId, capabilities);
84+
}
85+
86+
// This method is to avoid constructor escape of partially constructed session object
87+
public static Session createSession(DriverFactory factory, Clock clock, SessionId sessionId,
88+
Capabilities capabilities) throws Exception {
8389
File tmpDir = new File(System.getProperty("java.io.tmpdir"), sessionId.toString());
8490
if (!tmpDir.mkdir()) {
8591
throw new WebDriverException("Cannot create temp directory: " + tmpDir);
8692
}
8793
TemporaryFilesystem tempFs = TemporaryFilesystem.getTmpFsBasedOn(tmpDir);
8894

89-
return new DefaultSession(factory, tempFs, sessionId, capabilities);
95+
return new DefaultSession(factory, tempFs, clock, sessionId, capabilities);
9096
}
9197

9298
@VisibleForTesting
93-
public static Session createSession(DriverFactory factory, TemporaryFilesystem tempFs,
99+
public static Session createSession(DriverFactory factory, TemporaryFilesystem tempFs, Clock clock,
94100
SessionId sessionId, Capabilities capabilities)
95101
throws Exception {
96-
return new DefaultSession(factory, tempFs, sessionId, capabilities);
102+
return new DefaultSession(factory, tempFs, clock, sessionId, capabilities);
97103
}
98104

99-
private DefaultSession(final DriverFactory factory, TemporaryFilesystem tempFs,
105+
private DefaultSession(final DriverFactory factory, TemporaryFilesystem tempFs, Clock clock,
100106
SessionId sessionId, final Capabilities capabilities) throws Exception {
101107
this.knownElements = new KnownElements();
102108
this.sessionId = sessionId;
103109
this.tempFs = tempFs;
110+
this.clock = clock;
104111
final BrowserCreator browserCreator = new BrowserCreator(factory, capabilities);
105112
final FutureTask<EventFiringWebDriver> webDriverFutureTask =
106113
new FutureTask<EventFiringWebDriver>(browserCreator);
@@ -141,11 +148,11 @@ private static boolean isQuietModeEnabled(BrowserCreator browserCreator, Capabil
141148
* Touches the session.
142149
*/
143150
public void updateLastAccessTime() {
144-
lastAccess = System.currentTimeMillis();
151+
lastAccess = clock.now();
145152
}
146153

147154
public boolean isTimedOut(long timeout) {
148-
return timeout > 0 && (lastAccess + timeout) < System.currentTimeMillis();
155+
return timeout > 0 && (lastAccess + timeout) < clock.now();
149156
}
150157

151158
public void close() {

Diff for: java/server/src/org/openqa/selenium/remote/server/SessionCleaner.java

+7-5
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,17 @@ class SessionCleaner extends Thread { // Thread safety reviewed
3434
private final long insideBrowserTimeout;
3535
private final long sleepInterval;
3636
private final Logger log;
37+
private final Clock clock;
3738
private volatile boolean running = true;
3839

3940
SessionCleaner(DriverSessions driverSessions, Logger log, long clientGoneTimeout, long insideBrowserTimeout) {
41+
this(driverSessions, log, new SystemClock(), clientGoneTimeout, insideBrowserTimeout);
42+
}
43+
44+
SessionCleaner(DriverSessions driverSessions, Logger log, Clock clock, long clientGoneTimeout, long insideBrowserTimeout) {
4045
super("DriverServlet Session Cleaner");
4146
this.log = log;
47+
this.clock = clock;
4248
this.clientGoneTimeout = clientGoneTimeout;
4349
this.insideBrowserTimeout = insideBrowserTimeout;
4450
this.driverSessions = driverSessions;
@@ -60,11 +66,7 @@ class SessionCleaner extends Thread { // Thread safety reviewed
6066
public void run() {
6167
while (running) {
6268
checkExpiry();
63-
try {
64-
Thread.sleep(sleepInterval);
65-
} catch (InterruptedException e) {
66-
log.info("Exiting session cleaner thread");
67-
}
69+
clock.pass(sleepInterval);
6870
}
6971
}
7072

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
19+
package org.openqa.selenium.remote.server;
20+
21+
public class SystemClock implements Clock {
22+
23+
public long now() {
24+
return System.currentTimeMillis();
25+
}
26+
27+
public void pass(long durationInMillis) {
28+
try {
29+
Thread.sleep(durationInMillis);
30+
} catch (InterruptedException ignore) {
31+
}
32+
}
33+
}

Diff for: java/server/src/org/openqa/selenium/remote/server/build.desc

+4-2
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,20 @@ java_library(name = "server_core",
5959
java_library(name = "server_very_core",
6060
srcs = [
6161
"CapabilitiesComparator.java",
62+
"Clock.java",
6263
"DefaultDriverFactory.java",
6364
"DefaultDriverProvider.java",
6465
"DefaultDriverSessions.java",
66+
"DefaultSession.java",
6567
"DriverFactory.java",
6668
"DriverProvider.java",
6769
"DriverSessions.java",
68-
"KnownElements.java",
6970
"JsonHttpCommandHandler.java",
71+
"KnownElements.java",
7072
"MimeType.java",
71-
"DefaultSession.java",
7273
"SessionCleaner.java",
7374
"SnapshotScreenListener.java",
75+
"SystemClock.java",
7476
],
7577
deps = [
7678
":restish",

Diff for: java/server/test/org/openqa/selenium/remote/server/DefaultSessionTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public void shouldClearTempFsWhenSessionCloses() throws Exception {
3939
when(factory.newInstance(any(Capabilities.class))).thenReturn(mock(WebDriver.class));
4040
final TemporaryFilesystem tempFs = mock(TemporaryFilesystem.class);
4141

42-
Session session = DefaultSession.createSession(factory, tempFs, null, DesiredCapabilities.firefox());
42+
Session session = DefaultSession.createSession(factory, tempFs, new SystemClock(), null, DesiredCapabilities.firefox());
4343

4444
session.close();
4545
verify(tempFs).deleteTemporaryFiles();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Licensed to the Software Freedom Conservancy (SFC) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The SFC licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
19+
package org.openqa.selenium.remote.server;
20+
21+
public class FakeClock implements Clock {
22+
23+
private long now = 0;
24+
25+
public long now() {
26+
return now;
27+
}
28+
29+
public void pass(long durationInMillis) {
30+
now += durationInMillis;
31+
}
32+
}

Diff for: java/server/test/org/openqa/selenium/remote/server/SessionCleanerTest.java

+9-8
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public class SessionCleanerTest {
5050

5151
@Test
5252
public void testCleanup() throws Exception {
53-
DriverSessions defaultDriverSessions = getDriverSessions();
53+
DriverSessions defaultDriverSessions = getDriverSessions(new SystemClock());
5454
defaultDriverSessions.newSession(DesiredCapabilities.firefox());
5555
defaultDriverSessions.newSession(DesiredCapabilities.firefox());
5656
assertEquals(2, defaultDriverSessions.getSessions().size());
@@ -110,7 +110,7 @@ public Object call() {
110110

111111
@Test
112112
public void testCleanupWithThread() throws Exception {
113-
DriverSessions defaultDriverSessions = getDriverSessions();
113+
DriverSessions defaultDriverSessions = getDriverSessions(new SystemClock());
114114
defaultDriverSessions.newSession(DesiredCapabilities.firefox());
115115
defaultDriverSessions.newSession(DesiredCapabilities.firefox());
116116
assertEquals(2, defaultDriverSessions.getSessions().size());
@@ -144,23 +144,24 @@ void checkExpiry() {
144144

145145
@Test
146146
public void testCleanupWithSessionExtension() throws Exception {
147-
DriverSessions defaultDriverSessions = getDriverSessions();
147+
Clock clock = new FakeClock();
148+
DriverSessions defaultDriverSessions = getDriverSessions(clock);
148149
SessionId firstSession = defaultDriverSessions.newSession(DesiredCapabilities.firefox());
149150
defaultDriverSessions.newSession(DesiredCapabilities.firefox());
150-
SessionCleaner sessionCleaner = new SessionCleaner(defaultDriverSessions, log, 100, 100);
151+
SessionCleaner sessionCleaner = new SessionCleaner(defaultDriverSessions, log, clock, 100, 100);
151152
defaultDriverSessions.get(firstSession).updateLastAccessTime();
152-
waitForAllSessionsToExpire(120);
153+
clock.pass(120);
153154
defaultDriverSessions.get(firstSession).updateLastAccessTime();
154155
sessionCleaner.checkExpiry();
155156
assertEquals(1, defaultDriverSessions.getSessions().size());
156-
waitForAllSessionsToExpire(120);
157+
clock.pass(120);
157158
sessionCleaner.checkExpiry();
158159
assertEquals(0, defaultDriverSessions.getSessions().size());
159160
}
160161

161-
private DriverSessions getDriverSessions() {
162+
private DriverSessions getDriverSessions(Clock clock) {
162163
DriverFactory factory = mock(DriverFactory.class);
163164
when(factory.newInstance(any(Capabilities.class))).thenReturn(mock(WebDriver.class));
164-
return new DefaultDriverSessions(Platform.LINUX, factory);
165+
return new DefaultDriverSessions(Platform.LINUX, factory, clock);
165166
}
166167
}

Diff for: java/server/test/org/openqa/selenium/remote/server/handler/UploadFileTest.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.openqa.selenium.remote.server.DefaultSession;
4343
import org.openqa.selenium.remote.server.DriverFactory;
4444
import org.openqa.selenium.remote.server.Session;
45+
import org.openqa.selenium.remote.server.SystemClock;
4546

4647
import java.io.File;
4748
import java.io.IOException;
@@ -72,7 +73,7 @@ public void cleanUp() {
7273

7374
@Test
7475
public void shouldWriteABase64EncodedZippedFileToDiskAndKeepName() throws Exception {
75-
Session session = DefaultSession.createSession(driverFactory, tempFs, sessionId, DesiredCapabilities.firefox());
76+
Session session = DefaultSession.createSession(driverFactory, tempFs, new SystemClock(), sessionId, DesiredCapabilities.firefox());
7677

7778
File tempFile = touch(null, "foo");
7879
String encoded = new Zip().zipFile(tempFile.getParentFile(), tempFile);
@@ -88,7 +89,7 @@ public void shouldWriteABase64EncodedZippedFileToDiskAndKeepName() throws Except
8889

8990
@Test
9091
public void shouldThrowAnExceptionIfMoreThanOneFileIsSent() throws Exception {
91-
Session session = DefaultSession.createSession(driverFactory, tempFs, sessionId, DesiredCapabilities.firefox());
92+
Session session = DefaultSession.createSession(driverFactory, tempFs, new SystemClock(), sessionId, DesiredCapabilities.firefox());
9293
File baseDir = Files.createTempDir();
9394

9495
touch(baseDir, "example");

0 commit comments

Comments
 (0)