Blog

Testing KHTML with a Small Page

A page rendered incorrectly in Konqueror today, and the original example was nearly two hundred lines long. CSS, tables, scripts, and invalid markup were all competing for suspicion. Reading KHTML code at random was not going to settle the argument.

I reduced the page until only the bad behavior remained:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
  <title>KHTML test</title>
  <style type="text/css">
    .box { width: 120px; padding: 10px; border: 1px solid black; }
  </style>
</head>
<body>
  <div class="box">A small test.</div>
</body>
</html>

That small file answers several questions quickly. Does standards or quirks handling matter? Is the problem in parsing, style selection, or layout? Does removing the width make it disappear? It also gives me something suitable for a regression test rather than a private copy of somebody’s entire site.

Konqueror embeds KHTML as a part. KHTML parses the document into its DOM representation, applies style rules, creates rendering objects, and lays them out in the available view. A symptom on screen can therefore begin much earlier than painting. If the DOM tree is wrong because the markup triggered error recovery, adjusting the renderer only conceals the first mistake.

I use khtml’s debug areas selectively when running Konqueror from a terminal. Turning on every message produces a heroic quantity of text and very little enlightenment. A focused trace plus a ten-line document usually beats scrolling.

I also check the same local file after clearing variables from the experiment: no proxy, no stale cache, no external style sheet, and no script unless script is essential to the failure. Then I add pieces back one at a time.

When the reduced case involves script, I separate the DOM mutation from the layout result. First I save a static document representing the tree after the script has run. If the static page still fails, the problem is no longer dependent on timing or the script interpreter. If it succeeds, I inspect when the mutation occurs and which notification should have caused style recalculation or layout.

Fonts can produce similarly misleading reports. A line wrapping differently is not automatically a box calculation bug. I note the selected family and size and try a common fixed font before changing layout code. Image dimensions and delayed loading deserve the same treatment because they can trigger a later relayout.

Finally, I state the expected result in plain words beside the test. A small HTML file without an expectation becomes a puzzle for the next person, who may reasonably conclude that the current rendering was the intended one.

My preference is to preserve the smallest failing page alongside the fix. The caveat is that reduction can accidentally remove malformed input that caused the original path, so I keep the original page until the reduced case is proven equivalent. Minimal examples are excellent witnesses, but occasionally they change their story under questioning.

Do Not Block the KIO Job

I changed a file-opening routine from local paths to URLs and immediately made the window feel stuck. The transfer was not especially slow. My code was waiting synchronously in the user-interface path, so even a short delay became visible.

KIO is built around jobs. A call such as KIO::get() starts work and returns a KIO::TransferJob; data and completion arrive through signals while the event loop continues processing input and painting.

KIO::TransferJob *job = KIO::get(url, false, false);
connect(job, SIGNAL(data(KIO::Job *, const QByteArray &)),
        this, SLOT(slotData(KIO::Job *, const QByteArray &)));
connect(job, SIGNAL(result(KIO::Job *)),
        this, SLOT(slotResult(KIO::Job *)));

The job chooses an appropriate KIO slave from the URL’s protocol. The slave runs separately, speaks the protocol, and reports data, progress, redirections, and errors back through KIO. My application consumes a common job interface instead of containing FTP, HTTP, and file code.

There are two details I now handle every time. First, data may arrive in several chunks. The data signal is not a promise that one byte array equals one document. I append or parse incrementally. Second, I treat result as the final authority. An empty final chunk does not mean success, and a non-empty first chunk does not mean the transfer will finish.

For a tiny operation, KIO::NetAccess can provide a convenient synchronous wrapper. I still avoid it from slots reached directly through buttons and menus. Nested event loops and blocked windows are an expensive price for saving two slots.

Cancellation also matters. I keep the job pointer, clear it when the job finishes, and kill the job when the owning view goes away. Because jobs are QObjects, sensible parentage helps, but explicit user cancellation should still be reflected in the interface.

