Perl E01:signatures 的稳定状态不等于任意参数都可收
第 07 篇展示了普通子例程的 @_ 参数别名。这里换一种入口做对照,而不改日志 CLI 主线:在冻结的 Perl 5.38.2 下,use feature 'signatures' 可在词法范围内启用签名。官方 pod/perlexperiment.pod 记载 signatures 从 5.20 引入、5.36 被接受;签名本身在此版本不应被一律当成实验功能并压制警告。 123use feature 'signatures';sub level($value) { print "$value\n" }level('INFO'); 本机正例 STDOUT INFO\n、STDERR 0 字节、退出 0;反例 level() 报 Too few arguments for subroutine、STDOUT 空、退出非零。签名将参数显式绑定到词法变量,并为个数提供检查;它并没有使字符串自动符合 INFO/WARN/ERROR 的业务规则,也没有改变 report_rows 返回的是数组引用这一契约。与...
Perl 39:把合成日志从输入一路交接到报表
结课不是给前面文章再贴一份目录,而是确认当前 Course 入口能从头走到尾。fixtures/course.tsv 的五行 UTF-8 原始字节经 import 读取、解码、字段校验、duration 类型化、message NFC、reject 收集后,得到 accepted 四条与 rejected 一条;query 在 accepted JSON Lines 上执行 level、min-duration、contains 三类过滤;report 对 accepted 记录聚合、排序并输出 UTF-8 TSV。所有分支使用同一套 LogTool::Course 解析和 JSON Lines 契约,测试不允许坏行作为部分成功混入结果。 图:五字段合成日志的导入、查询与报表交接 12345Course TSV bytes -> import: accepted.jsonl + rejected.jsonl -> query: all / level / min-duration / contains -> report: TOTAL ...
Perl 38:发布前把失败阶段拆开测试
只跑一次成功链路,无法排除坏字节被吞掉、旧报表被截断、子进程启动失败却返回成功。当前 Course 审核以输入、解析、查询、写入、子进程与生命周期六类组织故障矩阵,逐项记 exit、signal、STDOUT、STDERR 和目标文件状态。tools/replay.pl --case all 把这些 case 收成一份 JSON:bad-byte、bad-json、directory-output、missing-parent、input-output-alias、output-pair-alias、kernel-faults、input-faults、data-contract、worker-and-lifecycle 都在同一入口下复跑。 成功对照仍然要保留:version 说明实际解释器,import 必须得到 INPUT<TAB>5、ACCEPTED<TAB>4、REJECTED<TAB>1,query-all 的 stdout 必须逐字节等于 accepted JSON Lines,report 必须输出固定排序 TSV。失败对照则看...
Perl 37:distribution 声明依赖,还得解包跑测试
前 36 篇的工具一直从工作树执行。要交付一个 Perl distribution,不能仅压缩 lib/ 并说“别人可以用”;至少要有模块版本、脚本清单、运行期/测试期依赖与能从发行包重新执行的测试。示例 Makefile.PL 用 ExtUtils::MakeMaker 7.70 声明 distribution 名 LogTool,版本取自 LogTool::Course 0.200,同时安装 bin/logtool 与 legacy bin/logtool.pl,本机只验证过的最低运行目标 5.38.2,以及核心模块与 Text::CSV >= 2.06、Text::CSV_XS 1.64、DBI 1.655、DBD::SQLite 1.78 与 Moo 2.005005。最低版本字段是本系列的测试范围边界,不是其他 Perl 构建都已复验的声明。 在 /tmp 的工作树副本先用仓库隔离安装的 vendor/lib/perl5 配置,生成 MANIFEST、再 make dist;对得到的 发行练习包 校验 SHA-256 c6616daa3203f8b0487c7f8...
Perl 36:拒绝超大文件,不等于已经是流式解析器
当前 read_records 读取整个文件并保留数组,report_rows 再遍历数组计数。若对任意大文件沿用这个设计,内存会随记录数增长。本工具在打开文件后先检查大小,并逐行累计已读字节:超过 1_048_576 字节就拒绝,不发布任何结果。测试合成一份恰好 1_048_577 字节的普通文件,执行 import 的 STDOUT 空、STDERR input exceeds 1048576 bytes\n、退出 2;三条金标输入则照常完成。限额是输入策略,不是渐进复杂度证明。 readline 仍可能在检查累计量之前一次分配整条长行;文件还可能在 -s 检查后被其他进程增长。现在只把这个上限用于受控、稳定的普通合成文件,不宣称对敌对流或并发改写输入给出严格峰值内存上界。若真的需要持续处理流,必须改变存储模型和验证顺序,还应说明输出排序是否允许单遍完成。这里也没有用一次小样本耗时宣称常数空间或线性复杂度。 练习:只加 die if @records > 1000,能限制一条巨长 message 引起的内存风险吗?不能;条数上限不等于字节上限。生成拒绝样本的语句、金标以...
Perl 35:锁文件不应该随报表一起替换
两个进程同时打开旧报表再在旧 inode 上加锁,随后若其中一个执行 rename,另一个可能继续锁着已经脱离目标路径的 inode。save_report 因此锁定稳定的 DESTINATION.lock 旁路文件;持锁期间写临时文件并替换,退出时关闭锁句柄,却不删除锁文件。两个协作进程分别报告 SAVED\n、状态 0,最后的目标只与两个完整的报表金标之一相同;测试不假设哪个进程最后取得锁。 这不是全局互斥。反例持有 DESTINATION.lock 的排他锁,同时用普通 open '>' 覆写报表为 BYPASS\n;它仍可成功。Perl 5.38.2 pod/perlfunc.pod 的 flock 也注明锁通常是 advisory:不遵守协议的写入者不会被自动阻止。测试再由协作命令恢复金标,确认锁文件本身未被替换。 锁范围覆盖单次写入和 rename,不涵盖读取输入;每个进程先独立解析全部记录。对抗目录、符号链接或分布式文件系统的竞态与锁语义仍未测。练习:为了“清理临时文件”,顺便 unlink DESTINATION.lock 会怎样?下一进程可能锁住新建的另一...
Perl 34:rename 成功不代表断电后文件一定还在
上篇的临时文件和 rename 可以将“旧的完整报表”切换成“新的完整报表”。在本机 Linux 同目录写法下,测试中两个协作写进程结束后,目标字节恰好是两个金标中的一个,而不是混合半截。把目标换成目录,rename 失败,旧有效目标仍在。这说明本次测试覆盖了通常运行中的替换边界,不说明断电后的磁盘状态。 close 负责 Perl 层的写出与关闭错误检查;它不等同于对临时文件和所在目录都成功做了 fsync。要论证“断电后仍可恢复”,至少需要文件数据和 rename 所在目录元数据的同步协议,并有支持该协议的文件系统与真实故障注入。这里没有执行系统断电、重启后的校验,也没有实现那套同步协议,断电持久化 NOT_RUN。官方 pod/perlfunc.pod 对 rename 明确说平台行为差异很大,不能把 Linux 上的一次成功推广为通用语义。 图:正常替换与断电恢复的证据边界 12本机程序正常运行:关闭成功 -> 同目录 rename 成功 -> 新目标可读尚无证据: 断电后磁盘刷新/目录项保存 -> 重启仍可读 练习:加一个 sleep...
Perl 33:先写临时文件,最后才触碰有效报表
如果 report-file 一开始就以 > 打开已有目标,任何后续解析、写入、关闭错误都可能只留下截断文件。当前流程先把输入完全读完、校验并编码,再在目标同一目录写独占临时文件;print 和 close 都确认成功后才 rename 覆盖目标。只有 rename 返回成功,CLI 才发出 SAVED\n。之前生成的有效目标不会在“输入刚报错”时被打开。 图:临时文件检查完成后替换目标 12345校验输入 -> 生成全部字节 -> 同目录新文件 -> 检查 print/close | 失败:删临时文件,旧目标不变 | 成功:rename 替换旧目标 t/32-36-safe-report.t 首先写入 golden/report.txt,再用 invalid.tsv 重试:命...
Perl 32:报表的顺序必须进入字节契约
LogTool::Course::Aggregator 按等级累计计数时,内部可以用散列;但报表协议不能暴露散列遍历顺序。当前 Course 入口固定 TOTAL 在第一行,出现过的等级按 rows 返回的排序行输出,行尾是 LF,字段隔符是 TAB,最后显式编码为 UTF-8。fixtures/course.tsv 通过 import 后有 4 条 accepted 记录,报表金标是 TOTAL<TAB>4、ERROR<TAB>1、INFO<TAB>2、WARN<TAB>1,每行后跟 LF。未出现的等级不输出零计数行,只有 TOTAL 总是存在。 examples/perl-language/bin/logtool report --input accepted.jsonl 不另造一套统计规则:它读取 JSON Lines,向 LogTool::Course::Aggregator 逐条 add,再把 TOTAL 与排序后的 rows 拼成 TSV 字符串。tools/replay.pl --case all 的 report c...
Perl 31:没有启动和启动后退出 7 是两种故障
上篇的子进程会写正常数据与独立的诊断流。probe demo.tsv fail 则让固定子脚本明确写 child failed\n 到 STDERR,再 exit 7。run_probe 用 waitpid($pid, 0) 回收进程,从 $? >> 8 取得 7;CLI 将其转为自身退出 2、STDOUT 0 字节、STDERR probe exited 7: child failed\n。子进程 7 与父工具 2 的含义不同,不能直接把 $? 当布尔值打印出去。 另一种错误发生在子程序启动之前:用 run_probe('/definitely/missing/perl-program', 'safe')(仅限库的测试入口)时,open3 抛出 exec of ... failed: No such file or directory;测试校验 probe start failed,而不是伪装成子程序退出 7。输出文本里包含当前文件行号,行号不是稳定外部接口。失败路径均不能输出看起来成功的报表。 图:进程启动、等待与退出状态 123exec 失败 -> 没有可...










