From a469f79a1e0d12f9d8de658a6161d8324e80cd4a Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Sat, 27 Dec 2025 16:30:05 +0800 Subject: [PATCH] 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 --- CMakeLists.txt | 34 ++++++++++ tests/test_bookmark.cpp | 103 ++++++++++++++++++++++++++++++ tests/test_html_parse.cpp | 129 ++++++++++++++++++++++++++++++++++++++ tests/test_http_async.cpp | 84 +++++++++++++++++++++++++ 4 files changed, 350 insertions(+) create mode 100644 tests/test_bookmark.cpp create mode 100644 tests/test_html_parse.cpp create mode 100644 tests/test_http_async.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index bd6ddcb..56291c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -129,3 +129,37 @@ target_link_libraries(tut CURL::libcurl ${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 +) diff --git a/tests/test_bookmark.cpp b/tests/test_bookmark.cpp new file mode 100644 index 0000000..1cb05ba --- /dev/null +++ b/tests/test_bookmark.cpp @@ -0,0 +1,103 @@ +#include "bookmark.h" +#include +#include + +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; +} diff --git a/tests/test_html_parse.cpp b/tests/test_html_parse.cpp new file mode 100644 index 0000000..ed7d6f1 --- /dev/null +++ b/tests/test_html_parse.cpp @@ -0,0 +1,129 @@ +#include "html_parser.h" +#include "dom_tree.h" +#include +#include + +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"( + + + Test Page + +

Hello World

+

This is a link.

+ + + )"; + + 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"( + + + Relative + Absolute + Same dir + + + )"; + + 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"( + + +
+ + + +
+ + + )"; + + 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"( + + + Image 1 + Image 2 + + + )"; + + 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"( + + 中文标题 + +

日本語テスト

+

한국어 테스트

+ + + )"; + + 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; +} diff --git a/tests/test_http_async.cpp b/tests/test_http_async.cpp new file mode 100644 index 0000000..37de242 --- /dev/null +++ b/tests/test_http_async.cpp @@ -0,0 +1,84 @@ +#include "http_client.h" +#include +#include +#include + +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(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; +}