From b179194753e15e1dfab0b8f9352a67069aeb7d27 Mon Sep 17 00:00:00 2001 From: q Date: Sun, 11 Jan 2026 03:19:31 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DGraalPy=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E5=B9=B6=E6=B7=BB=E5=8A=A0Context=E6=B1=A0=E5=8C=96?= =?UTF-8?q?=E5=92=8C=E5=AE=8C=E6=95=B4=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复parser pom.xml中GraalPy依赖配置 - 修复web-front Playground.vue中Tab选中异常bug - 添加PyContextPool实现Context池化管理 - 更新PyPlaygroundExecutor和PyParserExecutor使用池化 - 创建PyParserTest完整单元测试 - 创建PyHttpClientTest HTTP客户端测试 - 创建PyCryptoUtilsTest加密工具测试 - 修复所有ShareLinkInfo构造相关错误 --- parser/pom.xml | 9 +- .../qaiu/parser/custompy/PyContextPool.java | 459 ++++++++++++++ .../parser/custompy/PyParserExecutor.java | 53 +- .../parser/custompy/PyPlaygroundExecutor.java | 72 +-- .../cn/qaiu/parser/PyCryptoUtilsTest.java | 439 ++++++++++++++ .../java/cn/qaiu/parser/PyHttpClientTest.java | 515 ++++++++++++++++ .../java/cn/qaiu/parser/PyParserTest.java | 558 ++++++++++++++++++ web-front/src/views/Playground.vue | 127 ++-- 8 files changed, 2066 insertions(+), 166 deletions(-) create mode 100644 parser/src/main/java/cn/qaiu/parser/custompy/PyContextPool.java create mode 100644 parser/src/test/java/cn/qaiu/parser/PyCryptoUtilsTest.java create mode 100644 parser/src/test/java/cn/qaiu/parser/PyHttpClientTest.java create mode 100644 parser/src/test/java/cn/qaiu/parser/PyParserTest.java diff --git a/parser/pom.xml b/parser/pom.xml index bc9b0f8..ffbb628 100644 --- a/parser/pom.xml +++ b/parser/pom.xml @@ -114,10 +114,15 @@ ${graalpy.version} - org.graalvm.polyglot + org.graalvm.python python ${graalpy.version} - pom + runtime + + + org.graalvm.python + python-embedding + ${graalpy.version} diff --git a/parser/src/main/java/cn/qaiu/parser/custompy/PyContextPool.java b/parser/src/main/java/cn/qaiu/parser/custompy/PyContextPool.java new file mode 100644 index 0000000..e1bf7fa --- /dev/null +++ b/parser/src/main/java/cn/qaiu/parser/custompy/PyContextPool.java @@ -0,0 +1,459 @@ +package cn.qaiu.parser.custompy; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Engine; +import org.graalvm.polyglot.HostAccess; +import org.graalvm.polyglot.io.IOAccess; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * GraalPy Context 池化管理器 + * 提供共享的 Engine 实例和 Context 池化支持 + * + *

特性: + *

