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