1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "content/public/test/dump_accessibility_test_helper.h"
6
7 #include "base/command_line.h"
8 #include "base/files/file_util.h"
9 #include "base/logging.h"
10 #include "base/strings/string_split.h"
11 #include "base/strings/string_util.h"
12 #include "base/strings/stringprintf.h"
13 #include "base/threading/thread_restrictions.h"
14 #include "build/build_config.h"
15 #include "content/public/common/content_switches.h"
16 #include "ui/accessibility/platform/inspect/tree_formatter.h"
17
18 #if defined(OS_WIN)
19 #include "base/win/windows_version.h"
20 #endif
21
22 namespace content {
23
24 using base::FilePath;
25 using ui::AXNodeFilter;
26 using ui::AXPropertyFilter;
27
28 namespace {
29 const char kCommentToken = '#';
30 const char kMarkSkipFile[] = "#<skip";
31 const char kSignalDiff[] = "*";
32 const char kMarkEndOfFile[] = "<-- End-of-file -->";
33
34 struct TypeInfo {
35 std::string type;
36 struct Mapping {
37 std::string directive_prefix;
38 base::FilePath::StringType expectations_file_postfix;
39 } mapping;
40 };
41
42 const TypeInfo kTypeInfos[] = {{
43 "android",
44 {
45 "@ANDROID",
46 FILE_PATH_LITERAL("-android"),
47 },
48 },
49 {
50 "blink",
51 {
52 "@BLINK",
53 FILE_PATH_LITERAL("-blink"),
54 },
55 },
56 {
57 "linux",
58 {
59 "@AURALINUX",
60 FILE_PATH_LITERAL("-auralinux"),
61 },
62 },
63 {
64 "mac",
65 {
66 "@MAC",
67 FILE_PATH_LITERAL("-mac"),
68 },
69 },
70 {
71 "content",
72 {
73 "@",
74 FILE_PATH_LITERAL(""),
75 },
76 },
77 {
78 "uia",
79 {
80 "@UIA-WIN",
81 FILE_PATH_LITERAL("-uia-win"),
82 },
83 },
84 {
85 "win",
86 {
87 "@WIN",
88 FILE_PATH_LITERAL("-win"),
89 },
90 }};
91
TypeMapping(const std::string & type)92 const TypeInfo::Mapping* TypeMapping(const std::string& type) {
93 const TypeInfo::Mapping* mapping = nullptr;
94 for (const auto& info : kTypeInfos) {
95 if (info.type == type) {
96 mapping = &info.mapping;
97 }
98 }
99 CHECK(mapping) << "Unknown dump accessibility type " << type;
100 return mapping;
101 }
102
103 } // namespace
104
DumpAccessibilityTestHelper(const char * expectation_type)105 DumpAccessibilityTestHelper::DumpAccessibilityTestHelper(
106 const char* expectation_type)
107 : expectation_type_(expectation_type) {}
108
GetExpectationFilePath(const base::FilePath & test_file_path)109 base::FilePath DumpAccessibilityTestHelper::GetExpectationFilePath(
110 const base::FilePath& test_file_path) {
111 base::ScopedAllowBlockingForTesting allow_blocking;
112 base::FilePath expected_file_path;
113
114 // Try to get version specific expected file.
115 base::FilePath::StringType expected_file_suffix =
116 GetVersionSpecificExpectedFileSuffix();
117 if (expected_file_suffix != FILE_PATH_LITERAL("")) {
118 expected_file_path = base::FilePath(
119 test_file_path.RemoveExtension().value() + expected_file_suffix);
120 if (base::PathExists(expected_file_path))
121 return expected_file_path;
122 }
123
124 // If a version specific file does not exist, get the generic one.
125 expected_file_suffix = GetExpectedFileSuffix();
126 expected_file_path = base::FilePath(test_file_path.RemoveExtension().value() +
127 expected_file_suffix);
128 if (base::PathExists(expected_file_path))
129 return expected_file_path;
130
131 // If no expected file could be found, display error.
132 LOG(INFO) << "File not found: " << expected_file_path.LossyDisplayName();
133 LOG(INFO) << "To run this test, create "
134 << expected_file_path.LossyDisplayName()
135 << " (it can be empty) and then run this test "
136 << "with the switch: --"
137 << switches::kGenerateAccessibilityTestExpectations;
138 return base::FilePath();
139 }
140
ParsePropertyFilter(const std::string & line,std::vector<AXPropertyFilter> * filters) const141 bool DumpAccessibilityTestHelper::ParsePropertyFilter(
142 const std::string& line,
143 std::vector<AXPropertyFilter>* filters) const {
144 const TypeInfo::Mapping* mapping = TypeMapping(expectation_type_);
145 if (!mapping) {
146 return false;
147 }
148
149 std::string directive = mapping->directive_prefix + "-ALLOW-EMPTY:";
150 if (base::StartsWith(line, directive, base::CompareCase::SENSITIVE)) {
151 filters->emplace_back(line.substr(directive.size()),
152 AXPropertyFilter::ALLOW_EMPTY);
153 return true;
154 }
155
156 directive = mapping->directive_prefix + "-ALLOW:";
157 if (base::StartsWith(line, directive, base::CompareCase::SENSITIVE)) {
158 filters->emplace_back(line.substr(directive.size()),
159 AXPropertyFilter::ALLOW);
160 return true;
161 }
162
163 directive = mapping->directive_prefix + "-DENY:";
164 if (base::StartsWith(line, directive, base::CompareCase::SENSITIVE)) {
165 filters->emplace_back(line.substr(directive.size()),
166 AXPropertyFilter::DENY);
167 return true;
168 }
169
170 return false;
171 }
172
ParseNodeFilter(const std::string & line,std::vector<AXNodeFilter> * filters) const173 bool DumpAccessibilityTestHelper::ParseNodeFilter(
174 const std::string& line,
175 std::vector<AXNodeFilter>* filters) const {
176 const TypeInfo::Mapping* mapping = TypeMapping(expectation_type_);
177 if (!mapping) {
178 return false;
179 }
180
181 std::string directive = mapping->directive_prefix + "-DENY-NODE:";
182 if (base::StartsWith(line, directive, base::CompareCase::SENSITIVE)) {
183 const auto& node_filter = line.substr(directive.size());
184 const auto& parts = base::SplitString(
185 node_filter, "=", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
186 // Silently skip over parsing errors like the rest of the enclosing code.
187 if (parts.size() == 2) {
188 filters->emplace_back(parts[0], parts[1]);
189 return true;
190 }
191 }
192
193 return false;
194 }
195
196 DumpAccessibilityTestHelper::Directive
ParseDirective(const std::string & line) const197 DumpAccessibilityTestHelper::ParseDirective(const std::string& line) const {
198 // Directives have format of @directive:value.
199 if (!base::StartsWith(line, "@")) {
200 return {};
201 }
202
203 auto directive_end_pos = line.find_first_of(':');
204 if (directive_end_pos == std::string::npos) {
205 return {};
206 }
207
208 const TypeInfo::Mapping* mapping = TypeMapping(expectation_type_);
209 if (!mapping) {
210 return {};
211 }
212
213 std::string directive = line.substr(0, directive_end_pos);
214 std::string value = line.substr(directive_end_pos + 1);
215 if (directive == "@NO-LOAD-EXPECTED") {
216 return {Directive::kNoLoadExpected, value};
217 }
218 if (directive == "@WAIT-FOR") {
219 return {Directive::kWaitFor, value};
220 }
221 if (directive == "@EXECUTE-AND-WAIT-FOR") {
222 return {Directive::kExecuteAndWaitFor, value};
223 }
224 if (directive == mapping->directive_prefix + "-RUN-UNTIL-EVENT") {
225 return {Directive::kRunUntil, value};
226 }
227 if (directive == "@DEFAULT-ACTION-ON") {
228 return {Directive::kDefaultActionOn, value};
229 }
230 return {};
231 }
232
233 base::Optional<std::vector<std::string>>
LoadExpectationFile(const base::FilePath & expected_file)234 DumpAccessibilityTestHelper::LoadExpectationFile(
235 const base::FilePath& expected_file) {
236 base::ScopedAllowBlockingForTesting allow_blocking;
237
238 std::string expected_contents_raw;
239 base::ReadFileToString(expected_file, &expected_contents_raw);
240
241 // Tolerate Windows-style line endings (\r\n) in the expected file:
242 // normalize by deleting all \r from the file (if any) to leave only \n.
243 std::string expected_contents;
244 base::RemoveChars(expected_contents_raw, "\r", &expected_contents);
245
246 if (!expected_contents.compare(0, strlen(kMarkSkipFile), kMarkSkipFile)) {
247 return base::nullopt;
248 }
249
250 std::vector<std::string> expected_lines =
251 base::SplitString(expected_contents, "\n", base::KEEP_WHITESPACE,
252 base::SPLIT_WANT_NONEMPTY);
253
254 return expected_lines;
255 }
256
ValidateAgainstExpectation(const base::FilePath & test_file_path,const base::FilePath & expected_file,const std::vector<std::string> & actual_lines,const std::vector<std::string> & expected_lines)257 bool DumpAccessibilityTestHelper::ValidateAgainstExpectation(
258 const base::FilePath& test_file_path,
259 const base::FilePath& expected_file,
260 const std::vector<std::string>& actual_lines,
261 const std::vector<std::string>& expected_lines) {
262 // Output the test path to help anyone who encounters a failure and needs
263 // to know where to look.
264 LOG(INFO) << "Testing: "
265 << test_file_path.NormalizePathSeparatorsTo('/').LossyDisplayName();
266 LOG(INFO) << "Expected output: "
267 << expected_file.NormalizePathSeparatorsTo('/').LossyDisplayName();
268
269 // Perform a diff (or write the initial baseline).
270 std::vector<int> diff_lines = DiffLines(expected_lines, actual_lines);
271 bool is_different = diff_lines.size() > 0;
272 if (is_different) {
273 std::string diff;
274
275 // Mark the expected lines which did not match actual output with a *.
276 diff += "* Line Expected\n";
277 diff += "- ---- --------\n";
278 for (int line = 0, diff_index = 0;
279 line < static_cast<int>(expected_lines.size()); ++line) {
280 bool is_diff = false;
281 if (diff_index < static_cast<int>(diff_lines.size()) &&
282 diff_lines[diff_index] == line) {
283 is_diff = true;
284 ++diff_index;
285 }
286 diff += base::StringPrintf("%1s %4d %s\n", is_diff ? kSignalDiff : "",
287 line + 1, expected_lines[line].c_str());
288 }
289 diff += "\nActual\n";
290 diff += "------\n";
291 diff += base::JoinString(actual_lines, "\n");
292 diff += "\n";
293
294 // This is used by rebase_dump_accessibility_tree_test.py to signify
295 // the end of the file when parsing the actual output from remote logs.
296 diff += kMarkEndOfFile;
297 diff += "\n";
298 LOG(ERROR) << "Diff:\n" << diff;
299 } else {
300 LOG(INFO) << "Test output matches expectations.";
301 }
302
303 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
304 switches::kGenerateAccessibilityTestExpectations)) {
305 base::ScopedAllowBlockingForTesting allow_blocking;
306 std::string actual_contents_for_output =
307 base::JoinString(actual_lines, "\n") + "\n";
308 CHECK(base::WriteFile(expected_file, actual_contents_for_output));
309 LOG(INFO) << "Wrote expectations to: " << expected_file.LossyDisplayName();
310 #if defined(OS_ANDROID)
311 LOG(INFO) << "Generated expectations written to file on test device.";
312 LOG(INFO) << "To fetch, run: adb pull " << expected_file.LossyDisplayName();
313 #endif
314 }
315
316 return !is_different;
317 }
318
GetExpectedFileSuffix() const319 FilePath::StringType DumpAccessibilityTestHelper::GetExpectedFileSuffix()
320 const {
321 const TypeInfo::Mapping* mapping = TypeMapping(expectation_type_);
322 if (!mapping) {
323 return FILE_PATH_LITERAL("");
324 }
325 return FILE_PATH_LITERAL("-expected") + mapping->expectations_file_postfix +
326 FILE_PATH_LITERAL(".txt");
327 }
328
329 FilePath::StringType
GetVersionSpecificExpectedFileSuffix() const330 DumpAccessibilityTestHelper::GetVersionSpecificExpectedFileSuffix() const {
331 #if defined(OS_WIN)
332 if (expectation_type_ == "uia" &&
333 base::win::GetVersion() == base::win::Version::WIN7) {
334 return FILE_PATH_LITERAL("-expected-uia-win7.txt");
335 }
336 #endif
337 return FILE_PATH_LITERAL("");
338 }
339
DiffLines(const std::vector<std::string> & expected_lines,const std::vector<std::string> & actual_lines)340 std::vector<int> DumpAccessibilityTestHelper::DiffLines(
341 const std::vector<std::string>& expected_lines,
342 const std::vector<std::string>& actual_lines) {
343 int actual_lines_count = actual_lines.size();
344 int expected_lines_count = expected_lines.size();
345 std::vector<int> diff_lines;
346 int i = 0, j = 0;
347 while (i < actual_lines_count && j < expected_lines_count) {
348 if (expected_lines[j].size() == 0 ||
349 expected_lines[j][0] == kCommentToken) {
350 // Skip comment lines and blank lines in expected output.
351 ++j;
352 continue;
353 }
354
355 if (actual_lines[i] != expected_lines[j])
356 diff_lines.push_back(j);
357 ++i;
358 ++j;
359 }
360
361 // Report a failure if there are additional expected lines or
362 // actual lines.
363 if (i < actual_lines_count) {
364 diff_lines.push_back(j);
365 } else {
366 while (j < expected_lines_count) {
367 if (expected_lines[j].size() > 0 &&
368 expected_lines[j][0] != kCommentToken) {
369 diff_lines.push_back(j);
370 }
371 j++;
372 }
373 }
374
375 // Actual file has been fully checked.
376 return diff_lines;
377 }
378
379 } // namespace content
380