mirror of
https://github.com/m1ngsama/TUT.git
synced 2026-02-08 00:54:05 +00:00
test: Add comprehensive test suite for v2.0 release
- test_http_async: Async HTTP fetch, poll, and cancel tests - test_html_parse: HTML parsing, link resolution, forms, images, Unicode - test_bookmark: Add/remove/contains/persistence tests
This commit is contained in:
parent
e5276e0b4c
commit
a469f79a1e
4 changed files with 350 additions and 0 deletions
|
|
@ -129,3 +129,37 @@ target_link_libraries(tut
|
||||||
CURL::libcurl
|
CURL::libcurl
|
||||||
${GUMBO_LIBRARIES}
|
${GUMBO_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ==================== HTTP 异步测试程序 ====================
|
||||||
|
|
||||||
|
add_executable(test_http_async
|
||||||
|
src/http_client.cpp
|
||||||
|
tests/test_http_async.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(test_http_async
|
||||||
|
CURL::libcurl
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== HTML 解析测试程序 ====================
|
||||||
|
|
||||||
|
add_executable(test_html_parse
|
||||||
|
src/html_parser.cpp
|
||||||
|
src/dom_tree.cpp
|
||||||
|
tests/test_html_parse.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_directories(test_html_parse PRIVATE
|
||||||
|
${GUMBO_LIBRARY_DIRS}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(test_html_parse
|
||||||
|
${GUMBO_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== 书签测试程序 ====================
|
||||||
|
|
||||||
|
add_executable(test_bookmark
|
||||||
|
src/bookmark.cpp
|
||||||
|
tests/test_bookmark.cpp
|
||||||
|
)
|
||||||
|
|
|
||||||
103
tests/test_bookmark.cpp
Normal file
103
tests/test_bookmark.cpp
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
#include "bookmark.h"
|
||||||
|
#include <iostream>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::cout << "=== TUT 2.0 Bookmark Test ===" << std::endl;
|
||||||
|
|
||||||
|
// Note: Uses default path ~/.config/tut/bookmarks.json
|
||||||
|
// We'll test in-memory operations and clean up
|
||||||
|
|
||||||
|
tut::BookmarkManager manager;
|
||||||
|
|
||||||
|
// Store original count to restore later
|
||||||
|
size_t original_count = manager.count();
|
||||||
|
std::cout << " Original bookmark count: " << original_count << std::endl;
|
||||||
|
|
||||||
|
// Test 1: Add bookmarks
|
||||||
|
std::cout << "\n[Test 1] Add bookmarks..." << std::endl;
|
||||||
|
|
||||||
|
// Use unique URLs to avoid conflicts with existing bookmarks
|
||||||
|
std::string test_url1 = "https://test-example-12345.com";
|
||||||
|
std::string test_url2 = "https://test-google-12345.com";
|
||||||
|
std::string test_url3 = "https://test-github-12345.com";
|
||||||
|
|
||||||
|
bool added1 = manager.add(test_url1, "Test Example");
|
||||||
|
bool added2 = manager.add(test_url2, "Test Google");
|
||||||
|
bool added3 = manager.add(test_url3, "Test GitHub");
|
||||||
|
|
||||||
|
if (added1 && added2 && added3) {
|
||||||
|
std::cout << " ✓ Added 3 bookmarks" << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cout << " ✗ Failed to add bookmarks" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Duplicate detection
|
||||||
|
std::cout << "\n[Test 2] Duplicate detection..." << std::endl;
|
||||||
|
|
||||||
|
bool duplicate = manager.add(test_url1, "Duplicate");
|
||||||
|
if (!duplicate) {
|
||||||
|
std::cout << " ✓ Duplicate correctly rejected" << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cout << " ✗ Duplicate was incorrectly added" << std::endl;
|
||||||
|
// Clean up and fail
|
||||||
|
manager.remove(test_url1);
|
||||||
|
manager.remove(test_url2);
|
||||||
|
manager.remove(test_url3);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: Check existence
|
||||||
|
std::cout << "\n[Test 3] Check existence..." << std::endl;
|
||||||
|
|
||||||
|
if (manager.contains(test_url1) && !manager.contains("https://notexist-12345.com")) {
|
||||||
|
std::cout << " ✓ Existence check passed" << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cout << " ✗ Existence check failed" << std::endl;
|
||||||
|
manager.remove(test_url1);
|
||||||
|
manager.remove(test_url2);
|
||||||
|
manager.remove(test_url3);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 4: Count check
|
||||||
|
std::cout << "\n[Test 4] Count check..." << std::endl;
|
||||||
|
|
||||||
|
if (manager.count() == original_count + 3) {
|
||||||
|
std::cout << " ✓ Bookmark count correct: " << manager.count() << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cout << " ✗ Bookmark count incorrect" << std::endl;
|
||||||
|
manager.remove(test_url1);
|
||||||
|
manager.remove(test_url2);
|
||||||
|
manager.remove(test_url3);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 5: Remove bookmark
|
||||||
|
std::cout << "\n[Test 5] Remove bookmark..." << std::endl;
|
||||||
|
|
||||||
|
bool removed = manager.remove(test_url2);
|
||||||
|
if (removed && !manager.contains(test_url2) && manager.count() == original_count + 2) {
|
||||||
|
std::cout << " ✓ Bookmark removed successfully" << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cout << " ✗ Bookmark removal failed" << std::endl;
|
||||||
|
manager.remove(test_url1);
|
||||||
|
manager.remove(test_url3);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up test bookmarks
|
||||||
|
std::cout << "\n[Cleanup] Removing test bookmarks..." << std::endl;
|
||||||
|
manager.remove(test_url1);
|
||||||
|
manager.remove(test_url3);
|
||||||
|
|
||||||
|
if (manager.count() == original_count) {
|
||||||
|
std::cout << " ✓ Cleanup successful, restored to " << original_count << " bookmarks" << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cout << " ⚠ Cleanup may have issues" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "\n=== All bookmark tests passed! ===" << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
129
tests/test_html_parse.cpp
Normal file
129
tests/test_html_parse.cpp
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
#include "html_parser.h"
|
||||||
|
#include "dom_tree.h"
|
||||||
|
#include <iostream>
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::cout << "=== TUT 2.0 HTML Parser Test ===" << std::endl;
|
||||||
|
|
||||||
|
HtmlParser parser;
|
||||||
|
|
||||||
|
// Test 1: Basic HTML parsing
|
||||||
|
std::cout << "\n[Test 1] Basic HTML parsing..." << std::endl;
|
||||||
|
std::string html1 = R"(
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head><title>Test Page</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello World</h1>
|
||||||
|
<p>This is a <a href="https://example.com">link</a>.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)";
|
||||||
|
|
||||||
|
auto tree1 = parser.parse_tree(html1, "https://test.com");
|
||||||
|
std::cout << " ✓ Title: " << tree1.title << std::endl;
|
||||||
|
std::cout << " ✓ Links found: " << tree1.links.size() << std::endl;
|
||||||
|
|
||||||
|
if (tree1.title == "Test Page" && tree1.links.size() == 1) {
|
||||||
|
std::cout << " ✓ Basic parsing passed" << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cout << " ✗ Basic parsing failed" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Link URL resolution
|
||||||
|
std::cout << "\n[Test 2] Link URL resolution..." << std::endl;
|
||||||
|
std::string html2 = R"(
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<a href="/relative">Relative</a>
|
||||||
|
<a href="https://absolute.com">Absolute</a>
|
||||||
|
<a href="page.html">Same dir</a>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)";
|
||||||
|
|
||||||
|
auto tree2 = parser.parse_tree(html2, "https://base.com/dir/");
|
||||||
|
std::cout << " Found " << tree2.links.size() << " links:" << std::endl;
|
||||||
|
for (const auto& link : tree2.links) {
|
||||||
|
std::cout << " - " << link.url << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tree2.links.size() == 3) {
|
||||||
|
std::cout << " ✓ Link resolution passed" << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cout << " ✗ Link resolution failed" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: Form parsing
|
||||||
|
std::cout << "\n[Test 3] Form parsing..." << std::endl;
|
||||||
|
std::string html3 = R"(
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<form action="/submit" method="post">
|
||||||
|
<input type="text" name="username">
|
||||||
|
<input type="password" name="password">
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)";
|
||||||
|
|
||||||
|
auto tree3 = parser.parse_tree(html3, "https://form.com");
|
||||||
|
std::cout << " Form fields found: " << tree3.form_fields.size() << std::endl;
|
||||||
|
|
||||||
|
if (tree3.form_fields.size() >= 2) {
|
||||||
|
std::cout << " ✓ Form parsing passed" << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cout << " ✗ Form parsing failed" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 4: Image parsing
|
||||||
|
std::cout << "\n[Test 4] Image parsing..." << std::endl;
|
||||||
|
std::string html4 = R"(
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<img src="image1.png" alt="Image 1">
|
||||||
|
<img src="/images/image2.jpg" alt="Image 2">
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)";
|
||||||
|
|
||||||
|
auto tree4 = parser.parse_tree(html4, "https://images.com/page/");
|
||||||
|
std::cout << " Images found: " << tree4.images.size() << std::endl;
|
||||||
|
|
||||||
|
if (tree4.images.size() == 2) {
|
||||||
|
std::cout << " ✓ Image parsing passed" << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cout << " ✗ Image parsing failed" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 5: Unicode content
|
||||||
|
std::cout << "\n[Test 5] Unicode content..." << std::endl;
|
||||||
|
std::string html5 = R"(
|
||||||
|
<html>
|
||||||
|
<head><title>中文标题</title></head>
|
||||||
|
<body>
|
||||||
|
<h1>日本語テスト</h1>
|
||||||
|
<p>한국어 테스트</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)";
|
||||||
|
|
||||||
|
auto tree5 = parser.parse_tree(html5, "https://unicode.com");
|
||||||
|
std::cout << " ✓ Title: " << tree5.title << std::endl;
|
||||||
|
|
||||||
|
if (tree5.title == "中文标题") {
|
||||||
|
std::cout << " ✓ Unicode parsing passed" << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cout << " ✗ Unicode parsing failed" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "\n=== All HTML parser tests passed! ===" << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
84
tests/test_http_async.cpp
Normal file
84
tests/test_http_async.cpp
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
#include "http_client.h"
|
||||||
|
#include <iostream>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::cout << "=== TUT 2.0 HTTP Async Test ===" << std::endl;
|
||||||
|
|
||||||
|
HttpClient client;
|
||||||
|
|
||||||
|
// Test 1: Synchronous fetch
|
||||||
|
std::cout << "\n[Test 1] Synchronous fetch..." << std::endl;
|
||||||
|
auto response = client.fetch("https://example.com");
|
||||||
|
if (response.is_success()) {
|
||||||
|
std::cout << " ✓ Status: " << response.status_code << std::endl;
|
||||||
|
std::cout << " ✓ Content-Type: " << response.content_type << std::endl;
|
||||||
|
std::cout << " ✓ Body length: " << response.body.length() << " bytes" << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cout << " ✗ Failed: " << response.error_message << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: Asynchronous fetch
|
||||||
|
std::cout << "\n[Test 2] Asynchronous fetch..." << std::endl;
|
||||||
|
client.start_async_fetch("https://example.com");
|
||||||
|
|
||||||
|
int polls = 0;
|
||||||
|
auto start = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
auto state = client.poll_async();
|
||||||
|
polls++;
|
||||||
|
|
||||||
|
if (state == AsyncState::COMPLETE) {
|
||||||
|
auto end = std::chrono::steady_clock::now();
|
||||||
|
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
|
||||||
|
|
||||||
|
auto result = client.get_async_result();
|
||||||
|
std::cout << " ✓ Completed in " << ms << "ms after " << polls << " polls" << std::endl;
|
||||||
|
std::cout << " ✓ Status: " << result.status_code << std::endl;
|
||||||
|
std::cout << " ✓ Body length: " << result.body.length() << " bytes" << std::endl;
|
||||||
|
break;
|
||||||
|
} else if (state == AsyncState::FAILED) {
|
||||||
|
auto result = client.get_async_result();
|
||||||
|
std::cout << " ✗ Failed: " << result.error_message << std::endl;
|
||||||
|
return 1;
|
||||||
|
} else if (state == AsyncState::LOADING) {
|
||||||
|
// Non-blocking poll
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
} else {
|
||||||
|
std::cout << " ✗ Unexpected state" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (polls > 1000) {
|
||||||
|
std::cout << " ✗ Timeout" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 3: Cancel async
|
||||||
|
std::cout << "\n[Test 3] Cancel async..." << std::endl;
|
||||||
|
client.start_async_fetch("https://httpbin.org/delay/10");
|
||||||
|
|
||||||
|
// Poll a few times then cancel
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
client.poll_async();
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
client.cancel_async();
|
||||||
|
std::cout << " ✓ Request cancelled" << std::endl;
|
||||||
|
|
||||||
|
// Verify state is CANCELLED or IDLE
|
||||||
|
if (!client.is_async_active()) {
|
||||||
|
std::cout << " ✓ No active request after cancel" << std::endl;
|
||||||
|
} else {
|
||||||
|
std::cout << " ✗ Request still active after cancel" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "\n=== All tests passed! ===" << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue