Как найти ошибку в сложном модуле?

Я пишу на 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: перечислю здесь, что я попробовал сделать на текущий момент самостоятельно.

  1. Перед созданием дерева создать WebDocument и передавать его в билдер, чтобы сделать процесс максимально одинаковым. Не помогло.
  2. В работающем синтетическом тесте использовать идентичный конструктор, задавая ширину и высоту в -1 уже после его вызова (дело в том, что по умолчанию внутри конструктора без габаритов габариты элемента выставляются в 0,0, а не в -1, -1). Не сломалось, значит тоже мимо.
  3. Я подумал, что возможно, я сравниваю структуры слишком поздно (через 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 шт):

Автор решения: Alex Popov

Я нашёл ошибку. Всё оказалось весьма банально: в коде добавления элементов, который я написал в "адаптере", я не написал, что если у элемента нулевая ширина, то нужно проставить ему ширину родительского блока, в который я его "подселяю". В итоге элементы подставлялись с нулевой шириной, и поэтому ничего не отрисовывалось. Вторая же часть фикса заключалась в том, чтобы после создания документа (который создавался ещё до добавления в него элементов) проставить ему размеры - что я, к сожалению, забыл сделать.

И вообще, косяк с моей стороны в том, что у меня в классе документа нет нормальных сеттеров для ширины и высоты, а вместо этого я использую публичные поля. Были бы сеттеры - можно было бы просто задать размеры постфактум, и всё бы подстроилось автоматически.

Нужен рефакторинг.

→ Ссылка