Renderer telah berfungsi untuk kasus ideal — linked list dengan empat elemen, binary tree lengkap, graph kecil. Pada bagian ini kita menguji kasus edge yang sering luput dari rancangan awal: struktur kosong, tree miring (skewed tree), graph dengan cycle, dan node tunggal tanpa anak. Tujuan pengujian bukan sekadar membuktikan program berjalan, melainkan menyingkap batasan renderer agar pengguna tahu kapan harus berhati-hati.
Skenario Pengujian #
Kita akan menjalankan enam skenario berikut pada program Bagian 3 (dengan satu penyesuaian: tree mungkin saja skewed):
| # | Struktur | Kasus yang Diuji |
|---|---|---|
| 1 | Linked list kosong | Renderer menangani values kosong |
| 2 | Linked list satu elemen | Tidak ada -> antar elemen |
| 3 | Binary tree kosong | Renderer menangani root == nullptr |
| 4 | Binary tree node tunggal | Hanya root tanpa anak |
| 5 | Binary tree skewed kiri | Setiap node hanya punya anak kiri |
| 6 | Graph dengan cycle | Cycle 0→1→2→0 tetap tercetak |
Penyesuaian Kode untuk Edge Case #
Pada Bagian 3, renderer linked list dan tree sudah menangani kasus kosong secara implisit (loop tidak dieksekusi untuk values kosong; renderTree mengembalikan "<empty tree>" bila root == nullptr). Satu hal yang perlu ditambahkan: linked list kosong sebaiknya mencetak null sebagai penanda, bukan string kosong.
Berikut fungsi renderList yang diperbarui:
std::vector<std::string> renderList(const std::vector<std::string>& values) {
if (values.empty()) return { "null" };
std::string line;
for (size_t i = 0; i < values.size(); ++i) {
line += "[" + values[i] + "]";
line += (i + 1 == values.size()) ? " -> null" : " -> ";
}
return { line };
}Program Pengujian Lengkap #
Berikut program yang membangun keenam kasus uji dan mencetak hasilnya. Kita pakai kembali renderer dari Bagian 3.
#include <algorithm>
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
struct TreeNode {
std::string label;
std::unique_ptr<TreeNode> left;
std::unique_ptr<TreeNode> right;
explicit TreeNode(std::string l) : label(std::move(l)) {}
};
using NodePtr = std::unique_ptr<TreeNode>;
NodePtr node(std::string label, NodePtr l = nullptr, NodePtr r = nullptr) {
auto n = std::make_unique<TreeNode>(std::move(label));
n->left = std::move(l);
n->right = std::move(r);
return n;
}
int treeHeight(const TreeNode* p) {
if (!p) return 0;
return 1 + std::max(treeHeight(p->left.get()), treeHeight(p->right.get()));
}
void assignColumns(const TreeNode* p, int& col,
std::vector<std::pair<const TreeNode*, int>>& positions) {
if (!p) return;
assignColumns(p->left.get(), col, positions);
positions.push_back({p, col++});
assignColumns(p->right.get(), col, positions);
}
void drawNode(const TreeNode* p, int depth, int nodeCol,
std::vector<std::pair<const TreeNode*, int>>& positions,
std::vector<std::string>& grid, int colUnit) {
if (!p) return;
int row = depth * 2;
int col = nodeCol * colUnit;
std::string lbl = "[" + p->label + "]";
for (size_t i = 0; i < lbl.size(); ++i) grid[row][col + i] = lbl[i];
if (p->left) {
int lc = -1;
for (auto& pr : positions) if (pr.first == p->left.get()) { lc = pr.second; break; }
int slashCol = (col + lc * colUnit) / 2;
if (slashCol >= 0) grid[row + 1][slashCol] = '/';
drawNode(p->left.get(), depth + 1, lc, positions, grid, colUnit);
}
if (p->right) {
int rc = -1;
for (auto& pr : positions) if (pr.first == p->right.get()) { rc = pr.second; break; }
int slashCol = (col + rc * colUnit) / 2;
if (slashCol < (int)grid[row + 1].size()) grid[row + 1][slashCol] = '\\';
drawNode(p->right.get(), depth + 1, rc, positions, grid, colUnit);
}
}
std::vector<std::string> renderTree(const TreeNode* root) {
if (!root) return { "<empty tree>" };
int col = 0;
std::vector<std::pair<const TreeNode*, int>> positions;
assignColumns(root, col, positions);
int nNodes = (int)positions.size();
int colUnit = 4;
int width = std::max(1, nNodes * colUnit);
int h = treeHeight(root);
int rows = std::max(1, 2 * h - 1);
std::vector<std::string> grid(rows, std::string(width, ' '));
int rootCol = 0;
for (auto& pr : positions) if (pr.first == root) { rootCol = pr.second; break; }
drawNode(root, 0, rootCol, positions, grid, colUnit);
for (auto& line : grid) {
while (!line.empty() && line.back() == ' ') line.pop_back();
}
return grid;
}
std::vector<std::string> renderList(const std::vector<std::string>& values) {
if (values.empty()) return { "null" };
std::string line;
for (size_t i = 0; i < values.size(); ++i) {
line += "[" + values[i] + "]";
line += (i + 1 == values.size()) ? " -> null" : " -> ";
}
return { line };
}
std::vector<std::string> renderGraph(
const std::unordered_map<int, std::vector<int>>& adj, int n) {
std::vector<std::string> lines;
for (int u = 0; u < n; ++u) {
std::string line = std::to_string(u) + " -> ";
const auto& nbrs = adj.at(u);
for (size_t i = 0; i < nbrs.size(); ++i) {
line += std::to_string(nbrs[i]);
line += (i + 1 == nbrs.size()) ? "" : ", ";
}
lines.push_back(line);
}
return lines;
}
void printLines(const std::vector<std::string>& lines) {
for (const auto& l : lines) std::cout << l << "\n";
}
int main() {
std::cout << "=== Test 1: Linked list kosong ===\n";
printLines(renderList({}));
std::cout << "\n=== Test 2: Linked list satu elemen ===\n";
printLines(renderList({"a"}));
std::cout << "\n=== Test 3: Binary tree kosong ===\n";
printLines(renderTree(nullptr));
std::cout << "\n=== Test 4: Binary tree node tunggal ===\n";
auto single = node("X");
printLines(renderTree(single.get()));
std::cout << "\n=== Test 5: Binary tree skewed kiri ===\n";
auto skewed = node("1", node("2", node("3", node("4"))), nullptr);
printLines(renderTree(skewed.get()));
std::cout << "\n=== Test 6: Graph dengan cycle ===\n";
std::unordered_map<int, std::vector<int>> adj;
adj[0] = {1};
adj[1] = {2};
adj[2] = {0};
printLines(renderGraph(adj, 3));
return 0;
}Output yang Dihasilkan:
=== Test 1: Linked list kosong ===
null
=== Test 2: Linked list satu elemen ===
[a] -> null
=== Test 3: Binary tree kosong ===
<empty tree>
=== Test 4: Binary tree node tunggal ===
[X]
=== Test 5: Binary tree skewed kiri ===
[1]
/
[2]
/
[3]
/
[4]
=== Test 6: Graph dengan cycle ===
0 -> 1
1 -> 2
2 -> 0Bedah Hasil Pengujian #
- Test 1 — Linked list kosong: renderer kini mengembalikan
"null"alih-alih string kosong. Tanpa penanganan ini, output berupa baris kosong yang membingungkan pembaca. - Test 2 — Satu elemen: loop berjalan satu kali, suffix
-> nulldiappend dengan benar. Loop dengani + 1 == values.size()menjaditruesehingga tidak ada->di tengah. - Test 3 — Tree kosong: guard
if (!root) return { "<empty tree>" }aktif. Tanpa guard,assignColumnsmenerima pointer null dan dereferensip->leftmenyebabkan segfault. - Test 4 — Node tunggal:
treeHeightmengembalikan 1,rows = 1, hanya root yang ditulis. Edge tidak ada karena tidak ada anak. Output berupa[X]saja. - Test 5 — Skewed kiri: setiap node hanya punya anak kiri. In-order memberi posisi
3, 2, 1, 0(anak terdalam dapat kolom 0, root dapat kolom 3). Hasilnya tree vertikal yang miring ke kiri — visualisasi tetap terbaca meskipun tidak ideal. - Test 6 — Graph cycle: cycle 0→1→2→0 tercetak sebagai tiga baris adjacency list. Renderer graph tidak terpengaruh oleh cycle karena hanya iterasi
adj[u]tanpa traversal DFS.
Ringkasan Proyek #
Kita telah membangun tool visualisasi struktur data lengkap dengan empat tahap: rancangan, model, renderer, dan pengujian. Arsitektur model-view yang dipisahkan sejak Bagian 1 memudahkan penambahan struktur baru di masa depan — misalnya AVL tree atau heap — tanpa mengubah renderer secara besar. Batasan utama yang kita akui: label node dibatasi 1 karakter, tree skewed menghasilkan output vertikal yang panjang, dan graph dirender sebagai adjacency list alih-alih diagram 2D.
Kompilasi dan Pengujian #
g++ -std=c++17 -O2 -Wall -Wextra -o test.bin test.cpp
./test.binPastikan keenam test sesuai dengan output di atas. Bila ada perbedaan, periksa kembali penanganan edge case pada renderList dan renderTree.