View Javadoc
1   package io.guixer.tools;
2   
3   import static com.google.common.base.Preconditions.checkNotNull;
4   import static io.guixer.tools.Locators.by;
5   import static java.nio.charset.StandardCharsets.UTF_8;
6   import static org.apache.commons.lang3.StringUtils.isBlank;
7   import static org.apache.commons.lang3.StringUtils.normalizeSpace;
8   import static org.apache.commons.lang3.StringUtils.substringBeforeLast;
9   import static org.junit.jupiter.api.Assertions.assertEquals;
10  
11  import java.io.File;
12  import java.io.FileNotFoundException;
13  import java.io.IOException;
14  import java.io.InputStream;
15  import java.io.PrintWriter;
16  import java.io.StringWriter;
17  import java.lang.reflect.Constructor;
18  import java.lang.reflect.InvocationTargetException;
19  import java.lang.reflect.Method;
20  import java.time.Duration;
21  import java.util.Map;
22  import java.util.Properties;
23  import java.util.function.Consumer;
24  
25  import javax.annotation.Nullable;
26  
27  import org.apache.commons.io.FileUtils;
28  import org.apache.commons.lang3.NotImplementedException;
29  import org.apache.commons.lang3.tuple.Pair;
30  import org.joda.time.DateTime;
31  import org.junit.jupiter.api.Test;
32  import org.openqa.selenium.By;
33  import org.openqa.selenium.JavascriptExecutor;
34  import org.openqa.selenium.Keys;
35  import org.openqa.selenium.NoSuchElementException;
36  import org.openqa.selenium.OutputType;
37  import org.openqa.selenium.TakesScreenshot;
38  import org.openqa.selenium.WebDriver;
39  import org.openqa.selenium.WebElement;
40  import org.openqa.selenium.support.ui.ExpectedConditions;
41  import org.openqa.selenium.support.ui.WebDriverWait;
42  
43  import io.guixer.ext.GuixerExtension;
44  import io.guixer.lang.AtomicStep;
45  import io.guixer.lang.GroupStep;
46  import io.guixer.lang.GuixerScenario;
47  import io.guixer.lang.SetLaneStep;
48  import io.guixer.lang.StatusStep;
49  import io.guixer.lang.Step;
50  import io.guixer.lang.command.AssertAbsentCommand;
51  import io.guixer.lang.command.AssertFalseCommand;
52  import io.guixer.lang.command.AssertPresentCommand;
53  import io.guixer.lang.command.AssertTrueCommand;
54  import io.guixer.lang.command.AttributeCommand;
55  import io.guixer.lang.command.CallCommand;
56  import io.guixer.lang.command.ClearCommand;
57  import io.guixer.lang.command.ClickCommand;
58  import io.guixer.lang.command.Command;
59  import io.guixer.lang.command.ExecuteScriptCommand;
60  import io.guixer.lang.command.ExtCommand;
61  import io.guixer.lang.command.FailureCommand;
62  import io.guixer.lang.command.GetCommand;
63  import io.guixer.lang.command.MessageCommand;
64  import io.guixer.lang.command.SendKeysCommand;
65  import io.guixer.lang.command.SetLaneCommand;
66  import io.guixer.lang.command.SetMaskedVariableCommand;
67  import io.guixer.lang.command.SetVariableCommand;
68  import io.guixer.lang.command.SleepCommand;
69  import io.guixer.lang.command.StatusCommand;
70  import io.guixer.lang.command.SuccessCommand;
71  import io.guixer.lang.command.SwitchToFrameCommand;
72  import io.guixer.lang.command.TagCommand;
73  import io.guixer.lang.command.WaitForCommand;
74  import io.guixer.lang.command.WaitForNotCommand;
75  import io.guixer.types.AttributeScope;
76  
77  public class ScenarioRunner {
78  
79  	private final ExecutionContext context;
80  	private final Callables callables;
81  	private final GuixerScenario scenario;
82  	private final File guixerOutDir;
83  	private final File logFile;
84  
85  	private int screenshotCount = 0;
86  	private int successCount = 0;
87  	private int failureCount = 0;
88  
89  	public ScenarioRunner(
90  		final ExecutionContext context,
91  		final Callables callables,
92  		final GuixerScenario scenario
93  	) throws IOException {
94  
95  		this.context = checkNotNull(context, "context");
96  		this.callables = checkNotNull(callables, "callables");
97  		this.scenario = checkNotNull(scenario, "scenario");
98  
99  		final DateTime now = new DateTime();
100 
101 		final long timeMillis = now.getMillis();
102 
103 		final Pair<Class<?>, String> testClassAndMethod = extractCurrentTestClassAndMethod();
104 
105 		final Class<?> testClass = testClassAndMethod.getLeft();
106 		final String testMethodName = testClassAndMethod.getRight();
107 
108 		guixerOutDir = new File("target/guixer_out/" //
109 				+ testClass.getSimpleName() + "/" //
110 				+ testMethodName + "/" //
111 				+ timeMillis);
112 
113 		FileUtils.forceMkdir(guixerOutDir);
114 
115 		logFile = new File(guixerOutDir, "test.log");
116 
117 		final String VERSION = getVersion();
118 
119 		FileUtils.write(logFile, "", UTF_8.name());
120 
121 		log("guixerVersion: " + VERSION);
122 		log("date: " + now);
123 		log("testClassName: " + testClass.getName());
124 		log("testClassSimpleName: " + testClass.getSimpleName());
125 		log("testMethodName: " + testMethodName);
126 		log("timeMillis: " + timeMillis);
127 		log("scenario: " + scenario.getName());
128 
129 		for (final Map.Entry<String, String> entry : scenario.getAttributes().entrySet()) {
130 
131 			final String attributeName = entry.getKey();
132 
133 			@Nullable
134 			final String attributeValue = entry.getValue();
135 
136 			final AttributeScope attributeScope = AttributeScope.RUN;
137 
138 			if (attributeValue == null) {
139 
140 				log("tag: " + attributeScope //
141 						+ " \"" + escapeQuotes(context.filterPlain(attributeName)) + "\"");
142 
143 			} else {
144 
145 				log("attribute: " + attributeScope //
146 						+ " \"" + escapeQuotes(context.filterPlain(attributeName)) + "\"" //
147 						+ " \"" + escapeQuotes(context.filterPlain(attributeValue)) + "\"");
148 			}
149 		}
150 	}
151 
152 	private static String getVersion() throws IOException {
153 
154 		final Properties properties = new Properties();
155 
156 		final String resourcePath = "application.properties";
157 
158 		final InputStream is = ScenarioRunner.class.getResourceAsStream(resourcePath);
159 
160 		if (is == null) {
161 			throw new FileNotFoundException(resourcePath);
162 		}
163 
164 		try {
165 
166 			properties.load(is);
167 
168 		} finally {
169 			is.close();
170 		}
171 
172 		final String propertyName = "project.version";
173 
174 		final String version = properties.getProperty(propertyName);
175 
176 		if (isBlank(version) || version.contains("$")) {
177 			throw new IllegalStateException(
178 					"Property " + propertyName + " in " + resourcePath + " has not been processed: " + version);
179 		}
180 
181 		return version;
182 	}
183 
184 	public void run(
185 		final Consumer<RunnerContext> callable
186 	) throws IOException {
187 
188 		checkNotNull(callable, "callable");
189 
190 		final RunnerContext runnerContext = new RunnerContextImpl();
191 
192 		callable.accept(runnerContext);
193 
194 		IOException ioException = null;
195 		RuntimeException runtimeException = null;
196 		Error error = null;
197 
198 		try {
199 
200 			for (final Step step : scenario.getSteps()) {
201 
202 				runStep(step);
203 			}
204 
205 		} catch (final IOException e) {
206 
207 			logWithMillis(e);
208 
209 			takeScreenshot();
210 
211 			ioException = e;
212 
213 		} catch (final RuntimeException e) {
214 
215 			logWithMillis(e);
216 
217 			takeScreenshot();
218 
219 			runtimeException = e;
220 
221 		} catch (final Error e) {
222 
223 			logWithMillis(e);
224 
225 			takeScreenshot();
226 
227 			error = e;
228 		}
229 
230 		log();
231 
232 		// final int testSuccessCount = testRunner.getSuccessCount();
233 		// final int testRailureCount = testRunner.getFailureCount();
234 
235 		log("successCount: " + successCount);
236 		log("failureCount: " + failureCount);
237 
238 		log();
239 
240 		logWithMillis("Done.");
241 
242 		if (ioException != null) {
243 
244 			throw ioException;
245 
246 		} else if (runtimeException != null) {
247 
248 			throw runtimeException;
249 
250 		} else if (error != null) {
251 
252 			throw error;
253 		}
254 
255 		assertEquals(0, failureCount, "failureCount");
256 	}
257 
258 	@Nullable
259 	private StepResult runStep(
260 		final Step step
261 	) throws IOException {
262 
263 		checkNotNull(step, "step");
264 
265 		if (step instanceof GroupStep) {
266 
267 			return runGroupStep((GroupStep) step);
268 
269 		} else if (step instanceof AtomicStep) {
270 
271 			return runAtomicStep((AtomicStep) step);
272 
273 		} else if (step instanceof StatusStep) {
274 
275 			return runStatusStep((StatusStep) step);
276 
277 		} else if (step instanceof SetLaneStep) {
278 
279 			return runSetLaneStep((SetLaneStep) step);
280 
281 		} else if (step instanceof InlineStep) {
282 
283 			return runInlineStep((InlineStep) step);
284 
285 		} else {
286 
287 			throw new NotImplementedException("step.class: " + step.getClass().getName());
288 		}
289 	}
290 
291 	@Nullable
292 	private StepResult runAtomicStep(
293 		final AtomicStep step
294 	) throws IOException {
295 
296 		log();
297 
298 		logWithMillis("intent: " + normalizeSpace(step.getIntent()));
299 
300 		if (step.getSeq() != null) {
301 
302 			for (final Command command : step.getSeq()) {
303 
304 				runCommand(command);
305 			}
306 
307 		} else {
308 
309 			// Keep in order
310 
311 			runSetVariableCommand(step.getSetVariableCommand());
312 			runSetMaskedVariableCommand(step.getSetMaskedVariableCommand());
313 			runCallCommand(step.getCallCommand());
314 			runGetCommand(step.getGetCommand());
315 			runClearCommand(step.getClearCommand());
316 			runSendKeysCommand(step.getSendKeysCommand());
317 			runExecuteScriptCommand(step.getExecuteScriptCommand());
318 			runClickCommand(step.getClickCommand());
319 			runSwitchToFrameCommand(step.getSwitchToFrameCommand());
320 			runSleepCommand(step.getSleepCommand());
321 			runWaitForCommand(step.getWaitForCommand());
322 			runWaitForNotCommand(step.getWaitForNotCommand());
323 			runAssertPresentCommand(step.getAssertPresentCommand());
324 			runAssertAbsentCommand(step.getAssertAbsentCommand());
325 			runAssertTrueCommand(step.getAssertTrueCommand());
326 			runAssertFalseCommand(step.getAssertFalseCommand());
327 			runFailureCommand(step.getFailureCommand());
328 			runSuccessCommand(step.getSuccessCommand());
329 			runMessageCommand(step.getMessageCommand());
330 			runTagCommand(step.getTagCommand());
331 			runAttributeCommand(step.getAttributeCommand());
332 			runUnprocessedExtCommand(step.getExtCommand());
333 		}
334 
335 		if (step.hasTakeScreenshot()) {
336 
337 			takeScreenshot();
338 		}
339 
340 		return null;
341 	}
342 
343 	@Nullable
344 	private StepResult runInlineStep(
345 		final InlineStep step
346 	) throws IOException {
347 
348 		log();
349 
350 		logWithMillis("call: " + step.name);
351 
352 		// step.consumer.accept(getRunnerContext());
353 
354 		callables.execute(step.consumer);
355 
356 		return null;
357 	}
358 
359 	@Nullable
360 	private CommandResult runCommand(
361 		@Nullable final Command command
362 	) throws IOException {
363 
364 		if (command == null) {
365 
366 			return null;
367 
368 		} else if (command instanceof GetCommand) {
369 
370 			return runGetCommand((GetCommand) command);
371 
372 		} else if (command instanceof ClearCommand) {
373 
374 			return runClearCommand((ClearCommand) command);
375 
376 		} else if (command instanceof SendKeysCommand) {
377 
378 			return runSendKeysCommand((SendKeysCommand) command);
379 
380 		} else if (command instanceof ClickCommand) {
381 
382 			return runClickCommand((ClickCommand) command);
383 
384 		} else if (command instanceof CallCommand) {
385 
386 			return runCallCommand((CallCommand) command);
387 
388 		} else if (command instanceof ExecuteScriptCommand) {
389 
390 			return runExecuteScriptCommand((ExecuteScriptCommand) command);
391 
392 		} else if (command instanceof SwitchToFrameCommand) {
393 
394 			return runSwitchToFrameCommand((SwitchToFrameCommand) command);
395 
396 		} else if (command instanceof SleepCommand) {
397 
398 			return runSleepCommand((SleepCommand) command);
399 
400 		} else if (command instanceof WaitForCommand) {
401 
402 			return runWaitForCommand((WaitForCommand) command);
403 
404 		} else if (command instanceof WaitForNotCommand) {
405 
406 			return runWaitForNotCommand((WaitForNotCommand) command);
407 
408 		} else if (command instanceof FailureCommand) {
409 
410 			return runFailureCommand((FailureCommand) command);
411 
412 		} else if (command instanceof SuccessCommand) {
413 
414 			return runSuccessCommand((SuccessCommand) command);
415 
416 		} else if (command instanceof MessageCommand) {
417 
418 			return runMessageCommand((MessageCommand) command);
419 
420 		} else if (command instanceof AssertPresentCommand) {
421 
422 			return runAssertPresentCommand((AssertPresentCommand) command);
423 
424 		} else if (command instanceof AssertAbsentCommand) {
425 
426 			return runAssertAbsentCommand((AssertAbsentCommand) command);
427 
428 		} else if (command instanceof AssertTrueCommand) {
429 
430 			return runAssertTrueCommand((AssertTrueCommand) command);
431 
432 		} else if (command instanceof AssertFalseCommand) {
433 
434 			return runAssertFalseCommand((AssertFalseCommand) command);
435 
436 		} else if (command instanceof SetVariableCommand) {
437 
438 			return runSetVariableCommand((SetVariableCommand) command);
439 
440 		} else if (command instanceof SetMaskedVariableCommand) {
441 
442 			return runSetMaskedVariableCommand((SetMaskedVariableCommand) command);
443 
444 		} else if (command instanceof TagCommand) {
445 
446 			return runTagCommand((TagCommand) command);
447 
448 		} else if (command instanceof AttributeCommand) {
449 
450 			return runAttributeCommand((AttributeCommand) command);
451 
452 		} else if (command instanceof ExtCommand) {
453 
454 			return runUnprocessedExtCommand((ExtCommand) command);
455 
456 		} else {
457 
458 			throw new NotImplementedException("command.class: " + command.getClass().getName());
459 		}
460 	}
461 
462 	@Nullable
463 	private CommandResult runGetCommand(
464 		@Nullable final GetCommand command
465 	) throws IOException {
466 
467 		if (command == null) {
468 			return null;
469 		}
470 
471 		final String url = context.filterPlain(command.url);
472 
473 		logWithMillis("get: " + url);
474 
475 		context.getDriver().get(url);
476 
477 		return null;
478 	}
479 
480 	private CommandResult runSendKeysCommand(
481 		@Nullable final SendKeysCommand command
482 	) throws IOException {
483 
484 		if (command == null) {
485 			return null;
486 		}
487 
488 		final By by = by(command.locator);
489 
490 		final String filtered;
491 
492 		if (context.usesMaskedVariables(command.value)) {
493 
494 			filtered = context.filterMasked(command.value);
495 
496 			logWithMillis("sendKeys: " + by + " ***");
497 
498 		} else {
499 
500 			filtered = context.filterPlain(command.value);
501 
502 			logWithMillis("sendKeys: " + by + " '" + filtered + "'");
503 		}
504 
505 		if ("\"\\n\"".equals(filtered) || "\\n".equals(filtered)) {
506 
507 			findElement(by).sendKeys(Keys.ENTER);
508 
509 		} else {
510 
511 			findElement(by).sendKeys(filtered);
512 		}
513 
514 		return null;
515 	}
516 
517 	private CommandResult runWaitForCommand(
518 		@Nullable final WaitForCommand command
519 	) throws IOException {
520 
521 		if (command == null) {
522 			return null;
523 		}
524 
525 		final By by = by(command.locator);
526 
527 		logWithMillis("waitFor: " + by);
528 
529 		final WebDriverWait wait = new WebDriverWait(context.getDriver(), Duration.ofSeconds(20));
530 
531 		wait.until(ExpectedConditions.presenceOfElementLocated(by));
532 
533 		return null;
534 	}
535 
536 	private CommandResult runWaitForNotCommand(
537 		@Nullable final WaitForNotCommand command
538 	) throws IOException {
539 
540 		if (command == null) {
541 			return null;
542 		}
543 
544 		final By by = by(command.locator);
545 
546 		logWithMillis("waitForNot: " + by);
547 
548 		final WebDriverWait wait = new WebDriverWait(context.getDriver(), Duration.ofSeconds(20));
549 
550 		wait.until(ExpectedConditions.not(ExpectedConditions.presenceOfAllElementsLocatedBy(by)));
551 
552 		return null;
553 	}
554 
555 	@Nullable
556 	private CommandResult runClickCommand(
557 		@Nullable final ClickCommand command
558 	) throws IOException {
559 
560 		if (command == null) {
561 			return null;
562 		}
563 
564 		final By by = by(command.locator);
565 
566 		logWithMillis("click: " + by);
567 
568 		findElement(by).click();
569 
570 		return null;
571 	}
572 
573 	@Nullable
574 	private CommandResult runClearCommand(
575 		@Nullable final ClearCommand command
576 	) throws IOException {
577 
578 		if (command == null) {
579 			return null;
580 		}
581 
582 		final By by = by(command.locator);
583 
584 		logWithMillis("clear: " + by);
585 
586 		findElement(by).clear();
587 
588 		return null;
589 	}
590 
591 	@Nullable
592 	private CommandResult runSleepCommand(
593 		@Nullable final SleepCommand command
594 	) throws IOException {
595 
596 		if (command == null) {
597 			return null;
598 		}
599 
600 		final int seconds = command.seconds;
601 
602 		logWithMillis("sleep: " + seconds);
603 
604 		try {
605 
606 			Thread.sleep(seconds * 1_000);
607 
608 		} catch (final InterruptedException e) {
609 
610 			e.printStackTrace();
611 		}
612 
613 		return null;
614 	}
615 
616 	@Nullable
617 	private CommandResult runTagCommand(
618 		@Nullable final TagCommand command
619 	) throws IOException {
620 
621 		if (command == null) {
622 			return null;
623 		}
624 
625 		logWithMillis("tag: " + command.scope //
626 				+ " \"" + escapeQuotes(context.filterPlain(command.name)) + "\"");
627 
628 		return null;
629 	}
630 
631 	@Nullable
632 	private CommandResult runAttributeCommand(
633 		@Nullable final AttributeCommand command
634 	) throws IOException {
635 
636 		if (command == null) {
637 			return null;
638 		}
639 
640 		logWithMillis("attribute: " + command.scope //
641 				+ " \"" + escapeQuotes(context.filterPlain(command.name)) + "\"" //
642 				+ " \"" + escapeQuotes(context.filterPlain(command.value)) + "\"");
643 
644 		return null;
645 	}
646 
647 	private static String escapeQuotes(
648 		final String s
649 	) {
650 
651 		return s.replace("\\", "\\\\").replace("\"", "\\\"");
652 	}
653 
654 	@Nullable
655 	private CommandResult runProcessedExtCommand(
656 		final ExtCommand command
657 	) throws IOException {
658 
659 		checkNotNull(command, "command");
660 
661 		logWithMillis("ext: \"" + escapeQuotes(context.filterPlain(command.namespace)) + "\"" //
662 				+ " \"" + escapeQuotes(context.filterPlain(command.text)) + "\"");
663 
664 		return null;
665 	}
666 
667 	@Nullable
668 	private CommandResult runUnprocessedExtCommand(
669 		@Nullable final ExtCommand command
670 	) throws IOException {
671 
672 		if (command == null) {
673 			return null;
674 		}
675 
676 		final String className = command.namespace;
677 
678 		final Class<?> clazz;
679 
680 		try {
681 
682 			clazz = Class.forName(className);
683 
684 		} catch (final ClassNotFoundException e) {
685 
686 			throw new RuntimeException("Cannot load class: " + className, e);
687 		}
688 
689 		if (!GuixerExtension.class.isAssignableFrom(clazz)) {
690 
691 			throw new RuntimeException("Extension class should implement GuixerExtension, but was: " + className);
692 		}
693 
694 		final Constructor<?> constructor;
695 
696 		try {
697 
698 			constructor = clazz.getDeclaredConstructor();
699 
700 		} catch (final NoSuchMethodException e) {
701 
702 			throw new RuntimeException("Extension class should declare a default constructor, but was: " + className);
703 		}
704 
705 		final GuixerExtension instance;
706 
707 		try {
708 
709 			instance = (GuixerExtension) constructor.newInstance();
710 
711 		} catch (final InvocationTargetException | InstantiationException | IllegalAccessException e) {
712 
713 			throw new RuntimeException("Cannot instantiate extension class: " + className, e);
714 		}
715 
716 		final RunnerContext runnerContext = new RunnerContextImpl();
717 
718 		instance.executeCommand(runnerContext, command.text);
719 
720 		return null;
721 	}
722 
723 	@Nullable
724 	private CommandResult runCallCommand(
725 		@Nullable final CallCommand command
726 	) throws IOException {
727 
728 		if (command == null) {
729 			return null;
730 		}
731 
732 		final String callable = command.callable;
733 
734 		logWithMillis("call: " + callable);
735 
736 		callables.call(callable);
737 
738 		return null;
739 	}
740 
741 	@Nullable
742 	private ExecuteScriptCommandResult runExecuteScriptCommand(
743 		@Nullable final ExecuteScriptCommand command
744 	) throws IOException {
745 
746 		if (command == null) {
747 			return null;
748 		}
749 
750 		final String script = command.script;
751 
752 		logWithMillis("executeScript: " + script);
753 
754 		final JavascriptExecutor jsExecutor = (JavascriptExecutor) context.getDriver();
755 
756 		@Nullable
757 		final Object result = jsExecutor.executeScript(script);
758 
759 		return new ExecuteScriptCommandResult() {
760 
761 			@Nullable
762 			@Override
763 			public Object get() {
764 
765 				return result;
766 			}
767 
768 			@Override
769 			public int asInt() {
770 
771 				if (result == null) {
772 
773 					throw new NullPointerException("The script returned no value.");
774 
775 				} else if (result instanceof Double) {
776 
777 					return ((Double) result).intValue();
778 
779 				} else if (result instanceof Long) {
780 
781 					return ((Long) result).intValue();
782 
783 				} else {
784 
785 					return (int) result;
786 				}
787 			}
788 
789 			@Override
790 			public String asString() {
791 
792 				if (result == null) {
793 
794 					throw new NullPointerException("The script returned no value.");
795 
796 				} else if (result instanceof String) {
797 
798 					return (String) result;
799 
800 				} else {
801 
802 					return result.toString();
803 				}
804 			}
805 		};
806 	}
807 
808 	private WebElement findElement(
809 		final By by
810 	) throws IOException {
811 
812 		return context.getDriver().findElement(by);
813 	}
814 
815 	@Nullable
816 	private CommandResult runSwitchToFrameCommand(
817 		@Nullable final SwitchToFrameCommand command
818 	) throws IOException {
819 
820 		if (command == null) {
821 			return null;
822 		}
823 
824 		context.getDriver().switchTo().frame(command.frameName);
825 
826 		return null;
827 	}
828 
829 	@Nullable
830 	private CommandResult runFailureCommand(
831 		@Nullable final FailureCommand command
832 	) throws IOException {
833 
834 		if (command == null) {
835 			return null;
836 		}
837 
838 		final long timeMillis = System.currentTimeMillis();
839 
840 		logWithMillis(timeMillis, "failure: " + command.message + " -> " + successOrFailure(false));
841 
842 		return null;
843 	}
844 
845 	@Nullable
846 	private CommandResult runSuccessCommand(
847 		@Nullable final SuccessCommand command
848 	) throws IOException {
849 
850 		if (command == null) {
851 			return null;
852 		}
853 
854 		final long timeMillis = System.currentTimeMillis();
855 
856 		logWithMillis(timeMillis, "success: " + command.message + " -> " + successOrFailure(true));
857 
858 		return null;
859 	}
860 
861 	@Nullable
862 	private CommandResult runMessageCommand(
863 		@Nullable final MessageCommand command
864 	) throws IOException {
865 
866 		if (command == null) {
867 			return null;
868 		}
869 
870 		final long timeMillis = System.currentTimeMillis();
871 
872 		logWithMillis(timeMillis, "message: " + command.text);
873 
874 		return null;
875 	}
876 
877 	@Nullable
878 	private CommandResult runAssertPresentCommand(
879 		@Nullable final AssertPresentCommand command
880 	) throws IOException {
881 
882 		if (command == null) {
883 			return null;
884 		}
885 
886 		final By by = by(command.locator);
887 
888 		final long timeMillis = System.currentTimeMillis();
889 
890 		boolean isPresent = true;
891 
892 		try {
893 
894 			findElement(by);
895 
896 		} catch (final NoSuchElementException e) {
897 
898 			isPresent = false;
899 		}
900 
901 		logWithMillis(timeMillis, "assertPresent: " + by + " -> " + successOrFailure(isPresent));
902 
903 		return null;
904 	}
905 
906 	@Nullable
907 	private CommandResult runAssertAbsentCommand(
908 		@Nullable final AssertAbsentCommand command
909 	) throws IOException {
910 
911 		if (command == null) {
912 			return null;
913 		}
914 
915 		final By by = by(command.locator);
916 
917 		final long timeMillis = System.currentTimeMillis();
918 
919 		boolean isPresent = true;
920 
921 		try {
922 
923 			findElement(by);
924 
925 		} catch (final NoSuchElementException e) {
926 
927 			isPresent = false;
928 		}
929 
930 		logWithMillis(timeMillis, "assertAbsent: " + by + " -> " + successOrFailure(!isPresent));
931 
932 		return null;
933 	}
934 
935 	@Nullable
936 	private CommandResult runAssertTrueCommand(
937 		@Nullable final AssertTrueCommand command
938 	) throws IOException {
939 
940 		if (command == null) {
941 			return null;
942 		}
943 
944 		final String expression = command.expression;
945 
946 		if (!expression.endsWith("/@checked")) {
947 
948 			throw new NotImplementedException("expression: " + expression);
949 		}
950 
951 		final String locator = substringBeforeLast(expression, "/@checked");
952 
953 		final By by = by(locator);
954 
955 		final boolean selected = findElement(by).isSelected();
956 
957 		logWithMillis("assertTrue: " + by + " @checked -> " + successOrFailure(selected));
958 
959 		return null;
960 	}
961 
962 	@Nullable
963 	private CommandResult runAssertFalseCommand(
964 		@Nullable final AssertFalseCommand command
965 	) throws IOException {
966 
967 		if (command == null) {
968 			return null;
969 		}
970 
971 		final String expression = command.expression;
972 
973 		if (!expression.endsWith("/@checked")) {
974 
975 			throw new NotImplementedException("expression: " + expression);
976 		}
977 
978 		final String locator = substringBeforeLast(expression, "/@checked");
979 
980 		final By by = by(locator);
981 
982 		final boolean selected = findElement(by).isSelected();
983 
984 		logWithMillis("assertFalse: " + by + " @checked -> " + successOrFailure(!selected));
985 
986 		return null;
987 	}
988 
989 	private String successOrFailure(
990 		final boolean success
991 	) {
992 
993 		if (success) {
994 
995 			++successCount;
996 
997 		} else {
998 
999 			++failureCount;
1000 		}
1001 
1002 		return success ? "SUCCESS" : "FAILURE";
1003 	}
1004 
1005 	private void takeScreenshot() throws IOException {
1006 
1007 		final long timeMillis = System.currentTimeMillis();
1008 
1009 		final String screenshotFileName = String.format("%08d", screenshotCount) + ".png";
1010 
1011 		++screenshotCount;
1012 
1013 		final File file0 = ((TakesScreenshot) context.getDriver()).getScreenshotAs(OutputType.FILE);
1014 
1015 		final File file1 = new File(logFile.getParentFile(), screenshotFileName);
1016 
1017 		FileUtils.copyFile(file0, file1);
1018 
1019 		logWithMillis(timeMillis, "takeScreenshot: " + screenshotFileName);
1020 	}
1021 
1022 	@Nullable
1023 	private StepResult runGroupStep(
1024 		final GroupStep group
1025 	) throws IOException {
1026 
1027 		log();
1028 
1029 		logWithMillis("beginGroup: " + group.getName());
1030 
1031 		for (final Step step : group.getSteps()) {
1032 
1033 			runStep(step);
1034 		}
1035 
1036 		log();
1037 
1038 		logWithMillis("endGroup: " + group.getName());
1039 
1040 		return null;
1041 	}
1042 
1043 	@Nullable
1044 	private StepResult runStatusStep(
1045 		final StatusStep status
1046 	) throws IOException {
1047 
1048 		runStatusCommand(new StatusCommand(status.getLabel()));
1049 
1050 		return null; // TODO
1051 	}
1052 
1053 	@Nullable
1054 	private CommandResult runStatusCommand(
1055 		final StatusCommand status
1056 	) throws IOException {
1057 
1058 		log();
1059 
1060 		logWithMillis("status: " + status.label);
1061 
1062 		return null;
1063 	}
1064 
1065 	@Nullable
1066 	private StepResult runSetLaneStep(
1067 		final SetLaneStep setLane
1068 	) throws IOException {
1069 
1070 		runSetLaneCommand(new SetLaneCommand(setLane.getLaneId()));
1071 
1072 		return null; // TODO
1073 	}
1074 
1075 	private CommandResult runSetLaneCommand(
1076 		final SetLaneCommand command
1077 	) throws IOException {
1078 
1079 		checkNotNull(command, "command");
1080 
1081 		final String laneId = command.laneId;
1082 
1083 		log();
1084 
1085 		logWithMillis("setLane: " + laneId);
1086 
1087 		context.switchToDriver(laneId);
1088 
1089 		return null; // TODO
1090 	}
1091 
1092 	private CommandResult runSetVariableCommand(
1093 		@Nullable final SetVariableCommand command
1094 	) throws IOException {
1095 
1096 		if (command == null) {
1097 			return null;
1098 		}
1099 
1100 		final String name = command.name;
1101 		final String value = context.filterPlain(command.value);
1102 
1103 		logWithMillis("setVariable: " + name + " " + value);
1104 
1105 		context.setVariable(name, value);
1106 
1107 		return null; // TODO
1108 	}
1109 
1110 	private CommandResult runSetMaskedVariableCommand(
1111 		@Nullable final SetMaskedVariableCommand command
1112 	) throws IOException {
1113 
1114 		if (command == null) {
1115 			return null;
1116 		}
1117 
1118 		final String name = command.name;
1119 		final String value = context.filterMasked(command.value);
1120 
1121 		logWithMillis("setMaskedVariable: " + name + " ***");
1122 
1123 		context.setMaskedVariable(name, value);
1124 
1125 		return null; // TODO
1126 	}
1127 
1128 	private static Pair<Class<?>, String> extractCurrentTestClassAndMethod() {
1129 
1130 		Pair<Class<?>, String> testClassAndMethod = null;
1131 
1132 		for (final StackTraceElement ste : Thread.currentThread().getStackTrace()) {
1133 
1134 			final String className = ste.getClassName();
1135 			final String methodName = ste.getMethodName();
1136 
1137 			final Class<?> clazz;
1138 
1139 			try {
1140 
1141 				clazz = Class.forName(className);
1142 
1143 			} catch (final ClassNotFoundException e) {
1144 
1145 				continue;
1146 			}
1147 
1148 			final Method method;
1149 
1150 			// TODO this only fetches zero-argument test methods
1151 
1152 			try {
1153 
1154 				method = clazz.getMethod(methodName);
1155 
1156 			} catch (final NoSuchMethodException e) {
1157 
1158 				continue;
1159 			}
1160 
1161 			if (method.isAnnotationPresent(Test.class)) {
1162 
1163 				testClassAndMethod = Pair.of(clazz, methodName);
1164 			}
1165 		}
1166 
1167 		if (testClassAndMethod == null) {
1168 
1169 			throw new IllegalStateException(
1170 					"The run() method sould be called from a @Test-annotated method with no arguments.");
1171 		}
1172 
1173 		return testClassAndMethod;
1174 	}
1175 
1176 	private void log() throws IOException {
1177 
1178 		System.out.println();
1179 
1180 		FileUtils.write(logFile, "\n", UTF_8.name(), true);
1181 	}
1182 
1183 	private void log(
1184 		final String message
1185 	) throws IOException {
1186 
1187 		System.out.println(message);
1188 
1189 		FileUtils.write(logFile, message + "\n", UTF_8.name(), true);
1190 	}
1191 
1192 	private void logWithMillis(
1193 		final String message
1194 	) throws IOException {
1195 
1196 		logWithMillis(System.currentTimeMillis(), message);
1197 	}
1198 
1199 	private void logWithMillis(
1200 		final long timeMillis,
1201 		final String message
1202 	) throws IOException {
1203 
1204 		System.out.println(message);
1205 
1206 		FileUtils.write(logFile, timeMillis + " " + message + "\n", UTF_8.name(), true);
1207 	}
1208 
1209 	private void logWithMillis(
1210 		final Throwable throwable
1211 	) throws IOException {
1212 
1213 		final long timeMillis = System.currentTimeMillis();
1214 
1215 		System.out.print("error: ");
1216 
1217 		throwable.printStackTrace(System.out);
1218 
1219 		final StringWriter sw = new StringWriter();
1220 
1221 		try (PrintWriter pw = new PrintWriter(sw)) {
1222 
1223 			throwable.printStackTrace(pw);
1224 
1225 			pw.flush();
1226 		}
1227 
1228 		FileUtils.write(logFile, timeMillis + " error: " + sw.toString() + "\n", UTF_8.name(), true);
1229 	}
1230 
1231 	private class RunnerContextImpl implements RunnerContext {
1232 
1233 		@Override
1234 		public String[] getVariableNames() {
1235 
1236 			return context.getVariableNames();
1237 		}
1238 
1239 		@Override
1240 		public <T> T setVariable(
1241 			final String name,
1242 			final T value
1243 		) {
1244 
1245 			return context.setVariable(name, value);
1246 		}
1247 
1248 		@Override
1249 		public void setMaskedVariable(
1250 			final String name,
1251 			final Object value
1252 		) {
1253 
1254 			context.setMaskedVariable(name, value);
1255 		}
1256 
1257 		@Override
1258 		public Object getVariable(
1259 			final String name
1260 		) {
1261 
1262 			return context.getVariable(name);
1263 		}
1264 
1265 		@Override
1266 		public void removeVariable(
1267 			final String name
1268 		) {
1269 
1270 			context.removeVariable(name);
1271 		}
1272 
1273 		@Override
1274 		public boolean usesMaskedVariables(
1275 			final String value
1276 		) {
1277 
1278 			return context.usesMaskedVariables(value);
1279 		}
1280 
1281 		@Override
1282 		public String filterPlain(
1283 			final String value
1284 		) {
1285 
1286 			return context.filterPlain(value);
1287 		}
1288 
1289 		@Override
1290 		public String filterMasked(
1291 			final String value
1292 		) {
1293 
1294 			return context.filterMasked(value);
1295 		}
1296 
1297 		@Override
1298 		public void switchToDriver(
1299 			final String driverName
1300 		) {
1301 
1302 			context.switchToDriver(driverName);
1303 		}
1304 
1305 		@Override
1306 		public CommandResult intent(
1307 			final String intent
1308 		) throws IOException {
1309 
1310 			checkNotNull(intent, "intent");
1311 
1312 			log();
1313 
1314 			logWithMillis("intent: " + normalizeSpace(intent));
1315 
1316 			return null;
1317 		}
1318 
1319 		@Override
1320 		public CommandResult get(
1321 			final String url
1322 		) throws IOException {
1323 
1324 			return runGetCommand(new GetCommand(url));
1325 		}
1326 
1327 		@Override
1328 		public CommandResult clear(
1329 			final String locator
1330 		) throws IOException {
1331 
1332 			return runClearCommand(new ClearCommand(locator));
1333 		}
1334 
1335 		@Override
1336 		public CommandResult sendKeys(
1337 			final String locator,
1338 			final String value
1339 		) throws IOException {
1340 
1341 			return runSendKeysCommand(new SendKeysCommand(locator, value));
1342 		}
1343 
1344 		@Override
1345 		public CommandResult click(
1346 			final String locator
1347 		) throws IOException {
1348 
1349 			return runClickCommand(new ClickCommand(locator));
1350 		}
1351 
1352 		@Override
1353 		public CommandResult waitFor(
1354 			final String locator
1355 		) throws IOException {
1356 
1357 			return runWaitForCommand(new WaitForCommand(locator));
1358 		}
1359 
1360 		@Override
1361 		public CommandResult waitForNot(
1362 			final String locator
1363 		) throws IOException {
1364 
1365 			return runWaitForNotCommand(new WaitForNotCommand(locator));
1366 		}
1367 
1368 		@Override
1369 		public CommandResult failure(
1370 			final String message
1371 		) throws IOException {
1372 
1373 			return runFailureCommand(new FailureCommand(message));
1374 		}
1375 
1376 		@Override
1377 		public CommandResult success(
1378 			final String message
1379 		) throws IOException {
1380 
1381 			return runSuccessCommand(new SuccessCommand(message));
1382 		}
1383 
1384 		@Override
1385 		public CommandResult message(
1386 			final String message
1387 		) throws IOException {
1388 
1389 			return runMessageCommand(new MessageCommand(message));
1390 		}
1391 
1392 		@Override
1393 		public CommandResult assertPresent(
1394 			final String locator
1395 		) throws IOException {
1396 
1397 			return runAssertPresentCommand(new AssertPresentCommand(locator));
1398 		}
1399 
1400 		@Override
1401 		public CommandResult assertAbsent(
1402 			final String locator
1403 		) throws IOException {
1404 
1405 			return runAssertAbsentCommand(new AssertAbsentCommand(locator));
1406 		}
1407 
1408 		@Override
1409 		public CommandResult assertTrue(
1410 			final String expression
1411 		) throws IOException {
1412 
1413 			return runAssertTrueCommand(new AssertTrueCommand(expression));
1414 		}
1415 
1416 		@Override
1417 		public CommandResult assertFalse(
1418 			final String expression
1419 		) throws IOException {
1420 
1421 			return runAssertFalseCommand(new AssertFalseCommand(expression));
1422 		}
1423 
1424 		@Override
1425 		public ExecuteScriptCommandResult executeScript(
1426 			final String script
1427 		) throws IOException {
1428 
1429 			return runExecuteScriptCommand(new ExecuteScriptCommand(script));
1430 		}
1431 
1432 		@Override
1433 		public CommandResult setLane(
1434 			final String laneId
1435 		) throws IOException {
1436 
1437 			return runSetLaneCommand(new SetLaneCommand(laneId));
1438 		}
1439 
1440 		@Override
1441 		public CommandResult sleep(
1442 			final int seconds
1443 		) throws IOException {
1444 
1445 			return runSleepCommand(new SleepCommand(seconds));
1446 		}
1447 
1448 		@Override
1449 		public StepResult status(
1450 			final String label
1451 		) throws IOException {
1452 
1453 			return runStatusStep(new StatusStep() {
1454 
1455 				@Override
1456 				public String getLabel() {
1457 
1458 					return label;
1459 				}
1460 			});
1461 		}
1462 
1463 		@Override
1464 		public void takeScreenshot() throws IOException {
1465 
1466 			ScenarioRunner.this.takeScreenshot();
1467 		}
1468 
1469 		@Override
1470 		public WebDriver getDriver() {
1471 
1472 			return context.getDriver();
1473 		}
1474 
1475 		@Override
1476 		public CommandResult tag(
1477 			final AttributeScope scope,
1478 			final String name
1479 		) throws IOException {
1480 
1481 			return runTagCommand(new TagCommand(scope, name));
1482 		}
1483 
1484 		@Override
1485 		public CommandResult attribute(
1486 			final AttributeScope scope,
1487 			final String name,
1488 			final String value
1489 		) throws IOException {
1490 
1491 			return runAttributeCommand(new AttributeCommand(scope, name, value));
1492 		}
1493 
1494 		@Override
1495 		public CommandResult ext(
1496 			final String namespace,
1497 			final String text
1498 		) throws IOException {
1499 
1500 			return runProcessedExtCommand(new ExtCommand(namespace, text));
1501 		}
1502 	}
1503 }