mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-01-12 09:24:14 +00:00
fix: 修复GraalPy依赖并添加Context池化和完整单元测试
- 修复parser pom.xml中GraalPy依赖配置 - 修复web-front Playground.vue中Tab选中异常bug - 添加PyContextPool实现Context池化管理 - 更新PyPlaygroundExecutor和PyParserExecutor使用池化 - 创建PyParserTest完整单元测试 - 创建PyHttpClientTest HTTP客户端测试 - 创建PyCryptoUtilsTest加密工具测试 - 修复所有ShareLinkInfo构造相关错误
This commit is contained in:
@@ -114,10 +114,15 @@
|
||||
<version>${graalpy.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.graalvm.polyglot</groupId>
|
||||
<groupId>org.graalvm.python</groupId>
|
||||
<artifactId>python</artifactId>
|
||||
<version>${graalpy.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.graalvm.python</groupId>
|
||||
<artifactId>python-embedding</artifactId>
|
||||
<version>${graalpy.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Compression (Brotli) -->
|
||||
|
||||
459
parser/src/main/java/cn/qaiu/parser/custompy/PyContextPool.java
Normal file
459
parser/src/main/java/cn/qaiu/parser/custompy/PyContextPool.java
Normal file
@@ -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 池化支持
|
||||
*
|
||||
* <p>特性:
|
||||
* <ul>
|
||||
* <li>共享单个 Engine 实例,减少内存占用和启动时间</li>
|
||||
* <li>Context 对象池,避免重复创建和销毁的开销</li>
|
||||
* <li>支持安全的沙箱配置</li>
|
||||
* <li>线程安全的池化管理</li>
|
||||
* <li>支持优雅关闭和资源清理</li>
|
||||
* </ul>
|
||||
*
|
||||
* @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<PooledContext> 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();
|
||||
}
|
||||
}
|
||||
@@ -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<String> 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);
|
||||
|
||||
@@ -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<String> 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<List<FileInfo>> 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<String> 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("执行超时,已强制中断");
|
||||
|
||||
439
parser/src/test/java/cn/qaiu/parser/PyCryptoUtilsTest.java
Normal file
439
parser/src/test/java/cn/qaiu/parser/PyCryptoUtilsTest.java
Normal file
@@ -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 <a href="https://qaiu.top">QAIU</a>
|
||||
* 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 + " 个测试用例)");
|
||||
}
|
||||
}
|
||||
515
parser/src/test/java/cn/qaiu/parser/PyHttpClientTest.java
Normal file
515
parser/src/test/java/cn/qaiu/parser/PyHttpClientTest.java
Normal file
@@ -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 <a href="https://qaiu.top">QAIU</a>
|
||||
* 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<String, String> 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<String, Object> 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<String, String> 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<String, String> 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<String, String> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
558
parser/src/test/java/cn/qaiu/parser/PyParserTest.java
Normal file
558
parser/src/test/java/cn/qaiu/parser/PyParserTest.java
Normal file
@@ -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 <a href="https://qaiu.top">QAIU</a>
|
||||
* 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<PyPlaygroundLogger.LogEntry> 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<String, Object> 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<FileInfo> 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<String, Object> 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<PyPlaygroundLogger.LogEntry> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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功能:复制全部
|
||||
|
||||
Reference in New Issue
Block a user