量子电梯、影子电梯、猴子电梯、开子电梯……
JUnit、投喂包和编译打包相关#
JUnit#
OO 第二单元电梯,为了方便我们进行输入输出,课程组提供了 dataInput
程序对我们编译好的 jar 包进行管道输入,并提供一个 elevatorX.jar
作为我们代码的依赖。然而在使用过程中有诸多不便:
- 每次测试程序,都需要打包一遍,再从
out/
文件夹下拖出来 - Windows 下 powershell 还运行不了(奇奇怪怪,是为什么呢?)
即便好心的睿睿准备了一个更好用的数据投喂机,但它需要作为 main
方法来使用,而且还不能上传到公测提交中(当然会查重)。
这时候,我们可以使用曾经在 oop 中接触到的 JUnit 方法,将 testMain/dataInput
等定时向我们的程序提供输入的方法编写为一个测试单元,运用测试单元就可以不用担心两个 main
方法的问题啦。
因为 JUnit 方法虽然可以上传提交,但是仍然会经过查重,这里不提供详细代码。所以下面仅提供一个我的操作流程,供大家参考。
软盘+蟒蛇图标#
看到 dataInput
那个经典图标,我就知道这是 PyInstaller 打包的 Python 程序,于是从网上搜索了如何将它反编译出源码,并顺利得到了 dataInput
的源码。嗯,这么点源码打包成 5MB 的程序,太有 PyInstaller 那味了。
AI 机器,小子!#
其实不管是 dataInput
的源码,还是 TestMain
的源码,都有被查重的风险,所以最简单的办法当然是拿着这些源码去找 AI,让它根据源码为我们写一个 MainClass.main()
(或者 Main.main()
之类)的 JUnit 方法就好了。不过 AI 生成也不一定完全能够保证不被查重(
然后照着 oop 的方法设置 JUnit 就好了。
输入输出重定向#
我发现测试方法的运行配置里没有“重定向输入”的选择,只有“将控制台输出保存到文件”的选择。也就是说我们得在 JUnit 方法里实现输入输出重定向。
我们可以使用 java.nio.file
下的 Paths
、Path
、Files
等类来实现重定向。后面带 s
的俩类提供了许多静态方法。
Path testFilePath = Paths.get(System.getProperty("user.dir"), "stdin.txt");
Path outputFilePath = Paths.get(System.getProperty("user.dir"), "usrout.txt");
List<String> lines = Files.readAllLines(testFilePath);
PrintStream outputStream = new PrintStream(Files.newOutputStream(outputFilePath.toFile().toPath()));
System.setOut(outputStream);
这里我们将标准输出重定向到了 usrout.txt
文件,这个文件在用户当前工作目录下,也就是我们的项目的根目录下。
stdin.txt
就是带时间戳的输入,也放在同一目录下,使用 Paths.get()
和 Files.readAllLines()
读取到 List<String>
中。
清理线程#
实际使用 JUnit 方法时遇到了一些问题:
MainClass.main()
方法一般都是创建线程、启动线程,然后就不管这些线程的死活了。- JUnit 方法执行完成之后,IDEA 就直接把整个程序结束了。
也就是说,当 JUnit 方法把输入定时交给程序完成之后,程序就结束了,电梯线程以及其他线程就会被终止。
即便我们在 JUnit 方法里创建 main
方法的线程(比如叫 mainThread
),然后在最后使用 mainThread.join()
等待线程结束。
但我们等待的只是 mainThread
这一线程的结束:当 main
方法运行完成之后,它就结束了。我们并不能确定 main
方法中创建的其他线程的状态。
所以解决办法有两个:
- 在
main
方法中创建并收集线程,并在最后使用thread.join()
方法等待所有线程结束。 - 在 JUnit 方法中一开始记录程序运行中的所有线程;在方法结束之前,再次记录程序运行中的所有线程,减去前面的进程,就可以得到
main
方法中创建的进程了。之后再等待这些线程结束就好了。
对于第二个方法,部分代码如下:
Set<Thread> initThreads = new HashSet<>(Thread.getAllStackTraces().keySet());
// ... after start main thread
Set<Thread> curThreads = new HashSet<>(Thread.getAllStackTraces().keySet());
curThreads.removeAll(initThreads);
Set<Thread> userThreads = new HashSet<>();
for (Thread t : curThreads) {
if (t.isAlive() && !t.isDaemon()) {
userThreads.add(t);
}
}
这里需要排除一些 Daemon 守护线程,一般作垃圾回收、释放内存等用途。
编译打包#
在编写测评机的时候,我遇到了编译打包源码的问题。
首先是编译源码,javac 不能识别并找到官方包依赖。
其次是打包时,jar 也不能识别并找到官方包依赖。
查了一番资料后,我得到了以下解决方法:
javac -d <project_dir> <main_class_path> -sourcepath <src_path> -classpath <path_to_elevator1_jar> #带依赖编译源码
jar xf <path_to_elevator1_jar> # 解压官方包
# 将解压后的文件复制到 <project_dir> 下
jar cfm <jar_name> <manifest_path> -C <project_dir> . # 将文件夹打包成 jar
其中:
<project_dir>
就是源码的工作目录<main_class_path>
是main
方法所在类文件的位置<src_path>
是指main
方法所在类文件的所在目录,注意和<project_dir>
的不同<manifest_path>
是在 jar 包中固定位置与格式的一个MANIFEST.MF
文件,可以参考其他打包好的jar
包编写。
然后就可以得到正确的包了。