Как найти ошибку в сложном модуле?
Я пишу на Java HTML-движок. Сейчас я работаю над интеграцией парсера и рендера.
В алгоритме где-то есть скрытая ошибка: дело в том, что когда я создаю элементы для рендера явно, отрисовка проходит успешно, но когда я создаю их из HTML нод, полученных из парсера, ничего не отрисовывается.
Странность в том, что я создаю элементы используя те же конструкторы. А главное, я написал код, берущий два объекта и сравнивающий все их поля. Так вот, для всех трёх элементов рендера, задействованных в тесте, значения полей оказались идентичными (для рабочего и нерабочего кейса).
Как такое может быть? Две древовидных структуры, поля элементов которых одинаковы, дают разный результат работы. При этом все размеры и координаты элементов тоже оказались равными (они тоже хранятся в полях, которые мой метод сравнивает).
Кода очень много, поэтому весь постить сразу не вижу смысла. Но могу дать фрагмент из главного класса, где содержатся тесты, чтобы было понятно, о чём идёт речь.
Это сам тест:
public static void testBuilder() {
HTMLParser hp = new HTMLParser("test.htm");
System.out.println("----------------------------------");
hp.traverseTree();
System.out.println();
System.out.println("----------------------------------");
Builder builder = new Builder();
final Block root = builder.buildSubtree(null, hp.getRootNode().lastElementChild());
System.out.println(root);
System.out.println("----" + root.getChildren().get(0));
System.out.println("--------" + root.getChildren().get(0).getChildren().get(0));
root.removeElement(1);
visualBuilderTest(root);
System.out.println();
final Block root2 = visualBuilderSyntheticTest();
Timer t = new Timer(300, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
ArrayList<String> exclude = new ArrayList(Arrays.asList("lm", "parentListener", "border", "document", "layouter"));
HashMap<String, String> fields1 = getFields(root, exclude);
HashMap<String, String> fields2 = getFields(root2, exclude);
compareFieldsets(fields1, fields2);
}
});
t.setRepeats(false);
t.start();
}
Это тот код, который должен работать, но не работает (попытка отрисовать то дерево, что получено из парсера):
public static void visualBuilderTest(Block root) {
JFrame frame = new JFrame("Render Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel();
final WebDocument document = new WebDocument();
document.insertSubtree(document.root, root);
root.setId("root");
document.setPreferredSize(new Dimension(460, 240));
document.width = 460;
document.height = 240;
root.setBounds(0, 0, document.width, document.height);
root.setWidth(-1);
root.height = document.height;
root.viewport_height = root.height;
root.orig_height = root.height;
root.max_height = root.height;
root.auto_height = false;
document.root.getChildren().get(0).setBackgroundColor(Color.CYAN);
try {
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {}
document.ready = true;
document.panel.setBackground(Color.WHITE);
document.setBorder(BorderFactory.createLineBorder(Color.black, 1));
panel.add(document);
frame.add(panel);
//panel.setBorder(BorderFactory.createEmptyBorder(9, 10, 9, 10));
panel.setPreferredSize(new Dimension(document.width + 18, document.height + 18));
frame.pack();
frame.setLocationRelativeTo(null);
frame.addComponentListener(new java.awt.event.ComponentAdapter() {
@Override
public void componentMoved(java.awt.event.ComponentEvent evt) {}
@Override
public void componentResized(java.awt.event.ComponentEvent evt) {
document.resized();
}
});
frame.setVisible(true);
}
А это "синтетический тест", который работает, создавая те же элементы на лету:
public static Block visualBuilderSyntheticTest() {
JFrame frame = new JFrame("Render Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel();
final WebDocument document = new WebDocument();
Block root = document.root;
root.setId("root");
document.setPreferredSize(new Dimension(460, 240));
document.width = 460;
document.height = 240;
//document.ready = false;
root.setBounds(0, 0, document.width, document.height);
root.setWidth(-1);
root.height = document.height;
root.viewport_height = root.height;
root.orig_height = root.height;
root.max_height = root.height;
root.auto_height = false;
Block paragraph = new Block(document, root, -1, -1, 0, 0, Color.BLACK);
paragraph.setMargins(0, 0, 12, 0);
paragraph.addText("This is a paragraph");
root.addElement(paragraph);
root.getChildren().get(0).setBackgroundColor(Color.CYAN);
try {
UIManager.setLookAndFeel(
UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {}
document.ready = true;
document.panel.setBackground(Color.WHITE);
document.setBorder(BorderFactory.createLineBorder(Color.black, 1));
panel.add(document);
frame.add(panel);
//panel.setBorder(BorderFactory.createEmptyBorder(9, 10, 9, 10));
panel.setPreferredSize(new Dimension(document.width + 18, document.height + 18));
frame.pack();
//frame.setLocationRelativeTo(null);
frame.addComponentListener(new java.awt.event.ComponentAdapter() {
@Override
public void componentMoved(java.awt.event.ComponentEvent evt) {}
@Override
public void componentResized(java.awt.event.ComponentEvent evt) {
document.resized();
}
});
frame.setVisible(true);
return root;
}
Код билдера, который строит по нодам элементы для рендеринга:
public Block buildSubtree(WebDocument document, Node node) {
Block root = buildElement(document, node);
for (int i = 0; i < node.children.size(); i++) {
Block b = buildSubtree(document, node.children.get(i));
if (b != null) {
root.getChildren().add(b);
b.parent = root;
}
}
return root;
}
public Block buildElement(WebDocument document, Node node) {
Block b = new Block(document);
if (node.nodeType == ELEMENT) {
b.type = Block.NodeTypes.ELEMENT;
b.width = -1;
b.height = -1;
b.auto_width = true;
b.auto_height = true;
} else if (node.nodeType == TEXT) {
b.type = Block.NodeTypes.TEXT;
b.textContent = node.nodeValue;
return b;
} else if (node.nodeType == COMMENT) {
return null;
}
if (BlockElements.contains(node.tagName)) {
b.display_type = Block.Display.BLOCK;
} else if (InlineElements.contains(node.tagName)) {
b.display_type = Block.Display.INLINE;
} else if (node.tagName.equals("table")) {
b.display_type = Block.Display.TABLE;
} else if (node.tagName.equals("tr")) {
b.display_type = Block.Display.TABLE_ROW;
} else if (node.tagName.equals("td")) {
b.display_type = Block.Display.TABLE_CELL;
}
b.id = node.getAttribute("id");
b.setTextColor(node.getAttribute("color"));
b.setBackgroundColor(node.getAttribute("bgcolor"));
if (node.tagName.equals("a")) b.href = node.getAttribute("href");
else if (node.tagName.equals("img")) {
b.width = -1;
b.isImage = true;
b.setBackgroundImage(node.getAttribute("src"));
}
else if (node.tagName.equals("p")) {
b.setMargins(0, 0, 12, 0);
}
else if (node.tagName.equals("font")) {
if (node.getAttribute("size") != null) {
b.setFontSize(Integer.parseInt(node.getAttribute("size")));
}
}
else if (node.tagName.equals("li")) {
b.list_item_type = 2;
}
return b;
}
И напоследок код, сравнивающий поля двух элементов:
private static HashMap<String, String> getFields(Block block, List<String> exclude) {
HashMap<String, String> result = new HashMap<String, String>();
for (Field field : block.getClass().getDeclaredFields()) {
try {
field.setAccessible(true);
Object value = field.get(block);
if (value != null && !exclude.contains(field.getName())) {
//System.out.println(field.getName() + "=" + value);
String str = value.toString();
if (value != null && value.getClass().isArray()) {
if (value instanceof int[]) {
int[] a = (int[]) value;
str = "";
for (int i = 0; i < a.length; i++) {
if (i > 0) str += ", ";
str += a[i];
}
str = "[" + str + "]";
}
if (value instanceof Color[]) {
Color[] a = (Color[]) value;
str = "";
for (int i = 0; i < a.length; i++) {
if (i > 0) str += ", ";
Color col = (Color) a[i];
str = "color[" + col.getRed() + ", " + col.getGreen() + ", " + col.getBlue() + ", " + col.getAlpha() + "]";
}
str = "[" + str + "]";
}
//System.out.println(value.getClass().getComponentType());
}
if (value instanceof Color) {
Color col = (Color) value;
str = "color[" + col.getRed() + ", " + col.getGreen() + ", " + col.getBlue() + ", " + col.getAlpha() + "]";
}
if (value instanceof BufferedImage && value != null) {
BufferedImage img = (BufferedImage) value;
str = "BufferedImage[" + img.getWidth() + "x" + img.getHeight() + "]";
}
result.put(field.getName(), str);
}
} catch (IllegalArgumentException ex) {
ex.printStackTrace();
} catch (IllegalAccessException ex) {
ex.printStackTrace();
}
}
return result;
}
private static void compareFieldsets(HashMap<String, String> fields1, HashMap<String, String> fields2) {
String result = "";
Set keys = fields1.keySet();
Iterator it = keys.iterator();
while (it.hasNext()) {
String key = (String) it.next();
if (!fields1.get(key).equals(fields2.get(key))) {
result += key + ": " + fields1.get(key) + " <-> " + fields2.get(key) + "\n";
}
}
System.out.println(result.length() > 0 ? result : "Objects are equal");
}
Где стоит искать ошибку? У меня совсем закончились идеи, к сожалению.
UPDATE: перечислю здесь, что я попробовал сделать на текущий момент самостоятельно.
- Перед созданием дерева создать
WebDocumentи передавать его в билдер, чтобы сделать процесс максимально одинаковым. Не помогло. - В работающем синтетическом тесте использовать идентичный конструктор, задавая ширину и высоту в -1 уже после его вызова (дело в том, что по умолчанию внутри конструктора без габаритов габариты элемента выставляются в 0,0, а не в -1, -1). Не сломалось, значит тоже мимо.
- Я подумал, что возможно, я сравниваю структуры слишком поздно (через 300 мс по таймеру), и возможно, при отрисовке в первом кейсе всё ещё что-то имеет неверные значения.
Тогда я переписал обработчики ресайза контейнера (компоновка и отрисовка вызываются при каждом изменении размера, поэтому я даже не должен вызывать их явно):
frame.addComponentListener(new java.awt.event.ComponentAdapter() {
@Override
public void componentMoved(java.awt.event.ComponentEvent evt) {}
@Override
public void componentResized(java.awt.event.ComponentEvent evt) {
boolean flag = (document.getWidth() != document.last_width || document.getHeight() != document.last_height);
document.resized();
if (flag) {
ArrayList<String> exclude = new ArrayList(Arrays.asList("lm", "parentListener", "border", "document", "layouter"));
f1 = getFields(document.root, exclude);
}
}
});
f1 и f2 я сделал статическими полями класса, и в сравнение в коде таймера подставил уже их. В итоге по-прежнему после компоновки и отрисовки все значения равны.
Ответы (1 шт):
Я нашёл ошибку. Всё оказалось весьма банально: в коде добавления элементов, который я написал в "адаптере", я не написал, что если у элемента нулевая ширина, то нужно проставить ему ширину родительского блока, в который я его "подселяю". В итоге элементы подставлялись с нулевой шириной, и поэтому ничего не отрисовывалось. Вторая же часть фикса заключалась в том, чтобы после создания документа (который создавался ещё до добавления в него элементов) проставить ему размеры - что я, к сожалению, забыл сделать.
И вообще, косяк с моей стороны в том, что у меня в классе документа нет нормальных сеттеров для ширины и высоты, а вместо этого я использую публичные поля. Были бы сеттеры - можно было бы просто задать размеры постфактум, и всё бы подстроилось автоматически.
Нужен рефакторинг.