Redirection is another reason not to reduce a transfer to one blocking read. The final URL may differ from the requested one, and policy about accepting it belongs in the job flow. Authentication and certificate questions can also require interaction supplied by KDE. A hand-written socket loop tends to rediscover these cases individually, generally when somebody is trying to use the program rather than when I am prepared to debug it.

For uploads I apply the same rule in reverse: feed data when the job requests it and finish according to the job’s protocol. I do not assume that writing a local temporary file and copying it afterward has identical overwrite and error behavior.

I prefer KIO jobs even for local URLs when the operation already accepts a KURL. The mechanism remains uniform and remote files stop being an awkward special case. The caveat is that asynchronous code forces the state to be honest. That is inconvenient for about an hour and useful for the rest of the program’s life.

KParts Ownership in One Minute

I embedded a viewer part, closed its window, and received a crash during shutdown. The viewer worked. My ownership did not.

A KPart is a QObject, and its widget is usually parented into the host’s widget tree. Those are related lifetimes, but they are not an invitation to delete everything in sight. I now keep the part pointer in the host, give the constructor the intended object parent, and let that relationship determine destruction.

The shape is roughly this:

KParts::ReadOnlyPart *part = factory->createPart(
    parentWidget, "viewer widget",
    this, "viewer part",
    "KParts::ReadOnlyPart");

setCentralWidget(part->widget());

The factory creates the component. The part owns its document behavior and supplies a widget for presentation. The shell owns the part through the QObject parent passed to the factory. I do not separately delete the widget; its parent chain handles that.

The other lifetime to remember is the component factory or library handle used to create the part. KDE’s loader machinery normally keeps the code available while objects from it exist. Bypassing that machinery and unloading a library while its C++ object remains is an efficient way to make a virtual call jump into empty space.

My preference is one obvious owner and no emergency deletes in destructors. If I cannot explain who owns the part without drawing several arrows, I fix that before debugging anything else. The crash at exit is rarely impressed by a beautiful toolbar.

A Small DCOP Command Is a Good Test

I needed to tell a running application to refresh a document, and my first instinct was to add a socket and invent a tiny protocol. Then I remembered that a KDE session already has DCOP, including discovery, calls, and argument marshalling. It seemed wasteful to build a worse version before lunch.

The dcop command is the quickest way to see what is available:

dcop
dcop konqueror
dcop konqueror KonquerorIface

The first command lists registered applications. Supplying an application lists its objects and interfaces; adding an object shows callable functions. This is useful beyond scripting. It tells me whether the object registered under the name I expected and whether the signature exported by the program matches my mental version of it.

For a simple application object, I derive from DCOPObject and name the interface:

class RefreshIface : virtual public DCOPObject
{
    K_DCOP
    k_dcop:
        virtual void refresh(const QString &path) = 0;
};

The implementation registers through the application’s DCOPClient. Calls are sent through the session’s DCOP server, which locates the target client and carries the serialized arguments. The caller does not need the target’s process identifier, and the receiver can expose a small interface rather than its internal objects.

I keep DCOP methods coarse. Sending refresh(QString) is reasonable; reproducing every setter on a widget is not. Once the interface mirrors the user interface, outside programs depend on details I will want to change.

There is also a choice between a call that waits for a reply and one that merely sends a message. I use a synchronous call only when the result is genuinely needed. Blocking while another desktop process opens a file or asks a question can make both programs feel frozen.

Application names need care as well. A second instance may register with a numbered name rather than replacing the first one. If the operation concerns a particular document, guessing the first process in the list is unsafe. I either arrange single-instance behavior deliberately or discover the intended application and object from information I already have. DCOP removes the need for process identifiers; it does not remove the need to choose the right process.

Failures should remain visible. A call can fail because the application exited, the object disappeared, or the signature changed. I report that to the caller instead of treating a missing reply as an empty successful result.

My rule now is to prove the exchange with the command-line client first. If dcop cannot see or call the object, another page of C++ is unlikely to improve matters. This is one of those rare debugging techniques that removes code, which makes it suspiciously pleasant.

Porting an Application to KDE 3

