View Javadoc
1   /*
2    * Copyright 2023 Adrian Herscu
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package dev.aherscu.qa.jgiven.webdriver;
18  
19  import static com.google.common.base.Suppliers.*;
20  import static dev.aherscu.qa.jgiven.commons.utils.SessionName.*;
21  import static dev.aherscu.qa.testing.utils.EnumUtils.*;
22  import static dev.aherscu.qa.testing.utils.StringUtilsExtensions.*;
23  import static java.util.Collections.*;
24  import static java.util.Locale.*;
25  import static java.util.Objects.*;
26  import static java.util.stream.Collectors.*;
27  
28  import java.util.*;
29  import java.util.concurrent.atomic.*;
30  import java.util.function.*;
31  import java.util.stream.*;
32  
33  import org.apache.commons.configuration.*;
34  import org.openqa.selenium.Platform;
35  import org.openqa.selenium.chrome.*;
36  
37  import com.google.common.collect.*;
38  
39  import dev.aherscu.qa.jgiven.commons.utils.*;
40  import dev.aherscu.qa.testing.utils.*;
41  import dev.aherscu.qa.testing.utils.config.BaseConfiguration;
42  import lombok.*;
43  import lombok.extern.slf4j.*;
44  
45  /**
46   * Provides WebDriver-related configuration items.
47   */
48  @Slf4j
49  public class WebDriverConfiguration extends BaseConfiguration {
50  
51      enum DeviceType {
52          _WINDOWS, _IOS, _ANDROID, _WEB;
53  
54          static DeviceType from(final String deviceType) {
55              return isBlank(deviceType)
56                  ? _WEB
57                  : fromString(DeviceType.class,
58                      deviceType.toUpperCase(ROOT));
59          }
60  
61          @Override
62          public String toString() {
63              return EnumUtils.toString(this).toLowerCase(ROOT);
64          }
65      }
66  
67      // NOTE must be static otherwise all tests will run with same capabilities.
68      // This also means that if two instances are created with different
69      // devices, hence different sets of capabilities, this mechanism will break.
70      private static final AtomicInteger                  nextRequiredCapabilitiesIndex =
71          new AtomicInteger(0);
72      private static final AtomicReference<DeviceType>    theDeviceType                 =
73          new AtomicReference<>();
74      private final Supplier<List<DesiredCapabilitiesEx>> requiredCapabilities          =
75          memoize(() -> unmodifiableList(loadRequiredCapabilities()
76              .peek(capabilities -> log.trace("loaded {}", capabilities))
77              .collect(toList())));
78  
79      /**
80       * @param configurations
81       *            set of configurations to be aggregated; the `device.type`
82       *            property has a filtering effect on results of
83       *            {@link #requiredCapabilities()} and of {@link #capabilities()}
84       *            methods, hence these methods will work correctly only if all
85       *            instances are created with same `device.type`, or otherwise
86       *            {@code resetNextRequiredCapabilitiesIndex()} is called in
87       *            between
88       */
89      public WebDriverConfiguration(final Configuration... configurations) {
90          super(configurations);
91          if (theDeviceType.compareAndSet(null, deviceType())) {
92              log.info("initialized with {}", deviceType());
93          } else {
94              log.warn(
95                  "attempting to re-initialize with {}; resetting next required capabilities index from {}",
96                  deviceType(),
97                  resetNextRequiredCapabilitiesIndex());
98          }
99      }
100 
101     /**
102      * Resets the index of next required set of capabilities to specified value.
103      *
104      * <p>
105      * Useful for unit testing.
106      * </p>
107      *
108      * @param value
109      *            the new value
110      *
111      * @return previous index value
112      */
113     static int resetNextRequiredCapabilitiesIndex(final int value) {
114         val oldValue = nextRequiredCapabilitiesIndex.get();
115         nextRequiredCapabilitiesIndex.set(value);
116         return oldValue;
117     }
118 
119     /**
120      * Resets the index of next required set of capabilities to zero.
121      *
122      * <p>
123      * Useful for unit testing.
124      * </p>
125      *
126      * @return previous index value
127      */
128     static int resetNextRequiredCapabilitiesIndex() {
129         return resetNextRequiredCapabilitiesIndex(0);
130     }
131 
132     /**
133      * @param deviceType
134      *            a specific device type
135      * @return matching capabilities for configured {@link #provider()} and
136      *         specified device type
137      */
138     @SneakyThrows
139     public DesiredCapabilitiesEx capabilities(final DeviceType deviceType) {
140         return capabilitiesFor(provider() + deviceType.toString());
141     }
142 
143     /**
144      * @return capabilities for configured {@link #provider()} and
145      *         {@link #deviceType()};
146      *
147      *         <p>
148      *         if {@link #deviceType()} is not blank then retrieves only
149      *         matching capabilities from
150      *         {@code required-capabilities.properties};
151      *
152      *         <p>
153      *         if no matching capabilities found, then returns whatever is
154      *         defined in {@code webdriver.properties} for current
155      *         {@link #provider()}
156      */
157     @SneakyThrows
158     public DesiredCapabilitiesEx capabilities() {
159         return nextRequiredCapabilities(deviceType())
160             .orElse(capabilities(deviceType()));
161     }
162 
163     /**
164      * @param prefix
165      *            prefix
166      * @return capabilities for specified prefix; also adds
167      *         {@code <thread-name>:<current-time-millis>} to retrieved
168      *         capabilities
169      */
170     @SuppressWarnings("serial")
171     @SneakyThrows
172     public DesiredCapabilitiesEx capabilitiesFor(final String prefix) {
173         log.trace("building capabilities for {}", prefix);
174         return new DesiredCapabilitiesEx() {
175             {
176                 setCapability("sauce:name", generateFromCurrentThreadAndTime());
177                 entrySet()
178                     .stream()
179                     .map(entry -> new AbstractMap.SimpleImmutableEntry<>(
180                         entry.getKey().toString(),
181                         entry.getValue().toString()))
182                     .filter(entry -> entry.getKey()
183                         .startsWith(prefix + DOT))
184                     .peek(capabilitiesEntry -> log.trace(
185                         "adding capabilities entry {}",
186                         capabilitiesEntry))
187                     .forEach(capabilitiesEntry -> setCapability(
188                         substringAfter(capabilitiesEntry.getKey(),
189                             prefix + DOT),
190                         capabilitiesEntry.getValue()));
191             }
192         };
193     }
194 
195     /**
196      * @return the `device.type` as specified in source configuration, or
197      *         {@link Platform#ANY} if none specified
198      */
199     public DeviceType deviceType() {
200         return DeviceType.from(getString("device.type", EMPTY));
201     }
202 
203     /**
204      * @return the `provider` as specified source configuration
205      */
206     public String provider() {
207         return getString("provider");
208     }
209 
210     /**
211      * @param deviceType
212      *            device type to filter by
213      * @return all matching device capabilities, if any
214      */
215     public List<DesiredCapabilitiesEx> requiredCapabilities(
216         final DeviceType deviceType) {
217         return requiredCapabilities.get()
218             .stream()
219             .filter(capabilities -> DeviceType
220                 .from(requireNonNull(capabilities
221                     .getCapability("-x:type"),
222                     "internal error, no type capability")
223                     .toString())
224                 .equals(deviceType))
225             .peek(capabilities -> log.trace("found required capabilities {}",
226                 capabilities))
227             .collect(toList());
228     }
229 
230     /**
231      * @return device capabilities per configuration
232      */
233     public List<DesiredCapabilitiesEx> requiredCapabilities() {
234         return requiredCapabilities(deviceType());
235     }
236 
237     private Stream<DesiredCapabilitiesEx> loadRequiredCapabilities() {
238         return groupsOf("required.capability")
239             .map(requiredCapabilitiesGroup -> new DesiredCapabilitiesEx(
240                 capabilitiesFor(provider()
241                     + requiredCapabilitiesGroup.get("type")))
242                 .with(requiredCapabilitiesGroup
243                     .entrySet()
244                     .stream()
245                     .peek(e -> log.trace("adding capabilities entry {}", e))
246                     .map(e -> Maps.immutableEntry(
247                         "type".equals(e.getKey()) ? "-x:type" : e.getKey(),
248                         e.getValue()))));
249     }
250 
251     /**
252      * @param deviceType
253      *            device type to filter by
254      * @return next matching device capabilities as listed in
255      *         {@code required-capabilities.properties}
256      */
257     private Optional<DesiredCapabilitiesEx> nextRequiredCapabilities(
258         final DeviceType deviceType) {
259         val capabilities = requiredCapabilities(deviceType);
260 
261         // NOTE: this method might be called from multiple tests running in
262         // parallel hence must use an atomic update operation on the
263         // nextAvailableDeviceCapabilities index
264         return capabilities.isEmpty()
265             ? Optional.empty()
266             : Optional.of(capabilities
267                 .get(nextRequiredCapabilitiesIndex
268                     .getAndUpdate(
269                         currentIndex -> currentIndex < capabilities.size() - 1
270                             ? currentIndex + 1
271                             : 0))
272                 // NOTE: since the capabilities are loaded on the main thread
273                 // during the initialization of the test class, there is no way
274                 // to assign them a test (thread) name because these are not
275                 // running yet -- hence we do it here
276                 .with("sauce:name", generateFromCurrentThreadAndTime()));
277     }
278 }