1 package io.guixer.lang;
2
3 import static com.google.common.base.Preconditions.checkNotNull;
4 import static com.google.common.collect.Sets.newHashSet;
5 import static io.guixer.lang.GuixerSyntaxErrorType.DUPLICATE_COMMANDS_IN_ATOMIC_STEP;
6 import static io.guixer.lang.GuixerSyntaxErrorType.GROUP_NAME_SHOULD_NOT_BE_EMPTY;
7 import static io.guixer.lang.GuixerSyntaxErrorType.GROUP_NAME_SHOULD_START_BY_AN_UPPERCASE_CHARACTER;
8 import static io.guixer.lang.GuixerSyntaxErrorType.GROUP_STEPS_SHOULD_BE_A_LIST;
9 import static io.guixer.lang.GuixerSyntaxErrorType.ILLEGAL_COMMAND_NAME;
10 import static io.guixer.lang.GuixerSyntaxErrorType.INTENT_SHOULD_BE_AT_STEP_TOP_LEVEL;
11 import static io.guixer.lang.GuixerSyntaxErrorType.SCENARIO_NAME_SHOULD_BE_PRESENT;
12 import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_AND_COMMAND_SHOULD_NOT_BE_MIXED;
13 import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_SHOULD_BE_A_LIST;
14 import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_STEP_SHOULD_BE_A_MAP;
15 import static io.guixer.lang.GuixerSyntaxErrorType.SEQ_STEP_SHOULD_BE_A_SINGLETON_MAP;
16 import static io.guixer.lang.GuixerSyntaxErrorType.STEPS_SHOULD_BE_A_LIST;
17 import static io.guixer.lang.GuixerSyntaxErrorType.STEPS_SHOULD_BE_PRESENT;
18 import static io.guixer.lang.GuixerSyntaxErrorType.STEP_SHOULD_BE_A_MAP;
19 import static io.guixer.lang.GuixerSyntaxErrorType.TAKE_SCREENSHOT_SHOULD_BE_AT_STEP_TOP_LEVEL;
20 import static java.nio.charset.StandardCharsets.UTF_8;
21 import static org.apache.commons.lang3.StringUtils.isBlank;
22 import static org.apache.commons.lang3.StringUtils.substringBefore;
23 import static org.apache.commons.lang3.StringUtils.substringBetween;
24
25 import java.io.File;
26 import java.io.IOException;
27 import java.util.Optional;
28 import java.util.Set;
29
30 import org.apache.commons.io.FileUtils;
31 import org.apache.commons.lang3.ArrayUtils;
32
33 import com.avcompris.util.YamlUtils;
34 import com.avcompris.util.Yamled;
35
36 public class GuixerSyntaxChecker {
37
38 public void check(
39 final File yamlFile
40 ) throws IOException, GuixerSyntaxException {
41
42 checkNotNull(yamlFile, "yamlFile");
43
44 final Set<String> commandNamesInAtomicStep = newHashSet();
45
46 for (final String line : FileUtils.readLines(yamlFile, UTF_8)) {
47
48 final String trimmed = line.trim();
49
50 if (trimmed.startsWith("#") || trimmed.isEmpty()) {
51 continue;
52 }
53
54 if (trimmed.startsWith("-")) {
55 commandNamesInAtomicStep.clear();
56 }
57
58 final String commandName;
59
60 if (!line.contains(":")) {
61 continue;
62 }
63
64 if (trimmed.startsWith("-")) {
65
66 commandName = substringBetween(line, "-", ":").trim();
67
68 } else {
69
70 commandName = substringBefore(line, ":").trim();
71 }
72
73 if (commandNamesInAtomicStep.contains(commandName)) {
74
75 throw new GuixerSyntaxException("Duplicate command in AtomicStep: " + commandName,
76 DUPLICATE_COMMANDS_IN_ATOMIC_STEP);
77 }
78
79 commandNamesInAtomicStep.add(commandName);
80 }
81
82 final Yamled yaml = YamlUtils.loadYAML(yamlFile);
83
84 if (!yaml.has("name")) {
85 throw new GuixerSyntaxException("The \"name:\" attribute should be present in the YAML scenario file",
86 SCENARIO_NAME_SHOULD_BE_PRESENT);
87 }
88
89 if (!yaml.has("steps")) {
90 throw new GuixerSyntaxException("The \"steps:\" field should be present in the YAML scenario file",
91 STEPS_SHOULD_BE_PRESENT);
92 }
93
94 final Yamled steps = yaml.get("steps");
95
96 if (!steps.isList()) {
97 throw new GuixerSyntaxException("\"steps:\" should be a list in the YAML scenario file, but was: " + steps,
98 STEPS_SHOULD_BE_A_LIST);
99 }
100
101 for (final Yamled step : steps.items()) {
102
103 loadStep(step);
104 }
105 }
106
107 private static void loadStep(
108 final Yamled step
109 ) throws GuixerSyntaxException {
110
111 if (!step.isMap()) {
112 throw new GuixerSyntaxException("Step should be a map in the YAML scenario file, but was: " + step,
113 STEP_SHOULD_BE_A_MAP);
114 }
115
116 final Set<String> keys = newHashSet();
117
118 for (final Yamled item : step.items(false)) {
119
120 final String key = item.label();
121
122 keys.add(key);
123 }
124
125 final Optional<String> unknownCommandName = keys.stream().filter(key
126
127 -> !(ArrayUtils.contains(LEGIT_COMMAND_NAMES, key)
128 || isExtCommand(key))).findAny();
129
130 if (unknownCommandName.isPresent()) {
131
132 final String commandName = unknownCommandName.get();
133
134 if (keys.size() != 1) {
135 throw new GuixerSyntaxException("Illegal command name with map.size() > 2: " + commandName,
136 ILLEGAL_COMMAND_NAME);
137 }
138
139 final String groupName = commandName;
140
141 if (isBlank(groupName)) {
142 throw new GuixerSyntaxException("groupName should not be empty: " + groupName,
143 GROUP_NAME_SHOULD_NOT_BE_EMPTY);
144 }
145
146 if (!Character.isUpperCase(groupName.charAt(0))) {
147 throw new GuixerSyntaxException(
148 "groupName should start by an uppercase character, but was: " + groupName,
149 GROUP_NAME_SHOULD_START_BY_AN_UPPERCASE_CHARACTER);
150 }
151
152 final Yamled groupSteps = step.get(commandName);
153
154 if (!groupSteps.isList()) {
155 throw new GuixerSyntaxException(
156 "Group steps should be a list in the YAML scenario file, but was: " + groupSteps,
157 GROUP_STEPS_SHOULD_BE_A_LIST);
158 }
159
160 for (final Yamled groupStep : groupSteps.items()) {
161
162 loadStep(groupStep);
163 }
164
165 } else {
166
167 if (keys.contains("seq")) {
168
169 final Optional<String> forbiddenCommandName = keys.stream().filter(key
170
171 -> !ArrayUtils.contains(ALLOWED_COMMAND_NAMES_WITH_SEQ, key)).findAny();
172
173 if (forbiddenCommandName.isPresent()) {
174
175 final String commandName = forbiddenCommandName.get();
176
177 throw new GuixerSyntaxException("Command name should be inside \"seq:\": " + commandName,
178 SEQ_AND_COMMAND_SHOULD_NOT_BE_MIXED);
179 }
180
181 final Yamled seq = step.get("seq");
182
183 if (!seq.isList()) {
184 throw new GuixerSyntaxException(
185 "\"seq:\" should be a list in the YAML scenario file, but was: " + seq,
186 SEQ_SHOULD_BE_A_LIST);
187 }
188
189 for (final Yamled item : seq.items()) {
190
191 if (!item.isMap()) {
192 throw new GuixerSyntaxException(
193 "Step in \"seq:\" should be a map in the YAML scenario file, but was: " + item,
194 SEQ_STEP_SHOULD_BE_A_MAP);
195 }
196
197 final Yamled[] itemSteps = item.items(false);
198
199 if (itemSteps.length != 1) {
200 throw new GuixerSyntaxException(
201 "Step in \"seq:\" should be a a singleton map in the YAML scenario file, but was: "
202 + item,
203 SEQ_STEP_SHOULD_BE_A_SINGLETON_MAP);
204 }
205
206 final String stepCommandName = itemSteps[0].label();
207
208 if (!ArrayUtils.contains(LEGIT_COMMAND_NAMES, stepCommandName)
209 && !isExtCommand(stepCommandName)
210 ) {
211 throw new GuixerSyntaxException("Illegal command in \"seq:\": " + stepCommandName,
212 ILLEGAL_COMMAND_NAME);
213 } else if ("intent".equals(stepCommandName)) {
214 throw new GuixerSyntaxException(
215 "\"intent:\" should be at the same level as \"seq:\": " + stepCommandName,
216 INTENT_SHOULD_BE_AT_STEP_TOP_LEVEL);
217 } else if ("takeScreenshot".equals(stepCommandName)) {
218 throw new GuixerSyntaxException(
219 "\"takeScreenshot:\" should be at the same level as \"seq:\": " + stepCommandName,
220 TAKE_SCREENSHOT_SHOULD_BE_AT_STEP_TOP_LEVEL);
221 } else if (ArrayUtils.contains(ALLOWED_COMMAND_NAMES_WITH_SEQ, stepCommandName)) {
222 throw new GuixerSyntaxException("Illegal command in \"seq:\": " + stepCommandName,
223 ILLEGAL_COMMAND_NAME);
224 }
225 }
226 }
227 }
228 }
229
230 private static String[] LEGIT_COMMAND_NAMES = new String[] {
231 "assertAbsent",
232 "assertFalse",
233 "assertPresent",
234 "assertTrue",
235 "attribute",
236 "attribute[RUN]",
237 "attribute[STEP]",
238 "attribute[UPLOAD]",
239 "call",
240 "clear",
241 "click",
242 "executeScript",
243 "ext[*]",
244 "failure",
245 "get",
246 "intent",
247 "message",
248 "sendKeys",
249 "seq",
250 "setLane",
251 "setMaskedVariable",
252 "setVariable",
253 "sleep",
254 "status",
255 "success",
256 "switchToFrame",
257 "tag",
258 "tag[RUN]",
259 "tag[STEP]",
260 "tag[UPLOAD]",
261 "takeScreenshot",
262 "waitFor",
263 "waitForNot",
264 };
265
266 private static boolean isExtCommand(
267 final String command
268 ) {
269
270 return command.startsWith("ext[") && command.endsWith("]");
271 }
272
273 private static String[] ALLOWED_COMMAND_NAMES_WITH_SEQ = new String[] {
274 "intent",
275 "seq",
276 "takeScreenshot",
277 };
278 }