KDE 3.0 is out, which turned my experimental port into work that other people may actually try to compile. The application was modest, but it touched enough of KDE 2 and Qt 2 to make a straight rebuild unrealistic. I expected a grand rewrite. The successful approach was much duller: establish a clean baseline, move one boundary at a time, and keep the program runnable.

I started with a separate source copy and a fresh build directory. Building outside the source tree made stale generated files much easier to spot.

make -f Makefile.cvs
mkdir build-kde3
cd build-kde3
../configure --prefix="$KDEDIR" --enable-debug
make

The first pass was entirely mechanical. I fixed renamed headers, methods whose signatures changed, and code that depended on implicit conversions no longer accepted by Qt 3. I did not improve the design while doing this. Combining a port with a cleanup makes each failure ambiguous, and ambiguity is already supplied free of charge by C++ error messages.

Qt’s collection classes deserved special attention. Code that stored pointers in a QList-style class or relied on automatic deletion needed its ownership checked rather than merely adjusted until it compiled. Qt 3’s classes are familiar, but familiar is not identical. I wrote down who owned each object, especially widgets, list items, and objects parented to a QObject. Parent-child deletion is useful only when the parent is the intended owner.

Signals and slots were the next boundary. Every class using them needed Q_OBJECT, and every connection needed matching argument types. The string notation hides mistakes from the compiler:

connect(action, SIGNAL(activated()),
        this, SLOT(openDocument()));

I ran the application from a terminal and treated every QObject::connect warning as a real defect. It is tempting to ignore one because the window still appears. That tends to last until the ignored connection is the Save action.

The KDE layer was easier once Qt was quiet. I checked command-line handling through KCmdLineArgs, application setup through KApplication, actions, standard shortcuts, and XMLGUI resource files. An old .rc file can leave menus looking plausible while actions are missing or duplicated. Removing installed test copies before checking the new one prevented Konqueror and the application from finding an obsolete resource in another prefix.

Configuration code also needed a deliberate pass. I kept reads and writes together and checked defaults rather than assuming the old configuration file contained every key:

KConfig *config = KGlobal::config();
config->setGroup("View");
bool showStatus = config->readBoolEntry("ShowStatusBar", true);

For document handling, I resisted replacing everything at once. The application already separated its document widget reasonably well, so I first made that compile, then considered whether it should become a KPart. A port is a poor time to discover that the document, window, and network code have secretly been one object wearing three hats.

The most useful testing sequence was deliberately boring:

  1. Start with an empty configuration directory.
  2. Open a local file from the command line.
  3. Open it through the file dialog.
  4. Modify, save, and reopen it.
  5. Exercise every action and shortcut.
  6. Close with unsaved changes.
  7. Repeat with an existing KDE 2 configuration.

I also built with debugging enabled and ran under gdb. A port often exposes old lifetime errors because widget destruction order or an event sequence changes slightly. The new library did not necessarily create the dangling pointer; it merely stopped arranging the furniture around it.

Binary compatibility was not a goal for local plug-ins compiled against KDE 2. They had to be rebuilt against the KDE 3 and Qt 3 headers and libraries. Trying to preserve old C++ objects across that boundary would save a compilation and purchase a much stranger crash.

Before calling the port complete, I also built a source archive and compiled that archive in an empty directory. A working source tree can quietly depend on generated files, unlisted headers, or resources left by yesterday’s build. make distcheck is useful when the project supports it, but even the plain exercise of unpacking the archive elsewhere catches embarrassing omissions. I then installed into a temporary prefix and started a KDE session with that prefix in its search path. This tests what was shipped rather than what happens to be within reach of the compiler.

Warnings received their own pass after behavior was correct. I enabled the warnings supported by the older compiler and looked especially for suspicious casts, hidden overloads, and variables whose type changed under Qt 3. I did not turn every warning cleanup into part of the port, but I did investigate each one. A warning newly exposed by a library transition often points directly at an assumption that deserves daylight.

My preference after this exercise is to make the smallest source change that produces correct KDE 3 behavior, then do cleanup separately. The port remains reviewable, regressions have fewer hiding places, and users get a working application sooner. It is not heroic engineering, but heroic engineering has an alarming habit of requiring heroic debugging.