View Javadoc
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  			// e.g. "http://192.168.0.19:4444/wd/hub";
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 					// Sometimes, "driver.quit()" just crashes.
110 
111 					e.printStackTrace(); // log the error, and do nothing
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 		// We use an intermediate variable, so the variable we check as non null,
144 		// is still non null at the end of the method.
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 		// We use an intermediate variable, so the variable we check as non null,
237 		// is still non null at the end of the method.
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 			// e.g. options.addArguments("--window-size=1920,1080");
265 			//
266 			options.addArguments(seleniumOptions);
267 
268 			// options.EnableMobileEmulation(deviceName);
269 			// options.AddArgument("no-sandbox");
270 			// ChromeDriver drv = new
271 			// ChromeDriver(ChromeDriverService.CreateDefaultService(), options,
272 			// TimeSpan.FromMinutes(3));
273 			// drv.Manage().Timeouts().PageLoad.Add(System.TimeSpan.FromSeconds(30));
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 	 * Values are either instances of <code>GuixerConsumer</code>, or of
606 	 * <code>GuixerRunnable</code>
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 	 * Values are either instances of
666 	 * <code>Function&lt;RunnerContext, String&gt;</code>, or of
667 	 * <code>Callable&lt;String&gt;</code>,
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 }