1 package io.guixer.tools;
2
3 import static com.google.common.base.Preconditions.checkNotNull;
4 import static com.google.common.base.Preconditions.checkState;
5 import static com.google.common.collect.Lists.newArrayList;
6 import static com.google.common.collect.Maps.newHashMap;
7 import static io.guixer.tools.TestUtils.getTestProperties;
8 import static io.guixer.tools.TestUtils.getTestProperty;
9 import static io.guixer.tools.TestUtils.getTestPropertyOrNull;
10 import static java.nio.charset.StandardCharsets.UTF_8;
11 import static org.apache.commons.lang3.StringUtils.join;
12 import static org.apache.commons.lang3.StringUtils.split;
13 import static org.apache.commons.lang3.StringUtils.substringBeforeLast;
14
15 import java.io.File;
16 import java.io.FileNotFoundException;
17 import java.io.IOException;
18 import java.lang.reflect.Method;
19 import java.net.MalformedURLException;
20 import java.net.URL;
21 import java.net.URLDecoder;
22 import java.util.Arrays;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Map.Entry;
26 import java.util.Properties;
27 import java.util.concurrent.Callable;
28 import java.util.function.Function;
29 import java.util.stream.Collectors;
30
31 import javax.annotation.Nullable;
32
33 import org.apache.commons.lang3.NotImplementedException;
34 import org.junit.jupiter.api.AfterEach;
35 import org.junit.jupiter.api.BeforeEach;
36 import org.junit.jupiter.api.Test;
37 import org.openqa.selenium.Capabilities;
38 import org.openqa.selenium.WebDriver;
39 import org.openqa.selenium.WebDriverException;
40 import org.openqa.selenium.chrome.ChromeOptions;
41 import org.openqa.selenium.firefox.FirefoxOptions;
42 import org.openqa.selenium.remote.RemoteWebDriver;
43
44 import com.google.common.collect.ImmutableList;
45
46 import io.guixer.lang.GuixerScenario;
47 import io.guixer.lang.GuixerScenarioLoader2;
48 import io.guixer.lang.GuixerSyntaxException;
49
50 public abstract class GuixerTest {
51
52 @BeforeEach
53 public final void setUpGuixerContext() throws Exception {
54
55 @Nullable
56 final WebDriver defaultDriver = drivers.get("");
57
58 if (defaultDriver == null) {
59
60
61
62 final String seleniumServerUrl = getTestProperty("seleniumServer.url");
63 final String seleniumDesiredCapabilities = getTestProperty("selenium.desiredCapabilities", "firefox");
64 final List<String> seleniumOptions = parseOptions(getTestPropertyOrNull("selenium.options"));
65
66 System.out.println("seleniumServer.url: " + seleniumServerUrl);
67 System.out.println("selenium.desiredCapabilities: " + seleniumDesiredCapabilities);
68 System.out.println("selenium.options: " + seleniumOptions);
69
70 final Capabilities capabilities = getCapabilities(
71 seleniumDesiredCapabilities,
72 seleniumOptions);
73
74 driver = new RemoteWebDriver(new URL(seleniumServerUrl), capabilities);
75
76 injectDriver("", driver);
77
78 } else {
79
80 driver = defaultDriver;
81 }
82 }
83
84 private static List<String> parseOptions(
85 @Nullable final String optionsProperty
86 ) {
87
88 if (optionsProperty == null) {
89
90 return ImmutableList.of();
91 }
92
93 return ImmutableList.copyOf(split(optionsProperty, " "));
94 }
95
96 @AfterEach
97 public final void tearDownGuixerContext() throws Exception {
98
99 try {
100
101 for (final WebDriver driver : drivers.values()) {
102
103 try {
104
105 driver.quit();
106
107 } catch (final WebDriverException e) {
108
109
110
111 e.printStackTrace();
112 }
113 }
114
115 } finally {
116
117 driver = null;
118 }
119 }
120
121 public static void isolatedRun() throws Exception {
122
123 final GuixerTest test = new GuixerTest() {
124
125 };
126
127 test.setUpGuixerContext();
128
129 try {
130
131 test.run();
132
133 } finally {
134
135 test.tearDownGuixerContext();
136 }
137 }
138
139 private WebDriver driver = null;
140
141 protected final WebDriver getDriver() {
142
143
144
145
146 final WebDriver driver = this.driver;
147
148 if (driver == null) {
149
150 throw new IllegalStateException("Current driver should not be null.");
151 }
152
153 return driver;
154 }
155
156 private WebDriver switchToDriver(
157 final String driverName
158 ) {
159
160 final WebDriver cached = drivers.get(driverName);
161
162 if (cached != null) {
163
164 driver = cached;
165
166 return cached;
167 }
168
169 final String laneId = driverName;
170
171 final String seleniumServerUrl;
172
173 try {
174
175 seleniumServerUrl = getTestProperty("seleniumServer.url." + laneId);
176
177 } catch (final IOException e) {
178
179 throw new RuntimeException(e);
180 }
181
182 final String seleniumDesiredCapabilities;
183
184 try {
185
186 seleniumDesiredCapabilities = getTestProperty("selenium.desiredCapabilities." + laneId, "firefox");
187
188 } catch (final IOException e) {
189
190 throw new RuntimeException(e);
191 }
192
193 final List<String> seleniumOptions;
194
195 try {
196
197 seleniumOptions = parseOptions(getTestPropertyOrNull("selenium.options." + laneId));
198
199 } catch (final IOException e) {
200
201 throw new RuntimeException(e);
202 }
203
204 System.out.println("seleniumServer.url." + laneId + ": " + seleniumServerUrl);
205 System.out.println("selenium.desiredCapabilities." + laneId + ": " + seleniumDesiredCapabilities);
206 System.out.println("selenium.options." + laneId + ": " + seleniumOptions);
207
208 final Capabilities capabilities = getCapabilities(
209 seleniumDesiredCapabilities,
210 seleniumOptions);
211
212 final URL url;
213
214 try {
215
216 url = new URL(seleniumServerUrl);
217
218 } catch (final MalformedURLException e) {
219
220 throw new RuntimeException(e);
221 }
222
223 final WebDriver newDriver = new RemoteWebDriver(url, capabilities);
224
225 injectDriver(driverName, newDriver);
226
227 driver = newDriver;
228
229 return newDriver;
230 }
231
232 private RunnerContext runnerContext = null;
233
234 protected final RunnerContext getRunnerContext() {
235
236
237
238
239 final RunnerContext runnerContext = this.runnerContext;
240
241 if (runnerContext == null) {
242
243 throw new IllegalStateException("Current runnerContext should not be null.");
244 }
245
246 return runnerContext;
247 }
248
249 private static Capabilities getCapabilities(
250 final String seleniumDesiredCapabilities,
251 final List<String> seleniumOptions
252 ) {
253
254 if ("firefox".equals(seleniumDesiredCapabilities)) {
255
256 final FirefoxOptions options = new FirefoxOptions();
257
258 return options;
259
260 } else if ("chrome".equals(seleniumDesiredCapabilities)) {
261
262 final ChromeOptions options = new ChromeOptions();
263
264
265
266 options.addArguments(seleniumOptions);
267
268
269
270
271
272
273
274
275 return options;
276
277 } else {
278
279 throw new NotImplementedException("seleniumDesiredCapabilities: " + seleniumDesiredCapabilities
280 + " (should be one of: firefox, chrome)");
281 }
282 }
283
284 protected final void run() throws IOException, GuixerSyntaxException {
285
286 System.out.println("Incoming call: run()... Looking for yamlFileName...");
287
288 final String yamlFileName = extractScenarioFileNameFromCurrentThread();
289
290 System.out.println("Found yamlFileName: " + yamlFileName);
291
292 run(yamlFileName);
293 }
294
295 protected final void run(
296 final String yamlFileName
297 ) throws IOException, GuixerSyntaxException {
298
299 checkNotNull(yamlFileName, "yamlFileName");
300
301 System.out.println("Incoming call: run(\"" + yamlFileName + "\")... Looking for corresponding file...");
302
303 final File yamlFile;
304
305 final URL yamlUrl = Thread.currentThread().getContextClassLoader().getResource(yamlFileName);
306
307 if (yamlUrl != null) {
308
309 final String protocol = yamlUrl.getProtocol();
310
311 checkState("file".equals(protocol), "Protocol should be \"file:\", but was: " + yamlUrl);
312
313 final String path = yamlUrl.getPath();
314
315 final String decoded = URLDecoder.decode(path, UTF_8);
316
317 yamlFile = new File(decoded);
318
319 if (!yamlFile.isFile()) {
320
321 throw new FileNotFoundException(decoded);
322 }
323
324 } else {
325
326 yamlFile = findSomeYamlFileIn(yamlFileName, new File[] {
327 new File("target/classes"),
328 new File("target/test-classes"),
329 new File("src/main/yaml"),
330 new File("src/test/yaml"),
331 new File("src/main/resources"),
332 new File("src/test/resources"),
333 });
334 }
335
336 System.out.println("Using: " + yamlFile.getCanonicalPath());
337
338 run(yamlFile);
339 }
340
341 private static File findSomeYamlFileIn(
342 final String fileName,
343 final File[] dirs
344 ) throws IOException {
345
346 final List<String> yamlFileNames = newArrayList();
347
348 if (fileName.endsWith(".yaml")) {
349
350 yamlFileNames.add(fileName);
351
352 } else {
353
354 yamlFileNames.add(fileName + ".yaml");
355 yamlFileNames.add(fileName + ".yml");
356
357 if (fileName.endsWith("Test")) {
358
359 final String prefix = substringBeforeLast(fileName, "Test");
360
361 yamlFileNames.add(prefix + ".yaml");
362 yamlFileNames.add(prefix + ".yml");
363 }
364 }
365
366 for (final File dir : dirs) {
367
368 for (final String yamlFileName : yamlFileNames) {
369
370 final File yamlFile = new File(dir, yamlFileName);
371
372 if (yamlFile.isFile()) {
373
374 return yamlFile;
375 }
376 }
377 }
378
379 throw new FileNotFoundException("Could not find: " + fileName
380 + " and alternates, in: " + join(Arrays.asList(dirs).stream()
381 .map(dir -> dir.getName())
382 .collect(Collectors.toList()), ", "));
383 }
384
385 protected final void run(
386 final File yamlFile
387 ) throws IOException, GuixerSyntaxException {
388
389 System.out.println("Incoming call: run(File:" + yamlFile.getName() + ")...");
390
391 checkNotNull(yamlFile, "yamlFile");
392
393 if (!yamlFile.isFile()) {
394
395 throw new FileNotFoundException("yamlFile: " + yamlFile.getCanonicalPath());
396 }
397
398 final GuixerScenario scenario = GuixerScenarioLoader2.load(yamlFile);
399
400 run(scenario);
401 }
402
403 protected final void run(
404 final GuixerScenario scenario
405 ) throws IOException {
406
407 checkNotNull(scenario, "scenario");
408
409 final ExecutionContext context = new AbstractExecutionContext(new Functions() {
410
411 @Override
412 public String executeFunction(
413 final String functionName
414 ) {
415
416 return GuixerTest.this.executeFunction(functionName);
417 }
418
419 }) {
420
421 @Override
422 public WebDriver getDriver() {
423
424 return GuixerTest.this.getDriver();
425 }
426
427 @Override
428 public void switchToDriver(
429 final String driverName
430 ) {
431
432 GuixerTest.this.switchToDriver(driverName);
433 }
434 };
435
436 final Properties properties = getTestProperties();
437
438 for (final Entry<Object, Object> entry : properties.entrySet()) {
439
440 final String propertyName = entry.getKey().toString();
441 final String propertyValue = entry.getValue().toString();
442
443 context.setVariable(propertyName, propertyValue);
444 }
445
446 for (final Map.Entry<String, String> entry : scenario.getEnvironmentVariables().entrySet()) {
447
448 context.setVariable(entry.getKey(), entry.getValue());
449 }
450
451 new ScenarioRunner(context, new Callables() {
452
453 @Override
454 public void call(
455 final String callableName
456 ) throws IOException {
457
458 GuixerTest.this.call(callableName);
459 }
460
461 @Override
462 public void execute(
463 final GuixerConsumer consumer
464 ) throws IOException {
465
466 consumer.accept(GuixerTest.this.getRunnerContext());
467 }
468
469 }, scenario).run(runnerContext -> {
470
471 this.runnerContext = runnerContext;
472 });
473
474 runnerContext = null;
475 }
476
477 private static String extractScenarioFileNameFromCurrentThread() {
478
479 String yamlFileName = null;
480 String testClassSimpleName = null;
481
482 for (final StackTraceElement ste : Thread.currentThread().getStackTrace()) {
483
484 final String className = ste.getClassName();
485 final String methodName = ste.getMethodName();
486
487 final Class<?> clazz;
488
489 try {
490
491 clazz = Class.forName(className);
492
493 } catch (final ClassNotFoundException e) {
494
495 continue;
496 }
497
498 final Method method;
499
500 try {
501
502 method = clazz.getMethod(methodName);
503
504 } catch (final NoSuchMethodException e) {
505
506 continue;
507 }
508
509 if (method.isAnnotationPresent(Test.class)) {
510
511 testClassSimpleName = clazz.getSimpleName();
512
513 @Nullable
514 final Scenario scenarioMethodAnnotation = method.getAnnotation(Scenario.class);
515
516 if (scenarioMethodAnnotation != null) {
517
518 yamlFileName = scenarioMethodAnnotation.value();
519
520 } else {
521
522 @Nullable
523 final Scenario scenarioClassAnnotation = clazz.getAnnotation(Scenario.class);
524
525 if (scenarioClassAnnotation != null) {
526
527 yamlFileName = scenarioClassAnnotation.value();
528
529 } else {
530
531 @Nullable
532 final String superClassYamlFileName = extractScenarioFileNameFromSuperClasses(clazz);
533
534 if (superClassYamlFileName != null) {
535
536 yamlFileName = superClassYamlFileName;
537 }
538 }
539 }
540 }
541 }
542
543 if (yamlFileName != null) {
544
545 return yamlFileName;
546
547 } else if (testClassSimpleName != null) {
548
549 return testClassSimpleName;
550 }
551
552 throw new IllegalStateException("We should be within a call to a @Scenario-annotated method.");
553 }
554
555 @Nullable
556 private static String extractScenarioFileNameFromSuperClasses(
557 final Class<?> clazz
558 ) {
559
560 @Nullable
561 final Class<?> superClass = clazz.getSuperclass();
562
563 if (superClass != null) {
564
565 @Nullable
566 final Scenario scenarioAnnotation = clazz.getAnnotation(Scenario.class);
567
568 if (scenarioAnnotation != null) {
569
570 return scenarioAnnotation.value();
571 }
572
573 @Nullable
574 final String superClassYamlFileName = extractScenarioFileNameFromSuperClasses(superClass);
575
576 if (superClassYamlFileName != null) {
577
578 return superClassYamlFileName;
579 }
580 }
581
582 for (final Class<?> implementedInterface : clazz.getInterfaces()) {
583
584 @Nullable
585 final Scenario scenarioAnnotation = clazz.getAnnotation(Scenario.class);
586
587 if (scenarioAnnotation != null) {
588
589 return scenarioAnnotation.value();
590 }
591
592 @Nullable
593 final String interfaceYamlFileName = extractScenarioFileNameFromSuperClasses(implementedInterface);
594
595 if (interfaceYamlFileName != null) {
596
597 return interfaceYamlFileName;
598 }
599 }
600
601 return null;
602 }
603
604
605
606
607
608 private final Map<String, Object> callables = newHashMap();
609
610 protected final void registerCallable(
611 final String callableName,
612 final GuixerConsumer callable
613 ) {
614
615 checkNotNull(callableName, "callableName");
616 checkNotNull(callable, "callable");
617
618 callables.put(callableName, callable);
619 }
620
621 protected final void registerCallable(
622 final String callableName,
623 final GuixerRunnable callable
624 ) {
625
626 checkNotNull(callableName, "callableName");
627 checkNotNull(callable, "callable");
628
629 callables.put(callableName, callable);
630 }
631
632 private void call(
633 final String callableName
634 ) throws IOException {
635
636 @Nullable
637 final Object callable = callables.get(callableName);
638
639 if (callable == null) {
640 throw new IllegalStateException("Unknown callable: " + callableName
641 + "\nYou should call registerCallable() prior to using \"call\" in a scenario.");
642 }
643
644 if (callable instanceof GuixerConsumer) {
645
646 final GuixerConsumer consumer = (GuixerConsumer) callable;
647
648 consumer.accept(getRunnerContext());
649
650 } else if (callable instanceof GuixerRunnable) {
651
652 final GuixerRunnable runnable = (GuixerRunnable) callable;
653
654 runnable.run();
655
656 } else {
657
658 throw new IllegalStateException(
659 "callable should be either an instance of Consumer<RunnerContext>, or of Runnable, but was: "
660 + callable.getClass().getName());
661 }
662 }
663
664
665
666
667
668
669 private final Map<String, Object> functions = newHashMap();
670
671 protected final void registerFunction(
672 final String functionName,
673 final Function<RunnerContext, String> function
674 ) {
675
676 checkNotNull(functionName, "functionName");
677 checkNotNull(function, "function");
678
679 functions.put(functionName, function);
680 }
681
682 protected final void registerFunction(
683 final String functionName,
684 final Callable<String> function
685 ) {
686
687 checkNotNull(functionName, "functionName");
688 checkNotNull(function, "function");
689
690 functions.put(functionName, function);
691 }
692
693 private String executeFunction(
694 final String functionName
695 ) {
696
697 @Nullable
698 final Object function = functions.get(functionName);
699
700 if (function == null) {
701 throw new IllegalStateException("Unknown function: " + functionName
702 + "\nYou should call registerFunction() prior to using a function in a filtered value.");
703 }
704
705 if (function instanceof Function) {
706
707 @SuppressWarnings("unchecked")
708 final Function<RunnerContext, String> f = (Function<RunnerContext, String>) function;
709
710 return f.apply(getRunnerContext());
711
712 } else if (function instanceof Callable) {
713
714 @SuppressWarnings("unchecked")
715 final Callable<String> callable = (Callable<String>) function;
716
717 try {
718
719 return callable.call();
720
721 } catch (final Exception e) {
722
723 throw new RuntimeException("While calling function: " + functionName, e);
724 }
725
726 } else {
727
728 throw new IllegalStateException(
729 "function should be either an instance of Function<RunnerContext, String>, or of Callable<String>, but was: "
730 + function.getClass().getName());
731 }
732 }
733
734 private final Map<String, WebDriver> drivers = newHashMap();
735
736 final void injectDriver(
737 final WebDriver driver
738 ) {
739
740 injectDriver("", driver);
741 }
742
743 final void injectDriver(
744 final String name,
745 final WebDriver driver
746 ) {
747
748 checkNotNull(name, "name");
749 checkNotNull(driver, "driver");
750
751 drivers.put(name, driver);
752 }
753 }