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:
q
2026-01-11 03:19:31 +08:00
parent 62cc7449fd
commit b179194753
8 changed files with 2066 additions and 166 deletions

View File

@@ -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) -->

View 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();
}
}

View File

@@ -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);

View File

@@ -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("执行超时,已强制中断");

View 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 + " 个测试用例)");
}
}

View 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());
}
}
}

View 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());
}
}
}

View File

@@ -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功能复制全部