blob: a7ec1ff648f8b80278804551dcb71a9e8e51b18d [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/modules/accessibility/ax_debug_utils.h"
#include <memory>
#include <numeric>
#include <string>
#include <utility>
namespace blink {
namespace {
std::string NewlineToSpaceReplacer(std::string str) {
std::replace(str.begin(), str.end(), '\n', ' ');
return str;
}
size_t RecursiveIncludedNodeCount(AXObject* subtree) {
size_t count = 1; // For |subtree| itself.
for (const auto& child : subtree->ChildrenIncludingIgnored()) {
count += RecursiveIncludedNodeCount(child);
}
return count;
}
} // namespace
std::string TreeToStringHelper(const AXObject* obj, bool verbose) {
return TreeToStringWithMarkedObjectHelper(obj, nullptr, verbose);
}
std::string TreeToStringWithMarkedObjectHelperRecursive(
const AXObject* obj,
const AXObject* marked_object,
bool cached,
int indent,
bool verbose,
int* marked_object_found_count) {
if (!obj) {
return "";
}
if (marked_object_found_count && marked_object && obj == marked_object) {
++*marked_object_found_count;
}
std::string extra = obj == marked_object ? "*" : " ";
return std::accumulate(
obj->CachedChildrenIncludingIgnored().begin(),
obj->CachedChildrenIncludingIgnored().end(),
extra + std::string(std::max(2 * indent - 1, 0), ' ') +
NewlineToSpaceReplacer(obj->ToString(verbose, cached).Utf8()) + "\n",
[cached, indent, verbose, marked_object, marked_object_found_count](
const std::string& str, const AXObject* child) {
return str + TreeToStringWithMarkedObjectHelperRecursive(
child, marked_object, cached, indent + 1, verbose,
marked_object_found_count);
});
}
std::string TreeToStringWithMarkedObjectHelper(const AXObject* obj,
const AXObject* marked_object,
bool verbose) {
int marked_object_found_count = 0;
// Use cached properties only unless it's frozen and thus safe to use compute
// methods.
bool cached = !obj->IsDetached() && !obj->AXObjectCache().IsFrozen();
std::string tree_str = TreeToStringWithMarkedObjectHelperRecursive(
obj, marked_object, cached, 0, verbose, &marked_object_found_count);
if (marked_object_found_count == 1) {
return tree_str;
}
if (!marked_object) {
return tree_str;
}
return std::string("**** ERROR: Found marked objects was found ") +
String::Number(marked_object_found_count).Utf8() +
" times, should have been found exactly once.\n* Marked object: " +
marked_object->ToString(true, cached).Utf8() + "\n\n" + tree_str;
}
std::string ParentChainToStringHelper(const AXObject* obj) {
bool cached = !obj->IsDetached() && !obj->AXObjectCache().IsFrozen();
AXObject::AXObjectVector ancestors;
while (obj) {
ancestors.push_back(const_cast<AXObject*>(obj));
obj = obj->ParentObject();
}
size_t indent = 0;
std::string builder;
for (auto iter = ancestors.rbegin(); iter != ancestors.rend(); iter++) {
builder = builder + std::string(2 * indent, ' ') +
(*iter)->ToString(true, cached).Utf8() + '\n';
++indent;
}
return builder;
}
void CheckTreeConsistency(
AXObjectCacheImpl& cache,
ui::AXTreeSerializer<AXObject*, HeapVector<Member<AXObject>>>& serializer) {
// If all serializations are complete, check that the number of included nodes
// being serialized is the same as the number of included nodes according to
// the AXObjectCache.
size_t included_node_count_from_cache = cache.GetIncludedNodeCount();
if (included_node_count_from_cache != serializer.ClientTreeNodeCount()) {
// There was an inconsistency in the node count: provide a helpful message
// to facilitate debugging.
std::ostringstream msg;
msg << "AXTreeSerializer should have the expected number of included nodes:"
<< "\n* AXObjectCache: " << included_node_count_from_cache
<< "\n* Depth first cache count: "
<< RecursiveIncludedNodeCount(cache.Root())
<< "\n* Serializer: " << serializer.ClientTreeNodeCount();
HeapHashMap<AXID, Member<AXObject>>& all_objects = cache.GetObjects();
for (const auto& id_to_object_entry : all_objects) {
AXObject* obj = id_to_object_entry.value;
if (obj->LastKnownIsIncludedInTreeValue()) {
if (!serializer.IsInClientTree(obj)) {
if (obj->IsMissingParent()) {
msg << "\n* Included node not serialized, is missing parent: "
<< obj->ToString(true, true);
} else if (!obj->GetDocument()->GetFrame()) {
msg << "\n* Included node not serialized, in closed document: "
<< obj->ToString(true, true);
} else {
bool included_state_stale = !obj->AccessibilityIsIncludedInTree();
msg << "\n* Included node not serialized: " << obj->ToString(true);
if (included_state_stale) {
msg << "\n Included state was stale.";
}
msg << "\n Parent: " << obj->CachedParentObject()->ToString(true);
}
}
}
}
for (AXID id : serializer.ClientTreeNodeIds()) {
AXObject* obj = cache.ObjectFromAXID(id);
if (!obj) {
msg << "\n* Serialized node does not exist: " << id;
if (AXObject* parent = serializer.ParentOf(id)) {
msg << "\n* Parent = " << parent->ToString(true);
}
} else if (!obj->LastKnownIsIncludedInTreeValue()) {
msg << "\n* Serialized an unincluded node: " << obj->ToString(true);
}
}
DCHECK(false) << msg.str();
}
#if EXPENSIVE_DCHECKS_ARE_ON()
constexpr size_t kMaxNodesForDeepSlowConsistencyCheck = 100;
if (included_node_count_from_cache > kMaxNodesForDeepSlowConsistencyCheck) {
return;
}
DCHECK_EQ(included_node_count_from_cache,
RecursiveIncludedNodeCount(cache.Root()))
<< "\n* AXObjectCacheImpl's tree:\n"
<< TreeToStringHelper(cache.Root(), /* verbose */ true);
#endif // EXPENSIVE_DCHECKS_ARE_ON()
}
} // namespace blink