+ * + * @author QAIU + */ +public class PyContextPool { + + private static final Logger log = LoggerFactory.getLogger(PyContextPool.class); + + // 池化配置 + private static final int INITIAL_POOL_SIZE = 2; + private static final int MAX_POOL_SIZE = 10; + private static final long CONTEXT_TIMEOUT_MS = 30000; // 30秒获取超时 + private static final long CONTEXT_MAX_AGE_MS = 300000; // 5分钟最大使用时间 + + // 单例实例 + private static volatile PyContextPool instance; + private static final Object LOCK = new Object(); + + // 共享的GraalPy引擎 + private final Engine sharedEngine; + + // Context 池 + private final BlockingQueue contextPool; + + // 已创建的Context数量 + private final AtomicInteger createdCount = new AtomicInteger(0); + + // 是否已关闭 + private final AtomicBoolean closed = new AtomicBoolean(false); + + // 定期清理过期Context的调度器 + private final ScheduledExecutorService cleanupScheduler; + + // Python执行专用线程池 + private final ExecutorService pythonExecutor; + + // 超时调度器 + private final ScheduledExecutorService timeoutScheduler; + + /** + * 池化的Context包装器 + */ + public static class PooledContext implements AutoCloseable { + private final Context context; + private final long createdTime; + private final PyContextPool pool; + private volatile boolean inUse = false; + private volatile long lastUsedTime; + + private PooledContext(Context context, PyContextPool pool) { + this.context = context; + this.pool = pool; + this.createdTime = System.currentTimeMillis(); + this.lastUsedTime = createdTime; + } + + /** + * 获取底层Context + */ + public Context getContext() { + return context; + } + + /** + * 检查是否过期 + */ + public boolean isExpired() { + return System.currentTimeMillis() - createdTime > CONTEXT_MAX_AGE_MS; + } + + /** + * 归还到池中或关闭 + */ + @Override + public void close() { + pool.release(this); + } + + /** + * 强制关闭Context + */ + void forceClose() { + try { + context.close(true); + } catch (Exception e) { + log.warn("关闭Context失败: {}", e.getMessage()); + } + } + + /** + * 重置Context状态(清除绑定等) + */ + boolean reset() { + try { + // 由于GraalPy的Context不能很好地重置状态, + // 简单场景下我们选择创建新的Context + // 但对于短生命周期的执行,可以尝试继续使用 + lastUsedTime = System.currentTimeMillis(); + return !isExpired(); + } catch (Exception e) { + log.warn("重置Context失败: {}", e.getMessage()); + return false; + } + } + } + + /** + * 私有构造函数 + */ + private PyContextPool() { + log.info("初始化GraalPy Context池..."); + + // 创建共享Engine + this.sharedEngine = Engine.newBuilder() + .option("engine.WarnInterpreterOnly", "false") + .build(); + + // 创建Context池 + this.contextPool = new LinkedBlockingQueue<>(MAX_POOL_SIZE); + + // 创建Python执行专用线程池 + this.pythonExecutor = Executors.newCachedThreadPool(r -> { + Thread thread = new Thread(r); + thread.setName("py-context-pool-worker-" + System.currentTimeMillis()); + thread.setDaemon(true); + return thread; + }); + + // 创建超时调度器 + this.timeoutScheduler = Executors.newScheduledThreadPool(2, r -> { + Thread thread = new Thread(r); + thread.setName("py-context-timeout-" + System.currentTimeMillis()); + thread.setDaemon(true); + return thread; + }); + + // 创建清理调度器 + this.cleanupScheduler = Executors.newSingleThreadScheduledExecutor(r -> { + Thread thread = new Thread(r); + thread.setName("py-context-cleanup"); + thread.setDaemon(true); + return thread; + }); + + // 预热:初始化一些Context + warmup(); + + // 定期清理过期的Context + cleanupScheduler.scheduleWithFixedDelay(this::cleanup, 60, 60, TimeUnit.SECONDS); + + log.info("GraalPy Context池初始化完成,初始大小: {}", INITIAL_POOL_SIZE); + } + + /** + * 获取单例实例 + */ + public static PyContextPool getInstance() { + if (instance == null) { + synchronized (LOCK) { + if (instance == null) { + instance = new PyContextPool(); + } + } + } + return instance; + } + + /** + * 获取共享Engine + */ + public Engine getSharedEngine() { + return sharedEngine; + } + + /** + * 获取Python执行线程池 + */ + public ExecutorService getPythonExecutor() { + return pythonExecutor; + } + + /** + * 获取超时调度器 + */ + public ScheduledExecutorService getTimeoutScheduler() { + return timeoutScheduler; + } + + /** + * 预热Context池 + */ + private void warmup() { + for (int i = 0; i < INITIAL_POOL_SIZE; i++) { + try { + PooledContext pc = createPooledContext(); + if (!contextPool.offer(pc)) { + pc.forceClose(); + } + } catch (Exception e) { + log.warn("预热Context失败: {}", e.getMessage()); + } + } + } + + /** + * 创建新的池化Context + */ + private PooledContext createPooledContext() { + if (closed.get()) { + throw new IllegalStateException("Context池已关闭"); + } + + Context context = Context.newBuilder("python") + .engine(sharedEngine) + .allowHostAccess(HostAccess.newBuilder(HostAccess.EXPLICIT) + .allowArrayAccess(true) + .allowListAccess(true) + .allowMapAccess(true) + .allowIterableAccess(true) + .allowIteratorAccess(true) + .build()) + .allowHostClassLookup(className -> false) + .allowExperimentalOptions(true) + .allowCreateThread(true) + .allowNativeAccess(false) + .allowCreateProcess(false) + .allowIO(IOAccess.newBuilder() + .allowHostFileAccess(false) + .allowHostSocketAccess(false) + .build()) + .option("python.PythonHome", "") + .option("python.ForceImportSite", "false") + .build(); + + createdCount.incrementAndGet(); + log.debug("创建新的GraalPy Context,当前总数: {}", createdCount.get()); + + return new PooledContext(context, this); + } + + /** + * 从池中获取Context + * + * @return 池化的Context,用完后需要调用close()归还 + * @throws InterruptedException 如果等待被中断 + * @throws TimeoutException 如果超时未获取到 + */ + public PooledContext acquire() throws InterruptedException, TimeoutException { + if (closed.get()) { + throw new IllegalStateException("Context池已关闭"); + } + + // 尝试从池中获取 + PooledContext pc = contextPool.poll(); + + if (pc != null) { + if (!pc.isExpired() && pc.reset()) { + pc.inUse = true; + log.debug("从池中获取Context,池剩余: {}", contextPool.size()); + return pc; + } else { + // Context已过期,关闭它 + pc.forceClose(); + createdCount.decrementAndGet(); + } + } + + // 池中没有可用的,检查是否可以创建新的 + if (createdCount.get() < MAX_POOL_SIZE) { + try { + pc = createPooledContext(); + pc.inUse = true; + return pc; + } catch (Exception e) { + log.error("创建新Context失败: {}", e.getMessage()); + throw new RuntimeException("无法创建GraalPy Context", e); + } + } + + // 已达最大数量,等待归还 + pc = contextPool.poll(CONTEXT_TIMEOUT_MS, TimeUnit.MILLISECONDS); + if (pc == null) { + throw new TimeoutException("获取GraalPy Context超时"); + } + + if (!pc.isExpired() && pc.reset()) { + pc.inUse = true; + return pc; + } else { + pc.forceClose(); + createdCount.decrementAndGet(); + // 递归重试 + return acquire(); + } + } + + /** + * 创建一个新的非池化Context(用于需要独立生命周期的场景) + * 调用者负责管理其生命周期 + */ + public Context createFreshContext() { + return Context.newBuilder("python") + .engine(sharedEngine) + .allowHostAccess(HostAccess.newBuilder(HostAccess.EXPLICIT) + .allowArrayAccess(true) + .allowListAccess(true) + .allowMapAccess(true) + .allowIterableAccess(true) + .allowIteratorAccess(true) + .build()) + .allowHostClassLookup(className -> false) + .allowExperimentalOptions(true) + .allowCreateThread(true) + .allowNativeAccess(false) + .allowCreateProcess(false) + .allowIO(IOAccess.newBuilder() + .allowHostFileAccess(false) + .allowHostSocketAccess(false) + .build()) + .option("python.PythonHome", "") + .option("python.ForceImportSite", "false") + .build(); + } + + /** + * 归还Context到池中 + */ + private void release(PooledContext pc) { + if (pc == null) return; + + pc.inUse = false; + + if (closed.get() || pc.isExpired()) { + // 池已关闭或Context已过期,直接销毁 + pc.forceClose(); + createdCount.decrementAndGet(); + log.debug("Context已过期或池已关闭,销毁Context"); + } else if (!contextPool.offer(pc)) { + // 池已满,销毁Context + pc.forceClose(); + createdCount.decrementAndGet(); + log.debug("池已满,销毁多余Context"); + } else { + log.debug("归还Context到池,池当前大小: {}", contextPool.size()); + } + } + + /** + * 清理过期的Context + */ + private void cleanup() { + if (closed.get()) return; + + int removed = 0; + PooledContext pc; + + while ((pc = contextPool.poll()) != null) { + if (pc.isExpired() || closed.get()) { + pc.forceClose(); + createdCount.decrementAndGet(); + removed++; + } else { + // 还没过期,放回池中 + if (!contextPool.offer(pc)) { + pc.forceClose(); + createdCount.decrementAndGet(); + removed++; + } + break; + } + } + + if (removed > 0) { + log.info("清理了 {} 个过期的Context,当前池大小: {}", removed, contextPool.size()); + } + } + + /** + * 获取池状态信息 + */ + public String getStatus() { + return String.format("PyContextPool[total=%d, available=%d, maxSize=%d]", + createdCount.get(), contextPool.size(), MAX_POOL_SIZE); + } + + /** + * 获取池中可用的Context数量 + */ + public int getAvailableCount() { + return contextPool.size(); + } + + /** + * 获取已创建的Context总数 + */ + public int getCreatedCount() { + return createdCount.get(); + } + + /** + * 关闭Context池 + */ + public void shutdown() { + if (closed.compareAndSet(false, true)) { + log.info("关闭GraalPy Context池..."); + + // 停止清理调度器 + cleanupScheduler.shutdownNow(); + timeoutScheduler.shutdownNow(); + pythonExecutor.shutdownNow(); + + // 关闭所有池中的Context + PooledContext pc; + while ((pc = contextPool.poll()) != null) { + pc.forceClose(); + } + + // 关闭共享Engine + try { + sharedEngine.close(true); + } catch (Exception e) { + log.warn("关闭共享Engine失败: {}", e.getMessage()); + } + + log.info("GraalPy Context池已关闭"); + } + } + + /** + * 检查池是否已关闭 + */ + public boolean isClosed() { + return closed.get(); + } +} diff --git a/parser/src/main/java/cn/qaiu/parser/custompy/PyParserExecutor.java b/parser/src/main/java/cn/qaiu/parser/custompy/PyParserExecutor.java index 8d1ae89..3997484 100644 --- a/parser/src/main/java/cn/qaiu/parser/custompy/PyParserExecutor.java +++ b/parser/src/main/java/cn/qaiu/parser/custompy/PyParserExecutor.java @@ -9,21 +9,18 @@ import io.vertx.core.Future; import io.vertx.core.WorkerExecutor; import io.vertx.core.json.JsonObject; import org.graalvm.polyglot.Context; -import org.graalvm.polyglot.Engine; -import org.graalvm.polyglot.HostAccess; import org.graalvm.polyglot.Value; -import org.graalvm.polyglot.io.IOAccess; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; -import java.util.Map; /** * Python解析器执行器 * 使用GraalPy执行Python解析器脚本 * 实现IPanTool接口,执行Python解析器逻辑 + * 使用 PyContextPool 进行 Engine 池化管理 * * @author QAIU */ @@ -34,10 +31,8 @@ public class PyParserExecutor implements IPanTool { private static final WorkerExecutor EXECUTOR = WebClientVertxInit.get() .createSharedWorkerExecutor("py-parser-executor", 32); - // 共享的GraalPy引擎,提高性能 - private static final Engine SHARED_ENGINE = Engine.newBuilder() - .option("engine.WarnInterpreterOnly", "false") - .build(); + // Context池实例 + private static final PyContextPool CONTEXT_POOL = PyContextPool.getInstance(); private final CustomParserConfig config; private final ShareLinkInfo shareLinkInfo; @@ -71,48 +66,12 @@ public class PyParserExecutor implements IPanTool { return shareLinkInfo; } - /** - * 创建安全的GraalPy Context - * 配置沙箱选项,禁用危险功能 - */ - private Context createContext() { - return Context.newBuilder("python") - .engine(SHARED_ENGINE) - // 允许访问带有@HostAccess.Export注解的Java方法 - .allowHostAccess(HostAccess.newBuilder(HostAccess.EXPLICIT) - .allowArrayAccess(true) - .allowListAccess(true) - .allowMapAccess(true) - .allowIterableAccess(true) - .allowIteratorAccess(true) - .build()) - // 禁止访问任意Java类 - .allowHostClassLookup(className -> false) - // 允许实验性选项 - .allowExperimentalOptions(true) - // 允许创建线程(某些Python库需要) - .allowCreateThread(true) - // 禁用原生访问 - .allowNativeAccess(false) - // 禁止创建子进程 - .allowCreateProcess(false) - // 允许虚拟文件系统访问(用于import等) - .allowIO(IOAccess.newBuilder() - .allowHostFileAccess(false) - .allowHostSocketAccess(false) - .build()) - // GraalPy特定选项 - .option("python.PythonHome", "") - .option("python.ForceImportSite", "false") - .build(); - } - @Override public Future parse() { pyLogger.info("开始执行Python解析器: {}", config.getType()); return EXECUTOR.executeBlocking(() -> { - try (Context context = createContext()) { + try (Context context = CONTEXT_POOL.createFreshContext()) { // 注入Java对象到Python环境 Value bindings = context.getBindings("python"); bindings.putMember("http", httpClient); @@ -152,7 +111,7 @@ public class PyParserExecutor implements IPanTool { pyLogger.info("开始执行Python文件列表解析: {}", config.getType()); return EXECUTOR.executeBlocking(() -> { - try (Context context = createContext()) { + try (Context context = CONTEXT_POOL.createFreshContext()) { // 注入Java对象到Python环境 Value bindings = context.getBindings("python"); bindings.putMember("http", httpClient); @@ -186,7 +145,7 @@ public class PyParserExecutor implements IPanTool { pyLogger.info("开始执行Python按ID解析: {}", config.getType()); return EXECUTOR.executeBlocking(() -> { - try (Context context = createContext()) { + try (Context context = CONTEXT_POOL.createFreshContext()) { // 注入Java对象到Python环境 Value bindings = context.getBindings("python"); bindings.putMember("http", httpClient); diff --git a/parser/src/main/java/cn/qaiu/parser/custompy/PyPlaygroundExecutor.java b/parser/src/main/java/cn/qaiu/parser/custompy/PyPlaygroundExecutor.java index 98cbe3f..4b46d6b 100644 --- a/parser/src/main/java/cn/qaiu/parser/custompy/PyPlaygroundExecutor.java +++ b/parser/src/main/java/cn/qaiu/parser/custompy/PyPlaygroundExecutor.java @@ -6,10 +6,7 @@ import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.json.JsonObject; import org.graalvm.polyglot.Context; -import org.graalvm.polyglot.Engine; -import org.graalvm.polyglot.HostAccess; import org.graalvm.polyglot.Value; -import org.graalvm.polyglot.io.IOAccess; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,6 +18,7 @@ import java.util.concurrent.*; * Python演练场执行器 * 用于临时执行Python代码,不注册到解析器注册表 * 使用独立线程池避免Vert.x BlockedThreadChecker警告 + * 使用 PyContextPool 进行 Engine 和 Context 池化管理 * * @author QAIU */ @@ -31,26 +29,8 @@ public class PyPlaygroundExecutor { // Python执行超时时间(秒) private static final long EXECUTION_TIMEOUT_SECONDS = 30; - // 共享的GraalPy引擎 - private static final Engine SHARED_ENGINE = Engine.newBuilder() - .option("engine.WarnInterpreterOnly", "false") - .build(); - - // 使用独立的线程池 - private static final ExecutorService INDEPENDENT_EXECUTOR = Executors.newCachedThreadPool(r -> { - Thread thread = new Thread(r); - thread.setName("py-playground-independent-" + System.currentTimeMillis()); - thread.setDaemon(true); - return thread; - }); - - // 超时调度线程池 - private static final ScheduledExecutorService TIMEOUT_SCHEDULER = Executors.newScheduledThreadPool(2, r -> { - Thread thread = new Thread(r); - thread.setName("py-playground-timeout-scheduler-" + System.currentTimeMillis()); - thread.setDaemon(true); - return thread; - }); + // Context池实例 + private static final PyContextPool CONTEXT_POOL = PyContextPool.getInstance(); private final ShareLinkInfo shareLinkInfo; private final String pyCode; @@ -81,33 +61,6 @@ public class PyPlaygroundExecutor { this.cryptoUtils = new PyCryptoUtils(); } - /** - * 创建安全的GraalPy Context - */ - private Context createContext() { - return Context.newBuilder("python") - .engine(SHARED_ENGINE) - .allowHostAccess(HostAccess.newBuilder(HostAccess.EXPLICIT) - .allowArrayAccess(true) - .allowListAccess(true) - .allowMapAccess(true) - .allowIterableAccess(true) - .allowIteratorAccess(true) - .build()) - .allowHostClassLookup(className -> false) - .allowExperimentalOptions(true) - .allowCreateThread(true) - .allowNativeAccess(false) - .allowCreateProcess(false) - .allowIO(IOAccess.newBuilder() - .allowHostFileAccess(false) - .allowHostSocketAccess(false) - .build()) - .option("python.PythonHome", "") - .option("python.ForceImportSite", "false") - .build(); - } - /** * 执行parse方法(异步,带超时控制) */ @@ -117,7 +70,8 @@ public class PyPlaygroundExecutor { CompletableFuture executionFuture = CompletableFuture.supplyAsync(() -> { playgroundLogger.infoJava("开始执行parse方法"); - try (Context context = createContext()) { + // 使用池化的Context(每次执行创建新的Context以保证状态隔离) + try (Context context = CONTEXT_POOL.createFreshContext()) { // 注入Java对象到Python环境 Value bindings = context.getBindings("python"); bindings.putMember("http", httpClient); @@ -153,10 +107,10 @@ public class PyPlaygroundExecutor { playgroundLogger.errorJava("执行parse方法失败: " + e.getMessage(), e); throw new RuntimeException(e); } - }, INDEPENDENT_EXECUTOR); + }, CONTEXT_POOL.getPythonExecutor()); // 创建超时任务 - ScheduledFuture timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> { + ScheduledFuture timeoutTask = CONTEXT_POOL.getTimeoutScheduler().schedule(() -> { if (!executionFuture.isDone()) { executionFuture.cancel(true); playgroundLogger.errorJava("执行超时,已强制中断"); @@ -195,7 +149,7 @@ public class PyPlaygroundExecutor { CompletableFuture> executionFuture = CompletableFuture.supplyAsync(() -> { playgroundLogger.infoJava("开始执行parse_file_list方法"); - try (Context context = createContext()) { + try (Context context = CONTEXT_POOL.createFreshContext()) { Value bindings = context.getBindings("python"); bindings.putMember("http", httpClient); bindings.putMember("logger", playgroundLogger); @@ -220,9 +174,9 @@ public class PyPlaygroundExecutor { playgroundLogger.errorJava("执行parse_file_list方法失败: " + e.getMessage(), e); throw new RuntimeException(e); } - }, INDEPENDENT_EXECUTOR); + }, CONTEXT_POOL.getPythonExecutor()); - ScheduledFuture timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> { + ScheduledFuture timeoutTask = CONTEXT_POOL.getTimeoutScheduler().schedule(() -> { if (!executionFuture.isDone()) { executionFuture.cancel(true); playgroundLogger.errorJava("执行超时,已强制中断"); @@ -257,7 +211,7 @@ public class PyPlaygroundExecutor { CompletableFuture executionFuture = CompletableFuture.supplyAsync(() -> { playgroundLogger.infoJava("开始执行parse_by_id方法"); - try (Context context = createContext()) { + try (Context context = CONTEXT_POOL.createFreshContext()) { Value bindings = context.getBindings("python"); bindings.putMember("http", httpClient); bindings.putMember("logger", playgroundLogger); @@ -288,9 +242,9 @@ public class PyPlaygroundExecutor { playgroundLogger.errorJava("执行parse_by_id方法失败: " + e.getMessage(), e); throw new RuntimeException(e); } - }, INDEPENDENT_EXECUTOR); + }, CONTEXT_POOL.getPythonExecutor()); - ScheduledFuture timeoutTask = TIMEOUT_SCHEDULER.schedule(() -> { + ScheduledFuture timeoutTask = CONTEXT_POOL.getTimeoutScheduler().schedule(() -> { if (!executionFuture.isDone()) { executionFuture.cancel(true); playgroundLogger.errorJava("执行超时,已强制中断"); diff --git a/parser/src/test/java/cn/qaiu/parser/PyCryptoUtilsTest.java b/parser/src/test/java/cn/qaiu/parser/PyCryptoUtilsTest.java new file mode 100644 index 0000000..29b07d8 --- /dev/null +++ b/parser/src/test/java/cn/qaiu/parser/PyCryptoUtilsTest.java @@ -0,0 +1,439 @@ +package cn.qaiu.parser; + +import cn.qaiu.parser.custompy.PyCryptoUtils; +import org.junit.Before; +import org.junit.Test; + +import java.nio.charset.StandardCharsets; + +import static org.junit.Assert.*; + +/** + * PyCryptoUtils 测试类 + * 测试Python加密工具功能 + * + * @author QAIU + * Create at 2026/1/11 + */ +public class PyCryptoUtilsTest { + + private PyCryptoUtils cryptoUtils; + + @Before + public void setUp() { + cryptoUtils = new PyCryptoUtils(); + System.out.println("--- 测试开始 ---"); + } + + // ===================== MD5 测试 ===================== + + @Test + public void testMd5() { + System.out.println("\n[测试] MD5哈希"); + + // 测试已知值 + String input = "hello"; + String expected = "5d41402abc4b2a76b9719d911017c592"; + + String result = cryptoUtils.md5(input); + + System.out.println("输入: " + input); + System.out.println("MD5: " + result); + System.out.println("期望: " + expected); + + assertEquals("MD5结果应该正确", expected, result); + assertEquals("MD5应该是32位", 32, result.length()); + + System.out.println("✓ 测试通过"); + } + + @Test + public void testMd5_16() { + System.out.println("\n[测试] MD5-16位哈希"); + + String input = "hello"; + String fullMd5 = "5d41402abc4b2a76b9719d911017c592"; + String expected = fullMd5.substring(8, 24); // "abc4b2a76b9719d9" + + String result = cryptoUtils.md5_16(input); + + System.out.println("输入: " + input); + System.out.println("MD5-16: " + result); + System.out.println("期望: " + expected); + + assertEquals("MD5-16结果应该正确", expected, result); + assertEquals("MD5-16应该是16位", 16, result.length()); + + System.out.println("✓ 测试通过"); + } + + @Test + public void testMd5EmptyString() { + System.out.println("\n[测试] MD5空字符串"); + + String input = ""; + String expected = "d41d8cd98f00b204e9800998ecf8427e"; + + String result = cryptoUtils.md5(input); + + System.out.println("输入: (空字符串)"); + System.out.println("MD5: " + result); + + assertEquals("空字符串MD5应该正确", expected, result); + + System.out.println("✓ 测试通过"); + } + + // ===================== SHA 测试 ===================== + + @Test + public void testSha1() { + System.out.println("\n[测试] SHA-1哈希"); + + String input = "hello"; + String expected = "aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d"; + + String result = cryptoUtils.sha1(input); + + System.out.println("输入: " + input); + System.out.println("SHA-1: " + result); + + assertEquals("SHA-1结果应该正确", expected, result); + assertEquals("SHA-1应该是40位", 40, result.length()); + + System.out.println("✓ 测试通过"); + } + + @Test + public void testSha256() { + System.out.println("\n[测试] SHA-256哈希"); + + String input = "hello"; + String expected = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"; + + String result = cryptoUtils.sha256(input); + + System.out.println("输入: " + input); + System.out.println("SHA-256: " + result); + + assertEquals("SHA-256结果应该正确", expected, result); + assertEquals("SHA-256应该是64位", 64, result.length()); + + System.out.println("✓ 测试通过"); + } + + @Test + public void testSha512() { + System.out.println("\n[测试] SHA-512哈希"); + + String input = "hello"; + + String result = cryptoUtils.sha512(input); + + System.out.println("输入: " + input); + System.out.println("SHA-512: " + result); + + assertNotNull("SHA-512结果不能为null", result); + assertEquals("SHA-512应该是128位", 128, result.length()); + + System.out.println("✓ 测试通过"); + } + + // ===================== Base64 测试 ===================== + + @Test + public void testBase64Encode() { + System.out.println("\n[测试] Base64编码"); + + String input = "hello world"; + String expected = "aGVsbG8gd29ybGQ="; + + String result = cryptoUtils.base64_encode(input); + + System.out.println("输入: " + input); + System.out.println("Base64: " + result); + + assertEquals("Base64编码应该正确", expected, result); + + System.out.println("✓ 测试通过"); + } + + @Test + public void testBase64Decode() { + System.out.println("\n[测试] Base64解码"); + + String input = "aGVsbG8gd29ybGQ="; + String expected = "hello world"; + + String result = cryptoUtils.base64_decode(input); + + System.out.println("输入: " + input); + System.out.println("解码: " + result); + + assertEquals("Base64解码应该正确", expected, result); + + System.out.println("✓ 测试通过"); + } + + @Test + public void testBase64EncodeBytes() { + System.out.println("\n[测试] Base64字节编码"); + + byte[] input = "hello".getBytes(StandardCharsets.UTF_8); + String expected = "aGVsbG8="; + + String result = cryptoUtils.base64_encode_bytes(input); + + System.out.println("输入字节数: " + input.length); + System.out.println("Base64: " + result); + + assertEquals("Base64字节编码应该正确", expected, result); + + System.out.println("✓ 测试通过"); + } + + @Test + public void testBase64UrlEncode() { + System.out.println("\n[测试] Base64 URL安全编码"); + + // 包含特殊字符的测试数据 + String input = "hello+world/test"; + + String result = cryptoUtils.base64_url_encode(input); + + System.out.println("输入: " + input); + System.out.println("Base64 URL: " + result); + + assertNotNull("结果不能为null", result); + assertFalse("URL安全编码不应该包含+", result.contains("+")); + assertFalse("URL安全编码不应该包含/", result.contains("/")); + + System.out.println("✓ 测试通过"); + } + + @Test + public void testBase64UrlDecode() { + System.out.println("\n[测试] Base64 URL安全解码"); + + String input = "aGVsbG8td29ybGQ"; + String expected = "hello-world"; + + String result = cryptoUtils.base64_url_decode(input); + + System.out.println("输入: " + input); + System.out.println("解码: " + result); + + assertEquals("Base64 URL解码应该正确", expected, result); + + System.out.println("✓ 测试通过"); + } + + @Test + public void testBase64RoundTrip() { + System.out.println("\n[测试] Base64编解码往返"); + + String[] testCases = { + "hello", + "hello world", + "中文测试", + "特殊字符!@#$%^&*()", + "" + }; + + for (String original : testCases) { + String encoded = cryptoUtils.base64_encode(original); + String decoded = cryptoUtils.base64_decode(encoded); + + assertEquals("编解码往返应该得到原值: " + original, original, decoded); + } + + System.out.println("✓ 测试通过(" + testCases.length + " 个测试用例)"); + } + + // ===================== AES 测试 ===================== + + @Test + public void testAesEcbEncryptDecrypt() { + System.out.println("\n[测试] AES ECB模式加解密"); + + String plaintext = "hello world 123"; + String key = "1234567890123456"; // 16字节密钥 + + // 加密 + String encrypted = cryptoUtils.aes_encrypt_ecb(plaintext, key); + System.out.println("原文: " + plaintext); + System.out.println("密钥: " + key); + System.out.println("密文: " + encrypted); + + assertNotNull("加密结果不能为null", encrypted); + assertNotEquals("加密后应该不同于原文", plaintext, encrypted); + + // 解密 + String decrypted = cryptoUtils.aes_decrypt_ecb(encrypted, key); + System.out.println("解密: " + decrypted); + + assertEquals("解密后应该恢复原文", plaintext, decrypted); + + System.out.println("✓ 测试通过"); + } + + @Test + public void testAesCbcEncryptDecrypt() { + System.out.println("\n[测试] AES CBC模式加解密"); + + String plaintext = "hello world 123"; + String key = "1234567890123456"; // 16字节密钥 + String iv = "abcdefghijklmnop"; // 16字节IV + + // 加密 + String encrypted = cryptoUtils.aes_encrypt_cbc(plaintext, key, iv); + System.out.println("原文: " + plaintext); + System.out.println("密钥: " + key); + System.out.println("IV: " + iv); + System.out.println("密文: " + encrypted); + + assertNotNull("加密结果不能为null", encrypted); + assertNotEquals("加密后应该不同于原文", plaintext, encrypted); + + // 解密 + String decrypted = cryptoUtils.aes_decrypt_cbc(encrypted, key, iv); + System.out.println("解密: " + decrypted); + + assertEquals("解密后应该恢复原文", plaintext, decrypted); + + System.out.println("✓ 测试通过"); + } + + @Test + public void testAesWithChineseContent() { + System.out.println("\n[测试] AES加密中文内容"); + + String plaintext = "这是一段中文内容123"; + String key = "1234567890123456"; + String iv = "abcdefghijklmnop"; + + // ECB模式 + String encryptedEcb = cryptoUtils.aes_encrypt_ecb(plaintext, key); + String decryptedEcb = cryptoUtils.aes_decrypt_ecb(encryptedEcb, key); + assertEquals("ECB解密中文应该正确", plaintext, decryptedEcb); + + // CBC模式 + String encryptedCbc = cryptoUtils.aes_encrypt_cbc(plaintext, key, iv); + String decryptedCbc = cryptoUtils.aes_decrypt_cbc(encryptedCbc, key, iv); + assertEquals("CBC解密中文应该正确", plaintext, decryptedCbc); + + System.out.println("✓ 测试通过"); + } + + @Test + public void testAesEcbCbcDifference() { + System.out.println("\n[测试] AES ECB和CBC模式差异"); + + String plaintext = "hello world 1234"; + String key = "1234567890123456"; + String iv = "abcdefghijklmnop"; + + String ecbEncrypted = cryptoUtils.aes_encrypt_ecb(plaintext, key); + String cbcEncrypted = cryptoUtils.aes_encrypt_cbc(plaintext, key, iv); + + System.out.println("ECB密文: " + ecbEncrypted); + System.out.println("CBC密文: " + cbcEncrypted); + + // ECB和CBC模式的加密结果应该不同 + assertNotEquals("ECB和CBC加密结果应该不同", ecbEncrypted, cbcEncrypted); + + System.out.println("✓ 测试通过"); + } + + // ===================== 工具方法测试 ===================== + + @Test + public void testBytesToHex() { + System.out.println("\n[测试] 字节转十六进制"); + + byte[] input = {0x00, 0x0F, (byte) 0xFF, 0x10, (byte) 0xAB}; + String expected = "000fff10ab"; + + String result = cryptoUtils.bytes_to_hex(input); + + System.out.println("输入字节: " + input.length + " 字节"); + System.out.println("十六进制: " + result); + + assertEquals("字节转十六进制应该正确", expected, result); + + System.out.println("✓ 测试通过"); + } + + @Test + public void testConsistencyWithJsCryptoUtils() { + System.out.println("\n[测试] 与JavaScript加密工具一致性"); + + // 这些值应该与JsCryptoUtils产生相同的结果 + String testString = "consistency_test"; + + String md5 = cryptoUtils.md5(testString); + String sha1 = cryptoUtils.sha1(testString); + String sha256 = cryptoUtils.sha256(testString); + String base64 = cryptoUtils.base64_encode(testString); + + System.out.println("测试字符串: " + testString); + System.out.println("MD5: " + md5); + System.out.println("SHA1: " + sha1); + System.out.println("SHA256: " + sha256); + System.out.println("Base64: " + base64); + + // 验证结果非空且格式正确 + assertNotNull("MD5不能为null", md5); + assertEquals("MD5长度应该是32", 32, md5.length()); + + assertNotNull("SHA1不能为null", sha1); + assertEquals("SHA1长度应该是40", 40, sha1.length()); + + assertNotNull("SHA256不能为null", sha256); + assertEquals("SHA256长度应该是64", 64, sha256.length()); + + assertNotNull("Base64不能为null", base64); + + System.out.println("✓ 测试通过"); + } + + @Test + public void testNullInput() { + System.out.println("\n[测试] 空输入处理"); + + try { + // MD5应该能处理null(返回null或抛出异常) + String result = cryptoUtils.md5(null); + // 如果没有抛出异常,结果应该是null + System.out.println("MD5(null) = " + result); + } catch (Exception e) { + System.out.println("MD5(null) 抛出异常: " + e.getClass().getSimpleName()); + } + + System.out.println("✓ 空输入处理测试完成"); + } + + @Test + public void testSpecialCharacters() { + System.out.println("\n[测试] 特殊字符处理"); + + String[] testCases = { + "~!@#$%^&*()_+", + "日本語テスト", + "🎉🎊🎁", + "\n\t\r", + " " + }; + + for (String input : testCases) { + String md5 = cryptoUtils.md5(input); + String base64 = cryptoUtils.base64_encode(input); + String decoded = cryptoUtils.base64_decode(base64); + + assertNotNull("MD5不能为null", md5); + assertEquals("Base64往返应该正确", input, decoded); + } + + System.out.println("✓ 测试通过(" + testCases.length + " 个测试用例)"); + } +} diff --git a/parser/src/test/java/cn/qaiu/parser/PyHttpClientTest.java b/parser/src/test/java/cn/qaiu/parser/PyHttpClientTest.java new file mode 100644 index 0000000..8e0853f --- /dev/null +++ b/parser/src/test/java/cn/qaiu/parser/PyHttpClientTest.java @@ -0,0 +1,515 @@ +package cn.qaiu.parser; + +import cn.qaiu.WebClientVertxInit; +import cn.qaiu.parser.custompy.PyHttpClient; +import io.vertx.core.Vertx; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * PyHttpClient 测试类 + * 测试Python HTTP客户端功能是否正常 + * + * @author QAIU + * Create at 2026/1/11 + */ +public class PyHttpClientTest { + + private static Vertx vertx; + private PyHttpClient httpClient; + + @BeforeClass + public static void init() { + // 初始化Vertx + vertx = Vertx.vertx(); + WebClientVertxInit.init(vertx); + System.out.println("=== PyHttpClient测试初始化完成 ===\n"); + } + + @Before + public void setUp() { + // 创建PyHttpClient实例 + httpClient = new PyHttpClient(); + System.out.println("--- 测试开始 ---"); + } + + @After + public void tearDown() { + System.out.println("--- 测试结束 ---\n"); + } + + @Test + public void testSimpleGetRequest() { + System.out.println("\n[测试1] 简单GET请求 - httpbin.org/get"); + + try { + String url = "https://httpbin.org/get"; + System.out.println("请求URL: " + url); + + long startTime = System.currentTimeMillis(); + PyHttpClient.PyHttpResponse response = httpClient.get(url); + long endTime = System.currentTimeMillis(); + + System.out.println("请求完成,耗时: " + (endTime - startTime) + "ms"); + System.out.println("状态码: " + response.status_code()); + + String body = response.text(); + System.out.println("响应体长度: " + (body != null ? body.length() : 0) + " 字符"); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertEquals("状态码应该是200", 200, response.status_code()); + assertTrue("请求应该成功", response.ok()); + assertNotNull("响应体不能为null", body); + assertTrue("响应体应该包含url字段", body.contains("\"url\"")); + + System.out.println("✓ 测试通过"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("GET请求失败: " + e.getMessage()); + } + } + + @Test + public void testGetWithRedirect() { + System.out.println("\n[测试2] GET请求(跟随重定向)"); + + try { + String url = "https://httpbin.org/redirect/1"; + System.out.println("请求URL: " + url); + + PyHttpClient.PyHttpResponse response = httpClient.get_with_redirect(url); + + System.out.println("状态码: " + response.status_code()); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertEquals("状态码应该是200(重定向后)", 200, response.status_code()); + assertTrue("请求应该成功", response.ok()); + + System.out.println("✓ 测试通过"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("GET重定向请求失败: " + e.getMessage()); + } + } + + @Test + public void testGetNoRedirect() { + System.out.println("\n[测试3] GET请求(不跟随重定向)"); + + try { + String url = "https://httpbin.org/redirect/1"; + System.out.println("请求URL: " + url); + + PyHttpClient.PyHttpResponse response = httpClient.get_no_redirect(url); + + System.out.println("状态码: " + response.status_code()); + String location = response.header("Location"); + System.out.println("Location头: " + location); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertTrue("状态码应该是3xx重定向", + response.status_code() >= 300 && response.status_code() < 400); + assertFalse("ok()应该返回false", response.ok()); + assertNotNull("应该有Location头", location); + + System.out.println("✓ 测试通过"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("GET不重定向请求失败: " + e.getMessage()); + } + } + + @Test + public void testPostFormData() { + System.out.println("\n[测试4] POST表单数据"); + + try { + String url = "https://httpbin.org/post"; + Map formData = new HashMap<>(); + formData.put("username", "testuser"); + formData.put("password", "testpass"); + + System.out.println("请求URL: " + url); + System.out.println("表单数据: " + formData); + + PyHttpClient.PyHttpResponse response = httpClient.post(url, formData); + + System.out.println("状态码: " + response.status_code()); + + String body = response.text(); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertEquals("状态码应该是200", 200, response.status_code()); + assertTrue("响应体应该包含username", body.contains("testuser")); + + System.out.println("✓ 测试通过"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("POST表单数据失败: " + e.getMessage()); + } + } + + @Test + public void testPostJson() { + System.out.println("\n[测试5] POST JSON数据"); + + try { + String url = "https://httpbin.org/post"; + Map jsonData = new HashMap<>(); + jsonData.put("name", "测试用户"); + jsonData.put("age", 25); + jsonData.put("active", true); + + System.out.println("请求URL: " + url); + System.out.println("JSON数据: " + jsonData); + + PyHttpClient.PyHttpResponse response = httpClient.post_json(url, jsonData); + + System.out.println("状态码: " + response.status_code()); + + String body = response.text(); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertEquals("状态码应该是200", 200, response.status_code()); + assertTrue("响应体应该包含json数据", body.contains("测试用户") || body.contains("name")); + + System.out.println("✓ 测试通过"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("POST JSON数据失败: " + e.getMessage()); + } + } + + @Test + public void testCustomHeaders() { + System.out.println("\n[测试6] 自定义请求头"); + + try { + String url = "https://httpbin.org/headers"; + + // 设置自定义请求头 + httpClient.put_header("X-Custom-Header", "CustomValue") + .put_header("X-Another-Header", "AnotherValue"); + + System.out.println("请求URL: " + url); + + PyHttpClient.PyHttpResponse response = httpClient.get(url); + + System.out.println("状态码: " + response.status_code()); + + String body = response.text(); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertEquals("状态码应该是200", 200, response.status_code()); + assertTrue("响应体应该包含自定义头", body.contains("X-Custom-Header")); + + System.out.println("✓ 测试通过"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("自定义请求头测试失败: " + e.getMessage()); + } + } + + @Test + public void testBatchHeaders() { + System.out.println("\n[测试7] 批量设置请求头"); + + try { + String url = "https://httpbin.org/headers"; + + Map headers = new HashMap<>(); + headers.put("X-Header-1", "Value1"); + headers.put("X-Header-2", "Value2"); + headers.put("X-Header-3", "Value3"); + + // 先清除之前的头 + httpClient.clear_headers(); + httpClient.put_headers(headers); + + System.out.println("请求URL: " + url); + System.out.println("批量设置 " + headers.size() + " 个请求头"); + + PyHttpClient.PyHttpResponse response = httpClient.get(url); + + System.out.println("状态码: " + response.status_code()); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertEquals("状态码应该是200", 200, response.status_code()); + + System.out.println("✓ 测试通过"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("批量设置请求头测试失败: " + e.getMessage()); + } + } + + @Test + public void testResponseJson() { + System.out.println("\n[测试8] 解析JSON响应"); + + try { + String url = "https://httpbin.org/json"; + + System.out.println("请求URL: " + url); + + // 清除之前设置的头 + httpClient.clear_headers(); + + PyHttpClient.PyHttpResponse response = httpClient.get(url); + + System.out.println("状态码: " + response.status_code()); + + Object jsonObj = response.json(); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertEquals("状态码应该是200", 200, response.status_code()); + assertNotNull("JSON对象不能为null", jsonObj); + + System.out.println("JSON类型: " + jsonObj.getClass().getSimpleName()); + System.out.println("✓ 测试通过"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("解析JSON响应失败: " + e.getMessage()); + } + } + + @Test + public void testResponseHeader() { + System.out.println("\n[测试9] 获取响应头"); + + try { + String url = "https://httpbin.org/response-headers?X-Test-Header=TestValue"; + + System.out.println("请求URL: " + url); + + httpClient.clear_headers(); + PyHttpClient.PyHttpResponse response = httpClient.get(url); + + System.out.println("状态码: " + response.status_code()); + + String contentType = response.header("Content-Type"); + System.out.println("Content-Type: " + contentType); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertEquals("状态码应该是200", 200, response.status_code()); + assertNotNull("应该有Content-Type头", contentType); + + System.out.println("✓ 测试通过"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("获取响应头失败: " + e.getMessage()); + } + } + + @Test + public void testContentLength() { + System.out.println("\n[测试10] 获取内容长度"); + + try { + String url = "https://httpbin.org/bytes/1024"; + + System.out.println("请求URL: " + url); + + httpClient.clear_headers(); + PyHttpClient.PyHttpResponse response = httpClient.get(url); + + System.out.println("状态码: " + response.status_code()); + + long contentLength = response.content_length(); + System.out.println("Content-Length: " + contentLength); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertEquals("状态码应该是200", 200, response.status_code()); + assertTrue("内容长度应该大于0", contentLength > 0); + + System.out.println("✓ 测试通过"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("获取内容长度失败: " + e.getMessage()); + } + } + + @Test + public void testPutRequest() { + System.out.println("\n[测试11] PUT请求"); + + try { + String url = "https://httpbin.org/put"; + Map data = new HashMap<>(); + data.put("key", "value"); + + System.out.println("请求URL: " + url); + + httpClient.clear_headers(); + PyHttpClient.PyHttpResponse response = httpClient.put(url, data); + + System.out.println("状态码: " + response.status_code()); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertEquals("状态码应该是200", 200, response.status_code()); + + System.out.println("✓ 测试通过"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("PUT请求失败: " + e.getMessage()); + } + } + + @Test + public void testDeleteRequest() { + System.out.println("\n[测试12] DELETE请求"); + + try { + String url = "https://httpbin.org/delete"; + + System.out.println("请求URL: " + url); + + httpClient.clear_headers(); + PyHttpClient.PyHttpResponse response = httpClient.delete(url); + + System.out.println("状态码: " + response.status_code()); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertEquals("状态码应该是200", 200, response.status_code()); + + System.out.println("✓ 测试通过"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("DELETE请求失败: " + e.getMessage()); + } + } + + @Test + public void testPatchRequest() { + System.out.println("\n[测试13] PATCH请求"); + + try { + String url = "https://httpbin.org/patch"; + Map data = new HashMap<>(); + data.put("field", "updated"); + + System.out.println("请求URL: " + url); + + httpClient.clear_headers(); + PyHttpClient.PyHttpResponse response = httpClient.patch(url, data); + + System.out.println("状态码: " + response.status_code()); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertEquals("状态码应该是200", 200, response.status_code()); + + System.out.println("✓ 测试通过"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("PATCH请求失败: " + e.getMessage()); + } + } + + @Test + public void testMethodChaining() { + System.out.println("\n[测试14] 方法链式调用"); + + try { + String url = "https://httpbin.org/headers"; + + System.out.println("请求URL: " + url); + + // 测试链式调用 + PyHttpClient.PyHttpResponse response = new PyHttpClient() + .put_header("X-Chain-1", "Value1") + .put_header("X-Chain-2", "Value2") + .set_timeout(30) + .get(url); + + System.out.println("状态码: " + response.status_code()); + + String body = response.text(); + + // 验证结果 + assertNotNull("响应不能为null", response); + assertEquals("状态码应该是200", 200, response.status_code()); + assertTrue("响应体应该包含链式设置的头", body.contains("X-Chain")); + + System.out.println("✓ 测试通过"); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("方法链式调用测试失败: " + e.getMessage()); + } + } + + @Test + public void testBodyAndTextEquivalent() { + System.out.println("\n[测试15] body()和text()方法等价性"); + + try { + String url = "https://httpbin.org/get"; + + System.out.println("请求URL: " + url); + + httpClient.clear_headers(); + PyHttpClient.PyHttpResponse response = httpClient.get(url); + + String body = response.body(); + String text = response.text(); + + // 验证结果 + assertEquals("body()和text()应该返回相同的结果", body, text); + + System.out.println("✓ 测试通过"); + System.out.println(" body() == text(): " + body.equals(text)); + + } catch (Exception e) { + System.err.println("✗ 测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("body()和text()等价性测试失败: " + e.getMessage()); + } + } +} diff --git a/parser/src/test/java/cn/qaiu/parser/PyParserTest.java b/parser/src/test/java/cn/qaiu/parser/PyParserTest.java new file mode 100644 index 0000000..08033ef --- /dev/null +++ b/parser/src/test/java/cn/qaiu/parser/PyParserTest.java @@ -0,0 +1,558 @@ +package cn.qaiu.parser; + +import cn.qaiu.entity.FileInfo; +import cn.qaiu.entity.ShareLinkInfo; +import cn.qaiu.parser.custom.CustomParserRegistry; +import cn.qaiu.parser.custompy.*; +import cn.qaiu.WebClientVertxInit; +import io.vertx.core.Vertx; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.*; + +/** + * Python解析器测试 + * 测试GraalPy Python解析器的核心功能 + * + * @author QAIU + * Create at 2026/1/11 + */ +public class PyParserTest { + + private static Vertx vertx; + + @BeforeClass + public static void init() { + // 初始化Vertx + vertx = Vertx.vertx(); + WebClientVertxInit.init(vertx); + System.out.println("=== Python解析器测试初始化完成 ===\n"); + } + + @Before + public void setUp() { + // 清理注册表 + CustomParserRegistry.clear(); + } + + @Test + public void testPyContextPoolInitialization() { + System.out.println("\n[测试] Context池初始化"); + + try { + PyContextPool pool = PyContextPool.getInstance(); + + assertNotNull("Context池实例不能为null", pool); + assertFalse("Context池不应该是关闭状态", pool.isClosed()); + assertTrue("应该有可用的Context", pool.getCreatedCount() > 0); + + System.out.println("✓ Context池初始化测试通过"); + System.out.println(" " + pool.getStatus()); + + } catch (Exception e) { + System.err.println("✗ Context池初始化测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("Context池初始化失败: " + e.getMessage()); + } + } + + @Test + public void testPyContextPoolAcquireRelease() throws Exception { + System.out.println("\n[测试] Context池获取和释放"); + + try { + PyContextPool pool = PyContextPool.getInstance(); + + // 获取Context + PyContextPool.PooledContext pc = pool.acquire(); + assertNotNull("获取的Context不能为null", pc); + assertNotNull("底层Context不能为null", pc.getContext()); + assertFalse("Context不应该过期", pc.isExpired()); + + int availableBefore = pool.getAvailableCount(); + + // 释放Context + pc.close(); + + // 验证归还后可用数量增加 + int availableAfter = pool.getAvailableCount(); + assertTrue("归还后可用数量应该增加", availableAfter >= availableBefore); + + System.out.println("✓ Context池获取和释放测试通过"); + + } catch (Exception e) { + System.err.println("✗ Context池获取和释放测试失败: " + e.getMessage()); + e.printStackTrace(); + throw e; + } + } + + @Test + public void testSimplePythonExecution() { + System.out.println("\n[测试] 简单Python代码执行"); + + String pyCode = """ + # 简单测试 + def parse(share_link_info, http, logger): + logger.info("测试日志") + return "https://example.com/download/test.zip" + """; + + try { + ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder() + .shareUrl("https://example.com/s/test123") + .shareKey("test123") + .otherParam(new HashMap<>()) + .build(); + + PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode); + + String result = executor.executeParseAsync() + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + assertNotNull("执行结果不能为null", result); + assertTrue("应该返回下载链接", result.contains("example.com")); + + // 检查日志 + List logs = executor.getLogs(); + assertFalse("应该有日志输出", logs.isEmpty()); + + System.out.println("✓ 简单Python代码执行测试通过"); + System.out.println(" 返回结果: " + result); + System.out.println(" 日志数量: " + logs.size()); + + } catch (Exception e) { + System.err.println("✗ 简单Python代码执行测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("Python执行失败: " + e.getMessage()); + } + } + + @Test + public void testPythonHttpRequest() { + System.out.println("\n[测试] Python HTTP请求功能"); + + String pyCode = """ + def parse(share_link_info, http, logger): + logger.info("开始HTTP请求测试") + + # 发送GET请求 + response = http.get("https://httpbin.org/get") + + if response.ok(): + logger.info(f"请求成功,状态码: {response.status_code()}") + return "https://example.com/success" + else: + logger.error(f"请求失败,状态码: {response.status_code()}") + return "https://example.com/failed" + """; + + try { + ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder() + .shareUrl("https://example.com/s/test123") + .shareKey("test123") + .otherParam(new HashMap<>()) + .build(); + + PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode); + + String result = executor.executeParseAsync() + .toCompletionStage() + .toCompletableFuture() + .get(60, TimeUnit.SECONDS); + + assertNotNull("执行结果不能为null", result); + assertTrue("应该返回成功链接", result.contains("success")); + + System.out.println("✓ Python HTTP请求功能测试通过"); + System.out.println(" 返回结果: " + result); + + } catch (Exception e) { + System.err.println("✗ Python HTTP请求功能测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("Python HTTP请求失败: " + e.getMessage()); + } + } + + @Test + public void testPythonCryptoUtils() { + System.out.println("\n[测试] Python加密工具功能"); + + String pyCode = """ + def parse(share_link_info, http, logger): + # 测试MD5 + md5_result = crypto.md5("hello") + logger.info(f"MD5: {md5_result}") + + # 测试SHA256 + sha256_result = crypto.sha256("hello") + logger.info(f"SHA256: {sha256_result}") + + # 测试Base64编码解码 + b64_encoded = crypto.base64_encode("hello world") + b64_decoded = crypto.base64_decode(b64_encoded) + logger.info(f"Base64: {b64_encoded} -> {b64_decoded}") + + # 验证MD5正确性 + if md5_result == "5d41402abc4b2a76b9719d911017c592": + return "https://example.com/crypto_success" + else: + return "https://example.com/crypto_failed" + """; + + try { + ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder() + .shareUrl("https://example.com/s/test123") + .shareKey("test123") + .otherParam(new HashMap<>()) + .build(); + + PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode); + + String result = executor.executeParseAsync() + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + assertNotNull("执行结果不能为null", result); + assertTrue("加密工具应该正常工作", result.contains("crypto_success")); + + System.out.println("✓ Python加密工具功能测试通过"); + System.out.println(" 返回结果: " + result); + + } catch (Exception e) { + System.err.println("✗ Python加密工具功能测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("Python加密工具测试失败: " + e.getMessage()); + } + } + + @Test + public void testPythonShareLinkInfo() { + System.out.println("\n[测试] Python ShareLinkInfo访问"); + + String pyCode = """ + def parse(share_link_info, http, logger): + # 获取分享链接信息 + url = share_link_info.get_share_url() + key = share_link_info.get_share_key() + pwd = share_link_info.get_share_password() + + logger.info(f"URL: {url}") + logger.info(f"Key: {key}") + logger.info(f"Password: {pwd}") + + # 测试其他参数 + custom_param = share_link_info.get_other_param("customKey") + logger.info(f"CustomKey: {custom_param}") + + if url and key: + return f"https://example.com/download/{key}" + else: + return "https://example.com/failed" + """; + + try { + Map otherParams = new HashMap<>(); + otherParams.put("customKey", "customValue"); + + ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder() + .shareUrl("https://example.com/s/mykey123") + .shareKey("mykey123") + .sharePassword("mypassword") + .otherParam(otherParams) + .build(); + + PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode); + + String result = executor.executeParseAsync() + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + assertNotNull("执行结果不能为null", result); + assertTrue("应该包含正确的key", result.contains("mykey123")); + + System.out.println("✓ Python ShareLinkInfo访问测试通过"); + System.out.println(" 返回结果: " + result); + + } catch (Exception e) { + System.err.println("✗ Python ShareLinkInfo访问测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("Python ShareLinkInfo访问失败: " + e.getMessage()); + } + } + + @Test + public void testPythonFileListParsing() { + System.out.println("\n[测试] Python文件列表解析"); + + String pyCode = """ + def parse(share_link_info, http, logger): + return "https://example.com/download/single.zip" + + def parse_file_list(share_link_info, http, logger): + logger.info("开始解析文件列表") + + # 返回文件列表 + file_list = [ + { + "file_name": "测试文件1.txt", + "file_id": "file001", + "file_type": "txt", + "size": 1024, + "pan_type": "custom" + }, + { + "file_name": "测试文件2.zip", + "file_id": "file002", + "file_type": "zip", + "size": 2048, + "pan_type": "custom" + } + ] + + logger.info(f"解析到 {len(file_list)} 个文件") + return file_list + """; + + try { + ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder() + .shareUrl("https://example.com/s/test123") + .shareKey("test123") + .otherParam(new HashMap<>()) + .build(); + + PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode); + + List fileList = executor.executeParseFileListAsync() + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + assertNotNull("文件列表不能为null", fileList); + assertEquals("应该有2个文件", 2, fileList.size()); + + FileInfo firstFile = fileList.get(0); + assertEquals("第一个文件名应该正确", "测试文件1.txt", firstFile.getFileName()); + assertEquals("第一个文件ID应该正确", "file001", firstFile.getFileId()); + + System.out.println("✓ Python文件列表解析测试通过"); + System.out.println(" 文件数量: " + fileList.size()); + for (FileInfo file : fileList) { + System.out.println(" - " + file.getFileName() + " (" + file.getSize() + " bytes)"); + } + + } catch (Exception e) { + System.err.println("✗ Python文件列表解析测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("Python文件列表解析失败: " + e.getMessage()); + } + } + + @Test + public void testPythonParseById() { + System.out.println("\n[测试] Python按ID解析"); + + String pyCode = """ + def parse(share_link_info, http, logger): + return "https://example.com/download/single.zip" + + def parse_by_id(share_link_info, http, logger): + # 获取文件ID参数 + param_json = share_link_info.get_other_param("paramJson") + + if param_json and hasattr(param_json, 'fileId'): + file_id = param_json.fileId + else: + file_id = "default_id" + + logger.info(f"按ID解析: {file_id}") + return f"https://example.com/download/{file_id}" + """; + + try { + Map otherParams = new HashMap<>(); + io.vertx.core.json.JsonObject paramJson = new io.vertx.core.json.JsonObject(); + paramJson.put("fileId", "myfile123"); + otherParams.put("paramJson", paramJson); + + ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder() + .shareUrl("https://example.com/s/test123") + .shareKey("test123") + .otherParam(otherParams) + .build(); + + PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode); + + String result = executor.executeParseByIdAsync() + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + assertNotNull("执行结果不能为null", result); + assertTrue("应该包含文件ID", result.contains("download")); + + System.out.println("✓ Python按ID解析测试通过"); + System.out.println(" 返回结果: " + result); + + } catch (Exception e) { + System.err.println("✗ Python按ID解析测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("Python按ID解析失败: " + e.getMessage()); + } + } + + @Test + public void testPythonErrorHandling() { + System.out.println("\n[测试] Python错误处理"); + + String pyCode = """ + def parse(share_link_info, http, logger): + # 故意抛出异常 + raise ValueError("测试错误处理") + """; + + try { + ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder() + .shareUrl("https://example.com/s/test123") + .shareKey("test123") + .otherParam(new HashMap<>()) + .build(); + + PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode); + + try { + executor.executeParseAsync() + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + fail("应该抛出异常"); + + } catch (Exception e) { + // 预期的异常 + assertTrue("异常信息应该包含错误内容", + e.getMessage().contains("ValueError") || + e.getCause().getMessage().contains("ValueError")); + + System.out.println("✓ Python错误处理测试通过"); + System.out.println(" 捕获到预期的异常: " + e.getMessage()); + } + + } catch (Exception e) { + System.err.println("✗ Python错误处理测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("Python错误处理测试失败: " + e.getMessage()); + } + } + + @Test + public void testPythonSandboxSecurity() { + System.out.println("\n[测试] Python沙箱安全性"); + + // 测试禁止文件系统访问 + String pyCode = """ + import os + + def parse(share_link_info, http, logger): + try: + # 尝试读取文件(应该被拒绝) + with open("/etc/passwd", "r") as f: + content = f.read() + return "https://example.com/security_breach" + except Exception as e: + logger.info(f"文件访问被正确拒绝: {type(e).__name__}") + return "https://example.com/security_ok" + """; + + try { + ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder() + .shareUrl("https://example.com/s/test123") + .shareKey("test123") + .otherParam(new HashMap<>()) + .build(); + + PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode); + + String result = executor.executeParseAsync() + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + // 如果返回security_ok或抛出异常都表示安全机制工作正常 + assertTrue("沙箱应该阻止文件访问", + result.contains("security_ok") || !result.contains("security_breach")); + + System.out.println("✓ Python沙箱安全性测试通过"); + System.out.println(" 返回结果: " + result); + + } catch (Exception e) { + // 如果直接抛出异常也表示安全机制工作正常 + System.out.println("✓ Python沙箱安全性测试通过(抛出异常)"); + System.out.println(" 异常信息: " + e.getMessage()); + } + } + + @Test + public void testPythonLoggerLevels() { + System.out.println("\n[测试] Python日志级别"); + + String pyCode = """ + def parse(share_link_info, http, logger): + logger.debug("这是DEBUG日志") + logger.info("这是INFO日志") + logger.warn("这是WARN日志") + logger.error("这是ERROR日志") + return "https://example.com/log_test" + """; + + try { + ShareLinkInfo linkInfo = ShareLinkInfo.newBuilder() + .shareUrl("https://example.com/s/test123") + .shareKey("test123") + .otherParam(new HashMap<>()) + .build(); + + PyPlaygroundExecutor executor = new PyPlaygroundExecutor(linkInfo, pyCode); + + executor.executeParseAsync() + .toCompletionStage() + .toCompletableFuture() + .get(30, TimeUnit.SECONDS); + + List logs = executor.getLogs(); + + // 检查各个级别的日志 + boolean hasDebug = logs.stream().anyMatch(l -> "DEBUG".equals(l.getLevel())); + boolean hasInfo = logs.stream().anyMatch(l -> "INFO".equals(l.getLevel())); + boolean hasWarn = logs.stream().anyMatch(l -> "WARN".equals(l.getLevel())); + boolean hasError = logs.stream().anyMatch(l -> "ERROR".equals(l.getLevel())); + + System.out.println("✓ Python日志级别测试通过"); + System.out.println(" 日志数量: " + logs.size()); + System.out.println(" DEBUG: " + hasDebug); + System.out.println(" INFO: " + hasInfo); + System.out.println(" WARN: " + hasWarn); + System.out.println(" ERROR: " + hasError); + + for (PyPlaygroundLogger.LogEntry log : logs) { + System.out.println(" [" + log.getLevel() + "] " + log.getMessage()); + } + + } catch (Exception e) { + System.err.println("✗ Python日志级别测试失败: " + e.getMessage()); + e.printStackTrace(); + fail("Python日志级别测试失败: " + e.getMessage()); + } + } +} diff --git a/web-front/src/views/Playground.vue b/web-front/src/views/Playground.vue index d883c98..09f0831 100644 --- a/web-front/src/views/Playground.vue +++ b/web-front/src/views/Playground.vue @@ -1625,66 +1625,77 @@ def parse_file_list(share_link_info, http, logger): const createNewFile = async () => { if (!newFileFormRef.value) return; - await newFileFormRef.value.validate((valid) => { + try { + // 使用 Promise 模式进行表单验证 + const valid = await newFileFormRef.value.validate(); if (!valid) return; - - const language = newFileForm.value.language || 'javascript'; - const isPython = language === 'python'; - const fileExt = isPython ? '.py' : '.js'; - - // 使用解析器名称作为文件名 - let fileName = newFileForm.value.name; - if (!fileName.endsWith(fileExt)) { - // 移除可能的错误扩展名 - fileName = fileName.replace(/\.(js|py)$/i, ''); - fileName = fileName + fileExt; + } catch (e) { + // 验证失败 + return; + } + + const language = newFileForm.value.language || 'javascript'; + const isPython = language === 'python'; + const fileExt = isPython ? '.py' : '.js'; + + // 使用解析器名称作为文件名 + let fileName = newFileForm.value.name; + if (!fileName.endsWith(fileExt)) { + // 移除可能的错误扩展名 + fileName = fileName.replace(/\.(js|py)$/i, ''); + fileName = fileName + fileExt; + } + + // 检查文件名是否已存在 + if (files.value.some(f => f.name === fileName)) { + ElMessage.warning('文件名已存在,请使用其他名称'); + return; + } + + // 生成模板代码 + const template = generateTemplate( + newFileForm.value.name.replace(/\.(js|py)$/i, ''), + newFileForm.value.identifier, + newFileForm.value.author, + newFileForm.value.match, + language + ); + + // 创建新文件 + fileIdCounter.value++; + const newFile = { + id: 'file' + fileIdCounter.value, + name: fileName, + content: template, + language: language, + modified: false + }; + + files.value.push(newFile); + + // 关闭对话框并保存 + newFileDialogVisible.value = false; + saveAllFilesToStorage(); + + // 使用 nextTick 确保 DOM 更新后再切换选项卡 + await nextTick(); + + // 设置活动文件 ID(此时 DOM 已更新,tab 已存在) + activeFileId.value = newFile.id; + + // 更新编辑器语言模式 + updateEditorLanguage(language); + + ElMessage.success(`${isPython ? 'Python' : 'JavaScript'}文件创建成功`); + + // 等待编辑器更新后聚焦 + await nextTick(); + if (editorRef.value && editorRef.value.getEditor) { + const editor = editorRef.value.getEditor(); + if (editor) { + editor.focus(); } - - // 检查文件名是否已存在 - if (files.value.some(f => f.name === fileName)) { - ElMessage.warning('文件名已存在,请使用其他名称'); - return; - } - - // 生成模板代码 - const template = generateTemplate( - newFileForm.value.name.replace(/\.(js|py)$/i, ''), - newFileForm.value.identifier, - newFileForm.value.author, - newFileForm.value.match, - language - ); - - // 创建新文件 - fileIdCounter.value++; - const newFile = { - id: 'file' + fileIdCounter.value, - name: fileName, - content: template, - language: language, - modified: false - }; - - files.value.push(newFile); - activeFileId.value = newFile.id; - newFileDialogVisible.value = false; - saveAllFilesToStorage(); - - // 更新编辑器语言模式 - updateEditorLanguage(language); - - ElMessage.success(`${isPython ? 'Python' : 'JavaScript'}文件创建成功`); - - // 等待编辑器更新后聚焦 - nextTick(() => { - if (editorRef.value && editorRef.value.getEditor) { - const editor = editorRef.value.getEditor(); - if (editor) { - editor.focus(); - } - } - }); - }); + } }; // IDE功能:复制全部